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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/Rakefile +31 -0
- data/lib/torque/postgresql/adapter/database_statements.rb +103 -0
- data/lib/torque/postgresql/adapter/oid/array.rb +19 -0
- data/lib/torque/postgresql/adapter/oid/enum.rb +46 -0
- data/lib/torque/postgresql/adapter/oid/interval.rb +94 -0
- data/lib/torque/postgresql/adapter/oid.rb +15 -0
- data/lib/torque/postgresql/adapter/quoting.rb +23 -0
- data/lib/torque/postgresql/adapter/schema_definitions.rb +28 -0
- data/lib/torque/postgresql/adapter/schema_dumper.rb +31 -0
- data/lib/torque/postgresql/adapter/schema_statements.rb +89 -0
- data/lib/torque/postgresql/adapter.rb +29 -0
- data/lib/torque/postgresql/attributes/builder/enum.rb +151 -0
- data/lib/torque/postgresql/attributes/builder.rb +1 -0
- data/lib/torque/postgresql/attributes/enum.rb +231 -0
- data/lib/torque/postgresql/attributes/lazy.rb +33 -0
- data/lib/torque/postgresql/attributes/type_map.rb +46 -0
- data/lib/torque/postgresql/attributes.rb +32 -0
- data/lib/torque/postgresql/auxiliary_statement.rb +192 -0
- data/lib/torque/postgresql/base.rb +28 -0
- data/lib/torque/postgresql/collector.rb +31 -0
- data/lib/torque/postgresql/config.rb +50 -0
- data/lib/torque/postgresql/migration/command_recorder.rb +31 -0
- data/lib/torque/postgresql/migration.rb +1 -0
- data/lib/torque/postgresql/relation/auxiliary_statement.rb +65 -0
- data/lib/torque/postgresql/relation/distinct_on.rb +43 -0
- data/lib/torque/postgresql/relation.rb +62 -0
- data/lib/torque/postgresql/schema_dumper.rb +37 -0
- data/lib/torque/postgresql/version.rb +5 -0
- data/lib/torque/postgresql.rb +18 -0
- data/lib/torque-postgresql.rb +1 -0
- 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
|