Commit 39e22d6b authored by Maxime Orefice's avatar Maxime Orefice

Create spec for jsonb column with specific matcher

This MR makes sure we keep control on new jsonb column
introduced by enforcing developers to use a schema.
parent 5e470af3
......@@ -810,6 +810,14 @@ class BuildMetadata
end
```
When using a `JSONB` column, use the [JsonSchemaValidator](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/validators/json_schema_validator.rb) to keep control of the data being inserted over time.
```ruby
class BuildMetadata
validates: :config_options, json_schema: { filename: 'build_metadata_config_option' }
end
```
## Testing
See the [Testing Rails migrations](testing_guide/testing_migrations_guide.md) style guide.
......
......@@ -16,7 +16,7 @@ module Gitlab
end
# Namespace
class Namespace < ApplicationRecord
class Namespace < ActiveRecord::Base
self.table_name = 'namespaces'
self.inheritance_column = :_type_disabled
......
......@@ -8,6 +8,7 @@ RSpec.describe 'Database schema' do
let(:connection) { ActiveRecord::Base.connection }
let(:tables) { connection.tables }
let(:columns_name_with_jsonb) { retrieve_columns_name_with_jsonb }
# Use if you are certain that this column should not have a foreign key
# EE: edit the ee/spec/db/schema_support.rb
......@@ -169,8 +170,54 @@ RSpec.describe 'Database schema' do
end
end
# These pre-existing columns does not use a schema validation yet
IGNORED_JSONB_COLUMNS = {
"ApplicationSetting" => %w[repository_storages_weighted],
"AlertManagement::Alert" => %w[payload],
"Ci::BuildMetadata" => %w[config_options config_variables],
"Geo::Event" => %w[payload],
"GeoNodeStatus" => %w[status],
"Operations::FeatureFlagScope" => %w[strategies],
"Operations::FeatureFlags::Strategy" => %w[parameters],
"Packages::Composer::Metadatum" => %w[composer_json],
"Releases::Evidence" => %w[summary]
}.freeze
# We are skipping GEO models for now as it adds up complexity
describe 'for jsonb columns' do
it 'uses json schema validator' do
columns_name_with_jsonb.each do |hash|
next if models_by_table_name[hash["table_name"]].nil?
models_by_table_name[hash["table_name"]].each do |model|
jsonb_columns = [hash["column_name"]] - ignored_jsonb_columns(model.name)
expect(model).to validate_jsonb_schema(jsonb_columns)
end
end
end
end
private
def retrieve_columns_name_with_jsonb
sql = <<~SQL
SELECT table_name, column_name, data_type
FROM information_schema.columns
WHERE table_catalog = '#{ApplicationRecord.connection_config[:database]}'
AND table_schema = 'public'
AND table_name NOT LIKE 'pg_%'
AND data_type = 'jsonb'
ORDER BY table_name, column_name, data_type
SQL
ApplicationRecord.connection.select_all(sql).to_a
end
def models_by_table_name
@models_by_table_name ||= ApplicationRecord.descendants.reject(&:abstract_class).group_by(&:table_name)
end
def ignored_fk_columns(column)
IGNORED_FK_COLUMNS.fetch(column, [])
end
......@@ -178,4 +225,8 @@ RSpec.describe 'Database schema' do
def ignored_limit_enums(model)
IGNORED_LIMIT_ENUMS.fetch(model, [])
end
def ignored_jsonb_columns(model)
IGNORED_JSONB_COLUMNS.fetch(model, [])
end
end
# frozen_string_literal: true
RSpec::Matchers.define :validate_jsonb_schema do |jsonb_columns|
match do |actual|
next true if jsonb_columns.blank?
expect(actual.validators).to include(a_kind_of(JsonSchemaValidator))
end
failure_message do
<<~FAILURE_MESSAGE
Expected #{actual.name} to validate the schema of #{jsonb_columns.join(', ')}.
Use JsonSchemaValidator in your model when using a jsonb column.
See doc/development/migration_style_guide.html#storing-json-in-database for more information.
To fix this, please add `validates :#{jsonb_columns.first}, json_schema: { filename: "filename" }` in your model file, for example:
class #{actual.name}
validates :#{jsonb_columns.first}, json_schema: { filename: "filename" }
end
FAILURE_MESSAGE
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment