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.
- data/CHANGELOG +74 -0
- data/COPYING +1 -0
- data/README +17 -6
- data/Rakefile +16 -21
- data/lib/sequel_core.rb +18 -28
- data/lib/sequel_core/adapters/ado.rb +3 -15
- data/lib/sequel_core/adapters/dbi.rb +1 -14
- data/lib/sequel_core/adapters/informix.rb +3 -3
- data/lib/sequel_core/adapters/jdbc.rb +2 -2
- data/lib/sequel_core/adapters/mysql.rb +39 -59
- data/lib/sequel_core/adapters/odbc.rb +18 -38
- data/lib/sequel_core/adapters/openbase.rb +1 -17
- data/lib/sequel_core/adapters/oracle.rb +1 -19
- data/lib/sequel_core/adapters/postgres.rb +20 -60
- data/lib/sequel_core/adapters/sqlite.rb +4 -8
- data/lib/sequel_core/connection_pool.rb +150 -0
- data/lib/sequel_core/core_ext.rb +41 -0
- data/lib/sequel_core/core_sql.rb +35 -38
- data/lib/sequel_core/database.rb +20 -17
- data/lib/sequel_core/dataset.rb +49 -80
- data/lib/sequel_core/dataset/callback.rb +11 -13
- data/lib/sequel_core/dataset/convenience.rb +18 -136
- data/lib/sequel_core/dataset/pagination.rb +81 -0
- data/lib/sequel_core/dataset/sequelizer.rb +5 -4
- data/lib/sequel_core/dataset/sql.rb +43 -33
- data/lib/sequel_core/deprecated.rb +200 -0
- data/lib/sequel_core/exceptions.rb +0 -14
- data/lib/sequel_core/object_graph.rb +199 -0
- data/lib/sequel_core/pretty_table.rb +27 -24
- data/lib/sequel_core/schema/generator.rb +16 -4
- data/lib/sequel_core/schema/sql.rb +5 -3
- data/lib/sequel_core/worker.rb +1 -1
- data/spec/adapters/informix_spec.rb +1 -47
- data/spec/adapters/mysql_spec.rb +85 -54
- data/spec/adapters/oracle_spec.rb +1 -57
- data/spec/adapters/postgres_spec.rb +66 -49
- data/spec/adapters/sqlite_spec.rb +4 -29
- data/spec/connection_pool_spec.rb +358 -0
- data/spec/core_sql_spec.rb +24 -19
- data/spec/database_spec.rb +13 -9
- data/spec/dataset_spec.rb +59 -78
- data/spec/object_graph_spec.rb +202 -0
- data/spec/pretty_table_spec.rb +1 -9
- data/spec/schema_generator_spec.rb +7 -1
- data/spec/schema_spec.rb +27 -0
- data/spec/sequelizer_spec.rb +2 -2
- data/spec/spec_helper.rb +4 -2
- metadata +16 -57
- data/lib/sequel_core/array_keys.rb +0 -322
- data/lib/sequel_core/model.rb +0 -8
- 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
|
-
|
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 = "
|
335
|
-
|
336
|
-
|
337
|
-
|
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
|