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,160 @@
1
+ # -*- coding: utf-8 -*-
2
+ module SchemaComments
3
+ module SchemaDumper
4
+ def self.included(mod)
5
+ # mod.extend(ClassMethods)
6
+ # mod.instance_eval do
7
+ # alias :ignore_tables_without_schema_comments :ignore_tables
8
+ # alias :ignore_tables :ignore_tables_with_schema_comments
9
+ # end
10
+ mod.module_eval do
11
+ alias_method_chain :tables, :schema_comments
12
+ alias_method_chain :table, :schema_comments
13
+ end
14
+ end
15
+
16
+ IGNORED_TABLE = 'schema_comments'
17
+
18
+ # module ClassMethods
19
+ # def ignore_tables_with_schema_comments
20
+ # result = ignore_tables_without_schema_comments
21
+ # result << IGNORED_TABLE unless result.include?(IGNORED_TABLE)
22
+ # result
23
+ # end
24
+ # end
25
+
26
+ private
27
+ def tables_with_schema_comments(stream)
28
+ tables_without_schema_comments(stream)
29
+ if adapter_name == "mysql"
30
+ # ビューはtableの後に実行するようにしないと rake db:schema:load で失敗します。
31
+ mysql_views(stream)
32
+ end
33
+ end
34
+
35
+ def table_with_schema_comments(table, stream)
36
+ return if IGNORED_TABLE == table.downcase
37
+ # MySQLは、ビューもテーブルとして扱うので、一個一個チェックします。
38
+ if adapter_name == 'mysql'
39
+ config = ActiveRecord::Base.configurations[RAILS_ENV]
40
+ match_count = @connection.select_value(
41
+ "select count(*) from information_schema.TABLES where TABLE_TYPE = 'VIEW' AND TABLE_SCHEMA = '%s' AND TABLE_NAME = '%s'" % [
42
+ config["database"], table])
43
+ return if match_count.to_i > 0
44
+ end
45
+ columns = @connection.columns(table)
46
+ begin
47
+ tbl = StringIO.new
48
+
49
+ if @connection.respond_to?(:pk_and_sequence_for)
50
+ pk, pk_seq = @connection.pk_and_sequence_for(table)
51
+ end
52
+ pk ||= 'id'
53
+
54
+ tbl.print " create_table #{table.inspect}"
55
+ if columns.detect { |c| c.name == pk }
56
+ if pk != 'id'
57
+ tbl.print %Q(, :primary_key => "#{pk}")
58
+ end
59
+ else
60
+ tbl.print ", :id => false"
61
+ end
62
+ tbl.print ", :force => true"
63
+
64
+ table_comment = @connection.table_comment(table)
65
+ tbl.print ", :comment => '#{table_comment}'" unless table_comment.blank?
66
+
67
+ tbl.puts " do |t|"
68
+
69
+ column_specs = columns.map do |column|
70
+ raise StandardError, "Unknown type '#{column.sql_type}' for column '#{column.name}'" if @types[column.type].nil?
71
+ next if column.name == pk
72
+ spec = {}
73
+ spec[:name] = column.name.inspect
74
+ spec[:type] = column.type.to_s
75
+ spec[:limit] = column.limit.inspect if column.limit != @types[column.type][:limit] && column.type != :decimal
76
+ spec[:precision] = column.precision.inspect if !column.precision.nil?
77
+ spec[:scale] = column.scale.inspect if !column.scale.nil?
78
+ spec[:null] = 'false' if !column.null
79
+ spec[:default] = default_string(column.default) if !column.default.nil?
80
+ spec[:comment] = (column.comment || '').inspect
81
+ (spec.keys - [:name, :type]).each{ |k| spec[k].insert(0, "#{k.inspect} => ")}
82
+ spec
83
+ end.compact
84
+
85
+ # find all migration keys used in this table
86
+ keys = [:name, :limit, :precision, :scale, :default, :null, :comment] & column_specs.map(&:keys).flatten
87
+
88
+ # figure out the lengths for each column based on above keys
89
+ lengths = keys.map{ |key| column_specs.map{ |spec| spec[key] ? spec[key].length + 2 : 0 }.max }
90
+
91
+ # the string we're going to sprintf our values against, with standardized column widths
92
+ format_string = lengths.map{ |len| "%-#{len}s" }
93
+
94
+ # find the max length for the 'type' column, which is special
95
+ type_length = column_specs.map{ |column| column[:type].length }.max
96
+
97
+ # add column type definition to our format string
98
+ format_string.unshift " t.%-#{type_length}s "
99
+
100
+ format_string *= ''
101
+
102
+ column_specs.each do |colspec|
103
+ values = keys.zip(lengths).map{ |key, len| colspec.key?(key) ? colspec[key] + ", " : " " * len }
104
+ values.unshift colspec[:type]
105
+ tbl.print((format_string % values).gsub(/,\s*$/, ''))
106
+ tbl.puts
107
+ end
108
+
109
+ tbl.puts " end"
110
+ tbl.puts
111
+
112
+ indexes(table, tbl)
113
+
114
+ tbl.rewind
115
+ stream.print tbl.read
116
+ rescue => e
117
+ stream.puts "# Could not dump table #{table.inspect} because of following #{e.class}"
118
+ stream.puts "# #{e.message}"
119
+ stream.puts
120
+ end
121
+
122
+ stream
123
+ end
124
+
125
+ def adapter_name
126
+ config = ActiveRecord::Base.configurations[RAILS_ENV]
127
+ config ? config['adapter'] : ActiveRecord::Base.connection.adapter_name
128
+ end
129
+
130
+ def mysql_views(stream)
131
+ config = ActiveRecord::Base.configurations[RAILS_ENV]
132
+ view_names = @connection.select_values(
133
+ "select TABLE_NAME from information_schema.TABLES where TABLE_TYPE = 'VIEW' AND TABLE_SCHEMA = '%s'" % config["database"])
134
+ view_names.each do |view_name|
135
+ mysql_view(view_name, stream)
136
+ end
137
+ end
138
+
139
+ def mysql_view(view_name, stream)
140
+ ddl = @connection.select_value("show create view #{view_name}")
141
+ ddl.gsub!(/^CREATE .+? VIEW /i, "CREATE OR REPLACE VIEW ")
142
+ ddl.gsub!(/AS select/, "AS \n select\n")
143
+ ddl.gsub!(/( AS \`.+?\`\,)/){ "#{$1}\n" }
144
+ ddl.gsub!(/ from /i , "\n from \n")
145
+ ddl.gsub!(/ where /i , "\n where \n")
146
+ ddl.gsub!(/ order by /i , "\n order by \n")
147
+ ddl.gsub!(/ having /i , "\n having \n")
148
+ ddl.gsub!(/ union /i , "\n union \n")
149
+ ddl.gsub!(/ and /i , "\n and ")
150
+ ddl.gsub!(/ or /i , "\n or ")
151
+ ddl.gsub!(/inner join/i , "\n inner join")
152
+ ddl.gsub!(/left join/i , "\n left join")
153
+ ddl.gsub!(/left outer join/i, "\n left outer join")
154
+ stream.print(" ActiveRecord::Base.connection.execute(<<-EOS)\n")
155
+ stream.print(ddl.split(/\n/).map{|line| ' ' << line.strip}.join("\n"))
156
+ stream.print("\n EOS\n")
157
+ end
158
+
159
+ end
160
+ end
@@ -0,0 +1,53 @@
1
+ module SchemaComments
2
+ VERSION = '0.1.0'
3
+
4
+ autoload :Base, 'schema_comments/base'
5
+ autoload :ConnectionAdapters, 'schema_comments/connection_adapters'
6
+ autoload :Migration, 'schema_comments/migration'
7
+ autoload :Migrator, 'schema_comments/migrator'
8
+ autoload :Schema, 'schema_comments/schema'
9
+ autoload :SchemaComment, 'schema_comments/schema_comment'
10
+ autoload :SchemaDumper, 'schema_comments/schema_dumper'
11
+
12
+ DEFAULT_YAML_PATH = File.expand_path(File.join(RAILS_ROOT, 'db/schema_comments.yml'))
13
+
14
+ mattr_accessor :yaml_path
15
+ self.yaml_path = DEFAULT_YAML_PATH
16
+
17
+ mattr_accessor :quiet
18
+
19
+ class << self
20
+ def setup
21
+ base_names = %w(Base Migration Migrator Schema SchemaDumper) +
22
+ %w(Column ColumnDefinition TableDefinition).map{|name| "ConnectionAdapters::#{name}"}
23
+
24
+ base_names.each do |base_name|
25
+ ar_class = "ActiveRecord::#{base_name}".constantize
26
+ sc_class = "SchemaComments::#{base_name}".constantize
27
+ unless ar_class.ancestors.include?(sc_class)
28
+ ar_class.__send__(:include, sc_class)
29
+ end
30
+ end
31
+
32
+ unless ActiveRecord::ConnectionAdapters::AbstractAdapter.ancestors.include?(SchemaComments::ConnectionAdapters::Adapter)
33
+ ActiveRecord::ConnectionAdapters::AbstractAdapter.module_eval do
34
+ include SchemaComments::ConnectionAdapters::Adapter
35
+ end
36
+ end
37
+
38
+ # %w(Mysql PostgreSQL SQLite3 SQLite Firebird DB2 Oracle Sybase Openbase Frontbase)
39
+ %w(Mysql PostgreSQL SQLite3 SQLite).each do |adapter|
40
+ begin
41
+ require("active_record/connection_adapters/#{adapter.downcase}_adapter")
42
+ adapter_class = ('ActiveRecord::ConnectionAdapters::' << "#{adapter}Adapter").constantize
43
+ adapter_class.module_eval do
44
+ include SchemaComments::ConnectionAdapters::ConcreteAdapter
45
+ end
46
+ rescue Exception => e
47
+ end
48
+ end
49
+ end
50
+
51
+ end
52
+
53
+ end
data/spec/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ /human_readable_schema_comments.yml
2
+ /schema_comments.yml
3
+ /schema_comments_test.sqlite3.db
@@ -0,0 +1,56 @@
1
+ # -*- coding: utf-8 -*-
2
+ require File.join(File.dirname(__FILE__), 'spec_helper')
3
+
4
+ require File.join(File.dirname(__FILE__), '../lib/annotate_models.rb')
5
+
6
+ describe AnnotateModels do
7
+
8
+ before(:each) do
9
+ SchemaComments.yaml_path = File.expand_path(File.join(File.dirname(__FILE__), 'schema_comments.yml'))
10
+ FileUtils.rm(SchemaComments.yaml_path, :verbose => true) if File.exist?(SchemaComments.yaml_path)
11
+
12
+ (ActiveRecord::Base.connection.tables - IGNORED_TABLES).each do |t|
13
+ ActiveRecord::Base.connection.drop_table(t) rescue nil
14
+ end
15
+ ActiveRecord::Base.connection.initialize_schema_migrations_table
16
+ ActiveRecord::Base.connection.execute "DELETE FROM #{ActiveRecord::Migrator.schema_migrations_table_name}"
17
+ end
18
+
19
+ it "get_schema_info" do
20
+ (ActiveRecord::Base.connection.tables - %w(schema_migrations)).should == []
21
+
22
+ ActiveRecord::Schema.define(:version => "20090721185959") do
23
+ drop_table("books") rescue nil
24
+
25
+ create_table "books", :force => true, :comment => '書籍' do |t|
26
+ t.string "title", :limit => 100, :null => false, :comment => 'タイトル'
27
+ t.integer "size", :null => false, :default => 1, :comment => '判型'
28
+ t.decimal "price", :precision => 17, :scale => 14, :default => 0.0, :null => false, :comment => '価格'
29
+ t.datetime "created_at", :comment => '登録日時'
30
+ t.datetime "updated_at", :comment => '更新日時'
31
+ end
32
+ end
33
+
34
+ class Book < ActiveRecord::Base
35
+ end
36
+
37
+
38
+ AnnotateModels.get_schema_info(Book).should == %{# == Schema Info ==
39
+ #
40
+ # Schema version: 20090721185959
41
+ #
42
+ # Table name: books # 書籍
43
+ #
44
+ # id :integer not null, primary key
45
+ # title :string(100) not null # タイトル
46
+ # size :integer not null, default(1) # 判型
47
+ # price :decimal(17, 14) not null, default(0.0) # 価格
48
+ # created_at :datetime # 登録日時
49
+ # updated_at :datetime # 更新日時
50
+ #
51
+ # =================
52
+ #
53
+ }
54
+ end
55
+
56
+ end
data/spec/database.yml ADDED
@@ -0,0 +1,13 @@
1
+ sqlite:
2
+ :adapter: sqlite
3
+ :dbfile: schema_comments_test.sqlite.db
4
+ sqlite3:
5
+ :adapter: sqlite3
6
+ :database: schema_comments_test.sqlite3.db
7
+ mysql:
8
+ adapter: mysql
9
+ encoding: utf8
10
+ username: root
11
+ password:
12
+ socket: /opt/local/var/run/mysql5/mysqld.sock
13
+ database: schema_comments_test
File without changes
@@ -0,0 +1,48 @@
1
+ # -*- coding: utf-8 -*-
2
+ require File.join(File.dirname(__FILE__), 'spec_helper')
3
+
4
+ describe SchemaComments::Base do
5
+
6
+ MIGRATIONS_ROOT = File.join(File.dirname(__FILE__), 'migrations')
7
+
8
+ IGNORED_TABLES = %w(schema_migrations)
9
+
10
+ before(:each) do
11
+ SchemaComments.yaml_path = File.expand_path(File.join(File.dirname(__FILE__), 'schema_comments.yml'))
12
+ FileUtils.rm(SchemaComments.yaml_path, :verbose => true) if File.exist?(SchemaComments.yaml_path)
13
+
14
+ (ActiveRecord::Base.connection.tables - IGNORED_TABLES).each do |t|
15
+ ActiveRecord::Base.connection.drop_table(t) rescue nil
16
+ end
17
+ ActiveRecord::Base.connection.initialize_schema_migrations_table
18
+ ActiveRecord::Base.connection.execute "DELETE FROM #{ActiveRecord::Migrator.schema_migrations_table_name}"
19
+ end
20
+
21
+ it "test_valid_migration" do
22
+ (ActiveRecord::Base.connection.tables - %w(schema_migrations)).should == []
23
+
24
+ migration_path = File.join(MIGRATIONS_ROOT, 'valid')
25
+ Dir.glob('*.rb').each do |file|
26
+ require(file) if /^\d+?_.*/ =~ file
27
+ end
28
+
29
+ Product.reset_table_comments
30
+ Product.reset_column_comments
31
+
32
+ ActiveRecord::Migrator.up(migration_path, 1)
33
+ ActiveRecord::Migrator.current_version.should == 1
34
+
35
+ ActiveRecord::Base.export_i18n_models.keys.include?('product').should == true
36
+ ActiveRecord::Base.export_i18n_models['product'].should == '商品'
37
+
38
+ ActiveRecord::Base.export_i18n_attributes.keys.include?('product').should == true
39
+ ActiveRecord::Base.export_i18n_attributes['product'].should == {
40
+ 'product_type_cd' => '種別コード',
41
+ "price" => "価格",
42
+ "name" => "商品名",
43
+ "created_at" => "登録日時",
44
+ "updated_at" => "更新日時"
45
+ }
46
+ end
47
+
48
+ end
@@ -0,0 +1,96 @@
1
+ # -*- coding: utf-8 -*-
2
+ require File.join(File.dirname(__FILE__), 'spec_helper')
3
+
4
+ describe ActiveRecord::Migrator do
5
+
6
+ MIGRATIONS_ROOT = File.join(File.dirname(__FILE__), 'migrations')
7
+
8
+ IGNORED_TABLES = %w(schema_migrations)
9
+
10
+ before(:each) do
11
+ SchemaComments.yaml_path = File.expand_path(File.join(File.dirname(__FILE__), 'schema_comments.yml'))
12
+ FileUtils.rm(SchemaComments.yaml_path, :verbose => true) if File.exist?(SchemaComments.yaml_path)
13
+
14
+ (ActiveRecord::Base.connection.tables - IGNORED_TABLES).each do |t|
15
+ ActiveRecord::Base.connection.drop_table(t) rescue nil
16
+ end
17
+ ActiveRecord::Base.connection.initialize_schema_migrations_table
18
+ ActiveRecord::Base.connection.execute "DELETE FROM #{ActiveRecord::Migrator.schema_migrations_table_name}"
19
+ end
20
+
21
+ it "test_valid_migration" do
22
+ (ActiveRecord::Base.connection.tables - %w(schema_migrations)).should == []
23
+
24
+ migration_path = File.join(MIGRATIONS_ROOT, 'valid')
25
+ Dir.glob('*.rb').each do |file|
26
+ require(file) if /^\d+?_.*/ =~ file
27
+ end
28
+
29
+ Product.reset_table_comments
30
+ Product.reset_column_comments
31
+
32
+ ActiveRecord::Migrator.up(migration_path, 1)
33
+
34
+ ActiveRecord::Migrator.current_version.should == 1
35
+ Product.table_comment.should == '商品'
36
+ {
37
+ 'product_type_cd' => '種別コード',
38
+ "price" => "価格",
39
+ "name" => "商品名",
40
+ "created_at" => "登録日時",
41
+ "updated_at" => "更新日時"
42
+ }.each do |col_name, comment|
43
+ Product.columns.detect{|c| c.name.to_s == col_name}.comment.should == comment
44
+ end
45
+
46
+ ActiveRecord::Migrator.down(migration_path, 0)
47
+ # SchemaComments::SchemaComment.count.should == 0
48
+
49
+ ActiveRecord::Migrator.up(migration_path, 1)
50
+ ActiveRecord::Migrator.up(migration_path, 2)
51
+ ActiveRecord::Migrator.current_version.should == 2
52
+
53
+ ProductName.table_comment.should == '商品'
54
+ {
55
+ 'product_type_cd' => '種別コード',
56
+ "price" => "価格",
57
+ "name" => "商品名",
58
+ "created_at" => "登録日時",
59
+ "updated_at" => "更新日時"
60
+ }.each do |col_name, comment|
61
+ ProductName.columns.detect{|c| c.name == col_name}.comment.should == comment
62
+ end
63
+
64
+ ActiveRecord::Migrator.down(migration_path, 1)
65
+ ActiveRecord::Migrator.current_version.should == 1
66
+
67
+ Product.table_comment.should == '商品'
68
+ {
69
+ 'product_type_cd' => '種別コード',
70
+ "price" => "価格",
71
+ "name" => "商品名",
72
+ "created_at" => "登録日時",
73
+ "updated_at" => "更新日時"
74
+ }.each do |col_name, comment|
75
+ Product.columns.detect{|c| c.name == col_name}.comment.should == comment
76
+ end
77
+
78
+ ActiveRecord::Migrator.up(migration_path, 4)
79
+ ActiveRecord::Migrator.current_version.should == 4
80
+ # SchemaComments::SchemaComment.count.should == 5
81
+
82
+ ActiveRecord::Migrator.down(migration_path, 3)
83
+ ActiveRecord::Migrator.current_version.should == 3
84
+ # SchemaComments::SchemaComment.count.should == 6
85
+
86
+ ActiveRecord::Migrator.up(migration_path, 5)
87
+ ActiveRecord::Migrator.current_version.should == 5
88
+ Product.columns.detect{|c| c.name == 'name'}.comment.should == '商品名'
89
+
90
+ ActiveRecord::Migrator.up(migration_path, 6)
91
+ ActiveRecord::Migrator.current_version.should == 6
92
+ Product.reset_column_comments
93
+ Product.columns.detect{|c| c.name == 'name'}.comment.should == '名称'
94
+ end
95
+
96
+ end
@@ -0,0 +1,17 @@
1
+ # -*- coding: utf-8 -*-
2
+ class CreateProducts < ActiveRecord::Migration
3
+
4
+ def self.up
5
+ create_table "products", :comment => '商品' do |t|
6
+ t.string "product_type_cd", :comment => '種別コード'
7
+ t.integer "price", :comment => "価格"
8
+ t.string "name", :comment => "商品名"
9
+ t.datetime "created_at", :comment => "登録日時"
10
+ t.datetime "updated_at", :comment => "更新日時"
11
+ end
12
+ end
13
+
14
+ def self.down
15
+ drop_table "products"
16
+ end
17
+ end