module Sequel # The Schema module holds the schema generators. module Schema # Schema::CreateTableGenerator is an internal class that the user is not expected # to instantiate directly. Instances are created by Database#create_table. # It is used to specify table creation parameters. It takes a Database # object and a block of column/index/constraint specifications, and # gives the Database a table description, which the database uses to # create a table. # # Schema::CreateTableGenerator has some methods but also includes method_missing, # allowing users to specify column type as a method instead of using # the column method, which makes for a nicer DSL. # # For more information on Sequel's support for schema modification, see # the {"Schema Modification" guide}[link:files/doc/schema_modification_rdoc.html]. class CreateTableGenerator # Classes specifying generic types that Sequel will convert to database-specific types. GENERIC_TYPES=[String, Integer, Fixnum, Bignum, Float, Numeric, BigDecimal, Date, DateTime, Time, File, TrueClass, FalseClass] # Return the column hashes created by this generator attr_reader :columns # Return the constraint hashes created by this generator attr_reader :constraints # Return the index hashes created by this generator attr_reader :indexes # Set the database in which to create the table, and evaluate the block # in the context of this object. def initialize(db, &block) @db = db @columns = [] @indexes = [] @constraints = [] @primary_key = nil instance_eval(&block) if block @columns.unshift(@primary_key) if @primary_key && !has_column?(primary_key_name) end # Add a method for each of the given types that creates a column # with that type as a constant. Types given should either already # be constants/classes or a capitalized string/symbol with the same name # as a constant/class. def self.add_type_method(*types) types.each do |type| class_eval("def #{type}(name, opts={}); column(name, #{type}, opts); end", __FILE__, __LINE__) end end # Add an unnamed constraint to the DDL, specified by the given block # or args: # # check(:num=>1..5) # CHECK num >= 1 AND num <= 5 # check{num > 5} # CHECK num > 5 def check(*args, &block) constraint(nil, *args, &block) end # Add a column with the given name, type, and opts to the DDL. # # column :num, :integer # # num INTEGER # # column :name, String, :null=>false, :default=>'a' # # name varchar(255) NOT NULL DEFAULT 'a' # # inet :ip # # ip inet # # You can also create columns via method missing, so the following are # equivalent: # # column :number, :integer # integer :number # # The following options are supported: # # :default :: The default value for the column. # :deferrable :: This ensure Referential Integrity will work even if # reference table will use for its foreign key a value that does not # exists(yet) on referenced table. Basically it adds # DEFERRABLE INITIALLY DEFERRED on key creation. # :index :: Create an index on this column. If given a hash, use the hash as the # options for the index. # :key :: For foreign key columns, the column in the associated table # that this column references. Unnecessary if this column # references the primary key of the associated table, except if you are # using MySQL. # :null :: Mark the column as allowing NULL values (if true), # or not allowing NULL values (if false). If unspecified, will default # to whatever the database default is. # :on_delete :: Specify the behavior of this column when being deleted # (:restrict, :cascade, :set_null, :set_default, :no_action). # :on_update :: Specify the behavior of this column when being updated # (:restrict, :cascade, :set_null, :set_default, :no_action). # :primary_key :: Make the column as a single primary key column. This should only # be used if you have a single, nonautoincrementing primary key column. # :type :: Overrides the type given as the argument. Generally not used by column # itself, but can be passed as an option to other methods that call column. # :unique :: Mark the column as unique, generally has the same effect as # creating a unique index on the column. def column(name, type, opts = {}) columns << {:name => name, :type => type}.merge(opts) if index_opts = opts[:index] index(name, index_opts.is_a?(Hash) ? index_opts : {}) end end # Adds a named constraint (or unnamed if name is nil) to the DDL, # with the given block or args. # # constraint(:blah, :num=>1..5) # CONSTRAINT blah CHECK num >= 1 AND num <= 5 # check(:foo){num > 5} # CONSTRAINT foo CHECK num > 5 def constraint(name, *args, &block) constraints << {:name => name, :type => :check, :check => block || args} end # Add a foreign key in the table that references another table to the DDL. See column # for available options. # # foreign_key(:artist_id) # artist_id INTEGER # foreign_key(:artist_id, :artists) # artist_id INTEGER REFERENCES artists # foreign_key(:artist_id, :artists, :key=>:id) # artist_id INTEGER REFERENCES artists(id) # foreign_key(:artist_id, :artists, :type=>String) # artist_id varchar(255) REFERENCES artists(id) # # If you want a foreign key constraint without adding a column (usually because it is a # composite foreign key), you can provide an array of columns as the first argument, and # you can provide the :name option to name the constraint: # # foreign_key([:artist_name, :artist_location], :artists, :name=>:artist_fk) # # ADD CONSTRAINT artist_fk FOREIGN KEY (artist_name, artist_location) REFERENCES artists def foreign_key(name, table=nil, opts = {}) opts = case table when Hash table.merge(opts) when Symbol opts.merge(:table=>table) when NilClass opts else raise(Error, "The second argument to foreign_key should be a Hash, Symbol, or nil") end return composite_foreign_key(name, opts) if name.is_a?(Array) column(name, Integer, opts) end # Add a full text index on the given columns to the DDL. # # PostgreSQL specific options: # :language :: Set a language to use for the index (default: simple). # # Microsoft SQL Server specific options: # :key_index :: The KEY INDEX to use for the full text index. def full_text_index(columns, opts = {}) index(columns, opts.merge(:type => :full_text)) end # True if the DDL includes the creation of a column with the given name. def has_column?(name) columns.any?{|c| c[:name] == name} end # Add an index on the given column(s) with the given options to the DDL. # General options: # # :name :: The name to use for the index. If not given, a default name # based on the table and columns is used. # :type :: The type of index to use (only supported by some databases) # :unique :: Make the index unique, so duplicate values are not allowed. # :where :: Create a partial index (only supported by some databases) # # PostgreSQL specific options: # # :concurrently :: Create the index concurrently, so it doesn't block # operations on the table while the index is being # built. # :op_class :: Use a specific operator class in the index. # # Microsoft SQL Server specific options: # # :include :: Include additional column values in the index, without # actually indexing on those values. # # index :name # # CREATE INDEX table_name_index ON table (name) # # index [:artist_id, :name] # # CREATE INDEX table_artist_id_name_index ON table (artist_id, name) def index(columns, opts = {}) indexes << {:columns => Array(columns)}.merge(opts) end # Add a column with the given type, name, and opts to the DDL. See +column+ for available # options. def method_missing(type, name = nil, opts = {}) name ? column(name, type, opts) : super end # Adds an autoincrementing primary key column or a primary key constraint to the DDL. # To create a constraint, the first argument should be an array of column symbols # specifying the primary key columns. To create an autoincrementing primary key # column, a single symbol can be used. In both cases, an options hash can be used # as the second argument. # # If you want to create a primary key column that is not autoincrementing, you # should not use this method. Instead, you should use the regular +column+ method # with a :primary_key=>true option. # # Examples: # primary_key(:id) # primary_key([:street_number, :house_number]) def primary_key(name, *args) return composite_primary_key(name, *args) if name.is_a?(Array) @primary_key = @db.serial_primary_key_options.merge({:name => name}) if opts = args.pop opts = {:type => opts} unless opts.is_a?(Hash) if type = args.pop opts.merge!(:type => type) end @primary_key.merge!(opts) end @primary_key end # The name of the primary key for this generator, if it has a primary key. def primary_key_name @primary_key[:name] if @primary_key end # Add a spatial index on the given columns to the DDL. def spatial_index(columns, opts = {}) index(columns, opts.merge(:type => :spatial)) end # Add a unique constraint on the given columns to the DDL. # # unique(:name) # UNIQUE (name) def unique(columns, opts = {}) constraints << {:type => :unique, :columns => Array(columns)}.merge(opts) end private # Add a composite primary key constraint def composite_primary_key(columns, *args) opts = args.pop || {} constraints << {:type => :primary_key, :columns => columns}.merge(opts) end # Add a composite foreign key constraint def composite_foreign_key(columns, opts) constraints << {:type => :foreign_key, :columns => columns}.merge(opts) end add_type_method(*GENERIC_TYPES) end # Alias of CreateTableGenerator for backwards compatibility. Generator = CreateTableGenerator # Schema::AlterTableGenerator is an internal class that the user is not expected # to instantiate directly. Instances are created by Database#alter_table. # It is used to specify table alteration parameters. It takes a Database # object and a block of operations to perform on the table, and # gives the Database an array of table altering operations, which the database uses to # alter a table's description. # # For more information on Sequel's support for schema modification, see # the {"Schema Modification" guide}[link:files/doc/schema_modification_rdoc.html]. class AlterTableGenerator # An array of DDL operations to perform attr_reader :operations # Set the Database object to which to apply the DDL, and evaluate the # block in the context of this object. def initialize(db, &block) @db = db @operations = [] instance_eval(&block) if block end # Add a column with the given name, type, and opts to the DDL for the table. # See CreateTableGenerator#column for the available options. # # add_column(:name, String) # ADD COLUMN name varchar(255) def add_column(name, type, opts = {}) @operations << {:op => :add_column, :name => name, :type => type}.merge(opts) end # Add a constraint with the given name and args to the DDL for the table. # See CreateTableGenerator#constraint. # # add_constraint(:valid_name, Sequel.like(:name, 'A%')) # # ADD CONSTRAINT valid_name CHECK (name LIKE 'A%') def add_constraint(name, *args, &block) @operations << {:op => :add_constraint, :name => name, :type => :check, :check => block || args} end # Add a unique constraint to the given column(s) # # add_unique_constraint(:name) # ADD UNIQUE (name) # add_unique_constraint(:name, :name=>:unique_name) # ADD CONSTRAINT unique_name UNIQUE (name) def add_unique_constraint(columns, opts = {}) @operations << {:op => :add_constraint, :type => :unique, :columns => Array(columns)}.merge(opts) end # Add a foreign key with the given name and referencing the given table # to the DDL for the table. See CreateTableGenerator#column for the available options. # # You can also pass an array of column names for creating composite foreign # keys. In this case, it will assume the columns exist and will only add # the constraint. You can provide a :name option to name the constraint. # # NOTE: If you need to add a foreign key constraint to a single existing column # use the composite key syntax even if it is only one column. # # add_foreign_key(:artist_id, :table) # ADD COLUMN artist_id integer REFERENCES table # add_foreign_key([:name], :table) # ADD FOREIGN KEY (name) REFERENCES table # # PostgreSQL specific options: # # :not_valid :: Set to true to add the constraint with the NOT VALID syntax. # This makes it so that future inserts must respect referential # integrity, but allows the constraint to be added even if existing # column values reference rows that do not exist. After all the # existing data has been cleaned up, validate_constraint can be used # to mark the constraint as valid. Note that this option only makes # sense when using an array of columns. def add_foreign_key(name, table, opts = {}) return add_composite_foreign_key(name, table, opts) if name.is_a?(Array) add_column(name, Integer, {:table=>table}.merge(opts)) end # Add a full text index on the given columns to the DDL for the table. # See CreateTableGenerator#index for available options. def add_full_text_index(columns, opts = {}) add_index(columns, {:type=>:full_text}.merge(opts)) end # Add an index on the given columns to the DDL for the table. See # CreateTableGenerator#index for available options. # # add_index(:artist_id) # CREATE INDEX table_artist_id_index ON table (artist_id) def add_index(columns, opts = {}) @operations << {:op => :add_index, :columns => Array(columns)}.merge(opts) end # Add a primary key to the DDL for the table. See CreateTableGenerator#column # for the available options. Like +add_foreign_key+, if you specify # the column name as an array, it just creates a constraint: # # add_primary_key(:id) # ADD COLUMN id serial PRIMARY KEY # add_primary_key([:artist_id, :name]) # ADD PRIMARY KEY (artist_id, name) def add_primary_key(name, opts = {}) return add_composite_primary_key(name, opts) if name.is_a?(Array) opts = @db.serial_primary_key_options.merge(opts) add_column(name, opts.delete(:type), opts) end # Add a spatial index on the given columns to the DDL for the table. # See CreateTableGenerator#index for available options. def add_spatial_index(columns, opts = {}) add_index(columns, {:type=>:spatial}.merge(opts)) end # Remove a column from the DDL for the table. # # drop_column(:artist_id) # DROP COLUMN artist_id # drop_column(:artist_id, :cascade=>true) # DROP COLUMN artist_id CASCADE def drop_column(name, opts={}) @operations << {:op => :drop_column, :name => name}.merge(opts) end # Remove a constraint from the DDL for the table. MySQL/SQLite specific options: # # :type :: Set the type of constraint to drop, either :primary_key, :foreign_key, # or :unique. # # drop_constraint(:unique_name) # DROP CONSTRAINT unique_name # drop_constraint(:unique_name, :cascade=>true) # DROP CONSTRAINT unique_name CASCADE def drop_constraint(name, opts={}) @operations << {:op => :drop_constraint, :name => name}.merge(opts) end # Remove an index from the DDL for the table. General options: # # :name :: The name of the index to drop. If not given, uses the same name # that would be used by add_index with the same columns. # # PostgreSQL specific options: # # :cascade :: Cascade the index drop to dependent objects. # :concurrently :: Drop the index using CONCURRENTLY, which doesn't block # operations on the table. Supported in PostgreSQL 9.2+. # :if_exists :: Only drop the index if it already exists. # # drop_index(:artist_id) # DROP INDEX table_artist_id_index # drop_index([:a, :b]) # DROP INDEX table_a_b_index # drop_index([:a, :b], :name=>:foo) # DROP INDEX foo def drop_index(columns, options={}) @operations << {:op => :drop_index, :columns => Array(columns)}.merge(options) end # Modify a column's name in the DDL for the table. # # rename_column(:name, :artist_name) # RENAME COLUMN name TO artist_name def rename_column(name, new_name, opts = {}) @operations << {:op => :rename_column, :name => name, :new_name => new_name}.merge(opts) end # Modify a column's default value in the DDL for the table. # # set_column_default(:artist_name, 'a') # ALTER COLUMN artist_name SET DEFAULT 'a' def set_column_default(name, default) @operations << {:op => :set_column_default, :name => name, :default => default} end # Modify a column's type in the DDL for the table. # # set_column_type(:artist_name, 'char(10)') # ALTER COLUMN artist_name TYPE char(10) # # PostgreSQL specific options: # # :using :: Add a USING clause that specifies how to convert existing values to new values. def set_column_type(name, type, opts={}) @operations << {:op => :set_column_type, :name => name, :type => type}.merge(opts) end # Set a given column as allowing NULL values. # # set_column_allow_null(:artist_name) # ALTER COLUMN artist_name DROP NOT NULL def set_column_allow_null(name, allow_null=true) @operations << {:op => :set_column_null, :name => name, :null => allow_null} end # Set a given column as not allowing NULL values. # # set_column_not_null(:artist_name) # ALTER COLUMN artist_name SET NOT NULL def set_column_not_null(name) set_column_allow_null(name, false) end private # Add a composite primary key constraint def add_composite_primary_key(columns, opts) @operations << {:op => :add_constraint, :type => :primary_key, :columns => columns}.merge(opts) end # Add a composite foreign key constraint def add_composite_foreign_key(columns, table, opts) @operations << {:op => :add_constraint, :type => :foreign_key, :columns => columns, :table => table}.merge(opts) end end end end