torque-postgresql 3.4.1 → 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 (94) 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 +111 -94
  9. data/lib/torque/postgresql/adapter/oid/array.rb +17 -0
  10. data/lib/torque/postgresql/adapter/oid/line.rb +2 -6
  11. data/lib/torque/postgresql/adapter/oid/range.rb +4 -4
  12. data/lib/torque/postgresql/adapter/oid.rb +1 -23
  13. data/lib/torque/postgresql/adapter/quoting.rb +13 -7
  14. data/lib/torque/postgresql/adapter/schema_creation.rb +7 -28
  15. data/lib/torque/postgresql/adapter/schema_definitions.rb +58 -0
  16. data/lib/torque/postgresql/adapter/schema_dumper.rb +136 -34
  17. data/lib/torque/postgresql/adapter/schema_overrides.rb +45 -0
  18. data/lib/torque/postgresql/adapter/schema_statements.rb +109 -49
  19. data/lib/torque/postgresql/arel/infix_operation.rb +15 -28
  20. data/lib/torque/postgresql/arel/nodes.rb +16 -2
  21. data/lib/torque/postgresql/arel/operations.rb +7 -1
  22. data/lib/torque/postgresql/arel/visitors.rb +7 -9
  23. data/lib/torque/postgresql/associations/association_scope.rb +23 -31
  24. data/lib/torque/postgresql/associations/belongs_to_many_association.rb +25 -0
  25. data/lib/torque/postgresql/associations/builder/belongs_to_many.rb +16 -0
  26. data/lib/torque/postgresql/attributes/builder/enum.rb +12 -9
  27. data/lib/torque/postgresql/attributes/builder/full_text_search.rb +109 -0
  28. data/lib/torque/postgresql/attributes/builder/period.rb +21 -21
  29. data/lib/torque/postgresql/attributes/builder.rb +49 -11
  30. data/lib/torque/postgresql/attributes/enum.rb +7 -7
  31. data/lib/torque/postgresql/attributes/enum_set.rb +7 -7
  32. data/lib/torque/postgresql/attributes/full_text_search.rb +19 -0
  33. data/lib/torque/postgresql/attributes/period.rb +2 -2
  34. data/lib/torque/postgresql/attributes.rb +0 -4
  35. data/lib/torque/postgresql/auxiliary_statement/recursive.rb +3 -3
  36. data/lib/torque/postgresql/base.rb +5 -11
  37. data/lib/torque/postgresql/collector.rb +1 -1
  38. data/lib/torque/postgresql/config.rb +129 -5
  39. data/lib/torque/postgresql/function.rb +94 -0
  40. data/lib/torque/postgresql/inheritance.rb +52 -36
  41. data/lib/torque/postgresql/predicate_builder/arel_attribute_handler.rb +33 -0
  42. data/lib/torque/postgresql/predicate_builder/array_handler.rb +47 -0
  43. data/lib/torque/postgresql/predicate_builder/enumerator_lazy_handler.rb +37 -0
  44. data/lib/torque/postgresql/predicate_builder/regexp_handler.rb +21 -0
  45. data/lib/torque/postgresql/predicate_builder.rb +35 -0
  46. data/lib/torque/postgresql/railtie.rb +137 -30
  47. data/lib/torque/postgresql/reflection/abstract_reflection.rb +12 -44
  48. data/lib/torque/postgresql/reflection/belongs_to_many_reflection.rb +4 -0
  49. data/lib/torque/postgresql/reflection/has_many_reflection.rb +4 -0
  50. data/lib/torque/postgresql/reflection/runtime_reflection.rb +1 -1
  51. data/lib/torque/postgresql/relation/auxiliary_statement.rb +7 -2
  52. data/lib/torque/postgresql/relation/buckets.rb +124 -0
  53. data/lib/torque/postgresql/relation/distinct_on.rb +7 -2
  54. data/lib/torque/postgresql/relation/inheritance.rb +22 -15
  55. data/lib/torque/postgresql/relation/join_series.rb +112 -0
  56. data/lib/torque/postgresql/relation/merger.rb +17 -3
  57. data/lib/torque/postgresql/relation.rb +24 -38
  58. data/lib/torque/postgresql/schema_cache.rb +6 -12
  59. data/lib/torque/postgresql/version.rb +1 -1
  60. data/lib/torque/postgresql/versioned_commands/command_migration.rb +146 -0
  61. data/lib/torque/postgresql/versioned_commands/generator.rb +57 -0
  62. data/lib/torque/postgresql/versioned_commands/migration_context.rb +83 -0
  63. data/lib/torque/postgresql/versioned_commands/migrator.rb +39 -0
  64. data/lib/torque/postgresql/versioned_commands/schema_table.rb +101 -0
  65. data/lib/torque/postgresql/versioned_commands.rb +161 -0
  66. data/lib/torque/postgresql.rb +2 -1
  67. data/spec/fixtures/migrations/20250101000001_create_users.rb +0 -0
  68. data/spec/fixtures/migrations/20250101000002_create_function_count_users_v1.sql +0 -0
  69. data/spec/fixtures/migrations/20250101000003_create_internal_users.rb +0 -0
  70. data/spec/fixtures/migrations/20250101000004_update_function_count_users_v2.sql +0 -0
  71. data/spec/fixtures/migrations/20250101000005_create_view_all_users_v1.sql +0 -0
  72. data/spec/fixtures/migrations/20250101000006_create_type_user_id_v1.sql +0 -0
  73. data/spec/fixtures/migrations/20250101000007_remove_function_count_users_v2.sql +0 -0
  74. data/spec/initialize.rb +67 -0
  75. data/spec/mocks/cache_query.rb +21 -21
  76. data/spec/mocks/create_table.rb +6 -26
  77. data/spec/schema.rb +17 -12
  78. data/spec/spec_helper.rb +11 -2
  79. data/spec/tests/arel_spec.rb +32 -7
  80. data/spec/tests/auxiliary_statement_spec.rb +3 -3
  81. data/spec/tests/belongs_to_many_spec.rb +72 -5
  82. data/spec/tests/enum_set_spec.rb +12 -11
  83. data/spec/tests/enum_spec.rb +4 -2
  84. data/spec/tests/full_text_seach_test.rb +280 -0
  85. data/spec/tests/function_spec.rb +42 -0
  86. data/spec/tests/has_many_spec.rb +21 -8
  87. data/spec/tests/interval_spec.rb +1 -7
  88. data/spec/tests/period_spec.rb +61 -61
  89. data/spec/tests/predicate_builder_spec.rb +132 -0
  90. data/spec/tests/relation_spec.rb +229 -0
  91. data/spec/tests/schema_spec.rb +6 -9
  92. data/spec/tests/table_inheritance_spec.rb +25 -26
  93. data/spec/tests/versioned_commands_spec.rb +513 -0
  94. metadata +64 -39
@@ -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
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'relation/distinct_on'
4
- require_relative 'relation/auxiliary_statement'
5
4
  require_relative 'relation/inheritance'
6
5
 
7
6
  require_relative 'relation/merger'
@@ -12,19 +11,27 @@ module Torque
12
11
  extend ActiveSupport::Concern
13
12
 
14
13
  include DistinctOn
15
- include AuxiliaryStatement
16
14
  include Inheritance
17
15
 
18
- SINGLE_VALUE_METHODS = [:itself_only]
19
- 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
+
20
21
  VALUE_METHODS = SINGLE_VALUE_METHODS + MULTI_VALUE_METHODS
22
+ FROZEN_EMPTY_ARRAY = ::ActiveRecord::QueryMethods::FROZEN_EMPTY_ARRAY
21
23
 
22
24
  ARColumn = ::ActiveRecord::ConnectionAdapters::PostgreSQL::Column
23
25
 
24
26
  # :nodoc:
25
- def select_extra_values; get_value(:select_extra); end
27
+ def select_extra_values
28
+ @values.fetch(:select_extra, FROZEN_EMPTY_ARRAY)
29
+ end
26
30
  # :nodoc:
27
- 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
28
35
 
29
36
  # Resolve column name when calculating models, allowing the column name to
30
37
  # be more complex while keeping the query selection quality
@@ -92,22 +99,7 @@ module Torque
92
99
  arel
93
100
  end
94
101
 
95
- # Compatibility method with 5.0
96
- unless ActiveRecord::Relation.method_defined?(:get_value)
97
- def get_value(name)
98
- @values[name] || ActiveRecord::QueryMethods::FROZEN_EMPTY_ARRAY
99
- end
100
- end
101
-
102
- # Compatibility method with 5.0
103
- unless ActiveRecord::Relation.method_defined?(:set_value)
104
- def set_value(name, value)
105
- assert_mutability! if respond_to?(:assert_mutability!)
106
- @values[name] = value
107
- end
108
- end
109
-
110
- module ClassMethods
102
+ class_methods do
111
103
  # Easy and storable way to access the name used to get the record table
112
104
  # name when using inheritance tables
113
105
  def _record_class_attribute
@@ -115,7 +107,7 @@ module Torque
115
107
  .inheritance.record_class_column_name.to_sym
116
108
  end
117
109
 
118
- # Easy and storable way to access the name used to get the indicater of
110
+ # Easy and storable way to access the name used to get the indicate of
119
111
  # auto casting inherited records
120
112
  def _auto_cast_attribute
121
113
  @@auto_cast ||= Torque::PostgreSQL.config
@@ -135,29 +127,23 @@ module Torque
135
127
  end
136
128
 
137
129
  # Allow extra keyword arguments to be sent to +InsertAll+
138
- if Torque::PostgreSQL::AR720
139
- def upsert_all(attributes, **xargs)
140
- xargs = xargs.reverse_merge(on_duplicate: :update)
141
- ::ActiveRecord::InsertAll.execute(self, attributes, **xargs)
142
- end
130
+ def upsert_all(attributes, **xargs)
131
+ xargs = xargs.reverse_merge(on_duplicate: :update)
132
+ ::ActiveRecord::InsertAll.execute(self, attributes, **xargs)
143
133
  end
144
134
  end
145
135
  end
146
136
 
147
- # Include the methos here provided and then change the constants to ensure
137
+ # Include the methods here provided and then change the constants to ensure
148
138
  # the operation of ActiveRecord Relation
149
139
  ActiveRecord::Relation.include Relation
150
140
  ActiveRecord::Relation.prepend Relation::Initializer
151
141
 
152
- warn_level = $VERBOSE
153
- $VERBOSE = nil
154
-
155
- ActiveRecord::Relation::SINGLE_VALUE_METHODS += Relation::SINGLE_VALUE_METHODS
156
- ActiveRecord::Relation::MULTI_VALUE_METHODS += Relation::MULTI_VALUE_METHODS
157
- ActiveRecord::Relation::VALUE_METHODS += Relation::VALUE_METHODS
158
- ActiveRecord::QueryMethods::VALID_UNSCOPING_VALUES += %i[cast_records itself_only
159
- 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])
160
147
 
161
- $VERBOSE = warn_level
162
148
  end
163
149
  end
@@ -1,11 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'torque/postgresql/schema_cache/inheritance'
4
-
5
- if Torque::PostgreSQL::AR710
6
- require 'torque/postgresql/schema_cache/schema_reflection'
7
- require 'torque/postgresql/schema_cache/bound_schema_reflection'
8
- end
4
+ require 'torque/postgresql/schema_cache/schema_reflection'
5
+ require 'torque/postgresql/schema_cache/bound_schema_reflection'
9
6
 
10
7
  module Torque
11
8
  module PostgreSQL
@@ -73,7 +70,7 @@ module Torque
73
70
  end
74
71
 
75
72
  def clear_data_source_cache!(connection_or_name, name = connection_or_name) # :nodoc:
76
- Torque::PostgreSQL::AR710 ? super : super(name)
73
+ super
77
74
  @data_sources_model_names.delete name
78
75
  @inheritance_dependencies.delete name
79
76
  @inheritance_associations.delete name
@@ -130,8 +127,7 @@ module Torque
130
127
  def reload_inheritance_data!(source)
131
128
  return if @inheritance_loaded
132
129
 
133
- method_name = Torque::PostgreSQL::AR720 ? :with_connection : :then
134
- source.public_send(method_name) do |connection|
130
+ source.with_connection do |connection|
135
131
  @inheritance_dependencies = connection.inherited_tables
136
132
  @inheritance_associations = generate_associations
137
133
  @inheritance_loaded = true
@@ -145,14 +141,12 @@ module Torque
145
141
  end
146
142
 
147
143
  # Use this method to also load any irregular model name
148
- method_name = Torque::PostgreSQL::AR720 ? :add_all : :prepare_data_sources
149
- define_method(method_name) do |source = nil|
150
- Torque::PostgreSQL::AR710 ? super(source) : super()
144
+ def add_all(source = nil)
145
+ super
151
146
 
152
147
  data_sources = source.present? ? tables_to_cache(source) : @data_sources.keys
153
148
  @data_sources_model_names = prepare_irregular_models(data_sources)
154
149
  end
155
-
156
150
  end
157
151
 
158
152
  ActiveRecord::ConnectionAdapters::SchemaCache.prepend SchemaCache
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Torque
4
4
  module PostgreSQL
5
- VERSION = '3.4.1'
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
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Torque
4
+ module PostgreSQL
5
+ class IllegalCommandTypeError < ActiveRecord::MigrationError
6
+ def initialize(file)
7
+ super(<<~MSG.squish)
8
+ Illegal name for command file '#{file}'. Commands are more strict and require
9
+ the version, one of create, update, or remove, type of object, name
10
+ and operation version to be present in the filename.
11
+ (e.g. 20250101010101_create_function_my_function_v1.sql)
12
+ MSG
13
+ end
14
+ end
15
+
16
+ module VersionedCommands
17
+ module MigrationContext
18
+ InvalidMigrationTimestampError = ActiveRecord::InvalidMigrationTimestampError
19
+ PGAdapter = ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
20
+
21
+ def migrations
22
+ return super unless running_for_pg?
23
+
24
+ commands = command_files.map do |file|
25
+ version, op, type, name, op_version, scope = parse_command_filename(file)
26
+ raise IllegalCommandTypeError.new(file) unless version
27
+ if validate_timestamp? && !valid_migration_timestamp?(version)
28
+ raise InvalidMigrationTimestampError.new(version, [op, type, name, op_version].join('_'))
29
+ end
30
+
31
+ version = version.to_i
32
+ CommandMigration.new(file, version, op, type, name, op_version.to_i, scope)
33
+ end
34
+
35
+ super.concat(commands).sort_by(&:version)
36
+ end
37
+
38
+ def migrations_status
39
+ return super unless running_for_pg?
40
+ db_list = schema_migration.normalized_versions
41
+
42
+ commands = command_files.map do |file|
43
+ version, op, type, name, op_version, scope = parse_command_filename(file)
44
+ raise IllegalCommandTypeError.new(file) unless version
45
+ if validate_timestamp? && !valid_migration_timestamp?(version)
46
+ raise InvalidMigrationTimestampError.new(version, [op, type, name, op_version].join('_'))
47
+ end
48
+
49
+ version = schema_migration.normalize_migration_number(version)
50
+ status = db_list.delete(version) ? "up" : "down"
51
+ [status, version, "#{op.capitalize} #{type.capitalize} #{name}#{scope} (v#{op_version})"]
52
+ end
53
+
54
+ (commands + super).uniq(&:second).sort_by(&:second)
55
+ end
56
+
57
+ def migration_commands
58
+ migrations.select { |m| m.is_a?(VersionedCommands::CommandMigration) }
59
+ end
60
+
61
+ private
62
+
63
+ # Checks if the current migration context is running for PostgreSQL
64
+ def running_for_pg?
65
+ connection_pool.db_config.adapter_class <= PGAdapter
66
+ end
67
+
68
+ # Get the list of all versioned command files
69
+ def command_files
70
+ paths = Array(migrations_paths)
71
+ Dir[*paths.flat_map { |path| "#{path}/**/[0-9]*_*.sql" }]
72
+ end
73
+
74
+ # Commands are more strict with the filename format
75
+ def parse_command_filename(filename)
76
+ File.basename(filename).scan(VersionedCommands.filename_regexp).first
77
+ end
78
+ end
79
+
80
+ ActiveRecord::MigrationContext.prepend(MigrationContext)
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Torque
4
+ module PostgreSQL
5
+ module VersionedCommands
6
+ module Migrator
7
+ def execute_migration_in_transaction(migration)
8
+ @versioned_command = versioned_command?(migration) && migration
9
+ super
10
+ ensure
11
+ @versioned_command = false
12
+ end
13
+
14
+ def record_version_state_after_migrating(version)
15
+ return super if (command = @versioned_command) == false
16
+
17
+ @versioned_table ||= VersionedCommands::SchemaTable.new(connection.pool)
18
+ @versioned_counter ||= @versioned_table.count
19
+
20
+ if down?
21
+ @versioned_counter -= 1
22
+ @versioned_table.delete_version(command)
23
+ @versioned_table.drop_table if @versioned_counter.zero?
24
+ else
25
+ @versioned_table.create_table if @versioned_counter.zero?
26
+ @versioned_table.create_version(command)
27
+ @versioned_counter += 1
28
+ end
29
+ end
30
+
31
+ def versioned_command?(migration)
32
+ migration.is_a?(VersionedCommands::CommandMigration)
33
+ end
34
+ end
35
+
36
+ ActiveRecord::Migrator.prepend(Migrator)
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Torque
4
+ module PostgreSQL
5
+ module VersionedCommands
6
+ class SchemaTable
7
+ attr_reader :arel_table
8
+
9
+ def initialize(pool)
10
+ @pool = pool
11
+ @arel_table = ::Arel::Table.new(table_name)
12
+ end
13
+
14
+ def create_version(command)
15
+ im = ::Arel::InsertManager.new(arel_table)
16
+ im.insert(
17
+ arel_table[primary_key] => command.version,
18
+ arel_table['type'] => command.type,
19
+ arel_table['object_name'] => command.object_name,
20
+ )
21
+
22
+ @pool.with_connection do |connection|
23
+ connection.insert(im, "#{name} Create", primary_key, command.version)
24
+ end
25
+ end
26
+
27
+ def delete_version(command)
28
+ dm = ::Arel::DeleteManager.new(arel_table)
29
+ dm.wheres = [arel_table[primary_key].eq(command.version.to_s)]
30
+
31
+ @pool.with_connection do |connection|
32
+ connection.delete(dm, "#{name} Destroy")
33
+ end
34
+ end
35
+
36
+ def primary_key
37
+ 'version'
38
+ end
39
+
40
+ def name
41
+ 'Torque::PostgreSQL::VersionedCommand'
42
+ end
43
+
44
+ def table_name
45
+ [
46
+ ActiveRecord::Base.table_name_prefix,
47
+ PostgreSQL.config.versioned_commands.table_name,
48
+ ActiveRecord::Base.table_name_suffix,
49
+ ].join
50
+ end
51
+
52
+ def create_table
53
+ @pool.with_connection do |connection|
54
+ return if connection.table_exists?(table_name)
55
+
56
+ parent = @pool.schema_migration.table_name
57
+ connection.create_table(table_name, inherits: parent) do |t|
58
+ t.string :type, null: false, index: true
59
+ t.string :object_name, null: false, index: true
60
+ end
61
+ end
62
+ end
63
+
64
+ def drop_table
65
+ @pool.with_connection do |connection|
66
+ connection.drop_table table_name, if_exists: true
67
+ end
68
+ end
69
+
70
+ def count
71
+ return 0 unless table_exists?
72
+
73
+ sm = ::Arel::SelectManager.new(arel_table)
74
+ sm.project(*FN.count(::Arel.star))
75
+
76
+ @pool.with_connection do |connection|
77
+ connection.select_value(sm, "#{self.class} Count")
78
+ end
79
+ end
80
+
81
+ def table_exists?
82
+ @pool.with_connection { |connection| connection.data_source_exists?(table_name) }
83
+ end
84
+
85
+ def versions_of(type)
86
+ return [] unless table_exists?
87
+
88
+ sm = ::Arel::SelectManager.new(arel_table)
89
+ sm.project(arel_table['object_name'], FN.count(::Arel.star).as('version'))
90
+ sm.where(arel_table['type'].eq(type.to_s))
91
+ sm.group(arel_table['object_name'])
92
+ sm.order(arel_table['object_name'].asc)
93
+
94
+ @pool.with_connection do |connection|
95
+ connection.select_rows(sm, "#{name} Load")
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end