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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/torque/function_generator.rb +13 -0
  3. data/lib/generators/torque/templates/function.sql.erb +4 -0
  4. data/lib/generators/torque/templates/type.sql.erb +2 -0
  5. data/lib/generators/torque/templates/view.sql.erb +3 -0
  6. data/lib/generators/torque/type_generator.rb +13 -0
  7. data/lib/generators/torque/view_generator.rb +16 -0
  8. data/lib/torque/postgresql/adapter/database_statements.rb +48 -10
  9. data/lib/torque/postgresql/adapter/schema_definitions.rb +22 -0
  10. data/lib/torque/postgresql/adapter/schema_dumper.rb +47 -1
  11. data/lib/torque/postgresql/adapter/schema_statements.rb +45 -0
  12. data/lib/torque/postgresql/arel/nodes.rb +14 -0
  13. data/lib/torque/postgresql/arel/visitors.rb +4 -0
  14. data/lib/torque/postgresql/attributes/builder/full_text_search.rb +16 -28
  15. data/lib/torque/postgresql/base.rb +2 -1
  16. data/lib/torque/postgresql/config.rb +35 -1
  17. data/lib/torque/postgresql/function.rb +33 -0
  18. data/lib/torque/postgresql/railtie.rb +26 -1
  19. data/lib/torque/postgresql/relation/auxiliary_statement.rb +7 -2
  20. data/lib/torque/postgresql/relation/buckets.rb +124 -0
  21. data/lib/torque/postgresql/relation/distinct_on.rb +7 -2
  22. data/lib/torque/postgresql/relation/inheritance.rb +18 -8
  23. data/lib/torque/postgresql/relation/join_series.rb +112 -0
  24. data/lib/torque/postgresql/relation/merger.rb +17 -3
  25. data/lib/torque/postgresql/relation.rb +18 -28
  26. data/lib/torque/postgresql/version.rb +1 -1
  27. data/lib/torque/postgresql/versioned_commands/command_migration.rb +146 -0
  28. data/lib/torque/postgresql/versioned_commands/generator.rb +57 -0
  29. data/lib/torque/postgresql/versioned_commands/migration_context.rb +83 -0
  30. data/lib/torque/postgresql/versioned_commands/migrator.rb +39 -0
  31. data/lib/torque/postgresql/versioned_commands/schema_table.rb +101 -0
  32. data/lib/torque/postgresql/versioned_commands.rb +161 -0
  33. data/spec/fixtures/migrations/20250101000001_create_users.rb +0 -0
  34. data/spec/fixtures/migrations/20250101000002_create_function_count_users_v1.sql +0 -0
  35. data/spec/fixtures/migrations/20250101000003_create_internal_users.rb +0 -0
  36. data/spec/fixtures/migrations/20250101000004_update_function_count_users_v2.sql +0 -0
  37. data/spec/fixtures/migrations/20250101000005_create_view_all_users_v1.sql +0 -0
  38. data/spec/fixtures/migrations/20250101000006_create_type_user_id_v1.sql +0 -0
  39. data/spec/fixtures/migrations/20250101000007_remove_function_count_users_v2.sql +0 -0
  40. data/spec/initialize.rb +9 -0
  41. data/spec/schema.rb +2 -4
  42. data/spec/spec_helper.rb +6 -1
  43. data/spec/tests/full_text_seach_test.rb +30 -2
  44. data/spec/tests/relation_spec.rb +229 -0
  45. data/spec/tests/schema_spec.rb +4 -1
  46. data/spec/tests/versioned_commands_spec.rb +513 -0
  47. 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; get_value(:distinct_on); end
9
+ def distinct_on_values
10
+ @values.fetch(:distinct_on, FROZEN_EMPTY_ARRAY)
11
+ end
10
12
  # :nodoc:
11
- def distinct_on_values=(value); set_value(:distinct_on, value); end
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 cast_records_value; get_value(:cast_records); end
9
+ def cast_records_values
10
+ @values.fetch(:cast_records, FROZEN_EMPTY_ARRAY)
11
+ end
10
12
  # :nodoc:
11
- def cast_records_value=(value); set_value(:cast_records, value); end
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; get_value(:itself_only); end
19
+ def itself_only_value
20
+ @values.fetch(:itself_only, nil)
21
+ end
15
22
  # :nodoc:
16
- def itself_only_value=(value); set_value(:itself_only, value); end
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.cast_records_value = (types.present? ? types : model.casted_dependents.values)
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 unless self.cast_records_value.present?
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.cast_records_value)
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.cast_records_value))
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.cast_records_value.present?
50
- relation.cast_records_value += other.cast_records_value
51
- relation.cast_records_value.uniq!
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 = [:itself_only]
17
- MULTI_VALUE_METHODS = [:distinct_on, :auxiliary_statements, :cast_records, :select_extra]
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; get_value(:select_extra); end
27
+ def select_extra_values
28
+ @values.fetch(:select_extra, FROZEN_EMPTY_ARRAY)
29
+ end
24
30
  # :nodoc:
25
- def select_extra_values=(value); set_value(:select_extra, value); end
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
- warn_level = $VERBOSE
149
- $VERBOSE = nil
150
-
151
- ActiveRecord::Relation::SINGLE_VALUE_METHODS += Relation::SINGLE_VALUE_METHODS
152
- ActiveRecord::Relation::MULTI_VALUE_METHODS += Relation::MULTI_VALUE_METHODS
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Torque
4
4
  module PostgreSQL
5
- VERSION = '4.0.0.rc1'
5
+ VERSION = '4.0.0'
6
6
  end
7
7
  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