Skip to content

Commit a972636

Browse files
authored
Feat: Database migration process (#598)
* feat: database setup command * single source for tables structures * Improve initialization flow * refactor * add is active mode * projection v2 table manager * introduce reference extension object * fixes to tests * in memory * fix event streaming adapter * change naming * clean up * refactor * automatic table initialization * refactor in progress * rewritten * ecotone lite for dbal message channel * tracker * tracker * fixes * auto init * tests
1 parent 6183427 commit a972636

File tree

65 files changed

+2821
-838
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

65 files changed

+2821
-838
lines changed

packages/Dbal/src/Configuration/DbalConfiguration.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ class DbalConfiguration
5151
private int $minimumTimeToRemoveMessageInMilliseconds = DeduplicationModule::REMOVE_MESSAGE_AFTER_7_DAYS;
5252
private int $deduplicationRemovalBatchSize = 1000;
5353

54+
private bool $initializeDatabaseTables = true;
55+
5456
private function __construct()
5557
{
5658
}
@@ -357,4 +359,21 @@ public function getConsumerPositionTrackingConnectionReference(): string
357359
{
358360
return $this->consumerPositionTrackingConnectionReference;
359361
}
362+
363+
/**
364+
* Controls whether database tables are automatically initialized on first use.
365+
* When set to false, tables must be created manually using `ecotone:migration:database:setup --initialize`.
366+
*/
367+
public function withAutomaticTableInitialization(bool $enabled): self
368+
{
369+
$self = clone $this;
370+
$self->initializeDatabaseTables = $enabled;
371+
372+
return $self;
373+
}
374+
375+
public function isAutomaticTableInitializationEnabled(): bool
376+
{
377+
return $this->initializeDatabaseTables;
378+
}
360379
}

packages/Dbal/src/Configuration/DbalPublisherModule.php

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
namespace Ecotone\Dbal\Configuration;
44

55
use Ecotone\AnnotationFinder\AnnotationFinder;
6+
use Ecotone\Dbal\Database\DbalTableManagerReference;
7+
use Ecotone\Dbal\Database\EnqueueTableManager;
8+
use Ecotone\Dbal\DbalBackedMessageChannelBuilder;
69
use Ecotone\Dbal\DbalOutboundChannelAdapterBuilder;
710
use Ecotone\Messaging\Attribute\ModuleAnnotation;
811
use Ecotone\Messaging\Config\Annotation\AnnotationModule;
@@ -44,6 +47,21 @@ public function prepare(Configuration $messagingConfiguration, array $extensionO
4447
$registeredReferences = [];
4548
$applicationConfiguration = ExtensionObjectResolver::resolveUnique(ServiceConfiguration::class, $extensionObjects, ServiceConfiguration::createWithDefaults());
4649

50+
$dbalConfiguration = ExtensionObjectResolver::resolveUnique(DbalConfiguration::class, $extensionObjects, DbalConfiguration::createWithDefaults());
51+
$dbalMessageChannels = ExtensionObjectResolver::resolve(DbalBackedMessageChannelBuilder::class, $extensionObjects);
52+
$dbalPublishers = ExtensionObjectResolver::resolve(DbalMessagePublisherConfiguration::class, $extensionObjects);
53+
$hasMessageQueues = ! empty($dbalMessageChannels) || ! empty($dbalPublishers);
54+
$shouldAutoInitialize = $dbalConfiguration->isAutomaticTableInitializationEnabled();
55+
56+
$messagingConfiguration->registerServiceDefinition(
57+
EnqueueTableManager::class,
58+
new \Ecotone\Messaging\Config\Container\Definition(EnqueueTableManager::class, [
59+
EnqueueTableManager::DEFAULT_TABLE_NAME,
60+
$hasMessageQueues,
61+
$shouldAutoInitialize,
62+
])
63+
);
64+
4765
foreach (ExtensionObjectResolver::resolve(DbalMessagePublisherConfiguration::class, $extensionObjects) as $dbalPublisher) {
4866
if (in_array($dbalPublisher->getReferenceName(), $registeredReferences)) {
4967
throw ConfigurationException::create("Registering two publishers under same reference name {$dbalPublisher->getReferenceName()}. You need to create publisher with specific reference using `createWithReferenceName`.");
@@ -109,12 +127,16 @@ public function canHandle($extensionObject): bool
109127
{
110128
return
111129
$extensionObject instanceof DbalMessagePublisherConfiguration
112-
|| $extensionObject instanceof ServiceConfiguration;
130+
|| $extensionObject instanceof ServiceConfiguration
131+
|| $extensionObject instanceof DbalBackedMessageChannelBuilder
132+
|| $extensionObject instanceof DbalConfiguration;
113133
}
114134

115135
public function getModuleExtensions(ServiceConfiguration $serviceConfiguration, array $serviceExtensions): array
116136
{
117-
return [];
137+
return [
138+
new DbalTableManagerReference(EnqueueTableManager::class),
139+
];
118140
}
119141

120142
public function getModulePackageName(): string
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Ecotone\Dbal\Database;
6+
7+
use Ecotone\Messaging\Attribute\ConsoleCommand;
8+
use Ecotone\Messaging\Attribute\ConsoleParameterOption;
9+
use Ecotone\Messaging\Config\ConsoleCommandResultSet;
10+
11+
/**
12+
* Console command handler for database drop operations.
13+
*
14+
* licence Apache-2.0
15+
*/
16+
class DatabaseDropCommand
17+
{
18+
public function __construct(
19+
private DatabaseSetupManager $databaseSetupManager,
20+
) {
21+
}
22+
23+
#[ConsoleCommand('ecotone:migration:database:drop')]
24+
public function drop(
25+
#[ConsoleParameterOption] bool $force = false,
26+
#[ConsoleParameterOption] bool $all = false,
27+
): ?ConsoleCommandResultSet {
28+
$featureNames = $this->databaseSetupManager->getFeatureNames($all);
29+
30+
if (count($featureNames) === 0) {
31+
return ConsoleCommandResultSet::create(
32+
['Status'],
33+
[['No database tables registered for drop.']]
34+
);
35+
}
36+
37+
if ($force) {
38+
$this->databaseSetupManager->dropAll($all);
39+
return ConsoleCommandResultSet::create(
40+
['Feature', 'Status'],
41+
array_map(fn (string $feature) => [$feature, 'Dropped'], $featureNames)
42+
);
43+
}
44+
45+
return ConsoleCommandResultSet::create(
46+
['Feature', 'Warning'],
47+
array_map(fn (string $feature) => [$feature, 'Would be dropped (use --force to confirm)'], $featureNames)
48+
);
49+
}
50+
}
51+
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Ecotone\Dbal\Database;
6+
7+
use Ecotone\Messaging\Attribute\ConsoleCommand;
8+
use Ecotone\Messaging\Attribute\ConsoleParameterOption;
9+
use Ecotone\Messaging\Config\ConsoleCommandResultSet;
10+
11+
/**
12+
* Console command handler for database setup operations.
13+
*
14+
* licence Apache-2.0
15+
*/
16+
class DatabaseSetupCommand
17+
{
18+
public function __construct(
19+
private DatabaseSetupManager $databaseSetupManager,
20+
) {
21+
}
22+
23+
#[ConsoleCommand('ecotone:migration:database:setup')]
24+
public function setup(
25+
#[ConsoleParameterOption] bool $initialize = false,
26+
#[ConsoleParameterOption] bool $sql = false,
27+
#[ConsoleParameterOption] bool $all = false,
28+
): ?ConsoleCommandResultSet {
29+
$featureNames = $this->databaseSetupManager->getFeatureNames($all);
30+
31+
if (count($featureNames) === 0) {
32+
return ConsoleCommandResultSet::create(
33+
['Status'],
34+
[['No database tables registered for setup.']]
35+
);
36+
}
37+
38+
if ($sql) {
39+
$statements = $this->databaseSetupManager->getCreateSqlStatements($all);
40+
return ConsoleCommandResultSet::create(
41+
['SQL Statement'],
42+
array_map(fn (string $statement) => [$statement], $statements)
43+
);
44+
}
45+
46+
if ($initialize) {
47+
$this->databaseSetupManager->initializeAll($all);
48+
return ConsoleCommandResultSet::create(
49+
['Feature', 'Status'],
50+
array_map(fn (string $feature) => [$feature, 'Created'], $featureNames)
51+
);
52+
}
53+
54+
$initializationStatus = $this->databaseSetupManager->getInitializationStatus($all);
55+
$rows = [];
56+
foreach ($featureNames as $featureName) {
57+
$isInitialized = $initializationStatus[$featureName] ?? false;
58+
$rows[] = [$featureName, $isInitialized ? 'Yes' : 'No'];
59+
}
60+
61+
return ConsoleCommandResultSet::create(
62+
['Feature', 'Initialized'],
63+
$rows
64+
);
65+
}
66+
}
67+
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Ecotone\Dbal\Database;
6+
7+
use Doctrine\DBAL\Connection;
8+
use Ecotone\Dbal\DbalReconnectableConnectionFactory;
9+
use Ecotone\Messaging\Config\Container\DefinedObject;
10+
use Ecotone\Messaging\Config\Container\Definition;
11+
use Enqueue\Dbal\DbalContext;
12+
use Interop\Queue\ConnectionFactory;
13+
14+
/**
15+
* Manages database setup and teardown for all registered table managers.
16+
*
17+
* licence Apache-2.0
18+
*/
19+
class DatabaseSetupManager implements DefinedObject
20+
{
21+
/**
22+
* @param DbalTableManager[] $tableManagers
23+
*/
24+
public function __construct(
25+
private ConnectionFactory $connectionFactory,
26+
private array $tableManagers = [],
27+
) {
28+
}
29+
30+
/**
31+
* @return string[] List of feature names that require database tables
32+
*/
33+
public function getFeatureNames(bool $includeInactive = false): array
34+
{
35+
return array_map(
36+
fn (DbalTableManager $manager) => $manager->getFeatureName(),
37+
$this->getManagers($includeInactive)
38+
);
39+
}
40+
41+
/**
42+
* @return string[] SQL statements to create all tables
43+
*/
44+
public function getCreateSqlStatements(bool $includeInactive = false): array
45+
{
46+
$connection = $this->getConnection();
47+
$statements = [];
48+
49+
foreach ($this->getManagers($includeInactive) as $manager) {
50+
$sql = $manager->getCreateTableSql($connection);
51+
if (is_array($sql)) {
52+
$statements = array_merge($statements, $sql);
53+
} else {
54+
$statements[] = $sql;
55+
}
56+
}
57+
58+
return $statements;
59+
}
60+
61+
/**
62+
* @return string[] SQL statements to drop all tables
63+
*/
64+
public function getDropSqlStatements(bool $includeInactive = false): array
65+
{
66+
$connection = $this->getConnection();
67+
$statements = [];
68+
69+
foreach ($this->getManagers($includeInactive) as $manager) {
70+
$statements[] = $manager->getDropTableSql($connection);
71+
}
72+
73+
return $statements;
74+
}
75+
76+
/**
77+
* Creates all tables.
78+
*/
79+
public function initializeAll(bool $includeInactive = false): void
80+
{
81+
$connection = $this->getConnection();
82+
83+
foreach ($this->getManagers($includeInactive) as $manager) {
84+
if ($manager->isInitialized($connection)) {
85+
continue;
86+
}
87+
88+
$manager->createTable($connection);
89+
}
90+
}
91+
92+
/**
93+
* Drops all tables.
94+
*/
95+
public function dropAll(bool $includeInactive = false): void
96+
{
97+
$connection = $this->getConnection();
98+
99+
foreach ($this->getManagers($includeInactive) as $manager) {
100+
$manager->dropTable($connection);
101+
}
102+
}
103+
104+
/**
105+
* Returns initialization status for each table manager.
106+
*
107+
* @return array<string, bool> Map of feature name to initialization status
108+
*/
109+
public function getInitializationStatus(bool $includeInactive = false): array
110+
{
111+
$connection = $this->getConnection();
112+
$status = [];
113+
114+
foreach ($this->getManagers($includeInactive) as $manager) {
115+
$status[$manager->getFeatureName()] = $manager->isInitialized($connection);
116+
}
117+
118+
return $status;
119+
}
120+
121+
/**
122+
* @return DbalTableManager[]
123+
*/
124+
private function getManagers(bool $includeInactive): array
125+
{
126+
if ($includeInactive) {
127+
return $this->tableManagers;
128+
}
129+
130+
return array_filter(
131+
$this->tableManagers,
132+
fn (DbalTableManager $manager) => $manager->isActive()
133+
);
134+
}
135+
136+
private function getConnection(): Connection
137+
{
138+
/** @var DbalContext $context */
139+
$context = $this->connectionFactory->createContext();
140+
141+
return $context->getDbalConnection();
142+
}
143+
144+
public function getDefinition(): Definition
145+
{
146+
$tableManagerDefinitions = array_map(
147+
fn (DbalTableManager $manager) => $manager->getDefinition(),
148+
$this->tableManagers
149+
);
150+
151+
return new Definition(
152+
self::class,
153+
[
154+
new Definition(DbalReconnectableConnectionFactory::class, [
155+
$this->connectionFactory,
156+
]),
157+
$tableManagerDefinitions,
158+
]
159+
);
160+
}
161+
}
162+

0 commit comments

Comments
 (0)