torque-postgresql 0.2.16 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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