Skip to content

Commit

Permalink
Feature/unique wheres (#5)
Browse files Browse the repository at this point in the history
Extending creating unique indexes with conditions on where, example:

```
Schema::create('cities', static function (Blueprint $table) {
       ... 
       $table->uniquePartial(['name', 'author_id'])
            ->where('editor_id', 1)
            ->whereRaw('field1 = ? and field2 <= ?', ['field1', 'field2'])
            ->whereNotNull('deleted_at')
            ->whereBetween('author_id', [1, 2, 3]);
      ...
```
PS. If you will use $table->uniquePartial() without extend methods after call uniquePartial(), it works as $table->unique().
  • Loading branch information
pvsaintpe authored and lazeevv committed Jul 9, 2019
1 parent a09e18e commit e070c05
Show file tree
Hide file tree
Showing 8 changed files with 401 additions and 21 deletions.
2 changes: 2 additions & 0 deletions src/.meta.php
Expand Up @@ -5,12 +5,14 @@
use Illuminate\Support\Fluent;
use Umbrellio\Postgres\Schema\Definitions\AttachPartitionDefinition;
use Umbrellio\Postgres\Schema\Definitions\LikeDefinition;
use Umbrellio\Postgres\Schema\Definitions\UniqueDefinition;

/**
* @method AttachPartitionDefinition attachPartition(string $partition)
* @method void detachPartition(string $partition)
* @method LikeDefinition like(string $table)
* @method Fluent ifNotExists()
* @method UniqueDefinition uniquePartial($columns, ?string $index = null, ?string $algorithm = null)
*/
class Blueprint
{
Expand Down
128 changes: 128 additions & 0 deletions src/Compilers/UniqueWhereCompiler.php
@@ -0,0 +1,128 @@
<?php

declare(strict_types=1);

namespace Umbrellio\Postgres\Compilers;

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Schema\Grammars\Grammar;
use Umbrellio\Postgres\Schema\Builders\UniquePartialBuilder;
use Umbrellio\Postgres\Schema\Builders\UniqueWhereBuilder;

class UniqueWhereCompiler
{
public static function compile(
Grammar $grammar,
Blueprint $blueprint,
UniquePartialBuilder $fluent,
UniqueWhereBuilder $command
): string {
$wheres = collect($command->get('wheres'))
->map(function ($where) use ($grammar, $blueprint) {
return implode(' ', [
$where['boolean'],
'(' . static::{"where{$where['type']}"}($grammar, $blueprint, $where) . ')',
]);
})
->all();

return sprintf(
'CREATE UNIQUE INDEX %s ON %s (%s) WHERE %s',
$fluent->get('index'),
$blueprint->getTable(),
implode(',', $fluent->get('columns')),
static::removeLeadingBoolean(implode(' ', $wheres))
);
}

protected static function whereRaw(Grammar $grammar, Blueprint $blueprint, array $where = []): string
{
return call_user_func_array('sprintf', array_merge(
[str_replace('?', '%s', $where['sql'])],
static::wrapValues($where['bindings'])
));
}

protected static function whereBasic(Grammar $grammar, Blueprint $blueprint, array $where): string
{
return implode(' ', [
$grammar->wrap($where['column']),
$where['operator'],
static::wrapValue($where['value']),
]);
}

protected static function whereColumn(Grammar $grammar, Blueprint $blueprint, array $where): string
{
return implode(' ', [
$grammar->wrap($where['first']),
$where['operator'],
$grammar->wrap($where['second']),
]);
}

protected static function whereIn(Grammar $grammar, Blueprint $blueprint, array $where = []): string
{
if (!empty($where['values'])) {
return implode(' ', [
$grammar->wrap($where['column']),
'in',
'(' . implode(',', static::wrapValues($where['values'])) . ')',
]);
}
return '0 = 1';
}

protected static function whereNotIn(Grammar $grammar, Blueprint $blueprint, array $where = []): string
{
if (!empty($where['values'])) {
return implode(' ', [
$grammar->wrap($where['column']),
'not in',
'(' . implode(',', static::wrapValues($where['values'])) . ')',
]);
}
return '1 = 1';
}

protected static function whereNull(Grammar $grammar, Blueprint $blueprint, array $where): string
{
return implode(' ', [$grammar->wrap($where['column']), 'is null']);
}

protected static function whereNotNull(Grammar $grammar, Blueprint $blueprint, array $where): string
{
return implode(' ', [$grammar->wrap($where['column']), 'is not null']);
}

protected static function whereBetween(Grammar $grammar, Blueprint $blueprint, array $where): string
{
return implode(' ', [
$grammar->wrap($where['column']),
$where['not'] ? 'not between' : 'between',
static::wrapValue(reset($where['values'])),
'and',
static::wrapValue(end($where['values'])),
]);
}

protected static function wrapValues(array $values = []): array
{
return collect($values)->map(function ($value) {
return static::wrapValue($value);
})->toArray();
}

protected static function wrapValue($value)
{
if (is_string($value)) {
return "'{$value}'";
}
return (int) $value;
}

protected static function removeLeadingBoolean(string $value): string
{
return preg_replace('/and |or /i', '', $value, 1);
}
}
30 changes: 28 additions & 2 deletions src/Schema/Blueprint.php
Expand Up @@ -6,13 +6,15 @@

use Illuminate\Database\Schema\Blueprint as BaseBlueprint;
use Illuminate\Support\Fluent;
use Umbrellio\Postgres\Schema\Builders\UniquePartialBuilder;
use Umbrellio\Postgres\Schema\Definitions\AttachPartitionDefinition;
use Umbrellio\Postgres\Schema\Definitions\LikeDefinition;
use Umbrellio\Postgres\Schema\Definitions\UniqueDefinition;

class Blueprint extends BaseBlueprint
{
/**
* @return AttachPartitionDefinition|Fluent
* @return AttachPartitionDefinition
*/
public function attachPartition(string $partition)
{
Expand All @@ -25,7 +27,7 @@ public function detachPartition(string $partition): void
}

/**
* @return LikeDefinition|Fluent
* @return LikeDefinition
*/
public function like(string $table)
{
Expand All @@ -36,4 +38,28 @@ public function ifNotExists(): Fluent
{
return $this->addCommand('ifNotExists');
}

/**
* @param array|string $columns
* @return UniqueDefinition
*/
public function uniquePartial($columns, ?string $index = null, ?string $algorithm = null)
{
$columns = (array) $columns;

$index = $index ?: $this->createIndexName('unique', $columns);

return $this->addExtendedCommand(
UniquePartialBuilder::class,
'uniquePartial',
compact('columns', 'index', 'algorithm')
);
}

private function addExtendedCommand(string $fluent, string $name, array $parameters = [])
{
$command = new $fluent(array_merge(compact('name'), $parameters));
$this->commands[] = $command;
return $command;
}
}
17 changes: 17 additions & 0 deletions src/Schema/Builders/UniquePartialBuilder.php
@@ -0,0 +1,17 @@
<?php

declare(strict_types=1);

namespace Umbrellio\Postgres\Schema\Builders;

use Illuminate\Support\Fluent;

class UniquePartialBuilder extends Fluent
{
public function __call($method, $parameters)
{
$command = new UniqueWhereBuilder();
$this->attributes['constraints'] = call_user_func_array([$command, $method], $parameters);
return $command;
}
}
61 changes: 61 additions & 0 deletions src/Schema/Builders/UniqueWhereBuilder.php
@@ -0,0 +1,61 @@
<?php

declare(strict_types=1);

namespace Umbrellio\Postgres\Schema\Builders;

use Illuminate\Support\Fluent;

class UniqueWhereBuilder extends Fluent
{
public function whereRaw(string $sql, array $bindings = [], string $boolean = 'and'): self
{
return $this->compileWhere('Raw', $boolean, compact('sql', 'bindings'));
}

public function where(string $column, string $operator, string $value, string $boolean = 'and'): self
{
return $this->compileWhere('Basic', $boolean, compact('column', 'operator', 'value'));
}

public function whereColumn(string $first, string $operator, string $second, string $boolean = 'and'): self
{
return $this->compileWhere('Column', $boolean, compact('first', 'operator', 'second'));
}

public function whereIn(string $column, array $values, string $boolean = 'and', bool $not = false): self
{
return $this->compileWhere($not ? 'NotIn' : 'In', $boolean, compact('column', 'values'));
}

public function whereNotIn(string $column, array $values = [], string $boolean = 'and'): self
{
return $this->whereIn($column, $values, $boolean, true);
}

public function whereNull(string $column, string $boolean = 'and', bool $not = false): self
{
return $this->compileWhere($not ? 'NotNull' : 'Null', $boolean, compact('column'));
}

public function whereBetween(string $column, array $values = [], string $boolean = 'and', bool $not = false): self
{
return $this->compileWhere('Between', $boolean, compact('column', 'values', 'not'));
}

public function whereNotBetween(string $column, array $values = [], string $boolean = 'and'): self
{
return $this->whereBetween($column, $values, $boolean, true);
}

public function whereNotNull(string $column, string $boolean = 'and'): self
{
return $this->whereNull($column, $boolean, true);
}

protected function compileWhere(string $type, string $boolean, array $parameters = []): self
{
$this->attributes['wheres'][] = array_merge(compact('type', 'boolean'), $parameters);
return $this;
}
}
22 changes: 22 additions & 0 deletions src/Schema/Definitions/UniqueDefinition.php
@@ -0,0 +1,22 @@
<?php

declare(strict_types=1);

namespace Umbrellio\Postgres\Schema\Definitions;

use Illuminate\Support\Fluent;

/**
* @method UniqueDefinition where($column, $operator, $value, $boolean = 'and')
* @method UniqueDefinition whereRaw($sql, $bindings = [], $boolean = 'and')
* @method UniqueDefinition whereColumn($first, $operator, $second, $boolean = 'and')
* @method UniqueDefinition whereIn($column, $values = [], $boolean = 'and', $not = false)
* @method UniqueDefinition whereNotIn($column, $values = [], $boolean = 'and')
* @method UniqueDefinition whereBetween($column, $values, $boolean = 'and', $not = false)
* @method UniqueDefinition whereNotBetween($column, $values, $boolean = 'and')
* @method UniqueDefinition whereNull($column, $boolean = 'and', $not = false)
* @method UniqueDefinition whereNotNull($column, $boolean = 'and')
*/
class UniqueDefinition extends Fluent
{
}
35 changes: 16 additions & 19 deletions src/Schema/Grammars/PostgresGrammar.php
Expand Up @@ -4,20 +4,18 @@

namespace Umbrellio\Postgres\Schema\Grammars;

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Schema\Grammars\PostgresGrammar as BasePostgresGrammar;
use Illuminate\Support\Fluent;
use Umbrellio\Postgres\Compilers\AttachPartitionCompiler;
use Umbrellio\Postgres\Compilers\CreateCompiler;
use Umbrellio\Postgres\Schema\Blueprint;
use Umbrellio\Postgres\Compilers\UniqueWhereCompiler;
use Umbrellio\Postgres\Schema\Builders\UniquePartialBuilder;
use Umbrellio\Postgres\Schema\Builders\UniqueWhereBuilder;

class PostgresGrammar extends BasePostgresGrammar
{
/**
* @param Blueprint|\Illuminate\Database\Schema\Blueprint $blueprint
* @param Fluent $command
* @return string
*/
public function compileCreate($blueprint, Fluent $command): string
public function compileCreate(Blueprint $blueprint, Fluent $command): string
{
$like = $this->getCommandByName($blueprint, 'like');
$ifNotExists = $this->getCommandByName($blueprint, 'ifNotExists');
Expand All @@ -30,26 +28,25 @@ public function compileCreate($blueprint, Fluent $command): string
);
}

/**
* @param Blueprint|\Illuminate\Database\Schema\Blueprint $blueprint
* @param Fluent $command
* @return string
*/
public function compileAttachPartition($blueprint, Fluent $command): string
public function compileAttachPartition(Blueprint $blueprint, Fluent $command): string
{
return AttachPartitionCompiler::compile($this, $blueprint, $command);
}

/**
* @param Blueprint|\Illuminate\Database\Schema\Blueprint $blueprint
* @param Fluent $command
* @return string
*/
public function compileDetachPartition($blueprint, Fluent $command): string
public function compileDetachPartition(Blueprint $blueprint, Fluent $command): string
{
return sprintf('alter table %s detach partition %s',
$this->wrapTable($blueprint),
$command->get('partition')
);
}

public function compileUniquePartial(Blueprint $blueprint, UniquePartialBuilder $command): string
{
$constraints = $command->get('constraints');
if ($constraints instanceof UniqueWhereBuilder) {
return UniqueWhereCompiler::compile($this, $blueprint, $command, $constraints);
}
return $this->compileUnique($blueprint, $command);
}
}

0 comments on commit e070c05

Please sign in to comment.