class InventoryRefresh::SaveCollection::Saver::Base
Attributes
Public Class Methods
@param inventory_collection
[InventoryRefresh::InventoryCollection] InventoryCollection
object we will be saving
# File lib/inventory_refresh/save_collection/saver/base.rb, line 13 def initialize(inventory_collection) @inventory_collection = inventory_collection @association = inventory_collection.db_collection_for_comparison # Private attrs @model_class = inventory_collection.model_class @table_name = @model_class.table_name @q_table_name = get_connection.quote_table_name(@table_name) @primary_key = @model_class.primary_key @arel_primary_key = @model_class.arel_attribute(@primary_key) @unique_index_keys = inventory_collection.unique_index_keys @unique_index_keys_to_s = inventory_collection.manager_ref_to_cols.map(&:to_s) @select_keys = [@primary_key] + @unique_index_keys_to_s + internal_columns.map(&:to_s) @unique_db_primary_keys = Set.new @unique_db_indexes = Set.new @batch_size_for_persisting = inventory_collection.batch_size_pure_sql @batch_size = inventory_collection.use_ar_object? ? @batch_size_for_persisting : inventory_collection.batch_size @record_key_method = inventory_collection.pure_sql_record_fetching? ? :pure_sql_record_key : :ar_record_key @select_keys_indexes = @select_keys.each_with_object({}).with_index { |(key, obj), index| obj[key.to_s] = index } @pg_types = @model_class.attribute_names.each_with_object({}) do |key, obj| obj[key.to_sym] = inventory_collection.model_class.columns_hash[key] .try(:sql_type_metadata) .try(:instance_values) .try(:[], "sql_type") end @serializable_keys = {} @deserializable_keys = {} @model_class.attribute_names.each do |key| attribute_type = @model_class.type_for_attribute(key.to_s) pg_type = @pg_types[key.to_sym] if inventory_collection.use_ar_object? # When using AR object, lets make sure we type.serialize(value) every value, so we have a slow but always # working way driven by a configuration @serializable_keys[key.to_sym] = attribute_type @deserializable_keys[key.to_sym] = attribute_type elsif attribute_type.respond_to?(:coder) || attribute_type.type == :int4range || attribute_type.type == :jsonb || pg_type == "text[]" || pg_type == "character varying[]" # Identify columns that needs to be encoded by type.serialize(value), it's a costy operations so lets do # do it only for columns we need it for. # TODO: should these set @deserializable_keys too? @serializable_keys[key.to_sym] = attribute_type elsif attribute_type.type == :decimal # Postgres formats decimal columns with fixed number of digits e.g. '0.100' # Need to parse and let Ruby format the value to have a comparable string. @serializable_keys[key.to_sym] = attribute_type @deserializable_keys[key.to_sym] = attribute_type end end end
Public Instance Methods
Saves the InventoryCollection
# File lib/inventory_refresh/save_collection/saver/base.rb, line 71 def save_inventory_collection! # Create/Update/Archive/Delete records based on InventoryCollection data and scope save!(association) end
Protected Instance Methods
# File lib/inventory_refresh/save_collection/saver/base.rb, line 104 def transform_to_hash!(all_attribute_keys, hash) if serializable_keys? values_for_database!(all_attribute_keys, hash) else hash end end
Applies serialize method for each relevant attribute, which will cast the value to the right type.
@param all_attribute_keys [Symbol] attribute keys we want to process @param attributes [Hash] attributes hash @return [Hash] modified hash from parameter attributes with casted values
# File lib/inventory_refresh/save_collection/saver/base.rb, line 91 def values_for_database!(all_attribute_keys, attributes) # TODO(lsmola) we'll need to fill default value from the DB to the NOT_NULL columns here, since sending NULL # to column with NOT_NULL constraint always fails, even if there is a default value all_attribute_keys.each do |key| next unless attributes.key?(key) if (type = serializable_keys[key]) attributes[key] = type.serialize(attributes[key]) end end attributes end
Private Instance Methods
Check if relation provided is distinct, i.e. the relation should not return the same primary key value twice.
@param primary_key_value [Bigint] primary key value @raise [Exception] if env is not production and relation is not distinct @return [Boolean] false if env is production and relation is not distinct
# File lib/inventory_refresh/save_collection/saver/base.rb, line 132 def assert_distinct_relation(primary_key_value) if unique_db_primary_keys.include?(primary_key_value) # Include on Set is O(1) # Change the InventoryCollection's :association or :arel parameter to return distinct results. The :through # relations can return the same record multiple times. We don't want to do SELECT DISTINCT by default, since # it can be very slow. unless inventory_collection.assert_graph_integrity logger.warn("Please update :association or :arel for #{inventory_collection} to return a DISTINCT result. "\ " The duplicate value is being ignored.") return false else raise("Please update :association or :arel for #{inventory_collection} to return a DISTINCT result. ") end else unique_db_primary_keys << primary_key_value end true end
Check that the needed foreign key leads to real value. This check simulates NOT NULL and FOREIGN KEY constraints we should have in the DB. The needed foreign keys are identified as fixed_foreign_keys, which are the foreign keys needed for saving of the record.
@param hash [Hash] data we want to save @raise [Exception] if env is not production and a foreign_key is missing @return [Boolean] false if env is production and a foreign_key is missing
# File lib/inventory_refresh/save_collection/saver/base.rb, line 157 def assert_referential_integrity(hash) inventory_collection.fixed_foreign_keys.each do |x| next unless hash[x].nil? subject = "#{hash} of #{inventory_collection} because of missing foreign key #{x} for "\ "#{inventory_collection.parent.class.name}:"\ "#{inventory_collection.parent.try(:id)}" unless inventory_collection.assert_graph_integrity logger.warn("Referential integrity check violated, ignoring #{subject}") return false else raise("Referential integrity check violated for #{subject}") end end true end
Enriches data hash with timestamp and type columns
@param hash [Hash] data hash @param create_time [Time] data hash
# File lib/inventory_refresh/save_collection/saver/base.rb, line 197 def assign_attributes_for_create!(hash, create_time) hash[:created_on] = create_time if supports_column?(:created_on) hash[:created_at] = create_time if supports_column?(:created_at) assign_attributes_for_update!(hash, create_time) end
Enriches data hash with timestamp columns
@param hash [Hash] data hash @param update_time [Time] data hash
# File lib/inventory_refresh/save_collection/saver/base.rb, line 187 def assign_attributes_for_update!(hash, update_time) hash[:type] = model_class.name if supports_sti? && hash[:type].nil? hash[:updated_on] = update_time if supports_column?(:updated_on) hash[:updated_at] = update_time if supports_column?(:updated_at) end
# File lib/inventory_refresh/save_collection/saver/base.rb, line 203 def internal_columns @internal_columns ||= inventory_collection.internal_columns end
@return [String] a string for logging purposes
# File lib/inventory_refresh/save_collection/saver/base.rb, line 123 def inventory_collection_details "strategy: #{inventory_collection.strategy}, saver_strategy: #{inventory_collection.saver_strategy}" end
@return [Boolean] true if any serializable keys are present
# File lib/inventory_refresh/save_collection/saver/base.rb, line 234 def serializable_keys? @serializable_keys_bool_cache ||= serializable_keys.present? end
@return [Boolean] true if the keys we are saving have resource_timestamp column
# File lib/inventory_refresh/save_collection/saver/base.rb, line 239 def supports_remote_data_timestamp?(all_attribute_keys) all_attribute_keys.include?(:resource_timestamp) # include? on Set is O(1) end
@return [Boolean] true if the keys we are saving have resource_counter column
# File lib/inventory_refresh/save_collection/saver/base.rb, line 244 def supports_remote_data_version?(all_attribute_keys) all_attribute_keys.include?(:resource_counter) # include? on Set is O(1) end
@return [Boolean] true if the keys we are saving have resource_version column, which solves for a quick check
if the record was modified
# File lib/inventory_refresh/save_collection/saver/base.rb, line 250 def supports_resource_version?(all_attribute_keys) all_attribute_keys.include?(resource_version_column) # include? on Set is O(1) end
@return [Boolean] true if the model_class
supports STI
# File lib/inventory_refresh/save_collection/saver/base.rb, line 229 def supports_sti? @supports_sti_cache ||= inventory_collection.supports_sti? end
@return [Time] A rails friendly time getting config from ActiveRecord::Base.default_timezone (can be :local
or :utc)
# File lib/inventory_refresh/save_collection/saver/base.rb, line 175 def time_now if ActiveRecord::Base.default_timezone == :utc Time.now.utc else Time.zone.now end end
@return [Array<Symbol>] all columns that are part of the best fit unique index
# File lib/inventory_refresh/save_collection/saver/base.rb, line 217 def unique_index_columns @unique_index_columns ||= inventory_collection.unique_index_columns end
@return [Array<String>] all columns that are part of the best fit unique index
# File lib/inventory_refresh/save_collection/saver/base.rb, line 222 def unique_index_columns_to_s return @unique_index_columns_to_s if @unique_index_columns_to_s @unique_index_columns_to_s = unique_index_columns.map(&:to_s) end
Finds an index that fits the list of columns (keys) the best
@param keys [Array<Symbol>] @raise [Exception] if the unique index for the columns was not found @return [ActiveRecord::ConnectionAdapters::IndexDefinition] unique index fitting the keys
# File lib/inventory_refresh/save_collection/saver/base.rb, line 212 def unique_index_for(keys) inventory_collection.unique_index_for(keys) end