torque-postgresql 0.1.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 (33) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/Rakefile +31 -0
  4. data/lib/torque/postgresql/adapter/database_statements.rb +103 -0
  5. data/lib/torque/postgresql/adapter/oid/array.rb +19 -0
  6. data/lib/torque/postgresql/adapter/oid/enum.rb +46 -0
  7. data/lib/torque/postgresql/adapter/oid/interval.rb +94 -0
  8. data/lib/torque/postgresql/adapter/oid.rb +15 -0
  9. data/lib/torque/postgresql/adapter/quoting.rb +23 -0
  10. data/lib/torque/postgresql/adapter/schema_definitions.rb +28 -0
  11. data/lib/torque/postgresql/adapter/schema_dumper.rb +31 -0
  12. data/lib/torque/postgresql/adapter/schema_statements.rb +89 -0
  13. data/lib/torque/postgresql/adapter.rb +29 -0
  14. data/lib/torque/postgresql/attributes/builder/enum.rb +151 -0
  15. data/lib/torque/postgresql/attributes/builder.rb +1 -0
  16. data/lib/torque/postgresql/attributes/enum.rb +231 -0
  17. data/lib/torque/postgresql/attributes/lazy.rb +33 -0
  18. data/lib/torque/postgresql/attributes/type_map.rb +46 -0
  19. data/lib/torque/postgresql/attributes.rb +32 -0
  20. data/lib/torque/postgresql/auxiliary_statement.rb +192 -0
  21. data/lib/torque/postgresql/base.rb +28 -0
  22. data/lib/torque/postgresql/collector.rb +31 -0
  23. data/lib/torque/postgresql/config.rb +50 -0
  24. data/lib/torque/postgresql/migration/command_recorder.rb +31 -0
  25. data/lib/torque/postgresql/migration.rb +1 -0
  26. data/lib/torque/postgresql/relation/auxiliary_statement.rb +65 -0
  27. data/lib/torque/postgresql/relation/distinct_on.rb +43 -0
  28. data/lib/torque/postgresql/relation.rb +62 -0
  29. data/lib/torque/postgresql/schema_dumper.rb +37 -0
  30. data/lib/torque/postgresql/version.rb +5 -0
  31. data/lib/torque/postgresql.rb +18 -0
  32. data/lib/torque-postgresql.rb +1 -0
  33. metadata +236 -0
@@ -0,0 +1,46 @@
1
+ module Torque
2
+ module PostgreSQL
3
+ module Attributes
4
+ module TypeMap
5
+
6
+ class << self
7
+
8
+ # Reader of the list of tyes
9
+ def types
10
+ @types ||= {}
11
+ end
12
+
13
+ # Register a type that can be processed by a given block
14
+ def register_type(key, &block)
15
+ raise_type_defined(key) if present?(key)
16
+ types[key] = block
17
+ end
18
+
19
+ # Search for a type match and process it if any
20
+ def lookup(key, klass, *args)
21
+ return unless present?(key)
22
+ klass.instance_exec(key, *args, &types[key.class])
23
+ rescue LocalJumpError
24
+ # There's a bug or misbehavior that blocks being called through
25
+ # instance_exec don't accept neither return nor break
26
+ return false
27
+ end
28
+
29
+ # Check if the given type class is registered
30
+ def present?(key)
31
+ types.key?(key.class)
32
+ end
33
+
34
+ # Message when trying to define multiple types
35
+ def raise_type_defined(key)
36
+ raise ArgumentError, <<-MSG.strip
37
+ Type #{key} is already defined here: #{types[key].source_location.join(':')}
38
+ MSG
39
+ end
40
+
41
+ end
42
+
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,32 @@
1
+
2
+ require_relative 'attributes/type_map'
3
+ require_relative 'attributes/lazy'
4
+
5
+ require_relative 'attributes/builder'
6
+
7
+ require_relative 'attributes/enum'
8
+
9
+ module Torque
10
+ module PostgreSQL
11
+ module Attributes
12
+ extend ActiveSupport::Concern
13
+
14
+ included do
15
+ class_attribute :enum_save_on_bang, instance_accessor: true
16
+ self.enum_save_on_bang = Torque::PostgreSQL.config.enum.save_on_bang
17
+ end
18
+
19
+ module ClassMethods
20
+ private
21
+
22
+ def define_attribute_method(attribute)
23
+ type = attribute_types[attribute]
24
+ super unless TypeMap.lookup(type, self, attribute, true)
25
+ end
26
+
27
+ end
28
+ end
29
+
30
+ ActiveRecord::Base.include Attributes
31
+ end
32
+ end
@@ -0,0 +1,192 @@
1
+ module Torque
2
+ module PostgreSQL
3
+ class AuxiliaryStatement
4
+
5
+ # The settings collector class
6
+ Settings = Collector.new(:attributes, :join, :join_type, :query)
7
+
8
+ class << self
9
+ # These attributes require that the class is setup
10
+ #
11
+ # The attributes separation means
12
+ # exposed_attributes -> Will be projected to the main query
13
+ # selected_attributes -> Will be selected on the configurated query
14
+ # join_attributes -> Will be used to join the the queries
15
+ [:exposed_attributes, :selected_attributes, :join_attributes, :table, :query,
16
+ :join_type].each do |attribute|
17
+ define_method(attribute) do
18
+ setup
19
+ instance_variable_get("@#{attribute}")
20
+ end
21
+ end
22
+
23
+ # Find or create the class that will handle statement
24
+ def lookup(name, base)
25
+ const = name.to_s.camelize << '_' << self.name.demodulize
26
+ return base.const_get(const) if base.const_defined?(const)
27
+ base.const_set(const, Class.new(AuxiliaryStatement))
28
+ end
29
+
30
+ # Set a configuration block, if the class is already set up, just clean
31
+ # the query and wait it to be setup again
32
+ def configurator(block)
33
+ @config = block
34
+ @query = nil if setup?
35
+ end
36
+
37
+ # Get the base class associated to this statement
38
+ def base
39
+ self.parent
40
+ end
41
+
42
+ # Get the arel table of the base class
43
+ def base_table
44
+ base.arel_table
45
+ end
46
+
47
+ # Get the arel table of the query
48
+ def query_table
49
+ query.arel_table
50
+ end
51
+
52
+ private
53
+ # Just setup the class if it's not setup
54
+ def setup
55
+ setup! unless setup?
56
+ end
57
+
58
+ # Check if the class is setup
59
+ def setup?
60
+ defined?(@query) && @query
61
+ end
62
+
63
+ # Setup the class
64
+ def setup!
65
+ # attributes key
66
+ # Provides a map of attributes to be exposed to the main query.
67
+ #
68
+ # For instace, if the statement query has an 'id' column that you
69
+ # want it to be accessed on the main query as 'item_id',
70
+ # you can use:
71
+ # attributes id: :item_id
72
+ #
73
+ # If its statement has more tables, and you want to expose those
74
+ # fields, then:
75
+ # attributes 'table.name': :item_name
76
+ #
77
+ # join_type key
78
+ # Changes the type of the join and set the constraints
79
+ #
80
+ # The left side of the hash is the source table column, the right
81
+ # side is the statement table column, now it's only accepting '='
82
+ # constraints
83
+ # join id: :user_id
84
+ # join id: :'user.id'
85
+ # join 'post.id': :'user.last_post_id'
86
+ #
87
+ # It's possible to change the default type of join
88
+ # join :left, id: :user_id
89
+ #
90
+ # join key
91
+ # Changes the type of the join
92
+ #
93
+ # query key
94
+ # Save the query command to be performand
95
+ settings = Settings.new
96
+ @config.call(settings)
97
+
98
+ table_name = self.name.demodulize.split('_').first.underscore
99
+ @join_type = settings.join_type || :inner
100
+ @table = Arel::Table.new(table_name)
101
+ @query = settings.query
102
+
103
+ @selected_attributes = []
104
+ @exposed_attributes = []
105
+ @join_attributes = []
106
+
107
+ # Iterate the attributes settings
108
+ # Attributes (left => right)
109
+ # left -> query.selected_attributes AS right
110
+ # right -> table.exposed_attributes
111
+ settings.attributes.each do |left, right|
112
+ @exposed_attributes << project(right)
113
+ @selected_attributes << project(left, query_table).as(right.to_s)
114
+ end
115
+
116
+ # Iterate the join settings
117
+ # Join (left => right)
118
+ # left -> base.join_attributes.eq(right)
119
+ # right -> table.selected_attributes
120
+ if settings.join.nil?
121
+ check_auto_join
122
+ else
123
+ settings.join.each do |left, right|
124
+ @selected_attributes << project(right, query_table)
125
+ @join_attributes << project(left, base_table).eq(project(right))
126
+ end
127
+ end
128
+ end
129
+
130
+ # Check if it's possible to identify the connection between the main
131
+ # query and the statement query
132
+ def check_auto_join
133
+ foreign_key = base.name.foreign_key
134
+ if query.columns_hash.key?(foreign_key)
135
+ primary_key = project(base.primary_key, base_table)
136
+ @selected_attributes << project(foreign_key, query_table)
137
+ @join_attributes << primary_key.eq(project(foreign_key))
138
+ end
139
+ end
140
+
141
+ # Project a column on a given table, or use the column table
142
+ def project(column, table = @table)
143
+ if column.to_s.include?('.')
144
+ table, column = column.split('.')
145
+ table = Arel::Table.new(table)
146
+ end
147
+
148
+ table[column]
149
+ end
150
+ end
151
+
152
+ # Start a new auxiliary statement giving extra options
153
+ def initialize(*args)
154
+ @options = args.extract_options!
155
+ end
156
+
157
+ # Get the columns that will be selected for this statement
158
+ def columns
159
+ self.class.exposed_attributes
160
+ end
161
+
162
+ # Build the statement on the given arel and return the WITH statement
163
+ def build_arel(arel)
164
+ klass = self.class
165
+ query = klass.query.select(*klass.selected_attributes)
166
+
167
+ # Build the join for this statement
168
+ arel.join(klass.table, arel_join).on(*klass.join_attributes)
169
+
170
+ # Return the subquery for this statement
171
+ Arel::Nodes::As.new(klass.table, query.send(:build_arel))
172
+ end
173
+
174
+ private
175
+
176
+ # Get the class of the join on arel
177
+ def arel_join
178
+ case @options.fetch(:join_type, self.class.join_type)
179
+ when :inner then Arel::Nodes::InnerJoin
180
+ when :left then Arel::Nodes::OuterJoin
181
+ when :right then Arel::Nodes::RightOuterJoin
182
+ when :full then Arel::Nodes::FullOuterJoin
183
+ else
184
+ raise ArgumentError, <<-MSG.gsub(/^ +| +$|\n/, '')
185
+ The '#{@join_type}' is not implemented as a join type.
186
+ MSG
187
+ end
188
+ end
189
+
190
+ end
191
+ end
192
+ end
@@ -0,0 +1,28 @@
1
+ module Torque
2
+ module PostgreSQL
3
+ module Base
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ class_attribute :auxiliary_statements_list, instance_accessor: true
8
+ self.auxiliary_statements_list = {}
9
+ end
10
+
11
+ module ClassMethods
12
+ delegate :distinct_on, :with, to: :all
13
+
14
+ protected
15
+
16
+ # Creates a new auxiliary statement (CTE) under the base class
17
+ def auxiliary_statement(table, &block)
18
+ klass = AuxiliaryStatement.lookup(table, self)
19
+ auxiliary_statements_list[table.to_sym] = klass
20
+ klass.configurator(block)
21
+ end
22
+ alias cte auxiliary_statement
23
+ end
24
+ end
25
+
26
+ ActiveRecord::Base.include Base
27
+ end
28
+ end
@@ -0,0 +1,31 @@
1
+ module Torque
2
+ module PostgreSQL
3
+ module Collector
4
+
5
+ def self.new(*args)
6
+ klass = Class.new
7
+
8
+ args.flatten!
9
+ args.compact!
10
+
11
+ klass.module_eval do
12
+ args.each do |attribute|
13
+ define_method attribute do |*args|
14
+ if args.empty?
15
+ instance_variable_get("@#{attribute}")
16
+ elsif args.size > 1
17
+ instance_variable_set("@#{attribute}", args)
18
+ else
19
+ instance_variable_set("@#{attribute}", args.first)
20
+ end
21
+ end
22
+ alias_method "#{attribute}=", attribute
23
+ end
24
+ end
25
+
26
+ klass
27
+ end
28
+
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,50 @@
1
+ module Torque
2
+ module PostgreSQL
3
+ include ActiveSupport::Configurable
4
+
5
+ # Allow nested configurations
6
+ config.define_singleton_method(:nested) do |name, &block|
7
+ klass = Class.new(ActiveSupport::Configurable::Configuration).new
8
+ block.call(klass) if block
9
+ send("#{name}=", klass)
10
+ end
11
+
12
+ # Configure ENUM features
13
+ config.nested(:enum) do |enum|
14
+
15
+ # Indicates if the enum features on ActiveRecord::Base should be initiated
16
+ # automatically or not
17
+ enum.initializer = false
18
+
19
+ # The name of the method to be used on any ActiveRecord::Base to
20
+ # initialize model-based enum features
21
+ enum.base_method = :enum
22
+
23
+ # Indicates if bang methods like 'disabled!' should update the record on
24
+ # database or not
25
+ enum.save_on_bang = true
26
+
27
+ # Specify the namespace of each enum type of value
28
+ enum.namespace = ::Object.const_set('Enum', Module.new)
29
+
30
+ # Specify the scopes for I18n translations
31
+ enum.i18n_scopes = [
32
+ 'activerecord.attributes.%{model}.%{attr}.%{value}',
33
+ 'activerecord.attributes.%{attr}.%{value}',
34
+ 'activerecord.enums.%{type}.%{value}',
35
+ 'enum.%{type}.%{value}',
36
+ 'enum.%{value}'
37
+ ]
38
+
39
+ # Specify the scopes for I18n translations but with type only
40
+ enum.i18n_type_scopes = Enumerator.new do |yielder|
41
+ enum.i18n_scopes.each do |key|
42
+ next if key.include?('%{model}') || key.include?('%{attr}')
43
+ yielder << key
44
+ end
45
+ end
46
+
47
+ end
48
+
49
+ end
50
+ end
@@ -0,0 +1,31 @@
1
+ module Torque
2
+ module PostgreSQL
3
+ module Migration
4
+ module CommandRecorder
5
+
6
+ # Records the rename operation for types.
7
+ def rename_type(*args, &block)
8
+ record(:rename_type, args, &block)
9
+ end
10
+
11
+ # Inverts the type name.
12
+ def invert_rename_type(args)
13
+ [:rename_type, args.reverse]
14
+ end
15
+
16
+ # Records the creation of the enum to be reverted.
17
+ def create_enum(*args, &block)
18
+ record(:create_enum, args, &block)
19
+ end
20
+
21
+ # Inverts the creation of the enum.
22
+ def invert_create_enum(args)
23
+ [:drop_type, [args.first]]
24
+ end
25
+
26
+ end
27
+
28
+ ActiveRecord::Migration::CommandRecorder.include CommandRecorder
29
+ end
30
+ end
31
+ end
@@ -0,0 +1 @@
1
+ require_relative 'migration/command_recorder'
@@ -0,0 +1,65 @@
1
+ module Torque
2
+ module PostgreSQL
3
+ module Relation
4
+ module AuxiliaryStatement
5
+
6
+ attr_accessor :auxiliary_statements
7
+
8
+ # Set use of an auxiliary statement already configurated on the model
9
+ def with(*args)
10
+ spawn.with!(*args)
11
+ end
12
+
13
+ # Like #with, but modifies relation in place.
14
+ def with!(*args)
15
+ options = args.extract_options!
16
+ self.auxiliary_statements ||= []
17
+ args.each do |table|
18
+ unless self.auxiliary_statements_list.key?(table)
19
+ raise ArgumentError, <<-MSG.gsub(/^ +| +$|\n/, '')
20
+ There's no '#{table}' auxiliary statement defined for #{self.class.name}.
21
+ MSG
22
+ end
23
+
24
+ klass = self.auxiliary_statements_list[table]
25
+ self.auxiliary_statements << klass.new(options)
26
+ end
27
+ self
28
+ end
29
+
30
+ private
31
+
32
+ # Hook arel build to add the distinct on clause
33
+ def build_arel
34
+ arel = super
35
+
36
+ if self.auxiliary_statements.present?
37
+ columns = []
38
+ subqueries = self.auxiliary_statements.map do |klass|
39
+ columns << klass.columns
40
+ klass.build_arel(arel)
41
+ end
42
+
43
+ arel.with(subqueries)
44
+ if select_values.empty? && columns.any?
45
+ columns.unshift table[Arel.sql('*')]
46
+ arel.projections = columns
47
+ end
48
+ end
49
+
50
+ arel
51
+ end
52
+
53
+ # Throw an error showing that an auxiliary statement of the given
54
+ # table name isn't defined
55
+ def auxiliary_statement_error(name)
56
+ raise ArgumentError, <<-MSG.gsub(/^ +| +$|\n/, '')
57
+ There's no '#{name}' auxiliary statement defined for
58
+ #{self.class.name}.
59
+ MSG
60
+ end
61
+
62
+ end
63
+ end
64
+ end
65
+ end