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,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