Skip to content

Commit 803a5a5

Browse files
committed
feat(migration): timescale support
1 parent a011cff commit 803a5a5

37 files changed

+1285
-6
lines changed

.github/workflows/phpunit.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ jobs:
7676

7777
services:
7878
postgres:
79-
image: ankane/pgvector
79+
image: timescale/timescaledb-ha:pg17
8080
env:
8181
POSTGRES_PASSWORD: postgres
8282
ports:

README.md

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,8 @@ The minimal breaking changes of the past years are listed in the [breaking chang
8383
- [Refresh Data on Save](#refresh-data-on-save)
8484
- [Date Formats](#date-formats)
8585
- [Expressions](#expressions)
86-
86+
- [Supported Extensions](#supported-extensions)
87+
- [Timescale](#timescale)
8788
## IDE Autocomplete
8889

8990
Laravel provides many extension capabilities, making it hard for IDEs to do proper autocomplete.
@@ -1338,6 +1339,93 @@ Schema::create('comments', function (Blueprint $table) {
13381339
});
13391340
```
13401341

1342+
# Supported Extensions
1343+
1344+
You can use any extension with this PostgreSQL you like but some have received a deeper Laravel integration.
1345+
1346+
## Timescale
1347+
1348+
Timescale is fantastic and has many features.
1349+
Therefore, it is impossible to explain everything here; consult their docs about the different features this extension provides.
1350+
Here's a list of supported features and an example showcasing its usage:
1351+
1352+
| Feature | Actions |
1353+
|-----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
1354+
| Hypertable | `new CreateHypertable(string $column, string\|int $interval, string $partitionFunction = null)`<br/>`new ChangeChunkTimeInterval(string\|int $interval)` |
1355+
| Chunk Skipping | `new EnableChunkSkipping(string $column)`<br/>`new DisableChunkSkipping(string $column)` |
1356+
| Compression | `new EnableCompression(string\|array $orderBy = null, string\|array $segmentBy = null)`<br/> `new DisableCompression()`<br/>`new CreateCompressionPolicy(string\|int $compressAfter)`<br/>`new DropCompressionPolicy()`<br/>`new CompressChunks(DateTimeInterface\|string\|int $olderThan = null, DateTimeInterface\|string\|int $newerThan = null)`<br/>`new DecompressChunks(DateTimeInterface\|string\|int $olderThan = null, DateTimeInterface\|string\|int $newerThan = null)` |
1357+
| Reordering | `new CreateReorderPolicy(string $index)`<br/>`new CreateReorderPolicyByIndex(...$columns)`<br/>`new CreateReorderPolicyByUnique(...$columns)`<br/>`new DropReorderPolicy()`<br/>`new ReorderChunks(DateTimeInterface\|string\|int $olderThan = null, DateTimeInterface\|string\|int $newerThan = null)` |
1358+
| Data Retention | `new CreateRetentionPolicy(string\|int $dropAfter)`<br/>`new DropRetentionPolicy()`<br/>`new DropChunks(DateTimeInterface\|string\|int $olderThan = null, DateTimeInterface\|string\|int $newerThan = null)` |
1359+
| Tiered Storage | `new CreateTieringPolicy(string\|int $dropAfter)`<br/>`new DropTieringPolicy()`<br/>`new TierChunks(DateTimeInterface\|string\|int $olderThan = null, DateTimeInterface\|string\|int $newerThan = null)`<br/>`new UntierChunks(DateTimeInterface\|string\|int $olderThan = null, DateTimeInterface\|string\|int $newerThan = null)` |
1360+
| Continuous Aggregates | `new CreateRefreshPolicy(string $interval, string\|int\|null $start, string\|int\|null $end)`<br/>`new DropRefreshPolicy()`<br/>`new RefreshData(DateTimeInterface\|int\|null $start, DateTimeInterface\|int\|null $end)` |
1361+
1362+
```php
1363+
use Illuminate\Database\Migrations\Migration;
1364+
use Illuminate\Database\Migrations\Migration;
1365+
use Tpetry\PostgresqlEnhanced\Schema\Timescale\Actions\CreateCompressionPolicy;
1366+
use Tpetry\PostgresqlEnhanced\Schema\Timescale\Actions\CreateHypertable;
1367+
use Tpetry\PostgresqlEnhanced\Schema\Timescale\Actions\CreateRefreshPolicy;
1368+
use Tpetry\PostgresqlEnhanced\Schema\Timescale\Actions\CreateReorderPolicyByIndex;
1369+
use Tpetry\PostgresqlEnhanced\Schema\Timescale\Actions\CreateRetentionPolicy;
1370+
use Tpetry\PostgresqlEnhanced\Schema\Timescale\Actions\EnableChunkSkipping;
1371+
use Tpetry\PostgresqlEnhanced\Schema\Timescale\Actions\EnableCompression;
1372+
use Tpetry\PostgresqlEnhanced\Schema\Timescale\CaggBlueprint;
1373+
use Tpetry\PostgresqlEnhanced\Support\Facades\Schema;
1374+
use Tpetry\PostgresqlEnhanced\Schema\Blueprint;
1375+
1376+
return new class extends Migration
1377+
{
1378+
public function up(): void
1379+
{
1380+
Schema::createExtensionIfNotExists('timescaledb');
1381+
1382+
Schema::create('visits', function (Blueprint $table) {
1383+
$table->identity();
1384+
$table->bigInteger('website_id');
1385+
$table->text('url');
1386+
$table->float('duration');
1387+
$table->timestampTz('created_at')->nullable();
1388+
1389+
$table->primary(['id', 'created_at']);
1390+
$table->index(['website_id', 'created_at']);
1391+
1392+
$table->timescale(
1393+
new CreateHypertable('created_at', '1 day'),
1394+
new CreateReorderPolicyByIndex('website_id', 'created_at'),
1395+
new EnableCompression(segmentBy: 'website_id'),
1396+
new CreateCompressionPolicy('3 days'),
1397+
new CreateRetentionPolicy('1 year'),
1398+
new EnableChunkSkipping('id'),
1399+
);
1400+
});
1401+
1402+
Schema::continuousAggregate('visits_agg', function(CaggBlueprint $table) {
1403+
$table->as("
1404+
SELECT
1405+
time_bucket('1 hour', created_at) AS bucket,
1406+
website_id,
1407+
url,
1408+
SUM(duration) AS duration
1409+
FROM visits
1410+
GROUP BY bucket, website_id, url
1411+
");
1412+
$table->realtime();
1413+
$table->index(['website_id','url']);
1414+
1415+
$table->timescale(
1416+
new CreateRefreshPolicy('5 minutes', '1 days', '2 hours'),
1417+
new EnableCompression(),
1418+
new CreateCompressionPolicy('2 days'),
1419+
);
1420+
});
1421+
}
1422+
};
1423+
```
1424+
1425+
> [!WARNING]
1426+
> Indexes are not automatically created when creating hypertables or continuous aggregates.
1427+
> You have to create them manually.
1428+
13411429
# Breaking Changes
13421430

13431431
* **2.0.0**

src/Schema/BlueprintTable.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,22 @@
55
namespace Tpetry\PostgresqlEnhanced\Schema;
66

77
use Illuminate\Support\Fluent;
8+
use Tpetry\PostgresqlEnhanced\Schema\Timescale\Actions\Action;
89

910
trait BlueprintTable
1011
{
12+
/**
13+
* Set timescale hypertable options.
14+
*
15+
* @param Action ...$actions
16+
*/
17+
public function timescale(...$actions): void
18+
{
19+
foreach ($actions as $action) {
20+
$this->addCommand('timescale', ['action' => $action]);
21+
}
22+
}
23+
1124
/**
1225
* Set a table to be (un)logged.
1326
*/

src/Schema/BuilderView.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,22 @@
44

55
namespace Tpetry\PostgresqlEnhanced\Schema;
66

7+
use Closure;
78
use Illuminate\Database\Query\Builder as QueryBuilder;
9+
use Tpetry\PostgresqlEnhanced\Schema\Timescale\CaggBlueprint;
810
use Tpetry\PostgresqlEnhanced\Support\Helpers\Query;
911

1012
trait BuilderView
1113
{
14+
/**
15+
* Create/Modify a continuous aggregate on the schema.
16+
*/
17+
public function continuousAggregate($table, Closure $callback): void
18+
{
19+
$blueprint = new CaggBlueprint($table, $callback);
20+
$blueprint->build($this->getConnection(), $this->getConnection()->getSchemaGrammar());
21+
}
22+
1223
/**
1324
* Create a materialized view on the schema.
1425
*/
@@ -74,6 +85,22 @@ public function createViewOrReplace(string $name, QueryBuilder|string $query, ar
7485
$this->getConnection()->statement("create or replace view {$name} as {$query}");
7586
}
7687

88+
/**
89+
* Drop continuous aggregates from the schema.
90+
*/
91+
public function dropContinuousAggregate(string ...$name): void
92+
{
93+
$this->dropMaterializedView(...$name);
94+
}
95+
96+
/**
97+
* Drop continuous aggregates from the schema if they exist.
98+
*/
99+
public function dropContinuousAggregateIfExists(string ...$name): void
100+
{
101+
$this->dropMaterializedViewIfExists(...$name);
102+
}
103+
77104
/**
78105
* Drop materialized views from the schema.
79106
*/

src/Schema/Grammars/Grammar.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ class Grammar extends PostgresGrammar
1212
use GrammarBackportEscape;
1313
use GrammarIndex;
1414
use GrammarTable;
15+
use GrammarTimescale;
1516
use GrammarTrigger;
1617
use GrammarTypes;
1718

src/Schema/Grammars/GrammarIndex.php

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -146,10 +146,7 @@ private function genericCompileCreateIndex(Blueprint $blueprint, Fluent $command
146146

147147
// In case index parameters are provided the column needs to escaped correctly and the rest is provided
148148
// exactly as provided.
149-
$parts = explode(' ', $column, 2);
150-
$column = trim(\sprintf('%s %s', $this->wrap($parts[0]), $parts[1] ?? ''));
151-
152-
return $column;
149+
return $this->columnizeWithSuffix([$column]);
153150
}, $command['columns']);
154151

155152
// A fulltext index needs special handling to wrap the columns into to_tsvector calls.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tpetry\PostgresqlEnhanced\Schema\Grammars;
6+
7+
use Illuminate\Database\Schema\Blueprint;
8+
use Illuminate\Support\Fluent;
9+
10+
trait GrammarTimescale
11+
{
12+
/**
13+
* Compile a timescale action command.
14+
*/
15+
public function compileTimescale(Blueprint $blueprint, Fluent $command): array
16+
{
17+
/** @var \Tpetry\PostgresqlEnhanced\Schema\Timescale\Actions\Action $action */
18+
$action = $command->get('action');
19+
20+
return $action->getValue($this, $blueprint->getTable());
21+
}
22+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tpetry\PostgresqlEnhanced\Schema\Timescale\Actions;
6+
7+
use Tpetry\PostgresqlEnhanced\Schema\Grammars\Grammar;
8+
9+
/**
10+
* @internal
11+
*/
12+
interface Action
13+
{
14+
public function getValue(Grammar $grammar, string $table): array;
15+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tpetry\PostgresqlEnhanced\Schema\Timescale\Actions;
6+
7+
use Tpetry\PostgresqlEnhanced\Schema\Grammars\Grammar;
8+
9+
class ChangeChunkTimeInterval implements Action
10+
{
11+
public function __construct(
12+
private string|int $interval,
13+
) {
14+
}
15+
16+
public function getValue(Grammar $grammar, string $table): array
17+
{
18+
return match (is_numeric($this->interval)) {
19+
true => ["select set_chunk_time_interval({$grammar->escape($table)}, {$this->interval})"],
20+
false => ["select set_chunk_time_interval({$grammar->escape($table)}, interval '{$this->interval}')"],
21+
};
22+
}
23+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tpetry\PostgresqlEnhanced\Schema\Timescale\Actions;
6+
7+
use Tpetry\PostgresqlEnhanced\Schema\Grammars\Grammar;
8+
9+
class CompressChunks extends ShowChunks
10+
{
11+
public function getValue(Grammar $grammar, string $table): array
12+
{
13+
return ["select compress_chunk(c) from {$this->getShowChunksCall($grammar, $table)} c"];
14+
}
15+
}

0 commit comments

Comments
 (0)