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
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
}
],
"require": {
"phpnomad/db": "dev-release/2.2 as 2.2.x-dev",
"phpnomad/db": "dev-codex/operation-local-handler-bridge#4b0b7709e5945a4290b04c2e3562ef919923ed08 as 2.2.x-dev",
"phpnomad/loader": "^1.0 || ^2.0",
"phpnomad/logger": "^1.0",
"ext-pdo": "*"
Expand Down
10 changes: 5 additions & 5 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions lib/Interfaces/ProvidesOperationBuilders.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

namespace PHPNomad\MySql\Integration\Interfaces;

use PHPNomad\Database\Interfaces\ClauseBuilder;
use PHPNomad\Database\Interfaces\QueryBuilder;
use PHPNomad\Database\Interfaces\QueryStrategy;

/** @internal Builder access for the exact active coordinated query handle. */
interface ProvidesOperationBuilders
{
/** @return array{QueryBuilder, ClauseBuilder} */
public function createOperationBuilders(QueryStrategy $queryStrategy): array;
}
34 changes: 34 additions & 0 deletions lib/PdoCoordinationInitializer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

namespace PHPNomad\MySql\Integration;

use PHPNomad\Database\Interfaces\CoordinatedQueryStrategy as CoreCoordinatedQueryStrategy;
use PHPNomad\Database\Interfaces\OperationDatabaseProviderFactory;
use PHPNomad\Database\Interfaces\QueryStrategy as CoreQueryStrategy;
use PHPNomad\Loader\Interfaces\HasClassDefinitions;
use PHPNomad\MySql\Integration\Interfaces\CoordinatedDatabaseStrategy;
use PHPNomad\MySql\Integration\Interfaces\DatabaseStrategy;
use PHPNomad\MySql\Integration\Interfaces\ProvidesOperationBuilders;
use PHPNomad\MySql\Integration\Services\MySqlOperationDatabaseProviderFactory;
use PHPNomad\MySql\Integration\Strategies\CoordinatedQueryStrategy;
use PHPNomad\MySql\Integration\Strategies\PdoCoordinatedDatabaseStrategy;

/** Optional PDO coordination bindings. Load after MySqlInitializer. */
final class PdoCoordinationInitializer implements HasClassDefinitions
{
public function getClassDefinitions(): array
{
return [
PdoCoordinatedDatabaseStrategy::class => [
DatabaseStrategy::class,
CoordinatedDatabaseStrategy::class,
],
CoordinatedQueryStrategy::class => [
CoreQueryStrategy::class,
CoreCoordinatedQueryStrategy::class,
ProvidesOperationBuilders::class,
],
MySqlOperationDatabaseProviderFactory::class => OperationDatabaseProviderFactory::class,
];
}
}
39 changes: 39 additions & 0 deletions lib/Services/MySqlOperationDatabaseProviderFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

namespace PHPNomad\MySql\Integration\Services;

use PHPNomad\Database\Interfaces\DatabaseHandler;
use PHPNomad\Database\Interfaces\OperationDatabaseProviderFactory;
use PHPNomad\Database\Interfaces\QueryStrategy;
use PHPNomad\Database\Providers\DatabaseServiceProvider;
use PHPNomad\Database\Services\OperationCacheableService;
use PHPNomad\Database\Services\OperationEventStrategy;
use PHPNomad\MySql\Integration\Interfaces\ProvidesOperationBuilders;

/** Creates fresh MySQL builders for one operation-bound handler. */
final class MySqlOperationDatabaseProviderFactory implements OperationDatabaseProviderFactory
{
private ProvidesOperationBuilders $coordinator;

public function __construct(ProvidesOperationBuilders $coordinator)
{
$this->coordinator = $coordinator;
}

public function create(
DatabaseHandler $handler,
QueryStrategy $queryStrategy,
OperationCacheableService $cache,
OperationEventStrategy $events
): DatabaseServiceProvider {
[$queryBuilder, $clauseBuilder] = $this->coordinator->createOperationBuilders($queryStrategy);

return $handler->getDatabaseServiceProvider()->forOperation(
$queryStrategy,
$queryBuilder,
$clauseBuilder,
$cache,
$events
);
}
}
87 changes: 87 additions & 0 deletions lib/Strategies/CoordinatedQueryStrategy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<?php

namespace PHPNomad\MySql\Integration\Strategies;

use PHPNomad\Database\Interfaces\CoordinatedQueryStrategy as CoreCoordinatedQueryStrategy;
use PHPNomad\Database\Interfaces\QueryStrategy as CoreQueryStrategy;
use PHPNomad\Database\Interfaces\Table;
use PHPNomad\Database\Exceptions\UnsupportedCoordinationException;
use PHPNomad\Database\Services\TableSchemaService;
use PHPNomad\Database\Strategies\OperationQueryStrategy;
use PHPNomad\MySql\Integration\Builders\MySqlClauseBuilder;
use PHPNomad\MySql\Integration\Builders\QueryBuilder;
use PHPNomad\MySql\Integration\Interfaces\CoordinatedDatabaseStrategy;
use PHPNomad\MySql\Integration\Interfaces\DatabaseStrategy;
use PHPNomad\MySql\Integration\Interfaces\ProvidesOperationBuilders;

/** MySQL query coordination over one backend-owned database operation. */
final class CoordinatedQueryStrategy extends QueryStrategy implements CoreCoordinatedQueryStrategy, ProvidesOperationBuilders
{
private CoordinatedDatabaseStrategy $coordinatedDatabaseStrategy;

private ?OperationQueryStrategy $activeQueryStrategy = null;

private ?DatabaseStrategy $activeDatabaseStrategy = null;

public function __construct(
CoordinatedDatabaseStrategy $db,
TableSchemaService $tableSchemaService,
\PHPNomad\Database\Interfaces\ClauseBuilder $clauseBuilder
) {
parent::__construct($db, $tableSchemaService, $clauseBuilder);
$this->coordinatedDatabaseStrategy = $db;
}

/**
* @template TResult
* @param non-empty-array<string, int|string> $identity
* @param non-empty-list<Table> $participants
* @param callable(CoreQueryStrategy): TResult $operation
* @return TResult
*/
public function coordinate(
Table $coordinationTable,
array $identity,
array $participants,
callable $operation
) {
return $this->coordinatedDatabaseStrategy->coordinate(
$coordinationTable,
$identity,
$participants,
function (DatabaseStrategy $database) use ($participants, $operation) {
$delegate = new OperationBoundQueryStrategy(
$database,
$this->tableSchemaService,
new MySqlClauseBuilder()
);
$queryStrategy = new OperationQueryStrategy($delegate, $participants);
$this->activeQueryStrategy = $queryStrategy;
$this->activeDatabaseStrategy = $database;

try {
return $operation($queryStrategy);
} finally {
$this->activeQueryStrategy = null;
$this->activeDatabaseStrategy = null;
$queryStrategy->close();
}
}
);
}

/** @return array{\PHPNomad\Database\Interfaces\QueryBuilder, \PHPNomad\Database\Interfaces\ClauseBuilder} */
public function createOperationBuilders(CoreQueryStrategy $queryStrategy): array
{
if (
$queryStrategy !== $this->activeQueryStrategy
|| $this->activeDatabaseStrategy === null
) {
throw new UnsupportedCoordinationException(
'Operation builders require the exact active coordinated query strategy.'
);
}

return [new QueryBuilder(), new MySqlClauseBuilder()];
}
}
68 changes: 68 additions & 0 deletions lib/Strategies/OperationBoundQueryStrategy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php

namespace PHPNomad\MySql\Integration\Strategies;

use PHPNomad\Database\Exceptions\QueryBuilderException;
use PHPNomad\Database\Exceptions\UnsupportedCoordinationException;
use PHPNomad\Database\Interfaces\QueryBuilder;
use PHPNomad\Database\Interfaces\Table;
use PHPNomad\Datastore\Exceptions\DatastoreErrorException;
use PHPNomad\Datastore\Exceptions\RecordNotFoundException;
use PHPNomad\MySql\Integration\Interfaces\CanBuildWithDatabaseStrategy;
use PHPNomad\Utils\Helpers\Arr;

/** Query operations fixed to the backend owned by one coordinated attempt. */
final class OperationBoundQueryStrategy extends QueryStrategy
{
/** @return array<array-key, mixed> */
public function query(QueryBuilder $builder): array
{
if (!$builder instanceof CanBuildWithDatabaseStrategy) {
throw new UnsupportedCoordinationException('Coordinated queries need a resource-bound query builder.');
}

try {
$query = $builder->buildWithDatabaseStrategy($this->db);
/** @var array<array-key, mixed> $result */
$result = $this->db->query($query);
} catch (QueryBuilderException $e) {
throw new DatastoreErrorException('Get results failed. Invalid query: ' . $e->getMessage(), 500, $e);
}

if (empty($result)) {
throw new RecordNotFoundException();
}

return $result;
}

/**
* Insert through the owned backend without starting or ending a transaction.
*
* @param array<string, mixed> $data
* @return array<string, int>
*/
public function insert(Table $table, array $data): array
{
$columns = Arr::process($data)
->keys()
->map(fn (string $column): string => $this->db->parse('?n', $column))
->setSeparator(',')
->toString();

$placeholders = Arr::process($data)
->map(fn () => '?s')
->setSeparator(',')
->toString();

$query = $this->db->parse(
"INSERT INTO ?n ($columns) VALUES ($placeholders)",
$table->getName(),
...Arr::values($data)
);

$this->db->query($query);

return $this->resolveInsertIdentity($table, $data);
}
}
2 changes: 1 addition & 1 deletion lib/Strategies/QueryStrategy.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ protected function resolveInsertIdentity(Table $table, array $data)
{
/** @var array<string, int> $identity */
$identity = [];
$primaryColumns = $this->tableSchemaService->getPrimaryColumnsForTable($table);
$primaryColumns = $this->tableSchemaService->getPrimaryColumnsForTableUncached($table);

foreach ($primaryColumns as $column) {
$name = $column->getName();
Expand Down
25 changes: 22 additions & 3 deletions lib/Strategies/TableExistsStrategy.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,30 @@ public function __construct(DatabaseStrategy $db)
public function exists(string $tableName): bool
{
try {
$query = $this->db->parse("SHOW TABLES LIKE ?s", $tableName);
return $this->db->query($query)->fetchColumn() === $tableName;

$query = $this->db->parse(
'SELECT TABLE_NAME AS table_name FROM information_schema.TABLES'
. ' WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?s',
$tableName
);
$rows = $this->db->query($query);
} catch (DatastoreErrorException $e) {
return false;
}

if (!is_array($rows)) {
throw new \UnexpectedValueException('The table metadata query must return an array.');
}

if ($rows === []) {
return false;
}

$firstRow = reset($rows);
if (count($rows) !== 1 || !is_array($firstRow) || !array_key_exists('table_name', $firstRow)
|| !is_string($firstRow['table_name'])) {
throw new \UnexpectedValueException('The table metadata query returned an invalid row.');
}

return $firstRow['table_name'] === $tableName;
}
}
Loading
Loading