swift 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
+