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
data/spec/tests/relation_spec.rb
CHANGED
@@ -54,4 +54,233 @@ RSpec.describe 'Relation', type: :helper do
|
|
54
54
|
end
|
55
55
|
end
|
56
56
|
|
57
|
+
context 'on joining series' do
|
58
|
+
let(:source) { Video.all }
|
59
|
+
|
60
|
+
it 'works' do
|
61
|
+
list = create_list(:video, 5)[1..4]
|
62
|
+
range = list.first.id..list.last.id
|
63
|
+
expect(source.join_series(range, with: :id).to_a).to eq(list)
|
64
|
+
expect(source.join_series(range, with: :id, step: 3).to_a).to eq([list.first, list.last])
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'produces the right SQL' do
|
68
|
+
sql = 'SELECT "videos".* FROM "videos"'
|
69
|
+
sql += ' INNER JOIN GENERATE_SERIES(1::integer, 10::integer)'
|
70
|
+
sql += ' AS series ON "series" = "videos"."id"'
|
71
|
+
expect(source.join_series(1..10, with: :id).to_sql).to eq(sql)
|
72
|
+
end
|
73
|
+
|
74
|
+
it 'can be renamed' do
|
75
|
+
sql = 'SELECT "videos".* FROM "videos"'
|
76
|
+
sql += ' INNER JOIN GENERATE_SERIES(1::integer, 10::integer)'
|
77
|
+
sql += ' AS seq ON "seq" = "videos"."id"'
|
78
|
+
expect(source.join_series(1..10, with: :id, as: :seq).to_sql).to eq(sql)
|
79
|
+
end
|
80
|
+
|
81
|
+
it 'can contain the step' do
|
82
|
+
sql = 'SELECT "videos".* FROM "videos"'
|
83
|
+
sql += ' INNER JOIN GENERATE_SERIES(1::integer, 10::integer, 2::integer)'
|
84
|
+
sql += ' AS series ON "series" = "videos"."id"'
|
85
|
+
expect(source.join_series(1..10, with: :id, step: 2).to_sql).to eq(sql)
|
86
|
+
end
|
87
|
+
|
88
|
+
it 'works with float values' do
|
89
|
+
sql = 'SELECT "videos".* FROM "videos"'
|
90
|
+
sql += ' INNER JOIN GENERATE_SERIES(1.0::numeric, 10.0::numeric, 0.5::numeric)'
|
91
|
+
sql += ' AS series ON "series" = "videos"."id"'
|
92
|
+
expect(source.join_series(1.0..10.0, with: :id, step: 0.5).to_sql).to eq(sql)
|
93
|
+
end
|
94
|
+
|
95
|
+
it 'works with time values' do
|
96
|
+
sql = 'SELECT "videos".* FROM "videos"'
|
97
|
+
sql += ' INNER JOIN GENERATE_SERIES('
|
98
|
+
sql += "'2025-01-01 00:00:00'::timestamp, '2025-01-01 01:00:00'::timestamp"
|
99
|
+
sql += ", 'PT1M'::interval"
|
100
|
+
sql += ') AS series ON "series" = "videos"."created_at"'
|
101
|
+
range = (Time.utc(2025, 1, 1, 0)..Time.utc(2025, 1, 1, 1))
|
102
|
+
expect(source.join_series(range, with: :created_at, step: 1.minute).to_sql).to eq(sql)
|
103
|
+
end
|
104
|
+
|
105
|
+
it 'works with date values' do
|
106
|
+
sql = 'SELECT "videos".* FROM "videos"'
|
107
|
+
sql += ' INNER JOIN GENERATE_SERIES('
|
108
|
+
sql += "'2025-01-01 00:00:00'::timestamp, '2025-01-02 00:00:00'::timestamp"
|
109
|
+
sql += ", 'P1D'::interval"
|
110
|
+
sql += ') AS series ON "series" = "videos"."created_at"'
|
111
|
+
range = (Date.new(2025, 1, 1)..Date.new(2025, 1, 2))
|
112
|
+
expect(source.join_series(range, with: :created_at, step: 1.day).to_sql).to eq(sql)
|
113
|
+
end
|
114
|
+
|
115
|
+
it 'works with time with zones values' do
|
116
|
+
sql = 'SELECT "videos".* FROM "videos"'
|
117
|
+
sql += ' INNER JOIN GENERATE_SERIES('
|
118
|
+
sql += "'2025-01-01 00:00:00'::timestamptz, '2025-01-01 01:00:00'::timestamptz"
|
119
|
+
sql += ", 'PT1M'::interval"
|
120
|
+
sql += ') AS series ON "series" = "videos"."id"'
|
121
|
+
left = ActiveSupport::TimeZone['UTC'].local(2025, 1, 1, 0)
|
122
|
+
right = ActiveSupport::TimeZone['UTC'].local(2025, 1, 1, 1)
|
123
|
+
expect(source.join_series(left..right, with: :id, step: 1.minute).to_sql).to eq(sql)
|
124
|
+
end
|
125
|
+
|
126
|
+
it 'can provide the additional time zone value' do
|
127
|
+
sql = 'SELECT "videos".* FROM "videos"'
|
128
|
+
sql += ' INNER JOIN GENERATE_SERIES('
|
129
|
+
sql += "'2025-01-01 00:00:00'::timestamptz, '2025-01-01 01:00:00'::timestamptz"
|
130
|
+
sql += ", 'PT1M'::interval, 'UTC'::text"
|
131
|
+
sql += ') AS series ON "series" = "videos"."id"'
|
132
|
+
left = ActiveSupport::TimeZone['UTC'].local(2025, 1, 1, 0)
|
133
|
+
right = ActiveSupport::TimeZone['UTC'].local(2025, 1, 1, 1)
|
134
|
+
|
135
|
+
query = source.join_series(left..right, with: :id, step: 1.minute, time_zone: 'UTC')
|
136
|
+
expect(query.to_sql).to eq(sql)
|
137
|
+
end
|
138
|
+
|
139
|
+
it 'can use other types of joins' do
|
140
|
+
sql = ' LEFT OUTER JOIN GENERATE_SERIES(1::integer, 10::integer)'
|
141
|
+
expect(source.join_series(1..10, with: :id, mode: :left).to_sql).to include(sql)
|
142
|
+
|
143
|
+
sql = ' RIGHT OUTER JOIN GENERATE_SERIES(1::integer, 10::integer)'
|
144
|
+
expect(source.join_series(1..10, with: :id, mode: :right).to_sql).to include(sql)
|
145
|
+
|
146
|
+
sql = ' FULL OUTER JOIN GENERATE_SERIES(1::integer, 10::integer)'
|
147
|
+
expect(source.join_series(1..10, with: :id, mode: :full).to_sql).to include(sql)
|
148
|
+
end
|
149
|
+
|
150
|
+
it 'supports a complex way of joining' do
|
151
|
+
query = source.join_series(1..10) do |series, table|
|
152
|
+
table['id'].lteq(series)
|
153
|
+
end
|
154
|
+
|
155
|
+
sql = 'SELECT "videos".* FROM "videos"'
|
156
|
+
sql += ' INNER JOIN GENERATE_SERIES(1::integer, 10::integer)'
|
157
|
+
sql += ' AS series ON "videos"."id" <= "series"'
|
158
|
+
expect(query.to_sql).to eq(sql)
|
159
|
+
end
|
160
|
+
|
161
|
+
it 'properly binds all provided values' do
|
162
|
+
query = source.join_series(1..10, with: :id, step: 2)
|
163
|
+
sql, binds = get_query_with_binds { query.load }
|
164
|
+
|
165
|
+
expect(sql).to include('GENERATE_SERIES($1::integer, $2::integer, $3::integer)')
|
166
|
+
expect(binds.map(&:value)).to eq([1, 10, 2])
|
167
|
+
end
|
168
|
+
|
169
|
+
context 'on errors' do
|
170
|
+
it 'does not support non-range values' do
|
171
|
+
expect do
|
172
|
+
source.join_series(1, with: :id)
|
173
|
+
end.to raise_error(ArgumentError, /Range/)
|
174
|
+
end
|
175
|
+
|
176
|
+
it 'does not support beginless ranges' do
|
177
|
+
expect do
|
178
|
+
source.join_series(..10, with: :id)
|
179
|
+
end.to raise_error(ArgumentError, /Beginless/)
|
180
|
+
end
|
181
|
+
|
182
|
+
it 'does not support endless ranges' do
|
183
|
+
expect do
|
184
|
+
source.join_series(1.., with: :id)
|
185
|
+
end.to raise_error(ArgumentError, /Endless/)
|
186
|
+
end
|
187
|
+
|
188
|
+
it 'requires a step when using non-numeric ranges' do
|
189
|
+
range = Date.new(2025, 1, 1)..Date.new(2025, 1, 10)
|
190
|
+
expect do
|
191
|
+
source.join_series(range, with: :id)
|
192
|
+
end.to raise_error(ArgumentError, /:step/)
|
193
|
+
end
|
194
|
+
|
195
|
+
it 'has strict type of join support' do
|
196
|
+
expect do
|
197
|
+
source.join_series(1..10, with: :id, mode: :cross)
|
198
|
+
end.to raise_error(ArgumentError, /join type/)
|
199
|
+
end
|
200
|
+
|
201
|
+
it 'requires a :with keyword' do
|
202
|
+
expect do
|
203
|
+
source.join_series(1..10)
|
204
|
+
end.to raise_error(ArgumentError, /:with/)
|
205
|
+
end
|
206
|
+
|
207
|
+
it 'does not support unexpected values' do
|
208
|
+
expect do
|
209
|
+
source.join_series(1..10, step: :other)
|
210
|
+
end.to raise_error(ArgumentError, /value type/)
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
context 'on buckets' do
|
216
|
+
let(:source) { User.all }
|
217
|
+
|
218
|
+
it 'produces the right query' do
|
219
|
+
query = source.buckets(:age, 0..50, count: 5)
|
220
|
+
sql, binds = get_query_with_binds { query.load }
|
221
|
+
|
222
|
+
expect(sql).to include(<<~SQL.squish)
|
223
|
+
WIDTH_BUCKET("users"."age", $1::numeric, $2::numeric, $3::integer) AS bucket
|
224
|
+
SQL
|
225
|
+
expect(binds.map(&:value)).to eq([0, 50, 5])
|
226
|
+
end
|
227
|
+
|
228
|
+
it 'can query records by buckets' do
|
229
|
+
list = [create(:user, age: 5), create(:user, age: 5), create(:user, age: 15)]
|
230
|
+
query = source.buckets(:age, 0..50, count: 5).records
|
231
|
+
|
232
|
+
expect(query).to be_a(Hash)
|
233
|
+
expect(query.keys).to match_array([0...10, 10...20])
|
234
|
+
expect(query[0...10]).to match_array([list[0], list[1]])
|
235
|
+
expect(query[10...20]).to match_array([list[2]])
|
236
|
+
end
|
237
|
+
|
238
|
+
it 'can query buckets of roles' do
|
239
|
+
list = [create(:user, role: :visitor)]
|
240
|
+
list << create(:user, role: :assistant)
|
241
|
+
list << create(:user, role: :manager)
|
242
|
+
query = source.buckets(:role, %w[assistant manager], cast: :roles).records
|
243
|
+
|
244
|
+
expect(query).to be_a(Hash)
|
245
|
+
expect(query.keys).to match_array([nil, 'assistant', 'manager'])
|
246
|
+
expect(query[nil]).to eq([list[0]])
|
247
|
+
expect(query['assistant']).to eq([list[1]])
|
248
|
+
expect(query['manager']).to eq([list[2]])
|
249
|
+
end
|
250
|
+
|
251
|
+
it 'works with calculations' do
|
252
|
+
list = [create(:user, age: 5), create(:user, age: 5), create(:user, age: 15)]
|
253
|
+
query = source.buckets(:age, 0..50, count: 5).count
|
254
|
+
|
255
|
+
expect(query).to be_a(Hash)
|
256
|
+
expect(query.keys).to match_array([0...10, 10...20])
|
257
|
+
expect(query[0...10]).to eq(2)
|
258
|
+
expect(query[10...20]).to eq(1)
|
259
|
+
end
|
260
|
+
|
261
|
+
it 'works with other types of calculations' do
|
262
|
+
list = [create(:user, age: 5), create(:user, age: 5), create(:user, age: 15)]
|
263
|
+
query = source.buckets(:age, 0..50, count: 5).sum(:age)
|
264
|
+
|
265
|
+
expect(query).to be_a(Hash)
|
266
|
+
expect(query.keys).to match_array([0...10, 10...20])
|
267
|
+
expect(query[0...10]).to eq(10)
|
268
|
+
expect(query[10...20]).to eq(15)
|
269
|
+
end
|
270
|
+
|
271
|
+
it 'work with joins and merge' do
|
272
|
+
list = [create(:user, age: 5), create(:user, age: 5), create(:user, age: 15)]
|
273
|
+
records = [create(:comment, user: list[0], content: 'Hello')]
|
274
|
+
records << create(:comment, user: list[1], content: 'World')
|
275
|
+
records << create(:comment, user: list[2], content: 'Test')
|
276
|
+
|
277
|
+
query = Comment.joins(:user).merge(source.buckets(:age, 0..50, count: 5)).records
|
278
|
+
|
279
|
+
expect(query).to be_a(Hash)
|
280
|
+
expect(query.keys).to match_array([0...10, 10...20])
|
281
|
+
expect(query[0...10]).to match_array([records[0], records[1]])
|
282
|
+
expect(query[10...20]).to match_array([records[2]])
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
57
286
|
end
|
data/spec/tests/schema_spec.rb
CHANGED
@@ -45,7 +45,10 @@ RSpec.describe 'Schema' do
|
|
45
45
|
context 'reverting' do
|
46
46
|
let(:migration) { ActiveRecord::Migration::Current.new('Testing') }
|
47
47
|
|
48
|
-
before
|
48
|
+
before do
|
49
|
+
allow_any_instance_of(ActiveRecord::Migration).to receive(:puts) # Disable messages
|
50
|
+
connection.create_schema(:legacy)
|
51
|
+
end
|
49
52
|
|
50
53
|
it 'reverts the creation of a schema' do
|
51
54
|
expect(connection.schema_exists?(:legacy, filtered: false)).to be_truthy
|