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

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.

Returns:

  • (Boolean)


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