diff --git a/citier.gemspec b/citier.gemspec index 32915e6..a776065 100644 --- a/citier.gemspec +++ b/citier.gemspec @@ -38,8 +38,8 @@ Gem::Specification.new do |s| s.rubyforge_project = %q{citier} s.rubygems_version = %q{1.3.7} s.summary = s.description - - s.add_dependency('rails_sql_views') #needs the 'rails_sql_views', :git => 'git://github.com/morgz/rails_sql_views.git' fork. Set this in your apps bundle + + s.add_dependency('rails_sql_views') #needs the 'rails_sql_views', :git => 'https://github.com/ryanlitalien/rails_sql_views.git' fork. Set this in your apps bundle if s.respond_to? :specification_version then current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION diff --git a/lib/citier.rb b/lib/citier.rb index 6f0a0b3..81d3704 100644 --- a/lib/citier.rb +++ b/lib/citier.rb @@ -20,11 +20,8 @@ def citier_debug(s) # Methods that will be used for the instances of the Non Root Classes require 'citier/child_instance_methods' -# Require SQL Adapters -require 'citier/sql_adapters' - #Require acts_as_citier hook require 'citier/acts_as_citier' # Methods that override ActiveRecord::Relation -require 'citier/relation_methods' \ No newline at end of file +require 'citier/relation_methods' diff --git a/lib/citier/child_instance_methods.rb b/lib/citier/child_instance_methods.rb index be772c5..42b4ae4 100644 --- a/lib/citier/child_instance_methods.rb +++ b/lib/citier/child_instance_methods.rb @@ -3,21 +3,23 @@ module ChildInstanceMethods def save(options={}) return false if (options[:validate] != false && !self.valid?) - + #citier_debug("Callback (#{self.inspect})") citier_debug("SAVING #{self.class.to_s}") - + #AIT NOTE: Will change any protected values back to original values so any models onwards won't see changes. # Run save and create/update callbacks, just like ActiveRecord does self.run_callbacks(:save) do self.run_callbacks(self.new_record? ? :create : :update) do #get the attributes of the class which are inherited from it's parent. - attributes_for_parent = self.attributes.reject { |key,value| !self.class.superclass.column_names.include?(key) } - changed_attributes_for_parent = self.changed_attributes.reject { |key,value| !self.class.superclass.column_names.include?(key) } + attributes = self.instance_variable_get(:@attributes) + changed_attributes = self.instance_variable_get(:@changed_attributes) + attributes_for_parent = attributes.reject { |key,value| !self.class.superclass.column_names.include?(key) } + changed_attributes_for_parent = changed_attributes.reject { |key,value| !self.class.superclass.column_names.include?(key) } # Get the attributes of the class which are unique to this class and not inherited. - attributes_for_current = self.attributes.reject { |key,value| self.class.superclass.column_names.include?(key) } - changed_attributes_for_current = self.changed_attributes.reject { |key,value| self.class.superclass.column_names.include?(key) } + attributes_for_current = attributes.reject { |key,value| self.class.superclass.column_names.include?(key) } + changed_attributes_for_current = changed_attributes.reject { |key,value| self.class.superclass.column_names.include?(key) } citier_debug("Attributes for #{self.class.superclass.to_s}: #{attributes_for_parent.inspect}") citier_debug("Changed attributes for #{self.class.superclass.to_s}: #{changed_attributes_for_parent.keys.inspect}") @@ -27,71 +29,75 @@ def save(options={}) ######## # # Parent saving - + #create a new instance of the superclass, passing the inherited attributes. parent = self.class.superclass.new - + parent.force_attributes(attributes_for_parent, :merge => true) changed_attributes_for_parent["id"] = 0 # We need to change at least something to force a timestamps update. parent.force_changed_attributes(changed_attributes_for_parent) - + parent.id = self.id if id parent.type = self.type - + parent.is_new_record(new_record?) - - # If we're root (AR subclass) this will just be saved as normal through AR. If we're a child it will call this method again. + + # If we're root (AR subclass) this will just be saved as normal through AR. If we're a child it will call this method again. # It will try and save it's parent and then save itself through the Writable constant. - parent_saved = parent.save - self.id = parent.id - - if !parent_saved - # Couldn't save parent class - citier_debug("Class (#{self.class.superclass.to_s}) could not be saved") - citier_debug("Errors = #{parent.errors.to_s}") - return false # Return false and exit run_callbacks :save and :create/:update, so the after_ callback won't run. - end - - #End of parent saving - - ###### - ## - ## Self Saving - ## - - # If there are attributes for the current class (unique & not inherited), save current model - if !attributes_for_current.empty? - current = self.class::Writable.new - - current.force_attributes(attributes_for_current, :merge => true) - current.force_changed_attributes(changed_attributes_for_current) - - current.id = self.id - current.is_new_record(new_record?) - - current_saved = current.save - - current.after_save_change_request if current.respond_to?('after_save_change_request') #Specific to an app I'm building - - if !current_saved + parent.transaction do + parent_saved = parent.save + self.id = parent.id + self.created_at ||= parent.created_at # should set the generated timestamps to view object + self.updated_at ||= parent.updated_at + + if !parent_saved + # Couldn't save parent class citier_debug("Class (#{self.class.superclass.to_s}) could not be saved") - citier_debug("Errors = #{current.errors.to_s}") - return false # Return false and exit run_callbacks :save and :create/:update, so the after callback won't run. + citier_debug("Errors = #{parent.errors.to_s}") + return false # Return false and exit run_callbacks :save and :create/:update, so the after_ callback won't run. + end + + #End of parent saving + + ###### + ## + ## Self Saving + ## + + # If there are attributes for the current class (unique & not inherited), save current model + if !attributes_for_current.empty? + current = self.class::Writable.new + + current.force_attributes(attributes_for_current, :merge => true) + current.force_changed_attributes(changed_attributes_for_current) + + current.id = self.id + current.is_new_record(new_record?) + + current_saved = current.save + + current.after_save_change_request if current.respond_to?('after_save_change_request') #Specific to an app I'm building + + if !current_saved + citier_debug("Class (#{self.class.superclass.to_s}) could not be saved") + citier_debug("Errors = #{current.errors.to_s}") + return false # Return false and exit run_callbacks :save and :create/:update, so the after callback won't run. + end end - end - # at this point, parent_saved && current_saved - - is_new_record(false) # This is no longer a new record + # at this point, parent_saved && current_saved + + is_new_record(false) # This is no longer a new record + + self.force_changed_attributes({}) # Reset changed_attributes so future changes will be tracked correctly - self.force_changed_attributes({}) # Reset changed_attributes so future changes will be tracked correctly - - # No return, because we want the after callback to run. + # No return, because we want the after callback to run. + end end end return true end - + def save!(options={}) raise ActiveRecord::RecordInvalid.new(self) if (options[:validate] != false && !self.valid?) self.save || raise(ActiveRecord::RecordNotSaved) @@ -99,4 +105,4 @@ def save!(options={}) include InstanceMethods end -end \ No newline at end of file +end diff --git a/lib/citier/core_ext.rb b/lib/citier/core_ext.rb index 4eb17f1..0c1f7a5 100644 --- a/lib/citier/core_ext.rb +++ b/lib/citier/core_ext.rb @@ -30,6 +30,13 @@ def self.create_class_writable(class_reference) #creation of a new class which # set the name of the table associated to this class # this class will be associated to the writable table of the class_reference class self.table_name = t_name + + class_attribute :view_class, :writable_serialized_attributes + self.view_class = class_reference + + def self.serialized_attributes + self.writable_serialized_attributes ||= view_class.serialized_attributes.reject { |key, value| view_class.superclass.column_names.include?(key) } + end end end end @@ -51,7 +58,7 @@ def create_citier_view(theclass) #function for creating views for migrations self_read_table = theclass.table_name self_write_table = theclass::Writable.table_name parent_read_table = theclass.superclass.table_name - select_sql = "SELECT #{parent_read_table}.id, #{columns.map { |c| "\"#{c}\"" }.join(',')} FROM #{parent_read_table}, #{self_write_table} WHERE #{parent_read_table}.id = #{self_write_table}.id" + select_sql = "SELECT #{parent_read_table}.id, #{columns.map { |c| theclass.connection.quote_column_name(c) }.join(',')} FROM #{parent_read_table}, #{self_write_table} WHERE #{parent_read_table}.id = #{self_write_table}.id" sql = "CREATE VIEW #{self_read_table} AS #{select_sql}" #Use our rails_sql_views gem to create the view so we get it outputted to schema diff --git a/lib/citier/relation_methods.rb b/lib/citier/relation_methods.rb index 2e4919a..1ca0441 100644 --- a/lib/citier/relation_methods.rb +++ b/lib/citier/relation_methods.rb @@ -1,96 +1,108 @@ -module ActiveRecord - class Relation - - alias_method :relation_delete_all, :delete_all - def delete_all(conditions = nil) - return relation_delete_all(conditions) if !@klass.acts_as_citier? - - return relation_delete_all(conditions) if conditions - - deleted = true - ids = nil - c = @klass - - bind_values.each do |bind_value| - if bind_value[0].name == "id" - ids = bind_value[1] - break +module Citier + module ActsAsCitier + module Relation + extend ActiveSupport::Concern + + included do + class_eval do + alias_method_chain :delete_all, :citier + alias_method_chain :to_a, :citier + alias_method_chain :apply_finder_options, :citier end end - ids ||= where_values_hash["id"] || where_values_hash[:id] - where_hash = ids ? { :id => ids } : nil - - deleted &= c.base_class.where(where_hash).relation_delete_all - while c.superclass != ActiveRecord::Base - if c.const_defined?(:Writable) - citier_debug("Deleting back up hierarchy #{c}") - deleted &= c::Writable.where(where_hash).delete_all + + def delete_all_with_citier(conditions = nil) + return delete_all_without_citier(conditions) if !@klass.acts_as_citier? + return delete_all_without_citier(conditions) if conditions + + deleted = true + ids = nil + c = @klass + + bind_values.each do |bind_value| + if bind_value[0].name == "id" + ids = bind_value[1] + break + end end - c = c.superclass - end - - deleted - end - - alias_method :relation_to_a, :to_a - def to_a - return relation_to_a if !@klass.acts_as_citier? - - records = relation_to_a - - c = @klass - - if records.all? { |record| record.class == c } - return records - end - - full_records = [] - ids_wanted = {} - - # Map all the ids wanted per type - records.each do |record| - if record.class == c # We don't need to find the record again if this is already the correct one - full_records << record - next + ids ||= where_values_hash["id"] || where_values_hash[:id] + if ids.nil? + arel.projections = [Arel::SqlLiteral.new("#{c.table_name}.id")] + ids = c.find_by_sql(arel, bind_values).map(&:id) end - - ids_wanted[record.class] ||= [] - ids_wanted[record.class] << record.id - end - - # Find all wanted records - ids_wanted.each do |type_class, ids| - full_records.push(*type_class.find(ids)) - end - - # Make a new array with the found records at the right places - records.each do |record| - full_record = full_records.find { |full_record| full_record.id == record.id } - record.force_attributes(full_record.instance_variable_get(:@attributes), :merge => true, :clear_caches => false) + where_hash = { :id => ids } + + deleted &= c.base_class.where(where_hash).delete_all_without_citier + while c.superclass != ActiveRecord::Base + if c.const_defined?(:Writable) + citier_debug("Deleting back up hierarchy #{c}") + deleted &= c::Writable.where(where_hash).delete_all_without_citier + end + c = c.superclass + end + + deleted end - - return records - end - - alias_method :relation_apply_finder_options, :apply_finder_options - def apply_finder_options(options) - return relation_apply_finder_options(options) if !@klass.acts_as_citier? - - relation = self - - # With option :no_children set to true, only records of type self will be returned. - # So Root.all(:no_children => true) won't return Child records. - no_children = options.delete(:no_children) - if no_children - relation = clone + + def to_a_with_citier + records = to_a_without_citier + + return records if !@klass.acts_as_citier? c = @klass - - self_type = c.superclass == ActiveRecord::Base ? nil : c.name - relation = relation.where(:type => self_type) + + if records.all? { |record| record.class == c } + return records + end + + full_records = [] + ids_wanted = {} + + # Map all the ids wanted per type + records.each do |record| + if record.class != c # We don't need to find the record again if this is already the correct one + ids_wanted[record.class] ||= [] + ids_wanted[record.class] << record.id + end + end + + # Find all wanted records + ids_wanted.each do |type_class, ids| + full_records.push(*type_class.find(ids)) + end + + # Make a new array with the found records at the right places + records.each do |record| + if record.class != c + full_record = full_records.find { |full_record| full_record.id == record.id } + record.force_attributes(full_record.instance_variable_get(:@attributes), :merge => true, :clear_caches => false) + end + end + + return records + end + + def apply_finder_options_with_citier(options) + return apply_finder_options_without_citier(options) if !@klass.acts_as_citier? + + relation = self + + # With option :no_children set to true, only records of type self will be returned. + # So Root.all(:no_children => true) won't return Child records. + no_children = options.delete(:no_children) + if no_children + relation = clone + + c = @klass + + self_type = c.superclass == ActiveRecord::Base ? nil : c.name + relation = relation.where(:type => self_type) + end + + relation.apply_finder_options_without_citier(options) end - - relation.relation_apply_finder_options(options) end end -end \ No newline at end of file +end + +ActiveRecord::Relation.send(:include, Citier::ActsAsCitier::Relation) diff --git a/lib/citier/sql_adapters.rb b/lib/citier/sql_adapters.rb deleted file mode 100644 index ed305c3..0000000 --- a/lib/citier/sql_adapters.rb +++ /dev/null @@ -1,249 +0,0 @@ -#------------------------------------------------------------------------------------------------# -# # -# Modifications for SQL Adapters : needed to take views into account # -# (only SQLite, PostGreSQL & MySQL have been considered) # -# # -#------------------------------------------------------------------------------------------------# - -unless defined?(JRUBY_VERSION) - require 'active_record' - -# SQLite - begin - #require 'active_record/connection_adapters/sqlite_adapter' - require 'active_record/connection_adapters/sqlite3_adapter' - - module ActiveRecord - module ConnectionAdapters - class SQLiteAdapter < AbstractAdapter - - def tables(name = 'SCHEMA', table_name = nil) - sql = <<-SQL - SELECT name - FROM sqlite_master - WHERE (type = 'table' or type='view') AND NOT name = 'sqlite_sequence' - SQL - # Modification : the where clause was intially WHERE type = 'table' AND NOT name = 'sqlite_sequence' - # now it is WHERE (type = 'table' or type='view') AND NOT name = 'sqlite_sequence' - # this modification is made to consider tables AND VIEWS as tables - sql << " AND name = #{quote_table_name(table_name)}" if table_name - - execute(sql, name).map do |row| - row['name'] - end - end - end - end - end - rescue Gem::LoadError - # not installed - end - - # PostGreSQL - begin - require 'active_record/connection_adapters/postgresql_adapter' - - module ActiveRecord - module ConnectionAdapters - class PostgreSQLAdapter < AbstractAdapter - def tables(name = nil) - a=tablesL(name) - b=viewsL(name) - if(b!=[]) - a=a+b - end - return a - end - - def tablesL(name = nil) - - query(<<-SQL, name).map { |row| row[0] } - SELECT tablename - FROM pg_tables - WHERE schemaname = ANY (current_schemas(false)) - SQL - end - def viewsL(name = nil) - - query(<<-SQL, name).map { |row| row[0] } - SELECT viewname - FROM pg_views - WHERE schemaname = ANY (current_schemas(false)) - SQL - end - - def table_exists?(name) - a=table_existsB?(name) - b=views_existsB?(name) - return a||b - end - - - def table_existsB?(name) - name = name.to_s - schema, table = name.split('.', 2) - - unless table # A table was provided without a schema - table = schema - schema = nil - end - - if name =~ /^"/ # Handle quoted table names - table = name - schema = nil - end - - query(<<-SQL).first[0].to_i > 0 - SELECT COUNT(*) - FROM pg_tables - WHERE tablename = '#{table.gsub(/(^"|"$)/,'')}' - #{schema ? "AND schemaname = '#{schema}'" : ''} - SQL - - end - def views_existsB?(name) - name = name.to_s - schema, table = name.split('.', 2) - - unless table # A table was provided without a schema - table = schema - schema = nil - end - - if name =~ /^"/ # Handle quoted table names - table = name - schema = nil - end - - query(<<-SQL).first[0].to_i > 0 - SELECT COUNT(*) - FROM pg_views - WHERE viewname = '#{table.gsub(/(^"|"$)/,'')}' - #{schema ? "AND schemaname = '#{schema}'" : ''} - SQL - - end - end - end - end - rescue Gem::LoadError - # not installed - end - - # MySQL - # No Modification needed, this essentially comes from the fact that MySQL "show" command - # lists tables & views simultaneously -end - - -if defined?(JRUBY_VERSION) -# SQLite - begin - #require 'active_record/connection_adapters/sqlite_adapter' - require 'active_record/connection_adapters/sqlite3_adapter' - - module ActiveRecord - module ConnectionAdapters - class SQLiteAdapter < JdbcAdapter - - def tables(name = 'SCHEMA', table_name = nil) - sql = <<-SQL - SELECT name - FROM sqlite_master - WHERE (type = 'table' or type='view') AND NOT name = 'sqlite_sequence' - SQL - # Modification : the where clause was intially WHERE type = 'table' AND NOT name = 'sqlite_sequence' - # now it is WHERE (type = 'table' or type='view') AND NOT name = 'sqlite_sequence' - # this modification is made to consider tables AND VIEWS as tables - sql << " AND name = #{quote_table_name(table_name)}" if table_name - - execute(sql, name).map do |row| - row['name'] - end - end - end - end - end - rescue Gem::LoadError - # not installed - end - - # PostGreSQL - begin - require 'active_record/connection_adapters/postgresql_adapter' - - module ActiveRecord - module ConnectionAdapters - class PostgreSQLAdapter < JdbcAdapter - def tables(name = nil) - a=tablesL(name) - b=viewsL(name) - if(b!=[]) - a=a+b - end - return a - end - - def tablesL(name = nil) - - exec_query(<<-SQL, name).map { |row| row[0] } - SELECT tablename - FROM pg_tables - WHERE schemaname = ANY (current_schemas(false)) - SQL - end - def viewsL(name = nil) - - exec_query(<<-SQL, name).map { |row| row[0] } - SELECT viewname - FROM pg_views - WHERE schemaname = ANY (current_schemas(false)) - SQL - end - - def table_exists?(name) - a=table_existsB?(name) - b=views_existsB?(name) - return a||b - end - - def table_existsB?(name) - schema, table = extract_schema_and_table(name.to_s) - return false unless table # Abstract classes is having nil table name - - binds = [[nil, table.gsub(/(^"|"$)/,'')]] - binds << [nil, schema] if schema - - exec_query(<<-SQL, 'SCHEMA', binds).first["table_count"] > 0 - SELECT COUNT(*) as table_count - FROM pg_tables - WHERE tablename = ? - AND schemaname = #{schema ? "?" : "ANY (current_schemas(false))"} - SQL - end - - def views_existsB?(name) - schema, table = extract_schema_and_table(name.to_s) - return false unless table # Abstract classes is having nil table name - - binds = [[nil, table.gsub(/(^"|"$)/,'')]] - binds << [nil, schema] if schema - - exec_query(<<-SQL, 'SCHEMA', binds).first["view_count"] > 0 - SELECT COUNT(*) as view_count - FROM pg_views - WHERE viewname = ? - AND schemaname = #{schema ? "?" : "ANY (current_schemas(false))"} - SQL - end - end - end - end - rescue Gem::LoadError - # not installed - end - - # MySQL - # No Modification needed, this essentially comes from the fact that MySQL "show" command - # lists tables & views simultaneously -end \ No newline at end of file