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
@@ -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
@@ -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 { connection.create_schema(:legacy) }
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