├── .coveralls.yml ├── .github ├── auto_assign.yml ├── dependabot.yml ├── release-drafter.yml └── workflows │ ├── auto_assignee.yml │ ├── auto_release.yml │ ├── ci.yml │ └── stale.yml ├── .gitignore ├── .run ├── Functional.run.xml ├── Tests .run.xml └── Unit.run.xml ├── LICENSE ├── README.md ├── build └── logs │ └── clover.xml ├── composer.json ├── ecs.php ├── ecs.yml ├── phpcs.xml ├── phpunit.xml.dist ├── src ├── .meta.php ├── Compilers │ ├── AttachPartitionCompiler.php │ ├── CheckCompiler.php │ ├── CreateCompiler.php │ ├── ExcludeCompiler.php │ ├── Traits │ │ └── WheresBuilder.php │ └── UniqueCompiler.php ├── Connectors │ └── ConnectionFactory.php ├── Extensions │ ├── AbstractComponent.php │ ├── AbstractExtension.php │ ├── Connectors │ │ └── AbstractConnection.php │ ├── Exceptions │ │ ├── ExtensionInvalidException.php │ │ ├── MacroableMissedException.php │ │ └── MixinInvalidException.php │ └── Schema │ │ ├── AbstractBlueprint.php │ │ ├── AbstractBuilder.php │ │ └── Grammar │ │ └── AbstractGrammar.php ├── Helpers │ ├── ColumnAssertions.php │ ├── IndexAssertions.php │ ├── PostgresTextSanitizer.php │ ├── TableAssertions.php │ └── ViewAssertions.php ├── PostgresConnection.php ├── Schema │ ├── Blueprint.php │ ├── Builder.php │ ├── Builders │ │ ├── Constraints │ │ │ ├── Check │ │ │ │ └── CheckBuilder.php │ │ │ └── Exclude │ │ │ │ └── ExcludeBuilder.php │ │ ├── Indexes │ │ │ └── Unique │ │ │ │ ├── UniqueBuilder.php │ │ │ │ └── UniquePartialBuilder.php │ │ └── WhereBuilderTrait.php │ ├── Definitions │ │ ├── AttachPartitionDefinition.php │ │ ├── CheckDefinition.php │ │ ├── ExcludeDefinition.php │ │ ├── ForeignKeyDefinition.php │ │ ├── LikeDefinition.php │ │ ├── UniqueDefinition.php │ │ └── ViewDefinition.php │ ├── Grammars │ │ └── PostgresGrammar.php │ └── Types │ │ ├── DateRangeType.php │ │ ├── NumericType.php │ │ ├── TsRangeType.php │ │ └── TsTzRangeType.php └── UmbrellioPostgresProvider.php ├── tests.sh └── tests ├── Functional ├── Connection │ └── ConnectionTest.php └── Schema │ ├── CreateIndexTest.php │ └── CreateTableTest.php ├── FunctionalTestCase.php ├── TestCase.php ├── Unit ├── Extensions │ └── AbstractExtensionTest.php ├── Helpers │ └── BlueprintAssertions.php └── Schema │ ├── Blueprint │ ├── IndexTest.php │ └── PartitionTest.php │ └── Types │ ├── DateRangeTypeTest.php │ ├── NumericTypeTest.php │ ├── TsRangeTypeTest.php │ └── TsTzRangeTypeTest.php └── _data ├── CustomSQLiteConnection.php └── database.sqlite /.coveralls.yml: -------------------------------------------------------------------------------- 1 | coverage_clover: build/logs/clover.xml 2 | json_path: build/logs/coveralls-upload.json 3 | service_name: travis-ci 4 | -------------------------------------------------------------------------------- /.github/auto_assign.yml: -------------------------------------------------------------------------------- 1 | addReviewers: true 2 | 3 | numberOfReviewers: 1 4 | 5 | reviewers: 6 | - pvsaitpe 7 | 8 | addAssignees: true 9 | 10 | assignees: 11 | - pvsaintpe 12 | 13 | numberOfAssignees: 1 14 | 15 | skipKeywords: 16 | - wip 17 | - draft 18 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: composer 5 | directory: "/" 6 | schedule: 7 | interval: daily 8 | open-pull-requests-limit: 10 9 | reviewers: 10 | - pvsaintpe 11 | assignees: 12 | - pvsaintpe 13 | labels: 14 | - type:build 15 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | template: | 2 | ## Changes 3 | 4 | $CHANGES 5 | 6 | change-template: '- **$TITLE** (#$NUMBER)' 7 | 8 | version-template: "$MAJOR.$MINOR.$PATCH" 9 | name-template: '$RESOLVED_VERSION' 10 | tag-template: '$RESOLVED_VERSION' 11 | 12 | categories: 13 | - title: 'Features' 14 | labels: 15 | - 'feature' 16 | - 'type:compilers' 17 | - 'type:helpers' 18 | - 'type:routines' 19 | - 'type:indexes' 20 | - 'type:schema' 21 | - title: 'Bug Fixes' 22 | labels: 23 | - 'fix' 24 | - 'bugfix' 25 | - 'bug' 26 | - 'hotfix' 27 | - 'duplicate' 28 | - title: 'Maintenance' 29 | labels: 30 | - 'type:build' 31 | - 'refactoring' 32 | - 'theme:docs' 33 | - 'type:tests' 34 | - 'analysis' 35 | 36 | change-title-escapes: '\<*_&' 37 | 38 | version-resolver: 39 | major: 40 | labels: 41 | - major 42 | minor: 43 | labels: 44 | - minor 45 | patch: 46 | labels: 47 | - patch 48 | default: patch 49 | -------------------------------------------------------------------------------- /.github/workflows/auto_assignee.yml: -------------------------------------------------------------------------------- 1 | name: 'Auto assign assignees or reviewers' 2 | on: pull_request 3 | 4 | jobs: 5 | add-reviews: 6 | name: "Auto assignment of a assignee" 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: kentaro-m/auto-assign-action@v1.1.2 10 | with: 11 | configuration-path: ".github/auto_assign.yml" 12 | -------------------------------------------------------------------------------- /.github/workflows/auto_release.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | update_release_draft: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: release-drafter/release-drafter@v5 13 | with: 14 | publish: true 15 | env: 16 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 17 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | types: 9 | - opened 10 | - reopened 11 | - edited 12 | - synchronize 13 | - labeled 14 | - assigned 15 | - unlabeled 16 | - unlocked 17 | - review_requested 18 | - review_request_removed 19 | - unassigned 20 | 21 | env: 22 | COVERAGE: '1' 23 | php_extensions: 'apcu, bcmath, ctype, curl, dom, iconv, intl, json, mbstring, opcache, openssl, pdo, pdo_pgsql, pcntl, pcov, posix, redis, session, simplexml, sockets, tokenizer, xml, xmlwriter, zip, xdebug' 24 | key: cache-v0.1 25 | DB_USER: 'postgres' 26 | DB_NAME: 'postgres' 27 | DB_PASSWORD: 'postgres' 28 | DB_HOST: '127.0.0.1' 29 | 30 | jobs: 31 | lint: 32 | runs-on: '${{ matrix.operating_system }}' 33 | timeout-minutes: 20 34 | strategy: 35 | matrix: 36 | operating_system: 37 | - ubuntu-latest 38 | php_versions: 39 | - '8.3' 40 | fail-fast: false 41 | env: 42 | PHP_CS_FIXER_FUTURE_MODE: '0' 43 | name: 'Linter PHP' 44 | steps: 45 | - name: 'Checkout' 46 | uses: actions/checkout@v2 47 | - name: 'Setup cache environment' 48 | id: cache-env 49 | uses: shivammathur/cache-extensions@v1 50 | with: 51 | php-version: '${{ matrix.php_versions }}' 52 | extensions: '${{ env.php_extensions }}' 53 | key: '${{ env.key }}' 54 | - name: 'Cache extensions' 55 | uses: actions/cache@v4 56 | with: 57 | path: '${{ steps.cache-env.outputs.dir }}' 58 | key: '${{ steps.cache-env.outputs.key }}' 59 | restore-keys: '${{ steps.cache-env.outputs.key }}' 60 | - name: 'Setup PHP' 61 | uses: shivammathur/setup-php@v2 62 | with: 63 | php-version: ${{ matrix.php_versions }} 64 | extensions: '${{ env.php_extensions }}' 65 | ini-values: memory_limit=-1 66 | tools: pecl, composer 67 | coverage: none 68 | - name: 'Setup problem matchers for PHP (aka PHP error logs)' 69 | run: 'echo "::add-matcher::${{ runner.tool_cache }}/php.json"' 70 | - name: 'Setup problem matchers for PHPUnit' 71 | run: 'echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"' 72 | - name: 'Install PHP dependencies with Composer' 73 | run: COMPOSER_MEMORY_LIMIT=-1 composer install --prefer-dist --no-progress --no-suggest --optimize-autoloader 74 | working-directory: './' 75 | - name: 'Linting PHP source files' 76 | run: 'vendor/bin/ecs check --config=ecs.php .' 77 | test: 78 | strategy: 79 | fail-fast: false 80 | matrix: 81 | coverage: [true] 82 | experimental: [false] 83 | operating_system: [ubuntu-latest] 84 | postgres: ['13', '14', '15'] 85 | php_versions: ['8.3'] 86 | include: 87 | - operating_system: 'ubuntu-latest' 88 | php_versions: '8.4' 89 | postgres: '16' 90 | coverage: false 91 | experimental: true 92 | runs-on: '${{ matrix.operating_system }}' 93 | services: 94 | postgres: 95 | image: 'postgres:${{ matrix.postgres }}' 96 | env: 97 | POSTGRES_USER: ${{ env.DB_USER }} 98 | POSTGRES_PASSWORD: ${{ env.DB_PASSWORD }} 99 | POSTGRES_DB: ${{ env.DB_NAME }} 100 | ports: 101 | - 5432:5432 102 | # needed because the postgres container does not provide a healthcheck 103 | options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 104 | name: 'Testing / PHP ${{ matrix.php_versions }} / Postgres ${{ matrix.postgres }}' 105 | needs: 106 | - lint 107 | steps: 108 | - name: Checkout 109 | uses: actions/checkout@v2 110 | with: 111 | fetch-depth: 1 112 | - name: apt-get 113 | run: | 114 | sudo apt-get update -y 115 | sudo apt-get install -y libpq-dev postgresql-client 116 | - name: 'Setup cache environment' 117 | id: cache-env 118 | uses: shivammathur/cache-extensions@v1 119 | with: 120 | php-version: ${{ matrix.php_versions }} 121 | extensions: ${{ env.php_extensions }} 122 | key: '${{ env.key }}' 123 | - name: 'Cache extensions' 124 | uses: actions/cache@v4 125 | with: 126 | path: '${{ steps.cache-env.outputs.dir }}' 127 | key: '${{ steps.cache-env.outputs.key }}' 128 | restore-keys: '${{ steps.cache-env.outputs.key }}' 129 | - name: 'Setup PHP' 130 | uses: shivammathur/setup-php@v2 131 | with: 132 | php-version: ${{ matrix.php_versions }} 133 | extensions: ${{ env.php_extensions }} 134 | ini-values: 'date.timezone=UTC, upload_max_filesize=20M, post_max_size=20M, memory_limit=512M, short_open_tag=Off, xdebug.mode="develop,coverage"' 135 | coverage: xdebug 136 | tools: 'phpunit' 137 | - name: 'Install PHP dependencies with Composer' 138 | continue-on-error: ${{ matrix.experimental }} 139 | run: COMPOSER_MEMORY_LIMIT=-1 composer install --prefer-dist --no-progress --no-suggest --optimize-autoloader 140 | working-directory: './' 141 | - name: 'Run Unit Tests with PHPUnit' 142 | continue-on-error: ${{ matrix.experimental }} 143 | run: | 144 | php -v 145 | sed -e "s/\${USERNAME}/${{ env.DB_USER }}/" \ 146 | -e "s/\${PASSWORD}/${{ env.DB_PASSWORD }}/" \ 147 | -e "s/\${DATABASE}/${{ env.DB_NAME }}/" \ 148 | -e "s/\${HOST}/${{ env.DB_HOST }}/" \ 149 | phpunit.xml.dist > phpunit.xml 150 | ./vendor/bin/phpunit \ 151 | --stderr \ 152 | --coverage-clover build/logs/clover.xml \ 153 | --coverage-text 154 | cat build/logs/clover.xml 155 | working-directory: './' 156 | - name: Upload coverage results to Coveralls 157 | if: ${{ !matrix.experimental && matrix.coverage }} 158 | env: 159 | COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} 160 | COVERALLS_PARALLEL: true 161 | COVERALLS_FLAG_NAME: php-${{ matrix.php_versions }}-postgres-${{ matrix.postgres }} 162 | run: ./vendor/bin/php-coveralls --coverage_clover=build/logs/clover.xml -v 163 | coverage: 164 | needs: test 165 | runs-on: ubuntu-latest 166 | name: "Code coverage" 167 | steps: 168 | - name: Coveralls Finished 169 | uses: coverallsapp/github-action@v1.1.2 170 | with: 171 | github-token: ${{ secrets.GITHUB_TOKEN }} 172 | parallel-finished: true 173 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: "Close stale issues" 2 | on: 3 | schedule: 4 | - cron: "30 1 * * *" 5 | 6 | jobs: 7 | stale: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/stale@v3 11 | with: 12 | repo-token: ${{ secrets.GITHUB_TOKEN }} 13 | stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days' 14 | days-before-stale: 30 15 | days-before-close: 5 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | /.idea 3 | .ecs_cache 4 | phpunit.xml 5 | .phpunit.result.cache 6 | .phpunit.cache 7 | composer.lock 8 | /build 9 | -------------------------------------------------------------------------------- /.run/Functional.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /.run/Tests .run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /.run/Unit.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Umbrellio 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel PG extensions 2 | 3 | [![Github Status](https://github.com/umbrellio/laravel-pg-extensions/workflows/CI/badge.svg)](https://github.com/umbrellio/laravel-pg-extensions/actions) 4 | [![Coverage Status](https://coveralls.io/repos/github/umbrellio/laravel-pg-extensions/badge.svg?branch=master)](https://coveralls.io/github/umbrellio/laravel-pg-extensions?branch=master) 5 | [![Latest Stable Version](https://poser.pugx.org/umbrellio/laravel-pg-extensions/v/stable.png)](https://packagist.org/packages/umbrellio/laravel-pg-extensions) 6 | [![Total Downloads](https://poser.pugx.org/umbrellio/laravel-pg-extensions/downloads.png)](https://packagist.org/packages/umbrellio/laravel-pg-extensions) 7 | [![Code Intelligence Status](https://scrutinizer-ci.com/g/umbrellio/laravel-pg-extensions/badges/code-intelligence.svg?b=master)](https://scrutinizer-ci.com/code-intelligence) 8 | [![Build Status](https://scrutinizer-ci.com/g/umbrellio/laravel-pg-extensions/badges/build.png?b=master)](https://scrutinizer-ci.com/g/umbrellio/laravel-pg-extensions/build-status/master) 9 | [![Code Coverage](https://scrutinizer-ci.com/g/umbrellio/laravel-pg-extensions/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/umbrellio/laravel-pg-extensions/?branch=master) 10 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/umbrellio/laravel-pg-extensions/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/umbrellio/laravel-pg-extensions/?branch=master) 11 | 12 | This project extends Laravel's database layer to allow use specific Postgres features without raw queries. 13 | 14 | ## Installation 15 | 16 | Run this command to install: 17 | ```bash 18 | composer require umbrellio/laravel-pg-extensions 19 | ``` 20 | 21 | ## Features 22 | 23 | - [Extended `Schema::create()`](#extended-table-creation) 24 | - [Added Support NUMERIC Type](#numeric-column-type) 25 | - [Extended `Schema` with USING](#extended-schema-using) 26 | - [Extended `Schema` for views](#create-views) 27 | - [Working with UNIQUE indexes](#extended-unique-indexes-creation) 28 | - [Working with EXCLUDE constraints](#exclude-constraints-creation) 29 | - [Working with CHECK constraints](#check-constraints-creation) 30 | - [Working with partitions](#partitions) 31 | - [Check existing index before manipulation](#check-existing-index) 32 | - [Getting foreign keys for table](#get-foreign-keys) 33 | 34 | ### Extended table creation 35 | 36 | Example: 37 | ```php 38 | Schema::create('table', function (Blueprint $table) { 39 | $table->like('other_table')->includingAll(); 40 | $table->ifNotExists(); 41 | }); 42 | ``` 43 | 44 | ### Extended Schema USING 45 | 46 | Example: 47 | ```php 48 | Schema::create('table', function (Blueprint $table) { 49 | $table->integer('number'); 50 | }); 51 | 52 | //modifications with data... 53 | 54 | Schema::table('table', function (Blueprint $table) { 55 | $table 56 | ->string('number') 57 | ->using("('[' || number || ']')::character varying") 58 | ->change(); 59 | }); 60 | ``` 61 | 62 | ### Create views 63 | 64 | Example: 65 | ```php 66 | // Facade methods: 67 | Schema::createView('active_users', "SELECT * FROM users WHERE active = 1"); 68 | Schema::dropView('active_users') 69 | 70 | // Schema methods: 71 | Schema::create('users', function (Blueprint $table) { 72 | $table 73 | ->createView('active_users', "SELECT * FROM users WHERE active = 1") 74 | ->materialize(); 75 | }); 76 | ``` 77 | 78 | ### Get foreign keys 79 | 80 | Example: 81 | ```php 82 | // Facade methods: 83 | /** @var ForeignKeyDefinition[] $fks */ 84 | $fks = Schema::getForeignKeys('some_table'); 85 | 86 | foreach ($fks as $fk) { 87 | // $fk->source_column_name 88 | // $fk->target_table_name 89 | // $fk->target_column_name 90 | } 91 | ``` 92 | 93 | ### Extended unique indexes creation 94 | 95 | Example: 96 | ```php 97 | Schema::create('table', function (Blueprint $table) { 98 | $table->string('code'); 99 | $table->softDeletes(); 100 | $table->uniquePartial('code')->whereNull('deleted_at'); 101 | }); 102 | ``` 103 | 104 | If you want to delete partial unique index, use this method: 105 | ```php 106 | Schema::create('table', function (Blueprint $table) { 107 | $table->dropUniquePartial(['code']); 108 | }); 109 | ``` 110 | 111 | `$table->dropUnique()` doesn't work for Partial Unique Indexes, because PostgreSQL doesn't 112 | define a partial (ie conditional) UNIQUE constraint. If you try to delete such a Partial Unique 113 | Index you will get an error. 114 | 115 | ```SQL 116 | CREATE UNIQUE INDEX CONCURRENTLY examples_new_col_idx ON examples (new_col); 117 | ALTER TABLE examples 118 | ADD CONSTRAINT examples_unique_constraint USING INDEX examples_new_col_idx; 119 | ``` 120 | 121 | When you create a unique index without conditions, PostgresSQL will create Unique Constraint 122 | automatically for you, and when you try to delete such an index, Constraint will be deleted 123 | first, then Unique Index. 124 | 125 | ### Exclude constraints creation 126 | 127 | Using the example below: 128 | ```php 129 | Schema::create('table', function (Blueprint $table) { 130 | $table->integer('type_id'); 131 | $table->date('date_start'); 132 | $table->date('date_end'); 133 | $table->softDeletes(); 134 | $table 135 | ->exclude(['date_start', 'date_end']) 136 | ->using('type_id', '=') 137 | ->using('daterange(date_start, date_end)', '&&') 138 | ->method('gist') 139 | ->with('some_arg', 1) 140 | ->with('any_arg', 'some_value') 141 | ->whereNull('deleted_at'); 142 | }); 143 | ``` 144 | 145 | An Exclude Constraint will be generated for your table: 146 | ```SQL 147 | ALTER TABLE test_table 148 | ADD CONSTRAINT test_table_date_start_date_end_excl 149 | EXCLUDE USING gist (type_id WITH =, daterange(date_start, date_end) WITH &&) 150 | WITH (some_arg = 1, any_arg = 'some_value') 151 | WHERE ("deleted_at" is null) 152 | ``` 153 | 154 | ### Check constraints creation 155 | 156 | Using the example below: 157 | ```php 158 | Schema::create('table', function (Blueprint $table) { 159 | $table->integer('type_id'); 160 | $table->date('date_start'); 161 | $table->date('date_end'); 162 | $table 163 | ->check(['date_start', 'date_end']) 164 | ->whereColumn('date_end', '>', 'date_start') 165 | ->whereIn('type_id', [1, 2, 3]); 166 | }); 167 | ``` 168 | 169 | An Check Constraint will be generated for your table: 170 | ```SQL 171 | ALTER TABLE test_table 172 | ADD CONSTRAINT test_table_date_start_date_end_chk 173 | CHECK ("date_end" > "date_start" AND "type_id" IN [1, 2, 3]) 174 | ``` 175 | 176 | ### Partitions 177 | 178 | Support for attaching and detaching partitions. 179 | 180 | Example: 181 | ```php 182 | Schema::table('table', function (Blueprint $table) { 183 | $table->attachPartition('partition')->range([ 184 | 'from' => now()->startOfDay(), // Carbon will be converted to date time string 185 | 'to' => now()->tomorrow(), 186 | ]); 187 | }); 188 | ``` 189 | 190 | ### Check existing index 191 | 192 | ```php 193 | Schema::table('some_table', function (Blueprint $table) { 194 | // check unique index exists on column 195 | if ($table->hasIndex(['column'], true)) { 196 | $table->dropUnique(['column']); 197 | } 198 | $table->uniquePartial('column')->whereNull('deleted_at'); 199 | }); 200 | ``` 201 | 202 | ### Numeric column type 203 | Unlike standard laravel `decimal` type, this type can be with [variable precision](https://www.postgresql.org/docs/current/datatype-numeric.html) 204 | ```php 205 | Schema::table('some_table', function (Blueprint $table) { 206 | $table->numeric('column_with_variable_precision'); 207 | $table->numeric('column_with_defined_precision', 8); 208 | $table->numeric('column_with_defined_precision_and_scale', 8, 2); 209 | }); 210 | ``` 211 | 212 | ## Custom Extensions 213 | 214 | 1). Create a repository for your extension. 215 | 216 | 2). Add this package as a dependency in composer. 217 | 218 | 3). Inherit the classes you intend to extend from abstract classes with namespace: `namespace Umbrellio\Postgres\Extensions` 219 | 220 | 4). Implement extension methods in closures, example: 221 | 222 | ```php 223 | use Umbrellio\Postgres\Extensions\Schema\AbstractBlueprint; 224 | class SomeBlueprint extends AbstractBlueprint 225 | { 226 | public function someMethod() 227 | { 228 | return function (string $column): Fluent { 229 | return $this->addColumn('someColumn', $column); 230 | }; 231 | } 232 | } 233 | ``` 234 | 235 | 5). Create Extension class and mix these methods using the following syntax, ex: 236 | 237 | ```php 238 | use Umbrellio\Postgres\PostgresConnection; 239 | use Umbrellio\Postgres\Schema\Blueprint; 240 | use Umbrellio\Postgres\Schema\Grammars\PostgresGrammar; 241 | use Umbrellio\Postgres\Extensions\AbstractExtension; 242 | 243 | class SomeExtension extends AbstractExtension 244 | { 245 | public static function getMixins(): array 246 | { 247 | return [ 248 | SomeBlueprint::class => Blueprint::class, 249 | SomeConnection::class => PostgresConnection::class, 250 | SomeSchemaGrammar::class => PostgresGrammar::class, 251 | ... 252 | ]; 253 | } 254 | 255 | public static function getTypes(): string 256 | { 257 | // where SomeType extends Doctrine\DBAL\Types\Type 258 | return [ 259 | 'some' => SomeType::class, 260 | ]; 261 | } 262 | 263 | public static function getName(): string 264 | { 265 | return 'some'; 266 | } 267 | } 268 | ``` 269 | 270 | 6). Register your Extension in ServiceProvider and put in config/app.php, ex: 271 | 272 | ```php 273 | use Illuminate\Support\ServiceProvider; 274 | use Umbrellio\Postgres\PostgresConnection; 275 | 276 | class SomeServiceProvider extends ServiceProvider 277 | { 278 | public function register(): void 279 | { 280 | PostgresConnection::registerExtension(SomeExtension::class); 281 | } 282 | } 283 | ``` 284 | 285 | ## TODO features 286 | 287 | - Extend `CreateCommand` with `inherits` and `partition by` 288 | - Extend working with partitions 289 | - COPY support 290 | - DISTINCT on specific columns 291 | - INSERT ON CONFLICT support 292 | - ... 293 | 294 | ## License 295 | 296 | Released under MIT License. 297 | 298 | ## Authors 299 | 300 | Created by Vitaliy Lazeev & Korben Dallas. 301 | 302 | ## Contributing 303 | 304 | - Fork it ( https://github.com/umbrellio/laravel-pg-extensions ) 305 | - Create your feature branch (`git checkout -b feature/my-new-feature`) 306 | - Commit your changes (`git commit -am 'Add some feature'`) 307 | - Push to the branch (`git push origin feature/my-new-feature`) 308 | - Create new Pull Request 309 | 310 | 311 | Supported by Umbrellio 312 | 313 | -------------------------------------------------------------------------------- /build/logs/clover.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | 656 | 657 | 658 | 659 | 660 | 661 | 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | 674 | 675 | 676 | 677 | 678 | 679 | 680 | 681 | 682 | 683 | 684 | 685 | 686 | 687 | 688 | 689 | 690 | 691 | 692 | 693 | 694 | 695 | 696 | 697 | 698 | 699 | 700 | 701 | 702 | 703 | 704 | 705 | 706 | 707 | 708 | 709 | 710 | 711 | 712 | 713 | 714 | 715 | 716 | 717 | 718 | 719 | 720 | 721 | 722 | 723 | 724 | 725 | 726 | 727 | 728 | 729 | 730 | 731 | 732 | 733 | 734 | 735 | 736 | 737 | 738 | 739 | 740 | 741 | 742 | 743 | 744 | 745 | 746 | 747 | 748 | 749 | 750 | 751 | 752 | 753 | 754 | 755 | 756 | 757 | 758 | 759 | 760 | 761 | 762 | 763 | 764 | 765 | 766 | 767 | 768 | 769 | 770 | 771 | 772 | 773 | 774 | 775 | 776 | 777 | 778 | 779 | 780 | 781 | 782 | 783 | 784 | 785 | 786 | 787 | 788 | 789 | 790 | 791 | 792 | 793 | 794 | 795 | 796 | 797 | 798 | 799 | 800 | 801 | 802 | 803 | 804 | 805 | 806 | 807 | 808 | 809 | 810 | 811 | 812 | 813 | 814 | 815 | 816 | 817 | 818 | 819 | 820 | 821 | 822 | 823 | 824 | 825 | 826 | 827 | 828 | 829 | 830 | 831 | 832 | 833 | 834 | 835 | 836 | 837 | 838 | 839 | 840 | 841 | 842 | 843 | 844 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "umbrellio/laravel-pg-extensions", 3 | "type": "library", 4 | "description": "Extensions for Postgres Laravel", 5 | "minimum-stability": "stable", 6 | "license": "MIT", 7 | "keywords": [ 8 | "laravel", 9 | "php", 10 | "postgres", 11 | "postgresql", 12 | "extension", 13 | "migrations", 14 | "schema", 15 | "builder" 16 | ], 17 | "authors": [ 18 | { 19 | "name": "Vitaliy Lazeev", 20 | "email": "vetal@umbrellio.biz" 21 | }, 22 | { 23 | "name": "Korben Dallas", 24 | "email": "pvsaintpe@umbrellio.biz" 25 | } 26 | ], 27 | "suggest": { 28 | "umbrellio/laravel-ltree": "Package for working with Postgres LTree extension", 29 | "umbrellio/laravel-common-objects": "Package with helpers for common Laravel components" 30 | }, 31 | "support": { 32 | "issues": "https://github.com/umbrellio/laravel-pg-extensions/issues", 33 | "source": "https://github.com/umbrellio/laravel-pg-extensions" 34 | }, 35 | "require": { 36 | "ext-pdo": "*", 37 | "php": "^8.3|^8.4", 38 | "doctrine/dbal": "3.6.*", 39 | "laravel/framework": "^11.0|^12.0" 40 | }, 41 | "require-dev": { 42 | "umbrellio/code-style-php": "^1.2", 43 | "orchestra/testbench": "^9.0|^10.0", 44 | "php-coveralls/php-coveralls": "^2.7", 45 | "codeception/codeception": "^5.0", 46 | "phpunit/phpunit": "^11.0" 47 | }, 48 | "scripts": { 49 | "lint": [ 50 | "ecs check --config=ecs.php . --fix" 51 | ] 52 | }, 53 | "autoload": { 54 | "psr-4": { 55 | "Umbrellio\\Postgres\\": "src/" 56 | } 57 | }, 58 | "autoload-dev": { 59 | "psr-4": { 60 | "Umbrellio\\Postgres\\Tests\\": "tests/" 61 | } 62 | }, 63 | "extra": { 64 | "laravel": { 65 | "providers": [ 66 | "Umbrellio\\Postgres\\UmbrellioPostgresProvider" 67 | ] 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /ecs.php: -------------------------------------------------------------------------------- 1 | import(__DIR__ . '/vendor/umbrellio/code-style-php/umbrellio-cs.php'); 12 | 13 | $services = $containerConfigurator->services(); 14 | 15 | $services->set(PhpUnitTestAnnotationFixer::class) 16 | ->call('configure', [[ 17 | 'style' => 'annotation', 18 | ]]); 19 | 20 | $services->set(DeclareStrictTypesFixer::class); 21 | 22 | $services->set(BinaryOperatorSpacesFixer::class) 23 | ->call('configure', [[ 24 | 'default' => 'single_space', 25 | ]]); 26 | 27 | $parameters = $containerConfigurator->parameters(); 28 | 29 | $parameters->set('cache_directory', '.ecs_cache'); 30 | $parameters->set('skip', [ 31 | 'PhpCsFixer\Fixer\Phpdoc\GeneralPhpdocAnnotationRemoveFixer' => null, 32 | ]); 33 | 34 | $parameters->set('exclude_files', ['vendor/*', 'database/*']); 35 | }; 36 | -------------------------------------------------------------------------------- /ecs.yml: -------------------------------------------------------------------------------- 1 | imports: 2 | - { resource: vendor/umbrellio/code-style-php/umbrellio-cs.yml } 3 | 4 | services: 5 | PhpCsFixer\Fixer\PhpUnit\PhpUnitTestAnnotationFixer: 6 | style: annotation 7 | PhpCsFixer\Fixer\Strict\DeclareStrictTypesFixer: ~ 8 | PhpCsFixer\Fixer\Operator\BinaryOperatorSpacesFixer: 9 | default: single_space 10 | 11 | parameters: 12 | cache_directory: .ecs_cache 13 | exclude_files: 14 | - vendor/* 15 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | ./ 4 | ./vendor/* 5 | ./database/* 6 | ./tests/* 7 | ./.github/* 8 | 9 | 10 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | ./tests 21 | 22 | 23 | 24 | 25 | ./src 26 | 27 | 28 | ./src/.meta.php 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/.meta.php: -------------------------------------------------------------------------------- 1 | wrapTable($blueprint), 20 | $command->get('partition'), 21 | self::compileForValues($command) 22 | ); 23 | } 24 | 25 | private static function compileForValues(Fluent $command): string 26 | { 27 | $range = $command->get('range'); 28 | if ($range) { 29 | $from = self::formatValue($range['from']); 30 | $to = self::formatValue($range['to']); 31 | return "for values from ({$from}) to ({$to})"; 32 | } 33 | 34 | throw new InvalidArgumentException('Not set "for values" for attachPartition'); 35 | } 36 | 37 | private static function formatValue($value) 38 | { 39 | if ($value instanceof Carbon) { 40 | return "'{$value->toDateTimeString()}'"; 41 | } 42 | 43 | if (is_string($value)) { 44 | return "'{$value}'"; 45 | } 46 | 47 | return $value; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Compilers/CheckCompiler.php: -------------------------------------------------------------------------------- 1 | getTable(), 23 | $command->get('index'), 24 | static::removeLeadingBoolean(implode(' ', $wheres)) 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Compilers/CreateCompiler.php: -------------------------------------------------------------------------------- 1 | temporary ? 'create temporary' : 'create', 21 | self::beforeTable($commands['ifNotExists']), 22 | $grammar->wrapTable($blueprint), 23 | $commands['like'] 24 | ? self::compileLike($grammar, $commands['like']) 25 | : self::compileColumns($columns) 26 | ); 27 | 28 | return str_replace(' ', ' ', trim($compiledCommand)); 29 | } 30 | 31 | private static function beforeTable(?Fluent $command = null): string 32 | { 33 | return $command ? 'if not exists' : ''; 34 | } 35 | 36 | /** 37 | * @codeCoverageIgnore 38 | */ 39 | private static function compileLike(Grammar $grammar, Fluent $command): string 40 | { 41 | $table = $command->get('table'); 42 | $includingAll = $command->get('includingAll') ? ' including all' : ''; 43 | return "like {$grammar->wrapTable($table)}{$includingAll}"; 44 | } 45 | 46 | private static function compileColumns(array $columns): string 47 | { 48 | return implode(', ', $columns); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Compilers/ExcludeCompiler.php: -------------------------------------------------------------------------------- 1 | getTable(), $command->get('index')), 21 | static::compileMethod($command), 22 | sprintf('(%s)', static::compileExclude($command)), 23 | static::compileWith($command), 24 | static::compileTablespace($command), 25 | static::compileWheres($grammar, $blueprint, $command), 26 | ])); 27 | } 28 | 29 | private static function compileExclude(Fluent $command): string 30 | { 31 | $items = collect($command->get('using')) 32 | ->map(static function ($operator, $excludeElement) { 33 | return sprintf('%s WITH %s', $excludeElement, $operator); 34 | }); 35 | 36 | return implode(', ', $items->toArray()); 37 | } 38 | 39 | private static function compileWith(Fluent $command): ?string 40 | { 41 | $items = collect($command->get('with')) 42 | ->map(static function ($value, $storageParameter) { 43 | return sprintf('%s = %s', $storageParameter, static::wrapValue($value)); 44 | }); 45 | 46 | if ($items->count() > 0) { 47 | return sprintf('WITH (%s)', implode(', ', $items->toArray())); 48 | } 49 | 50 | return null; 51 | } 52 | 53 | private static function compileTablespace(Fluent $command): ?string 54 | { 55 | if ($command->get('tableSpace')) { 56 | return sprintf('USING INDEX TABLESPACE %s', $command->get('tableSpace')); 57 | } 58 | 59 | return null; 60 | } 61 | 62 | private static function compileMethod(Fluent $command): ?string 63 | { 64 | if ($command->get('method')) { 65 | return sprintf('USING %s', $command->get('method')); 66 | } 67 | 68 | return null; 69 | } 70 | 71 | private static function compileWheres(Grammar $grammar, Blueprint $blueprint, Fluent $command): ?string 72 | { 73 | $wheres = static::build($grammar, $blueprint, $command); 74 | 75 | if (! empty($wheres)) { 76 | return sprintf('WHERE %s', static::removeLeadingBoolean(implode(' ', $wheres))); 77 | } 78 | 79 | return null; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Compilers/Traits/WheresBuilder.php: -------------------------------------------------------------------------------- 1 | wrap($where['column']), 25 | $where['operator'], 26 | static::wrapValue($where['value']), 27 | ]); 28 | } 29 | 30 | protected static function whereColumn(Grammar $grammar, Blueprint $blueprint, array $where): string 31 | { 32 | return implode(' ', [ 33 | $grammar->wrap($where['first']), 34 | $where['operator'], 35 | $grammar->wrap($where['second']), 36 | ]); 37 | } 38 | 39 | protected static function whereIn(Grammar $grammar, Blueprint $blueprint, array $where = []): string 40 | { 41 | if (! empty($where['values'])) { 42 | return implode(' ', [ 43 | $grammar->wrap($where['column']), 44 | 'in', 45 | '(' . implode(',', static::wrapValues($where['values'])) . ')', 46 | ]); 47 | } 48 | return '0 = 1'; 49 | } 50 | 51 | protected static function whereNotIn(Grammar $grammar, Blueprint $blueprint, array $where = []): string 52 | { 53 | if (! empty($where['values'])) { 54 | return implode(' ', [ 55 | $grammar->wrap($where['column']), 56 | 'not in', 57 | '(' . implode(',', static::wrapValues($where['values'])) . ')', 58 | ]); 59 | } 60 | return '1 = 1'; 61 | } 62 | 63 | protected static function whereNull(Grammar $grammar, Blueprint $blueprint, array $where): string 64 | { 65 | return implode(' ', [$grammar->wrap($where['column']), 'is null']); 66 | } 67 | 68 | protected static function whereNotNull(Grammar $grammar, Blueprint $blueprint, array $where): string 69 | { 70 | return implode(' ', [$grammar->wrap($where['column']), 'is not null']); 71 | } 72 | 73 | protected static function whereBetween(Grammar $grammar, Blueprint $blueprint, array $where): string 74 | { 75 | return implode(' ', [ 76 | $grammar->wrap($where['column']), 77 | $where['not'] ? 'not between' : 'between', 78 | static::wrapValue(reset($where['values'])), 79 | 'and', 80 | static::wrapValue(end($where['values'])), 81 | ]); 82 | } 83 | 84 | protected static function wrapValues(array $values = []): array 85 | { 86 | return collect($values)->map(function ($value) { 87 | return static::wrapValue($value); 88 | })->toArray(); 89 | } 90 | 91 | protected static function wrapValue($value) 92 | { 93 | if (is_string($value)) { 94 | return "'{$value}'"; 95 | } 96 | return (int) $value; 97 | } 98 | 99 | protected static function removeLeadingBoolean(string $value): string 100 | { 101 | return preg_replace('/and |or /i', '', $value, 1); 102 | } 103 | 104 | private static function build(Grammar $grammar, Blueprint $blueprint, Fluent $command): array 105 | { 106 | return collect($command->get('wheres')) 107 | ->map(function ($where) use ($grammar, $blueprint) { 108 | return implode(' ', [ 109 | $where['boolean'], 110 | '(' . static::{"where{$where['type']}"}($grammar, $blueprint, $where) . ')', 111 | ]); 112 | }) 113 | ->all(); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/Compilers/UniqueCompiler.php: -------------------------------------------------------------------------------- 1 | get('index'), 28 | $blueprint->getTable(), 29 | implode(',', (array) $fluent->get('columns')), 30 | static::removeLeadingBoolean(implode(' ', $wheres)) 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Connectors/ConnectionFactory.php: -------------------------------------------------------------------------------- 1 | each(static function ($extension, $mixin) { 28 | if (! is_subclass_of($mixin, AbstractComponent::class)) { 29 | throw new MixinInvalidException(sprintf( 30 | 'Mixed class %s is not descendant of %s.', 31 | $mixin, 32 | AbstractComponent::class 33 | )); 34 | } 35 | if (! method_exists($extension, 'mixin')) { 36 | throw new MacroableMissedException(sprintf('Class %s doesn’t use Macroable Trait.', $extension)); 37 | } 38 | /** @var Macroable $extension */ 39 | $extension::mixin(new $mixin()); 40 | }); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Extensions/Connectors/AbstractConnection.php: -------------------------------------------------------------------------------- 1 | getCommentListing($table, $column); 19 | 20 | if ($expected === null) { 21 | $this->assertNull($comment); 22 | } 23 | 24 | $this->assertSame($expected, $comment); 25 | } 26 | 27 | protected function assertDefaultOnColumn(string $table, string $column, ?string $expected = null): void 28 | { 29 | $defaultValue = $this->getDefaultListing($table, $column); 30 | 31 | if ($expected === null) { 32 | $this->assertNull($defaultValue); 33 | } 34 | 35 | $this->assertSame($expected, $defaultValue); 36 | } 37 | 38 | protected function assertLaravelTypeColumn(string $table, string $column, string $expected): void 39 | { 40 | $this->assertSame($expected, Schema::getColumnType($table, $column)); 41 | } 42 | 43 | protected function assertPostgresTypeColumn(string $table, string $column, string $expected): void 44 | { 45 | $this->assertSame($expected, $this->getTypeListing($table, $column)); 46 | } 47 | 48 | private function getCommentListing(string $table, string $column) 49 | { 50 | $definition = DB::selectOne( 51 | ' 52 | SELECT pgd.description 53 | FROM pg_catalog.pg_statio_all_tables AS st 54 | INNER JOIN pg_catalog.pg_description pgd ON (pgd.objoid = st.relid) 55 | INNER JOIN information_schema.columns c ON pgd.objsubid = c.ordinal_position 56 | AND c.table_schema = st.schemaname AND c.table_name = st.relname 57 | WHERE c.table_name = ? AND c.column_name = ? 58 | ', 59 | [$table, $column] 60 | ); 61 | 62 | return $definition ? $definition->description : null; 63 | } 64 | 65 | private function getTypeListing(string $table, string $column): ?string 66 | { 67 | $definition = DB::selectOne( 68 | ' 69 | SELECT data_type 70 | FROM information_schema.columns 71 | WHERE table_name = ? AND column_name = ? 72 | ', 73 | [$table, $column] 74 | ); 75 | 76 | return $definition ? $definition->data_type : null; 77 | } 78 | 79 | private function getDefaultListing(string $table, string $column) 80 | { 81 | $definition = DB::selectOne( 82 | ' 83 | SELECT column_default 84 | FROM information_schema.columns c 85 | WHERE c.table_name = ? and c.column_name = ? 86 | ', 87 | [$table, $column] 88 | ); 89 | 90 | return $definition ? $definition->column_default : null; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Helpers/IndexAssertions.php: -------------------------------------------------------------------------------- 1 | assertNotNull($this->getIndexListing($index)); 33 | } 34 | 35 | protected function notSeeIndex(string $index): void 36 | { 37 | $this->assertNull($this->getIndexListing($index)); 38 | } 39 | 40 | protected function assertSameIndex(string $index, string $expectedDef): void 41 | { 42 | $definition = $this->getIndexListing($index); 43 | 44 | $this->seeIndex($index); 45 | $this->assertSame($expectedDef, $definition); 46 | } 47 | 48 | protected function assertRegExpIndex(string $index, string $expectedDef): void 49 | { 50 | $definition = $this->getIndexListing($index); 51 | 52 | $this->seeIndex($index); 53 | $this->assertMatchesRegularExpression($expectedDef, $definition ?: ''); 54 | } 55 | 56 | protected function dontSeeConstraint(string $table, string $index): void 57 | { 58 | $this->assertFalse($this->existConstraintOnTable($table, $index)); 59 | } 60 | 61 | protected function seeConstraint(string $table, string $index): void 62 | { 63 | $this->assertTrue($this->existConstraintOnTable($table, $index)); 64 | } 65 | 66 | private function getIndexListing($index): ?string 67 | { 68 | $definition = DB::selectOne('SELECT * FROM pg_indexes WHERE indexname = ?', [$index]); 69 | 70 | return $definition ? $definition->indexdef : null; 71 | } 72 | 73 | private function existConstraintOnTable(string $table, string $index): bool 74 | { 75 | $expression = ' 76 | SELECT c.conname 77 | FROM pg_constraint c 78 | LEFT JOIN pg_class t ON c.conrelid = t.oid 79 | LEFT JOIN pg_class t2 ON c.confrelid = t2.oid 80 | WHERE t.relname = ? AND c.conname = ?; 81 | '; 82 | $definition = DB::selectOne($expression, [$table, $index]); 83 | return $definition ? true : false; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Helpers/PostgresTextSanitizer.php: -------------------------------------------------------------------------------- 1 | assertSame($this->getTableDefinition($sourceTable), $this->getTableDefinition($destinationTable)); 21 | } 22 | 23 | protected function assertSameTable(array $expectedDef, string $table): void 24 | { 25 | $definition = $this->getTableDefinition($table); 26 | 27 | $this->assertSame($expectedDef, $definition); 28 | } 29 | 30 | protected function seeTable(string $table): void 31 | { 32 | $this->assertTrue(Schema::hasTable($table)); 33 | } 34 | 35 | private function getTableDefinition(string $table): array 36 | { 37 | return Schema::getColumnListing($table); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Helpers/ViewAssertions.php: -------------------------------------------------------------------------------- 1 | getViewDefinition($view); 23 | 24 | $this->assertSame($expectedDef, $definition); 25 | } 26 | 27 | protected function seeView(string $view): void 28 | { 29 | $this->assertTrue(Schema::hasView($view)); 30 | } 31 | 32 | protected function notSeeView(string $view): void 33 | { 34 | $this->assertFalse(Schema::hasView($view)); 35 | } 36 | 37 | private function getViewDefinition(string $view): string 38 | { 39 | return preg_replace( 40 | "#\s+#", 41 | ' ', 42 | strtolower(trim(str_replace("\n", ' ', Schema::getViewDefinition($view)))) 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/PostgresConnection.php: -------------------------------------------------------------------------------- 1 | TsRangeType::class, 31 | TsTzRangeType::TYPE_NAME => TsTzRangeType::class, 32 | NumericType::TYPE_NAME => NumericType::class, 33 | ]; 34 | 35 | /** 36 | * @param AbstractExtension|string $extension 37 | * @codeCoverageIgnore 38 | */ 39 | final public static function registerExtension(string $extension): void 40 | { 41 | if (! is_subclass_of($extension, AbstractExtension::class)) { 42 | throw new ExtensionInvalidException(sprintf( 43 | 'Class %s must be implemented from %s', 44 | $extension, 45 | AbstractExtension::class 46 | )); 47 | } 48 | self::$extensions[$extension::getName()] = $extension; 49 | } 50 | 51 | public function getSchemaBuilder() 52 | { 53 | if ($this->schemaGrammar === null) { 54 | $this->schemaGrammar = $this->getDefaultSchemaGrammar(); 55 | } 56 | return new Builder($this); 57 | } 58 | 59 | public function useDefaultPostProcessor(): void 60 | { 61 | parent::useDefaultPostProcessor(); 62 | 63 | $this->registerExtensions(); 64 | $this->registerInitialTypes(); 65 | } 66 | 67 | public function bindValues($statement, $bindings) 68 | { 69 | if ($this->getPdo()->getAttribute(PDO::ATTR_EMULATE_PREPARES)) { 70 | foreach ($bindings as $key => $value) { 71 | $parameter = is_string($key) ? $key : $key + 1; 72 | 73 | switch (true) { 74 | case is_bool($value): 75 | $dataType = PDO::PARAM_BOOL; 76 | break; 77 | 78 | case $value === null: 79 | $dataType = PDO::PARAM_NULL; 80 | break; 81 | 82 | default: 83 | $dataType = PDO::PARAM_STR; 84 | } 85 | 86 | $statement->bindValue($parameter, $value, $dataType); 87 | } 88 | } else { 89 | parent::bindValues($statement, $bindings); 90 | } 91 | } 92 | 93 | public function prepareBindings(array $bindings) 94 | { 95 | if ($this->getPdo()->getAttribute(PDO::ATTR_EMULATE_PREPARES)) { 96 | $grammar = $this->getQueryGrammar(); 97 | 98 | foreach ($bindings as $key => $value) { 99 | if ($value instanceof DateTimeInterface) { 100 | $bindings[$key] = $value->format($grammar->getDateFormat()); 101 | } 102 | if (is_string($value)) { 103 | $bindings[$key] = PostgresTextSanitizer::sanitize($value); 104 | } 105 | } 106 | 107 | return $bindings; 108 | } 109 | 110 | return parent::prepareBindings($bindings); 111 | } 112 | 113 | protected function getDefaultSchemaGrammar() 114 | { 115 | return new PostgresGrammar($this); 116 | } 117 | 118 | private function registerInitialTypes(): void 119 | { 120 | foreach ($this->initialTypes as $type => $typeClass) { 121 | if (! Type::hasType($type)) { 122 | Type::addType($type, $typeClass); 123 | } 124 | } 125 | } 126 | 127 | /** 128 | * @codeCoverageIgnore 129 | */ 130 | private function registerExtensions(): void 131 | { 132 | collect(self::$extensions)->each(function ($extension) { 133 | /** @var AbstractExtension $extension */ 134 | $extension::register(); 135 | foreach ($extension::getTypes() as $type => $typeClass) { 136 | if (! Type::hasType($type)) { 137 | Type::addType($type, $typeClass); 138 | } 139 | } 140 | }); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/Schema/Blueprint.php: -------------------------------------------------------------------------------- 1 | addCommand('attachPartition', compact('partition')); 35 | } 36 | 37 | public function detachPartition(string $partition): void 38 | { 39 | $this->addCommand('detachPartition', compact('partition')); 40 | } 41 | 42 | /** 43 | * @codeCoverageIgnore 44 | * @return LikeDefinition|Fluent 45 | */ 46 | public function like(string $table): Fluent 47 | { 48 | return $this->addCommand('like', compact('table')); 49 | } 50 | 51 | /** 52 | * @codeCoverageIgnore 53 | */ 54 | public function ifNotExists(): Fluent 55 | { 56 | return $this->addCommand('ifNotExists'); 57 | } 58 | 59 | /** 60 | * @param array|string $columns 61 | * @return UniqueDefinition|UniqueBuilder 62 | */ 63 | public function uniquePartial($columns, ?string $index = null, ?string $algorithm = null): Fluent 64 | { 65 | $columns = (array) $columns; 66 | 67 | $index = $index ?: $this->createIndexName('unique', $columns); 68 | 69 | return $this->addExtendedCommand( 70 | UniqueBuilder::class, 71 | 'uniquePartial', 72 | compact('columns', 'index', 'algorithm') 73 | ); 74 | } 75 | 76 | public function dropUniquePartial($index): Fluent 77 | { 78 | return $this->dropIndexCommand('dropIndex', 'unique', $index); 79 | } 80 | 81 | /** 82 | * @param array|string $columns 83 | * @return ExcludeDefinition|ExcludeBuilder 84 | */ 85 | public function exclude($columns, ?string $index = null): Fluent 86 | { 87 | $columns = (array) $columns; 88 | 89 | $index = $index ?: $this->createIndexName('excl', $columns); 90 | 91 | return $this->addExtendedCommand(ExcludeBuilder::class, 'exclude', compact('columns', 'index')); 92 | } 93 | 94 | /** 95 | * @param array|string $columns 96 | * @return CheckDefinition|CheckBuilder 97 | */ 98 | public function check($columns, ?string $index = null): Fluent 99 | { 100 | $columns = (array) $columns; 101 | 102 | $index = $index ?: $this->createIndexName('chk', $columns); 103 | 104 | return $this->addExtendedCommand(CheckBuilder::class, 'check', compact('columns', 'index')); 105 | } 106 | 107 | public function dropExclude($index): Fluent 108 | { 109 | return $this->dropIndexCommand('dropUnique', 'excl', $index); 110 | } 111 | 112 | public function dropCheck($index): Fluent 113 | { 114 | return $this->dropIndexCommand('dropUnique', 'chk', $index); 115 | } 116 | 117 | /** 118 | * @codeCoverageIgnore 119 | */ 120 | public function hasIndex($index, bool $unique = false): bool 121 | { 122 | if (is_array($index)) { 123 | $index = $this->createIndexName($unique === false ? 'index' : 'unique', $index); 124 | } 125 | 126 | return array_key_exists($index, $this->getSchemaManager()->listTableIndexes($this->getTable())); 127 | } 128 | 129 | /** 130 | * @codeCoverageIgnore 131 | * @return ViewDefinition|Fluent 132 | */ 133 | public function createView(string $view, string $select, bool $materialize = false): Fluent 134 | { 135 | return $this->addCommand('createView', compact('view', 'select', 'materialize')); 136 | } 137 | 138 | /** 139 | * @codeCoverageIgnore 140 | */ 141 | public function dropView(string $view): Fluent 142 | { 143 | return $this->addCommand('dropView', compact('view')); 144 | } 145 | 146 | /** 147 | * Almost like 'decimal' type, but can be with variable precision (by default) 148 | * 149 | * @return Fluent|ColumnDefinition 150 | */ 151 | public function numeric(string $column, ?int $precision = null, ?int $scale = null): Fluent 152 | { 153 | return $this->addColumn('numeric', $column, compact('precision', 'scale')); 154 | } 155 | 156 | /** 157 | * @return Fluent|ColumnDefinition 158 | */ 159 | public function tsrange(string $column): Fluent 160 | { 161 | return $this->addColumn(TsRangeType::TYPE_NAME, $column); 162 | } 163 | 164 | /** 165 | * @return Fluent|ColumnDefinition 166 | */ 167 | public function tstzrange(string $column): Fluent 168 | { 169 | return $this->addColumn(TsTzRangeType::TYPE_NAME, $column); 170 | } 171 | 172 | /** 173 | * @return Fluent|ColumnDefinition 174 | */ 175 | public function daterange(string $column): Fluent 176 | { 177 | return $this->addColumn(DateRangeType::TYPE_NAME, $column); 178 | } 179 | 180 | /** 181 | * @codeCoverageIgnore 182 | */ 183 | protected function getSchemaManager() 184 | { 185 | /** @scrutinizer ignore-call */ 186 | $connection = Schema::getConnection(); 187 | $doctrineConnection = DriverManager::getConnection($connection->getConfig()); 188 | return $doctrineConnection->getSchemaManager(); 189 | } 190 | 191 | private function addExtendedCommand(string $fluent, string $name, array $parameters = []): Fluent 192 | { 193 | $command = new $fluent(array_merge(compact('name'), $parameters)); 194 | $this->commands[] = $command; 195 | return $command; 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /src/Schema/Builder.php: -------------------------------------------------------------------------------- 1 | createBlueprint($view); 23 | $blueprint->createView($view, $select, $materialize); 24 | $this->build($blueprint); 25 | } 26 | 27 | /** 28 | * @codeCoverageIgnore 29 | */ 30 | public function dropView(string $view): void 31 | { 32 | $blueprint = $this->createBlueprint($view); 33 | $blueprint->dropView($view); 34 | $this->build($blueprint); 35 | } 36 | 37 | /** 38 | * @codeCoverageIgnore 39 | */ 40 | public function hasView($view): bool 41 | { 42 | return count($this->connection->selectFromWriteConnection($this->grammar->compileViewExists(), [ 43 | $this->connection->getConfig()['schema'], 44 | $this->connection->getTablePrefix() . $view, 45 | ])) > 0; 46 | } 47 | 48 | /** 49 | * @codeCoverageIgnore 50 | */ 51 | public function getForeignKeys($tableName): array 52 | { 53 | return $this->connection->selectFromWriteConnection($this->grammar->compileForeignKeysListing($tableName)); 54 | } 55 | 56 | /** 57 | * @codeCoverageIgnore 58 | */ 59 | public function getViewDefinition($view): string 60 | { 61 | $results = $this->connection->selectFromWriteConnection($this->grammar->compileViewDefinition(), [ 62 | $this->connection->getConfig()['schema'], 63 | $this->connection->getTablePrefix() . $view, 64 | ]); 65 | return count($results) > 0 ? $results[0]->view_definition : ''; 66 | } 67 | 68 | /** 69 | * @param string $table 70 | * @return Blueprint|\Illuminate\Database\Schema\Blueprint 71 | */ 72 | protected function createBlueprint($table, Closure $callback = null) 73 | { 74 | return new Blueprint($this->connection, $table, $callback); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Schema/Builders/Constraints/Check/CheckBuilder.php: -------------------------------------------------------------------------------- 1 | attributes['method'] = $method; 17 | return $this; 18 | } 19 | 20 | public function with(string $storageParameter, $value): self 21 | { 22 | $this->attributes['with'][$storageParameter] = $value; 23 | return $this; 24 | } 25 | 26 | public function tableSpace(string $tableSpace): self 27 | { 28 | $this->attributes['tableSpace'] = $tableSpace; 29 | return $this; 30 | } 31 | 32 | public function using(string $excludeElement, string $operator): self 33 | { 34 | $this->attributes['using'][$excludeElement] = $operator; 35 | return $this; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Schema/Builders/Indexes/Unique/UniqueBuilder.php: -------------------------------------------------------------------------------- 1 | attributes['constraints'] = call_user_func_array([$command, $method], $parameters); 15 | return $command; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Schema/Builders/Indexes/Unique/UniquePartialBuilder.php: -------------------------------------------------------------------------------- 1 | compileWhere('Raw', $boolean, compact('sql', 'bindings')); 19 | } 20 | 21 | public function where(string $column, string $operator, string $value, string $boolean = 'and'): self 22 | { 23 | return $this->compileWhere('Basic', $boolean, compact('column', 'operator', 'value')); 24 | } 25 | 26 | public function whereColumn(string $first, string $operator, string $second, string $boolean = 'and'): self 27 | { 28 | return $this->compileWhere('Column', $boolean, compact('first', 'operator', 'second')); 29 | } 30 | 31 | public function whereIn(string $column, array $values, string $boolean = 'and', bool $not = false): self 32 | { 33 | return $this->compileWhere($not ? 'NotIn' : 'In', $boolean, compact('column', 'values')); 34 | } 35 | 36 | public function whereNotIn(string $column, array $values = [], string $boolean = 'and'): self 37 | { 38 | return $this->whereIn($column, $values, $boolean, true); 39 | } 40 | 41 | public function whereNull(string $column, string $boolean = 'and', bool $not = false): self 42 | { 43 | return $this->compileWhere($not ? 'NotNull' : 'Null', $boolean, compact('column')); 44 | } 45 | 46 | public function whereBetween(string $column, array $values = [], string $boolean = 'and', bool $not = false): self 47 | { 48 | return $this->compileWhere('Between', $boolean, compact('column', 'values', 'not')); 49 | } 50 | 51 | public function whereNotBetween(string $column, array $values = [], string $boolean = 'and'): self 52 | { 53 | return $this->whereBetween($column, $values, $boolean, true); 54 | } 55 | 56 | public function whereNotNull(string $column, string $boolean = 'and'): self 57 | { 58 | return $this->whereNull($column, $boolean, true); 59 | } 60 | 61 | protected function compileWhere(string $type, string $boolean, array $parameters = []): self 62 | { 63 | $this->attributes['wheres'][] = array_merge(compact('type', 'boolean'), $parameters); 64 | return $this; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Schema/Definitions/AttachPartitionDefinition.php: -------------------------------------------------------------------------------- 1 | getCommandByName($blueprint, 'like'); 29 | $ifNotExists = $this->getCommandByName($blueprint, 'ifNotExists'); 30 | 31 | return CreateCompiler::compile( 32 | $this, 33 | $blueprint, 34 | $this->getColumns($blueprint), 35 | compact('like', 'ifNotExists') 36 | ); 37 | } 38 | 39 | /** 40 | * @codeCoverageIgnore 41 | */ 42 | public function compileAttachPartition(Blueprint $blueprint, Fluent $command): string 43 | { 44 | return AttachPartitionCompiler::compile($this, $blueprint, $command); 45 | } 46 | 47 | /** 48 | * @codeCoverageIgnore 49 | */ 50 | public function compileDetachPartition(Blueprint $blueprint, Fluent $command): string 51 | { 52 | return sprintf( 53 | 'alter table %s detach partition %s', 54 | $this->wrapTable($blueprint), 55 | $command->get('partition') 56 | ); 57 | } 58 | 59 | /** 60 | * @codeCoverageIgnore 61 | */ 62 | public function compileCreateView(Blueprint $blueprint, Fluent $command): string 63 | { 64 | $materialize = $command->get('materialize') ? 'materialized' : ''; 65 | return implode(' ', array_filter([ 66 | 'create', 67 | $materialize, 68 | 'view', 69 | $this->wrapTable($command->get('view')), 70 | 'as', 71 | $command->get('select'), 72 | ])); 73 | } 74 | 75 | /** 76 | * @codeCoverageIgnore 77 | */ 78 | public function compileDropView(Blueprint $blueprint, Fluent $command): string 79 | { 80 | return 'drop view ' . $this->wrapTable($command->get('view')); 81 | } 82 | 83 | /** 84 | * @codeCoverageIgnore 85 | */ 86 | public function compileViewExists(): string 87 | { 88 | return 'select * from information_schema.views where table_schema = ? and table_name = ?'; 89 | } 90 | 91 | /** 92 | * @codeCoverageIgnore 93 | */ 94 | public function compileForeignKeysListing(string $tableName): string 95 | { 96 | return sprintf(" 97 | SELECT 98 | kcu.column_name as source_column_name, 99 | ccu.table_name AS target_table_name, 100 | ccu.column_name AS target_column_name 101 | FROM 102 | information_schema.table_constraints AS tc 103 | JOIN information_schema.key_column_usage AS kcu 104 | ON tc.constraint_name = kcu.constraint_name 105 | AND tc.table_schema = kcu.table_schema 106 | JOIN information_schema.constraint_column_usage AS ccu 107 | ON ccu.constraint_name = tc.constraint_name 108 | AND ccu.table_schema = tc.table_schema 109 | WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_name='%s'; 110 | ", $tableName); 111 | } 112 | 113 | /** 114 | * @codeCoverageIgnore 115 | */ 116 | public function compileViewDefinition(): string 117 | { 118 | return 'select view_definition from information_schema.views where table_schema = ? and table_name = ?'; 119 | } 120 | 121 | public function compileUniquePartial(Blueprint $blueprint, UniqueBuilder $command): string 122 | { 123 | $constraints = $command->get('constraints'); 124 | if ($constraints instanceof UniquePartialBuilder) { 125 | return UniqueCompiler::compile($this, $blueprint, $command, $constraints); 126 | } 127 | return $this->compileUnique($blueprint, $command); 128 | } 129 | 130 | public function compileExclude(Blueprint $blueprint, ExcludeBuilder $command): string 131 | { 132 | return ExcludeCompiler::compile($this, $blueprint, $command); 133 | } 134 | 135 | public function compileCheck(Blueprint $blueprint, CheckBuilder $command): string 136 | { 137 | return CheckCompiler::compile($this, $blueprint, $command); 138 | } 139 | 140 | protected function typeNumeric(Fluent $column): string 141 | { 142 | $type = NumericType::TYPE_NAME; 143 | $precision = $column->get('precision'); 144 | $scale = $column->get('scale'); 145 | 146 | if ($precision && $scale) { 147 | return "{$type}({$precision}, {$scale})"; 148 | } 149 | 150 | if ($precision) { 151 | return "{$type}({$precision})"; 152 | } 153 | 154 | return $type; 155 | } 156 | 157 | protected function typeTsrange(Fluent $column): string 158 | { 159 | return TsRangeType::TYPE_NAME; 160 | } 161 | 162 | protected function typeTstzrange(Fluent $column): string 163 | { 164 | return TsTzRangeType::TYPE_NAME; 165 | } 166 | 167 | protected function typeDaterange(Fluent $column): string 168 | { 169 | return DateRangeType::TYPE_NAME; 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/Schema/Types/DateRangeType.php: -------------------------------------------------------------------------------- 1 | app->singleton('db.factory', function ($app) { 20 | return new ConnectionFactory($app); 21 | }); 22 | 23 | $this->app->singleton('db', function ($app) { 24 | return new DatabaseManager($app, $app['db.factory']); 25 | }); 26 | 27 | $this->app->bind('db.connection', function ($app) { 28 | return $app['db']->connection(); 29 | }); 30 | 31 | $this->app->bind('db.schema', function ($app) { 32 | return $app['db']->connection()->getSchemaBuilder(); 33 | }); 34 | 35 | $this->app->singleton('db.transactions', function ($app) { 36 | return new DatabaseTransactionsManager(); 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | psql postgres -U postgres -tc "SELECT 1 FROM pg_database WHERE datname = 'testing'" | grep -q 1 || psql postgres -U postgres -c "CREATE DATABASE testing" 4 | sed -e "s/\${USERNAME}/postgres/" \ 5 | -e "s/\${PASSWORD}//" \ 6 | -e "s/\${DATABASE}/testing/" \ 7 | -e "s/\${HOST}/127.0.0.1/" \ 8 | phpunit.xml.dist > phpunit.xml 9 | COMPOSER_MEMORY_LIMIT=-1 composer update 10 | composer lint 11 | php vendor/bin/phpunit -c phpunit.xml --migrate-configuration 12 | if [ "x$EXCLUDE_GROUP" != "x" ]; then 13 | php -d pcov.directory='.' vendor/bin/phpunit \ 14 | --exclude-group $EXCLUDE_GROUP \ 15 | --coverage-html build \ 16 | --coverage-text 17 | else 18 | php -d pcov.directory='.' vendor/bin/phpunit \ 19 | --exclude-group WithoutSchema,forPHP7 \ 20 | --coverage-html build \ 21 | --coverage-text 22 | fi 23 | -------------------------------------------------------------------------------- /tests/Functional/Connection/ConnectionTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(SQLiteConnection::class, $factory->make(config('database.connections.sqlite'))); 36 | } 37 | 38 | #[Test] 39 | public function resolverFor(): void 40 | { 41 | Connection::resolverFor('sqlite', function ($connection, $database, $prefix, $config) { 42 | return new CustomSQLiteConnection($connection, $database, $prefix, $config); 43 | }); 44 | 45 | $factory = new ConnectionFactory(app()); 46 | 47 | $this->assertInstanceOf( 48 | CustomSQLiteConnection::class, 49 | $factory->make(config('database.connections.sqlite')) 50 | ); 51 | } 52 | 53 | #[Test] 54 | #[DataProvider('boolDataProvider')] 55 | public function boolTrueBindingsWorks($value) 56 | { 57 | $table = 'test_table'; 58 | $data = [ 59 | 'field' => $value, 60 | ]; 61 | Schema::create($table, function (Blueprint $table) { 62 | $table->increments('id'); 63 | $table->boolean('field'); 64 | }); 65 | DB::table($table)->insert($data); 66 | $result = DB::table($table)->select($data); 67 | $this->assertSame(1, $result->count()); 68 | } 69 | 70 | #[Test] 71 | #[DataProvider('intDataProvider')] 72 | public function intBindingsWorks($value) 73 | { 74 | $table = 'test_table'; 75 | $data = [ 76 | 'field' => $value, 77 | ]; 78 | Schema::create($table, function (Blueprint $table) { 79 | $table->increments('id'); 80 | $table->integer('field'); 81 | }); 82 | DB::table($table)->insert($data); 83 | $result = DB::table($table)->select($data); 84 | $this->assertSame(1, $result->count()); 85 | } 86 | 87 | #[Test] 88 | public function stringBindingsWorks() 89 | { 90 | $table = 'test_table'; 91 | $data = [ 92 | 'field' => 'string', 93 | ]; 94 | Schema::create($table, function (Blueprint $table) { 95 | $table->increments('id'); 96 | $table->string('field'); 97 | }); 98 | DB::table($table)->insert($data); 99 | $result = DB::table($table)->select($data); 100 | $this->assertSame(1, $result->count()); 101 | } 102 | 103 | #[Test] 104 | public function nullBindingsWorks() 105 | { 106 | $table = 'test_table'; 107 | $data = [ 108 | 'field' => null, 109 | ]; 110 | Schema::create($table, function (Blueprint $table) { 111 | $table->increments('id'); 112 | $table->string('field') 113 | ->nullable(); 114 | }); 115 | DB::table($table)->insert($data); 116 | $result = DB::table($table)->whereNull('field')->get(); 117 | $this->assertSame(1, $result->count()); 118 | } 119 | 120 | #[Test] 121 | #[DataProvider('dateDataProvider')] 122 | public function dateTimeBindingsWorks($value) 123 | { 124 | $table = 'test_table'; 125 | $data = [ 126 | 'field' => $value, 127 | ]; 128 | Schema::create($table, function (Blueprint $table) { 129 | $table->increments('id'); 130 | $table->dateTime('field'); 131 | }); 132 | DB::table($table)->insert($data); 133 | $result = DB::table($table)->select($data); 134 | $this->assertSame(1, $result->count()); 135 | } 136 | 137 | public static function boolDataProvider(): Generator 138 | { 139 | yield 'true' => [true]; 140 | yield 'false' => [false]; 141 | } 142 | 143 | public static function intDataProvider(): Generator 144 | { 145 | yield 'zero' => [0]; 146 | yield 'non-zero' => [10]; 147 | } 148 | 149 | public static function dateDataProvider(): Generator 150 | { 151 | yield 'as string' => ['2019-01-01 13:12:22']; 152 | yield 'as Carbon object' => [new Carbon('2019-01-01 13:12:22')]; 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /tests/Functional/Schema/CreateIndexTest.php: -------------------------------------------------------------------------------- 1 | increments('id'); 37 | $table->string('name'); 38 | $table->string('code'); 39 | $table->integer('phone'); 40 | $table->boolean('enabled'); 41 | $table->integer('icq'); 42 | $table->softDeletes(); 43 | 44 | $callback($table); 45 | }); 46 | 47 | $this->seeTable('test_table'); 48 | $this->assertRegExpIndex('test_table_name_unique', '/' . $this->getDummyIndex() . $expected . '/'); 49 | 50 | Schema::table('test_table', function (Blueprint $table) { 51 | if (! $this->existConstraintOnTable($table->getTable(), 'test_table_name_unique')) { 52 | $table->dropUniquePartial(['name']); 53 | } else { 54 | $table->dropUnique(['name']); 55 | } 56 | }); 57 | 58 | $this->notSeeIndex('test_table_name_unique'); 59 | } 60 | 61 | #[Test] 62 | public function createSpecifyIndex(): void 63 | { 64 | Schema::create('test_table', function (Blueprint $table) { 65 | $table->string('name') 66 | ->index('specify_index_name'); 67 | }); 68 | 69 | $this->seeTable('test_table'); 70 | 71 | $this->assertRegExpIndex( 72 | 'specify_index_name', 73 | '/CREATE INDEX specify_index_name ON (public.)?test_table USING btree \(name\)/' 74 | ); 75 | } 76 | 77 | public static function provideIndexes(): Generator 78 | { 79 | yield ['', function (Blueprint $table) { 80 | $table->uniquePartial('name'); 81 | }]; 82 | yield [ 83 | ' WHERE \(deleted_at IS NULL\)', 84 | function (Blueprint $table) { 85 | $table->uniquePartial('name') 86 | ->whereNull('deleted_at'); 87 | }, 88 | ]; 89 | yield [ 90 | ' WHERE \(deleted_at IS NOT NULL\)', 91 | function (Blueprint $table) { 92 | $table->uniquePartial('name') 93 | ->whereNotNull('deleted_at'); 94 | }, 95 | ]; 96 | yield [ 97 | ' WHERE \(phone = 1234\)', 98 | function (Blueprint $table) { 99 | $table->uniquePartial('name') 100 | ->where('phone', '=', 1234); 101 | }, 102 | ]; 103 | yield [ 104 | " WHERE \(\(code\)::text = 'test'::text\)", 105 | function (Blueprint $table) { 106 | $table->uniquePartial('name') 107 | ->where('code', '=', 'test'); 108 | }, 109 | ]; 110 | yield [ 111 | ' WHERE \(\(phone >= 1\) AND \(phone <= 2\)\)', 112 | function (Blueprint $table) { 113 | $table->uniquePartial('name') 114 | ->whereBetween('phone', [1, 2]); 115 | }, 116 | ]; 117 | yield [ 118 | ' WHERE \(\(phone < 1\) OR \(phone > 2\)\)', 119 | function (Blueprint $table) { 120 | $table->uniquePartial('name') 121 | ->whereNotBetween('phone', [1, 2]); 122 | }, 123 | ]; 124 | yield [ 125 | ' WHERE \(phone <> icq\)', 126 | function (Blueprint $table) { 127 | $table->uniquePartial('name') 128 | ->whereColumn('phone', '<>', 'icq'); 129 | }, 130 | ]; 131 | yield [ 132 | ' WHERE \(\(phone = 1\) AND \(icq < 2\)\)', 133 | function (Blueprint $table) { 134 | $table->uniquePartial('name') 135 | ->whereRaw('phone = ? and icq < ?', [1, 2]); 136 | }, 137 | ]; 138 | yield [ 139 | ' WHERE \(phone = ANY \(ARRAY\[1, 2, 4\]\)\)', 140 | function (Blueprint $table) { 141 | $table->uniquePartial('name') 142 | ->whereIn('phone', [1, 2, 4]); 143 | }, 144 | ]; 145 | yield [ 146 | ' WHERE \(0 = 1\)', 147 | function (Blueprint $table) { 148 | $table->uniquePartial('name') 149 | ->whereIn('phone', []); 150 | }, 151 | ]; 152 | yield [ 153 | ' WHERE \(phone <> ALL \(ARRAY\[1, 2, 4\]\)\)', 154 | function (Blueprint $table) { 155 | $table->uniquePartial('name') 156 | ->whereNotIn('phone', [1, 2, 4]); 157 | }, 158 | ]; 159 | yield [ 160 | ' WHERE \(1 = 1\)', 161 | function (Blueprint $table) { 162 | $table->uniquePartial('name') 163 | ->whereNotIn('phone', []); 164 | }, 165 | ]; 166 | } 167 | 168 | #[Test] 169 | public function addExcludeConstraints(): void 170 | { 171 | DB::statement('CREATE EXTENSION IF NOT EXISTS btree_gist'); 172 | 173 | Schema::create('test_table', function (Blueprint $table) { 174 | $table->increments('id'); 175 | $table->string('code') 176 | ->unique(); 177 | $table->integer('period_type_id'); 178 | $table->date('period_start'); 179 | $table->date('period_end'); 180 | $table->softDeletes(); 181 | 182 | $table 183 | ->exclude(['period_start', 'period_end']) 184 | ->using('period_type_id', '=') 185 | ->using('daterange(period_start, period_end)', '&&') 186 | ->method('gist') 187 | ->whereNull('deleted_at'); 188 | }); 189 | 190 | $this->seeConstraint('test_table', 'test_table_period_start_period_end_excl'); 191 | 192 | Schema::table('test_table', function (Blueprint $table) { 193 | $table->dropExclude(['period_start', 'period_end']); 194 | }); 195 | 196 | $this->dontSeeConstraint('test_table', 'test_table_period_start_period_end_excl'); 197 | } 198 | 199 | #[Test] 200 | public function addCheckConstraints(): void 201 | { 202 | Schema::create('test_table', function (Blueprint $table) { 203 | $table->increments('id'); 204 | $table->integer('period_type_id'); 205 | $table->date('period_start'); 206 | $table->date('period_end'); 207 | $table->softDeletes(); 208 | 209 | $table 210 | ->check(['period_start', 'period_end']) 211 | ->whereColumn('period_end', '>', 'period_start') 212 | ->whereIn('period_type_id', [1, 2, 3]); 213 | }); 214 | 215 | foreach ($this->provideSuccessData() as [$period_type_id, $period_start, $period_end]) { 216 | $data = compact('period_type_id', 'period_start', 'period_end'); 217 | DB::table('test_table')->insert($data); 218 | $this->assertDatabaseHas('test_table', $data); 219 | } 220 | 221 | foreach ($this->provideWrongData() as [$period_type_id, $period_start, $period_end]) { 222 | $data = compact('period_type_id', 'period_start', 'period_end'); 223 | $this->expectException(QueryException::class); 224 | DB::table('test_table')->insert($data); 225 | } 226 | } 227 | 228 | #[Test] 229 | public function dropCheckConstraints(): void 230 | { 231 | Schema::create('test_table', function (Blueprint $table) { 232 | $table->increments('id'); 233 | $table->integer('period_type_id'); 234 | $table 235 | ->check(['period_type_id']) 236 | ->whereNotNull('period_type_id'); 237 | }); 238 | 239 | $this->seeConstraint('test_table', 'test_table_period_type_id_chk'); 240 | 241 | Schema::table('test_table', function (Blueprint $table) { 242 | $table->dropCheck(['period_type_id']); 243 | }); 244 | 245 | $this->dontSeeConstraint('test_table', 'test_table_period_type_id_chk'); 246 | } 247 | 248 | protected function getDummyIndex(): string 249 | { 250 | return 'CREATE UNIQUE INDEX test_table_name_unique ON (public.)?test_table USING btree \(name\)'; 251 | } 252 | 253 | private function createIndexDefinition(): void 254 | { 255 | Schema::create('test_table', function (Blueprint $table) { 256 | $table->increments('id'); 257 | $table->string('name'); 258 | 259 | if (! $table->hasIndex(['name'], true)) { 260 | $table->unique(['name']); 261 | } 262 | }); 263 | 264 | $this->seeTable('test_table'); 265 | 266 | Schema::table('test_table', function (Blueprint $table) { 267 | if (! $table->hasIndex(['name'], true)) { 268 | $table->unique(['name']); 269 | } 270 | }); 271 | 272 | $this->seeIndex('test_table_name_unique'); 273 | } 274 | 275 | private static function provideSuccessData(): Generator 276 | { 277 | yield [1, '2019-01-01', '2019-01-31']; 278 | yield [2, '2019-02-15', '2019-04-20']; 279 | yield [3, '2019-03-07', '2019-06-24']; 280 | } 281 | 282 | private static function provideWrongData(): Generator 283 | { 284 | yield [4, '2019-01-01', '2019-01-31']; 285 | yield [1, '2019-07-15', '2019-04-20']; 286 | yield [2, '2019-12-07', '2019-06-24']; 287 | } 288 | } 289 | -------------------------------------------------------------------------------- /tests/Functional/Schema/CreateTableTest.php: -------------------------------------------------------------------------------- 1 | increments('id'); 28 | $table->string('name'); 29 | $table->string('field_comment') 30 | ->comment('test'); 31 | $table->integer('field_default') 32 | ->default(123); 33 | }); 34 | 35 | $this->seeTable('test_table'); 36 | } 37 | 38 | #[Test] 39 | public function columnAssertions(): void 40 | { 41 | Schema::create('test_table', function (Blueprint $table) { 42 | $table->increments('id'); 43 | $table->string('name'); 44 | $table->string('field_comment') 45 | ->comment('test'); 46 | $table->integer('field_default') 47 | ->default(123); 48 | }); 49 | 50 | $this->assertSameTable(['id', 'name', 'field_comment', 'field_default'], 'test_table'); 51 | 52 | $this->assertPostgresTypeColumn('test_table', 'id', 'integer'); 53 | $this->assertLaravelTypeColumn('test_table', 'name', 'varchar'); 54 | $this->assertPostgresTypeColumn('test_table', 'name', 'character varying'); 55 | 56 | $this->assertDefaultOnColumn('test_table', 'field_default', '123'); 57 | $this->assertCommentOnColumn('test_table', 'field_comment', 'test'); 58 | 59 | $this->assertDefaultOnColumn('test_table', 'name'); 60 | $this->assertCommentOnColumn('test_table', 'name'); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tests/FunctionalTestCase.php: -------------------------------------------------------------------------------- 1 | getConnectionParams(); 26 | 27 | $app['config']->set('database.default', 'main'); 28 | $app['config']->set('database.connections.main', [ 29 | 'driver' => 'pgsql', 30 | 'host' => $params['host'], 31 | 'port' => (int) $params['port'], 32 | 'database' => $params['database'], 33 | 'username' => $params['user'], 34 | 'password' => $params['password'], 35 | 'charset' => 'utf8', 36 | 'prefix' => '', 37 | 'schema' => 'public', 38 | ]); 39 | 40 | $app['config']->set('database.connections.sqlite', [ 41 | 'driver' => 'sqlite', 42 | 'host' => '127.0.0.1', 43 | 'port' => '3306', 44 | 'database' => __DIR__ . '/_data/database.sqlite', 45 | ]); 46 | 47 | if ($this->emulatePrepares) { 48 | $app['config']->set('database.connections.main.options', [ 49 | PDO::ATTR_EMULATE_PREPARES => true, 50 | ]); 51 | } 52 | } 53 | 54 | private function getConnectionParams(): array 55 | { 56 | return [ 57 | 'driver' => $GLOBALS['db_type'] ?? 'pdo_pgsql', 58 | 'user' => $GLOBALS['db_username'], 59 | 'password' => $GLOBALS['db_password'], 60 | 'host' => $GLOBALS['db_host'], 61 | 'database' => $GLOBALS['db_database'], 62 | 'port' => $GLOBALS['db_port'], 63 | ]; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | expectException(MixinInvalidException::class); 25 | 26 | /** @var AbstractExtension $abstractExtension */ 27 | $abstractExtension::register(); 28 | } 29 | 30 | #[Test] 31 | public function registerWithInvalidMixin(): void 32 | { 33 | $abstractExtension = new InvalidExtensionStub(); 34 | 35 | $this->expectException(MacroableMissedException::class); 36 | 37 | /** @var AbstractExtension $abstractExtension */ 38 | $abstractExtension::register(); 39 | } 40 | } 41 | 42 | class InvalidExtensionStub extends AbstractExtension 43 | { 44 | public static function getName(): string 45 | { 46 | return 'extension'; 47 | } 48 | 49 | public static function getMixins(): array 50 | { 51 | return [ 52 | ComponentStub::class => ServiceProvider::class, 53 | ]; 54 | } 55 | } 56 | 57 | class ComponentStub extends AbstractComponent 58 | { 59 | } 60 | 61 | class ExtensionStub extends AbstractExtension 62 | { 63 | public static function getName(): string 64 | { 65 | return 'extension'; 66 | } 67 | 68 | public static function getMixins(): array 69 | { 70 | return [ 71 | \Umbrellio\Postgres\Tests\Unit\Extensions\InvalidComponentStub::class => Blueprint::class, 72 | ]; 73 | } 74 | } 75 | 76 | class InvalidComponentStub extends Model 77 | { 78 | } 79 | -------------------------------------------------------------------------------- /tests/Unit/Helpers/BlueprintAssertions.php: -------------------------------------------------------------------------------- 1 | postgresConnection = $connection->makePartial(); 32 | $this->postgresGrammar = new PostgresGrammar($this->postgresConnection); 33 | $this->postgresConnection->setSchemaGrammar($this->postgresGrammar); 34 | $this->blueprint = new Blueprint($this->postgresConnection, $table); 35 | } 36 | 37 | /** 38 | * @param string|array $sql 39 | */ 40 | protected function assertSameSql($sql): void 41 | { 42 | $this->assertSame((array) $sql, $this->runToSql()); 43 | } 44 | 45 | protected function assertRegExpSql(string $regexpExpected): void 46 | { 47 | foreach ($this->runToSql() as $sql) { 48 | $this->assertMatchesRegularExpression($regexpExpected, $sql); 49 | } 50 | } 51 | 52 | private function runToSql(): array 53 | { 54 | return $this->blueprint->toSql(); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tests/Unit/Schema/Blueprint/IndexTest.php: -------------------------------------------------------------------------------- 1 | initializeMock(static::TABLE); 26 | } 27 | 28 | #[Test] 29 | #[DataProvider('provideExcludeConstraints')] 30 | public function addConstraint(Closure $callback, string $expectedSQL): void 31 | { 32 | $callback($this->blueprint); 33 | $this->assertSameSql($expectedSQL); 34 | } 35 | 36 | public static function provideExcludeConstraints(): Generator 37 | { 38 | yield [ 39 | static function (Blueprint $table) { 40 | $table 41 | ->exclude(['period_start', 'period_end']) 42 | ->using('period_type_id', '=') 43 | ->using('daterange(period_start, period_end)', '&&') 44 | ->method('gist') 45 | ->whereNull('deleted_at'); 46 | }, 47 | implode(' ', [ 48 | 'ALTER TABLE test_table ADD CONSTRAINT test_table_period_start_period_end_excl', 49 | 'EXCLUDE USING gist (period_type_id WITH =, daterange(period_start, period_end) WITH &&)', 50 | 'WHERE ("deleted_at" is null)', 51 | ]), 52 | ]; 53 | yield [ 54 | static function (Blueprint $table) { 55 | $table 56 | ->exclude(['period_start', 'period_end']) 57 | ->using('period_type_id', '=') 58 | ->using('daterange(period_start, period_end)', '&&') 59 | ->whereNull('deleted_at'); 60 | }, 61 | implode(' ', [ 62 | 'ALTER TABLE test_table ADD CONSTRAINT test_table_period_start_period_end_excl', 63 | 'EXCLUDE (period_type_id WITH =, daterange(period_start, period_end) WITH &&)', 64 | 'WHERE ("deleted_at" is null)', 65 | ]), 66 | ]; 67 | yield [ 68 | static function (Blueprint $table) { 69 | $table 70 | ->exclude(['period_start', 'period_end']) 71 | ->using('period_type_id', '=') 72 | ->using('daterange(period_start, period_end)', '&&'); 73 | }, 74 | implode(' ', [ 75 | 'ALTER TABLE test_table ADD CONSTRAINT test_table_period_start_period_end_excl', 76 | 'EXCLUDE (period_type_id WITH =, daterange(period_start, period_end) WITH &&)', 77 | ]), 78 | ]; 79 | yield [ 80 | static function (Blueprint $table) { 81 | $table 82 | ->exclude(['period_start', 'period_end']) 83 | ->using('period_type_id', '=') 84 | ->using('daterange(period_start, period_end)', '&&') 85 | ->tableSpace('excludeSpace'); 86 | }, 87 | implode(' ', [ 88 | 'ALTER TABLE test_table ADD CONSTRAINT test_table_period_start_period_end_excl', 89 | 'EXCLUDE (period_type_id WITH =, daterange(period_start, period_end) WITH &&)', 90 | 'USING INDEX TABLESPACE excludeSpace', 91 | ]), 92 | ]; 93 | yield [ 94 | static function (Blueprint $table) { 95 | $table 96 | ->exclude(['period_start', 'period_end']) 97 | ->using('period_type_id', '=') 98 | ->using('daterange(period_start, period_end)', '&&') 99 | ->with('some_arg', 1) 100 | ->with('any_arg', 'some_value'); 101 | }, 102 | implode(' ', [ 103 | 'ALTER TABLE test_table ADD CONSTRAINT test_table_period_start_period_end_excl', 104 | 'EXCLUDE (period_type_id WITH =, daterange(period_start, period_end) WITH &&)', 105 | "WITH (some_arg = 1, any_arg = 'some_value')", 106 | ]), 107 | ]; 108 | yield [ 109 | static function (Blueprint $table) { 110 | $table 111 | ->check(['period_start', 'period_end']) 112 | ->whereColumn('period_end', '>', 'period_start') 113 | ->whereRaw('period_start NOT NULL or period_end NOT NULL'); 114 | }, 115 | implode(' ', [ 116 | 'ALTER TABLE test_table ADD CONSTRAINT test_table_period_start_period_end_chk', 117 | 'CHECK (("period_end" > "period_start") and (period_start NOT NULL or period_end NOT NULL))', 118 | ]), 119 | ]; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /tests/Unit/Schema/Blueprint/PartitionTest.php: -------------------------------------------------------------------------------- 1 | initializeMock(static::TABLE); 24 | } 25 | 26 | #[Test] 27 | public function detachPartition(): void 28 | { 29 | $this->blueprint->detachPartition('some_partition'); 30 | $this->assertSameSql('alter table "test_table" detach partition some_partition'); 31 | } 32 | 33 | #[Test] 34 | public function attachPartitionRangeInt(): void 35 | { 36 | $this->blueprint->attachPartition('some_partition') 37 | ->range([ 38 | 'from' => 10, 39 | 'to' => 100, 40 | ]); 41 | $this->assertSameSql('alter table "test_table" attach partition some_partition for values from (10) to (100)'); 42 | } 43 | 44 | #[Test] 45 | public function attachPartitionFailedWithoutForValuesPart(): void 46 | { 47 | $this->blueprint->attachPartition('some_partition'); 48 | $this->expectException(InvalidArgumentException::class); 49 | $this->runToSql(); 50 | } 51 | 52 | #[Test] 53 | public function attachPartitionRangeDates(): void 54 | { 55 | $today = Carbon::today(); 56 | $tomorrow = Carbon::tomorrow(); 57 | $this->blueprint->attachPartition('some_partition') 58 | ->range([ 59 | 'from' => $today, 60 | 'to' => $tomorrow, 61 | ]); 62 | 63 | $this->assertSameSql(sprintf( 64 | 'alter table "test_table" attach partition some_partition for values from (\'%s\') to (\'%s\')', 65 | $today->toDateTimeString(), 66 | $tomorrow->toDateTimeString() 67 | )); 68 | } 69 | 70 | #[Test] 71 | public function attachPartitionStringDates(): void 72 | { 73 | $today = '2010-01-01'; 74 | $tomorrow = '2010-12-31'; 75 | $this->blueprint->attachPartition('some_partition') 76 | ->range([ 77 | 'from' => $today, 78 | 'to' => $tomorrow, 79 | ]); 80 | 81 | $this->assertSameSql(sprintf( 82 | 'alter table "test_table" attach partition some_partition for values from (\'%s\') to (\'%s\')', 83 | $today, 84 | $tomorrow 85 | )); 86 | } 87 | 88 | #[Test] 89 | public function addingTsrangeColumn() 90 | { 91 | $this->blueprint->tsrange('foo'); 92 | $this->assertSameSql('alter table "test_table" add column "foo" tsrange not null'); 93 | } 94 | 95 | #[Test] 96 | public function addingTstzrangeColumn() 97 | { 98 | $this->blueprint->tstzrange('foo'); 99 | $this->assertSameSql('alter table "test_table" add column "foo" tstzrange not null'); 100 | } 101 | 102 | #[Test] 103 | public function addingDaterangeColumn() 104 | { 105 | $this->blueprint->daterange('foo'); 106 | $this->assertSameSql('alter table "test_table" add column "foo" daterange not null'); 107 | } 108 | 109 | #[Test] 110 | public function addingNumericColumnWithVariablePrecicion() 111 | { 112 | $this->blueprint->numeric('foo'); 113 | $this->assertSameSql('alter table "test_table" add column "foo" numeric not null'); 114 | } 115 | 116 | #[Test] 117 | public function addingNumericColumnWithDefinedPrecicion() 118 | { 119 | $this->blueprint->numeric('foo', 8); 120 | $this->assertSameSql('alter table "test_table" add column "foo" numeric(8) not null'); 121 | } 122 | 123 | #[Test] 124 | public function addingNumericColumnWithDefinedPrecicionAndScope() 125 | { 126 | $this->blueprint->numeric('foo', 8, 2); 127 | $this->assertSameSql('alter table "test_table" add column "foo" numeric(8, 2) not null'); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /tests/Unit/Schema/Types/DateRangeTypeTest.php: -------------------------------------------------------------------------------- 1 | type = new DateRangeType(); 25 | $this->abstractPlatform = $this 26 | ->getMockBuilder(PostgreSQLPlatform::class) 27 | ->getMock(); 28 | } 29 | 30 | #[Test] 31 | public function getSQLDeclaration(): void 32 | { 33 | $this->assertSame(DateRangeType::TYPE_NAME, $this->type->getSQLDeclaration([], $this->abstractPlatform)); 34 | } 35 | 36 | #[Test] 37 | public function getTypeName(): void 38 | { 39 | $this->assertSame(DateRangeType::TYPE_NAME, $this->type->getName()); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tests/Unit/Schema/Types/NumericTypeTest.php: -------------------------------------------------------------------------------- 1 | type = new NumericType(); 25 | $this->abstractPlatform = $this 26 | ->getMockBuilder(PostgreSQLPlatform::class) 27 | ->getMock(); 28 | } 29 | 30 | #[Test] 31 | public function getSQLDeclaration(): void 32 | { 33 | $this->assertSame(NumericType::TYPE_NAME, $this->type->getSQLDeclaration([], $this->abstractPlatform)); 34 | } 35 | 36 | #[Test] 37 | public function getTypeName(): void 38 | { 39 | $this->assertSame(NumericType::TYPE_NAME, $this->type->getName()); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tests/Unit/Schema/Types/TsRangeTypeTest.php: -------------------------------------------------------------------------------- 1 | type = new TsRangeType(); 25 | $this->abstractPlatform = $this 26 | ->getMockBuilder(PostgreSQLPlatform::class) 27 | ->getMock(); 28 | } 29 | 30 | #[Test] 31 | public function getSQLDeclaration(): void 32 | { 33 | $this->assertSame(TsRangeType::TYPE_NAME, $this->type->getSQLDeclaration([], $this->abstractPlatform)); 34 | } 35 | 36 | #[Test] 37 | public function getTypeName(): void 38 | { 39 | $this->assertSame(TsRangeType::TYPE_NAME, $this->type->getName()); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tests/Unit/Schema/Types/TsTzRangeTypeTest.php: -------------------------------------------------------------------------------- 1 | type = new TsTzRangeType(); 25 | $this->abstractPlatform = $this 26 | ->getMockBuilder(PostgreSQLPlatform::class) 27 | ->getMock(); 28 | } 29 | 30 | #[Test] 31 | public function getSQLDeclaration(): void 32 | { 33 | $this->assertSame(TsTzRangeType::TYPE_NAME, $this->type->getSQLDeclaration([], $this->abstractPlatform)); 34 | } 35 | 36 | #[Test] 37 | public function getTypeName(): void 38 | { 39 | $this->assertSame(TsTzRangeType::TYPE_NAME, $this->type->getName()); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tests/_data/CustomSQLiteConnection.php: -------------------------------------------------------------------------------- 1 |