schema_plus 0.1.0.pre1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. data/.gitignore +25 -0
  2. data/Gemfile +3 -0
  3. data/MIT-LICENSE +25 -0
  4. data/README.rdoc +147 -0
  5. data/Rakefile +70 -0
  6. data/init.rb +1 -0
  7. data/lib/schema_plus/active_record/associations.rb +211 -0
  8. data/lib/schema_plus/active_record/base.rb +81 -0
  9. data/lib/schema_plus/active_record/connection_adapters/abstract_adapter.rb +96 -0
  10. data/lib/schema_plus/active_record/connection_adapters/column.rb +55 -0
  11. data/lib/schema_plus/active_record/connection_adapters/foreign_key_definition.rb +115 -0
  12. data/lib/schema_plus/active_record/connection_adapters/index_definition.rb +51 -0
  13. data/lib/schema_plus/active_record/connection_adapters/mysql_adapter.rb +111 -0
  14. data/lib/schema_plus/active_record/connection_adapters/postgresql_adapter.rb +163 -0
  15. data/lib/schema_plus/active_record/connection_adapters/schema_statements.rb +39 -0
  16. data/lib/schema_plus/active_record/connection_adapters/sqlite3_adapter.rb +78 -0
  17. data/lib/schema_plus/active_record/connection_adapters/table_definition.rb +130 -0
  18. data/lib/schema_plus/active_record/migration.rb +220 -0
  19. data/lib/schema_plus/active_record/schema.rb +27 -0
  20. data/lib/schema_plus/active_record/schema_dumper.rb +122 -0
  21. data/lib/schema_plus/active_record/validations.rb +139 -0
  22. data/lib/schema_plus/railtie.rb +12 -0
  23. data/lib/schema_plus/version.rb +3 -0
  24. data/lib/schema_plus.rb +248 -0
  25. data/schema_plus.gemspec +37 -0
  26. data/schema_plus.gemspec.rails3.0 +36 -0
  27. data/schema_plus.gemspec.rails3.1 +36 -0
  28. data/spec/association_spec.rb +529 -0
  29. data/spec/connections/mysql/connection.rb +18 -0
  30. data/spec/connections/mysql2/connection.rb +18 -0
  31. data/spec/connections/postgresql/connection.rb +15 -0
  32. data/spec/connections/sqlite3/connection.rb +14 -0
  33. data/spec/foreign_key_definition_spec.rb +23 -0
  34. data/spec/foreign_key_spec.rb +142 -0
  35. data/spec/index_definition_spec.rb +139 -0
  36. data/spec/index_spec.rb +71 -0
  37. data/spec/migration_spec.rb +405 -0
  38. data/spec/models/comment.rb +2 -0
  39. data/spec/models/post.rb +2 -0
  40. data/spec/models/user.rb +2 -0
  41. data/spec/references_spec.rb +78 -0
  42. data/spec/schema/auto_schema.rb +23 -0
  43. data/spec/schema/core_schema.rb +21 -0
  44. data/spec/schema_dumper_spec.rb +167 -0
  45. data/spec/schema_spec.rb +71 -0
  46. data/spec/spec_helper.rb +59 -0
  47. data/spec/support/extensions/active_model.rb +13 -0
  48. data/spec/support/helpers.rb +16 -0
  49. data/spec/support/matchers/automatic_foreign_key_matchers.rb +2 -0
  50. data/spec/support/matchers/have_index.rb +52 -0
  51. data/spec/support/matchers/reference.rb +66 -0
  52. data/spec/support/reference.rb +66 -0
  53. data/spec/validations_spec.rb +294 -0
  54. data/spec/views_spec.rb +140 -0
  55. metadata +269 -0
data/.gitignore ADDED
@@ -0,0 +1,25 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ .*.sw?
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
22
+ .rvmrc
23
+ *.log
24
+ *.sqlite3
25
+ Gemfile.lock
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec
data/MIT-LICENSE ADDED
@@ -0,0 +1,25 @@
1
+ Copyright (c) 2006 RedHill Consulting, Pty. Ltd.
2
+ Copyright (c) 2009 Michal Lomnicki & Ronen Barzel
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining
5
+ a copy of this software and associated documentation files (the
6
+ "Software"), to deal in the Software without restriction, including
7
+ without limitation the rights to use, copy, modify, merge, publish,
8
+ distribute, sublicense, and/or sell copies of the Software, and to
9
+ permit persons to whom the Software is furnished to do so, subject to
10
+ the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be
13
+ included in all copies or substantial portions of the Software.
14
+
15
+ Except as contained in this notice, the name(s) of the above copyright
16
+ holders shall not be used in advertising or otherwise to promote the sale,
17
+ use or other dealings in this Software without prior written authorization.
18
+
19
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
20
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
21
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
22
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
23
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
24
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
25
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,147 @@
1
+ = SchemaPlus
2
+
3
+ == Overview
4
+
5
+ SchemaPlus is an ActiveRecord extension that provides enhanced capabilities for schema definition and querying, including: enhanced and more DRY index capabilities, support and automation for foreign key constraints, and support for views.
6
+
7
+ For added rails DRYness see also the gems {+schema_associations+}[http://rubygems.org/gems/schema_associations] and {+schema_validations+}[http://rubygems.org/gems/schema_associations] <b>IMPORTANT PRERELEASE NOTE: <i>Those are not yet separate gems, they are currently bundled in here. They're not documented yet though.</i></b>
8
+
9
+ == Compatibility
10
+
11
+ SchemaPlus supports all combinations of:
12
+ * rails 3.0 or 3.1
13
+ * MRI ruby 1.8.7 or 1.9.2
14
+ * Postgres, MySQL (using mysql or mysql2 gem), or Sqlite3
15
+
16
+ == Installation
17
+
18
+ Install from http://rubygems.org via
19
+
20
+ $ gem install "schema_plus"
21
+
22
+ or in a Gemfile
23
+
24
+ gem "schema_plus"
25
+
26
+ == Features
27
+
28
+ Here some examples that show off the high points. For full details see the RDoc documentation.
29
+
30
+ === Indexes
31
+
32
+ With standard rails migrations, you specify indexes separately from the table definition:
33
+
34
+ # Standard Rails approach...
35
+ create_table :parts do |t|
36
+ t.string :name
37
+ t.string :product_code
38
+ end
39
+
40
+ add_index :parts, :name # index repeats table and column names and is defined separately
41
+ add_index :parts, :product_code, :unique => true
42
+
43
+ But with SchemaPlus rather than specify your outside your table definition you can specify your indexes when you define each column:
44
+
45
+ # More DRY way...
46
+ create_table :parts do |t|
47
+ t.string :name, :index => true
48
+ t.string :product_code, :index => :unique
49
+ end
50
+
51
+ Options can be provided index using a hash, for example:
52
+
53
+ t.string :product_code, :index => { :unique => true, :name => "my_index_name" }
54
+
55
+ You can also create multi-column indexes, for example:
56
+
57
+ t.string :first_name
58
+ t.string :last_name, :index => { :with => :first_name }
59
+
60
+ t.string :country_code
61
+ t.string :area_code
62
+ t.string :local_number :index => { :width => [:country_code, :area_code], :unique => true }
63
+
64
+ If you're using Postgresql, SchemaPlus provides support for conditions, expressions, index methods, and case-insensitive indexes; see doc at SchemaPlus::ActiveRecord::ConnectionAdapters::PostgresqlAdapter and SchemaPlus::ActiveRecord::ConnectionAdapters::IndexDefinition
65
+
66
+ And when you query column information using ActiveRecord::Base#columns, SchemaPlus analogously provides index information relevant to each column: which indexes reference the column, whether the column must be unique, etc. See doc at SchemaPlus::ActiveRecord::ConnectionAdapters::Column
67
+
68
+ === Foreign Key Constraints
69
+
70
+ SchemaPlus adds support for foreign key constraints. In fact, for the
71
+ common convention that you name a column with suffix +_id+ to indicate that
72
+ it's a foreign key, SchemaPlus automatically defines the appropriate
73
+ constraint.
74
+
75
+ You can explicitly specify foreign key constraints, or override the
76
+ automatic ones, using the +:references+ option to specify the table
77
+ name (and optionally that table's key column name, if it's not +id+).
78
+
79
+ Here are some examples:
80
+
81
+ t.integer :author_id # automatically references table 'authors', key id
82
+ t.integer :parent_id # special name parent_id automatically references its own table (for tree nodes)
83
+ t.integer :author, :references => :authors # non-conventional column name needs :references for a constraint
84
+ t.integer :author_id, :refences => :authors # same as automatic behavior
85
+ t.integer :author_id, :refences => [:authors, :id] # same as automatic behavior
86
+ t.integer :author_id, :references => :people # override table name
87
+ t.integer :author_id, :references => [:people, :ssn] # override table name and key
88
+ t.integer :author_id, :referencs => nil # don't create a constraint
89
+
90
+
91
+ You can also modify the behavior using +:on_delete+, +:on_update+, and +:deferrable+
92
+
93
+ t.integer :author_id, :on_delete => :cascade
94
+
95
+ The foreign key behavior can be configured globally (see Config) or per-table (see create_table).
96
+
97
+ To examine your foreign key constraints, connection.foreign_keys returns a
98
+ list of foreign key constraints defined for a given table, and
99
+ connection.reverse_foreign_keys returns a list of foreign key constraints
100
+ that reference a given table. See SchemaPlus::ActiveRecord::ConnectionAdapters::ForeignKeyDefinition.
101
+
102
+ === Views
103
+
104
+ SchemaPlus provides support for creating and dropping views. For example:
105
+
106
+ create_view :uncommented_posts, "SELECT * FROM posts LEFT OUTER JOIN comments ON comments.post_id = posts.id WHERE comments.id IS NULL"
107
+ drop_view :uncommented_posts
108
+
109
+ ActiveRecord works with views the same as with ordinary tables. That is, for the above view you can define
110
+
111
+ class UncommentedPosts < ActiveRecord::Base
112
+ end
113
+
114
+
115
+ == History
116
+
117
+ * SchemaPlus is derived from several "Red Hill On Rails" plugins
118
+ originally created by harukizaemon (https://github.com/harukizaemon)
119
+ with later contributions from
120
+ * Michał Łomnicki (https://github.com/mlomnicki)
121
+ * François Beausoleil (https://github.com/francois)
122
+ * Greg Barnett (https://github.com/greg-barnett)
123
+ * Ronen Barzel (https://github.com/ronen)
124
+
125
+ * SchemaPlus was created in 2011 by Michal Lomnicki and Ronen Barzel
126
+
127
+
128
+
129
+ == Testing
130
+
131
+ SchemaPlus is tested using rspec. To run the tests, after you've forked & cloned: Make sure you have Postgresql and MySQL running. Create database user "schema_plus" with permissions for database "schema_plus_unittest". Then:
132
+
133
+ $ cd schema_plus
134
+ $ bundle install
135
+ $ rake postgresql:build_databases
136
+ $ rake mysql:build_databases
137
+ $ rake postgresql:spec # to run postgresql tests only
138
+ $ rake mysql:spec # to run mysql tests only
139
+ $ rake mysql2:spec # to run mysql2 tests only
140
+ $ rake sqlite3:spec # to run sqlite3 tests only
141
+ $ rake spec # run all tests
142
+
143
+ If you're running ruby 1.9.2, code coverage results will be in coverage/index.html -- it should be at 100% coverage.
144
+
145
+ == License
146
+
147
+ This plugin is released under the MIT license.
data/Rakefile ADDED
@@ -0,0 +1,70 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ require 'rspec/core/rake_task'
5
+ %w[postgresql mysql mysql2 sqlite3].each do |adapter|
6
+ namespace adapter do
7
+ RSpec::Core::RakeTask.new(:spec) do |spec|
8
+ spec.rspec_opts = "-Ispec/connections/#{adapter}"
9
+ spec.fail_on_error = false
10
+ end
11
+ end
12
+ end
13
+
14
+ desc 'Run postgresql, mysql2 and sqlite3 tests'
15
+ task :spec do
16
+ %w[postgresql mysql mysql2 sqlite3].each do |adapter|
17
+ Rake::Task["#{adapter}:spec"].invoke
18
+ end
19
+ end
20
+
21
+ task :default => :spec
22
+
23
+ require 'rake/rdoctask'
24
+ Rake::RDocTask.new do |rdoc|
25
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
26
+
27
+ rdoc.rdoc_dir = 'rdoc'
28
+ rdoc.title = "schema_plus #{version}"
29
+ rdoc.rdoc_files.include('README*')
30
+ rdoc.rdoc_files.include('lib/**/*.rb')
31
+ end
32
+
33
+ namespace :postgresql do
34
+ desc 'Build the PostgreSQL test databases'
35
+ task :build_databases do
36
+ %x( createdb -E UTF8 schema_plus_unittest )
37
+ end
38
+
39
+ desc 'Drop the PostgreSQL test databases'
40
+ task :drop_databases do
41
+ %x( dropdb schema_plus_unittest )
42
+ end
43
+
44
+ desc 'Rebuild the PostgreSQL test databases'
45
+ task :rebuild_databases => [:drop_databases, :build_databases]
46
+ end
47
+
48
+ task :build_postgresql_databases => 'postgresql:build_databases'
49
+ task :drop_postgresql_databases => 'postgresql:drop_databases'
50
+ task :rebuild_postgresql_databases => 'postgresql:rebuild_databases'
51
+
52
+ MYSQL_DB_USER = 'schema_plus'
53
+ namespace :mysql do
54
+ desc 'Build the MySQL test databases'
55
+ task :build_databases do
56
+ %x( echo "create DATABASE schema_plus_unittest DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_unicode_ci " | mysql --user=#{MYSQL_DB_USER})
57
+ end
58
+
59
+ desc 'Drop the MySQL test databases'
60
+ task :drop_databases do
61
+ %x( mysqladmin --user=#{MYSQL_DB_USER} -f drop schema_plus_unittest )
62
+ end
63
+
64
+ desc 'Rebuild the MySQL test databases'
65
+ task :rebuild_databases => [:drop_databases, :build_databases]
66
+ end
67
+
68
+ task :build_mysql_databases => 'mysql:build_databases'
69
+ task :drop_mysql_databases => 'mysql:drop_databases'
70
+ task :rebuild_mysql_databases => 'mysql:rebuild_databases'
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'schema_plus' unless defined?(SchemaPlus)
@@ -0,0 +1,211 @@
1
+ require 'ostruct'
2
+
3
+ module SchemaPlus
4
+ module ActiveRecord
5
+ module Associations
6
+
7
+ module Relation #:nodoc:
8
+ def self.included(base)
9
+ base.alias_method_chain :initialize, :schema_plus
10
+ end
11
+
12
+ def initialize_with_schema_plus(klass, *args)
13
+ klass.send :_load_schema_plus_associations
14
+ initialize_without_schema_plus(klass, *args)
15
+ end
16
+ end
17
+
18
+ def self.extended(base) #:nodoc:
19
+ class << base
20
+ alias_method_chain :reflect_on_association, :schema_plus
21
+ alias_method_chain :reflect_on_all_associations, :schema_plus
22
+ end
23
+ ::ActiveRecord::Relation.send :include, Relation
24
+ end
25
+
26
+ def reflect_on_association_with_schema_plus(*args) #:nodoc:
27
+ _load_schema_plus_associations
28
+ reflect_on_association_without_schema_plus(*args)
29
+ end
30
+
31
+ def reflect_on_all_associations_with_schema_plus(*args) #:nodoc:
32
+ _load_schema_plus_associations
33
+ reflect_on_all_associations_without_schema_plus(*args)
34
+ end
35
+
36
+ def define_attribute_methods(*args) #:nodoc:
37
+ super
38
+ _load_schema_plus_associations
39
+ end
40
+
41
+ private
42
+
43
+ def _load_schema_plus_associations #:nodoc:
44
+ return if @schema_plus_associations_loaded
45
+ @schema_plus_associations_loaded = true
46
+ return unless schema_plus_config.associations.auto_create?
47
+
48
+ reverse_foreign_keys.each do | foreign_key |
49
+ if foreign_key.table_name =~ /^#{table_name}_(.*)$/ || foreign_key.table_name =~ /^(.*)_#{table_name}$/
50
+ other_table = $1
51
+ if other_table == other_table.pluralize and connection.columns(foreign_key.table_name).any?{|col| col.name == "#{other_table.singularize}_id"}
52
+ _define_association(:has_and_belongs_to_many, foreign_key, other_table)
53
+ else
54
+ _define_association(:has_one_or_many, foreign_key)
55
+ end
56
+ else
57
+ _define_association(:has_one_or_many, foreign_key)
58
+ end
59
+ end
60
+
61
+ foreign_keys.each do | foreign_key |
62
+ _define_association(:belongs_to, foreign_key)
63
+ end
64
+ end
65
+
66
+ def _define_association(macro, fk, referencing_table_name = nil) #:nodoc:
67
+ return unless fk.column_names.size == 1
68
+
69
+ referencing_table_name ||= fk.table_name
70
+
71
+ column_name = fk.column_names.first
72
+ reference_name = column_name.sub(/_id$/, '')
73
+ references_name = fk.references_table_name.singularize
74
+ referencing_name = referencing_table_name.singularize
75
+
76
+ references_class_name = references_name.classify
77
+ referencing_class_name = referencing_name.classify
78
+
79
+ references_concise = _concise_name(references_name, referencing_name)
80
+ referencing_concise = _concise_name(referencing_name, references_name)
81
+
82
+ case reference_name
83
+ when 'parent'
84
+ belongs_to = 'parent'
85
+ belongs_to_concise = 'parent'
86
+
87
+ has_one = 'child'
88
+ has_one_concise = 'child'
89
+
90
+ has_many = 'children'
91
+ has_many_concise = 'children'
92
+
93
+ when references_name
94
+ belongs_to = references_name
95
+ belongs_to_concise = references_concise
96
+
97
+ has_one = referencing_name
98
+ has_one_concise = referencing_concise
99
+
100
+ has_many = referencing_name.pluralize
101
+ has_many_concise = referencing_concise.pluralize
102
+
103
+ when /(.*)_#{references_name}$/, /(.*)_#{references_concise}$/
104
+ label = $1
105
+ belongs_to = "#{label}_#{references_name}"
106
+ belongs_to_concise = "#{label}_#{references_concise}"
107
+
108
+ has_one = "#{referencing_name}_as_#{label}"
109
+ has_one_concise = "#{referencing_concise}_as_#{label}"
110
+
111
+ has_many = "#{referencing_name.pluralize}_as_#{label}"
112
+ has_many_concise = "#{referencing_concise.pluralize}_as_#{label}"
113
+
114
+ when /^#{references_name}_(.*)$/, /^#{references_concise}_(.*)$/
115
+ label = $1
116
+ belongs_to = "#{references_name}_#{label}"
117
+ belongs_to_concise = "#{references_concise}_#{label}"
118
+
119
+ has_one = "#{referencing_name}_as_#{label}"
120
+ has_one_concise = "#{referencing_concise}_as_#{label}"
121
+
122
+ has_many = "#{referencing_name.pluralize}_as_#{label}"
123
+ has_many_concise = "#{referencing_concise.pluralize}_as_#{label}"
124
+
125
+ else
126
+ belongs_to = reference_name
127
+ belongs_to_concise = reference_name
128
+
129
+ has_one = "#{referencing_name}_as_#{reference_name}"
130
+ has_one_concise = "#{referencing_concise}_as_#{reference_name}"
131
+
132
+ has_many = "#{referencing_name.pluralize}_as_#{reference_name}"
133
+ has_many_concise = "#{referencing_concise.pluralize}_as_#{reference_name}"
134
+ end
135
+
136
+ case macro
137
+ when :has_and_belongs_to_many
138
+ name = has_many
139
+ name_concise = has_many_concise
140
+ opts = {:class_name => referencing_class_name, :join_table => fk.table_name, :foreign_key => column_name}
141
+ when :belongs_to
142
+ name = belongs_to
143
+ name_concise = belongs_to_concise
144
+ opts = {:class_name => references_class_name, :foreign_key => column_name}
145
+ when :has_one_or_many
146
+ opts = {:class_name => referencing_class_name, :foreign_key => column_name}
147
+ # use connection.indexes and connection.colums rather than class
148
+ # methods of the referencing class because using the class
149
+ # methods would require getting the class -- which might trigger
150
+ # an autoload which could start some recursion making things much
151
+ # harder to debug.
152
+ if connection.indexes(referencing_table_name, "#{referencing_table_name} Indexes").any?{|index| index.unique && index.columns == [column_name]}
153
+ macro = :has_one
154
+ name = has_one
155
+ name_concise = has_one_concise
156
+ else
157
+ macro = :has_many
158
+ name = has_many
159
+ name_concise = has_many_concise
160
+ if connection.columns(referencing_table_name, "#{referencing_table_name} Columns").any?{ |col| col.name == 'position' }
161
+ opts[:order] = :position
162
+ end
163
+ end
164
+ end
165
+ name = name_concise if _use_concise_name?
166
+ name = name.to_sym
167
+ if (_filter_association(macro, name) && !_method_exists?(name))
168
+ logger.info "SchemaPlus associations: #{self.name || self.table_name.classify}.#{macro} #{name.inspect}, #{opts.inspect[1...-1]}"
169
+ send macro, name, opts.dup
170
+ end
171
+ end
172
+
173
+ def _concise_name(string, other) #:nodoc:
174
+ case
175
+ when string =~ /^#{other}_(.*)$/ then $1
176
+ when string =~ /(.*)_#{other}$/ then $1
177
+ when leader = _common_leader(string,other) then string[leader.length, string.length-leader.length]
178
+ else string
179
+ end
180
+ end
181
+
182
+ def _common_leader(string, other) #:nodoc:
183
+ leader = nil
184
+ other.split('_').each do |part|
185
+ test = "#{leader}#{part}_"
186
+ break unless string.start_with? test
187
+ leader = test
188
+ end
189
+ return leader
190
+ end
191
+
192
+ def _use_concise_name? #:nodoc:
193
+ schema_plus_config.associations.concise_names?
194
+ end
195
+
196
+ def _filter_association(macro, name) #:nodoc:
197
+ config = schema_plus_config.associations
198
+ return false if config.only and not Array.wrap(config.only).include?(name)
199
+ return false if config.except and Array.wrap(config.except).include?(name)
200
+ return false if config.only_type and not Array.wrap(config.only_type).include?(macro)
201
+ return false if config.except_type and Array.wrap(config.except_type).include?(macro)
202
+ return true
203
+ end
204
+
205
+ def _method_exists?(name) #:nodoc:
206
+ method_defined?(name) || private_method_defined?(name) and not (name == :type && [Object, Kernel].include?(instance_method(:type).owner))
207
+ end
208
+
209
+ end
210
+ end
211
+ end