Module: DeclareSchema::Model::ClassMethods

Defined in:
lib/declare_schema/model.rb

Instance Method Summary collapse

Instance Method Details

#_declared_primary_keyObject

returns the primary key (String) as declared with primary_key = unlike the ‘primary_key` method, DOES NOT query the database to find the actual primary key in use right now if no explicit primary key set, returns the _default_declared_primary_key



266
267
268
269
270
271
272
273
274
# File 'lib/declare_schema/model.rb', line 266

def _declared_primary_key
  if !defined?(@primary_key) ||
     (ActiveSupport.version >= Gem::Version.new('7.1.0') &&
       @primary_key == ActiveRecord::AttributeMethods::PrimaryKey::ClassMethods::PRIMARY_KEY_NOT_SET)
    _default_declared_primary_key
  else
    @primary_key&.to_s
  end
end

#_infer_fk_limit(foreign_key_column, reflection) ⇒ Object



241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
# File 'lib/declare_schema/model.rb', line 241

def _infer_fk_limit(foreign_key_column, reflection)
  if reflection.options[:polymorphic]
    if (foreign_key_column = columns_hash[foreign_key_column.to_s]) && foreign_key_column.type == :integer
      foreign_key_column.limit
    end
  else
    klass = reflection.klass or raise "Couldn't find belongs_to klass for #{name} in #{reflection.inspect}"
    if (pk_id_type = klass._table_options&.[](:id))
      if pk_id_type == :integer
        4
      end
    else
      if klass.table_exists? && (pk_column = klass.columns_hash[klass._declared_primary_key])
        pk_id_type = pk_column.type
        if pk_id_type == :integer
          pk_column.limit
        end
      end
    end
  end
end

#attr_type(name) ⇒ Object

Returns the type (a class) for a given field or association. If the association is a collection (has_many or habtm) return the AssociationReflection instead



383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
# File 'lib/declare_schema/model.rb', line 383

public \
def attr_type(name)
  if attr_types.nil? && self != self.name.constantize
    raise "attr_types called on a stale class object (#{self.name}). Avoid storing persistent references to classes"
  end

  attr_types[name] ||
    if (reflection = reflections[name.to_s])
      if reflection.macro.in?([:has_one, :belongs_to]) && !reflection.options[:polymorphic]
        reflection.klass
      else
        reflection
      end
    end ||
    if (col = _column(name.to_s))
      DeclareSchema::PLAIN_TYPES[col.type] || col.klass
    end
end

#belongs_to(name, scope = nil, **options) ⇒ Object

Extend belongs_to so that it

  1. creates a FieldSpec for the foreign key

  2. declares an index on the foreign key (optional)

  3. declares a foreign_key constraint (optional)

Other options are passed through to super

Parameters:

  • name (Symbol)

    the name of the association to pass to super

  • scope (Proc) (defaults to: nil)

    the scope of the association to pass to super

  • options (Hash)

    a customizable set of options

Options Hash (**options):

  • :optional (Boolean) — default: default: false

    whether the foreign key column should be nullable and whether ActiveRecord should validate presence of the foreign key (passed through to super)

  • :null (Boolean) — default: default: inferred from options[:optional]

    whether the foreign key column should be nullable (‘null:` should only be passed if it is the inverse of `optional:`; otherwise it is redundant)

  • :limit (Integer) — default: default: inferred from the primary key limit:

    the limit of the foreign key column size (4 or 8)

  • :index (Boolean|Hash<Symbol>) — default: default: true

    whether to create an index on the foreign key; can be true or false or a hash of options to pass to the index declaration, with keys like { name: …, unique: … }

  • :allow_equivalent (Boolean) — default: default: false

    whether to allow an existing index with a different name

  • :constraint (Boolean|String) — default: default: true

    whether to create a foreign key constraint on the foreign key; may be true or false or a string to use as the constraint name

  • :polymorphic (Boolean) — default: default: false

    whether this is a polymorphic belongs_to with a _type column next to the foreign key _id column (also passed through to super)

  • :far_end_dependent (Boolean) — default: default: nil

    whether to add a dependent: :delete to the far end of the foreign key constraint

  • :foreign_type (String) — default: default: "#{name}_type"

    the name prefix for the _type column for a polymorphic belongs_to (passed through to super)



138
139
140
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
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
# File 'lib/declare_schema/model.rb', line 138

def belongs_to(name, scope = nil, **options)
  if options[:null].in?([true, false]) && options[:optional] == options[:null]
    STDERR.puts("[declare_schema warning] belongs_to #{name.inspect}, null: with the same value as optional: is redundant; omit null: #{options[:null]} (called from #{caller[0]})")
  elsif !options.has_key?(:optional)
    case options[:null]
    when true
      STDERR.puts("[declare_schema] belongs_to #{name.inspect}, null: true is deprecated in favor of optional: true (called from #{caller[0]})")
    when false
      STDERR.puts("[declare_schema] belongs_to #{name.inspect}, null: false is implied and can be omitted (called from #{caller[0]})")
    end
  end

  column_options = {}

  column_options[:null] = if options.has_key?(:null)
                            options.delete(:null)
                          elsif options.has_key?(:optional)
                            options[:optional] # infer :null from :optional
                          end || false
  column_options[:default] = options.delete(:default) if options.has_key?(:default)
  if options.has_key?(:limit)
    limit = options.delete(:limit)
    DeclareSchema.deprecator.warn("belongs_to #{name.inspect}, limit: #{limit} is deprecated since it is now inferred")
  end

  # index: true means create an index on the foreign key
  # index: false means do not create an index on the foreign key
  # index: { ... } means create an index on the foreign key with the given options
  index_value = options.delete(:index)
  if index_value == false # don't create an index
    options.delete(:unique)
    options.delete(:allow_equivalent)
  else
    index_options = {} # create an index
    case index_value
    when String, Symbol
      DeclareSchema.deprecator.warn("[declare_schema] belongs_to #{name.inspect}, index: 'name' is deprecated; use index: { name: 'name' } instead (in #{self.name})")
      index_options[:name] = index_value.to_s
    when true
    when nil
    when Hash
      index_options = index_value
    else
      raise ArgumentError, "[declare_schema] belongs_to #{name.inspect}, index: must be true or false or a Hash; got #{index_value.inspect} (in #{self.name})"
    end

    if options.has_key?(:unique)
      DeclareSchema.deprecator.warn("[declare_schema] belongs_to #{name.inspect}, unique: true|false is deprecated; use index: { unique: true|false } instead (in #{self.name})")
      index_options[:unique] = options.delete(:unique)
    end

    index_options[:allow_equivalent] = options.delete(:allow_equivalent) if options.has_key?(:allow_equivalent)
  end

  constraint_name = options.delete(:constraint)

  dependent_delete = :delete if options.delete(:far_end_dependent) == :delete

  # infer :optional from :null
  if !options.has_key?(:optional)
    options[:optional] = column_options[:null]
  end

  super

  reflection = reflections[name.to_s] or raise "Couldn't find reflection #{name} in #{reflections.keys}"
  foreign_key_column = reflection.foreign_key or raise "Couldn't find foreign_key for #{name} in #{reflection.inspect}"
  foreign_key_column_options = column_options.dup

  # Note: the foreign key limit: should match the primary key limit:. (If there is a foreign key constraint,
  # those limits _must_ match.) We'd like to call _infer_fk_limit and get the limit right from the PK.
  # But we can't here, because that will mess up the autoloader to follow every belongs_to association right
  # when it is declared. So instead we assume :bigint (integer limit: 8) below, while also registering this
  # pre_migration: callback to double-check that assumption Just In Time--right before we generate a migration.
  #
  # The one downside of this approach is that application code that asks the field_spec for the declared
  # foreign key limit: will always get 8 back even if this is a grandfathered foreign key that points to
  # a limit: 4 primary key. It seems unlikely that any application code would do this.
  foreign_key_column_options[:pre_migration] = ->(field_spec) do
    if (inferred_limit = _infer_fk_limit(foreign_key_column, reflection))
      field_spec.sql_options[:limit] = inferred_limit
    end
  end

  declare_field(foreign_key_column.to_sym, :bigint, **foreign_key_column_options)

  if reflection.options[:polymorphic]
    foreign_type = options[:foreign_type] || "#{name}_type"
    _declare_polymorphic_type_field(foreign_type, column_options)
    if ::DeclareSchema.default_generate_indexing && index_options
      index([foreign_type, foreign_key_column], **index_options)
    end
  else
    if ::DeclareSchema.default_generate_indexing && index_options
      index([foreign_key_column], **index_options)
    end

    if ::DeclareSchema.default_generate_foreign_keys && constraint_name != false
      constraint(foreign_key_column, constraint_name: constraint_name || index_options&.[](:name), parent_class_name: reflection.class_name, dependent: dependent_delete)
    end
  end
end

#constraint(foreign_key_column, parent_table_name: nil, constraint_name: nil, parent_class_name: nil, dependent: nil) ⇒ Object



76
77
78
79
80
81
82
83
84
# File 'lib/declare_schema/model.rb', line 76

def constraint(foreign_key_column, parent_table_name: nil, constraint_name: nil, parent_class_name: nil, dependent: nil)
  constraint_definition = ::DeclareSchema::Model::ForeignKeyDefinition.new(
    foreign_key_column.to_s,
    constraint_name: constraint_name,
    child_table_name: table_name, parent_table_name: parent_table_name, parent_class_name: parent_class_name, dependent: dependent
  )

  constraint_definitions << constraint_definition # Set<> implements idempotent insert.
end

#declare_field(name, type, *args, **options) ⇒ Object

Declare named field with a type and an arbitrary set of arguments. The arguments are forwarded to the #field_added callback, allowing custom metadata to be added to field declarations.



96
97
98
99
100
101
102
103
104
105
# File 'lib/declare_schema/model.rb', line 96

def declare_field(name, type, *args, **options)
  try(:field_added, name, type, args, options)
  _add_serialize_for_field(name, type, options)
  _add_formatting_for_field(name, type)
  _add_validations_for_field(name, type, args, options)
  _add_index_for_field(name, args, **options)
  _add_scopes_for_field(name, type, **options)
  field_specs[name] = ::DeclareSchema::Model::FieldSpec.new(self, name, type, position: field_specs.size, **options)
  attr_order << name unless attr_order.include?(name)
end

#ignore_index(index_name) ⇒ Object

tell the migration generator to ignore the named index. Useful for existing indexes, or for indexes that can’t be automatically generated.



88
89
90
# File 'lib/declare_schema/model.rb', line 88

def ignore_index(index_name)
  ignore_indexes << index_name.to_s
end

#index(columns, name: nil, allow_equivalent: true, ignore_equivalent_definitions: false, unique: false, where: nil, length: nil) ⇒ Object



52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# File 'lib/declare_schema/model.rb', line 52

def index(columns, name: nil, allow_equivalent: true, ignore_equivalent_definitions: false, unique: false, where: nil, length: nil)
  index_definition = ::DeclareSchema::Model::IndexDefinition.new(
    columns,
    name: name, table_name: table_name, allow_equivalent: allow_equivalent, unique: unique, where: where, length: length
  )

  if (equivalent = index_definitions.find { index_definition.equivalent?(_1) }) # differs only by name
    if equivalent == index_definition
      # identical is always idempotent
    else
      # equivalent is idempotent iff ignore_equivalent_definitions: true passed
      ignore_equivalent_definitions or
        raise ArgumentError, "equivalent index definition found (pass ignore_equivalent_definitions: true to ignore):\n" \
                             "#{index_definition.inspect}\n#{equivalent.inspect}"
    end
  else
    index_definitions << index_definition
  end
end

#index_definitions_with_primary_keyObject



107
108
109
110
111
112
113
# File 'lib/declare_schema/model.rb', line 107

def index_definitions_with_primary_key
  if index_definitions.any?(&:primary_key?)
    index_definitions
  else
    index_definitions + [_rails_default_primary_key]
  end
end

#primary_key_index(*columns) ⇒ Object



72
73
74
# File 'lib/declare_schema/model.rb', line 72

def primary_key_index(*columns)
  index(columns.flatten, unique: true, name: ::DeclareSchema::Model::IndexDefinition::PRIMARY_KEY_NAME)
end