├── .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 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/.run/Tests .run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/.run/Unit.run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
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 | [](https://github.com/umbrellio/laravel-pg-extensions/actions)
4 | [](https://coveralls.io/github/umbrellio/laravel-pg-extensions?branch=master)
5 | [](https://packagist.org/packages/umbrellio/laravel-pg-extensions)
6 | [](https://packagist.org/packages/umbrellio/laravel-pg-extensions)
7 | [](https://scrutinizer-ci.com/code-intelligence)
8 | [](https://scrutinizer-ci.com/g/umbrellio/laravel-pg-extensions/build-status/master)
9 | [](https://scrutinizer-ci.com/g/umbrellio/laravel-pg-extensions/?branch=master)
10 | [](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 |
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 |