colincasey-sequel 2.10.1 → 2.10.2

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 (39) hide show
  1. data/CHANGELOG +10 -0
  2. data/lib/sequel_core/adapters/ado.rb +3 -0
  3. data/lib/sequel_core/adapters/db2.rb +0 -11
  4. data/lib/sequel_core/adapters/dbi.rb +0 -11
  5. data/lib/sequel_core/adapters/do.rb +0 -12
  6. data/lib/sequel_core/adapters/firebird.rb +19 -14
  7. data/lib/sequel_core/adapters/informix.rb +1 -11
  8. data/lib/sequel_core/adapters/jdbc/h2.rb +3 -11
  9. data/lib/sequel_core/adapters/jdbc/mysql.rb +0 -10
  10. data/lib/sequel_core/adapters/jdbc/postgresql.rb +3 -15
  11. data/lib/sequel_core/adapters/jdbc.rb +1 -13
  12. data/lib/sequel_core/adapters/mysql.rb +6 -13
  13. data/lib/sequel_core/adapters/odbc.rb +34 -28
  14. data/lib/sequel_core/adapters/openbase.rb +0 -11
  15. data/lib/sequel_core/adapters/oracle.rb +11 -9
  16. data/lib/sequel_core/adapters/postgres.rb +13 -16
  17. data/lib/sequel_core/adapters/shared/ms_access.rb +8 -2
  18. data/lib/sequel_core/adapters/shared/mssql.rb +6 -15
  19. data/lib/sequel_core/adapters/shared/mysql.rb +23 -14
  20. data/lib/sequel_core/adapters/shared/oracle.rb +4 -0
  21. data/lib/sequel_core/adapters/shared/postgres.rb +27 -25
  22. data/lib/sequel_core/adapters/shared/progress.rb +4 -0
  23. data/lib/sequel_core/adapters/shared/sqlite.rb +9 -16
  24. data/lib/sequel_core/adapters/sqlite.rb +5 -14
  25. data/lib/sequel_core/core_sql.rb +7 -3
  26. data/lib/sequel_core/dataset/convenience.rb +1 -1
  27. data/lib/sequel_core/dataset/prepared_statements.rb +1 -1
  28. data/lib/sequel_core/dataset/sql.rb +116 -30
  29. data/lib/sequel_core/dataset.rb +2 -2
  30. data/lib/sequel_core/sql.rb +2 -31
  31. data/lib/sequel_model/base.rb +23 -7
  32. data/spec/integration/dataset_test.rb +2 -2
  33. data/spec/sequel_core/core_sql_spec.rb +9 -0
  34. data/spec/sequel_core/dataset_spec.rb +27 -31
  35. data/spec/sequel_model/base_spec.rb +66 -0
  36. data/spec/sequel_model/spec_helper.rb +3 -0
  37. metadata +1 -1
  38. data/lib/sequel_core/dataset/stored_procedures.rb +0 -75
  39. data/lib/sequel_core/dataset/unsupported.rb +0 -43
@@ -161,7 +161,7 @@ module Sequel
161
161
 
162
162
  # Methods shared by Database instances that connect to PostgreSQL.
163
163
  module DatabaseMethods
164
- PREPARED_ARG_PLACEHOLDER = '$'.lit.freeze
164
+ PREPARED_ARG_PLACEHOLDER = LiteralString.new('$').freeze
165
165
  RE_CURRVAL_ERROR = /currval of sequence "(.*)" is not yet defined in this session|relation "(.*)" does not exist/.freeze
166
166
  SQL_BEGIN = 'BEGIN'.freeze
167
167
  SQL_SAVEPOINT = 'SAVEPOINT autopoint_%d'.freeze
@@ -550,6 +550,7 @@ module Sequel
550
550
  FOR_SHARE = ' FOR SHARE'.freeze
551
551
  FOR_UPDATE = ' FOR UPDATE'.freeze
552
552
  LOCK = 'LOCK TABLE %s IN %s MODE'.freeze
553
+ NULL = LiteralString.new('NULL').freeze
553
554
  PG_TIMESTAMP_FORMAT = "TIMESTAMP '%Y-%m-%d %H:%M:%S".freeze
554
555
  QUERY_PLAN = 'QUERY PLAN'.to_sym
555
556
  ROW_EXCLUSIVE = 'ROW EXCLUSIVE'.freeze
@@ -629,29 +630,6 @@ module Sequel
629
630
  single_record(default_server_opts(:naked=>true, :sql=>insert_returning_sql(nil, *values))) if server_version >= 80200
630
631
  end
631
632
 
632
- # Handle microseconds for Time and DateTime values, as well as PostgreSQL
633
- # specific boolean values and string escaping.
634
- def literal(v)
635
- case v
636
- when LiteralString
637
- v
638
- when SQL::Blob
639
- "'#{v.gsub(/[\000-\037\047\134\177-\377]/){|b| "\\#{("%o" % b[0..1].unpack("C")[0]).rjust(3, '0')}"}}'"
640
- when String
641
- "'#{v.gsub("'", "''")}'"
642
- when Time
643
- "#{v.strftime(PG_TIMESTAMP_FORMAT)}.#{sprintf("%06d",v.usec)}'"
644
- when DateTime
645
- "#{v.strftime(PG_TIMESTAMP_FORMAT)}.#{sprintf("%06d", (v.sec_fraction * 86400000000).to_i)}'"
646
- when TrueClass
647
- BOOL_TRUE
648
- when FalseClass
649
- BOOL_FALSE
650
- else
651
- super
652
- end
653
- end
654
-
655
633
  # Locks the table with the specified mode.
656
634
  def lock(mode, server=nil)
657
635
  sql = LOCK % [source_list(@opts[:from]), mode]
@@ -679,9 +657,33 @@ module Sequel
679
657
  # Use the RETURNING clause to return the primary key of the inserted record, if it exists
680
658
  def insert_returning_pk_sql(*values)
681
659
  pk = db.primary_key(opts[:from].first)
682
- insert_returning_sql(pk ? Sequel::SQL::Identifier.new(pk) : 'NULL'.lit, *values)
660
+ insert_returning_sql(pk ? Sequel::SQL::Identifier.new(pk) : NULL, *values)
683
661
  end
684
662
 
663
+ def literal_blob(v)
664
+ "'#{v.gsub(/[\000-\037\047\134\177-\377]/){|b| "\\#{("%o" % b[0..1].unpack("C")[0]).rjust(3, '0')}"}}'"
665
+ end
666
+
667
+ def literal_datetime(v)
668
+ "#{v.strftime(PG_TIMESTAMP_FORMAT)}.#{sprintf("%06d", (v.sec_fraction * 86400000000).to_i)}'"
669
+ end
670
+
671
+ def literal_false
672
+ BOOL_FALSE
673
+ end
674
+
675
+ def literal_string(v)
676
+ "'#{v.gsub("'", "''")}'"
677
+ end
678
+
679
+ def literal_true
680
+ BOOL_TRUE
681
+ end
682
+
683
+ def literal_time(v)
684
+ "#{v.strftime(PG_TIMESTAMP_FORMAT)}.#{sprintf("%06d",v.usec)}'"
685
+ end
686
+
685
687
  # PostgreSQL is smart and can use parantheses around all datasets to get
686
688
  # the correct answers.
687
689
  def select_compounds_sql(sql, opts)
@@ -1,3 +1,6 @@
1
+ require 'sequel_core/adapters/utils/date_format'
2
+ require 'sequel_core/adapters/utils/unsupported'
3
+
1
4
  module Sequel
2
5
  module Progress
3
6
  module DatabaseMethods
@@ -11,6 +14,7 @@ module Sequel
11
14
 
12
15
  module DatasetMethods
13
16
  include Dataset::UnsupportedIntersectExcept
17
+ include Dataset::SQLStandardDateFormat
14
18
 
15
19
  SELECT_CLAUSE_ORDER = %w'limit distinct columns from join where group order having compounds'.freeze
16
20
 
@@ -1,3 +1,5 @@
1
+ require 'sequel_core/adapters/utils/unsupported'
2
+
1
3
  module Sequel
2
4
  module SQLite
3
5
  module DatabaseMethods
@@ -142,7 +144,7 @@ module Sequel
142
144
  # The array of column schema hashes, except for the ones given in opts[:except]
143
145
  def defined_columns_for(table, opts={})
144
146
  cols = parse_pragma(table, {})
145
- cols.each{|c| c[:default] = c[:default].lit if c[:default]}
147
+ cols.each{|c| c[:default] = LiteralString.new(c[:default]) if c[:default]}
146
148
  if opts[:except]
147
149
  nono= Array(opts[:except]).compact.map{|n| n.to_s}
148
150
  cols.reject!{|c| nono.include? c[:name] }
@@ -234,27 +236,18 @@ module Sequel
234
236
  end
235
237
  end
236
238
 
237
- def literal(v)
238
- case v
239
- when ::Sequel::SQL::Blob
240
- blob = ''
241
- v.each_byte{|x| blob << sprintf('%02x', x)}
242
- "X'#{blob}'"
243
- when Time
244
- literal(v.iso8601)
245
- when Date, DateTime
246
- literal(v.to_s)
247
- else
248
- super
249
- end
250
- end
251
-
252
239
  # SQLite uses the nonstandard ` (backtick) for quoting identifiers.
253
240
  def quoted_identifier(c)
254
241
  "`#{c}`"
255
242
  end
256
243
 
257
244
  private
245
+
246
+ def literal_blob(v)
247
+ blob = ''
248
+ v.each_byte{|x| blob << sprintf('%02x', x)}
249
+ "X'#{blob}'"
250
+ end
258
251
 
259
252
  # SQLite uses string literals instead of identifiers in AS clauses.
260
253
  def as_sql(expression, aliaz)
@@ -159,7 +159,7 @@ module Sequel
159
159
  # SQLite uses a : before the name of the argument for named
160
160
  # arguments.
161
161
  def prepared_arg(k)
162
- "#{prepared_arg_placeholder}#{k}".lit
162
+ LiteralString.new("#{prepared_arg_placeholder}#{k}")
163
163
  end
164
164
  end
165
165
 
@@ -214,19 +214,6 @@ module Sequel
214
214
  end
215
215
  end
216
216
 
217
- # Use the ISO format for dates and timestamps, and quote strings
218
- # using the ::SQLite3::Database.quote method.
219
- def literal(v)
220
- case v
221
- when LiteralString, ::Sequel::SQL::Blob
222
- super
223
- when String
224
- "'#{::SQLite3::Database.quote(v)}'"
225
- else
226
- super
227
- end
228
- end
229
-
230
217
  # Prepare the given type of query with the given name and store
231
218
  # it in the database. Note that a new native prepared statement is
232
219
  # created on each call to this prepared statement.
@@ -239,6 +226,10 @@ module Sequel
239
226
 
240
227
  private
241
228
 
229
+ def literal_string(v)
230
+ "'#{::SQLite3::Database.quote(v)}'"
231
+ end
232
+
242
233
  # SQLite uses a : before the name of the argument as a placeholder.
243
234
  def prepared_arg_placeholder
244
235
  PREPARED_ARG_PLACEHOLDER
@@ -121,7 +121,7 @@ class String
121
121
  include Sequel::SQL::AliasMethods
122
122
  include Sequel::SQL::CastMethods
123
123
 
124
- # Converts a string into an LiteralString, in order to override string
124
+ # Converts a string into a Sequel::LiteralString, in order to override string
125
125
  # literalization, e.g.:
126
126
  #
127
127
  # DB[:items].filter(:abc => 'def').sql #=>
@@ -130,8 +130,12 @@ class String
130
130
  # DB[:items].filter(:abc => 'def'.lit).sql #=>
131
131
  # "SELECT * FROM items WHERE (abc = def)"
132
132
  #
133
- def lit
134
- Sequel::LiteralString.new(self)
133
+ # You can also provide arguments, to create a Sequel::SQL::PlaceholderLiteralString:
134
+ #
135
+ # DB[:items].select{|o| o.count('DISTINCT ?'.lit(:a))}.sql #=>
136
+ # "SELECT count(DISTINCT a) FROM items"
137
+ def lit(*args)
138
+ args.empty? ? Sequel::LiteralString.new(self) : Sequel::SQL::PlaceholderLiteralString.new(self, args)
135
139
  end
136
140
  alias_method :expr, :lit
137
141
 
@@ -1,7 +1,7 @@
1
1
  module Sequel
2
2
  class Dataset
3
3
  COMMA_SEPARATOR = ', '.freeze
4
- COUNT_OF_ALL_AS_COUNT = SQL::Function.new(:count, '*'.lit).as(:count)
4
+ COUNT_OF_ALL_AS_COUNT = SQL::Function.new(:count, LiteralString.new('*'.freeze)).as(:count)
5
5
 
6
6
  # Returns the first record matching the conditions.
7
7
  def [](*conditions)
@@ -1,6 +1,6 @@
1
1
  module Sequel
2
2
  class Dataset
3
- PREPARED_ARG_PLACEHOLDER = '?'.lit.freeze
3
+ PREPARED_ARG_PLACEHOLDER = LiteralString.new('?').freeze
4
4
 
5
5
  # Default implementation of the argument mapper to allow
6
6
  # native database support for bind variables and prepared
@@ -7,13 +7,11 @@ module Sequel
7
7
  COLUMN_REF_RE2 = /\A([\w ]+)___([\w ]+)\z/.freeze
8
8
  COLUMN_REF_RE3 = /\A([\w ]+)__([\w ]+)\z/.freeze
9
9
  COUNT_FROM_SELF_OPTS = [:distinct, :group, :sql, :limit, :compounds]
10
- DATE_FORMAT = "DATE '%Y-%m-%d'".freeze
11
10
  N_ARITY_OPERATORS = ::Sequel::SQL::ComplexExpression::N_ARITY_OPERATORS
12
11
  NULL = "NULL".freeze
13
12
  QUESTION_MARK = '?'.freeze
14
- STOCK_COUNT_OPTS = {:select => ["COUNT(*)".lit], :order => nil}.freeze
13
+ STOCK_COUNT_OPTS = {:select => [LiteralString.new("COUNT(*)").freeze], :order => nil}.freeze
15
14
  SELECT_CLAUSE_ORDER = %w'distinct columns from join where group having compounds order limit'.freeze
16
- TIMESTAMP_FORMAT = "TIMESTAMP '%Y-%m-%d %H:%M:%S'".freeze
17
15
  TWO_ARITY_OPERATORS = ::Sequel::SQL::ComplexExpression::TWO_ARITY_OPERATORS
18
16
  WILDCARD = '*'.freeze
19
17
 
@@ -131,7 +129,7 @@ module Sequel
131
129
  # DB.select(1).where(DB[:items].exists).sql
132
130
  # #=> "SELECT 1 WHERE EXISTS (SELECT * FROM items)"
133
131
  def exists(opts = nil)
134
- "EXISTS (#{select_sql(opts)})".lit
132
+ LiteralString.new("EXISTS (#{select_sql(opts)})")
135
133
  end
136
134
 
137
135
  # Returns a copy of the dataset with the given conditions imposed upon it.
@@ -468,38 +466,39 @@ module Sequel
468
466
  # If an unsupported object is given, an exception is raised.
469
467
  def literal(v)
470
468
  case v
471
- when LiteralString
472
- v
473
469
  when String
474
- "'#{v.gsub(/\\/, "\\\\\\\\").gsub(/'/, "''")}'"
475
- when Integer, Float
476
- v.to_s
470
+ return v if v.is_a?(LiteralString)
471
+ v.is_a?(SQL::Blob) ? literal_blob(v) : literal_string(v)
472
+ when Symbol
473
+ literal_symbol(v)
474
+ when Integer
475
+ literal_integer(v)
476
+ when Hash
477
+ literal_hash(v)
478
+ when SQL::Expression
479
+ literal_expression(v)
480
+ when Float
481
+ literal_float(v)
477
482
  when BigDecimal
478
- d = v.to_s("F")
479
- d = "'#{d}'" if v.nan? || v.infinite?
480
- d
483
+ literal_big_decimal(v)
481
484
  when NilClass
482
485
  NULL
483
486
  when TrueClass
484
- BOOL_TRUE
487
+ literal_true
485
488
  when FalseClass
486
- BOOL_FALSE
487
- when Symbol
488
- symbol_to_column_ref(v)
489
- when ::Sequel::SQL::Expression
490
- v.to_s(self)
489
+ literal_false
491
490
  when Array
492
- v.all_two_pairs? ? literal(v.sql_expr) : array_sql(v)
493
- when Hash
494
- literal(v.sql_expr)
495
- when Time, DateTime
496
- v.strftime(TIMESTAMP_FORMAT)
491
+ literal_array(v)
492
+ when Time
493
+ literal_time(v)
494
+ when DateTime
495
+ literal_datetime(v)
497
496
  when Date
498
- v.strftime(DATE_FORMAT)
497
+ literal_date(v)
499
498
  when Dataset
500
- "(#{subselect_sql(v)})"
499
+ literal_dataset(v)
501
500
  else
502
- raise Error, "can't express #{v.inspect} as a SQL literal"
501
+ literal_other(v)
503
502
  end
504
503
  end
505
504
 
@@ -674,9 +673,7 @@ module Sequel
674
673
  # :items__abc___a.to_column_ref(ds) #=> "items.abc AS a"
675
674
  #
676
675
  def symbol_to_column_ref(sym)
677
- c_table, column, c_alias = split_symbol(sym)
678
- qc = "#{"#{quote_identifier(c_table)}." if c_table}#{quote_identifier(column)}"
679
- c_alias ? as_sql(qc, c_alias) : qc
676
+ literal_symbol(sym)
680
677
  end
681
678
 
682
679
  # Returns a copy of the dataset with no filters (HAVING or WHERE clause) applied.
@@ -745,6 +742,12 @@ module Sequel
745
742
  sql
746
743
  end
747
744
 
745
+ # Returns a copy of the dataset with the static SQL used. This is useful if you want
746
+ # to keep the same row_proc/transform/graph, but change the SQL used to custom SQL.
747
+ def with_sql(sql)
748
+ clone(:sql=>sql)
749
+ end
750
+
748
751
  [:inner, :full_outer, :right_outer, :left_outer].each do |jtype|
749
752
  class_eval("def #{jtype}_join(*args, &block); join_table(:#{jtype}, *args, &block) end")
750
753
  end
@@ -816,7 +819,7 @@ module Sequel
816
819
  when TrueClass, FalseClass
817
820
  SQL::BooleanExpression.new(:NOOP, expr)
818
821
  when String
819
- "(#{expr})".lit
822
+ LiteralString.new("(#{expr})")
820
823
  else
821
824
  raise(Error, 'Invalid filter argument')
822
825
  end
@@ -857,6 +860,89 @@ module Sequel
857
860
  "#{join_type.to_s.gsub('_', ' ').upcase} JOIN"
858
861
  end
859
862
 
863
+ # SQL fragment for Array. Treats as an expression if an array of all two pairs, or as a SQL array otherwise.
864
+ def literal_array(v)
865
+ v.all_two_pairs? ? literal_expression(v.sql_expr) : array_sql(v)
866
+ end
867
+
868
+ # SQL fragment for BigDecimal
869
+ def literal_big_decimal(v)
870
+ d = v.to_s("F")
871
+ v.nan? || v.infinite? ? "'#{d}'" : d
872
+ end
873
+
874
+ # SQL fragment for SQL::Blob
875
+ def literal_blob(v)
876
+ literal_string(v)
877
+ end
878
+
879
+ # SQL fragment for Dataset. Does a subselect inside parantheses.
880
+ def literal_dataset(v)
881
+ "(#{subselect_sql(v)})"
882
+ end
883
+
884
+ # SQL fragment for Date, using the ISO8601 format.
885
+ def literal_date(v)
886
+ "'#{v}'"
887
+ end
888
+
889
+ # SQL fragment for DateTime, using the ISO8601 format.
890
+ def literal_datetime(v)
891
+ "'#{v}'"
892
+ end
893
+
894
+ # SQL fragment for SQL::Expression, result depends on the specific type of expression.
895
+ def literal_expression(v)
896
+ v.to_s(self)
897
+ end
898
+
899
+ # SQL fragment for false
900
+ def literal_false
901
+ BOOL_FALSE
902
+ end
903
+
904
+ # SQL fragment for Float
905
+ def literal_float(v)
906
+ v.to_s
907
+ end
908
+
909
+ # SQL fragment for Hash, treated as an expression
910
+ def literal_hash(v)
911
+ literal_expression(v.sql_expr)
912
+ end
913
+
914
+ # SQL fragment for Integer
915
+ def literal_integer(v)
916
+ v.to_s
917
+ end
918
+
919
+ # SQL fragmento for a type of object not handled by Dataset#literal. Raises an error. If a database specific type is allowed, this should be overriden in a subclass.
920
+ def literal_other(v)
921
+ raise Error, "can't express #{v.inspect} as a SQL literal"
922
+ end
923
+
924
+ # SQL fragment for String. Doubles \ and ' by default.
925
+ def literal_string(v)
926
+ "'#{v.gsub(/\\/, "\\\\\\\\").gsub(/'/, "''")}'"
927
+ end
928
+
929
+ # SQL fragment for Symbol, treated as an identifier, possibly aliased and/or qualified.
930
+ def literal_symbol(v)
931
+ c_table, column, c_alias = split_symbol(v)
932
+ qc = "#{"#{quote_identifier(c_table)}." if c_table}#{quote_identifier(column)}"
933
+ c_alias ? as_sql(qc, c_alias) : qc
934
+ end
935
+
936
+ # SQL fragment for Time, uses the ISO8601 format.
937
+ def literal_time(v)
938
+ "'#{v.iso8601}'"
939
+ end
940
+
941
+ # SQL fragment for true.
942
+ def literal_true
943
+ BOOL_TRUE
944
+ end
945
+
860
946
  # Returns a qualified column name (including a table name) if the column
861
947
  # name isn't already qualified.
862
948
  def qualified_column_name(column, table)
@@ -1,4 +1,4 @@
1
- %w'callback convenience pagination prepared_statements query schema sql unsupported'.each do |f|
1
+ %w'callback convenience pagination prepared_statements query schema sql'.each do |f|
2
2
  require "sequel_core/dataset/#{f}"
3
3
  end
4
4
 
@@ -54,7 +54,7 @@ module Sequel
54
54
  left_outer_join limit naked or order order_by order_more paginate query reject
55
55
  reverse reverse_order right_outer_join select select_all select_more
56
56
  set_defaults set_graph_aliases set_model set_overrides sort sort_by
57
- unfiltered union unordered where'.collect{|x| x.to_sym}
57
+ unfiltered union unordered where with_sql'.collect{|x| x.to_sym}
58
58
 
59
59
  NOTIMPL_MSG = "This method must be overridden in Sequel adapters".freeze
60
60
  STOCK_TRANSFORMS = {
@@ -336,12 +336,12 @@ module Sequel
336
336
  module ComplexExpressionMethods
337
337
  # Extract a datetime_part (e.g. year, month) from self:
338
338
  #
339
- # :date.extract(:year) # SQL: extract(year FROM date)
339
+ # :date.extract(:year) # SQL: extract(year FROM "date")
340
340
  #
341
341
  # Also has the benefit of returning the result as a
342
342
  # NumericExpression instead of a generic ComplexExpression.
343
343
  def extract(datetime_part)
344
- IrregularFunction.new(:extract, datetime_part.to_s.lit, :FROM, self).sql_number
344
+ Function.new(:extract, PlaceholderLiteralString.new("#{datetime_part} FROM ?", [self])).sql_number
345
345
  end
346
346
 
347
347
  # Return a BooleanExpression representation of self.
@@ -592,35 +592,6 @@ module Sequel
592
592
  end
593
593
  end
594
594
 
595
- # IrregularFunction is used for the SQL EXTRACT function,
596
- # which don't use regular function calling syntax. The IrregularFunction
597
- # replaces the commas the regular function uses with a custom
598
- # join string.
599
- #
600
- # This shouldn't be used directly, see
601
- # ComplexExpressionMethods#extract.
602
- class IrregularFunction < Function
603
- # The arguments to pass to the function (may be blank)
604
- attr_reader :arg1, :arg2
605
-
606
- # The SQL function to call
607
- attr_reader :f
608
-
609
- # The literal string to use in place of a comma to join arguments
610
- attr_reader :joiner
611
-
612
- # Set the attributes to the given arguments
613
- def initialize(f, arg1, joiner, arg2)
614
- @f, @arg1, @joiner, @arg2 = f, arg1, joiner, arg2
615
- end
616
-
617
- # Delegate the creation of the resulting SQL to the given dataset,
618
- # since it may be database dependent.
619
- def to_s(ds)
620
- ds.irregular_function_sql(self)
621
- end
622
- end
623
-
624
595
  # Represents an SQL JOIN clause, used for joining tables.
625
596
  class JoinClause < SpecificExpression
626
597
  # The type of join to do
@@ -14,6 +14,8 @@ module Sequel
14
14
  @raise_on_typecast_failure = true
15
15
  @restrict_primary_key = true
16
16
  @restricted_columns = nil
17
+ @simple_pk = nil
18
+ @simple_table = nil
17
19
  @skip_superclass_validations = nil
18
20
  @sti_dataset = nil
19
21
  @sti_key = nil
@@ -48,6 +50,14 @@ module Sequel
48
50
  # (default: no columns).
49
51
  metaattr_reader :restricted_columns
50
52
 
53
+ # Should be the literal primary key column name if this Model's table has a simple primary key, or
54
+ # nil if the model has a compound primary key or no primary key.
55
+ metaattr_reader :simple_pk
56
+
57
+ # Should be the literal table name if this Model's dataset is a simple table (no select, order, join, etc.),
58
+ # or nil otherwise.
59
+ metaattr_reader :simple_table
60
+
51
61
  # The base dataset for STI, to which filters are added to get
52
62
  # only the models for the specific STI subclass.
53
63
  metaattr_reader :sti_dataset
@@ -75,12 +85,13 @@ module Sequel
75
85
  left_outer_join limit map multi_insert naked order order_by order_more
76
86
  paginate print query range reverse_order right_outer_join select
77
87
  select_all select_more server set set_graph_aliases single_value size to_csv to_hash
78
- transform union uniq unfiltered unordered update where'.map{|x| x.to_sym}
88
+ transform union uniq unfiltered unordered update where with_sql'.map{|x| x.to_sym}
79
89
 
80
90
  # Instance variables that are inherited in subclasses
81
91
  INHERITED_INSTANCE_VARIABLES = {:@allowed_columns=>:dup, :@cache_store=>nil,
82
92
  :@cache_ttl=>nil, :@dataset_methods=>:dup, :@primary_key=>nil,
83
93
  :@raise_on_save_failure=>nil, :@restricted_columns=>:dup, :@restrict_primary_key=>nil,
94
+ :@simple_pk=>nil, :@simple_table=>nil,
84
95
  :@sti_dataset=>nil, :@sti_key=>nil, :@strict_param_setting=>nil,
85
96
  :@typecast_empty_string_to_nil=>nil, :@typecast_on_assignment=>nil,
86
97
  :@raise_on_typecast_failure=>nil, :@association_reflections=>:dup}
@@ -95,11 +106,12 @@ module Sequel
95
106
  # first before a dataset lookup is attempted unless a hash is supplied.
96
107
  def self.[](*args)
97
108
  args = args.first if (args.size == 1)
98
-
99
- if Hash === args
100
- dataset[args]
109
+ return dataset[args] if args.is_a?(Hash)
110
+ return cache_lookup(args) if @cache_store
111
+ if t = simple_table and p = simple_pk
112
+ with_sql("SELECT * FROM #{t} WHERE #{p} = #{dataset.literal(args)} LIMIT 1").first
101
113
  else
102
- @cache_store ? cache_lookup(args) : dataset[primary_key_hash(args)]
114
+ dataset[primary_key_hash(args)]
103
115
  end
104
116
  end
105
117
 
@@ -223,7 +235,7 @@ module Sequel
223
235
  db
224
236
  begin
225
237
  if sup_class == Model
226
- subclass.set_dataset(Model.db[subclass.implicit_table_name]) unless subclass.name.blank?
238
+ subclass.set_dataset(subclass.implicit_table_name) unless subclass.name.blank?
227
239
  elsif ds = sup_class.instance_variable_get(:@dataset)
228
240
  subclass.set_dataset(sup_class.sti_key ? sup_class.sti_dataset.filter(sup_class.sti_key=>subclass.name.to_s) : ds.clone, :inherited=>true)
229
241
  end
@@ -251,7 +263,7 @@ module Sequel
251
263
  # Mark the model as not having a primary key. Not having a primary key
252
264
  # can cause issues, among which is that you won't be able to update records.
253
265
  def self.no_primary_key
254
- @primary_key = nil
266
+ @simple_pk = @primary_key = nil
255
267
  end
256
268
 
257
269
  # Returns primary key attribute hash. If using a composite primary key
@@ -329,8 +341,10 @@ module Sequel
329
341
  inherited = opts[:inherited]
330
342
  @dataset = case ds
331
343
  when Symbol
344
+ @simple_table = db.literal(ds)
332
345
  db[ds]
333
346
  when Dataset
347
+ @simple_table = nil
334
348
  @db = ds.db
335
349
  ds
336
350
  else
@@ -339,6 +353,7 @@ module Sequel
339
353
  @dataset.set_model(self)
340
354
  @dataset.transform(@transform) if @transform
341
355
  if inherited
356
+ @simple_table = sti_key ? nil : superclass.simple_table
342
357
  @columns = @dataset.columns rescue nil
343
358
  else
344
359
  @dataset.extend(DatasetMethods)
@@ -366,6 +381,7 @@ module Sequel
366
381
  # You can set it to nil to not have a primary key, but that
367
382
  # cause certain things not to work, see #no_primary_key.
368
383
  def self.set_primary_key(*key)
384
+ @simple_pk = key.length == 1 ? db.literal(key.first) : nil
369
385
  @primary_key = (key.length == 1) ? key[0] : key.flatten
370
386
  end
371
387
 
@@ -101,7 +101,7 @@ describe "Dataset UNION, EXCEPT, and INTERSECT" do
101
101
 
102
102
  specify "should give the correct results for simple UNION, EXCEPT, and INTERSECT" do
103
103
  @ds1.union(@ds2).order(:number).map{|x| x[:number].to_s}.should == %w'10 20 30'
104
- unless @ds1.class.ancestors.include?(Sequel::Dataset::UnsupportedIntersectExcept)
104
+ unless defined?(Sequel::Dataset::UnsupportedIntersectExcept) and @ds1.class.ancestors.include?(Sequel::Dataset::UnsupportedIntersectExcept)
105
105
  @ds1.except(@ds2).order(:number).map{|x| x[:number].to_s}.should == %w'20'
106
106
  @ds1.intersect(@ds2).order(:number).map{|x| x[:number].to_s}.should == %w'10'
107
107
  end
@@ -110,7 +110,7 @@ describe "Dataset UNION, EXCEPT, and INTERSECT" do
110
110
  specify "should give the correct results for compound UNION, EXCEPT, and INTERSECT" do
111
111
  @ds1.union(@ds2).union(@ds3).order(:number).map{|x| x[:number].to_s}.should == %w'10 20 30 40'
112
112
  @ds1.union(@ds2.union(@ds3)).order(:number).map{|x| x[:number].to_s}.should == %w'10 20 30 40'
113
- unless @ds1.class.ancestors.include?(Sequel::Dataset::UnsupportedIntersectExcept)
113
+ unless defined?(Sequel::Dataset::UnsupportedIntersectExcept) and @ds1.class.ancestors.include?(Sequel::Dataset::UnsupportedIntersectExcept)
114
114
  @ds1.union(@ds2).except(@ds3).order(:number).map{|x| x[:number].to_s}.should == %w'20 30'
115
115
  @ds1.union(@ds2.except(@ds3)).order(:number).map{|x| x[:number].to_s}.should == %w'10 20 30'
116
116
  @ds1.union(@ds2).intersect(@ds3).order(:number).map{|x| x[:number].to_s}.should == %w'10 '
@@ -124,6 +124,15 @@ context "String#lit" do
124
124
  Sequel::Database.new[:t].update_sql(:stamp => "NOW()".expr).should == \
125
125
  "UPDATE t SET stamp = NOW()"
126
126
  end
127
+
128
+ specify "should return a PlaceholderLiteralString object if args are given" do
129
+ a = 'DISTINCT ?'.lit(:a)
130
+ a.should be_a_kind_of(Sequel::SQL::PlaceholderLiteralString)
131
+ ds = MockDatabase.new.dataset
132
+ ds.literal(a).should == 'DISTINCT a'
133
+ ds.quote_identifiers = true
134
+ ds.literal(a).should == 'DISTINCT "a"'
135
+ end
127
136
  end
128
137
 
129
138
  context "String#to_blob and #to_sequel_blob" do