torque-postgresql 0.1.0

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