sequel 0.1.8 → 0.1.9

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/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