sequel 3.39.0 → 3.40.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. data/CHANGELOG +30 -0
  2. data/README.rdoc +4 -3
  3. data/doc/active_record.rdoc +1 -1
  4. data/doc/opening_databases.rdoc +7 -0
  5. data/doc/release_notes/3.40.0.txt +73 -0
  6. data/lib/sequel/adapters/ado.rb +29 -3
  7. data/lib/sequel/adapters/ado/access.rb +334 -0
  8. data/lib/sequel/adapters/ado/mssql.rb +0 -6
  9. data/lib/sequel/adapters/cubrid.rb +143 -0
  10. data/lib/sequel/adapters/jdbc.rb +26 -18
  11. data/lib/sequel/adapters/jdbc/cubrid.rb +52 -0
  12. data/lib/sequel/adapters/jdbc/derby.rb +7 -7
  13. data/lib/sequel/adapters/jdbc/hsqldb.rb +5 -0
  14. data/lib/sequel/adapters/jdbc/mysql.rb +9 -4
  15. data/lib/sequel/adapters/mysql.rb +0 -3
  16. data/lib/sequel/adapters/mysql2.rb +0 -3
  17. data/lib/sequel/adapters/oracle.rb +4 -1
  18. data/lib/sequel/adapters/postgres.rb +4 -4
  19. data/lib/sequel/adapters/shared/access.rb +205 -3
  20. data/lib/sequel/adapters/shared/cubrid.rb +216 -0
  21. data/lib/sequel/adapters/shared/db2.rb +7 -2
  22. data/lib/sequel/adapters/shared/mssql.rb +3 -34
  23. data/lib/sequel/adapters/shared/mysql.rb +4 -33
  24. data/lib/sequel/adapters/shared/mysql_prepared_statements.rb +11 -0
  25. data/lib/sequel/adapters/shared/oracle.rb +5 -0
  26. data/lib/sequel/adapters/shared/postgres.rb +2 -1
  27. data/lib/sequel/adapters/utils/split_alter_table.rb +36 -0
  28. data/lib/sequel/database/connecting.rb +1 -1
  29. data/lib/sequel/database/query.rb +30 -7
  30. data/lib/sequel/database/schema_methods.rb +7 -2
  31. data/lib/sequel/dataset/query.rb +9 -10
  32. data/lib/sequel/dataset/sql.rb +14 -26
  33. data/lib/sequel/extensions/pg_hstore.rb +19 -0
  34. data/lib/sequel/extensions/pg_row.rb +5 -5
  35. data/lib/sequel/plugins/association_pks.rb +121 -18
  36. data/lib/sequel/plugins/json_serializer.rb +19 -0
  37. data/lib/sequel/sql.rb +11 -0
  38. data/lib/sequel/version.rb +1 -1
  39. data/spec/adapters/postgres_spec.rb +42 -0
  40. data/spec/core/database_spec.rb +17 -0
  41. data/spec/core/dataset_spec.rb +11 -0
  42. data/spec/core/expression_filters_spec.rb +13 -0
  43. data/spec/extensions/association_pks_spec.rb +163 -3
  44. data/spec/extensions/pg_hstore_spec.rb +6 -0
  45. data/spec/extensions/pg_row_spec.rb +17 -0
  46. data/spec/integration/associations_test.rb +1 -1
  47. data/spec/integration/dataset_test.rb +13 -13
  48. data/spec/integration/plugin_test.rb +232 -7
  49. data/spec/integration/schema_test.rb +8 -12
  50. data/spec/integration/spec_helper.rb +1 -1
  51. data/spec/integration/type_test.rb +6 -0
  52. metadata +9 -2
@@ -11,12 +11,6 @@ module Sequel
11
11
  # delete query.
12
12
  ROWS_AFFECTED = "SELECT @@ROWCOUNT AS AffectedRows"
13
13
 
14
- # Just execute so it doesn't attempt to return the number of rows modified.
15
- def execute_ddl(sql, opts={})
16
- execute(sql, opts)
17
- end
18
- alias execute_insert execute_ddl
19
-
20
14
  # Issue a separate query to get the rows modified. ADO appears to
21
15
  # use pass by reference with an integer variable, which is obviously
22
16
  # not supported directly in ruby, and I'm not aware of a workaround.
@@ -0,0 +1,143 @@
1
+ require 'cubrid'
2
+ Sequel.require 'adapters/shared/cubrid'
3
+
4
+ module Sequel
5
+ module Cubrid
6
+ CUBRID_TYPE_PROCS = {
7
+ ::Cubrid::DATE => lambda{|t| Date.new(t.year, t.month, t.day)},
8
+ ::Cubrid::TIME => lambda{|t| SQLTime.create(t.hour, t.min, t.sec)},
9
+ 21 => lambda{|s| s.to_i}
10
+ }
11
+
12
+ class Database < Sequel::Database
13
+ include Sequel::Cubrid::DatabaseMethods
14
+
15
+ ROW_COUNT = "SELECT ROW_COUNT()".freeze
16
+ LAST_INSERT_ID = "SELECT LAST_INSERT_ID()".freeze
17
+
18
+ set_adapter_scheme :cubrid
19
+
20
+ def connect(server)
21
+ opts = server_opts(server)
22
+ conn = ::Cubrid.connect(
23
+ opts[:database],
24
+ opts[:host] || 'localhost',
25
+ opts[:port] || 30000,
26
+ opts[:user] || 'public',
27
+ opts[:password] || ''
28
+ )
29
+ conn.auto_commit = true
30
+ conn
31
+ end
32
+
33
+ def server_version
34
+ @server_version ||= synchronize{|c| c.server_version}
35
+ end
36
+
37
+ def execute(sql, opts={})
38
+ synchronize(opts[:server]) do |conn|
39
+ r = log_yield(sql) do
40
+ begin
41
+ conn.query(sql)
42
+ rescue => e
43
+ raise_error(e)
44
+ end
45
+ end
46
+ if block_given?
47
+ yield(r)
48
+ else
49
+ begin
50
+ case opts[:type]
51
+ when :dui
52
+ # This is cubrid's API, but it appears to be completely broken,
53
+ # giving StandardError: ERROR: CCI, -18, Invalid request handle
54
+ #r.affected_rows
55
+
56
+ # Work around bugs by using the ROW_COUNT function.
57
+ begin
58
+ r2 = conn.query(ROW_COUNT)
59
+ r2.each{|a| return a.first.to_i}
60
+ ensure
61
+ r2.close if r2
62
+ end
63
+ when :insert
64
+ begin
65
+ r2 = conn.query(LAST_INSERT_ID)
66
+ r2.each{|a| return a.first.to_i}
67
+ ensure
68
+ r2.close if r2
69
+ end
70
+ end
71
+ ensure
72
+ r.close
73
+ end
74
+ end
75
+ end
76
+ end
77
+
78
+ def execute_ddl(sql, opts={})
79
+ execute(sql, opts.merge(:type=>:ddl))
80
+ end
81
+
82
+ def execute_dui(sql, opts={})
83
+ execute(sql, opts.merge(:type=>:dui))
84
+ end
85
+
86
+ def execute_insert(sql, opts={})
87
+ execute(sql, opts.merge(:type=>:insert))
88
+ end
89
+
90
+ private
91
+
92
+ def begin_transaction(conn, opts={})
93
+ log_yield(TRANSACTION_BEGIN){conn.auto_commit = false}
94
+ end
95
+
96
+ def commit_transaction(conn, opts={})
97
+ log_yield(TRANSACTION_COMMIT){conn.commit}
98
+ end
99
+
100
+ def disconnect_connection(c)
101
+ c.close
102
+ end
103
+
104
+ def remove_transaction(conn, committed)
105
+ conn.auto_commit = true
106
+ ensure
107
+ super
108
+ end
109
+
110
+ # This doesn't actually work, as the cubrid ruby driver
111
+ # does not implement transactions correctly.
112
+ def rollback_transaction(conn, opts={})
113
+ log_yield(TRANSACTION_ROLLBACK){conn.rollback}
114
+ end
115
+ end
116
+
117
+ class Dataset < Sequel::Dataset
118
+ include Sequel::Cubrid::DatasetMethods
119
+ COLUMN_INFO_NAME = "name".freeze
120
+ COLUMN_INFO_TYPE = "type_name".freeze
121
+
122
+ Database::DatasetClass = self
123
+
124
+ def fetch_rows(sql)
125
+ execute(sql) do |stmt|
126
+ begin
127
+ procs =
128
+ cols = stmt.column_info.map{|c| [output_identifier(c[COLUMN_INFO_NAME]), CUBRID_TYPE_PROCS[c[COLUMN_INFO_TYPE]]]}
129
+ @columns = cols.map{|c| c.first}
130
+ stmt.each do |r|
131
+ row = {}
132
+ cols.zip(r).each{|(k, p), v| row[k] = (v && p) ? p.call(v) : v}
133
+ yield row
134
+ end
135
+ ensure
136
+ stmt.close
137
+ end
138
+ end
139
+ self
140
+ end
141
+ end
142
+ end
143
+ end
@@ -122,6 +122,12 @@ module Sequel
122
122
  db.extend(Sequel::JDBC::Progress::DatabaseMethods)
123
123
  db.extend_datasets Sequel::Progress::DatasetMethods
124
124
  com.progress.sql.jdbc.JdbcProgressDriver
125
+ end,
126
+ :cubrid=>proc do |db|
127
+ Sequel.ts_require 'adapters/jdbc/cubrid'
128
+ db.extend(Sequel::JDBC::Cubrid::DatabaseMethods)
129
+ db.extend_datasets Sequel::Cubrid::DatasetMethods
130
+ Java::cubrid.jdbc.driver.CUBRIDDriver
125
131
  end
126
132
  }
127
133
 
@@ -248,13 +254,7 @@ module Sequel
248
254
  when :ddl
249
255
  log_yield(sql){stmt.execute(sql)}
250
256
  when :insert
251
- log_yield(sql) do
252
- if requires_return_generated_keys?
253
- stmt.executeUpdate(sql, JavaSQL::Statement.RETURN_GENERATED_KEYS)
254
- else
255
- stmt.executeUpdate(sql)
256
- end
257
- end
257
+ log_yield(sql){execute_statement_insert(stmt, sql)}
258
258
  last_insert_id(conn, opts.merge(:stmt=>stmt))
259
259
  else
260
260
  log_yield(sql){stmt.executeUpdate(sql)}
@@ -264,7 +264,7 @@ module Sequel
264
264
  end
265
265
  end
266
266
  alias execute_dui execute
267
-
267
+
268
268
  # Execute the given DDL SQL, which should not return any
269
269
  # values or rows.
270
270
  def execute_ddl(sql, opts={})
@@ -361,7 +361,7 @@ module Sequel
361
361
  cps = cps[1]
362
362
  else
363
363
  log_yield("CLOSE #{name}"){cps[1].close} if cps
364
- cps = log_yield("PREPARE#{" #{name}:" if name} #{sql}"){conn.prepareStatement(sql)}
364
+ cps = log_yield("PREPARE#{" #{name}:" if name} #{sql}"){prepare_jdbc_statement(conn, sql, opts)}
365
365
  cps_sync(conn){|cpsh| cpsh[name] = [sql, cps]} if name
366
366
  end
367
367
  i = 0
@@ -380,8 +380,8 @@ module Sequel
380
380
  when :ddl
381
381
  log_yield(msg, args){cps.execute}
382
382
  when :insert
383
- log_yield(msg, args){cps.executeUpdate}
384
- last_insert_id(conn, opts.merge(:prepared=>true))
383
+ log_yield(msg, args){execute_prepared_statement_insert(cps)}
384
+ last_insert_id(conn, opts.merge(:prepared=>true, :stmt=>cps))
385
385
  else
386
386
  log_yield(msg, args){cps.executeUpdate}
387
387
  end
@@ -394,6 +394,16 @@ module Sequel
394
394
  end
395
395
  end
396
396
 
397
+ # Execute the prepared insert statement
398
+ def execute_prepared_statement_insert(stmt)
399
+ stmt.executeUpdate
400
+ end
401
+
402
+ # Execute the insert SQL using the statement
403
+ def execute_statement_insert(stmt, sql)
404
+ stmt.executeUpdate(sql)
405
+ end
406
+
397
407
  # Gets the connection from JNDI.
398
408
  def get_connection_from_jndi
399
409
  jndi_name = JNDI_URI_REGEXP.match(uri)[1]
@@ -461,6 +471,11 @@ module Sequel
461
471
  end
462
472
  end
463
473
 
474
+ # Created a JDBC prepared statement on the connection with the given SQL.
475
+ def prepare_jdbc_statement(conn, sql, opts)
476
+ conn.prepareStatement(sql)
477
+ end
478
+
464
479
  # Java being java, you need to specify the type of each argument
465
480
  # for the prepared statement, and bind it individually. This
466
481
  # guesses which JDBC method to use, and hopefully JRuby will convert
@@ -545,13 +560,6 @@ module Sequel
545
560
  ensure
546
561
  stmt.close if stmt
547
562
  end
548
-
549
- # This method determines whether or not to add
550
- # Statement.RETURN_GENERATED_KEYS as an argument when inserting rows.
551
- # Sub-adapters that require this should override this method.
552
- def requires_return_generated_keys?
553
- false
554
- end
555
563
  end
556
564
 
557
565
  class Dataset < Sequel::Dataset
@@ -0,0 +1,52 @@
1
+ Sequel.require 'adapters/shared/cubrid'
2
+ Sequel.require 'adapters/jdbc/transactions'
3
+
4
+ module Sequel
5
+ module JDBC
6
+ module Cubrid
7
+ module DatabaseMethods
8
+ include Sequel::Cubrid::DatabaseMethods
9
+ include Sequel::JDBC::Transactions
10
+
11
+ def supports_savepoints?
12
+ false
13
+ end
14
+
15
+ private
16
+
17
+ # Get the last inserted id using LAST_INSERT_ID().
18
+ def last_insert_id(conn, opts={})
19
+ if stmt = opts[:stmt]
20
+ rs = stmt.getGeneratedKeys
21
+ begin
22
+ if rs.next
23
+ rs.getInt(1)
24
+ end
25
+ rescue NativeException
26
+ nil
27
+ ensure
28
+ rs.close
29
+ end
30
+ end
31
+ end
32
+
33
+ # Use execute instead of executeUpdate.
34
+ def execute_prepared_statement_insert(stmt)
35
+ stmt.execute
36
+ end
37
+
38
+ # Return generated keys for insert statements, and use
39
+ # execute intead of executeUpdate as CUBRID doesn't
40
+ # return generated keys in executeUpdate.
41
+ def execute_statement_insert(stmt, sql)
42
+ stmt.execute(sql, JavaSQL::Statement.RETURN_GENERATED_KEYS)
43
+ end
44
+
45
+ # Return generated keys for insert statements.
46
+ def prepare_jdbc_statement(conn, sql, opts)
47
+ opts[:type] == :insert ? conn.prepareStatement(sql, JavaSQL::Statement.RETURN_GENERATED_KEYS) : super
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -148,6 +148,11 @@ module Sequel
148
148
  super
149
149
  end
150
150
  end
151
+
152
+ # Derby uses clob for text types.
153
+ def uses_clob_for_text?
154
+ true
155
+ end
151
156
  end
152
157
 
153
158
  # Dataset class for Derby datasets accessed via JDBC.
@@ -174,15 +179,10 @@ module Sequel
174
179
  EMULATED_FUNCTION_MAP = {:char_length=>'length'.freeze}
175
180
 
176
181
  # Derby doesn't support an expression between CASE and WHEN,
177
- # so emulate it by using an equality statement for all of the
182
+ # so remove
178
183
  # conditions.
179
184
  def case_expression_sql_append(sql, ce)
180
- if ce.expression?
181
- e = ce.expression
182
- case_expression_sql_append(sql, ::Sequel::SQL::CaseExpression.new(ce.conditions.map{|c, r| [::Sequel::SQL::BooleanExpression.new(:'=', e, c), r]}, ce.default))
183
- else
184
- super
185
- end
185
+ super(sql, ce.with_merged_expression)
186
186
  end
187
187
 
188
188
  # If the type is String, trim the extra spaces since CHAR is used instead
@@ -83,6 +83,11 @@ module Sequel
83
83
  super
84
84
  end
85
85
  end
86
+
87
+ # HSQLDB uses clob for text types.
88
+ def uses_clob_for_text?
89
+ true
90
+ end
86
91
  end
87
92
 
88
93
  # Dataset class for HSQLDB datasets accessed via JDBC.
@@ -42,12 +42,17 @@ module Sequel
42
42
  end
43
43
  end
44
44
 
45
- # MySQL 5.1.12 JDBC adapter requires this to be true,
45
+ # MySQL 5.1.12 JDBC adapter requires generated keys
46
46
  # and previous versions don't mind.
47
- def requires_return_generated_keys?
48
- true
47
+ def execute_statement_insert(stmt, sql)
48
+ stmt.executeUpdate(sql, JavaSQL::Statement.RETURN_GENERATED_KEYS)
49
49
  end
50
-
50
+
51
+ # Return generated keys for insert statements.
52
+ def prepare_jdbc_statement(conn, sql, opts)
53
+ opts[:type] == :insert ? conn.prepareStatement(sql, JavaSQL::Statement.RETURN_GENERATED_KEYS) : super
54
+ end
55
+
51
56
  # Convert tinyint(1) type to boolean
52
57
  def schema_column_type(db_type)
53
58
  db_type == 'tinyint(1)' ? :boolean : super
@@ -41,9 +41,6 @@ module Sequel
41
41
  include Sequel::MySQL::DatabaseMethods
42
42
  include Sequel::MySQL::PreparedStatements::DatabaseMethods
43
43
 
44
- # Mysql::Error messages that indicate the current connection should be disconnected
45
- MYSQL_DATABASE_DISCONNECT_ERRORS = /\A(Commands out of sync; you can't run this command now|Can't connect to local MySQL server through socket|MySQL server has gone away|Lost connection to MySQL server during query)/
46
-
47
44
  # Regular expression used for getting accurate number of rows
48
45
  # matched by an update statement.
49
46
  AFFECTED_ROWS_RE = /Rows matched:\s+(\d+)\s+Changed:\s+\d+\s+Warnings:\s+\d+/.freeze
@@ -9,9 +9,6 @@ module Sequel
9
9
  include Sequel::MySQL::DatabaseMethods
10
10
  include Sequel::MySQL::PreparedStatements::DatabaseMethods
11
11
 
12
- # Mysql::Error messages that indicate the current connection should be disconnected
13
- MYSQL_DATABASE_DISCONNECT_ERRORS = /\A(Commands out of sync; you can't run this command now|Can't connect to local MySQL server through socket|MySQL server has gone away|This connection is still waiting for a result, try again once you have the result|closed MySQL connection)/
14
-
15
12
  set_adapter_scheme :mysql2
16
13
 
17
14
  # Whether to convert tinyint columns to bool for this database
@@ -13,7 +13,10 @@ module Sequel
13
13
  # ORA-03114: not connected to ORACLE
14
14
  CONNECTION_ERROR_CODES = [ 28, 1012, 3113, 3114 ]
15
15
 
16
- ORACLE_TYPES = {:blob=>lambda{|b| Sequel::SQL::Blob.new(b.read)}}
16
+ ORACLE_TYPES = {
17
+ :blob=>lambda{|b| Sequel::SQL::Blob.new(b.read)},
18
+ :clob=>lambda{|b| b.read}
19
+ }
17
20
 
18
21
  # Hash of conversion procs for this database.
19
22
  attr_reader :conversion_procs
@@ -95,7 +95,7 @@ module Sequel
95
95
  # PGconn subclass for connection specific methods used with the
96
96
  # pg, postgres, or postgres-pr driver.
97
97
  class Adapter < ::PGconn
98
- DISCONNECT_ERROR_RE = /\Acould not receive data from server: Software caused connection abort/
98
+ DISCONNECT_ERROR_RE = /\Acould not receive data from server/
99
99
 
100
100
  self.translate_results = false if respond_to?(:translate_results=)
101
101
 
@@ -109,7 +109,7 @@ module Sequel
109
109
  def check_disconnect_errors
110
110
  begin
111
111
  yield
112
- rescue PGError =>e
112
+ rescue PGError => e
113
113
  disconnect = false
114
114
  begin
115
115
  s = status
@@ -121,7 +121,7 @@ module Sequel
121
121
  disconnect ||= e.message =~ DISCONNECT_ERROR_RE
122
122
  disconnect ? raise(Sequel.convert_exception_class(e, Sequel::DatabaseDisconnectError)) : raise
123
123
  ensure
124
- block if status_ok
124
+ block if status_ok && !disconnect
125
125
  end
126
126
  end
127
127
 
@@ -382,7 +382,7 @@ module Sequel
382
382
  synchronize(opts[:server]) do |conn|
383
383
  begin
384
384
  channels = Array(channels)
385
- channels.each{|channel| conn.execute("LISTEN #{channel}")}
385
+ channels.each{|channel| conn.execute("LISTEN #{dataset.send(:table_ref, channel)}")}
386
386
  opts[:after_listen].call(conn) if opts[:after_listen]
387
387
  timeout = opts[:timeout] ? [opts[:timeout]] : []
388
388
  if l = opts[:loop]
@@ -7,8 +7,15 @@ module Sequel
7
7
  end
8
8
 
9
9
  # Doesn't work, due to security restrictions on MSysObjects
10
- def tables
11
- from(:MSysObjects).filter(:Type=>1, :Flags=>0).select_map(:Name).map{|x| x.to_sym}
10
+ #def tables
11
+ # from(:MSysObjects).filter(:Type=>1, :Flags=>0).select_map(:Name).map{|x| x.to_sym}
12
+ #end
13
+
14
+ # Access doesn't support renaming tables from an SQL query,
15
+ # so create a copy of the table and then drop the from table.
16
+ def rename_table(from_table, to_table)
17
+ create_table(to_table, :as=>from(from_table))
18
+ drop_table(from_table)
12
19
  end
13
20
 
14
21
  # Access uses type Counter for an autoincrementing keys
@@ -18,6 +25,29 @@ module Sequel
18
25
 
19
26
  private
20
27
 
28
+ def alter_table_op_sql(table, op)
29
+ case op[:op]
30
+ when :set_column_type
31
+ "ALTER COLUMN #{quote_identifier(op[:name])} #{type_literal(op)}"
32
+ else
33
+ super
34
+ end
35
+ end
36
+
37
+ # Access doesn't support CREATE TABLE AS, it only supports SELECT INTO.
38
+ # Emulating CREATE TABLE AS using SELECT INTO is only possible if a dataset
39
+ # is given as the argument, it can't work with a string, so raise an
40
+ # Error if a string is given.
41
+ def create_table_as(name, ds, options)
42
+ raise(Error, "must provide dataset instance as value of create_table :as option on Access") unless ds.is_a?(Sequel::Dataset)
43
+ run(ds.into(name).sql)
44
+ end
45
+
46
+ # The SQL to drop an index for the table.
47
+ def drop_index_sql(table, op)
48
+ "DROP INDEX #{quote_identifier(op[:name] || default_index_name(table, op[:columns]))} ON #{quote_schema_table(table)}"
49
+ end
50
+
21
51
  def identifier_input_method_default
22
52
  nil
23
53
  end
@@ -25,21 +55,155 @@ module Sequel
25
55
  def identifier_output_method_default
26
56
  nil
27
57
  end
58
+
59
+ # Access doesn't have a 64-bit integer type, so use integer and hope
60
+ # the user isn't using more than 32 bits.
61
+ def type_literal_generic_bignum(column)
62
+ :integer
63
+ end
64
+
65
+ # Access doesn't have a true boolean class, so it uses bit
66
+ def type_literal_generic_trueclass(column)
67
+ :bit
68
+ end
69
+
70
+ # Access uses image type for blobs
71
+ def type_literal_generic_file(column)
72
+ :image
73
+ end
28
74
  end
29
75
 
30
76
  module DatasetMethods
31
- SELECT_CLAUSE_METHODS = Dataset.clause_methods(:select, %w'select limit distinct columns from join where group order having compounds')
77
+ SELECT_CLAUSE_METHODS = Dataset.clause_methods(:select, %w'select distinct limit columns into from join where group order having compounds')
32
78
  DATE_FORMAT = '#%Y-%m-%d#'.freeze
33
79
  TIMESTAMP_FORMAT = '#%Y-%m-%d %H:%M:%S#'.freeze
34
80
  TOP = " TOP ".freeze
35
81
  BRACKET_CLOSE = Dataset::BRACKET_CLOSE
36
82
  BRACKET_OPEN = Dataset::BRACKET_OPEN
83
+ PAREN_CLOSE = Dataset::PAREN_CLOSE
84
+ PAREN_OPEN = Dataset::PAREN_OPEN
85
+ INTO = Dataset::INTO
86
+ FROM = Dataset::FROM
87
+ NOT_EQUAL = ' <> '.freeze
88
+ OPS = {:'%'=>' Mod '.freeze, :'||'=>' & '.freeze}
89
+ BOOL_FALSE = '0'.freeze
90
+ BOOL_TRUE = '-1'.freeze
91
+ DATE_FUNCTION = 'Date()'.freeze
92
+ NOW_FUNCTION = 'Now()'.freeze
93
+ TIME_FUNCTION = 'Time()'.freeze
94
+ CAST_TYPES = {String=>:CStr, Integer=>:CLng, Date=>:CDate, Time=>:CDate, DateTime=>:CDate, Numeric=>:CDec, BigDecimal=>:CDec, File=>:CStr, Float=>:CDbl, TrueClass=>:CBool, FalseClass=>:CBool}
95
+
96
+ EXTRACT_MAP = {:year=>"'yyyy'", :month=>"'m'", :day=>"'d'", :hour=>"'h'", :minute=>"'n'", :second=>"'s'"}
97
+ COMMA = Dataset::COMMA
98
+ DATEPART_OPEN = "datepart(".freeze
99
+
100
+ # Access doesn't support CASE, but it can be emulated with nested
101
+ # IIF function calls.
102
+ def case_expression_sql_append(sql, ce)
103
+ literal_append(sql, ce.with_merged_expression.conditions.reverse.inject(ce.default){|exp,(cond,val)| Sequel::SQL::Function.new(:IIF, cond, val, exp)})
104
+ end
105
+
106
+ # Access doesn't support CAST, it uses separate functions for
107
+ # type conversion
108
+ def cast_sql_append(sql, expr, type)
109
+ sql << CAST_TYPES.fetch(type, type).to_s
110
+ sql << PAREN_OPEN
111
+ literal_append(sql, expr)
112
+ sql << PAREN_CLOSE
113
+ end
114
+
115
+ def complex_expression_sql_append(sql, op, args)
116
+ case op
117
+ when :ILIKE
118
+ super(sql, :LIKE, args)
119
+ when :'NOT ILIKE'
120
+ super(sql, :'NOT LIKE', args)
121
+ when :'!='
122
+ sql << PAREN_OPEN
123
+ literal_append(sql, args.at(0))
124
+ sql << NOT_EQUAL
125
+ literal_append(sql, args.at(1))
126
+ sql << PAREN_CLOSE
127
+ when :'%', :'||'
128
+ sql << PAREN_OPEN
129
+ c = false
130
+ op_str = OPS[op]
131
+ args.each do |a|
132
+ sql << op_str if c
133
+ literal_append(sql, a)
134
+ c ||= true
135
+ end
136
+ sql << PAREN_CLOSE
137
+ when :extract
138
+ part = args.at(0)
139
+ raise(Sequel::Error, "unsupported extract argument: #{part.inspect}") unless format = EXTRACT_MAP[part]
140
+ sql << DATEPART_OPEN << format.to_s << COMMA
141
+ literal_append(sql, args.at(1))
142
+ sql << PAREN_CLOSE
143
+ else
144
+ super
145
+ end
146
+ end
147
+
148
+ # Use Date() and Now() for CURRENT_DATE and CURRENT_TIMESTAMP
149
+ def constant_sql_append(sql, constant)
150
+ case constant
151
+ when :CURRENT_DATE
152
+ sql << DATE_FUNCTION
153
+ when :CURRENT_TIMESTAMP
154
+ sql << NOW_FUNCTION
155
+ when :CURRENT_TIME
156
+ sql << TIME_FUNCTION
157
+ else
158
+ super
159
+ end
160
+ end
161
+
162
+ # Emulate cross join by using multiple tables in the FROM clause.
163
+ def cross_join(table)
164
+ clone(:from=>@opts[:from] + [table])
165
+ end
166
+
167
+ def emulated_function_sql_append(sql, f)
168
+ case f.f
169
+ when :char_length
170
+ literal_append(sql, SQL::Function.new(:len, f.args.first))
171
+ else
172
+ super
173
+ end
174
+ end
175
+
176
+ # Specify a table for a SELECT ... INTO query.
177
+ def into(table)
178
+ clone(:into => table)
179
+ end
37
180
 
38
181
  # Access doesn't support INTERSECT or EXCEPT
39
182
  def supports_intersect_except?
40
183
  false
41
184
  end
42
185
 
186
+ # Access does not support IS TRUE
187
+ def supports_is_true?
188
+ false
189
+ end
190
+
191
+ # Access doesn't support JOIN USING
192
+ def supports_join_using?
193
+ false
194
+ end
195
+
196
+ # Access does not support multiple columns for the IN/NOT IN operators
197
+ def supports_multiple_column_in?
198
+ false
199
+ end
200
+
201
+ # Access doesn't support truncate, so do a delete instead.
202
+ def truncate
203
+ delete
204
+ nil
205
+ end
206
+
43
207
  private
44
208
 
45
209
  # Access uses # to quote dates
@@ -53,6 +217,44 @@ module Sequel
53
217
  end
54
218
  alias literal_time literal_datetime
55
219
 
220
+ # Use 0 for false on MSSQL
221
+ def literal_false
222
+ BOOL_FALSE
223
+ end
224
+
225
+ # Use 0 for false on MSSQL
226
+ def literal_true
227
+ BOOL_TRUE
228
+ end
229
+
230
+ # Access requires parentheses when joining more than one table
231
+ def select_from_sql(sql)
232
+ if f = @opts[:from]
233
+ sql << FROM
234
+ if (j = @opts[:join]) && !j.empty?
235
+ sql << (PAREN_OPEN * j.length)
236
+ end
237
+ source_list_append(sql, f)
238
+ end
239
+ end
240
+
241
+ def select_into_sql(sql)
242
+ if i = @opts[:into]
243
+ sql << INTO
244
+ identifier_append(sql, i)
245
+ end
246
+ end
247
+
248
+ # Access requires parentheses when joining more than one table
249
+ def select_join_sql(sql)
250
+ if js = @opts[:join]
251
+ js.each do |j|
252
+ literal_append(sql, j)
253
+ sql << PAREN_CLOSE
254
+ end
255
+ end
256
+ end
257
+
56
258
  # Access uses TOP for limits
57
259
  def select_limit_sql(sql)
58
260
  if l = @opts[:limit]