sequel 0.3.2 → 0.3.3

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,19 @@
1
+ === 0.3.3 (2007-11-04)
2
+
3
+ * Revised code to generate SQL statements without trailing semicolons.
4
+
5
+ * Added Sequel::Worker implementation of a simple worker thread for asynchronous execution.
6
+
7
+ * Added spec for Oracle adapter.
8
+
9
+ * Fixed Oracle adapter to format INSERT statements without semicolons (thanks Liming Lian).
10
+
11
+ * Renamed alias to Array#keys as Array#columns instead of Array#fields.
12
+
13
+ * Renamed FieldCompositionMethods as ColumnCompositionMethods.
14
+
15
+ * Implemented Sequel::NumericExtensions to provide stuff like 30.days.ago.
16
+
1
17
  === 0.3.2 (2007-11-01)
2
18
 
3
19
  * Added #to_column_name as alias to #to_field_name, #column_title as alias to #field_title.
data/Rakefile CHANGED
@@ -6,7 +6,7 @@ require 'fileutils'
6
6
  include FileUtils
7
7
 
8
8
  NAME = "sequel"
9
- VERS = "0.3.2"
9
+ VERS = "0.3.3"
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",
data/lib/sequel.rb CHANGED
@@ -2,7 +2,7 @@ require 'metaid'
2
2
 
3
3
  files = %w[
4
4
  core_ext array_keys error connection_pool pretty_table
5
- dataset migration model schema database
5
+ dataset migration model schema database worker
6
6
  ]
7
7
  dir = File.join(File.dirname(__FILE__), 'sequel')
8
8
  files.each {|f| require(File.join(dir, f))}
@@ -183,8 +183,8 @@ module ArrayKeys
183
183
  end
184
184
  end
185
185
 
186
- alias_method :fields, :keys
187
- alias_method :fields=, :keys=
186
+ alias_method :columns, :keys
187
+ alias_method :columns=, :keys=
188
188
  end
189
189
 
190
190
  # The DatasetExtensions module provides extensions that modify
@@ -43,7 +43,7 @@ module Sequel
43
43
  # Assigns a connection to the current thread, yielding the connection
44
44
  # to the supplied block.
45
45
  #
46
- # pool.hold {|conn| conn.execute('DROP TABLE posts;')}
46
+ # pool.hold {|conn| conn.execute('DROP TABLE posts')}
47
47
  #
48
48
  # Pool#hold is re-entrant, meaning it can be called recursively in
49
49
  # the same thread without blocking.
@@ -17,6 +17,14 @@ class Array
17
17
  end
18
18
  end
19
19
 
20
+ # Range extensions
21
+ class Range
22
+ # Returns the interval between the beginning and end of the range.
23
+ def interval
24
+ last - first
25
+ end
26
+ end
27
+
20
28
  module Sequel
21
29
  # LiteralString is used to represent literal SQL expressions. An
22
30
  # LiteralString is copied verbatim into an SQL statement. Instances of
@@ -68,7 +76,7 @@ end
68
76
 
69
77
  # Methods to format column names and associated constructs. This module is
70
78
  # included in String and Symbol.
71
- module FieldCompositionMethods
79
+ module ColumnCompositionMethods
72
80
  # Constructs a DESC clause for use in an ORDER BY clause.
73
81
  def DESC
74
82
  "#{to_column_name} DESC".lit
@@ -84,14 +92,14 @@ module FieldCompositionMethods
84
92
  "#{to_s}.*".lit
85
93
  end
86
94
 
87
- FIELD_TITLE_RE1 = /^(.*)\sAS\s(.+)$/i.freeze
88
- FIELD_TITLE_RE2 = /^([^\.]+)\.([^\.]+)$/.freeze
95
+ COLUMN_TITLE_RE1 = /^(.*)\sAS\s(.+)$/i.freeze
96
+ COLUMN_TITLE_RE2 = /^([^\.]+)\.([^\.]+)$/.freeze
89
97
 
90
98
  # Returns the column name. If the column name is aliased, the alias is
91
99
  # returned.
92
100
  def field_title
93
101
  case s = to_column_name
94
- when FIELD_TITLE_RE1, FIELD_TITLE_RE2: $2
102
+ when COLUMN_TITLE_RE1, COLUMN_TITLE_RE2: $2
95
103
  else
96
104
  s
97
105
  end
@@ -101,17 +109,17 @@ end
101
109
 
102
110
  # String extensions
103
111
  class String
104
- include FieldCompositionMethods
112
+ include ColumnCompositionMethods
105
113
  end
106
114
 
107
115
  # Symbol extensions
108
116
  class Symbol
109
- include FieldCompositionMethods
117
+ include ColumnCompositionMethods
110
118
 
111
119
 
112
- FIELD_REF_RE1 = /^(\w+)__(\w+)___(\w+)/.freeze
113
- FIELD_REF_RE2 = /^(\w+)___(\w+)$/.freeze
114
- FIELD_REF_RE3 = /^(\w+)__(\w+)$/.freeze
120
+ COLUMN_REF_RE1 = /^(\w+)__(\w+)___(\w+)/.freeze
121
+ COLUMN_REF_RE2 = /^(\w+)___(\w+)$/.freeze
122
+ COLUMN_REF_RE3 = /^(\w+)__(\w+)$/.freeze
115
123
 
116
124
  # Converts a symbol into a column name. This method supports underscore
117
125
  # notation in order to express qualified (two underscores) and aliased
@@ -125,9 +133,9 @@ class Symbol
125
133
  def to_field_name
126
134
  s = to_s
127
135
  case s
128
- when FIELD_REF_RE1: "#{$1}.#{$2} AS #{$3}"
129
- when FIELD_REF_RE2: "#{$1} AS #{$2}"
130
- when FIELD_REF_RE3: "#{$1}.#{$2}"
136
+ when COLUMN_REF_RE1: "#{$1}.#{$2} AS #{$3}"
137
+ when COLUMN_REF_RE2: "#{$1} AS #{$2}"
138
+ when COLUMN_REF_RE3: "#{$1}.#{$2}"
131
139
  else
132
140
  s
133
141
  end
@@ -147,4 +155,37 @@ class Symbol
147
155
  end
148
156
  end
149
157
 
158
+ module Sequel
159
+ # Facilitates time calculations by providing methods to convert from larger
160
+ # time units to seconds, and to convert relative time intervals to absolute
161
+ # ones. This module duplicates some of the functionality provided by Rails'
162
+ # ActiveSupport::CoreExtensions::Numeric::Time module.
163
+ module NumericExtensions
164
+ MINUTE = 60
165
+ HOUR = 3600
166
+ DAY = 86400
167
+ WEEK = DAY * 7
168
+
169
+ # Converts self from minutes to seconds
170
+ def minutes; self * MINUTE; end; alias_method :minute, :minutes
171
+ # Converts self from hours to seconds
172
+ def hours; self * HOUR; end; alias_method :hour, :hours
173
+ # Converts self from days to seconds
174
+ def days; self * DAY; end; alias_method :day, :days
175
+ # Converts self from weeks to seconds
176
+ def weeks; self * WEEK; end; alias_method :week, :weeks
177
+
178
+ # Returns the time at now - self.
179
+ def ago(t = Time.now); t - self; end
180
+ alias_method :before, :ago
150
181
 
182
+ # Returns the time at now + self.
183
+ def from_now(t = Time.now); t + self; end
184
+ alias_method :since, :from_now
185
+
186
+ # Extends the Numeric class with numeric extensions.
187
+ def self.enable
188
+ Numeric.send(:include, self)
189
+ end
190
+ end
191
+ end
@@ -180,7 +180,7 @@ module Sequel
180
180
 
181
181
  # Drops one or more tables corresponding to the given table names.
182
182
  def drop_table(*names)
183
- execute(names.map {|n| drop_table_sql(n)}.join)
183
+ names.each {|n| execute(drop_table_sql(n))}
184
184
  end
185
185
 
186
186
  # Returns true if the given table exists.
@@ -438,7 +438,7 @@ module Sequel
438
438
  # 'INSERT INTO items (a, b) VALUES (1, 2)'
439
439
  def insert_sql(*values)
440
440
  if values.empty?
441
- "INSERT INTO #{@opts[:from]} DEFAULT VALUES;"
441
+ "INSERT INTO #{@opts[:from]} DEFAULT VALUES"
442
442
  else
443
443
  values = values[0] if values.size == 1
444
444
  case values
@@ -446,27 +446,27 @@ module Sequel
446
446
  insert_sql(values.values)
447
447
  when Array
448
448
  if values.empty?
449
- "INSERT INTO #{@opts[:from]} DEFAULT VALUES;"
450
- elsif values.fields
451
- fl = values.fields
449
+ "INSERT INTO #{@opts[:from]} DEFAULT VALUES"
450
+ elsif values.keys
451
+ fl = values.keys
452
452
  vl = transform_save(values.values).map {|v| literal(v)}
453
- "INSERT INTO #{@opts[:from]} (#{fl.join(COMMA_SEPARATOR)}) VALUES (#{vl.join(COMMA_SEPARATOR)});"
453
+ "INSERT INTO #{@opts[:from]} (#{fl.join(COMMA_SEPARATOR)}) VALUES (#{vl.join(COMMA_SEPARATOR)})"
454
454
  else
455
- "INSERT INTO #{@opts[:from]} VALUES (#{literal(values)});"
455
+ "INSERT INTO #{@opts[:from]} VALUES (#{literal(values)})"
456
456
  end
457
457
  when Hash
458
458
  values = transform_save(values) if @transform
459
459
  if values.empty?
460
- "INSERT INTO #{@opts[:from]} DEFAULT VALUES;"
460
+ "INSERT INTO #{@opts[:from]} DEFAULT VALUES"
461
461
  else
462
462
  fl, vl = [], []
463
463
  values.each {|k, v| fl << column_name(k); vl << literal(v)}
464
- "INSERT INTO #{@opts[:from]} (#{fl.join(COMMA_SEPARATOR)}) VALUES (#{vl.join(COMMA_SEPARATOR)});"
464
+ "INSERT INTO #{@opts[:from]} (#{fl.join(COMMA_SEPARATOR)}) VALUES (#{vl.join(COMMA_SEPARATOR)})"
465
465
  end
466
466
  when Dataset
467
- "INSERT INTO #{@opts[:from]} #{literal(values)};"
467
+ "INSERT INTO #{@opts[:from]} #{literal(values)}"
468
468
  else
469
- "INSERT INTO #{@opts[:from]} VALUES (#{literal(values)});"
469
+ "INSERT INTO #{@opts[:from]} VALUES (#{literal(values)})"
470
470
  end
471
471
  end
472
472
  end
@@ -484,7 +484,7 @@ module Sequel
484
484
  raise SequelError, "Can't update a joined dataset"
485
485
  end
486
486
 
487
- if values.is_a?(Array) && values.fields
487
+ if values.is_a?(Array) && values.keys
488
488
  values = values.to_hash
489
489
  end
490
490
  values = transform_save(values) if @transform
data/lib/sequel/mysql.rb CHANGED
@@ -96,9 +96,9 @@ module Sequel
96
96
  @opts[:database], @opts[:port])
97
97
  conn.query_with_result = false
98
98
  if encoding = @opts[:encoding] || @opts[:charset]
99
- conn.query("set character_set_connection = '#{encoding}';")
100
- conn.query("set character_set_client = '#{encoding}';")
101
- conn.query("set character_set_results = '#{encoding}';")
99
+ conn.query("set character_set_connection = '#{encoding}'")
100
+ conn.query("set character_set_client = '#{encoding}'")
101
+ conn.query("set character_set_results = '#{encoding}'")
102
102
  end
103
103
  conn.reconnect = true
104
104
  conn
@@ -172,20 +172,20 @@ module Sequel
172
172
  end
173
173
 
174
174
  class Dataset < Sequel::Dataset
175
- UNQUOTABLE_FIELD_RE = /^(`(.+)`)|\*$/.freeze
175
+ UNQUOTABLE_COLUMN_RE = /^(`(.+)`)|\*$/.freeze
176
176
  def quote_column(f)
177
- (f.nil? || f.empty? || f =~ UNQUOTABLE_FIELD_RE) ? f : "`#{f}`"
177
+ (f.nil? || f.empty? || f =~ UNQUOTABLE_COLUMN_RE) ? f : "`#{f}`"
178
178
  end
179
179
 
180
- FIELD_EXPR_RE = /^([^\(]+\()?([^\.]+\.)?([^\s\)]+)?(\))?(\sAS\s(.+))?$/i.freeze
181
- FIELD_ORDER_RE = /^(.*) (DESC|ASC)$/i.freeze
180
+ COLUMN_EXPR_RE = /^([^\(]+\()?([^\.]+\.)?([^\s\)]+)?(\))?(\sAS\s(.+))?$/i.freeze
181
+ COLUMN_ORDER_RE = /^(.*) (DESC|ASC)$/i.freeze
182
182
  def quoted_column_name(name)
183
183
  case name
184
- when FIELD_EXPR_RE:
184
+ when COLUMN_EXPR_RE:
185
185
  $6 ? \
186
186
  "#{$1}#{$2}#{quote_column($3)}#{$4} AS #{quote_column($6)}" : \
187
187
  "#{$1}#{$2}#{quote_column($3)}#{$4}"
188
- when FIELD_ORDER_RE: "#{quote_column($1)} #{$2}"
188
+ when COLUMN_ORDER_RE: "#{quote_column($1)} #{$2}"
189
189
  else
190
190
  quote_column(name)
191
191
  end
@@ -284,7 +284,7 @@ module Sequel
284
284
  end
285
285
 
286
286
  def drop_table_sql(name)
287
- "DROP TABLE #{name} CASCADE;"
287
+ "DROP TABLE #{name} CASCADE"
288
288
  end
289
289
  end
290
290
 
@@ -350,7 +350,7 @@ module Sequel
350
350
  analysis.join("\r\n")
351
351
  end
352
352
 
353
- LOCK = 'LOCK TABLE %s IN %s MODE;'.freeze
353
+ LOCK = 'LOCK TABLE %s IN %s MODE'.freeze
354
354
 
355
355
  ACCESS_SHARE = 'ACCESS SHARE'.freeze
356
356
  ROW_SHARE = 'ROW SHARE'.freeze
@@ -63,9 +63,9 @@ module Sequel
63
63
  columns = index[:columns].join(COMMA_SEPARATOR)
64
64
  index_name = index[:name] || default_index_name(table_name, index[:columns])
65
65
  if index[:unique]
66
- "CREATE UNIQUE INDEX #{index_name} ON #{table_name} (#{columns});"
66
+ "CREATE UNIQUE INDEX #{index_name} ON #{table_name} (#{columns})"
67
67
  else
68
- "CREATE INDEX #{index_name} ON #{table_name} (#{columns});"
68
+ "CREATE INDEX #{index_name} ON #{table_name} (#{columns})"
69
69
  end
70
70
  end
71
71
 
@@ -74,7 +74,7 @@ module Sequel
74
74
  end
75
75
 
76
76
  def create_table_sql_list(name, columns, indexes = nil)
77
- sql = ["CREATE TABLE #{name} (#{column_list_sql(columns)});"]
77
+ sql = ["CREATE TABLE #{name} (#{column_list_sql(columns)})"]
78
78
  if indexes && !indexes.empty?
79
79
  sql.concat(index_list_sql_list(name, indexes))
80
80
  end
@@ -82,7 +82,7 @@ module Sequel
82
82
  end
83
83
 
84
84
  def drop_table_sql(name)
85
- "DROP TABLE #{name};"
85
+ "DROP TABLE #{name}"
86
86
  end
87
87
  end
88
88
  end
data/lib/sequel/sqlite.rb CHANGED
@@ -61,11 +61,11 @@ module Sequel
61
61
  end
62
62
 
63
63
  def pragma_get(name)
64
- single_value("PRAGMA #{name};")
64
+ single_value("PRAGMA #{name}")
65
65
  end
66
66
 
67
67
  def pragma_set(name, value)
68
- execute("PRAGMA #{name} = #{value};")
68
+ execute("PRAGMA #{name} = #{value}")
69
69
  end
70
70
 
71
71
  AUTO_VACUUM = {'0' => :none, '1' => :full, '2' => :incremental}.freeze
@@ -149,7 +149,7 @@ module Sequel
149
149
  def array_tuples_fetch_rows(sql, &block)
150
150
  @db.execute_select(sql) do |result|
151
151
  @columns = result.columns.map {|c| c.to_sym}
152
- result.each(&block)
152
+ result.each {|r| r.keys = @columns; block[r]}
153
153
  end
154
154
  end
155
155
 
@@ -0,0 +1,44 @@
1
+ require 'thread'
2
+
3
+ module Sequel
4
+ class Worker < Thread
5
+ class WorkerStopError < RuntimeError; end
6
+
7
+ attr_reader :queue
8
+
9
+ def initialize(db = nil)
10
+ @queue = Queue.new
11
+ t = self
12
+ if db
13
+ super {db.transaction {t.work}}
14
+ else
15
+ super {t.work}
16
+ end
17
+ end
18
+
19
+ def work
20
+ begin
21
+ loop {@cur = @queue.pop; @cur.call; @cur = nil}
22
+ rescue WorkerStopError # do nothing
23
+ end
24
+ end
25
+
26
+ def busy?
27
+ @cur || !@queue.empty?
28
+ end
29
+
30
+ def async(proc = nil, &block)
31
+ @queue << (proc || block)
32
+ end
33
+ alias_method :add, :async
34
+ alias_method :<<, :async
35
+
36
+ def join
37
+ while busy?
38
+ sleep 0.1
39
+ end
40
+ self.raise WorkerStopError
41
+ super
42
+ end
43
+ end
44
+ end
@@ -107,7 +107,7 @@ context "A MySQL dataset" do
107
107
  'SELECT max(items.`name`) AS `max_name` FROM items'
108
108
 
109
109
  @d.insert_sql(:value => 333).should == \
110
- 'INSERT INTO items (`value`) VALUES (333);'
110
+ 'INSERT INTO items (`value`) VALUES (333)'
111
111
  end
112
112
 
113
113
  specify "should support ORDER clause in UPDATE statements" do
@@ -0,0 +1,130 @@
1
+ require File.join(File.dirname(__FILE__), '../../lib/sequel/oracle')
2
+
3
+ ORACLE_DB = Sequel('oracle://hr:hr@loalhost/XE')
4
+ if ORACLE_DB.table_exists?(:test)
5
+ ORACLE_DB.drop_table :test
6
+ end
7
+ ORACLE_DB.create_table :test do
8
+ text :name
9
+ integer :value
10
+
11
+ index :value
12
+ end
13
+
14
+ context "A Oracle database" do
15
+ specify "should provide disconnect functionality" do
16
+ ORACLE_DB.execute("select user from dual")
17
+ ORACLE_DB.pool.size.should == 1
18
+ ORACLE_DB.disconnect
19
+ ORACLE_DB.pool.size.should == 0
20
+ end
21
+ end
22
+
23
+ context "A Oracle dataset" do
24
+ setup do
25
+ @d = ORACLE_DB[:test]
26
+ @d.delete # remove all records
27
+ end
28
+
29
+ specify "should return the correct record count" do
30
+ @d.count.should == 0
31
+ @d << {:name => 'abc', :value => 123}
32
+ @d << {:name => 'abc', :value => 456}
33
+ @d << {:name => 'def', :value => 789}
34
+ @d.count.should == 3
35
+ end
36
+
37
+ specify "should return the correct records" do
38
+ @d.to_a.should == []
39
+ @d << {:name => 'abc', :value => 123}
40
+ @d << {:name => 'abc', :value => 456}
41
+ @d << {:name => 'def', :value => 789}
42
+
43
+ @d.order(:value).to_a.should == [
44
+ {:name => 'abc', :value => 123},
45
+ {:name => 'abc', :value => 456},
46
+ {:name => 'def', :value => 789}
47
+ ]
48
+ end
49
+
50
+ specify "should update records correctly" do
51
+ @d << {:name => 'abc', :value => 123}
52
+ @d << {:name => 'abc', :value => 456}
53
+ @d << {:name => 'def', :value => 789}
54
+ @d.filter(:name => 'abc').update(:value => 530)
55
+
56
+ # the third record should stay the same
57
+ # floating-point precision bullshit
58
+ @d[:name => 'def'][:value].should == 789
59
+ @d.filter(:value => 530).count.should == 2
60
+ end
61
+
62
+ specify "should delete records correctly" do
63
+ @d << {:name => 'abc', :value => 123}
64
+ @d << {:name => 'abc', :value => 456}
65
+ @d << {:name => 'def', :value => 789}
66
+ @d.filter(:name => 'abc').delete
67
+
68
+ @d.count.should == 1
69
+ @d.first[:name].should == 'def'
70
+ end
71
+
72
+ specify "should be able to literalize booleans" do
73
+ proc {@d.literal(true)}.should_not raise_error
74
+ proc {@d.literal(false)}.should_not raise_error
75
+ end
76
+
77
+ specify "should support transactions" do
78
+ ORACLE_DB.transaction do
79
+ @d << {:name => 'abc', :value => 1}
80
+ end
81
+
82
+ @d.count.should == 1
83
+ end
84
+ end
85
+
86
+ context "A Oracle dataset in array tuples mode" do
87
+ setup do
88
+ @d = ORACLE_DB[:test]
89
+ @d.delete # remove all records
90
+ Sequel.use_array_tuples
91
+ end
92
+
93
+ teardown do
94
+ Sequel.use_hash_tuples
95
+ end
96
+
97
+ specify "should return the correct records" do
98
+ @d.to_a.should == []
99
+ @d << {:name => 'abc', :value => 123}
100
+ @d << {:name => 'abc', :value => 456}
101
+ @d << {:name => 'def', :value => 789}
102
+
103
+ @d.order(:value).select(:name, :value).to_a.should == [
104
+ ['abc', 123],
105
+ ['abc', 456],
106
+ ['def', 789]
107
+ ]
108
+ end
109
+
110
+ specify "should work correctly with transforms" do
111
+ @d.transform(:value => [proc {|v| v.to_s}, proc {|v| v.to_i}])
112
+
113
+ @d.to_a.should == []
114
+ @d << {:name => 'abc', :value => 123}
115
+ @d << {:name => 'abc', :value => 456}
116
+ @d << {:name => 'def', :value => 789}
117
+
118
+ @d.order(:value).select(:name, :value).to_a.should == [
119
+ ['abc', '123'],
120
+ ['abc', '456'],
121
+ ['def', '789']
122
+ ]
123
+
124
+ a = @d.order(:value).first
125
+ a.values.should == ['abc', '123']
126
+ a.keys.should == [:name, :value]
127
+ a[:name].should == 'abc'
128
+ a[:value].should == '123'
129
+ end
130
+ end