torque-postgresql 0.1.7 → 0.2.1

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 (32) hide show
  1. checksums.yaml +4 -4
  2. data/README.rdoc +74 -0
  3. data/lib/torque/postgresql/adapter/database_statements.rb +44 -2
  4. data/lib/torque/postgresql/adapter/schema_creation.rb +55 -0
  5. data/lib/torque/postgresql/adapter/schema_definitions.rb +26 -1
  6. data/lib/torque/postgresql/adapter/schema_statements.rb +20 -0
  7. data/lib/torque/postgresql/adapter.rb +1 -1
  8. data/lib/torque/postgresql/arel/join_source.rb +15 -0
  9. data/lib/torque/postgresql/arel/select_manager.rb +21 -0
  10. data/lib/torque/postgresql/arel/using.rb +10 -0
  11. data/lib/torque/postgresql/arel/visitors.rb +18 -1
  12. data/lib/torque/postgresql/arel.rb +4 -0
  13. data/lib/torque/postgresql/attributes/enum.rb +7 -1
  14. data/lib/torque/postgresql/attributes.rb +9 -1
  15. data/lib/torque/postgresql/auxiliary_statement/settings.rb +7 -0
  16. data/lib/torque/postgresql/auxiliary_statement.rb +19 -10
  17. data/lib/torque/postgresql/base.rb +76 -7
  18. data/lib/torque/postgresql/coder.rb +132 -0
  19. data/lib/torque/postgresql/collector.rb +2 -0
  20. data/lib/torque/postgresql/config.rb +35 -1
  21. data/lib/torque/postgresql/inheritance.rb +133 -0
  22. data/lib/torque/postgresql/railtie.rb +16 -0
  23. data/lib/torque/postgresql/relation/auxiliary_statement.rb +26 -19
  24. data/lib/torque/postgresql/relation/distinct_on.rb +9 -7
  25. data/lib/torque/postgresql/relation/inheritance.rb +112 -0
  26. data/lib/torque/postgresql/relation/merger.rb +48 -0
  27. data/lib/torque/postgresql/relation.rb +67 -2
  28. data/lib/torque/postgresql/schema_cache.rb +192 -0
  29. data/lib/torque/postgresql/schema_dumper.rb +49 -1
  30. data/lib/torque/postgresql/version.rb +1 -1
  31. data/lib/torque/postgresql.rb +6 -4
  32. metadata +14 -8
@@ -0,0 +1,132 @@
1
+ module Torque
2
+ module PostgreSQL
3
+ module Coder
4
+
5
+ # This class represents an Record to be encoded, instead of a literal Array
6
+ class Record < Array
7
+ end
8
+
9
+ class << self
10
+
11
+ NEED_QUOTE_FOR = /[\\"(){}, \t\n\r\v\f]/m
12
+ DELIMITER = ','.freeze
13
+
14
+ # This method replace the +read_array+ method from PG gem
15
+ # See https://github.com/ged/ruby-pg/blob/master/ext/pg_text_decoder.c#L177
16
+ # for more information
17
+ def decode(value)
18
+ # TODO: Use StringScanner
19
+ # See http://ruby-doc.org/stdlib-1.9.3/libdoc/strscan/rdoc/StringScanner.html
20
+ _decode(::StringIO.new(value))
21
+ end
22
+
23
+ # This method replace the ++ method from PG gem
24
+ # See https://github.com/ged/ruby-pg/blob/master/ext/pg_text_encoder.c#L398
25
+ # for more information
26
+ def encode(value)
27
+ _encode(value)
28
+ end
29
+
30
+ private
31
+
32
+ def _decode(stream)
33
+ quoted = 0
34
+ escaped = false
35
+ result = []
36
+ part = ''
37
+
38
+ # Always start getting the non-collection character, the second char
39
+ stream.getc if stream.pos == 0
40
+
41
+ # Check for an empty list
42
+ return result if %w[} )].include?(stream.getc)
43
+
44
+ # If it's not an empty list, return one position before iterating
45
+ stream.pos -= 1
46
+ stream.each_char do |c|
47
+
48
+ case
49
+ when quoted < 1
50
+ case
51
+ when c == DELIMITER, c == '}', c == ')'
52
+
53
+ unless escaped
54
+ # Non-quoted empty string or NULL as extense
55
+ part = nil if quoted == 0 && ( part.length == 0 || part == 'NULL' )
56
+ result << part
57
+ end
58
+
59
+ return result unless c == DELIMITER
60
+
61
+ escaped = false
62
+ quoted = 0
63
+ part = ''
64
+
65
+ when c == '"'
66
+ quoted = 1
67
+ when c == '{', c == '('
68
+ result << _decode(stream)
69
+ escaped = true
70
+ else
71
+ part << c
72
+ end
73
+ when escaped
74
+ escaped = false
75
+ part << c
76
+ when c == '\\'
77
+ escaped = true
78
+ when c == '"'
79
+ if stream.getc == '"'
80
+ part << c
81
+ else
82
+ stream.pos -= 1
83
+ quoted = -1
84
+ end
85
+ else
86
+ if ( c == '"' || c == "'" ) && stream.getc != c
87
+ stream.pos -= 1
88
+ quoted = -1
89
+ else
90
+ part << c
91
+ end
92
+ end
93
+
94
+ end
95
+ end
96
+
97
+ def _encode(list)
98
+ is_record = list.is_a?(Record)
99
+ list.map! do |part|
100
+ case part
101
+ when NilClass
102
+ is_record ? '' : 'NULL'
103
+ when Array
104
+ _encode(part)
105
+ else
106
+ _quote(part.to_s)
107
+ end
108
+ end
109
+
110
+ result = is_record ? '(%s)' : '{%s}'
111
+ result % list.join(DELIMITER)
112
+ end
113
+
114
+ def _quote(string)
115
+ len = string.length
116
+
117
+ # Fast results
118
+ return '""' if len == 0
119
+ return '"NULL"' if len == 4 && string == 'NULL'
120
+
121
+ # Check if the string don't need quotes
122
+ return string unless string =~ NEED_QUOTE_FOR
123
+
124
+ # Use the original string escape function
125
+ PG::Connection.escape_string(string).inspect
126
+ end
127
+
128
+ end
129
+
130
+ end
131
+ end
132
+ end
@@ -2,6 +2,8 @@ module Torque
2
2
  module PostgreSQL
3
3
  module Collector
4
4
 
5
+ # This classe helps to collect data in different ways. Used to configure
6
+ # auxiliary statements
5
7
  def self.new(*args)
6
8
  klass = Class.new
7
9
 
@@ -3,12 +3,26 @@ module Torque
3
3
  include ActiveSupport::Configurable
4
4
 
5
5
  # Allow nested configurations
6
+ # :TODO: Rely on +inheritable_copy+ to make nested configurations
6
7
  config.define_singleton_method(:nested) do |name, &block|
7
8
  klass = Class.new(ActiveSupport::Configurable::Configuration).new
8
9
  block.call(klass) if block
9
10
  send("#{name}=", klass)
10
11
  end
11
12
 
13
+ # Set if any information that requires querying and searching or collectiong
14
+ # information shuld be eager loaded. This automatically changes when rails
15
+ # same configuration is set to true
16
+ config.eager_load = false
17
+
18
+ # Set a list of irregular model name when associated with table names
19
+ config.irregular_models = {}
20
+ def config.irregular_models=(hash)
21
+ PostgreSQL.config[:irregular_models] = hash.map do |(table, model)|
22
+ [table.to_s, model.to_s]
23
+ end.to_h
24
+ end
25
+
12
26
  # Configure ENUM features
13
27
  config.nested(:enum) do |enum|
14
28
 
@@ -51,7 +65,27 @@ module Torque
51
65
 
52
66
  # Define the key that is used on auxiliary statements to send extra
53
67
  # arguments to format string or send on a proc
54
- cte.send_arguments_key = :uses
68
+ cte.send_arguments_key = :args
69
+
70
+ end
71
+
72
+ # Configure inheritance features
73
+ config.nested(:inheritance) do |inheritance|
74
+
75
+ # Define the lookup of models from their given name to be inverted, which
76
+ # means that they are going to be form the last namespaced one to the
77
+ # most namespaced one
78
+ inheritance.inverse_lookup = true
79
+
80
+ # Determines the name of the column used to collect the table of each
81
+ # record. When the table has inheritance tables, this column will return
82
+ # the name of the table that actually holds the record
83
+ inheritance.record_class_column_name = :_record_class
84
+
85
+ # Determines the name of the column used when identifying that the loaded
86
+ # records should be casted to its correctly model. This will be TRUE for
87
+ # the records mentioned on `cast_records`
88
+ inheritance.auto_cast_column_name = :_auto_cast
55
89
 
56
90
  end
57
91
 
@@ -0,0 +1,133 @@
1
+ module Torque
2
+ module PostgreSQL
3
+ InheritanceError = Class.new(ArgumentError)
4
+
5
+ module Inheritance
6
+ extend ActiveSupport::Concern
7
+
8
+ # Cast the given object to its correct class
9
+ def cast_record
10
+ record_class_value = send(self.class._record_class_attribute)
11
+
12
+ return self unless self.class.table_name != record_class_value
13
+ klass = self.class.casted_dependents[record_class_value]
14
+ self.class.raise_unable_to_cast(record_class_value) if klass.nil?
15
+
16
+ # The record need to be re-queried to have its attributes loaded
17
+ # :TODO: Improve this by only loading the necessary extra columns
18
+ klass.find(self.id)
19
+ end
20
+
21
+ private
22
+
23
+ def using_single_table_inheritance?(record) # :nodoc:
24
+ self.class.physically_inherited? || super
25
+ end
26
+
27
+ module ClassMethods
28
+
29
+ delegate :_auto_cast_attribute, :_record_class_attribute, to: ActiveRecord::Relation
30
+
31
+ # Manually set the model name associated with tables name in order to
32
+ # facilitates the identification of inherited records
33
+ def inherited(subclass)
34
+ super
35
+
36
+ return unless Torque::PostgreSQL.config.eager_load &&
37
+ !subclass.abstract_class?
38
+
39
+ connection.schema_cache.add_model_name(table_name, subclass)
40
+ end
41
+
42
+ # Get a full list of all attributes from a model and all its dependents
43
+ def inheritance_merged_attributes
44
+ @inheritance_merged_attributes ||= begin
45
+ list = attribute_names
46
+ list += casted_dependents.values.map(&:attribute_names)
47
+ list.flatten.to_set.freeze
48
+ end
49
+ end
50
+
51
+ # Check if the model's table depends on any inheritance
52
+ def physically_inherited?
53
+ @physically_inherited ||= connection.schema_cache.dependencies(
54
+ defined?(@table_name) ? @table_name : decorated_table_name,
55
+ ).present?
56
+ end
57
+
58
+ # Get the list of all tables directly or indirectly dependent of the
59
+ # current one
60
+ def inheritance_dependents
61
+ connection.schema_cache.associations(table_name) || []
62
+ end
63
+
64
+ # Check whether the model's table has directly or indirectly dependents
65
+ def physically_inheritances?
66
+ inheritance_dependents.present?
67
+ end
68
+
69
+ # Get the list of all ActiveRecord classes directly or indirectly
70
+ # associated by inheritance
71
+ def casted_dependents
72
+ @casted_dependents ||= inheritance_dependents.map do |table_name|
73
+ [table_name, connection.schema_cache.lookup_model(table_name)]
74
+ end.to_h
75
+ end
76
+
77
+ # Get the final decorated table, regardless of any special condition
78
+ def decorated_table_name
79
+ if parent < Base && !parent.abstract_class?
80
+ contained = parent.table_name
81
+ contained = contained.singularize if parent.pluralize_table_names
82
+ contained += "_"
83
+ end
84
+
85
+ "#{full_table_name_prefix}#{contained}#{undecorated_table_name(name)}#{full_table_name_suffix}"
86
+ end
87
+
88
+ # Add an additional check to return the name of the table even when the
89
+ # class is inherited, but only if it is a physical inheritance
90
+ def compute_table_name
91
+ return super unless physically_inherited?
92
+ decorated_table_name
93
+ end
94
+
95
+ # Raises an error message saying that the giver record class was not
96
+ # able to be casted since the model was not identified
97
+ def raise_unable_to_cast(record_class_value)
98
+ raise InheritanceError.new(<<~MSG.squish)
99
+ An record was not able to be casted to type '#{record_class_value}'.
100
+ If this table name doesn't represent a guessable model,
101
+ please use 'Torque::PostgreSQL.conf.irregular_models =
102
+ { '#{record_class_value}' => 'ModelName' }'.
103
+ MSG
104
+ end
105
+
106
+ private
107
+
108
+ def discriminate_class_for_record(record) # :nodoc:
109
+ auto_cast = _auto_cast_attribute.to_s
110
+ record_class = _record_class_attribute.to_s
111
+
112
+ return super unless record.key?(record_class) &&
113
+ record[auto_cast] === true && record[record_class] != table_name
114
+
115
+ klass = casted_dependents[record[record_class]]
116
+ raise_unable_to_cast(record[record_class]) if klass.nil?
117
+ filter_attributes_for_cast(record, klass)
118
+ klass
119
+ end
120
+
121
+ # Filter the record attributes to be loaded to not included those from
122
+ # another inherited dependent
123
+ def filter_attributes_for_cast(record, klass)
124
+ remove_attrs = (inheritance_merged_attributes - klass.attribute_names)
125
+ record.reject!{ |attribute| remove_attrs.include?(attribute) }
126
+ end
127
+
128
+ end
129
+ end
130
+
131
+ ActiveRecord::Base.include Inheritance
132
+ end
133
+ end
@@ -0,0 +1,16 @@
1
+ module Torque
2
+ module PostgreSQL
3
+ # = Torque PostgreSQL Railtie
4
+ class Railtie < Rails::Railtie # :nodoc:
5
+
6
+ # Eger load PostgreSQL namespace
7
+ config.eager_load_namespaces << Torque::PostgreSQL
8
+
9
+ # Get information from the running rails app
10
+ runner do |app|
11
+ Torque::PostgreSQL.config.eager_load = app.config.eager_load
12
+ end
13
+
14
+ end
15
+ end
16
+ end
@@ -3,7 +3,10 @@ module Torque
3
3
  module Relation
4
4
  module AuxiliaryStatement
5
5
 
6
- attr_accessor :auxiliary_statements
6
+ # :nodoc:
7
+ def auxiliary_statements_values; get_value(:auxiliary_statements); end
8
+ # :nodoc:
9
+ def auxiliary_statements_values=(value); set_value(:auxiliary_statements, value); end
7
10
 
8
11
  # Set use of an auxiliary statement already configurated on the model
9
12
  def with(*args)
@@ -13,46 +16,50 @@ module Torque
13
16
  # Like #with, but modifies relation in place.
14
17
  def with!(*args)
15
18
  options = args.extract_options!
16
- self.auxiliary_statements ||= {}
17
19
  args.each do |table|
18
- instance = instantiate(table, self, options)
20
+ instance = table.is_a?(PostgreSQL::AuxiliaryStatement) \
21
+ ? table.class.new(options) \
22
+ : PostgreSQL::AuxiliaryStatement.instantiate(table, self, options)
19
23
  instance.ensure_dependencies!(self)
20
- self.auxiliary_statements[table] = instance
24
+ self.auxiliary_statements_values |= [instance]
21
25
  end
22
26
 
23
27
  self
24
28
  end
25
29
 
30
+ alias_method :auxiliary_statements, :with
31
+ alias_method :auxiliary_statements!, :with!
32
+
26
33
  # Get all auxiliary statements bound attributes and the base bound
27
34
  # attributes as well
28
35
  def bound_attributes
29
- return super unless self.auxiliary_statements.present?
30
- bindings = self.auxiliary_statements.values.map(&:bound_attributes)
36
+ return super unless self.auxiliary_statements_values.present?
37
+ bindings = self.auxiliary_statements_values.map(&:bound_attributes)
31
38
  (bindings + super).flatten
32
39
  end
33
40
 
34
41
  private
35
- delegate :instantiate, to: PostgreSQL::AuxiliaryStatement
36
42
 
37
43
  # Hook arel build to add the distinct on clause
38
44
  def build_arel
39
45
  arel = super
46
+ build_auxiliary_statements(arel)
47
+ arel
48
+ end
40
49
 
41
- if self.auxiliary_statements.present?
42
- columns = []
43
- subqueries = self.auxiliary_statements.values.map do |klass|
44
- columns << klass.columns
45
- klass.build_arel(arel, self)
46
- end
50
+ # Build all necessary data for auxiliary statements
51
+ def build_auxiliary_statements(arel)
52
+ return unless self.auxiliary_statements_values.present?
47
53
 
48
- arel.with(subqueries.flatten)
49
- if select_values.empty? && columns.any?
50
- columns.unshift table[::Arel.star]
51
- arel.projections = columns
52
- end
54
+ columns = []
55
+ subqueries = self.auxiliary_statements_values.map do |klass|
56
+ columns << klass.columns
57
+ klass.build_arel(arel, self)
53
58
  end
54
59
 
55
- arel
60
+ columns.flatten!
61
+ arel.with(subqueries.flatten)
62
+ arel.project(*columns) if columns.any?
56
63
  end
57
64
 
58
65
  # Throw an error showing that an auxiliary statement of the given
@@ -3,10 +3,13 @@ module Torque
3
3
  module Relation
4
4
  module DistinctOn
5
5
 
6
- attr_accessor :distinct_on_value
6
+ # :nodoc:
7
+ def distinct_on_values; get_value(:distinct_on); end
8
+ # :nodoc:
9
+ def distinct_on_values=(value); set_value(:distinct_on, value); end
7
10
 
8
- # Specifies whether the records should be unique or not by a given set of fields.
9
- # For example:
11
+ # Specifies whether the records should be unique or not by a given set
12
+ # of fields. For example:
10
13
  #
11
14
  # User.distinct_on(:name)
12
15
  # # Returns 1 record per distinct name
@@ -22,7 +25,7 @@ module Torque
22
25
 
23
26
  # Like #distinct_on, but modifies relation in place.
24
27
  def distinct_on!(*value)
25
- self.distinct_on_value = value
28
+ self.distinct_on_values = value
26
29
  self
27
30
  end
28
31
 
@@ -31,9 +34,8 @@ module Torque
31
34
  # Hook arel build to add the distinct on clause
32
35
  def build_arel
33
36
  arel = super
34
-
35
- value = self.distinct_on_value
36
- arel.distinct_on(resolve_column(value)) unless value.nil?
37
+ value = self.distinct_on_values
38
+ arel.distinct_on(resolve_column(value)) if value.present?
37
39
  arel
38
40
  end
39
41
 
@@ -0,0 +1,112 @@
1
+ module Torque
2
+ module PostgreSQL
3
+ module Relation
4
+ module Inheritance
5
+
6
+ # :nodoc:
7
+ def cast_records_value; get_value(:cast_records); end
8
+ # :nodoc:
9
+ def cast_records_value=(value); set_value(:cast_records, value); end
10
+
11
+ # :nodoc:
12
+ def itself_only_value; get_value(:itself_only); end
13
+ # :nodoc:
14
+ def itself_only_value=(value); set_value(:itself_only, value); end
15
+
16
+ delegate :quote_table_name, :quote_column_name, to: :connection
17
+
18
+ # Specify that the results should come only from the table that the
19
+ # entries were created on. For example:
20
+ #
21
+ # Activity.itself_only
22
+ # # Does not return entries for inherited tables
23
+ def itself_only
24
+ spawn.itself_only!
25
+ end
26
+
27
+ # Like #itself_only, but modifies relation in place.
28
+ def itself_only!(*)
29
+ self.itself_only_value = true
30
+ self
31
+ end
32
+
33
+ # Enables the casting of all returned records. The result will include
34
+ # all the information needed to instantiate the inherited models
35
+ #
36
+ # Activity.cast_records
37
+ # # The result list will have many different classes, for all
38
+ # # inherited models of activities
39
+ def cast_records(*types, **options)
40
+ spawn.cast_records!(*types, **options)
41
+ end
42
+
43
+ # Like #cast_records, but modifies relation in place
44
+ def cast_records!(*types, **options)
45
+ record_class = self.class._record_class_attribute
46
+
47
+ with!(record_class)
48
+ where!(record_class => types.map(&:table_name)) if options[:filter]
49
+
50
+ self.cast_records_value = (types.present? ? types : model.casted_dependents.values)
51
+ self
52
+ end
53
+
54
+ private
55
+
56
+ # Hook arel build to add any necessary table
57
+ def build_arel
58
+ arel = super
59
+ arel.only if self.itself_only_value === true
60
+ build_inheritances(arel)
61
+ arel
62
+ end
63
+
64
+ # Build all necessary data for inheritances
65
+ def build_inheritances(arel)
66
+ return unless self.cast_records_value.present?
67
+
68
+ columns = build_inheritances_joins(arel, self.cast_records_value)
69
+ columns = columns.map do |column, arel_tables|
70
+ next arel_tables.first[column] if arel_tables.size == 1
71
+ list = arel_tables.each_with_object(column).map(&:[])
72
+ ::Arel::Nodes::NamedFunction.new('COALESCE', list).as(column)
73
+ end
74
+
75
+ columns.push(build_auto_caster_marker(arel, self.cast_records_value))
76
+ arel.project(*columns) if columns.any?
77
+ end
78
+
79
+ # Build as many left outer join as necessary for each dependent table
80
+ def build_inheritances_joins(arel, types)
81
+ columns = Hash.new{ |h, k| h[k] = [] }
82
+ primary_key = quoted_primary_key
83
+ base_attributes = model.attribute_names
84
+
85
+ # Iterate over each casted dependent calculating the columns
86
+ types.each.with_index do |model, idx|
87
+ join_table = model.arel_table.alias("\"i_#{idx}\"")
88
+ arel.outer_join(join_table).using(primary_key)
89
+ (model.attribute_names - base_attributes).each do |column|
90
+ columns[column] << join_table
91
+ end
92
+ end
93
+
94
+ # Return the list of needed columns
95
+ columns.default_proc = nil
96
+ columns
97
+ end
98
+
99
+ def build_auto_caster_marker(arel, types)
100
+ types = types.map(&:table_name)
101
+ type_attribute = self.class._record_class_attribute.to_s
102
+ auto_cast_attribute = self.class._auto_cast_attribute.to_s
103
+
104
+ table = ::Arel::Table.new(type_attribute.camelize.underscore)
105
+ column = table[type_attribute].in(types)
106
+ ::Arel::Nodes::SqlLiteral.new(column.to_sql).as(auto_cast_attribute)
107
+ end
108
+
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,48 @@
1
+ module Torque
2
+ module PostgreSQL
3
+ module Relation
4
+ module Merger
5
+
6
+ def merge # :nodoc:
7
+ super
8
+
9
+ merge_distinct_on
10
+ merge_auxiliary_statements
11
+ merge_inheritance
12
+
13
+ relation
14
+ end
15
+
16
+ private
17
+
18
+ # Merge distinct on columns
19
+ def merge_distinct_on
20
+ return if other.distinct_on_values.blank?
21
+ relation.distinct_on_values += other.distinct_on_values
22
+ end
23
+
24
+ # Merge auxiliary statements activated by +with+
25
+ def merge_auxiliary_statements
26
+ return if other.auxiliary_statements_values.blank?
27
+
28
+ current = relation.auxiliary_statements_values.map{ |cte| cte.class }
29
+ other.auxiliary_statements_values.each do |other|
30
+ next if current.include?(other.class)
31
+ relation.auxiliary_statements_values += [other]
32
+ current << other.class
33
+ end
34
+ end
35
+
36
+ # Merge settings related to inheritance tables
37
+ def merge_inheritance
38
+ relation.itself_only_value = true if other.itself_only_value
39
+ relation.cast_records_value.concat(other.cast_records_value).uniq! \
40
+ if other.cast_records_value.present?
41
+ end
42
+
43
+ end
44
+
45
+ ActiveRecord::Relation::Merger.prepend Merger
46
+ end
47
+ end
48
+ end