# frozen-string-literal: true module Sequel class Model extend Enumerable extend Inflections # Class methods for Sequel::Model that implement basic model functionality. # # * All of the following methods have class methods created that send the method # to the model's dataset: all, as_hash, avg, count, cross_join, distinct, each, # each_server, empty?, except, exclude, exclude_having, fetch_rows, # filter, first, first!, for_update, from, from_self, full_join, full_outer_join, # get, graph, grep, group, group_and_count, group_append, group_by, having, import, # inner_join, insert, intersect, invert, join, join_table, last, left_join, # left_outer_join, limit, lock_style, map, max, min, multi_insert, naked, natural_full_join, # natural_join, natural_left_join, natural_right_join, offset, order, order_append, order_by, # order_more, order_prepend, paged_each, qualify, reverse, reverse_order, right_join, # right_outer_join, select, select_all, select_append, select_group, select_hash, # select_hash_groups, select_map, select_more, select_order_map, server, # single_record, single_record!, single_value, single_value!, sum, to_hash, to_hash_groups, # truncate, unfiltered, ungraphed, ungrouped, union, unlimited, unordered, where, where_all, # where_each, where_single_value, with, with_recursive, with_sql module ClassMethods # Whether to cache the anonymous models created by Sequel::Model(), true by default. This is # required for reloading them correctly (avoiding the superclass mismatch). attr_accessor :cache_anonymous_models # Array of modules that extend this model's dataset. Stored # so that if the model's dataset is changed, it will be extended # with all of these modules. attr_reader :dataset_method_modules # The Module subclass to use for dataset_module blocks. attr_reader :dataset_module_class # The default options to use for Model#set_fields. These are merged with # the options given to set_fields. attr_accessor :default_set_fields_options # SQL string fragment used for faster DELETE statement creation when deleting/destroying # model instances, or nil if the optimization should not be used. For internal use only. attr_reader :fast_instance_delete_sql # SQL string fragment used for faster lookups by primary key, or nil if the optimization # should not be used. For internal use only. attr_reader :fast_pk_lookup_sql # The dataset that instance datasets (#this) are based on. Generally a naked version of # the model's dataset limited to one row. For internal use only. attr_reader :instance_dataset # Array of plugin modules loaded by this class # # Sequel::Model.plugins # # => [Sequel::Model, Sequel::Model::Associations] attr_reader :plugins # The primary key for the class. Sequel can determine this automatically for # many databases, but not all, so you may need to set it manually. If not # determined automatically, the default is :id. attr_reader :primary_key # Whether to raise an error instead of returning nil on a failure # to save/create/save_changes/update/destroy due to a validation failure or # a before_* hook returning false (default: true). attr_accessor :raise_on_save_failure # Whether to raise an error when unable to typecast data for a column # (default: false). This should be set to true if you want to have model # setter methods raise errors if the argument cannot be typecast properly. attr_accessor :raise_on_typecast_failure # Whether to raise an error if an UPDATE or DELETE query related to # a model instance does not modify exactly 1 row. If set to false, # Sequel will not check the number of rows modified (default: true). attr_accessor :require_modification # If true (the default), requires that all models have valid tables, # raising exceptions if creating a model without a valid table backing it. # Setting this to false will allow the creation of model classes where the # underlying table doesn't exist. attr_accessor :require_valid_table # Should be the literal primary key column name if this Model's table has a simple primary key, or # nil if the model has a compound primary key or no primary key. attr_reader :simple_pk # Should be the literal table name if this Model's dataset is a simple table (no select, order, join, etc.), # or nil otherwise. This and simple_pk are used for an optimization in Model.[]. attr_reader :simple_table # Whether mass assigning via .create/.new/#set/#update should raise an error # if an invalid key is used. A key is invalid if no setter method exists # for that key or the access to the setter method is restricted (e.g. due to it # being a primary key field). If set to false, silently skip # any key where the setter method doesn't exist or access to it is restricted. attr_accessor :strict_param_setting # Whether to typecast the empty string ('') to nil for columns that # are not string or blob. In most cases the empty string would be the # way to specify a NULL SQL value in string form (nil.to_s == ''), # and an empty string would not usually be typecast correctly for other # types, so the default is true. attr_accessor :typecast_empty_string_to_nil # Whether to typecast attribute values on assignment (default: true). # If set to false, no typecasting is done, so it will be left up to the # database to typecast the value correctly. attr_accessor :typecast_on_assignment # Whether to use a transaction by default when saving/deleting records (default: true). # If you are sending database queries in before_* or after_* hooks, you shouldn't change # the default setting without a good reason. attr_accessor :use_transactions # Define a Model method on the given module that calls the Model # method on the receiver. This is how the Sequel::Model() method is # defined, and allows you to define Model() methods on other modules, # making it easier to have custom model settings for all models under # a namespace. Example: # # module Foo # Model = Class.new(Sequel::Model) # Model.def_Model(self) # DB = Model.db = Sequel.connect(ENV['FOO_DATABASE_URL']) # Model.plugin :prepared_statements # # class Bar < Model # # Uses Foo::DB[:bars] # end # # class Baz < Model(:my_baz) # # Uses Foo::DB[:my_baz] # end # end def def_Model(mod) model = self mod.define_singleton_method(:Model) do |source| model.Model(source) end end # Lets you create a Model subclass with its dataset already set. # +source+ should be an instance of one of the following classes: # # Database :: Sets the database for this model to +source+. # Generally only useful when subclassing directly # from the returned class, where the name of the # subclass sets the table name (which is combined # with the +Database+ in +source+ to create the # dataset to use) # Dataset :: Sets the dataset for this model to +source+. # other :: Sets the table name for this model to +source+. The # class will use the default database for model # classes in order to create the dataset. # # The purpose of this method is to set the dataset/database automatically # for a model class, if the table name doesn't match the default table # name that Sequel would use. # # When creating subclasses of Sequel::Model itself, this method is usually # called on Sequel itself, using Sequel::Model(:something). # # # Using a symbol # class Comment < Sequel::Model(:something) # table_name # => :something # end # # # Using a dataset # class Comment < Sequel::Model(DB1[:something]) # dataset # => DB1[:something] # end # # # Using a database # class Comment < Sequel::Model(DB1) # dataset # => DB1[:comments] # end def Model(source) if cache_anonymous_models cache = Sequel.synchronize{@Model_cache ||= {}} if klass = Sequel.synchronize{cache[source]} return klass end end klass = Class.new(self) if source.is_a?(::Sequel::Database) klass.db = source else klass.set_dataset(source) end if cache_anonymous_models Sequel.synchronize{cache[source] = klass} end klass end # Returns the first record from the database matching the conditions. # If a hash is given, it is used as the conditions. If another # object is given, it finds the first record whose primary key(s) match # the given argument(s). If no object is returned by the dataset, returns nil. # # Artist[1] # SELECT * FROM artists WHERE id = 1 # # => #1, ...}> # # Artist[name: 'Bob'] # SELECT * FROM artists WHERE (name = 'Bob') LIMIT 1 # # => #'Bob', ...}> def [](*args) args = args.first if args.size <= 1 args.is_a?(Hash) ? first(args) : (primary_key_lookup(args) unless args.nil?) end # Initializes a model instance as an existing record. This constructor is # used by Sequel to initialize model instances when fetching records. # Requires that values be a hash where all keys are symbols. It # probably should not be used by external code. def call(values) o = allocate o.instance_variable_set(:@values, values) o end # Clear the setter_methods cache def clear_setter_methods_cache @setter_methods = nil unless frozen? end # Returns the columns in the result set in their original order. # Generally, this will use the columns determined via the database # schema, but in certain cases (e.g. models that are based on a joined # dataset) it will use Dataset#columns to find the columns. # # Artist.columns # # => [:id, :name] def columns return @columns if @columns return nil if frozen? set_columns(dataset.naked.columns) end # Creates instance using new with the given values and block, and saves it. # # Artist.create(name: 'Bob') # # INSERT INTO artists (name) VALUES ('Bob') # # Artist.create do |a| # a.name = 'Jim' # end # INSERT INTO artists (name) VALUES ('Jim') def create(values = OPTS, &block) new(values, &block).save end # Returns the dataset associated with the Model class. Raises # an +Error+ if there is no associated dataset for this class. # In most cases, you don't need to call this directly, as Model # proxies many dataset methods to the underlying dataset. # # Artist.dataset.all # SELECT * FROM artists def dataset @dataset || raise(Error, "No dataset associated with #{self}") end # Alias of set_dataset def dataset=(ds) set_dataset(ds) end # Extend the dataset with a module, similar to adding # a plugin with the methods defined in DatasetMethods. # This is the recommended way to add methods to model datasets. # # If given an argument, it should be a module, and is used to extend # the underlying dataset. Otherwise an anonymous module is created, and # if a block is given, it is module_evaled, allowing you do define # dataset methods directly using the standard ruby def syntax. # Returns the module given or the anonymous module created. # # # Usage with existing module # Album.dataset_module Sequel::ColumnsIntrospection # # # Usage with anonymous module # Album.dataset_module do # def foo # :bar # end # end # Album.dataset.foo # # => :bar # Album.foo # # => :bar # # Any anonymous modules created are actually instances of Sequel::Model::DatasetModule # (a Module subclass), which allows you to call the subset method on them, which # defines a dataset method that adds a filter. There are also a number of other # methods with the same names as the dataset methods, which can use to define # named dataset methods: # # Album.dataset_module do # where(:released, Sequel[:release_date] <= Sequel::CURRENT_DATE) # order :by_release_date, :release_date # select :for_select_options, :id, :name, :release_date # end # Album.released.sql # # => "SELECT * FROM artists WHERE (release_date <= CURRENT_DATE)" # Album.by_release_date.sql # # => "SELECT * FROM artists ORDER BY release_date" # Album.for_select_options.sql # # => "SELECT id, name, release_date FROM artists" # Album.released.by_release_date.for_select_options.sql # # => "SELECT id, name, release_date FROM artists WHERE (release_date <= CURRENT_DATE) ORDER BY release_date" # # The following methods are supported: distinct, eager, exclude, exclude_having, grep, group, group_and_count, # group_append, having, limit, offset, order, order_append, order_prepend, select, select_all, # select_append, select_group, where, and server. # # The advantage of using these DatasetModule methods to define your dataset # methods is that they can take advantage of dataset caching to improve # performance. # # Any public methods in the dataset module will have class methods created that # call the method on the dataset, assuming that the class method is not already # defined. def dataset_module(mod = nil) if mod raise Error, "can't provide both argument and block to Model.dataset_module" if block_given? dataset_extend(mod) mod else @dataset_module ||= dataset_module_class.new(self) @dataset_module.module_eval(&Proc.new) if block_given? dataset_extend(@dataset_module) @dataset_module end end # Returns the database associated with the Model class. # If this model doesn't have a database associated with it, # assumes the superclass's database, or the first object in # Sequel::DATABASES. If no Sequel::Database object has # been created, raises an error. # # Artist.db.transaction do # BEGIN # Artist.create(name: 'Bob') # # INSERT INTO artists (name) VALUES ('Bob') # end # COMMIT def db return @db if @db @db = self == Model ? Sequel.synchronize{DATABASES.first} : superclass.db raise(Error, "No database associated with #{self}: have you called Sequel.connect or #{self}.db= ?") unless @db @db end # Sets the database associated with the Model class. # Should only be used if the Model class currently does not # have a dataset defined. # # This can be used directly on Sequel::Model to set the default database to be used # by subclasses, or to override the database used for specific models: # # Sequel::Model.db = DB1 # Artist = Class.new(Sequel::Model) # Artist.db = DB2 # # Note that you should not use this to change the model's database # at runtime. If you have that need, you should look into Sequel's # sharding support, or consider using separate model classes per Database. def db=(db) raise Error, "Cannot use Sequel::Model.db= on model with existing dataset. Use Sequel::Model.dataset= instead." if @dataset @db = db end # Returns the cached schema information if available or gets it # from the database. This is a hash where keys are column symbols # and values are hashes of information related to the column. See # Database#schema. # # Artist.db_schema # # {:id=>{:type=>:integer, :primary_key=>true, ...}, # # :name=>{:type=>:string, :primary_key=>false, ...}} def db_schema return @db_schema if @db_schema return nil if frozen? @db_schema = get_db_schema end # Create a column alias, where the column methods have one name, but the underlying storage uses a # different name. def def_column_alias(meth, column) clear_setter_methods_cache overridable_methods_module.module_eval do define_method(meth){self[column]} define_method("#{meth}="){|v| self[column] = v} end end # Finds a single record according to the supplied filter. # You are encouraged to use Model.[] or Model.first instead of this method. # # Artist.find(name: 'Bob') # # SELECT * FROM artists WHERE (name = 'Bob') LIMIT 1 # # Artist.find{name > 'M'} # # SELECT * FROM artists WHERE (name > 'M') LIMIT 1 def find(*args, &block) first(*args, &block) end # Like +find+ but invokes create with given conditions when record does not # exist. Unlike +find+ in that the block used in this method is not passed # to +find+, but instead is passed to +create+ only if +find+ does not # return an object. # # Artist.find_or_create(name: 'Bob') # # SELECT * FROM artists WHERE (name = 'Bob') LIMIT 1 # # INSERT INTO artists (name) VALUES ('Bob') # # Artist.find_or_create(name: 'Jim'){|a| a.hometown = 'Sactown'} # # SELECT * FROM artists WHERE (name = 'Jim') LIMIT 1 # # INSERT INTO artists (name, hometown) VALUES ('Jim', 'Sactown') def find_or_create(cond, &block) find(cond) || create(cond, &block) end # Freeze a model class, disallowing any further changes to it. def freeze return self if frozen? dataset_module.freeze overridable_methods_module.freeze if @dataset db_schema.freeze.each_value(&:freeze) columns.freeze setter_methods.freeze else @setter_methods = [].freeze end @dataset_method_modules.freeze @default_set_fields_options.freeze @plugins.freeze super end # Whether the model has a dataset. True for most model classes, # but can be false if the model class is an abstract model class # designed for subclassing, such as Sequel::Model itself. def has_dataset? !@dataset.nil? end # Clear the setter_methods cache when a module is included, as it # may contain setter methods. def include(*mods) clear_setter_methods_cache super end # If possible, set the dataset for the model subclass as soon as it # is created. Also, make sure the inherited class instance variables # are copied into the subclass. # # Sequel queries the database to get schema information as soon as # a model class is created: # # class Artist < Sequel::Model # Causes schema query # end def inherited(subclass) super ivs = subclass.instance_variables inherited_instance_variables.each do |iv, dup| next if ivs.include?(iv) if (sup_class_value = instance_variable_get(iv)) && dup sup_class_value = case dup when :dup sup_class_value.dup when :hash_dup h = {} sup_class_value.each{|k,v| h[k] = v.dup} h when Proc dup.call(sup_class_value) else raise Error, "bad inherited instance variable type: #{dup.inspect}" end end subclass.instance_variable_set(iv, sup_class_value) end unless ivs.include?(:@dataset) if @dataset && self != Model subclass.set_dataset(@dataset.clone, :inherited=>true) elsif (n = subclass.name) && !n.to_s.empty? db subclass.set_dataset(subclass.implicit_table_name) end end end # Returns the implicit table name for the model class, which is the demodulized, # underscored, pluralized name of the class. # # Artist.implicit_table_name # => :artists # Foo::ArtistAlias.implicit_table_name # => :artist_aliases def implicit_table_name pluralize(underscore(demodulize(name))).to_sym end # Calls #call with the values hash. def load(values) call(values) end # Clear the setter_methods cache when a setter method is added. def method_added(meth) clear_setter_methods_cache if meth.to_s.end_with?('=') super end # Mark the model as not having a primary key. Not having a primary key # can cause issues, among which is that you won't be able to update records. # # Artist.primary_key # => :id # Artist.no_primary_key # Artist.primary_key # => nil def no_primary_key clear_setter_methods_cache self.simple_pk = @primary_key = nil end # Loads a plugin for use with the model class, passing optional arguments # to the plugin. If the plugin is a module, load it directly. Otherwise, # require the plugin from sequel/plugins/#{plugin} and then attempt to load # the module using a the camelized plugin name under Sequel::Plugins. def plugin(plugin, *args, &block) m = plugin.is_a?(Module) ? plugin : plugin_module(plugin) unless @plugins.include?(m) @plugins << m m.apply(self, *args, &block) if m.respond_to?(:apply) extend(m::ClassMethods) if m.const_defined?(:ClassMethods, false) include(m::InstanceMethods) if m.const_defined?(:InstanceMethods, false) if m.const_defined?(:DatasetMethods, false) dataset_extend(m::DatasetMethods, :create_class_methods=>false) end end m.configure(self, *args, &block) if m.respond_to?(:configure) end # Returns primary key attribute hash. If using a composite primary key # value such be an array with values for each primary key in the correct # order. For a standard primary key, value should be an object with a # compatible type for the key. If the model does not have a primary key, # raises an +Error+. # # Artist.primary_key_hash(1) # => {:id=>1} # Artist.primary_key_hash([1, 2]) # => {:id1=>1, :id2=>2} def primary_key_hash(value) case key = @primary_key when Symbol {key => value} when Array hash = {} key.zip(Array(value)){|k,v| hash[k] = v} hash else raise(Error, "#{self} does not have a primary key") end end # Return a hash where the keys are qualified column references. Uses the given # qualifier if provided, or the table_name otherwise. This is useful if you # plan to join other tables to this table and you want the column references # to be qualified. # # Artist.where(Artist.qualified_primary_key_hash(1)) # # SELECT * FROM artists WHERE (artists.id = 1) def qualified_primary_key_hash(value, qualifier=table_name) case key = @primary_key when Symbol {SQL::QualifiedIdentifier.new(qualifier, key) => value} when Array hash = {} key.zip(Array(value)){|k,v| hash[SQL::QualifiedIdentifier.new(qualifier, k)] = v} hash else raise(Error, "#{self} does not have a primary key") end end # Restrict the setting of the primary key(s) when using mass assignment (e.g. +set+). Because # this is the default, this only make sense to use in a subclass where the # parent class has used +unrestrict_primary_key+. def restrict_primary_key clear_setter_methods_cache @restrict_primary_key = true end # Whether or not setting the primary key(s) when using mass assignment (e.g. +set+) is # restricted, true by default. def restrict_primary_key? @restrict_primary_key end # Sets the dataset associated with the Model class. +ds+ can be a +Symbol+, # +LiteralString+, SQL::Identifier, SQL::QualifiedIdentifier, # SQL::AliasedExpression # (all specifying a table name in the current database), or a +Dataset+. # If a dataset is used, the model's database is changed to the database of the given # dataset. If a dataset is not used, a dataset is created from the current # database with the table name given. Other arguments raise an +Error+. # Returns self. # # It also attempts to determine the database schema for the model, # based on the given dataset. # # Note that you should not use this to change the model's dataset # at runtime. If you have that need, you should look into Sequel's # sharding support, or creating a separate Model class per dataset # # You should avoid calling this method directly if possible. Instead you should # set the table name or dataset when creating the model class: # # # table name # class Artist < Sequel::Model(:tbl_artists) # end # # # dataset # class Artist < Sequel::Model(DB[:tbl_artists]) # end def set_dataset(ds, opts=OPTS) inherited = opts[:inherited] @dataset = convert_input_dataset(ds) @require_modification = @dataset.provides_accurate_rows_matched? if require_modification.nil? if inherited self.simple_table = superclass.simple_table @columns = superclass.instance_variable_get(:@columns) @db_schema = superclass.instance_variable_get(:@db_schema) else @dataset = @dataset.with_extend(*@dataset_method_modules.reverse) if @dataset_method_modules @db_schema = get_db_schema end reset_instance_dataset self end # Sets the primary key for this model. You can use either a regular # or a composite primary key. To not use a primary key, set to nil # or use +no_primary_key+. On most adapters, Sequel can automatically # determine the primary key to use, so this method is not needed often. # # class Person < Sequel::Model # # regular key # set_primary_key :person_id # end # # class Tagging < Sequel::Model # # composite key # set_primary_key [:taggable_id, :tag_id] # end def set_primary_key(key) clear_setter_methods_cache if key.is_a?(Array) if key.length < 2 key = key.first else key = key.dup.freeze end end self.simple_pk = if key && !key.is_a?(Array) (@dataset || db).literal(key).freeze end @primary_key = key end # Cache of setter methods to allow by default, in order to speed up mass assignment. def setter_methods return @setter_methods if @setter_methods @setter_methods = get_setter_methods end # Returns name of primary table for the dataset. If the table for the dataset # is aliased, returns the aliased name. # # Artist.table_name # => :artists # Sequel::Model(:foo).table_name # => :foo # Sequel::Model(Sequel[:foo].as(:bar)).table_name # => :bar def table_name dataset.first_source_alias end # Allow the setting of the primary key(s) when using the mass assignment methods. # Using this method can open up security issues, be very careful before using it. # # Artist.set(id: 1) # Error # Artist.unrestrict_primary_key # Artist.set(id: 1) # No Error def unrestrict_primary_key clear_setter_methods_cache @restrict_primary_key = false end # Return the model instance with the primary key, or nil if there is no matching record. def with_pk(pk) primary_key_lookup(pk) end # Return the model instance with the primary key, or raise NoMatchingRow if there is no matching record. def with_pk!(pk) with_pk(pk) || raise(NoMatchingRow.new(dataset)) end # Add model methods that call dataset methods Plugins.def_dataset_methods(self, (Dataset::ACTION_METHODS + Dataset::QUERY_METHODS + [:each_server]) - [:<<, :or, :[], :columns, :columns!, :delete, :update, :set_graph_aliases, :add_graph_aliases]) private # Yield to the passed block and if do_raise is false, swallow all errors other than DatabaseConnectionErrors. def check_non_connection_error(do_raise=require_valid_table) begin db.transaction(:savepoint=>:only){yield} rescue Sequel::DatabaseConnectionError raise rescue Sequel::Error raise if do_raise end end # Convert the given object to a Dataset that should be used as # this model's dataset. def convert_input_dataset(ds) case ds when Symbol, SQL::Identifier, SQL::QualifiedIdentifier, SQL::AliasedExpression, LiteralString self.simple_table = db.literal(ds).freeze ds = db.from(ds) when Dataset ds = ds.from_self(:alias=>ds.first_source) if ds.joined_dataset? self.simple_table = if ds.send(:simple_select_all?) ds.literal(ds.first_source_table).freeze end @db = ds.db else raise(Error, "Model.set_dataset takes one of the following classes as an argument: Symbol, LiteralString, SQL::Identifier, SQL::QualifiedIdentifier, SQL::AliasedExpression, Dataset") end set_dataset_row_proc(ds.clone(:model=>self)) end # Add the module to the class's dataset_method_modules. Extend the dataset with the # module if the model has a dataset. Add dataset methods to the class for all # public dataset methods. def dataset_extend(mod, opts=OPTS) @dataset = @dataset.with_extend(mod) if @dataset reset_instance_dataset dataset_method_modules << mod unless opts[:create_class_methods] == false mod.public_instance_methods.each{|meth| def_model_dataset_method(meth)} end end # Create a column accessor for a column with a method name that is hard to use in ruby code. def def_bad_column_accessor(column) im = instance_methods overridable_methods_module.module_eval do meth = :"#{column}=" define_method(column){self[column]} unless im.include?(column) define_method(meth){|v| self[column] = v} unless im.include?(meth) end end # Create the column accessors. For columns that can be used as method names directly in ruby code, # use a string to define the method for speed. For other columns names, use a block. def def_column_accessor(*columns) clear_setter_methods_cache columns, bad_columns = columns.partition{|x| /\A[A-Za-z_][A-Za-z0-9_]*\z/.match(x.to_s)} bad_columns.each{|x| def_bad_column_accessor(x)} im = instance_methods columns.each do |column| meth = :"#{column}=" overridable_methods_module.module_eval("def #{column}; self[:#{column}] end", __FILE__, __LINE__) unless im.include?(column) overridable_methods_module.module_eval("def #{meth}(v); self[:#{column}] = v end", __FILE__, __LINE__) unless im.include?(meth) end end # Define a model method that calls the dataset method with the same name, # only used for methods with names that can't be represented directly in # ruby code. def def_model_dataset_method(meth) return if respond_to?(meth, true) if meth.to_s =~ /\A[A-Za-z_][A-Za-z0-9_]*\z/ instance_eval("def #{meth}(*args, &block); dataset.#{meth}(*args, &block) end", __FILE__, __LINE__) else define_singleton_method(meth){|*args, &block| dataset.public_send(meth, *args, &block)} end end # Get the schema from the database, fall back on checking the columns # via the database if that will return inaccurate results or if # it raises an error. def get_db_schema(reload = reload_db_schema?) set_columns(nil) return nil unless @dataset schema_hash = {} ds_opts = dataset.opts get_columns = proc{check_non_connection_error{columns} || []} schema_array = check_non_connection_error(false){db.schema(dataset, :reload=>reload)} if db.supports_schema_parsing? if schema_array schema_array.each{|k,v| schema_hash[k] = v} # Set the primary key(s) based on the schema information, # if the schema information includes primary key information if schema_array.all?{|k,v| v.has_key?(:primary_key)} pks = schema_array.map{|k,v| k if v[:primary_key]}.compact pks.length > 0 ? set_primary_key(pks) : no_primary_key end if (select = ds_opts[:select]) && !(select.length == 1 && select.first.is_a?(SQL::ColumnAll)) # We don't remove the columns from the schema_hash, # as it's possible they will be used for typecasting # even if they are not selected. cols = get_columns.call cols.each{|c| schema_hash[c] ||= {}} def_column_accessor(*schema_hash.keys) else # Dataset is for a single table with all columns, # so set the columns based on the order they were # returned by the schema. cols = schema_array.map{|k,v| k} set_columns(cols) # Also set the columns for the dataset, so the dataset # doesn't have to do a query to get them. dataset.send(:columns=, cols) end else # If the dataset uses multiple tables or custom sql or getting # the schema raised an error, just get the columns and # create an empty schema hash for it. get_columns.call.each{|c| schema_hash[c] = {}} end schema_hash end # Uncached version of setter_methods, to be overridden by plugins # that want to modify the methods used. def get_setter_methods meths = instance_methods.map(&:to_s).select{|l| l.end_with?('=')} - RESTRICTED_SETTER_METHODS meths -= Array(primary_key).map{|x| "#{x}="} if primary_key && restrict_primary_key? meths end # A hash of instance variables to automatically set up in subclasses. # Keys are instance variable symbols, values should be: # nil :: Assign directly from superclass to subclass (frozen objects) # :dup :: Dup object when assigning from superclass to subclass (mutable objects) # :hash_dup :: Assign hash with same keys, but dup all the values # Proc :: Call with subclass to do the assignment def inherited_instance_variables { :@cache_anonymous_models=>nil, :@dataset_method_modules=>:dup, :@dataset_module_class=>nil, :@db=>nil, :@default_set_fields_options=>:dup, :@fast_instance_delete_sql=>nil, :@fast_pk_lookup_sql=>nil, :@plugins=>:dup, :@primary_key=>nil, :@raise_on_save_failure=>nil, :@raise_on_typecast_failure=>nil, :@require_modification=>nil, :@require_valid_table=>nil, :@restrict_primary_key=>nil, :@setter_methods=>nil, :@simple_pk=>nil, :@simple_table=>nil, :@strict_param_setting=>nil, :@typecast_empty_string_to_nil=>nil, :@typecast_on_assignment=>nil, :@use_transactions=>nil } end # For the given opts hash and default name or :class option, add a # :class_name option unless already present which contains the name # of the class to use as a string. The purpose is to allow late # binding to the class later using constantize. def late_binding_class_option(opts, default) case opts[:class] when String, Symbol # Delete :class to allow late binding class_name = opts.delete(:class).to_s if (namespace = opts[:class_namespace]) && !class_name.start_with?('::') class_name = "::#{namespace}::#{class_name}" end opts[:class_name] ||= class_name when Class opts[:class_name] ||= opts[:class].name end opts[:class_name] ||= '::' + ((name || '').split("::")[0..-2] + [camelize(default)]).join('::') end # Module that the class includes that holds methods the class adds for column accessors and # associations so that the methods can be overridden with +super+. def overridable_methods_module include(@overridable_methods_module = Module.new) unless @overridable_methods_module @overridable_methods_module end # Returns the module for the specified plugin. If the module is not # defined, the corresponding plugin required. def plugin_module(plugin) module_name = plugin.to_s.gsub(/(^|_)(.)/){|x| x[-1..-1].upcase} unless Sequel::Plugins.const_defined?(module_name, false) require "sequel/plugins/#{plugin}" end Sequel::Plugins.const_get(module_name) end # Find the row in the dataset that matches the primary key. Uses # a static SQL optimization if the table and primary key are simple. # # This method should not be called with a nil primary key, in case # it is overridden by plugins which assume that the passed argument # is valid. def primary_key_lookup(pk) if sql = @fast_pk_lookup_sql sql = sql.dup ds = dataset ds.literal_append(sql, pk) ds.fetch_rows(sql){|r| return ds.row_proc.call(r)} nil else dataset.first(primary_key_hash(pk)) end end # Whether to reload the database schema by default, ignoring any cached value. def reload_db_schema? false end # Reset the cached fast primary lookup SQL if a simple table and primary key # are used, or set it to nil if not used. def reset_fast_pk_lookup_sql @fast_pk_lookup_sql = if @simple_table && @simple_pk "SELECT * FROM #{@simple_table} WHERE #{@simple_pk} = ".freeze end @fast_instance_delete_sql = if @simple_table && @simple_pk "DELETE FROM #{@simple_table} WHERE #{@simple_pk} = ".freeze end end # Reset the instance dataset to a modified copy of the current dataset, # should be used whenever the model's dataset is modified. def reset_instance_dataset @instance_dataset = @dataset.limit(1).naked.skip_limit_check if @dataset end # Set the columns for this model and create accessor methods for each column. def set_columns(new_columns) @columns = new_columns def_column_accessor(*new_columns) if new_columns @columns end # Set the dataset's row_proc to the current model. def set_dataset_row_proc(ds) ds.with_row_proc(self) end # Reset the fast primary key lookup SQL when the simple_pk value changes. def simple_pk=(pk) @simple_pk = pk reset_fast_pk_lookup_sql end # Reset the fast primary key lookup SQL when the simple_table value changes. def simple_table=(t) @simple_table = t reset_fast_pk_lookup_sql end # Returns a copy of the model's dataset with custom SQL # # Artist.fetch("SELECT * FROM artists WHERE name LIKE 'A%'") # Artist.fetch("SELECT * FROM artists WHERE id = ?", 1) alias fetch with_sql end # Sequel::Model instance methods that implement basic model functionality. # # * All of the model before/after/around hooks are implemented as instance methods that are called # by Sequel when the appropriate action occurs. For example, when destroying # a model object, Sequel will call +around_destroy+, which will call +before_destroy+, do # the destroy, and then call +after_destroy+. # * The following instance_methods all call the class method of the same # name: columns, db, primary_key, db_schema. # * The following accessor methods are defined via metaprogramming: # raise_on_save_failure, raise_on_typecast_failure, require_modification, # strict_param_setting, typecast_empty_string_to_nil, typecast_on_assignment, # and use_transactions. The setter methods will change the setting for the # instance, and the getter methods will check for an instance setting, then # try the class setting if no instance setting has been set. module InstanceMethods HOOKS.each{|h| class_eval("def #{h}; end", __FILE__, __LINE__)} [:around_create, :around_update, :around_save, :around_destroy, :around_validation].each{|h| class_eval("def #{h}; yield end", __FILE__, __LINE__)} # Define instance method(s) that calls class method(s) of the # same name. Replaces the construct: # # define_method(meth){self.class.public_send(meth)} [:columns, :db, :primary_key, :db_schema].each{|meth| class_eval("def #{meth}; self.class.#{meth} end", __FILE__, __LINE__)} # Define instance method(s) that calls class method(s) of the # same name, caching the result in an instance variable. Define # standard attr_writer method for modifying that instance variable. [:typecast_empty_string_to_nil, :typecast_on_assignment, :strict_param_setting, :raise_on_save_failure, :raise_on_typecast_failure, :require_modification, :use_transactions].each do |meth| class_eval("def #{meth}; !defined?(@#{meth}) ? (frozen? ? self.class.#{meth} : (@#{meth} = self.class.#{meth})) : @#{meth} end", __FILE__, __LINE__) attr_writer(meth) end # The hash of attribute values. Keys are symbols with the names of the # underlying database columns. The returned hash is a reference to the # receiver's values hash, and modifying it will also modify the receiver's # values. # # Artist.new(name: 'Bob').values # => {:name=>'Bob'} # Artist[1].values # => {:id=>1, :name=>'Jim', ...} attr_reader :values alias to_hash values # Get the value of the column. Takes a single symbol or string argument. # By default it calls send with the argument to get the value. This can # be overridden if you have columns that conflict with existing # method names. alias get_column_value send # Set the value of the column. Takes two arguments. The first is a # symbol or string argument for the column name, suffixed with =. The # second is the value to set for the column. By default it calls send # with the argument to set the value. This can be overridden if you have # columns that conflict with existing method names (unlikely for setter # methods, but possible). alias set_column_value send # Creates new instance and passes the given values to set. # If a block is given, yield the instance to the block. # # Arguments: # values :: should be a hash to pass to set. # # Artist.new(name: 'Bob') # # Artist.new do |a| # a.name = 'Bob' # end def initialize(values = OPTS) @values = {} @new = true @modified = true initialize_set(values) _changed_columns.clear yield self if block_given? end # Returns value of the column's attribute. # # Artist[1][:id] #=> 1 def [](column) @values[column] end # Sets the value for the given column. If typecasting is enabled for # this object, typecast the value based on the column's type. # If this is a new record or the typecasted value isn't the same # as the current value for the column, mark the column as changed. # # a = Artist.new # a[:name] = 'Bob' # a.values #=> {:name=>'Bob'} def []=(column, value) # If it is new, it doesn't have a value yet, so we should # definitely set the new value. # If the column isn't in @values, we can't assume it is # NULL in the database, so assume it has changed. v = typecast_value(column, value) vals = @values if new? || !vals.include?(column) || v != (c = vals[column]) || v.class != c.class change_column_value(column, v) end end # Alias of eql? def ==(obj) eql?(obj) end # Case equality. By default, checks equality of the primary key value, see # pk_equal?. # # Artist[1] === Artist[1] # => true # Artist.new === Artist.new # => false # Artist[1].set(:name=>'Bob') === Artist[1] # => true def ===(obj) case pkv = pk when nil return false when Array return false if pk.any?(&:nil?) end (obj.class == model) && (obj.pk == pkv) end # If the receiver has a primary key value, returns true if the objects have # the same class and primary key value. # If the receiver's primary key value is nil or is an array containing # nil, returns false. # # Artist[1].pk_equal?(Artist[1]) # => true # Artist.new.pk_equal?(Artist.new) # => false # Artist[1].set(:name=>'Bob').pk_equal?(Artist[1]) # => true alias pk_equal? === # class is defined in Object, but it is also a keyword, # and since a lot of instance methods call class methods, # this alias makes it so you can use model instead of # self.class. # # Artist.new.model # => Artist alias_method :model, :class # The autoincrementing primary key for this model object. Should be # overridden if you have a composite primary key with one part of it # being autoincrementing. def autoincrementing_primary_key primary_key end # Cancel the current action. Should be called in before hooks to halt # the processing of the action. If a +msg+ argument is given and # the model instance is configured to raise exceptions on failure, # sets the message to use for the raised HookFailed exception. def cancel_action(msg=nil) raise_hook_failure(msg) end # The columns that have been updated. This isn't completely accurate, # as it could contain columns whose values have not changed. # # a = Artist[1] # a.changed_columns # => [] # a.name = 'Bob' # a.changed_columns # => [:name] def changed_columns _changed_columns end # Deletes and returns +self+. Does not run destroy hooks. # Look into using +destroy+ instead. # # Artist[1].delete # DELETE FROM artists WHERE (id = 1) # # => #1, ...}> def delete raise Sequel::Error, "can't delete frozen object" if frozen? _delete self end # Like delete but runs hooks before and after delete. # Uses a transaction if use_transactions is true or if the # :transaction option is given and true. # # Artist[1].destroy # BEGIN; DELETE FROM artists WHERE (id = 1); COMMIT; # # => #1, ...}> def destroy(opts = OPTS) raise Sequel::Error, "can't destroy frozen object" if frozen? checked_save_failure(opts){checked_transaction(opts){_destroy(opts)}} end # Iterates through all of the current values using each. # # Album[1].each{|k, v| puts "#{k} => #{v}"} # # id => 1 # # name => 'Bob' def each(&block) @values.each(&block) end # Compares model instances by values. # # Artist[1] == Artist[1] # => true # Artist.new == Artist.new # => true # Artist[1].set(:name=>'Bob') == Artist[1] # => false def eql?(obj) (obj.class == model) && (obj.values == @values) end # Returns the validation errors associated with this object. # See +Errors+. def errors @errors ||= errors_class.new end # Returns true when current instance exists, false otherwise. # Generally an object that isn't new will exist unless it has # been deleted. Uses a database query to check for existence, # unless the model object is new, in which case this is always # false. # # Artist[1].exists? # SELECT 1 FROM artists WHERE (id = 1) # # => true # Artist.new.exists? # # => false def exists? new? ? false : !this.get(SQL::AliasedExpression.new(1, :one)).nil? end # Ignore the model's setter method cache when this instances extends a module, as the # module may contain setter methods. def extend(mod) @singleton_setter_added = true super end # Freeze the object in such a way that it is still usable but not modifiable. # Once an object is frozen, you cannot modify it's values, changed_columns, # errors, or dataset. def freeze values.freeze _changed_columns.freeze unless errors.frozen? validate errors.freeze end this if !new? && model.primary_key super end # Value that should be unique for objects with the same class and pk (if pk is not nil), or # the same class and values (if pk is nil). # # Artist[1].hash == Artist[1].hash # true # Artist[1].set(name: 'Bob').hash == Artist[1].hash # true # Artist.new.hash == Artist.new.hash # true # Artist.new(name: 'Bob').hash == Artist.new.hash # false def hash case primary_key when Array [model, !pk.all? ? @values : pk].hash when Symbol [model, pk.nil? ? @values : pk].hash else [model, @values].hash end end # Returns value for the :id attribute, even if the primary key is # not id. To get the primary key value, use +pk+. # # Artist[1].id # => 1 def id @values[:id] end # Returns a string representation of the model instance including # the class name and values. def inspect "#<#{model.name} @values=#{inspect_values}>" end # Returns the keys in +values+. May not include all column names. # # Artist.new.keys # => [] # Artist.new(name: 'Bob').keys # => [:name] # Artist[1].keys # => [:id, :name] def keys @values.keys end # Refresh this record using +for_update+ (by default, or the specified style when given) # unless this is a new record. Returns self. This can be used to make sure no other # process is updating the record at the same time. # # If style is a string, it will be used directly. You should never pass a string # to this method that is derived from user input, as that can lead to # SQL injection. # # A symbol may be used for database independent locking behavior, but # all supported symbols have separate methods (e.g. for_update). # # # a = Artist[1] # Artist.db.transaction do # a.lock! # a.update(:name=>'A') # end # # a = Artist[2] # Artist.db.transaction do # a.lock!('FOR NO KEY UPDATE') # a.update(:name=>'B') # end def lock!(style=:update) _refresh(this.lock_style(style)) unless new? self end # Remove elements of the model object that make marshalling fail. Returns self. # # a = Artist[1] # a.marshallable! # Marshal.dump(a) def marshallable! @this = nil self end # Explicitly mark the object as modified, so +save_changes+/+update+ will # run callbacks even if no columns have changed. # # a = Artist[1] # a.save_changes # No callbacks run, as no changes # a.modified! # a.save_changes # Callbacks run, even though no changes made # # If a column is given, specifically marked that column as modified, # so that +save_changes+/+update+ will include that column in the # update. This should be used if you plan on mutating the column # value instead of assigning a new column value: # # a.modified!(:name) # a.name.gsub!(/[aeou]/, 'i') def modified!(column=nil) _add_changed_column(column) if column @modified = true end # Whether this object has been modified since last saved, used by # save_changes to determine whether changes should be saved. New # values are always considered modified. # # a = Artist[1] # a.modified? # => false # a.set(name: 'Jim') # a.modified? # => true # # If a column is given, specifically check if the given column has # been modified: # # a.modified?(:num_albums) # => false # a.num_albums = 10 # a.modified?(:num_albums) # => true def modified?(column=nil) if column changed_columns.include?(column) else @modified || !changed_columns.empty? end end # Returns true if the current instance represents a new record. # # Artist.new.new? # => true # Artist[1].new? # => false def new? defined?(@new) ? @new : (@new = false) end # Returns the primary key value identifying the model instance. # Raises an +Error+ if this model does not have a primary key. # If the model has a composite primary key, returns an array of values. # # Artist[1].pk # => 1 # Artist[[1, 2]].pk # => [1, 2] def pk raise(Error, "No primary key is associated with this model") unless key = primary_key if key.is_a?(Array) vals = @values key.map{|k| vals[k]} else @values[key] end end # Returns a hash mapping the receivers primary key column(s) to their values. # # Artist[1].pk_hash # => {:id=>1} # Artist[[1, 2]].pk_hash # => {:id1=>1, :id2=>2} def pk_hash model.primary_key_hash(pk) end # Returns a hash mapping the receivers qualified primary key column(s) to their values. # # Artist[1].qualified_pk_hash # # => {Sequel[:artists][:id]=>1} # Artist[[1, 2]].qualified_pk_hash # # => {Sequel[:artists][:id1]=>1, Sequel[:artists][:id2]=>2} def qualified_pk_hash(qualifier=model.table_name) model.qualified_primary_key_hash(pk, qualifier) end # Reloads attributes from database and returns self. Also clears all # changed_columns information. Raises an +Error+ if the record no longer # exists in the database. # # a = Artist[1] # a.name = 'Jim' # a.refresh # a.name # => 'Bob' def refresh raise Sequel::Error, "can't refresh frozen object" if frozen? _refresh(this) self end # Alias of refresh, but not aliased directly to make overriding in a plugin easier. def reload refresh end # Creates or updates the record, after making sure the record # is valid and before hooks execute successfully. Fails if: # # * the record is not valid, or # * before_save calls cancel_action, or # * the record is new and before_create calls cancel_action, or # * the record is not new and before_update calls cancel_action. # # If +save+ fails and either raise_on_save_failure or the # :raise_on_failure option is true, it raises ValidationFailed # or HookFailed. Otherwise it returns nil. # # If it succeeds, it returns self. # # Takes the following options: # # :changed :: save all changed columns, instead of all columns or the columns given # :columns :: array of specific columns that should be saved. # :raise_on_failure :: set to true or false to override the current # +raise_on_save_failure+ setting # :server :: set the server/shard on the object before saving, and use that # server/shard in any transaction. # :transaction :: set to true or false to override the current # +use_transactions+ setting # :validate :: set to false to skip validation def save(opts=OPTS) raise Sequel::Error, "can't save frozen object" if frozen? set_server(opts[:server]) if opts[:server] unless _save_valid?(opts) raise(ValidationFailed.new(self)) if raise_on_failure?(opts) return end checked_save_failure(opts){checked_transaction(opts){_save(opts)}} end # Saves only changed columns if the object has been modified. # If the object has not been modified, returns nil. If unable to # save, returns false unless +raise_on_save_failure+ is true. # # a = Artist[1] # a.save_changes # => nil # a.name = 'Jim' # a.save_changes # UPDATE artists SET name = 'Bob' WHERE (id = 1) # # => #1, :name=>'Jim', ...} def save_changes(opts=OPTS) save(Hash[opts].merge!(:changed=>true)) || false if modified? end # Updates the instance with the supplied values with support for virtual # attributes, raising an exception if a value is used that doesn't have # a setter method (or ignoring it if strict_param_setting = false). # Does not save the record. # # artist.set(name: 'Jim') # artist.name # => 'Jim' def set(hash) set_restricted(hash, :default) end # For each of the fields in the given array +fields+, call the setter # method with the value of that +hash+ entry for the field. Returns self. # # You can provide an options hash, with the following options currently respected: # :missing :: Can be set to :skip to skip missing entries or :raise to raise an # Error for missing entries. The default behavior is not to check for # missing entries, in which case the default value is used. To be # friendly with most web frameworks, the missing check will also check # for the string version of the argument in the hash if given a symbol. # # Examples: # # artist.set_fields({name: 'Jim'}, [:name]) # artist.name # => 'Jim' # # artist.set_fields({hometown: 'LA'}, [:name]) # artist.name # => nil # artist.hometown # => 'Sac' # # artist.name # => 'Jim' # artist.set_fields({}, [:name], missing: :skip) # artist.name # => 'Jim' # # artist.name # => 'Jim' # artist.set_fields({}, [:name], missing: :raise) # # Sequel::Error raised def set_fields(hash, fields, opts=nil) opts = if opts Hash[model.default_set_fields_options].merge!(opts) else model.default_set_fields_options end case missing = opts[:missing] when :skip, :raise do_raise = true if missing == :raise fields.each do |f| if hash.has_key?(f) set_column_value("#{f}=", hash[f]) elsif f.is_a?(Symbol) && hash.has_key?(sf = f.to_s) set_column_value("#{sf}=", hash[sf]) elsif do_raise raise(Sequel::Error, "missing field in hash: #{f.inspect} not in #{hash.inspect}") end end else fields.each{|f| set_column_value("#{f}=", hash[f])} end self end # Set the shard that this object is tied to. Returns self. def set_server(s) @server = s @this = @this.server(s) if @this self end # Clear the setter_methods cache when a method is added def singleton_method_added(meth) @singleton_setter_added = true if meth.to_s.end_with?('=') super end # Skip all validation of the object on the next call to #save, # including the running of validation hooks. This is designed for # and should only be used in cases where #valid? is called before # saving and the validate: false option cannot be passed to # #save. def skip_validation_on_next_save! @skip_validation_on_next_save = true end # Returns (naked) dataset that should return only this instance. # # Artist[1].this # # SELECT * FROM artists WHERE (id = 1) LIMIT 1 def this return @this if @this raise Error, "No dataset for model #{model}" unless ds = model.instance_dataset @this = use_server(ds.where(pk_hash)) end # Runs #set with the passed hash and then runs save_changes. # # artist.update(name: 'Jim') # UPDATE artists SET name = 'Jim' WHERE (id = 1) def update(hash) update_restricted(hash, :default) end # Update the instance's values by calling set_fields with the arguments, then # calls save_changes. # # artist.update_fields({name: 'Jim'}, [:name]) # # UPDATE artists SET name = 'Jim' WHERE (id = 1) # # artist.update_fields({hometown: 'LA'}, [:name]) # # UPDATE artists SET name = NULL WHERE (id = 1) def update_fields(hash, fields, opts=nil) set_fields(hash, fields, opts) save_changes end # Validates the object. If the object is invalid, errors should be added # to the errors attribute. By default, does nothing, as all models # are valid by default. See the {"Model Validations" guide}[rdoc-ref:doc/validations.rdoc]. # for details about validation. Should not be called directly by # user code, call valid? instead to check if an object # is valid. def validate end # Validates the object and returns true if no errors are reported. # # artist.set(name: 'Valid').valid? # => true # artist.set(name: 'Invalid').valid? # => false # artist.errors.full_messages # => ['name cannot be Invalid'] def valid?(opts = OPTS) begin _valid?(opts) rescue HookFailed false end end private # Add a column as a changed column. def _add_changed_column(column) cc = _changed_columns cc << column unless cc.include?(column) end # Internal changed_columns method that just returns stored array. def _changed_columns @changed_columns ||= [] end # Do the deletion of the object's dataset, and check that the row # was actually deleted. def _delete n = _delete_without_checking raise(NoExistingObject, "Attempt to delete object did not result in a single row modification (Rows Deleted: #{n}, SQL: #{_delete_dataset.delete_sql})") if require_modification && n != 1 n end # The dataset to use when deleting the object. The same as the object's # dataset by default. def _delete_dataset this end # Actually do the deletion of the object's dataset. Return the # number of rows modified. def _delete_without_checking if sql = (m = model).fast_instance_delete_sql sql = sql.dup ds = use_server(m.dataset) ds.literal_append(sql, pk) ds.with_sql_delete(sql) else _delete_dataset.delete end end # Internal destroy method, separted from destroy to # allow running inside a transaction def _destroy(opts) called = false around_destroy do called = true before_destroy _destroy_delete after_destroy end raise_hook_failure(:around_destroy) unless called self end # Internal delete method to call when destroying an object, # separated from delete to allow you to override destroy's version # without affecting delete. def _destroy_delete delete end # Insert the record into the database, returning the primary key if # the record should be refreshed from the database. def _insert ds = _insert_dataset if _use_insert_select?(ds) && !(h = _insert_select_raw(ds)).nil? _save_set_values(h) if h nil else iid = _insert_raw(ds) # if we have a regular primary key and it's not set in @values, # we assume it's the last inserted id if (pk = autoincrementing_primary_key) && pk.is_a?(Symbol) && !(vals = @values)[pk] vals[pk] = iid end pk end end # The dataset to use when inserting a new object. The same as the model's # dataset by default. def _insert_dataset use_server(model.instance_dataset) end # Insert into the given dataset and return the primary key created (if any). def _insert_raw(ds) ds.insert(_insert_values) end # Insert into the given dataset and return the hash of column values. def _insert_select_raw(ds) ds.insert_select(_insert_values) end # The values hash to use when inserting a new record. alias _insert_values values # Refresh using a particular dataset, used inside save to make sure the same server # is used for reading newly inserted values from the database def _refresh(dataset) _refresh_set_values(_refresh_get(dataset) || raise(NoExistingObject, "Record not found")) _changed_columns.clear end # Get the row of column data from the database. def _refresh_get(dataset) if (sql = model.fast_pk_lookup_sql) && !dataset.opts[:lock] sql = sql.dup ds = use_server(dataset) ds.literal_append(sql, pk) ds.with_sql_first(sql) else dataset.first end end # Set the values to the given hash after refreshing. def _refresh_set_values(h) @values = h end # Internal version of save, split from save to allow running inside # it's own transaction. def _save(opts) pk = nil called_save = false called_cu = false around_save do called_save = true before_save if new? around_create do called_cu = true before_create pk = _insert @this = nil @new = false @modified = false pk ? _save_refresh : _changed_columns.clear after_create true end raise_hook_failure(:around_create) unless called_cu else around_update do called_cu = true before_update columns = opts[:columns] if columns.nil? if opts[:changed] cc = changed_columns columns_updated = @values.reject{|k,v| !cc.include?(k)} cc.clear else columns_updated = _save_update_all_columns_hash _changed_columns.clear end else # update only the specified columns columns = Array(columns) columns_updated = @values.reject{|k, v| !columns.include?(k)} _changed_columns.reject!{|c| columns.include?(c)} end _update_columns(columns_updated) @this = nil @modified = false after_update true end raise_hook_failure(:around_update) unless called_cu end after_save true end raise_hook_failure(:around_save) unless called_save self end # Refresh the object after saving it, used to get # default values of all columns. Separated from _save so it # can be overridden to avoid the refresh. def _save_refresh _save_set_values(_refresh_get(this.server?(:default)) || raise(NoExistingObject, "Record not found")) _changed_columns.clear end # Set values to the provided hash. Called after a create, # to set the full values from the database in the model instance. def _save_set_values(h) @values = h end # Return a hash of values used when saving all columns of an # existing object (i.e. not passing specific columns to save # or using update/save_changes). Defaults to all of the # object's values except unmodified primary key columns, as some # databases don't like you setting primary key values even # to their existing values. def _save_update_all_columns_hash v = Hash[@values] cc = changed_columns Array(primary_key).each{|x| v.delete(x) unless cc.include?(x)} v end # Validate the object if validating on save. Skips validation # completely (including validation hooks) if # skip_validation_on_save! has been called on the object, # resetting the flag so that future saves will validate. def _save_valid?(opts) if @skip_validation_on_next_save @skip_validation_on_next_save = false return true end checked_save_failure(opts){_valid?(opts)} end # Call _update with the given columns, if any are present. # Plugins can override this method in order to update with # additional columns, even when the column hash is initially empty. def _update_columns(columns) _update(columns) unless columns.empty? end # Update this instance's dataset with the supplied column hash, # checking that only a single row was modified. def _update(columns) n = _update_without_checking(columns) raise(NoExistingObject, "Attempt to update object did not result in a single row modification (SQL: #{_update_dataset.update_sql(columns)})") if require_modification && n != 1 n end # The dataset to use when updating an object. The same as the object's # dataset by default. def _update_dataset this end # Update this instances dataset with the supplied column hash. def _update_without_checking(columns) _update_dataset.update(columns) end # Whether to use insert_select when inserting a new row. def _use_insert_select?(ds) (!ds.opts[:select] || ds.opts[:returning]) && ds.supports_insert_select? end # Internal validation method, running validation hooks. def _valid?(opts) return errors.empty? if frozen? errors.clear called = false skip_validate = opts[:validate] == false around_validation do called = true before_validation validate unless skip_validate after_validation end return true if skip_validate if called errors.empty? else raise_hook_failure(:around_validation) end end # If not raising on failure, check for HookFailed # being raised by yielding and swallow it. def checked_save_failure(opts) if raise_on_failure?(opts) yield else begin yield rescue HookFailed nil end end end # If transactions should be used, wrap the yield in a transaction block. def checked_transaction(opts=OPTS) use_transaction?(opts) ? db.transaction({:server=>this_server}.merge!(opts)){yield} : yield end # Change the value of the column to given value, recording the change. def change_column_value(column, value) _add_changed_column(column) @values[column] = value end # Default error class used for errors. def errors_class Errors end # Clone constructor -- freeze internal data structures if the original's # are frozen. def initialize_clone(other) super freeze if other.frozen? self end # Copy constructor -- Duplicate internal data structures. def initialize_copy(other) super @values = Hash[@values] @changed_columns = @changed_columns.dup if @changed_columns @errors = @errors.dup if @errors self end # Set the columns with the given hash. By default, the same as +set+, but # exists so it can be overridden. This is called only for new records, before # changed_columns is cleared. def initialize_set(h) set(h) unless h.empty? end # Default inspection output for the values hash, overwrite to change what #inspect displays. def inspect_values @values.inspect end # Whether to raise or return false if this action fails. If the # :raise_on_failure option is present in the hash, use that, otherwise, # fallback to the object's raise_on_save_failure (if set), or # class's default (if not). def raise_on_failure?(opts) opts.fetch(:raise_on_failure, raise_on_save_failure) end # Raise an error appropriate to the hook type. May be swallowed by # checked_save_failure depending on the raise_on_failure? setting. def raise_hook_failure(type=nil) msg = case type when String type when Symbol "the #{type} hook failed" else "a hook failed" end raise HookFailed.new(msg, self) end # Get the ruby class or classes related to the given column's type. def schema_type_class(column) if (sch = db_schema[column]) && (type = sch[:type]) db.schema_type_class(type) end end # Call setter methods based on keys in hash, with the appropriate values. # Restrict which methods can be called based on the provided type. def set_restricted(hash, type) return self if hash.empty? meths = setter_methods(type) strict = strict_param_setting hash.each do |k,v| m = "#{k}=" if meths.include?(m) set_column_value(m, v) elsif strict # Avoid using respond_to? or creating symbols from user input if public_methods.map(&:to_s).include?(m) if Array(model.primary_key).map(&:to_s).member?(k.to_s) && model.restrict_primary_key? raise MassAssignmentRestriction, "#{k} is a restricted primary key" else raise MassAssignmentRestriction, "#{k} is a restricted column" end else raise MassAssignmentRestriction, "method #{m} doesn't exist" end end end self end # Returns all methods that can be used for attribute assignment (those that end with =), # depending on the type: # # :default :: Use the default methods allowed in the model class. # :all :: Allow setting all setters, except those specifically restricted (such as ==). # Array :: Only allow setting of columns in the given array. def setter_methods(type) if type == :default && !@singleton_setter_added return model.setter_methods end meths = methods.map(&:to_s).select{|l| l.end_with?('=')} - RESTRICTED_SETTER_METHODS meths -= Array(primary_key).map{|x| "#{x}="} if primary_key && model.restrict_primary_key? meths end # The server/shard that the model object's dataset uses, or :default if the # model object's dataset does not have an associated shard. def this_server if (s = @server) s elsif (t = @this) t.opts[:server] || :default else model.dataset.opts[:server] || :default end end # Typecast the value to the column's type if typecasting. Calls the database's # typecast_value method, so database adapters can override/augment the handling # for database specific column types. def typecast_value(column, value) return value unless typecast_on_assignment && db_schema && (col_schema = db_schema[column]) value = nil if '' == value and typecast_empty_string_to_nil and col_schema[:type] and ![:string, :blob].include?(col_schema[:type]) raise(InvalidValue, "nil/NULL is not allowed for the #{column} column") if raise_on_typecast_failure && value.nil? && (col_schema[:allow_null] == false) begin model.db.typecast_value(col_schema[:type], value) rescue InvalidValue raise_on_typecast_failure ? raise : value end end # Set the columns, filtered by the only and except arrays. def update_restricted(hash, type) set_restricted(hash, type) save_changes end # Set the given dataset to use the current object's shard. def use_server(ds) @server ? ds.server(@server) : ds end # Whether to use a transaction for this action. If the :transaction # option is present in the hash, use that, otherwise, fallback to the # object's default (if set), or class's default (if not). def use_transaction?(opts = OPTS) opts.fetch(:transaction, use_transactions) end end # DatasetMethods contains methods that all model datasets have. module DatasetMethods # The model class associated with this dataset # # Artist.dataset.model # => Artist def model @opts[:model] end # Assume if a single integer is given that it is a lookup by primary # key, and call with_pk with the argument. # # Artist.dataset[1] # SELECT * FROM artists WHERE (id = 1) LIMIT 1 def [](*args) if args.length == 1 && (i = args[0]) && i.is_a?(Integer) with_pk(i) else super end end # Destroy each row in the dataset by instantiating it and then calling # destroy on the resulting model object. This isn't as fast as deleting # the dataset, which does a single SQL call, but this runs any destroy # hooks on each object in the dataset. # # Artist.dataset.destroy # # DELETE FROM artists WHERE (id = 1) # # DELETE FROM artists WHERE (id = 2) # # ... def destroy pr = proc{all(&:destroy).length} model.use_transactions ? @db.transaction(:server=>opts[:server], &pr) : pr.call end # If there is no order already defined on this dataset, order it by # the primary key and call last. # # Album.last # # SELECT * FROM albums ORDER BY id DESC LIMIT 1 def last(*a, &block) if ds = _primary_key_order ds.last(*a, &block) else super end end # If there is no order already defined on this dataset, order it by # the primary key and call paged_each. # # Album.paged_each{|row| } # # SELECT * FROM albums ORDER BY id LIMIT 1000 OFFSET 0 # # SELECT * FROM albums ORDER BY id LIMIT 1000 OFFSET 1000 # # SELECT * FROM albums ORDER BY id LIMIT 1000 OFFSET 2000 # # ... def paged_each(*a, &block) if ds = _primary_key_order ds.paged_each(*a, &block) else super end end # This allows you to call +as_hash+ without any arguments, which will # result in a hash with the primary key value being the key and the # model object being the value. # # Artist.dataset.as_hash # SELECT * FROM artists # # => {1=>#1, ...}>, # # 2=>#2, ...}>, # # ...} def as_hash(key_column=nil, value_column=nil, opts=OPTS) if key_column super else raise(Sequel::Error, "No primary key for model") unless model && (pk = model.primary_key) super(pk, value_column, opts) end end # Alias of as_hash for backwards compatibility. def to_hash(*a) as_hash(*a) end # Given a primary key value, return the first record in the dataset with that primary key # value. If no records matches, returns nil. # # # Single primary key # Artist.dataset.with_pk(1) # # SELECT * FROM artists WHERE (artists.id = 1) LIMIT 1 # # # Composite primary key # Artist.dataset.with_pk([1, 2]) # # SELECT * FROM artists WHERE ((artists.id1 = 1) AND (artists.id2 = 2)) LIMIT 1 def with_pk(pk) if pk && (loader = _with_pk_loader) loader.first(*pk) else first(model.qualified_primary_key_hash(pk)) end end # Same as with_pk, but raises NoMatchingRow instead of returning nil if no # row matches. def with_pk!(pk) with_pk(pk) || raise(NoMatchingRow.new(self)) end private # If the dataset is not already ordered, and the model has a primary key, # return a clone ordered by the primary key. def _primary_key_order if @opts[:order].nil? && model && (pk = model.primary_key) cached_dataset(:_pk_order_ds){order(*pk)} end end # A cached placeholder literalizer, if one exists for the current dataset. def _with_pk_loader cached_placeholder_literalizer(:_with_pk_loader) do |pl| table = model.table_name cond = case primary_key = model.primary_key when Array primary_key.map{|key| [SQL::QualifiedIdentifier.new(table, key), pl.arg]} when Symbol {SQL::QualifiedIdentifier.new(table, primary_key)=>pl.arg} else raise(Error, "#{model} does not have a primary key") end where(cond).limit(1) end end def non_sql_option?(key) super || key == :model end end extend ClassMethods plugin self singleton_class.send(:undef_method, :dup, :clone, :initialize_copy) if RUBY_VERSION >= '1.9.3' singleton_class.send(:undef_method, :initialize_clone, :initialize_dup) end end end