Swift

Description

A rational rudimentary object relational mapper.

Dependencies

Caveats

DB2

  • The server needs to be running under DB2_COMPATIBILITY_VECTOR=77FF if you want to use the ORM features of swift.

  • DB2 asynchronous operations are highly experimental at this point due to inherent limitations of the underlying api. It is more an academic exercise and is not ready for real-world use.

Features

  • Multiple databases.

  • Prepared statements.

  • Bind values.

  • Transactions and named save points.

  • EventMachine asynchronous interface.

  • IdentityMap.

  • Migrations.

Synopsis

DB

require 'swift'

Swift.trace true # Debugging.
Swift.setup :default, Swift::DB::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 Scheme 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/migrations'

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

class User < Swift::Scheme
  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::Time
end # User

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

  # Select Scheme 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

Scheme CRUD

Scheme/relation level helpers.

require 'swift'
require 'swift/migrations'

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

class User < Swift::Scheme
  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.destroy

Conditions SQL syntax.

SQL is easy and most people know it so Swift ORM provides a simple symbol like syntax to convert resource names to field names.

class User < Swift::Scheme
  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.all(':name like ? and :age > ?', '%Arthurton', 20)

Identity Map

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

require 'swift'
require 'swift/identity_map'
require 'swift/migrations'

class User < Swift::Scheme
  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

User.first(':name = ?', 'James Arthurton')
User.first(':name = ?', 'James Arthurton') # Gets same object reference

Bulk inserts

Swift comes with adapter level support for bulk inserts for MySQL, PostgreSQL and DB2. 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'

Swift.setup :default, Swift::DB::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.

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 github.com/shanna/swift/tree/master/benchmarks

Benchmarks

ORM

The following bechmarks were run on a machine with 4G ram, 5200rpm sata drive, Intel Core2Duo P8700 2.53GHz and stock PostgreSQL 8.4.1.

  • 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      0.800000  6.620000  7.420000  9.898821  369.44m
    ar #select      0.020000  0.300000  0.320000  0.372809  38.83m
    ar #update      0.770000  6.550000  7.320000  10.02434  361.92m
    
    dm #create      0.110000  3.590000  3.700000  4.847609  248.74m
    dm #select      0.120000  1.840000  1.960000  2.029552  128.98m
    dm #update      0.400000  7.750000  8.150000  9.741249  599.52m
    
    sequel #create  0.770000  3.910000  4.680000  7.432611  263.59m
    sequel #select  0.020000  0.080000  0.100000  0.147321  9.82m
    sequel #update  0.730000  3.910000  4.640000  7.594949  259.18m
    
    swift #create   0.210000  0.850000  1.060000  2.618661  32.72m
    swift #select   0.000000  0.080000  0.080000  0.132245  9.91m
    swift #update   0.270000  0.650000  0.920000  2.204108  37.48m
    
    -- bulk insert api --
    swift #write    0.000000  0.080000  0.080000  0.151146  7.29m
    

Adapter

The adapter level SELECT benchmarks without using ORM.

  • Same dataset as above.

  • All rows are selected 5 times.

  • The pg benchmark uses pg_typecast gem to provide typecasting support for pg gem and also makes the benchmarks more fair.

PostgreSQL
benchmark       sys       user      total     real      rss
do #select      0.060000  1.070000  1.130000  1.370092  99.83m
pg #select      0.030000  0.270000  0.300000  0.584091  46.13m
swift #select   0.020000  0.290000  0.310000  0.571635  39.08m
MySQL
benchmark       sys       user      total     real      rss
do #select      0.030000  1.070000  1.100000  1.200177  99.26m
mysql2 #select  0.060000  0.450000  0.510000  0.609236  76.44m
swift #select   0.050000  0.170000  0.220000  0.334932  33.61m

TODO

  • More tests.

  • Make db2 async api more stable.

  • Assertions for dumb stuff.

  • Abstract interface for other adapters? Move dbic++ to Swift::DBI::(Adapter, Pool, Result, Statment etc.)

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.