Module: Pcrd::Transform::TypeMap
- Defined in:
- lib/pcrd/transform/type_map.rb
Overview
Registry of known PostgreSQL type transitions and their safety classification.
Works with pg’s internal type_name values (int4, int8, bool, etc.) for the source side, and normalizes user-facing spec strings (bigint, timestamptz) to the same internal names for matching.
Safety levels:
:no_op — source and target are the same type; nothing to do
:always_safe — widening cast; no possible data loss; no validation needed
:validated — cast may lose data; Validator must run a pre-migration check
:unsupported — pcrd cannot handle this cast; user must provide a custom transform
Constant Summary collapse
- SPEC_TO_PG =
Maps user-facing type strings in migration specs to pg internal type names.
{ "bigint" => "int8", "int8" => "int8", "integer" => "int4", "int4" => "int4", "int" => "int4", "smallint" => "int2", "int2" => "int2", "real" => "float4", "float4" => "float4", "double precision" => "float8", "float8" => "float8", "boolean" => "bool", "bool" => "bool", "text" => "text", "date" => "date", "timestamp" => "timestamp", "timestamp without time zone" => "timestamp", "timestamptz" => "timestamptz", "timestamp with time zone" => "timestamptz", "time" => "time", "time without time zone" => "time", "timetz" => "timetz", "time with time zone" => "timetz", "numeric" => "numeric", "decimal" => "numeric", "money" => "money", "uuid" => "uuid", "json" => "json", "jsonb" => "jsonb", "bytea" => "bytea", "oid" => "oid", }.freeze
- ALWAYS_SAFE_PAIRS =
Always-safe casts: pure widening, no possible data loss. Keys are [pg_source_type, pg_target_type].
Set.new([ %w[int2 int4], %w[int2 int8], %w[int4 int8], %w[int2 float4], %w[int4 float4], %w[int2 float8], %w[int4 float8], %w[int8 float8], %w[float4 float8], %w[int2 numeric], %w[int4 numeric], %w[int8 numeric], %w[float4 numeric], %w[float8 numeric], %w[date timestamp], %w[date timestamptz], %w[timestamp timestamptz], %w[bpchar text], # char(n) → text %w[varchar text], # varchar(n) → text %w[bpchar varchar], # char(n) → varchar(m) — validated below if m < n %w[name text], %w[json jsonb], ]).freeze
- VALIDATED_RULES =
Validated casts: may lose data; Validator generates SQL to check. Values include: :description, :check_expr (a Proc → SQL fragment), :warn_only.
[ { from: "int8", to: "int4", description: "values must fit in integer range [-2,147,483,648 … 2,147,483,647]", check_expr: ->(col) { "#{col} NOT BETWEEN -2147483648 AND 2147483647" }, warn_only: false }, { from: "int8", to: "int2", description: "values must fit in smallint range [-32,768 … 32,767]", check_expr: ->(col) { "#{col} NOT BETWEEN -32768 AND 32767" }, warn_only: false }, { from: "int4", to: "int2", description: "values must fit in smallint range [-32,768 … 32,767]", check_expr: ->(col) { "#{col} NOT BETWEEN -32768 AND 32767" }, warn_only: false }, { from: "float8", to: "float4", description: "precision will be reduced (double precision → real); some values may differ", check_expr: nil, warn_only: true }, { from: "timestamptz", to: "timestamp", description: "timezone information will be discarded", check_expr: nil, warn_only: true }, { from: "numeric", to: "int8", description: "fractional parts will be truncated; values must be whole numbers", check_expr: ->(col) { "#{col} <> floor(#{col})" }, warn_only: false }, { from: "numeric", to: "int4", description: "fractional parts truncated; values must fit in integer range", check_expr: ->(col) { "floor(#{col}) NOT BETWEEN -2147483648 AND 2147483647 OR #{col} <> floor(#{col})" }, warn_only: false }, # text/varchar → varchar(n): length check — handled separately via varchar_length_check { from: "text", to: "varchar", description: "all values must fit within target length", check_expr: :varchar_length_check, warn_only: false }, { from: "varchar", to: "varchar", description: "all values must fit within target length", check_expr: :varchar_length_check, warn_only: false }, { from: "varchar", to: "bpchar", description: "all values must fit within target length", check_expr: :varchar_length_check, warn_only: false }, { from: "text", to: "bpchar", description: "all values must fit within target length", check_expr: :varchar_length_check, warn_only: false }, ].freeze
Class Method Summary collapse
-
.cast_safety(source_pg_type, target_type_str) ⇒ Object
Returns the safety classification for a source→target type transition.
-
.extract_length(type_str) ⇒ Object
Extracts the length parameter from a varchar(N) / char(N) type string.
-
.known_target?(type_str) ⇒ Boolean
Returns true if a target type string refers to a known type.
-
.validated_rule(source_pg_type, target_type_str) ⇒ Object
Returns the validated rule for a source→target pair, or nil.
Class Method Details
.cast_safety(source_pg_type, target_type_str) ⇒ Object
Returns the safety classification for a source→target type transition.
source_pg_type: pg internal type name from Schema::Column#type_name target_type_str: type string from the migration spec (e.g. “bigint”, “varchar(255)”)
141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 |
# File 'lib/pcrd/transform/type_map.rb', line 141 def self.cast_safety(source_pg_type, target_type_str) src = source_pg_type.to_s.strip tgt_pg, tgt_base = normalize_target(target_type_str) # Same base type: usually no-op, but with special cases for parameterized types. if src == tgt_pg || (src == tgt_base && tgt_pg.nil?) # varchar/char → varchar/char with a length constraint needs validation. if %w[bpchar varchar].include?(src) && extract_length(target_type_str) return :validated end # numeric → numeric with any parameterization is always safe (precision can # only be widened without validation — pcrd doesn't restrict to widening only, # but narrowing numeric is caught by the validated rule below). return :no_op end return :always_safe if ALWAYS_SAFE_PAIRS.include?([src, tgt_base]) # varchar → varchar(m): validated (length comparison handled by Validator) if %w[bpchar varchar].include?(src) && %w[varchar bpchar].include?(tgt_base) tgt_len = extract_length(target_type_str) return :always_safe if tgt_len.nil? # → text (already covered above) return :validated end validated = VALIDATED_RULES.find { |r| r[:from] == src && r[:to] == tgt_base } return :validated if validated :unsupported end |
.extract_length(type_str) ⇒ Object
Extracts the length parameter from a varchar(N) / char(N) type string. Returns nil if not parameterized.
186 187 188 189 190 |
# File 'lib/pcrd/transform/type_map.rb', line 186 def self.extract_length(type_str) return nil unless type_str m = type_str.match(/\((\d+)/) m ? m[1].to_i : nil end |
.known_target?(type_str) ⇒ Boolean
Returns true if a target type string refers to a known type.
179 180 181 182 |
# File 'lib/pcrd/transform/type_map.rb', line 179 def self.known_target?(type_str) _, base = normalize_target(type_str) SPEC_TO_PG.value?(base) || %w[varchar bpchar].include?(base) end |
.validated_rule(source_pg_type, target_type_str) ⇒ Object
Returns the validated rule for a source→target pair, or nil.
173 174 175 176 |
# File 'lib/pcrd/transform/type_map.rb', line 173 def self.validated_rule(source_pg_type, target_type_str) _, tgt_base = normalize_target(target_type_str) VALIDATED_RULES.find { |r| r[:from] == source_pg_type && r[:to] == tgt_base } end |