3838use Flow \PostgreSql \Migrations \Store \MigrationStore ;
3939use Flow \PostgreSql \Migrations \VersionGenerator \TimestampVersionGenerator ;
4040use Flow \PostgreSql \Migrations \VersionResolver ;
41+ use Flow \PostgreSql \Schema \Exclusion \AnyExclusionPolicy ;
42+ use Flow \PostgreSql \Schema \Exclusion \EndsWithExclusionPolicy ;
43+ use Flow \PostgreSql \Schema \Exclusion \ExactMatchExclusionPolicy ;
44+ use Flow \PostgreSql \Schema \Exclusion \PatternExclusionPolicy ;
45+ use Flow \PostgreSql \Schema \Exclusion \ScopedExclusionPolicy ;
46+ use Flow \PostgreSql \Schema \Exclusion \SchemaObjectType ;
47+ use Flow \PostgreSql \Schema \Exclusion \StartsWithExclusionPolicy ;
48+ use Flow \PostgreSql \Schema \Exclusion \WholeSchemaExclusionPolicy ;
4149use Flow \Telemetry \Provider \Clock \SystemClock ;
4250use LogicException ;
4351use Override ;
@@ -324,6 +332,50 @@ public function configure(DefinitionConfigurator $definition): void
324332 ->defaultTrue ()
325333 ->info ('Generate rollback files when creating migrations (default: true) ' )
326334 ->end ()
335+ ->arrayNode ('exclude ' )
336+ ->info ('Schema objects excluded from migration diffing (e.g. tables created dynamically at runtime). Each entry defines exactly one matcher: schema, table, exact, starts_with, ends_with, pattern or policy_id. ' )
337+ ->arrayPrototype ()
338+ ->children ()
339+ ->scalarNode ('schema ' )
340+ ->defaultNull ()
341+ ->info ('Exclude an entire schema with all of its objects. ' )
342+ ->end ()
343+ ->scalarNode ('table ' )
344+ ->defaultNull ()
345+ ->info ('Exclude a table by exact name (shorthand for exact scoped to tables). ' )
346+ ->end ()
347+ ->scalarNode ('exact ' )
348+ ->defaultNull ()
349+ ->info ('Exclude any object whose name matches exactly. ' )
350+ ->end ()
351+ ->scalarNode ('starts_with ' )
352+ ->defaultNull ()
353+ ->info ('Exclude objects whose name starts with this prefix. ' )
354+ ->end ()
355+ ->scalarNode ('ends_with ' )
356+ ->defaultNull ()
357+ ->info ('Exclude objects whose name ends with this suffix. ' )
358+ ->end ()
359+ ->scalarNode ('pattern ' )
360+ ->defaultNull ()
361+ ->info ('Exclude objects whose name matches this PCRE pattern (with delimiters). ' )
362+ ->end ()
363+ ->scalarNode ('policy_id ' )
364+ ->defaultNull ()
365+ ->info ('Service ID of a custom Flow \\PostgreSql \\Schema \\Exclusion \\ExclusionPolicy. ' )
366+ ->end ()
367+ ->enumNode ('type ' )
368+ ->values (['table ' , 'view ' , 'materialized_view ' , 'sequence ' , 'function ' , 'procedure ' , 'domain ' , 'extension ' ])
369+ ->defaultNull ()
370+ ->info ('Narrow the matcher to a single object type (ignored when using "schema" or "table"). ' )
371+ ->end ()
372+ ->scalarNode ('for_schema ' )
373+ ->defaultNull ()
374+ ->info ('Narrow the matcher to a single schema. ' )
375+ ->end ()
376+ ->end ()
377+ ->end ()
378+ ->end ()
327379 ->end ()
328380 ->end ()
329381 ->arrayNode ('catalog_providers ' )
@@ -347,7 +399,7 @@ public function configure(DefinitionConfigurator $definition): void
347399 }
348400
349401 /**
350- * @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}, catalog_providers: list<array{catalog_provider_id: ?string, catalog: ?array<string, mixed>}>} $config
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
351403 */
352404 #[Override]
353405 public function loadExtension (array $ config , ContainerConfigurator $ configurator , ContainerBuilder $ container ): void
@@ -578,7 +630,7 @@ private function registerMessenger(
578630 }
579631
580632 /**
581- * @param 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} $mc
633+ * @param 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}> } $mc
582634 */
583635 private function registerMigrations (string $ name , array $ mc , ContainerBuilder $ container , bool $ isFirst ): void
584636 {
@@ -595,6 +647,7 @@ private function registerMigrations(string $name, array $mc, ContainerBuilder $c
595647 $ mc ['rollback_file_name ' ],
596648 $ mc ['all_or_nothing ' ],
597649 $ mc ['generate_rollback ' ],
650+ $ this ->buildExclusionPolicy ($ mc ['exclude ' ] ?? []),
598651 ]);
599652 $ configDef ->setPublic (true );
600653 $ container ->setDefinition ("flow.postgresql. {$ name }.migrations.configuration " , $ configDef );
@@ -684,6 +737,61 @@ private function registerMigrations(string $name, array $mc, ContainerBuilder $c
684737 }
685738 }
686739
740+ /**
741+ * @param list<array{schema: ?string, table: ?string, exact: ?string, starts_with: ?string, ends_with: ?string, pattern: ?string, policy_id: ?string, type: ?string, for_schema: ?string}> $exclude
742+ */
743+ private function buildExclusionPolicy (array $ exclude ): ?Definition
744+ {
745+ if ($ exclude === []) {
746+ return null ;
747+ }
748+
749+ $ policies = [];
750+
751+ foreach ($ exclude as $ entry ) {
752+ $ policies [] = $ this ->buildExclusionPolicyEntry ($ entry );
753+ }
754+
755+ return new Definition (AnyExclusionPolicy::class, $ policies );
756+ }
757+
758+ /**
759+ * @param array{schema: ?string, table: ?string, exact: ?string, starts_with: ?string, ends_with: ?string, pattern: ?string, policy_id: ?string, type: ?string, for_schema: ?string} $entry
760+ */
761+ private function buildExclusionPolicyEntry (array $ entry ): Definition |Reference
762+ {
763+ if ($ entry ['schema ' ] !== null ) {
764+ return new Definition (WholeSchemaExclusionPolicy::class, [$ entry ['schema ' ]]);
765+ }
766+
767+ $ impliedType = null ;
768+
769+ if ($ entry ['table ' ] !== null ) {
770+ $ matcher = new Definition (ExactMatchExclusionPolicy::class, [$ entry ['table ' ]]);
771+ $ impliedType = SchemaObjectType::TABLE ;
772+ } elseif ($ entry ['exact ' ] !== null ) {
773+ $ matcher = new Definition (ExactMatchExclusionPolicy::class, [$ entry ['exact ' ]]);
774+ } elseif ($ entry ['starts_with ' ] !== null ) {
775+ $ matcher = new Definition (StartsWithExclusionPolicy::class, [$ entry ['starts_with ' ]]);
776+ } elseif ($ entry ['ends_with ' ] !== null ) {
777+ $ matcher = new Definition (EndsWithExclusionPolicy::class, [$ entry ['ends_with ' ]]);
778+ } elseif ($ entry ['pattern ' ] !== null ) {
779+ $ matcher = new Definition (PatternExclusionPolicy::class, [$ entry ['pattern ' ]]);
780+ } elseif ($ entry ['policy_id ' ] !== null ) {
781+ $ matcher = new Reference ($ entry ['policy_id ' ]);
782+ } else {
783+ throw new LogicException ('Each "flow_postgresql.migrations.exclude" entry must define exactly one of: schema, table, exact, starts_with, ends_with, pattern, policy_id. ' );
784+ }
785+
786+ $ type = $ entry ['type ' ] !== null ? SchemaObjectType::from ($ entry ['type ' ]) : $ impliedType ;
787+
788+ if ($ type === null && $ entry ['for_schema ' ] === null ) {
789+ return $ matcher ;
790+ }
791+
792+ return new Definition (ScopedExclusionPolicy::class, [$ matcher , $ type , $ entry ['for_schema ' ]]);
793+ }
794+
687795 /**
688796 * @param 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} $sessionConfig
689797 * @param list<string> $connectionNames
0 commit comments