Skip to content

Commit 207b006

Browse files
authored
Annotate model check constraints (#105)
Supports annotating model check constraints, credit goes to folks who worked on ctran/annotate_models#868. Adds new option `show_check_constraints` that defaults to `false`. When enabled, it will add check constraints annotations rails/rails#31323 to the model annotations. It can be enabled also through the command line with options `-c` or `--show-check-constraints`. Resolves #104
1 parent a779627 commit 207b006

File tree

10 files changed

+270
-3
lines changed

10 files changed

+270
-3
lines changed

lib/annotate_rb/model_annotator.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,5 +26,6 @@ module ModelAnnotator
2626
autoload :AnnotatedFile, "annotate_rb/model_annotator/annotated_file"
2727
autoload :FileParser, "annotate_rb/model_annotator/file_parser"
2828
autoload :ZeitwerkClassGetter, "annotate_rb/model_annotator/zeitwerk_class_getter"
29+
autoload :CheckConstraintAnnotation, "annotate_rb/model_annotator/check_constraint_annotation"
2930
end
3031
end

lib/annotate_rb/model_annotator/annotation_builder.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ def build
4444
@info += ForeignKeyAnnotation::AnnotationBuilder.new(@model, @options).build
4545
end
4646

47+
if @options[:show_check_constraints] && @model.table_exists?
48+
@info += CheckConstraintAnnotation::AnnotationBuilder.new(@model, @options).build
49+
end
50+
4751
@info += schema_footer_text
4852

4953
@info
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# frozen_string_literal: true
2+
3+
module AnnotateRb
4+
module ModelAnnotator
5+
module CheckConstraintAnnotation
6+
autoload :AnnotationBuilder, "annotate_rb/model_annotator/check_constraint_annotation/annotation_builder"
7+
end
8+
end
9+
end
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# frozen_string_literal: true
2+
3+
module AnnotateRb
4+
module ModelAnnotator
5+
module CheckConstraintAnnotation
6+
class AnnotationBuilder
7+
def initialize(model, options)
8+
@model = model
9+
@options = options
10+
end
11+
12+
def build
13+
constraint_info = if @options[:format_markdown]
14+
"#\n# ### Check Constraints\n#\n"
15+
else
16+
"#\n# Check Constraints\n#\n"
17+
end
18+
19+
return "" unless @model.connection.respond_to?(:supports_check_constraints?) &&
20+
@model.connection.supports_check_constraints? && @model.connection.respond_to?(:check_constraints)
21+
22+
check_constraints = @model.connection.check_constraints(@model.table_name)
23+
return "" if check_constraints.empty?
24+
25+
max_size = check_constraints.map { |check_constraint| check_constraint.name.size }.max + 1
26+
check_constraints.sort_by(&:name).each do |check_constraint|
27+
expression = check_constraint.expression ? "(#{check_constraint.expression.squish})" : nil
28+
29+
constraint_info += if @options[:format_markdown]
30+
cc_info_in_markdown(check_constraint.name, expression)
31+
else
32+
cc_info_string(check_constraint.name, expression, max_size)
33+
end
34+
end
35+
36+
constraint_info
37+
end
38+
39+
private
40+
41+
def cc_info_in_markdown(name, expression)
42+
cc_info_markdown = sprintf("# * `%s`", name)
43+
cc_info_markdown += sprintf(": `%s`", expression) if expression
44+
cc_info_markdown += "\n"
45+
46+
cc_info_markdown
47+
end
48+
49+
def cc_info_string(name, expression, max_size)
50+
# standard:disable Lint/FormatParameterMismatch
51+
sprintf("# %-#{max_size}.#{max_size}s %s", name, expression).rstrip + "\n"
52+
# standard:enable Lint/FormatParameterMismatch
53+
end
54+
end
55+
end
56+
end
57+
end

lib/annotate_rb/options.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ def from(options = {}, state = {})
4545
ignore_unknown_models: false, # ModelAnnotator
4646
include_version: false, # ModelAnnotator
4747
show_complete_foreign_keys: false, # ModelAnnotator
48+
show_check_constraints: false, # ModelAnnotator
4849
show_foreign_keys: true, # ModelAnnotator
4950
show_indexes: true, # ModelAnnotator
5051
simple_indexes: false, # ModelAnnotator
@@ -109,6 +110,7 @@ def from(options = {}, state = {})
109110
:ignore_model_sub_dir,
110111
:ignore_unknown_models,
111112
:include_version,
113+
:show_check_constraints,
112114
:show_complete_foreign_keys,
113115
:show_foreign_keys,
114116
:show_indexes,

lib/annotate_rb/parser.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,12 @@ def add_model_options_to_parser(option_parser)
198198
@options[:simple_indexes] = true
199199
end
200200

201+
option_parser.on("-c",
202+
"--show-check-constraints",
203+
"List the table's check constraints in the annotation") do
204+
@options[:show_check_constraints] = true
205+
end
206+
201207
option_parser.on("--hide-limit-column-types VALUES",
202208
"don't show limit for given column types, separated by commas (i.e., `integer,boolean,text`)") do |values|
203209
@options[:hide_limit_column_types] = values.to_s

spec/lib/annotate_rb/model_annotator/annotation_builder_spec.rb

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1967,6 +1967,77 @@
19671967
end
19681968
end
19691969
end
1970+
1971+
context 'when "show_check_constraints" is true' do
1972+
let :klass do
1973+
mock_class_with_custom_connection(:users, primary_key, columns, custom_connection)
1974+
end
1975+
1976+
let :custom_connection do
1977+
check_constraints = [
1978+
mock_check_constraint("must_be_us_adult", "age >= 18")
1979+
]
1980+
1981+
mock_connection([], [], check_constraints)
1982+
end
1983+
1984+
let :primary_key do
1985+
:id
1986+
end
1987+
1988+
let :options do
1989+
{show_check_constraints: true}
1990+
end
1991+
1992+
let :columns do
1993+
[
1994+
mock_column("id", :integer),
1995+
mock_column("age", :integer)
1996+
]
1997+
end
1998+
1999+
let :expected_result do
2000+
<<~EOS
2001+
# == Schema Information
2002+
#
2003+
# Table name: users
2004+
#
2005+
# id :integer not null, primary key
2006+
# age :integer not null
2007+
#
2008+
# Check Constraints
2009+
#
2010+
# must_be_us_adult (age >= 18)
2011+
#
2012+
EOS
2013+
end
2014+
2015+
it "returns schema info with check constraints" do
2016+
is_expected.to eq expected_result
2017+
end
2018+
2019+
context "when option is set to false" do
2020+
let(:options) do
2021+
{show_check_constraints: false}
2022+
end
2023+
2024+
let :expected_result do
2025+
<<~EOS
2026+
# == Schema Information
2027+
#
2028+
# Table name: users
2029+
#
2030+
# id :integer not null, primary key
2031+
# age :integer not null
2032+
#
2033+
EOS
2034+
end
2035+
2036+
it "returns schema info without check constraints" do
2037+
is_expected.to eq expected_result
2038+
end
2039+
end
2040+
end
19702041
end
19712042

19722043
describe "#schema_header_text" do
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# frozen_string_literal: true
2+
3+
RSpec.describe AnnotateRb::ModelAnnotator::CheckConstraintAnnotation::AnnotationBuilder do
4+
include AnnotateTestHelpers
5+
6+
describe "#build" do
7+
subject { described_class.new(model, options).build }
8+
9+
let(:model) do
10+
instance_double(
11+
AnnotateRb::ModelAnnotator::ModelWrapper,
12+
connection: connection,
13+
table_name: "Foo"
14+
)
15+
end
16+
let(:connection) do
17+
mock_connection([], [], check_constraints)
18+
end
19+
let(:options) { AnnotateRb::Options.new }
20+
let(:check_constraints) do
21+
[
22+
mock_check_constraint("alive", "age < 150"),
23+
mock_check_constraint("must_be_adult", "age >= 18"),
24+
mock_check_constraint("missing_expression", nil),
25+
mock_check_constraint("multiline_test", <<~SQL)
26+
CASE
27+
WHEN (age >= 18) THEN (age <= 21)
28+
ELSE true
29+
END
30+
SQL
31+
]
32+
end
33+
34+
let(:expected_result) do
35+
<<~RESULT
36+
#
37+
# Check Constraints
38+
#
39+
# alive (age < 150)
40+
# missing_expression
41+
# multiline_test (CASE WHEN (age >= 18) THEN (age <= 21) ELSE true END)
42+
# must_be_adult (age >= 18)
43+
RESULT
44+
end
45+
46+
it "annotates the check constraints" do
47+
is_expected.to eq(expected_result)
48+
end
49+
50+
context "when model connection does not support check constraints" do
51+
let(:connection) do
52+
conn_options = {supports_check_constraints?: false}
53+
54+
mock_connection([], [], [], conn_options)
55+
end
56+
57+
it { is_expected.to be_empty }
58+
end
59+
60+
context "when check constraints is empty" do
61+
let(:connection) do
62+
conn_options = {supports_check_constraints?: true}
63+
64+
mock_connection([], [], [], conn_options)
65+
end
66+
67+
it { is_expected.to be_empty }
68+
end
69+
70+
context "when there are check constraints using markdown" do
71+
let(:options) { AnnotateRb::Options.new({format_markdown: true}) }
72+
let(:expected_result) do
73+
<<~RESULT
74+
#
75+
# ### Check Constraints
76+
#
77+
# * `alive`: `(age < 150)`
78+
# * `missing_expression`
79+
# * `multiline_test`: `(CASE WHEN (age >= 18) THEN (age <= 21) ELSE true END)`
80+
# * `must_be_adult`: `(age >= 18)`
81+
RESULT
82+
end
83+
84+
it { is_expected.to eq(expected_result) }
85+
end
86+
87+
context "when it is just the header using markdown" do
88+
let(:options) { AnnotateRb::Options.new({format_markdown: true}) }
89+
let(:connection) do
90+
mock_connection([], [], [])
91+
end
92+
93+
it { is_expected.to be_empty }
94+
end
95+
end
96+
end

spec/lib/annotate_rb/parser_spec.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,16 @@ module AnnotateRb # rubocop:disable Metrics/ModuleLength
295295
end
296296
end
297297

298+
%w[-c --show-check-constraints].each do |option|
299+
describe option do
300+
let(:args) { [option] }
301+
302+
it "sets show_check_constraints to true" do
303+
expect(result).to include(show_check_constraints: true)
304+
end
305+
end
306+
end
307+
298308
describe "--model-dir" do
299309
let(:option) { "--model-dir" }
300310
let(:set_value) { "some_dir/" }

spec/support/annotate_test_helpers.rb

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,16 @@ def mock_foreign_key(name, from_column, to_table, to_column = "id", constraints
3636
on_update: constraints[:on_update])
3737
end
3838

39-
def mock_connection(indexes = [], foreign_keys = [])
40-
double("Conn",
39+
def mock_connection(indexes = [], foreign_keys = [], check_constraints = [], options = {})
40+
double_options = {
4141
indexes: indexes,
42+
check_constraints: check_constraints,
4243
foreign_keys: foreign_keys,
43-
supports_foreign_keys?: true)
44+
supports_foreign_keys?: true,
45+
supports_check_constraints?: true
46+
}.merge(options)
47+
48+
double("Conn", double_options)
4449
end
4550

4651
def mock_connection_with_table_fields(indexes, foreign_keys, table_exists, table_comment)
@@ -97,4 +102,10 @@ def mock_column(name, type, options = {})
97102

98103
double("Column", stubs)
99104
end
105+
106+
def mock_check_constraint(name, expression)
107+
double("CheckConstraintDefinition",
108+
name: name,
109+
expression: expression)
110+
end
100111
end

0 commit comments

Comments
 (0)