schema_comments 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. data/.gitignore +4 -0
  2. data/LICENSE.txt +60 -0
  3. data/README +149 -0
  4. data/Rakefile +46 -0
  5. data/VERSION +1 -0
  6. data/autotest/discover.rb +10 -0
  7. data/init.rb +4 -0
  8. data/lib/annotate_models.rb +224 -0
  9. data/lib/schema_comments/base.rb +72 -0
  10. data/lib/schema_comments/connection_adapters.rb +170 -0
  11. data/lib/schema_comments/migration.rb +20 -0
  12. data/lib/schema_comments/migrator.rb +20 -0
  13. data/lib/schema_comments/schema.rb +20 -0
  14. data/lib/schema_comments/schema_comment.rb +195 -0
  15. data/lib/schema_comments/schema_dumper.rb +160 -0
  16. data/lib/schema_comments.rb +53 -0
  17. data/spec/.gitignore +3 -0
  18. data/spec/annotate_models_spec.rb +56 -0
  19. data/spec/database.yml +13 -0
  20. data/spec/fixtures/.gitignore +0 -0
  21. data/spec/i18n_export_spec.rb +48 -0
  22. data/spec/migration_spec.rb +96 -0
  23. data/spec/migrations/valid/001_create_products.rb +17 -0
  24. data/spec/migrations/valid/002_rename_products.rb +10 -0
  25. data/spec/migrations/valid/003_rename_products_again.rb +10 -0
  26. data/spec/migrations/valid/004_remove_price.rb +10 -0
  27. data/spec/migrations/valid/005_change_products_name.rb +10 -0
  28. data/spec/migrations/valid/006_change_products_name_with_comment.rb +10 -0
  29. data/spec/resources/models/product.rb +2 -0
  30. data/spec/resources/models/product_name.rb +2 -0
  31. data/spec/schema.rb +2 -0
  32. data/spec/schema_dumper_spec.rb +74 -0
  33. data/spec/spec.opts +6 -0
  34. data/spec/spec_helper.rb +46 -0
  35. data/spec/yaml_export_spec.rb +52 -0
  36. data/tasks/annotate_models_tasks.rake +12 -0
  37. data/tasks/schema_comments.rake +204 -0
  38. metadata +115 -0
@@ -0,0 +1,72 @@
1
+ module SchemaComments
2
+ module Base
3
+ def self.included(mod)
4
+ mod.extend ClassMethods
5
+ mod.instance_eval do
6
+ alias :columns_without_schema_comments :columns
7
+ alias :columns :columns_with_schema_comments
8
+ end
9
+ end
10
+
11
+ module ClassMethods
12
+ def table_comment
13
+ @table_comment ||= connection.table_comment(table_name)
14
+ end
15
+
16
+ def columns_with_schema_comments
17
+ result = columns_without_schema_comments
18
+ unless @column_comments_loaded
19
+ column_comment_hash = connection.column_comments(table_name)
20
+ result.each do |column|
21
+ column.comment = column_comment_hash[column.name.to_s]
22
+ end
23
+ @column_comments_loaded = true
24
+ end
25
+ result
26
+ end
27
+
28
+ def reset_column_comments
29
+ @column_comments_loaded = false
30
+ end
31
+
32
+ def reset_table_comments
33
+ @table_comment = nil
34
+ end
35
+
36
+ attr_accessor_with_default :ignore_pattern_to_export_i18n, /\[.*\]/
37
+
38
+ def export_i18n_models
39
+ subclasses = ActiveRecord::Base.send(:subclasses).select do |klass|
40
+ (klass != SchemaComments::SchemaComment) and
41
+ klass.respond_to?(:table_exists?) and klass.table_exists?
42
+ end
43
+ subclasses.inject({}) do |d, m|
44
+ comment = m.table_comment
45
+ comment.gsub!(ignore_pattern_to_export_i18n, '') if ignore_pattern_to_export_i18n
46
+ d[m.name.underscore] = comment
47
+ d
48
+ end
49
+ end
50
+
51
+ def export_i18n_attributes(connection = ActiveRecord::Base.connection)
52
+ subclasses = ActiveRecord::Base.send(:subclasses).select do |klass|
53
+ (klass != SchemaComments::SchemaComment) and
54
+ klass.respond_to?(:table_exists?) and klass.table_exists?
55
+ end
56
+ subclasses.inject({}) do |d, m|
57
+ attrs = {}
58
+ m.columns.each do |col|
59
+ next if col.name == 'id'
60
+ comment = (col.comment || '').dup
61
+ comment.gsub!(ignore_pattern_to_export_i18n, '') if ignore_pattern_to_export_i18n
62
+ attrs[col.name] = comment
63
+ end
64
+ d[m.name.underscore] = attrs
65
+ d
66
+ end
67
+ end
68
+
69
+ end
70
+
71
+ end
72
+ end
@@ -0,0 +1,170 @@
1
+ # -*- coding: utf-8 -*-
2
+ module SchemaComments
3
+ module ConnectionAdapters
4
+
5
+ module Column
6
+ attr_accessor :comment
7
+ end
8
+
9
+ module ColumnDefinition
10
+ attr_accessor :comment
11
+ end
12
+
13
+ module TableDefinition
14
+ def self.included(mod)
15
+ mod.module_eval do
16
+ alias_method_chain(:column, :schema_comments)
17
+ end
18
+ end
19
+ attr_accessor :comment
20
+
21
+ def column_with_schema_comments(name, type, options = {})
22
+ column_without_schema_comments(name, type, options)
23
+ column = self[name]
24
+ column.comment = options[:comment]
25
+ self
26
+ end
27
+ end
28
+
29
+ module Adapter
30
+ def column_comment(table_name, column_name, comment = nil) #:nodoc:
31
+ if comment
32
+ SchemaComment.save_column_comment(table_name, column_name, comment) unless SchemaComments.quiet
33
+ return comment
34
+ else
35
+ SchemaComment.column_comment(table_name, column_name)
36
+ end
37
+ end
38
+
39
+ # Mass assignment of comments in the form of a hash. Example:
40
+ # column_comments {
41
+ # :users => {:first_name => "User's given name", :last_name => "Family name"},
42
+ # :tags => {:id => "Tag IDentifier"}}
43
+ def column_comments(contents)
44
+ if contents.is_a?(Hash)
45
+ contents.each_pair do |table, cols|
46
+ cols.each_pair do |col, comment|
47
+ column_comment(table, col, comment) unless SchemaComments.quiet
48
+ end
49
+ end
50
+ else
51
+ SchemaComment.column_comments(contents)
52
+ end
53
+ end
54
+
55
+ def table_comment(table_name, comment = nil) #:nodoc:
56
+ if comment
57
+ comment = (comment[:comment] || comment['comment']) if comment.is_a?(Hash)
58
+ SchemaComment.save_table_comment(table_name, comment) unless SchemaComments.quiet
59
+ return comment
60
+ else
61
+ SchemaComment.table_comment(table_name)
62
+ end
63
+ end
64
+
65
+ def delete_schema_comments(table_name, column_name = nil)
66
+ SchemaComment.destroy_of(table_name, column_name) unless SchemaComments.quiet
67
+ end
68
+
69
+ def update_schema_comments_table_name(table_name, new_name)
70
+ SchemaComment.update_table_name(table_name, new_name) unless SchemaComments.quiet
71
+ end
72
+
73
+ def update_schema_comments_column_name(table_name, column_name, new_name)
74
+ SchemaComment.update_column_name(table_name, column_name, new_name) unless SchemaComments.quiet
75
+ end
76
+ end
77
+
78
+ module ConcreteAdapter
79
+ def self.included(mod)
80
+ mod.module_eval do
81
+ alias_method_chain :columns, :schema_comments
82
+ alias_method_chain :create_table, :schema_comments
83
+ alias_method_chain :drop_table, :schema_comments
84
+ alias_method_chain :rename_table, :schema_comments
85
+ alias_method_chain :remove_column, :schema_comments
86
+ alias_method_chain :add_column, :schema_comments
87
+ alias_method_chain :change_column, :schema_comments
88
+ alias_method_chain :rename_column, :schema_comments
89
+ end
90
+ end
91
+
92
+ def columns_with_schema_comments(table_name, name = nil, &block)
93
+ result = columns_without_schema_comments(table_name, name, &block)
94
+ column_comment_hash = column_comments(table_name)
95
+ result.each do |column|
96
+ column.comment = column_comment_hash[column.name]
97
+ end
98
+ result
99
+ end
100
+
101
+ def create_table_with_schema_comments(table_name, options = {}, &block)
102
+ table_def = nil
103
+ result = create_table_without_schema_comments(table_name, options) do |t|
104
+ table_def = t
105
+ yield(t)
106
+ end
107
+ table_comment(table_name, options[:comment]) unless options[:comment].blank?
108
+ table_def.columns.each do |col|
109
+ column_comment(table_name, col.name, col.comment) unless col.comment.blank?
110
+ end
111
+ result
112
+ end
113
+
114
+ def drop_table_with_schema_comments(table_name, options = {}, &block)
115
+ result = drop_table_without_schema_comments(table_name, options)
116
+ delete_schema_comments(table_name) unless @ignore_drop_table
117
+ result
118
+ end
119
+
120
+ def rename_table_with_schema_comments(table_name, new_name)
121
+ result = rename_table_without_schema_comments(table_name, new_name)
122
+ update_schema_comments_table_name(table_name, new_name)
123
+ result
124
+ end
125
+
126
+ def remove_column_with_schema_comments(table_name, *column_names)
127
+ # sqlite3ではremove_columnがないので、以下のフローでスキーマ更新します。
128
+ # 1. CREATE TEMPORARY TABLE "altered_xxxxxx" (・・・)
129
+ # 2. PRAGMA index_list("xxxxxx")
130
+ # 3. DROP TABLE "xxxxxx"
131
+ # 4. CREATE TABLE "xxxxxx"
132
+ # 5. PRAGMA index_list("altered_xxxxxx")
133
+ # 6. DROP TABLE "altered_xxxxxx"
134
+ #
135
+ # このdrop tableの際に、schema_commentsを変更しないようにフラグを立てています。
136
+ @ignore_drop_table = true
137
+ remove_column_without_schema_comments(table_name, *column_names)
138
+ column_names.each do |column_name|
139
+ delete_schema_comments(table_name, column_name)
140
+ end
141
+ ensure
142
+ @ignore_drop_table = false
143
+ end
144
+
145
+ def add_column_with_schema_comments(table_name, column_name, type, options = {})
146
+ comment = options.delete(:comment)
147
+ result = add_column_without_schema_comments(table_name, column_name, type, options)
148
+ column_comment(table_name, column_name, comment) if comment
149
+ result
150
+ end
151
+
152
+ def change_column_with_schema_comments(table_name, column_name, type, options = {})
153
+ comment = options.delete(:comment)
154
+ @ignore_drop_table = true
155
+ result = change_column_without_schema_comments(table_name, column_name, type, options)
156
+ column_comment(table_name, column_name, comment) if comment
157
+ result
158
+ ensure
159
+ @ignore_drop_table = false
160
+ end
161
+
162
+ def rename_column_with_schema_comments(table_name, column_name, new_column_name)
163
+ result = rename_column_without_schema_comments(table_name, column_name, new_column_name)
164
+ comment = update_schema_comments_column_name(table_name, column_name, new_column_name)
165
+ result
166
+ end
167
+
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,20 @@
1
+ module SchemaComments
2
+ module Migration
3
+ def self.included(mod)
4
+ mod.extend(ClassMethods)
5
+ mod.instance_eval do
6
+ alias :migrate_without_schema_comments :migrate
7
+ alias :migrate :migrate_with_schema_comments
8
+ end
9
+ end
10
+
11
+ module ClassMethods
12
+ def migrate_with_schema_comments(*args, &block)
13
+ SchemaComments::SchemaComment.yaml_access do
14
+ migrate_without_schema_comments(*args, &block)
15
+ end
16
+ end
17
+ end
18
+
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ module SchemaComments
2
+ module Migrator
3
+ def self.included(mod)
4
+ mod.extend(ClassMethods)
5
+ mod.instance_eval do
6
+ alias :migrate_without_schema_comments :migrate
7
+ alias :migrate :migrate_with_schema_comments
8
+ end
9
+ end
10
+
11
+ module ClassMethods
12
+ def migrate_with_schema_comments(*args, &block)
13
+ SchemaComments::SchemaComment.yaml_access do
14
+ migrate_without_schema_comments(*args, &block)
15
+ end
16
+ end
17
+ end
18
+
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ module SchemaComments
2
+ module Schema
3
+ def self.included(mod)
4
+ mod.extend(ClassMethods)
5
+ mod.instance_eval do
6
+ alias :define_without_schema_comments :define
7
+ alias :define :define_with_schema_comments
8
+ end
9
+ end
10
+
11
+ module ClassMethods
12
+ def define_with_schema_comments(*args, &block)
13
+ SchemaComments::SchemaComment.yaml_access do
14
+ define_without_schema_comments(*args, &block)
15
+ end
16
+ end
17
+ end
18
+
19
+ end
20
+ end
@@ -0,0 +1,195 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'yaml/store'
3
+
4
+ module SchemaComments
5
+ # 現在はActiveRecord::Baseを継承していますが、将来移行が完全に終了した
6
+ # 時点で、ActiveRecord::Baseの継承をやめます。
7
+ #
8
+ # それまではDBからのロードは可能ですが、YAMLにのみ保存します。
9
+ class SchemaComment < ActiveRecord::Base
10
+ set_table_name('schema_comments')
11
+
12
+ TABLE_KEY = 'table_comments'
13
+ COLUMN_KEY = 'column_comments'
14
+
15
+ class << self
16
+ def table_comment(table_name)
17
+ if yaml_exist?
18
+ @table_names ||= yaml_access{|db| db[TABLE_KEY]}.dup
19
+ return @table_names[table_name.to_s]
20
+ end
21
+ return nil unless table_exists?
22
+ connection.select_value(sanitize_conditions("select descriptions from schema_comments where table_name = '%s' and column_name is null" % table_name))
23
+ end
24
+
25
+ def column_comment(table_name, column_name)
26
+ if yaml_exist?
27
+ @column_names ||= yaml_access{|db| db[COLUMN_KEY] }.dup
28
+ column_hash = @column_names[table_name.to_s] || {}
29
+ return column_hash[column_name.to_s]
30
+ end
31
+ return nil unless table_exists?
32
+ connection.select_value(sanitize_conditions("select descriptions from schema_comments where table_name = '%s' and column_name = '%s'" % [table_name, column_name]))
33
+ end
34
+
35
+ def column_comments(table_name)
36
+ if yaml_exist?
37
+ result = nil
38
+ @column_names ||= yaml_access{|db| db[COLUMN_KEY] }.dup
39
+ result = @column_names[table_name.to_s]
40
+ return result || {}
41
+ end
42
+ return {} unless table_exists?
43
+ hash_array = connection.select_all(sanitize_conditions("select column_name, descriptions from schema_comments where table_name = '%s' and column_name is not null" % table_name))
44
+ hash_array.inject({}){|dest, r| dest[r['column_name']] = r['descriptions']; dest}
45
+ end
46
+
47
+ def save_table_comment(table_name, comment)
48
+ yaml_access do |db|
49
+ db[TABLE_KEY][table_name.to_s] = comment
50
+ end
51
+ @table_names = nil
52
+ end
53
+
54
+ def save_column_comment(table_name, column_name, comment)
55
+ yaml_access do |db|
56
+ db[COLUMN_KEY][table_name.to_s] ||= {}
57
+ db[COLUMN_KEY][table_name.to_s][column_name.to_s] = comment
58
+ end
59
+ @column_names = nil
60
+ end
61
+
62
+ def destroy_of(table_name, column_name)
63
+ yaml_access do |db|
64
+ column_hash = db[COLUMN_KEY][table_name.to_s]
65
+ column_hash.delete(column_name) if column_hash
66
+ end
67
+ @column_names = nil
68
+ end
69
+
70
+ def update_table_name(table_name, new_name)
71
+ if yaml_exist?
72
+ yaml_access do |db|
73
+ db[TABLE_KEY][new_name.to_s] = db[TABLE_KEY].delete(table_name.to_s)
74
+ db[COLUMN_KEY][new_name.to_s] = db[COLUMN_KEY].delete(table_name.to_s)
75
+ end
76
+ end
77
+ @table_names = nil
78
+ @column_names = nil
79
+ end
80
+
81
+ def update_column_name(table_name, column_name, new_name)
82
+ if yaml_exist?
83
+ yaml_access do |db|
84
+ table_cols = db[COLUMN_KEY][table_name.to_s]
85
+ if table_cols
86
+ table_cols[new_name.to_s] = table_cols.delete(column_name.to_s)
87
+ end
88
+ end
89
+ end
90
+ @table_names = nil
91
+ @column_names = nil
92
+ end
93
+
94
+ def yaml_exist?
95
+ File.exist?(SchemaComments.yaml_path)
96
+ end
97
+
98
+ def yaml_access(&block)
99
+ if @yaml_transaction
100
+ yield(@yaml_transaction) if block_given?
101
+ else
102
+ db = SortedStore.new(SchemaComments.yaml_path)
103
+ result = nil
104
+ # t = Time.now.to_f
105
+ db.transaction do
106
+ @yaml_transaction = db
107
+ begin
108
+ db[TABLE_KEY] ||= {}
109
+ db[COLUMN_KEY] ||= {}
110
+ result = yield(db) if block_given?
111
+ ensure
112
+ @yaml_transaction = nil
113
+ end
114
+ end
115
+ # puts("SchemaComment#yaml_access %fms from %s" % [Time.now.to_f - t, caller[0].gsub(/^.+:in /, '')])
116
+ result
117
+ end
118
+ end
119
+
120
+ end
121
+
122
+ class SortedStore < YAML::Store
123
+ module ColumnNamedHash
124
+ def each
125
+ @column_names.each do |column_name|
126
+ yield(column_name, self[column_name])
127
+ end
128
+ end
129
+ end
130
+
131
+ def dump(table)
132
+ root = nil
133
+ StringIO.open do |io|
134
+ YAML.dump(@table, io)
135
+ io.rewind
136
+ root = YAML.load(io)
137
+ end
138
+
139
+ table_comments = root['table_comments']
140
+ column_comments = root['column_comments']
141
+ # 大元は
142
+ # table_comments:
143
+ # ...
144
+ # column_comments:
145
+ # ...
146
+ # その他
147
+ # ...
148
+ # の順番です。
149
+ root.instance_eval do
150
+ def each
151
+ yield('table_comments', self['table_comments'])
152
+ yield('column_comments', self['column_comments'])
153
+ (self.keys - ['table_comments', 'column_comments']).each do |key|
154
+ yield(key, self[key])
155
+ end
156
+ end
157
+ end
158
+ # table_comments はテーブル名のアルファベット順
159
+ table_names = ActiveRecord::Base.connection.tables.sort - ['schema_migrations']
160
+ table_comments.instance_variable_set(:@table_names, table_names)
161
+ table_comments.instance_eval do
162
+ def each
163
+ @table_names.each do |key|
164
+ yield(key, self[key])
165
+ end
166
+ end
167
+ end
168
+ # column_comments もテーブル名のアルファベット順
169
+ column_comments.instance_variable_set(:@table_names, table_names)
170
+ column_comments.instance_eval do
171
+ def each
172
+ @table_names.each do |key|
173
+ yield(key, self[key])
174
+ end
175
+ end
176
+ end
177
+ # column_comments の各値はテーブルのカラム順
178
+ column_comments.each do |table_name, column_hash|
179
+ column_names = nil
180
+ begin
181
+ columns = ActiveRecord::Base.connection.columns_without_schema_comments(table_name, "#{table_name.classify} Columns")
182
+ column_names = columns.map(&:name)
183
+ rescue ActiveRecord::ActiveRecordError
184
+ column_names = column_hash.keys.sort
185
+ end
186
+ column_names.delete('id')
187
+ column_hash.instance_variable_set(:@column_names, column_names)
188
+ column_hash.extend(ColumnNamedHash)
189
+ end
190
+ root.to_yaml(@opt)
191
+ end
192
+ end
193
+
194
+ end
195
+ end