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.
- 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 +111 -94
- data/lib/torque/postgresql/adapter/oid/array.rb +17 -0
- data/lib/torque/postgresql/adapter/oid/line.rb +2 -6
- data/lib/torque/postgresql/adapter/oid/range.rb +4 -4
- data/lib/torque/postgresql/adapter/oid.rb +1 -23
- data/lib/torque/postgresql/adapter/quoting.rb +13 -7
- data/lib/torque/postgresql/adapter/schema_creation.rb +7 -28
- data/lib/torque/postgresql/adapter/schema_definitions.rb +58 -0
- data/lib/torque/postgresql/adapter/schema_dumper.rb +136 -34
- data/lib/torque/postgresql/adapter/schema_overrides.rb +45 -0
- data/lib/torque/postgresql/adapter/schema_statements.rb +109 -49
- data/lib/torque/postgresql/arel/infix_operation.rb +15 -28
- data/lib/torque/postgresql/arel/nodes.rb +16 -2
- data/lib/torque/postgresql/arel/operations.rb +7 -1
- data/lib/torque/postgresql/arel/visitors.rb +7 -9
- data/lib/torque/postgresql/associations/association_scope.rb +23 -31
- data/lib/torque/postgresql/associations/belongs_to_many_association.rb +25 -0
- data/lib/torque/postgresql/associations/builder/belongs_to_many.rb +16 -0
- data/lib/torque/postgresql/attributes/builder/enum.rb +12 -9
- data/lib/torque/postgresql/attributes/builder/full_text_search.rb +109 -0
- data/lib/torque/postgresql/attributes/builder/period.rb +21 -21
- data/lib/torque/postgresql/attributes/builder.rb +49 -11
- data/lib/torque/postgresql/attributes/enum.rb +7 -7
- data/lib/torque/postgresql/attributes/enum_set.rb +7 -7
- data/lib/torque/postgresql/attributes/full_text_search.rb +19 -0
- data/lib/torque/postgresql/attributes/period.rb +2 -2
- data/lib/torque/postgresql/attributes.rb +0 -4
- data/lib/torque/postgresql/auxiliary_statement/recursive.rb +3 -3
- data/lib/torque/postgresql/base.rb +5 -11
- data/lib/torque/postgresql/collector.rb +1 -1
- data/lib/torque/postgresql/config.rb +129 -5
- data/lib/torque/postgresql/function.rb +94 -0
- data/lib/torque/postgresql/inheritance.rb +52 -36
- data/lib/torque/postgresql/predicate_builder/arel_attribute_handler.rb +33 -0
- data/lib/torque/postgresql/predicate_builder/array_handler.rb +47 -0
- data/lib/torque/postgresql/predicate_builder/enumerator_lazy_handler.rb +37 -0
- data/lib/torque/postgresql/predicate_builder/regexp_handler.rb +21 -0
- data/lib/torque/postgresql/predicate_builder.rb +35 -0
- data/lib/torque/postgresql/railtie.rb +137 -30
- data/lib/torque/postgresql/reflection/abstract_reflection.rb +12 -44
- data/lib/torque/postgresql/reflection/belongs_to_many_reflection.rb +4 -0
- data/lib/torque/postgresql/reflection/has_many_reflection.rb +4 -0
- data/lib/torque/postgresql/reflection/runtime_reflection.rb +1 -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 +22 -15
- 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 +24 -38
- data/lib/torque/postgresql/schema_cache.rb +6 -12
- 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/lib/torque/postgresql.rb +2 -1
- 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 +67 -0
- data/spec/mocks/cache_query.rb +21 -21
- data/spec/mocks/create_table.rb +6 -26
- data/spec/schema.rb +17 -12
- data/spec/spec_helper.rb +11 -2
- data/spec/tests/arel_spec.rb +32 -7
- data/spec/tests/auxiliary_statement_spec.rb +3 -3
- data/spec/tests/belongs_to_many_spec.rb +72 -5
- data/spec/tests/enum_set_spec.rb +12 -11
- data/spec/tests/enum_spec.rb +4 -2
- data/spec/tests/full_text_seach_test.rb +280 -0
- data/spec/tests/function_spec.rb +42 -0
- data/spec/tests/has_many_spec.rb +21 -8
- data/spec/tests/interval_spec.rb +1 -7
- data/spec/tests/period_spec.rb +61 -61
- data/spec/tests/predicate_builder_spec.rb +132 -0
- data/spec/tests/relation_spec.rb +229 -0
- data/spec/tests/schema_spec.rb +6 -9
- data/spec/tests/table_inheritance_spec.rb +25 -26
- data/spec/tests/versioned_commands_spec.rb +513 -0
- 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.
|
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
|
@@ -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 = [
|
19
|
-
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
|
+
|
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
|
27
|
+
def select_extra_values
|
28
|
+
@values.fetch(:select_extra, FROZEN_EMPTY_ARRAY)
|
29
|
+
end
|
26
30
|
# :nodoc:
|
27
|
-
def select_extra_values=(value)
|
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
|
-
|
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
|
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
|
-
|
139
|
-
|
140
|
-
|
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
|
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
|
-
|
153
|
-
|
154
|
-
|
155
|
-
ActiveRecord::
|
156
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
149
|
-
|
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
|
@@ -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
|