empty_eye 0.4.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.
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