Skip to content

Commit ff5aecd

Browse files
authored
Merge pull request #2412 from flow-php/postgresql-db-suffix
feature: allow to prefix postgresql db name
2 parents 9b1867e + 1691dbf commit ff5aecd

8 files changed

Lines changed: 397 additions & 4 deletions

File tree

documentation/components/bridges/symfony-postgresql-bundle.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,56 @@ flow_postgresql:
4949
dsn: '%env(DATABASE_URL)%'
5050
```
5151
52+
### Connection Overrides
53+
54+
Each connection may override individual parts of the parsed DSN. Every key maps 1:1 onto an immutable
55+
`ConnectionParameters` method in the `flow-php/postgresql` library; the bundle adds no logic of its own.
56+
Overrides are applied at service-factory time, so values may be `%env(...)%` placeholders — they are
57+
resolved at runtime, not when the configuration is parsed.
58+
59+
```yaml
60+
flow_postgresql:
61+
connections:
62+
default:
63+
dsn: '%env(DATABASE_URL)%'
64+
65+
dbname: null # Replaces the database name parsed from the DSN
66+
host: null # Replaces the host parsed from the DSN
67+
port: null # Replaces the port parsed from the DSN
68+
user: null # Replaces the user parsed from the DSN
69+
password: null # Replaces the password parsed from the DSN
70+
dbname_suffix: '' # Appends the given suffix to the configured database name
71+
```
72+
73+
| Key | Type | Default | Effect |
74+
|-----------------|--------|---------|-----------------------------------------------------------|
75+
| `dbname` | string | `null` | Replaces the database name parsed from the DSN. |
76+
| `host` | string | `null` | Replaces the host parsed from the DSN. |
77+
| `port` | int | `null` | Replaces the port parsed from the DSN. |
78+
| `user` | string | `null` | Replaces the user parsed from the DSN. |
79+
| `password` | string | `null` | Replaces the password parsed from the DSN. |
80+
| `dbname_suffix` | string | `''` | Appends the given suffix to the configured database name. |
81+
82+
`dbname`/`host`/`port`/`user`/`password` are applied first, then `dbname_suffix` **last** — so
83+
`dbname: 'foo'` + `dbname_suffix: '_test'` yields `foo_test`. A connection with only `dsn` behaves
84+
exactly as before.
85+
86+
#### `dbname_suffix` for parallel tests
87+
88+
```yaml
89+
# config/packages/flow_postgresql.yaml
90+
flow_postgresql:
91+
connections:
92+
default:
93+
dsn: '%env(DATABASE_ANALYTICAL_URL)%'
94+
95+
when@test:
96+
flow_postgresql:
97+
connections:
98+
default:
99+
dbname_suffix: '_test%env(default::TEST_TOKEN)%'
100+
```
101+
52102
### Telemetry
53103

54104
Enable telemetry per connection to get distributed tracing, query logging, and metrics.

documentation/components/libs/postgresql/client-connection.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,44 @@ $client = pgsql_client(
9898
);
9999
```
100100

101+
## Overriding Parsed Parameters
102+
103+
`ConnectionParameters` is immutable. Each `with*()` method returns a new instance, so a parsed DSN
104+
can be adjusted without re-parsing.
105+
106+
```php
107+
<?php
108+
109+
use Flow\PostgreSql\Client\DsnParser;
110+
111+
$params = (new DsnParser())->parse('postgresql://user:pass@localhost:5432/mydb')
112+
->withHost('db.internal')
113+
->withPort(5544)
114+
->withUser('svc')
115+
->withPassword('secret')
116+
->withDatabase('analytics');
117+
```
118+
119+
### Database Suffix
120+
121+
`withDatabaseSuffix()` appends a suffix to the configured database name. The base DSN stays constant
122+
and only the suffix changes per environment, which makes per-worker parallel-test databases work
123+
(`mydb``mydb_test7`) without a second DSN. The suffix is appended verbatim — nothing is inserted
124+
automatically, so include a separator yourself if you want one (e.g. `_test7`). An empty suffix is a
125+
no-op:
126+
127+
```php
128+
<?php
129+
130+
use Flow\PostgreSql\Client\DsnParser;
131+
132+
$params = (new DsnParser())->parse('postgresql://user:pass@localhost:5432/mydb')
133+
->withDatabaseSuffix('_test7'); // database() === 'mydb_test7'
134+
```
135+
136+
When combined with `withDatabase()`, apply the override first — the suffix is appended to the
137+
overridden name (`withDatabase('other')->withDatabaseSuffix('_test')``other_test`).
138+
101139
## Connection Lifecycle
102140

103141
### Checking Connection Status
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Flow\Bridge\Symfony\PostgreSqlBundle\Connection;
6+
7+
use Flow\PostgreSql\Client\ConnectionParameters;
8+
use Flow\PostgreSql\Client\DsnParser;
9+
10+
final class ConnectionParametersFactory
11+
{
12+
/**
13+
* @param array{dbname?: ?string, host?: ?string, port?: ?int, user?: ?string, password?: ?string, dbname_suffix?: string} $overrides
14+
*/
15+
public static function create(DsnParser $parser, string $dsn, array $overrides = []): ConnectionParameters
16+
{
17+
$params = $parser->parse($dsn);
18+
19+
if (($overrides['dbname'] ?? null) !== null) {
20+
$params = $params->withDatabase($overrides['dbname']);
21+
}
22+
23+
if (($overrides['host'] ?? null) !== null) {
24+
$params = $params->withHost($overrides['host']);
25+
}
26+
27+
if (($overrides['port'] ?? null) !== null) {
28+
$params = $params->withPort($overrides['port']);
29+
}
30+
31+
if (($overrides['user'] ?? null) !== null) {
32+
$params = $params->withUser($overrides['user']);
33+
}
34+
35+
if (($overrides['password'] ?? null) !== null) {
36+
$params = $params->withPassword($overrides['password']);
37+
}
38+
39+
return $params->withDatabaseSuffix($overrides['dbname_suffix'] ?? '');
40+
}
41+
}

src/bridge/symfony/postgresql-bundle/src/Flow/Bridge/Symfony/PostgreSqlBundle/FlowPostgreSqlBundle.php

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use Flow\Bridge\Symfony\PostgreSqlBundle\Attribute\AsCatalogProvider;
99
use Flow\Bridge\Symfony\PostgreSqlBundle\CatalogProvider\ArrayCatalogProvider;
1010
use Flow\Bridge\Symfony\PostgreSqlBundle\Command\SessionPurgeCommand;
11+
use Flow\Bridge\Symfony\PostgreSqlBundle\Connection\ConnectionParametersFactory;
1112
use Flow\Bridge\Symfony\PostgreSqlBundle\DependencyInjection\Compiler\CatalogProviderPass;
1213
use Flow\Bridge\Symfony\PostgreSqlBundle\DependencyInjection\Compiler\CommandLocatorPass;
1314
use Flow\Bridge\Symfony\PostgreSqlBundle\Generator\TwigMigrationGenerator;
@@ -107,6 +108,30 @@ public function configure(DefinitionConfigurator $definition): void
107108
->cannotBeEmpty()
108109
->info('PostgreSQL connection DSN (e.g. postgresql://user:pass@localhost:5432/dbname)')
109110
->end()
111+
->scalarNode('dbname')
112+
->defaultNull()
113+
->info('Overrides the database name parsed from the DSN.')
114+
->end()
115+
->scalarNode('host')
116+
->defaultNull()
117+
->info('Overrides the host parsed from the DSN.')
118+
->end()
119+
->integerNode('port')
120+
->defaultNull()
121+
->info('Overrides the port parsed from the DSN.')
122+
->end()
123+
->scalarNode('user')
124+
->defaultNull()
125+
->info('Overrides the user parsed from the DSN.')
126+
->end()
127+
->scalarNode('password')
128+
->defaultNull()
129+
->info('Overrides the password parsed from the DSN.')
130+
->end()
131+
->scalarNode('dbname_suffix')
132+
->defaultValue('')
133+
->info('Adds the given suffix to the configured database name.')
134+
->end()
110135
->booleanNode('test_transaction_rollback')
111136
->defaultFalse()
112137
->info(
@@ -399,7 +424,7 @@ public function configure(DefinitionConfigurator $definition): void
399424
}
400425

401426
/**
402-
* @param array{connections: array<string, array{dsn: string, test_transaction_rollback: bool, context?: array<string, mixed>, telemetry?: array{service_id: string, clock_service_id: ?string, trace_queries: bool, trace_transactions: bool, collect_metrics: bool, log_queries: bool, max_query_length: int, include_parameters: bool, max_parameters: int, max_parameter_length: int}}>, messenger: array{enabled: bool, table_name: string, schema: string}, cache: array{pools?: array<string, array{connection: ?string, table_name: string, schema: string, id_col: string, data_col: string, lifetime_col: string, time_col: string, namespace: string, default_lifetime: int, marshaller_service_id: ?string, share_connection: bool}>}, session: array{enabled: bool, connection: ?string, table_name: string, schema: string, id_col: string, data_col: string, lifetime_col: string, time_col: string, lock_mode: string, ttl: ?int, share_connection: bool}, migrations: array{enabled: bool, directory: string, namespace: string, table_name: string, table_schema: string, migration_file_name: string, rollback_file_name: string, all_or_nothing: bool, generate_rollback: bool, exclude?: list<array{schema: ?string, table: ?string, exact: ?string, starts_with: ?string, ends_with: ?string, pattern: ?string, policy_id: ?string, type: ?string, for_schema: ?string}>}, catalog_providers: list<array{catalog_provider_id: ?string, catalog: ?array<string, mixed>}>} $config
427+
* @param array{connections: array<string, array{dsn: string, dbname: ?string, host: ?string, port: ?int, user: ?string, password: ?string, dbname_suffix: string, test_transaction_rollback: bool, context?: array<string, mixed>, telemetry?: array{service_id: string, clock_service_id: ?string, trace_queries: bool, trace_transactions: bool, collect_metrics: bool, log_queries: bool, max_query_length: int, include_parameters: bool, max_parameters: int, max_parameter_length: int}}>, messenger: array{enabled: bool, table_name: string, schema: string}, cache: array{pools?: array<string, array{connection: ?string, table_name: string, schema: string, id_col: string, data_col: string, lifetime_col: string, time_col: string, namespace: string, default_lifetime: int, marshaller_service_id: ?string, share_connection: bool}>}, session: array{enabled: bool, connection: ?string, table_name: string, schema: string, id_col: string, data_col: string, lifetime_col: string, time_col: string, lock_mode: string, ttl: ?int, share_connection: bool}, migrations: array{enabled: bool, directory: string, namespace: string, table_name: string, table_schema: string, migration_file_name: string, rollback_file_name: string, all_or_nothing: bool, generate_rollback: bool, exclude?: list<array{schema: ?string, table: ?string, exact: ?string, starts_with: ?string, ends_with: ?string, pattern: ?string, policy_id: ?string, type: ?string, for_schema: ?string}>}, catalog_providers: list<array{catalog_provider_id: ?string, catalog: ?array<string, mixed>}>} $config
403428
*/
404429
#[Override]
405430
public function loadExtension(array $config, ContainerConfigurator $configurator, ContainerBuilder $container): void
@@ -537,7 +562,7 @@ private function registerCatalogProviders(array $catalogProviders, ContainerBuil
537562
}
538563

539564
/**
540-
* @param array{dsn: string, test_transaction_rollback: bool, context?: array<string, mixed>, telemetry?: array{service_id: string, clock_service_id: ?string, trace_queries: bool, trace_transactions: bool, collect_metrics: bool, log_queries: bool, max_query_length: int, include_parameters: bool, max_parameters: int, max_parameter_length: int}} $connectionConfig
565+
* @param array{dsn: string, dbname: ?string, host: ?string, port: ?int, user: ?string, password: ?string, dbname_suffix: string, test_transaction_rollback: bool, context?: array<string, mixed>, telemetry?: array{service_id: string, clock_service_id: ?string, trace_queries: bool, trace_transactions: bool, collect_metrics: bool, log_queries: bool, max_query_length: int, include_parameters: bool, max_parameters: int, max_parameter_length: int}} $connectionConfig
541566
*/
542567
private function registerConnection(
543568
string $name,
@@ -549,8 +574,19 @@ private function registerConnection(
549574
$container->setDefinition("flow.postgresql.{$name}.dsn_parser", $parserDef);
550575

551576
$paramsDef = new Definition(ConnectionParameters::class);
552-
$paramsDef->setFactory([new Reference("flow.postgresql.{$name}.dsn_parser"), 'parse']);
553-
$paramsDef->setArguments([$connectionConfig['dsn']]);
577+
$paramsDef->setFactory([ConnectionParametersFactory::class, 'create']);
578+
$paramsDef->setArguments([
579+
new Reference("flow.postgresql.{$name}.dsn_parser"),
580+
$connectionConfig['dsn'],
581+
[
582+
'dbname' => $connectionConfig['dbname'] ?? null,
583+
'host' => $connectionConfig['host'] ?? null,
584+
'port' => $connectionConfig['port'] ?? null,
585+
'user' => $connectionConfig['user'] ?? null,
586+
'password' => $connectionConfig['password'] ?? null,
587+
'dbname_suffix' => $connectionConfig['dbname_suffix'] ?? '',
588+
],
589+
]);
554590
$container->setDefinition("flow.postgresql.{$name}.connection_parameters", $paramsDef);
555591

556592
$paramsDef->setPublic(true);

src/bridge/symfony/postgresql-bundle/tests/Flow/Bridge/Symfony/PostgreSqlBundle/Tests/Integration/FlowPostgreSqlExtensionTest.php

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -651,6 +651,117 @@ public function test_client_without_telemetry_is_not_decorated(): void
651651
static::assertFalse($this->getContainer()->has('flow.postgresql.default.telemetry.config'));
652652
}
653653

654+
public function test_connection_only_dsn_behaves_as_before(): void
655+
{
656+
$this->bootKernel([
657+
'config' => static function (TestKernel $kernel): void {
658+
$kernel->addTestExtensionConfig('flow_postgresql', [
659+
'connections' => [
660+
'default' => [
661+
'dsn' => 'postgresql://postgres:postgres@localhost:5432/app',
662+
],
663+
],
664+
]);
665+
},
666+
]);
667+
668+
$params = $this->getContainer()->get('flow.postgresql.default.connection_parameters');
669+
static::assertSame('app', $params->database());
670+
static::assertSame('localhost', $params->host());
671+
}
672+
673+
public function test_connection_parameters_apply_dbname_override_before_suffix(): void
674+
{
675+
$this->bootKernel([
676+
'config' => static function (TestKernel $kernel): void {
677+
$kernel->addTestExtensionConfig('flow_postgresql', [
678+
'connections' => [
679+
'default' => [
680+
'dsn' => 'postgresql://postgres:postgres@localhost:5432/app',
681+
'dbname' => 'other',
682+
'dbname_suffix' => '_test',
683+
],
684+
],
685+
]);
686+
},
687+
]);
688+
689+
static::assertSame(
690+
'other_test',
691+
$this->getContainer()->get('flow.postgresql.default.connection_parameters')->database(),
692+
);
693+
}
694+
695+
public function test_connection_parameters_apply_dsn_part_overrides(): void
696+
{
697+
$this->bootKernel([
698+
'config' => static function (TestKernel $kernel): void {
699+
$kernel->addTestExtensionConfig('flow_postgresql', [
700+
'connections' => [
701+
'default' => [
702+
'dsn' => 'postgresql://user:pass@localhost:5432/app',
703+
'host' => 'db.internal',
704+
'port' => 5544,
705+
'user' => 'svc',
706+
'password' => 'pw',
707+
],
708+
],
709+
]);
710+
},
711+
]);
712+
713+
$params = $this->getContainer()->get('flow.postgresql.default.connection_parameters');
714+
static::assertSame('db.internal', $params->host());
715+
static::assertSame(5544, $params->port());
716+
static::assertSame('svc', $params->user());
717+
static::assertSame('pw', $params->password());
718+
}
719+
720+
public function test_connection_parameters_apply_dbname_suffix(): void
721+
{
722+
$this->bootKernel([
723+
'config' => static function (TestKernel $kernel): void {
724+
$kernel->addTestExtensionConfig('flow_postgresql', [
725+
'connections' => [
726+
'default' => [
727+
'dsn' => 'postgresql://postgres:postgres@localhost:5432/app',
728+
'dbname_suffix' => '_test',
729+
],
730+
],
731+
]);
732+
},
733+
]);
734+
735+
static::assertSame(
736+
'app_test',
737+
$this->getContainer()->get('flow.postgresql.default.connection_parameters')->database(),
738+
);
739+
}
740+
741+
public function test_connection_parameters_resolve_dbname_suffix_from_env(): void
742+
{
743+
$this->bootKernel([
744+
'config' => static function (TestKernel $kernel): void {
745+
$kernel->addTestContainerConfigurator(static function (ContainerBuilder $container): void {
746+
$container->setParameter('env(FLOW_TEST_DB_SUFFIX)', '_test7');
747+
});
748+
$kernel->addTestExtensionConfig('flow_postgresql', [
749+
'connections' => [
750+
'default' => [
751+
'dsn' => 'postgresql://postgres:postgres@localhost:5432/app',
752+
'dbname_suffix' => '%env(FLOW_TEST_DB_SUFFIX)%',
753+
],
754+
],
755+
]);
756+
},
757+
]);
758+
759+
static::assertSame(
760+
'app_test7',
761+
$this->getContainer()->get('flow.postgresql.default.connection_parameters')->database(),
762+
);
763+
}
764+
654765
public function test_connection_with_migrations_registers_migration_services(): void
655766
{
656767
$this->bootKernel([

0 commit comments

Comments
 (0)