Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Changelog

## [Unreleased]

### Added

- `Formal\Access\Layer\Query\Select::columns()` now accepts row values
- `Formal\Access\Layer\Query\Insert::into()` now accepts a `Select` query to specify the rows to insert

## 4.1.0 - 2025-03-21

### Added
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
},
"require-dev": {
"innmind/static-analysis": "^1.2.1",
"innmind/black-box": "^5.6.1|^6.0.2",
"innmind/black-box": "^5.8|^6.0.2",
"innmind/coding-standard": "~2.0"
},
"conflict": {
Expand Down
24 changes: 24 additions & 0 deletions documentation/queries/insert.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,27 @@ $connection($insert(Sequence::of(
```
!!! warning ""
Each `Row` must specify the same amount of columns and in the same order, otherwise it will fail.

## Select insert

`Insert` allows you to insert multiple values at once coming from another table via a `Select` query.

```php
use Formal\AccessLayer\{
Query\Insert,
Query\Select,
Table\Name,
Table\Column,
};
$insert = Insert::into(
Name::of('users'),
Select::from('leads')->columns(
Column\Name::of('username'),
Column\Name::of('name'),
),
);

$connection($insert);
```

This example copies all `leads` as new `users`.
30 changes: 30 additions & 0 deletions documentation/queries/select.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,33 @@ $users = $connection($select);
The property name can include the name of the table to match by using the format `'{table}.{column}'`.

The value of the specification can also be a query (this will translated to a sub query).

## Select inline values

On top of specifying the columns to fetch from the table you can specify values as virtual columns. This is very useful when using a `Select` to [insert multiple values at once](insert.md#select-insert).

```php
use Formal\AccesLayer\{
Query\Select,
Table\Name,
Table\Column,
Row,
};

$select = Select::from(Name::of('users'))->columns(
Column\Name::of('id'),
Row\Value::of(
Column\Name::of('score'),
0,
),
);
```

This will return as many rows as you have users and with a `0` in the `score` column.

You could use this query to populate a new table `users_score`.

??? warning
PostgreSQL is more strict than MySQL when specifying inline strings. It will fail when it deems strings to not be valid utf8 strings.

Initially this feature was tested against any [unicode string](https://innmind.org/BlackBox/sets/#__tabbed_2_3) but PostgreSQL would regularly fail on some of them. It may only be some character blocks that cause problems. But for the time being it's now only tested on alphanumerical characters.
2 changes: 2 additions & 0 deletions properties/Connection.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ public static function list(): array
Connection\AQueryWithoutTheCorrectNumberOfParametersMustThrow::class,
Connection\MustThrowWhenValueDoesntFitTheSchema::class,
Connection\Insert::class,
Connection\InsertSelect::class,
Connection\MultipleInsertsAtOnce::class,
Connection\ParametersCanBeBoundByName::class,
Connection\ParametersCanBeBoundByIndex::class,
Expand Down Expand Up @@ -71,6 +72,7 @@ public static function list(): array
Connection\SelectOffset::class,
Connection\SelectLimit::class,
Connection\SelectOrder::class,
Connection\SelectValues::class,
Connection\Update::class,
Connection\UpdateSpecificRow::class,
Connection\Delete::class,
Expand Down
124 changes: 124 additions & 0 deletions properties/Connection/InsertSelect.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
<?php
declare(strict_types = 1);

namespace Properties\Formal\AccessLayer\Connection;

use Formal\AccessLayer\{
Query\SQL,
Query,
Table,
Row,
Connection,
};
use Innmind\Specification\{
Comparator,
Sign,
};
use Innmind\BlackBox\{
Property,
Set,
Runner\Assert,
};

/**
* @implements Property<Connection>
*/
final class InsertSelect implements Property
{
private function __construct(
private string $uuid,
private string $username,
private int $number,
private string $value,
) {
}

public static function any(): Set
{
return Set\Composite::immutable(
static fn(...$args) => new self(...$args),
Set\Uuid::any(),
Set\Strings::madeOf(Set\Chars::alphanumerical())->between(0, 100),
Set\Integers::any(),
Set\Strings::madeOf(Set\Chars::alphanumerical())->between(10, 100), // to avoid collisions
);
}

public function applicableTo(object $connection): bool
{
return true;
}

public function ensureHeldBy(Assert $assert, object $connection): object
{
$select = SQL::of("SELECT * FROM test_values WHERE id = '{$this->uuid}'");
$rows = $connection($select);

$assert->count(0, $rows);

$sequence = $connection(Query\Insert::into(
Table\Name::of('test'),
Row::of([
'id' => $this->uuid,
'username' => $this->username,
'registerNumber' => $this->number,
]),
));

$assert->count(0, $sequence);

$sequence = $connection(Query\Insert::into(
Table\Name::of('test_values'),
Query\Select::from(Table\Name::of('test'))
->columns(
Row\Value::of(
Table\Column\Name::of('value'),
$this->value,
),
Table\Column\Name::of('id'),
)
->where(Comparator\Property::of(
'id',
Sign::equality,
$this->uuid,
)),
));

$assert->count(0, $sequence);

$rows = $connection($select);

$assert->count(1, $rows);
$assert->same(
$this->uuid,
$rows
->first()
->flatMap(static fn($row) => $row->column('id'))
->match(
static fn($id) => $id,
static fn() => null,
),
);
$assert->same(
$this->value,
$rows
->first()
->flatMap(static fn($row) => $row->column('value'))
->match(
static fn($username) => $username,
static fn() => null,
),
);

$assert
->array(
$connection(SQL::of("SELECT * FROM test_values WHERE id <> '{$this->uuid}'"))
->flatMap(static fn($row) => $row->column('value')->toSequence())
->toList(),
)
->not()
->contains($this->value);

return $connection;
}
}
150 changes: 150 additions & 0 deletions properties/Connection/SelectValues.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
<?php
declare(strict_types = 1);

namespace Properties\Formal\AccessLayer\Connection;

use Formal\AccessLayer\{
Query\Insert,
Query\Select,
Table\Name,
Table\Column,
Row,
Connection,
};
use Innmind\Specification\{
Comparator,
Sign,
};
use Innmind\BlackBox\{
Property,
Set,
Runner\Assert,
};
use Fixtures\Formal\AccessLayer\Table\Column\Name as FName;

/**
* @implements Property<Connection>
*/
final class SelectValues implements Property
{
private function __construct(
private string $uuid,
private string $username,
private int $number,
private $valueName,
private int|string|bool|null $value,
) {
}

public static function any(): Set
{
return Set\Composite::immutable(
static fn(...$args) => new self(...$args),
Set\Uuid::any(),
Set\Strings::madeOf(Set\Chars::ascii())->between(0, 255),
Set\Integers::any(),
FName::any(),
Set\Either::any(
Set\Integers::any(),
Set\Strings::madeOf(Set\Chars::alphanumerical()),
Set\Elements::of(null, true, false),
),
);
}

public function applicableTo(object $connection): bool
{
return true;
}

public function ensureHeldBy(Assert $assert, object $connection): object
{
$connection(Insert::into(
Name::of('test'),
Row::of([
'id' => $this->uuid,
'username' => $this->username,
'registerNumber' => $this->number,
]),
));

$select = Select::from(Name::of('test'))
->columns(
Column\Name::of('id'),
Column\Name::of('username'),
Column\Name::of('registerNumber'),
Row\Value::of(
$this->valueName,
$this->value,
),
)
->where(Comparator\Property::of(
'id',
Sign::equality,
$this->uuid,
));
$rows = $connection($select);

$assert->count(1, $rows);
$assert->same(
$this->uuid,
$rows
->first()
->flatMap(static fn($row) => $row->column('id'))
->match(
static fn($value) => $value,
static fn() => null,
),
);
$assert->same(
$this->username,
$rows
->first()
->flatMap(static fn($row) => $row->column('username'))
->match(
static fn($value) => $value,
static fn() => null,
),
);
$assert->same(
$this->number,
$rows
->first()
->flatMap(static fn($row) => $row->column('registerNumber'))
->match(
static fn($value) => $value,
static fn() => null,
),
);

$value = $rows
->first()
->flatMap(fn($row) => $row->column($this->valueName->toString()))
->match(
static fn($value) => $value,
static fn() => null,
);

// Custom assertions here due to the way the different drivers interpret
// them and return them.
// Since selecting inline values should be used in an insert query the
// drivers should handle casting values to the correct types internally.
if ($this->value === true) {
$assert
->array([1, 't'])
->contains($value);
} else if ($this->value === false) {
$assert
->array([0, 'f'])
->contains($value);
} else if (\is_int($this->value)) {
$assert
->array([$this->value, (string) $this->value])
->contains($value);
} else {
$assert->same($this->value, $value);
}

return $connection;
}
}
Loading