You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
feat: support range types (CREATE TYPE ... AS RANGE) - #684
Phases 2 and 3 of the range type design, following the Phase 1 fixes in #683. A user-defined range type is now inspected, dumped, created, dropped, compared and replaced.
Inspector: new GetRangeTypesForSchema query over pg_range (subtype, operator class, collation, canonical, subtype_diff, multirange). Opclass and collation are captured only when they differ from the subtype's defaults, as pg_dump prints them. Type privilege queries include typtype = 'r', so GRANT USAGE ON TYPE on a range round-trips.
Rendering: CREATE TYPE ... AS RANGE (...) in pg_dump's option order. multirange_type_name is printed only when it is not the default, using a Go port of PostgreSQL's makeMultirangeTypeName.
Ordering: a range over a new enum or domain, and a composite attribute or domain over a range or its multirange, sort after what they need. A range whose subtype_diff is a new function is created after the functions phase, and tables using the range or its multirange wait for it.
Modified ranges: PostgreSQL has no ALTER TYPE for a range's definition, so a changed range is dropped and recreated as a single plan step (one "modify" in the plan).
Drop order fix: functions are dropped before types, so dropping a range (or a domain with a function CHECK) together with the function it references failed with a dependency error. Such functions now wait until after the types are dropped.
A replaced range is scheduled through the create phase's type buckets (after the drop phase, after a new subtype_diff function or subtype it needs), still as one plan step.
Deferral propagates to types built on a deferred range, to tables with array columns of it, and to functions whose signature uses it.
Types drop in reverse dependency order; a held-back subtype_diff function drops between the types that reference it and the types its signature uses, with its callees held too.
Inspector fix uncovered by that: GetFunctionDependencies keyed functions by identity arguments (with parameter names) while the Functions map is keyed by types, so any function with named parameters had no dependencies. It now uses oidvectortypes. No existing golden file changed.
Plan-time schema remapping covers SubtypeOpclass and Collation; the multirange-name port clips like the server.
Dropped types and held-back functions are ordered together by dependency, which also covers a range over a function-checked domain whose subtype_diff takes that domain.
A replaced range is dropped in the drop phase (inside that joint order, before an old subtype_diff or subtype that goes away) and created in the create phase. The plan reports one drop and one add of the type.
subtype_diff is matched by signature, so an overload taking the range itself is ordered as a dependent of the range.
subtype_opclass and collation in the target schema lose their prefix, like a column collation.
subtype_diff matching normalizes the subtype the way parameter types are normalized, so a timestamp (or varchar) range still finds its difference function.
A held-back function also drops before a type its SQL body or a parameter default names.
After the fourth and fifth reviews
Function-to-type dependencies come from the catalog: a new GetFunctionTypeDependencies query (pg_depend, pg_proc → pg_type) fills Function.TypeDependencies, which the joint drop order uses as certain edges. The textual body match was tried and removed: a literal or parameter named like a type makes a false edge, and two such hints can hide a real one. The field is kept out of the JSON fingerprint.
Not in scope
COMMENT ON TYPE is not generated for any type kind today (the IR captures the comment, no diff code emits it), so range comments are not round-tripped either. Noted in the design doc and the dump suite manifest; worth its own PR.
A range column removed from a retained table is dropped in the modify phase, after the replaced range's DROP TYPE, so that combination fails with PostgreSQL's dependency error.
A chain function → domain (CHECK) → function (taking the domain) → range (subtype_diff) creates the range before the second function; domains have the same limit, and it needs interleaved type/function scheduling in the create phase.
Phases 2 and 3 of docs/plans/2026-11-05-range-types-design.md. A user-defined
range type is now inspected, dumped, created, dropped, compared and replaced.
Inspector: a new GetRangeTypesForSchema query joins pg_range to the subtype,
operator class, collation, canonical and subtype_diff functions, and the
multirange. Subtype follows the domain base-type convention; function
references follow the aggregate convention (bare in the type's own schema);
opclass and collation are empty when they are the subtype's defaults, as
pg_dump prints them. The type privilege queries include typtype 'r'.
Diff: CREATE TYPE ... AS RANGE in pg_dump's option order, printing
multirange_type_name only when it differs from a port of PostgreSQL's
makeMultirangeTypeName. A range over a new enum or domain, and a composite
or domain over a range or its multirange, sort after what they need. A range
whose subtype_diff is a new function is created after the functions phase,
and tables using the range or its multirange wait for it. A changed range
has no ALTER TYPE, so it is dropped and recreated as one plan step.
Dropping a range (or a domain) together with a function it references
previously failed because functions are dropped before types; such
functions now wait until after the types.
Tests: nine create_type cases, privilege/grant_type_range, an include
fixture, unit tests for the renderer, equality, sort edges and the
multirange name, and the regenerated testdata/dump/range golden file.
add_range_type joins the cross-version smoke set. COMMENT ON TYPE is not
generated for any type kind yet, so range comments are not round-tripped.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Adds inspection, SQL rendering, comparison, and replacement for user-defined range types.
Schema dumps now include user-defined range definitions.
Migration plans put range types between their prerequisites and dependents.
Plans now include grants on range types.
Acknowledged limits:
tianzhou explicitly deferred COMMENT ON TYPE because no type kind currently emits it.
Canonical function creation is explicitly documented as outside the supported creation path.
Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Inspect current and desired schemas] --> B[Compare range definitions]
B --> C[Replace changed ranges]
C --> D[Drop removed objects]
D --> E[Create early types]
E --> F[Create functions]
F --> G[Create deferred ranges]
G --> H[Create dependent tables]
H --> I[Apply changed grants]
The reason will be displayed to describe this comment to others. Learn more.
🟡 Changes recommended
Critical dependency-ordering defects can cause schema changes to fail, while range handling can also lose privileges or restore incorrect multirange names.
Adds user-defined range types to pgschema’s inspect, dump, plan, and apply workflow, building on the constructor-filtering and schema-qualification fixes in #683.
Changes:
Captures range definitions and privileges, renders range DDL, and detects definition changes for replacement.
Adds dependency ordering for range subtypes, supporting functions, and range/multirange consumers.
Expands migration fixtures, dump and include coverage, and documentation; type comments remain out of scope.
…ents of a deferred range
Review of #684 (greptile, Copilot) found ordering gaps around range types:
- A replaced range was emitted before the drop phase, so a table using the
old definition that is dropped in the same plan blocked DROP TYPE RESTRICT,
and a new subtype_diff function did not exist yet for the CREATE. The
replacement (still one DROP + CREATE step) now goes through the create
phase's type buckets like a new range would.
- Types built on a deferred range (a composite attribute, a domain over the
multirange, a range over a deferred domain), tables with array columns of
it, and functions whose signature uses it were created before the range.
Deferral now propagates through the type graph, deferred-type matching
strips array and quote syntax, and signature-dependent functions move to
the batch after the type.
- Types were dropped alphabetically. They now drop in reverse dependency
order, and a held-back subtype_diff function drops between the types that
reference it and the types its signature uses, with its callees held too.
That exposed an inspector bug: GetFunctionDependencies keyed functions by
identity arguments (with parameter names) while the Functions map is keyed
by types, so named-parameter functions never had dependencies. It now uses
oidvectortypes with the same same-schema stripping.
- Plan-time schema remapping covers SubtypeOpclass and Collation.
- defaultMultirangeTypeName clips the range name to NAMEDATALEN-12 bytes
before appending _multirange, as the server does.
Grants on a replaced range are lost, as for recreated functions and views
(open follow-up from #677); not addressed here.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The reason will be displayed to describe this comment to others. Learn more.
🟡 Changes recommended
Unresolved dependency-ordering defects can break range creation, replacement, and deletion, while overload matching and schema qualification can resolve references incorrectly.
Match subtype-difference functions by identity when ordering drops
internal/diff/diff.go:3934
For a range r using diff(integer, integer), this name-only lookup also holds an unrelated overload diff(r) until after the range. Dropping all three objects then fails at DROP TYPE r RESTRICT because diff(r) still depends on it. Match the actual subtype-difference function identity, including argument types, so the consuming overload drops before the range.
Apply schema-prefix stripping to opclass and collation references
internal/diff/type.go:323
Same-schema opclass and collation references bypass smart schema qualification here. The inspector always qualifies a custom collation, so a default dump of schema tenant emits an unqualified range but retains collation = tenant.my_coll. Reusing that dump in another schema either binds the original schema's collation or fails, unlike column collations, which strip the target prefix. Pass both options through stripSchemaPrefixMode, preserving genuinely cross-schema references and --qualify-schema behavior.
…nt across phases
Second review round on #684:
- Dropped types and the functions they hold back are now ordered together
by dependency (orderTypeAndFunctionDrops): a type before the type it is
built on, a type before the function it names, a function before the
type its signature uses, a caller before its callee. This covers a range
over a function-checked domain whose subtype_diff takes that domain
(drop_range_over_checked_domain).
- A replaced range is no longer one DROP + CREATE step in the create
phase. The old definition is dropped in the drop phase inside that joint
order, so it precedes an old subtype_diff or subtype that goes away too,
and the new one is created in the create phase through the usual buckets.
The plan reports one drop and one add of the type, which is what runs
(alter_range_drop_old_subtype_diff).
- A range's subtype_diff is matched by signature, not just name, so an
overload taking the range itself is ordered as a dependent of the range
(drop_range_subtype_diff_overload).
- subtype_opclass and collation in the target schema lose their prefix
like a column collation, so a dump applies to another schema.
A chain of function -> domain (CHECK) -> function (taking the domain) ->
range (subtype_diff) still creates the range before the second function;
domains have the same limit, and it needs interleaved scheduling of types
and functions in the create phase. Recorded in the design doc.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Two notes from the second Copilot review had no thread; both are fixed in 45c8f93:
Match subtype_diff by identity: typeReferencesNewFunction now matches name(subtype, subtype) against signature keys in the function lookup, so an overload taking the range itself is treated as a dependent of the range and dropped before it (drop_range_subtype_diff_overload).
Strip the target-schema prefix from opclass and collation: both go through stripSchemaPrefixMode like a column collation, so a dump of schema tenant applies to another schema; pg_catalog and cross-schema references keep their prefix.
Hoisting ignores type dependencies in multi-file dumps
internal/diff/diff.go:2297
With an unrelated table plus time_diff, timerange, and shift_length(timerange), multi-file formatting hoists shift_length ahead of the range because it checks only table/view dependencies (internal/dump/formatter.go:178–183). Loading the dump then fails because the parameter type does not exist. Make hoisting preserve prerequisite type files and add a multi-file replay case with an unrelated early table.
Domain CHECK overloads can create incorrect drop ordering
internal/diff/diff.go:3953
If domain positive checks retained is_positive(integer), dropping the domain and overload is_positive(positive) incorrectly holds the overload because domain matching uses only function names. The joint graph then contains edges in both directions, and its cycle fallback drops the domain first, failing because the overload still uses it. Use catalog-resolved CHECK/default function identities for holdback and joint drop edges (internal/diff/diff.go:4028–4029).
Range-to-enum changes are silently omitted
internal/diff/type.go:386
Changing an otherwise unused span from a range to an enum, or vice versa, is silently ignored. Type comparison records a modification, but this filter excludes it and both modify generators skip changes between different kinds, so the resulting plan reports no changes. Include modifications involving a range on either side in the replacement path.
Array-based domains are misidentified as array subtypes
ir/queries/queries.sql:2164
This branch also matches domains over arrays: PostgreSQL copies the base type's typcategory = 'A' into the domain, but the domain's typelem is zero. For CREATE DOMAIN endpoints AS integer[] followed by a range with subtype = endpoints, the element joins are NULL, so this expression returns NULL and inspection records an empty subtype. Dumps then emit an invalid subtype = option. Restrict this branch to actual array types and render an array-based domain by its qualified domain name; regenerate the sqlc output too.
…d functions before types their bodies name
Third review round on #684:
- A range's subtype_diff is matched by signature against the function
lookup. Parameter types are normalized (timestamp without time zone ->
timestamp, character varying -> varchar) while the subtype keeps the
format_type spelling, so the match now normalizes the subtype the same
way (ir.NormalizeTypeName). Without it a timestamp range was created
before its new difference function (add_range_subtype_diff_timestamp).
- In the joint drop order a held-back function also precedes a type that
its SQL body or a parameter default names, since PostgreSQL records that
dependency for SQL-standard bodies and defaults
(drop_range_subtype_diff_body_type).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Fourth review round on #684: matching a held-back function's body text
against dropped type names can invent a function -> type edge (a parameter
or literal named like the type). Against the certain type -> function edge
of a range naming its subtype_diff that made a cycle, and the fallback
could drop the enum before the function.
Certain edges are added first (type structure, a type naming a function,
signatures, recorded function dependencies); a textual edge is added only
when it does not contradict them, so it can over-order a drop but never
reverse one (drop_range_subtype_diff_body_names_range).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The reason will be displayed to describe this comment to others. Learn more.
🟡 Changes recommended
A false textual dependency hint can cause a real type dependency to be rejected as cyclic, leading to DROP TYPE executing while a function still depends on it and failing.
A function's body or parameter default naming a type was a textual hint,
added to the drop order only when it did not contradict an edge added
before it. That check also ran against earlier hints, so a false match
(a literal naming another range) sorted first could veto a true,
catalog-recorded dependency (a SQL-standard body casting to a range), and
DROP TYPE ran while the function still depended on it.
Read pg_depend from pg_proc to pg_type (signature, SQL-standard body,
parameter defaults; arrays as their element) into Function.TypeDependencies
with a new GetFunctionTypeDependencies query, and use it as a certain edge
in orderTypeAndFunctionDrops. The textual match is removed: for drops,
only a recorded dependency can make DROP ... RESTRICT fail, and the catalog
is the source of those. drop_two_ranges_cross_body covers the veto case.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
Multiple critical gaps in type-transition handling and dependency ordering require human assessment because they can omit migrations or produce SQL that fails to execute.
…TypeDependencies from JSON
The plan.json was generated while Function.TypeDependencies was still
serialized; it is ordering data and now carries json:"-" so existing
fingerprints do not shift, which also changes this new case's hash.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Each diff case carries its own fixture, plan and apply run, so the twenty
range cases are now one transition that drops, replaces, adds and keeps
ranges at once, with the privilege grant folded in. Its diff.sql is the
specification for the whole drop order, the create order and the no-op
guard; the drop-order algorithm keeps its unit test in type_test.go.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Phases 2 and 3 of the range type design, following the Phase 1 fixes in #683. A user-defined range type is now inspected, dumped, created, dropped, compared and replaced.
GetRangeTypesForSchemaquery overpg_range(subtype, operator class, collation, canonical, subtype_diff, multirange). Opclass and collation are captured only when they differ from the subtype's defaults, as pg_dump prints them. Type privilege queries includetyptype = 'r', soGRANT USAGE ON TYPEon a range round-trips.CREATE TYPE ... AS RANGE (...)in pg_dump's option order.multirange_type_nameis printed only when it is not the default, using a Go port of PostgreSQL'smakeMultirangeTypeName.subtype_diffis a new function is created after the functions phase, and tables using the range or its multirange wait for it.ALTER TYPEfor a range's definition, so a changed range is dropped and recreated as a single plan step (one "modify" in the plan).After review (216785b)
subtype_difffunction or subtype it needs), still as one plan step.subtype_difffunction drops between the types that reference it and the types its signature uses, with its callees held too.GetFunctionDependencieskeyed functions by identity arguments (with parameter names) while the Functions map is keyed by types, so any function with named parameters had no dependencies. It now usesoidvectortypes. No existing golden file changed.SubtypeOpclassandCollation; the multirange-name port clips like the server.After the second review (45c8f93)
subtype_difftakes that domain.subtype_diffor subtype that goes away) and created in the create phase. The plan reports one drop and one add of the type.subtype_diffis matched by signature, so an overload taking the range itself is ordered as a dependent of the range.subtype_opclassandcollationin the target schema lose their prefix, like a column collation.After the third review (9f205e1)
subtype_diffmatching normalizes the subtype the way parameter types are normalized, so atimestamp(orvarchar) range still finds its difference function.After the fourth and fifth reviews
GetFunctionTypeDependenciesquery (pg_depend,pg_proc→pg_type) fillsFunction.TypeDependencies, which the joint drop order uses as certain edges. The textual body match was tried and removed: a literal or parameter named like a type makes a false edge, and two such hints can hide a real one. The field is kept out of the JSON fingerprint.Not in scope
COMMENT ON TYPEis not generated for any type kind today (the IR captures the comment, no diff code emits it), so range comments are not round-tripped either. Noted in the design doc and the dump suite manifest; worth its own PR.subtype_diff) creates the range before the second function; domains have the same limit, and it needs interleaved type/function scheduling in the create phase.Test plan
go test ./internal/diff ./ir ./internal/...go test ./cmd -run TestPlanAndApply(full suite, PostgreSQL 18)create_type/,create_domain/,privilege/grant_type*applied for real after the drop-order fixTestIncludeIntegrationPGSCHEMA_POSTGRES_VERSION=14forcreate_type/and the range dump suiteTestDumpCommand_Rangeon 18 and 14go test ./cmd/dumpsolo run (550s, all fixtures byte-identical)🤖 Generated with Claude Code