├── .gitignore
├── box.json
├── .dev
├── docker
│ ├── bin
│ │ ├── box_build.sh
│ │ ├── composer_install.sh
│ │ ├── composer_update.sh
│ │ ├── composer_show_outdated.sh
│ │ ├── composer_download.sh
│ │ ├── code_fix.sh
│ │ ├── run_tests.sh
│ │ └── refresh.sh
│ └── images
│ │ ├── php
│ │ └── Dockerfile
│ │ ├── release
│ │ └── Dockerfile
│ │ └── postgres
│ │ ├── docker-entrypoint-initdb.d
│ │ └── structure.sql
│ │ └── config
│ │ ├── pg_hba.conf
│ │ └── postgresql.conf
└── keeper-config.php
├── tests
├── bootstrap.php
├── phpunit.xml
├── SchemaKeeper
│ ├── Core
│ │ ├── SchemaFilterTest.php
│ │ ├── SectionComparatorTest.php
│ │ ├── DumpComparatorTest.php
│ │ ├── ArrayConverterTest.php
│ │ └── DumperTest.php
│ ├── KeeperTest.php
│ ├── CLI
│ │ ├── ShellEntryPointTest.php
│ │ ├── ParserTest.php
│ │ ├── RunnerTest.php
│ │ └── EntryPointTest.php
│ ├── Provider
│ │ └── PostgreSQL
│ │ │ ├── PSQLClientTest.php
│ │ │ └── SavepointHelperTest.php
│ ├── Filesystem
│ │ ├── SectionReaderTest.php
│ │ ├── SectionWriterTest.php
│ │ ├── DumpReaderTest.php
│ │ └── DumpWriterTest.php
│ └── Worker
│ │ ├── VerifierTest.php
│ │ ├── DeployerTest.php
│ │ └── SaverTest.php
└── helpers
│ └── SchemaTestCase.php
├── .travis.yml
├── src
└── SchemaKeeper
│ ├── Exception
│ ├── KeeperException.php
│ └── NotEquals.php
│ ├── CLI
│ ├── Version.php
│ ├── Result.php
│ ├── Parsed.php
│ ├── Parser.php
│ ├── Runner.php
│ └── EntryPoint.php
│ ├── Core
│ ├── SchemaFilter.php
│ ├── Dump.php
│ ├── DumpComparator.php
│ ├── ArrayConverter.php
│ ├── Dumper.php
│ ├── SectionComparator.php
│ └── SchemaStructure.php
│ ├── Provider
│ ├── PostgreSQL
│ │ ├── PSQLChecker.php
│ │ ├── SavepointHelper.php
│ │ ├── PSQLParameters.php
│ │ ├── PSQLClient.php
│ │ └── PSQLProvider.php
│ ├── IProvider.php
│ └── ProviderFactory.php
│ ├── Worker
│ ├── Saver.php
│ ├── Verifier.php
│ └── Deployer.php
│ ├── Filesystem
│ ├── SectionReader.php
│ ├── SectionWriter.php
│ ├── FilesystemHelper.php
│ ├── DumpWriter.php
│ └── DumpReader.php
│ ├── Outside
│ └── DeployedFunctions.php
│ └── Keeper.php
├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
├── CONTRIBUTING.md
└── CODE_OF_CONDUCT.md
├── docker-compose.yml
├── LICENSE
├── composer.json
├── bin
└── schemakeeper
├── CHANGELOG.md
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | /composer.phar
2 | /box.phar
3 | /vendor
4 | /composer.lock
5 | /coverage.xml
--------------------------------------------------------------------------------
/box.json:
--------------------------------------------------------------------------------
1 | {
2 | "directories": ["src"],
3 | "files": [
4 | "LICENSE"
5 | ],
6 | "banner": false
7 | }
--------------------------------------------------------------------------------
/.dev/docker/bin/box_build.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | docker-compose run --rm php /data/box.phar compile -c /data/box.json
--------------------------------------------------------------------------------
/.dev/docker/bin/composer_install.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | docker-compose run --rm php php /data/composer.phar --working-dir=/data install
--------------------------------------------------------------------------------
/.dev/docker/bin/composer_update.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | docker-compose run --rm php php /data/composer.phar --working-dir=/data update
--------------------------------------------------------------------------------
/.dev/docker/bin/composer_show_outdated.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | docker-compose run --rm php php /data/composer.phar --working-dir=/data outdated -D
--------------------------------------------------------------------------------
/.dev/docker/bin/composer_download.sh:
--------------------------------------------------------------------------------
1 | docker-compose run --rm php php -r "copy(\"https://getcomposer.org/composer.phar\", \"/data/composer.phar\");"
2 |
--------------------------------------------------------------------------------
/.dev/docker/bin/code_fix.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | docker-compose run --rm php php /data/vendor/bin/phpcbf \
4 | -p \
5 | --standard=PSR2 \
6 | /data/src \
7 | /data/tests
--------------------------------------------------------------------------------
/.dev/docker/bin/run_tests.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | docker-compose run --rm php /data/vendor/bin/phpunit -c /data/tests/phpunit.xml --coverage-clover=/data/coverage.xml /data/tests/SchemaKeeper
--------------------------------------------------------------------------------
/.dev/keeper-config.php:
--------------------------------------------------------------------------------
1 | setSkippedSchemas([
8 | 'information_schema',
9 | 'pg_%'
10 | ]);
11 |
12 | return $params;
--------------------------------------------------------------------------------
/.dev/docker/images/php/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM php:7.1-cli-stretch
2 |
3 | RUN mkdir /usr/share/man/man1 /usr/share/man/man7
4 |
5 | RUN apt-get update \
6 | && apt-get install -y unzip libpq-dev libz-dev postgresql-client \
7 | && docker-php-ext-install pdo_pgsql zip \
8 | && pecl install xdebug-2.7.2 \
9 | && docker-php-ext-enable xdebug
--------------------------------------------------------------------------------
/tests/bootstrap.php:
--------------------------------------------------------------------------------
1 |
5 | * For the full copyright and license information, please view the LICENSE file that was distributed with this source code.
6 | */
7 |
8 | require_once dirname(__DIR__).'/vendor/autoload.php';
9 |
10 | error_reporting(E_ALL);
11 |
--------------------------------------------------------------------------------
/.dev/docker/bin/refresh.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | docker-compose exec -T postgres bash <<'EOF'
3 | export PGPASSWORD=postgres \
4 | && echo "SELECT pg_terminate_backend(pg_stat_activity.pid) FROM pg_stat_activity WHERE pg_stat_activity.datname = 'schema_keeper' AND pid <> pg_backend_pid();" | psql -hpostgres -Upostgres \
5 | && for f in /docker-entrypoint-initdb.d/*; do psql -hpostgres -Upostgres < "$f"; done
6 | EOF
--------------------------------------------------------------------------------
/.dev/docker/images/release/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM php:5.6.40-cli-alpine
2 |
3 | RUN apk add postgresql-dev postgresql-client wget \
4 | && docker-php-ext-install pdo_pgsql
5 |
6 | RUN mkdir /data \
7 | && cd /data \
8 | && wget https://github.com/dmytro-demchyna/schema-keeper/releases/latest/download/schemakeeper.phar \
9 | && chmod +x /data/schemakeeper.phar
10 |
11 | ENTRYPOINT ["/data/schemakeeper.phar"]
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: minimal
2 |
3 | services:
4 | - docker
5 |
6 | script:
7 | - docker-compose up -d --build
8 | - ./.dev/docker/bin/composer_download.sh
9 | - ./.dev/docker/bin/composer_update.sh
10 | - ./.dev/docker/bin/code_fix.sh
11 | - ./.dev/docker/bin/run_tests.sh
12 |
13 | after_success:
14 | - bash <(curl -s https://codecov.io/bash) -f coverage.xml
15 |
16 | notifications:
17 | email: false
--------------------------------------------------------------------------------
/src/SchemaKeeper/Exception/KeeperException.php:
--------------------------------------------------------------------------------
1 |
5 | * For the full copyright and license information, please view the LICENSE file that was distributed with this source code.
6 | */
7 |
8 | namespace SchemaKeeper\Exception;
9 |
10 | /**
11 | * @api
12 | */
13 | class KeeperException extends \RuntimeException
14 | {
15 | }
16 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1.
16 |
17 | **Expected behavior**
18 | A clear and concise description of what you expected to happen.
19 |
20 | **Additional context**
21 | Add any other context about the problem here.
22 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 |
3 | services:
4 | php:
5 | build: ./.dev/docker/images/php
6 | volumes:
7 | - ./:/data
8 | depends_on:
9 | - postgres
10 |
11 | postgres:
12 | image: postgres:10.7
13 | ports:
14 | - "54322:5432"
15 | volumes:
16 | - ./.dev/docker/images/postgres/docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d
17 | - ./.dev/docker/images/postgres/config:/etc/postgresql
18 | environment:
19 | POSTGRES_PASSWORD: postgres
--------------------------------------------------------------------------------
/src/SchemaKeeper/CLI/Version.php:
--------------------------------------------------------------------------------
1 |
5 | * For the full copyright and license information, please view the LICENSE file that was distributed with this source code.
6 | */
7 |
8 | namespace SchemaKeeper\CLI;
9 |
10 | /**
11 | * @internal
12 | */
13 | class Version
14 | {
15 | public const VERSION = 'v3.0-dev';
16 |
17 | public static function getVersionText(): string
18 | {
19 | return 'SchemaKeeper ' . self::VERSION . ' by Dmytro Demchyna and contributors.' . PHP_EOL. PHP_EOL;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/tests/phpunit.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 | ignore
11 |
12 |
13 |
14 |
15 | ../src
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/SchemaKeeper/CLI/Result.php:
--------------------------------------------------------------------------------
1 |
5 | * For the full copyright and license information, please view the LICENSE file that was distributed with this source code.
6 | */
7 |
8 | namespace SchemaKeeper\CLI;
9 |
10 | /**
11 | * @internal
12 | */
13 | class Result
14 | {
15 | /**
16 | * @var string
17 | */
18 | private $message;
19 |
20 | /**
21 | * @var int
22 | */
23 | private $status;
24 |
25 | public function __construct(string $message, int $status)
26 | {
27 | $this->message = $message;
28 | $this->status = $status;
29 | }
30 |
31 | public function getMessage(): string
32 | {
33 | return $this->message;
34 | }
35 |
36 | public function getStatus(): int
37 | {
38 | return $this->status;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/.dev/docker/images/postgres/docker-entrypoint-initdb.d/structure.sql:
--------------------------------------------------------------------------------
1 | DROP DATABASE IF EXISTS schema_keeper;
2 | CREATE DATABASE schema_keeper;
3 |
4 | \connect schema_keeper
5 |
6 | CREATE TABLE public.test_table (
7 | id bigserial primary key ,
8 | values text
9 | );
10 |
11 | CREATE view public.test_view AS SELECT * FROM public.test_table;
12 |
13 | CREATE materialized view public.test_mat_view AS SELECT * FROM public.test_table;
14 |
15 | CREATE OR REPLACE FUNCTION public.trig_test()
16 | RETURNS trigger
17 | LANGUAGE plpgsql
18 | AS $function$
19 | DECLARE
20 | BEGIN
21 | RETURN NEW;
22 | END;
23 | $function$;
24 |
25 | CREATE TRIGGER test_trigger BEFORE UPDATE ON public.test_table FOR EACH ROW EXECUTE PROCEDURE public.trig_test();
26 |
27 | CREATE TYPE public.test_type AS (
28 | id bigint,
29 | values character varying
30 | );
31 |
32 | CREATE TYPE public.test_enum_type AS ENUM (
33 | 'enum1',
34 | 'enum2'
35 | );
36 |
37 | CREATE SCHEMA test_schema;
--------------------------------------------------------------------------------
/src/SchemaKeeper/Core/SchemaFilter.php:
--------------------------------------------------------------------------------
1 |
5 | * For the full copyright and license information, please view the LICENSE file that was distributed with this source code.
6 | */
7 |
8 | namespace SchemaKeeper\Core;
9 |
10 | /**
11 | * @internal
12 | */
13 | class SchemaFilter
14 | {
15 | /**
16 | * @param string $schemaName
17 | * @param array $items
18 | * @return array
19 | */
20 | public function filter(string $schemaName, array $items): array
21 | {
22 | $filteredItems = [];
23 | foreach ($items as $itemName => $itemContent) {
24 | $schemaNameLength = strlen($schemaName);
25 | if (substr($itemName, 0, $schemaNameLength + 1) == $schemaName.'.') {
26 | $newName = substr($itemName, $schemaNameLength + 1);
27 | $filteredItems[$newName] = $itemContent;
28 | }
29 | }
30 |
31 | return $filteredItems;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2019 Dmytro Demchyna
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.
--------------------------------------------------------------------------------
/tests/SchemaKeeper/Core/SchemaFilterTest.php:
--------------------------------------------------------------------------------
1 |
5 | * For the full copyright and license information, please view the LICENSE file that was distributed with this source code.
6 | */
7 |
8 | namespace SchemaKeeper\Tests\Core;
9 |
10 | use SchemaKeeper\Core\SchemaFilter;
11 | use SchemaKeeper\Tests\SchemaTestCase;
12 |
13 | class SchemaFilterTest extends SchemaTestCase
14 | {
15 | /**
16 | * @var SchemaFilter
17 | */
18 | private $target;
19 |
20 | public function setUp()
21 | {
22 | parent::setUp();
23 |
24 | $this->target = new SchemaFilter();
25 | }
26 |
27 | public function testOk()
28 | {
29 | $actual = $this->target->filter('schema1', [
30 | 'schema1.table_name1' => 'table_content1',
31 | 'schema2.table_name2' => 'table_content2',
32 | ]);
33 |
34 | $expected = [
35 | 'table_name1' => 'table_content1'
36 | ];
37 |
38 | self::assertEquals($expected, $actual);
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/SchemaKeeper/Core/Dump.php:
--------------------------------------------------------------------------------
1 |
5 | * For the full copyright and license information, please view the LICENSE file that was distributed with this source code.
6 | */
7 |
8 | namespace SchemaKeeper\Core;
9 |
10 | /**
11 | * @internal
12 | */
13 | class Dump
14 | {
15 | /**
16 | * @var SchemaStructure[]
17 | */
18 | private $schemas;
19 |
20 | /**
21 | * @var string[]
22 | */
23 | private $extensions;
24 |
25 | /**
26 | * @param SchemaStructure[] $schemas
27 | * @param string[] $extensions
28 | */
29 | public function __construct(array $schemas, array $extensions)
30 | {
31 | $this->schemas = $schemas;
32 | $this->extensions = $extensions;
33 | }
34 |
35 | /**
36 | * @return SchemaStructure[]
37 | */
38 | public function getSchemas(): array
39 | {
40 | return $this->schemas;
41 | }
42 |
43 | /**
44 | * @return string[]
45 | */
46 | public function getExtensions(): array
47 | {
48 | return $this->extensions;
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/SchemaKeeper/CLI/Parsed.php:
--------------------------------------------------------------------------------
1 |
5 | * For the full copyright and license information, please view the LICENSE file that was distributed with this source code.
6 | */
7 |
8 | namespace SchemaKeeper\CLI;
9 |
10 | use SchemaKeeper\Provider\PostgreSQL\PSQLParameters;
11 |
12 | /**
13 | * @internal
14 | */
15 | class Parsed
16 | {
17 | /**
18 | * @var PSQLParameters
19 | */
20 | private $params;
21 |
22 | /**
23 | * @var string
24 | */
25 | private $command;
26 |
27 | /**
28 | * @var string
29 | */
30 | private $path;
31 |
32 | public function __construct(PSQLParameters $params, string $command, string $path)
33 | {
34 | $this->params = $params;
35 | $this->command = $command;
36 | $this->path = $path;
37 | }
38 |
39 | public function getParams(): PSQLParameters
40 | {
41 | return $this->params;
42 | }
43 |
44 | public function getCommand(): string
45 | {
46 | return $this->command;
47 | }
48 |
49 | public function getPath(): string
50 | {
51 | return $this->path;
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/SchemaKeeper/Provider/PostgreSQL/PSQLChecker.php:
--------------------------------------------------------------------------------
1 |
5 | * For the full copyright and license information, please view the LICENSE file that was distributed with this source code.
6 | */
7 |
8 | namespace SchemaKeeper\Provider\PostgreSQL;
9 |
10 | use SchemaKeeper\Exception\KeeperException;
11 |
12 | /**
13 | * @internal
14 | */
15 | class PSQLChecker
16 | {
17 | /**
18 | * @var PSQLParameters
19 | */
20 | private $parameters;
21 |
22 |
23 | public function __construct(PSQLParameters $parameters)
24 | {
25 | $this->parameters = $parameters;
26 | }
27 |
28 | public function check(): void
29 | {
30 | if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
31 | throw new KeeperException('OS Windows is currently not supported');
32 | }
33 |
34 | $executable = $this->parameters->getExecutable();
35 |
36 | exec('command -v ' . $executable . ' >/dev/null 2>&1 || exit 1', $output, $retVal);
37 |
38 | if ($retVal !== 0) {
39 | throw new KeeperException($executable . ' not installed. Please, install "postgresql-client" package');
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/.github/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | ## Contributor Code of Conduct
4 |
5 | Please note that this project is released with a [Contributor Code of Conduct](https://github.com/dmytro-demchyna/schema-keeper/blob/master/.github/CODE_OF_CONDUCT.md). By participating in this project you agree to abide by its terms.
6 |
7 | ## Environment
8 |
9 | Development environment are fully virtualized, so it requires installed [Docker Compose](https://docs.docker.com/compose/).
10 |
11 | Please, use steps below to setting the project on your machine:
12 |
13 | 1. Clone project via `git clone`
14 | 1. Open project directory in terminal
15 | 1. Execute `docker-compose up -d`
16 | 1. Execute `./.dev/docker/bin/composer_download.sh`
17 | 1. Execute `./.dev/docker/bin/composer_install.sh`
18 | 1. Execute `./.dev/docker/bin/run_tests.sh` to ensure that project works as expected
19 |
20 | > Directory `.dev/docker/images/postgres/docker-entrypoint-initdb.d/` contains scripts that [container](https://hub.docker.com/_/postgres) will automatically run on startup.
21 |
22 | ## Workflow
23 |
24 | 1. Fork the project
25 | 1. Make your changes
26 | 1. Add tests for it
27 | 1. Send a pull request
28 |
29 | ## Coding guidelines
30 |
31 | This project comes with an executable `./docker/bin/code_fix.sh` that you can use to format source code for compliance with this project's coding guidelines.
--------------------------------------------------------------------------------
/src/SchemaKeeper/Worker/Saver.php:
--------------------------------------------------------------------------------
1 |
5 | * For the full copyright and license information, please view the LICENSE file that was distributed with this source code.
6 | */
7 |
8 | namespace SchemaKeeper\Worker;
9 |
10 | use Exception;
11 | use SchemaKeeper\Core\Dumper;
12 | use SchemaKeeper\Core\SchemaFilter;
13 | use SchemaKeeper\Filesystem\DumpWriter;
14 | use SchemaKeeper\Filesystem\FilesystemHelper;
15 | use SchemaKeeper\Filesystem\SectionWriter;
16 | use SchemaKeeper\Provider\IProvider;
17 |
18 | /**
19 | * @internal
20 | */
21 | class Saver
22 | {
23 | /**
24 | * @var Dumper
25 | */
26 | private $dumper;
27 |
28 | /**
29 | * @var DumpWriter
30 | */
31 | private $writer;
32 |
33 | public function __construct(IProvider $provider)
34 | {
35 | $schemaFilter = new SchemaFilter();
36 | $this->dumper = new Dumper($provider, $schemaFilter);
37 | $helper = new FilesystemHelper();
38 | $sectionWriter = new SectionWriter($helper);
39 | $this->writer = new DumpWriter($sectionWriter, $helper);
40 | }
41 |
42 | public function save(string $destinationPath): void
43 | {
44 | $dump = $this->dumper->dump();
45 | $this->writer->write($destinationPath, $dump);
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/SchemaKeeper/CLI/Parser.php:
--------------------------------------------------------------------------------
1 |
5 | * For the full copyright and license information, please view the LICENSE file that was distributed with this source code.
6 | */
7 |
8 | namespace SchemaKeeper\CLI;
9 |
10 | use SchemaKeeper\Exception\KeeperException;
11 | use SchemaKeeper\Provider\PostgreSQL\PSQLParameters;
12 |
13 | /**
14 | * @internal
15 | */
16 | class Parser
17 | {
18 | public function parse(array $options, array $argv): Parsed
19 | {
20 | $configPath = isset($options['c']) ? $options['c'] : null;
21 |
22 | if (!$configPath || !is_readable($configPath)) {
23 | throw new KeeperException("Config file not found or not readable ".$configPath);
24 | }
25 |
26 | $params = require $configPath;
27 |
28 | if (!($params instanceof PSQLParameters)) {
29 | throw new KeeperException("Config file must return instance of ".PSQLParameters::class);
30 | }
31 |
32 | $path = isset($options['d']) ? $options['d'] : null;
33 |
34 | if (!$path) {
35 | throw new KeeperException("Destination path not specified");
36 | }
37 |
38 | $count = count($argv);
39 | $command = isset($argv[$count - 1]) ? $argv[$count - 1] : null;
40 |
41 | return new Parsed($params, $command, $path);
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/SchemaKeeper/Filesystem/SectionReader.php:
--------------------------------------------------------------------------------
1 |
5 | * For the full copyright and license information, please view the LICENSE file that was distributed with this source code.
6 | */
7 |
8 | namespace SchemaKeeper\Filesystem;
9 |
10 | /**
11 | * @internal
12 | */
13 | class SectionReader
14 | {
15 | /**
16 | * @var FilesystemHelper
17 | */
18 | private $helper;
19 |
20 |
21 | public function __construct(FilesystemHelper $helper)
22 | {
23 | $this->helper = $helper;
24 | }
25 |
26 | /**
27 | * @param string $sectionPath
28 | * @return array
29 | * @throws \Exception
30 | */
31 | public function readSection(string $sectionPath): array
32 | {
33 | $list = [];
34 |
35 | if (!$this->helper->isDir($sectionPath)) {
36 | return [];
37 | }
38 |
39 | foreach ($this->helper->glob($sectionPath . '/*') as $itemPath) {
40 | $parts = pathinfo($itemPath);
41 |
42 | $parts['extension'] = $parts['extension'] ?? '';
43 |
44 | if (!in_array($parts['extension'], ['txt', 'sql'])) {
45 | continue;
46 | }
47 |
48 | $content = $this->helper->fileGetContents($itemPath);
49 | $list[$parts['filename']] = $content;
50 | }
51 |
52 | return $list;
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "schema-keeper/schema-keeper",
3 | "description": "Database development kit for PostgreSQL",
4 | "minimum-stability": "stable",
5 | "type": "library",
6 | "license": "MIT",
7 | "keywords": [
8 | "database",
9 | "db",
10 | "postgresql",
11 | "postgres",
12 | "plpgsql",
13 | "pgsql",
14 | "stored procedures",
15 | "schema",
16 | "dump",
17 | "deploy",
18 | "sync"
19 | ],
20 | "authors": [
21 | {
22 | "name": "Dmytro Demchyna",
23 | "email": "dmitry.demchina@gmail.com",
24 | "role": "Developer",
25 | "homepage": "https://github.com/dmytro-demchyna"
26 | }
27 | ],
28 | "autoload": {
29 | "psr-4": {
30 | "SchemaKeeper\\": "src/SchemaKeeper"
31 | }
32 | },
33 | "autoload-dev": {
34 | "psr-4": {
35 | "SchemaKeeper\\Tests\\": "tests/SchemaKeeper"
36 | },
37 | "classmap": ["tests/helpers"]
38 | },
39 | "require": {
40 | "php": ">=7.1",
41 | "ext-pdo": "*",
42 | "ext-pdo_pgsql": "*",
43 | "ext-json": "*"
44 | },
45 | "require-dev": {
46 | "phpunit/phpunit": "^5.0||^6.0||^7.0||^8.0",
47 | "mockery/mockery": "^1.0",
48 | "squizlabs/php_codesniffer": "^3.0",
49 | "phpstan/phpstan": "*"
50 | },
51 | "suggest": {
52 | "doctrine/migrations": ""
53 | },
54 | "bin": [
55 | "bin/schemakeeper"
56 | ],
57 | "extra": {
58 | "branch-alias": {
59 | "dev-master": "v3.0-dev"
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/SchemaKeeper/Filesystem/SectionWriter.php:
--------------------------------------------------------------------------------
1 |
5 | * For the full copyright and license information, please view the LICENSE file that was distributed with this source code.
6 | */
7 |
8 | namespace SchemaKeeper\Filesystem;
9 |
10 | /**
11 | * @internal
12 | */
13 | class SectionWriter
14 | {
15 | /**
16 | * @var FilesystemHelper
17 | */
18 | private $helper;
19 |
20 | public function __construct(FilesystemHelper $helper)
21 | {
22 | $this->helper = $helper;
23 | }
24 |
25 | /**
26 | * @param string $sectionPath
27 | * @param array $sectionContent
28 | * @throws \Exception
29 | */
30 | public function writeSection(string $sectionPath, array $sectionContent): void
31 | {
32 | if (!$sectionContent) {
33 | return;
34 | }
35 |
36 | $this->helper->mkdir($sectionPath, 0775, true);
37 |
38 | $parts = pathinfo($sectionPath);
39 | $sectionName = $parts['filename'];
40 |
41 | foreach ($sectionContent as $name => $content) {
42 | if (in_array($sectionName, ['functions', 'triggers'])) {
43 | $name = $name . '.sql';
44 | } else {
45 | $name = $name . '.txt';
46 | }
47 |
48 | $this->helper->filePutContents($sectionPath . '/' . $name, $content);
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/SchemaKeeper/Provider/PostgreSQL/SavepointHelper.php:
--------------------------------------------------------------------------------
1 |
5 | * For the full copyright and license information, please view the LICENSE file that was distributed with this source code.
6 | */
7 |
8 | namespace SchemaKeeper\Provider\PostgreSQL;
9 |
10 | use PDO;
11 |
12 | /**
13 | * @internal
14 | */
15 | class SavepointHelper
16 | {
17 | /**
18 | * @var PDO
19 | */
20 | private $conn;
21 |
22 |
23 | public function __construct(PDO $conn)
24 | {
25 | $this->conn = $conn;
26 | }
27 |
28 | public function beginTransaction(string $possibleSavePointName, bool $isTransaction): bool
29 | {
30 | if ($isTransaction) {
31 | return (bool) $this->conn->exec('SAVEPOINT '.$possibleSavePointName);
32 | }
33 |
34 | return $this->conn->beginTransaction();
35 | }
36 |
37 | public function commit(string $possibleSavePointName, bool $isTransaction): bool
38 | {
39 | if ($isTransaction) {
40 | return (bool) $this->conn->exec('RELEASE SAVEPOINT '.$possibleSavePointName);
41 | }
42 |
43 | return $this->conn->commit();
44 | }
45 |
46 | public function rollback(string $possibleSavePointName, bool $isTransaction): bool
47 | {
48 | if ($isTransaction) {
49 | return (bool) $this->conn->exec('ROLLBACK TO SAVEPOINT '.$possibleSavePointName);
50 | }
51 |
52 | return $this->conn->rollBack();
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/SchemaKeeper/Exception/NotEquals.php:
--------------------------------------------------------------------------------
1 |
5 | * For the full copyright and license information, please view the LICENSE file that was distributed with this source code.
6 | */
7 |
8 | namespace SchemaKeeper\Exception;
9 |
10 | /**
11 | * @api
12 | */
13 | class NotEquals extends KeeperException
14 | {
15 | /**
16 | * @var array
17 | */
18 | private $expected = [];
19 |
20 | /**
21 | * @var array
22 | */
23 | private $actual = [];
24 |
25 | /**
26 | * @param string $message
27 | * @param array $expected
28 | * @param array $actual
29 | */
30 | public function __construct($message, array $expected, array $actual)
31 | {
32 | $message .= PHP_EOL . json_encode([
33 | 'expected' => $expected,
34 | 'actual' => $actual,
35 | ], JSON_PRETTY_PRINT);
36 |
37 | if (json_last_error() !== JSON_ERROR_NONE) {
38 | throw new \RuntimeException('Json error: ' . json_last_error_msg());
39 | }
40 |
41 | parent::__construct($message);
42 |
43 | $this->expected = $expected;
44 | $this->actual = $actual;
45 | }
46 |
47 | /**
48 | * @return array
49 | */
50 | public function getExpected()
51 | {
52 | return $this->expected;
53 | }
54 |
55 | /**
56 | * @return array
57 | */
58 | public function getActual()
59 | {
60 | return $this->actual;
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/tests/SchemaKeeper/KeeperTest.php:
--------------------------------------------------------------------------------
1 |
5 | * For the full copyright and license information, please view the LICENSE file that was distributed with this source code.
6 | */
7 |
8 | namespace SchemaKeeper\Tests;
9 |
10 | use SchemaKeeper\Keeper;
11 |
12 | class KeeperTest extends SchemaTestCase
13 | {
14 | /**
15 | * @var Keeper
16 | */
17 | private $target;
18 |
19 | public function setUp()
20 | {
21 | parent::setUp();
22 |
23 | $conn = $this->getConn();
24 | $params = $this->getDbParams();
25 | $this->target = new Keeper($conn, $params);
26 |
27 | exec('rm -rf /tmp/schema_keeper');
28 | }
29 |
30 | public function testOk()
31 | {
32 | $this->target->saveDump('/tmp/schema_keeper');
33 |
34 | $conn = $this->getConn();
35 | $conn->beginTransaction();
36 | $this->target->deployDump('/tmp/schema_keeper');
37 | $conn->rollBack();
38 |
39 | $this->target->verifyDump('/tmp/schema_keeper');
40 |
41 | self::assertTrue(true);
42 | }
43 |
44 | /**
45 | * @expectedException \SchemaKeeper\Exception\KeeperException
46 | * @expectedExceptionMessage blabla not installed. Please, install "postgresql-client" package
47 | */
48 | function testRequirementsCheck()
49 | {
50 | $conn = $this->getConn();
51 | $params = $this->getDbParams();
52 | $params->setExecutable('blabla');
53 |
54 | $this->target = new Keeper($conn, $params);
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/bin/schemakeeper:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 |
6 | * For the full copyright and license information, please view the LICENSE file that was distributed with this source code.
7 | */
8 |
9 | if (version_compare(PHP_VERSION, '5.6.0', '<')) {
10 | fwrite(STDERR, 'SchemaKeeper requires PHP >= 5.6' . PHP_EOL);
11 | exit(1);
12 | }
13 |
14 | foreach (['pdo', 'pdo_pgsql', 'json'] as $extension) {
15 | if (!extension_loaded($extension)) {
16 | fwrite(STDERR, 'SchemaKeeper requires "' . $extension . '" extension' . PHP_EOL);
17 | exit(1);
18 | }
19 | }
20 |
21 | $autoloadVariants = [
22 | __DIR__ . '/../../../autoload.php',
23 | __DIR__ . '/../vendor/autoload.php',
24 | ];
25 |
26 | $autoloadPath = null;
27 |
28 | foreach ($autoloadVariants as $variantPath) {
29 | if (file_exists($variantPath)) {
30 | $autoloadPath = $variantPath;
31 |
32 | break;
33 | }
34 | }
35 |
36 | if (!$autoloadPath) {
37 | fwrite(STDERR,
38 | 'You must set up the project dependencies:' . PHP_EOL .
39 | 'curl -s http://getcomposer.org/installer | php' . PHP_EOL .
40 | 'php composer.phar install' . PHP_EOL);
41 |
42 | exit(1);
43 | }
44 |
45 | require_once $autoloadPath;
46 |
47 | echo \SchemaKeeper\CLI\Version::getVersionText();
48 |
49 | $options = getopt('c:d:', ['help', 'version']);
50 |
51 | $entryPoint = new \SchemaKeeper\CLI\EntryPoint();
52 | $result = $entryPoint->run($options, $argv);
53 |
54 | echo $result->getMessage() . PHP_EOL;
55 | exit($result->getStatus());
--------------------------------------------------------------------------------
/src/SchemaKeeper/Core/DumpComparator.php:
--------------------------------------------------------------------------------
1 |
5 | * For the full copyright and license information, please view the LICENSE file that was distributed with this source code.
6 | */
7 |
8 | namespace SchemaKeeper\Core;
9 |
10 | /**
11 | * @internal
12 | */
13 | class DumpComparator
14 | {
15 | /**
16 | * @var ArrayConverter
17 | */
18 | private $converter;
19 |
20 | /**
21 | * @var SectionComparator
22 | */
23 | private $sectionComparator;
24 |
25 | public function __construct(ArrayConverter $converter, SectionComparator $sectionComparator)
26 | {
27 | $this->converter = $converter;
28 | $this->sectionComparator = $sectionComparator;
29 | }
30 |
31 | /**
32 | * @param Dump $expectedDump
33 | * @param Dump $actualDump
34 | * @return array{expected:array,actual:array}
35 | */
36 | public function compare(Dump $expectedDump, Dump $actualDump): array
37 | {
38 | $diff = [
39 | 'expected' => [],
40 | 'actual' => [],
41 | ];
42 |
43 | $expectedArray = $this->converter->dump2Array($expectedDump);
44 | $actualArray = $this->converter->dump2Array($actualDump);
45 |
46 | $keys = array_keys($expectedArray);
47 |
48 | foreach ($keys as $key) {
49 | $diff = array_merge_recursive(
50 | $diff,
51 | $this->sectionComparator->compareSection($key, $expectedArray[$key], $actualArray[$key])
52 | );
53 | }
54 |
55 | return $diff;
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/tests/SchemaKeeper/Core/SectionComparatorTest.php:
--------------------------------------------------------------------------------
1 |
5 | * For the full copyright and license information, please view the LICENSE file that was distributed with this source code.
6 | */
7 |
8 | namespace SchemaKeeper\Tests\Core;
9 |
10 | use SchemaKeeper\Core\SectionComparator;
11 | use SchemaKeeper\Tests\SchemaTestCase;
12 |
13 | class SectionComparatorTest extends SchemaTestCase
14 | {
15 | /**
16 | * @var SectionComparator
17 | */
18 | private $target;
19 |
20 | public function setUp()
21 | {
22 | parent::setUp();
23 |
24 | $this->target = new SectionComparator();
25 | }
26 |
27 | public function testOk()
28 | {
29 | $sectionName = 'tables';
30 |
31 | $leftContent = [
32 | 'test1' => 'test_content1',
33 | 'test2' => 'test_content2',
34 | 'test3' => 'test_content3',
35 | ];
36 |
37 | $rightContent = [
38 | 'test1' => 'test_content1',
39 | 'test2' => 'test_content',
40 | 'test3' => 'test_content3',
41 | ];
42 |
43 | $expected = [
44 | 'expected' => [
45 | 'tables' => [
46 | 'test2' => "test_content2",
47 | ],
48 | ],
49 | 'actual' => [
50 | 'tables' => [
51 | 'test2' => "test_content",
52 | ],
53 | ],
54 | ];
55 |
56 | $actual = $this->target->compareSection($sectionName, $leftContent, $rightContent);
57 |
58 | self::assertEquals($expected, $actual);
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/tests/helpers/SchemaTestCase.php:
--------------------------------------------------------------------------------
1 |
5 | * For the full copyright and license information, please view the LICENSE file that was distributed with this source code.
6 | */
7 |
8 | namespace SchemaKeeper\Tests;
9 |
10 | use PDO;
11 | use PHPUnit\Framework\TestCase;
12 | use SchemaKeeper\Provider\PostgreSQL\PSQLParameters;
13 |
14 | abstract class SchemaTestCase extends TestCase
15 | {
16 | /**
17 | * @var \PDO
18 | */
19 | private static $conn;
20 |
21 | protected function tearDown()
22 | {
23 | parent::tearDown();
24 |
25 | $this->addToAssertionCount(
26 | \Mockery::getContainer()->mockery_getExpectationCount()
27 | );
28 |
29 | \Mockery::close();
30 | }
31 |
32 | /**
33 | * @return PDO
34 | */
35 | protected static function getConn()
36 | {
37 | if (!self::$conn) {
38 | self::$conn = self::createConn();
39 | }
40 |
41 | return self::$conn;
42 | }
43 |
44 |
45 | /**
46 | * @return PSQLParameters
47 | */
48 | protected static function getDbParams()
49 | {
50 | $dbParams = include '/data/.dev/keeper-config.php';
51 |
52 | return $dbParams;
53 | }
54 |
55 | /**
56 | * @return PDO
57 | */
58 | private static function createConn()
59 | {
60 | $dbParams = self::getDbParams();
61 |
62 | $dsn = 'pgsql:dbname=' . $dbParams->getDbName() . ';host=' . $dbParams->getHost();
63 |
64 | $conn = new \PDO($dsn, $dbParams->getUser(), $dbParams->getPassword(), [
65 | \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION
66 | ]);
67 |
68 | return $conn;
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 | All notable changes to this project will be documented in this file.
3 |
4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6 |
7 | ## [2.2.0] - 2019-06-05
8 |
9 | ### Changed
10 | - Pretty print instead of minified print for a json output
11 |
12 | ## [2.1.1] - 2019-05-27
13 |
14 | ### Added
15 | - PHAR builder
16 |
17 | ## [2.1.0] - 2019-05-14
18 |
19 | ### Added
20 | - More accurate CLI output
21 | - CLI parameter --version
22 | - Failure on unrecognized CLI parameter
23 | - OS checking
24 |
25 | ### Changed
26 | - Output with exit-code 1 to STDOUT instead of STDERR for SchemaKeeper's native exceptions
27 |
28 | ## [2.0.1] - 2019-05-10
29 |
30 | ### Changed
31 | - `schemakeeper deploy` runs in transaction by default
32 |
33 | ## [2.0.0] - 2019-05-10
34 |
35 | ### Added
36 | - bin/schemakeeper
37 |
38 | ### Changed
39 | - `verifyDump` returns void
40 | - `deployDump` returns object instead of array
41 | - `verifyDump` and `deployDump` throws exception on diff
42 |
43 | ## [1.0.5] - 2019-05-08
44 |
45 | ### Added
46 | - Protection from removing all functions using deployDump
47 |
48 | ### Changed
49 | - Fix bug that provoke error in case running deployDump without transaction
50 |
51 | ## [1.0.4] - 2019-05-02
52 |
53 | ### Added
54 | - Throwing exception if `psql` not installed
55 |
56 | ## [1.0.3] - 2019-04-17
57 |
58 | ### Changed
59 | - Exception message in some cases
60 |
61 | ## [1.0.2] - 2019-04-11
62 |
63 | ### Changed
64 | - README.md
65 | - "suggest" block in composer.json
66 |
67 | ## [1.0.1] - 2019-03-22
68 | ### Added
69 | - CHANGELOG.md
70 |
71 | ### Changed
72 | - Keywords and description in composer.json
73 |
74 | ## [1.0.0] - 2019-03-22
--------------------------------------------------------------------------------
/src/SchemaKeeper/Outside/DeployedFunctions.php:
--------------------------------------------------------------------------------
1 |
5 | * For the full copyright and license information, please view the LICENSE file that was distributed with this source code.
6 | */
7 |
8 | namespace SchemaKeeper\Outside;
9 |
10 | /**
11 | * @api
12 | */
13 | class DeployedFunctions
14 | {
15 | /**
16 | * @var string[]
17 | */
18 | private $changed = [];
19 |
20 | /**
21 | * @var string[]
22 | */
23 | private $created = [];
24 |
25 | /**
26 | * @var string[]
27 | */
28 | private $deleted = [];
29 |
30 | /**
31 | * @param string[] $changed
32 | * @param string[] $created
33 | * @param string[] $deleted
34 | */
35 | public function __construct(array $changed, array $created, array $deleted)
36 | {
37 | $this->changed = $changed;
38 | $this->created = $created;
39 | $this->deleted = $deleted;
40 | }
41 |
42 | /**
43 | * List of functions that were changed in the current database, as their source code is different between saved dump and current database
44 | * @return string[]
45 | */
46 | public function getChanged(): array
47 | {
48 | return $this->changed;
49 | }
50 |
51 | /**
52 | * List of functions that were created in the current database, as they do not exist in the saved dump
53 | * @return string[]
54 | */
55 | public function getCreated(): array
56 | {
57 | return $this->created;
58 | }
59 |
60 | /**
61 | * List of functions that were deleted from the current database, as they do not exist in the saved dump
62 | * @return string[]
63 | */
64 | public function getDeleted(): array
65 | {
66 | return $this->deleted;
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/SchemaKeeper/Provider/IProvider.php:
--------------------------------------------------------------------------------
1 |
5 | * For the full copyright and license information, please view the LICENSE file that was distributed with this source code.
6 | */
7 |
8 | namespace SchemaKeeper\Provider;
9 |
10 | /**
11 | * @internal
12 | */
13 | interface IProvider
14 | {
15 | /**
16 | * @return array
17 | */
18 | public function getTables(): array;
19 |
20 | /**
21 | * @return array
22 | */
23 | public function getViews(): array;
24 |
25 | /**
26 | * @return array
27 | */
28 | public function getMaterializedViews(): array;
29 |
30 | /**
31 | * @return array
32 | */
33 | public function getTriggers(): array;
34 |
35 | /**
36 | * @return array
37 | */
38 | public function getFunctions(): array;
39 |
40 | /**
41 | * @return array
42 | */
43 | public function getTypes(): array;
44 |
45 | /**
46 | * @return array
47 | */
48 | public function getSchemas(): array;
49 |
50 | /**
51 | * @return array
52 | */
53 | public function getExtensions(): array;
54 |
55 | /**
56 | * @return array
57 | */
58 | public function getSequences(): array;
59 |
60 | /**
61 | * @param string $definition
62 | */
63 | public function createFunction(string $definition): void;
64 |
65 | /**
66 | * @param string $name
67 | */
68 | public function deleteFunction(string $name): void;
69 |
70 | /**
71 | * @param string $name
72 | * @param string $definition
73 | */
74 | public function changeFunction(string $name, string $definition): void;
75 | }
76 |
--------------------------------------------------------------------------------
/tests/SchemaKeeper/CLI/ShellEntryPointTest.php:
--------------------------------------------------------------------------------
1 |
5 | * For the full copyright and license information, please view the LICENSE file that was distributed with this source code.
6 | */
7 |
8 | namespace SchemaKeeper\Tests\CLI;
9 |
10 | use SchemaKeeper\CLI\Version;
11 | use SchemaKeeper\Tests\SchemaTestCase;
12 |
13 | class ShellEntryPointTest extends SchemaTestCase
14 | {
15 | function setUp()
16 | {
17 | parent::setUp();
18 |
19 | exec('rm -rf /tmp/dump');
20 | }
21 |
22 | function testOk()
23 | {
24 | exec('/data/bin/schemakeeper -c /data/.dev/keeper-config.php -d /tmp/dump save', $output, $status);
25 | $output = implode(PHP_EOL, $output);
26 | self::assertEquals(Version::getVersionText() . 'Success: Dump saved /tmp/dump', $output);
27 | self::assertSame(0, $status);
28 | }
29 |
30 | function testHelp()
31 | {
32 | exec('/data/bin/schemakeeper --help', $output, $status);
33 | $output = implode(PHP_EOL, $output);
34 | self::assertContains('Usage: schemakeeper [options] ', $output);
35 | self::assertSame(0, $status);
36 | }
37 |
38 | function testVersion()
39 | {
40 | exec('/data/bin/schemakeeper --version', $output, $status);
41 | $output = implode(PHP_EOL, $output);
42 |
43 | self::assertEquals(Version::getVersionText(), $output);
44 | self::assertSame(0, $status);
45 | }
46 |
47 | function testError()
48 | {
49 | exec('/data/bin/schemakeeper -c /data/.dev/keeper-config.php -d /tmp/dump verify', $output, $status);
50 | $output = implode(PHP_EOL, $output);
51 | self::assertEquals(Version::getVersionText() . 'Failure: Dump is empty /tmp/dump', $output);
52 | self::assertSame(1, $status);
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/SchemaKeeper/Provider/ProviderFactory.php:
--------------------------------------------------------------------------------
1 |
5 | * For the full copyright and license information, please view the LICENSE file that was distributed with this source code.
6 | */
7 |
8 | namespace SchemaKeeper\Provider;
9 |
10 | use PDO;
11 | use SchemaKeeper\Exception\KeeperException;
12 | use SchemaKeeper\Provider\PostgreSQL\PSQLChecker;
13 | use SchemaKeeper\Provider\PostgreSQL\PSQLClient;
14 | use SchemaKeeper\Provider\PostgreSQL\PSQLParameters;
15 | use SchemaKeeper\Provider\PostgreSQL\PSQLProvider;
16 |
17 | /**
18 | * @internal
19 | */
20 | class ProviderFactory
21 | {
22 | /**
23 | * @param PDO $conn
24 | * @param object $parameters
25 | * @return IProvider
26 | * @throws KeeperException
27 | */
28 | public function createProvider(PDO $conn, $parameters = null): IProvider
29 | {
30 | if ($conn->getAttribute(PDO::ATTR_DRIVER_NAME) !== 'pgsql') {
31 | throw new KeeperException('Only pgsql driver is supported');
32 | }
33 |
34 | if (!($parameters instanceof PSQLParameters)) {
35 | throw new KeeperException('$parameters must be instance of '.PSQLParameters::class);
36 | }
37 |
38 | $checker = new PSQLChecker($parameters);
39 | $checker->check();
40 |
41 | $client = new PSQLClient(
42 | $parameters->getExecutable(),
43 | $parameters->getDbName(),
44 | $parameters->getHost(),
45 | $parameters->getPort(),
46 | $parameters->getUser(),
47 | $parameters->getPassword()
48 | );
49 |
50 | $provider = new PSQLProvider(
51 | $conn,
52 | $client,
53 | $parameters->getSkippedSchemas(),
54 | $parameters->getSkippedExtensions()
55 | );
56 |
57 | return $provider;
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/SchemaKeeper/Core/ArrayConverter.php:
--------------------------------------------------------------------------------
1 |
5 | * For the full copyright and license information, please view the LICENSE file that was distributed with this source code.
6 | */
7 |
8 | namespace SchemaKeeper\Core;
9 |
10 | use SchemaKeeper\Exception\KeeperException;
11 |
12 | /**
13 | * @internal
14 | */
15 | class ArrayConverter
16 | {
17 | /**
18 | * @param Dump $dump
19 | * @return array
20 | */
21 | public function dump2Array(Dump $dump): array
22 | {
23 | $keysMapping = [
24 | 'tables' => 'getTables',
25 | 'views' => 'getViews',
26 | 'materialized_views' => 'getMaterializedViews',
27 | 'types' => 'getTypes',
28 | 'functions' => 'getFunctions',
29 | 'triggers' => 'getTriggers',
30 | 'sequences' => 'getSequences',
31 | ];
32 |
33 | $result = array_combine(array_keys($keysMapping), array_fill(0, count($keysMapping), []));
34 |
35 | if ($result === false) {
36 | throw new KeeperException('array_combine() problem');
37 | }
38 |
39 | $result['schemas'] = [];
40 | $result['extensions'] = $dump->getExtensions();
41 |
42 | foreach ($dump->getSchemas() as $schemaDump) {
43 | $result['schemas'][] = $schemaDump->getSchemaName();
44 |
45 | foreach ($keysMapping as $key => $methodName) {
46 | $newItems = [];
47 |
48 | foreach ($schemaDump->$methodName() as $itemName => $itemContent) {
49 | $newItems[$schemaDump->getSchemaName().'.'.$itemName] = $itemContent;
50 | }
51 |
52 | $result[$key] = isset($result[$key]) ? $result[$key] : [];
53 | $result[$key] = array_merge($result[$key], $newItems);
54 | }
55 | }
56 |
57 | return $result;
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/SchemaKeeper/Filesystem/FilesystemHelper.php:
--------------------------------------------------------------------------------
1 |
5 | * For the full copyright and license information, please view the LICENSE file that was distributed with this source code.
6 | */
7 |
8 | namespace SchemaKeeper\Filesystem;
9 |
10 | use Exception;
11 | use SchemaKeeper\Exception\KeeperException;
12 |
13 | /**
14 | * @internal
15 | */
16 | class FilesystemHelper
17 | {
18 | public function isDir(string $path): bool
19 | {
20 | return is_dir($path);
21 | }
22 |
23 | public function fileGetContents(string $filename): string
24 | {
25 | $content = file_get_contents($filename);
26 |
27 | if ($content === false) {
28 | throw new KeeperException('file_get_contents error on: '.$filename);
29 | }
30 |
31 | return $content;
32 | }
33 |
34 | public function filePutContents(string $filename, string $data): void
35 | {
36 | $result = file_put_contents($filename, $data);
37 |
38 | if ($result === false) {
39 | throw new KeeperException('file_put_contents error on: '.$filename);
40 | }
41 | }
42 |
43 | public function glob(string $pattern): array
44 | {
45 | $result = glob($pattern);
46 |
47 | if ($result === false) {
48 | throw new KeeperException('glob() error');
49 | }
50 |
51 | return $result;
52 | }
53 |
54 | public function mkdir(string $pathname, int $mode = 0775, bool $recursive = false): void
55 | {
56 | $result = mkdir($pathname, $mode, $recursive);
57 |
58 | if ($result === false) {
59 | throw new KeeperException('mkdir error on: '.$pathname);
60 | }
61 | }
62 |
63 | public function rmDirIfExisted(string $path): void
64 | {
65 | if ($this->isDir($path)) {
66 | shell_exec("rm -rf ".escapeshellarg($path));
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/tests/SchemaKeeper/CLI/ParserTest.php:
--------------------------------------------------------------------------------
1 |
5 | * For the full copyright and license information, please view the LICENSE file that was distributed with this source code.
6 | */
7 |
8 | namespace SchemaKeeper\Tests\CLI;
9 |
10 | use SchemaKeeper\CLI\Parser;
11 | use SchemaKeeper\Provider\PostgreSQL\PSQLParameters;
12 | use SchemaKeeper\Tests\SchemaTestCase;
13 |
14 | class ParserTest extends SchemaTestCase
15 | {
16 | /**
17 | * @var Parser
18 | */
19 | private $target;
20 |
21 | function setUp()
22 | {
23 | parent::setUp();
24 |
25 | $this->target = new Parser();
26 | }
27 |
28 | function testOk()
29 | {
30 | $parsed = $this->target->parse(['c' => '/data/.dev/keeper-config.php', 'd' => '/tmp/dump'], ['save']);
31 |
32 | self::assertInstanceOf(PSQLParameters::class, $parsed->getParams());
33 | self::assertEquals('/tmp/dump', $parsed->getPath());
34 | self::assertEquals('save', $parsed->getCommand());
35 | }
36 |
37 | /**
38 | * @expectedException \SchemaKeeper\Exception\KeeperException
39 | * @expectedExceptionMessage Config file not found or not readable /data/dummyconfig
40 | */
41 | function testConfigNotExisted()
42 | {
43 | $this->target->parse(['c' => '/data/dummyconfig'], []);
44 | }
45 |
46 | /**
47 | * @expectedException \SchemaKeeper\Exception\KeeperException
48 | * @expectedExceptionMessage Config file must return instance of SchemaKeeper\Provider\PostgreSQL\PSQLParameters
49 | */
50 | function testConfigBadInstance()
51 | {
52 | file_put_contents('/tmp/dummyconfig', '');
53 |
54 | $this->target->parse(['c' => '/tmp/dummyconfig'], []);
55 | }
56 |
57 | /**
58 | * @expectedException \SchemaKeeper\Exception\KeeperException
59 | * @expectedExceptionMessage Destination path not specified
60 | */
61 | function testWithoutDestinationPath()
62 | {
63 | $this->target->parse(['c' => '/data/.dev/keeper-config.php'], []);
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/SchemaKeeper/Worker/Verifier.php:
--------------------------------------------------------------------------------
1 |
5 | * For the full copyright and license information, please view the LICENSE file that was distributed with this source code.
6 | */
7 |
8 | namespace SchemaKeeper\Worker;
9 |
10 | use Exception;
11 | use SchemaKeeper\Core\ArrayConverter;
12 | use SchemaKeeper\Core\DumpComparator;
13 | use SchemaKeeper\Core\Dumper;
14 | use SchemaKeeper\Core\SchemaFilter;
15 | use SchemaKeeper\Core\SectionComparator;
16 | use SchemaKeeper\Exception\NotEquals;
17 | use SchemaKeeper\Filesystem\DumpReader;
18 | use SchemaKeeper\Filesystem\FilesystemHelper;
19 | use SchemaKeeper\Filesystem\SectionReader;
20 | use SchemaKeeper\Provider\IProvider;
21 |
22 | /**
23 | * @internal
24 | */
25 | class Verifier
26 | {
27 | /**
28 | * @var Dumper
29 | */
30 | private $dumper;
31 |
32 | /**
33 | * @var DumpReader
34 | */
35 | private $dumpReader;
36 |
37 | /**
38 | * @var DumpComparator
39 | */
40 | private $comparator;
41 |
42 |
43 | public function __construct(IProvider $provider)
44 | {
45 | $schemaFilter = new SchemaFilter();
46 | $this->dumper = new Dumper($provider, $schemaFilter);
47 |
48 | $helper = new FilesystemHelper();
49 | $sectionReader = new SectionReader($helper);
50 | $this->dumpReader = new DumpReader($sectionReader, $helper);
51 | $converter = new ArrayConverter();
52 | $sectionComparator = new SectionComparator();
53 | $this->comparator = new DumpComparator($converter, $sectionComparator);
54 | }
55 |
56 | public function verify(string $sourcePath): void
57 | {
58 | $actual = $this->dumper->dump();
59 | $expected = $this->dumpReader->read($sourcePath);
60 |
61 | $comparisonResult = $this->comparator->compare($expected, $actual);
62 |
63 | if ($comparisonResult['expected'] !== $comparisonResult['actual']) {
64 | throw new NotEquals('Dump and current database not equals:', $comparisonResult['expected'], $comparisonResult['actual']);
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/tests/SchemaKeeper/Provider/PostgreSQL/PSQLClientTest.php:
--------------------------------------------------------------------------------
1 |
5 | * For the full copyright and license information, please view the LICENSE file that was distributed with this source code.
6 | */
7 |
8 | namespace SchemaKeeper\Tests\Provider\PostgreSQL;
9 |
10 | use SchemaKeeper\Provider\PostgreSQL\PSQLClient;
11 | use SchemaKeeper\Tests\SchemaTestCase;
12 |
13 | class PSQLClientTest extends SchemaTestCase
14 | {
15 | /**
16 | * @var PSQLClient
17 | */
18 | private $target;
19 |
20 | public function setUp()
21 | {
22 | parent::setUp();
23 |
24 | $dbParams = $this->getDbParams();
25 |
26 | $this->target = new PSQLClient(
27 | $dbParams->getExecutable(),
28 | $dbParams->getDbName(),
29 | $dbParams->getHost(),
30 | $dbParams->getPort(),
31 | $dbParams->getUser(),
32 | $dbParams->getPassword()
33 | );
34 | }
35 |
36 | public function testOneCommandOk()
37 | {
38 | $expected = "12345";
39 |
40 | $actual = $this->target->run("\qecho -n 12345");
41 |
42 | self::assertEquals($expected, $actual);
43 | }
44 |
45 | public function testMultipleCommands()
46 | {
47 | $expected = [
48 | "12345",
49 | "54321"
50 | ];
51 |
52 | $actual = $this->target->runMultiple(["\qecho -n 12345", "\qecho -n 54321"]);
53 |
54 | self::assertEquals($expected, $actual);
55 | }
56 |
57 | public function testBatchCommands()
58 | {
59 | $commands = [];
60 |
61 | for ($i = 0; $i < 502; $i++) {
62 | $commands[] = '\qecho -n num'.$i;
63 | }
64 |
65 | $actual = $this->target->runMultiple($commands);
66 |
67 | self::assertCount(502, $actual);
68 | self::assertEquals('num501', $actual[501]);
69 | }
70 |
71 | public function testEmptyCommands()
72 | {
73 | $expected = [];
74 |
75 | $actual = $this->target->runMultiple([]);
76 |
77 | self::assertEquals($expected, $actual);
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/SchemaKeeper/Core/Dumper.php:
--------------------------------------------------------------------------------
1 |
5 | * For the full copyright and license information, please view the LICENSE file that was distributed with this source code.
6 | */
7 |
8 | namespace SchemaKeeper\Core;
9 |
10 | use SchemaKeeper\Provider\IProvider;
11 |
12 | /**
13 | * @internal
14 | */
15 | class Dumper
16 | {
17 | /**
18 | * @var IProvider
19 | */
20 | private $provider;
21 |
22 | /**
23 | * @var SchemaFilter
24 | */
25 | private $filter;
26 |
27 |
28 | public function __construct(IProvider $provider, SchemaFilter $filter)
29 | {
30 | $this->provider = $provider;
31 | $this->filter = $filter;
32 | }
33 |
34 | public function dump(): Dump
35 | {
36 | $schemas = [];
37 |
38 | $tables = $this->provider->getTables();
39 | $views = $this->provider->getViews();
40 | $materializedViews = $this->provider->getMaterializedViews();
41 | $types = $this->provider->getTypes();
42 | $functions = $this->provider->getFunctions();
43 | $triggers = $this->provider->getTriggers();
44 | $sequences = $this->provider->getSequences();
45 |
46 | foreach ($this->provider->getSchemas() as $schemaName) {
47 | $structure = new SchemaStructure($schemaName);
48 |
49 | $structure->setTables($this->filter->filter($schemaName, $tables));
50 | $structure->setViews($this->filter->filter($schemaName, $views));
51 | $structure->setMaterializedViews($this->filter->filter($schemaName, $materializedViews));
52 | $structure->setTypes($this->filter->filter($schemaName, $types));
53 | $structure->setFunctions($this->filter->filter($schemaName, $functions));
54 | $structure->setTriggers($this->filter->filter($schemaName, $triggers));
55 | $structure->setSequences($this->filter->filter($schemaName, $sequences));
56 |
57 | $schemas[] = $structure;
58 | }
59 |
60 | $dump = new Dump($schemas, $this->provider->getExtensions());
61 |
62 | return $dump;
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/tests/SchemaKeeper/Filesystem/SectionReaderTest.php:
--------------------------------------------------------------------------------
1 |
5 | * For the full copyright and license information, please view the LICENSE file that was distributed with this source code.
6 | */
7 |
8 | namespace SchemaKeeper\Tests\Filesystem;
9 |
10 | use Mockery\MockInterface;
11 | use SchemaKeeper\Filesystem\FilesystemHelper;
12 | use SchemaKeeper\Filesystem\SectionReader;
13 | use SchemaKeeper\Tests\SchemaTestCase;
14 |
15 | class SectionReaderTest extends SchemaTestCase
16 | {
17 | /**
18 | * @var SectionReader
19 | */
20 | private $target;
21 |
22 | /**
23 | * @var FilesystemHelper|MockInterface
24 | */
25 | private $helper;
26 |
27 | public function setUp()
28 | {
29 | parent::setUp();
30 |
31 | $this->helper = \Mockery::mock(FilesystemHelper::class);
32 | $this->helper->shouldReceive('isDir')->andReturnTrue()->byDefault();
33 |
34 | $this->target = new SectionReader($this->helper);
35 | }
36 |
37 | public function testOk()
38 | {
39 | $files = [
40 | '/etc/file1.sql',
41 | '/etc/file2.sh',
42 | '/etc/file3.txt',
43 | ];
44 |
45 | $this->helper->shouldReceive('glob')->with('/etc/*')->andReturn($files)->once();
46 |
47 | $this->helper->shouldReceive('fileGetContents')->with('/etc/file1.sql')->andReturn('file_content1')->ordered();
48 | $this->helper->shouldReceive('fileGetContents')->with('/etc/file3.txt')->andReturn('file_content2')->ordered();
49 |
50 | $actual = $this->target->readSection('/etc');
51 |
52 | $expected = [
53 | 'file1' => 'file_content1',
54 | 'file3' => 'file_content2'
55 | ];
56 |
57 | self::assertEquals($expected, $actual);
58 | }
59 |
60 | public function testNotExistedDirectory()
61 | {
62 | $this->helper->shouldReceive('isDir')->andReturnFalse()->once();
63 | $this->helper->shouldNotReceive('glob');
64 |
65 | $actual = $this->target->readSection('/etc');
66 |
67 | self::assertEquals([], $actual);
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/tests/SchemaKeeper/CLI/RunnerTest.php:
--------------------------------------------------------------------------------
1 |
5 | * For the full copyright and license information, please view the LICENSE file that was distributed with this source code.
6 | */
7 |
8 | namespace SchemaKeeper\Tests\CLI;
9 |
10 | use Mockery\MockInterface;
11 | use SchemaKeeper\CLI\Runner;
12 | use SchemaKeeper\Keeper;
13 | use SchemaKeeper\Outside\DeployedFunctions;
14 | use SchemaKeeper\Tests\SchemaTestCase;
15 |
16 | class RunnerTest extends SchemaTestCase
17 | {
18 | /**
19 | * @var Runner
20 | */
21 | private $target;
22 |
23 | /**
24 | * @var Keeper|MockInterface
25 | */
26 | private $keeper;
27 |
28 | function setUp()
29 | {
30 | parent::setUp();
31 |
32 | $this->keeper = \Mockery::mock(Keeper::class);
33 | $this->target = new Runner($this->keeper);
34 | }
35 |
36 | function testSave()
37 | {
38 | $this->keeper->shouldReceive('saveDump')->with('/tmp/dump')->once();
39 |
40 | $message = $this->target->run('save', '/tmp/dump');
41 |
42 | self::assertEquals('Dump saved /tmp/dump', $message);
43 | }
44 |
45 | function testVerify()
46 | {
47 | $this->keeper->shouldReceive('verifyDump')->with('/tmp/dump')->once();
48 |
49 | $message = $this->target->run('verify', '/tmp/dump');
50 |
51 | self::assertEquals('Dump verified /tmp/dump', $message);
52 | }
53 |
54 | function testDeploy()
55 | {
56 | $this->keeper->shouldReceive('deployDump')->with('/tmp/dump')->andReturn(new DeployedFunctions(['2'], ['1', '11'], ['3']))
57 | ->once();
58 |
59 | $message = $this->target->run('deploy', '/tmp/dump');
60 |
61 | self::assertEquals("Dump deployed /tmp/dump\n Deleted 3\n Created 1\n Created 11\n Changed 2", $message);
62 | }
63 |
64 | /**
65 | * @expectedException \SchemaKeeper\Exception\KeeperException
66 | * @expectedExceptionMessage Unrecognized command blabla. Available commands: save, verify, deploy
67 | */
68 | function testUndefinedFunction()
69 | {
70 | $this->target->run('blabla', '/tmp/dump');
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/SchemaKeeper/CLI/Runner.php:
--------------------------------------------------------------------------------
1 |
5 | * For the full copyright and license information, please view the LICENSE file that was distributed with this source code.
6 | */
7 |
8 | namespace SchemaKeeper\CLI;
9 |
10 | use SchemaKeeper\Exception\KeeperException;
11 | use SchemaKeeper\Keeper;
12 |
13 | /**
14 | * @internal
15 | */
16 | class Runner
17 | {
18 | /**
19 | * @var Keeper
20 | */
21 | private $keeper;
22 |
23 | public function __construct(Keeper $keeper)
24 | {
25 | $this->keeper = $keeper;
26 | }
27 |
28 | public function run(string $command, string $path): string
29 | {
30 | switch ($command) {
31 | case 'save':
32 | $this->keeper->saveDump($path);
33 | $message = 'Dump saved ' . $path;
34 |
35 | break;
36 | case 'verify':
37 | $this->keeper->verifyDump($path);
38 |
39 | $message = 'Dump verified ' . $path;
40 |
41 | break;
42 | case 'deploy':
43 | $result = $this->keeper->deployDump($path);
44 |
45 | $message = '';
46 |
47 | foreach ($result->getDeleted() as $nameDeleted) {
48 | $message .= PHP_EOL . " Deleted $nameDeleted";
49 | }
50 |
51 | foreach ($result->getCreated() as $nameCreated) {
52 | $message .= PHP_EOL . " Created $nameCreated";
53 | }
54 |
55 | foreach ($result->getChanged() as $nameChanged) {
56 | $message .= PHP_EOL . " Changed $nameChanged";
57 | }
58 |
59 | if ($message) {
60 | $message = 'Dump deployed ' . $path . $message;
61 | } else {
62 | $message = 'Nothing to deploy ' . $path;
63 | }
64 |
65 | break;
66 | default:
67 | throw new KeeperException('Unrecognized command ' . $command . '. Available commands: save, verify, deploy');
68 |
69 | break;
70 | }
71 |
72 | return $message;
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/SchemaKeeper/Filesystem/DumpWriter.php:
--------------------------------------------------------------------------------
1 |
5 | * For the full copyright and license information, please view the LICENSE file that was distributed with this source code.
6 | */
7 |
8 | namespace SchemaKeeper\Filesystem;
9 |
10 | use Exception;
11 | use SchemaKeeper\Core\Dump;
12 |
13 | /**
14 | * @internal
15 | */
16 | class DumpWriter
17 | {
18 | /**
19 | * @var SectionWriter
20 | */
21 | private $sectionWriter;
22 |
23 | /**
24 | * @var FilesystemHelper
25 | */
26 | private $helper;
27 |
28 |
29 | public function __construct(SectionWriter $sectionWriter, FilesystemHelper $helper)
30 | {
31 | $this->sectionWriter = $sectionWriter;
32 | $this->helper = $helper;
33 | }
34 |
35 | public function write(string $path, Dump $dump): void
36 | {
37 | $structurePath = $path.'/structure';
38 | $extensionsPath = $path.'/extensions';
39 |
40 | $this->helper->rmDirIfExisted($extensionsPath);
41 | $this->sectionWriter->writeSection($extensionsPath, $dump->getExtensions());
42 |
43 | foreach ($dump->getSchemas() as $schemaDump) {
44 | $schemaPath = $structurePath.'/'.$schemaDump->getSchemaName();
45 |
46 | $this->helper->rmDirIfExisted($schemaPath);
47 | $this->helper->mkdir($schemaPath, 0775, true);
48 |
49 | $this->helper->filePutContents($schemaPath.'/.gitkeep', '');
50 |
51 | $this->sectionWriter->writeSection($schemaPath . '/tables', $schemaDump->getTables());
52 | $this->sectionWriter->writeSection($schemaPath . '/views', $schemaDump->getViews());
53 | $this->sectionWriter->writeSection($schemaPath . '/materialized_views', $schemaDump->getMaterializedViews());
54 | $this->sectionWriter->writeSection($schemaPath . '/types', $schemaDump->getTypes());
55 | $this->sectionWriter->writeSection($schemaPath . '/functions', $schemaDump->getFunctions());
56 | $this->sectionWriter->writeSection($schemaPath . '/triggers', $schemaDump->getTriggers());
57 | $this->sectionWriter->writeSection($schemaPath . '/sequences', $schemaDump->getSequences());
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/tests/SchemaKeeper/Filesystem/SectionWriterTest.php:
--------------------------------------------------------------------------------
1 |
5 | * For the full copyright and license information, please view the LICENSE file that was distributed with this source code.
6 | */
7 |
8 | namespace SchemaKeeper\Tests\Filesystem;
9 |
10 | use Mockery\MockInterface;
11 | use SchemaKeeper\Filesystem\FilesystemHelper;
12 | use SchemaKeeper\Filesystem\SectionWriter;
13 | use SchemaKeeper\Tests\SchemaTestCase;
14 |
15 | class SectionWriterTest extends SchemaTestCase
16 | {
17 | /**
18 | * @var SectionWriter
19 | */
20 | private $target;
21 |
22 | /**
23 | * @var FilesystemHelper|MockInterface
24 | */
25 | private $helper;
26 |
27 | public function setUp()
28 | {
29 | parent::setUp();
30 |
31 | $this->helper = \Mockery::mock(FilesystemHelper::class);
32 | $this->helper->shouldReceive('mkdir')->andReturnTrue()->byDefault();
33 |
34 | $this->target = new SectionWriter($this->helper);
35 | }
36 |
37 | public function testTablesOk()
38 | {
39 | $this->helper->shouldReceive('filePutContents')->with('/etc/tables/test1.txt', 'content1')->ordered();
40 | $this->helper->shouldReceive('filePutContents')->with('/etc/tables/test2.txt', 'content2')->ordered();
41 |
42 | $this->target->writeSection('/etc/tables', [
43 | 'test1' => 'content1',
44 | 'test2' => 'content2',
45 | ]);
46 | }
47 |
48 | public function testFunctionsOk()
49 | {
50 | $this->helper->shouldReceive('filePutContents')->with('/etc/functions/test1.sql', 'content1')->ordered();
51 |
52 | $this->target->writeSection('/etc/functions', [
53 | 'test1' => 'content1',
54 | ]);
55 | }
56 |
57 | public function testTriggersOk()
58 | {
59 | $this->helper->shouldReceive('filePutContents')->with('/etc/triggers/test1.sql', 'content1')->ordered();
60 |
61 | $this->target->writeSection('/etc/triggers', [
62 | 'test1' => 'content1',
63 | ]);
64 | }
65 |
66 | public function testEmptyContent()
67 | {
68 | $this->helper->shouldNotReceive('mkdir');
69 | $this->target->writeSection('/etc/tables', []);
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/SchemaKeeper/Core/SectionComparator.php:
--------------------------------------------------------------------------------
1 |
5 | * For the full copyright and license information, please view the LICENSE file that was distributed with this source code.
6 | */
7 |
8 | namespace SchemaKeeper\Core;
9 |
10 | /**
11 | * @internal
12 | */
13 | class SectionComparator
14 | {
15 | /**
16 | * @param string $sectionName
17 | * @param array $expectedSection
18 | * @param array $actualSection
19 | * @return array{expected:array,actual:array}
20 | */
21 | public function compareSection(string $sectionName, array $expectedSection, array $actualSection): array
22 | {
23 | if ($expectedSection === $actualSection) {
24 | return ['expected' => [], 'actual' => []];
25 | }
26 |
27 | $compared = $this->doCompare($sectionName, $expectedSection, $actualSection);
28 | $comparedInverted = $this->doCompare($sectionName, $actualSection, $expectedSection);
29 |
30 | $result = [
31 | 'expected' => array_merge($compared['expected'], $comparedInverted['actual']),
32 | 'actual' => array_merge($compared['actual'], $comparedInverted['expected']),
33 | ];
34 |
35 | return $result;
36 | }
37 |
38 | /**
39 | * @param string $sectionName
40 | * @param array $expectedSection
41 | * @param array $actualSection
42 | * @return array
43 | */
44 | private function doCompare(string $sectionName, array $expectedSection, array $actualSection): array
45 | {
46 | $result = [
47 | 'expected' => [],
48 | 'actual' => [],
49 | ];
50 |
51 | foreach ($expectedSection as $expectedItemName => $expectedItemContent) {
52 | $actualItemContent = isset($actualSection[$expectedItemName]) ? $actualSection[$expectedItemName] : '';
53 | if ($expectedItemContent === $actualItemContent) {
54 | continue;
55 | }
56 |
57 | $result['expected'][$sectionName][$expectedItemName] = $expectedSection[$expectedItemName];
58 | if ($actualItemContent) {
59 | $result['actual'][$sectionName][$expectedItemName] = $actualItemContent;
60 | }
61 | }
62 |
63 | return $result;
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/SchemaKeeper/Filesystem/DumpReader.php:
--------------------------------------------------------------------------------
1 |
5 | * For the full copyright and license information, please view the LICENSE file that was distributed with this source code.
6 | */
7 |
8 | namespace SchemaKeeper\Filesystem;
9 |
10 | use SchemaKeeper\Core\Dump;
11 | use SchemaKeeper\Core\SchemaStructure;
12 | use SchemaKeeper\Exception\KeeperException;
13 |
14 | /**
15 | * @internal
16 | */
17 | class DumpReader
18 | {
19 | /**
20 | * @var SectionReader
21 | */
22 | private $sectionReader;
23 |
24 | /**
25 | * @var FilesystemHelper
26 | */
27 | private $helper;
28 |
29 |
30 | public function __construct(SectionReader $sectionReader, FilesystemHelper $helper)
31 | {
32 | $this->sectionReader = $sectionReader;
33 | $this->helper = $helper;
34 | }
35 |
36 | public function read(string $path): Dump
37 | {
38 | $structurePath = $path.'/structure';
39 | $extensionsPath = $path.'/extensions';
40 |
41 | $extensions = $this->sectionReader->readSection($extensionsPath);
42 |
43 | $schemas = [];
44 |
45 | foreach ($this->helper->glob($structurePath . '/*') as $schemaPath) {
46 | $parts = pathinfo($schemaPath);
47 | $schemaName = $parts['filename'];
48 |
49 | $structure = new SchemaStructure($schemaName);
50 |
51 | $structure->setTables($this->sectionReader->readSection($schemaPath.'/tables'));
52 | $structure->setViews($this->sectionReader->readSection($schemaPath.'/views'));
53 | $structure->setMaterializedViews($this->sectionReader->readSection($schemaPath.'/materialized_views'));
54 | $structure->setTypes($this->sectionReader->readSection($schemaPath.'/types'));
55 | $structure->setFunctions($this->sectionReader->readSection($schemaPath.'/functions'));
56 | $structure->setTriggers($this->sectionReader->readSection($schemaPath.'/triggers'));
57 | $structure->setSequences($this->sectionReader->readSection($schemaPath.'/sequences'));
58 |
59 | $schemas[] = $structure;
60 | }
61 |
62 | if (!$schemas) {
63 | throw new KeeperException('Dump is empty '.$path);
64 | }
65 |
66 | $dump = new Dump($schemas, $extensions);
67 |
68 | return $dump;
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/SchemaKeeper/Keeper.php:
--------------------------------------------------------------------------------
1 |
5 | * For the full copyright and license information, please view the LICENSE file that was distributed with this source code.
6 | */
7 |
8 | namespace SchemaKeeper;
9 |
10 | use Exception;
11 | use PDO;
12 | use SchemaKeeper\Outside\DeployedFunctions;
13 | use SchemaKeeper\Provider\ProviderFactory;
14 | use SchemaKeeper\Worker\Deployer;
15 | use SchemaKeeper\Worker\Saver;
16 | use SchemaKeeper\Worker\Verifier;
17 |
18 | /**
19 | * @api
20 | */
21 | class Keeper
22 | {
23 | /**
24 | * @var Saver
25 | */
26 | private $saver;
27 |
28 | /**
29 | * @var Deployer
30 | */
31 | private $deployer;
32 |
33 | /**
34 | * @var Verifier
35 | */
36 | private $verifier;
37 |
38 | /**
39 | * @param PDO $conn
40 | * @param object $parameters Depends on DBMS. Only PSQLParameters supported now (PostgreSQL)
41 | * @throws Exception
42 | * @see \SchemaKeeper\Provider\PostgreSQL\PSQLParameters
43 | */
44 | public function __construct(PDO $conn, $parameters = null)
45 | {
46 | $factory = new ProviderFactory();
47 | $provider = $factory->createProvider($conn, $parameters);
48 |
49 | $this->saver = new Saver($provider);
50 | $this->deployer = new Deployer($provider);
51 | $this->verifier = new Verifier($provider);
52 | }
53 |
54 | /**
55 | * Make structure dump from current database and save it in filesystem
56 | * @param string $destinationPath Dump will be saved in this folder
57 | * @throws Exception
58 | */
59 | public function saveDump(string $destinationPath): void
60 | {
61 | $this->saver->save($destinationPath);
62 | }
63 |
64 | /**
65 | * Compare current dump with dump previously saved in filesystem.
66 | * @param string $dumpPath Path to previously saved dump
67 | * @throws Exception
68 | */
69 | public function verifyDump(string $dumpPath): void
70 | {
71 | $this->verifier->verify($dumpPath);
72 | }
73 |
74 | /**
75 | * Deploy functions from dump previously saved in filesystem.
76 | * @param string $dumpPath Path to previously saved dump
77 | * @return DeployedFunctions
78 | * @throws Exception
79 | */
80 | public function deployDump(string $dumpPath): DeployedFunctions
81 | {
82 | return $this->deployer->deploy($dumpPath);
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/tests/SchemaKeeper/Provider/PostgreSQL/SavepointHelperTest.php:
--------------------------------------------------------------------------------
1 |
5 | * For the full copyright and license information, please view the LICENSE file that was distributed with this source code.
6 | */
7 |
8 | namespace SchemaKeeper\Tests\Provider\PostgreSQL;
9 |
10 | use Mockery\MockInterface;
11 | use PDO;
12 | use SchemaKeeper\Provider\PostgreSQL\SavepointHelper;
13 | use SchemaKeeper\Tests\SchemaTestCase;
14 |
15 | class SavepointHelperTest extends SchemaTestCase
16 | {
17 | /**
18 | * @var SavepointHelper
19 | */
20 | private $target;
21 |
22 | /**
23 | * @var PDO|MockInterface
24 | */
25 | private $conn;
26 |
27 | public function setUp()
28 | {
29 | parent::setUp();
30 |
31 | $this->conn = \Mockery::mock(PDO::class);
32 |
33 | $this->target = new SavepointHelper($this->conn);
34 | }
35 |
36 | public function testBeginTransaction()
37 | {
38 | $this->conn->shouldNotReceive('exec');
39 | $this->conn->shouldReceive('beginTransaction')->andReturnFalse()->once();
40 |
41 | $this->target->beginTransaction('test', false);
42 | }
43 |
44 | public function testBeginTransactionUsingSavepoint()
45 | {
46 | $this->conn->shouldReceive('exec')->with('SAVEPOINT test')->once();
47 | $this->conn->shouldNotReceive('beginTransaction');
48 |
49 | $this->target->beginTransaction('test', true);
50 | }
51 |
52 | public function testCommit()
53 | {
54 | $this->conn->shouldNotReceive('exec');
55 | $this->conn->shouldReceive('commit')->andReturnFalse()->once();
56 |
57 | $this->target->commit('test', false);
58 | }
59 |
60 | public function testCommitUsingSavepoint()
61 | {
62 | $this->conn->shouldReceive('exec')->with('RELEASE SAVEPOINT test')->once();
63 | $this->conn->shouldNotReceive('commit');
64 |
65 | $this->target->commit('test', true);
66 | }
67 |
68 | public function testRollback()
69 | {
70 | $this->conn->shouldNotReceive('exec');
71 | $this->conn->shouldReceive('rollback')->andReturnFalse()->once();
72 |
73 | $this->target->rollback('test', false);
74 | }
75 |
76 | public function testRollbackUsingSavepoint()
77 | {
78 | $this->conn->shouldReceive('exec')->with('ROLLBACK TO SAVEPOINT test')->once();
79 | $this->conn->shouldNotReceive('rollback');
80 |
81 | $this->target->rollback('test', true);
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/tests/SchemaKeeper/Core/DumpComparatorTest.php:
--------------------------------------------------------------------------------
1 |
5 | * For the full copyright and license information, please view the LICENSE file that was distributed with this source code.
6 | */
7 |
8 | namespace SchemaKeeper\Tests\Core;
9 |
10 | use Mockery\MockInterface;
11 | use SchemaKeeper\Core\ArrayConverter;
12 | use SchemaKeeper\Core\Dump;
13 | use SchemaKeeper\Core\DumpComparator;
14 | use SchemaKeeper\Core\SectionComparator;
15 | use SchemaKeeper\Tests\SchemaTestCase;
16 |
17 | class DumpComparatorTest extends SchemaTestCase
18 | {
19 | /**
20 | * @var DumpComparator
21 | */
22 | private $target;
23 |
24 | /**
25 | * @var ArrayConverter|MockInterface
26 | */
27 | private $converter;
28 |
29 | /**
30 | * @var SectionComparator|MockInterface
31 | */
32 | private $sectionComparator;
33 |
34 | public function setUp()
35 | {
36 | parent::setUp();
37 |
38 | $this->converter = \Mockery::mock(ArrayConverter::class);
39 | $this->sectionComparator = \Mockery::mock(SectionComparator::class);
40 | $this->target = new DumpComparator($this->converter, $this->sectionComparator);
41 | }
42 |
43 | public function testOk()
44 | {
45 | $dump1 = new Dump([], ['ext1']);
46 | $dump2 = new Dump([], ['ext2']);
47 |
48 | $this->converter->shouldReceive('dump2Array')->with($dump1)->andReturn([
49 | 'tables' => ['table1'],
50 | 'functions' => ['function1'],
51 | ])->once();
52 |
53 | $this->converter->shouldReceive('dump2Array')->with($dump2)->andReturn([
54 | 'tables' => ['table2'],
55 | 'functions' => ['function2'],
56 | ])->once();
57 |
58 | $this->sectionComparator->shouldReceive('compareSection')->with('tables', ['table1'], ['table2'])->andReturn([
59 | 'expected' => 'left1',
60 | 'actual' => 'right1',
61 | ])->once();
62 |
63 | $this->sectionComparator->shouldReceive('compareSection')->with('functions', ['function1'], ['function2'])->andReturn([
64 | 'expected' => 'left2',
65 | 'actual' => 'right2',
66 | ])->once();
67 |
68 | $expected = [
69 | 'expected' => [
70 | 'left1',
71 | 'left2',
72 | ],
73 | 'actual' => [
74 | 'right1',
75 | 'right2',
76 | ],
77 | ];
78 |
79 | $actual = $this->target->compare($dump1, $dump2);
80 |
81 | self::assertEquals($expected, $actual);
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/src/SchemaKeeper/Provider/PostgreSQL/PSQLParameters.php:
--------------------------------------------------------------------------------
1 |
5 | * For the full copyright and license information, please view the LICENSE file that was distributed with this source code.
6 | */
7 |
8 | namespace SchemaKeeper\Provider\PostgreSQL;
9 |
10 | /**
11 | * @api
12 | */
13 | class PSQLParameters
14 | {
15 | /**
16 | * @var string
17 | */
18 | private $host;
19 |
20 | /**
21 | * @var int
22 | */
23 | private $port;
24 |
25 | /**
26 | * @var string
27 | */
28 | private $dbName;
29 |
30 | /**
31 | * @var string
32 | */
33 | private $user;
34 |
35 | /**
36 | * @var string
37 | */
38 | private $password;
39 |
40 | /**
41 | * @var string
42 | */
43 | private $executable = 'psql';
44 |
45 | /**
46 | * @var string[]
47 | */
48 | private $skippedSchemaNames = [];
49 |
50 | /**
51 | * @var string[]
52 | */
53 | private $skippedExtensionNames = [];
54 |
55 |
56 | public function __construct(string $host, int $port, string $dbName, string $user, string $password)
57 | {
58 | $this->host = $host;
59 | $this->port = $port;
60 | $this->dbName = $dbName;
61 | $this->user = $user;
62 | $this->password = $password;
63 | }
64 |
65 | public function getDbName(): string
66 | {
67 | return $this->dbName;
68 | }
69 |
70 | public function getUser(): string
71 | {
72 | return $this->user;
73 | }
74 |
75 | public function getPassword(): string
76 | {
77 | return $this->password;
78 | }
79 |
80 | public function getHost(): string
81 | {
82 | return $this->host;
83 | }
84 |
85 | public function getPort(): int
86 | {
87 | return $this->port;
88 | }
89 |
90 | public function getExecutable(): string
91 | {
92 | return $this->executable;
93 | }
94 |
95 | public function setExecutable(string $executable): void
96 | {
97 | $this->executable = $executable;
98 | }
99 |
100 | public function getSkippedSchemas(): array
101 | {
102 | return $this->skippedSchemaNames;
103 | }
104 |
105 | public function setSkippedSchemas(array $skippedSchemaNames): void
106 | {
107 | $this->skippedSchemaNames = $skippedSchemaNames;
108 | }
109 |
110 | public function getSkippedExtensions(): array
111 | {
112 | return $this->skippedExtensionNames;
113 | }
114 |
115 | public function setSkippedExtensions(array $skippedExtensionNames): void
116 | {
117 | $this->skippedExtensionNames = $skippedExtensionNames;
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/tests/SchemaKeeper/Worker/VerifierTest.php:
--------------------------------------------------------------------------------
1 |
5 | * For the full copyright and license information, please view the LICENSE file that was distributed with this source code.
6 | */
7 |
8 | namespace SchemaKeeper\Tests\Worker;
9 |
10 | use SchemaKeeper\Exception\NotEquals;
11 | use SchemaKeeper\Provider\ProviderFactory;
12 | use SchemaKeeper\Tests\SchemaTestCase;
13 | use SchemaKeeper\Worker\Saver;
14 | use SchemaKeeper\Worker\Verifier;
15 |
16 | class VerifierTest extends SchemaTestCase
17 | {
18 | /**
19 | * @var Verifier
20 | */
21 | private $target;
22 |
23 | /**
24 | * @var Saver
25 | */
26 | private $saver;
27 |
28 | public function setUp()
29 | {
30 | parent::setUp();
31 |
32 | $conn = $this->getConn();
33 | $params = $this->getDbParams();
34 | $providerFactory = new ProviderFactory();
35 | $provider = $providerFactory->createProvider($conn, $params);
36 |
37 | $this->target = new Verifier($provider);
38 | $this->saver = new Saver($provider);
39 |
40 | exec('rm -rf /tmp/schema_keeper');
41 | }
42 |
43 | public function testOk()
44 | {
45 | $this->saver->save('/tmp/schema_keeper');
46 | $this->target->verify('/tmp/schema_keeper');
47 |
48 | self::assertTrue(true);
49 | }
50 |
51 | public function testDiff()
52 | {
53 | $this->saver->save('/tmp/schema_keeper');
54 | exec('rm -r /tmp/schema_keeper/structure/public/triggers');
55 |
56 | $catch = false;
57 |
58 | try {
59 | $this->target->verify('/tmp/schema_keeper');
60 | } catch (NotEquals $e) {
61 | $catch = true;
62 | $expectedMessage = 'Dump and current database not equals:
63 | {
64 | "expected": [],
65 | "actual": {
66 | "triggers": {
67 | "public.test_table.test_trigger": "CREATE TRIGGER test_trigger BEFORE UPDATE ON test_table FOR EACH ROW EXECUTE PROCEDURE trig_test()"
68 | }
69 | }
70 | }';
71 | $expectedTriggers = [
72 | 'triggers' => [
73 | 'public.test_table.test_trigger' => 'CREATE TRIGGER test_trigger BEFORE UPDATE ON test_table FOR EACH ROW EXECUTE PROCEDURE trig_test()',
74 | ],
75 | ];
76 |
77 | self::assertEquals($expectedMessage, $e->getMessage());
78 | self::assertEquals([], $e->getExpected());
79 | self::assertEquals($expectedTriggers, $e->getActual());
80 | }
81 |
82 | self::assertTrue($catch);
83 | }
84 |
85 | /**
86 | * @expectedException \SchemaKeeper\Exception\KeeperException
87 | * @expectedExceptionMessage Dump is empty /tmp/schema_keeper
88 | */
89 | function testEmptyDump()
90 | {
91 | $this->target->verify('/tmp/schema_keeper');
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/tests/SchemaKeeper/CLI/EntryPointTest.php:
--------------------------------------------------------------------------------
1 |
5 | * For the full copyright and license information, please view the LICENSE file that was distributed with this source code.
6 | */
7 |
8 | namespace SchemaKeeper\Tests\CLI;
9 |
10 | use SchemaKeeper\CLI\EntryPoint;
11 | use SchemaKeeper\Tests\SchemaTestCase;
12 |
13 | class EntryPointTest extends SchemaTestCase
14 | {
15 | /**
16 | * @var EntryPoint
17 | */
18 | private $target;
19 |
20 | function setUp()
21 | {
22 | parent::setUp();
23 |
24 | $this->target = new EntryPoint();
25 |
26 | exec('rm -rf /tmp/dump');
27 | }
28 |
29 | function testOk()
30 | {
31 | $result = $this->target->run(['c' => '/data/.dev/keeper-config.php', 'd' => '/tmp/dump'], ['save']);
32 | self::assertEquals('Success: Dump saved /tmp/dump', $result->getMessage());
33 | self::assertSame(0, $result->getStatus());
34 |
35 | $result = $this->target->run(['c' => '/data/.dev/keeper-config.php', 'd' => '/tmp/dump'], ['verify']);
36 | self::assertEquals('Success: Dump verified /tmp/dump', $result->getMessage());
37 | self::assertSame(0, $result->getStatus());
38 |
39 | $result = $this->target->run(['c' => '/data/.dev/keeper-config.php', 'd' => '/tmp/dump'], ['deploy']);
40 | self::assertEquals('Success: Nothing to deploy /tmp/dump', $result->getMessage());
41 | self::assertSame(0, $result->getStatus());
42 | }
43 |
44 | function testHelp()
45 | {
46 | $result = $this->target->run(['help' => 0], []);
47 | self::assertContains('Usage: schemakeeper [options] ', $result->getMessage());
48 | self::assertSame(0, $result->getStatus());
49 | }
50 |
51 | function testVersion()
52 | {
53 | $result = $this->target->run(['version' => 0], []);
54 | self::assertEquals('', $result->getMessage());
55 | self::assertSame(0, $result->getStatus());
56 | }
57 |
58 | function testConfigError()
59 | {
60 | $result = $this->target->run([], []);
61 | self::assertEquals('Failure: Config file not found or not readable ', $result->getMessage());
62 |
63 | self::assertSame(1, $result->getStatus());
64 | }
65 |
66 | function testUnrecognizedCommand()
67 | {
68 | $result = $this->target->run(['c' => '/data/.dev/keeper-config.php', 'd' => '/tmp/dump'], ['blabla']);
69 | self::assertEquals(
70 | 'Failure: Unrecognized command blabla. Available commands: save, verify, deploy',
71 | $result->getMessage()
72 | );
73 | self::assertSame(1, $result->getStatus());
74 | }
75 |
76 | function testUnrecognizedOption()
77 | {
78 | $result = $this->target->run(['blabla' => 0], []);
79 | self::assertEquals('Unrecognized option: blabla', $result->getMessage());
80 | self::assertSame(1, $result->getStatus());
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/SchemaKeeper/CLI/EntryPoint.php:
--------------------------------------------------------------------------------
1 |
5 | * For the full copyright and license information, please view the LICENSE file that was distributed with this source code.
6 | */
7 |
8 | namespace SchemaKeeper\CLI;
9 |
10 | use PDO;
11 | use SchemaKeeper\Exception\KeeperException;
12 | use SchemaKeeper\Keeper;
13 |
14 | /**
15 | * @internal
16 | */
17 | class EntryPoint
18 | {
19 | public function run(array $options, array $argv): Result
20 | {
21 | $message = '';
22 |
23 | foreach ($options as $optionName => $optionValue) {
24 | if (!in_array($optionName, ['c', 'd', 'help', 'version'])) {
25 | $message .= 'Unrecognized option: ' . $optionName;
26 |
27 | return new Result($message, 1);
28 | }
29 | }
30 |
31 | if (isset($options['help'])) {
32 | $message .= "Usage: schemakeeper [options] " . PHP_EOL . PHP_EOL .
33 | 'Example: schemakeeper -c /path_to_config.php -d /path_to_dump save' . PHP_EOL . PHP_EOL
34 | . 'Options:' . PHP_EOL
35 | . ' -c The path to a config file' . PHP_EOL
36 | . ' -d The destination path to a dump directory' . PHP_EOL . PHP_EOL
37 | . ' --help Print this help message' . PHP_EOL
38 | . ' --version Print version information' . PHP_EOL . PHP_EOL
39 | . 'Available commands:' . PHP_EOL
40 | . ' save' . PHP_EOL
41 | . ' verify' . PHP_EOL
42 | . ' deploy';
43 |
44 | return new Result($message, 0);
45 | }
46 |
47 | if (isset($options['version'])) {
48 | return new Result($message, 0);
49 | }
50 |
51 | try {
52 | $parser = new Parser();
53 | $parsed = $parser->parse($options, $argv);
54 |
55 | $params = $parsed->getParams();
56 | $path = $parsed->getPath();
57 | $command = $parsed->getCommand();
58 |
59 | $dsn = 'pgsql:dbname=' . $params->getDbName() . ';host=' . $params->getHost() . ';port=' . $params->getPort();
60 | $conn = new PDO(
61 | $dsn,
62 | $params->getUser(),
63 | $params->getPassword(),
64 | [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
65 | );
66 |
67 | $keeper = new Keeper($conn, $params);
68 | $runner = new Runner($keeper);
69 |
70 | $conn->beginTransaction();
71 |
72 | try {
73 | $result = $runner->run($command, $path);
74 | $message .= 'Success: ' . $result;
75 | $conn->commit();
76 | } catch (\Exception $exception) {
77 | $conn->rollBack();
78 | throw $exception;
79 | }
80 |
81 | return new Result($message, 0);
82 | } catch (KeeperException $e) {
83 | $message .= 'Failure: ' . $e->getMessage();
84 | return new Result($message, 1);
85 | }
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/src/SchemaKeeper/Core/SchemaStructure.php:
--------------------------------------------------------------------------------
1 |
5 | * For the full copyright and license information, please view the LICENSE file that was distributed with this source code.
6 | */
7 |
8 | namespace SchemaKeeper\Core;
9 |
10 | /**
11 | * @internal
12 | */
13 | class SchemaStructure
14 | {
15 | /**
16 | * @var string
17 | */
18 | private $schemaName;
19 |
20 | /**
21 | * @var array
22 | */
23 | private $tables = [];
24 |
25 | /**
26 | * @var array
27 | */
28 | private $views = [];
29 |
30 | /**
31 | * @var array
32 | */
33 | private $materializedViews = [];
34 |
35 | /**
36 | * @var array
37 | */
38 | private $types = [];
39 |
40 | /**
41 | * @var array
42 | */
43 | private $functions = [];
44 |
45 | /**
46 | * @var array
47 | */
48 | private $triggers = [];
49 |
50 | /**
51 | * @var array
52 | */
53 | private $sequences = [];
54 |
55 |
56 | public function __construct(string $schemaName)
57 | {
58 | $this->schemaName = $schemaName;
59 | }
60 |
61 | public function getSchemaName(): string
62 | {
63 | return $this->schemaName;
64 | }
65 |
66 | public function getTables(): array
67 | {
68 | return $this->tables;
69 | }
70 |
71 | public function setTables(array $tables)
72 | {
73 | $this->tables = $tables;
74 | }
75 |
76 | public function getViews(): array
77 | {
78 | return $this->views;
79 | }
80 |
81 | public function setViews(array $views)
82 | {
83 | $this->views = $views;
84 | }
85 |
86 | public function getTypes(): array
87 | {
88 | return $this->types;
89 | }
90 |
91 | public function getMaterializedViews(): array
92 | {
93 | return $this->materializedViews;
94 | }
95 |
96 | public function setMaterializedViews(array $materializedViews)
97 | {
98 | $this->materializedViews = $materializedViews;
99 | }
100 |
101 | public function setTypes(array $types)
102 | {
103 | $this->types = $types;
104 | }
105 |
106 | public function getFunctions(): array
107 | {
108 | return $this->functions;
109 | }
110 |
111 | public function setFunctions(array $functions)
112 | {
113 | $this->functions = $functions;
114 | }
115 |
116 | public function getTriggers(): array
117 | {
118 | return $this->triggers;
119 | }
120 |
121 | public function setTriggers(array $triggers)
122 | {
123 | $this->triggers = $triggers;
124 | }
125 |
126 | public function getSequences(): array
127 | {
128 | return $this->sequences;
129 | }
130 |
131 | public function setSequences(array $sequences)
132 | {
133 | $this->sequences = $sequences;
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/src/SchemaKeeper/Provider/PostgreSQL/PSQLClient.php:
--------------------------------------------------------------------------------
1 |
5 | * For the full copyright and license information, please view the LICENSE file that was distributed with this source code.
6 | */
7 |
8 | namespace SchemaKeeper\Provider\PostgreSQL;
9 |
10 | use SchemaKeeper\Exception\KeeperException;
11 |
12 | /**
13 | * @internal
14 | */
15 | class PSQLClient
16 | {
17 | /**
18 | * @var string
19 | */
20 | private $executable;
21 |
22 | /**
23 | * @var string
24 | */
25 | protected $dbName;
26 |
27 | /**
28 | * @var string
29 | */
30 | protected $host;
31 |
32 | /**
33 | * @var int
34 | */
35 | protected $port;
36 |
37 | /**
38 | * @var string
39 | */
40 | protected $user;
41 |
42 | /**
43 | * @var string
44 | */
45 | protected $password;
46 |
47 | public function __construct(string $executable, string $dbName, string $host, int $port, string $user, string $password)
48 | {
49 | $this->executable = $executable;
50 | $this->dbName = $dbName;
51 | $this->host = $host;
52 | $this->port = $port;
53 | $this->user = $user;
54 | $this->password = $password;
55 | }
56 |
57 | public function run(string $command): ?string
58 | {
59 | $this->putPassword();
60 |
61 | $req = "echo " . escapeshellarg($command) . " | ".$this->generateScript();
62 |
63 | return shell_exec($req);
64 | }
65 |
66 | /**
67 | * @param array $commands
68 | * @return array
69 | * @throws KeeperException
70 | */
71 | public function runMultiple(array $commands): array
72 | {
73 | $this->putPassword();
74 |
75 | if (!$commands) {
76 | return [];
77 | }
78 |
79 | $results = [];
80 |
81 | if (count($commands) > 500) {
82 | $parts = array_chunk($commands, 500, true);
83 |
84 | foreach ($parts as $part) {
85 | $results = array_merge($results, $this->runMultiple($part));
86 | }
87 |
88 | return $results;
89 | }
90 |
91 | $commandsString = '';
92 | $separator = '##|$$1$$$$#$$1$$$|##';
93 |
94 |
95 | foreach ($commands as $cmd) {
96 | $commandsString .= ' -c ' . escapeshellarg($cmd) . ' -c ' . escapeshellarg("\qecho -n '" . $separator . "'");
97 | }
98 |
99 | $req = $this->generateScript() . $commandsString;
100 |
101 | $rawOutput = (string) shell_exec($req);
102 |
103 | $outputs = explode($separator, $rawOutput);
104 |
105 | $i = 0;
106 | foreach ($commands as $table => $cmd) {
107 | $results[$table] = $outputs[$i];
108 | $i++;
109 | }
110 |
111 | return $results;
112 | }
113 |
114 | private function generateScript(): string
115 | {
116 | return $this->executable . ' -U' . $this->user . ' -h' . $this->host . ' -p' . $this->port . ' -d' . $this->dbName;
117 | }
118 |
119 | private function putPassword(): void
120 | {
121 | putenv("PGPASSWORD=" . $this->password);
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/tests/SchemaKeeper/Core/ArrayConverterTest.php:
--------------------------------------------------------------------------------
1 |
5 | * For the full copyright and license information, please view the LICENSE file that was distributed with this source code.
6 | */
7 |
8 | namespace SchemaKeeper\Tests\Core;
9 |
10 | use SchemaKeeper\Core\ArrayConverter;
11 | use SchemaKeeper\Core\Dump;
12 | use SchemaKeeper\Core\SchemaStructure;
13 | use SchemaKeeper\Tests\SchemaTestCase;
14 |
15 | class ArrayConverterTest extends SchemaTestCase
16 | {
17 | /**
18 | * @var ArrayConverter
19 | */
20 | private $target;
21 |
22 | public function setUp()
23 | {
24 | parent::setUp();
25 |
26 | $this->target = new ArrayConverter();
27 | }
28 |
29 | public function testOk()
30 | {
31 | $extensions = ['ext1', 'ext2'];
32 | $tables = ['table' => 'table_content'];
33 | $views = ['view' => 'view_content'];
34 | $materializedViews = ['m_view' => 'm_view_content'];
35 | $types = ['type' => 'type_content'];
36 | $functions = ['function' => 'function_content'];
37 | $triggers = ['trigger' => 'trigger_content'];
38 | $sequences = ['sequence' => 'sequence_content'];
39 |
40 | $structure = new SchemaStructure('schema');
41 | $dump = new Dump([$structure], $extensions);
42 | $structure->setTables($tables);
43 | $structure->setViews($views);
44 | $structure->setMaterializedViews($materializedViews);
45 | $structure->setTypes($types);
46 | $structure->setFunctions($functions);
47 | $structure->setTriggers($triggers);
48 | $structure->setSequences($sequences);
49 |
50 | $expected = [
51 | 'schemas' => [
52 | 'schema',
53 | ],
54 | 'extensions' => [
55 | 'ext1',
56 | 'ext2',
57 | ],
58 | 'tables' => [
59 | 'schema.table' => 'table_content',
60 | ],
61 | 'views' => [
62 | 'schema.view' => 'view_content',
63 | ],
64 | 'materialized_views' => [
65 | 'schema.m_view' => 'm_view_content',
66 | ],
67 | 'types' => [
68 | 'schema.type' => 'type_content',
69 | ],
70 | 'functions' => [
71 | 'schema.function' => 'function_content',
72 | ],
73 | 'triggers' => [
74 | 'schema.trigger' => 'trigger_content',
75 | ],
76 | 'sequences' => [
77 | 'schema.sequence' => 'sequence_content',
78 | ],
79 | ];
80 |
81 | $actual = $this->target->dump2Array($dump);
82 |
83 | self::assertEquals($expected, $actual);
84 | }
85 |
86 | function testEmptyDump()
87 | {
88 | $dump = new Dump([], []);
89 |
90 | $actual = $this->target->dump2Array($dump);
91 |
92 | $expected = [
93 | 'tables' => [],
94 | 'views' => [],
95 | 'materialized_views' => [],
96 | 'types' => [],
97 | 'functions' => [],
98 | 'triggers' => [],
99 | 'sequences' => [],
100 | 'schemas' => [],
101 | 'extensions' => [],
102 | ];
103 |
104 | self::assertSame($expected, $actual);
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/tests/SchemaKeeper/Filesystem/DumpReaderTest.php:
--------------------------------------------------------------------------------
1 |
5 | * For the full copyright and license information, please view the LICENSE file that was distributed with this source code.
6 | */
7 |
8 | namespace SchemaKeeper\Tests\Filesystem;
9 |
10 | use Mockery\MockInterface;
11 | use SchemaKeeper\Filesystem\DumpReader;
12 | use SchemaKeeper\Filesystem\FilesystemHelper;
13 | use SchemaKeeper\Filesystem\SectionReader;
14 | use SchemaKeeper\Tests\SchemaTestCase;
15 |
16 | class DumpReaderTest extends SchemaTestCase
17 | {
18 | /**
19 | * @var DumpReader
20 | */
21 | private $target;
22 |
23 | /**
24 | * @var SectionReader|MockInterface
25 | */
26 | private $sectionReader;
27 |
28 | /**
29 | * @var FilesystemHelper|MockInterface
30 | */
31 | private $helper;
32 |
33 | public function setUp()
34 | {
35 | parent::setUp();
36 |
37 | $this->helper = \Mockery::mock(FilesystemHelper::class);
38 | $this->sectionReader = \Mockery::mock(SectionReader::class);
39 |
40 | $this->target = new DumpReader($this->sectionReader, $this->helper);
41 | }
42 |
43 | public function testOk()
44 | {
45 | $this->helper->shouldReceive('glob')->with('/etc/structure/*')->andReturn(['/etc/schema'])->once();
46 |
47 | $extensions = ['ext1', 'ext2'];
48 | $tables = ['table' => 'table_content'];
49 | $views = ['view' => 'view_content'];
50 | $materializedViews = ['m_view' => 'm_view_content'];
51 | $types = ['type' => 'type_content'];
52 | $functions = ['function' => 'function_content'];
53 | $triggers = ['trigger' => 'trigger_content'];
54 | $sequences = ['sequence' => 'sequence_content'];
55 |
56 | $this->sectionReader->shouldReceive('readSection')->with('/etc/extensions')->andReturn($extensions)->once();
57 | $this->sectionReader->shouldReceive('readSection')->with('/etc/schema/tables')->andReturn($tables)->once();
58 | $this->sectionReader->shouldReceive('readSection')->with('/etc/schema/views')->andReturn($views)->once();
59 | $this->sectionReader->shouldReceive('readSection')->with('/etc/schema/materialized_views')->andReturn($materializedViews)->once();
60 | $this->sectionReader->shouldReceive('readSection')->with('/etc/schema/types')->andReturn($types)->once();
61 | $this->sectionReader->shouldReceive('readSection')->with('/etc/schema/functions')->andReturn($functions)->once();
62 | $this->sectionReader->shouldReceive('readSection')->with('/etc/schema/triggers')->andReturn($triggers)->once();
63 | $this->sectionReader->shouldReceive('readSection')->with('/etc/schema/sequences')->andReturn($sequences)->once();
64 |
65 | $dump = $this->target->read('/etc');
66 |
67 | self::assertEquals($extensions, $dump->getExtensions());
68 | self::assertCount(1, $dump->getSchemas());
69 |
70 | $structure = $dump->getSchemas()[0];
71 | self::assertEquals($tables, $structure->getTables());
72 | self::assertEquals($views, $structure->getViews());
73 | self::assertEquals($materializedViews, $structure->getMaterializedViews());
74 | self::assertEquals($types, $structure->getTypes());
75 | self::assertEquals($functions, $structure->getFunctions());
76 | self::assertEquals($triggers, $structure->getTriggers());
77 | self::assertEquals($sequences, $structure->getSequences());
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/.github/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, sex characteristics, gender identity and expression,
9 | level of experience, education, socio-economic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies within all project spaces, and it also applies when
49 | an individual is representing the project or its community in public spaces.
50 | Examples of representing a project or community include using an official
51 | project e-mail address, posting via an official social media account, or acting
52 | as an appointed representative at an online or offline event. Representation of
53 | a project may be further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at . All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
75 | For answers to common questions about this code of conduct, see
76 | https://www.contributor-covenant.org/faq
77 |
--------------------------------------------------------------------------------
/tests/SchemaKeeper/Filesystem/DumpWriterTest.php:
--------------------------------------------------------------------------------
1 |
5 | * For the full copyright and license information, please view the LICENSE file that was distributed with this source code.
6 | */
7 |
8 | namespace SchemaKeeper\Tests\Filesystem;
9 |
10 | use Mockery\MockInterface;
11 | use SchemaKeeper\Core\Dump;
12 | use SchemaKeeper\Core\SchemaStructure;
13 | use SchemaKeeper\Filesystem\DumpWriter;
14 | use SchemaKeeper\Filesystem\FilesystemHelper;
15 | use SchemaKeeper\Filesystem\SectionWriter;
16 | use SchemaKeeper\Tests\SchemaTestCase;
17 |
18 | class DumpWriterTest extends SchemaTestCase
19 | {
20 | /**
21 | * @var DumpWriter
22 | */
23 | private $target;
24 |
25 | /**
26 | * @var SectionWriter|MockInterface
27 | */
28 | private $sectionWriter;
29 |
30 | /**
31 | * @var FilesystemHelper|MockInterface
32 | */
33 | private $helper;
34 |
35 | public function setUp()
36 | {
37 | parent::setUp();
38 |
39 | $this->helper = \Mockery::mock(FilesystemHelper::class);
40 | $this->sectionWriter = \Mockery::mock(SectionWriter::class);
41 |
42 | $this->target = new DumpWriter($this->sectionWriter, $this->helper);
43 | }
44 |
45 | public function testOk()
46 | {
47 | $extensions = ['ext1', 'ext2'];
48 | $tables = ['table' => 'table_content'];
49 | $views = ['view' => 'view_content'];
50 | $materializedViews = ['m_view' => 'm_view_content'];
51 | $types = ['type' => 'type_content'];
52 | $functions = ['function' => 'function_content'];
53 | $triggers = ['trigger' => 'trigger_content'];
54 | $sequences = ['sequence' => 'sequence_content'];
55 |
56 | $this->helper->shouldReceive('rmDirIfExisted')->with('/etc/extensions')->once();
57 | $this->sectionWriter->shouldReceive('writeSection')->with('/etc/extensions', $extensions)->once();
58 | $this->helper->shouldReceive('rmDirIfExisted')->with('/etc/structure/schema')->once();
59 | $this->helper->shouldReceive('mkdir')->with('/etc/structure/schema', 0775, true)->once();
60 | $this->helper->shouldReceive('filePutContents')->with('/etc/structure/schema/.gitkeep', '')->once();
61 | $this->sectionWriter->shouldReceive('writeSection')->with('/etc/structure/schema/tables', $tables)->once();
62 | $this->sectionWriter->shouldReceive('writeSection')->with('/etc/structure/schema/views', $views)->once();
63 | $this->sectionWriter->shouldReceive('writeSection')->with('/etc/structure/schema/materialized_views', $materializedViews)->once();
64 | $this->sectionWriter->shouldReceive('writeSection')->with('/etc/structure/schema/types', $types)->once();
65 | $this->sectionWriter->shouldReceive('writeSection')->with('/etc/structure/schema/functions', $functions)->once();
66 | $this->sectionWriter->shouldReceive('writeSection')->with('/etc/structure/schema/triggers', $triggers)->once();
67 | $this->sectionWriter->shouldReceive('writeSection')->with('/etc/structure/schema/sequences', $sequences)->once();
68 |
69 | $structure = new SchemaStructure('schema');
70 | $structure->setTables($tables);
71 | $structure->setViews($views);
72 | $structure->setMaterializedViews($materializedViews);
73 | $structure->setTypes($types);
74 | $structure->setFunctions($functions);
75 | $structure->setTriggers($triggers);
76 | $structure->setSequences($sequences);
77 |
78 | $dump = new Dump([$structure], $extensions);
79 | $this->target->write('/etc', $dump);
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/tests/SchemaKeeper/Core/DumperTest.php:
--------------------------------------------------------------------------------
1 |
5 | * For the full copyright and license information, please view the LICENSE file that was distributed with this source code.
6 | */
7 |
8 | namespace SchemaKeeper\Tests\Core;
9 |
10 | use Mockery\MockInterface;
11 | use SchemaKeeper\Core\Dump;
12 | use SchemaKeeper\Core\Dumper;
13 | use SchemaKeeper\Core\SchemaFilter;
14 | use SchemaKeeper\Provider\IProvider;
15 | use SchemaKeeper\Tests\SchemaTestCase;
16 |
17 | class DumperTest extends SchemaTestCase
18 | {
19 | /**
20 | * @var Dumper
21 | */
22 | private $target;
23 |
24 | /**
25 | * @var IProvider|MockInterface
26 | */
27 | private $provider;
28 |
29 | /**
30 | * @var SchemaFilter|MockInterface
31 | */
32 | private $filter;
33 |
34 | public function setUp()
35 | {
36 | parent::setUp();
37 |
38 | $this->provider = \Mockery::mock(IProvider::class);
39 | $this->filter = \Mockery::mock(SchemaFilter::class);
40 |
41 | $this->target = new Dumper($this->provider, $this->filter);
42 | }
43 |
44 | public function testOk()
45 | {
46 | $schemas = ['schema1', 'schema1'];
47 | $extensions = ['ext1', 'ext2'];
48 | $tables = ['schema1.table1' => 'table_content1'];
49 | $views = ['schema1.view1' => 'view_content1'];
50 | $matViews = ['schema1.mat_view1' => 'mat_view_content1'];
51 | $types = ['schema1.type1' => 'type_content1'];
52 | $functions = ['schema1.function1' => 'function_content1'];
53 | $triggers = ['schema1.trigger1' => 'trigger_content1'];
54 | $sequences = ['schema1.sequence1' => 'sequence_content1'];
55 |
56 | $this->provider->shouldReceive('getSchemas')->andReturn($schemas)->once();
57 | $this->provider->shouldReceive('getExtensions')->andReturn($extensions)->once();
58 | $this->provider->shouldReceive('getTables')->andReturn($tables)->once();
59 | $this->provider->shouldReceive('getViews')->andReturn($views)->once();
60 | $this->provider->shouldReceive('getMaterializedViews')->andReturn($matViews)->once();
61 | $this->provider->shouldReceive('getTypes')->andReturn($types)->once();
62 | $this->provider->shouldReceive('getFunctions')->andReturn($functions)->once();
63 | $this->provider->shouldReceive('getTriggers')->andReturn($triggers)->once();
64 | $this->provider->shouldReceive('getSequences')->andReturn($sequences)->once();
65 |
66 | $this->filter->shouldReceive('filter')->andReturnUsing(function ($schemaName, $items) {
67 | return $items;
68 | });
69 |
70 | $actualDump = $this->target->dump();
71 |
72 | self::assertInstanceOf(Dump::class, $actualDump);
73 | self::assertCount(2, $actualDump->getSchemas());
74 | self::assertEquals($extensions, $actualDump->getExtensions());
75 |
76 | $schemaStructure1 = $actualDump->getSchemas()[0];
77 | $schemaStructure2 = $actualDump->getSchemas()[1];
78 | self::assertEquals($schemaStructure1, $schemaStructure2);
79 |
80 | self::assertEquals($tables, $schemaStructure1->getTables());
81 | self::assertEquals($views, $schemaStructure1->getViews());
82 | self::assertEquals($matViews, $schemaStructure1->getMaterializedViews());
83 | self::assertEquals($types, $schemaStructure1->getTypes());
84 | self::assertEquals($functions, $schemaStructure1->getFunctions());
85 | self::assertEquals($triggers, $schemaStructure1->getTriggers());
86 | self::assertEquals($sequences, $schemaStructure1->getSequences());
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/src/SchemaKeeper/Worker/Deployer.php:
--------------------------------------------------------------------------------
1 |
5 | * For the full copyright and license information, please view the LICENSE file that was distributed with this source code.
6 | */
7 |
8 | namespace SchemaKeeper\Worker;
9 |
10 | use SchemaKeeper\Core\ArrayConverter;
11 | use SchemaKeeper\Core\SectionComparator;
12 | use SchemaKeeper\Exception\KeeperException;
13 | use SchemaKeeper\Exception\NotEquals;
14 | use SchemaKeeper\Filesystem\DumpReader;
15 | use SchemaKeeper\Filesystem\FilesystemHelper;
16 | use SchemaKeeper\Filesystem\SectionReader;
17 | use SchemaKeeper\Outside\DeployedFunctions;
18 | use SchemaKeeper\Provider\IProvider;
19 |
20 | /**
21 | * @internal
22 | */
23 | class Deployer
24 | {
25 | /**
26 | * @var DumpReader
27 | */
28 | private $reader;
29 |
30 | /**
31 | * @var IProvider
32 | */
33 | private $provider;
34 |
35 | /**
36 | * @var SectionComparator
37 | */
38 | private $comparator;
39 |
40 | /**
41 | * @var ArrayConverter
42 | */
43 | private $converter;
44 |
45 |
46 | public function __construct(IProvider $provider)
47 | {
48 | $helper = new FilesystemHelper();
49 | $sectionReader = new SectionReader($helper);
50 | $this->reader = new DumpReader($sectionReader, $helper);
51 | $this->converter = new ArrayConverter();
52 | $this->comparator = new SectionComparator();
53 | $this->provider = $provider;
54 | }
55 |
56 | public function deploy(string $sourcePath): DeployedFunctions
57 | {
58 | $functions = $this->provider->getFunctions();
59 | $actualFunctionNames = array_keys($functions);
60 |
61 | $structurePath = $sourcePath;
62 | $expectedDump = $this->reader->read($structurePath);
63 | $expectedFunctions = $this->converter->dump2Array($expectedDump)['functions'];
64 |
65 | $expectedFunctionNames = array_keys($expectedFunctions);
66 |
67 | $functionNamesToCreate = array_diff($expectedFunctionNames, $actualFunctionNames);
68 | $functionNamesToDelete = array_diff($actualFunctionNames, $expectedFunctionNames);
69 | $functionsToChange = array_diff_assoc($expectedFunctions, $functions);
70 |
71 | if (count($functionNamesToDelete) == count($functions)
72 | && count($functionNamesToDelete) > 0
73 | && count($functionsToChange) == 0
74 | && count($functionNamesToCreate) == 0
75 | ) {
76 | throw new KeeperException('Forbidden to remove all functions using SchemaKeeper');
77 | }
78 |
79 | $lastExecutedName = null;
80 |
81 | try {
82 | foreach ($functionNamesToDelete as $nameToDelete) {
83 | $lastExecutedName = $nameToDelete;
84 | $this->provider->deleteFunction($nameToDelete);
85 |
86 | unset($functionsToChange[$nameToDelete]);
87 | }
88 |
89 | foreach ($functionNamesToCreate as $nameToCreate) {
90 | $lastExecutedName = $nameToCreate;
91 | $functionContent = $expectedFunctions[$nameToCreate];
92 | $this->provider->createFunction($functionContent);
93 |
94 | unset($functionsToChange[$nameToCreate]);
95 | }
96 |
97 | foreach ($functionsToChange as $nameToChange => $contentToChange) {
98 | $lastExecutedName = $nameToChange;
99 | $this->provider->changeFunction($nameToChange, $contentToChange);
100 | }
101 | } catch (\PDOException $e) {
102 | $keeperException = new KeeperException("TARGET: $lastExecutedName; " . $e->getMessage(), 0, $e);
103 |
104 | throw $keeperException;
105 | }
106 |
107 | $actualFunctions = $this->provider->getFunctions();
108 | $comparisonResult = $this->comparator->compareSection('functions', $expectedFunctions, $actualFunctions);
109 |
110 | if ($comparisonResult['actual'] !== $comparisonResult['expected']) {
111 | $message = 'Some functions have diff between their definitions before deploy and their definitions after deploy:';
112 |
113 | throw new NotEquals($message, $comparisonResult['expected'], $comparisonResult['actual']);
114 | }
115 |
116 | return new DeployedFunctions(array_keys($functionsToChange), $functionNamesToCreate, $functionNamesToDelete);
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/.dev/docker/images/postgres/config/pg_hba.conf:
--------------------------------------------------------------------------------
1 | # PostgreSQL Client Authentication Configuration File
2 | # ===================================================
3 | #
4 | # Refer to the "Client Authentication" section in the PostgreSQL
5 | # documentation for a complete description of this file. A short
6 | # synopsis follows.
7 | #
8 | # This file controls: which hosts are allowed to connect, how clients
9 | # are authenticated, which PostgreSQL user names they can use, which
10 | # databases they can access. Records take one of these forms:
11 | #
12 | # local DATABASE USER METHOD [OPTIONS]
13 | # host DATABASE USER ADDRESS METHOD [OPTIONS]
14 | # hostssl DATABASE USER ADDRESS METHOD [OPTIONS]
15 | # hostnossl DATABASE USER ADDRESS METHOD [OPTIONS]
16 | #
17 | # (The uppercase items must be replaced by actual values.)
18 | #
19 | # The first field is the connection type: "local" is a Unix-domain
20 | # socket, "host" is either a plain or SSL-encrypted TCP/IP socket,
21 | # "hostssl" is an SSL-encrypted TCP/IP socket, and "hostnossl" is a
22 | # plain TCP/IP socket.
23 | #
24 | # DATABASE can be "all", "sameuser", "samerole", "replication", a
25 | # database name, or a comma-separated list thereof. The "all"
26 | # keyword does not match "replication". Access to replication
27 | # must be enabled in a separate record (see example below).
28 | #
29 | # USER can be "all", a user name, a group name prefixed with "+", or a
30 | # comma-separated list thereof. In both the DATABASE and USER fields
31 | # you can also write a file name prefixed with "@" to include names
32 | # from a separate file.
33 | #
34 | # ADDRESS specifies the set of hosts the record matches. It can be a
35 | # host name, or it is made up of an IP address and a CIDR mask that is
36 | # an integer (between 0 and 32 (IPv4) or 128 (IPv6) inclusive) that
37 | # specifies the number of significant bits in the mask. A host name
38 | # that starts with a dot (.) matches a suffix of the actual host name.
39 | # Alternatively, you can write an IP address and netmask in separate
40 | # columns to specify the set of hosts. Instead of a CIDR-address, you
41 | # can write "samehost" to match any of the server's own IP addresses,
42 | # or "samenet" to match any address in any subnet that the server is
43 | # directly connected to.
44 | #
45 | # METHOD can be "trust", "reject", "md5", "password", "gss", "sspi",
46 | # "ident", "peer", "pam", "ldap", "radius" or "cert". Note that
47 | # "password" sends passwords in clear text; "md5" is preferred since
48 | # it sends encrypted passwords.
49 | #
50 | # OPTIONS are a set of options for the authentication in the format
51 | # NAME=VALUE. The available options depend on the different
52 | # authentication methods -- refer to the "Client Authentication"
53 | # section in the documentation for a list of which options are
54 | # available for which authentication methods.
55 | #
56 | # Database and user names containing spaces, commas, quotes and other
57 | # special characters must be quoted. Quoting one of the keywords
58 | # "all", "sameuser", "samerole" or "replication" makes the name lose
59 | # its special character, and just match a database or username with
60 | # that name.
61 | #
62 | # This file is read on server startup and when the postmaster receives
63 | # a SIGHUP signal. If you edit the file on a running system, you have
64 | # to SIGHUP the postmaster for the changes to take effect. You can
65 | # use "pg_ctl reload" to do that.
66 |
67 | # Put your actual configuration here
68 | # ----------------------------------
69 | #
70 | # If you want to allow non-local connections, you need to add more
71 | # "host" records. In that case you will also need to make PostgreSQL
72 | # listen on a non-local interface via the listen_addresses
73 | # configuration parameter, or via the -i or -h command line switches.
74 |
75 | # CAUTION: Configuring the system for local "trust" authentication
76 | # allows any local user to connect as any PostgreSQL user, including
77 | # the database superuser. If you do not trust all your local users,
78 | # use another authentication method.
79 |
80 |
81 | # TYPE DATABASE USER ADDRESS METHOD
82 |
83 | # "local" is for Unix domain socket connections only
84 | local all all trust
85 | # IPv4 local connections:
86 | host all all 127.0.0.1/32 trust
87 | # IPv6 local connections:
88 | host all all ::1/128 trust
89 | # Allow replication connections from localhost, by a user with the
90 | # replication privilege.
91 | #local replication postgres trust
92 | #host replication postgres 127.0.0.1/32 trust
93 | #host replication postgres ::1/128 trust
94 |
95 | host all all all md5
96 |
--------------------------------------------------------------------------------
/tests/SchemaKeeper/Worker/DeployerTest.php:
--------------------------------------------------------------------------------
1 |
5 | * For the full copyright and license information, please view the LICENSE file that was distributed with this source code.
6 | */
7 |
8 | namespace SchemaKeeper\Tests\Worker;
9 |
10 | use PDO;
11 | use SchemaKeeper\Provider\ProviderFactory;
12 | use SchemaKeeper\Tests\SchemaTestCase;
13 | use SchemaKeeper\Worker\Deployer;
14 | use SchemaKeeper\Worker\Saver;
15 |
16 | class DeployerTest extends SchemaTestCase
17 | {
18 | /**
19 | * @var Deployer
20 | */
21 | private $target;
22 |
23 | /**
24 | * @var Saver
25 | */
26 | private $saver;
27 |
28 | /**
29 | * @var PDO
30 | */
31 | private $conn;
32 |
33 | public function setUp()
34 | {
35 | parent::setUp();
36 |
37 | $this->conn = $this->getConn();
38 | $params = $this->getDbParams();
39 | $providerFactory = new ProviderFactory();
40 | $provider = $providerFactory->createProvider($this->conn, $params);
41 |
42 | $this->target = new Deployer($provider);
43 | $this->saver = new Saver($provider);
44 |
45 | exec('rm -rf /tmp/schema_keeper');
46 | $this->conn->beginTransaction();
47 | }
48 |
49 | public function tearDown()
50 | {
51 | parent::tearDown();
52 |
53 | if ($this->conn->inTransaction()) {
54 | $this->conn->rollBack();
55 | }
56 | }
57 |
58 | public function testOk()
59 | {
60 | $this->saver->save('/tmp/schema_keeper');
61 | $result = $this->target->deploy('/tmp/schema_keeper');
62 |
63 | self::assertEquals([], $result->getChanged());
64 | self::assertEquals([], $result->getCreated());
65 | self::assertEquals([], $result->getDeleted());
66 | }
67 |
68 | public function testCreateFunction()
69 | {
70 | $this->saver->save('/tmp/schema_keeper');
71 |
72 | $function = 'CREATE OR REPLACE FUNCTION public.func_test()
73 | RETURNS void
74 | LANGUAGE plpgsql
75 | AS $function$
76 | DECLARE
77 | BEGIN
78 | RAISE NOTICE \'test\';
79 | END;
80 | $function$
81 | ';
82 |
83 | file_put_contents('/tmp/schema_keeper/structure/public/functions/func_test().sql', $function);
84 |
85 | $result = $this->target->deploy('/tmp/schema_keeper');
86 |
87 | $created = [
88 | 'public.func_test()'
89 | ];
90 |
91 | self::assertEquals([], $result->getChanged());
92 | self::assertEquals($created, $result->getCreated());
93 | self::assertEquals([], $result->getDeleted());
94 | }
95 |
96 | public function testChangeFunction()
97 | {
98 | $this->saver->save('/tmp/schema_keeper');
99 |
100 | $function = 'CREATE OR REPLACE FUNCTION public.trig_test()
101 | RETURNS trigger
102 | LANGUAGE plpgsql
103 | AS $function$
104 | DECLARE
105 | BEGIN
106 | RAISE NOTICE \'test\';
107 | RETURN NEW;
108 | END;
109 | $function$
110 | ';
111 |
112 | file_put_contents('/tmp/schema_keeper/structure/public/functions/trig_test().sql', $function);
113 |
114 | $result = $this->target->deploy('/tmp/schema_keeper');
115 |
116 | $changed = [
117 | 'public.trig_test()'
118 | ];
119 |
120 | self::assertEquals($changed, $result->getChanged());
121 | self::assertEquals([], $result->getCreated());
122 | self::assertEquals([], $result->getDeleted());
123 | }
124 |
125 | /**
126 | * @expectedException \SchemaKeeper\Exception\KeeperException
127 | * @expectedExceptionMessage Some functions have diff between their definitions before deploy and their definitions after deploy:
128 | */
129 | public function testChangeFunctionWithDiff()
130 | {
131 | $this->saver->save('/tmp/schema_keeper');
132 |
133 | $function = 'cREATE OR REPLACE FUNCTION public.trig_test()
134 | RETURNS trigger
135 | LANGUAGE plpgsql
136 | AS $function$
137 | DECLARE
138 | BEGIN
139 | RETURN NEW;
140 | END;
141 | $function$
142 | ';
143 |
144 | file_put_contents('/tmp/schema_keeper/structure/public/functions/trig_test().sql', $function);
145 |
146 | $this->target->deploy('/tmp/schema_keeper');
147 | }
148 |
149 | public function testDeleteFunction()
150 | {
151 | $this->saver->save('/tmp/schema_keeper');
152 |
153 | $function = 'CREATE OR REPLACE FUNCTION public.func_test()
154 | RETURNS void
155 | LANGUAGE plpgsql
156 | AS $function$
157 | DECLARE
158 | BEGIN
159 | RAISE NOTICE \'test\';
160 | END;
161 | $function$
162 | ';
163 |
164 | $this->conn->exec($function);
165 |
166 | $result = $this->target->deploy('/tmp/schema_keeper');
167 |
168 | $deleted = [
169 | 'public.func_test()'
170 | ];
171 |
172 | self::assertEquals([], $result->getChanged());
173 | self::assertEquals([], $result->getCreated());
174 | self::assertEquals($deleted, $result->getDeleted());
175 | }
176 |
177 | public function testChangeFunctionReturnType()
178 | {
179 | $function = 'CREATE OR REPLACE FUNCTION public.func_test()
180 | RETURNS void
181 | LANGUAGE plpgsql
182 | AS $function$
183 | DECLARE
184 | BEGIN
185 | RAISE NOTICE \'test\';
186 | END;
187 | $function$
188 | ';
189 |
190 | $this->conn->exec($function);
191 |
192 | $this->saver->save('/tmp/schema_keeper');
193 |
194 | $changedFunction = 'CREATE OR REPLACE FUNCTION public.func_test()
195 | RETURNS boolean
196 | LANGUAGE plpgsql
197 | AS $function$
198 | DECLARE
199 | BEGIN
200 | RETURN TRUE;
201 | END;
202 | $function$
203 | ';
204 |
205 | file_put_contents('/tmp/schema_keeper/structure/public/functions/func_test().sql', $changedFunction);
206 |
207 | $result = $this->target->deploy('/tmp/schema_keeper');
208 |
209 | $changed =
210 | [
211 | 'public.func_test()'
212 | ];
213 |
214 | self::assertEquals($changed, $result->getChanged());
215 | self::assertEquals([], $result->getCreated());
216 | self::assertEquals([], $result->getDeleted());
217 | }
218 |
219 | /**
220 | * @expectedException \SchemaKeeper\Exception\KeeperException
221 | * @expectedExceptionMessage TARGET: public.trig_test()
222 | */
223 | public function testError()
224 | {
225 | $this->saver->save('/tmp/schema_keeper');
226 |
227 | $function = 'fd';
228 |
229 | file_put_contents('/tmp/schema_keeper/structure/public/functions/trig_test().sql', $function);
230 |
231 | $this->target->deploy('/tmp/schema_keeper');
232 | }
233 |
234 | /**
235 | * @expectedException \SchemaKeeper\Exception\KeeperException
236 | * @expectedExceptionMessage Forbidden to remove all functions using SchemaKeeper
237 | */
238 | public function testEmptyDump()
239 | {
240 | $this->saver->save('/tmp/schema_keeper');
241 |
242 | unlink('/tmp/schema_keeper/structure/public/functions/trig_test().sql');
243 |
244 | $this->target->deploy('/tmp/schema_keeper');
245 | }
246 | }
247 |
--------------------------------------------------------------------------------
/tests/SchemaKeeper/Worker/SaverTest.php:
--------------------------------------------------------------------------------
1 |
5 | * For the full copyright and license information, please view the LICENSE file that was distributed with this source code.
6 | */
7 |
8 | namespace SchemaKeeper\Tests\Worker;
9 |
10 | use SchemaKeeper\Provider\ProviderFactory;
11 | use SchemaKeeper\Tests\SchemaTestCase;
12 | use SchemaKeeper\Worker\Saver;
13 |
14 | class SaverTest extends SchemaTestCase
15 | {
16 | /**
17 | * @var Saver
18 | */
19 | private $target;
20 |
21 | public function setUp()
22 | {
23 | parent::setUp();
24 |
25 | $conn = $this->getConn();
26 | $params = $this->getDbParams();
27 | $providerFactory = new ProviderFactory();
28 | $provider = $providerFactory->createProvider($conn, $params);
29 |
30 | $this->target = new Saver($provider);
31 |
32 | exec('rm -rf /tmp/schema_keeper');
33 | }
34 |
35 | public function testOk()
36 | {
37 | $this->target->save('/tmp/schema_keeper');
38 |
39 | self::assertEquals([
40 | '/tmp/schema_keeper/extensions',
41 | '/tmp/schema_keeper/structure',
42 | ], glob('/tmp/schema_keeper/*'));
43 |
44 | self::assertEquals([
45 | '/tmp/schema_keeper/extensions/plpgsql.txt',
46 | ], glob('/tmp/schema_keeper/extensions/*'));
47 |
48 | self::assertEquals('pg_catalog', file_get_contents('/tmp/schema_keeper/extensions/plpgsql.txt'));
49 |
50 | self::assertEquals([
51 | '/tmp/schema_keeper/structure/public',
52 | '/tmp/schema_keeper/structure/test_schema',
53 | ], glob('/tmp/schema_keeper/structure/*'));
54 |
55 | self::assertEquals([
56 | '/tmp/schema_keeper/structure/public/functions',
57 | '/tmp/schema_keeper/structure/public/materialized_views',
58 | '/tmp/schema_keeper/structure/public/sequences',
59 | '/tmp/schema_keeper/structure/public/tables',
60 | '/tmp/schema_keeper/structure/public/triggers',
61 | '/tmp/schema_keeper/structure/public/types',
62 | '/tmp/schema_keeper/structure/public/views',
63 | ], glob('/tmp/schema_keeper/structure/public/*'));
64 |
65 | self::assertEquals([
66 | '/tmp/schema_keeper/structure/public/tables/test_table.txt',
67 | ], glob('/tmp/schema_keeper/structure/public/tables/*'));
68 |
69 | $expectedTable = ' Table "public.test_table"
70 | Column | Type | Modifiers
71 | --------+--------+---------------------------------------------------------
72 | id | bigint | not null default nextval(\'test_table_id_seq\'::regclass)
73 | values | text |
74 | Indexes:
75 | "test_table_pkey" PRIMARY KEY, btree (id)
76 | Triggers:
77 | test_trigger BEFORE UPDATE ON test_table FOR EACH ROW EXECUTE PROCEDURE trig_test()
78 |
79 | ';
80 |
81 | $actualTable = file_get_contents('/tmp/schema_keeper/structure/public/tables/test_table.txt');
82 | self::assertEquals($expectedTable, $actualTable);
83 |
84 | self::assertEquals([
85 | '/tmp/schema_keeper/structure/public/sequences/test_table_id_seq.txt',
86 | ], glob('/tmp/schema_keeper/structure/public/sequences/*'));
87 |
88 | $expectedSequence = '{
89 | "seq_path": "public.test_table_id_seq",
90 | "data_type": "bigint",
91 | "start_value": "1",
92 | "minimum_value": "1",
93 | "maximum_value": "9223372036854775807",
94 | "increment": "1",
95 | "cycle_option": "NO"
96 | }';
97 |
98 | $actualSequence = file_get_contents('/tmp/schema_keeper/structure/public/sequences/test_table_id_seq.txt');
99 | self::assertEquals($expectedSequence, $actualSequence);
100 |
101 | self::assertEquals([
102 | '/tmp/schema_keeper/structure/public/views/test_view.txt',
103 | ], glob('/tmp/schema_keeper/structure/public/views/*'));
104 |
105 | $expectedView = ' View "public.test_view"
106 | Column | Type | Modifiers | Storage | Description
107 | --------+--------+-----------+----------+-------------
108 | id | bigint | | plain |
109 | values | text | | extended |
110 | View definition:
111 | SELECT test_table.id,
112 | test_table."values"
113 | FROM test_table;
114 |
115 | ';
116 | $actualView = file_get_contents('/tmp/schema_keeper/structure/public/views/test_view.txt');
117 |
118 | self::assertEquals($expectedView, $actualView);
119 |
120 | self::assertEquals([
121 | '/tmp/schema_keeper/structure/public/materialized_views/test_mat_view.txt',
122 | ], glob('/tmp/schema_keeper/structure/public/materialized_views/*'));
123 |
124 | $actualMaterializedView = file_get_contents('/tmp/schema_keeper/structure/public/materialized_views/test_mat_view.txt');
125 | $expectedMaterializedView = ' Materialized view "public.test_mat_view"
126 | Column | Type | Modifiers | Storage | Stats target | Description
127 | --------+--------+-----------+----------+--------------+-------------
128 | id | bigint | | plain | |
129 | values | text | | extended | |
130 | View definition:
131 | SELECT test_table.id,
132 | test_table."values"
133 | FROM test_table;
134 |
135 | ';
136 | self::assertEquals($expectedMaterializedView, $actualMaterializedView);
137 |
138 | self::assertEquals([
139 | '/tmp/schema_keeper/structure/public/functions/trig_test().sql',
140 | ], glob('/tmp/schema_keeper/structure/public/functions/*'));
141 |
142 | $actualFunction = file_get_contents('/tmp/schema_keeper/structure/public/functions/trig_test().sql');
143 | $expectedFunction = 'CREATE OR REPLACE FUNCTION public.trig_test()
144 | RETURNS trigger
145 | LANGUAGE plpgsql
146 | AS $function$
147 | DECLARE
148 | BEGIN
149 | RETURN NEW;
150 | END;
151 | $function$
152 | ';
153 |
154 | self::assertEquals($expectedFunction, $actualFunction);
155 |
156 | self::assertEquals([
157 | '/tmp/schema_keeper/structure/public/triggers/test_table.test_trigger.sql',
158 | ], glob('/tmp/schema_keeper/structure/public/triggers/*'));
159 |
160 | $actualTrigger = file_get_contents('/tmp/schema_keeper/structure/public/triggers/test_table.test_trigger.sql');
161 | $expectedTrigger = 'CREATE TRIGGER test_trigger BEFORE UPDATE ON test_table FOR EACH ROW EXECUTE PROCEDURE trig_test()';
162 |
163 | self::assertEquals($expectedTrigger, $actualTrigger);
164 |
165 | self::assertEquals([
166 | '/tmp/schema_keeper/structure/public/types/test_enum_type.txt',
167 | '/tmp/schema_keeper/structure/public/types/test_type.txt',
168 | ], glob('/tmp/schema_keeper/structure/public/types/*'));
169 |
170 | $actualType = file_get_contents('/tmp/schema_keeper/structure/public/types/test_type.txt');
171 | $expectedType = ' Composite type "public.test_type"
172 | Column | Type | Modifiers
173 | --------+-------------------+-----------
174 | id | bigint |
175 | values | character varying |
176 |
177 | ';
178 |
179 | self::assertEquals($expectedType, $actualType);
180 |
181 | $actualEnumType = file_get_contents('/tmp/schema_keeper/structure/public/types/test_enum_type.txt');
182 | $expectedEnumType = '{enum1,enum2}';
183 |
184 | self::assertEquals($expectedEnumType, $actualEnumType);
185 | }
186 | }
187 |
--------------------------------------------------------------------------------
/src/SchemaKeeper/Provider/PostgreSQL/PSQLProvider.php:
--------------------------------------------------------------------------------
1 |
5 | * For the full copyright and license information, please view the LICENSE file that was distributed with this source code.
6 | */
7 |
8 | namespace SchemaKeeper\Provider\PostgreSQL;
9 |
10 | use Exception;
11 | use PDO;
12 | use SchemaKeeper\Exception\KeeperException;
13 | use SchemaKeeper\Provider\IProvider;
14 |
15 | /**
16 | * @internal
17 | */
18 | class PSQLProvider implements IProvider
19 | {
20 | /**
21 | * @var PDO
22 | */
23 | protected $conn;
24 |
25 | /**
26 | * @var PSQLClient
27 | */
28 | protected $psqlClient;
29 |
30 | /**
31 | * @var string[]
32 | */
33 | protected $skippedSchemaNames;
34 |
35 | /**
36 | * @var string[]
37 | */
38 | protected $skippedExtensionNames;
39 |
40 | /**
41 | * @var SavepointHelper
42 | */
43 | protected $savePointHelper;
44 |
45 |
46 | public function __construct(
47 | PDO $conn,
48 | PSQLClient $psqlClient,
49 | array $skippedSchemaNames,
50 | array $skippedExtensionNames
51 | ) {
52 | $this->conn = $conn;
53 | $this->psqlClient = $psqlClient;
54 | $this->skippedSchemaNames = $skippedSchemaNames;
55 | $this->skippedExtensionNames = $skippedExtensionNames;
56 |
57 | $this->savePointHelper = new SavepointHelper($conn);
58 | }
59 |
60 | public function getTables(): array
61 | {
62 | $sql = sprintf("
63 | SELECT concat_ws('.', schemaname, tablename) AS table_path
64 | FROM pg_catalog.pg_tables
65 | WHERE %s
66 | ORDER BY table_path
67 | ", $this->expandLike('schemaname', $this->skippedSchemaNames));
68 |
69 | $stmt = $this->query($sql);
70 |
71 | $commands = [];
72 | while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
73 | $table = $this->prepareName($row['table_path']);
74 | $cmd = '\d ' . $table;
75 | $commands[$table] = $cmd;
76 | }
77 |
78 | $actualTables = $this->psqlClient->runMultiple($commands);
79 |
80 | return $actualTables;
81 | }
82 |
83 | public function getViews(): array
84 | {
85 | $sql = sprintf("
86 | SELECT (schemaname || '.' || viewname) AS view_path
87 | FROM pg_catalog.pg_views
88 | WHERE %s
89 | ORDER BY view_path
90 | ", $this->expandLike('schemaname', $this->skippedSchemaNames));
91 |
92 | $stmt = $this->query($sql);
93 |
94 | $commands = [];
95 | while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
96 | $view = $this->prepareName($row['view_path']);
97 | $commands[$view] = '\d+ ' . $view;
98 | }
99 |
100 | $actualViews = $this->psqlClient->runMultiple($commands);
101 |
102 | return $actualViews;
103 | }
104 |
105 | public function getMaterializedViews(): array
106 | {
107 | $sql = sprintf("
108 | SELECT (schemaname || '.' || matviewname) AS view_path
109 | FROM pg_catalog.pg_matviews
110 | WHERE %s
111 | ORDER BY view_path
112 | ", $this->expandLike('schemaname', $this->skippedSchemaNames));
113 |
114 | $stmt = $this->query($sql);
115 |
116 | $commands = [];
117 | while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
118 | $view = $this->prepareName($row['view_path']);
119 | $commands[$view] = '\d+ ' . $view;
120 | }
121 |
122 | $actualViews = $this->psqlClient->runMultiple($commands);
123 |
124 | return $actualViews;
125 | }
126 |
127 | public function getTriggers(): array
128 | {
129 | $actualTriggers = [];
130 |
131 | $sql = "
132 | SELECT
133 | concat_ws('.', n.nspname, c.relname, t.tgname) as tg_path,
134 | pg_catalog.pg_get_triggerdef(t.OID, true) AS tg_def
135 | FROM pg_catalog.pg_trigger t
136 | INNER JOIN pg_catalog.pg_class c
137 | ON c.OID = t.tgrelid
138 | INNER JOIN pg_catalog.pg_namespace n
139 | ON n.OID = c.relnamespace
140 | WHERE t.tgisinternal = FALSE
141 | ORDER BY tg_path
142 | ";
143 |
144 | $stmt = $this->query($sql);
145 |
146 | while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
147 | $trigger = $this->prepareName($row['tg_path']);
148 | $definition = $row['tg_def'];
149 |
150 | $actualTriggers[$trigger] = $definition;
151 | }
152 |
153 | return $actualTriggers;
154 | }
155 |
156 | public function getFunctions(): array
157 | {
158 | $actualFunctions = [];
159 |
160 | $sql = sprintf("
161 | SELECT
162 | concat_ws('.', n.nspname, p.proname) AS pro_path,
163 | ARRAY(
164 | SELECT concat_ws('.', n1.nspname, pgt.typname) as typname
165 | FROM (SELECT
166 | u AS type_oid,
167 | row_number()
168 | OVER () AS row_number
169 | FROM unnest(p.proargtypes) u) types
170 | LEFT JOIN pg_catalog.pg_type pgt ON
171 | pgt.OID = types.type_oid
172 | LEFT JOIN pg_catalog.pg_namespace n1
173 | ON n1.OID = pgt.typnamespace
174 | AND n1.nspname NOT IN ('pg_catalog')
175 | ORDER BY types.row_number
176 | ) AS arg_types,
177 | pg_catalog.pg_get_functiondef(p.oid) AS pro_def
178 | FROM pg_catalog.pg_namespace n
179 | JOIN pg_catalog.pg_proc p
180 | ON p.pronamespace = n.oid
181 | WHERE %s
182 | ORDER BY pro_path
183 | ", $this->expandLike('n.nspname', $this->skippedSchemaNames));
184 |
185 | $stmt = $this->query($sql);
186 |
187 | while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
188 | $argTypes = $row['arg_types'];
189 | $function = $this->prepareName($row['pro_path'] . $argTypes);
190 | $definition = $row['pro_def'];
191 | $actualFunctions[$function] = $definition;
192 | }
193 |
194 | return $actualFunctions;
195 | }
196 |
197 | public function getTypes(): array
198 | {
199 | $actualTypes = [];
200 |
201 | $sql = sprintf("
202 | SELECT
203 | concat_ws('.', n.nspname, t.typname) AS type_path,
204 | t.typbyval
205 | FROM pg_catalog.pg_type t
206 | LEFT JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace
207 | WHERE (t.typrelid = 0 OR (SELECT c.relkind = 'c'
208 | FROM pg_catalog.pg_class c
209 | WHERE c.oid = t.typrelid))
210 | AND NOT EXISTS(SELECT 1
211 | FROM pg_catalog.pg_type el
212 | WHERE el.oid = t.typelem AND el.typarray = t.oid)
213 | AND %s
214 | ORDER BY type_path
215 | ", $this->expandLike('n.nspname', $this->skippedSchemaNames));
216 |
217 | $stmt = $this->query($sql);
218 |
219 | while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
220 | $type = $row['type_path'];
221 | if ($row['typbyval']) {
222 | $stmtEnum = $this->conn->query('select enum_range(null::' . $type . ')');
223 | $definition = $stmtEnum ? $stmtEnum->fetchColumn() : '';
224 | } else {
225 | $definition = $this->psqlClient->run('\d ' . $type);
226 | }
227 |
228 | $type = $this->prepareName($type);
229 | $actualTypes[$type] = (string) $definition;
230 | }
231 |
232 | return $actualTypes;
233 | }
234 |
235 | public function getSchemas(): array
236 | {
237 | $actualSchemas = [];
238 |
239 | $sql = sprintf("
240 | SELECT schema_name
241 | FROM information_schema.schemata
242 | WHERE %s
243 | ORDER BY schema_name
244 | ", $this->expandLike('schema_name', $this->skippedSchemaNames));
245 |
246 | $stmt = $this->query($sql);
247 |
248 | while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
249 | $schema = $this->prepareName($row['schema_name']);
250 | $actualSchemas[$schema] = $schema;
251 | }
252 |
253 | return $actualSchemas;
254 | }
255 |
256 | public function getExtensions(): array
257 | {
258 | $actualExtensions = [];
259 |
260 | $sql = sprintf("
261 | SELECT
262 | ext.extname,
263 | nsp.nspname
264 | FROM pg_catalog.pg_extension ext
265 | LEFT JOIN pg_catalog.pg_namespace nsp
266 | ON nsp.OID = ext.extnamespace
267 | WHERE %s
268 | ORDER BY extname;
269 | ", $this->expandLike('extname', $this->skippedExtensionNames));
270 |
271 | $stmt = $this->query($sql);
272 |
273 | while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
274 | $extension = $this->prepareName($row['extname']);
275 | $schema = $row['nspname'];
276 | $actualExtensions[$extension] = $schema;
277 | }
278 |
279 | return $actualExtensions;
280 | }
281 |
282 | public function getSequences(): array
283 | {
284 | $sql = "
285 | SELECT
286 | concat(s.sequence_schema, '.', s.sequence_name) as seq_path,
287 | data_type,
288 | start_value,
289 | minimum_value,
290 | maximum_value,
291 | increment,
292 | cycle_option
293 | FROM information_schema.sequences s
294 | ORDER BY seq_path
295 | ";
296 |
297 | $stmt = $this->query($sql);
298 |
299 | $actualSequences = [];
300 | while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
301 | $sequence = $this->prepareName($row['seq_path']);
302 | $actualSequences[$sequence] = (string) json_encode($row, JSON_PRETTY_PRINT);
303 |
304 | if (json_last_error() !== JSON_ERROR_NONE) {
305 | throw new \RuntimeException('Json error: ' . json_last_error_msg());
306 | }
307 | }
308 |
309 | return $actualSequences;
310 | }
311 |
312 | public function createFunction(string $definition): void
313 | {
314 | $this->conn->exec($definition);
315 | }
316 |
317 | public function deleteFunction(string $name): void
318 | {
319 | $sqlDelete = 'DROP FUNCTION ' . $name;
320 | $this->conn->exec($sqlDelete);
321 | }
322 |
323 | public function changeFunction(string $name, string $definition): void
324 | {
325 | $isTransaction = $this->conn->inTransaction();
326 |
327 | try {
328 | $this->savePointHelper->beginTransaction('before_change', $isTransaction);
329 | $this->conn->exec($definition);
330 | $this->savePointHelper->commit('before_change', $isTransaction);
331 | } catch (Exception $e) {
332 | $this->savePointHelper->rollback('before_change', $isTransaction);
333 | $this->conn->exec('DROP FUNCTION ' . $name);
334 | $this->conn->exec($definition);
335 | }
336 | }
337 |
338 | private function prepareName(string $name): string
339 | {
340 | return str_replace(['{', '}'], ['(', ')'], $name);
341 | }
342 |
343 | private function expandLike(string $columnName, array $patterns): string
344 | {
345 | $sql = '';
346 |
347 | foreach ($patterns as $pattern) {
348 | $sql .= " AND $columnName NOT LIKE '$pattern'";
349 | }
350 |
351 | if (!$sql) {
352 | $sql = ' AND TRUE';
353 | }
354 |
355 | $sql = trim($sql, " AND");
356 |
357 | return $sql;
358 | }
359 |
360 | private function query(string $sql): \PDOStatement
361 | {
362 | $stmt = $this->conn->query($sql);
363 |
364 | if ($stmt === false) {
365 | throw new KeeperException(print_r($this->conn->errorInfo(), true));
366 | }
367 |
368 | return $stmt;
369 | }
370 | }
371 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SchemaKeeper
2 |
3 | [](https://packagist.org/packages/schema-keeper/schema-keeper)
4 | [](https://php.net/)
5 | [](https://www.postgresql.org/)
6 | [](https://travis-ci.com/dmytro-demchyna/schema-keeper)
7 | [](https://codecov.io/gh/dmytro-demchyna/schema-keeper)
8 | [](https://github.com/dmytro-demchyna/schema-keeper/blob/master/LICENSE)
9 |
10 | Track a structure of your PostgreSQL database in a VCS using SchemaKeeper.
11 |
12 | SchemaKeeper provides 3 functions:
13 | 1. `save` — saves a database structure as separate text files to a specified directory
14 | 1. `verify` — detects changes between an actual database structure and the saved one
15 | 1. `deploy` — deploys stored procedures to a database from the saved structure
16 |
17 | SchemaKeeper allows to use `gitflow` principles for a database development. Each branch contains its own database structure dump, and when branches are merged, dumps are merged too.
18 |
19 | ## Table of contents
20 | - [Installation](#installation)
21 | - [Composer](#composer)
22 | - [PHAR](#phar)
23 | - [Docker](#docker)
24 | - [Basic usage](#basic-usage)
25 | - [save](#save)
26 | - [verify](#verify)
27 | - [deploy](#deploy)
28 | - [Extended usage](#extended-usage)
29 | - [PHPUnit](#phpunit)
30 | - [Custom transaction block](#custom-transaction-block)
31 | - [Workflow recommendations](#workflow-recommendations)
32 | - [Safe deploy to a production](#safe-deploy-to-a-production)
33 | - [Conflicts resolving](#conflicts-resolving)
34 | - [Extra links](#extra-links)
35 | - [Contributing](#contributing)
36 |
37 | ## Installation
38 |
39 | > If you choose the installation via Composer or PHAR, please, install [psql](https://www.postgresql.org/docs/current/app-psql.html) app on machines where SchemaKeeper will be used. A Docker build includes pre-installed [psql](https://www.postgresql.org/docs/current/app-psql.html).
40 |
41 | ### Composer
42 |
43 | ```bash
44 | $ composer require schema-keeper/schema-keeper
45 | ```
46 |
47 | ### PHAR
48 |
49 | ```bash
50 | $ wget https://github.com/dmytro-demchyna/schema-keeper/releases/latest/download/schemakeeper.phar
51 | ```
52 |
53 | ### Docker
54 |
55 | ```bash
56 | $ docker pull dmytrodemchyna/schema-keeper
57 | ```
58 |
59 | ## Basic Usage
60 |
61 | Create a `config.php` file:
62 |
63 | ```php
64 | setSkippedSchemas(['information_schema', 'pg_%']);
73 |
74 | // These extensions will be ignored
75 | $params->setSkippedExtensions(['pgtap']);
76 |
77 | // The path to psql executable
78 | $params->setExecutable('/bin/psql');
79 |
80 | return $params;
81 | ```
82 |
83 | Now you can use the `schemakeeper` binary. It returns exit-code `0` on success and exit-code `1` on failure.
84 |
85 | ### save
86 |
87 | ```bash
88 | $ schemakeeper -c config.php -d /project_path/db_name save
89 | ```
90 |
91 | The command above saves a database structure to a `/project_path/db_name` directory.
92 |
93 | - /project_path/db_name:
94 | - structure:
95 | - public:
96 | - functions:
97 | - func1(int8).sql
98 | - materialized_views:
99 | - mat_view1.txt
100 | - sequences:
101 | - sequence1.txt
102 | - tables:
103 | - table1.txt
104 | - triggers:
105 | - trigger1.sql
106 | - types:
107 | - type1.txt
108 | - views:
109 | - view1.txt
110 | - schema2:
111 | - views:
112 | - view2.txt
113 | - ...
114 | - extensions:
115 | - plpgsql.txt
116 |
117 | Examples of conversion database structure to files:
118 |
119 | Object type | Schema | Name | Relative file path | File content
120 | --------------------|----------------|------------------------------------------|------------------------------------|---------------
121 | Table | public | table1 | ./public/tables/table1.txt | A description of the table structure obtained by `\d` [meta](https://www.postgresql.org/docs/current/app-psql.html#APP-PSQL-META-COMMANDS) command
122 | Stored procedure | public | func1(param bigint) | ./public/functions/func1(int8).sql | A definition of the stored procedure, including a `CREATE OR REPLACE FUNCTION` block, obtained by [pg_get_functiondef](https://www.postgresql.org/docs/current/functions-info.html#FUNCTIONS-INFO-CATALOG-TABLE)
123 | View | schema2 | view2 | ./schema2/views/view2.txt | A description of the view structure obtained by `\d+` [meta](https://www.postgresql.org/docs/current/app-psql.html#APP-PSQL-META-COMMANDS) command
124 | ... | ... | ... | ... | ...
125 |
126 | The file path stores information about a type, a scheme and a name of a object. This approach makes an easier navigation through the database structure, as well as code review of changes in VCS.
127 |
128 | ### verify
129 |
130 | ```bash
131 | $ schemakeeper -c config.php -d /project_path/db_name verify
132 | ```
133 |
134 | The command above compares an actual database structure with the previously saved in `/project_path/db_name` one and displays an information about changed objects.
135 |
136 | If changes exists, the `verify` will returns an exit-code `1`.
137 |
138 | An alternative way to find changes is to call the `save` again, specifying the same directory `/project_path/db_name`, and check changes in the VCS. Since objects from the database are stored in separate files, the VCS will show only changed objects. A main disadvantage of this way — a need to overwrite files.
139 |
140 | ### deploy
141 |
142 | ```bash
143 | $ schemakeeper -c config.php -d /project_path/db_name deploy
144 | ```
145 |
146 | The command above deploys stored procedures from the `/project_path/db_name` to the actual database.
147 |
148 | You can edit a source code of stored procedures in the same way as a rest of an application source code. Modification of a stored procedure occurs by making changes to the corresponding file in the `/project_path/db_name` directory, which is automatically reflected in the VCS.
149 |
150 | For example, to create a new stored procedure in the `public` schema, just create a new file with a `.sql` extension in the `/project_path/db_name/structure/public/functions` directory, place a source code of the stored procedure into it, including a `CREATE OR REPLACE FUNCTION` block, then call the `deploy`. Similarly occur modifying or removal of stored procedures. Thus, the code simultaneously enters both the VCS and the database.
151 |
152 | The `deploy` changes parameters of a function or a return type without additional actions, while with a classical approach it would be necessary to first perform `DROP FUNCTION`, and only then `CREATE OR REPLACE FUNCTION`.
153 |
154 | Unfortunately, in some situations `deploy` is not able to automatically apply changes. For example, if you try to delete a trigger function, that is used by at least one trigger. Such situations must be solved manually using migration files.
155 |
156 | The `deploy` transfers changes only from stored procedures. To transfer other changes, please, use migration files (for example, [doctrine/migrations](https://packagist.org/packages/doctrine/migrations)).
157 |
158 | Migrations must be applied before the `deploy` to resolve possible problem situations.
159 |
160 | > The `deploy` is designed to work with stored procedures written in [PL/pgSQL](https://www.postgresql.org/docs/current/plpgsql.html). Using with other languages may be less effective or impossible.
161 |
162 | ## Extended usage
163 |
164 | You can inject SchemaKeeper to your own code.
165 |
166 | ```php
167 | PDO::ERRMODE_EXCEPTION]);
180 |
181 | $params = new PSQLParameters($host, $port, $dbName, $user, $password);
182 | $keeper = new Keeper($conn, $params);
183 | ```
184 |
185 | ```php
186 | saveDump('path_to_dump');
189 | $keeper->verifyDump('path_to_dump');
190 | $keeper->deployDump('path_to_dump');
191 | ```
192 |
193 | ### PHPUnit
194 |
195 | You can wrap `verifyDump` into a PHPUnit test:
196 |
197 | ```php
198 | verifyDump('/path_to_dump');
208 | } catch (\SchemaKeeper\Exception\NotEquals $e) {
209 | $expectedFormatted = print_r($e->getExpected(), true);
210 | $actualFormatted = print_r($e->getActual(), true);
211 |
212 | // assertEquals will show the detailed diff between the saved dump and actual database
213 | self::assertEquals($expectedFormatted, $actualFormatted);
214 | }
215 | }
216 | }
217 |
218 | ```
219 |
220 | ### Custom transaction block
221 |
222 | You can wrap `deployDump` into a custom transaction block:
223 |
224 | ```php
225 | beginTransaction();
232 |
233 | try {
234 | $result = $keeper->deployDump('/path_to_dump');
235 |
236 | // $result->getDeleted() - these functions were deleted from the current database
237 | // $result->getCreated() - these functions were created in the current database
238 | // $result->getChanged() - these functions were changed in the current database
239 |
240 | $conn->commit();
241 | } catch (\Exception $e) {
242 | $conn->rollBack();
243 | }
244 | ```
245 |
246 | ## Workflow recommendations
247 |
248 | ### Safe deploy to a production
249 |
250 | A dump of a database structure saved in a VCS allows you to check a production database for exact match to a required structure. This ensures that only intended changes were transferred to the production-DB by deploy.
251 |
252 | Since the PostgreSQL [DDL](https://www.postgresql.org/docs/current/ddl.html) is [transactional](https://wiki.postgresql.org/wiki/Transactional_DDL_in_PostgreSQL:_A_Competitive_Analysis), the following deployment order is recommended:
253 | 1. Start transaction
254 | 1. Apply all migrations in the transaction
255 | 1. In the same transaction, perform `deployDump`
256 | 1. Perform `verifyDump`. If there are no errors, execute `COMMIT`. If there are errors, execute `ROLLBACK`
257 |
258 | ### Conflicts resolving
259 | A possible conflict situation: *branch1* and *branch2* are branched from *develop*. They haven't conflict with *develop*, but have conflict with each other. A goal is to merge *branch1* and *branch2* into *develop*.
260 |
261 | First, merge *branch1* into *develop*, then merge *develop* into *branch2*, resolve conflicts in *branch2*, and then merge *branch2* into *develop*. At the stage of conflict resolution inside *branch2*, you may have to correct a migration file in *branch2* to match the final dump that contains merge results.
262 |
263 | ## Extra links
264 |
265 | If you are not satisfied with SchemaKeeper, look at the list of another tools: https://wiki.postgresql.org/wiki/Change_management_tools_and_techniques
266 |
267 | ## Contributing
268 | Any contributions are welcome.
269 |
270 | Please refer to [CONTRIBUTING.md](https://github.com/dmytro-demchyna/schema-keeper/blob/master/.github/CONTRIBUTING.md) for information on how to contribute to SchemaKeeper.
271 |
--------------------------------------------------------------------------------
/.dev/docker/images/postgres/config/postgresql.conf:
--------------------------------------------------------------------------------
1 | # -----------------------------
2 | # PostgreSQL configuration file
3 | # -----------------------------
4 | #
5 | # This file consists of lines of the form:
6 | #
7 | # name = value
8 | #
9 | # (The "=" is optional.) Whitespace may be used. Comments are introduced with
10 | # "#" anywhere on a line. The complete list of parameter names and allowed
11 | # values can be found in the PostgreSQL documentation.
12 | #
13 | # The commented-out settings shown in this file represent the default values.
14 | # Re-commenting a setting is NOT sufficient to revert it to the default value;
15 | # you need to reload the server.
16 | #
17 | # This file is read on server startup and when the server receives a SIGHUP
18 | # signal. If you edit the file on a running system, you have to SIGHUP the
19 | # server for the changes to take effect, or use "pg_ctl reload". Some
20 | # parameters, which are marked below, require a server shutdown and restart to
21 | # take effect.
22 | #
23 | # Any parameter can also be given as a command-line option to the server, e.g.,
24 | # "postgres -c log_connections=on". Some parameters can be changed at run time
25 | # with the "SET" SQL command.
26 | #
27 | # Memory units: kB = kilobytes Time units: ms = milliseconds
28 | # MB = megabytes s = seconds
29 | # GB = gigabytes min = minutes
30 | # TB = terabytes h = hours
31 | # d = days
32 |
33 |
34 | #------------------------------------------------------------------------------
35 | # FILE LOCATIONS
36 | #------------------------------------------------------------------------------
37 |
38 | # The default values of these variables are driven from the -D command-line
39 | # option or PGDATA environment variable, represented here as ConfigDir.
40 |
41 | #data_directory = 'ConfigDir' # use data in another directory
42 | # (change requires restart)
43 | hba_file = '/etc/postgresql/pg_hba.conf' # host-based authentication file
44 | # (change requires restart)
45 | #ident_file = 'ConfigDir/pg_ident.conf' # ident configuration file
46 | # (change requires restart)
47 |
48 | # If external_pid_file is not explicitly set, no extra PID file is written.
49 | #external_pid_file = '' # write an extra PID file
50 | # (change requires restart)
51 |
52 |
53 | #------------------------------------------------------------------------------
54 | # CONNECTIONS AND AUTHENTICATION
55 | #------------------------------------------------------------------------------
56 |
57 | # - Connection Settings -
58 |
59 | listen_addresses = '*'
60 | # comma-separated list of addresses;
61 | # defaults to 'localhost'; use '*' for all
62 | # (change requires restart)
63 | #port = 5432 # (change requires restart)
64 | #max_connections = 100 # (change requires restart)
65 | #superuser_reserved_connections = 3 # (change requires restart)
66 | #unix_socket_directories = '/tmp' # comma-separated list of directories
67 | # (change requires restart)
68 | #unix_socket_group = '' # (change requires restart)
69 | #unix_socket_permissions = 0777 # begin with 0 to use octal notation
70 | # (change requires restart)
71 | #bonjour = off # advertise server via Bonjour
72 | # (change requires restart)
73 | #bonjour_name = '' # defaults to the computer name
74 | # (change requires restart)
75 |
76 | # - Security and Authentication -
77 |
78 | #authentication_timeout = 1min # 1s-600s
79 | #ssl = off # (change requires restart)
80 | #ssl_ciphers = 'HIGH:MEDIUM:+3DES:!aNULL' # allowed SSL ciphers
81 | # (change requires restart)
82 | #ssl_prefer_server_ciphers = on # (change requires restart)
83 | #ssl_ecdh_curve = 'prime256v1' # (change requires restart)
84 | #ssl_cert_file = 'server.crt' # (change requires restart)
85 | #ssl_key_file = 'server.key' # (change requires restart)
86 | #ssl_ca_file = '' # (change requires restart)
87 | #ssl_crl_file = '' # (change requires restart)
88 | #password_encryption = on
89 | #db_user_namespace = off
90 | #row_security = on
91 |
92 | # GSSAPI using Kerberos
93 | #krb_server_keyfile = ''
94 | #krb_caseins_users = off
95 |
96 | # - TCP Keepalives -
97 | # see "man 7 tcp" for details
98 |
99 | #tcp_keepalives_idle = 0 # TCP_KEEPIDLE, in seconds;
100 | # 0 selects the system default
101 | #tcp_keepalives_interval = 0 # TCP_KEEPINTVL, in seconds;
102 | # 0 selects the system default
103 | #tcp_keepalives_count = 0 # TCP_KEEPCNT;
104 | # 0 selects the system default
105 |
106 |
107 | #------------------------------------------------------------------------------
108 | # RESOURCE USAGE (except WAL)
109 | #------------------------------------------------------------------------------
110 |
111 | # - Memory -
112 |
113 | #shared_buffers = 32MB # min 128kB
114 | # (change requires restart)
115 | #huge_pages = try # on, off, or try
116 | # (change requires restart)
117 | #temp_buffers = 8MB # min 800kB
118 | #max_prepared_transactions = 0 # zero disables the feature
119 | # (change requires restart)
120 | # Caution: it is not advisable to set max_prepared_transactions nonzero unless
121 | # you actively intend to use prepared transactions.
122 | #work_mem = 4MB # min 64kB
123 | #maintenance_work_mem = 64MB # min 1MB
124 | #autovacuum_work_mem = -1 # min 1MB, or -1 to use maintenance_work_mem
125 | #max_stack_depth = 2MB # min 100kB
126 | #dynamic_shared_memory_type = posix # the default is the first option
127 | # supported by the operating system:
128 | # posix
129 | # sysv
130 | # windows
131 | # mmap
132 | # use none to disable dynamic shared memory
133 | # (change requires restart)
134 |
135 | # - Disk -
136 |
137 | #temp_file_limit = -1 # limits per-session temp file space
138 | # in kB, or -1 for no limit
139 |
140 | # - Kernel Resource Usage -
141 |
142 | #max_files_per_process = 1000 # min 25
143 | # (change requires restart)
144 | #shared_preload_libraries = '' # (change requires restart)
145 |
146 | # - Cost-Based Vacuum Delay -
147 |
148 | #vacuum_cost_delay = 0 # 0-100 milliseconds
149 | #vacuum_cost_page_hit = 1 # 0-10000 credits
150 | #vacuum_cost_page_miss = 10 # 0-10000 credits
151 | #vacuum_cost_page_dirty = 20 # 0-10000 credits
152 | #vacuum_cost_limit = 200 # 1-10000 credits
153 |
154 | # - Background Writer -
155 |
156 | #bgwriter_delay = 200ms # 10-10000ms between rounds
157 | #bgwriter_lru_maxpages = 100 # 0-1000 max buffers written/round
158 | #bgwriter_lru_multiplier = 2.0 # 0-10.0 multipler on buffers scanned/round
159 |
160 | # - Asynchronous Behavior -
161 |
162 | #effective_io_concurrency = 1 # 1-1000; 0 disables prefetching
163 | #max_worker_processes = 8
164 |
165 |
166 | #------------------------------------------------------------------------------
167 | # WRITE AHEAD LOG
168 | #------------------------------------------------------------------------------
169 |
170 | # - Settings -
171 |
172 | #wal_level = minimal # minimal, archive, hot_standby, or logical
173 | # (change requires restart)
174 | #fsync = on # turns forced synchronization on or off
175 | #synchronous_commit = on # synchronization level;
176 | # off, local, remote_write, or on
177 | #wal_sync_method = fsync # the default is the first option
178 | # supported by the operating system:
179 | # open_datasync
180 | # fdatasync (default on Linux)
181 | # fsync
182 | # fsync_writethrough
183 | # open_sync
184 | #full_page_writes = on # recover from partial page writes
185 | #wal_compression = off # enable compression of full-page writes
186 | #wal_log_hints = off # also do full page writes of non-critical updates
187 | # (change requires restart)
188 | #wal_buffers = -1 # min 32kB, -1 sets based on shared_buffers
189 | # (change requires restart)
190 | #wal_writer_delay = 200ms # 1-10000 milliseconds
191 |
192 | #commit_delay = 0 # range 0-100000, in microseconds
193 | #commit_siblings = 5 # range 1-1000
194 |
195 | # - Checkpoints -
196 |
197 | #checkpoint_timeout = 5min # range 30s-1h
198 | #max_wal_size = 1GB
199 | #min_wal_size = 80MB
200 | #checkpoint_completion_target = 0.5 # checkpoint target duration, 0.0 - 1.0
201 | #checkpoint_warning = 30s # 0 disables
202 |
203 | # - Archiving -
204 |
205 | #archive_mode = off # enables archiving; off, on, or always
206 | # (change requires restart)
207 | #archive_command = '' # command to use to archive a logfile segment
208 | # placeholders: %p = path of file to archive
209 | # %f = file name only
210 | # e.g. 'test ! -f /mnt/server/archivedir/%f && cp %p /mnt/server/archivedir/%f'
211 | #archive_timeout = 0 # force a logfile segment switch after this
212 | # number of seconds; 0 disables
213 |
214 |
215 | #------------------------------------------------------------------------------
216 | # REPLICATION
217 | #------------------------------------------------------------------------------
218 |
219 | # - Sending Server(s) -
220 |
221 | # Set these on the master and on any standby that will send replication data.
222 |
223 | #max_wal_senders = 0 # max number of walsender processes
224 | # (change requires restart)
225 | #wal_keep_segments = 0 # in logfile segments, 16MB each; 0 disables
226 | #wal_sender_timeout = 60s # in milliseconds; 0 disables
227 |
228 | #max_replication_slots = 0 # max number of replication slots
229 | # (change requires restart)
230 | #track_commit_timestamp = off # collect timestamp of transaction commit
231 | # (change requires restart)
232 |
233 | # - Master Server -
234 |
235 | # These settings are ignored on a standby server.
236 |
237 | #synchronous_standby_names = '' # standby servers that provide sync rep
238 | # comma-separated list of application_name
239 | # from standby(s); '*' = all
240 | #vacuum_defer_cleanup_age = 0 # number of xacts by which cleanup is delayed
241 |
242 | # - Standby Servers -
243 |
244 | # These settings are ignored on a master server.
245 |
246 | #hot_standby = off # "on" allows queries during recovery
247 | # (change requires restart)
248 | #max_standby_archive_delay = 30s # max delay before canceling queries
249 | # when reading WAL from archive;
250 | # -1 allows indefinite delay
251 | #max_standby_streaming_delay = 30s # max delay before canceling queries
252 | # when reading streaming WAL;
253 | # -1 allows indefinite delay
254 | #wal_receiver_status_interval = 10s # send replies at least this often
255 | # 0 disables
256 | #hot_standby_feedback = off # send info from standby to prevent
257 | # query conflicts
258 | #wal_receiver_timeout = 60s # time that receiver waits for
259 | # communication from master
260 | # in milliseconds; 0 disables
261 | #wal_retrieve_retry_interval = 5s # time to wait before retrying to
262 | # retrieve WAL after a failed attempt
263 |
264 |
265 | #------------------------------------------------------------------------------
266 | # QUERY TUNING
267 | #------------------------------------------------------------------------------
268 |
269 | # - Planner Method Configuration -
270 |
271 | #enable_bitmapscan = on
272 | #enable_hashagg = on
273 | #enable_hashjoin = on
274 | #enable_indexscan = on
275 | #enable_indexonlyscan = on
276 | #enable_material = on
277 | #enable_mergejoin = on
278 | #enable_nestloop = on
279 | #enable_seqscan = on
280 | #enable_sort = on
281 | #enable_tidscan = on
282 |
283 | # - Planner Cost Constants -
284 |
285 | #seq_page_cost = 1.0 # measured on an arbitrary scale
286 | #random_page_cost = 4.0 # same scale as above
287 | #cpu_tuple_cost = 0.01 # same scale as above
288 | #cpu_index_tuple_cost = 0.005 # same scale as above
289 | #cpu_operator_cost = 0.0025 # same scale as above
290 | #effective_cache_size = 4GB
291 |
292 | # - Genetic Query Optimizer -
293 |
294 | #geqo = on
295 | #geqo_threshold = 12
296 | #geqo_effort = 5 # range 1-10
297 | #geqo_pool_size = 0 # selects default based on effort
298 | #geqo_generations = 0 # selects default based on effort
299 | #geqo_selection_bias = 2.0 # range 1.5-2.0
300 | #geqo_seed = 0.0 # range 0.0-1.0
301 |
302 | # - Other Planner Options -
303 |
304 | #default_statistics_target = 100 # range 1-10000
305 | #constraint_exclusion = partition # on, off, or partition
306 | #cursor_tuple_fraction = 0.1 # range 0.0-1.0
307 | #from_collapse_limit = 8
308 | #join_collapse_limit = 8 # 1 disables collapsing of explicit
309 | # JOIN clauses
310 |
311 |
312 | #------------------------------------------------------------------------------
313 | # ERROR REPORTING AND LOGGING
314 | #------------------------------------------------------------------------------
315 |
316 | # - Where to Log -
317 |
318 | #log_destination = 'stderr' # Valid values are combinations of
319 | # stderr, csvlog, syslog, and eventlog,
320 | # depending on platform. csvlog
321 | # requires logging_collector to be on.
322 |
323 | # This is used when logging to stderr:
324 | #logging_collector = off # Enable capturing of stderr and csvlog
325 | # into log files. Required to be on for
326 | # csvlogs.
327 | # (change requires restart)
328 |
329 | # These are only used if logging_collector is on:
330 | #log_directory = 'pg_log' # directory where log files are written,
331 | # can be absolute or relative to PGDATA
332 | #log_filename = 'postgresql-%Y-%m-%d_%H%M%S.log' # log file name pattern,
333 | # can include strftime() escapes
334 | #log_file_mode = 0600 # creation mode for log files,
335 | # begin with 0 to use octal notation
336 | #log_truncate_on_rotation = off # If on, an existing log file with the
337 | # same name as the new log file will be
338 | # truncated rather than appended to.
339 | # But such truncation only occurs on
340 | # time-driven rotation, not on restarts
341 | # or size-driven rotation. Default is
342 | # off, meaning append to existing files
343 | # in all cases.
344 | #log_rotation_age = 1d # Automatic rotation of logfiles will
345 | # happen after that time. 0 disables.
346 | #log_rotation_size = 10MB # Automatic rotation of logfiles will
347 | # happen after that much log output.
348 | # 0 disables.
349 |
350 | # These are relevant when logging to syslog:
351 | #syslog_facility = 'LOCAL0'
352 | #syslog_ident = 'postgres'
353 |
354 | # This is only relevant when logging to eventlog (win32):
355 | # (change requires restart)
356 | #event_source = 'PostgreSQL'
357 |
358 | # - When to Log -
359 |
360 | #client_min_messages = notice # values in order of decreasing detail:
361 | # debug5
362 | # debug4
363 | # debug3
364 | # debug2
365 | # debug1
366 | # log
367 | # notice
368 | # warning
369 | # error
370 |
371 | #log_min_messages = warning # values in order of decreasing detail:
372 | # debug5
373 | # debug4
374 | # debug3
375 | # debug2
376 | # debug1
377 | # info
378 | # notice
379 | # warning
380 | # error
381 | # log
382 | # fatal
383 | # panic
384 |
385 | #log_min_error_statement = error # values in order of decreasing detail:
386 | # debug5
387 | # debug4
388 | # debug3
389 | # debug2
390 | # debug1
391 | # info
392 | # notice
393 | # warning
394 | # error
395 | # log
396 | # fatal
397 | # panic (effectively off)
398 |
399 | #log_min_duration_statement = -1 # -1 is disabled, 0 logs all statements
400 | # and their durations, > 0 logs only
401 | # statements running at least this number
402 | # of milliseconds
403 |
404 |
405 | # - What to Log -
406 |
407 | #debug_print_parse = off
408 | #debug_print_rewritten = off
409 | #debug_print_plan = off
410 | #debug_pretty_print = on
411 | #log_checkpoints = off
412 | #log_connections = off
413 | #log_disconnections = off
414 | #log_duration = off
415 | #log_error_verbosity = default # terse, default, or verbose messages
416 | #log_hostname = off
417 | #log_line_prefix = '' # special values:
418 | # %a = application name
419 | # %u = user name
420 | # %d = database name
421 | # %r = remote host and port
422 | # %h = remote host
423 | # %p = process ID
424 | # %t = timestamp without milliseconds
425 | # %m = timestamp with milliseconds
426 | # %i = command tag
427 | # %e = SQL state
428 | # %c = session ID
429 | # %l = session line number
430 | # %s = session start timestamp
431 | # %v = virtual transaction ID
432 | # %x = transaction ID (0 if none)
433 | # %q = stop here in non-session
434 | # processes
435 | # %% = '%'
436 | # e.g. '<%u%%%d> '
437 | #log_lock_waits = off # log lock waits >= deadlock_timeout
438 | #log_statement = 'none' # none, ddl, mod, all
439 | #log_replication_commands = off
440 | #log_temp_files = -1 # log temporary files equal or larger
441 | # than the specified size in kilobytes;
442 | # -1 disables, 0 logs all temp files
443 | #log_timezone = 'GMT'
444 |
445 |
446 | # - Process Title -
447 |
448 | #cluster_name = '' # added to process titles if nonempty
449 | # (change requires restart)
450 | #update_process_title = on
451 |
452 |
453 | #------------------------------------------------------------------------------
454 | # RUNTIME STATISTICS
455 | #------------------------------------------------------------------------------
456 |
457 | # - Query/Index Statistics Collector -
458 |
459 | #track_activities = on
460 | #track_counts = on
461 | #track_io_timing = off
462 | #track_functions = none # none, pl, all
463 | #track_activity_query_size = 1024 # (change requires restart)
464 | #stats_temp_directory = 'pg_stat_tmp'
465 |
466 |
467 | # - Statistics Monitoring -
468 |
469 | #log_parser_stats = off
470 | #log_planner_stats = off
471 | #log_executor_stats = off
472 | #log_statement_stats = off
473 |
474 |
475 | #------------------------------------------------------------------------------
476 | # AUTOVACUUM PARAMETERS
477 | #------------------------------------------------------------------------------
478 |
479 | #autovacuum = on # Enable autovacuum subprocess? 'on'
480 | # requires track_counts to also be on.
481 | #log_autovacuum_min_duration = -1 # -1 disables, 0 logs all actions and
482 | # their durations, > 0 logs only
483 | # actions running at least this number
484 | # of milliseconds.
485 | #autovacuum_max_workers = 3 # max number of autovacuum subprocesses
486 | # (change requires restart)
487 | #autovacuum_naptime = 1min # time between autovacuum runs
488 | #autovacuum_vacuum_threshold = 50 # min number of row updates before
489 | # vacuum
490 | #autovacuum_analyze_threshold = 50 # min number of row updates before
491 | # analyze
492 | #autovacuum_vacuum_scale_factor = 0.2 # fraction of table size before vacuum
493 | #autovacuum_analyze_scale_factor = 0.1 # fraction of table size before analyze
494 | #autovacuum_freeze_max_age = 200000000 # maximum XID age before forced vacuum
495 | # (change requires restart)
496 | #autovacuum_multixact_freeze_max_age = 400000000 # maximum multixact age
497 | # before forced vacuum
498 | # (change requires restart)
499 | #autovacuum_vacuum_cost_delay = 20ms # default vacuum cost delay for
500 | # autovacuum, in milliseconds;
501 | # -1 means use vacuum_cost_delay
502 | #autovacuum_vacuum_cost_limit = -1 # default vacuum cost limit for
503 | # autovacuum, -1 means use
504 | # vacuum_cost_limit
505 |
506 |
507 | #------------------------------------------------------------------------------
508 | # CLIENT CONNECTION DEFAULTS
509 | #------------------------------------------------------------------------------
510 |
511 | # - Statement Behavior -
512 |
513 | #search_path = '"$user", public' # schema names
514 | #default_tablespace = '' # a tablespace name, '' uses the default
515 | #temp_tablespaces = '' # a list of tablespace names, '' uses
516 | # only default tablespace
517 | #check_function_bodies = on
518 | #default_transaction_isolation = 'read committed'
519 | #default_transaction_read_only = off
520 | #default_transaction_deferrable = off
521 | #session_replication_role = 'origin'
522 | #statement_timeout = 0 # in milliseconds, 0 is disabled
523 | #lock_timeout = 0 # in milliseconds, 0 is disabled
524 | #vacuum_freeze_min_age = 50000000
525 | #vacuum_freeze_table_age = 150000000
526 | #vacuum_multixact_freeze_min_age = 5000000
527 | #vacuum_multixact_freeze_table_age = 150000000
528 | #bytea_output = 'hex' # hex, escape
529 | #xmlbinary = 'base64'
530 | #xmloption = 'content'
531 | #gin_fuzzy_search_limit = 0
532 | #gin_pending_list_limit = 4MB
533 |
534 | # - Locale and Formatting -
535 |
536 | #datestyle = 'iso, mdy'
537 | #intervalstyle = 'postgres'
538 | #timezone = 'GMT'
539 | #timezone_abbreviations = 'Default' # Select the set of available time zone
540 | # abbreviations. Currently, there are
541 | # Default
542 | # Australia (historical usage)
543 | # India
544 | # You can create your own file in
545 | # share/timezonesets/.
546 | #extra_float_digits = 0 # min -15, max 3
547 | #client_encoding = sql_ascii # actually, defaults to database
548 | # encoding
549 |
550 | # These settings are initialized by initdb, but they can be changed.
551 | #lc_messages = 'C' # locale for system error message
552 | # strings
553 | #lc_monetary = 'C' # locale for monetary formatting
554 | #lc_numeric = 'C' # locale for number formatting
555 | #lc_time = 'C' # locale for time formatting
556 |
557 | # default configuration for text search
558 | #default_text_search_config = 'pg_catalog.simple'
559 |
560 | # - Other Defaults -
561 |
562 | #dynamic_library_path = '$libdir'
563 | #local_preload_libraries = ''
564 | #session_preload_libraries = ''
565 |
566 |
567 | #------------------------------------------------------------------------------
568 | # LOCK MANAGEMENT
569 | #------------------------------------------------------------------------------
570 |
571 | #deadlock_timeout = 1s
572 | #max_locks_per_transaction = 64 # min 10
573 | # (change requires restart)
574 | #max_pred_locks_per_transaction = 64 # min 10
575 | # (change requires restart)
576 |
577 |
578 | #------------------------------------------------------------------------------
579 | # VERSION/PLATFORM COMPATIBILITY
580 | #------------------------------------------------------------------------------
581 |
582 | # - Previous PostgreSQL Versions -
583 |
584 | #array_nulls = on
585 | #backslash_quote = safe_encoding # on, off, or safe_encoding
586 | #default_with_oids = off
587 | #escape_string_warning = on
588 | #lo_compat_privileges = off
589 | #operator_precedence_warning = off
590 | #quote_all_identifiers = off
591 | #sql_inheritance = on
592 | #standard_conforming_strings = on
593 | #synchronize_seqscans = on
594 |
595 | # - Other Platforms and Clients -
596 |
597 | #transform_null_equals = off
598 |
599 |
600 | #------------------------------------------------------------------------------
601 | # ERROR HANDLING
602 | #------------------------------------------------------------------------------
603 |
604 | #exit_on_error = off # terminate session on any error?
605 | #restart_after_crash = on # reinitialize after backend crash?
606 |
607 |
608 | #------------------------------------------------------------------------------
609 | # CONFIG FILE INCLUDES
610 | #------------------------------------------------------------------------------
611 |
612 | # These options allow settings to be loaded from files other than the
613 | # default postgresql.conf.
614 |
615 | #include_dir = 'conf.d' # include files ending in '.conf' from
616 | # directory 'conf.d'
617 | #include_if_exists = 'exists.conf' # include file only if it exists
618 | #include = 'special.conf' # include file
619 |
620 |
621 | #------------------------------------------------------------------------------
622 | # CUSTOMIZED OPTIONS
623 | #------------------------------------------------------------------------------
624 |
625 | # Add settings for extensions here
626 |
627 |
--------------------------------------------------------------------------------