swift 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/LICENSE +20 -0
- data/README.rdoc +236 -0
- data/Rakefile +40 -0
- data/VERSION +1 -0
- data/examples/async.rb +60 -0
- data/examples/db.rb +40 -0
- data/examples/scheme.rb +46 -0
- data/ext/extconf.rb +39 -0
- data/ext/swift.cc +756 -0
- data/lib/swift.rb +48 -0
- data/lib/swift/adapter.rb +120 -0
- data/lib/swift/attribute.rb +25 -0
- data/lib/swift/db.rb +39 -0
- data/lib/swift/header.rb +45 -0
- data/lib/swift/identity_map.rb +41 -0
- data/lib/swift/pool.rb +74 -0
- data/lib/swift/scheme.rb +70 -0
- data/lib/swift/type.rb +12 -0
- data/swift.gemspec +75 -0
- data/test/helper.rb +31 -0
- data/test/house-explode.jpg +0 -0
- data/test/test_adapter.rb +127 -0
- data/test/test_encoding.rb +40 -0
- data/test/test_identity_map.rb +17 -0
- data/test/test_io.rb +27 -0
- data/test/test_timestamps.rb +27 -0
- metadata +109 -0
data/lib/swift.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
# Extension.
|
2
|
+
require_relative '../ext/swift'
|
3
|
+
require_relative 'swift/adapter'
|
4
|
+
require_relative 'swift/attribute'
|
5
|
+
require_relative 'swift/db'
|
6
|
+
require_relative 'swift/header'
|
7
|
+
require_relative 'swift/scheme'
|
8
|
+
require_relative 'swift/type'
|
9
|
+
|
10
|
+
module Swift
|
11
|
+
class << self
|
12
|
+
def setup name, type, options = {}
|
13
|
+
unless type.kind_of?(Class) && type < Swift::Adapter
|
14
|
+
raise TypeError, "Expected +type+ Swift::Adapter subclass but got #{type.inspect}"
|
15
|
+
end
|
16
|
+
(@repositories ||= {})[name] = type.new(options)
|
17
|
+
end
|
18
|
+
|
19
|
+
def db name = nil, &block
|
20
|
+
# I pilfered the logic from DM but I don't really understand what is/isn't thread safe.
|
21
|
+
scopes = (Thread.current[:swift_db] ||= [])
|
22
|
+
repository = if name || scopes.empty?
|
23
|
+
@repositories[name || :default] or raise "Unknown db '#{name || :default}', did you forget to #setup?"
|
24
|
+
else
|
25
|
+
scopes.last
|
26
|
+
end
|
27
|
+
|
28
|
+
if block_given?
|
29
|
+
begin
|
30
|
+
scopes.push(repository)
|
31
|
+
block.call(repository)
|
32
|
+
ensure
|
33
|
+
scopes.pop
|
34
|
+
end
|
35
|
+
end
|
36
|
+
repository
|
37
|
+
end
|
38
|
+
|
39
|
+
def schema
|
40
|
+
@schema ||= []
|
41
|
+
end
|
42
|
+
|
43
|
+
def migrate! name = nil
|
44
|
+
db(name){ schema.each(&:migrate!)}
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end # Swift
|
48
|
+
|
@@ -0,0 +1,120 @@
|
|
1
|
+
module Swift
|
2
|
+
#--
|
3
|
+
# TODO: Still not convinced all and first are necessary.
|
4
|
+
class Adapter
|
5
|
+
attr_reader :options
|
6
|
+
|
7
|
+
def identity_map
|
8
|
+
@identity_map ||= IdentityMap.new
|
9
|
+
end
|
10
|
+
|
11
|
+
def get scheme, keys
|
12
|
+
relation = scheme.new(keys)
|
13
|
+
prepare_get(scheme).execute(*relation.tuple.values_at(*scheme.header.keys)).first
|
14
|
+
end
|
15
|
+
|
16
|
+
def all scheme, conditions = '', *binds, &block
|
17
|
+
where = "where #{exchange_names(scheme, conditions)}" unless conditions.empty?
|
18
|
+
prepare(scheme, "select * from #{scheme.store} #{where}").execute(*binds, &block)
|
19
|
+
end
|
20
|
+
|
21
|
+
def first scheme, conditions = '', *binds, &block
|
22
|
+
all(scheme, "#{conditions} limit 1", *binds, &block).first
|
23
|
+
end
|
24
|
+
|
25
|
+
def create scheme, *relations
|
26
|
+
statement = prepare_create(scheme)
|
27
|
+
relations.map do |relation|
|
28
|
+
relation = scheme.new(relation) unless relation.kind_of?(scheme)
|
29
|
+
if statement.execute(*relation.tuple.values_at(*scheme.header.insertable)) && scheme.header.serial
|
30
|
+
relation.tuple[scheme.header.serial] = statement.insert_id
|
31
|
+
end
|
32
|
+
relation
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def update scheme, *relations
|
37
|
+
statement = prepare_update(scheme)
|
38
|
+
relations.map do |relation|
|
39
|
+
relation = scheme.new(relation) unless relation.kind_of?(scheme)
|
40
|
+
statement.execute(*relation.tuple.values_at(*scheme.header.updatable, *scheme.header.keys))
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def destroy scheme, *relations
|
45
|
+
statement = prepare_destroy(scheme)
|
46
|
+
relations.map do |relation|
|
47
|
+
relation = scheme.new(relation) unless relation.kind_of?(scheme)
|
48
|
+
if result = statement.execute(*relation.tuple.values_at(*scheme.header.keys))
|
49
|
+
relation.freeze
|
50
|
+
end
|
51
|
+
result
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def migrate! scheme
|
56
|
+
keys = scheme.header.keys
|
57
|
+
fields = scheme.header.map{|p| field_definition(p)}.join(', ')
|
58
|
+
fields += ", primary key (#{keys.join(', ')})" unless keys.empty?
|
59
|
+
|
60
|
+
execute("drop table if exists #{scheme.store}")
|
61
|
+
execute("create table #{scheme.store} (#{fields})")
|
62
|
+
end
|
63
|
+
|
64
|
+
protected
|
65
|
+
def exchange_names scheme, query
|
66
|
+
query.gsub(/:(\w+)/){ scheme.send($1.to_sym).field }
|
67
|
+
end
|
68
|
+
|
69
|
+
def returning?
|
70
|
+
raise NotImplementedError
|
71
|
+
end
|
72
|
+
|
73
|
+
def prepare_cached scheme, name, &block
|
74
|
+
@prepared ||= Hash.new{|h,k| h[k] = Hash.new} # Autovivification please Matz!
|
75
|
+
@prepared[scheme][name] ||= prepare(scheme, yield)
|
76
|
+
end
|
77
|
+
|
78
|
+
def prepare_get scheme
|
79
|
+
prepare_cached(scheme, :get) do
|
80
|
+
where = scheme.header.keys.map{|key| "#{key} = ?"}.join(' and ')
|
81
|
+
"select * from #{scheme.store} where #{where} limit 1"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def prepare_create scheme
|
86
|
+
prepare_cached(scheme, :create) do
|
87
|
+
values = (['?'] * scheme.header.insertable.size).join(', ')
|
88
|
+
returning = "returning #{scheme.header.serial}" if scheme.header.serial and returning?
|
89
|
+
"insert into #{scheme.store} (#{scheme.header.insertable.join(', ')}) values (#{values}) #{returning}"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def prepare_update scheme
|
94
|
+
prepare_cached(scheme, :update) do
|
95
|
+
set = scheme.header.updatable.map{|field| "#{field} = ?"}.join(', ')
|
96
|
+
where = scheme.header.keys.map{|key| "#{key} = ?"}.join(' and ')
|
97
|
+
"update #{scheme.store} set #{set} where #{where}"
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def prepare_destroy scheme
|
102
|
+
prepare_cached(scheme, :destroy) do
|
103
|
+
where = scheme.header.keys.map{|key| "#{key} = ?"}.join(' and ')
|
104
|
+
"delete from #{scheme.store} where #{where}"
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def field_definition attribute
|
109
|
+
"#{attribute.field} " + case attribute
|
110
|
+
when Type::String then 'text'
|
111
|
+
when Type::Integer then attribute.serial ? 'serial' : 'integer'
|
112
|
+
when Type::Float then 'float'
|
113
|
+
when Type::BigDecimal then 'numeric'
|
114
|
+
when Type::Time then 'timestamp'
|
115
|
+
when Type::Boolean then 'boolean'
|
116
|
+
else 'text'
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end # Adapter
|
120
|
+
end # Swift
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Swift
|
2
|
+
class Attribute
|
3
|
+
attr_reader :name, :field, :key, :serial
|
4
|
+
|
5
|
+
def initialize scheme, name, options = {}
|
6
|
+
@name = name
|
7
|
+
@default = options.fetch(:default, nil)
|
8
|
+
@field = options.fetch(:field, name)
|
9
|
+
@key = options.fetch(:key, false)
|
10
|
+
@serial = options.fetch(:serial, false)
|
11
|
+
define_scheme_methods(scheme)
|
12
|
+
end
|
13
|
+
|
14
|
+
def default
|
15
|
+
@default.respond_to?(:call) ? @default.call : (@default.nil? ? nil : @default.dup)
|
16
|
+
end
|
17
|
+
|
18
|
+
def define_scheme_methods scheme
|
19
|
+
scheme.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
20
|
+
def #{name}; tuple.fetch(:#{field}) end
|
21
|
+
def #{name}= value; tuple.store(:#{field}, value) end
|
22
|
+
RUBY
|
23
|
+
end
|
24
|
+
end # Attribute
|
25
|
+
end # Swift
|
data/lib/swift/db.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
module Swift
|
2
|
+
module DB
|
3
|
+
class Mysql < Adapter
|
4
|
+
def initialize options = {}
|
5
|
+
super options.update(driver: 'mysql')
|
6
|
+
execute('select unix_timestamp() - unix_timestamp(utc_timestamp()) as offset') {|r| @tzoffset = r[:offset] }
|
7
|
+
end
|
8
|
+
|
9
|
+
def timezone *args
|
10
|
+
super(*args)
|
11
|
+
execute('select unix_timestamp() - unix_timestamp(utc_timestamp()) as offset') {|r| @tzoffset = r[:offset] }
|
12
|
+
@tzoffset
|
13
|
+
end
|
14
|
+
|
15
|
+
def returning?
|
16
|
+
false
|
17
|
+
end
|
18
|
+
end # Mysql
|
19
|
+
|
20
|
+
class Postgres < Adapter
|
21
|
+
def initialize options = {}
|
22
|
+
super options.update(driver: 'postgresql')
|
23
|
+
sql = "select extract(epoch from now())::bigint - extract(epoch from now() at time zone 'UTC')::bigint"
|
24
|
+
execute('%s as offset' % sql) {|r| @tzoffset = r[:offset] }
|
25
|
+
end
|
26
|
+
|
27
|
+
def timezone *args
|
28
|
+
super(*args)
|
29
|
+
sql = "select extract(epoch from now())::bigint - extract(epoch from now() at time zone 'UTC')::bigint"
|
30
|
+
execute('%s as offset' % sql) {|r| @tzoffset = r[:offset] }
|
31
|
+
@tzoffset
|
32
|
+
end
|
33
|
+
|
34
|
+
def returning?
|
35
|
+
true
|
36
|
+
end
|
37
|
+
end # Postgres
|
38
|
+
end # DB
|
39
|
+
end # Swift
|
data/lib/swift/header.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
module Swift
|
2
|
+
class Header
|
3
|
+
include Enumerable
|
4
|
+
|
5
|
+
def initialize *attributes
|
6
|
+
@attributes = {}
|
7
|
+
push *attributes unless attributes.empty?
|
8
|
+
end
|
9
|
+
|
10
|
+
def new_tuple
|
11
|
+
Hash[insertable.map{|field| [field, @attributes[field].default]}]
|
12
|
+
end
|
13
|
+
|
14
|
+
def push *attributes
|
15
|
+
@attributes.update Hash[attributes.map{|attribute| [attribute.field, attribute]}]
|
16
|
+
end
|
17
|
+
|
18
|
+
def insertable
|
19
|
+
@insertable ||= all - [serial]
|
20
|
+
end
|
21
|
+
|
22
|
+
def updatable
|
23
|
+
@updatable ||= all - (keys | [serial])
|
24
|
+
end
|
25
|
+
|
26
|
+
def all
|
27
|
+
@all ||= @attributes.keys
|
28
|
+
end
|
29
|
+
|
30
|
+
def serial
|
31
|
+
return @serial if defined? @serial
|
32
|
+
serial = find(&:serial)
|
33
|
+
@serial = serial ? serial.field : nil
|
34
|
+
end
|
35
|
+
|
36
|
+
def keys
|
37
|
+
@keys ||= select(&:key).map(&:field)
|
38
|
+
end
|
39
|
+
|
40
|
+
def each &block
|
41
|
+
@attributes.values.each{|v| yield v}
|
42
|
+
end
|
43
|
+
end # Header
|
44
|
+
end # Swift
|
45
|
+
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Swift
|
2
|
+
# Weak hash set.
|
3
|
+
#--
|
4
|
+
# TODO: Is 'hash set' the real name for a hash where both the keys and values must be unique?
|
5
|
+
class IdentityMap
|
6
|
+
def initialize
|
7
|
+
@cache, @reverse_cache, @finalize = {}, {}, method(:finalize)
|
8
|
+
end
|
9
|
+
|
10
|
+
def get key
|
11
|
+
value_id = @cache[key]
|
12
|
+
return ObjectSpace._id2ref(value_id) unless value_id.nil?
|
13
|
+
nil
|
14
|
+
end
|
15
|
+
|
16
|
+
#--
|
17
|
+
# TODO: Barf if the value.object_id already exists in the cache.
|
18
|
+
def set key, value
|
19
|
+
@reverse_cache[value.object_id] = key
|
20
|
+
@cache[key] = value.object_id
|
21
|
+
ObjectSpace.define_finalizer(value, @finalize)
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
def finalize value_id
|
26
|
+
@cache.delete @reverse_cache.delete value_id
|
27
|
+
end
|
28
|
+
end # IdentityMap
|
29
|
+
|
30
|
+
class Scheme
|
31
|
+
def self.load tuple
|
32
|
+
im = [self, *tuple.values_at(*header.keys)]
|
33
|
+
unless scheme = Swift.db.identity_map.get(im)
|
34
|
+
scheme = allocate
|
35
|
+
scheme.tuple = tuple
|
36
|
+
Swift.db.identity_map.set(im, scheme)
|
37
|
+
end
|
38
|
+
scheme
|
39
|
+
end
|
40
|
+
end # Scheme
|
41
|
+
end # Swift
|
data/lib/swift/pool.rb
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
|
3
|
+
module Swift
|
4
|
+
class Pool
|
5
|
+
module Handler
|
6
|
+
def initialize request, pool
|
7
|
+
@request, @pool = request, pool
|
8
|
+
end
|
9
|
+
|
10
|
+
def socket
|
11
|
+
@request.socket
|
12
|
+
end
|
13
|
+
|
14
|
+
def notify_readable
|
15
|
+
if @request.process
|
16
|
+
detach
|
17
|
+
@pool.detach self
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end # Handler
|
21
|
+
|
22
|
+
def initialize size, options
|
23
|
+
@pool = Swift::ConnectionPool.new size, options
|
24
|
+
@stop_reactor = EM.reactor_running? ? false : true
|
25
|
+
@pending = {}
|
26
|
+
@queue = []
|
27
|
+
end
|
28
|
+
|
29
|
+
def attach c
|
30
|
+
@pending[c] = true
|
31
|
+
end
|
32
|
+
|
33
|
+
def detach c
|
34
|
+
@pending.delete(c)
|
35
|
+
if @queue.empty?
|
36
|
+
EM.stop if @stop_reactor && @pending.empty?
|
37
|
+
else
|
38
|
+
sql, bind, callback = @queue.shift
|
39
|
+
execute(sql, *bind, &callback)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def attached? fd
|
44
|
+
@pending.keys.select{|c| c.socket == fd}.length > 0
|
45
|
+
end
|
46
|
+
|
47
|
+
def execute sql, *bind, &callback
|
48
|
+
request = @pool.execute sql, *bind, &callback
|
49
|
+
# TODO EM throws exceptions in C++ land which are not trapped in the extension.
|
50
|
+
# This is somehow causing everything to unravel and result in a segfault which
|
51
|
+
# I cannot track down. I'll buy a beer for someone who can get this fixed :)
|
52
|
+
# Oh, here it throws an exception if we try to attach same fd twice.
|
53
|
+
if request && !attached?(request.socket)
|
54
|
+
EM.watch(request.socket, Handler, request, self) do |c|
|
55
|
+
attach c
|
56
|
+
c.notify_writable = false
|
57
|
+
c.notify_readable = true
|
58
|
+
end
|
59
|
+
else
|
60
|
+
@queue << [ sql, bind, callback ]
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def run &block
|
65
|
+
EM.run{ yield self }
|
66
|
+
end
|
67
|
+
end # Pool
|
68
|
+
|
69
|
+
def self.pool size, name = :default, &block
|
70
|
+
pool = Pool.new(size, Swift.db(name).options)
|
71
|
+
pool.run(&block) if block_given?
|
72
|
+
pool
|
73
|
+
end
|
74
|
+
end # Swift
|
data/lib/swift/scheme.rb
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
module Swift
|
2
|
+
class Scheme
|
3
|
+
attr_accessor :tuple
|
4
|
+
alias_method :scheme, :class
|
5
|
+
|
6
|
+
def initialize options = {}
|
7
|
+
@tuple = scheme.header.new_tuple
|
8
|
+
options.each{|k, v| send(:"#{k}=", v)}
|
9
|
+
end
|
10
|
+
|
11
|
+
def update options = {}
|
12
|
+
options.each{|k, v| send(:"#{k}=", v)}
|
13
|
+
Swift.db.update(scheme, self)
|
14
|
+
end
|
15
|
+
|
16
|
+
def destroy
|
17
|
+
Swift.db.destroy(scheme, self)
|
18
|
+
end
|
19
|
+
|
20
|
+
class << self
|
21
|
+
attr_accessor :header
|
22
|
+
|
23
|
+
def inherited klass
|
24
|
+
klass.header = Header.new
|
25
|
+
klass.header.push(*header) if header
|
26
|
+
Swift.schema.push(klass) if klass.name
|
27
|
+
end
|
28
|
+
|
29
|
+
def load tuple
|
30
|
+
scheme = allocate
|
31
|
+
scheme.tuple = tuple
|
32
|
+
scheme
|
33
|
+
end
|
34
|
+
|
35
|
+
def attribute name, type, options = {}
|
36
|
+
header.push(attribute = type.new(self, name, options))
|
37
|
+
(class << self; self end).send(:define_method, name, lambda{ attribute })
|
38
|
+
end
|
39
|
+
|
40
|
+
def store name = nil
|
41
|
+
name ? @store = name : @store
|
42
|
+
end
|
43
|
+
|
44
|
+
def migration &migration
|
45
|
+
(class << self; self end).send(:define_method, :migrate!, lambda{ Swift.db.instance_eval(&migration) })
|
46
|
+
end
|
47
|
+
|
48
|
+
def migrate!
|
49
|
+
Swift.db.migrate!(self)
|
50
|
+
end
|
51
|
+
|
52
|
+
def create options = {}
|
53
|
+
Swift.db.create(self, options)
|
54
|
+
end
|
55
|
+
|
56
|
+
def get keys
|
57
|
+
Swift.db.get(self, keys)
|
58
|
+
end
|
59
|
+
|
60
|
+
def all conditions = '', *binds, &block
|
61
|
+
Swift.db.all(self, conditions, *binds, &block)
|
62
|
+
end
|
63
|
+
|
64
|
+
def first conditions = '', *binds, &block
|
65
|
+
Swift.db.first(self, conditions, *binds, &block)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end # Scheme
|
69
|
+
end # Swift
|
70
|
+
|