activerecord-spanner-adapter 0.7.0 → 1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f8f379074a93053e240a8dc13c74b3a3f85cf2b767830901fffd7bcf3bf2b3ba
4
- data.tar.gz: f5ac8e18ed5cc732b753c40b36add02b164a39ea14396fa720371b7d65645684
3
+ metadata.gz: e91546c93ad037bfaa453c4e80f0f9bd3ce5f6ced1e98cfd74ea92a8495996ec
4
+ data.tar.gz: 1180932c1f3644a0338b856f1b170ed5a4aa2735ac8410071981db28697c7bf4
5
5
  SHA512:
6
- metadata.gz: 39062a1c09f09cc4da8c1dd7d592905a61ae4b6e914aa88060572c9bda1dbe2626808ab92efcdef21ca18c61c7c116c3a4f55903c5829ff5044984e72e69763c
7
- data.tar.gz: 41a26004516514ec6573e554ba664824105a5f0687d4a831057ff69a92ba9d329fbb185d310d9e5e2d6f1449470aaf573873ffddb038063166aee64c360f10d1
6
+ metadata.gz: cbb029027fc34279f095294df20756d1bcfa2a077b45919d4447d4be8a29f7fb5bbb911896b8a5a3441766af735a0ec3ad38b4dfae2527ef401c1d4e9a39132e
7
+ data.tar.gz: 33a6509c950572f3880dc9d48c34dc35784909b46fc1d1226f2e20c57c77e734c53149f6620be17a8d3e85b4e086624bf545dae5bcc91a671cd1089cad885b36
data/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.0.0](https://www.github.com/googleapis/ruby-spanner-activerecord/compare/activerecord-spanner-adapter/v0.7.1...activerecord-spanner-adapter/v1.0.0) (2021-12-07)
4
+
5
+
6
+ ### Features
7
+
8
+ * release 1.0.0 ([#146](https://www.github.com/googleapis/ruby-spanner-activerecord/issues/146)) ([15ce616](https://www.github.com/googleapis/ruby-spanner-activerecord/commit/15ce6161fd052aaf6a1f078e65c0f836e8d640f3))
9
+
10
+ ### [0.7.1](https://www.github.com/googleapis/ruby-spanner-activerecord/compare/activerecord-spanner-adapter/v0.7.0...activerecord-spanner-adapter/v0.7.1) (2021-11-21)
11
+
12
+
13
+ ### Performance Improvements
14
+
15
+ * inline BeginTransaction with first statement in the transaction ([#139](https://www.github.com/googleapis/ruby-spanner-activerecord/issues/139)) ([ed88647](https://www.github.com/googleapis/ruby-spanner-activerecord/commit/ed88647a4df995b4f4221ac056f9204ee45ce90f))
16
+
3
17
  ## [0.7.0](https://www.github.com/googleapis/ruby-spanner-activerecord/compare/activerecord-spanner-adapter/v0.6.0...activerecord-spanner-adapter/v0.7.0) (2021-10-03)
4
18
 
5
19
 
@@ -24,7 +38,6 @@
24
38
  * Add support for NUMERIC type ([#73](https://www.github.com/googleapis/ruby-spanner-activerecord/issues/73)) ([176cf99](https://www.github.com/googleapis/ruby-spanner-activerecord/commit/176cf99dc8c26b3fd34d9e85d82a91dbde2b15c8))
25
39
  * Add support for ARRAY data type ([#86](https://www.github.com/googleapis/ruby-spanner-activerecord/issues/86)) ([0c66a62](https://www.github.com/googleapis/ruby-spanner-activerecord/commit/0c66a620cab968779de04faf48e03eec643ebea9))
26
40
  * google-cloud-spanner version upgraded to 2.2 ([#55](https://www.github.com/googleapis/ruby-spanner-activerecord/issues/55)) ([d7581d6](https://www.github.com/googleapis/ruby-spanner-activerecord/commit/d7581d60bd9a9e7b9989565449119f73e2caa694))
27
- * Support interleaved tables ([#83](https://www.github.com/googleapis/ruby-spanner-activerecord/issues/83)) ([82265f9](https://www.github.com/googleapis/ruby-spanner-activerecord/commit/82265f94ace79964639a2c65554714752be39724))
28
41
  * retry session not found ([#81](https://www.github.com/googleapis/ruby-spanner-activerecord/issues/81)) ([88fd3b7](https://www.github.com/googleapis/ruby-spanner-activerecord/commit/88fd3b70a03a90de2b667bb0f2e86efe5dc9328b))
29
42
  * support and test multiple ActiveRecord versions ([#107](https://www.github.com/googleapis/ruby-spanner-activerecord/issues/107)) ([db9d96c](https://www.github.com/googleapis/ruby-spanner-activerecord/commit/db9d96c44b9560f6904209df1a9aa42bf50a5844))
30
43
  * support DDL batches on connection ([#72](https://www.github.com/googleapis/ruby-spanner-activerecord/issues/72)) ([0d18cd4](https://www.github.com/googleapis/ruby-spanner-activerecord/commit/0d18cd49641bdb567012d6ac88b1909461d42551))
data/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  ![rubocop](https://github.com/googleapis/ruby-spanner-activerecord/workflows/rubocop/badge.svg)
6
6
 
7
- This project provides a Cloud Spanner adapter for ActiveRecord. It has the __Preview__ release status and supports the following versions:
7
+ This project provides a Cloud Spanner adapter for ActiveRecord. It supports the following versions:
8
8
 
9
9
  - ActiveRecord 6.0.x with Ruby 2.6 and 2.7.
10
10
  - ActiveRecord 6.1.x with Ruby 2.6 and higher.
@@ -69,7 +69,6 @@ Some noteworthy examples in the snippets directory:
69
69
  - [mutations](examples/snippets/mutations): Shows how you can use [mutations instead of DML](https://cloud.google.com/spanner/docs/dml-versus-mutations)
70
70
  for inserting, updating and deleting data in a Cloud Spanner database. Mutations can have a significant performance
71
71
  advantage compared to DML statements, but do not allow read-your-writes semantics during a transaction.
72
- - [interleaved-tables](examples/snippets/interleaved-tables): Shows how to create and work with a hierarchy of `INTERLEAVED IN` tables.
73
72
  - [array-data-type](examples/snippets/array-data-type): Shows how to work with `ARRAY` data types.
74
73
 
75
74
  ## Limitations
@@ -93,4 +92,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
93
92
 
94
93
  ## Code of Conduct
95
94
 
96
- Everyone interacting in the Activerecord::Spanner project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/googleapis/ruby-spanner-activerecord/blob/master/CODE_OF_CONDUCT.md).
95
+ Everyone interacting in the Activerecord::Spanner project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/googleapis/ruby-spanner-activerecord/blob/master/CODE_OF_CONDUCT.md).
@@ -208,27 +208,53 @@ module ActiveRecordSpannerAdapter
208
208
  self.current_transaction = nil
209
209
  end
210
210
 
211
- begin
212
- session.execute_query \
213
- sql,
214
- params: converted_params,
215
- types: types,
216
- transaction: transaction_selector || single_use_selector,
217
- seqno: (current_transaction&.next_sequence_number)
218
- rescue Google::Cloud::AbortedError
219
- # Mark the current transaction as aborted to prevent any unnecessary further requests on the transaction.
220
- current_transaction&.mark_aborted
221
- raise
222
- rescue Google::Cloud::NotFoundError => e
223
- if session_not_found?(e) || transaction_not_found?(e)
224
- reset!
225
- # Force a retry of the entire transaction if this statement was executed as part of a transaction.
226
- # Otherwise, just retry the statement itself.
227
- raise_aborted_err if current_transaction&.active?
228
- retry
229
- end
230
- raise
211
+ selector = transaction_selector || single_use_selector
212
+ execute_sql_request sql, converted_params, types, selector
213
+ end
214
+
215
+ def execute_sql_request sql, converted_params, types, selector
216
+ res = session.execute_query \
217
+ sql,
218
+ params: converted_params,
219
+ types: types,
220
+ transaction: selector,
221
+ seqno: (current_transaction&.next_sequence_number)
222
+ current_transaction.grpc_transaction = res.metadata.transaction \
223
+ if current_transaction && res&.metadata&.transaction
224
+ res
225
+ rescue Google::Cloud::AbortedError
226
+ # Mark the current transaction as aborted to prevent any unnecessary further requests on the transaction.
227
+ current_transaction&.mark_aborted
228
+ raise
229
+ rescue Google::Cloud::NotFoundError => e
230
+ if session_not_found?(e) || transaction_not_found?(e)
231
+ reset!
232
+ # Force a retry of the entire transaction if this statement was executed as part of a transaction.
233
+ # Otherwise, just retry the statement itself.
234
+ raise_aborted_err if current_transaction&.active?
235
+ retry
236
+ end
237
+ raise
238
+ rescue Google::Cloud::Error => e
239
+ # Check if it was the first statement in a transaction that included a BeginTransaction
240
+ # option in the request. If so, execute an explicit BeginTransaction and then retry the
241
+ # request without the BeginTransaction option.
242
+ if current_transaction && selector&.begin&.read_write
243
+ selector = create_transaction_after_failed_first_statement e
244
+ retry
231
245
  end
246
+ # It was not the first statement, so propagate the error.
247
+ raise
248
+ end
249
+
250
+ # Creates a transaction using a BeginTransaction RPC. This is used if the first statement of a
251
+ # transaction fails, as that also means that no transaction id was returned.
252
+ def create_transaction_after_failed_first_statement original_error
253
+ transaction = current_transaction.force_begin_read_write
254
+ Google::Spanner::V1::TransactionSelector.new id: transaction.transaction_id
255
+ rescue Google::Cloud::Error
256
+ # Raise the original error if the BeginTransaction RPC also fails.
257
+ raise original_error
232
258
  end
233
259
 
234
260
  # Transactions
@@ -62,7 +62,8 @@ module ActiveRecordSpannerAdapter
62
62
  end
63
63
 
64
64
  def table_columns table_name, column_name: nil
65
- sql = +"SELECT COLUMN_NAME, SPANNER_TYPE, IS_NULLABLE, COLUMN_DEFAULT, ORDINAL_POSITION"
65
+ sql = +"SELECT COLUMN_NAME, SPANNER_TYPE, IS_NULLABLE,"
66
+ sql << " CAST(COLUMN_DEFAULT AS STRING) AS COLUMN_DEFAULT, ORDINAL_POSITION"
66
67
  sql << " FROM INFORMATION_SCHEMA.COLUMNS"
67
68
  sql << " WHERE TABLE_NAME=%<table_name>s"
68
69
  sql << " AND COLUMN_NAME=%<column_name>s" if column_name
@@ -30,28 +30,37 @@ module ActiveRecordSpannerAdapter
30
30
  @mutations << mutation
31
31
  end
32
32
 
33
+ # Begins the transaction.
34
+ #
35
+ # Read-only and PDML transactions are started by executing a BeginTransaction RPC.
36
+ # Read/write transactions are not really started by this method, and instead a
37
+ # transaction selector is prepared that will be included with the first statement
38
+ # on the transaction.
33
39
  def begin
34
40
  raise "Nested transactions are not allowed" if @state != :INITIALIZED
35
41
  begin
36
- @grpc_transaction =
37
- case @isolation
38
- when Hash
39
- if @isolation[:timestamp]
40
- @connection.session.create_snapshot timestamp: @isolation[:timestamp]
41
- elsif @isolation[:staleness]
42
- @connection.session.create_snapshot staleness: @isolation[:staleness]
43
- elsif @isolation[:strong]
44
- @connection.session.create_snapshot strong: true
45
- else
46
- raise "Invalid snapshot argument: #{@isolation}"
47
- end
48
- when :read_only
49
- @connection.session.create_snapshot strong: true
50
- when :pdml
51
- @connection.session.create_pdml
42
+ case @isolation
43
+ when Hash
44
+ if @isolation[:timestamp]
45
+ @grpc_transaction = @connection.session.create_snapshot timestamp: @isolation[:timestamp]
46
+ elsif @isolation[:staleness]
47
+ @grpc_transaction = @connection.session.create_snapshot staleness: @isolation[:staleness]
48
+ elsif @isolation[:strong]
49
+ @grpc_transaction = @connection.session.create_snapshot strong: true
52
50
  else
53
- @connection.session.create_transaction
51
+ raise "Invalid snapshot argument: #{@isolation}"
54
52
  end
53
+ when :read_only
54
+ @grpc_transaction = @connection.session.create_snapshot strong: true
55
+ when :pdml
56
+ @grpc_transaction = @connection.session.create_pdml
57
+ else
58
+ @begin_transaction_selector = Google::Spanner::V1::TransactionSelector.new \
59
+ begin: Google::Spanner::V1::TransactionOptions.new(
60
+ read_write: Google::Spanner::V1::TransactionOptions::ReadWrite.new
61
+ )
62
+
63
+ end
55
64
  @state = :STARTED
56
65
  rescue Google::Cloud::NotFoundError => e
57
66
  if @connection.session_not_found? e
@@ -66,6 +75,12 @@ module ActiveRecordSpannerAdapter
66
75
  end
67
76
  end
68
77
 
78
+ # Forces a BeginTransaction RPC for a read/write transaction. This is used by a
79
+ # connection if the first statement of a transaction failed.
80
+ def force_begin_read_write
81
+ @grpc_transaction = @connection.session.create_transaction
82
+ end
83
+
69
84
  def next_sequence_number
70
85
  @sequence_number += 1 if @committable
71
86
  end
@@ -74,7 +89,10 @@ module ActiveRecordSpannerAdapter
74
89
  raise "This transaction is not active" unless active?
75
90
 
76
91
  begin
77
- @connection.session.commit_transaction @grpc_transaction, @mutations if @committable
92
+ # Start a transaction with an explicit BeginTransaction RPC if the transaction only contains mutations.
93
+ force_begin_read_write if @committable && !@mutations.empty? && !@grpc_transaction
94
+
95
+ @connection.session.commit_transaction @grpc_transaction, @mutations if @committable && @grpc_transaction
78
96
  @state = :COMMITTED
79
97
  rescue Google::Cloud::NotFoundError => e
80
98
  if @connection.session_not_found? e
@@ -93,7 +111,7 @@ module ActiveRecordSpannerAdapter
93
111
  def rollback
94
112
  # Allow rollback after abort and/or a failed commit.
95
113
  raise "This transaction is not active" unless active? || @state == :FAILED || @state == :ABORTED
96
- if active?
114
+ if active? && @grpc_transaction
97
115
  # We do a shoot-and-forget rollback here, as the error that caused the transaction to be rolled back could
98
116
  # also have invalidated the transaction (e.g. `Session not found`). If the rollback fails for any other
99
117
  # reason, we also do not need to retry it or propagate the error to the application, as the transaction will
@@ -113,11 +131,24 @@ module ActiveRecordSpannerAdapter
113
131
  @state = :ABORTED
114
132
  end
115
133
 
134
+ # Sets the underlying gRPC transaction to use for this Transaction.
135
+ # This is used for queries/DML statements that inlined the BeginTransaction option and returned
136
+ # a transaction in the metadata.
137
+ def grpc_transaction= grpc
138
+ @grpc_transaction = Google::Cloud::Spanner::Transaction.from_grpc grpc, @connection.session
139
+ end
140
+
116
141
  def transaction_selector
117
142
  return unless active?
118
143
 
119
- Google::Spanner::V1::TransactionSelector.new \
120
- id: @grpc_transaction.transaction_id
144
+ # Use the transaction that has been started by a BeginTransaction RPC or returned by a
145
+ # statement, if present.
146
+ return Google::Spanner::V1::TransactionSelector.new id: @grpc_transaction.transaction_id \
147
+ if @grpc_transaction
148
+
149
+ # Return a transaction selector that will instruct the statement to also start a transaction
150
+ # and return its id as a side effect.
151
+ @begin_transaction_selector
121
152
  end
122
153
  end
123
154
  end
@@ -5,5 +5,5 @@
5
5
  # https://opensource.org/licenses/MIT.
6
6
 
7
7
  module ActiveRecordSpannerAdapter
8
- VERSION = "0.7.0".freeze
8
+ VERSION = "1.0.0".freeze
9
9
  end
@@ -95,6 +95,10 @@ module Google
95
95
  end
96
96
  end
97
97
 
98
+ class Results
99
+ attr_reader :metadata
100
+ end
101
+
98
102
  class Transaction
99
103
  attr_accessor :seqno, :commit
100
104
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activerecord-spanner-adapter
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Google LLC
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-10-04 00:00:00.000000000 Z
11
+ date: 2021-12-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: google-cloud-spanner
@@ -378,16 +378,6 @@ files:
378
378
  - examples/snippets/hints/db/seeds.rb
379
379
  - examples/snippets/hints/models/album.rb
380
380
  - examples/snippets/hints/models/singer.rb
381
- - examples/snippets/interleaved-tables/README.md
382
- - examples/snippets/interleaved-tables/Rakefile
383
- - examples/snippets/interleaved-tables/application.rb
384
- - examples/snippets/interleaved-tables/config/database.yml
385
- - examples/snippets/interleaved-tables/db/migrate/01_create_tables.rb
386
- - examples/snippets/interleaved-tables/db/schema.rb
387
- - examples/snippets/interleaved-tables/db/seeds.rb
388
- - examples/snippets/interleaved-tables/models/album.rb
389
- - examples/snippets/interleaved-tables/models/singer.rb
390
- - examples/snippets/interleaved-tables/models/track.rb
391
381
  - examples/snippets/migrations/README.md
392
382
  - examples/snippets/migrations/Rakefile
393
383
  - examples/snippets/migrations/application.rb
@@ -1,152 +0,0 @@
1
- # Sample - Interleaved Tables
2
-
3
- This example shows how to use interleaved tables with the Spanner ActiveRecord adapter.
4
-
5
- See https://cloud.google.com/spanner/docs/schema-and-data-model#creating-interleaved-tables for more information
6
- on interleaved tables if you are not familiar with this concept.
7
-
8
- ## Creating Interleaved Tables in ActiveRecord
9
- You can create interleaved tables using migrations in ActiveRecord by using the following Spanner ActiveRecord specific
10
- methods that are defined on `TableDefinition`:
11
- * `interleave_in`: Specifies which parent table a child table should be interleaved in and optionally whether
12
- deletes of a parent record should automatically cascade delete all child records.
13
- * `parent_key`: Creates a column that is a reference to (a part of) the primary key of the parent table. Each child
14
- table must include all the primary key columns of the parent table as a `parent_key`.
15
-
16
- Cloud Spanner requires a child table to include the exact same primary key columns as the parent table in addition to
17
- the primary key column(s) of the child table. This means that the default `id` primary key column of ActiveRecord is
18
- not usable in combination with interleaved tables. Instead each primary key column should be prefixed with the table
19
- name of the table that it references, or use some other unique name.
20
-
21
- This example uses the following table schema:
22
-
23
- ```sql
24
- CREATE TABLE singers (
25
- singerid INT64 NOT NULL,
26
- first_name STRING(MAX),
27
- last_name STRING(MAX)
28
- ) PRIMARY KEY (singerid);
29
-
30
- CREATE TABLE albums (
31
- albumid INT64 NOT NULL,
32
- singerid INT64 NOT NULL,
33
- title STRING(MAX)
34
- ) PRIMARY KEY (singerid, albumid), INTERLEAVE IN PARENT singers;
35
-
36
- CREATE TABLE tracks (
37
- trackid INT64 NOT NULL,
38
- singerid INT64 NOT NULL,
39
- albumid INT64 NOT NULL,
40
- title STRING(MAX),
41
- duration NUMERIC
42
- ) PRIMARY KEY (singerid, albumid, trackid), INTERLEAVE IN PARENT albums ON DELETE CASCADE;
43
- ```
44
-
45
- This schema can be created in ActiveRecord as follows:
46
-
47
- ```ruby
48
- create_table :singers, id: false do |t|
49
- # Explicitly define the primary key with a custom name to prevent all primary key columns from being named `id`.
50
- t.primary_key :singerid
51
- t.string :first_name
52
- t.string :last_name
53
- end
54
-
55
- create_table :albums, id: false do |t|
56
- # Interleave the `albums` table in the parent table `singers`.
57
- t.interleave_in :singers
58
- t.primary_key :albumid
59
- # `singerid` is defined as a `parent_key` which makes it a part of the primary key in the table definition, but
60
- # it is not presented to ActiveRecord as part of the primary key, to prevent ActiveRecord from considering this
61
- # to be an entity with a composite primary key (which is not supported by ActiveRecord).
62
- t.parent_key :singerid
63
- t.string :title
64
- end
65
-
66
- create_table :tracks, id: false do |t|
67
- # Interleave the `tracks` table in the parent table `albums` and cascade delete all tracks that belong to an
68
- # album when an album is deleted.
69
- t.interleave_in :albums, :cascade
70
- # `trackid` is considered the only primary key column by ActiveRecord.
71
- t.primary_key :trackid
72
- # `singerid` and `albumid` form the parent key of `tracks`. These are part of the primary key definition in the
73
- # database, but are presented as parent keys to ActiveRecord.
74
- t.parent_key :singerid
75
- t.parent_key :albumid
76
- t.string :title
77
- t.numeric :duration
78
- end
79
- ```
80
-
81
- ## Models for Interleaved Tables
82
- An interleaved table parent/child relationship must be modelled as a `belongs_to`/`has_many` association in
83
- ActiveRecord. As the columns that are used to reference a parent record use a custom column name, it is required to also
84
- include the custom column name in the `belongs_to` and `has_many` definitions.
85
-
86
- Instances of these models can be used in the same way as any other association in ActiveRecord, but with a couple of
87
- inherent limitations:
88
- * It is not possible to change the parent record of a child record. For instance, changing the singer of an album in the
89
- above example is impossible, as Cloud Spanner does not allow such an update.
90
- * It is not possible to de-reference a parent record by setting it to null.
91
- * It is only possible to delete a parent record with existing child records, if the child records are also deleted. This
92
- can be done by enabling ON DELETE CASCADE in Cloud Spanner, or by deleting the child records using ActiveRecord.
93
-
94
- ### Example Models
95
-
96
- ```ruby
97
- class Singer < ActiveRecord::Base
98
- # `albums` is defined as INTERLEAVE IN PARENT `singers`. The primary key of `albums` is (`singerid`, `albumid`), but
99
- # only `albumid` is used by ActiveRecord as the primary key. The `singerid` column is defined as a `parent_key` of
100
- # `albums` (see also the `db/migrate/01_create_tables.rb` file).
101
- has_many :albums, foreign_key: "singerid"
102
-
103
- # `tracks` is defined as INTERLEAVE IN PARENT `albums`. The primary key of `tracks` is
104
- # (`singerid`, `albumid`, `trackid`), but only `trackid` is used by ActiveRecord as the primary key. The `singerid`
105
- # and `albumid` columns are defined as `parent_key` of `tracks` (see also the `db/migrate/01_create_tables.rb` file).
106
- # The `singerid` column can therefore be used to associate tracks with a singer without the need to go through albums.
107
- # Note also that the inclusion of `singerid` as a column in `tracks` is required in order to make `tracks` a child
108
- # table of `albums` which has primary key (`singerid`, `albumid`).
109
- has_many :tracks, foreign_key: "singerid"
110
- end
111
-
112
- class Album < ActiveRecord::Base
113
- # `albums` is defined as INTERLEAVE IN PARENT `singers`. The primary key of `singers` is `singerid`.
114
- belongs_to :singer, foreign_key: "singerid"
115
-
116
- # `tracks` is defined as INTERLEAVE IN PARENT `albums`. The primary key of `albums` is (`singerid`, `albumid`), but
117
- # only `albumid` is used by ActiveRecord as the primary key. The `singerid` column is defined as a `parent_key` of
118
- # `albums` (see also the `db/migrate/01_create_tables.rb` file).
119
- has_many :tracks, foreign_key: "albumid"
120
- end
121
-
122
- class Track < ActiveRecord::Base
123
- # `tracks` is defined as INTERLEAVE IN PARENT `albums`. The primary key of `albums` is ()`singerid`, `albumid`).
124
- belongs_to :album, foreign_key: "albumid"
125
-
126
- # `tracks` also has a `singerid` column should be used to associate a Track with a Singer.
127
- belongs_to :singer, foreign_key: "singerid"
128
-
129
- # Override the default initialize method to automatically set the singer attribute when an album is given.
130
- def initialize attributes = nil
131
- super
132
- self.singer ||= album&.singer
133
- end
134
-
135
- def album=value
136
- super
137
- # Ensure the singer of this track is equal to the singer of the album that is set.
138
- self.singer = value&.singer
139
- end
140
- end
141
- ```
142
-
143
- ## Running the Sample
144
-
145
- The sample will automatically start a Spanner Emulator in a docker container and execute the sample
146
- against that emulator. The emulator will automatically be stopped when the application finishes.
147
-
148
- Run the application with the command
149
-
150
- ```bash
151
- bundle exec rake run
152
- ```
@@ -1,13 +0,0 @@
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
- require_relative "../config/environment"
8
- require "sinatra/activerecord/rake"
9
-
10
- desc "Sample showing how to work with interleaved tables in ActiveRecord."
11
- task :run do
12
- Dir.chdir("..") { sh "bundle exec rake run[interleaved-tables]" }
13
- end
@@ -1,109 +0,0 @@
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
- require "io/console"
8
- require_relative "../config/environment"
9
- require_relative "models/singer"
10
- require_relative "models/album"
11
- require_relative "models/track"
12
-
13
- class Application
14
- def self.run
15
- # List all singers, albums and tracks.
16
- list_singers_albums_tracks
17
-
18
- # Create a new album with some tracks.
19
- create_new_album
20
-
21
- # Try to update the singer of an album. This is not possible as albums are interleaved in singers.
22
- update_singer_of_album
23
-
24
- # Try to delete a singer that has at least one album. This is NOT possible as albums is NOT marked with
25
- # ON DELETE CASCADE.
26
- delete_singer_with_albums
27
-
28
- # Try to delete an album that has at least one track. This IS possible as tracks IS marked with
29
- # ON DELETE CASCADE.
30
- delete_album_with_tracks
31
-
32
- puts ""
33
- puts "Press any key to end the application"
34
- STDIN.getch
35
- end
36
-
37
- def self.list_singers_albums_tracks
38
- puts ""
39
- puts "Listing all singers with corresponding albums and tracks"
40
- Singer.all.order("last_name, first_name").each do |singer|
41
- puts "#{singer.first_name} #{singer.last_name} has #{singer.albums.count} albums:"
42
- singer.albums.order("title").each do |album|
43
- puts " #{album.title} has #{album.tracks.count} tracks:"
44
- album.tracks.each do |track|
45
- puts " #{track.title}"
46
- end
47
- end
48
- end
49
- end
50
-
51
- def self.create_new_album
52
- # Create a new album with some tracks.
53
- puts ""
54
- singer = Singer.all.sample
55
- puts "Creating a new album for #{singer.first_name} #{singer.last_name}"
56
- album = singer.albums.build title: "New Title"
57
- album.tracks.build title: "Track 1", duration: 3.5, singer: singer
58
- album.tracks.build title: "Track 2", duration: 3.6, singer: singer
59
- # This will save the album and corresponding tracks in one transaction.
60
- album.save!
61
-
62
- album.reload
63
- puts "Album #{album.title} has #{album.tracks.count} tracks:"
64
- album.tracks.order("title").each do |track|
65
- puts " #{track.title} with duration #{track.duration}"
66
- end
67
- end
68
-
69
- def self.update_singer_of_album
70
- # It is not possible to change the singer of an album or the album of a track. This is because the associations
71
- # between these are not traditional foreign keys, but an immutable parent-child relationship.
72
- album = Album.all.sample
73
- new_singer = Singer.all.except(album.singer).sample
74
- # This will fail as we cannot assign a new singer to an album as it is an INTERLEAVE IN PARENT relationship.
75
- begin
76
- album.update! singer: new_singer
77
- raise StandardError, "Unexpected error: Updating the singer of an album should not be possible."
78
- rescue ActiveRecord::StatementInvalid
79
- puts ""
80
- puts "Failed to update the singer of an album. This is expected."
81
- end
82
- end
83
-
84
- def self.delete_singer_with_albums
85
- # Deleting a singer that has albums is not possible, as the INTERLEAVE IN PARENT of albums is not marked with
86
- # ON DELETE CASCADE.
87
- singer = Album.all.sample.singer
88
- begin
89
- singer.delete
90
- raise StandardError, "Unexpected error: Updating the singer of an album should not be possible."
91
- rescue ActiveRecord::StatementInvalid
92
- puts ""
93
- puts "Failed to delete a singer that has #{singer.albums.count} albums. This is expected."
94
- end
95
- end
96
-
97
- def self.delete_album_with_tracks
98
- # Deleting an album with tracks is supported, as the INTERLEAVE IN PARENT relationship between tracks and albums is
99
- # marked with ON DELETE CASCADE.
100
- puts ""
101
- puts "Total track count: #{Track.count}"
102
- album = Track.all.sample.album
103
- puts "Deleting album #{album.title} with #{album.tracks.count} tracks"
104
- album.delete
105
- puts "Total track count after deletion: #{Track.count}"
106
- end
107
- end
108
-
109
- Application.run
@@ -1,8 +0,0 @@
1
- development:
2
- adapter: spanner
3
- emulator_host: localhost:9010
4
- project: test-project
5
- instance: test-instance
6
- database: testdb
7
- pool: 5
8
- timeout: 5000
@@ -1,44 +0,0 @@
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
- class CreateTables < ActiveRecord::Migration[6.0]
8
- def change
9
- # Execute the entire migration as one DDL batch.
10
- connection.ddl_batch do
11
- create_table :singers, id: false do |t|
12
- # Explicitly define the primary key with a custom name to prevent all primary key columns from being named `id`.
13
- t.primary_key :singerid
14
- t.string :first_name
15
- t.string :last_name
16
- end
17
-
18
- create_table :albums, id: false do |t|
19
- # Interleave the `albums` table in the parent table `singers`.
20
- t.interleave_in :singers
21
- t.primary_key :albumid
22
- # `singerid` is defined as a `parent_key` which makes it a part of the primary key in the table definition, but
23
- # it is not presented to ActiveRecord as part of the primary key, to prevent ActiveRecord from considering this
24
- # to be an entity with a composite primary key (which is not supported by ActiveRecord).
25
- t.parent_key :singerid
26
- t.string :title
27
- end
28
-
29
- create_table :tracks, id: false do |t|
30
- # Interleave the `tracks` table in the parent table `albums` and cascade delete all tracks that belong to an
31
- # album when an album is deleted.
32
- t.interleave_in :albums, :cascade
33
- # `trackid` is considered the only primary key column by ActiveRecord.
34
- t.primary_key :trackid
35
- # `singerid` and `albumid` form the parent key of `tracks`. These are part of the primary key definition in the
36
- # database, but are presented as parent keys to ActiveRecord.
37
- t.parent_key :singerid
38
- t.parent_key :albumid
39
- t.string :title
40
- t.numeric :duration
41
- end
42
- end
43
- end
44
- end
@@ -1,32 +0,0 @@
1
- # This file is auto-generated from the current state of the database. Instead
2
- # of editing this file, please use the migrations feature of Active Record to
3
- # incrementally modify your database, and then regenerate this schema definition.
4
- #
5
- # This file is the source Rails uses to define your schema when running `rails
6
- # db:schema:load`. When creating a new database, `rails db:schema:load` tends to
7
- # be faster and is potentially less error prone than running all of your
8
- # migrations from scratch. Old migrations may fail to apply correctly if those
9
- # migrations use external dependencies or application code.
10
- #
11
- # It's strongly recommended that you check this file into your version control system.
12
-
13
- ActiveRecord::Schema.define(version: 1) do
14
-
15
- create_table "albums", primary_key: "albumid", force: :cascade do |t|
16
- t.integer "singerid", limit: 8, null: false
17
- t.string "title"
18
- end
19
-
20
- create_table "singers", primary_key: "singerid", force: :cascade do |t|
21
- t.string "first_name"
22
- t.string "last_name"
23
- end
24
-
25
- create_table "tracks", primary_key: "trackid", force: :cascade do |t|
26
- t.integer "singerid", limit: 8, null: false
27
- t.integer "albumid", limit: 8, null: false
28
- t.string "title"
29
- t.decimal "duration"
30
- end
31
-
32
- end
@@ -1,40 +0,0 @@
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
- require_relative "../../config/environment.rb"
8
- require_relative "../models/singer"
9
- require_relative "../models/album"
10
- require_relative "../models/track"
11
-
12
- first_names = %w[Pete Alice John Ethel Trudy Naomi Wendy Ruben Thomas Elly]
13
- last_names = %w[Wendelson Allison Peterson Johnson Henderson Ericsson Aronson Tennet Courtou]
14
-
15
- adjectives = %w[daily happy blue generous cooked bad open]
16
- nouns = %w[windows potatoes bank street tree glass bottle]
17
-
18
- verbs = %w[operate waste package chew yield express polish stress slip want cough campaign cultivate report park refer]
19
- adverbs = %w[directly right hopefully personally economically privately supposedly consequently fully later urgently]
20
-
21
- durations = [3.14, 5.4, 3.3, 4.1, 5.0, 3.2, 3.0, 3.5, 4.0, 4.5, 5.5, 6.0]
22
-
23
- # This ensures all the records are inserted using one read/write transaction that will use mutations instead of DML.
24
- ActiveRecord::Base.transaction isolation: :buffered_mutations do
25
- singers = []
26
- 5.times do
27
- singers << Singer.create(first_name: first_names.sample, last_name: last_names.sample)
28
- end
29
-
30
- albums = []
31
- 20.times do
32
- singer = singers.sample
33
- albums << Album.create(title: "#{adjectives.sample} #{nouns.sample}", singer: singer)
34
- end
35
-
36
- 200.times do
37
- album = albums.sample
38
- Track.create title: "#{verbs.sample} #{adverbs.sample}", duration: durations.sample, album: album
39
- end
40
- end
@@ -1,15 +0,0 @@
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
- class Album < ActiveRecord::Base
8
- # `albums` is defined as INTERLEAVE IN PARENT `singers`. The primary key of `singers` is `singerid`.
9
- belongs_to :singer, foreign_key: "singerid"
10
-
11
- # `tracks` is defined as INTERLEAVE IN PARENT `albums`. The primary key of `albums` is (`singerid`, `albumid`), but
12
- # only `albumid` is used by ActiveRecord as the primary key. The `singerid` column is defined as a `parent_key` of
13
- # `albums` (see also the `db/migrate/01_create_tables.rb` file).
14
- has_many :tracks, foreign_key: "albumid"
15
- end
@@ -1,20 +0,0 @@
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
- class Singer < ActiveRecord::Base
8
- # `albums` is defined as INTERLEAVE IN PARENT `singers`. The primary key of `albums` is (`singerid`, `albumid`), but
9
- # only `albumid` is used by ActiveRecord as the primary key. The `singerid` column is defined as a `parent_key` of
10
- # `albums` (see also the `db/migrate/01_create_tables.rb` file).
11
- has_many :albums, foreign_key: "singerid"
12
-
13
- # `tracks` is defined as INTERLEAVE IN PARENT `albums`. The primary key of `tracks` is
14
- # (`singerid`, `albumid`, `trackid`), but only `trackid` is used by ActiveRecord as the primary key. The `singerid`
15
- # and `albumid` columns are defined as `parent_key` of `tracks` (see also the `db/migrate/01_create_tables.rb` file).
16
- # The `singerid` column can therefore be used to associate tracks with a singer without the need to go through albums.
17
- # Note also that the inclusion of `singerid` as a column in `tracks` is required in order to make `tracks` a child
18
- # table of `albums` which has primary key (`singerid`, `albumid`).
19
- has_many :tracks, foreign_key: "singerid"
20
- end
@@ -1,25 +0,0 @@
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
- class Track < ActiveRecord::Base
8
- # `tracks` is defined as INTERLEAVE IN PARENT `albums`. The primary key of `albums` is ()`singerid`, `albumid`).
9
- belongs_to :album, foreign_key: "albumid"
10
-
11
- # `tracks` also has a `singerid` column should be used to associate a Track with a Singer.
12
- belongs_to :singer, foreign_key: "singerid"
13
-
14
- # Override the default initialize method to automatically set the singer attribute when an album is given.
15
- def initialize attributes = nil
16
- super
17
- self.singer ||= album&.singer
18
- end
19
-
20
- def album=value
21
- super
22
- # Ensure the singer of this track is equal to the singer of the album that is set.
23
- self.singer = value&.singer
24
- end
25
- end