class Sequel::Annotate

Attributes

model[R]

The model to annotate

Public Class Methods

annotate(paths, options = {}) click to toggle source

Append/replace the schema comment for all of the given files. Attempts to guess the model for each file using a regexp match of the file's content, if this doesn't work, you'll need to create an instance manually and pass in the model and path. Example:

Sequel::Annotate.annotate(Dir['models/*.rb'])
   # File lib/sequel/annotate.rb
11 def self.annotate(paths, options = {})
12   Sequel.extension :inflector
13   namespace = options[:namespace]
14 
15   paths.each do |path|
16     text = File.read(path)
17 
18     next if text.match(/^#\s+sequel-annotate:\s+false$/i)
19 
20     name = nil
21     if namespace == true
22       constants = text.scan(/\bmodule ([^\s]+)|class ([^\s<]+)\s*</).flatten.compact
23       name = constants.join("::") if constants.any?
24     elsif match = text.match(/class ([^\s<]+)\s*</)
25       name = match[1]
26       if namespace
27         name = "#{namespace}::#{name}"
28       end
29     end
30 
31     if name
32       klass = name.constantize
33       if klass.ancestors.include?(Sequel::Model)
34         new(klass).annotate(path, options)
35       end
36     end
37   end
38 end
new(model) click to toggle source

Store the model to annotate

   # File lib/sequel/annotate.rb
44 def initialize(model)
45   @model = model
46 end

Public Instance Methods

annotate(path, options = {}) click to toggle source

Append the schema comment (or replace it if one already exists) to the file at the given path.

   # File lib/sequel/annotate.rb
50 def annotate(path, options = {})
51   return if skip_model?
52 
53   orig = current = File.read(path).rstrip
54 
55   if options[:position] == :before
56     if current =~ /\A((?:^\s*$|^#\s*(?:frozen_string_literal|coding|encoding|warn_indent|warn_past_scope)[^\n]*\s*)*)/m
57       magic_comments = $1
58       current.slice!(0, magic_comments.length)
59     end
60 
61     current = current.gsub(/\A#\sTable[^\n\r]+\r?\n(?:#[^\n\r]*\r?\n)*/m, '').lstrip
62     current = "#{magic_comments}#{schema_comment(options)}#{$/}#{$/}#{current}"
63   else
64     if m = current.reverse.match(/#{"#{$/}# Table: ".reverse}/m)
65       offset = current.length - m.end(0) + 1
66       unless current[offset..-1].match(/^[^#]/)
67         # If Table: comment exists, and there are no
68         # uncommented lines between it and the end of the file
69         # then replace current comment instead of appending it
70         current = current[0...offset].rstrip
71       end
72     end
73     current += "#{$/}#{$/}#{schema_comment(options)}"
74   end
75 
76   if orig != current
77     File.open(path, "wb") do |f|
78       f.puts current
79     end
80   end
81 end
schema_comment(options = {}) click to toggle source

The schema comment to use for this model.

For all databases, includes columns, indexes, and foreign key constraints in this table referencing other tables. On PostgreSQL, also includes check constraints, triggers, and foreign key constraints in other tables referencing this table.

Options:

:border

Include a border above and below the comment.

:indexes

Do not include indexes in annotation if set to false.

:foreign_keys

Do not include foreign key constraints in annotation if set to false.

PostgreSQL-specific options:

:constraints

Do not include check constraints if set to false.

:references

Do not include foreign key constraints in other tables referencing this table if set to false.

:triggers

Do not include triggers in annotation if set to false.

    # File lib/sequel/annotate.rb
 99 def schema_comment(options = {})
100   return "" if skip_model?
101 
102   output = []
103   output << "# Table: #{model.dataset.with_quote_identifiers(false).literal(model.table_name)}"
104 
105   meth = :"_schema_comment_#{model.db.database_type}"
106   if respond_to?(meth, true)
107     send(meth, output, options)
108   else
109     schema_comment_columns(output)
110     schema_comment_indexes(output) unless options[:indexes] == false
111     schema_comment_foreign_keys(output) unless options[:foreign_keys] == false
112   end
113 
114 
115   # Add beginning and end to the table if specified
116   if options[:border]
117     border = "# #{'-' * (output.map(&:size).max - 2)}"
118     output.push(border)
119     output.insert(1, border)
120   end
121 
122   output.join($/)
123 end

Private Instance Methods

_column_comments_postgres() click to toggle source
    # File lib/sequel/annotate.rb
239     def _column_comments_postgres
240       model.db.fetch(<<SQL, :oid=>model.db.send(:regclass_oid, model.table_name)).to_hash(:attname, :description)
241 SELECT a.attname, d.description
242 FROM pg_description d
243 JOIN pg_attribute a ON (d.objoid = a.attrelid AND d.objsubid = a.attnum)
244 WHERE d.objoid = :oid AND COALESCE(d.description, '') != '';
245 SQL
246     end
_schema_comment_postgres(output, options = {}) click to toggle source

Use the standard columns schema output, but use PostgreSQL specific code for additional schema information.

    # File lib/sequel/annotate.rb
149     def _schema_comment_postgres(output, options = {})
150       _table_comment_postgres(output, options)
151       schema_comment_columns(output, options)
152       oid = model.db.send(:regclass_oid, model.table_name)
153 
154       # These queries below are all based on the queries that psql
155       # uses, captured using the -E option to psql.
156 
157       unless options[:indexes] == false
158         rows = model.db.fetch(<<SQL, :oid=>oid).all
159 SELECT c2.relname, i.indisprimary, i.indisunique, pg_catalog.pg_get_indexdef(i.indexrelid, 0, true)
160 FROM pg_catalog.pg_class c, pg_catalog.pg_class c2, pg_catalog.pg_index i
161   LEFT JOIN pg_catalog.pg_constraint con ON (conrelid = i.indrelid AND conindid = i.indexrelid AND contype IN ('p','u','x'))
162 WHERE c.oid = :oid AND c.oid = i.indrelid AND i.indexrelid = c2.oid AND indisvalid
163 ORDER BY i.indisprimary DESC, i.indisunique DESC, c2.relname;
164 SQL
165         unless rows.empty?
166           output << "# Indexes:"
167           rows = rows.map do |r|
168             [r[:relname], "#{"PRIMARY KEY " if r[:indisprimary]}#{"UNIQUE " if r[:indisunique] && !r[:indisprimary]}#{r[:pg_get_indexdef].match(/USING (.+)\z/m)[1]}"]
169           end
170           output.concat(align(rows))
171         end
172       end
173 
174       unless options[:constraints] == false
175         rows = model.db.fetch(<<SQL, :oid=>oid).all
176 SELECT r.conname, pg_catalog.pg_get_constraintdef(r.oid, true)
177 FROM pg_catalog.pg_constraint r
178 WHERE r.conrelid = :oid AND r.contype = 'c'
179 ORDER BY 1;
180 SQL
181         unless rows.empty?
182           output << "# Check constraints:"
183           rows = rows.map do |r|
184             [r[:conname], r[:pg_get_constraintdef].match(/CHECK (.+)\z/m)[1]]
185           end
186           output.concat(align(rows))
187         end
188       end
189 
190       unless options[:foreign_keys] == false
191         rows = model.db.fetch(<<SQL, :oid=>oid).all
192 SELECT conname,
193   pg_catalog.pg_get_constraintdef(r.oid, true) as condef
194 FROM pg_catalog.pg_constraint r
195 WHERE r.conrelid = :oid AND r.contype = 'f' ORDER BY 1;
196 SQL
197         unless rows.empty?
198           output << "# Foreign key constraints:"
199           rows = rows.map do |r|
200             [r[:conname], r[:condef].match(/FOREIGN KEY (.+)\z/m)[1]]
201           end
202           output.concat(align(rows))
203         end
204       end
205 
206       unless options[:references] == false
207         rows = model.db.fetch(<<SQL, :oid=>oid).all
208 SELECT conname, conrelid::pg_catalog.regclass::text,
209   pg_catalog.pg_get_constraintdef(c.oid, true) as condef
210 FROM pg_catalog.pg_constraint c
211 WHERE c.confrelid = :oid AND c.contype = 'f' ORDER BY 2, 1;
212 SQL
213         unless rows.empty?
214           output << "# Referenced By:"
215           rows = rows.map do |r|
216             [r[:conrelid], r[:conname], r[:condef].match(/FOREIGN KEY (.+)\z/m)[1]]
217           end
218           output.concat(align(rows))
219         end
220       end
221 
222       unless options[:triggers] == false
223         rows = model.db.fetch(<<SQL, :oid=>oid).all
224 SELECT t.tgname, pg_catalog.pg_get_triggerdef(t.oid, true), t.tgenabled, t.tgisinternal
225 FROM pg_catalog.pg_trigger t
226 WHERE t.tgrelid = :oid AND (NOT t.tgisinternal OR (t.tgisinternal AND t.tgenabled = 'D'))
227 ORDER BY 1;
228 SQL
229         unless rows.empty?
230           output << "# Triggers:"
231           rows = rows.map do |r|
232             [r[:tgname], r[:pg_get_triggerdef].match(/((?:BEFORE|AFTER) .+)\z/m)[1]]
233           end
234           output.concat(align(rows))
235         end
236       end
237     end
_table_comment_postgres(output, options = {}) click to toggle source
    # File lib/sequel/annotate.rb
248     def _table_comment_postgres(output, options = {})
249       return if options[:comments] == false
250 
251       if table_comment = model.db.fetch(<<SQL, :oid=>model.db.send(:regclass_oid, model.table_name)).single_value
252 SELECT obj_description(CAST(:oid AS regclass), 'pg_class') AS "comment" LIMIT 1;
253 SQL
254         text = align([["Comment: #{table_comment}"]])
255         text[0][1] = ''
256         output.concat(text)
257       end
258     end
align(rows) click to toggle source

Returns an array of strings for each array of array, such that each string is aligned and commented appropriately. Example:

align([['abcdef', '1'], ['g', '123456']])
# => ["#  abcdef  1", "#  g       123456"]
    # File lib/sequel/annotate.rb
132 def align(rows)
133   cols = rows.first.length
134   lengths = [0] * cols
135 
136   cols.times do |i|
137     rows.each do |r|
138       lengths[i] = r[i].length if r[i] && r[i].length > lengths[i]
139     end
140   end
141 
142   rows.map do |r|
143     "#  #{r.zip(lengths).map{|c, l| c.to_s.ljust(l).gsub("\n", "\n#    ")}.join(' | ')}".strip
144   end
145 end
schema_comment_columns(output, options = {}) click to toggle source

The standard column schema information to output.

    # File lib/sequel/annotate.rb
261 def schema_comment_columns(output, options = {})
262   if cpk = model.primary_key.is_a?(Array)
263     output << "# Primary Key: (#{model.primary_key.join(', ')})"
264   end
265   output << "# Columns:"
266 
267   meth = :"_column_comments_#{model.db.database_type}"
268   column_comments = if options[:comments] != false && respond_to?(meth, true)
269     send(meth)
270   else
271     {}
272   end
273 
274   rows = model.columns.map do |col|
275     sch = model.db_schema[col]
276     parts = [
277       col.to_s,
278       sch[:db_domain_type] || sch[:db_type],
279       "#{"PRIMARY KEY #{"AUTOINCREMENT " if sch[:auto_increment] && model.db.database_type != :postgres}" if sch[:primary_key] && !cpk}#{"NOT NULL " if sch[:allow_null] == false && !sch[:primary_key]}#{"DEFAULT #{sch[:default]}" if sch[:default]}#{"GENERATED BY DEFAULT AS IDENTITY" if sch[:auto_increment] && !sch[:default] && model.db.database_type == :postgres && model.db.server_version >= 100000}",
280     ]
281     parts << (column_comments[col.to_s] || '') unless column_comments.empty?
282     parts
283   end
284   output.concat(align(rows))
285 end
schema_comment_foreign_keys(output) click to toggle source

The standard foreign key information to output.

    # File lib/sequel/annotate.rb
299 def schema_comment_foreign_keys(output)
300   unless (fks = model.db.foreign_key_list(model.table_name)).empty?
301     output << "# Foreign key constraints:"
302     rows = fks.map do |fk|
303       ["(#{fk[:columns].join(', ')}) REFERENCES #{fk[:table]}#{"(#{fk[:key].join(', ')})" if fk[:key]}"]
304     end
305     output.concat(align(rows).sort)
306   end
307 end
schema_comment_indexes(output) click to toggle source

The standard index information to output.

    # File lib/sequel/annotate.rb
288 def schema_comment_indexes(output)
289   unless (indexes = model.db.indexes(model.table_name)).empty?
290     output << "# Indexes:"
291     rows = indexes.map do |name, metadata|
292       [name.to_s, "#{'UNIQUE ' if metadata[:unique]}(#{metadata[:columns].join(', ')})"]
293     end
294     output.concat(align(rows).sort)
295   end
296 end
skip_model?() click to toggle source

Whether we should skip annotations for the model. True if the model selects from a dataset.

    # File lib/sequel/annotate.rb
311 def skip_model?
312   model.dataset.joined_dataset? || model.dataset.first_source_table.is_a?(Dataset)
313 rescue Sequel::Error
314   true
315 end