Class: DeclareSchema::Model::FieldSpec

Inherits:
Object
  • Object
show all
Defined in:
lib/declare_schema/model/field_spec.rb

Constant Summary collapse

MYSQL_TINYTEXT_LIMIT =
0xff
MYSQL_TEXT_LIMIT =
0xffff
MYSQL_MEDIUMTEXT_LIMIT =
0xff_ffff
MYSQL_LONGTEXT_LIMIT =
0xffff_ffff
MYSQL_TEXT_LIMITS_ASCENDING =
[MYSQL_TINYTEXT_LIMIT, MYSQL_TEXT_LIMIT, MYSQL_MEDIUMTEXT_LIMIT, MYSQL_LONGTEXT_LIMIT].freeze
TYPE_SYNONYMS =

TODO: drop this synonym. -Colin

{ timestamp: :datetime }.freeze
SQL_OPTIONS =
[:limit, :precision, :scale, :null, :default, :charset, :collation].freeze
NON_SQL_OPTIONS =
[:ruby_default, :validates].freeze
VALID_OPTIONS =
(SQL_OPTIONS + NON_SQL_OPTIONS).freeze
OPTION_INDEXES =
Hash[VALID_OPTIONS.each_with_index.to_a].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(model, name, type, position: 0, **options) ⇒ FieldSpec

Returns a new instance of FieldSpec.



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
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
# File 'lib/declare_schema/model/field_spec.rb', line 49

def initialize(model, name, type, position: 0, **options)
  _declared_primary_key = model._declared_primary_key

  name.to_s == _declared_primary_key and raise ArgumentError, "you may not provide a field spec for the primary key #{name.inspect}"

  @model = model
  @name = name.to_sym
  type.is_a?(Symbol) or raise ArgumentError, "type must be a Symbol; got #{type.inspect}"
  @type = TYPE_SYNONYMS[type] || type
  @position = position
  @options = options.dup

  @options.has_key?(:null) or @options[:null] = ::DeclareSchema.default_null
  @options[:null].nil? and raise "null: must be provided for field #{model}##{@name}: #{@options.inspect} since ::DeclareSchema#default_null is set to 'nil'; do you want `null: false`?"

  case @type
  when :text
    if self.class.mysql_text_limits?
      @options[:default].nil? or raise MysqlTextMayNotHaveDefault, "when using MySQL, non-nil default may not be given for :text field #{model}##{@name}"
      @options[:limit] ||= ::DeclareSchema.default_text_limit or
            raise("limit: must be provided for :text field #{model}##{@name}: #{@options.inspect} since ::DeclareSchema#default_text_limit is set to 'nil'; do you want `limit: 0xffff_ffff`?")
      @options[:limit] = self.class.round_up_mysql_text_limit(@options[:limit])
    else
      @options.delete(:limit)
    end
  when :string
    @options[:limit] ||= ::DeclareSchema.default_string_limit or raise "limit: must be provided for :string field #{model}##{@name}: #{@options.inspect} since ::DeclareSchema#default_string_limit is set to 'nil'; do you want `limit: 255`?"
  when :bigint
    @type = :integer
    @options[:limit] = 8
  when :enum
    @options[:default].nil? || @options[:default].is_a?(Symbol) or
      raise ArgumentError, "enum default: must be nil or a Symbol; got #{@options[:default].inspect}"
    @options[:limit].is_a?(Array) && @options[:limit].size >= 1 && @options[:limit].all? { |value| value.is_a?(Symbol) } or
      raise ArgumentError, "enum limit: must be an array of 1 or more Symbols; got #{@options[:limit].inspect}"
  end

  Column.native_type?(@type) or raise UnknownTypeError, "#{@type.inspect} not found in #{Column.native_types.inspect} for adapter #{ActiveRecord::Base.connection.class.name}"

  if @type.in?([:string, :text, :binary, :varbinary, :integer])
    @options[:limit] ||= Column.native_types.dig(@type, :limit)
  elsif @type.in?([:enum])
    # nothing to do
  else
    @type != :decimal && @options.has_key?(:limit) and warn("unsupported limit: for SQL type #{@type} in field #{model}##{@name}")
    @options.delete(:limit)
  end

  if @type == :decimal
    @options[:precision] or warn("precision: required for :decimal type in field #{model}##{@name}")
    @options[:scale] or warn("scale: required for :decimal type in field #{model}##{@name}")
  else
    if @type != :datetime
      @options.has_key?(:precision) and warn("precision: only allowed for :decimal type or :datetime for SQL type #{@type} in field #{model}##{@name}")
    end
    @options.has_key?(:scale) and warn("scale: only allowed for :decimal type for SQL type #{@type} in field #{model}##{@name}")
  end

  if @type.in?([:text, :string])
    if ActiveRecord::Base.connection.class.name.match?(/mysql/i)
      @options[:charset]   ||= model._table_options&.[](:charset)   || ::DeclareSchema.default_charset
      @options[:charset] = DeclareSchema.normalize_charset(@options[:charset])
      @options[:collation] ||= model._table_options&.[](:collation) || ::DeclareSchema.default_collation
      @options[:collation] = DeclareSchema.normalize_collation(@options[:collation])
    else
      @options.delete(:charset)
      @options.delete(:collation)
    end
  else
    @options[:charset]   and warn("charset may only given for :string and :text fields for SQL type #{@type} in field #{model}##{@name}")
    @options[:collation] and warn("collation may only given for :string and :text fields for SQL type #{@type} in field #{model}##{@name}")
  end

  @options = Hash[@options.sort_by { |k, _v| OPTION_INDEXES[k] || 9999 }]

  @sql_options = @options.slice(*SQL_OPTIONS)
end

Instance Attribute Details

#modelObject (readonly)

Returns the value of attribute model.



36
37
38
# File 'lib/declare_schema/model/field_spec.rb', line 36

def model
  @model
end

#nameObject (readonly)

Returns the value of attribute name.



36
37
38
# File 'lib/declare_schema/model/field_spec.rb', line 36

def name
  @name
end

#optionsObject (readonly)

Returns the value of attribute options.



36
37
38
# File 'lib/declare_schema/model/field_spec.rb', line 36

def options
  @options
end

#positionObject (readonly)

Returns the value of attribute position.



36
37
38
# File 'lib/declare_schema/model/field_spec.rb', line 36

def position
  @position
end

#sql_optionsObject (readonly)

Returns the value of attribute sql_options.



36
37
38
# File 'lib/declare_schema/model/field_spec.rb', line 36

def sql_options
  @sql_options
end

#typeObject (readonly)

Returns the value of attribute type.



36
37
38
# File 'lib/declare_schema/model/field_spec.rb', line 36

def type
  @type
end

Class Method Details

.mysql_text_limits?Boolean

Returns:



19
20
21
22
23
24
25
# File 'lib/declare_schema/model/field_spec.rb', line 19

def mysql_text_limits?
  if defined?(@mysql_text_limits)
    @mysql_text_limits
  else
    @mysql_text_limits = ActiveRecord::Base.connection.class.name.match?(/mysql/i)
  end
end

.round_up_mysql_text_limit(limit) ⇒ Object



27
28
29
30
31
32
33
# File 'lib/declare_schema/model/field_spec.rb', line 27

def round_up_mysql_text_limit(limit)
  MYSQL_TEXT_LIMITS_ASCENDING.find do |mysql_supported_text_limit|
    if limit <= mysql_supported_text_limit
      mysql_supported_text_limit
    end
  end or raise ArgumentError, "limit of #{limit} is too large for MySQL"
end

Instance Method Details

#schema_attributes(col_spec) ⇒ Object

returns the attributes for schema migrations as a Hash omits name and position since those are meta-data above the schema omits keys with nil values



130
131
132
133
134
# File 'lib/declare_schema/model/field_spec.rb', line 130

def schema_attributes(col_spec)
  @sql_options.merge(type: @type).tap do |attrs|
    attrs[:default] = Column.deserialize_default_value(col_spec, @type, attrs[:default])
  end.compact
end