diff --git a/docs/workflow/online-ddl.mdx b/docs/workflow/online-ddl.mdx index 356caf33..b75fdfba 100644 --- a/docs/workflow/online-ddl.mdx +++ b/docs/workflow/online-ddl.mdx @@ -115,6 +115,8 @@ ALTER TABLE users VALIDATE CONSTRAINT users_email_not_null; The constraint name matches what PostgreSQL generates for a `NOT NULL` column in `CREATE TABLE`, so the migrated table converges with a freshly created one. +If the `VALIDATE` step never ran (an interrupted apply, or a `NOT VALID` constraint added by hand), the column already reads as `NOT NULL` in the catalog, but existing rows are unchecked. pgschema detects the unvalidated constraint on the next `plan` and emits just the `VALIDATE CONSTRAINT` step, so the migration can be finished by re-applying. + On PostgreSQL 14-17, adding `NOT NULL` uses a check constraint based process: ```sql diff --git a/internal/diff/column.go b/internal/diff/column.go index 53d5934e..3b822952 100644 --- a/internal/diff/column.go +++ b/internal/diff/column.go @@ -67,6 +67,13 @@ func (cd *ColumnDiff) generateColumnSQL(tableSchema, tableName string, targetSch qualifiedTableName, ir.QuoteIdentifier(cd.New.Name)) statements = append(statements, sql) } + } else if !cd.New.IsNullable && cd.Old.InvalidNotNullConstraint != "" { + // Both sides are NOT NULL, but the current constraint was added NOT VALID + // (PG18+) and never validated, e.g. an interrupted online apply. Finish + // the job so existing rows are actually checked (issue #564). + sql := fmt.Sprintf("ALTER TABLE %s VALIDATE CONSTRAINT %s;", + qualifiedTableName, ir.QuoteIdentifier(cd.Old.InvalidNotNullConstraint)) + statements = append(statements, sql) } // Handle default value changes @@ -173,6 +180,11 @@ func columnsEqual(old, new *ir.Column, targetSchema string) bool { if old.IsNullable != new.IsNullable { return false } + // A NOT NULL constraint that is still NOT VALID must be validated to reach + // the desired NOT NULL state (issue #564). + if !new.IsNullable && old.InvalidNotNullConstraint != "" { + return false + } // Compare default values (already normalized by ir.normalizeColumn) if (old.DefaultValue == nil) != (new.DefaultValue == nil) { diff --git a/internal/plan/rewrite.go b/internal/plan/rewrite.go index 3126b1ae..09315aab 100644 --- a/internal/plan/rewrite.go +++ b/internal/plan/rewrite.go @@ -100,6 +100,20 @@ func generateRewrite(d diff.Diff, newlyCreatedTables map[string]bool, newlyCreat } } } + // A pending VALIDATE CONSTRAINT for a NOT VALID NOT NULL constraint + // (issue #564) must run in its own transaction like every other + // VALIDATE step, so the scan is not batched with lock-taking DDL. + if !columnDiff.New.IsNullable && columnDiff.Old.InvalidNotNullConstraint != "" { + for _, stmt := range d.Statements { + if strings.Contains(stmt.SQL, "VALIDATE CONSTRAINT") { + return []RewriteStep{{ + SQL: stmt.SQL, + CanRunInTransaction: true, + RequiresIsolation: true, + }} + } + } + } // Check if identity is being added or changed on an existing column // This includes: adding identity, or changing identity generation (drop + re-add) if columnDiff.New.Identity != nil { diff --git a/internal/plan/rewrite_test.go b/internal/plan/rewrite_test.go index ca57fea8..2b1d1629 100644 --- a/internal/plan/rewrite_test.go +++ b/internal/plan/rewrite_test.go @@ -4,6 +4,7 @@ import ( "strings" "testing" + "github.com/pgplex/pgschema/internal/diff" "github.com/pgplex/pgschema/ir" ) @@ -181,3 +182,36 @@ func TestGenerateColumnNotNullRewriteNameCollision(t *testing.T) { } }) } + +// TestPendingNotNullValidateIsolated verifies that a VALIDATE CONSTRAINT step +// emitted for a NOT NULL constraint that was added NOT VALID and never +// validated (issue #564) runs in its own execution group, like every other +// VALIDATE step, rather than being batched with surrounding DDL. +func TestPendingNotNullValidateIsolated(t *testing.T) { + colDiff := &diff.ColumnDiff{ + Old: &ir.Column{Name: "phone", DataType: "text", IsNullable: false, InvalidNotNullConstraint: "users_phone_not_null"}, + New: &ir.Column{Name: "phone", DataType: "text", IsNullable: false}, + } + validate := diff.Diff{ + Type: diff.DiffTypeTableColumn, + Operation: diff.DiffOperationAlter, + Path: "public.users.phone", + Source: colDiff, + Statements: []diff.SQLStatement{{SQL: "ALTER TABLE users VALIDATE CONSTRAINT users_phone_not_null;"}}, + } + other := diff.Diff{ + Type: diff.DiffTypeTableColumn, + Operation: diff.DiffOperationAlter, + Path: "public.users.email", + Source: &diff.ColumnDiff{Old: &ir.Column{Name: "email"}, New: &ir.Column{Name: "email"}}, + Statements: []diff.SQLStatement{{SQL: "ALTER TABLE users ALTER COLUMN email SET DEFAULT '';"}}, + } + + groups := groupDiffs([]diff.Diff{other, validate}, 18, nil) + if len(groups) != 2 { + t.Fatalf("got %d groups, want 2 (VALIDATE must be isolated): %+v", len(groups), groups) + } + if got := groups[1].Steps[0].SQL; got != validate.Statements[0].SQL { + t.Errorf("isolated step SQL = %q, want %q", got, validate.Statements[0].SQL) + } +} diff --git a/ir/inspector.go b/ir/inspector.go index 8d487790..85e8c1a5 100644 --- a/ir/inspector.go +++ b/ir/inspector.go @@ -331,6 +331,9 @@ func (i *Inspector) buildColumns(ctx context.Context, schema *IR, targetSchema s IsNullable: i.safeInterfaceToString(col.IsNullable) == "YES", Comment: comment, } + if col.InvalidNotNullConstraint.Valid { + column.InvalidNotNullConstraint = col.InvalidNotNullConstraint.String + } // Handle generated columns first (attgenerated: 's' = STORED, 'v' = VIRTUAL in PG18+) attgenerated := i.safeInterfaceToString(col.Attgenerated) diff --git a/ir/ir.go b/ir/ir.go index 7140dba3..d2eb6ec3 100644 --- a/ir/ir.go +++ b/ir/ir.go @@ -98,6 +98,13 @@ type Column struct { GeneratedExpr *string `json:"generated_expr,omitempty"` // Expression for generated columns IsGenerated bool `json:"is_generated,omitempty"` // True if this is a generated column GeneratedKind string `json:"generated_kind,omitempty"` // "s" for STORED, "v" for VIRTUAL (PG18+) + // InvalidNotNullConstraint is the name of a NOT NULL constraint on this + // column that was added NOT VALID and has not been validated yet (PG18+). + // The column already reads as NOT NULL (attnotnull is set), so without this + // the pending VALIDATE CONSTRAINT would be invisible to the diff (issue #564). + // Only ever set on the current state; a freshly created desired state has no + // invalid constraints. + InvalidNotNullConstraint string `json:"invalid_not_null_constraint,omitempty"` // IsSerial is true when the column was created with the SERIAL shorthand: // its default is nextval() on a sequence that is owned by this column // (pg_depend) and that carries PostgreSQL's default __seq diff --git a/ir/queries/queries.sql b/ir/queries/queries.sql index 78e2639b..9525a7f0 100644 --- a/ir/queries/queries.sql +++ b/ir/queries/queries.sql @@ -72,6 +72,12 @@ WITH column_base AS ( c.numeric_scale, c.udt_name, COALESCE(d.description, '') AS column_comment, + -- Name of a NOT NULL constraint on this column that was added NOT VALID + -- and never validated (PostgreSQL 18+; contype 'n' does not exist before + -- that, so the join simply yields ''). attnotnull is already set for such + -- a column, so this is the only signal that VALIDATE CONSTRAINT is still + -- pending (issue #564). + COALESCE(nn.conname, '') AS invalid_not_null_constraint, CASE WHEN dt.typtype = 'd' THEN quote_ident(dn.nspname) || '.' || quote_ident(dt.typname) @@ -118,6 +124,7 @@ WITH column_base AS ( LEFT JOIN pg_namespace dn ON dt.typnamespace = dn.oid LEFT JOIN pg_type et ON dt.typelem = et.oid LEFT JOIN pg_namespace en ON et.typnamespace = en.oid + LEFT JOIN pg_constraint nn ON nn.conrelid = cl.oid AND nn.contype = 'n' AND NOT nn.convalidated AND a.attnum = ANY(nn.conkey) WHERE c.table_schema NOT IN ('information_schema', 'pg_catalog', 'pg_toast') AND c.table_schema NOT LIKE 'pg_temp_%' @@ -137,6 +144,7 @@ SELECT cb.numeric_scale, cb.udt_name, cb.column_comment, + cb.invalid_not_null_constraint, cb.resolved_type, cb.is_identity, cb.identity_generation, @@ -186,6 +194,12 @@ WITH column_base AS ( c.numeric_scale, c.udt_name, COALESCE(d.description, '') AS column_comment, + -- Name of a NOT NULL constraint on this column that was added NOT VALID + -- and never validated (PostgreSQL 18+; contype 'n' does not exist before + -- that, so the join simply yields ''). attnotnull is already set for such + -- a column, so this is the only signal that VALIDATE CONSTRAINT is still + -- pending (issue #564). + COALESCE(nn.conname, '') AS invalid_not_null_constraint, CASE WHEN dt.typtype = 'd' THEN quote_ident(dn.nspname) || '.' || quote_ident(dt.typname) @@ -233,6 +247,7 @@ WITH column_base AS ( LEFT JOIN pg_namespace dn ON dt.typnamespace = dn.oid LEFT JOIN pg_type et ON dt.typelem = et.oid LEFT JOIN pg_namespace en ON et.typnamespace = en.oid + LEFT JOIN pg_constraint nn ON nn.conrelid = cl.oid AND nn.contype = 'n' AND NOT nn.convalidated AND a.attnum = ANY(nn.conkey) WHERE c.table_schema = $1 ) @@ -250,6 +265,7 @@ SELECT cb.numeric_scale, cb.udt_name, cb.column_comment, + cb.invalid_not_null_constraint, cb.resolved_type, cb.is_identity, cb.identity_generation, diff --git a/ir/queries/queries.sql.go b/ir/queries/queries.sql.go index e37427f8..20bc5eb8 100644 --- a/ir/queries/queries.sql.go +++ b/ir/queries/queries.sql.go @@ -387,6 +387,12 @@ WITH column_base AS ( c.numeric_scale, c.udt_name, COALESCE(d.description, '') AS column_comment, + -- Name of a NOT NULL constraint on this column that was added NOT VALID + -- and never validated (PostgreSQL 18+; contype 'n' does not exist before + -- that, so the join simply yields ''). attnotnull is already set for such + -- a column, so this is the only signal that VALIDATE CONSTRAINT is still + -- pending (issue #564). + COALESCE(nn.conname, '') AS invalid_not_null_constraint, CASE WHEN dt.typtype = 'd' THEN quote_ident(dn.nspname) || '.' || quote_ident(dt.typname) @@ -433,6 +439,7 @@ WITH column_base AS ( LEFT JOIN pg_namespace dn ON dt.typnamespace = dn.oid LEFT JOIN pg_type et ON dt.typelem = et.oid LEFT JOIN pg_namespace en ON et.typnamespace = en.oid + LEFT JOIN pg_constraint nn ON nn.conrelid = cl.oid AND nn.contype = 'n' AND NOT nn.convalidated AND a.attnum = ANY(nn.conkey) WHERE c.table_schema NOT IN ('information_schema', 'pg_catalog', 'pg_toast') AND c.table_schema NOT LIKE 'pg_temp_%' @@ -452,6 +459,7 @@ SELECT cb.numeric_scale, cb.udt_name, cb.column_comment, + cb.invalid_not_null_constraint, cb.resolved_type, cb.is_identity, cb.identity_generation, @@ -487,28 +495,29 @@ ORDER BY cb.table_schema, cb.table_name, cb.ordinal_position ` type GetColumnsRow struct { - TableSchema interface{} `db:"table_schema" json:"table_schema"` - TableName interface{} `db:"table_name" json:"table_name"` - ColumnName interface{} `db:"column_name" json:"column_name"` - OrdinalPosition interface{} `db:"ordinal_position" json:"ordinal_position"` - ColumnDefault sql.NullString `db:"column_default" json:"column_default"` - IsNullable interface{} `db:"is_nullable" json:"is_nullable"` - DataType interface{} `db:"data_type" json:"data_type"` - CharacterMaximumLength interface{} `db:"character_maximum_length" json:"character_maximum_length"` - NumericPrecision interface{} `db:"numeric_precision" json:"numeric_precision"` - NumericScale interface{} `db:"numeric_scale" json:"numeric_scale"` - UdtName interface{} `db:"udt_name" json:"udt_name"` - ColumnComment sql.NullString `db:"column_comment" json:"column_comment"` - ResolvedType sql.NullString `db:"resolved_type" json:"resolved_type"` - IsIdentity interface{} `db:"is_identity" json:"is_identity"` - IdentityGeneration interface{} `db:"identity_generation" json:"identity_generation"` - IdentityStart interface{} `db:"identity_start" json:"identity_start"` - IdentityIncrement interface{} `db:"identity_increment" json:"identity_increment"` - IdentityMaximum interface{} `db:"identity_maximum" json:"identity_maximum"` - IdentityMinimum interface{} `db:"identity_minimum" json:"identity_minimum"` - IdentityCycle interface{} `db:"identity_cycle" json:"identity_cycle"` - Attgenerated interface{} `db:"attgenerated" json:"attgenerated"` - GeneratedExpr sql.NullString `db:"generated_expr" json:"generated_expr"` + TableSchema interface{} `db:"table_schema" json:"table_schema"` + TableName interface{} `db:"table_name" json:"table_name"` + ColumnName interface{} `db:"column_name" json:"column_name"` + OrdinalPosition interface{} `db:"ordinal_position" json:"ordinal_position"` + ColumnDefault sql.NullString `db:"column_default" json:"column_default"` + IsNullable interface{} `db:"is_nullable" json:"is_nullable"` + DataType interface{} `db:"data_type" json:"data_type"` + CharacterMaximumLength interface{} `db:"character_maximum_length" json:"character_maximum_length"` + NumericPrecision interface{} `db:"numeric_precision" json:"numeric_precision"` + NumericScale interface{} `db:"numeric_scale" json:"numeric_scale"` + UdtName interface{} `db:"udt_name" json:"udt_name"` + ColumnComment sql.NullString `db:"column_comment" json:"column_comment"` + InvalidNotNullConstraint sql.NullString `db:"invalid_not_null_constraint" json:"invalid_not_null_constraint"` + ResolvedType sql.NullString `db:"resolved_type" json:"resolved_type"` + IsIdentity interface{} `db:"is_identity" json:"is_identity"` + IdentityGeneration interface{} `db:"identity_generation" json:"identity_generation"` + IdentityStart interface{} `db:"identity_start" json:"identity_start"` + IdentityIncrement interface{} `db:"identity_increment" json:"identity_increment"` + IdentityMaximum interface{} `db:"identity_maximum" json:"identity_maximum"` + IdentityMinimum interface{} `db:"identity_minimum" json:"identity_minimum"` + IdentityCycle interface{} `db:"identity_cycle" json:"identity_cycle"` + Attgenerated interface{} `db:"attgenerated" json:"attgenerated"` + GeneratedExpr sql.NullString `db:"generated_expr" json:"generated_expr"` } // GetColumns retrieves all columns for all tables @@ -534,6 +543,7 @@ func (q *Queries) GetColumns(ctx context.Context) ([]GetColumnsRow, error) { &i.NumericScale, &i.UdtName, &i.ColumnComment, + &i.InvalidNotNullConstraint, &i.ResolvedType, &i.IsIdentity, &i.IdentityGeneration, @@ -573,6 +583,12 @@ WITH column_base AS ( c.numeric_scale, c.udt_name, COALESCE(d.description, '') AS column_comment, + -- Name of a NOT NULL constraint on this column that was added NOT VALID + -- and never validated (PostgreSQL 18+; contype 'n' does not exist before + -- that, so the join simply yields ''). attnotnull is already set for such + -- a column, so this is the only signal that VALIDATE CONSTRAINT is still + -- pending (issue #564). + COALESCE(nn.conname, '') AS invalid_not_null_constraint, CASE WHEN dt.typtype = 'd' THEN quote_ident(dn.nspname) || '.' || quote_ident(dt.typname) @@ -620,6 +636,7 @@ WITH column_base AS ( LEFT JOIN pg_namespace dn ON dt.typnamespace = dn.oid LEFT JOIN pg_type et ON dt.typelem = et.oid LEFT JOIN pg_namespace en ON et.typnamespace = en.oid + LEFT JOIN pg_constraint nn ON nn.conrelid = cl.oid AND nn.contype = 'n' AND NOT nn.convalidated AND a.attnum = ANY(nn.conkey) WHERE c.table_schema = $1 ) @@ -637,6 +654,7 @@ SELECT cb.numeric_scale, cb.udt_name, cb.column_comment, + cb.invalid_not_null_constraint, cb.resolved_type, cb.is_identity, cb.identity_generation, @@ -684,28 +702,29 @@ ORDER BY cb.table_name, cb.ordinal_position ` type GetColumnsForSchemaRow struct { - TableSchema interface{} `db:"table_schema" json:"table_schema"` - TableName interface{} `db:"table_name" json:"table_name"` - ColumnName interface{} `db:"column_name" json:"column_name"` - OrdinalPosition interface{} `db:"ordinal_position" json:"ordinal_position"` - ColumnDefault sql.NullString `db:"column_default" json:"column_default"` - IsNullable interface{} `db:"is_nullable" json:"is_nullable"` - DataType interface{} `db:"data_type" json:"data_type"` - CharacterMaximumLength interface{} `db:"character_maximum_length" json:"character_maximum_length"` - NumericPrecision interface{} `db:"numeric_precision" json:"numeric_precision"` - NumericScale interface{} `db:"numeric_scale" json:"numeric_scale"` - UdtName interface{} `db:"udt_name" json:"udt_name"` - ColumnComment sql.NullString `db:"column_comment" json:"column_comment"` - ResolvedType sql.NullString `db:"resolved_type" json:"resolved_type"` - IsIdentity interface{} `db:"is_identity" json:"is_identity"` - IdentityGeneration interface{} `db:"identity_generation" json:"identity_generation"` - IdentityStart interface{} `db:"identity_start" json:"identity_start"` - IdentityIncrement interface{} `db:"identity_increment" json:"identity_increment"` - IdentityMaximum interface{} `db:"identity_maximum" json:"identity_maximum"` - IdentityMinimum interface{} `db:"identity_minimum" json:"identity_minimum"` - IdentityCycle interface{} `db:"identity_cycle" json:"identity_cycle"` - Attgenerated interface{} `db:"attgenerated" json:"attgenerated"` - GeneratedExpr sql.NullString `db:"generated_expr" json:"generated_expr"` + TableSchema interface{} `db:"table_schema" json:"table_schema"` + TableName interface{} `db:"table_name" json:"table_name"` + ColumnName interface{} `db:"column_name" json:"column_name"` + OrdinalPosition interface{} `db:"ordinal_position" json:"ordinal_position"` + ColumnDefault sql.NullString `db:"column_default" json:"column_default"` + IsNullable interface{} `db:"is_nullable" json:"is_nullable"` + DataType interface{} `db:"data_type" json:"data_type"` + CharacterMaximumLength interface{} `db:"character_maximum_length" json:"character_maximum_length"` + NumericPrecision interface{} `db:"numeric_precision" json:"numeric_precision"` + NumericScale interface{} `db:"numeric_scale" json:"numeric_scale"` + UdtName interface{} `db:"udt_name" json:"udt_name"` + ColumnComment sql.NullString `db:"column_comment" json:"column_comment"` + InvalidNotNullConstraint sql.NullString `db:"invalid_not_null_constraint" json:"invalid_not_null_constraint"` + ResolvedType sql.NullString `db:"resolved_type" json:"resolved_type"` + IsIdentity interface{} `db:"is_identity" json:"is_identity"` + IdentityGeneration interface{} `db:"identity_generation" json:"identity_generation"` + IdentityStart interface{} `db:"identity_start" json:"identity_start"` + IdentityIncrement interface{} `db:"identity_increment" json:"identity_increment"` + IdentityMaximum interface{} `db:"identity_maximum" json:"identity_maximum"` + IdentityMinimum interface{} `db:"identity_minimum" json:"identity_minimum"` + IdentityCycle interface{} `db:"identity_cycle" json:"identity_cycle"` + Attgenerated interface{} `db:"attgenerated" json:"attgenerated"` + GeneratedExpr sql.NullString `db:"generated_expr" json:"generated_expr"` } // GetColumnsForSchema retrieves all columns for tables in a specific schema @@ -731,6 +750,7 @@ func (q *Queries) GetColumnsForSchema(ctx context.Context, tableSchema sql.NullS &i.NumericScale, &i.UdtName, &i.ColumnComment, + &i.InvalidNotNullConstraint, &i.ResolvedType, &i.IsIdentity, &i.IdentityGeneration, diff --git a/testdata/diff/online/add_not_null/diff.sql b/testdata/diff/online/add_not_null/diff.sql index 3e39bc8b..be5c2b3b 100644 --- a/testdata/diff/online/add_not_null/diff.sql +++ b/testdata/diff/online/add_not_null/diff.sql @@ -1 +1,2 @@ ALTER TABLE users ALTER COLUMN email SET NOT NULL; +ALTER TABLE users VALIDATE CONSTRAINT users_phone_not_null; diff --git a/testdata/diff/online/add_not_null/new.sql b/testdata/diff/online/add_not_null/new.sql index d2cd90d8..ad75e843 100644 --- a/testdata/diff/online/add_not_null/new.sql +++ b/testdata/diff/online/add_not_null/new.sql @@ -2,5 +2,6 @@ CREATE TABLE public.users ( id integer NOT NULL, username text NOT NULL, email text NOT NULL, + phone text NOT NULL, created_at timestamp with time zone DEFAULT now() NOT NULL -); \ No newline at end of file +); diff --git a/testdata/diff/online/add_not_null/old.sql b/testdata/diff/online/add_not_null/old.sql index c488833d..067b0560 100644 --- a/testdata/diff/online/add_not_null/old.sql +++ b/testdata/diff/online/add_not_null/old.sql @@ -2,5 +2,11 @@ CREATE TABLE public.users ( id integer NOT NULL, username text NOT NULL, email text, + phone text, created_at timestamp with time zone DEFAULT now() NOT NULL -); \ No newline at end of file +); + +-- Issue #564: a NOT NULL constraint that was added NOT VALID (e.g. apply was +-- interrupted before VALIDATE, or the ADD was run by hand) must be re-validated +-- on the next plan instead of being silently treated as done. +ALTER TABLE public.users ADD CONSTRAINT users_phone_not_null NOT NULL phone NOT VALID; diff --git a/testdata/diff/online/add_not_null/plan.json b/testdata/diff/online/add_not_null/plan.json index 9c2bd25b..9f9dc9bf 100644 --- a/testdata/diff/online/add_not_null/plan.json +++ b/testdata/diff/online/add_not_null/plan.json @@ -3,7 +3,7 @@ "pgschema_version": "1.13.0", "created_at": "1970-01-01T00:00:00Z", "source_fingerprint": { - "hash": "653938aaa4f39adc46e8b751c1d67ef7229a57a407833953c0b0176e33e70a58" + "hash": "c37218dfe7dcac89bfc7a28a3e7a661ca9d48345001260c2998dc67eddfc3589" }, "groups": [ { @@ -25,6 +25,16 @@ "path": "public.users.email" } ] + }, + { + "steps": [ + { + "sql": "ALTER TABLE users VALIDATE CONSTRAINT users_phone_not_null;", + "type": "table.column", + "operation": "alter", + "path": "public.users.phone" + } + ] } ] } diff --git a/testdata/diff/online/add_not_null/plan.sql b/testdata/diff/online/add_not_null/plan.sql index b3922d9c..eb569148 100644 --- a/testdata/diff/online/add_not_null/plan.sql +++ b/testdata/diff/online/add_not_null/plan.sql @@ -1,3 +1,5 @@ ALTER TABLE users ADD CONSTRAINT users_email_not_null NOT NULL email NOT VALID; ALTER TABLE users VALIDATE CONSTRAINT users_email_not_null; + +ALTER TABLE users VALIDATE CONSTRAINT users_phone_not_null; diff --git a/testdata/diff/online/add_not_null/plan.txt b/testdata/diff/online/add_not_null/plan.txt index 1afac778..521f8eca 100644 --- a/testdata/diff/online/add_not_null/plan.txt +++ b/testdata/diff/online/add_not_null/plan.txt @@ -6,6 +6,7 @@ Summary by type: Tables: ~ users ~ email (column) + ~ phone (column) DDL to be executed: -------------------------------------------------- @@ -15,3 +16,6 @@ ALTER TABLE users ADD CONSTRAINT users_email_not_null NOT NULL email NOT VALID; -- Transaction Group #2 ALTER TABLE users VALIDATE CONSTRAINT users_email_not_null; + +-- Transaction Group #3 +ALTER TABLE users VALIDATE CONSTRAINT users_phone_not_null;