Module: LcpRuby::AssociationFkType
- Defined in:
- lib/lcp_ruby/association_fk_type.rb
Overview
Shared helpers for resolving foreign-key column types from an AssociationDefinition.
Two flavors are exposed:
-
‘schema_type(assoc)` — returns a schema-level symbol suitable for `connection.add_column` / `t.<type>` (`:bigint`, `:integer`, `:string`, …). Used by `SchemaManager` when auto-creating FK columns.
-
‘field_type(assoc)` — returns a string from `Metadata::FieldDefinition::BASE_TYPES` (`“integer”`, `“string”`, …) suitable for constructing a synthetic `FieldDefinition`. Used by `Import::RowProcessor`, `Import::FieldTreeBuilder`, and `Export::FieldTreeBuilder` which build field metadata for FK columns.
The two vocabularies are not the same — ‘:bigint` is a schema concept only and is normalized to `“integer”` for FieldDefinition purposes.
Lives outside SchemaManager so all of the above call sites can reach it without cross-class plumbing.
Constant Summary collapse
- SCHEMA_TO_FIELD_TYPE =
Schema-level vocabulary → FieldDefinition::BASE_TYPES vocabulary.
{ bigint: "integer", integer: "integer", string: "string", text: "text", float: "float", decimal: "decimal", boolean: "boolean", date: "date", datetime: "datetime", uuid: "uuid" }.freeze
- EQUIVALENT_TYPE_PAIRS =
True iff two adapter-level type strings represent the same column storage. Used in ‘validate_managed_field_collisions` to allow legitimate idempotent re-checks (e.g., `:integer` vs `:bigint` for FK columns).
Designed conservatively: silent type widening (string ↔ text, decimal ↔ float) is rejected because the failure mode of a quiet mismatch is data corruption far from the source.
[ %w[integer bigint].sort.freeze, %w[json jsonb].sort.freeze ].to_set.freeze
- NON_PG_EQUIVALENT_TYPE_PAIRS =
Adapter-gated equivalents — only compatible on certain adapters. ‘text` ↔ `json` are stored identically on SQLite and MySQL (json is a label over TEXT/LONGTEXT), so a host that declared the column as `t.text` for a YAML `type: array` (json) field is honoring the same storage. PostgreSQL has a real `json`/`jsonb` type — keep strict.
[ %w[json text].sort.freeze ].to_set.freeze
Class Method Summary collapse
-
.adapter_normalized_type(field, connection) ⇒ Object
Normalize a FieldDefinition’s *declared YAML type* to the type the underlying adapter actually stores, so the validator can compare against ‘existing.sql_type_metadata.type` without spurious mismatches.
-
.compatible_column_types?(declared, actual, adapter: nil) ⇒ Boolean
‘adapter` is optional — pass `connection.adapter_name` so non-PG storage equivalents can be honored.
-
.field_type(assoc) ⇒ Object
FieldDefinition-level type string suitable for ‘Metadata::FieldDefinition.new(type: …)`.
-
.schema_type(assoc) ⇒ Object
Schema-level type symbol suitable for ‘connection.add_column` / `t.<type>`.
Class Method Details
.adapter_normalized_type(field, connection) ⇒ Object
Normalize a FieldDefinition’s *declared YAML type* to the type the underlying adapter actually stores, so the validator can compare against ‘existing.sql_type_metadata.type` without spurious mismatches.
Reads ‘field.type` (raw declared YAML type) rather than `field.column_type` because `column_type` already collapses some types (`:uuid` → `:string`) before the adapter dimension is known —we want the YAML’s declared shape and apply the adapter mapping here.
Examples (declared YAML type → AR’s ‘sql_type_metadata.type` string):
field type PG MySQL SQLite
──────────────────────────────────────────────
json jsonb json json (AR preserves the type label)
jsonb jsonb json json
uuid uuid string string
enum string string string
rich_text text text text
file string string string
array <pg array> text text
<other base> <as decl> <as decl> <as decl>
Without this normalization, a YAML row like ‘type: json, lcp_managed: true` paired with a host column declared `t.json :settings` reports differently across adapters at the metadata layer (e.g., :jsonb on PG vs :json on SQLite/MySQL) and would spuriously error on every boot.
122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 |
# File 'lib/lcp_ruby/association_fk_type.rb', line 122 def adapter_normalized_type(field, connection) declared = field.type.to_s adapter = connection.adapter_name.to_s.downcase case declared when "json", "jsonb" # PG returns :jsonb at the metadata layer for both JSON and JSONB # columns; MySQL and SQLite both report :json. The pair {json, jsonb} # is treated as compatible by compatible_column_types?, so the # normalization here just picks the canonical adapter label. adapter.include?("postgres") ? "jsonb" : "json" when "uuid" adapter.include?("postgres") ? "uuid" : "string" when "enum" "string" when "rich_text" "text" when "file" "string" when "array" # Mirror SchemaManager#build_column_options: PG uses native typed # arrays; non-PG stores via LcpRuby.json_column_type. On SQLite that # resolves to :json (AR preserves the type label even though the # underlying storage is TEXT); on MySQL to :json. Keeping symmetry # with the actual storage label avoids spurious validator collisions # when a managed `type: array` field is declared against an existing # host column whose AR type is :json. adapter.include?("postgres") ? declared : LcpRuby.json_column_type.to_s else declared end end |
.compatible_column_types?(declared, actual, adapter: nil) ⇒ Boolean
‘adapter` is optional — pass `connection.adapter_name` so non-PG storage equivalents can be honored. Without it, only the strict cross-adapter equivalents (EQUIVALENT_TYPE_PAIRS) apply.
179 180 181 182 183 184 185 186 187 188 189 |
# File 'lib/lcp_ruby/association_fk_type.rb', line 179 def compatible_column_types?(declared, actual, adapter: nil) d = declared.to_s a = actual.to_s return true if d == a pair = [ d, a ].sort return true if EQUIVALENT_TYPE_PAIRS.include?(pair) if adapter && !adapter.to_s.downcase.include?("postgres") return true if NON_PG_EQUIVALENT_TYPE_PAIRS.include?(pair) end false end |
.field_type(assoc) ⇒ Object
FieldDefinition-level type string suitable for ‘Metadata::FieldDefinition.new(type: …)`.
Wraps ‘schema_type` and normalizes schema-only aliases (`:bigint` →`“integer”`) onto `FieldDefinition::BASE_TYPES` so the result can be fed into `FieldDefinition.new` without raising.
90 91 92 93 |
# File 'lib/lcp_ruby/association_fk_type.rb', line 90 def field_type(assoc) type = schema_type(assoc) SCHEMA_TO_FIELD_TYPE[type.to_sym] || type.to_s end |
.schema_type(assoc) ⇒ Object
Schema-level type symbol suitable for ‘connection.add_column` / `t.<type>`. Implements the resolution rules in the spec’s “FK Column Creation” section. Raises ‘MetadataError` for non-LCP targets with `primary_key:` set (Decision 12) — the validator should have caught this earlier; this is a defensive tripwire so we never silently fall back to `:bigint` and corrupt the schema.
47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
# File 'lib/lcp_ruby/association_fk_type.rb', line 47 def schema_type(assoc) if assoc.primary_key.present? && !assoc.lcp_model? raise LcpRuby::MetadataError, "Association '#{assoc.owner_name}##{assoc.name}': primary_key " \ "'#{assoc.primary_key}' targets an external class " \ "('#{assoc.class_name}'). This should have been caught by " \ "ConfigurationValidator — please file a bug. Workaround: " \ "declare the FK column explicitly via 'fields:'." end return :bigint unless assoc.lcp_model? target_def = LcpRuby.loader.model_definitions[assoc.target_model] if assoc.primary_key.present? unless target_def raise LcpRuby::MetadataError, "Association '#{assoc.owner_name}##{assoc.name}': target_model " \ "'#{assoc.target_model}' not found in loader (dangling reference)" end ref_field = target_def.fields.find { |f| f.name == assoc.primary_key } return ref_field.type.to_sym if ref_field if target_def.primary_key_column == assoc.primary_key return target_def.primary_key_type end raise LcpRuby::MetadataError, "Association '#{assoc.owner_name}##{assoc.name}': primary_key " \ "'#{assoc.primary_key}' not found on target '#{assoc.target_model}'" end return :bigint unless target_def target_def.primary_key_type end |