schema_plus 0.1.0.pre1

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