feat: PostgreSQL partitioning support for multi-tenant data isolation#235
feat: PostgreSQL partitioning support for multi-tenant data isolation#235
Conversation
…isolation - Add `database.partitioned` module type with PartitionedDatabase, PartitionKeyProvider and PartitionManager interfaces for managing PostgreSQL LIST partitions - Add `tenantKey` config to `step.db_query` and `step.db_exec` for automatic tenant scoping via PartitionKeyProvider - Add `step.db_create_partition` step for idempotent runtime partition creation - Add `appendTenantFilter` helper to sql_placeholders.go - Register all new types in storage plugin, pipelinesteps plugin, schema registry, coreModuleTypes/coreStepTypes, and type_registry - Add tests for new functionality Co-authored-by: intel352 <77607+intel352@users.noreply.github.com>
…teps Co-authored-by: intel352 <77607+intel352@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds first-class PostgreSQL LIST partitioning support to the workflow engine to enable multi-tenant isolation, including runtime partition creation and optional automatic tenant scoping in DB pipeline steps.
Changes:
- Introduces
database.partitionedmodule withPartitionKeyProvider/PartitionManagerand idempotentEnsurePartition. - Adds
step.db_create_partitionand enhancesstep.db_query/step.db_execwith optionaltenantKeyscoping. - Registers new module/step types across schema + plugins +
wfctltype registry and adds/updates tests.
Reviewed changes
Copilot reviewed 16 out of 21 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| schema/snippets_export.go | Formatting-only alignment adjustments. |
| schema/schema.go | Adds new core module/step types to schema type lists. |
| schema/module_schema.go | Registers schema for database.partitioned, adds tenantKey fields, adds step.db_create_partition schema. |
| plugins/storage/plugin_test.go | Updates expected module type/schema counts for storage plugin. |
| plugins/storage/plugin.go | Registers database.partitioned module type + factory + module schema in storage plugin. |
| plugins/pipelinesteps/plugin_test.go | Adds step.db_create_partition to expected steps list. |
| plugins/pipelinesteps/plugin.go | Registers step.db_create_partition in step types + factories. |
| module/sql_placeholders.go | Adds appendTenantFilter helper used for tenant scoping. |
| module/pipeline_step_token_revoke_test.go | Removes trailing blank line (formatting). |
| module/pipeline_step_sandbox_exec.go | Formatting-only alignment changes. |
| module/pipeline_step_db_tenant_test.go | New tests for tenantKey scoping and appendTenantFilter. |
| module/pipeline_step_db_query_test.go | Formatting-only alignment change. |
| module/pipeline_step_db_query.go | Adds tenantKey support and applies tenant filter + extra param. |
| module/pipeline_step_db_exec.go | Adds tenantKey support and applies tenant filter + extra param. |
| module/pipeline_step_db_create_partition.go | New step.db_create_partition implementation. |
| module/kafka_broker.go | Import ordering + formatting alignment. |
| module/http_server.go | Formatting-only alignment changes. |
| module/database_partitioned_test.go | New tests for partitioned database behaviors + create-partition step. |
| module/database_partitioned.go | New database.partitioned module implementation. |
| module/cache_redis.go | Formatting-only alignment change. |
| cmd/wfctl/type_registry.go | Registers new module/step types + config keys in CLI registry. |
| // isSupportedPartitionDriver returns true for PostgreSQL-compatible drivers. | ||
| func isSupportedPartitionDriver(driver string) bool { | ||
| switch driver { | ||
| case "pgx", "pgx/v5", "postgres", "postgresql": | ||
| return true | ||
| } | ||
| return false | ||
| } |
There was a problem hiding this comment.
isSupportedPartitionDriver treats "postgresql" as supported, but elsewhere the codebase (e.g. database.workflow schema/options and sql placeholder normalization) only recognizes "postgres"/"pgx"/"pgx/v5" as Postgres driver names. Unless there is a real sql driver registered under "postgresql" in this repo, consider removing it here (and in the test) or adding it consistently across driver detection + schema options.
| // Apply automatic tenant scoping when tenantKey is configured. | ||
| query := s.query | ||
| if s.tenantKey != "" { | ||
| pkp, ok := svc.(PartitionKeyProvider) | ||
| if !ok { | ||
| return nil, fmt.Errorf("db_exec step %q: tenantKey requires database %q to implement PartitionKeyProvider (use database.partitioned)", s.name, s.database) | ||
| } | ||
| partKey := pkp.PartitionKey() | ||
| if err := validateIdentifier(partKey); err != nil { | ||
| return nil, fmt.Errorf("db_exec step %q: invalid partition key %q: %w", s.name, partKey, err) | ||
| } | ||
| tenantVal := resolveBodyFrom(s.tenantKey, pc) | ||
| if tenantVal == nil { | ||
| return nil, fmt.Errorf("db_exec step %q: tenantKey %q resolved to nil in pipeline context", s.name, s.tenantKey) | ||
| } | ||
| tenantStr := fmt.Sprintf("%v", tenantVal) | ||
| nextParam := len(resolvedParams) + 1 | ||
| query = appendTenantFilter(query, partKey, nextParam) | ||
| resolvedParams = append(resolvedParams, tenantStr) | ||
| } |
There was a problem hiding this comment.
tenantKey scoping is applied to all db_exec statements by blindly appending WHERE/AND. This will generate invalid SQL for INSERT statements (and for any statement types that don't support WHERE). Consider either rejecting tenantKey for non-UPDATE/DELETE statements (with a clear error) or implementing statement-aware handling (e.g., require the tenant column be part of INSERT values instead of appending a filter).
| func TestAppendTenantFilter_TrailingWhitespace(t *testing.T) { | ||
| query := "SELECT * FROM forms ORDER BY created_at " | ||
| result := appendTenantFilter(query, "tenant_id", 1) | ||
| expected := "SELECT * FROM forms ORDER BY created_at WHERE tenant_id = $1" | ||
| if result != expected { | ||
| t.Errorf("expected %q, got %q", expected, result) | ||
| } | ||
| } |
There was a problem hiding this comment.
This test currently codifies behavior that generates invalid SQL (placing WHERE after ORDER BY). Once tenant scoping is supported for typical SELECTs with ORDER BY/LIMIT, appendTenantFilter should insert the tenant predicate before ORDER BY and this expectation should be updated accordingly.
|
@copilot apply changes based on the comments in this thread
|
- Fix appendTenantFilter to insert tenant predicate before ORDER BY/
LIMIT/GROUP BY/HAVING/OFFSET clauses instead of blindly appending
- Reject tenantKey for INSERT statements in step.db_exec with clear error
- Remove "postgresql" from isSupportedPartitionDriver for consistency
- Fix schema tenantKey placeholders to use "steps." prefix consistently
- Add partitionType config (list/range) with RANGE partition DDL support
- Add partitionNameFormat config ({table}_{tenant}, {tenant}_{table}, etc.)
- Add PartitionTableName method to PartitionKeyProvider interface
- Add sourceTable/sourceColumn config for auto-partition sync
- Add SyncPartitionsFromSource method to PartitionManager interface
- Add step.db_sync_partitions for triggering partition sync from source
- Add comprehensive tests for all new functionality
Co-authored-by: intel352 <77607+intel352@users.noreply.github.com>
All three requests addressed in commit d440278: 1. Dynamic table name format — Added config:
partitionNameFormat: "{tenant}_{table}"2. Auto-partition from source table — Added config:
sourceTable: tenants
sourceColumn: id3. RANGE partition support — Added |
|
@copilot Address the merge conflict on the branch |
Merge features from main (allow_dynamic_sql, auth.m2m custom claims) with our partitioning features (tenantKey, database.partitioned, step.db_create_partition, step.db_sync_partitions). Both db_query and db_exec now support both allow_dynamic_sql and tenantKey simultaneously. Schema includes all fields from both feature branches. Co-authored-by: intel352 <77607+intel352@users.noreply.github.com>
In commit 6a2a32d, I've incorporated all changes from However, I'm unable to resolve the Git-level merge conflict directly because I cannot run |
appendTenantFilterto handle ORDER BY / LIMIT / GROUP BY / HAVING clauses properlytenantKeyfor INSERT statements instep.db_execisSupportedPartitionDriverfor consistencytenantKeyplaceholders to use "steps." prefix consistentlypartitionNameFormatfor dynamic table name resolutionpartitionTypeconfig to distinguish LIST vs RANGE partitioningstep.db_sync_partitionsfor syncing partitions from source tableOriginal prompt
🔒 GitHub Advanced Security automatically protects Copilot coding agent pull requests. You can protect all pull requests by enabling Advanced Security for your repositories. Learn more about Advanced Security.