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;