--- title: Sequel category: Ruby libraries --- ### About {: .-intro} Sequel is a database toolkit for Ruby. - ### Open a database ``` require 'rubygems' require 'sequel' DB = Sequel.sqlite('my_blog.db') DB = Sequel.connect('postgres://user:password@localhost/my_db') DB = Sequel.postgres('my_db', :user => 'user', :password => 'password', :host => 'localhost') DB = Sequel.ado('mydb') ``` ### Open an SQLite memory database Without a filename argument, the sqlite adapter will setup a new sqlite database in memory. ``` DB = Sequel.sqlite ``` ### Logging SQL statements ``` require 'logger' DB = Sequel.sqlite '', :loggers => [Logger.new($stdout)] # or DB.loggers << Logger.new(...) ``` ### Using raw SQL ``` DB.run "CREATE TABLE users (name VARCHAR(255) NOT NULL, age INT(3) NOT NULL)" dataset = DB["SELECT age FROM users WHERE name = ?", name] dataset.map(:age) DB.fetch("SELECT name FROM users") do |row| p row[:name] end ``` ### Create a dataset ``` dataset = DB[:items] dataset = DB.from(:items) ``` ### Most dataset methods are chainable ``` dataset = DB[:managers].where(:salary => 5000..10000).order(:name, :department) ``` ### Insert rows ``` dataset.insert(:name => 'Sharon', :grade => 50) ``` ### Retrieve rows ``` dataset.each{|r| p r} dataset.all # => [{...}, {...}, ...] dataset.first # => {...} ``` ### Update/Delete rows ``` dataset.filter(~:active).delete dataset.filter('price < ?', 100).update(:active => true) ``` ### Datasets are Enumerable ``` dataset.map{|r| r[:name]} dataset.map(:name) # same as above dataset.inject(0){|sum, r| sum + r[:value]} dataset.sum(:value) # same as above ``` ### Filtering (see also doc/dataset_filtering.rdoc) #### Equality ``` dataset.filter(:name => 'abc') dataset.filter('name = ?', 'abc') ``` #### Inequality ``` dataset.filter{value > 100} dataset.exclude{value <= 100} ``` #### Inclusion ``` dataset.filter(:value => 50..100) dataset.where{(value >= 50) & (value <= 100)} dataset.where('value IN ?', [50,75,100]) dataset.where(:value=>[50,75,100]) dataset.where(:id=>other_dataset.select(:other_id)) ``` #### Subselects as scalar values ``` dataset.where('price > (SELECT avg(price) + 100 FROM table)') dataset.filter{price > dataset.select(avg(price) + 100)} ``` #### LIKE/Regexp ``` DB[:items].filter(:name.like('AL%')) DB[:items].filter(:name => /^AL/) ``` #### AND/OR/NOT ``` DB[:items].filter{(x > 5) & (y > 10)}.sql # SELECT * FROM items WHERE ((x > 5) AND (y > 10)) DB[:items].filter({:x => 1, :y => 2}.sql_or & ~{:z => 3}).sql # SELECT * FROM items WHERE (((x = 1) OR (y = 2)) AND (z != 3)) ``` #### Mathematical operators ``` DB[:items].filter((:x + :y) > :z).sql # SELECT * FROM items WHERE ((x + y) > z) DB[:items].filter{price - 100 < avg(price)}.sql # SELECT * FROM items WHERE ((price - 100) < avg(price)) ``` ### Ordering ``` dataset.order(:kind) dataset.reverse_order(:kind) dataset.order(:kind.desc, :name) ``` ### Limit/Offset ``` dataset.limit(30) # LIMIT 30 dataset.limit(30, 10) # LIMIT 30 OFFSET 10 ``` ### Joins ``` DB[:items].left_outer_join(:categories, :id => :category_id).sql # SELECT * FROM items LEFT OUTER JOIN categories ON categories.id = items.category_id DB[:items].join(:categories, :id => :category_id).join(:groups, :id => :items__group_id) # SELECT * FROM items INNER JOIN categories ON categories.id = items.category_id INNER JOIN groups ON groups.id = items.group_id ``` ### Aggregate functions methods ``` dataset.count #=> record count dataset.max(:price) dataset.min(:price) dataset.avg(:price) dataset.sum(:stock) dataset.group_and_count(:category) dataset.group(:category).select(:category, :AVG.sql_function(:price)) ``` ### SQL Functions / Literals ``` dataset.update(:updated_at => :NOW.sql_function) dataset.update(:updated_at => 'NOW()'.lit) dataset.update(:updated_at => "DateValue('1/1/2001')".lit) dataset.update(:updated_at => :DateValue.sql_function('1/1/2001')) ``` ### Schema Manipulation ``` DB.create_table :items do primary_key :id String :name, :unique => true, :null => false TrueClass :active, :default => true foreign_key :category_id, :categories DateTime :created_at index :created_at end DB.drop_table :items DB.create_table :test do String :zipcode enum :system, :elements => ['mac', 'linux', 'windows'] end ``` ### Aliasing ``` DB[:items].select(:name.as(:item_name)) DB[:items].select(:name___item_name) DB[:items___items_table].select(:items_table__name___item_name) # SELECT items_table.name AS item_name FROM items AS items_table ``` ### Transactions ``` DB.transaction do dataset.insert(:first_name => 'Inigo', :last_name => 'Montoya') dataset.insert(:first_name => 'Farm', :last_name => 'Boy') end # Either both are inserted or neither are inserted ``` Database#transaction is re-entrant: ``` DB.transaction do # BEGIN issued only here DB.transaction dataset << {:first_name => 'Inigo', :last_name => 'Montoya'} end end # COMMIT issued only here ``` Transactions are aborted if an error is raised: ``` DB.transaction do raise "some error occurred" end # ROLLBACK issued and the error is re-raised ``` Transactions can also be aborted by raising Sequel::Rollback: ``` DB.transaction do raise(Sequel::Rollback) if something_bad_happened end # ROLLBACK issued and no error raised ``` Savepoints can be used if the database supports it: ``` DB.transaction do dataset << {:first_name => 'Farm', :last_name => 'Boy'} # Inserted DB.transaction(:savepoint=>true) # This savepoint is rolled back dataset << {:first_name => 'Inigo', :last_name => 'Montoya'} # Not inserted raise(Sequel::Rollback) if something_bad_happened end dataset << {:first_name => 'Prince', :last_name => 'Humperdink'} # Inserted end ``` ### Miscellaneous: ``` dataset.sql # "SELECT * FROM items" dataset.delete_sql # "DELETE FROM items" dataset.where(:name => 'sequel').exists # "EXISTS ( SELECT * FROM items WHERE name = 'sequel' )" dataset.columns #=> array of columns in the result set, does a SELECT DB.schema(:items) => [[:id, {:type=>:integer, ...}], [:name, {:type=>:string, ...}], ...] ``` --- ### Documents ``` http://sequel.rubyforge.org/rdoc/files/doc/association_basics_rdoc.html http://sequel.rubyforge.org/rdoc/classes/Sequel/Schema/Generator.html http://sequel.rubyforge.org/rdoc/files/doc/validations_rdoc.html http://sequel.rubyforge.org/rdoc/classes/Sequel/Model.html ``` ### Alter table ``` database.alter_table :deals do add_column :name, String drop_column :column_name rename_column :from, :to add_constraint :valid_name, :name.like('A%') drop_constraint :constraint add_full_text_index :body add_spacial_index [columns] add_index :price drop_index :index add_foreign_key :artist_id, :table add_primary_key :id add_unique_constraint [columns] set_column_allow_null :foo, false set_column_default :title, '' set_column_type :price, 'char(10)' end ``` ### Model associations ``` class Deal < Sequel::Model # Us (left) <=> Them (right) many_to_many :images, left_key: :deal_id, right_key: :image_id, join_table: :image_links one_to_many :files, key: :deal_id, class: :DataFile, many_to_one :parent, class: self one_to_many :children, key: :parent_id, class: self one_to_many :gold_albums, class: :Album do |ds| ds.filter { copies_sold > 50000 } end ``` Provided by many_to_many ``` Deal[1].images Deal[1].add_image Deal[1].remove_image Deal[1].remove_all_images ``` ### Validations ``` def validate super errors.add(:name, 'cannot be empty') if !name || name.empty? validates_presence [:title, :site] validates_unique :name validates_format /\Ahttps?:\/\//, :website, :message=>'is not a valid URL' validates_includes %w(a b c), :type validates_integer :rating validates_numeric :number validates_type String, [:title, :description] validates_integer :rating if new? # options: :message =>, :allow_nil =>, :allow_blank =>, # :allow_missing =>, validates_exact_length 17, :isbn validates_min_length 3, :name validates_max_length 100, :name validates_length_range 3..100, :name # Setter override def filename=(name) @values[:filename] = name end end end deal.errors ``` ### Model stuff ``` deal = Deal[1] deal.changed_columns deal.destroy # Calls hooks deal.delete # No hooks deal.exists? deal.new? deal.hash # Only uniques deal.keys #=> [:id, :name] deal.modified! deal.modified? deal.lock! ``` ### Callbacks ``` before_create after_create before_validation after_validation before_save before_update UPDATE QUERY after_update after_save before_destroy DELETE QUERY after_destroy ``` ### Schema ``` class Deal < Sequel::Model set_schema do primary_key :id primary_key [:id, :title] String :name, primary_key: true String :title Numeric :price DateTime :expires unique :whatever check(:price) { num > 0 } foreign_key :artist_id String :artist_name, key: :id index :title index [:artist_id, :name] full_text_index :title # String, Integer, Fixnum, Bignum, Float, Numeric, BigDecimal, # Date, DateTime, Time, File, TrueClass, FalseClass end end ``` ### Unrestrict primary key ``` Category.create id: 'travel' # error Category.unrestrict_primary_key Category.create id: 'travel' # ok ```