em_postgresql 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,43 @@
1
+ em_postgresql
2
+ ---------------
3
+
4
+ An EventMachine-aware driver for using Postgresql with ActiveRecord.
5
+
6
+ Requirements
7
+ ==============
8
+
9
+ * Ruby 1.9
10
+ * EventMachine 0.12.10
11
+ * postgres-pr 0.6.1
12
+ * Rails 2.3.5
13
+
14
+ Tested with these version, other versions might work. YMMV.
15
+
16
+ You CANNOT have the **pg** gem installed. ActiveRecord prefers the **pg** gem but this code requires
17
+ the **postgres-pr** gem to be loaded. I'm not sure if there is a way to make them live together in harmony.
18
+
19
+ You'll need to ensure your code is running within an active Fiber using the FiberPool defined in fiber_pool.rb. If you are running Rails in Thin, the following code is a good place to start to figure out how to do this:
20
+
21
+ <http://github.com/espace/neverblock/blob/master/lib/never_block/servers/thin.rb>
22
+
23
+ Usage
24
+ =======
25
+
26
+ List this gem in your `config/environment.rb`:
27
+
28
+ config.gem 'postgres-pr', :lib => false
29
+ config.gem 'em_postgresql', :lib => false
30
+
31
+ and update your `config/database.yml` to contain the proper adapter attribute:
32
+
33
+ adapter: em_postgresql
34
+
35
+
36
+ Author
37
+ =========
38
+
39
+ Mike Perham, mperham AT gmail.com,
40
+ [Github](http://github.com/mperham),
41
+ [Twitter](http://twitter.com/mperham),
42
+ [Blog](http://mikeperham.com)
43
+
@@ -0,0 +1,31 @@
1
+ # vim: syntax=Ruby
2
+ require 'rubygems'
3
+ require 'rake/testtask'
4
+
5
+ begin
6
+ require 'jeweler'
7
+ Jeweler::Tasks.new do |s|
8
+ s.name = "em_postgresql"
9
+ s.summary = s.description = "An ActiveRecord driver for using Postgresql with EventMachine"
10
+ s.email = "mperham@gmail.com"
11
+ s.homepage = "http://github.com/mperham/em_postgresql"
12
+ s.authors = ['Mike Perham']
13
+ s.files = FileList["[A-Z]*", "{lib,test}/**/*"]
14
+ s.test_files = FileList["test/test_*.rb"]
15
+ s.add_dependency 'postgres-pr', '>=0.6.1'
16
+ s.add_dependency 'eventmachine', '>=0.12.10'
17
+ end
18
+
19
+ rescue LoadError
20
+ puts "Jeweler not available. Install it for jeweler-related tasks with: sudo gem install jeweler"
21
+ end
22
+
23
+
24
+ task :gin => [:gemspec, :build] do
25
+ puts `gem install pkg/em_postgresql-#{File.read('VERSION').strip}.gem`
26
+ end
27
+
28
+
29
+ Rake::TestTask.new
30
+
31
+ task :default => :test
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.3.0
@@ -0,0 +1,116 @@
1
+ require 'postgres_connection'
2
+
3
+ require 'active_record'
4
+ require 'active_record/connection_adapters/postgresql_adapter'
5
+ require 'active_record/patches'
6
+
7
+ if !PGconn.respond_to?(:quote_ident)
8
+ def PGconn.quote_ident(name)
9
+ %("#{name}")
10
+ end
11
+ end
12
+
13
+ module ActiveRecord
14
+ module ConnectionAdapters
15
+
16
+ class EmPostgreSQLAdapter < PostgreSQLAdapter
17
+ # checkin :logi
18
+ # checkout :logo
19
+ #
20
+ # def logo
21
+ # puts "#{Fiber.current.object_id} #{self.object_id} checkout"
22
+ # end
23
+ # def logi
24
+ # puts "#{Fiber.current.object_id} #{self.object_id} checkin"
25
+ # end
26
+
27
+ def initialize(connection, logger, host_parameters, connection_parameters, config)
28
+ @hostname = host_parameters[0]
29
+ @port = host_parameters[1]
30
+ @connect_parameters, @config = connection_parameters, config
31
+ super(connection, logger, nil, config)
32
+ end
33
+
34
+ def connect
35
+ @logger.info "Connecting to #{@hostname}:#{@port}"
36
+ @connection = ::EM.connect(@hostname, @port, ::EM::P::PostgresConnection)
37
+
38
+ fiber = Fiber.current
39
+ yielding = true
40
+ result = false
41
+ message = nil
42
+ task = @connection.connect(*@connect_parameters)
43
+ task.callback do |rc, msg|
44
+ result = rc
45
+ message = msg
46
+ fiber.resume
47
+ end
48
+ task.errback do |msg|
49
+ result = false
50
+ message = msg
51
+ yielding = false
52
+ end
53
+ Fiber.yield if yielding
54
+
55
+ raise RuntimeError, "Connection failed: #{message}" if !result
56
+
57
+ # Use escape string syntax if available. We cannot do this lazily when encountering
58
+ # the first string, because that could then break any transactions in progress.
59
+ # See: http://www.postgresql.org/docs/current/static/runtime-config-compatible.html
60
+ # If PostgreSQL doesn't know the standard_conforming_strings parameter then it doesn't
61
+ # support escape string syntax. Don't override the inherited quoted_string_prefix.
62
+ if supports_standard_conforming_strings?
63
+ self.class.instance_eval do
64
+ define_method(:quoted_string_prefix) { 'E' }
65
+ end
66
+ end
67
+
68
+ # Money type has a fixed precision of 10 in PostgreSQL 8.2 and below, and as of
69
+ # PostgreSQL 8.3 it has a fixed precision of 19. PostgreSQLColumn.extract_precision
70
+ # should know about this but can't detect it there, so deal with it here.
71
+ money_precision = (postgresql_version >= 80300) ? 19 : 10
72
+ PostgreSQLColumn.module_eval(<<-end_eval)
73
+ def extract_precision(sql_type) # def extract_precision(sql_type)
74
+ if sql_type =~ /^money$/ # if sql_type =~ /^money$/
75
+ #{money_precision} # 19
76
+ else # else
77
+ super # super
78
+ end # end
79
+ end # end
80
+ end_eval
81
+
82
+ configure_connection
83
+ @connection
84
+ end
85
+
86
+ def active?
87
+ !@connection.closed? && @connection.exec('SELECT 1')
88
+ rescue RuntimeError => re
89
+ false
90
+ end
91
+
92
+ end
93
+ end
94
+
95
+ class Base
96
+ # Establishes a connection to the database that's used by all Active Record objects
97
+ def self.em_postgresql_connection(config) # :nodoc:
98
+ config = config.symbolize_keys
99
+ host = config[:host]
100
+ port = config[:port] || 5432
101
+ username = config[:username].to_s if config[:username]
102
+ password = config[:password].to_s if config[:password]
103
+
104
+ if config.has_key?(:database)
105
+ database = config[:database]
106
+ else
107
+ raise ArgumentError, "No database specified. Missing argument: database."
108
+ end
109
+
110
+ # The postgres drivers don't allow the creation of an unconnected PGconn object,
111
+ # so just pass a nil connection object for the time being.
112
+ ConnectionAdapters::EmPostgreSQLAdapter.new(nil, logger, [host, port], [database, username, password], config)
113
+ end
114
+ end
115
+
116
+ end
@@ -0,0 +1,102 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+
4
+ def self.fiber_pools
5
+ @fiber_pools ||= []
6
+ end
7
+ def self.register_fiber_pool(fp)
8
+ fiber_pools << fp
9
+ end
10
+
11
+ class FiberedMonitor
12
+ class Queue
13
+ def initialize
14
+ @queue = []
15
+ end
16
+
17
+ def wait(timeout)
18
+ t = timeout || 5
19
+ fiber = Fiber.current
20
+ x = EM::Timer.new(t) do
21
+ @queue.delete(fiber)
22
+ fiber.resume(false)
23
+ end
24
+ @queue << fiber
25
+ returning Fiber.yield do
26
+ x.cancel
27
+ end
28
+ end
29
+
30
+ def signal
31
+ fiber = @queue.pop
32
+ fiber.resume(true) if fiber
33
+ end
34
+ end
35
+
36
+ def synchronize
37
+ yield
38
+ end
39
+
40
+ def new_cond
41
+ Queue.new
42
+ end
43
+ end
44
+
45
+ # ActiveRecord's connection pool is based on threads. Since we are working
46
+ # with EM and a single thread, multiple fiber design, we need to provide
47
+ # our own connection pool that keys off of Fiber.current so that different
48
+ # fibers running in the same thread don't try to use the same connection.
49
+ class ConnectionPool
50
+ def initialize(spec)
51
+ @spec = spec
52
+
53
+ # The cache of reserved connections mapped to threads
54
+ @reserved_connections = {}
55
+
56
+ # The mutex used to synchronize pool access
57
+ @connection_mutex = FiberedMonitor.new
58
+ @queue = @connection_mutex.new_cond
59
+
60
+ # default 5 second timeout unless on ruby 1.9
61
+ @timeout = spec.config[:wait_timeout] || 5
62
+
63
+ # default max pool size to 5
64
+ @size = (spec.config[:pool] && spec.config[:pool].to_i) || 5
65
+
66
+ @connections = []
67
+ @checked_out = []
68
+ end
69
+
70
+ private
71
+
72
+ def current_connection_id #:nodoc:
73
+ Fiber.current.object_id
74
+ end
75
+
76
+ # Remove stale fibers from the cache.
77
+ def remove_stale_cached_threads!(cache, &block)
78
+ keys = Set.new(cache.keys)
79
+
80
+ ActiveRecord::ConnectionAdapters.fiber_pools.each do |pool|
81
+ pool.busy_fibers.each_pair do |object_id, fiber|
82
+ keys.delete(object_id)
83
+ end
84
+ end
85
+ # puts "Pruning stale connections: #{f.busy_fibers.size} #{f.fibers.size} #{keys.inspect}"
86
+ keys.each do |key|
87
+ next unless cache.has_key?(key)
88
+ block.call(key, cache[key])
89
+ cache.delete(key)
90
+ end
91
+ end
92
+
93
+ def checkout_and_verify(c)
94
+ @checked_out << c
95
+ c.run_callbacks :checkout
96
+ c.verify!
97
+ c
98
+ end
99
+ end
100
+
101
+ end
102
+ end
@@ -0,0 +1,82 @@
1
+ # Author:: Mohammad A. Ali (mailto:oldmoe@gmail.com)
2
+ # Copyright:: Copyright (c) 2008 eSpace, Inc.
3
+ # License:: Distributes under the same terms as Ruby
4
+
5
+ require 'fiber'
6
+
7
+ class Fiber
8
+
9
+ #Attribute Reference--Returns the value of a fiber-local variable, using
10
+ #either a symbol or a string name. If the specified variable does not exist,
11
+ #returns nil.
12
+ def [](key)
13
+ local_fiber_variables[key]
14
+ end
15
+
16
+ #Attribute Assignment--Sets or creates the value of a fiber-local variable,
17
+ #using either a symbol or a string. See also Fiber#[].
18
+ def []=(key,value)
19
+ local_fiber_variables[key] = value
20
+ end
21
+
22
+ private
23
+
24
+ def local_fiber_variables
25
+ @local_fiber_variables ||= {}
26
+ end
27
+ end
28
+
29
+ class FiberPool
30
+
31
+ # gives access to the currently free fibers
32
+ attr_reader :fibers
33
+ attr_reader :busy_fibers
34
+
35
+ # Code can register a proc with this FiberPool to be called
36
+ # every time a Fiber is finished. Good for releasing resources
37
+ # like ActiveRecord database connections.
38
+ attr_accessor :generic_callbacks
39
+
40
+ # Prepare a list of fibers that are able to run different blocks of code
41
+ # every time. Once a fiber is done with its block, it attempts to fetch
42
+ # another one from the queue
43
+ def initialize(count = 100)
44
+ @fibers,@busy_fibers,@queue,@generic_callbacks = [],{},[],[]
45
+ count.times do |i|
46
+ fiber = Fiber.new do |block|
47
+ loop do
48
+ block.call
49
+ # callbacks are called in a reverse order, much like c++ destructor
50
+ Fiber.current[:callbacks].pop.call while Fiber.current[:callbacks].length > 0
51
+ generic_callbacks.each do |cb|
52
+ cb.call
53
+ end
54
+ unless @queue.empty?
55
+ block = @queue.shift
56
+ else
57
+ @busy_fibers.delete(Fiber.current.object_id)
58
+ @fibers << Fiber.current
59
+ block = Fiber.yield
60
+ end
61
+ end
62
+ end
63
+ fiber[:callbacks] = []
64
+ fiber[:em_keys] = []
65
+ @fibers << fiber
66
+ end
67
+ end
68
+
69
+ # If there is an available fiber use it, otherwise, leave it to linger
70
+ # in a queue
71
+ def spawn(&block)
72
+ if fiber = @fibers.shift
73
+ fiber[:callbacks] = []
74
+ @busy_fibers[fiber.object_id] = fiber
75
+ fiber.resume(block)
76
+ else
77
+ @queue << block
78
+ end
79
+ self # we are keen on hiding our queue
80
+ end
81
+
82
+ end
@@ -0,0 +1,209 @@
1
+ require 'eventmachine'
2
+ require 'postgres-pr/message'
3
+ require 'postgres-pr/connection'
4
+ require 'stringio'
5
+ require 'fiber'
6
+
7
+ class StringIO # :nodoc:
8
+ # Reads exactly +n+ bytes.
9
+ #
10
+ # If the data read is nil an EOFError is raised.
11
+ #
12
+ # If the data read is too short a TruncatedDataError is raised and the read
13
+ # data is obtainable via its #data method.
14
+ def readbytes(n)
15
+ str = read(n)
16
+ if str == nil
17
+ raise EOFError, "End of file reached"
18
+ end
19
+ if str.size < n
20
+ raise TruncatedDataError.new("data truncated", str)
21
+ end
22
+ str
23
+ end
24
+ alias read_exactly_n_bytes readbytes
25
+ end
26
+
27
+
28
+ module EventMachine
29
+ module Protocols
30
+ class PostgresConnection < EventMachine::Connection
31
+ include PostgresPR
32
+
33
+ def initialize
34
+ @data = ""
35
+ @params = {}
36
+ @connected = false
37
+ end
38
+
39
+ # Fibered impl for synchronous execution of SQL within EM
40
+ def exec(sql)
41
+ fiber = Fiber.current
42
+ # p [fiber.object_id, self.object_id, sql]
43
+ yielding = true
44
+ (status, result, errors) = nil
45
+ d = query(sql)
46
+ d.callback do |s, r, e|
47
+ (status, result, errors) = s, r, e
48
+ fiber.resume
49
+ end
50
+ d.errback do |msg|
51
+ errors = msg
52
+ status = false
53
+ # errback is called from the same fiber
54
+ yielding = false
55
+ end
56
+
57
+ Fiber.yield if yielding
58
+ # p [fiber.object_id, self.object_id, result]
59
+ return PGresult.new(result) if status
60
+ raise RuntimeError, (errors || result).inspect
61
+ end
62
+
63
+ def close
64
+ close_connection
65
+ end
66
+
67
+ def closed?
68
+ !@connected
69
+ end
70
+
71
+ def post_init
72
+ @connected = true
73
+ end
74
+
75
+ def unbind
76
+ @connected = false
77
+ if o = (@pending_query || @pending_conn)
78
+ o.succeed false, "lost connection"
79
+ end
80
+ end
81
+
82
+ def connect(db, user, psw=nil)
83
+ d = EM::DefaultDeferrable.new
84
+ d.timeout 15
85
+
86
+ if @pending_query || @pending_conn
87
+ d.fail "Operation already in progress"
88
+ else
89
+ @pending_conn = d
90
+ prms = {"user"=>user, "database"=>db}
91
+ @user = user
92
+ if psw
93
+ @password = psw
94
+ #prms["password"] = psw
95
+ end
96
+ send_data PostgresPR::StartupMessage.new( 3 << 16, prms ).dump
97
+ end
98
+
99
+ d
100
+ end
101
+
102
+ def query(sql)
103
+ d = EM::DefaultDeferrable.new
104
+ d.timeout 15
105
+
106
+ if !@connected
107
+ d.fail "Not connected"
108
+ elsif @pending_query || @pending_conn
109
+ d.fail "Operation already in progress"
110
+ else
111
+ @r = PostgresPR::Connection::Result.new
112
+ @e = []
113
+ @pending_query = d
114
+ send_data PostgresPR::Query.dump(sql)
115
+ end
116
+
117
+ d
118
+ end
119
+
120
+ def receive_data(data)
121
+ @data << data
122
+ while @data.length >= 5
123
+ pktlen = @data[1...5].unpack("N").first
124
+ if @data.length >= (1 + pktlen)
125
+ pkt = @data.slice!(0...(1+pktlen))
126
+ m = StringIO.open( pkt, "r" ) {|io| PostgresPR::Message.read( io ) }
127
+ if @pending_conn
128
+ dispatch_conn_message m
129
+ elsif @pending_query
130
+ dispatch_query_message m
131
+ else
132
+ raise "Unexpected message from database"
133
+ end
134
+ else
135
+ break # very important, break out of the while
136
+ end
137
+ end
138
+ end
139
+
140
+ # Cloned and modified from the postgres-pr.
141
+ def dispatch_conn_message(msg)
142
+ case msg
143
+ when AuthentificationClearTextPassword
144
+ raise ArgumentError, "no password specified" if @password.nil?
145
+ send_data PasswordMessage.new(@password).dump
146
+
147
+ when AuthentificationCryptPassword
148
+ raise ArgumentError, "no password specified" if @password.nil?
149
+ send_data PasswordMessage.new(@password.crypt(msg.salt)).dump
150
+
151
+ when AuthentificationMD5Password
152
+ raise ArgumentError, "no password specified" if @password.nil?
153
+ require 'digest/md5'
154
+
155
+ m = Digest::MD5.hexdigest(@password + @user)
156
+ m = Digest::MD5.hexdigest(m + msg.salt)
157
+ m = 'md5' + m
158
+ send_data PasswordMessage.new(m).dump
159
+
160
+ when AuthentificationKerberosV4, AuthentificationKerberosV5, AuthentificationSCMCredential
161
+ raise "unsupported authentification"
162
+
163
+ when AuthentificationOk
164
+ when ErrorResponse
165
+ raise msg.field_values.join("\t")
166
+ when NoticeResponse
167
+ @notice_processor.call(msg) if @notice_processor
168
+ when ParameterStatus
169
+ @params[msg.key] = msg.value
170
+ when BackendKeyData
171
+ # TODO
172
+ #p msg
173
+ when ReadyForQuery
174
+ # TODO: use transaction status
175
+ pc,@pending_conn = @pending_conn,nil
176
+ pc.succeed true
177
+ else
178
+ raise "unhandled message type"
179
+ end
180
+ end
181
+
182
+ # Cloned and modified from the postgres-pr.
183
+ def dispatch_query_message(msg)
184
+ case msg
185
+ when DataRow
186
+ @r.rows << msg.columns
187
+ when CommandComplete
188
+ @r.cmd_tag = msg.cmd_tag
189
+ when ReadyForQuery
190
+ pq,@pending_query = @pending_query,nil
191
+ pq.succeed @e.size == 0, @r, @e
192
+ when RowDescription
193
+ @r.fields = msg.fields
194
+ when CopyInResponse
195
+ when CopyOutResponse
196
+ when EmptyQueryResponse
197
+ when ErrorResponse
198
+ @e << msg.field_values[2]
199
+ when NoticeResponse
200
+ @notice_processor.call(msg) if @notice_processor
201
+ when ParameterStatus
202
+ else
203
+ # TODO
204
+ puts "Unknown Postgres message: #{msg}"
205
+ end
206
+ end
207
+ end
208
+ end
209
+ end
@@ -0,0 +1,10 @@
1
+ test:
2
+ adapter: em_postgresql
3
+ host: localhost
4
+ username: mike
5
+ password: password
6
+ database: onespot_test
7
+ schema_search_path: bdu,public
8
+ statement_timeout: 60
9
+ encoding: UTF8
10
+ port: 5432
@@ -0,0 +1,51 @@
1
+ require 'rubygems'
2
+ require 'logger'
3
+ require 'yaml'
4
+ require 'erb'
5
+
6
+ gem 'activerecord', '>= 2.3.5'
7
+ require 'active_record'
8
+
9
+ RAILS_ENV='test'
10
+
11
+ ActiveRecord::Base.configurations = YAML::load(ERB.new(File.read(File.join(File.dirname(__FILE__), 'database.yml'))).result)
12
+ ActiveRecord::Base.default_timezone = :utc
13
+ ActiveRecord::Base.logger = Logger.new(STDOUT)
14
+ ActiveRecord::Base.logger.level = Logger::INFO
15
+ ActiveRecord::Base.pluralize_table_names = false
16
+ ActiveRecord::Base.time_zone_aware_attributes = true
17
+ Time.zone = 'UTC'
18
+
19
+ require 'eventmachine'
20
+ require 'test/unit'
21
+
22
+ class Site < ActiveRecord::Base
23
+ set_table_name 'site'
24
+ end
25
+
26
+ class TestDatabase < Test::Unit::TestCase
27
+ def test_live_server
28
+ EM.run do
29
+ Fiber.new do
30
+ ActiveRecord::Base.establish_connection
31
+
32
+ result = ActiveRecord::Base.connection.query('select id, domain_name from site')
33
+ assert result
34
+ assert_equal 3, result.size
35
+
36
+ result = Site.all
37
+ assert result
38
+ assert_equal 3, result.size
39
+
40
+ result = Site.find(1)
41
+ assert_equal 1, result.id
42
+ assert_equal 'somedomain.com', result.domain_name
43
+ end.resume
44
+
45
+ EM.add_timer(1) do
46
+ EM.stop
47
+ end
48
+
49
+ end
50
+ end
51
+ end
metadata ADDED
@@ -0,0 +1,97 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: em_postgresql
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 3
8
+ - 0
9
+ version: 0.3.0
10
+ platform: ruby
11
+ authors:
12
+ - Mike Perham
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-04-03 00:00:00 -05:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: postgres-pr
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ segments:
28
+ - 0
29
+ - 6
30
+ - 1
31
+ version: 0.6.1
32
+ type: :runtime
33
+ version_requirements: *id001
34
+ - !ruby/object:Gem::Dependency
35
+ name: eventmachine
36
+ prerelease: false
37
+ requirement: &id002 !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ segments:
42
+ - 0
43
+ - 12
44
+ - 10
45
+ version: 0.12.10
46
+ type: :runtime
47
+ version_requirements: *id002
48
+ description: An ActiveRecord driver for using Postgresql with EventMachine
49
+ email: mperham@gmail.com
50
+ executables: []
51
+
52
+ extensions: []
53
+
54
+ extra_rdoc_files:
55
+ - README.md
56
+ files:
57
+ - README.md
58
+ - Rakefile
59
+ - VERSION
60
+ - lib/active_record/connection_adapters/em_postgresql_adapter.rb
61
+ - lib/active_record/patches.rb
62
+ - lib/fiber_pool.rb
63
+ - lib/postgres_connection.rb
64
+ - test/database.yml
65
+ - test/test_database.rb
66
+ has_rdoc: true
67
+ homepage: http://github.com/mperham/em_postgresql
68
+ licenses: []
69
+
70
+ post_install_message:
71
+ rdoc_options:
72
+ - --charset=UTF-8
73
+ require_paths:
74
+ - lib
75
+ required_ruby_version: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ segments:
80
+ - 0
81
+ version: "0"
82
+ required_rubygems_version: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ segments:
87
+ - 0
88
+ version: "0"
89
+ requirements: []
90
+
91
+ rubyforge_project:
92
+ rubygems_version: 1.3.6
93
+ signing_key:
94
+ specification_version: 3
95
+ summary: An ActiveRecord driver for using Postgresql with EventMachine
96
+ test_files:
97
+ - test/test_database.rb