Module: Gitlab::Database::Migrations::ConstraintsHelpers
- Includes:
- LockRetriesHelpers, TimeoutHelpers
- Included in:
- Gitlab::Database::MigrationHelpers
- Defined in:
- lib/gitlab/database/migrations/constraints_helpers.rb
Constant Summary collapse
- MAX_IDENTIFIER_NAME_LENGTH =
63
Class Method Summary collapse
Instance Method Summary collapse
-
#add_check_constraint(table, check, constraint_name, validate: true) ⇒ Object
Adds a check constraint to a table.
- #add_multi_column_not_null_constraint(table, *columns, limit: 1, operator: '=', constraint_name: nil, validate: true) ⇒ Object
-
#add_not_null_constraint(table, column, constraint_name: nil, validate: true) ⇒ Object
Migration Helpers for managing not null constraints.
-
#add_text_limit(table, column, limit, constraint_name: nil, validate: true) ⇒ Object
Migration Helpers for adding limit to text columns.
- #check_constraint_exists?(table, constraint_name) ⇒ Boolean
-
#check_constraint_name(table, column, type) ⇒ Object
Returns the name for a check constraint.
- #check_not_null_constraint_exists?(table, column, constraint_name: nil) ⇒ Boolean
- #check_text_limit_exists?(table, column, constraint_name: nil) ⇒ Boolean
-
#copy_check_constraints(table, old, new, schema: nil) ⇒ Object
Copies all check constraints for the old column to the new column.
- #drop_constraint(table_name, constraint_name, cascade: false) ⇒ Object
- #remove_check_constraint(table, constraint_name) ⇒ Object
- #remove_multi_column_not_null_constraint(table, *columns, constraint_name: nil) ⇒ Object
- #remove_not_null_constraint(table, column, constraint_name: nil) ⇒ Object
- #remove_text_limit(table, column, constraint_name: nil) ⇒ Object
- #rename_constraint(table_name, old_name, new_name) ⇒ Object
- #switch_constraint_names(table_name, constraint_a, constraint_b) ⇒ Object
- #text_limit_name(table, column, name: nil) ⇒ Object
- #validate_check_constraint(table, constraint_name) ⇒ Object
- #validate_check_constraint_name!(constraint_name) ⇒ Object
- #validate_multi_column_not_null_constraint(table, *columns, constraint_name: nil) ⇒ Object
- #validate_not_null_constraint(table, column, constraint_name: nil) ⇒ Object
- #validate_text_limit(table, column, constraint_name: nil) ⇒ Object
Methods included from TimeoutHelpers
Methods included from LockRetriesHelpers
Class Method Details
.check_constraint_exists?(table, constraint_name, connection:) ⇒ Boolean
13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
# File 'lib/gitlab/database/migrations/constraints_helpers.rb', line 13 def self.check_constraint_exists?(table, constraint_name, connection:) table_name, schema_name = table.to_s.split('.').reverse schema_name ||= connection.current_schema # Constraint names are unique per table in Postgres, not per schema # Two tables can have constraints with the same name, so we filter by # the table name in addition to using the constraint_name check_sql = <<~SQL SELECT COUNT(*) FROM pg_catalog.pg_constraint con INNER JOIN pg_catalog.pg_class rel ON rel.oid = con.conrelid INNER JOIN pg_catalog.pg_namespace nsp ON nsp.oid = con.connamespace WHERE con.contype = 'c' AND con.conname = #{connection.quote(constraint_name)} AND nsp.nspname = #{connection.quote(schema_name)} AND rel.relname = #{connection.quote(table_name)} SQL connection.select_value(check_sql.squish) > 0 end |
Instance Method Details
#add_check_constraint(table, check, constraint_name, validate: true) ⇒ Object
Adds a check constraint to a table
This method is the generic helper for adding any check constraint More specialized helpers may use it (e.g. add_text_limit or add_not_null)
This method only requires minimal locking:
-
The constraint is added using NOT VALID This allows us to add the check constraint without validating it
-
The check will be enforced for new data (inserts) coming in
-
If ‘validate: true` the constraint is also validated Otherwise, validate_check_constraint() can be used at a later stage
-
Check comments on add_concurrent_foreign_key for more info
table - The table the constraint will be added to check - The check clause to add
e.g. 'char_length(name) <= 5' or 'store IS NOT NULL'
constraint_name - The name of the check constraint (otherwise auto-generated)
Should be unique per table (not per column)
validate - Whether to validate the constraint in this call
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 |
# File 'lib/gitlab/database/migrations/constraints_helpers.rb', line 79 def add_check_constraint(table, check, constraint_name, validate: true) # Transactions would result in ALTER TABLE locks being held for the # duration of the transaction, defeating the purpose of this method. validate_not_in_transaction!(:add_check_constraint) validate_check_constraint_name!(constraint_name) if check_constraint_exists?(table, constraint_name) = <<~MESSAGE Check constraint was not created because it exists already (this may be due to an aborted migration or similar) table: #{table}, check: #{check}, constraint name: #{constraint_name} MESSAGE Gitlab::AppLogger.warn else # Only add the constraint without validating it # Even though it is fast, ADD CONSTRAINT requires an EXCLUSIVE lock # Use with_lock_retries to make sure that this operation # will not timeout on tables accessed by many processes with_lock_retries do execute <<~SQL ALTER TABLE #{table} ADD CONSTRAINT #{constraint_name} CHECK ( #{check} ) NOT VALID; SQL end end validate_check_constraint(table, constraint_name) if validate end |
#add_multi_column_not_null_constraint(table, *columns, limit: 1, operator: '=', constraint_name: nil, validate: true) ⇒ Object
255 256 257 258 259 260 261 262 263 264 265 266 |
# File 'lib/gitlab/database/migrations/constraints_helpers.rb', line 255 def add_multi_column_not_null_constraint( table, *columns, limit: 1, operator: '=', constraint_name: nil, validate: true) raise 'Expected multiple columns, use add_not_null_constraint for a single column' unless columns.size > 1 add_check_constraint( table, "num_nonnulls(#{columns.sort.join(', ')}) #{operator} #{limit}", multi_column_not_null_constraint_name(table, columns, name: constraint_name), validate: validate ) end |
#add_not_null_constraint(table, column, constraint_name: nil, validate: true) ⇒ Object
Migration Helpers for managing not null constraints
216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 |
# File 'lib/gitlab/database/migrations/constraints_helpers.rb', line 216 def add_not_null_constraint(table, column, constraint_name: nil, validate: true) if column_is_nullable?(table, column) add_check_constraint( table, "#{column} IS NOT NULL", not_null_constraint_name(table, column, name: constraint_name), validate: validate ) else = <<~MESSAGE NOT NULL check constraint was not created: column #{table}.#{column} is already defined as `NOT NULL` MESSAGE Gitlab::AppLogger.warn end end |
#add_text_limit(table, column, limit, constraint_name: nil, validate: true) ⇒ Object
Migration Helpers for adding limit to text columns
194 195 196 197 198 199 200 201 |
# File 'lib/gitlab/database/migrations/constraints_helpers.rb', line 194 def add_text_limit(table, column, limit, constraint_name: nil, validate: true) add_check_constraint( table, "char_length(#{column}) <= #{limit}", text_limit_name(table, column, name: constraint_name), validate: validate ) end |
#check_constraint_exists?(table, constraint_name) ⇒ Boolean
55 56 57 |
# File 'lib/gitlab/database/migrations/constraints_helpers.rb', line 55 def check_constraint_exists?(table, constraint_name) ConstraintsHelpers.check_constraint_exists?(table, constraint_name, connection: connection) end |
#check_constraint_name(table, column, type) ⇒ Object
Returns the name for a check constraint
type:
-
Any value, as long as it is unique
-
Constraint names are unique per table in Postgres, and, additionally, we can have multiple check constraints over a column So we use the (table, column, type) triplet as a unique name
-
e.g. we use ‘max_length’ when adding checks for text limits
or 'not_null' when adding a NOT NULL constraint
47 48 49 50 51 52 53 |
# File 'lib/gitlab/database/migrations/constraints_helpers.rb', line 47 def check_constraint_name(table, column, type) identifier = "#{table}_#{column}_check_#{type}" # Check concurrent_foreign_key_name() for info on why we use a hash hashed_identifier = Digest::SHA256.hexdigest(identifier).first(10) "check_#{hashed_identifier}" end |
#check_not_null_constraint_exists?(table, column, constraint_name: nil) ⇒ Boolean
248 249 250 251 252 253 |
# File 'lib/gitlab/database/migrations/constraints_helpers.rb', line 248 def check_not_null_constraint_exists?(table, column, constraint_name: nil) check_constraint_exists?( table, not_null_constraint_name(table, column, name: constraint_name) ) end |
#check_text_limit_exists?(table, column, constraint_name: nil) ⇒ Boolean
211 212 213 |
# File 'lib/gitlab/database/migrations/constraints_helpers.rb', line 211 def check_text_limit_exists?(table, column, constraint_name: nil) check_constraint_exists?(table, text_limit_name(table, column, name: constraint_name)) end |
#copy_check_constraints(table, old, new, schema: nil) ⇒ Object
Copies all check constraints for the old column to the new column.
table - The table containing the columns. old - The old column. new - The new column. schema - The schema the table is defined for
If it is not provided, then the current_schema is used
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 |
# File 'lib/gitlab/database/migrations/constraints_helpers.rb', line 150 def copy_check_constraints(table, old, new, schema: nil) raise 'copy_check_constraints can not be run inside a transaction' if transaction_open? raise "Column #{old} does not exist on #{table}" unless column_exists?(table, old) raise "Column #{new} does not exist on #{table}" unless column_exists?(table, new) table_with_schema = schema.present? ? "#{schema}.#{table}" : table check_constraints_for(table, old, schema: schema).each do |check_c| validate = !(check_c["constraint_def"].end_with? "NOT VALID") # Normalize: # - Old constraint definitions: # '(char_length(entity_path) <= 5500)' # - Definitionss from pg_get_constraintdef(oid): # 'CHECK ((char_length(entity_path) <= 5500))' # - Definitions from pg_get_constraintdef(oid, pretty_bool): # 'CHECK (char_length(entity_path) <= 5500)' # - Not valid constraints: 'CHECK (...) NOT VALID' # to a single format that we can use: # '(char_length(entity_path) <= 5500)' check_definition = check_c["constraint_def"] .sub(/^\s*(CHECK)?\s*\({0,2}/, '(') .sub(/\){0,2}\s*(NOT VALID)?\s*$/, ')') constraint_name = if check_definition == "(#{old} IS NOT NULL)" not_null_constraint_name(table_with_schema, new) elsif check_definition.start_with? "(char_length(#{old}) <=" text_limit_name(table_with_schema, new) else check_constraint_name(table_with_schema, new, 'copy_check_constraint') end add_check_constraint( table_with_schema, check_definition.gsub(old.to_s, new.to_s), constraint_name, validate: validate ) end end |
#drop_constraint(table_name, constraint_name, cascade: false) ⇒ Object
286 287 288 289 290 |
# File 'lib/gitlab/database/migrations/constraints_helpers.rb', line 286 def drop_constraint(table_name, constraint_name, cascade: false) execute <<~SQL ALTER TABLE #{quote_table_name(table_name)} DROP CONSTRAINT #{quote_column_name(constraint_name)} #{cascade_statement(cascade)} SQL end |
#remove_check_constraint(table, constraint_name) ⇒ Object
126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 |
# File 'lib/gitlab/database/migrations/constraints_helpers.rb', line 126 def remove_check_constraint(table, constraint_name) # This is technically not necessary, but aligned with add_check_constraint # and allows us to continue use with_lock_retries here validate_not_in_transaction!(:remove_check_constraint) validate_check_constraint_name!(constraint_name) # DROP CONSTRAINT requires an EXCLUSIVE lock # Use with_lock_retries to make sure that this will not timeout with_lock_retries do execute <<-SQL ALTER TABLE #{table} DROP CONSTRAINT IF EXISTS #{constraint_name} SQL end end |
#remove_multi_column_not_null_constraint(table, *columns, constraint_name: nil) ⇒ Object
275 276 277 |
# File 'lib/gitlab/database/migrations/constraints_helpers.rb', line 275 def remove_multi_column_not_null_constraint(table, *columns, constraint_name: nil) remove_check_constraint(table, multi_column_not_null_constraint_name(table, columns, name: constraint_name)) end |
#remove_not_null_constraint(table, column, constraint_name: nil) ⇒ Object
241 242 243 244 245 246 |
# File 'lib/gitlab/database/migrations/constraints_helpers.rb', line 241 def remove_not_null_constraint(table, column, constraint_name: nil) remove_check_constraint( table, not_null_constraint_name(table, column, name: constraint_name) ) end |
#remove_text_limit(table, column, constraint_name: nil) ⇒ Object
207 208 209 |
# File 'lib/gitlab/database/migrations/constraints_helpers.rb', line 207 def remove_text_limit(table, column, constraint_name: nil) remove_check_constraint(table, text_limit_name(table, column, name: constraint_name)) end |
#rename_constraint(table_name, old_name, new_name) ⇒ Object
279 280 281 282 283 284 |
# File 'lib/gitlab/database/migrations/constraints_helpers.rb', line 279 def rename_constraint(table_name, old_name, new_name) execute <<~SQL ALTER TABLE #{quote_table_name(table_name)} RENAME CONSTRAINT #{quote_column_name(old_name)} TO #{quote_column_name(new_name)} SQL end |
#switch_constraint_names(table_name, constraint_a, constraint_b) ⇒ Object
292 293 294 295 296 297 298 299 300 |
# File 'lib/gitlab/database/migrations/constraints_helpers.rb', line 292 def switch_constraint_names(table_name, constraint_a, constraint_b) validate_not_in_transaction!(:switch_constraint_names) with_lock_retries do rename_constraint(table_name, constraint_a, :temp_name_for_renaming) rename_constraint(table_name, constraint_b, constraint_a) rename_constraint(table_name, :temp_name_for_renaming, constraint_b) end end |
#text_limit_name(table, column, name: nil) ⇒ Object
308 309 310 |
# File 'lib/gitlab/database/migrations/constraints_helpers.rb', line 308 def text_limit_name(table, column, name: nil) name.presence || check_constraint_name(table, column, 'max_length') end |
#validate_check_constraint(table, constraint_name) ⇒ Object
112 113 114 115 116 117 118 119 120 121 122 123 124 |
# File 'lib/gitlab/database/migrations/constraints_helpers.rb', line 112 def validate_check_constraint(table, constraint_name) validate_check_constraint_name!(constraint_name) unless check_constraint_exists?(table, constraint_name) raise (table, "check constraint", constraint_name) end disable_statement_timeout do # VALIDATE CONSTRAINT only requires a SHARE UPDATE EXCLUSIVE LOCK # It only conflicts with other validations and creating indexes execute("ALTER TABLE #{table} VALIDATE CONSTRAINT #{constraint_name};") end end |
#validate_check_constraint_name!(constraint_name) ⇒ Object
302 303 304 305 306 |
# File 'lib/gitlab/database/migrations/constraints_helpers.rb', line 302 def validate_check_constraint_name!(constraint_name) return unless constraint_name.to_s.length > MAX_IDENTIFIER_NAME_LENGTH raise "The maximum allowed constraint name is #{MAX_IDENTIFIER_NAME_LENGTH} characters" end |
#validate_multi_column_not_null_constraint(table, *columns, constraint_name: nil) ⇒ Object
268 269 270 271 272 273 |
# File 'lib/gitlab/database/migrations/constraints_helpers.rb', line 268 def validate_multi_column_not_null_constraint(table, *columns, constraint_name: nil) validate_check_constraint( table, multi_column_not_null_constraint_name(table, columns, name: constraint_name) ) end |
#validate_not_null_constraint(table, column, constraint_name: nil) ⇒ Object
234 235 236 237 238 239 |
# File 'lib/gitlab/database/migrations/constraints_helpers.rb', line 234 def validate_not_null_constraint(table, column, constraint_name: nil) validate_check_constraint( table, not_null_constraint_name(table, column, name: constraint_name) ) end |
#validate_text_limit(table, column, constraint_name: nil) ⇒ Object
203 204 205 |
# File 'lib/gitlab/database/migrations/constraints_helpers.rb', line 203 def validate_text_limit(table, column, constraint_name: nil) validate_check_constraint(table, text_limit_name(table, column, name: constraint_name)) end |