Skip to content

Commit ea4cd00

Browse files
authored
Add support for annotating check constraints (#868)
This adds annotation of check constraints with an option to disable/enable annotation. Most of the work done in this PR is based off of existing implementation for annotating indexes and foreign keys. Signed-off-by: Lovro Bikic <[email protected]>
1 parent 76a1804 commit ea4cd00

File tree

8 files changed

+200
-9
lines changed

8 files changed

+200
-9
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,7 @@ you can do so with a simple environment variable, instead of editing the
224224
-a, --active-admin Annotate active_admin models
225225
-v, --version Show the current version of this gem
226226
-m, --show-migration Include the migration version number in the annotation
227+
-c, --show-check-constraints List the table's check constraints in the annotation
227228
-k, --show-foreign-keys List the table's foreign key constraints in the annotation
228229
--ck, --complete-foreign-keys
229230
Complete foreign key names in the annotation

lib/annotate/annotate_models.rb

+34-1
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ def retrieve_indexes_from_table(klass)
131131
# to create a comment block containing a line for
132132
# each column. The line contains the column name,
133133
# the type (and length), and any optional attributes
134-
def get_schema_info(klass, header, options = {})
134+
def get_schema_info(klass, header, options = {}) # rubocop:disable Metrics/MethodLength
135135
info = "# #{header}\n"
136136
info << get_schema_header_text(klass, options)
137137

@@ -178,6 +178,10 @@ def get_schema_info(klass, header, options = {})
178178
info << get_foreign_key_info(klass, options)
179179
end
180180

181+
if options[:show_check_constraints] && klass.table_exists?
182+
info << get_check_constraint_info(klass, options)
183+
end
184+
181185
info << get_schema_footer_text(klass, options)
182186
end
183187

@@ -352,6 +356,35 @@ def get_foreign_key_info(klass, options = {})
352356
fk_info
353357
end
354358

359+
def get_check_constraint_info(klass, options = {})
360+
cc_info = if options[:format_markdown]
361+
"#\n# ### Check Constraints\n#\n"
362+
else
363+
"#\n# Check Constraints\n#\n"
364+
end
365+
366+
return '' unless klass.connection.respond_to?(:supports_check_constraints?) &&
367+
klass.connection.supports_check_constraints? && klass.connection.respond_to?(:check_constraints)
368+
369+
check_constraints = klass.connection.check_constraints(klass.table_name)
370+
return '' if check_constraints.empty?
371+
372+
max_size = check_constraints.map { |check_constraint| check_constraint.name.size }.max + 1
373+
check_constraints.sort_by(&:name).each do |check_constraint|
374+
expression = check_constraint.expression ? "(#{check_constraint.expression.squish})" : nil
375+
376+
cc_info << if options[:format_markdown]
377+
cc_info_markdown = sprintf("# * `%s`", check_constraint.name)
378+
cc_info_markdown << sprintf(": `%s`", expression) if expression
379+
cc_info_markdown << "\n"
380+
else
381+
sprintf("# %-#{max_size}.#{max_size}s %s", check_constraint.name, expression).rstrip + "\n"
382+
end
383+
end
384+
385+
cc_info
386+
end
387+
355388
# Add a schema block to a file. If the file already contains
356389
# a schema info block (a comment starting with "== Schema Information"),
357390
# check if it matches the block that is already there. If so, leave it be.

lib/annotate/constants.rb

+2-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ module Constants
1818
:trace, :timestamp, :exclude_serializers, :classified_sort,
1919
:show_foreign_keys, :show_complete_foreign_keys,
2020
:exclude_scaffolds, :exclude_controllers, :exclude_helpers,
21-
:exclude_sti_subclasses, :ignore_unknown_models, :with_comment
21+
:exclude_sti_subclasses, :ignore_unknown_models, :with_comment,
22+
:show_check_constraints
2223
].freeze
2324

2425
OTHER_OPTIONS = [

lib/annotate/parser.rb

+7-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ def commit
4848
end
4949
end
5050

51-
def add_options_to_parser(option_parser) # rubocop:disable Metrics/MethodLength
51+
def add_options_to_parser(option_parser) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
5252
has_set_position = {}
5353

5454
option_parser.banner = 'Usage: annotate [options] [model_file]*'
@@ -173,6 +173,12 @@ def add_options_to_parser(option_parser) # rubocop:disable Metrics/MethodLength
173173
env['include_version'] = 'yes'
174174
end
175175

176+
option_parser.on('-c',
177+
'--show-check-constraints',
178+
"List the table's check constraints in the annotation") do
179+
env['show_check_constraints'] = 'yes'
180+
end
181+
176182
option_parser.on('-k',
177183
'--show-foreign-keys',
178184
"List the table's foreign key constraints in the annotation") do

lib/generators/annotate/templates/auto_annotate_models.rake

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ if Rails.env.development?
1717
'position_in_fixture' => 'before',
1818
'position_in_factory' => 'before',
1919
'position_in_serializer' => 'before',
20+
'show_check_constraints' => 'false',
2021
'show_foreign_keys' => 'true',
2122
'show_complete_foreign_keys' => 'false',
2223
'show_indexes' => 'true',

lib/tasks/annotate_models.rake

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ task annotate_models: :environment do
1818
options[:position_in_factory] = Annotate::Helpers.fallback(ENV['position_in_factory'], ENV['position'])
1919
options[:position_in_test] = Annotate::Helpers.fallback(ENV['position_in_test'], ENV['position'])
2020
options[:position_in_serializer] = Annotate::Helpers.fallback(ENV['position_in_serializer'], ENV['position'])
21+
options[:show_check_constraints] = Annotate::Helpers.true?(ENV['show_check_constraints'])
2122
options[:show_foreign_keys] = Annotate::Helpers.true?(ENV['show_foreign_keys'])
2223
options[:show_complete_foreign_keys] = Annotate::Helpers.true?(ENV['show_complete_foreign_keys'])
2324
options[:show_indexes] = Annotate::Helpers.true?(ENV['show_indexes'])

spec/lib/annotate/annotate_models_spec.rb

+143-6
Original file line numberDiff line numberDiff line change
@@ -41,16 +41,25 @@ def mock_foreign_key(name, from_column, to_table, to_column = 'id', constraints
4141
on_update: constraints[:on_update])
4242
end
4343

44-
def mock_connection(indexes = [], foreign_keys = [])
44+
def mock_check_constraint(name, expression)
45+
double('CheckConstraintDefinition',
46+
name: name,
47+
expression: expression)
48+
end
49+
50+
def mock_connection(indexes = [], foreign_keys = [], check_constraints = [])
4551
double('Conn',
4652
indexes: indexes,
4753
foreign_keys: foreign_keys,
48-
supports_foreign_keys?: true)
54+
check_constraints: check_constraints,
55+
supports_foreign_keys?: true,
56+
supports_check_constraints?: true)
4957
end
5058

51-
def mock_class(table_name, primary_key, columns, indexes = [], foreign_keys = [])
59+
# rubocop:disable Metrics/ParameterLists
60+
def mock_class(table_name, primary_key, columns, indexes = [], foreign_keys = [], check_constraints = [])
5261
options = {
53-
connection: mock_connection(indexes, foreign_keys),
62+
connection: mock_connection(indexes, foreign_keys, check_constraints),
5463
table_exists?: true,
5564
table_name: table_name,
5665
primary_key: primary_key,
@@ -62,6 +71,7 @@ def mock_class(table_name, primary_key, columns, indexes = [], foreign_keys = []
6271

6372
double('An ActiveRecord class', options)
6473
end
74+
# rubocop:enable Metrics/ParameterLists
6575

6676
def mock_column(name, type, options = {})
6777
default_options = {
@@ -221,7 +231,7 @@ def mock_column(name, type, options = {})
221231
end
222232

223233
let :klass do
224-
mock_class(:users, primary_key, columns, indexes, foreign_keys)
234+
mock_class(:users, primary_key, columns, indexes, foreign_keys, check_constraints)
225235
end
226236

227237
let :indexes do
@@ -232,6 +242,10 @@ def mock_column(name, type, options = {})
232242
[]
233243
end
234244

245+
let :check_constraints do
246+
[]
247+
end
248+
235249
context 'when option is not present' do
236250
let :options do
237251
{}
@@ -391,7 +405,7 @@ def mock_column(name, type, options = {})
391405
end
392406
end
393407

394-
context 'with Globalize gem' do
408+
context 'with Globalize gem' do # rubocop:disable RSpec/MultipleMemoizedHelpers
395409
let :translation_klass do
396410
double('Folder::Post::Translation',
397411
to_s: 'Folder::Post::Translation',
@@ -756,6 +770,82 @@ def mock_column(name, type, options = {})
756770
end
757771
end
758772

773+
context 'when check constraints exist' do
774+
let :columns do
775+
[
776+
mock_column(:id, :integer),
777+
mock_column(:age, :integer)
778+
]
779+
end
780+
781+
context 'when option "show_check_constraints" is true' do
782+
let :options do
783+
{ show_check_constraints: true }
784+
end
785+
786+
context 'when check constraints are defined' do
787+
let :check_constraints do
788+
[
789+
mock_check_constraint('alive', 'age < 150'),
790+
mock_check_constraint('must_be_adult', 'age >= 18'),
791+
mock_check_constraint('missing_expression', nil),
792+
mock_check_constraint('multiline_test', <<~SQL)
793+
CASE
794+
WHEN (age >= 18) THEN (age <= 21)
795+
ELSE true
796+
END
797+
SQL
798+
]
799+
end
800+
801+
let :expected_result do
802+
<<~EOS
803+
# Schema Info
804+
#
805+
# Table name: users
806+
#
807+
# id :integer not null, primary key
808+
# age :integer not null
809+
#
810+
# Check Constraints
811+
#
812+
# alive (age < 150)
813+
# missing_expression
814+
# multiline_test (CASE WHEN (age >= 18) THEN (age <= 21) ELSE true END)
815+
# must_be_adult (age >= 18)
816+
#
817+
EOS
818+
end
819+
820+
it 'returns schema info with check constraint information' do
821+
is_expected.to eq expected_result
822+
end
823+
end
824+
825+
context 'when check constraint is not defined' do
826+
let :check_constraints do
827+
[]
828+
end
829+
830+
let :expected_result do
831+
<<~EOS
832+
# Schema Info
833+
#
834+
# Table name: users
835+
#
836+
# id :integer not null, primary key
837+
# age :integer not null
838+
#
839+
EOS
840+
end
841+
842+
it 'returns schema info without check constraint information' do
843+
is_expected.to eq expected_result
844+
end
845+
end
846+
end
847+
end
848+
759849
context 'when foreign keys exist' do
760850
let :columns do
761851
[
@@ -1492,6 +1582,53 @@ def mock_column(name, type, options = {})
14921582
end
14931583
end
14941584

1585+
context 'when option "show_check_constraints" is true' do
1586+
let :options do
1587+
{ format_markdown: true, show_check_constraints: true }
1588+
end
1589+
1590+
context 'when check constraints are defined' do
1591+
let :check_constraints do
1592+
[
1593+
mock_check_constraint('min_name_length', 'LENGTH(name) > 2'),
1594+
mock_check_constraint('missing_expression', nil),
1595+
mock_check_constraint('multiline_test', <<~SQL)
1596+
CASE
1597+
WHEN (age >= 18) THEN (age <= 21)
1598+
ELSE true
1599+
END
1600+
SQL
1601+
]
1602+
end
1603+
1604+
let :expected_result do
1605+
<<~EOS
1606+
# == Schema Information
1607+
#
1608+
# Table name: `users`
1609+
#
1610+
# ### Columns
1611+
#
1612+
# Name | Type | Attributes
1613+
# ----------- | ------------------ | ---------------------------
1614+
# **`id`** | `integer` | `not null, primary key`
1615+
# **`name`** | `string(50)` | `not null`
1616+
#
1617+
# ### Check Constraints
1618+
#
1619+
# * `min_name_length`: `(LENGTH(name) > 2)`
1620+
# * `missing_expression`
1621+
# * `multiline_test`: `(CASE WHEN (age >= 18) THEN (age <= 21) ELSE true END)`
1622+
#
1623+
EOS
1624+
end
1625+
1626+
it 'returns schema info with check constraint information in Markdown format' do
1627+
is_expected.to eq expected_result
1628+
end
1629+
end
1630+
end
1631+
14951632
context 'when option "show_foreign_keys" is true' do
14961633
let :options do
14971634
{ format_markdown: true, show_foreign_keys: true }

spec/lib/annotate/parser_spec.rb

+11
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,17 @@ module Annotate # rubocop:disable Metrics/ModuleLength
260260
end
261261
end
262262

263+
%w[-c --show-check-constraints].each do |option|
264+
describe option do
265+
let(:env_key) { 'show_check_constraints' }
266+
let(:set_value) { 'yes' }
267+
it 'sets the ENV variable' do
268+
expect(ENV).to receive(:[]=).with(env_key, set_value)
269+
Parser.parse([option])
270+
end
271+
end
272+
end
273+
263274
%w[-k --show-foreign-keys].each do |option|
264275
describe option do
265276
let(:env_key) { 'show_foreign_keys' }

0 commit comments

Comments
 (0)