Skip to content

Commit f2976f8

Browse files
committed
create indexes concurrently (resolves #82)
1 parent 75c6760 commit f2976f8

File tree

6 files changed

+183
-16
lines changed

6 files changed

+183
-16
lines changed

README.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ composer require tpetry/laravel-postgresql-enhanced
2828
- [Views](#views)
2929
- [Materialized Views](#materialized-views)
3030
- [Indexes](#indexes)
31+
- [Concurrently](#concurrently)
3132
- [Nulls Not Distinct](#nulls-not-distinct)
3233
- [Partial Indexes](#partial-indexes)
3334
- [Include Columns](#include-columns)
@@ -395,6 +396,34 @@ Schema::table('users', function(Blueprint $table) {
395396
In addition to the Laravel methods to drop indexes, methods to drop indexes if they exist have been added.
396397
The methods `dropFullTextIfExists`, `dropIndexIfExists`, `dropPrimaryIfExists`, `dropSpatialIndexIfExists` and `dropSpatialIndexIfExists` match the semantics of their laravel originals.
397398

399+
#### Concurrently
400+
401+
With PostgreSQL, you can say goodbye to half-executed migrations on errors and the tedious effort to restore the database to a stable state.
402+
This is all thanks to its transactional approach: either all changes of a migration to your database will succeed or will be rolled back.
403+
Yay!
404+
Because of that, creating an index on a big table will take a long time and block all SQL queries during that time.
405+
You can now instruct PostgreSQL to create the index in the background without blocking any SQL query, but you must opt out of running those changes in a transaction.
406+
407+
```php
408+
<?php
409+
410+
use Illuminate\Database\Migrations\Migration;
411+
use Tpetry\PostgresqlEnhanced\Schema\Blueprint;
412+
use Tpetry\PostgresqlEnhanced\Support\Facades\Schema;
413+
414+
return new class extends Migration
415+
{
416+
public $withinTransaction = false;
417+
418+
public function up(): void
419+
{
420+
Schema::table('blog_visits', function (Blueprint $table) {
421+
$table->index(['url', 'ip_address'])->concurrently();
422+
});
423+
}
424+
};
425+
```
426+
398427
#### Nulls Not Distinct
399428

400429
NULL values in unique indexes are handled in a non-comprehensible way for most developers.

src/Schema/Grammars/GrammarIndex.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ private function genericCompileCreateIndex(Blueprint $blueprint, Fluent $command
167167

168168
$index = [
169169
$unique ? 'create unique index' : 'create index',
170+
$command['concurrently'] ? 'concurrently' : '',
170171
$command['ifNotExists'] ? 'if not exists' : '',
171172
$this->wrap($command['index']),
172173
'on',

src/Schema/IndexDefinition.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,14 @@
1111
*/
1212
class IndexDefinition extends BaseIndexDefinition
1313
{
14+
/**
15+
* Create index concurrently to current migration (PostgreSQL).
16+
*/
17+
public function concurrently(): self
18+
{
19+
return $this;
20+
}
21+
1422
/**
1523
* Only create index if it does not exist yet (PostgreSQL).
1624
*/

tests/Connection/ReturningTest.php

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -24,19 +24,15 @@ protected function setUp(): void
2424
public function testExecutesNothingOnPretend(): void
2525
{
2626
$this->getConnection()->table('example')->insert(['str' => '8lnreu2H']);
27-
$this->getConnection()->pretend(function (): void {
28-
$queries = $this->withQueryLog(function (): void {
29-
$this->assertEquals([], $this->getConnection()->returningStatement('update example set str = ? where str = ? returning str', ['IS7PD2jn', '8lnreu2H']));
30-
});
31-
32-
// The pretend mode has been changed in Laravel 10.30.0 to include the bindings in the query string
33-
if (Comparator::greaterThanOrEqualTo($this->app->version(), '10.30.0')) {
34-
$this->assertEquals(["update example set str = 'IS7PD2jn' where str = '8lnreu2H' returning str"], array_column($queries, 'query'));
35-
} else {
36-
$this->assertEquals(['update example set str = ? where str = ? returning str'], array_column($queries, 'query'));
37-
}
38-
});
39-
27+
$queries = $this->withQueryLog(function (): void {
28+
$this->assertEquals([], $this->getConnection()->returningStatement('update example set str = ? where str = ? returning str', ['IS7PD2jn', '8lnreu2H']));
29+
}, pretend: true);
30+
31+
// The pretend mode has been changed in Laravel 10.30.0 to include the bindings in the query string
32+
match (Comparator::greaterThanOrEqualTo($this->app->version(), '10.30.0')) {
33+
true => $this->assertEquals(["update example set str = 'IS7PD2jn' where str = '8lnreu2H' returning str"], array_column($queries, 'query')),
34+
false => $this->assertEquals(['update example set str = ? where str = ? returning str'], array_column($queries, 'query')),
35+
};
4036
$this->assertEquals(1, $this->getConnection()->selectOne('SELECT COUNT(*) AS count FROM example WHERE str = ?', ['8lnreu2H'])->count);
4137
}
4238

tests/Migration/IndexOptionsTest.php

Lines changed: 130 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,135 @@
1212

1313
class IndexOptionsTest extends TestCase
1414
{
15+
public function testConcurrentlyFulltextByColumn(): void
16+
{
17+
if (Comparator::lessThan($this->app->version(), '8.74.0')) {
18+
$this->markTestSkipped('Fulltext indexes have been added in a later Laraverl version.');
19+
}
20+
21+
Schema::create('test_260745', function (Blueprint $table): void {
22+
$table->string('col_818827');
23+
});
24+
$queries = $this->withQueryLog(function (): void {
25+
Schema::table('test_260745', function (Blueprint $table): void {
26+
$table->fullText(['col_818827'])->concurrently();
27+
});
28+
}, pretend: true);
29+
$this->assertEquals(['create index concurrently "test_260745_col_818827_fulltext" on "test_260745" using gin ((to_tsvector(\'english\', "col_818827")))'], array_column($queries, 'query'));
30+
}
31+
32+
public function testConcurrentlyFulltextByName(): void
33+
{
34+
if (Comparator::lessThan($this->app->version(), '8.74.0')) {
35+
$this->markTestSkipped('Fulltext indexes have been added in a later Laraverl version.');
36+
}
37+
38+
Schema::create('test_406163', function (Blueprint $table): void {
39+
$table->string('col_833985');
40+
});
41+
$queries = $this->withQueryLog(function (): void {
42+
Schema::table('test_406163', function (Blueprint $table): void {
43+
$table->fullText(['col_833985'], 'index_495761')->concurrently();
44+
});
45+
}, pretend: true);
46+
$this->assertEquals(['create index concurrently "index_495761" on "test_406163" using gin ((to_tsvector(\'english\', "col_833985")))'], array_column($queries, 'query'));
47+
}
48+
49+
public function testConcurrentlyIndexByColumn(): void
50+
{
51+
Schema::create('test_684553', function (Blueprint $table): void {
52+
$table->string('col_930758');
53+
});
54+
$queries = $this->withQueryLog(function (): void {
55+
Schema::table('test_684553', function (Blueprint $table): void {
56+
$table->index(['col_930758'])->concurrently();
57+
});
58+
}, pretend: true);
59+
$this->assertEquals(['create index concurrently "test_684553_col_930758_index" on "test_684553" ("col_930758")'], array_column($queries, 'query'));
60+
}
61+
62+
public function testConcurrentlyIndexByName(): void
63+
{
64+
Schema::create('test_323396', function (Blueprint $table): void {
65+
$table->string('col_677415');
66+
});
67+
$queries = $this->withQueryLog(function (): void {
68+
Schema::table('test_323396', function (Blueprint $table): void {
69+
$table->index(['col_677415'], 'index_336745')->concurrently();
70+
});
71+
}, pretend: true);
72+
$this->assertEquals(['create index concurrently "index_336745" on "test_323396" ("col_677415")'], array_column($queries, 'query'));
73+
}
74+
75+
public function testConcurrentlyRawIndex(): void
76+
{
77+
if (Comparator::lessThan($this->app->version(), '7.7.0')) {
78+
$this->markTestSkipped('Raw indexes have been added in a later Laravel version.');
79+
}
80+
81+
Schema::create('test_142731', function (Blueprint $table): void {
82+
$table->string('col_247155');
83+
});
84+
$queries = $this->withQueryLog(function (): void {
85+
Schema::table('test_142731', function (Blueprint $table): void {
86+
$table->rawIndex('col_247155', 'idx_585783')->concurrently();
87+
});
88+
}, pretend: true);
89+
$this->assertEquals(['create index concurrently "idx_585783" on "test_142731" (col_247155)'], array_column($queries, 'query'));
90+
}
91+
92+
public function testConcurrentlySpatialIndexByColumn(): void
93+
{
94+
Schema::create('test_734987', function (Blueprint $table): void {
95+
$table->integerRange('col_617117');
96+
});
97+
$queries = $this->withQueryLog(function (): void {
98+
Schema::table('test_734987', function (Blueprint $table): void {
99+
$table->spatialIndex(['col_617117'])->concurrently();
100+
});
101+
}, pretend: true);
102+
$this->assertEquals(['create index concurrently "test_734987_col_617117_spatialindex" on "test_734987" using gist ("col_617117")'], array_column($queries, 'query'));
103+
}
104+
105+
public function testConcurrentlySpatialIndexByName(): void
106+
{
107+
Schema::create('test_469394', function (Blueprint $table): void {
108+
$table->integerRange('col_562801');
109+
});
110+
$queries = $this->withQueryLog(function (): void {
111+
Schema::table('test_469394', function (Blueprint $table): void {
112+
$table->spatialIndex(['col_562801'], 'index_623983')->concurrently();
113+
});
114+
}, pretend: true);
115+
$this->assertEquals(['create index concurrently "index_623983" on "test_469394" using gist ("col_562801")'], array_column($queries, 'query'));
116+
}
117+
118+
public function testConcurrentlyUniqueIndexByColumn(): void
119+
{
120+
Schema::create('test_144373', function (Blueprint $table): void {
121+
$table->string('col_988745');
122+
});
123+
$queries = $this->withQueryLog(function (): void {
124+
Schema::table('test_144373', function (Blueprint $table): void {
125+
$table->uniqueIndex(['col_988745'])->concurrently();
126+
});
127+
}, pretend: true);
128+
$this->assertEquals(['create unique index concurrently "test_144373_col_988745_unique" on "test_144373" ("col_988745")'], array_column($queries, 'query'));
129+
}
130+
131+
public function testConcurrentlyUniqueIndexByName(): void
132+
{
133+
Schema::create('test_583449', function (Blueprint $table): void {
134+
$table->string('col_134696');
135+
});
136+
$queries = $this->withQueryLog(function (): void {
137+
Schema::table('test_583449', function (Blueprint $table): void {
138+
$table->uniqueIndex(['col_134696'], 'index_304869')->concurrently();
139+
});
140+
}, pretend: true);
141+
$this->assertEquals(['create unique index concurrently "index_304869" on "test_583449" ("col_134696")'], array_column($queries, 'query'));
142+
}
143+
15144
public function testIfNotExistsFulltextByColumn(): void
16145
{
17146
if (Comparator::lessThan($this->app->version(), '8.74.0')) {
@@ -25,7 +154,7 @@ public function testIfNotExistsFulltextByColumn(): void
25154
Schema::table('test_806712', function (Blueprint $table): void {
26155
$table->fullText(['col_274742'])->ifNotExists();
27156
});
28-
});
157+
}, pretend: true);
29158
$this->assertEquals(['create index if not exists "test_806712_col_274742_fulltext" on "test_806712" using gin ((to_tsvector(\'english\', "col_274742")))'], array_column($queries, 'query'));
30159
}
31160

tests/TestCase.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,15 @@ protected function getPackageProviders($app)
4242
];
4343
}
4444

45-
protected function withQueryLog(Closure $fn): array
45+
protected function withQueryLog(Closure $fn, bool $pretend = false): array
4646
{
4747
$this->getConnection()->flushQueryLog();
4848
$this->getConnection()->enableQueryLog();
49-
$fn();
49+
50+
match ($pretend) {
51+
true => $this->getConnection()->pretend($fn),
52+
false => $fn(),
53+
};
5054

5155
return $this->getConnection()->getQueryLog();
5256
}

0 commit comments

Comments
 (0)