activerecord-spanner-adapter 0.1.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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