2013-10-14 02:36:58 +00:00
|
|
|
---
|
|
|
|
title: Sequel
|
2015-11-24 05:02:17 +00:00
|
|
|
category: Ruby libraries
|
2013-10-14 02:36:58 +00:00
|
|
|
---
|
|
|
|
|
2023-03-13 12:02:33 +00:00
|
|
|
### About
|
|
|
|
{: .-intro}
|
|
|
|
|
|
|
|
Sequel is a database toolkit for Ruby.
|
|
|
|
|
|
|
|
- <https://github.com/jeremyevans/sequel>
|
|
|
|
|
2013-10-14 02:36:58 +00:00
|
|
|
### Open a database
|
|
|
|
|
2023-12-13 21:45:04 +00:00
|
|
|
```
|
|
|
|
require 'rubygems'
|
|
|
|
require 'sequel'
|
2013-10-14 02:36:58 +00:00
|
|
|
|
2023-12-13 21:45:04 +00:00
|
|
|
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')
|
|
|
|
```
|
2013-10-14 02:36:58 +00:00
|
|
|
|
|
|
|
### Open an SQLite memory database
|
|
|
|
|
|
|
|
Without a filename argument, the sqlite adapter will setup a new sqlite database in memory.
|
|
|
|
|
2023-12-13 21:45:04 +00:00
|
|
|
```
|
|
|
|
DB = Sequel.sqlite
|
|
|
|
```
|
2013-10-14 02:36:58 +00:00
|
|
|
|
|
|
|
### Logging SQL statements
|
|
|
|
|
2023-12-13 21:45:04 +00:00
|
|
|
```
|
|
|
|
require 'logger'
|
|
|
|
DB = Sequel.sqlite '', :loggers => [Logger.new($stdout)]
|
|
|
|
# or
|
|
|
|
DB.loggers << Logger.new(...)
|
|
|
|
```
|
2013-10-14 02:36:58 +00:00
|
|
|
|
|
|
|
### Using raw SQL
|
|
|
|
|
2023-12-13 21:45:04 +00:00
|
|
|
```
|
|
|
|
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
|
|
|
|
```
|
2013-10-14 02:36:58 +00:00
|
|
|
|
|
|
|
### Create a dataset
|
|
|
|
|
2023-12-13 21:45:04 +00:00
|
|
|
```
|
|
|
|
dataset = DB[:items]
|
|
|
|
dataset = DB.from(:items)
|
|
|
|
```
|
2013-10-14 02:36:58 +00:00
|
|
|
|
|
|
|
### Most dataset methods are chainable
|
|
|
|
|
2023-12-13 21:45:04 +00:00
|
|
|
```
|
|
|
|
dataset = DB[:managers].where(:salary => 5000..10000).order(:name, :department)
|
|
|
|
```
|
2013-10-14 02:36:58 +00:00
|
|
|
|
|
|
|
### Insert rows
|
|
|
|
|
2023-12-13 21:45:04 +00:00
|
|
|
```
|
|
|
|
dataset.insert(:name => 'Sharon', :grade => 50)
|
|
|
|
```
|
2013-10-14 02:36:58 +00:00
|
|
|
|
|
|
|
### Retrieve rows
|
|
|
|
|
2023-12-13 21:45:04 +00:00
|
|
|
```
|
|
|
|
dataset.each{|r| p r}
|
|
|
|
dataset.all # => [{...}, {...}, ...]
|
|
|
|
dataset.first # => {...}
|
|
|
|
```
|
2013-10-14 02:36:58 +00:00
|
|
|
|
|
|
|
### Update/Delete rows
|
|
|
|
|
2023-12-13 21:45:04 +00:00
|
|
|
```
|
|
|
|
dataset.filter(~:active).delete
|
|
|
|
dataset.filter('price < ?', 100).update(:active => true)
|
|
|
|
```
|
2013-10-14 02:36:58 +00:00
|
|
|
|
|
|
|
### Datasets are Enumerable
|
|
|
|
|
2023-12-13 21:45:04 +00:00
|
|
|
```
|
|
|
|
dataset.map{|r| r[:name]}
|
|
|
|
dataset.map(:name) # same as above
|
2013-10-14 02:36:58 +00:00
|
|
|
|
2023-12-13 21:45:04 +00:00
|
|
|
dataset.inject(0){|sum, r| sum + r[:value]}
|
|
|
|
dataset.sum(:value) # same as above
|
|
|
|
```
|
2013-10-14 02:36:58 +00:00
|
|
|
|
|
|
|
### Filtering (see also doc/dataset_filtering.rdoc)
|
|
|
|
|
|
|
|
#### Equality
|
|
|
|
|
2023-12-13 21:45:04 +00:00
|
|
|
```
|
|
|
|
dataset.filter(:name => 'abc')
|
|
|
|
dataset.filter('name = ?', 'abc')
|
|
|
|
```
|
2013-10-14 02:36:58 +00:00
|
|
|
|
|
|
|
#### Inequality
|
|
|
|
|
2023-12-13 21:45:04 +00:00
|
|
|
```
|
|
|
|
dataset.filter{value > 100}
|
|
|
|
dataset.exclude{value <= 100}
|
|
|
|
```
|
2013-10-14 02:36:58 +00:00
|
|
|
|
|
|
|
#### Inclusion
|
|
|
|
|
2023-12-13 21:45:04 +00:00
|
|
|
```
|
|
|
|
dataset.filter(:value => 50..100)
|
|
|
|
dataset.where{(value >= 50) & (value <= 100)}
|
2013-10-14 02:36:58 +00:00
|
|
|
|
2023-12-13 21:45:04 +00:00
|
|
|
dataset.where('value IN ?', [50,75,100])
|
|
|
|
dataset.where(:value=>[50,75,100])
|
2013-10-14 02:36:58 +00:00
|
|
|
|
2023-12-13 21:45:04 +00:00
|
|
|
dataset.where(:id=>other_dataset.select(:other_id))
|
|
|
|
```
|
2013-10-14 02:36:58 +00:00
|
|
|
|
|
|
|
#### Subselects as scalar values
|
|
|
|
|
2023-12-13 21:45:04 +00:00
|
|
|
```
|
|
|
|
dataset.where('price > (SELECT avg(price) + 100 FROM table)')
|
|
|
|
dataset.filter{price > dataset.select(avg(price) + 100)}
|
|
|
|
```
|
2013-10-14 02:36:58 +00:00
|
|
|
|
|
|
|
#### LIKE/Regexp
|
|
|
|
|
2023-12-13 21:45:04 +00:00
|
|
|
```
|
|
|
|
DB[:items].filter(:name.like('AL%'))
|
|
|
|
DB[:items].filter(:name => /^AL/)
|
|
|
|
```
|
2013-10-14 02:36:58 +00:00
|
|
|
|
|
|
|
#### AND/OR/NOT
|
|
|
|
|
2023-12-13 21:45:04 +00:00
|
|
|
```
|
|
|
|
DB[:items].filter{(x > 5) & (y > 10)}.sql
|
|
|
|
# SELECT * FROM items WHERE ((x > 5) AND (y > 10))
|
2013-10-14 02:36:58 +00:00
|
|
|
|
2023-12-13 21:45:04 +00:00
|
|
|
DB[:items].filter({:x => 1, :y => 2}.sql_or & ~{:z => 3}).sql
|
|
|
|
# SELECT * FROM items WHERE (((x = 1) OR (y = 2)) AND (z != 3))
|
|
|
|
```
|
2013-10-14 02:36:58 +00:00
|
|
|
|
|
|
|
#### Mathematical operators
|
|
|
|
|
2023-12-13 21:45:04 +00:00
|
|
|
```
|
|
|
|
DB[:items].filter((:x + :y) > :z).sql
|
|
|
|
# SELECT * FROM items WHERE ((x + y) > z)
|
2013-10-14 02:36:58 +00:00
|
|
|
|
2023-12-13 21:45:04 +00:00
|
|
|
DB[:items].filter{price - 100 < avg(price)}.sql
|
|
|
|
# SELECT * FROM items WHERE ((price - 100) < avg(price))
|
|
|
|
```
|
2013-10-14 02:36:58 +00:00
|
|
|
|
|
|
|
### Ordering
|
|
|
|
|
2023-12-13 21:45:04 +00:00
|
|
|
```
|
|
|
|
dataset.order(:kind)
|
|
|
|
dataset.reverse_order(:kind)
|
|
|
|
dataset.order(:kind.desc, :name)
|
|
|
|
```
|
2013-10-14 02:36:58 +00:00
|
|
|
|
|
|
|
### Limit/Offset
|
|
|
|
|
2023-12-13 21:45:04 +00:00
|
|
|
```
|
|
|
|
dataset.limit(30) # LIMIT 30
|
|
|
|
dataset.limit(30, 10) # LIMIT 30 OFFSET 10
|
|
|
|
```
|
2013-10-14 02:36:58 +00:00
|
|
|
|
|
|
|
### Joins
|
|
|
|
|
2023-12-13 21:45:04 +00:00
|
|
|
```
|
|
|
|
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
|
|
|
|
```
|
2013-10-14 02:36:58 +00:00
|
|
|
|
|
|
|
### Aggregate functions methods
|
|
|
|
|
2023-12-13 21:45:04 +00:00
|
|
|
```
|
|
|
|
dataset.count #=> record count
|
|
|
|
dataset.max(:price)
|
|
|
|
dataset.min(:price)
|
|
|
|
dataset.avg(:price)
|
|
|
|
dataset.sum(:stock)
|
2013-10-14 02:36:58 +00:00
|
|
|
|
2023-12-13 21:45:04 +00:00
|
|
|
dataset.group_and_count(:category)
|
|
|
|
dataset.group(:category).select(:category, :AVG.sql_function(:price))
|
|
|
|
```
|
2013-10-14 02:36:58 +00:00
|
|
|
|
|
|
|
### SQL Functions / Literals
|
|
|
|
|
2023-12-13 21:45:04 +00:00
|
|
|
```
|
|
|
|
dataset.update(:updated_at => :NOW.sql_function)
|
|
|
|
dataset.update(:updated_at => 'NOW()'.lit)
|
2013-10-14 02:36:58 +00:00
|
|
|
|
2023-12-13 21:45:04 +00:00
|
|
|
dataset.update(:updated_at => "DateValue('1/1/2001')".lit)
|
|
|
|
dataset.update(:updated_at => :DateValue.sql_function('1/1/2001'))
|
|
|
|
```
|
2013-10-14 02:36:58 +00:00
|
|
|
|
|
|
|
### Schema Manipulation
|
|
|
|
|
2023-12-13 21:45:04 +00:00
|
|
|
```
|
|
|
|
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
|
2013-10-14 02:36:58 +00:00
|
|
|
|
2023-12-13 21:45:04 +00:00
|
|
|
index :created_at
|
|
|
|
end
|
2013-10-14 02:36:58 +00:00
|
|
|
|
2023-12-13 21:45:04 +00:00
|
|
|
DB.drop_table :items
|
|
|
|
|
|
|
|
DB.create_table :test do
|
|
|
|
String :zipcode
|
|
|
|
enum :system, :elements => ['mac', 'linux', 'windows']
|
|
|
|
end
|
|
|
|
```
|
2013-10-14 02:36:58 +00:00
|
|
|
|
|
|
|
### Aliasing
|
|
|
|
|
2023-12-13 21:45:04 +00:00
|
|
|
```
|
|
|
|
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
|
|
|
|
```
|
2013-10-14 02:36:58 +00:00
|
|
|
|
|
|
|
### Transactions
|
|
|
|
|
2023-12-13 21:45:04 +00:00
|
|
|
```
|
|
|
|
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
|
|
|
|
```
|
2013-10-14 02:36:58 +00:00
|
|
|
|
|
|
|
Database#transaction is re-entrant:
|
|
|
|
|
2023-12-13 21:45:04 +00:00
|
|
|
```
|
|
|
|
DB.transaction do # BEGIN issued only here
|
|
|
|
DB.transaction
|
|
|
|
dataset << {:first_name => 'Inigo', :last_name => 'Montoya'}
|
|
|
|
end
|
|
|
|
end # COMMIT issued only here
|
|
|
|
```
|
2013-10-14 02:36:58 +00:00
|
|
|
|
|
|
|
Transactions are aborted if an error is raised:
|
|
|
|
|
2023-12-13 21:45:04 +00:00
|
|
|
```
|
|
|
|
DB.transaction do
|
|
|
|
raise "some error occurred"
|
|
|
|
end # ROLLBACK issued and the error is re-raised
|
|
|
|
```
|
2013-10-14 02:36:58 +00:00
|
|
|
|
|
|
|
Transactions can also be aborted by raising Sequel::Rollback:
|
|
|
|
|
2023-12-13 21:45:04 +00:00
|
|
|
```
|
|
|
|
DB.transaction do
|
|
|
|
raise(Sequel::Rollback) if something_bad_happened
|
|
|
|
end # ROLLBACK issued and no error raised
|
|
|
|
```
|
2013-10-14 02:36:58 +00:00
|
|
|
|
|
|
|
Savepoints can be used if the database supports it:
|
|
|
|
|
2023-12-13 21:45:04 +00:00
|
|
|
```
|
|
|
|
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
|
|
|
|
```
|
2013-10-14 02:36:58 +00:00
|
|
|
|
|
|
|
### Miscellaneous:
|
|
|
|
|
2023-12-13 21:45:04 +00:00
|
|
|
```
|
|
|
|
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, ...}], ...]
|
|
|
|
```
|
2013-10-14 02:36:58 +00:00
|
|
|
|
2023-12-13 21:45:04 +00:00
|
|
|
---
|
2013-10-14 02:36:58 +00:00
|
|
|
|
|
|
|
### Documents
|
|
|
|
|
2023-12-13 21:45:04 +00:00
|
|
|
```
|
|
|
|
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
|
|
|
|
```
|
2013-10-14 02:36:58 +00:00
|
|
|
|
|
|
|
### Alter table
|
|
|
|
|
2023-12-13 21:45:04 +00:00
|
|
|
```
|
|
|
|
database.alter_table :deals do
|
|
|
|
add_column :name, String
|
|
|
|
drop_column :column_name
|
|
|
|
rename_column :from, :to
|
2013-10-14 02:36:58 +00:00
|
|
|
|
2023-12-13 21:45:04 +00:00
|
|
|
add_constraint :valid_name, :name.like('A%')
|
|
|
|
drop_constraint :constraint
|
2013-10-14 02:36:58 +00:00
|
|
|
|
2023-12-13 21:45:04 +00:00
|
|
|
add_full_text_index :body
|
|
|
|
add_spacial_index [columns]
|
2013-10-14 02:36:58 +00:00
|
|
|
|
2023-12-13 21:45:04 +00:00
|
|
|
add_index :price
|
|
|
|
drop_index :index
|
2013-10-14 02:36:58 +00:00
|
|
|
|
2023-12-13 21:45:04 +00:00
|
|
|
add_foreign_key :artist_id, :table
|
|
|
|
add_primary_key :id
|
|
|
|
add_unique_constraint [columns]
|
|
|
|
set_column_allow_null :foo, false
|
|
|
|
set_column_default :title, ''
|
2013-10-14 02:36:58 +00:00
|
|
|
|
2023-12-13 21:45:04 +00:00
|
|
|
set_column_type :price, 'char(10)'
|
|
|
|
end
|
|
|
|
```
|
2013-10-14 02:36:58 +00:00
|
|
|
|
|
|
|
### Model associations
|
|
|
|
|
2023-12-13 21:45:04 +00:00
|
|
|
```
|
|
|
|
class Deal < Sequel::Model
|
2013-10-14 02:36:58 +00:00
|
|
|
|
2023-12-13 21:45:04 +00:00
|
|
|
# Us (left) <=> Them (right)
|
|
|
|
many_to_many :images,
|
|
|
|
left_key: :deal_id,
|
|
|
|
right_key: :image_id,
|
|
|
|
join_table: :image_links
|
2013-10-14 02:36:58 +00:00
|
|
|
|
2023-12-13 21:45:04 +00:00
|
|
|
one_to_many :files,
|
|
|
|
key: :deal_id,
|
|
|
|
class: :DataFile,
|
2013-10-14 02:36:58 +00:00
|
|
|
|
2023-12-13 21:45:04 +00:00
|
|
|
many_to_one :parent, class: self
|
|
|
|
one_to_many :children, key: :parent_id, class: self
|
2013-10-14 02:36:58 +00:00
|
|
|
|
2023-12-13 21:45:04 +00:00
|
|
|
one_to_many :gold_albums, class: :Album do |ds|
|
|
|
|
ds.filter { copies_sold > 50000 }
|
|
|
|
end
|
|
|
|
```
|
2013-10-14 02:36:58 +00:00
|
|
|
|
|
|
|
Provided by many_to_many
|
|
|
|
|
2023-12-13 21:45:04 +00:00
|
|
|
```
|
|
|
|
Deal[1].images
|
|
|
|
Deal[1].add_image
|
|
|
|
Deal[1].remove_image
|
|
|
|
Deal[1].remove_all_images
|
|
|
|
```
|
2013-10-14 02:36:58 +00:00
|
|
|
|
|
|
|
### Validations
|
|
|
|
|
2023-12-13 21:45:04 +00:00
|
|
|
```
|
|
|
|
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
|
2013-10-14 02:36:58 +00:00
|
|
|
end
|
2023-12-13 21:45:04 +00:00
|
|
|
end
|
|
|
|
end
|
2013-10-14 02:36:58 +00:00
|
|
|
|
2023-12-13 21:45:04 +00:00
|
|
|
deal.errors
|
|
|
|
```
|
2013-10-14 02:36:58 +00:00
|
|
|
|
|
|
|
### Model stuff
|
|
|
|
|
2023-12-13 21:45:04 +00:00
|
|
|
```
|
|
|
|
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!
|
|
|
|
```
|
2013-10-14 02:36:58 +00:00
|
|
|
|
|
|
|
### Callbacks
|
|
|
|
|
2023-12-13 21:45:04 +00:00
|
|
|
```
|
|
|
|
before_create
|
|
|
|
after_create
|
2013-10-14 02:36:58 +00:00
|
|
|
|
2023-12-13 21:45:04 +00:00
|
|
|
before_validation
|
|
|
|
after_validation
|
|
|
|
before_save
|
|
|
|
before_update
|
|
|
|
UPDATE QUERY
|
|
|
|
after_update
|
|
|
|
after_save
|
2013-10-14 02:36:58 +00:00
|
|
|
|
2023-12-13 21:45:04 +00:00
|
|
|
before_destroy
|
|
|
|
DELETE QUERY
|
|
|
|
after_destroy
|
|
|
|
```
|
2013-10-14 02:36:58 +00:00
|
|
|
|
|
|
|
### Schema
|
|
|
|
|
2023-12-13 21:45:04 +00:00
|
|
|
```
|
|
|
|
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
|
|
|
|
```
|
2013-10-14 02:36:58 +00:00
|
|
|
|
|
|
|
### Unrestrict primary key
|
|
|
|
|
2023-12-13 21:45:04 +00:00
|
|
|
```
|
|
|
|
Category.create id: 'travel' # error
|
|
|
|
Category.unrestrict_primary_key
|
|
|
|
Category.create id: 'travel' # ok
|
|
|
|
```
|