class Sequel::Annotate
Attributes
The model to annotate
Public Class Methods
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
Store the model to annotate
# File lib/sequel/annotate.rb 44 def initialize(model) 45 @model = model 46 end
Public Instance Methods
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
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
# 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
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
# 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
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
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
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
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
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