sequel 0.1.8 → 0.1.9

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG CHANGED
@@ -1,3 +1,27 @@
1
+ === 0.1.9 (2007-07-21)
2
+
3
+ * Fixed #update_sql and #insert_sql to support field quoting by calling #field_name.
4
+
5
+ * Implemented automatic data type conversion in mysql adapter.
6
+
7
+ * Added support for boolean literals in mysql adapter.
8
+
9
+ * Added support for ORDER and LIMIT clauses in UPDATE statements in mysql adapter.
10
+
11
+ * Implemented correct field quoting (using back-ticks) in mysql adapter.
12
+
13
+ * Wrote basic MySQL spec.
14
+
15
+ * Fixd MySQL::Dataset to return correct data types with symbols as hash keys.
16
+
17
+ * Removed discunctional MySQL::Database#transaction.
18
+
19
+ * Added support for single threaded operation.
20
+
21
+ * Fixed bug in Dataset#format_eq_expression where Range objects would not be literalized correctly.
22
+
23
+ * Added parens around postgres LIKE expressions using regexps.
24
+
1
25
  === 0.1.8 (2007-07-10)
2
26
 
3
27
  * Implemented Dataset#columns for retrieving the columns in the result set.
data/README CHANGED
@@ -32,7 +32,7 @@ Sequel currently supports:
32
32
  * SQLite 3
33
33
  * DBI
34
34
  * ODBC
35
- * MySQL (basic support)
35
+ * MySQL
36
36
 
37
37
  == The Sequel Console
38
38
 
data/Rakefile CHANGED
@@ -6,7 +6,7 @@ require 'fileutils'
6
6
  include FileUtils
7
7
 
8
8
  NAME = "sequel"
9
- VERS = "0.1.8"
9
+ VERS = "0.1.9"
10
10
  CLEAN.include ['**/.*.sw?', 'pkg/*', '.config', 'doc/*', 'coverage/*']
11
11
  RDOC_OPTS = ['--quiet', '--title', "Sequel: Concise ORM for Ruby",
12
12
  "--opname", "index.html",
@@ -38,6 +38,11 @@ class String
38
38
  def expr
39
39
  Sequel::ExpressionString.new(self)
40
40
  end
41
+
42
+ # Convert a string into a Time object
43
+ def to_time
44
+ Time.parse(self)
45
+ end
41
46
  end
42
47
 
43
48
  # Symbol extensions
@@ -5,6 +5,25 @@ require File.join(File.dirname(__FILE__), 'dataset')
5
5
  require File.join(File.dirname(__FILE__), 'model')
6
6
 
7
7
  module Sequel
8
+ # A SingleThreadedPool acts as a replacement for a ConnectionPool for use
9
+ # in single-threaded applications. ConnectionPool imposes a substantial
10
+ # performance penalty, so SingleThreadedPool is used to gain some speed.
11
+ class SingleThreadedPool
12
+ attr_writer :connection_proc
13
+
14
+ def initialize(&block)
15
+ @connection_proc = block
16
+ end
17
+
18
+ def hold
19
+ @conn ||= @connection_proc.call
20
+ yield @conn
21
+ rescue Exception => e
22
+ # if the error is not a StandardError it is converted into RuntimeError.
23
+ raise e.is_a?(StandardError) ? e : e.message
24
+ end
25
+ end
26
+
8
27
  # A Database object represents a virtual connection to a database.
9
28
  # The Database class is meant to be subclassed by database adapters in order
10
29
  # to provide the functionality needed for executing queries.
@@ -18,15 +37,32 @@ module Sequel
18
37
  def initialize(opts = {}, &block)
19
38
  Model.database_opened(self)
20
39
  @opts = opts
21
- @pool = ConnectionPool.new(@opts[:max_connections] || 4, &block)
22
- @logger = opts[:logger]
40
+
41
+ # Determine if the DB is single threaded or multi threaded
42
+ @single_threaded = opts[:single_threaded] || @@single_threaded
43
+ # Construct pool
44
+ if @single_threaded
45
+ @pool = SingleThreadedPool.new(&block)
46
+ else
47
+ @pool = ConnectionPool.new(opts[:max_connections] || 4, &block)
48
+ end
23
49
  @pool.connection_proc = block || proc {connect}
50
+
51
+ @logger = opts[:logger]
24
52
  end
25
53
 
26
54
  def connect
27
55
  raise NotImplementedError, "#connect should be overriden by adapters"
28
56
  end
29
57
 
58
+ def multi_threaded?
59
+ !@single_threaded
60
+ end
61
+
62
+ def single_threaded?
63
+ @single_threaded
64
+ end
65
+
30
66
  def uri
31
67
  uri = URI::Generic.new(
32
68
  self.class.adapter_scheme.to_s,
@@ -188,6 +224,12 @@ module Sequel
188
224
  raise SequelError, "Invalid database scheme" unless c
189
225
  c.new(c.uri_to_options(uri).merge(more_opts || {}))
190
226
  end
227
+
228
+ @@single_threaded = false
229
+
230
+ def self.single_threaded=(value)
231
+ @@single_threaded = value
232
+ end
191
233
  end
192
234
  end
193
235
 
@@ -12,7 +12,8 @@ module Sequel
12
12
  # Returns the first value of the first reecord in the dataset.
13
13
  def single_value(opts = nil)
14
14
  opts = opts ? NAKED_HASH.merge(opts) : NAKED_HASH
15
- each(opts) {|r| return r.values.first}
15
+ # reset the columns cache so it won't fuck subsequent calls to columns
16
+ each(opts) {|r| @columns = nil; return r.values.first}
16
17
  end
17
18
 
18
19
  # Returns the first record in the dataset. If the num argument is specified,
@@ -96,8 +96,8 @@ module Sequel
96
96
  case right
97
97
  when Range:
98
98
  right.exclude_end? ? \
99
- "(#{left} >= #{right.begin} AND #{left} < #{right.end})" : \
100
- "(#{left} >= #{right.begin} AND #{left} <= #{right.end})"
99
+ "(#{left} >= #{literal(right.begin)} AND #{left} < #{literal(right.end)})" : \
100
+ "(#{left} >= #{literal(right.begin)} AND #{left} <= #{literal(right.end)})"
101
101
  when Array:
102
102
  "(#{left} IN (#{literal(right)}))"
103
103
  when Dataset:
@@ -464,7 +464,7 @@ module Sequel
464
464
  field_list = []
465
465
  value_list = []
466
466
  values[0].each do |k, v|
467
- field_list << k
467
+ field_list << field_name(k)
468
468
  value_list << literal(v)
469
469
  end
470
470
  fl = field_list.join(COMMA_SEPARATOR)
@@ -488,7 +488,7 @@ module Sequel
488
488
  raise SequelError, "Can't update a joined dataset"
489
489
  end
490
490
 
491
- set_list = values.map {|k, v| "#{k} = #{literal(v)}"}.
491
+ set_list = values.map {|k, v| "#{field_name(k)} = #{literal(v)}"}.
492
492
  join(COMMA_SEPARATOR)
493
493
  sql = "UPDATE #{@opts[:from]} SET #{set_list}"
494
494
 
data/lib/sequel/mysql.rb CHANGED
@@ -4,21 +4,74 @@ end
4
4
 
5
5
  require 'mysql'
6
6
 
7
+ # Monkey patch Mysql::Result to yield hashes with symbol keys
8
+ class Mysql::Result
9
+ MYSQL_TYPES = {
10
+ 0 => :to_i,
11
+ 1 => :to_i,
12
+ 2 => :to_i,
13
+ 3 => :to_i,
14
+ 4 => :to_f,
15
+ 5 => :to_f,
16
+ 7 => :to_time,
17
+ 8 => :to_i,
18
+ 9 => :to_i,
19
+ 10 => :to_time,
20
+ 11 => :to_time,
21
+ 12 => :to_time,
22
+ 13 => :to_i,
23
+ 14 => :to_time,
24
+ 247 => :to_i,
25
+ 248 => :to_i
26
+ }
27
+
28
+ def convert_type(v, type)
29
+ v ? ((t = MYSQL_TYPES[type]) ? v.send(t) : v) : nil
30
+ end
31
+
32
+ def columns(with_table = nil)
33
+ unless @columns
34
+ @column_types = []
35
+ @columns = fetch_fields.map do |f|
36
+ @column_types << f.type
37
+ (with_table ? (f.table + "." + f.name) : f.name).to_sym
38
+ end
39
+ end
40
+ @columns
41
+ end
42
+
43
+ def each_hash(with_table=nil)
44
+ c = columns
45
+ while row = fetch_row
46
+ h = {}
47
+ c.each_with_index {|f, i| h[f] = convert_type(row[i], @column_types[i])}
48
+ yield h
49
+ end
50
+ end
51
+ end
52
+
7
53
  module Sequel
8
54
  module MySQL
9
-
10
55
  class Database < Sequel::Database
11
56
  set_adapter_scheme :mysql
12
57
 
13
58
  def connect
14
- Mysql.real_connect(@opts[:host], @opts[:user], @opts[:password],
59
+ conn = Mysql.real_connect(@opts[:host], @opts[:user], @opts[:password],
15
60
  @opts[:database], @opts[:port])
61
+ conn.query_with_result = false
62
+ conn
63
+ end
64
+
65
+ def tables
66
+ @pool.hold do |conn|
67
+ conn.list_tables.map {|t| t.to_sym}
68
+ end
16
69
  end
17
70
 
18
71
  def dataset(opts = nil)
19
72
  MySQL::Dataset.new(self, opts)
20
73
  end
21
-
74
+
22
75
  def execute(sql)
23
76
  @logger.info(sql) if @logger
24
77
  @pool.hold do |conn|
@@ -26,6 +79,14 @@ module Sequel
26
79
  end
27
80
  end
28
81
 
82
+ def query(sql)
83
+ @logger.info(sql) if @logger
84
+ @pool.hold do |conn|
85
+ conn.query(sql)
86
+ conn.use_result
87
+ end
88
+ end
89
+
29
90
  def execute_insert(sql)
30
91
  @logger.info(sql) if @logger
31
92
  @pool.hold do |conn|
@@ -41,13 +102,69 @@ module Sequel
41
102
  conn.affected_rows
42
103
  end
43
104
  end
44
-
45
- def transaction(&block)
46
- @pool.hold {|conn| conn.transaction(&block)}
105
+
106
+ def transaction
107
+ @pool.hold do |conn|
108
+ @transactions ||= []
109
+ if @transactions.include? Thread.current
110
+ return yield(conn)
111
+ end
112
+ conn.query(SQL_BEGIN)
113
+ begin
114
+ @transactions << Thread.current
115
+ result = yield(conn)
116
+ conn.query(SQL_COMMIT)
117
+ result
118
+ rescue => e
119
+ conn.query(SQL_ROLLBACK)
120
+ raise e
121
+ ensure
122
+ @transactions.delete(Thread.current)
123
+ end
124
+ end
47
125
  end
48
126
  end
49
127
 
50
128
  class Dataset < Sequel::Dataset
129
+ def field_name(field)
130
+ f = field.is_a?(Symbol) ? field.to_field_name : field
131
+ if f =~ /^(([^\(]*)\(\*\))|\*$/
132
+ f
133
+ elsif f =~ /^([^\(]*)\(([^\*\)]*)\)$/
134
+ "#{$1}(`#{$2}`)"
135
+ elsif f =~ /^(.*) (DESC|ASC)$/
136
+ "`#{$1}` #{$2}"
137
+ else
138
+ "`#{f}`"
139
+ end
140
+ end
141
+
142
+ def literal(v)
143
+ case v
144
+ when true: '1'
145
+ when false: '0'
146
+ else
147
+ super
148
+ end
149
+ end
150
+
151
+ # MySQL supports ORDER and LIMIT clauses in UPDATE statements.
152
+ def update_sql(values, opts = nil)
153
+ sql = super
154
+
155
+ opts = opts ? @opts.merge(opts) : @opts
156
+
157
+ if order = opts[:order]
158
+ sql << " ORDER BY #{field_list(order)}"
159
+ end
160
+
161
+ if limit = opts[:limit]
162
+ sql << " LIMIT #{limit}"
163
+ end
164
+
165
+ sql
166
+ end
167
+
51
168
  def insert(*values)
52
169
  @db.execute_insert(insert_sql(*values))
53
170
  end
@@ -62,20 +179,16 @@ module Sequel
62
179
 
63
180
  def fetch_rows(sql)
64
181
  @db.synchronize do
65
- result = @db.execute(sql)
182
+ r = @db.query(sql)
66
183
  begin
67
- fetch_columns(result)
68
- result.each_hash {|r| yield r}
184
+ @columns = r.columns
185
+ r.each_hash {|row| yield row}
69
186
  ensure
70
- result.free
187
+ r.free
71
188
  end
72
189
  end
73
190
  self
74
191
  end
75
-
76
- def fetch_columns(result)
77
- @columns = result.fetch_fields.map {|c| c.name.to_sym}
78
- end
79
192
  end
80
193
  end
81
194
  end
@@ -122,10 +122,6 @@ class String
122
122
  nil
123
123
  end
124
124
  end
125
-
126
- def postgres_to_time
127
- Time.parse(self)
128
- end
129
125
  end
130
126
 
131
127
  module Sequel
@@ -138,7 +134,7 @@ module Sequel
138
134
  23 => :to_i,
139
135
  700 => :to_f,
140
136
  701 => :to_f,
141
- 1114 => :postgres_to_time
137
+ 1114 => :to_time
142
138
  }
143
139
 
144
140
  class Database < Sequel::Database
@@ -272,9 +268,6 @@ module Sequel
272
268
  end
273
269
 
274
270
  class Dataset < Sequel::Dataset
275
- TRUE = "'t'".freeze
276
- FALSE = "'f'".freeze
277
-
278
271
  def literal(v)
279
272
  case v
280
273
  when String, Fixnum, Float, TrueClass, FalseClass: PGconn.quote(v)
@@ -283,14 +276,17 @@ module Sequel
283
276
  end
284
277
  end
285
278
 
286
- LIKE = '%s ~ %s'.freeze
279
+ LIKE = '(%s ~ %s)'.freeze
287
280
  LIKE_CI = '%s ~* %s'.freeze
288
281
 
289
282
  def format_eq_expression(left, right)
290
283
  case right
291
284
  when Regexp:
292
- (right.casefold? ? LIKE_CI : LIKE) %
293
- [field_name(left), PGconn.quote(right.source)]
285
+ l = field_name(left)
286
+ r = PGconn.quote(right.source)
287
+ right.casefold? ? \
288
+ "(#{l} ~* #{r})" : \
289
+ "(#{l} ~ #{r})"
294
290
  else
295
291
  super
296
292
  end
@@ -0,0 +1,105 @@
1
+ require File.join(File.dirname(__FILE__), '../../lib/sequel/mysql')
2
+
3
+ MYSQL_DB = Sequel('mysql://root@localhost/sandbox')
4
+ if MYSQL_DB.table_exists?(:items)
5
+ MYSQL_DB.drop_table :items
6
+ end
7
+ MYSQL_DB.create_table :items do
8
+ text :name
9
+ integer :value
10
+ end
11
+
12
+ context "A MySQL dataset" do
13
+ setup do
14
+ @d = MYSQL_DB[:items]
15
+ @d.delete # remove all records
16
+ end
17
+
18
+ specify "should return the correct record count" do
19
+ @d.count.should == 0
20
+ @d << {:name => 'abc', :value => 123}
21
+ @d << {:name => 'abc', :value => 456}
22
+ @d << {:name => 'def', :value => 789}
23
+ @d.count.should == 3
24
+ end
25
+
26
+ # specify "should return the last inserted id when inserting records" do
27
+ # id = @d << {:name => 'abc', :value => 1.23}
28
+ # id.should == @d.first[:id]
29
+ # end
30
+ #
31
+
32
+ specify "should return all records" do
33
+ @d << {:name => 'abc', :value => 123}
34
+ @d << {:name => 'abc', :value => 456}
35
+ @d << {:name => 'def', :value => 789}
36
+
37
+ @d.order(:value).all.should == [
38
+ {:name => 'abc', :value => 123},
39
+ {:name => 'abc', :value => 456},
40
+ {:name => 'def', :value => 789}
41
+ ]
42
+ end
43
+
44
+ specify "should update records correctly" do
45
+ @d << {:name => 'abc', :value => 123}
46
+ @d << {:name => 'abc', :value => 456}
47
+ @d << {:name => 'def', :value => 789}
48
+ @d.filter(:name => 'abc').update(:value => 530)
49
+
50
+ # the third record should stay the same
51
+ # floating-point precision bullshit
52
+ @d[:name => 'def'][:value].should == 789
53
+ @d.filter(:value => 530).count.should == 2
54
+ end
55
+
56
+ specify "should delete records correctly" do
57
+ @d << {:name => 'abc', :value => 123}
58
+ @d << {:name => 'abc', :value => 456}
59
+ @d << {:name => 'def', :value => 789}
60
+ @d.filter(:name => 'abc').delete
61
+
62
+ @d.count.should == 1
63
+ @d.first[:name].should == 'def'
64
+ end
65
+
66
+ specify "should be able to literalize booleans" do
67
+ proc {@d.literal(true)}.should_not raise_error
68
+ proc {@d.literal(false)}.should_not raise_error
69
+ end
70
+
71
+ specify "should quote fields using back-ticks" do
72
+ @d.select(:name).sql.should == \
73
+ 'SELECT `name` FROM items'
74
+
75
+ @d.select('COUNT(*)').sql.should == \
76
+ 'SELECT COUNT(*) FROM items'
77
+
78
+ @d.select(:value.MAX).sql.should == \
79
+ 'SELECT max(`value`) FROM items'
80
+
81
+ @d.order(:name.DESC).sql.should == \
82
+ 'SELECT * FROM items ORDER BY `name` DESC'
83
+
84
+ @d.insert_sql(:value => 333).should == \
85
+ 'INSERT INTO items (`value`) VALUES (333)'
86
+ end
87
+
88
+ specify "should support ORDER clause in UPDATE statements" do
89
+ @d.order(:name).update_sql(:value => 1).should == \
90
+ 'UPDATE items SET `value` = 1 ORDER BY `name`'
91
+ end
92
+
93
+ specify "should support LIMIT clause in UPDATE statements" do
94
+ @d.limit(10).update_sql(:value => 1).should == \
95
+ 'UPDATE items SET `value` = 1 LIMIT 10'
96
+ end
97
+
98
+ specify "should support transactions" do
99
+ MYSQL_DB.transaction do
100
+ @d << {:name => 'abc', :value => 1}
101
+ end
102
+
103
+ @d.count.should == 1
104
+ end
105
+ end
@@ -71,6 +71,13 @@ context "String#split_sql" do
71
71
  end
72
72
  end
73
73
 
74
+ context "String#to_time" do
75
+ specify "should convert the string into a Time object" do
76
+ "2007-07-11".to_time.should == Time.parse("2007-07-11")
77
+ "06:30".to_time.should == Time.parse("06:30")
78
+ end
79
+ end
80
+
74
81
  context "Symbol#DESC" do
75
82
  specify "should append the symbol with DESC" do
76
83
  :hey.DESC.should == 'hey DESC'
@@ -363,4 +363,83 @@ context "Database#uri_to_options" do
363
363
  h[:port].should == 1234
364
364
  h[:database].should == 'blah'
365
365
  end
366
+ end
367
+
368
+ context "A single threaded database" do
369
+ teardown do
370
+ Sequel::Database.single_threaded = false
371
+ end
372
+
373
+ specify "should use a SingleThreadedPool instead of a ConnectionPool" do
374
+ db = Sequel::Database.new(:single_threaded => true)
375
+ db.pool.should be_a_kind_of(Sequel::SingleThreadedPool)
376
+ end
377
+
378
+ specify "should be constructable using :single_threaded => true option" do
379
+ db = Sequel::Database.new(:single_threaded => true)
380
+ db.pool.should be_a_kind_of(Sequel::SingleThreadedPool)
381
+ end
382
+
383
+ specify "should be constructable using Database.single_threaded = true" do
384
+ Sequel::Database.single_threaded = true
385
+ db = Sequel::Database.new
386
+ db.pool.should be_a_kind_of(Sequel::SingleThreadedPool)
387
+ end
388
+ end
389
+
390
+ context "A single threaded database" do
391
+ setup do
392
+ conn = 1234567
393
+ @db = Sequel::Database.new(:single_threaded => true) do
394
+ conn += 1
395
+ end
396
+ end
397
+
398
+ specify "should invoke connection_proc only once" do
399
+ @db.pool.hold {|c| c.should == 1234568}
400
+ @db.pool.hold {|c| c.should == 1234568}
401
+ end
402
+
403
+ specify "should convert an Exception into a RuntimeError" do
404
+ db = Sequel::Database.new(:single_threaded => true) do
405
+ raise Exception
406
+ end
407
+
408
+ proc {db.pool.hold {|c|}}.should raise_error(RuntimeError)
409
+ end
410
+ end
411
+
412
+ context "A database" do
413
+ setup do
414
+ Sequel::Database.single_threaded = false
415
+ end
416
+
417
+ teardown do
418
+ Sequel::Database.single_threaded = false
419
+ end
420
+
421
+ specify "should be either single_threaded? or multi_threaded?" do
422
+ db = Sequel::Database.new(:single_threaded => true)
423
+ db.should be_single_threaded
424
+ db.should_not be_multi_threaded
425
+
426
+ db = Sequel::Database.new(:max_options => 1)
427
+ db.should_not be_single_threaded
428
+ db.should be_multi_threaded
429
+
430
+ db = Sequel::Database.new
431
+ db.should_not be_single_threaded
432
+ db.should be_multi_threaded
433
+
434
+ Sequel::Database.single_threaded = true
435
+
436
+ db = Sequel::Database.new
437
+ db.should be_single_threaded
438
+ db.should_not be_multi_threaded
439
+
440
+ db = Sequel::Database.new(:max_options => 4)
441
+ db.should be_single_threaded
442
+ db.should_not be_multi_threaded
443
+
444
+ end
366
445
  end
metadata CHANGED
@@ -3,8 +3,8 @@ rubygems_version: 0.9.4
3
3
  specification_version: 1
4
4
  name: sequel
5
5
  version: !ruby/object:Gem::Version
6
- version: 0.1.8
7
- date: 2007-07-10 00:00:00 +03:00
6
+ version: 0.1.9
7
+ date: 2007-07-21 00:00:00 +03:00
8
8
  summary: Concise ORM for Ruby.
9
9
  require_paths:
10
10
  - lib
@@ -33,12 +33,14 @@ files:
33
33
  - README
34
34
  - Rakefile
35
35
  - bin/sequel
36
+ - doc/rdoc
36
37
  - spec/adapters
37
38
  - spec/connection_pool_spec.rb
38
39
  - spec/core_ext_spec.rb
39
40
  - spec/database_spec.rb
40
41
  - spec/dataset_spec.rb
41
42
  - spec/expressions_spec.rb
43
+ - spec/adapters/mysql_spec.rb
42
44
  - spec/adapters/sqlite_spec.rb
43
45
  - lib/sequel
44
46
  - lib/sequel.rb