torque-postgresql 0.2.16 → 1.0.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 (64) hide show
  1. checksums.yaml +4 -4
  2. data/README.rdoc +76 -3
  3. data/lib/torque-postgresql.rb +1 -0
  4. data/lib/torque/postgresql.rb +6 -0
  5. data/lib/torque/postgresql/adapter.rb +2 -4
  6. data/lib/torque/postgresql/adapter/database_statements.rb +23 -9
  7. data/lib/torque/postgresql/adapter/oid.rb +12 -1
  8. data/lib/torque/postgresql/adapter/oid/box.rb +28 -0
  9. data/lib/torque/postgresql/adapter/oid/circle.rb +37 -0
  10. data/lib/torque/postgresql/adapter/oid/enum.rb +9 -5
  11. data/lib/torque/postgresql/adapter/oid/enum_set.rb +44 -0
  12. data/lib/torque/postgresql/adapter/oid/line.rb +59 -0
  13. data/lib/torque/postgresql/adapter/oid/range.rb +52 -0
  14. data/lib/torque/postgresql/adapter/oid/segment.rb +73 -0
  15. data/lib/torque/postgresql/adapter/quoting.rb +21 -0
  16. data/lib/torque/postgresql/adapter/schema_definitions.rb +7 -0
  17. data/lib/torque/postgresql/adapter/schema_dumper.rb +10 -1
  18. data/lib/torque/postgresql/arel.rb +3 -0
  19. data/lib/torque/postgresql/arel/infix_operation.rb +42 -0
  20. data/lib/torque/postgresql/arel/nodes.rb +32 -0
  21. data/lib/torque/postgresql/arel/operations.rb +18 -0
  22. data/lib/torque/postgresql/arel/visitors.rb +28 -2
  23. data/lib/torque/postgresql/associations.rb +8 -0
  24. data/lib/torque/postgresql/associations/association.rb +30 -0
  25. data/lib/torque/postgresql/associations/association_scope.rb +116 -0
  26. data/lib/torque/postgresql/associations/belongs_to_many_association.rb +117 -0
  27. data/lib/torque/postgresql/associations/builder.rb +2 -0
  28. data/lib/torque/postgresql/associations/builder/belongs_to_many.rb +121 -0
  29. data/lib/torque/postgresql/associations/builder/has_many.rb +15 -0
  30. data/lib/torque/postgresql/associations/join_dependency/join_association.rb +15 -0
  31. data/lib/torque/postgresql/associations/preloader.rb +25 -0
  32. data/lib/torque/postgresql/associations/preloader/association.rb +64 -0
  33. data/lib/torque/postgresql/attributes.rb +2 -0
  34. data/lib/torque/postgresql/attributes/builder.rb +1 -0
  35. data/lib/torque/postgresql/attributes/builder/enum.rb +23 -15
  36. data/lib/torque/postgresql/attributes/builder/period.rb +452 -0
  37. data/lib/torque/postgresql/attributes/enum.rb +11 -8
  38. data/lib/torque/postgresql/attributes/enum_set.rb +256 -0
  39. data/lib/torque/postgresql/attributes/lazy.rb +1 -1
  40. data/lib/torque/postgresql/attributes/period.rb +31 -0
  41. data/lib/torque/postgresql/attributes/type_map.rb +3 -5
  42. data/lib/torque/postgresql/autosave_association.rb +40 -0
  43. data/lib/torque/postgresql/auxiliary_statement.rb +201 -198
  44. data/lib/torque/postgresql/auxiliary_statement/settings.rb +20 -12
  45. data/lib/torque/postgresql/base.rb +161 -2
  46. data/lib/torque/postgresql/config.rb +91 -9
  47. data/lib/torque/postgresql/geometry_builder.rb +92 -0
  48. data/lib/torque/postgresql/i18n.rb +1 -1
  49. data/lib/torque/postgresql/railtie.rb +18 -5
  50. data/lib/torque/postgresql/reflection.rb +21 -0
  51. data/lib/torque/postgresql/reflection/abstract_reflection.rb +109 -0
  52. data/lib/torque/postgresql/reflection/association_reflection.rb +30 -0
  53. data/lib/torque/postgresql/reflection/belongs_to_many_reflection.rb +44 -0
  54. data/lib/torque/postgresql/reflection/has_many_reflection.rb +13 -0
  55. data/lib/torque/postgresql/reflection/runtime_reflection.rb +12 -0
  56. data/lib/torque/postgresql/reflection/through_reflection.rb +11 -0
  57. data/lib/torque/postgresql/relation.rb +11 -10
  58. data/lib/torque/postgresql/relation/auxiliary_statement.rb +11 -18
  59. data/lib/torque/postgresql/relation/inheritance.rb +2 -2
  60. data/lib/torque/postgresql/relation/merger.rb +11 -7
  61. data/lib/torque/postgresql/schema_cache.rb +1 -1
  62. data/lib/torque/postgresql/version.rb +1 -1
  63. data/lib/torque/range.rb +40 -0
  64. metadata +41 -9
@@ -2,18 +2,29 @@ module Torque
2
2
  module PostgreSQL
3
3
  class AuxiliaryStatement
4
4
  class Settings < Collector.new(:attributes, :join, :join_type, :query, :requires,
5
- :polymorphic)
5
+ :polymorphic, :through)
6
6
 
7
- attr_reader :source
8
- alias cte source
7
+ attr_reader :base, :source
8
+ alias_method :select, :attributes
9
+ alias_method :cte, :source
9
10
 
10
- delegate :base, :base_name, :base_table, :table, :table_name, to: :@source
11
11
  delegate :relation_query?, to: Torque::PostgreSQL::AuxiliaryStatement
12
+ delegate :table, :table_name, to: :@source
13
+ delegate :sql, to: ::Arel
12
14
 
13
- def initialize(source)
15
+ def initialize(base, source)
16
+ @base = base
14
17
  @source = source
15
18
  end
16
19
 
20
+ def base_name
21
+ @base.name
22
+ end
23
+
24
+ def base_table
25
+ @base.arel_table
26
+ end
27
+
17
28
  # Get the arel version of the table set on the query
18
29
  def query_table
19
30
  raise StandardError, 'The query is not defined yet' if query.nil?
@@ -28,11 +39,6 @@ module Torque
28
39
 
29
40
  alias column col
30
41
 
31
- # Grant an easy access to arel sql literal
32
- def sql(string)
33
- ::Arel::Nodes::SqlLiteral.new(string)
34
- end
35
-
36
42
  # There are two ways of setting the query:
37
43
  # - A simple relation based on a Model
38
44
  # - A Arel-based select manager
@@ -48,11 +54,13 @@ module Torque
48
54
  end
49
55
 
50
56
  valid_type = command.respond_to?(:call) || command.is_a?(String)
51
- raise ArgumentError, <<-MSG.strip.gsub(/\n +/, ' ') if command.nil?
57
+
58
+ raise ArgumentError, <<-MSG.squish if command.nil?
52
59
  To use proc or string as query, you need to provide the table name
53
60
  as the first argument
54
61
  MSG
55
- raise ArgumentError, <<-MSG.strip.gsub(/\n +/, ' ') unless valid_type
62
+
63
+ raise ArgumentError, <<-MSG.squish unless valid_type
56
64
  Only relation, string and proc are valid object types for query,
57
65
  #{command.inspect} given.
58
66
  MSG
@@ -3,6 +3,10 @@ module Torque
3
3
  module Base
4
4
  extend ActiveSupport::Concern
5
5
 
6
+ included do
7
+ mattr_accessor :belongs_to_many_required_by_default, instance_accessor: false
8
+ end
9
+
6
10
  module ClassMethods
7
11
  delegate :distinct_on, :with, :itself_only, :cast_records, to: :all
8
12
 
@@ -12,7 +16,8 @@ module Torque
12
16
  super
13
17
 
14
18
  subclass.class_attribute(:auxiliary_statements_list)
15
- subclass.auxiliary_statements_list = Hash.new
19
+ subclass.auxiliary_statements_list = {}
20
+
16
21
  record_class = ActiveRecord::Relation._record_class_attribute
17
22
 
18
23
  # Define helper methods to return the class of the given records
@@ -44,6 +49,160 @@ module Torque
44
49
  end
45
50
  end
46
51
 
52
+ # Specifies a one-to-many association. The following methods for
53
+ # retrieval and query of collections of associated objects will be
54
+ # added:
55
+ #
56
+ # +collection+ is a placeholder for the symbol passed as the +name+
57
+ # argument, so <tt>belongs_to_many :tags</tt> would add among others
58
+ # <tt>tags.empty?</tt>.
59
+ #
60
+ # [collection]
61
+ # Returns a Relation of all the associated objects.
62
+ # An empty Relation is returned if none are found.
63
+ # [collection<<(object, ...)]
64
+ # Adds one or more objects to the collection by adding their ids to
65
+ # the array of ids on the parent object.
66
+ # Note that this operation instantly fires update SQL without waiting
67
+ # for the save or update call on the parent object, unless the parent
68
+ # object is a new record.
69
+ # This will also run validations and callbacks of associated
70
+ # object(s).
71
+ # [collection.delete(object, ...)]
72
+ # Removes one or more objects from the collection by removing their
73
+ # ids from the list on the parent object.
74
+ # Objects will be in addition destroyed if they're associated with
75
+ # <tt>dependent: :destroy</tt>, and deleted if they're associated
76
+ # with <tt>dependent: :delete_all</tt>.
77
+ # [collection.destroy(object, ...)]
78
+ # Removes one or more objects from the collection by running
79
+ # <tt>destroy</tt> on each record, regardless of any dependent option,
80
+ # ensuring callbacks are run. They will also be removed from the list
81
+ # on the parent object.
82
+ # [collection=objects]
83
+ # Replaces the collections content by deleting and adding objects as
84
+ # appropriate.
85
+ # [collection_singular_ids]
86
+ # Returns an array of the associated objects' ids
87
+ # [collection_singular_ids=ids]
88
+ # Replace the collection with the objects identified by the primary
89
+ # keys in +ids+. This method loads the models and calls
90
+ # <tt>collection=</tt>. See above.
91
+ # [collection.clear]
92
+ # Removes every object from the collection. This destroys the
93
+ # associated objects if they are associated with
94
+ # <tt>dependent: :destroy</tt>, deletes them directly from the
95
+ # database if <tt>dependent: :delete_all</tt>, otherwise just remove
96
+ # them from the list on the parent object.
97
+ # [collection.empty?]
98
+ # Returns +true+ if there are no associated objects.
99
+ # [collection.size]
100
+ # Returns the number of associated objects.
101
+ # [collection.find(...)]
102
+ # Finds an associated object according to the same rules as
103
+ # ActiveRecord::FinderMethods#find.
104
+ # [collection.exists?(...)]
105
+ # Checks whether an associated object with the given conditions exists.
106
+ # Uses the same rules as ActiveRecord::FinderMethods#exists?.
107
+ # [collection.build(attributes = {}, ...)]
108
+ # Returns one or more new objects of the collection type that have
109
+ # been instantiated with +attributes+ and linked to this object by
110
+ # adding its +id+ to the list after saving.
111
+ # [collection.create(attributes = {})]
112
+ # Returns a new object of the collection type that has been
113
+ # instantiated with +attributes+, linked to this object by adding its
114
+ # +id+ to the list after performing the save (if it passed the
115
+ # validation).
116
+ # [collection.create!(attributes = {})]
117
+ # Does the same as <tt>collection.create</tt>, but raises
118
+ # ActiveRecord::RecordInvalid if the record is invalid.
119
+ # [collection.reload]
120
+ # Returns a Relation of all of the associated objects, forcing a
121
+ # database read. An empty Relation is returned if none are found.
122
+ #
123
+ # === Example
124
+ #
125
+ # A <tt>Video</tt> class declares <tt>belongs_to_many :tags</tt>,
126
+ # which will add:
127
+ # * <tt>Video#tags</tt> (similar to <tt>Tag.where([id] && tag_ids)</tt>)
128
+ # * <tt>Video#tags<<</tt>
129
+ # * <tt>Video#tags.delete</tt>
130
+ # * <tt>Video#tags.destroy</tt>
131
+ # * <tt>Video#tags=</tt>
132
+ # * <tt>Video#tag_ids</tt>
133
+ # * <tt>Video#tag_ids=</tt>
134
+ # * <tt>Video#tags.clear</tt>
135
+ # * <tt>Video#tags.empty?</tt>
136
+ # * <tt>Video#tags.size</tt>
137
+ # * <tt>Video#tags.find</tt>
138
+ # * <tt>Video#tags.exists?(name: 'ACME')</tt>
139
+ # * <tt>Video#tags.build</tt>
140
+ # * <tt>Video#tags.create</tt>
141
+ # * <tt>Video#tags.create!</tt>
142
+ # * <tt>Video#tags.reload</tt>
143
+ # The declaration can also include an +options+ hash to specialize the
144
+ # behavior of the association.
145
+ #
146
+ # === Options
147
+ # [:class_name]
148
+ # Specify the class name of the association. Use it only if that name
149
+ # can't be inferred from the association name. So <tt>belongs_to_many
150
+ # :tags</tt> will by default be linked to the +Tag+ class, but if the
151
+ # real class name is +SpecialTag+, you'll have to specify it with this
152
+ # option.
153
+ # [:foreign_key]
154
+ # Specify the foreign key used for the association. By default this is
155
+ # guessed to be the name of this class in lower-case and "_ids"
156
+ # suffixed. So a Video class that makes a #belongs_to_many association
157
+ # with Tag will use "tag_ids" as the default <tt>:foreign_key</tt>.
158
+ #
159
+ # It is a good idea to set the <tt>:inverse_of</tt> option as well.
160
+ # [:primary_key]
161
+ # Specify the name of the column to use as the primary key for the
162
+ # association. By default this is +id+.
163
+ # [:dependent]
164
+ # Controls what happens to the associated objects when their owner is
165
+ # destroyed. Note that these are implemented as callbacks, and Rails
166
+ # executes callbacks in order. Therefore, other similar callbacks may
167
+ # affect the <tt>:dependent</tt> behavior, and the <tt>:dependent</tt>
168
+ # behavior may affect other callbacks.
169
+ # [:touch]
170
+ # If true, the associated objects will be touched (the updated_at/on
171
+ # attributes set to current time) when this record is either saved or
172
+ # destroyed. If you specify a symbol, that attribute will be updated
173
+ # with the current time in addition to the updated_at/on attribute.
174
+ # Please note that with touching no validation is performed and only
175
+ # the +after_touch+, +after_commit+ and +after_rollback+ callbacks are
176
+ # executed.
177
+ # [:optional]
178
+ # When set to +true+, the association will not have its presence
179
+ # validated.
180
+ # [:required]
181
+ # When set to +true+, the association will also have its presence
182
+ # validated. This will validate the association itself, not the id.
183
+ # You can use +:inverse_of+ to avoid an extra query during validation.
184
+ # NOTE: <tt>required</tt> is set to <tt>false</tt> by default and is
185
+ # deprecated. If you want to have association presence validated,
186
+ # use <tt>required: true</tt>.
187
+ # [:default]
188
+ # Provide a callable (i.e. proc or lambda) to specify that the
189
+ # association should be initialized with a particular record before
190
+ # validation.
191
+ # [:inverse_of]
192
+ # Specifies the name of the #has_many association on the associated
193
+ # object that is the inverse of this #belongs_to_many association.
194
+ # See ActiveRecord::Associations::ClassMethods's overview on
195
+ # Bi-directional associations for more detail.
196
+ #
197
+ # Option examples:
198
+ # belongs_to_many :tags, dependent: :nullify
199
+ # belongs_to_many :tags, required: true, touch: true
200
+ # belongs_to_many :tags, default: -> { Tag.default }
201
+ def belongs_to_many(name, scope = nil, **options)
202
+ reflection = Associations::Builder::BelongsToMany.build(self, name, scope, options)
203
+ ::ActiveRecord::Reflection.add_reflection(self, name, reflection)
204
+ end
205
+
47
206
  protected
48
207
 
49
208
  # Allow optional select attributes to be loaded manually when they are
@@ -134,6 +293,6 @@ module Torque
134
293
  end
135
294
  end
136
295
 
137
- ActiveRecord::Base.include Base
296
+ ::ActiveRecord::Base.include(Base)
138
297
  end
139
298
  end
@@ -26,17 +26,39 @@ module Torque
26
26
  end.to_h
27
27
  end
28
28
 
29
+ # Configure associations features
30
+ config.nested(:associations) do |assoc|
31
+
32
+ # Define if +belongs_to_many+ associations are marked as required by
33
+ # default. False means that no validation will be performed
34
+ assoc.belongs_to_many_required_by_default = false
35
+
36
+ end
37
+
38
+ # Configure auxiliary statement features
39
+ config.nested(:auxiliary_statement) do |cte|
40
+
41
+ # Define the key that is used on auxiliary statements to send extra
42
+ # arguments to format string or send on a proc
43
+ cte.send_arguments_key = :args
44
+
45
+ # Estipulate a class name (which may contain namespace) that expose the
46
+ # auxiliary statement in order to perform detached CTEs
47
+ cte.exposed_class = 'TorqueCTE'
48
+
49
+ end
50
+
29
51
  # Configure ENUM features
30
52
  config.nested(:enum) do |enum|
31
53
 
32
- # Indicates if the enum features on ActiveRecord::Base should be initiated
33
- # automatically or not
34
- enum.initializer = false
35
-
36
54
  # The name of the method to be used on any ActiveRecord::Base to
37
55
  # initialize model-based enum features
38
56
  enum.base_method = :enum
39
57
 
58
+ # The name of the method to be used on any ActiveRecord::Base to
59
+ # initialize model-based enum set features
60
+ enum.set_method = :enum_set
61
+
40
62
  # Indicates if bang methods like 'disabled!' should update the record on
41
63
  # database or not
42
64
  enum.save_on_bang = true
@@ -63,12 +85,28 @@ module Torque
63
85
 
64
86
  end
65
87
 
66
- # Configure auxiliary statement features
67
- config.nested(:auxiliary_statement) do |cte|
88
+ # Configure geometry data types
89
+ config.nested(:geometry) do |geometry|
68
90
 
69
- # Define the key that is used on auxiliary statements to send extra
70
- # arguments to format string or send on a proc
71
- cte.send_arguments_key = :args
91
+ # Define the class that will be handling Point data types after decoding
92
+ # it. Any class provided here must respond to 'x', and 'y'
93
+ geometry.point_class = ActiveRecord::Point
94
+
95
+ # Define the class that will be handling Box data types after decoding it.
96
+ # Any class provided here must respond to 'x1', 'y1', 'x2', and 'y2'
97
+ geometry.box_class = nil
98
+
99
+ # Define the class that will be handling Circle data types after decoding
100
+ # it. Any class provided here must respond to 'x', 'y', and 'r'
101
+ geometry.circle_class = nil
102
+
103
+ # Define the class that will be handling Line data types after decoding
104
+ # it. Any class provided here must respond to 'a', 'b', and 'c'
105
+ geometry.line_class = nil
106
+
107
+ # Define the class that will be handling Segment data types after decoding
108
+ # it. Any class provided here must respond to 'x1', 'y1', 'x2', and 'y2'
109
+ geometry.segment_class = nil
72
110
 
73
111
  end
74
112
 
@@ -92,5 +130,49 @@ module Torque
92
130
 
93
131
  end
94
132
 
133
+ # Configure period features
134
+ config.nested(:period) do |period|
135
+
136
+ # The name of the method to be used on any ActiveRecord::Base to
137
+ # initialize model-based period features
138
+ period.base_method = :period_for
139
+
140
+ # Define the list of methods that will be created by default while setting
141
+ # up a new period field
142
+ period.method_names = {
143
+ current_on: '%s_on', # 0
144
+ current: 'current_%s', # 1
145
+ not_current: 'not_current_%s', # 2
146
+ containing: '%s_containing', # 3
147
+ not_containing: '%s_not_containing', # 4
148
+ overlapping: '%s_overlapping', # 5
149
+ not_overlapping: '%s_not_overlapping', # 6
150
+ starting_after: '%s_starting_after', # 7
151
+ starting_before: '%s_starting_before', # 8
152
+ finishing_after: '%s_finishing_after', # 9
153
+ finishing_before: '%s_finishing_before', # 10
154
+
155
+ real_containing: '%s_real_containing', # 11
156
+ real_overlapping: '%s_real_overlapping', # 12
157
+ real_starting_after: '%s_real_starting_after', # 13
158
+ real_starting_before: '%s_real_starting_before', # 14
159
+ real_finishing_after: '%s_real_finishing_after', # 15
160
+ real_finishing_before: '%s_real_finishing_before', # 16
161
+
162
+ containing_date: '%s_containing_date', # 17
163
+ not_containing_date: '%s_not_containing_date', # 18
164
+ overlapping_date: '%s_overlapping_date', # 19
165
+ not_overlapping_date: '%s_not_overlapping_date', # 20
166
+
167
+ current?: 'current_%s?', # 21
168
+ current_on?: 'current_%s_on?', # 22
169
+ start: '%s_start', # 23
170
+ finish: '%s_finish', # 24
171
+ real: 'real_%s', # 25
172
+ real_start: '%s_real_start', # 26
173
+ real_finish: '%s_real_finish', # 27
174
+ }
175
+
176
+ end
95
177
  end
96
178
  end
@@ -0,0 +1,92 @@
1
+ module Torque
2
+ module PostgreSQL
3
+ class GeometryBuilder < ActiveModel::Type::Value
4
+
5
+ DESTRUCTOR = /[<>{}()]/.freeze
6
+ NUMBER_SERIALIZER = ->(num) { num.to_s.gsub(/\.0$/, '') }
7
+
8
+ def type
9
+ return self.class.const_get('TYPE') if self.class.const_defined?('TYPE')
10
+ self.class.const_set('TYPE', self.class.name.demodulize.underscore)
11
+ end
12
+
13
+ def pieces
14
+ self.class.const_get('PIECES')
15
+ end
16
+
17
+ def formation
18
+ self.class.const_get('FORMATION')
19
+ end
20
+
21
+ def cast(value)
22
+ case value
23
+ when ::String
24
+ return if value.blank?
25
+ value.gsub!(DESTRUCTOR, '')
26
+ build_klass(*value.split(','))
27
+ when ::Hash
28
+ build_klass(*value.symbolize_keys.slice(*pieces).values)
29
+ when ::Array
30
+ build_klass(*(value.flatten))
31
+ else
32
+ value
33
+ end
34
+ end
35
+
36
+ def serialize(value)
37
+ parts =
38
+ case value
39
+ when config_class
40
+ pieces.map { |piece| value.public_send(piece) }
41
+ when ::Hash
42
+ value.symbolize_keys.slice(*pieces).values
43
+ when ::Array
44
+ value.flatten
45
+ end
46
+
47
+ parts = parts&.compact&.flatten
48
+ return if parts.blank?
49
+
50
+ raise 'Invalid format' if parts.size < pieces.size
51
+ format(formation, *parts.first(pieces.size).map(&number_serializer))
52
+ end
53
+
54
+ def deserialize(value)
55
+ build_klass(*value.gsub(DESTRUCTOR, '').split(',')) unless value.nil?
56
+ end
57
+
58
+ def type_cast_for_schema(value)
59
+ if config_class === value
60
+ pieces.map { |piece| value.public_send(piece) }
61
+ else
62
+ super
63
+ end
64
+ end
65
+
66
+ def changed_in_place?(raw_old_value, new_value)
67
+ raw_old_value != serialize(new_value)
68
+ end
69
+
70
+ protected
71
+
72
+ def number_serializer
73
+ self.class.const_get('NUMBER_SERIALIZER')
74
+ end
75
+
76
+ def config_class
77
+ Torque::PostgreSQL.config.geometry.public_send("#{type}_class")
78
+ end
79
+
80
+ def build_klass(*args)
81
+ return nil if args.empty?
82
+ check_invalid_format!(args)
83
+
84
+ config_class.new(*args.try(:first, pieces.size)&.map(&:to_f))
85
+ end
86
+
87
+ def check_invalid_format!(args)
88
+ raise 'Invalid format' if args.size < pieces.size
89
+ end
90
+ end
91
+ end
92
+ end