schema_comments 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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