activerecord-spanner-adapter 0.1.0 → 0.3.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
  SHA1:
3
- metadata.gz: 8ff8fa6b4d8ed65a702bde98a87cd0c8f2297d66
4
- data.tar.gz: bfce942a21417f01f4aec9c211c7c27fc7a5b278
3
+ metadata.gz: 98b65b0b5292ed6027d2dd1305a9794565438374
4
+ data.tar.gz: ff0fc0355000390592dd1c916d8eaedfdb4c24e9
5
5
  SHA512:
6
- metadata.gz: 54da92b648a12aa2f8add6dc52f03c1255fd529c178aadfcfb112520d8f6c918095bcdd6c33907f848da124edcf6a390e5c39133c0ebc35ce2baf9b53fb6effd
7
- data.tar.gz: 88072ff07c34f9d9d2a3314a0c8b258bc3bb34433e811c6a825775ffa663852ca3a01bf30e992c94ce2e029ce4c2ec87f1967175532c34af4231787459f1b5a8
6
+ metadata.gz: 25ec93d1aa400524c5feb62efbb0bf1dce21468fdee84c5ba8d701cdeedd55b91880ff27880e46c6a19f5bd6d5791d289458a623a675d1089b1c99e1b892b92c
7
+ data.tar.gz: 83cd0c3ecec42157a2f0675c1c77900045c74dee42ba83a44474ce966e0dab83b1b5f8fbfb57a8d9eab60b0bf8ebdac3d1177ba4a81faf2734ebb9e62c20142d
data/Gemfile CHANGED
@@ -3,6 +3,5 @@ source 'https://rubygems.org'
3
3
  # Specify your gem's dependencies in activerecord-spanner-adapter.gemspec
4
4
  gemspec
5
5
 
6
- gem 'google-cloud-spanner', path: 'vendor/gcloud-ruby/google-cloud-spanner'
7
6
  gem 'pry'
8
7
  gem 'pry-byebug'
data/Rakefile CHANGED
@@ -1,10 +1,6 @@
1
1
  require "bundler/gem_tasks"
2
- require "rake/testtask"
2
+ require "rspec/core/rake_task"
3
3
 
4
- Rake::TestTask.new(:test) do |t|
5
- t.libs << "test"
6
- t.libs << "lib"
7
- t.test_files = FileList['test/**/*_test.rb']
8
- end
4
+ RSpec::Core::RakeTask.new("spec")
5
+ task :default => :spec
9
6
 
10
- task :default => :test
@@ -19,9 +19,9 @@ Gem::Specification.new do |spec|
19
19
  spec.require_paths = ["lib"]
20
20
 
21
21
  spec.add_dependency 'activerecord', "~> 5.0"
22
- spec.add_dependency 'google-cloud-spanner'
22
+ spec.add_dependency 'google-cloud-spanner', "~> 0.23"
23
23
  spec.add_dependency 'google-gax', '~> 0.8'
24
24
  spec.add_development_dependency "bundler", "~> 1.14"
25
25
  spec.add_development_dependency "rake", "~> 10.0"
26
- spec.add_development_dependency "minitest", "~> 5.0"
26
+ spec.add_development_dependency "rspec", "~> 3.6.0"
27
27
  end
@@ -0,0 +1,190 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ module Spanner
4
+ # raised on unsupported operations in a certain transaction state.
5
+ class TransactionStateError < ActiveRecordError
6
+ end
7
+
8
+ class InitialPhaseClient
9
+ def initialize(client)
10
+ @client = client
11
+ @next = self
12
+ end
13
+
14
+ attr_reader :next
15
+
16
+ delegate :insert, :update, :delete, :execute, :close, to: :client
17
+
18
+ def begin_transaction
19
+ @next = ReadWriteTransactionClient.begin(client)
20
+ end
21
+
22
+ def begin_snapshot(**options)
23
+ @next = SnapshotClient.begin(client, **options)
24
+ end
25
+
26
+ %w[ commit rollback ].each do |name|
27
+ class_eval(<<-"EOS", __FILE__, __LINE__+1)
28
+ def #{name}
29
+ raise TransactionStateError, 'not in a transaction'
30
+ end
31
+ EOS
32
+ end
33
+
34
+ private
35
+ attr_reader :client
36
+ end
37
+
38
+ class ReadWriteTransactionClient
39
+ def initialize(client, tx, commit_op, rollback_op)
40
+ @next = self
41
+ @client = client
42
+ @tx = tx
43
+
44
+ @commit_op = commit_op
45
+ @rollback_op = rollback_op
46
+ end
47
+
48
+ class << self
49
+ def begin(client)
50
+ enum = enum_for(:transaction, client, deadline: 0)
51
+ closer = ->{
52
+ begin
53
+ enum.next
54
+ rescue StopIteration => stop
55
+ stop.result
56
+ end
57
+ }
58
+
59
+ begin
60
+ tx, rollback_op = enum.next
61
+ on_rollback = ->{
62
+ begin
63
+ rollback_op.call
64
+ ensure
65
+ closer.call
66
+ end
67
+ }
68
+ return ReadPhaseClient.new(client, tx, closer, on_rollback)
69
+ rescue
70
+ closer.call
71
+ raise
72
+ end
73
+ end
74
+
75
+ private
76
+ def transaction(client, *args)
77
+ client.transaction(*args) do |tx|
78
+ rollbacked = false
79
+ yield tx, ->{ rollbacked = true }
80
+ raise Google::Cloud::Spanner::Rollback if rollbacked
81
+ end
82
+ end
83
+ end
84
+
85
+ attr_reader :next
86
+
87
+ delegate :close, to: :client
88
+ delegate :execute, :insert, :update, :delete, to: :tx
89
+
90
+ def commit
91
+ result = @commit_op.call
92
+ result.tap do
93
+ @next = InitialPhaseClient.new(@client)
94
+ end
95
+ end
96
+
97
+ def rollback
98
+ @rollback_op.call.tap do
99
+ @next = InitialPhaseClient.new(@client)
100
+ end
101
+ end
102
+
103
+ %w[ begin_transaction begin_snapshot ].each do |name|
104
+ class_eval(<<-"EOS", __FILE__, __LINE__+1)
105
+ def #{name}(*args)
106
+ raise TransactionStateError, 'already in a transaction'
107
+ end
108
+ EOS
109
+ end
110
+
111
+ private
112
+ attr_reader :client, :tx # :nodoc:
113
+ end
114
+
115
+ class ReadPhaseClient < ReadWriteTransactionClient
116
+ %w[ insert update delete ].each do |name|
117
+ class_eval(<<-"EOS", __FILE__, __LINE__+1)
118
+ def #{name}(*args)
119
+ super.tap { transit_to_write_phase }
120
+ end
121
+ EOS
122
+ end
123
+
124
+ private
125
+ def transit_to_write_phase
126
+ @next = WritePhaseClient.new(@client, @tx, @commit_op, @rollback_op)
127
+ end
128
+ end
129
+
130
+ class WritePhaseClient < ReadWriteTransactionClient
131
+ def execute(*args)
132
+ raise TransactionStateError, "cannot read after write within a transaction"
133
+ end
134
+ end
135
+
136
+ class SnapshotClient
137
+ def initialize(client, **options)
138
+ @client = client
139
+
140
+ enum = client.enum_for(:snapshot, **options)
141
+ @snapshot, @closer = enum.next, ->{ loop { enum.next } }
142
+ @next = self
143
+ end
144
+
145
+ class << self
146
+ alias begin new
147
+ end
148
+
149
+ attr_reader :next
150
+
151
+ delegate :close, to: :client
152
+ delegate :execute, to: :snapshot
153
+
154
+ %w[ insert update delete ].each do |name|
155
+ class_eval(<<-"EOS", __FILE__, __LINE__+1)
156
+ def #{name}(*)
157
+ raise TransactionStateError, "cannot write within a read-only transaction"
158
+ end
159
+ EOS
160
+ end
161
+
162
+ %w[ commit rollback ].each do |name|
163
+ class_eval(<<-"EOS", __FILE__, __LINE__+1)
164
+ def #{name}
165
+ terminate_phase
166
+ true
167
+ end
168
+ EOS
169
+ end
170
+
171
+ %w[ begin_transaction begin_snapshot ].each do |name|
172
+ class_eval(<<-"EOS", __FILE__, __LINE__+1)
173
+ def #{name}(*args)
174
+ raise TransactionStateError, 'already in a transaction'
175
+ end
176
+ EOS
177
+ end
178
+
179
+ private
180
+ attr_reader :client, :snapshot
181
+
182
+ def terminate_phase
183
+ @next = InitialPhaseClient.new(@client)
184
+ ensure
185
+ @closer.call
186
+ end
187
+ end
188
+ end
189
+ end
190
+ end
@@ -207,16 +207,14 @@ module ActiveRecord
207
207
  }
208
208
 
209
209
  log(fake_sql, name) do
210
- session.commit do |c|
211
- c.insert table, row
212
- end
210
+ with_phase_transition {|client| client.insert(table, row) }
213
211
  end
214
212
 
215
213
  id_value
216
214
  end
217
215
 
218
216
  def update(arel, name = nil, binds = [])
219
- raise NotImplementedError, "DELETE in raw SQL is not supported" unless arel.respond_to?(:ast)
217
+ raise NotImplementedError, "UPDATE statement in raw SQL is not supported" unless arel.respond_to?(:ast)
220
218
 
221
219
  type_casted_binds = binds.map {|attr| type_cast(attr.value_for_database) }
222
220
  table, target, values = MutationVisitor.new(self, type_casted_binds.dup).accept(arel.ast)
@@ -236,16 +234,13 @@ module ActiveRecord
236
234
  rows = target.map {|id| row.merge(pk => id) }
237
235
 
238
236
  log(fake_sql, name, binds) do
239
- session.commit do |c|
240
- c.update(table, rows)
241
- end
237
+ with_phase_transition {|client| client.update(table, rows) }
242
238
  end
243
-
244
- true
239
+ target.size
245
240
  end
246
241
 
247
242
  def delete(arel, name, binds)
248
- raise NotImplementedError, "DELETE in raw SQL is not supported" unless arel.respond_to?(:ast)
243
+ raise NotImplementedError, "DELETE statement in raw SQL is not supported" unless arel.respond_to?(:ast)
249
244
 
250
245
  type_casted_binds = binds.map {|attr| type_cast(attr.value_for_database) }
251
246
  table, target, wheres = MutationVisitor.new(self, type_casted_binds.dup).accept(arel.ast)
@@ -272,9 +267,7 @@ module ActiveRecord
272
267
  end
273
268
 
274
269
  log(fake_sql, name, binds) do
275
- session.commit do |c|
276
- c.delete(table, keyset)
277
- end
270
+ with_phase_transition {|client| client.delete(table, keyset) }
278
271
  end
279
272
 
280
273
  keyset.size
@@ -305,14 +298,36 @@ module ActiveRecord
305
298
  end
306
299
 
307
300
  log(sql, name, binds) do
308
- results = session.execute(sql, params: spanner_binds, streaming: false)
309
- columns = results.types.map(&:first)
310
- rows = results.rows.map {|row|
311
- columns.map {|col| row[col] }
312
- }
301
+ executor = client
302
+ executor = raw_client if name == 'SCHEMA'
303
+ results = executor.execute(sql, params: spanner_binds)
304
+ columns = results.fields.keys
305
+ rows = results.rows.map(&:to_a)
313
306
  ActiveRecord::Result.new(columns.map(&:to_s), rows)
314
307
  end
315
308
  end
309
+
310
+ def begin_db_transaction
311
+ with_phase_transition {|client| client.begin_transaction }
312
+ end
313
+
314
+ def begin_isolated_db_transaction(isolation)
315
+ with_phase_transition {|client| client.begin_snapshot(**isolation) }
316
+ end
317
+
318
+ def commit_db_transaction
319
+ with_phase_transition {|client| client.commit }
320
+ end
321
+
322
+ def exec_rollback_db_transaction
323
+ with_phase_transition {|client| client.rollback }
324
+ end
325
+
326
+ def with_phase_transition
327
+ result = yield client
328
+ @client = client.next
329
+ result
330
+ end
316
331
  end
317
332
  end
318
333
  end
@@ -5,10 +5,16 @@ module ActiveRecord
5
5
 
6
6
  class SchemaCreation < AbstractAdapter::SchemaCreation
7
7
  def visit_TableDefinition(o)
8
- pk = o.columns.find {|c| c.type == :primary_key }
8
+ pk = o.columns.find {|c| c.options[:primary_key] }
9
9
  ddl = "#{super} PRIMARY KEY (#{quote_column_name(pk.name)})"
10
10
  DDL.new(ddl)
11
11
  end
12
+
13
+ def add_column_options!(sql, options)
14
+ if options[:null] == false
15
+ sql << " NOT NULL"
16
+ end
17
+ end
12
18
  end
13
19
  end
14
20
  end
@@ -119,16 +119,14 @@ module ActiveRecord
119
119
  end
120
120
 
121
121
  def create_database(name, instance_id: nil, statements: [])
122
- service = instance.service
123
- job = service.create_database(instance_id || instance.instance_id, name,
124
- statements: statements)
122
+ job = conn.create_database(instance_id || @instance_id, name,
123
+ statements: statements)
125
124
  job.wait_until_done! unless job.done?
126
125
  raise_on_error(job)
127
126
  end
128
127
 
129
128
  def drop_database(name, instance_id: nil)
130
- service = instance.service
131
- service.drop_database(instance_id || instance.instance_id, name)
129
+ conn.service.drop_database(instance_id || @instance_id, name)
132
130
  end
133
131
 
134
132
  def drop_table(name, options = {})
@@ -1,10 +1,12 @@
1
1
  require 'google/cloud/spanner'
2
2
 
3
3
  require 'active_record/connection_adapters/abstract_adapter'
4
+ require 'active_record/connection_adapters/spanner/client'
4
5
  require 'active_record/connection_adapters/spanner/database_statements'
5
6
  require 'active_record/connection_adapters/spanner/schema_creation'
6
7
  require 'active_record/connection_adapters/spanner/schema_statements'
7
8
  require 'active_record/connection_adapters/spanner/quoting'
9
+ require 'grpc'
8
10
 
9
11
  module ActiveRecord
10
12
  module ConnectionHandling
@@ -28,9 +30,9 @@ module ActiveRecord
28
30
  include Spanner::Quoting
29
31
 
30
32
  def initialize(connection, logger, config)
31
- super(connection, logger, config)
32
33
  conn_params = config.symbolize_keys.slice(*ADAPTER_OPTS)
33
34
  connect(conn_params)
35
+ super(connection, logger, config)
34
36
  end
35
37
 
36
38
  def schema_creation # :nodoc:
@@ -42,19 +44,22 @@ module ActiveRecord
42
44
  end
43
45
 
44
46
  def active?
45
- !!@client
46
- # TODO(yugui) Check db.service.channel.connectivity_state once it is fixed?
47
+ [
48
+ ::GRPC::Core::ConnectivityStates::IDLE,
49
+ ::GRPC::Core::ConnectivityStates::READY,
50
+ ].include?(@conn.service.channel.connectivity_state)
47
51
  end
48
52
 
49
53
  def connect(params)
50
54
  client_params = params.slice(*CLIENT_PARAMS)
51
- @client = Google::Cloud::Spanner.new(**client_params)
55
+ @conn = Google::Cloud::Spanner.new(**client_params)
52
56
  @instance_id = params[:instance]
53
57
  @database_id = params[:database]
54
58
  end
55
59
 
56
60
  def disconnect!
57
- invalidate_session
61
+ super
62
+ release_client!
58
63
  end
59
64
 
60
65
  def prefetch_primary_key?(table_name = nil)
@@ -67,7 +72,7 @@ module ActiveRecord
67
72
  end
68
73
 
69
74
  private
70
- attr_reader :client
75
+ attr_reader :conn
71
76
 
72
77
  def initialize_type_map(m) # :nodoc:
73
78
  register_class_with_limit m, %r(STRING)i, Type::String
@@ -82,9 +87,8 @@ module ActiveRecord
82
87
  # TODO(yugui) Support array and struct
83
88
  end
84
89
 
85
-
86
90
  def instance
87
- @instance ||= client.instance(@instance_id)
91
+ @instance ||= conn.instance(@instance_id)
88
92
  raise ActiveRecord::NoDatabaseError unless @instance
89
93
 
90
94
  @instance
@@ -101,15 +105,22 @@ module ActiveRecord
101
105
  @db
102
106
  end
103
107
 
104
- def session
105
- @session ||= database.session
108
+ def raw_client
109
+ @raw_client ||= conn.client(@instance_id, @database_id)
106
110
  end
107
111
 
108
- def invalidate_session
109
- @session&.delete_session
110
- @session = nil
112
+ def client
113
+ @client ||= ConnectionAdapters::Spanner::InitialPhaseClient.new(raw_client)
111
114
  end
115
+
116
+ def release_client!
117
+ @client&.close
118
+ @client = nil
119
+ end
120
+
121
+ #def reset_transaction
122
+ # # @transaction_manager = Spanner::TransactionManager.new(self, client)
123
+ #end
112
124
  end
113
125
  end
114
126
  end
115
-
@@ -1,3 +1,3 @@
1
1
  module ActiveRecordSpannerAdapter
2
- VERSION = "0.1.0"
2
+ VERSION = "0.3.0"
3
3
  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.1.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yuki Yugui Sonoda
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-03-09 00:00:00.000000000 Z
11
+ date: 2017-08-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -28,16 +28,16 @@ dependencies:
28
28
  name: google-cloud-spanner
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - ">="
31
+ - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '0'
33
+ version: '0.23'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - ">="
38
+ - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: '0'
40
+ version: '0.23'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: google-gax
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -81,19 +81,19 @@ dependencies:
81
81
  - !ruby/object:Gem::Version
82
82
  version: '10.0'
83
83
  - !ruby/object:Gem::Dependency
84
- name: minitest
84
+ name: rspec
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
87
  - - "~>"
88
88
  - !ruby/object:Gem::Version
89
- version: '5.0'
89
+ version: 3.6.0
90
90
  type: :development
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
94
  - - "~>"
95
95
  - !ruby/object:Gem::Version
96
- version: '5.0'
96
+ version: 3.6.0
97
97
  description: Connection Adapter of Google Cloud Spanner to ActiveRecord O/R mapper
98
98
  library
99
99
  email:
@@ -103,7 +103,6 @@ extensions: []
103
103
  extra_rdoc_files: []
104
104
  files:
105
105
  - ".gitignore"
106
- - ".gitmodules"
107
106
  - ".travis.yml"
108
107
  - Gemfile
109
108
  - LICENSE
@@ -113,6 +112,7 @@ files:
113
112
  - bin/console
114
113
  - bin/setup
115
114
  - lib/active_record/connection_adapters/spanner.rb
115
+ - lib/active_record/connection_adapters/spanner/client.rb
116
116
  - lib/active_record/connection_adapters/spanner/database_statements.rb
117
117
  - lib/active_record/connection_adapters/spanner/quoting.rb
118
118
  - lib/active_record/connection_adapters/spanner/schema_creation.rb
@@ -138,7 +138,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
138
138
  version: '0'
139
139
  requirements: []
140
140
  rubyforge_project:
141
- rubygems_version: 2.6.8
141
+ rubygems_version: 2.6.10
142
142
  signing_key:
143
143
  specification_version: 4
144
144
  summary: Adapts Google Cloud Spanner to ActiveRecord
@@ -1,3 +0,0 @@
1
- [submodule "gcloud-ruby"]
2
- path = vendor/gcloud-ruby
3
- url = https://github.com/GoogleCloudPlatform/google-cloud-ruby