summaryrefslogtreecommitdiff
path: root/app/models/concerns
diff options
context:
space:
mode:
Diffstat (limited to 'app/models/concerns')
-rw-r--r--app/models/concerns/ci/has_status.rb4
-rw-r--r--app/models/concerns/ci/metadatable.rb10
-rw-r--r--app/models/concerns/ci/partitionable.rb9
-rw-r--r--app/models/concerns/ci/partitionable/switch.rb57
-rw-r--r--app/models/concerns/ci/raw_variable.rb17
-rw-r--r--app/models/concerns/ci/track_environment_usage.rb2
-rw-r--r--app/models/concerns/encrypted_user_password.rb82
-rw-r--r--app/models/concerns/enums/sbom.rb15
-rw-r--r--app/models/concerns/file_store_mounter.rb34
-rw-r--r--app/models/concerns/issuable.rb17
-rw-r--r--app/models/concerns/milestoneable.rb2
-rw-r--r--app/models/concerns/mirror_authentication.rb2
-rw-r--r--app/models/concerns/noteable.rb22
-rw-r--r--app/models/concerns/packages/debian/distribution.rb9
-rw-r--r--app/models/concerns/pg_full_text_searchable.rb47
-rw-r--r--app/models/concerns/project_features_compatibility.rb4
-rw-r--r--app/models/concerns/protected_ref.rb1
-rw-r--r--app/models/concerns/protected_ref_access.rb4
-rw-r--r--app/models/concerns/redis_cacheable.rb10
-rw-r--r--app/models/concerns/repository_storage_movable.rb4
-rw-r--r--app/models/concerns/subquery.rb24
-rw-r--r--app/models/concerns/ttl_expirable.rb2
22 files changed, 312 insertions, 66 deletions
diff --git a/app/models/concerns/ci/has_status.rb b/app/models/concerns/ci/has_status.rb
index 910885c833f..9a04776f1c6 100644
--- a/app/models/concerns/ci/has_status.rb
+++ b/app/models/concerns/ci/has_status.rb
@@ -110,6 +110,10 @@ module Ci
COMPLETED_STATUSES.include?(status)
end
+ def incomplete?
+ COMPLETED_STATUSES.exclude?(status)
+ end
+
def blocked?
BLOCKED_STATUS.include?(status)
end
diff --git a/app/models/concerns/ci/metadatable.rb b/app/models/concerns/ci/metadatable.rb
index ff884984099..d93f4a150d5 100644
--- a/app/models/concerns/ci/metadatable.rb
+++ b/app/models/concerns/ci/metadatable.rb
@@ -87,6 +87,16 @@ module Ci
ensure_metadata.id_tokens = value
end
+ def enqueue_immediately?
+ !!options[:enqueue_immediately]
+ end
+
+ def set_enqueue_immediately!
+ # ensures that even if `config_options: nil` in the database we set the
+ # new value correctly.
+ self.options = options.merge(enqueue_immediately: true)
+ end
+
private
def read_metadata_attribute(legacy_key, metadata_key, default_value = nil)
diff --git a/app/models/concerns/ci/partitionable.rb b/app/models/concerns/ci/partitionable.rb
index df803180e77..68a6714c892 100644
--- a/app/models/concerns/ci/partitionable.rb
+++ b/app/models/concerns/ci/partitionable.rb
@@ -57,9 +57,14 @@ module Ci
end
class_methods do
- private
+ def partitionable(scope:, through: nil)
+ if through
+ define_singleton_method(:routing_table_name) { through[:table] }
+ define_singleton_method(:routing_table_name_flag) { through[:flag] }
+
+ include Partitionable::Switch
+ end
- def partitionable(scope:)
define_method(:partition_scope_value) do
strong_memoize(:partition_scope_value) do
next Ci::Pipeline.current_partition_value if respond_to?(:importing?) && importing?
diff --git a/app/models/concerns/ci/partitionable/switch.rb b/app/models/concerns/ci/partitionable/switch.rb
new file mode 100644
index 00000000000..c1bbd107e9f
--- /dev/null
+++ b/app/models/concerns/ci/partitionable/switch.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+module Ci
+ module Partitionable
+ module Switch
+ extend ActiveSupport::Concern
+
+ # These methods are cached at the class level and depend on the value
+ # of `table_name`, changing that value resets them.
+ # `cached_find_by_statement` is used to cache SQL statements which can
+ # include the table name.
+ #
+ SWAPABLE_METHODS = %i[table_name quoted_table_name arel_table
+ predicate_builder cached_find_by_statement].freeze
+
+ included do |base|
+ partitioned = Class.new(base) do
+ self.table_name = base.routing_table_name
+
+ def self.routing_class?
+ true
+ end
+ end
+
+ base.const_set(:Partitioned, partitioned)
+ end
+
+ class_methods do
+ def routing_class?
+ false
+ end
+
+ def routing_table_enabled?
+ return false if routing_class?
+
+ Gitlab::SafeRequestStore.fetch(routing_table_name_flag) do
+ ::Feature.enabled?(routing_table_name_flag)
+ end
+ end
+
+ # We're delegating them to the `Partitioned` model.
+ # They do not require any check override since they come from AR core
+ # (are always defined) and we're using `super` to get the value.
+ #
+ SWAPABLE_METHODS.each do |name|
+ define_method(name) do |*args, &block|
+ if routing_table_enabled?
+ self::Partitioned.public_send(name, *args, &block) # rubocop: disable GitlabSecurity/PublicSend
+ else
+ super(*args, &block)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/models/concerns/ci/raw_variable.rb b/app/models/concerns/ci/raw_variable.rb
new file mode 100644
index 00000000000..5cfc781c9f1
--- /dev/null
+++ b/app/models/concerns/ci/raw_variable.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Ci
+ module RawVariable
+ extend ActiveSupport::Concern
+
+ included do
+ validates :raw, inclusion: { in: [true, false] }
+ end
+
+ private
+
+ def uncached_runner_variable
+ super.merge(raw: raw?)
+ end
+ end
+end
diff --git a/app/models/concerns/ci/track_environment_usage.rb b/app/models/concerns/ci/track_environment_usage.rb
index 45d9cdeeb59..fe548c77590 100644
--- a/app/models/concerns/ci/track_environment_usage.rb
+++ b/app/models/concerns/ci/track_environment_usage.rb
@@ -17,7 +17,7 @@ module Ci
end
def verifies_environment?
- has_environment? && environment_action == 'verify'
+ has_environment_keyword? && environment_action == 'verify'
end
def count_user_deployment?
diff --git a/app/models/concerns/encrypted_user_password.rb b/app/models/concerns/encrypted_user_password.rb
new file mode 100644
index 00000000000..97e6592f442
--- /dev/null
+++ b/app/models/concerns/encrypted_user_password.rb
@@ -0,0 +1,82 @@
+# frozen_string_literal: true
+
+# Support for both BCrypt and PBKDF2+SHA512 user passwords
+# Meant to be used exclusively with User model but extracted
+# to a concern for isolation and clarity.
+module EncryptedUserPassword
+ extend ActiveSupport::Concern
+
+ BCRYPT_PREFIX = '$2a$'
+ PBKDF2_SHA512_PREFIX = '$pbkdf2-sha512$'
+
+ BCRYPT_STRATEGY = :bcrypt
+ PBKDF2_SHA512_STRATEGY = :pbkdf2_sha512
+
+ # Use Devise DatabaseAuthenticatable#authenticatable_salt
+ # unless encrypted password is PBKDF2+SHA512.
+ def authenticatable_salt
+ return super unless pbkdf2_password?
+
+ Devise::Pbkdf2Encryptable::Encryptors::Pbkdf2Sha512.split_digest(encrypted_password)[:salt]
+ end
+
+ # Called by Devise during database authentication.
+ # Also migrates the user password to the configured
+ # encryption type (BCrypt or PBKDF2+SHA512), if needed.
+ def valid_password?(password)
+ return false unless password_matches?(password)
+
+ migrate_password!(password)
+ end
+
+ def password=(new_password)
+ @password = new_password # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ return unless new_password.present?
+
+ self.encrypted_password = if Gitlab::FIPS.enabled?
+ Devise::Pbkdf2Encryptable::Encryptors::Pbkdf2Sha512.digest(
+ new_password,
+ Devise::Pbkdf2Encryptable::Encryptors::Pbkdf2Sha512::STRETCHES,
+ Devise.friendly_token[0, 16])
+ else
+ Devise::Encryptor.digest(self.class, new_password)
+ end
+ end
+
+ private
+
+ def password_strategy
+ return BCRYPT_STRATEGY if encrypted_password.starts_with?(BCRYPT_PREFIX)
+ return PBKDF2_SHA512_STRATEGY if encrypted_password.starts_with?(PBKDF2_SHA512_PREFIX)
+
+ :unknown
+ end
+
+ def pbkdf2_password?
+ password_strategy == PBKDF2_SHA512_STRATEGY
+ end
+
+ def bcrypt_password?
+ password_strategy == BCRYPT_STRATEGY
+ end
+
+ def password_matches?(password)
+ if bcrypt_password?
+ Devise::Encryptor.compare(self.class, encrypted_password, password)
+ elsif pbkdf2_password?
+ Devise::Pbkdf2Encryptable::Encryptors::Pbkdf2Sha512.compare(encrypted_password, password)
+ end
+ end
+
+ def migrate_password!(password)
+ return true if password_strategy == encryptor
+
+ update_attribute(:password, password)
+ end
+
+ def encryptor
+ return BCRYPT_STRATEGY unless Gitlab::FIPS.enabled?
+
+ PBKDF2_SHA512_STRATEGY
+ end
+end
diff --git a/app/models/concerns/enums/sbom.rb b/app/models/concerns/enums/sbom.rb
index 518efa669ad..8848c0c5555 100644
--- a/app/models/concerns/enums/sbom.rb
+++ b/app/models/concerns/enums/sbom.rb
@@ -6,8 +6,23 @@ module Enums
library: 0
}.with_indifferent_access.freeze
+ PURL_TYPES = {
+ composer: 1, # refered to as `packagist` in gemnasium-db
+ conan: 2,
+ gem: 3,
+ golang: 4, # refered to as `go` in gemnasium-db
+ maven: 5,
+ npm: 6,
+ nuget: 7,
+ pypi: 8
+ }.with_indifferent_access.freeze
+
def self.component_types
COMPONENT_TYPES
end
+
+ def self.purl_types
+ PURL_TYPES
+ end
end
end
diff --git a/app/models/concerns/file_store_mounter.rb b/app/models/concerns/file_store_mounter.rb
index f1ac734635d..4d267dc69d0 100644
--- a/app/models/concerns/file_store_mounter.rb
+++ b/app/models/concerns/file_store_mounter.rb
@@ -1,31 +1,35 @@
# frozen_string_literal: true
module FileStoreMounter
+ ALLOWED_FILE_FIELDS = %i[file signed_file].freeze
+
extend ActiveSupport::Concern
class_methods do
- # When `skip_store_file: true` is used, the model MUST explicitly call `store_file_now!`
- def mount_file_store_uploader(uploader, skip_store_file: false)
- mount_uploader(:file, uploader)
+ # When `skip_store_file: true` is used, the model MUST explicitly call `store_#{file_field}_now!`
+ def mount_file_store_uploader(uploader, skip_store_file: false, file_field: :file)
+ raise ArgumentError, "file_field not allowed: #{file_field}" unless ALLOWED_FILE_FIELDS.include?(file_field)
+
+ mount_uploader(file_field, uploader)
+
+ define_method("update_#{file_field}_store") do
+ # The file.object_store is set during `uploader.store!` and `uploader.migrate!`
+ update_column("#{file_field}_store", public_send(file_field).object_store) # rubocop:disable GitlabSecurity/PublicSend
+ end
+
+ define_method("store_#{file_field}_now!") do
+ public_send("store_#{file_field}!") # rubocop:disable GitlabSecurity/PublicSend
+ public_send("update_#{file_field}_store") # rubocop:disable GitlabSecurity/PublicSend
+ end
if skip_store_file
- skip_callback :save, :after, :store_file!
+ skip_callback :save, :after, "store_#{file_field}!".to_sym
return
end
# This hook is a no-op when the file is uploaded after_commit
- after_save :update_file_store, if: :saved_change_to_file?
+ after_save "update_#{file_field}_store".to_sym, if: "saved_change_to_#{file_field}?".to_sym
end
end
-
- def update_file_store
- # The file.object_store is set during `uploader.store!` and `uploader.migrate!`
- update_column(:file_store, file.object_store)
- end
-
- def store_file_now!
- store_file!
- update_file_store
- end
end
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index f8389865f91..31b2a8d7cc1 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -217,6 +217,10 @@ module Issuable
false
end
+ def supports_confidentiality?
+ false
+ end
+
def severity
return IssuableSeverity::DEFAULT unless supports_severity?
@@ -236,7 +240,6 @@ module Issuable
end
def validate_assignee_size_length
- return true unless Feature.enabled?(:limit_assignees_per_issuable)
return true unless assignees.size > MAX_NUMBER_OF_ASSIGNEES_OR_REVIEWERS
errors.add :assignees,
@@ -460,18 +463,6 @@ module Issuable
end
end
- def today?
- Date.today == created_at.to_date
- end
-
- def created_hours_ago
- (Time.now.utc.to_i - created_at.utc.to_i) / 3600
- end
-
- def new?
- created_hours_ago < 24
- end
-
def open?
opened?
end
diff --git a/app/models/concerns/milestoneable.rb b/app/models/concerns/milestoneable.rb
index 14c54d99ef3..a95bed7ad42 100644
--- a/app/models/concerns/milestoneable.rb
+++ b/app/models/concerns/milestoneable.rb
@@ -18,7 +18,7 @@ module Milestoneable
scope :with_milestone, ->(title) { left_joins_milestones.where(milestones: { title: title }) }
scope :without_particular_milestones, ->(titles) { left_outer_joins(:milestone).where("milestones.title NOT IN (?) OR milestone_id IS NULL", titles) }
scope :any_release, -> { joins_milestone_releases }
- scope :with_release, -> (tag, project_id) { joins_milestone_releases.where( milestones: { releases: { tag: tag, project_id: project_id } } ) }
+ scope :with_release, -> (tag, project_id) { joins_milestone_releases.where(milestones: { releases: { tag: tag, project_id: project_id } }) }
scope :without_particular_release, -> (tag, project_id) { joins_milestone_releases.where.not(milestones: { releases: { tag: tag, project_id: project_id } }) }
scope :left_joins_milestones, -> { joins("LEFT OUTER JOIN milestones ON #{table_name}.milestone_id = milestones.id") }
diff --git a/app/models/concerns/mirror_authentication.rb b/app/models/concerns/mirror_authentication.rb
index 14c8be93ce0..e3bfeaf7f95 100644
--- a/app/models/concerns/mirror_authentication.rb
+++ b/app/models/concerns/mirror_authentication.rb
@@ -11,7 +11,7 @@ module MirrorAuthentication
# We should generate a key even if there's no SSH URL present
before_validation :generate_ssh_private_key!, if: -> {
- regenerate_ssh_private_key || ( auth_method == 'ssh_public_key' && ssh_private_key.blank? )
+ regenerate_ssh_private_key || (auth_method == 'ssh_public_key' && ssh_private_key.blank?)
}
credentials_field :auth_method, reader: false
diff --git a/app/models/concerns/noteable.rb b/app/models/concerns/noteable.rb
index c1aac235d33..492d55c74e2 100644
--- a/app/models/concerns/noteable.rb
+++ b/app/models/concerns/noteable.rb
@@ -210,11 +210,23 @@ module Noteable
# Synthetic system notes don't have discussion IDs because these are generated dynamically
# in Ruby. These are always root notes anyway so we don't need to group by discussion ID.
def synthetic_note_ids_relations
- [
- resource_label_events.select("'resource_label_events'", "'NULL'", :id, :created_at),
- resource_milestone_events.select("'resource_milestone_events'", "'NULL'", :id, :created_at),
- resource_state_events.select("'resource_state_events'", "'NULL'", :id, :created_at)
- ]
+ relations = []
+
+ # currently multiple models include Noteable concern, but not all of them support
+ # all resource events, so we check if given model supports given resource event.
+ if respond_to?(:resource_label_events)
+ relations << resource_label_events.select("'resource_label_events'", "'NULL'", :id, :created_at)
+ end
+
+ if respond_to?(:resource_state_events)
+ relations << resource_state_events.select("'resource_state_events'", "'NULL'", :id, :created_at)
+ end
+
+ if respond_to?(:resource_milestone_events)
+ relations << resource_milestone_events.select("'resource_milestone_events'", "'NULL'", :id, :created_at)
+ end
+
+ relations
end
end
diff --git a/app/models/concerns/packages/debian/distribution.rb b/app/models/concerns/packages/debian/distribution.rb
index 1520ec0828e..75fd45d13a9 100644
--- a/app/models/concerns/packages/debian/distribution.rb
+++ b/app/models/concerns/packages/debian/distribution.rb
@@ -85,8 +85,7 @@ module Packages
scope :with_codename_or_suite, ->(codename_or_suite) { with_codename(codename_or_suite).or(with_suite(codename_or_suite)) }
mount_file_store_uploader Packages::Debian::DistributionReleaseFileUploader
- mount_uploader :signed_file, Packages::Debian::DistributionReleaseFileUploader
- after_save :update_signed_file_store, if: :saved_change_to_signed_file?
+ mount_file_store_uploader Packages::Debian::DistributionReleaseFileUploader, file_field: :signed_file
def component_names
components.pluck(:name).sort
@@ -119,12 +118,6 @@ module Packages
self.class.with_container(container).with_codename(suite).exists?
end
-
- def update_signed_file_store
- # The signed_file.object_store is set during `uploader.store!`
- # which happens after object is inserted/updated
- self.update_column(:signed_file_store, signed_file.object_store)
- end
end
end
end
diff --git a/app/models/concerns/pg_full_text_searchable.rb b/app/models/concerns/pg_full_text_searchable.rb
index 335fcec2611..562c8cf23f3 100644
--- a/app/models/concerns/pg_full_text_searchable.rb
+++ b/app/models/concerns/pg_full_text_searchable.rb
@@ -25,6 +25,7 @@ module PgFullTextSearchable
TSVECTOR_MAX_LENGTH = 1.megabyte.freeze
TEXT_SEARCH_DICTIONARY = 'english'
URL_SCHEME_REGEX = %r{(?<=\A|\W)\w+://(?=\w+)}.freeze
+ TSQUERY_DISALLOWED_CHARACTERS_REGEX = %r{[^a-zA-Z0-9 .@/\-_"]}.freeze
def update_search_data!
tsvector_sql_nodes = self.class.pg_full_text_searchable_columns.map do |column, weight|
@@ -102,21 +103,16 @@ module PgFullTextSearchable
end
end
- def pg_full_text_search(search_term)
+ def pg_full_text_search(query, matched_columns: [])
search_data_table = reflect_on_association(:search_data).klass.arel_table
- # This fixes an inconsistency with how to_tsvector and websearch_to_tsquery process URLs
- # See https://gitlab.com/gitlab-org/gitlab/-/issues/354784#note_905431920
- search_term = remove_url_scheme(search_term)
- search_term = ActiveSupport::Inflector.transliterate(search_term)
-
joins(:search_data).where(
Arel::Nodes::InfixOperation.new(
'@@',
search_data_table[:search_vector],
Arel::Nodes::NamedFunction.new(
- 'websearch_to_tsquery',
- [Arel::Nodes.build_quoted(TEXT_SEARCH_DICTIONARY), Arel::Nodes.build_quoted(search_term)]
+ 'to_tsquery',
+ [Arel::Nodes.build_quoted(TEXT_SEARCH_DICTIONARY), build_tsquery(query, matched_columns)]
)
)
)
@@ -124,8 +120,39 @@ module PgFullTextSearchable
private
- def remove_url_scheme(search_term)
- search_term.gsub(URL_SCHEME_REGEX, '')
+ def build_tsquery(query, matched_columns)
+ # URLs get broken up into separate words when : is removed below, so we just remove the whole scheme.
+ query = remove_url_scheme(query)
+ # Remove accents from search term to match indexed data
+ query = ActiveSupport::Inflector.transliterate(query)
+ # Prevent users from using tsquery operators that can cause syntax errors.
+ query = filter_allowed_characters(query)
+
+ weights = matched_columns.map do |column_name|
+ pg_full_text_searchable_columns[column_name]
+ end.compact.join
+ prefix_search_suffix = ":*#{weights}"
+
+ tsquery = Gitlab::SQL::Pattern.split_query_to_search_terms(query).map do |search_term|
+ case search_term
+ when /\A\d+\z/ # Handles https://gitlab.com/gitlab-org/gitlab/-/issues/375337
+ "(#{search_term + prefix_search_suffix} | -#{search_term + prefix_search_suffix})"
+ when /\s/
+ search_term.split.map { |t| "#{t}:#{weights}" }.join(' <-> ')
+ else
+ search_term + prefix_search_suffix
+ end
+ end.join(' & ')
+
+ Arel::Nodes.build_quoted(tsquery)
+ end
+
+ def remove_url_scheme(query)
+ query.gsub(URL_SCHEME_REGEX, '')
+ end
+
+ def filter_allowed_characters(query)
+ query.gsub(TSQUERY_DISALLOWED_CHARACTERS_REGEX, ' ')
end
end
end
diff --git a/app/models/concerns/project_features_compatibility.rb b/app/models/concerns/project_features_compatibility.rb
index 2976b6f02a7..d37f20e2e7c 100644
--- a/app/models/concerns/project_features_compatibility.rb
+++ b/app/models/concerns/project_features_compatibility.rb
@@ -110,6 +110,10 @@ module ProjectFeaturesCompatibility
write_feature_attribute_string(:releases_access_level, value)
end
+ def infrastructure_access_level=(value)
+ write_feature_attribute_string(:infrastructure_access_level, value)
+ end
+
# TODO: Remove this method after we drop support for project create/edit APIs to set the
# container_registry_enabled attribute. They can instead set the container_registry_access_level
# attribute.
diff --git a/app/models/concerns/protected_ref.rb b/app/models/concerns/protected_ref.rb
index ec56f4a32af..7e1ebd1eba3 100644
--- a/app/models/concerns/protected_ref.rb
+++ b/app/models/concerns/protected_ref.rb
@@ -7,7 +7,6 @@ module ProtectedRef
belongs_to :project, touch: true
validates :name, presence: true
- validates :project, presence: true
delegate :matching, :matches?, :wildcard?, to: :ref_matcher
diff --git a/app/models/concerns/protected_ref_access.rb b/app/models/concerns/protected_ref_access.rb
index 618ad96905d..facf0808e7a 100644
--- a/app/models/concerns/protected_ref_access.rb
+++ b/app/models/concerns/protected_ref_access.rb
@@ -21,8 +21,8 @@ module ProtectedRefAccess
included do
scope :maintainer, -> { where(access_level: Gitlab::Access::MAINTAINER) }
scope :developer, -> { where(access_level: Gitlab::Access::DEVELOPER) }
- scope :by_user, -> (user) { where(user_id: user ) }
- scope :by_group, -> (group) { where(group_id: group ) }
+ scope :by_user, -> (user) { where(user_id: user) }
+ scope :by_group, -> (group) { where(group_id: group) }
scope :for_role, -> { where(user_id: nil, group_id: nil) }
scope :for_user, -> { where.not(user_id: nil) }
scope :for_group, -> { where.not(group_id: nil) }
diff --git a/app/models/concerns/redis_cacheable.rb b/app/models/concerns/redis_cacheable.rb
index 2d4ed51ce3b..f1d29ad5a90 100644
--- a/app/models/concerns/redis_cacheable.rb
+++ b/app/models/concerns/redis_cacheable.rb
@@ -26,8 +26,8 @@ module RedisCacheable
end
def cache_attributes(values)
- Gitlab::Redis::Cache.with do |redis|
- redis.set(cache_attribute_key, values.to_json, ex: CACHED_ATTRIBUTES_EXPIRY_TIME)
+ with_redis do |redis|
+ redis.set(cache_attribute_key, Gitlab::Json.dump(values), ex: CACHED_ATTRIBUTES_EXPIRY_TIME)
end
clear_memoization(:cached_attributes)
@@ -41,13 +41,17 @@ module RedisCacheable
def cached_attributes
strong_memoize(:cached_attributes) do
- Gitlab::Redis::Cache.with do |redis|
+ with_redis do |redis|
data = redis.get(cache_attribute_key)
Gitlab::Json.parse(data, symbolize_names: true) if data
end
end
end
+ def with_redis(&block)
+ Gitlab::Redis::Cache.with(&block) # rubocop:disable CodeReuse/ActiveRecord
+ end
+
def cast_value_from_cache(attribute, value)
self.class.type_for_attribute(attribute.to_s).cast(value)
end
diff --git a/app/models/concerns/repository_storage_movable.rb b/app/models/concerns/repository_storage_movable.rb
index b7fd52ab305..87ff413f2c1 100644
--- a/app/models/concerns/repository_storage_movable.rb
+++ b/app/models/concerns/repository_storage_movable.rb
@@ -19,9 +19,7 @@ module RepositoryStorageMovable
inclusion: { in: ->(_) { Gitlab.config.repositories.storages.keys } }
validate :container_repository_writable, on: :create
- default_value_for(:destination_storage_name, allows_nil: false) do
- Repository.pick_storage_shard
- end
+ attribute :destination_storage_name, default: -> { Repository.pick_storage_shard }
state_machine initial: :initial do
event :schedule do
diff --git a/app/models/concerns/subquery.rb b/app/models/concerns/subquery.rb
new file mode 100644
index 00000000000..ae92d2137c1
--- /dev/null
+++ b/app/models/concerns/subquery.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+# Distinguish between a top level query and a subselect.
+#
+# Retrieve column values when the relation has already been loaded, otherwise reselect the relation.
+# Useful for preload query patterns where the typical Rails #preload does not fit. Such as:
+#
+# projects = Project.where(...)
+# projects.load
+# ...
+# options[members] = ProjectMember.where(...).where(source_id: projects.select(:id))
+module Subquery
+ extend ActiveSupport::Concern
+
+ class_methods do
+ def subquery(*column_names, max_limit: 5_000)
+ if current_scope.loaded? && current_scope.size <= max_limit
+ current_scope.pluck(*column_names)
+ else
+ current_scope.reselect(*column_names)
+ end
+ end
+ end
+end
diff --git a/app/models/concerns/ttl_expirable.rb b/app/models/concerns/ttl_expirable.rb
index 1c2147beedd..d09ce4873b1 100644
--- a/app/models/concerns/ttl_expirable.rb
+++ b/app/models/concerns/ttl_expirable.rb
@@ -4,8 +4,8 @@ module TtlExpirable
extend ActiveSupport::Concern
included do
+ attribute :read_at, default: -> { Time.zone.now }
validates :status, presence: true
- default_value_for :read_at, Time.zone.now
enum status: { default: 0, pending_destruction: 1, processing: 2, error: 3 }