Swift

Description

A rational rudimentary object relational mapper.

Dependencies

  • MRI Ruby >= 1.9.1
  • swift-db-sqlite3 or swift-db-postgres or swift-db-mysql

Installation

Dependencies

Install one of the following drivers you would like to use.

gem install swift-db-sqlite3
gem install swift-db-postgres
gem install swift-db-mysql

Install Swift

gem install swift

Features

  • Multiple databases.
  • Prepared statements.
  • Bind values.
  • Transactions and named save points.
  • Asynchronous API for PostgreSQL and MySQL.
  • IdentityMap.
  • Migrations.

DB

  require 'swift'
  require 'swift/adapter/postgres'

  Swift.trace true # Debugging.
  Swift.setup :default, Swift::Adapter::Postgres, db: 'swift'

  # Block form db context.
  Swift.db do |db|
    db.execute('drop table if exists users')
    db.execute('create table users(id serial, name text, email text)')

    # Save points are supported.
    db.transaction :named_save_point do
      st = db.prepare('insert into users (name, email) values (?, ?) returning id')
      puts st.execute('Apple Arthurton', '[email protected]').insert_id
      puts st.execute('Benny Arthurton', '[email protected]').insert_id
    end

    # Block result iteration.
    db.prepare('select * from users').execute do |row|
      puts row.inspect
    end

    # Enumerable.
    result = db.prepare('select * from users where name like ?').execute('Benny%')
    puts result.first
  end

DB Record Operations

Rudimentary object mapping. Provides a definition to the db methods for prepared (and cached) statements plus native primitive Ruby type conversion.

  require 'swift'
  require 'swift/adapter/postgres'
  require 'swift/migrations'

  Swift.trace true # Debugging.
  Swift.setup :default, Swift::Adapter::Postgres, db: 'swift'

  class User < Swift::Record
    store     :users
    attribute :id,         Swift::Type::Integer, serial: true, key: true
    attribute :name,       Swift::Type::String
    attribute :email,      Swift::Type::String
    attribute :updated_at, Swift::Type::DateTime
  end # User

  Swift.db do |db|
    db.migrate! User

    # Select Record instance (relation) instead of Hash.
    users = db.prepare(User, 'select * from users limit 1').execute

    # Make a change and update.
    users.each{|user| user.updated_at = Time.now}
    db.update(User, *users)

    # Get a specific user by id.
    user = db.get(User, id: 1)
    puts user.name, user.email
  end

Record CRUD

Record/relation level helpers.

  require 'swift'
  require 'swift/adapter/postgres'
  require 'swift/migrations'

  Swift.trace true # Debugging.
  Swift.setup :default, Swift::Adapter::Postgres, db: 'swift'

  class User < Swift::Record
    store     :users
    attribute :id,    Swift::Type::Integer, serial: true, key: true
    attribute :name,  Swift::Type::String
    attribute :email, Swift::Type::String
  end # User

  # Migrate it.
  User.migrate!

  # Create
  User.create name: 'Apple Arthurton', email: '[email protected]' # => User

  # Get by key.
  user = User.get id: 1

  # Alter attribute and update in one.
  user.update name: 'Jimmy Arthurton'

  # Alter attributes and update.
  user.name = 'Apple Arthurton'
  user.update

  # Destroy
  user.delete

Conditions SQL syntax.

SQL is easy and most people know it so Swift ORM provides simple #to_s attribute to table and field name typecasting.

  class User < Swift::Record
    store     :users
    attribute :id,    Swift::Type::Integer, serial: true, key: true
    attribute :age,   Swift::Type::Integer, field: 'ega'
    attribute :name,  Swift::Type::String,  field: 'eman'
    attribute :email, Swift::Type::String,  field: 'liame'
  end # User

  # Convert :name and :age to fields.
  # select * from users where eman like '%Arthurton' and ega > 20
  users = User.execute(
    %Q{select * from #{User} where #{User.name} like ? and #{User.age} > ?},
    '%Arthurton', 20
  )

Identity Map

Swift comes with a simple identity map. Just require it after you load swift.

  require 'swift'
  require 'swift/adapter/postgres'
  require 'swift/identity_map'
  require 'swift/migrations'

  Swift.setup :default, Swift::Adapter::Postgres, db: 'swift'

  class User < Swift::Record
    store     :users
    attribute :id,    Swift::Type::Integer, serial: true, key: true
    attribute :age,   Swift::Type::Integer, field: 'ega'
    attribute :name,  Swift::Type::String,  field: 'eman'
    attribute :email, Swift::Type::String,  field: 'liame'
  end # User

  # Migrate it.
  User.migrate!

  # Create
  User.create name: 'James Arthurton', email: '[email protected]' # => User

  find_user = User.prepare(%Q{select * from #{User} where #{User.name = ?})
  find_user.execute('James Arthurton')
  find_user.execute('James Arthurton') # Gets same object reference

Bulk inserts

Swift comes with adapter level support for bulk inserts for MySQL and PostgreSQL. This is usually very fast (~5-10x faster) than regular prepared insert statements for larger sets of data.

MySQL adapter - Overrides the MySQL C API and implements its own infile handlers. This means currently you cannot execute the following SQL using Swift

  LOAD DATA LOCAL INFILE '/tmp/users.tab' INTO TABLE users;

But you can do it almost as fast in ruby,

  require 'swift'
  require 'swift/adapter/mysql'

  Swift.setup :default, Swift::Adapter::Mysql, db: 'swift'

  # MySQL packet size is the usual limit, 8k is the packet size by default.
  Swift.db do |db|
    File.open('/tmp/users.tab') do |file|
      count = db.write('users', %w{name email balance}, file)
    end
  end

You are not just limited to files - you can stream data from anywhere into your database without creating temporary files.

Asynchronous API

Swift::Adapter::Sql#query runs a query asynchronously. You can either poll the corresponding Swift::Adapter::Sql#fileno and then call Swift::Adapter::Sql#result when ready or use a block form like below which implicitly uses rb_thread_wait_fd

  require 'swift'
  require 'swift/adapter/postgres'

  pool = 3.times.map.with_index {|n| Swift.setup n, Swift::Adapter::Postgres, db: 'swift' }

  Thread.new do
    pool[0].query('select pg_sleep(3), 1 as qid') {|row| p row}
  end

  Thread.new do
    pool[1].query('select pg_sleep(2), 2 as qid') {|row| p row}
  end

  Thread.new do
    pool[2].query('select pg_sleep(1), 3 as qid') {|row| p row}
  end

  Thread.list.reject {|thread| Thread.current == thread}.each(&:join)

or use the swift/eventmachine api.

  require 'swift'
  require 'swift/adapter/em/postgres'

  EM.run do
    pool = 3.times.map { Swift.setup(:default, Swift::Adapter::EM::Postgres, db: "swift") }

    3.times.each do |n|
      defer = pool[n].execute("select pg_sleep(3 - #{n}), #{n + 1} as qid")

      defer.callback do |res|
        p res.first
      end

      defer.errback do |e|
        p 'error', e
      end
    end
  end

or use the em-synchrony api for swift

  require 'swift'
  require 'swift/adapter/synchrony/postgres'

  EM.run do
    3.times.each do |n|
      EM.synchrony do
        db     = Swift.setup(:default, Swift::Adapter::Synchrony::Postgres, db: "swift")
        result = db.execute("select pg_sleep(3 - #{n}), #{n + 1} as qid")

        p result.first
        EM.stop if n == 0
      end
    end
  end

Fibers and Connection Pools

If you intend to use Swift::Record with em-synchrony you will need to use a fiber aware connection pool.

require 'swift/fiber_connection_pool'

EM.run do
  Swift.setup(:default) do
    Swift::FiberConnectionPool.new(size: 5) {Swift::Adapter::Synchrony::Postgres.new(db: 'swift')}
  end

  5.times do
    EM.synchrony do
      p User.execute("select * from users").entries
    end
  end
end

Performance

Swift prefers performance when it doesn't compromise the Ruby-ish interface. It's unfair to compare Swift to DataMapper and ActiveRecord which suffer under the weight of support for many more databases and legacy/alternative Ruby implementations. That said obviously if Swift were slower it would be redundant so benchmark code does exist in http://github.com/shanna/swift/tree/master/benchmarks

Benchmarks

ORM

The test environment:

$ uname -a

Linux deepfryed.local 3.0.0-1-amd64 #1 SMP Sun Jul 24 02:24:44 UTC 2011 x86_64 GNU/Linux

$ cat /proc/cpuinfo | grep "processor\|model name"

processor    : 0
model name   : Intel(R) Core(TM) i7-2677M CPU @ 1.80GHz
processor    : 1
model name   : Intel(R) Core(TM) i7-2677M CPU @ 1.80GHz
processor    : 2
model name   : Intel(R) Core(TM) i7-2677M CPU @ 1.80GHz
processor    : 3
model name   : Intel(R) Core(TM) i7-2677M CPU @ 1.80GHz

$ ruby -v

ruby 1.9.3p125 (2012-02-16 revision 34643) [x86_64-linux]

PostgreSQL config:

shared_buffers           = 800MB     # min 128kB
effective_cache_size     = 512MB
work_mem                 = 64MB      # min 64kB
maintenance_work_mem     = 64MB      # min 1MB

The test setup:

  • 10,000 rows are created once.
  • All the rows are selected once.
  • All the rows are selected once and updated once.
  • Memory footprint(rss) shows how much memory the benchmark used with GC disabled. This gives an idea of total memory use and indirectly an idea of the number of objects allocated and the pressure on Ruby GC if it were running. When GC is enabled, the actual memory consumption might be much lower than the numbers below.
    ./simple.rb -n1 -r10000 -s ar -s dm -s sequel -s swift

    benchmark           sys         user       total       real        rss

    ar #create          1.960000    15.81000   17.770000   22.753109   266.21m
    ar #select          0.020000     0.38000    0.400000    0.433041    50.82m
    ar #update          2.000000    17.90000   19.900000   26.674921   317.48m

    dm #create          0.660000    11.55000   12.210000   15.592424   236.86m
    dm #select          0.030000     1.30000    1.330000    1.351911    87.18m
    dm #update          0.950000    17.25000   18.200000   22.109859   474.81m

    sequel #create      1.960000    14.48000   16.440000   23.004864   226.68m
    sequel #select      0.000000     0.09000    0.090000    0.134619    12.77m
    sequel #update      1.900000    14.37000   16.270000   22.945636   200.20m

    swift #create       0.520000     1.95000    2.470000    5.828846    75.26m
    swift #select       0.010000    0.070000    0.080000    0.095124    11.23m
    swift #update       0.440000     1.95000    2.390000    6.044971    59.35m
    swift #write        0.010000    0.050000    0.060000    0.117195    13.46m

TODO

  • More tests.
  • Assertions for dumb stuff.
  • Auto-generate schema?
  • Move examples to Wiki. Examples of models built on top of Schema.

Contributing

Go nuts! There is no style guide and I do not care if you write tests or comment code. If you write something neat just send a pull request, tweet, email or yell it at me line by line in person.