torque-postgresql 0.1.7 → 0.2.1

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