empty_eye 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/CHANGELOG.md ADDED
@@ -0,0 +1,43 @@
1
+ # 0.4.0 / 2012-03-11 / Grady Griffin
2
+
3
+ * added tests for validation and configurations
4
+ * reorganized files
5
+ * added more comments
6
+
7
+
8
+ # 0.3.1 / 2012-03-11 / Grady Griffin
9
+
10
+ * Cleaned up some sti problems
11
+ * added more comments
12
+ * removed some unnecessary codez
13
+
14
+ # 0.3.0 / 2012-03-11 / Grady Griffin
15
+
16
+ * inherits validations as well
17
+ * MTI to MTI is working
18
+ * STI to STI is working
19
+ * MTI to STI to MTI is working
20
+ * bulletproofed shard system by giving it its own association classes
21
+
22
+ # 0.2.1 / 2012-03-09 / Grady Griffin
23
+
24
+ * added some reasonable defaults for shard associations
25
+ * already had CRU added D for CRUD
26
+ * updated logic to support polymorphic associations
27
+ * added crud tests
28
+
29
+ # 0.2.0 / 2012-03-09 / Grady Griffin
30
+
31
+ * revamped entire library to use associations as table shards
32
+ * general cleanup
33
+ * added more options for associations
34
+
35
+
36
+ # 0.1.0 / 2012-03-08 / Grady Griffin
37
+
38
+ * can make a simple MTI class
39
+ * modified schema dumper to omit views
40
+
41
+ # 0.0.1 / 2012-03-07 / Grady Griffin
42
+
43
+ * initial commit
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in empty_eye.gemspec
4
+ gemspec
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2012 Grady Griffin
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,109 @@
1
+ # Empty Eye
2
+
3
+ ActiveRecord based MTI gem powered by database views
4
+
5
+ #Issues
6
+
7
+ * No known issues major issues; has been successful within data structures of high complexity (MTI to MTI, MTI to STI to MTI relationships)
8
+ * Not sure why but new mti instances have a id of zero; this has caused no problems so far however.
9
+ * No mechanism to change mti class table name but that is minor
10
+ * More complex testing needed to ensure reliability
11
+ * Uses ARel so should be compatible with ARel supported database that support view; only tested with MySQL
12
+ * SchemaDumper support for omitting views with databases other than MySQL is untested
13
+
14
+ Create MTI classes by renaming your base table with the core suffix and wrapping your associations in a mti\_class block
15
+
16
+ Test example from http://techspry.com/ruby_and_rails/multiple-table-inheritance-in-rails-3/ which uses mixins to accomplish MTI:
17
+
18
+ ActiveRecord::Migration.create_table :restaurants_core, :force => true do |t|
19
+ t.boolean :kids_area
20
+ t.boolean :wifi
21
+ t.integer :food_genre
22
+ t.datetime :created_at
23
+ t.datetime :updated_at
24
+ t.datetime :deleted_at
25
+ end
26
+
27
+ ActiveRecord::Migration.create_table :bars_core, :force => true do |t|
28
+ t.string :music_genre
29
+ t.string :best_nights
30
+ t.string :dress_code
31
+ t.datetime :created_at
32
+ t.datetime :updated_at
33
+ t.datetime :deleted_at
34
+ end
35
+
36
+ ActiveRecord::Migration.create_table :businesses, :force => true do |t|
37
+ t.integer :biz_id
38
+ t.string :biz_type
39
+ t.string :name
40
+ t.string :address
41
+ t.string :phone
42
+ end
43
+
44
+ class Business < ActiveRecord::Base
45
+ belongs_to :biz, :polymorphic => true
46
+ end
47
+
48
+ class Restaurant < ActiveRecord::Base
49
+ mti_class do
50
+ has_one :business, :as => :biz
51
+ end
52
+ end
53
+
54
+ class Bar < ActiveRecord::Base
55
+ mti_class(:bars_core) do
56
+ has_one :business, :as => :biz
57
+ end
58
+ end
59
+
60
+ For now the convention is to name the base tables with the suffix core as the view will use the rails table name
61
+
62
+ In the background the following association options are used :autosave => true, :validate => true, :dependent => :destroy
63
+
64
+ MTI associations take the only and except options to limit the inherited columns.
65
+
66
+ class SmallMechanic < ActiveRecord::Base
67
+ mti_class :mechanics_core do |t|
68
+ has_one :garage, :foreign_key => :mechanic_id, :except => 'specialty'
69
+ end
70
+ end
71
+
72
+ class TinyMechanic < ActiveRecord::Base
73
+ mti_class :mechanics_core do |t|
74
+ has_one :garage, :foreign_key => :mechanic_id, :only => 'specialty'
75
+ end
76
+ end
77
+
78
+ Validations are also inherited but only for validations for attributes/columns that are inherited
79
+
80
+ Changing or adding these options will have no effect but the MTI would be senseless without them
81
+
82
+ If the class does not descend active record the correct table will be used.
83
+
84
+ If you dont want to use the core suffix convention a table can be specified (see Bar class mti implementation)
85
+
86
+
87
+ 1.9.3p0 :005 > Bar
88
+ => Bar(id: integer, music_genre: string, best_nights: string, dress_code: string, created_at: datetime, updated_at: datetime, deleted_at: datetime, name: string, address: string, phone: string)
89
+
90
+ 1.9.3p0 :006 > bar = Bar.create(:music_genre => "Latin", :best_nights => "Tuesdays", :dress_code => "casual", :address => "1904 Easy Kaley Orlando, FL 32806", :name => 'Chicos', :phone => '123456789')
91
+ => #<Bar id: 2, music_genre: "Latin", best_nights: "Tuesdays", dress_code: "casual", created_at: "2012-03-09 18:41:17", updated_at: "2012-03-09 18:41:17", deleted_at: nil, name: "Chicos", address: "1904 Easy Kaley Orlando, FL 32806", phone: "123456789">
92
+
93
+ 1.9.3p0 :008 > bar.phone = '987654321'
94
+ => "987654321"
95
+ 1.9.3p0 :009 > bar.save
96
+ => true
97
+
98
+ 1.9.3p0 :010 > bar.reload
99
+ => #<Bar id: 2, music_genre: "Latin", best_nights: "Tuesdays", dress_code: "casual", created_at: "2012-03-09 18:41:17", updated_at: "2012-03-09 18:41:17", deleted_at: nil, name: "Chicos", address: "1904 Easy Kaley Orlando, FL 32806", phone: "987654321">
100
+
101
+ 1.9.3p0 :011 > bar.destroy
102
+ => #<Bar id: 2, music_genre: "Latin", best_nights: "Tuesdays", dress_code: "casual", created_at: "2012-03-09 18:41:17", updated_at: "2012-03-09 18:41:17", deleted_at: nil, name: "Chicos", address: "1904 Easy Kaley Orlando, FL 32806", phone: "987654321">
103
+
104
+ 1.9.3p0 :013 > Bar.find_by_id(2)
105
+ => nil
106
+
107
+
108
+
109
+
data/Rakefile ADDED
@@ -0,0 +1,17 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new('spec')
5
+
6
+ # If you want to make this the default task
7
+ task :default => :spec
8
+
9
+ desc "Open an irb session preloaded with this library"
10
+ task :console do
11
+ sh "irb -rubygems -I lib -r empty_eye.rb"
12
+ end
13
+
14
+ task :test_console do
15
+ sh "irb -rubygems -I lib -r empty_eye.rb -I spec -r spec_helper.rb "
16
+ end
17
+
data/empty_eye.gemspec ADDED
@@ -0,0 +1,32 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "empty_eye/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "empty_eye"
7
+ s.version = EmptyEye::VERSION::STRING
8
+ s.authors = ["thegboat"]
9
+ s.email = ["gradygriffin@gmail.com"]
10
+ s.homepage = ""
11
+ s.summary = %q{Active Record MTI gem}
12
+ s.description = %q{Active Record MTI gem powered by database views}
13
+
14
+ s.rubyforge_project = "empty_eye"
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+
21
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
22
+ s.add_runtime_dependency('activerecord', '>= 2.3.0')
23
+ s.add_runtime_dependency('arel', '>= 3.0.0')
24
+ s.add_development_dependency("rspec")
25
+ s.add_development_dependency("mysql2")
26
+ else
27
+ s.add_dependency('activerecord', '>= 2.3.0')
28
+ s.add_dependency('arel', '>= 3.0.0')
29
+ s.add_development_dependency("rspec")
30
+ s.add_development_dependency("mysql2")
31
+ end
32
+ end
@@ -0,0 +1,133 @@
1
+ module ActiveRecord
2
+ class Base
3
+
4
+ class << self
5
+
6
+ #am i an mti class? easier than making a new class type ... i tried
7
+ def mti_class?
8
+ extended_with.any?
9
+ end
10
+
11
+ #interface for building mti_class
12
+ #primary table is not necessary if the table named correctly (Bar => bars_core)
13
+ #OR if the class inherits a primary table
14
+ #simply wrap your greasy association in this block
15
+ def mti_class(primary_table = nil)
16
+ self.primary_key = "id"
17
+ raise(EmptyEye::AlreadyExtended, "MTI class method already invoked") if mti_class?
18
+ set_mti_primary_table(primary_table)
19
+ self.table_name = compute_view_name
20
+ extended_with.primary_table(mti_primary_table)
21
+ before_yield = reflect_on_multiple_associations(:has_one)
22
+ yield nil if block_given?
23
+ mti_associations = reflect_on_multiple_associations(:has_one) - before_yield
24
+ extend_mti_class(mti_associations)
25
+ true
26
+ end
27
+
28
+ #all data for mti class is stored here
29
+ #when empty it is not so MT-I
30
+ def extended_with
31
+ @extended_with ||= EmptyEye::ViewExtensionCollection.new(self)
32
+ end
33
+
34
+ #the class of primary shard
35
+ def mti_primary_shard
36
+ extended_with.primary.shard
37
+ end
38
+
39
+ #we dont need no freakin' finder
40
+ #the view handles this
41
+ def finder_needs_type_condition?
42
+ !mti_class? and super
43
+ end
44
+
45
+ private
46
+
47
+ #we know the associations and we know what they can do
48
+ #we will make a mti class accordingly here
49
+ def extend_mti_class(mti_associations)
50
+ mti_associations.each do |assoc|
51
+ extended_with.association(assoc)
52
+ end
53
+ create_view
54
+ reset_column_information
55
+ inherit_mti_validations
56
+ end
57
+
58
+ #we need a name for the view
59
+ #need to have a way to set this
60
+ def compute_view_name
61
+ descends_from_active_record? ? compute_table_name : name.underscore.pluralize
62
+ end
63
+
64
+ #determine the primary table
65
+ #first detrmine if our view name exists; this will need to chage one day
66
+ #if they didnt specify try using the core convention else the superclass
67
+ #if they specified use what the set
68
+ def set_mti_primary_table(primary_table_name)
69
+ @mti_primary_table = if ordinary_table_exists?
70
+ raise(EmptyEye::ViewNameError, "MTI view cannot be created because a table named '#{compute_view_name}' already exists")
71
+ elsif primary_table_name.nil?
72
+ descends_from_active_record? ? "#{compute_table_name}_core" : superclass.table_name
73
+ else
74
+ primary_table_name
75
+ end
76
+ end
77
+
78
+ def mti_primary_table
79
+ @mti_primary_table
80
+ end
81
+
82
+ #we need this when we add new associaton types to extend with
83
+ #we could use the baked in version for now
84
+ def reflect_on_multiple_associations(*assoc_types)
85
+ assoc_types.collect do |assoc_type|
86
+ reflect_on_all_associations
87
+ end.flatten.uniq
88
+ end
89
+
90
+ #determine if what we want to name our view already exists
91
+ def ordinary_table_exists?
92
+ connection.tables_without_views.include?(compute_view_name)
93
+ end
94
+
95
+ #drop the view; dont check if we can just rescue any errors
96
+ #create the view
97
+ def create_view
98
+ connection.execute("DROP VIEW #{table_name}") rescue nil
99
+ connection.execute(extended_with.create_view_sql)
100
+ end
101
+
102
+ #we may need to inherit these... not using for now
103
+ def superclass_extensions
104
+ superclass.extended_with.dup.descend(self) unless descends_from_active_record?
105
+ end
106
+
107
+ #we know how to rebuild the validations from the shards
108
+ #lets call our inherited validation here
109
+ def inherit_mti_validations
110
+ extended_with.validations.each {|args| send(*args)}
111
+ end
112
+ end
113
+
114
+ private
115
+
116
+ #a pseudo association method back to instances primary shard
117
+ def mti_primary_shard
118
+ @mti_primary_shard ||= if new_record?
119
+ self.class.mti_primary_shard.new(:mti_instance => self)
120
+ else
121
+ rtn = self.class.mti_primary_shard.find_by_id(id)
122
+ rtn.mti_instance = self
123
+ rtn
124
+ end
125
+ end
126
+
127
+ #is the instance an instance of mti_class?
128
+ def mti_class?
129
+ self.class.mti_class?
130
+ end
131
+
132
+ end
133
+ end
@@ -0,0 +1,46 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ class AbstractAdapter
4
+ #we need this logic to correctly build schema file
5
+ def tables_without_views
6
+ #trying to make compatible with rails 2.3.x; failing
7
+ if respond_to?('execute_and_free')
8
+ execute_and_free(tables_without_views_sql) do |result|
9
+ result.collect { |field| field.first }
10
+ end
11
+ else
12
+ result = execute(tables_without_views_sql)
13
+ rtn = result.collect { |field| field.first }
14
+ result.free rescue nil
15
+ rtn
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ #i coulda made this more OO but this seemed to handle more unknown speed bumps
22
+ #perusing rails_sql_views helped a bit
23
+ # https://github.com/aeden/rails_sql_views
24
+ def tables_without_views_sql
25
+ case self.to_s
26
+ when /^mysql/i
27
+ "SHOW FULL TABLES WHERE table_type = 'BASE TABLE'"
28
+ when /postgresql/i
29
+ %{SELECT tablename
30
+ FROM pg_tables
31
+ WHERE schemaname = ANY (current_schemas(false)) AND table_type = 'BASE TABLE'}
32
+ when /sqlite/i
33
+ %{SELECT name
34
+ FROM sqlite_master
35
+ WHERE type = 'table' AND NOT name = 'sqlite_sequence'}
36
+ when /oracle/i
37
+ "SELECT TABLE_NAME FROM USER_TABLES"
38
+ when /sqlserver/i
39
+ "SELECT table_name FROM information_schema.tables"
40
+ else
41
+ "SHOW FULL TABLES WHERE table_type = 'BASE TABLE'"
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,18 @@
1
+ module ActiveRecord
2
+ class SchemaDumper
3
+ #we dont want views in our schema file
4
+ def tables(stream)
5
+ @connection.tables_without_views.sort.each do |tbl|
6
+ next if ['schema_migrations', ignore_tables].flatten.any? do |ignored|
7
+ case ignored
8
+ when String; tbl == ignored
9
+ when Regexp; tbl =~ ignored
10
+ else
11
+ raise StandardError, 'ActiveRecord::SchemaDumper.ignore_tables accepts an array of String and / or Regexp values.'
12
+ end
13
+ end
14
+ table(tbl, stream)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,20 @@
1
+ module EmptyEye
2
+ module Associations
3
+ module Builder
4
+ class ShardHasOne < ActiveRecord::Associations::Builder::HasOne
5
+ #special association builder for shard
6
+ #very verbose but will be easier to update later
7
+ #better than monkey patching
8
+ #this builder allows the other special shard association-ish classes to be created
9
+ #the ground floor ...
10
+
11
+ def build
12
+ reflection = super
13
+ configure_dependency unless options[:through]
14
+ reflection
15
+ end
16
+
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,79 @@
1
+ module EmptyEye
2
+ module Associations
3
+ class ShardAssociationScope < ActiveRecord::Associations::AssociationScope
4
+ #special association scope for shard
5
+ #very verbose but will be easier to update later
6
+ #better than monkey patching
7
+ #here we are patching the need to set the polymorphic type for a association lookup
8
+ #the shard is the 'owner' but we want the type to be the master class
9
+
10
+
11
+ def add_constraints(scope)
12
+ tables = construct_tables
13
+
14
+ chain.each_with_index do |reflection, i|
15
+ table, foreign_table = tables.shift, tables.first
16
+
17
+ if reflection.source_macro == :has_and_belongs_to_many
18
+ join_table = tables.shift
19
+
20
+ scope = scope.joins(join(
21
+ join_table,
22
+ table[reflection.association_primary_key].
23
+ eq(join_table[reflection.association_foreign_key])
24
+ ))
25
+
26
+ table, foreign_table = join_table, tables.first
27
+ end
28
+
29
+ if reflection.source_macro == :belongs_to
30
+ if reflection.options[:polymorphic]
31
+ key = reflection.association_primary_key(klass)
32
+ else
33
+ key = reflection.association_primary_key
34
+ end
35
+
36
+ foreign_key = reflection.foreign_key
37
+ else
38
+ key = reflection.foreign_key
39
+ foreign_key = reflection.active_record_primary_key
40
+ end
41
+
42
+ conditions = self.conditions[i]
43
+
44
+ if reflection == chain.last
45
+ scope = scope.where(table[key].eq(owner[foreign_key]))
46
+
47
+ if reflection.type
48
+ scope = scope.where(table[reflection.type].eq(owner.mti_master_class.base_class.name))
49
+ end
50
+
51
+ conditions.each do |condition|
52
+ if options[:through] && condition.is_a?(Hash)
53
+ condition = { table.name => condition }
54
+ end
55
+
56
+ scope = scope.where(interpolate(condition))
57
+ end
58
+ else
59
+ constraint = table[key].eq(foreign_table[foreign_key])
60
+
61
+ if reflection.type
62
+ type = chain[i + 1].klass.base_class.name
63
+ constraint = constraint.and(table[reflection.type].eq(type))
64
+ end
65
+
66
+ scope = scope.joins(join(foreign_table, constraint))
67
+
68
+ unless conditions.empty?
69
+ scope = scope.where(sanitize(conditions, table))
70
+ end
71
+ end
72
+ end
73
+
74
+ scope
75
+ end
76
+
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,35 @@
1
+ module EmptyEye
2
+ module Associations
3
+ class ShardHasOneAssociation < ActiveRecord::Associations::HasOneAssociation
4
+ #special association for shard
5
+ #very verbose but will be easier to update later
6
+ #better than monkey patching
7
+ #here we are patching the need to set the polymorphic type for a association
8
+ #the shard is the 'owner' but we want the type to be the master class
9
+
10
+ def association_scope
11
+ if klass
12
+ @association_scope ||= ShardAssociationScope.new(self).scope
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ def creation_attributes
19
+ attributes = {}
20
+
21
+ if reflection.macro.in?([:has_one, :has_many]) && !options[:through]
22
+ attributes[reflection.foreign_key] = owner[reflection.active_record_primary_key]
23
+
24
+ if reflection.options[:as]
25
+ attributes[reflection.type] = owner.mti_master_class.base_class.name
26
+ end
27
+ end
28
+
29
+ attributes
30
+ end
31
+
32
+ end
33
+ end
34
+ end
35
+
@@ -0,0 +1,5 @@
1
+ module EmptyEye
2
+ class AlreadyExtended < StandardError; end
3
+ class ViewNameError < StandardError; end
4
+ class InvalidUpdate < StandardError; end
5
+ end
@@ -0,0 +1,50 @@
1
+ module EmptyEye
2
+ module Persistence
3
+ extend ActiveSupport::Concern
4
+
5
+ #if it is not a mti_class do what you do
6
+ #else let the primary shard do the saving
7
+ def update(attribute_names = @attributes.keys)
8
+ return super unless mti_class?
9
+ mti_primary_shard.cascade_save
10
+ 1
11
+ end
12
+
13
+ #if it is not a mti_class do what you do
14
+ #else let the primary shard do the saving
15
+ #come back and cleanup
16
+ def create
17
+ return super unless mti_class?
18
+ mti_primary_shard.cascade_save
19
+ ActiveRecord::IdentityMap.add(self) if ActiveRecord::IdentityMap.enabled?
20
+ @new_record = false
21
+ self.id
22
+ end
23
+
24
+ #if it is not a mti_class do what you do
25
+ #else let the primary shard do the destruction
26
+ #come back and cleanup
27
+ def destroy
28
+ return super unless mti_class?
29
+ mti_primary_shard.destroy
30
+ if ActiveRecord::IdentityMap.enabled? and persisted?
31
+ ActiveRecord::IdentityMap.remove(self)
32
+ end
33
+ @destroyed = true
34
+ freeze
35
+ end
36
+
37
+ #if it is not a mti_class do what you do
38
+ #else let the primary shard do the deletion
39
+ #come back and cleanup
40
+ def delete
41
+ return super unless mti_class?
42
+ self.class.delete_all(:id => id)
43
+ if ActiveRecord::IdentityMap.enabled? and persisted?
44
+ ActiveRecord::IdentityMap.remove(self)
45
+ end
46
+ @destroyed = true
47
+ freeze
48
+ end
49
+ end
50
+ end