sequel 3.21.0 → 3.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. data/CHANGELOG +294 -0
  2. data/README.rdoc +20 -6
  3. data/Rakefile +20 -15
  4. data/doc/association_basics.rdoc +210 -43
  5. data/doc/dataset_basics.rdoc +4 -4
  6. data/doc/mass_assignment.rdoc +54 -0
  7. data/doc/migration.rdoc +15 -538
  8. data/doc/model_hooks.rdoc +64 -27
  9. data/doc/opening_databases.rdoc +37 -10
  10. data/doc/prepared_statements.rdoc +16 -10
  11. data/doc/reflection.rdoc +8 -2
  12. data/doc/release_notes/3.22.0.txt +39 -0
  13. data/doc/release_notes/3.23.0.txt +172 -0
  14. data/doc/release_notes/3.24.0.txt +420 -0
  15. data/doc/release_notes/3.25.0.txt +88 -0
  16. data/doc/release_notes/3.26.0.txt +88 -0
  17. data/doc/release_notes/3.27.0.txt +82 -0
  18. data/doc/release_notes/3.28.0.txt +304 -0
  19. data/doc/schema_modification.rdoc +547 -0
  20. data/doc/testing.rdoc +106 -0
  21. data/doc/transactions.rdoc +97 -0
  22. data/doc/virtual_rows.rdoc +2 -2
  23. data/lib/sequel/adapters/ado.rb +12 -1
  24. data/lib/sequel/adapters/amalgalite.rb +4 -0
  25. data/lib/sequel/adapters/db2.rb +95 -58
  26. data/lib/sequel/adapters/do.rb +12 -0
  27. data/lib/sequel/adapters/firebird.rb +25 -203
  28. data/lib/sequel/adapters/ibmdb.rb +440 -0
  29. data/lib/sequel/adapters/informix.rb +4 -19
  30. data/lib/sequel/adapters/jdbc/as400.rb +0 -7
  31. data/lib/sequel/adapters/jdbc/db2.rb +49 -0
  32. data/lib/sequel/adapters/jdbc/firebird.rb +34 -0
  33. data/lib/sequel/adapters/jdbc/h2.rb +16 -5
  34. data/lib/sequel/adapters/jdbc/informix.rb +31 -0
  35. data/lib/sequel/adapters/jdbc/jtds.rb +34 -0
  36. data/lib/sequel/adapters/jdbc/mssql.rb +0 -32
  37. data/lib/sequel/adapters/jdbc/mysql.rb +9 -0
  38. data/lib/sequel/adapters/jdbc/oracle.rb +2 -27
  39. data/lib/sequel/adapters/jdbc/postgresql.rb +6 -0
  40. data/lib/sequel/adapters/jdbc/sqlserver.rb +46 -0
  41. data/lib/sequel/adapters/jdbc/transactions.rb +34 -0
  42. data/lib/sequel/adapters/jdbc.rb +62 -29
  43. data/lib/sequel/adapters/mysql.rb +22 -139
  44. data/lib/sequel/adapters/mysql2.rb +9 -14
  45. data/lib/sequel/adapters/odbc/db2.rb +21 -0
  46. data/lib/sequel/adapters/odbc.rb +15 -3
  47. data/lib/sequel/adapters/oracle.rb +17 -1
  48. data/lib/sequel/adapters/postgres.rb +111 -16
  49. data/lib/sequel/adapters/shared/access.rb +21 -0
  50. data/lib/sequel/adapters/shared/db2.rb +290 -0
  51. data/lib/sequel/adapters/shared/firebird.rb +214 -0
  52. data/lib/sequel/adapters/shared/informix.rb +45 -0
  53. data/lib/sequel/adapters/shared/mssql.rb +85 -47
  54. data/lib/sequel/adapters/shared/mysql.rb +50 -7
  55. data/lib/sequel/adapters/shared/mysql_prepared_statements.rb +143 -0
  56. data/lib/sequel/adapters/shared/oracle.rb +0 -4
  57. data/lib/sequel/adapters/shared/postgres.rb +75 -43
  58. data/lib/sequel/adapters/shared/sqlite.rb +56 -8
  59. data/lib/sequel/adapters/sqlite.rb +12 -11
  60. data/lib/sequel/adapters/swift/mysql.rb +9 -0
  61. data/lib/sequel/adapters/tinytds.rb +139 -7
  62. data/lib/sequel/adapters/utils/emulate_offset_with_row_number.rb +55 -0
  63. data/lib/sequel/ast_transformer.rb +190 -0
  64. data/lib/sequel/connection_pool/threaded.rb +3 -2
  65. data/lib/sequel/connection_pool.rb +1 -1
  66. data/lib/sequel/core.rb +6 -5
  67. data/lib/sequel/database/connecting.rb +5 -5
  68. data/lib/sequel/database/dataset.rb +1 -1
  69. data/lib/sequel/database/dataset_defaults.rb +1 -1
  70. data/lib/sequel/database/logging.rb +1 -1
  71. data/lib/sequel/database/misc.rb +38 -17
  72. data/lib/sequel/database/query.rb +50 -19
  73. data/lib/sequel/database/schema_generator.rb +8 -5
  74. data/lib/sequel/database/schema_methods.rb +52 -27
  75. data/lib/sequel/dataset/actions.rb +167 -48
  76. data/lib/sequel/dataset/features.rb +57 -8
  77. data/lib/sequel/dataset/graph.rb +1 -1
  78. data/lib/sequel/dataset/misc.rb +39 -20
  79. data/lib/sequel/dataset/mutation.rb +3 -3
  80. data/lib/sequel/dataset/prepared_statements.rb +29 -14
  81. data/lib/sequel/dataset/query.rb +182 -32
  82. data/lib/sequel/dataset/sql.rb +31 -58
  83. data/lib/sequel/dataset.rb +8 -0
  84. data/lib/sequel/exceptions.rb +4 -0
  85. data/lib/sequel/extensions/columns_introspection.rb +61 -0
  86. data/lib/sequel/extensions/migration.rb +6 -4
  87. data/lib/sequel/extensions/to_dot.rb +95 -83
  88. data/lib/sequel/model/associations.rb +893 -309
  89. data/lib/sequel/model/base.rb +302 -105
  90. data/lib/sequel/model/errors.rb +1 -1
  91. data/lib/sequel/model/exceptions.rb +5 -1
  92. data/lib/sequel/model.rb +13 -7
  93. data/lib/sequel/plugins/association_pks.rb +22 -4
  94. data/lib/sequel/plugins/defaults_setter.rb +58 -0
  95. data/lib/sequel/plugins/identity_map.rb +113 -6
  96. data/lib/sequel/plugins/many_through_many.rb +67 -5
  97. data/lib/sequel/plugins/prepared_statements.rb +140 -0
  98. data/lib/sequel/plugins/prepared_statements_associations.rb +84 -0
  99. data/lib/sequel/plugins/prepared_statements_safe.rb +82 -0
  100. data/lib/sequel/plugins/prepared_statements_with_pk.rb +59 -0
  101. data/lib/sequel/plugins/serialization_modification_detection.rb +51 -0
  102. data/lib/sequel/plugins/sharding.rb +12 -20
  103. data/lib/sequel/plugins/single_table_inheritance.rb +2 -0
  104. data/lib/sequel/plugins/update_primary_key.rb +1 -1
  105. data/lib/sequel/plugins/xml_serializer.rb +3 -3
  106. data/lib/sequel/sql.rb +107 -51
  107. data/lib/sequel/timezones.rb +12 -3
  108. data/lib/sequel/version.rb +1 -1
  109. data/spec/adapters/db2_spec.rb +146 -0
  110. data/spec/adapters/mssql_spec.rb +36 -0
  111. data/spec/adapters/mysql_spec.rb +36 -19
  112. data/spec/adapters/postgres_spec.rb +115 -28
  113. data/spec/adapters/spec_helper.rb +6 -0
  114. data/spec/adapters/sqlite_spec.rb +11 -0
  115. data/spec/core/connection_pool_spec.rb +62 -77
  116. data/spec/core/database_spec.rb +244 -287
  117. data/spec/core/dataset_spec.rb +383 -34
  118. data/spec/core/expression_filters_spec.rb +159 -41
  119. data/spec/core/schema_spec.rb +326 -3
  120. data/spec/core/spec_helper.rb +45 -0
  121. data/spec/extensions/association_pks_spec.rb +38 -0
  122. data/spec/extensions/columns_introspection_spec.rb +91 -0
  123. data/spec/extensions/defaults_setter_spec.rb +64 -0
  124. data/spec/extensions/identity_map_spec.rb +162 -0
  125. data/spec/extensions/many_through_many_spec.rb +195 -20
  126. data/spec/extensions/migration_spec.rb +17 -17
  127. data/spec/extensions/nested_attributes_spec.rb +1 -0
  128. data/spec/extensions/prepared_statements_associations_spec.rb +126 -0
  129. data/spec/extensions/prepared_statements_safe_spec.rb +79 -0
  130. data/spec/extensions/prepared_statements_spec.rb +72 -0
  131. data/spec/extensions/prepared_statements_with_pk_spec.rb +38 -0
  132. data/spec/extensions/schema_dumper_spec.rb +2 -2
  133. data/spec/extensions/schema_spec.rb +12 -20
  134. data/spec/extensions/serialization_modification_detection_spec.rb +36 -0
  135. data/spec/extensions/single_table_inheritance_spec.rb +11 -0
  136. data/spec/extensions/spec_helper.rb +3 -1
  137. data/spec/extensions/to_dot_spec.rb +3 -5
  138. data/spec/extensions/xml_serializer_spec.rb +16 -4
  139. data/spec/integration/associations_test.rb +405 -15
  140. data/spec/integration/database_test.rb +4 -2
  141. data/spec/integration/dataset_test.rb +240 -20
  142. data/spec/integration/plugin_test.rb +142 -5
  143. data/spec/integration/prepared_statement_test.rb +174 -95
  144. data/spec/integration/schema_test.rb +128 -16
  145. data/spec/integration/spec_helper.rb +15 -0
  146. data/spec/integration/transaction_test.rb +40 -0
  147. data/spec/integration/type_test.rb +16 -2
  148. data/spec/model/association_reflection_spec.rb +91 -0
  149. data/spec/model/associations_spec.rb +476 -5
  150. data/spec/model/base_spec.rb +91 -1
  151. data/spec/model/eager_loading_spec.rb +519 -31
  152. data/spec/model/hooks_spec.rb +161 -0
  153. data/spec/model/model_spec.rb +89 -2
  154. data/spec/model/plugins_spec.rb +17 -0
  155. data/spec/model/record_spec.rb +184 -12
  156. data/spec/model/spec_helper.rb +5 -0
  157. data/spec/model/validations_spec.rb +11 -0
  158. metadata +85 -34
@@ -1,7 +1,7 @@
1
1
  module Sequel
2
2
  class Dataset
3
3
  # ---------------------
4
- # :section: User Methods relating to SQL Creation
4
+ # :section: 3 - User Methods relating to SQL Creation
5
5
  # These are methods you can call to see what SQL will be generated by the dataset.
6
6
  # ---------------------
7
7
 
@@ -101,7 +101,7 @@ module Sequel
101
101
  when Array
102
102
  literal_array(v)
103
103
  when Time
104
- literal_time(v)
104
+ v.is_a?(SQLTime) ? literal_sqltime(v) : literal_time(v)
105
105
  when DateTime
106
106
  literal_datetime(v)
107
107
  when Date
@@ -163,7 +163,7 @@ module Sequel
163
163
  end
164
164
 
165
165
  # ---------------------
166
- # :section: Internal Methods relating to SQL Creation
166
+ # :section: 9 - Internal Methods relating to SQL Creation
167
167
  # These methods, while public, are not designed to be used directly by the end user.
168
168
  # ---------------------
169
169
 
@@ -212,7 +212,11 @@ module Sequel
212
212
 
213
213
  # SQL fragment for BooleanConstants
214
214
  def boolean_constant_sql(constant)
215
- literal(constant)
215
+ if (constant == true || constant == false) && !supports_where_true?
216
+ constant == true ? '(1 = 1)' : '(1 = 0)'
217
+ else
218
+ literal(constant)
219
+ end
216
220
  end
217
221
 
218
222
  # SQL fragment for CaseExpression
@@ -269,6 +273,7 @@ module Sequel
269
273
  literal(op == :IN ? expr : ~expr)
270
274
  else
271
275
  old_vals = vals
276
+ vals = vals.naked if vals.is_a?(Sequel::Dataset)
272
277
  vals = vals.to_a
273
278
  val_cols = old_vals.columns
274
279
  complex_expression_sql(op, [cols, vals.map!{|x| x.values_at(*val_cols)}])
@@ -300,6 +305,8 @@ module Sequel
300
305
  literal(args.at(0))
301
306
  when :'B~'
302
307
  "~#{literal(args.at(0))}"
308
+ when :extract
309
+ "extract(#{args.at(0)} FROM #{literal(args.at(1))})"
303
310
  else
304
311
  raise(InvalidOperation, "invalid operator #{op}")
305
312
  end
@@ -434,7 +441,7 @@ module Sequel
434
441
  when :all
435
442
  "ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING"
436
443
  when :rows
437
- "ROWS UNBOUNDED PRECEDING"
444
+ "ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW"
438
445
  when String
439
446
  opts[:frame]
440
447
  else
@@ -636,6 +643,15 @@ module Sequel
636
643
  end
637
644
  end
638
645
 
646
+ # SQL fragment specifying the values to return.
647
+ def insert_returning_sql(sql)
648
+ if opts.has_key?(:returning)
649
+ sql << " RETURNING #{column_list(Array(opts[:returning]))}"
650
+ end
651
+ end
652
+ alias delete_returning_sql insert_returning_sql
653
+ alias update_returning_sql insert_returning_sql
654
+
639
655
  # SQL fragment specifying a JOIN type, converts underscores to
640
656
  # spaces and upcases.
641
657
  def join_type_sql(join_type)
@@ -722,6 +738,11 @@ module Sequel
722
738
  end
723
739
  end
724
740
 
741
+ # SQL fragment for Sequel::SQLTime, containing just the time part
742
+ def literal_sqltime(v)
743
+ v.strftime("'%H:%M:%S#{format_timestamp_usec(v.usec) if supports_timestamp_usecs?}'")
744
+ end
745
+
725
746
  # SQL fragment for String. Doubles \ and ' by default.
726
747
  def literal_string(v)
727
748
  "'#{v.gsub(/\\/, "\\\\\\\\").gsub(/'/, "''")}'"
@@ -774,59 +795,7 @@ module Sequel
774
795
 
775
796
  # Qualify the given expression e to the given table.
776
797
  def qualified_expression(e, table)
777
- case e
778
- when Symbol
779
- t, column, aliaz = split_symbol(e)
780
- if t
781
- e
782
- elsif aliaz
783
- SQL::AliasedExpression.new(SQL::QualifiedIdentifier.new(table, SQL::Identifier.new(column)), aliaz)
784
- else
785
- SQL::QualifiedIdentifier.new(table, e)
786
- end
787
- when Array
788
- e.map{|a| qualified_expression(a, table)}
789
- when Hash
790
- h = {}
791
- e.each{|k,v| h[qualified_expression(k, table)] = qualified_expression(v, table)}
792
- h
793
- when SQL::Identifier
794
- SQL::QualifiedIdentifier.new(table, e)
795
- when SQL::OrderedExpression
796
- SQL::OrderedExpression.new(qualified_expression(e.expression, table), e.descending, :nulls=>e.nulls)
797
- when SQL::AliasedExpression
798
- SQL::AliasedExpression.new(qualified_expression(e.expression, table), e.aliaz)
799
- when SQL::CaseExpression
800
- args = [qualified_expression(e.conditions, table), qualified_expression(e.default, table)]
801
- args << qualified_expression(e.expression, table) if e.expression?
802
- SQL::CaseExpression.new(*args)
803
- when SQL::Cast
804
- SQL::Cast.new(qualified_expression(e.expr, table), e.type)
805
- when SQL::Function
806
- SQL::Function.new(e.f, *qualified_expression(e.args, table))
807
- when SQL::ComplexExpression
808
- SQL::ComplexExpression.new(e.op, *qualified_expression(e.args, table))
809
- when SQL::Subscript
810
- SQL::Subscript.new(qualified_expression(e.f, table), qualified_expression(e.sub, table))
811
- when SQL::WindowFunction
812
- SQL::WindowFunction.new(qualified_expression(e.function, table), qualified_expression(e.window, table))
813
- when SQL::Window
814
- o = e.opts.dup
815
- o[:partition] = qualified_expression(o[:partition], table) if o[:partition]
816
- o[:order] = qualified_expression(o[:order], table) if o[:order]
817
- SQL::Window.new(o)
818
- when SQL::PlaceholderLiteralString
819
- args = if e.args.is_a?(Hash)
820
- h = {}
821
- e.args.each{|k,v| h[k] = qualified_expression(v, table)}
822
- h
823
- else
824
- qualified_expression(e.args, table)
825
- end
826
- SQL::PlaceholderLiteralString.new(e.str, args, e.parens)
827
- else
828
- e
829
- end
798
+ Qualifier.new(self, table).transform(e)
830
799
  end
831
800
 
832
801
  # The order of methods to call to build the SELECT SQL statement
@@ -914,6 +883,10 @@ module Sequel
914
883
  return if !ws || ws.empty?
915
884
  sql.replace("#{select_with_sql_base}#{ws.map{|w| "#{quote_identifier(w[:name])}#{"(#{argument_list(w[:args])})" if w[:args]} AS #{literal_dataset(w[:dataset])}"}.join(COMMA_SEPARATOR)} #{sql}")
916
885
  end
886
+ alias delete_with_sql select_with_sql
887
+ alias insert_with_sql select_with_sql
888
+ alias update_with_sql select_with_sql
889
+
917
890
 
918
891
  # The base keyword to use for the SQL WITH clause
919
892
  def select_with_sql_base
@@ -26,6 +26,14 @@ module Sequel
26
26
  extend Metaprogramming
27
27
  include Metaprogramming
28
28
  include Enumerable
29
+ include SQL::AliasMethods
30
+ include SQL::BooleanMethods
31
+ include SQL::CastMethods
32
+ include SQL::ComplexExpressionMethods
33
+ include SQL::InequalityMethods
34
+ include SQL::NumericMethods
35
+ include SQL::OrderMethods
36
+ include SQL::StringMethods
29
37
  end
30
38
 
31
39
  require(%w"query actions features graph prepared_statements misc mutation sql", 'dataset')
@@ -44,6 +44,10 @@ module Sequel
44
44
  # and won't reraise it.
45
45
  class Rollback < Error ; end
46
46
 
47
+ # Exception that occurs when unbinding a dataset that has multiple different values
48
+ # for a given variable.
49
+ class UnbindDuplicate < Error; end
50
+
47
51
  class Error
48
52
  AdapterNotFound = Sequel::AdapterNotFound
49
53
  InvalidOperation = Sequel::InvalidOperation
@@ -0,0 +1,61 @@
1
+ # The columns_introspection extension attempts to introspect the
2
+ # selected columns for a dataset before issuing a query. If it
3
+ # thinks it can guess correctly at the columns the query will use,
4
+ # it will return the columns without issuing a database query.
5
+ # This method is not fool-proof, it's possible that some databases
6
+ # will use column names that Sequel does not expect.
7
+ #
8
+ # To enable this for a single dataset, extend the dataset with
9
+ # Sequel::ColumnIntrospection. To enable this for all datasets, run:
10
+ #
11
+ # Sequel::Dataset.introspect_all_columns
12
+
13
+ module Sequel
14
+ module ColumnsIntrospection
15
+ # Attempt to guess the columns that will be returned
16
+ # if there are columns selected, in order to skip a database
17
+ # query to retrieve the columns. This should work with
18
+ # Symbols, SQL::Identifiers, SQL::QualifiedIdentifiers, and
19
+ # SQL::AliasedExpressions.
20
+ def columns
21
+ return @columns if @columns
22
+ return columns_without_introspection unless cols = opts[:select] and !cols.empty?
23
+ probable_columns = cols.map{|c| probable_column_name(c)}
24
+ if probable_columns.all?
25
+ @columns = probable_columns
26
+ else
27
+ columns_without_introspection
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ # Return the probable name of the column, or nil if one
34
+ # cannot be determined.
35
+ def probable_column_name(c)
36
+ case c
37
+ when Symbol
38
+ _, c, a = split_symbol(c)
39
+ (a || c).to_sym
40
+ when SQL::Identifier
41
+ c.value.to_sym
42
+ when SQL::QualifiedIdentifier
43
+ col = c.column
44
+ col.is_a?(SQL::Identifier) ? col.value.to_sym : col.to_sym
45
+ when SQL::AliasedExpression
46
+ a = c.aliaz
47
+ a.is_a?(SQL::Identifier) ? a.value.to_sym : a.to_sym
48
+ end
49
+ end
50
+ end
51
+
52
+ class Dataset
53
+ alias columns_without_introspection columns
54
+
55
+ # Enable column introspection for every dataset.
56
+ def self.introspect_all_columns
57
+ include ColumnsIntrospection
58
+ remove_method(:columns) if instance_methods(false).map{|x| x.to_s}.include?('columns')
59
+ end
60
+ end
61
+ end
@@ -2,6 +2,7 @@
2
2
  # the user to easily group schema changes and migrate the database
3
3
  # to a newer version or revert to a previous version.
4
4
 
5
+ #
5
6
  module Sequel
6
7
  # Sequel's older migration class, available for backward compatibility.
7
8
  # Uses subclasses with up and down instance methods for each migration:
@@ -302,7 +303,7 @@ module Sequel
302
303
  #
303
304
  # Part of the +migration+ extension.
304
305
  class Migrator
305
- MIGRATION_FILE_PATTERN = /\A\d+_.+\.rb\z/i.freeze
306
+ MIGRATION_FILE_PATTERN = /\A(\d+)_.+\.rb\z/i.freeze
306
307
  MIGRATION_SPLITTER = '_'.freeze
307
308
  MINIMUM_TIMESTAMP = 20000101
308
309
 
@@ -375,7 +376,8 @@ module Sequel
375
376
  @db = db
376
377
  @directory = directory
377
378
  @files = get_migration_files
378
- @table = opts[:table] || self.class.const_get(:DEFAULT_SCHEMA_TABLE)
379
+ schema, table = @db.send(:schema_and_table, opts[:table] || self.class.const_get(:DEFAULT_SCHEMA_TABLE))
380
+ @table = schema ? Sequel::SQL::QualifiedIdentifier.new(schema, table) : table
379
381
  @column = opts[:column] || self.class.const_get(:DEFAULT_SCHEMA_COLUMN)
380
382
  @ds = schema_dataset
381
383
  end
@@ -423,7 +425,7 @@ module Sequel
423
425
  @current = opts[:current] || current_migration_version
424
426
 
425
427
  raise(Error, "No current version available") unless current
426
- raise(Error, "No target version available") unless target
428
+ raise(Error, "No target version available, probably because no migration files found or filenames don't follow the migration filename convention") unless target
427
429
 
428
430
  @direction = current < target ? :up : :down
429
431
  @migrations = get_migrations
@@ -588,7 +590,7 @@ module Sequel
588
590
  next unless MIGRATION_FILE_PATTERN.match(file)
589
591
  files << File.join(directory, file)
590
592
  end
591
- files.sort
593
+ files.sort_by{|f| MIGRATION_FILE_PATTERN.match(File.basename(f))[1].to_i}
592
594
  end
593
595
 
594
596
  # Returns tuples of migration, filename, and direction
@@ -4,109 +4,111 @@
4
4
  # of the dataset's abstract syntax tree.
5
5
 
6
6
  module Sequel
7
- class Dataset
7
+ class ToDot
8
8
  # The option keys that should be included in the dot output.
9
9
  TO_DOT_OPTIONS = [:with, :distinct, :select, :from, :join, :where, :group, :having, :compounds, :order, :limit, :offset, :lock].freeze
10
10
 
11
- # Return a string that can be processed by the +dot+ program (included
12
- # with graphviz) in order to see a visualization of the dataset's
13
- # abstract syntax tree.
14
- def to_dot
15
- i = 0
16
- dot = ["digraph G {", "#{i} [label=\"self\"];"]
17
- _to_dot(dot, "", i, self, i)
18
- dot << "}"
19
- dot.join("\n")
11
+ # Given a +Dataset+, return a string in +dot+ format that will
12
+ # generate a visualization of the dataset.
13
+ def self.output(ds)
14
+ new(ds).output
15
+ end
16
+
17
+ # Given a +Dataset+, parse the internal structure to generate
18
+ # a dataset visualization.
19
+ def initialize(ds)
20
+ @i = 0
21
+ @stack = [@i]
22
+ @dot = ["digraph G {", "0 [label=\"self\"];"]
23
+ v(ds, "")
24
+ @dot << "}"
25
+ end
26
+
27
+ # Output the dataset visualization as a string in +dot+ format.
28
+ def output
29
+ @dot.join("\n")
20
30
  end
21
31
 
22
32
  private
23
33
 
24
- # Internal recursive version that handles all object types understood
25
- # by Sequel. Arguments:
26
- # * dot :: An array of strings representing the lines in the returned
27
- # output. This function just pushes strings onto this array.
28
- # * l :: The transition label from the parent node of the AST to the
29
- # current node.
30
- # * c :: An integer representing the parent node of the AST.
31
- # * e :: The current node of the AST.
32
- # * i :: The integer representing the last created node of the AST.
33
- #
34
- # The basic algorithm is that the +i+ is incremented to get the current
35
- # node's integer. Then the transition from the parent node to the
36
- # current node is added to the +dot+ array. Finally, the current node
37
- # is added to the +dot+ array, and if it is a compound node with children,
38
- # its children are then added by recursively calling this method. The
39
- # return value is the integer representing the last created node.
40
- def _to_dot(dot, l, c, e, i)
41
- i += 1
42
- dot << "#{c} -> #{i} [label=\"#{l}\"];" if l
43
- c = i
34
+ # Add an entry to the +dot+ output with the given label. If +j+
35
+ # is given, it is used directly as the node or transition. Otherwise
36
+ # a node is created for the current object.
37
+ def dot(label, j=nil)
38
+ @dot << "#{j||@i} [label=#{label.to_s.inspect}];"
39
+ end
40
+
41
+ # Recursive method that parses all of Sequel's internal datastructures,
42
+ # adding the appropriate nodes and transitions to the internal +dot+
43
+ # structure.
44
+ def v(e, l)
45
+ @i += 1
46
+ dot(l, "#{@stack.last} -> #{@i}") if l
47
+ @stack.push(@i)
44
48
  case e
45
49
  when LiteralString
46
- dot << "#{i} [label=\"#{e.inspect.gsub('"', '\\"')}.lit\"];"
47
- i
50
+ dot "#{e.inspect}.lit"
48
51
  when Symbol, Numeric, String, Class, TrueClass, FalseClass, NilClass
49
- dot << "#{i} [label=\"#{e.inspect.gsub('"', '\\"')}\"];"
50
- i
52
+ dot e.inspect
51
53
  when Array
52
- dot << "#{i} [label=\"Array\"];"
53
- e.each_with_index do |v, j|
54
- i = _to_dot(dot, j, c, v, i)
54
+ dot "Array"
55
+ e.each_with_index do |val, j|
56
+ v(val, j)
55
57
  end
56
58
  when Hash
57
- dot << "#{i} [label=\"Hash\"];"
58
- e.each do |k, v|
59
- i = _to_dot(dot, k, c, v, i)
59
+ dot "Hash"
60
+ e.each do |k, val|
61
+ v(val, k)
60
62
  end
61
63
  when SQL::ComplexExpression
62
- dot << "#{i} [label=\"ComplexExpression: #{e.op}\"];"
63
- e.args.each_with_index do |v, j|
64
- i = _to_dot(dot, j, c, v, i)
64
+ dot "ComplexExpression: #{e.op}"
65
+ e.args.each_with_index do |val, j|
66
+ v(val, j)
65
67
  end
66
68
  when SQL::Identifier
67
- dot << "#{i} [label=\"Identifier\"];"
68
- i = _to_dot(dot, :value, c, e.value, i)
69
+ dot "Identifier"
70
+ v(e.value, :value)
69
71
  when SQL::QualifiedIdentifier
70
- dot << "#{i} [label=\"QualifiedIdentifier\"];"
71
- i = _to_dot(dot, :table, c, e.table, i)
72
- i = _to_dot(dot, :column, c, e.column, i)
72
+ dot "QualifiedIdentifier"
73
+ v(e.table, :table)
74
+ v(e.column, :column)
73
75
  when SQL::OrderedExpression
74
- dot << "#{i} [label=\"OrderedExpression: #{e.descending ? :DESC : :ASC}#{" NULLS #{e.nulls.to_s.upcase}" if e.nulls}\"];"
75
- i = _to_dot(dot, :expression, c, e.expression, i)
76
+ dot "OrderedExpression: #{e.descending ? :DESC : :ASC}#{" NULLS #{e.nulls.to_s.upcase}" if e.nulls}"
77
+ v(e.expression, :expression)
76
78
  when SQL::AliasedExpression
77
- dot << "#{i} [label=\"AliasedExpression\"];"
78
- i = _to_dot(dot, :expression, c, e.expression, i)
79
- i = _to_dot(dot, :alias, c, e.aliaz, i)
79
+ dot "AliasedExpression"
80
+ v(e.expression, :expression)
81
+ v(e.aliaz, :alias)
80
82
  when SQL::CaseExpression
81
- dot << "#{i} [label=\"CaseExpression\"];"
82
- i = _to_dot(dot, :expression, c, e.expression, i) if e.expression
83
- i = _to_dot(dot, :conditions, c, e.conditions, i)
84
- i = _to_dot(dot, :default, c, e.default, i)
83
+ dot "CaseExpression"
84
+ v(e.expression, :expression) if e.expression
85
+ v(e.conditions, :conditions)
86
+ v(e.default, :default)
85
87
  when SQL::Cast
86
- dot << "#{i} [label=\"Cast\"];"
87
- i = _to_dot(dot, :expr, c, e.expr, i)
88
- i = _to_dot(dot, :type, c, e.type, i)
88
+ dot "Cast"
89
+ v(e.expr, :expr)
90
+ v(e.type, :type)
89
91
  when SQL::Function
90
- dot << "#{i} [label=\"Function: #{e.f}\"];"
91
- e.args.each_with_index do |v, j|
92
- i = _to_dot(dot, j, c, v, i)
92
+ dot "Function: #{e.f}"
93
+ e.args.each_with_index do |val, j|
94
+ v(val, j)
93
95
  end
94
96
  when SQL::Subscript
95
- dot << "#{i} [label=\"Subscript: #{e.f}\"];"
96
- i = _to_dot(dot, :f, c, e.f, i)
97
- i = _to_dot(dot, :sub, c, e.sub, i)
97
+ dot "Subscript"
98
+ v(e.f, :f)
99
+ v(e.sub, :sub)
98
100
  when SQL::WindowFunction
99
- dot << "#{i} [label=\"WindowFunction\"];"
100
- i = _to_dot(dot, :function, c, e.function, i)
101
- i = _to_dot(dot, :window, c, e.window, i)
101
+ dot "WindowFunction"
102
+ v(e.function, :function)
103
+ v(e.window, :window)
102
104
  when SQL::Window
103
- dot << "#{i} [label=\"Window\"];"
104
- i = _to_dot(dot, :opts, c, e.opts, i)
105
+ dot "Window"
106
+ v(e.opts, :opts)
105
107
  when SQL::PlaceholderLiteralString
106
108
  str = e.str
107
109
  str = "(#{str})" if e.parens
108
- dot << "#{i} [label=\"PlaceholderLiteralString: #{str.inspect.gsub('"', '\\"')}\"];"
109
- i = _to_dot(dot, :args, c, e.args, i)
110
+ dot "PlaceholderLiteralString: #{str.inspect}"
111
+ v(e.args, :args)
110
112
  when SQL::JoinClause
111
113
  str = "#{e.join_type.to_s.upcase} JOIN"
112
114
  if e.is_a?(SQL::JoinOnClause)
@@ -114,24 +116,34 @@ module Sequel
114
116
  elsif e.is_a?(SQL::JoinUsingClause)
115
117
  str << " USING"
116
118
  end
117
- dot << "#{i} [label=\"#{str}\"];"
118
- i = _to_dot(dot, :table, c, e.table, i)
119
- i = _to_dot(dot, :alias, c, e.table_alias, i) if e.table_alias
119
+ dot str
120
+ v(e.table, :table)
121
+ v(e.table_alias, :alias) if e.table_alias
120
122
  if e.is_a?(SQL::JoinOnClause)
121
- i = _to_dot(dot, :on, c, e.on, i)
123
+ v(e.on, :on)
122
124
  elsif e.is_a?(SQL::JoinUsingClause)
123
- i = _to_dot(dot, :using, c, e.using, i)
125
+ v(e.using, :using)
124
126
  end
125
127
  when Dataset
126
- dot << "#{i} [label=\"Dataset\"];"
128
+ dot "Dataset"
127
129
  TO_DOT_OPTIONS.each do |k|
128
- next unless e.opts[k]
129
- i = _to_dot(dot, k, c, e.opts[k], i)
130
+ if val = e.opts[k]
131
+ v(val, k.to_s)
132
+ end
130
133
  end
131
134
  else
132
- dot << "#{i} [label=\"Unhandled: #{e.inspect.gsub('"', "''")}\"];"
135
+ dot "Unhandled: #{e.inspect}"
133
136
  end
134
- i
137
+ @stack.pop
138
+ end
139
+ end
140
+
141
+ class Dataset
142
+ # Return a string that can be processed by the +dot+ program (included
143
+ # with graphviz) in order to see a visualization of the dataset's
144
+ # abstract syntax tree.
145
+ def to_dot
146
+ ToDot.output(self)
135
147
  end
136
148
  end
137
149
  end