sequel 2.9.0 → 2.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (78) hide show
  1. data/CHANGELOG +56 -0
  2. data/{README → README.rdoc} +85 -57
  3. data/Rakefile +10 -5
  4. data/bin/sequel +7 -16
  5. data/doc/advanced_associations.rdoc +5 -17
  6. data/doc/cheat_sheet.rdoc +18 -20
  7. data/doc/dataset_filtering.rdoc +8 -32
  8. data/doc/schema.rdoc +20 -0
  9. data/lib/sequel_core.rb +35 -1
  10. data/lib/sequel_core/adapters/ado.rb +1 -1
  11. data/lib/sequel_core/adapters/db2.rb +2 -2
  12. data/lib/sequel_core/adapters/dbi.rb +2 -11
  13. data/lib/sequel_core/adapters/do.rb +205 -0
  14. data/lib/sequel_core/adapters/do/mysql.rb +38 -0
  15. data/lib/sequel_core/adapters/do/postgres.rb +92 -0
  16. data/lib/sequel_core/adapters/do/sqlite.rb +31 -0
  17. data/lib/sequel_core/adapters/firebird.rb +298 -0
  18. data/lib/sequel_core/adapters/informix.rb +10 -1
  19. data/lib/sequel_core/adapters/jdbc.rb +78 -19
  20. data/lib/sequel_core/adapters/jdbc/h2.rb +69 -0
  21. data/lib/sequel_core/adapters/jdbc/mysql.rb +10 -0
  22. data/lib/sequel_core/adapters/jdbc/postgresql.rb +7 -3
  23. data/lib/sequel_core/adapters/mysql.rb +53 -77
  24. data/lib/sequel_core/adapters/odbc.rb +1 -1
  25. data/lib/sequel_core/adapters/openbase.rb +1 -1
  26. data/lib/sequel_core/adapters/oracle.rb +2 -2
  27. data/lib/sequel_core/adapters/postgres.rb +16 -14
  28. data/lib/sequel_core/adapters/shared/mysql.rb +44 -9
  29. data/lib/sequel_core/adapters/shared/oracle.rb +4 -5
  30. data/lib/sequel_core/adapters/shared/postgres.rb +86 -82
  31. data/lib/sequel_core/adapters/shared/sqlite.rb +39 -24
  32. data/lib/sequel_core/adapters/sqlite.rb +11 -1
  33. data/lib/sequel_core/connection_pool.rb +10 -2
  34. data/lib/sequel_core/core_sql.rb +13 -3
  35. data/lib/sequel_core/database.rb +131 -30
  36. data/lib/sequel_core/database/schema.rb +5 -5
  37. data/lib/sequel_core/dataset.rb +31 -6
  38. data/lib/sequel_core/dataset/convenience.rb +11 -11
  39. data/lib/sequel_core/dataset/query.rb +2 -2
  40. data/lib/sequel_core/dataset/sql.rb +6 -6
  41. data/lib/sequel_core/exceptions.rb +4 -0
  42. data/lib/sequel_core/migration.rb +4 -4
  43. data/lib/sequel_core/schema/generator.rb +19 -3
  44. data/lib/sequel_core/schema/sql.rb +24 -20
  45. data/lib/sequel_core/sql.rb +13 -16
  46. data/lib/sequel_core/version.rb +11 -0
  47. data/lib/sequel_model.rb +2 -0
  48. data/lib/sequel_model/base.rb +2 -2
  49. data/lib/sequel_model/hooks.rb +46 -7
  50. data/lib/sequel_model/record.rb +11 -9
  51. data/lib/sequel_model/schema.rb +1 -1
  52. data/lib/sequel_model/validations.rb +72 -61
  53. data/spec/adapters/firebird_spec.rb +371 -0
  54. data/spec/adapters/mysql_spec.rb +118 -62
  55. data/spec/adapters/oracle_spec.rb +5 -5
  56. data/spec/adapters/postgres_spec.rb +33 -18
  57. data/spec/adapters/sqlite_spec.rb +2 -2
  58. data/spec/integration/dataset_test.rb +3 -3
  59. data/spec/integration/schema_test.rb +55 -5
  60. data/spec/integration/spec_helper.rb +11 -0
  61. data/spec/integration/type_test.rb +59 -16
  62. data/spec/sequel_core/connection_pool_spec.rb +14 -0
  63. data/spec/sequel_core/core_sql_spec.rb +24 -14
  64. data/spec/sequel_core/database_spec.rb +96 -11
  65. data/spec/sequel_core/dataset_spec.rb +97 -37
  66. data/spec/sequel_core/expression_filters_spec.rb +51 -40
  67. data/spec/sequel_core/object_graph_spec.rb +2 -2
  68. data/spec/sequel_core/schema_generator_spec.rb +31 -6
  69. data/spec/sequel_core/schema_spec.rb +25 -9
  70. data/spec/sequel_core/spec_helper.rb +4 -1
  71. data/spec/sequel_core/version_spec.rb +7 -0
  72. data/spec/sequel_model/associations_spec.rb +1 -1
  73. data/spec/sequel_model/hooks_spec.rb +68 -13
  74. data/spec/sequel_model/model_spec.rb +4 -4
  75. data/spec/sequel_model/record_spec.rb +22 -0
  76. data/spec/sequel_model/spec_helper.rb +2 -1
  77. data/spec/sequel_model/validations_spec.rb +107 -7
  78. metadata +15 -5
@@ -16,7 +16,7 @@ a different block when eager loading via Dataset#eager. Association blocks are
16
16
  useful for things like:
17
17
 
18
18
  Artist.one_to_many :gold_albums, :class=>:Album do |ds|
19
- ds.filter(:copies_sold > 500000)
19
+ ds.filter{|o| o.copies_sold > 500000}
20
20
  end
21
21
 
22
22
  There are a whole bunch of options for changing how the association is eagerly
@@ -124,7 +124,7 @@ a swiss army chainsaw.
124
124
  Sequel supports the same callbacks that ActiveRecord does: :before_add,
125
125
  :before_remove, :after_add, and :after_remove. It also supports a
126
126
  callback that ActiveRecord does not, :after_load, which is called
127
- after the association has been loaded (when lazy loading).
127
+ after the association has been loaded.
128
128
 
129
129
  Each of these options can be a Symbol specifying an instance method
130
130
  that takes one argument (the associated object), or a Proc that takes
@@ -150,7 +150,7 @@ otherwise modified:
150
150
  class Author < Sequel::Model
151
151
  one_to_many :authorships
152
152
  end
153
- Author.first.authorships_dataset.filter(:number < 10).first
153
+ Author.first.authorships_dataset.filter{|o| o.number < 10}.first
154
154
 
155
155
  You can extend a dataset with a module easily with :extend:
156
156
 
@@ -180,18 +180,6 @@ model object, you'll have to use a closure:
180
180
  end
181
181
  Author.first.authorships_dataset.find_or_create_by_name('Bob')
182
182
 
183
- You can cheat if you want to:
184
-
185
- module FindOrCreate
186
- def find_or_create(vals)
187
- # Exploits the fact that Sequel filters are ruby objects that
188
- # can be introspected.
189
- author_id = @opts[:where].args[1]
190
- first(vals) || \
191
- @opts[:models][nil].create(vals.merge(:author_id=>author_id))
192
- end
193
- end
194
-
195
183
  ===has_many :through associations
196
184
 
197
185
  many_to_many handles the usual case of a has_many :through with a belongs_to in
@@ -310,7 +298,7 @@ Sequel::Model:
310
298
  Firm.find(:first).invoices
311
299
 
312
300
  It is significantly more code in Sequel Model, but quite a bit of it is setting
313
- the intermediate associate record (the client) and the reciprocal association
301
+ the intermediate associated record (the client) and the reciprocal association
314
302
  in the associations cache for each object, which ActiveRecord won't do for you.
315
303
  The reason you would want to do this is that then firm.invoices.first.firm or
316
304
  firm.invoices.first.client doesn't do another query to get the firm/client.
@@ -551,7 +539,7 @@ node.children. You can even eager load the relationship up to a certain depth:
551
539
  # Eager load three generations of generations of children for a given node
552
540
  Node.filter(:id=>1).eager(:children=>{:children=>:children}).all.first
553
541
  # Load parents and grandparents for a group of nodes
554
- Node.filter(:id < 10).eager(:parent=>:parent).all
542
+ Node.filter{|o| o.id < 10}.eager(:parent=>:parent).all
555
543
 
556
544
  What if you want to get all ancestors up to the root node, or all descendents,
557
545
  without knowing the depth of the tree?
@@ -29,7 +29,7 @@ Without a filename argument, the sqlite adapter will setup a new sqlite database
29
29
  DB.fetch("SELECT name FROM users") do |row|
30
30
  p r[:name]
31
31
  end
32
- dataset = DB["SELECT age FROM users"]
32
+ dataset = DB["SELECT age FROM users WHERE name = ?", name]
33
33
  dataset.print
34
34
  dataset.map(:age)
35
35
 
@@ -70,17 +70,17 @@ Without a filename argument, the sqlite adapter will setup a new sqlite database
70
70
  dataset.map {|r| r[:name]}
71
71
  dataset.map(:name) # same effect as above
72
72
 
73
- dataset.inject {|sum, r| sum + r[:value]}
73
+ dataset.inject(0){|sum, r| sum + r[:value]}
74
74
 
75
75
  == Filtering (see also doc/dataset_filtering.rdoc)
76
76
 
77
77
  dataset.filter(:name => 'abc')
78
78
  dataset.filter('name = ?', 'abc')
79
- dataset.filter(:value > 100)
80
- dataset.exclude(:value <= 100)
79
+ dataset.filter{|o| o.value > 100}
80
+ dataset.exclude{|o| o.value <= 100}
81
81
 
82
82
  dataset.filter(:value => 50..100)
83
- dataset.where((:value >= 50) & (:value <= 100))
83
+ dataset.where{|o| (o.value >= 50) & (o.value <= 100)}
84
84
 
85
85
  dataset.where('value IN ?', [50,75,100])
86
86
 
@@ -91,11 +91,9 @@ Without a filename argument, the sqlite adapter will setup a new sqlite database
91
91
  # Filter using a subquery
92
92
  dataset.filter('price > ?', dataset.select('AVG(price) + 100'))
93
93
 
94
- === Advanced filtering using ruby expressions without blocks
94
+ === Advanced filtering using ruby expressions
95
95
 
96
- Available as of Sequel 2.0:
97
-
98
- DB[:items].filter(:price < 100).sql
96
+ DB[:items].filter{|o| o.price < 100}.sql
99
97
  #=> "SELECT * FROM items WHERE (price < 100)"
100
98
 
101
99
  DB[:items].filter(:name.like('AL%')).sql
@@ -103,7 +101,7 @@ Available as of Sequel 2.0:
103
101
 
104
102
  There's support for nested expressions with AND, OR and NOT:
105
103
 
106
- DB[:items].filter((:x > 5) & (:y > 10)).sql
104
+ DB[:items].filter{|o| (o.x > 5) & (o.y > 10)}.sql
107
105
  #=> "SELECT * FROM items WHERE ((x > 5) AND (y > 10))"
108
106
 
109
107
  DB[:items].filter({:x => 1, :y => 2}.sql_or & ~{:z => 3}).sql
@@ -114,8 +112,8 @@ You can use arithmetic operators and specify SQL functions:
114
112
  DB[:items].filter((:x + :y) > :z).sql
115
113
  #=> "SELECT * FROM items WHERE ((x + y) > z)"
116
114
 
117
- DB[:items].filter(:price < :AVG[:price] + 100).sql
118
- #=> "SELECT * FROM items WHERE (price < (AVG(price) + 100))"
115
+ DB[:items].filter{|o| :price - 100 < o.AVG(:price)}.sql
116
+ #=> "SELECT * FROM items WHERE ((price - 100) < AVG(price))"
119
117
 
120
118
  == Ordering
121
119
 
@@ -151,23 +149,24 @@ You can use arithmetic operators and specify SQL functions:
151
149
  dataset.avg(:price)
152
150
  dataset.sum(:stock)
153
151
 
154
- dataset.group(:category).select(:category, :AVG[:price])
152
+ dataset.group(:category).select(:category, :AVG.sql_function(:price))
155
153
 
156
154
  == SQL Functions / Literals
157
155
 
158
- dataset.update(:updated_at => :NOW[])
156
+ dataset.update(:updated_at => :NOW.sql_function)
159
157
  dataset.update(:updated_at => 'NOW()'.lit)
160
158
 
161
159
  dataset.update(:updated_at => "DateValue('1/1/2001')".lit)
162
- dataset.update(:updated_at => :DateValue['1/1/2001'])
160
+ dataset.update(:updated_at => :DateValue.sql_function('1/1/2001'))
163
161
 
164
162
  == Schema Manipulation
165
163
 
166
164
  DB.create_table :items do
167
165
  primary_key :id
168
- text :name, :unique => true, :null => false
166
+ String :name, :unique => true, :null => false
169
167
  boolean :active, :default => true
170
168
  foreign_key :category_id, :categories
169
+ Time :created_at
171
170
 
172
171
  index :grade
173
172
  end
@@ -175,14 +174,13 @@ You can use arithmetic operators and specify SQL functions:
175
174
  DB.drop_table :items
176
175
 
177
176
  DB.create_table :test do
178
- varchar :zipcode, :size => 10
177
+ String :zipcode, :size => 10
179
178
  enum :system, :elements => ['mac', 'linux', 'windows']
180
179
  end
181
180
 
182
181
  == Aliasing
183
182
 
184
183
  DB[:items].select(:name.as(:item_name))
185
- DB[:items].select(:name => :item_name)
186
184
  DB[:items].select(:name___item_name)
187
185
  DB[:items___items_table].select(:items_table__name___item_name)
188
186
  # => "SELECT items_table.name AS item_name FROM items AS items_table"
@@ -221,5 +219,5 @@ Miscellaneous:
221
219
  dataset.where(:name => 'sequel').exists #=> "EXISTS ( SELECT 1 FROM items WHERE name = 'sequel' )"
222
220
  dataset.print #=> pretty table print to $stdout
223
221
  dataset.columns #=> array of columns in the result set, does a SELECT
224
- DB.schema_for_table(:items) => [[:id, {:type=>:integer, ...}], [:name, {:type=>:string, ...}], ...]
225
- # Works on PostgreSQL, MySQL, SQLite, and possibly elsewhere
222
+ DB.schema(:items) => [[:id, {:type=>:integer, ...}], [:name, {:type=>:string, ...}], ...]
223
+ # Works on PostgreSQL, MySQL, SQLite, and JDBC
@@ -14,35 +14,11 @@ In order to prevent SQL injection, you can replace literal values with question
14
14
  items.filter('category = ?', 'ruby').sql
15
15
  #=> "SELECT * FROM items WHERE category = 'ruby'"
16
16
 
17
- == An aside: column references in Sequel
18
-
19
- Sequel expects column names to be specified using symbols. In addition, tuples always use symbols as their keys. This allows you to freely mix literal values and column references. For example, the two following lines produce equivalent SQL:
20
-
21
- items.filter(:x => 1) #=> "SELECT * FROM items WHERE (x = 1)"
22
- items.filter(1 => :x) #=> "SELECT * FROM items WHERE (1 = x)"
23
-
24
- === Qualifying column names
25
-
26
- Column references can be qualified by using the double underscore special notation :table__column:
27
-
28
- items.literal(:items__price) #=> "items.price"
29
-
30
- === Column aliases
31
-
32
- You can also alias columns by using the triple undersecore special notation :column___alias or :table__column___alias:
33
-
34
- items.literal(:price___p) #=> "price AS p"
35
- items.literal(:items__price___p) #=> "items.price AS p"
36
-
37
- Another way to alias columns is to use the #AS method:
38
-
39
- items.literal(:price.as(:p)) #=> "price AS p"
40
-
41
17
  === Specifying SQL functions
42
18
 
43
- Sequel also allows you to specify functions by using the Symbol#[] method:
19
+ Sequel also allows you to specify functions by using the Symbol#sql_function method (and the Symbol#[] method on ruby 1.8):
44
20
 
45
- items.literal(:avg[:price]) #=> "avg(price)"
21
+ items.literal(:avg.sql_function(:price)) #=> "avg(price)"
46
22
 
47
23
  == Filtering using a hash
48
24
 
@@ -76,10 +52,10 @@ Ranges (both inclusive and exclusive) can also be used:
76
52
 
77
53
  == Filtering using expressions
78
54
 
79
- New in Sequel 2.0 is the ability to use ruby expressions directly in the call to filter, without using a block:
55
+ Sequel allows you to use ruby expressions directly in the call to filter, without using a block:
80
56
 
81
- items.filter(:price < 100).sql
82
- #=> "SELECT * FROM items WHERE (price < 100)
57
+ items.filter(:price * 2 < 50).sql
58
+ #=> "SELECT * FROM items WHERE ((price * 2) < 50)
83
59
 
84
60
  This works for the standard inequality and arithmetic operators:
85
61
 
@@ -133,13 +109,13 @@ You can use the negation operator (~) in most cases:
133
109
 
134
110
  You can also compare against other columns:
135
111
 
136
- items.filter(:credit > :debit).sql
112
+ items.filter{|o| o.credit > :debit}.sql
137
113
  #=> "SELECT * FROM items WHERE (credit > debit)
138
114
 
139
115
  Or against SQL functions:
140
116
 
141
- items.filter(:price < :max[:price] + 100).sql
142
- #=> "SELECT * FROM items WHERE (price < (max(price) + 100))"
117
+ items.filter{|o| :price - 100 < o.max(:price)}.sql
118
+ #=> "SELECT * FROM items WHERE ((price - 100) < max(price))"
143
119
 
144
120
  == String search functions
145
121
 
@@ -7,3 +7,23 @@ The recommended way to set up schema modifications in Sequel is through migratio
7
7
  The format of the individual migration files themselves is explained in the Sequel::Migration documentation. Each migration file contains a single migration class. The migration class acts a proxy for the related database (given on the command line if the sequel command line tool is used, or by the db argument to Sequel::Migration#apply if the API is used). The methods that can be used inside Sequel::Migration#up or Sequel::Migration#down are just Sequel::Database instance methods, such as create_table, drop_table, and alter_table. Most database methods that alter the schema take regular arguments, but create_table and alter_table take a block. The methods you can use inside the create_table block are documented in Sequel::Schema::Generator, and the methods you can use inside the alter_table block are documented in Sequel::Schema::AlterTableGenerator.
8
8
 
9
9
  Migrations are not required, you can just call the schema modification methods directly on the database object. This is often done in test code and examples. However, it is recommended that you use the migration framework unless the database schema will not be changing in the future, as it provides a way to easily handle modifications to existing database schema.
10
+
11
+ Also, new in Sequel 2.10 is the ability to have database independent migrations using ruby classes as types. When you use a ruby class as a type, Sequel translates it to the most comparable type in the database you are using. Here's an example using all supported types:
12
+
13
+ DB.create_table(:cats) do
14
+ primary_key :id, :type=>Integer # integer
15
+ String :a # varchar(255)
16
+ column :b, File # blob
17
+ Fixnum :c # integer
18
+ foreign_key :d, :other_table, :type=>Bignum # bigint
19
+ Float :e # double precision
20
+ BigDecimal :f # numeric
21
+ Date :g # date
22
+ DateTime :h # timestamp
23
+ Time :i # timestamp
24
+ Numeric :j # numeric
25
+ TrueClass :k # boolean
26
+ FalseClass :l # boolean
27
+ end
28
+
29
+ Basically, if you use one of the ruby classes above, it will translate into a database specific type. If you use a lowercase method, symbol, or string to specify the type, Sequel won't attempt to translate it.
@@ -2,7 +2,7 @@
2
2
  require f
3
3
  end
4
4
  %w"core_ext sql core_sql connection_pool exceptions pretty_table
5
- dataset migration schema database object_graph".each do |f|
5
+ dataset migration schema database object_graph version".each do |f|
6
6
  require "sequel_core/#{f}"
7
7
  end
8
8
 
@@ -69,6 +69,37 @@ module Sequel
69
69
  end
70
70
  metaalias :open, :connect
71
71
 
72
+ # Set the method to call on identifiers going into the database. This affects
73
+ # the literalization of identifiers by calling this method on them before they are input.
74
+ # Sequel upcases identifiers in all SQL strings for most databases, so to turn that off:
75
+ #
76
+ # Sequel.identifier_input_method = nil
77
+ #
78
+ # to downcase instead:
79
+ #
80
+ # Sequel.identifier_input_method = :downcase
81
+ #
82
+ # Other string methods work as well.
83
+ def self.identifier_input_method=(value)
84
+ Database.identifier_input_method = value
85
+ end
86
+
87
+ # Set the method to call on identifiers coming out of the database. This affects
88
+ # the literalization of identifiers by calling this method on them when they are
89
+ # retrieved from the database. Sequel downcases identifiers retrieved for most
90
+ # databases, so to turn that off:
91
+ #
92
+ # Sequel.identifier_output_method = nil
93
+ #
94
+ # to upcase instead:
95
+ #
96
+ # Sequel.identifier_output_method = :upcase
97
+ #
98
+ # Other string methods work as well.
99
+ def self.identifier_output_method=(value)
100
+ Database.identifier_output_method = value
101
+ end
102
+
72
103
  # Set whether to quote identifiers for all databases by default. By default,
73
104
  # Sequel quotes identifiers in all SQL strings, so to turn that off:
74
105
  #
@@ -92,6 +123,9 @@ module Sequel
92
123
  # lower case (MySQL, PostgreSQL, and SQLite).
93
124
  #
94
125
  # Sequel.upcase_identifiers = false
126
+ #
127
+ # This will set the indentifier_input_method to :upcase if value is true
128
+ # or nil if value is false.
95
129
  def self.upcase_identifiers=(value)
96
130
  Database.upcase_identifiers = value
97
131
  end
@@ -58,7 +58,7 @@ module Sequel
58
58
  execute(sql) do |s|
59
59
  @columns = s.Fields.extend(Enumerable).map do |column|
60
60
  name = column.Name.empty? ? '(no column name)' : column.Name
61
- name.to_sym
61
+ output_identifier(name)
62
62
  end
63
63
 
64
64
  unless s.eof
@@ -89,7 +89,7 @@ module Sequel
89
89
  def fetch_rows(sql)
90
90
  execute(sql) do |sth|
91
91
  @column_info = get_column_info(sth)
92
- @columns = @column_info.map {|c| c[:name]}
92
+ @columns = @column_info.map {|c| output_identifier(c[:name])}
93
93
  while (rc = SQLFetch(@handle)) != SQL_NO_DATA_FOUND
94
94
  @db.check_error(rc, "Could not fetch row")
95
95
  yield hash_row(sth)
@@ -118,7 +118,7 @@ module Sequel
118
118
  rc, v = SQLGetData(sth, i+1, c[:db2_type], c[:precision])
119
119
  @db.check_error(rc, "Could not get data")
120
120
 
121
- @row[c[:name]] = convert_type(v)
121
+ row[output_identifier(c[:name])] = convert_type(v)
122
122
  end
123
123
  row
124
124
  end
@@ -3,8 +3,6 @@ require 'dbi'
3
3
  module Sequel
4
4
  module DBI
5
5
  class Database < Sequel::Database
6
- attr_writer :lowercase
7
-
8
6
  set_adapter_scheme :dbi
9
7
 
10
8
  DBI_ADAPTERS = {
@@ -69,11 +67,6 @@ module Sequel
69
67
  synchronize(opts[:server]){|conn| conn.do(sql)}
70
68
  end
71
69
  alias_method :execute_dui, :do
72
-
73
- # Converts all column names to lowercase
74
- def lowercase
75
- @lowercase ||= false
76
- end
77
70
 
78
71
  private
79
72
 
@@ -97,10 +90,8 @@ module Sequel
97
90
  def fetch_rows(sql, &block)
98
91
  execute(sql) do |s|
99
92
  begin
100
- @columns = s.column_names.map do |c|
101
- @db.lowercase ? c.downcase.to_sym : c.to_sym
102
- end
103
- s.fetch {|r| yield hash_row(s, r)}
93
+ @columns = s.column_names.map{|c| output_identifier(c)}
94
+ s.fetch{|r| yield hash_row(s, r)}
104
95
  ensure
105
96
  s.finish rescue nil
106
97
  end
@@ -0,0 +1,205 @@
1
+ require 'data_objects'
2
+
3
+ module Sequel
4
+ # Module holding the DataObjects support for Sequel. DataObjects is a
5
+ # ruby library with a standard API for accessing databases.
6
+ #
7
+ # The DataObjects adapter currently supports PostgreSQL, MySQL, and
8
+ # SQLite:
9
+ #
10
+ # * Sequel.connect('do:sqlite3::memory:')
11
+ # * Sequel.connect('do:postgres://user:password@host/database')
12
+ # * Sequel.connect('do:mysql://user:password@host/database')
13
+ module DataObjects
14
+ # Contains procs keyed on sub adapter type that extend the
15
+ # given database object so it supports the correct database type.
16
+ DATABASE_SETUP = {:postgres=>proc do |db|
17
+ require 'do_postgres'
18
+ require 'sequel_core/adapters/do/postgres'
19
+ db.converted_exceptions << PostgresError
20
+ db.extend(Sequel::DataObjects::Postgres::DatabaseMethods)
21
+ end,
22
+ :mysql=>proc do |db|
23
+ require 'do_mysql'
24
+ require 'sequel_core/adapters/do/mysql'
25
+ db.converted_exceptions << MysqlError
26
+ db.extend(Sequel::DataObjects::MySQL::DatabaseMethods)
27
+ end,
28
+ :sqlite3=>proc do |db|
29
+ require 'do_sqlite3'
30
+ require 'sequel_core/adapters/do/sqlite'
31
+ db.converted_exceptions << Sqlite3Error
32
+ db.extend(Sequel::DataObjects::SQLite::DatabaseMethods)
33
+ end
34
+ }
35
+
36
+ # DataObjects uses it's own internal connection pooling in addition to the
37
+ # pooling that Sequel uses. You should make sure that you don't set
38
+ # the connection pool size to more than 8 for a
39
+ # Sequel::DataObjects::Database object, or hack DataObjects (or Extlib) to
40
+ # use a pool size at least as large as the pool size being used by Sequel.
41
+ class Database < Sequel::Database
42
+ set_adapter_scheme :do
43
+
44
+ # Convert the given exceptions to Sequel:Errors, necessary
45
+ # because DO raises errors specific to database types in
46
+ # certain cases.
47
+ attr_accessor :converted_exceptions
48
+
49
+ # Call the DATABASE_SETUP proc directly after initialization,
50
+ # so the object always uses sub adapter specific code. Also,
51
+ # raise an error immediately if the connection doesn't have a
52
+ # uri, since DataObjects requires one.
53
+ def initialize(opts)
54
+ @opts = opts
55
+ @converted_exceptions = []
56
+ raise(Error, "No connection string specified") unless uri
57
+ if prok = DATABASE_SETUP[subadapter.to_sym]
58
+ prok.call(self)
59
+ end
60
+ super(opts)
61
+ end
62
+
63
+ # Setup a DataObjects::Connection to the database.
64
+ def connect(server)
65
+ setup_connection(::DataObjects::Connection.new(uri(server_opts(server))))
66
+ end
67
+
68
+ # Return a Sequel::DataObjects::Dataset object for this database.
69
+ def dataset(opts = nil)
70
+ DataObjects::Dataset.new(self, opts)
71
+ end
72
+
73
+ # Execute the given SQL. If a block is given, the DataObjects::Reader
74
+ # created is yielded to it. A block should not be provided unless a
75
+ # a SELECT statement is being used (or something else that returns rows).
76
+ # Otherwise, the return value is the insert id if opts[:type] is :insert,
77
+ # or the number of affected rows, otherwise.
78
+ def execute(sql, opts={})
79
+ log_info(sql)
80
+ synchronize(opts[:server]) do |conn|
81
+ begin
82
+ command = conn.create_command(sql)
83
+ res = block_given? ? command.execute_reader : command.execute_non_query
84
+ rescue Exception => e
85
+ raise_error(e, :classes=>@converted_exceptions)
86
+ end
87
+ if block_given?
88
+ begin
89
+ yield(res)
90
+ ensure
91
+ res.close if res
92
+ end
93
+ elsif opts[:type] == :insert
94
+ res.insert_id
95
+ else
96
+ res.affected_rows
97
+ end
98
+ end
99
+ end
100
+
101
+ # Execute the SQL on the this database, returning the number of affected
102
+ # rows.
103
+ def execute_dui(sql, opts={})
104
+ execute(sql, opts)
105
+ end
106
+
107
+ # Execute the SQL on this database, returning the primary key of the
108
+ # table being inserted to.
109
+ def execute_insert(sql, opts={})
110
+ execute(sql, opts.merge(:type=>:insert))
111
+ end
112
+
113
+ # Return the subadapter type for this database, i.e. sqlite3 for
114
+ # do:sqlite3::memory:.
115
+ def subadapter
116
+ uri.split(":").first
117
+ end
118
+
119
+ # Use DataObject's transaction support for transactions. This
120
+ # only supports single level transactions, and it always prepares
121
+ # transactions and commits them immediately after. It's wasteful,
122
+ # but required by DataObject's API.
123
+ def transaction(server=nil)
124
+ th = Thread.current
125
+ synchronize(server) do |conn|
126
+ return yield(conn) if @transactions.include?(th)
127
+ t = ::DataObjects::Transaction.create_for_uri(uri)
128
+ t.instance_variable_get(:@connection).close
129
+ t.instance_variable_set(:@connection, conn)
130
+ begin
131
+ log_info("Transaction.begin")
132
+ t.begin
133
+ @transactions << th
134
+ yield(conn)
135
+ rescue Exception => e
136
+ log_info("Transaction.rollback")
137
+ t.rollback
138
+ transaction_error(e)
139
+ ensure
140
+ unless e
141
+ log_info("Transaction.commit")
142
+ t.prepare
143
+ t.commit
144
+ end
145
+ @transactions.delete(th)
146
+ end
147
+ end
148
+ end
149
+
150
+ # Return the DataObjects URI for the Sequel URI, removing the do:
151
+ # prefix.
152
+ def uri(opts={})
153
+ opts = @opts.merge(opts)
154
+ (opts[:uri] || opts[:url]).sub(/\Ado:/, '')
155
+ end
156
+
157
+ private
158
+
159
+ # Close the given database connection.
160
+ def disconnect_connection(c)
161
+ c.close
162
+ end
163
+
164
+ # Allow extending the given connection when it is first created.
165
+ # By default, just returns the connection.
166
+ def setup_connection(conn)
167
+ conn
168
+ end
169
+
170
+ # The DataObjects adapter should convert exceptions by default.
171
+ def connection_pool_default_options
172
+ super.merge(:pool_convert_exceptions=>false)
173
+ end
174
+ end
175
+
176
+ # Dataset class for Sequel::DataObjects::Database objects.
177
+ class Dataset < Sequel::Dataset
178
+ # Handle the usual time class overrides.
179
+ def literal(v)
180
+ case v
181
+ when Time
182
+ literal(v.iso8601)
183
+ when Date, DateTime
184
+ literal(v.to_s)
185
+ else
186
+ super
187
+ end
188
+ end
189
+
190
+ # Execute the SQL on the database and yield the rows as hashes
191
+ # with symbol keys.
192
+ def fetch_rows(sql)
193
+ execute(sql) do |reader|
194
+ cols = @columns = reader.fields.map{|f| output_identifier(f)}
195
+ while(reader.next!) do
196
+ h = {}
197
+ cols.zip(reader.values).each{|k, v| h[k] = v}
198
+ yield h
199
+ end
200
+ end
201
+ self
202
+ end
203
+ end
204
+ end
205
+ end