activerecord-spanner-adapter 1.2.2 → 1.3.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/.github/CODEOWNERS +1 -1
  3. data/.github/blunderbuss.yml +1 -1
  4. data/.release-please-manifest.json +1 -1
  5. data/CHANGELOG.md +22 -0
  6. data/README.md +4 -0
  7. data/acceptance/cases/migration/schema_dumper_test.rb +69 -0
  8. data/acceptance/cases/models/generated_column_test.rb +21 -7
  9. data/acceptance/cases/models/interleave_test.rb +36 -0
  10. data/acceptance/cases/models/logging_test.rb +53 -0
  11. data/acceptance/cases/models/query_test.rb +6 -1
  12. data/acceptance/cases/tasks/database_tasks_test.rb +407 -0
  13. data/acceptance/models/album_partial_disabled.rb +17 -0
  14. data/acceptance/schema/schema.rb +139 -134
  15. data/acceptance/test_helper.rb +2 -0
  16. data/examples/snippets/array-data-type/db/schema.rb +8 -3
  17. data/examples/snippets/bulk-insert/db/schema.rb +9 -4
  18. data/examples/snippets/commit-timestamp/db/schema.rb +11 -6
  19. data/examples/snippets/create-records/db/schema.rb +9 -4
  20. data/examples/snippets/date-data-type/db/schema.rb +8 -3
  21. data/examples/snippets/generated-column/db/schema.rb +6 -1
  22. data/examples/snippets/hints/db/schema.rb +6 -1
  23. data/examples/snippets/interleaved-tables/README.md +2 -2
  24. data/examples/snippets/interleaved-tables/db/schema.rb +5 -0
  25. data/examples/snippets/migrations/db/schema.rb +10 -5
  26. data/examples/snippets/mutations/db/schema.rb +9 -4
  27. data/examples/snippets/optimistic-locking/db/schema.rb +9 -4
  28. data/examples/snippets/partitioned-dml/db/schema.rb +5 -0
  29. data/examples/snippets/quickstart/db/schema.rb +9 -4
  30. data/examples/snippets/read-only-transactions/db/schema.rb +5 -0
  31. data/examples/snippets/read-write-transactions/db/schema.rb +9 -4
  32. data/examples/snippets/stale-reads/db/schema.rb +5 -0
  33. data/examples/snippets/timestamp-data-type/db/schema.rb +8 -3
  34. data/lib/active_record/connection_adapters/spanner/column.rb +23 -0
  35. data/lib/active_record/connection_adapters/spanner/database_statements.rb +7 -4
  36. data/lib/active_record/connection_adapters/spanner/quoting.rb +9 -0
  37. data/lib/active_record/connection_adapters/spanner/schema_creation.rb +17 -4
  38. data/lib/active_record/connection_adapters/spanner/schema_definitions.rb +11 -2
  39. data/lib/active_record/connection_adapters/spanner/schema_dumper.rb +56 -0
  40. data/lib/active_record/connection_adapters/spanner/schema_statements.rb +56 -9
  41. data/lib/active_record/connection_adapters/spanner/type_metadata.rb +19 -4
  42. data/lib/active_record/connection_adapters/spanner_adapter.rb +11 -0
  43. data/lib/active_record/tasks/spanner_database_tasks.rb +18 -4
  44. data/lib/active_record/type/spanner/spanner_active_record_converter.rb +10 -0
  45. data/lib/active_record/type/spanner/time.rb +10 -3
  46. data/lib/activerecord_spanner_adapter/base.rb +41 -27
  47. data/lib/activerecord_spanner_adapter/connection.rb +8 -3
  48. data/lib/activerecord_spanner_adapter/information_schema.rb +52 -3
  49. data/lib/activerecord_spanner_adapter/table/column.rb +7 -2
  50. data/lib/activerecord_spanner_adapter/version.rb +1 -1
  51. data/lib/arel/visitors/spanner.rb +8 -2
  52. metadata +8 -2
@@ -0,0 +1,407 @@
1
+ # Copyright 2022 Google LLC
2
+ #
3
+ # Use of this source code is governed by an MIT-style
4
+ # license that can be found in the LICENSE file or at
5
+ # https://opensource.org/licenses/MIT.
6
+
7
+ # frozen_string_literal: true
8
+
9
+ require "test_helper"
10
+ require "active_record/tasks/spanner_database_tasks"
11
+
12
+ module ActiveRecord
13
+ module Tasks
14
+ class DatabaseTasksTest < SpannerAdapter::TestCase
15
+ attr_reader :connector_config, :connection
16
+
17
+ def setup
18
+ @database_id = "ar-tasks-test-#{SecureRandom.hex 4}"
19
+ @connector_config = {
20
+ "adapter" => "spanner",
21
+ "emulator_host" => ENV["SPANNER_EMULATOR_HOST"],
22
+ "project" => ENV["SPANNER_TEST_PROJECT"],
23
+ "instance" => ENV["SPANNER_TEST_INSTANCE"],
24
+ "credentials" => ENV["SPANNER_TEST_KEYFILE"],
25
+ "database" => @database_id
26
+ }
27
+
28
+ create_database
29
+ ActiveRecord::Base.establish_connection connector_config
30
+ @connection = ActiveRecord::Base.connection
31
+
32
+ begin
33
+ @original_db_dir = ActiveRecord::Tasks::DatabaseTasks.db_dir
34
+ @original_env = ActiveRecord::Tasks::DatabaseTasks.env
35
+ rescue NameError
36
+ # ignore `NameError: uninitialized constant primary::Rails`
37
+ end
38
+
39
+ db_dir = File.expand_path "./db", __dir__
40
+ ActiveRecord::Tasks::DatabaseTasks.db_dir = db_dir
41
+ FileUtils.mkdir db_dir
42
+
43
+ ActiveRecord::Tasks::DatabaseTasks.env = "test"
44
+ end
45
+
46
+ def teardown
47
+ ActiveRecord::Base.connection_pool.disconnect!
48
+ FileUtils.rm_rf ActiveRecord::Tasks::DatabaseTasks.db_dir
49
+ ActiveRecord::Tasks::DatabaseTasks.db_dir = @original_db_dir
50
+ ActiveRecord::Tasks::DatabaseTasks.env = @original_env
51
+ end
52
+
53
+ def create_database
54
+ job = spanner_instance.create_database @database_id
55
+ job.wait_until_done!
56
+ if job.error?
57
+ raise "Error in creating database. Error code#{job.error.message}"
58
+ end
59
+ end
60
+
61
+ def drop_database
62
+ ActiveRecord::Base.connection_pool.disconnect!
63
+ ActiveRecordSpannerAdapter::Connection.reset_information_schemas!
64
+ spanner_instance.database(@database_id)&.drop
65
+ end
66
+
67
+ def test_structure_dump_and_load
68
+ require_relative "../../schema/schema"
69
+ create_tables_in_test_schema
70
+
71
+ db_config =
72
+ if ActiveRecord.version >= Gem::Version.new("6.1")
73
+ ActiveRecord::DatabaseConfigurations::HashConfig.new "test",
74
+ "primary",
75
+ connector_config
76
+ else
77
+ connector_config
78
+ end
79
+
80
+ tables = connection.tables.sort
81
+ config_name = "primary"
82
+ config_name = db_config.name if db_config.respond_to?(:name)
83
+ if ActiveRecord::Tasks::DatabaseTasks.respond_to?(:dump_filename)
84
+ filename = ActiveRecord::Tasks::DatabaseTasks.dump_filename(config_name, :sql)
85
+ elsif ActiveRecord::Tasks::DatabaseTasks.respond_to?(:schema_dump_path)
86
+ filename = ActiveRecord::Tasks::DatabaseTasks.schema_dump_path(db_config, :sql)
87
+ end
88
+ ActiveRecord::Tasks::DatabaseTasks.dump_schema db_config, :sql
89
+ sql = File.read(filename)
90
+ if ENV["SPANNER_EMULATOR_HOST"]
91
+ assert_equal expected_schema_sql_on_emulator, sql, msg = sql
92
+ else
93
+ assert_equal expected_schema_sql_on_production, sql, msg = sql
94
+ end
95
+ drop_database
96
+ create_database
97
+ ActiveRecord::Tasks::DatabaseTasks.load_schema db_config, :sql
98
+ assert_equal tables, connection.tables.sort
99
+ end
100
+
101
+ def expected_schema_sql_on_emulator
102
+ "CREATE TABLE all_types (
103
+ id INT64 NOT NULL,
104
+ col_string STRING(MAX),
105
+ col_int64 INT64,
106
+ col_float64 FLOAT64,
107
+ col_numeric NUMERIC,
108
+ col_bool BOOL,
109
+ col_bytes BYTES(MAX),
110
+ col_date DATE,
111
+ col_timestamp TIMESTAMP,
112
+ col_json JSON,
113
+ col_array_string ARRAY<STRING(MAX)>,
114
+ col_array_int64 ARRAY<INT64>,
115
+ col_array_float64 ARRAY<FLOAT64>,
116
+ col_array_numeric ARRAY<NUMERIC>,
117
+ col_array_bool ARRAY<BOOL>,
118
+ col_array_bytes ARRAY<BYTES(MAX)>,
119
+ col_array_date ARRAY<DATE>,
120
+ col_array_timestamp ARRAY<TIMESTAMP>,
121
+ col_array_json ARRAY<JSON>,
122
+ ) PRIMARY KEY(id);
123
+ CREATE TABLE firms (
124
+ id INT64 NOT NULL,
125
+ name STRING(MAX),
126
+ rating INT64,
127
+ description STRING(MAX),
128
+ account_id INT64,
129
+ ) PRIMARY KEY(id);
130
+ CREATE INDEX index_firms_on_account_id ON firms(account_id);
131
+ CREATE TABLE customers (
132
+ id INT64 NOT NULL,
133
+ name STRING(MAX),
134
+ ) PRIMARY KEY(id);
135
+ CREATE TABLE accounts (
136
+ id INT64 NOT NULL,
137
+ customer_id INT64,
138
+ firm_id INT64,
139
+ name STRING(MAX),
140
+ credit_limit INT64,
141
+ transactions_count INT64,
142
+ ) PRIMARY KEY(id);
143
+ CREATE TABLE transactions (
144
+ id INT64 NOT NULL,
145
+ amount FLOAT64,
146
+ account_id INT64,
147
+ ) PRIMARY KEY(id);
148
+ CREATE TABLE departments (
149
+ id INT64 NOT NULL,
150
+ name STRING(MAX),
151
+ resource_type STRING(255),
152
+ resource_id INT64,
153
+ ) PRIMARY KEY(id);
154
+ CREATE INDEX index_departments_on_resource ON departments(resource_type, resource_id);
155
+ CREATE TABLE member_types (
156
+ id INT64 NOT NULL,
157
+ name STRING(MAX),
158
+ ) PRIMARY KEY(id);
159
+ CREATE TABLE members (
160
+ id INT64 NOT NULL,
161
+ name STRING(MAX),
162
+ member_type_id INT64,
163
+ admittable_type STRING(255),
164
+ admittable_id INT64,
165
+ ) PRIMARY KEY(id);
166
+ CREATE TABLE memberships (
167
+ id INT64 NOT NULL,
168
+ joined_on TIMESTAMP,
169
+ club_id INT64,
170
+ member_id INT64,
171
+ favourite BOOL,
172
+ ) PRIMARY KEY(id);
173
+ CREATE TABLE clubs (
174
+ id INT64 NOT NULL,
175
+ name STRING(MAX),
176
+ ) PRIMARY KEY(id);
177
+ CREATE TABLE authors (
178
+ id INT64 NOT NULL,
179
+ name STRING(MAX) NOT NULL,
180
+ registered_date DATE,
181
+ organization_id INT64,
182
+ ) PRIMARY KEY(id);
183
+ CREATE TABLE posts (
184
+ id INT64 NOT NULL,
185
+ title STRING(MAX),
186
+ content STRING(MAX),
187
+ author_id INT64,
188
+ comments_count INT64,
189
+ post_date DATE,
190
+ published_time TIMESTAMP,
191
+ ) PRIMARY KEY(id);
192
+ CREATE INDEX index_posts_on_author_id ON posts(author_id);
193
+ CREATE TABLE comments (
194
+ id INT64 NOT NULL,
195
+ comment STRING(MAX),
196
+ post_id INT64,
197
+ CONSTRAINT fk_rails_2fd19c0db7 FOREIGN KEY(post_id) REFERENCES posts(id),
198
+ ) PRIMARY KEY(id);
199
+ CREATE TABLE addresses (
200
+ id INT64 NOT NULL,
201
+ line1 STRING(MAX),
202
+ postal_code STRING(MAX),
203
+ city STRING(MAX),
204
+ author_id INT64,
205
+ ) PRIMARY KEY(id);
206
+ CREATE TABLE organizations (
207
+ id INT64 NOT NULL,
208
+ name STRING(MAX),
209
+ last_updated TIMESTAMP OPTIONS (
210
+ allow_commit_timestamp = true
211
+ ),
212
+ ) PRIMARY KEY(id);
213
+ CREATE TABLE singers (
214
+ singerid INT64 NOT NULL,
215
+ first_name STRING(200),
216
+ last_name STRING(MAX),
217
+ tracks_count INT64,
218
+ lock_version INT64,
219
+ full_name STRING(MAX) AS (COALESCE(first_name || ' ', '') || last_name) STORED,
220
+ ) PRIMARY KEY(singerid);
221
+ CREATE TABLE albums (
222
+ albumid INT64 NOT NULL,
223
+ singerid INT64 NOT NULL,
224
+ title STRING(MAX),
225
+ lock_version INT64,
226
+ ) PRIMARY KEY(singerid, albumid),
227
+ INTERLEAVE IN PARENT singers ON DELETE NO ACTION;
228
+ CREATE TABLE tracks (
229
+ trackid INT64 NOT NULL,
230
+ singerid INT64 NOT NULL,
231
+ albumid INT64 NOT NULL,
232
+ title STRING(MAX),
233
+ duration NUMERIC,
234
+ lock_version INT64,
235
+ ) PRIMARY KEY(singerid, albumid, trackid),
236
+ INTERLEAVE IN PARENT albums ON DELETE CASCADE;
237
+ CREATE NULL_FILTERED INDEX index_tracks_on_singerid_and_albumid_and_title ON tracks(singerid, albumid, title), INTERLEAVE IN albums;
238
+ CREATE TABLE schema_migrations (
239
+ version STRING(MAX) NOT NULL,
240
+ ) PRIMARY KEY(version);
241
+ CREATE TABLE ar_internal_metadata (
242
+ key STRING(MAX) NOT NULL,
243
+ value STRING(MAX),
244
+ created_at TIMESTAMP NOT NULL,
245
+ updated_at TIMESTAMP NOT NULL,
246
+ ) PRIMARY KEY(key);
247
+ INSERT INTO `schema_migrations` (version) VALUES
248
+ ('1');
249
+
250
+ "
251
+ end
252
+
253
+ def expected_schema_sql_on_production
254
+ "CREATE TABLE accounts (
255
+ id INT64 NOT NULL,
256
+ customer_id INT64,
257
+ firm_id INT64,
258
+ name STRING(MAX),
259
+ credit_limit INT64,
260
+ transactions_count INT64,
261
+ ) PRIMARY KEY(id);
262
+ CREATE TABLE addresses (
263
+ id INT64 NOT NULL,
264
+ line1 STRING(MAX),
265
+ postal_code STRING(MAX),
266
+ city STRING(MAX),
267
+ author_id INT64,
268
+ ) PRIMARY KEY(id);
269
+ CREATE TABLE all_types (
270
+ id INT64 NOT NULL,
271
+ col_string STRING(MAX),
272
+ col_int64 INT64,
273
+ col_float64 FLOAT64,
274
+ col_numeric NUMERIC,
275
+ col_bool BOOL,
276
+ col_bytes BYTES(MAX),
277
+ col_date DATE,
278
+ col_timestamp TIMESTAMP,
279
+ col_json JSON,
280
+ col_array_string ARRAY<STRING(MAX)>,
281
+ col_array_int64 ARRAY<INT64>,
282
+ col_array_float64 ARRAY<FLOAT64>,
283
+ col_array_numeric ARRAY<NUMERIC>,
284
+ col_array_bool ARRAY<BOOL>,
285
+ col_array_bytes ARRAY<BYTES(MAX)>,
286
+ col_array_date ARRAY<DATE>,
287
+ col_array_timestamp ARRAY<TIMESTAMP>,
288
+ col_array_json ARRAY<JSON>,
289
+ ) PRIMARY KEY(id);
290
+ CREATE TABLE ar_internal_metadata (
291
+ key STRING(MAX) NOT NULL,
292
+ value STRING(MAX),
293
+ created_at TIMESTAMP NOT NULL,
294
+ updated_at TIMESTAMP NOT NULL,
295
+ ) PRIMARY KEY(key);
296
+ CREATE TABLE authors (
297
+ id INT64 NOT NULL,
298
+ name STRING(MAX) NOT NULL,
299
+ registered_date DATE,
300
+ organization_id INT64,
301
+ ) PRIMARY KEY(id);
302
+ CREATE TABLE clubs (
303
+ id INT64 NOT NULL,
304
+ name STRING(MAX),
305
+ ) PRIMARY KEY(id);
306
+ CREATE TABLE comments (
307
+ id INT64 NOT NULL,
308
+ comment STRING(MAX),
309
+ post_id INT64,
310
+ ) PRIMARY KEY(id);
311
+ CREATE TABLE customers (
312
+ id INT64 NOT NULL,
313
+ name STRING(MAX),
314
+ ) PRIMARY KEY(id);
315
+ CREATE TABLE departments (
316
+ id INT64 NOT NULL,
317
+ name STRING(MAX),
318
+ resource_type STRING(255),
319
+ resource_id INT64,
320
+ ) PRIMARY KEY(id);
321
+ CREATE INDEX index_departments_on_resource ON departments(resource_type, resource_id);
322
+ CREATE TABLE firms (
323
+ id INT64 NOT NULL,
324
+ name STRING(MAX),
325
+ rating INT64,
326
+ description STRING(MAX),
327
+ account_id INT64,
328
+ ) PRIMARY KEY(id);
329
+ CREATE INDEX index_firms_on_account_id ON firms(account_id);
330
+ CREATE TABLE member_types (
331
+ id INT64 NOT NULL,
332
+ name STRING(MAX),
333
+ ) PRIMARY KEY(id);
334
+ CREATE TABLE members (
335
+ id INT64 NOT NULL,
336
+ name STRING(MAX),
337
+ member_type_id INT64,
338
+ admittable_type STRING(255),
339
+ admittable_id INT64,
340
+ ) PRIMARY KEY(id);
341
+ CREATE TABLE memberships (
342
+ id INT64 NOT NULL,
343
+ joined_on TIMESTAMP,
344
+ club_id INT64,
345
+ member_id INT64,
346
+ favourite BOOL,
347
+ ) PRIMARY KEY(id);
348
+ CREATE TABLE organizations (
349
+ id INT64 NOT NULL,
350
+ name STRING(MAX),
351
+ last_updated TIMESTAMP OPTIONS (
352
+ allow_commit_timestamp = true
353
+ ),
354
+ ) PRIMARY KEY(id);
355
+ CREATE TABLE posts (
356
+ id INT64 NOT NULL,
357
+ title STRING(MAX),
358
+ content STRING(MAX),
359
+ author_id INT64,
360
+ comments_count INT64,
361
+ post_date DATE,
362
+ published_time TIMESTAMP,
363
+ ) PRIMARY KEY(id);
364
+ ALTER TABLE comments ADD CONSTRAINT fk_rails_2fd19c0db7 FOREIGN KEY(post_id) REFERENCES posts(id);
365
+ CREATE INDEX index_posts_on_author_id ON posts(author_id);
366
+ CREATE TABLE schema_migrations (
367
+ version STRING(MAX) NOT NULL,
368
+ ) PRIMARY KEY(version);
369
+ CREATE TABLE singers (
370
+ singerid INT64 NOT NULL,
371
+ first_name STRING(200),
372
+ last_name STRING(MAX),
373
+ tracks_count INT64,
374
+ lock_version INT64,
375
+ full_name STRING(MAX) AS (COALESCE(first_name || ' ', '') || last_name) STORED,
376
+ ) PRIMARY KEY(singerid);
377
+ CREATE TABLE albums (
378
+ albumid INT64 NOT NULL,
379
+ singerid INT64 NOT NULL,
380
+ title STRING(MAX),
381
+ lock_version INT64,
382
+ ) PRIMARY KEY(singerid, albumid),
383
+ INTERLEAVE IN PARENT singers ON DELETE NO ACTION;
384
+ CREATE TABLE tracks (
385
+ trackid INT64 NOT NULL,
386
+ singerid INT64 NOT NULL,
387
+ albumid INT64 NOT NULL,
388
+ title STRING(MAX),
389
+ duration NUMERIC,
390
+ lock_version INT64,
391
+ ) PRIMARY KEY(singerid, albumid, trackid),
392
+ INTERLEAVE IN PARENT albums ON DELETE CASCADE;
393
+ CREATE NULL_FILTERED INDEX index_tracks_on_singerid_and_albumid_and_title ON tracks(singerid, albumid, title), INTERLEAVE IN albums;
394
+ CREATE TABLE transactions (
395
+ id INT64 NOT NULL,
396
+ amount FLOAT64,
397
+ account_id INT64,
398
+ ) PRIMARY KEY(id);
399
+ INSERT INTO `schema_migrations` (version) VALUES
400
+ ('1');
401
+
402
+ "
403
+ end
404
+ end
405
+ end
406
+ end
407
+
@@ -0,0 +1,17 @@
1
+ # Copyright 2021 Google LLC
2
+ #
3
+ # Use of this source code is governed by an MIT-style
4
+ # license that can be found in the LICENSE file or at
5
+ # https://opensource.org/licenses/MIT.
6
+
7
+ # frozen_string_literal: true
8
+
9
+ require "models/album"
10
+
11
+ class AlbumPartialDisabled < Album
12
+ self.table_name = :albums
13
+
14
+ if ActiveRecord::VERSION::MAJOR >= 7
15
+ self.partial_inserts = false
16
+ end
17
+ end