sequel_core 1.4.0 → 1.5.0

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.
Files changed (51) hide show
  1. data/CHANGELOG +74 -0
  2. data/COPYING +1 -0
  3. data/README +17 -6
  4. data/Rakefile +16 -21
  5. data/lib/sequel_core.rb +18 -28
  6. data/lib/sequel_core/adapters/ado.rb +3 -15
  7. data/lib/sequel_core/adapters/dbi.rb +1 -14
  8. data/lib/sequel_core/adapters/informix.rb +3 -3
  9. data/lib/sequel_core/adapters/jdbc.rb +2 -2
  10. data/lib/sequel_core/adapters/mysql.rb +39 -59
  11. data/lib/sequel_core/adapters/odbc.rb +18 -38
  12. data/lib/sequel_core/adapters/openbase.rb +1 -17
  13. data/lib/sequel_core/adapters/oracle.rb +1 -19
  14. data/lib/sequel_core/adapters/postgres.rb +20 -60
  15. data/lib/sequel_core/adapters/sqlite.rb +4 -8
  16. data/lib/sequel_core/connection_pool.rb +150 -0
  17. data/lib/sequel_core/core_ext.rb +41 -0
  18. data/lib/sequel_core/core_sql.rb +35 -38
  19. data/lib/sequel_core/database.rb +20 -17
  20. data/lib/sequel_core/dataset.rb +49 -80
  21. data/lib/sequel_core/dataset/callback.rb +11 -13
  22. data/lib/sequel_core/dataset/convenience.rb +18 -136
  23. data/lib/sequel_core/dataset/pagination.rb +81 -0
  24. data/lib/sequel_core/dataset/sequelizer.rb +5 -4
  25. data/lib/sequel_core/dataset/sql.rb +43 -33
  26. data/lib/sequel_core/deprecated.rb +200 -0
  27. data/lib/sequel_core/exceptions.rb +0 -14
  28. data/lib/sequel_core/object_graph.rb +199 -0
  29. data/lib/sequel_core/pretty_table.rb +27 -24
  30. data/lib/sequel_core/schema/generator.rb +16 -4
  31. data/lib/sequel_core/schema/sql.rb +5 -3
  32. data/lib/sequel_core/worker.rb +1 -1
  33. data/spec/adapters/informix_spec.rb +1 -47
  34. data/spec/adapters/mysql_spec.rb +85 -54
  35. data/spec/adapters/oracle_spec.rb +1 -57
  36. data/spec/adapters/postgres_spec.rb +66 -49
  37. data/spec/adapters/sqlite_spec.rb +4 -29
  38. data/spec/connection_pool_spec.rb +358 -0
  39. data/spec/core_sql_spec.rb +24 -19
  40. data/spec/database_spec.rb +13 -9
  41. data/spec/dataset_spec.rb +59 -78
  42. data/spec/object_graph_spec.rb +202 -0
  43. data/spec/pretty_table_spec.rb +1 -9
  44. data/spec/schema_generator_spec.rb +7 -1
  45. data/spec/schema_spec.rb +27 -0
  46. data/spec/sequelizer_spec.rb +2 -2
  47. data/spec/spec_helper.rb +4 -2
  48. metadata +16 -57
  49. data/lib/sequel_core/array_keys.rb +0 -322
  50. data/lib/sequel_core/model.rb +0 -8
  51. data/spec/array_keys_spec.rb +0 -682
@@ -41,6 +41,10 @@ module Sequel
41
41
  ODBC::Dataset.new(self, opts)
42
42
  end
43
43
 
44
+ # ODBC returns native statement objects, which must be dropped if
45
+ # you call execute manually, or you will get warnings. See the
46
+ # fetch_rows method source code for an example of how to drop
47
+ # the statements.
44
48
  def execute(sql)
45
49
  @logger.info(sql) if @logger
46
50
  @pool.hold do |conn|
@@ -59,6 +63,10 @@ module Sequel
59
63
  class Dataset < Sequel::Dataset
60
64
  BOOL_TRUE = '1'.freeze
61
65
  BOOL_FALSE = '0'.freeze
66
+ ODBC_TIMESTAMP_FORMAT = "{ts '%Y-%m-%d %H:%M:%S'}".freeze
67
+ ODBC_TIMESTAMP_AFTER_SECONDS =
68
+ ODBC_TIMESTAMP_FORMAT.index( '%S' ).succ - ODBC_TIMESTAMP_FORMAT.length
69
+ ODBC_DATE_FORMAT = "{d '%Y-%m-%d'}".freeze
62
70
 
63
71
  def literal(v)
64
72
  case v
@@ -66,6 +74,15 @@ module Sequel
66
74
  BOOL_TRUE
67
75
  when false
68
76
  BOOL_FALSE
77
+ when Time
78
+ formatted = v.strftime(ODBC_TIMESTAMP_FORMAT)
79
+ if v.usec >= 1000
80
+ msec = ( v.usec.to_f / 1000 ).round
81
+ formatted.insert ODBC_TIMESTAMP_AFTER_SECONDS, ".#{msec}"
82
+ end
83
+ formatted
84
+ when Date
85
+ v.strftime(ODBC_DATE_FORMAT)
69
86
  else
70
87
  super
71
88
  end
@@ -135,43 +152,6 @@ module Sequel
135
152
  end
136
153
  end
137
154
 
138
- def array_tuples_fetch_rows(sql, &block)
139
- @db.synchronize do
140
- s = @db.execute sql
141
- begin
142
- @columns = s.columns(true).map {|c| c.name.to_sym}
143
- rows = s.fetch_all
144
- rows.each {|r| yield array_tuples_make_row(r)}
145
- ensure
146
- s.drop unless s.nil? rescue nil
147
- end
148
- end
149
- self
150
- end
151
-
152
- def array_tuples_make_row(row)
153
- row.keys = @columns
154
- row.each_with_index do |v, idx|
155
- # When fetching a result set, the Ruby ODBC driver converts all ODBC
156
- # SQL types to an equivalent Ruby type; with the exception of
157
- # SQL_TYPE_DATE, SQL_TYPE_TIME and SQL_TYPE_TIMESTAMP.
158
- #
159
- # The conversions below are consistent with the mappings in
160
- # ODBCColumn#mapSqlTypeToGenericType and Column#klass.
161
- case v
162
- when ::ODBC::TimeStamp
163
- row[idx] = DateTime.new(v.year, v.month, v.day, v.hour, v.minute, v.second)
164
- when ::ODBC::Time
165
- now = DateTime.now
166
- row[idx] = Time.gm(now.year, now.month, now.day, v.hour, v.minute, v.second)
167
- when ::ODBC::Date
168
- row[idx] = Date.new(v.year, v.month, v.day)
169
- end
170
- end
171
- row
172
- end
173
-
174
-
175
155
  def insert(*values)
176
156
  @db.do insert_sql(*values)
177
157
  end
@@ -186,4 +166,4 @@ module Sequel
186
166
  end
187
167
  end
188
168
  end
189
- end
169
+ end
@@ -58,22 +58,6 @@ module Sequel
58
58
  self
59
59
  end
60
60
 
61
- def array_tuples_fetch_rows(sql, &block)
62
- @db.synchronize do
63
- result = @db.execute sql
64
- begin
65
- @columns = result.column_infos.map {|c| c.name.to_sym}
66
- result.each do |r|
67
- r.keys = @columns
68
- yield r
69
- end
70
- ensure
71
- # cursor.close
72
- end
73
- end
74
- self
75
- end
76
-
77
61
  def insert(*values)
78
62
  @db.do insert_sql(*values)
79
63
  end
@@ -87,4 +71,4 @@ module Sequel
87
71
  end
88
72
  end
89
73
  end
90
- end
74
+ end
@@ -100,24 +100,6 @@ module Sequel
100
100
  self
101
101
  end
102
102
 
103
- def array_tuples_fetch_rows(sql, &block)
104
- @db.synchronize do
105
- cursor = @db.execute sql
106
- begin
107
- @columns = cursor.get_col_names.map {|c| c.downcase.to_sym}
108
- raw_rnum_index = columns.index(:raw_rnum_)
109
- while r = cursor.fetch
110
- r.delete_at(raw_rnum_index) if raw_rnum_index
111
- r.keys = columns.delete_if{|x| x == :raw_rnum_}
112
- yield r
113
- end
114
- ensure
115
- cursor.close
116
- end
117
- end
118
- self
119
- end
120
-
121
103
  def insert(*values)
122
104
  @db.do insert_sql(*values)
123
105
  end
@@ -195,4 +177,4 @@ module Sequel
195
177
  alias sql select_sql
196
178
  end
197
179
  end
198
- end
180
+ end
@@ -328,16 +328,21 @@ module Sequel
328
328
 
329
329
  def index_definition_sql(table_name, index)
330
330
  index_name = index[:name] || default_index_name(table_name, index[:columns])
331
- if index[:full_text]
331
+ expr = "(#{literal(index[:columns])})"
332
+ unique = "UNIQUE " if index[:unique]
333
+ index_type = index[:type]
334
+ filter = index[:where] || index[:filter]
335
+ filter = " WHERE #{expression_list(filter)}" if filter
336
+ case index_type
337
+ when :full_text
332
338
  lang = index[:language] ? "#{literal(index[:language])}, " : ""
333
339
  cols = index[:columns].map {|c| literal(c)}.join(" || ")
334
- expr = "gin(to_tsvector(#{lang}#{cols}))"
335
- "CREATE INDEX #{index_name} ON #{table_name} USING #{expr}"
336
- elsif index[:unique]
337
- "CREATE UNIQUE INDEX #{index_name} ON #{table_name} (#{literal(index[:columns])})"
338
- else
339
- "CREATE INDEX #{index_name} ON #{table_name} (#{literal(index[:columns])})"
340
+ expr = "(to_tsvector(#{lang}#{cols}))"
341
+ index_type = :gin
342
+ when :spatial
343
+ index_type = :gist
340
344
  end
345
+ "CREATE #{unique}INDEX #{index_name} ON #{table_name} #{"USING #{index_type} " if index_type}#{expr}#{filter}"
341
346
  end
342
347
 
343
348
  def drop_table_sql(name)
@@ -346,6 +351,9 @@ module Sequel
346
351
  end
347
352
 
348
353
  class Dataset < Sequel::Dataset
354
+
355
+ PG_TIMESTAMP_FORMAT = "TIMESTAMP '%Y-%m-%d %H:%M:%S".freeze
356
+
349
357
  def quote_column_ref(c); "\"#{c}\""; end
350
358
 
351
359
  def literal(v)
@@ -354,6 +362,8 @@ module Sequel
354
362
  v
355
363
  when String, Fixnum, Float, TrueClass, FalseClass
356
364
  PGconn.quote(v)
365
+ when Time
366
+ "#{v.strftime(PG_TIMESTAMP_FORMAT)}.#{sprintf("%06d",v.usec)}'"
357
367
  else
358
368
  super
359
369
  end
@@ -433,7 +443,7 @@ module Sequel
433
443
 
434
444
  # Locks the table with the specified mode.
435
445
  def lock(mode, &block)
436
- sql = LOCK % [@opts[:from], mode]
446
+ sql = LOCK % [source_list(@opts[:from]), mode]
437
447
  @db.synchronize do
438
448
  if block # perform locking inside a transaction and yield to block
439
449
  @db.transaction {@db.execute(sql); yield}
@@ -450,11 +460,11 @@ module Sequel
450
460
  # postgresql 8.2 introduces support for insert
451
461
  columns = literal(columns)
452
462
  values = values.map {|r| "(#{literal(r)})"}.join(COMMA_SEPARATOR)
453
- ["INSERT INTO #{@opts[:from]} (#{columns}) VALUES #{values}"]
463
+ ["INSERT INTO #{source_list(@opts[:from])} (#{columns}) VALUES #{values}"]
454
464
  end
455
465
 
456
466
  def insert(*values)
457
- @db.execute_insert(insert_sql(*values), @opts[:from],
467
+ @db.execute_insert(insert_sql(*values), source_list(@opts[:from]),
458
468
  values.size == 1 ? values.first : values)
459
469
  end
460
470
 
@@ -506,56 +516,6 @@ module Sequel
506
516
  eval("lambda {|r| {#{kvs.join(COMMA_SEPARATOR)}}}")
507
517
  end
508
518
 
509
- def array_tuples_fetch_rows(sql, &block)
510
- @db.execute(sql) do |q|
511
- conv = array_tuples_row_converter(q)
512
- q.each {|r| yield conv[r]}
513
- end
514
- end
515
-
516
- @@array_tuples_converters_mutex = Mutex.new
517
- @@array_tuples_converters = {}
518
-
519
- def array_tuples_row_converter(result)
520
- @columns = []; translators = []
521
- result.fields.each_with_index do |f, idx|
522
- @columns << f.to_sym
523
- translators << PG_TYPES[result.type(idx)]
524
- end
525
-
526
- # create result signature and memoize the converter
527
- sig = [@columns, translators].hash
528
- @@array_tuples_converters_mutex.synchronize do
529
- @@array_tuples_converters[sig] ||= array_tuples_compile_converter(@columns, translators)
530
- end
531
- end
532
-
533
- def array_tuples_compile_converter(columns, translators)
534
- tr = []
535
- columns.each_with_index do |column, idx|
536
- if !AUTO_TRANSLATE and t = translators[idx]
537
- tr << "if (v = r[#{idx}]); r[#{idx}] = v.#{t}; end"
538
- end
539
- end
540
- eval("lambda {|r| r.keys = columns; #{tr.join(';')}; r}")
541
- end
542
-
543
- def array_tuples_transform_load(r)
544
- a = []; a.keys = []
545
- r.each_pair do |k, v|
546
- a[k] = (tt = @transform[k]) ? tt[0][v] : v
547
- end
548
- a
549
- end
550
-
551
- # Applies the value transform for data saved to the database.
552
- def array_tuples_transform_save(r)
553
- a = []; a.keys = []
554
- r.each_pair do |k, v|
555
- a[k] = (tt = @transform[k]) ? tt[1][v] : v
556
- end
557
- a
558
- end
559
519
  end
560
520
  end
561
521
  end
@@ -14,6 +14,7 @@ module Sequel
14
14
  @opts[:database] = ':memory:'
15
15
  end
16
16
  db = ::SQLite3::Database.new(@opts[:database])
17
+ db.busy_timeout(@opts.fetch(:timeout, 5000))
17
18
  db.type_translation = true
18
19
  # fix for timestamp translation
19
20
  db.translator.add_translator("timestamp") do |t, v|
@@ -131,6 +132,8 @@ module Sequel
131
132
  end
132
133
 
133
134
  class Dataset < Sequel::Dataset
135
+ def quote_column_ref(c); "`#{c}`"; end
136
+
134
137
  def literal(v)
135
138
  case v
136
139
  when Time
@@ -142,7 +145,7 @@ module Sequel
142
145
 
143
146
  def insert_sql(*values)
144
147
  if (values.size == 1) && values.first.is_a?(Sequel::Dataset)
145
- "INSERT INTO #{@opts[:from]} #{values.first.sql};"
148
+ "INSERT INTO #{source_list(@opts[:from])} #{values.first.sql};"
146
149
  else
147
150
  super(*values)
148
151
  end
@@ -160,13 +163,6 @@ module Sequel
160
163
  end
161
164
  end
162
165
 
163
- def array_tuples_fetch_rows(sql, &block)
164
- @db.execute_select(sql) do |result|
165
- @columns = result.columns.map {|c| c.to_sym}
166
- result.each {|r| r.keys = @columns; block[r]}
167
- end
168
- end
169
-
170
166
  def insert(*values)
171
167
  @db.execute_insert insert_sql(*values)
172
168
  end
@@ -0,0 +1,150 @@
1
+ require 'thread'
2
+
3
+ # A ConnectionPool manages access to database connections by keeping
4
+ # multiple connections and giving threads exclusive access to each
5
+ # connection.
6
+ class ConnectionPool
7
+ attr_reader :mutex
8
+
9
+ # The maximum number of connections.
10
+ attr_reader :max_size
11
+
12
+ # The proc used to create a new connection.
13
+ attr_accessor :connection_proc
14
+
15
+ attr_reader :available_connections, :allocated, :created_count
16
+
17
+ # Constructs a new pool with a maximum size. If a block is supplied, it
18
+ # is used to create new connections as they are needed.
19
+ #
20
+ # pool = ConnectionPool.new(10) {MyConnection.new(opts)}
21
+ #
22
+ # The connection creation proc can be changed at any time by assigning a
23
+ # Proc to pool#connection_proc.
24
+ #
25
+ # pool = ConnectionPool.new(10)
26
+ # pool.connection_proc = proc {MyConnection.new(opts)}
27
+ def initialize(max_size = 4, &block)
28
+ @max_size = max_size
29
+ @mutex = Mutex.new
30
+ @connection_proc = block
31
+
32
+ @available_connections = []
33
+ @allocated = {}
34
+ @created_count = 0
35
+ end
36
+
37
+ # Returns the number of created connections.
38
+ def size
39
+ @created_count
40
+ end
41
+
42
+ # Assigns a connection to the current thread, yielding the connection
43
+ # to the supplied block.
44
+ #
45
+ # pool.hold {|conn| conn.execute('DROP TABLE posts')}
46
+ #
47
+ # Pool#hold is re-entrant, meaning it can be called recursively in
48
+ # the same thread without blocking.
49
+ #
50
+ # If no connection is available, Pool#hold will block until a connection
51
+ # is available.
52
+ def hold
53
+ t = Thread.current
54
+ if (conn = owned_connection(t))
55
+ return yield(conn)
56
+ end
57
+ while !(conn = acquire(t))
58
+ sleep 0.001
59
+ end
60
+ begin
61
+ yield conn
62
+ ensure
63
+ release(t)
64
+ end
65
+ rescue Exception => e
66
+ # if the error is not a StandardError it is converted into RuntimeError.
67
+ raise e.is_a?(StandardError) ? e : e.message
68
+ end
69
+
70
+ # Removes all connection currently available, optionally yielding each
71
+ # connection to the given block. This method has the effect of
72
+ # disconnecting from the database. Once a connection is requested using
73
+ # #hold, the connection pool creates new connections to the database.
74
+ def disconnect(&block)
75
+ @mutex.synchronize do
76
+ @available_connections.each {|c| block[c]} if block
77
+ @available_connections = []
78
+ @created_count = @allocated.size
79
+ end
80
+ end
81
+
82
+ private
83
+ # Returns the connection owned by the supplied thread, if any.
84
+ def owned_connection(thread)
85
+ @mutex.synchronize {@allocated[thread]}
86
+ end
87
+
88
+ # Assigns a connection to the supplied thread, if one is available.
89
+ def acquire(thread)
90
+ @mutex.synchronize do
91
+ if conn = available
92
+ @allocated[thread] = conn
93
+ end
94
+ end
95
+ end
96
+
97
+ # Returns an available connection. If no connection is available,
98
+ # tries to create a new connection.
99
+ def available
100
+ @available_connections.pop || make_new
101
+ end
102
+
103
+ # Creates a new connection if the size of the pool is less than the
104
+ # maximum size.
105
+ def make_new
106
+ if @created_count < @max_size
107
+ @created_count += 1
108
+ @connection_proc ? @connection_proc.call : \
109
+ (raise Error, "No connection proc specified")
110
+ end
111
+ end
112
+
113
+ # Releases the connection assigned to the supplied thread.
114
+ def release(thread)
115
+ @mutex.synchronize do
116
+ @available_connections << @allocated[thread]
117
+ @allocated.delete(thread)
118
+ end
119
+ end
120
+ end
121
+
122
+ # A SingleThreadedPool acts as a replacement for a ConnectionPool for use
123
+ # in single-threaded applications. ConnectionPool imposes a substantial
124
+ # performance penalty, so SingleThreadedPool is used to gain some speed.
125
+ class SingleThreadedPool
126
+ attr_reader :conn
127
+ attr_writer :connection_proc
128
+
129
+ # Initializes the instance with the supplied block as the connection_proc.
130
+ def initialize(&block)
131
+ @connection_proc = block
132
+ end
133
+
134
+ # Yields the connection to the supplied block. This method simulates the
135
+ # ConnectionPool#hold API.
136
+ def hold
137
+ @conn ||= @connection_proc.call
138
+ yield @conn
139
+ rescue Exception => e
140
+ # if the error is not a StandardError it is converted into RuntimeError.
141
+ raise e.is_a?(StandardError) ? e : e.message
142
+ end
143
+
144
+ # Disconnects from the database. Once a connection is requested using
145
+ # #hold, the connection is reestablished.
146
+ def disconnect(&block)
147
+ block[@conn] if block && @conn
148
+ @conn = nil
149
+ end
150
+ end