torque-postgresql 4.0.0.rc1 → 4.0.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 +4 -4
- data/lib/generators/torque/function_generator.rb +13 -0
- data/lib/generators/torque/templates/function.sql.erb +4 -0
- data/lib/generators/torque/templates/type.sql.erb +2 -0
- data/lib/generators/torque/templates/view.sql.erb +3 -0
- data/lib/generators/torque/type_generator.rb +13 -0
- data/lib/generators/torque/view_generator.rb +16 -0
- data/lib/torque/postgresql/adapter/database_statements.rb +48 -10
- data/lib/torque/postgresql/adapter/schema_definitions.rb +22 -0
- data/lib/torque/postgresql/adapter/schema_dumper.rb +47 -1
- data/lib/torque/postgresql/adapter/schema_statements.rb +45 -0
- data/lib/torque/postgresql/arel/nodes.rb +14 -0
- data/lib/torque/postgresql/arel/visitors.rb +4 -0
- data/lib/torque/postgresql/attributes/builder/full_text_search.rb +16 -28
- data/lib/torque/postgresql/base.rb +2 -1
- data/lib/torque/postgresql/config.rb +35 -1
- data/lib/torque/postgresql/function.rb +33 -0
- data/lib/torque/postgresql/railtie.rb +26 -1
- data/lib/torque/postgresql/relation/auxiliary_statement.rb +7 -2
- data/lib/torque/postgresql/relation/buckets.rb +124 -0
- data/lib/torque/postgresql/relation/distinct_on.rb +7 -2
- data/lib/torque/postgresql/relation/inheritance.rb +18 -8
- data/lib/torque/postgresql/relation/join_series.rb +112 -0
- data/lib/torque/postgresql/relation/merger.rb +17 -3
- data/lib/torque/postgresql/relation.rb +18 -28
- data/lib/torque/postgresql/version.rb +1 -1
- data/lib/torque/postgresql/versioned_commands/command_migration.rb +146 -0
- data/lib/torque/postgresql/versioned_commands/generator.rb +57 -0
- data/lib/torque/postgresql/versioned_commands/migration_context.rb +83 -0
- data/lib/torque/postgresql/versioned_commands/migrator.rb +39 -0
- data/lib/torque/postgresql/versioned_commands/schema_table.rb +101 -0
- data/lib/torque/postgresql/versioned_commands.rb +161 -0
- data/spec/fixtures/migrations/20250101000001_create_users.rb +0 -0
- data/spec/fixtures/migrations/20250101000002_create_function_count_users_v1.sql +0 -0
- data/spec/fixtures/migrations/20250101000003_create_internal_users.rb +0 -0
- data/spec/fixtures/migrations/20250101000004_update_function_count_users_v2.sql +0 -0
- data/spec/fixtures/migrations/20250101000005_create_view_all_users_v1.sql +0 -0
- data/spec/fixtures/migrations/20250101000006_create_type_user_id_v1.sql +0 -0
- data/spec/fixtures/migrations/20250101000007_remove_function_count_users_v2.sql +0 -0
- data/spec/initialize.rb +9 -0
- data/spec/schema.rb +2 -4
- data/spec/spec_helper.rb +6 -1
- data/spec/tests/full_text_seach_test.rb +30 -2
- data/spec/tests/relation_spec.rb +229 -0
- data/spec/tests/schema_spec.rb +4 -1
- data/spec/tests/versioned_commands_spec.rb +513 -0
- metadata +33 -3
@@ -0,0 +1,124 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Torque
|
4
|
+
module PostgreSQL
|
5
|
+
module Relation
|
6
|
+
module Buckets
|
7
|
+
|
8
|
+
# :nodoc:
|
9
|
+
def buckets_value
|
10
|
+
@values.fetch(:buckets, nil)
|
11
|
+
end
|
12
|
+
# :nodoc:
|
13
|
+
def buckets_value=(value)
|
14
|
+
assert_modifiable!
|
15
|
+
@values[:buckets] = value
|
16
|
+
end
|
17
|
+
|
18
|
+
# Specifies how to bucket records. It works for both the calculations
|
19
|
+
# or just putting records into groups. For example:
|
20
|
+
#
|
21
|
+
# User.buckets(:created_at, [1.year.ago, 1.month.ago, 1.week.ago])
|
22
|
+
# # Returns all users grouped by created_at in the given time ranges
|
23
|
+
#
|
24
|
+
# User.buckets(:age, 0..100, step: 10).count
|
25
|
+
# # Counts all users grouped by age buckets of 10 years
|
26
|
+
def buckets(*value, **xargs)
|
27
|
+
spawn.buckets!(*value, **xargs)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Like #buckets, but modifies relation in place.
|
31
|
+
def buckets!(attribute, values, count: nil, cast: nil, as: nil)
|
32
|
+
raise ArgumentError, <<~MSG.squish if !values.is_a?(Array) && !values.is_a?(Range)
|
33
|
+
Buckets must be an array or a range.
|
34
|
+
MSG
|
35
|
+
|
36
|
+
count ||= 1 if values.is_a?(Range)
|
37
|
+
attribute = arel_table[attribute] unless ::Arel.arel_node?(attribute)
|
38
|
+
self.buckets_value = [attribute, values, count, cast, as]
|
39
|
+
self
|
40
|
+
end
|
41
|
+
|
42
|
+
# When performing calculations with buckets, this method add a grouping
|
43
|
+
# clause to the query by the bucket values, and then adjust the keys
|
44
|
+
# to match provided values
|
45
|
+
def calculate(*)
|
46
|
+
return super if buckets_value.blank?
|
47
|
+
|
48
|
+
raise ArgumentError, <<~MSG.squish if group_values.present?
|
49
|
+
Cannot calculate with buckets when there are already group values.
|
50
|
+
MSG
|
51
|
+
|
52
|
+
keys = buckets_keys
|
53
|
+
self.group_values = [FN.group_by(build_buckets_node, :bucket)]
|
54
|
+
super.transform_keys { |key| keys[key - 1] }
|
55
|
+
end
|
56
|
+
|
57
|
+
module Initializer
|
58
|
+
# Hook into the output of records to make sure we group by the buckets
|
59
|
+
def records
|
60
|
+
return super if buckets_value.blank?
|
61
|
+
|
62
|
+
keys = buckets_keys
|
63
|
+
col = buckets_column
|
64
|
+
super.group_by do |record|
|
65
|
+
val = (record[col] || 0) - 1
|
66
|
+
keys[val] if val >= 0 && val < keys.size
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
# Hook arel build to add the column
|
74
|
+
def build_arel(*)
|
75
|
+
return super if buckets_value.blank? || select_values.present?
|
76
|
+
|
77
|
+
self.select_extra_values += [build_buckets_node.as(buckets_column)]
|
78
|
+
super
|
79
|
+
end
|
80
|
+
|
81
|
+
# Build the Arel node for the buckets function
|
82
|
+
def build_buckets_node
|
83
|
+
attribute, values, count, cast, * = buckets_value
|
84
|
+
|
85
|
+
if values.is_a?(Range)
|
86
|
+
FN.width_bucket(
|
87
|
+
attribute,
|
88
|
+
FN.bind_type(values.begin, name: 'bucket_start', cast: 'numeric'),
|
89
|
+
FN.bind_type(values.end, name: 'bucket_end', cast: 'numeric'),
|
90
|
+
FN.bind_type(count, name: 'bucket_count', cast: 'integer'),
|
91
|
+
)
|
92
|
+
else
|
93
|
+
FN.width_bucket(attribute, ::Arel.array(values, cast: cast))
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# Returns the column used for buckets, if any
|
98
|
+
def buckets_column
|
99
|
+
buckets_value.last&.to_s || 'bucket'
|
100
|
+
end
|
101
|
+
|
102
|
+
# Transform a range into the proper keys for buckets
|
103
|
+
def buckets_keys
|
104
|
+
keys = buckets_value.second
|
105
|
+
return keys unless keys.is_a?(Range)
|
106
|
+
|
107
|
+
left = nil
|
108
|
+
step = buckets_value.third
|
109
|
+
step = (keys.end - keys.begin).fdiv(step)
|
110
|
+
step = step.to_i if step.to_i == step
|
111
|
+
keys.step(step).each_with_object([]) do |right, result|
|
112
|
+
next left = right if left.nil?
|
113
|
+
|
114
|
+
start, left = left, right
|
115
|
+
result << Range.new(start, left, true)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
end
|
120
|
+
|
121
|
+
Initializer.include(Buckets::Initializer)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
@@ -6,9 +6,14 @@ module Torque
|
|
6
6
|
module DistinctOn
|
7
7
|
|
8
8
|
# :nodoc:
|
9
|
-
def distinct_on_values
|
9
|
+
def distinct_on_values
|
10
|
+
@values.fetch(:distinct_on, FROZEN_EMPTY_ARRAY)
|
11
|
+
end
|
10
12
|
# :nodoc:
|
11
|
-
def distinct_on_values=(value)
|
13
|
+
def distinct_on_values=(value)
|
14
|
+
assert_modifiable!
|
15
|
+
@values[:distinct_on] = value
|
16
|
+
end
|
12
17
|
|
13
18
|
# Specifies whether the records should be unique or not by a given set
|
14
19
|
# of fields. For example:
|
@@ -6,14 +6,24 @@ module Torque
|
|
6
6
|
module Inheritance
|
7
7
|
|
8
8
|
# :nodoc:
|
9
|
-
def
|
9
|
+
def cast_records_values
|
10
|
+
@values.fetch(:cast_records, FROZEN_EMPTY_ARRAY)
|
11
|
+
end
|
10
12
|
# :nodoc:
|
11
|
-
def
|
13
|
+
def cast_records_values=(value)
|
14
|
+
assert_modifiable!
|
15
|
+
@values[:cast_records] = value
|
16
|
+
end
|
12
17
|
|
13
18
|
# :nodoc:
|
14
|
-
def itself_only_value
|
19
|
+
def itself_only_value
|
20
|
+
@values.fetch(:itself_only, nil)
|
21
|
+
end
|
15
22
|
# :nodoc:
|
16
|
-
def itself_only_value=(value)
|
23
|
+
def itself_only_value=(value)
|
24
|
+
assert_modifiable!
|
25
|
+
@values[:itself_only] = value
|
26
|
+
end
|
17
27
|
|
18
28
|
delegate :quote_table_name, :quote_column_name, to: :connection
|
19
29
|
|
@@ -46,7 +56,7 @@ module Torque
|
|
46
56
|
def cast_records!(*types, **options)
|
47
57
|
where!(regclass.pg_cast(:varchar).in(types.map(&:table_name))) if options[:filter]
|
48
58
|
self.select_extra_values += [regclass.as(_record_class_attribute.to_s)]
|
49
|
-
self.
|
59
|
+
self.cast_records_values = (types.present? ? types : model.casted_dependents.values)
|
50
60
|
self
|
51
61
|
end
|
52
62
|
|
@@ -62,11 +72,11 @@ module Torque
|
|
62
72
|
|
63
73
|
# Build all necessary data for inheritances
|
64
74
|
def build_inheritances(arel)
|
65
|
-
return
|
75
|
+
return if self.cast_records_values.empty?
|
66
76
|
|
67
77
|
mergeable = inheritance_mergeable_attributes
|
68
78
|
|
69
|
-
columns = build_inheritances_joins(arel, self.
|
79
|
+
columns = build_inheritances_joins(arel, self.cast_records_values)
|
70
80
|
columns = columns.map do |column, arel_tables|
|
71
81
|
next arel_tables.first[column] if arel_tables.size == 1
|
72
82
|
|
@@ -77,7 +87,7 @@ module Torque
|
|
77
87
|
end
|
78
88
|
end
|
79
89
|
|
80
|
-
columns.push(build_auto_caster_marker(arel, self.
|
90
|
+
columns.push(build_auto_caster_marker(arel, self.cast_records_values))
|
81
91
|
self.select_extra_values += columns.flatten if columns.any?
|
82
92
|
end
|
83
93
|
|
@@ -0,0 +1,112 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Torque
|
4
|
+
module PostgreSQL
|
5
|
+
module Relation
|
6
|
+
module JoinSeries
|
7
|
+
|
8
|
+
# Create the proper arel join
|
9
|
+
class << self
|
10
|
+
def build(relation, range, with: nil, as: :series, step: nil, time_zone: nil, mode: :inner, &block)
|
11
|
+
validate_build!(range, step)
|
12
|
+
|
13
|
+
args = [bind_value(range.begin), bind_value(range.end)]
|
14
|
+
args << bind_value(step) if step
|
15
|
+
args << bind_value(time_zone) if time_zone
|
16
|
+
|
17
|
+
result = Arel::Nodes::Ref.new(as.to_s)
|
18
|
+
func = FN.generate_series(*args).as(as.to_s)
|
19
|
+
condition = build_join_on(result, relation, with, &block)
|
20
|
+
arel_join(mode).new(func, func.create_on(condition))
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
# Make sure we have a viable range
|
26
|
+
def validate_build!(range, step)
|
27
|
+
raise ArgumentError, <<~MSG.squish unless range.is_a?(Range)
|
28
|
+
Value must be a Range.
|
29
|
+
MSG
|
30
|
+
|
31
|
+
raise ArgumentError, <<~MSG.squish if range.begin.nil?
|
32
|
+
Beginless Ranges are not supported.
|
33
|
+
MSG
|
34
|
+
|
35
|
+
raise ArgumentError, <<~MSG.squish if range.end.nil?
|
36
|
+
Endless Ranges are not supported.
|
37
|
+
MSG
|
38
|
+
|
39
|
+
raise ArgumentError, <<~MSG.squish if !range.begin.is_a?(Numeric) && step.nil?
|
40
|
+
missing keyword: :step
|
41
|
+
MSG
|
42
|
+
end
|
43
|
+
|
44
|
+
# Creates the proper bind value
|
45
|
+
def bind_value(value)
|
46
|
+
case value
|
47
|
+
when Integer
|
48
|
+
FN.bind_type(value, :integer, name: 'series', cast: 'integer')
|
49
|
+
when Float
|
50
|
+
FN.bind_type(value, :float, name: 'series', cast: 'numeric')
|
51
|
+
when String
|
52
|
+
FN.bind_type(value, :string, name: 'series', cast: 'text')
|
53
|
+
when ActiveSupport::TimeWithZone
|
54
|
+
FN.bind_type(value, :time, name: 'series', cast: 'timestamptz')
|
55
|
+
when Time
|
56
|
+
FN.bind_type(value, :time, name: 'series', cast: 'timestamp')
|
57
|
+
when DateTime
|
58
|
+
FN.bind_type(value, :datetime, name: 'series', cast: 'timestamp')
|
59
|
+
when ActiveSupport::Duration
|
60
|
+
type = Adapter::OID::Interval.new
|
61
|
+
FN.bind_type(value, type, name: 'series', cast: 'interval')
|
62
|
+
when Date then bind_value(value.to_time(:utc))
|
63
|
+
when ::Arel::Attributes::Attribute then value
|
64
|
+
else
|
65
|
+
raise ArgumentError, "Unsupported value type: #{value.class}"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Get the class of the join on arel
|
70
|
+
def arel_join(mode)
|
71
|
+
case mode.to_sym
|
72
|
+
when :inner then ::Arel::Nodes::InnerJoin
|
73
|
+
when :left then ::Arel::Nodes::OuterJoin
|
74
|
+
when :right then ::Arel::Nodes::RightOuterJoin
|
75
|
+
when :full then ::Arel::Nodes::FullOuterJoin
|
76
|
+
else
|
77
|
+
raise ArgumentError, <<-MSG.squish
|
78
|
+
The '#{mode}' is not implemented as a join type.
|
79
|
+
MSG
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Build the join on clause
|
84
|
+
def build_join_on(result, relation, with)
|
85
|
+
raise ArgumentError, <<~MSG.squish if with.nil? && !block_given?
|
86
|
+
missing keyword: :with
|
87
|
+
MSG
|
88
|
+
|
89
|
+
return yield(result, relation.arel_table) if block_given?
|
90
|
+
|
91
|
+
result.eq(with.is_a?(Symbol) ? relation.arel_table[with.to_s] : with)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# Creates a new join based on PG +generate_series()+ function. It is
|
96
|
+
# based on ranges, supports numbers and dates (as per PG documentation),
|
97
|
+
# custom stepping, time zones, and more. This simply coordinates the
|
98
|
+
# initialization of the the proper join
|
99
|
+
def join_series(range, **xargs, &block)
|
100
|
+
spawn.join_series!(range, **xargs, &block)
|
101
|
+
end
|
102
|
+
|
103
|
+
# Like #join_series, but modifies relation in place.
|
104
|
+
def join_series!(range, **xargs, &block)
|
105
|
+
self.joins_values |= [JoinSeries.build(self, range, **xargs, &block)]
|
106
|
+
self
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -12,6 +12,7 @@ module Torque
|
|
12
12
|
merge_distinct_on
|
13
13
|
merge_auxiliary_statements
|
14
14
|
merge_inheritance
|
15
|
+
merge_buckets
|
15
16
|
|
16
17
|
relation
|
17
18
|
end
|
@@ -26,12 +27,15 @@ module Torque
|
|
26
27
|
|
27
28
|
# Merge distinct on columns
|
28
29
|
def merge_distinct_on
|
30
|
+
return unless relation.is_a?(Relation::DistinctOn)
|
29
31
|
return if other.distinct_on_values.blank?
|
32
|
+
|
30
33
|
relation.distinct_on_values += other.distinct_on_values
|
31
34
|
end
|
32
35
|
|
33
36
|
# Merge auxiliary statements activated by +with+
|
34
37
|
def merge_auxiliary_statements
|
38
|
+
return unless relation.is_a?(Relation::AuxiliaryStatement)
|
35
39
|
return if other.auxiliary_statements_values.blank?
|
36
40
|
|
37
41
|
current = relation.auxiliary_statements_values.map{ |cte| cte.class }
|
@@ -44,14 +48,24 @@ module Torque
|
|
44
48
|
|
45
49
|
# Merge settings related to inheritance tables
|
46
50
|
def merge_inheritance
|
51
|
+
return unless relation.is_a?(Relation::Inheritance)
|
52
|
+
|
47
53
|
relation.itself_only_value = true if other.itself_only_value.present?
|
48
54
|
|
49
|
-
if other.
|
50
|
-
relation.
|
51
|
-
relation.
|
55
|
+
if other.cast_records_values.present?
|
56
|
+
relation.cast_records_values += other.cast_records_values
|
57
|
+
relation.cast_records_values.uniq!
|
52
58
|
end
|
53
59
|
end
|
54
60
|
|
61
|
+
# Merge settings related to buckets
|
62
|
+
def merge_buckets
|
63
|
+
return unless relation.is_a?(Relation::Buckets)
|
64
|
+
return if other.buckets_value.blank?
|
65
|
+
|
66
|
+
relation.buckets_value = other.buckets_value
|
67
|
+
end
|
68
|
+
|
55
69
|
end
|
56
70
|
|
57
71
|
ActiveRecord::Relation::Merger.prepend Merger
|
@@ -13,16 +13,25 @@ module Torque
|
|
13
13
|
include DistinctOn
|
14
14
|
include Inheritance
|
15
15
|
|
16
|
-
SINGLE_VALUE_METHODS = [
|
17
|
-
MULTI_VALUE_METHODS = [
|
16
|
+
SINGLE_VALUE_METHODS = %i[itself_only buckets]
|
17
|
+
MULTI_VALUE_METHODS = %i[
|
18
|
+
select_extra distinct_on auxiliary_statements cast_records
|
19
|
+
]
|
20
|
+
|
18
21
|
VALUE_METHODS = SINGLE_VALUE_METHODS + MULTI_VALUE_METHODS
|
22
|
+
FROZEN_EMPTY_ARRAY = ::ActiveRecord::QueryMethods::FROZEN_EMPTY_ARRAY
|
19
23
|
|
20
24
|
ARColumn = ::ActiveRecord::ConnectionAdapters::PostgreSQL::Column
|
21
25
|
|
22
26
|
# :nodoc:
|
23
|
-
def select_extra_values
|
27
|
+
def select_extra_values
|
28
|
+
@values.fetch(:select_extra, FROZEN_EMPTY_ARRAY)
|
29
|
+
end
|
24
30
|
# :nodoc:
|
25
|
-
def select_extra_values=(value)
|
31
|
+
def select_extra_values=(value)
|
32
|
+
assert_modifiable!
|
33
|
+
@values[:select_extra] = value
|
34
|
+
end
|
26
35
|
|
27
36
|
# Resolve column name when calculating models, allowing the column name to
|
28
37
|
# be more complex while keeping the query selection quality
|
@@ -90,21 +99,6 @@ module Torque
|
|
90
99
|
arel
|
91
100
|
end
|
92
101
|
|
93
|
-
# Compatibility method with 5.0
|
94
|
-
unless ActiveRecord::Relation.method_defined?(:get_value)
|
95
|
-
def get_value(name)
|
96
|
-
@values[name] || ActiveRecord::QueryMethods::FROZEN_EMPTY_ARRAY
|
97
|
-
end
|
98
|
-
end
|
99
|
-
|
100
|
-
# Compatibility method with 5.0
|
101
|
-
unless ActiveRecord::Relation.method_defined?(:set_value)
|
102
|
-
def set_value(name, value)
|
103
|
-
assert_mutability! if respond_to?(:assert_mutability!)
|
104
|
-
@values[name] = value
|
105
|
-
end
|
106
|
-
end
|
107
|
-
|
108
102
|
class_methods do
|
109
103
|
# Easy and storable way to access the name used to get the record table
|
110
104
|
# name when using inheritance tables
|
@@ -145,15 +139,11 @@ module Torque
|
|
145
139
|
ActiveRecord::Relation.include Relation
|
146
140
|
ActiveRecord::Relation.prepend Relation::Initializer
|
147
141
|
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
ActiveRecord::
|
152
|
-
|
153
|
-
ActiveRecord::Relation::VALUE_METHODS += Relation::VALUE_METHODS
|
154
|
-
ActiveRecord::QueryMethods::VALID_UNSCOPING_VALUES += %i[cast_records itself_only
|
155
|
-
distinct_on auxiliary_statements]
|
142
|
+
ActiveRecord::Relation::SINGLE_VALUE_METHODS.concat(Relation::SINGLE_VALUE_METHODS)
|
143
|
+
ActiveRecord::Relation::MULTI_VALUE_METHODS.concat(Relation::MULTI_VALUE_METHODS)
|
144
|
+
ActiveRecord::Relation::VALUE_METHODS.concat(Relation::VALUE_METHODS)
|
145
|
+
ActiveRecord::QueryMethods::VALID_UNSCOPING_VALUES.merge(%i[cast_records itself_only
|
146
|
+
distinct_on auxiliary_statements buckets])
|
156
147
|
|
157
|
-
$VERBOSE = warn_level
|
158
148
|
end
|
159
149
|
end
|
@@ -0,0 +1,146 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Torque
|
4
|
+
module PostgreSQL
|
5
|
+
module VersionedCommands
|
6
|
+
module Migration
|
7
|
+
def initialize(*args)
|
8
|
+
@command = args.pop
|
9
|
+
super(*args)
|
10
|
+
end
|
11
|
+
|
12
|
+
# Prepare the description based on the direction
|
13
|
+
def migrate(direction)
|
14
|
+
@description = description_for(direction)
|
15
|
+
super
|
16
|
+
end
|
17
|
+
|
18
|
+
# Uses the command to execute the proper action
|
19
|
+
def exec_migration(conn, direction)
|
20
|
+
@connection = conn
|
21
|
+
direction == :up ? @command.up : @command.down
|
22
|
+
ensure
|
23
|
+
@connection = nil
|
24
|
+
@execution_strategy = nil
|
25
|
+
end
|
26
|
+
|
27
|
+
# Better formatting of the output
|
28
|
+
def announce(message)
|
29
|
+
action, result = @description
|
30
|
+
|
31
|
+
title = [
|
32
|
+
@command.type.capitalize,
|
33
|
+
@command.object_name,
|
34
|
+
"v#{@command.op_version}"
|
35
|
+
].join(' ')
|
36
|
+
|
37
|
+
timing = message.split(' ', 2).second
|
38
|
+
action = "#{result} #{timing}" if timing.present?
|
39
|
+
text = "#{@command.version} #{title}: #{action}"
|
40
|
+
length = [0, 75 - text.length].max
|
41
|
+
|
42
|
+
write "== %s %s" % [text, "=" * length]
|
43
|
+
end
|
44
|
+
|
45
|
+
# Produces a nice description of what is being done
|
46
|
+
def description_for(direction)
|
47
|
+
base = @command.op.chomp('e') if direction == :up
|
48
|
+
base ||=
|
49
|
+
case @command.op
|
50
|
+
when 'create' then 'dropp'
|
51
|
+
when 'update' then 'revert'
|
52
|
+
when 'remove' then 're-creat'
|
53
|
+
end
|
54
|
+
|
55
|
+
["#{base}ing", "#{base}ed"]
|
56
|
+
end
|
57
|
+
|
58
|
+
# Print the command and then execute it
|
59
|
+
def execute(command)
|
60
|
+
write "-- #{command.gsub(/(?<!\A)^/, ' ').gsub(/[\s\n]*\z/, '')}"
|
61
|
+
execution_strategy.execute(command)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
CommandMigration = Struct.new(*%i[filename version op type object_name op_version scope]) do
|
66
|
+
delegate :execute, to: '@migration'
|
67
|
+
|
68
|
+
def initialize(filename, *args)
|
69
|
+
super(File.expand_path(filename), *args)
|
70
|
+
@migration = nil
|
71
|
+
end
|
72
|
+
|
73
|
+
# Rails uses this to avoid duplicate migrations
|
74
|
+
def name
|
75
|
+
"#{op}_#{type}_#{object_name}_v#{op_version}"
|
76
|
+
end
|
77
|
+
|
78
|
+
# There is no way to setup this, so it is always false
|
79
|
+
def disable_ddl_transaction
|
80
|
+
false
|
81
|
+
end
|
82
|
+
|
83
|
+
# Down is more complicated, then this just starts separating the logic
|
84
|
+
def migrate(direction)
|
85
|
+
@migration = ActiveRecord::Migration.allocate
|
86
|
+
@migration.extend(Migration)
|
87
|
+
@migration.send(:initialize, name, version, self)
|
88
|
+
@migration.migrate(direction)
|
89
|
+
ensure
|
90
|
+
@migration = nil
|
91
|
+
end
|
92
|
+
|
93
|
+
# Simply executes the underlying command
|
94
|
+
def up
|
95
|
+
content = File.read(filename)
|
96
|
+
VersionedCommands.validate!(type, content, object_name)
|
97
|
+
execute content
|
98
|
+
end
|
99
|
+
|
100
|
+
# Find the previous command and executes it
|
101
|
+
def down
|
102
|
+
return drop if op_version == 1
|
103
|
+
dirs = @migration.pool.migrations_paths
|
104
|
+
version = op_version - (op == 'remove' ? 0 : 1)
|
105
|
+
execute VersionedCommands.fetch_command(dirs, type, object_name, version)
|
106
|
+
end
|
107
|
+
|
108
|
+
# Drops the type created
|
109
|
+
def drop
|
110
|
+
method_name = :"drop_#{type}"
|
111
|
+
return send(method_name) if VersionedCommands.valid_type?(type)
|
112
|
+
raise ArgumentError, "Unknown versioned command type: #{type}"
|
113
|
+
end
|
114
|
+
|
115
|
+
private
|
116
|
+
|
117
|
+
# Drop all functions all at once
|
118
|
+
def drop_function
|
119
|
+
definitions = File.read(filename).scan(Regexp.new([
|
120
|
+
"FUNCTION\\s+#{NAME_MATCH}",
|
121
|
+
'\s*(\([_a-z0-9 ,]*\))?',
|
122
|
+
].join, 'mi'))
|
123
|
+
|
124
|
+
functions = definitions.map(&:join).join(', ')
|
125
|
+
execute "DROP FUNCTION #{functions};"
|
126
|
+
end
|
127
|
+
|
128
|
+
# Drop the type
|
129
|
+
def drop_type
|
130
|
+
name = File.read(filename).scan(Regexp.new("TYPE\\s+#{NAME_MATCH}", 'mi'))
|
131
|
+
execute "DROP TYPE #{name.first.first};"
|
132
|
+
end
|
133
|
+
|
134
|
+
# Drop view or materialized view
|
135
|
+
def drop_view
|
136
|
+
mat, name = File.read(filename).scan(Regexp.new([
|
137
|
+
'(MATERIALIZED)?\s+(?:RECURSIVE\s+)?',
|
138
|
+
"VIEW\\s+#{NAME_MATCH}",
|
139
|
+
].join, 'mi')).first
|
140
|
+
|
141
|
+
execute "DROP#{' MATERIALIZED' if mat.present?} VIEW #{name};"
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails/generators/base'
|
4
|
+
require 'rails/generators/active_record/migration'
|
5
|
+
|
6
|
+
module Torque
|
7
|
+
module PostgreSQL
|
8
|
+
module VersionedCommands
|
9
|
+
module Generator
|
10
|
+
TEMPLATES_PATH = '../../../generators/torque/templates'
|
11
|
+
|
12
|
+
attr_reader :file_name
|
13
|
+
|
14
|
+
def self.included(base)
|
15
|
+
type = base.name.demodulize.chomp('Generator').underscore
|
16
|
+
|
17
|
+
base.send(:source_root, File.expand_path(TEMPLATES_PATH, __dir__))
|
18
|
+
base.include(ActiveRecord::Generators::Migration)
|
19
|
+
|
20
|
+
base.instance_variable_set(:@type, type)
|
21
|
+
base.instance_variable_set(:@desc, <<~DESC.squish)
|
22
|
+
Generates a migration for creating, updating, or removing a #{type}.
|
23
|
+
DESC
|
24
|
+
|
25
|
+
base.class_option :operation, type: :string, aliases: %i(--op),
|
26
|
+
desc: 'The name for the operation'
|
27
|
+
|
28
|
+
base.argument :name, type: :string,
|
29
|
+
desc: "The name of the #{type}"
|
30
|
+
end
|
31
|
+
|
32
|
+
def type
|
33
|
+
self.class.instance_variable_get(:@type)
|
34
|
+
end
|
35
|
+
|
36
|
+
def create_migration_file
|
37
|
+
version = count_object_entries
|
38
|
+
operation = options[:operation] || (version == 0 ? 'create' : 'update')
|
39
|
+
@file_name = "#{operation}_#{type}_#{name.underscore}_v#{version + 1}"
|
40
|
+
|
41
|
+
validate_file_name!
|
42
|
+
migration_template "#{type}.sql.erb", File.join(db_migrate_path, "#{file_name}.sql")
|
43
|
+
end
|
44
|
+
|
45
|
+
def count_object_entries
|
46
|
+
Dir.glob("#{db_migrate_path}/*_#{type}_#{name.underscore}_v*.sql").size
|
47
|
+
end
|
48
|
+
|
49
|
+
def validate_file_name!
|
50
|
+
unless /^[_a-z0-9]+$/.match?(file_name)
|
51
|
+
raise ActiveRecord::IllegalMigrationNameError.new(file_name)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|