cequel 1.0.4 → 1.1.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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/Appraisals +3 -0
  3. data/CHANGELOG.md +7 -0
  4. data/CONTRIBUTING.md +1 -1
  5. data/Gemfile +5 -4
  6. data/Gemfile.lock +215 -22
  7. data/README.md +19 -6
  8. data/Vagrantfile +6 -1
  9. data/lib/cequel.rb +3 -2
  10. data/lib/cequel/metal/batch.rb +16 -1
  11. data/lib/cequel/metal/data_set.rb +63 -39
  12. data/lib/cequel/metal/deleter.rb +2 -2
  13. data/lib/cequel/metal/incrementer.rb +2 -2
  14. data/lib/cequel/metal/inserter.rb +8 -6
  15. data/lib/cequel/metal/keyspace.rb +127 -34
  16. data/lib/cequel/metal/logger.rb +1 -1
  17. data/lib/cequel/metal/row.rb +3 -4
  18. data/lib/cequel/metal/updater.rb +2 -2
  19. data/lib/cequel/metal/writer.rb +18 -12
  20. data/lib/cequel/record/associations.rb +7 -1
  21. data/lib/cequel/record/bound.rb +1 -1
  22. data/lib/cequel/record/callbacks.rb +10 -6
  23. data/lib/cequel/record/data_set_builder.rb +13 -2
  24. data/lib/cequel/record/persistence.rb +14 -12
  25. data/lib/cequel/record/properties.rb +3 -3
  26. data/lib/cequel/record/railtie.rb +1 -1
  27. data/lib/cequel/record/record_set.rb +12 -12
  28. data/lib/cequel/record/schema.rb +5 -1
  29. data/lib/cequel/schema/keyspace.rb +1 -1
  30. data/lib/cequel/schema/table_property.rb +1 -1
  31. data/lib/cequel/type.rb +44 -10
  32. data/lib/cequel/uuids.rb +46 -0
  33. data/lib/cequel/version.rb +1 -1
  34. data/spec/examples/metal/data_set_spec.rb +93 -0
  35. data/spec/examples/metal/keyspace_spec.rb +74 -0
  36. data/spec/examples/record/associations_spec.rb +84 -0
  37. data/spec/examples/record/persistence_spec.rb +23 -0
  38. data/spec/examples/record/properties_spec.rb +1 -1
  39. data/spec/examples/record/record_set_spec.rb +12 -3
  40. data/spec/examples/record/schema_spec.rb +1 -1
  41. data/spec/examples/record/secondary_index_spec.rb +1 -1
  42. data/spec/examples/schema/table_reader_spec.rb +2 -3
  43. data/spec/examples/spec_helper.rb +8 -0
  44. data/spec/examples/type_spec.rb +7 -5
  45. data/spec/examples/uuids_spec.rb +22 -0
  46. data/spec/support/helpers.rb +25 -19
  47. data/templates/config/cequel.yml +5 -5
  48. metadata +40 -33
@@ -25,9 +25,12 @@ module Cequel
25
25
  # @see Keyspace#batch
26
26
  #
27
27
  def initialize(keyspace, options = {})
28
+ options.assert_valid_keys(:auto_apply, :unlogged, :consistency)
28
29
  @keyspace = keyspace
29
30
  @auto_apply = options[:auto_apply]
30
31
  @unlogged = options.fetch(:unlogged, false)
32
+ @consistency = options.fetch(:consistency,
33
+ keyspace.default_consistency)
31
34
  reset
32
35
  end
33
36
 
@@ -54,7 +57,8 @@ module Cequel
54
57
  @statement.prepend(begin_statement)
55
58
  @statement.append("APPLY BATCH\n")
56
59
  end
57
- @keyspace.execute(*@statement.args)
60
+ @keyspace.execute_with_consistency(
61
+ @statement.args.first, @statement.args.drop(1), @consistency)
58
62
  end
59
63
 
60
64
  #
@@ -74,6 +78,17 @@ module Cequel
74
78
  !unlogged?
75
79
  end
76
80
 
81
+ # @private
82
+ def execute_with_consistency(cql, bind_vars, query_consistency)
83
+ if query_consistency && query_consistency != @consistency
84
+ raise ArgumentError,
85
+ "Attempting to perform query with consistency " \
86
+ "#{query_consistency.to_s.upcase} in batch with consistency " \
87
+ "#{@consistency.upcase}"
88
+ end
89
+ execute(cql, *bind_vars)
90
+ end
91
+
77
92
  private
78
93
 
79
94
  def reset
@@ -44,10 +44,12 @@ module Cequel
44
44
  attr_reader :sort_order
45
45
  # @return [Integer] maximum number of rows to return, `nil` if no limit
46
46
  attr_reader :row_limit
47
+ # @return [Symbol] what consistency level queries from this data set will
48
+ # use
49
+ # @since 1.1.0
50
+ attr_reader :query_consistency
47
51
 
48
- def_delegator :keyspace, :execute, :execute_cql
49
- private :execute_cql
50
- def_delegator :keyspace, :write
52
+ def_delegator :keyspace, :write_with_consistency
51
53
 
52
54
  #
53
55
  # @param table_name [Symbol] column family for this data set
@@ -79,7 +81,7 @@ module Cequel
79
81
  # CQL documentation for INSERT
80
82
  #
81
83
  def insert(data, options = {})
82
- inserter(options) { insert(data) }.execute
84
+ inserter { insert(data) }.execute(options)
83
85
  end
84
86
 
85
87
  #
@@ -125,10 +127,10 @@ module Cequel
125
127
  #
126
128
  def update(*args, &block)
127
129
  if block
128
- updater(args.extract_options!, &block).execute
130
+ updater(&block).execute(args.extract_options!)
129
131
  else
130
132
  data = args.shift
131
- updater(args.extract_options!) { set(data) }.execute
133
+ updater { set(data) }.execute(args.extract_options!)
132
134
  end
133
135
  end
134
136
 
@@ -151,7 +153,7 @@ module Cequel
151
153
  # CQL documentation for counter columns
152
154
  #
153
155
  def increment(deltas, options = {})
154
- incrementer(options) { increment(deltas) }.execute
156
+ incrementer { increment(deltas) }.execute(options)
155
157
  end
156
158
  alias_method :incr, :increment
157
159
 
@@ -168,7 +170,7 @@ module Cequel
168
170
  # @since 0.5.0
169
171
  #
170
172
  def decrement(deltas, options = {})
171
- incrementer(options) { decrement(deltas) }.execute
173
+ incrementer { decrement(deltas) }.execute(options)
172
174
  end
173
175
  alias_method :decr, :decrement
174
176
 
@@ -193,7 +195,7 @@ module Cequel
193
195
  # @see #update
194
196
  #
195
197
  def list_prepend(column, elements, options = {})
196
- updater(options) { list_prepend(column, elements) }.execute
198
+ updater { list_prepend(column, elements) }.execute(options)
197
199
  end
198
200
 
199
201
  #
@@ -216,7 +218,7 @@ module Cequel
216
218
  # @since 1.0.0
217
219
  #
218
220
  def list_append(column, elements, options = {})
219
- updater(options) { list_append(column, elements) }.execute
221
+ updater { list_append(column, elements) }.execute(options)
220
222
  end
221
223
 
222
224
  #
@@ -238,7 +240,7 @@ module Cequel
238
240
  # @since 1.0.0
239
241
  #
240
242
  def list_replace(column, index, value, options = {})
241
- updater(options) { list_replace(column, index, value) }.execute
243
+ updater { list_replace(column, index, value) }.execute(options)
242
244
  end
243
245
 
244
246
  #
@@ -260,7 +262,7 @@ module Cequel
260
262
  # @since 1.0.0
261
263
  #
262
264
  def list_remove(column, value, options = {})
263
- updater(options) { list_remove(column, value) }.execute
265
+ updater { list_remove(column, value) }.execute(options)
264
266
  end
265
267
 
266
268
  #
@@ -284,7 +286,7 @@ module Cequel
284
286
  #
285
287
  def list_remove_at(column, *positions)
286
288
  options = positions.extract_options!
287
- deleter(options) { list_remove_at(column, *positions) }.execute
289
+ deleter { list_remove_at(column, *positions) }.execute(options)
288
290
  end
289
291
 
290
292
  #
@@ -307,7 +309,7 @@ module Cequel
307
309
  #
308
310
  def map_remove(column, *keys)
309
311
  options = keys.extract_options!
310
- deleter(options) { map_remove(column, *keys) }.execute
312
+ deleter { map_remove(column, *keys) }.execute(options)
311
313
  end
312
314
 
313
315
  #
@@ -328,7 +330,7 @@ module Cequel
328
330
  # @since 1.0.0
329
331
  #
330
332
  def set_add(column, values, options = {})
331
- updater(options) { set_add(column, values) }.execute
333
+ updater { set_add(column, values) }.execute(options)
332
334
  end
333
335
 
334
336
  #
@@ -349,7 +351,7 @@ module Cequel
349
351
  # @since 1.0.0
350
352
  #
351
353
  def set_remove(column, value, options = {})
352
- updater(options) { set_remove(column, value) }.execute
354
+ updater { set_remove(column, value) }.execute(options)
353
355
  end
354
356
 
355
357
  #
@@ -370,7 +372,7 @@ module Cequel
370
372
  # @since 1.0.0
371
373
  #
372
374
  def map_update(column, updates, options = {})
373
- updater(options) { map_update(column, updates) }.execute
375
+ updater { map_update(column, updates) }.execute(options)
374
376
  end
375
377
 
376
378
  #
@@ -422,11 +424,11 @@ module Cequel
422
424
  def delete(*columns, &block)
423
425
  options = columns.extract_options!
424
426
  if block
425
- deleter(options, &block).execute
427
+ deleter(&block).execute(options)
426
428
  elsif columns.empty?
427
- deleter(options) { delete_row }.execute
429
+ deleter { delete_row }.execute(options)
428
430
  else
429
- deleter(options) { delete_columns(*columns) }.execute
431
+ deleter { delete_columns(*columns) }.execute(options)
430
432
  end
431
433
  end
432
434
 
@@ -545,6 +547,25 @@ module Cequel
545
547
  end
546
548
  end
547
549
 
550
+ # rubocop:disable LineLength
551
+
552
+ #
553
+ # Change the consistency for queries performed by this data set
554
+ #
555
+ # @param consistency [Symbol] a consistency level
556
+ # @return [DataSet] new data set tuned to the given consistency
557
+ #
558
+ # @see http://www.datastax.com/documentation/cassandra/2.0/cassandra/dml/dml_config_consistency_c.html
559
+ # @since 1.1.0
560
+ #
561
+ def consistency(consistency)
562
+ clone.tap do |data_set|
563
+ data_set.query_consistency = consistency
564
+ end
565
+ end
566
+
567
+ # rubocop:enable LineLength
568
+
548
569
  #
549
570
  # Enumerate over rows in this data set. Along with #each, all other
550
571
  # Enumerable methods are implemented.
@@ -560,14 +581,15 @@ module Cequel
560
581
  #
561
582
  def each
562
583
  return enum_for(:each) unless block_given?
563
- execute_cql(*cql).fetch { |row| yield Row.from_result_row(row) }
584
+ result = execute_cql(*cql)
585
+ result.each { |row| yield Row.from_result_row(row) }
564
586
  end
565
587
 
566
588
  #
567
589
  # @return [Hash] the first row in this data set
568
590
  #
569
591
  def first
570
- row = execute_cql(*limit(1).cql).fetch_row
592
+ row = execute_cql(*limit(1).cql).first
571
593
  Row.from_result_row(row)
572
594
  end
573
595
 
@@ -575,7 +597,7 @@ module Cequel
575
597
  # @return [Fixnum] the number of rows in this data set
576
598
  #
577
599
  def count
578
- execute_cql(*count_cql).fetch_row['count']
600
+ execute_cql(*count_cql).first['count']
579
601
  end
580
602
 
581
603
  #
@@ -606,7 +628,7 @@ module Cequel
606
628
  #
607
629
  def inspect
608
630
  "#<#{self.class.name}: " \
609
- "#{CassandraCQL::Statement.sanitize(cql.first, cql[1..-1])}>"
631
+ "#{Keyspace.sanitize(cql.first, cql.drop(1))}>"
610
632
  end
611
633
 
612
634
  #
@@ -630,28 +652,30 @@ module Cequel
630
652
  end
631
653
  end
632
654
 
633
- # @private
634
- def updater(options = {}, &block)
635
- Updater.new(self, options, &block)
636
- end
637
-
638
- # @private
639
- def deleter(options = {}, &block)
640
- Deleter.new(self, options, &block)
641
- end
642
-
643
655
  protected
644
656
 
645
- attr_writer :row_limit
657
+ attr_writer :row_limit, :query_consistency
646
658
 
647
659
  private
648
660
 
649
- def inserter(options = {}, &block)
650
- Inserter.new(self, options, &block)
661
+ def execute_cql(cql, *bind_vars)
662
+ keyspace.execute_with_consistency(cql, bind_vars, query_consistency)
663
+ end
664
+
665
+ def inserter(&block)
666
+ Inserter.new(self, &block)
667
+ end
668
+
669
+ def incrementer(&block)
670
+ Incrementer.new(self, &block)
671
+ end
672
+
673
+ def updater(&block)
674
+ Updater.new(self, &block)
651
675
  end
652
676
 
653
- def incrementer(options = {}, &block)
654
- Incrementer.new(self, options, &block)
677
+ def deleter(&block)
678
+ Deleter.new(self, &block)
655
679
  end
656
680
 
657
681
  def initialize_copy(source)
@@ -61,7 +61,7 @@ module Cequel
61
61
 
62
62
  private
63
63
 
64
- def write_to_statement(statement)
64
+ def write_to_statement(statement, options)
65
65
  if @delete_row
66
66
  statement.append("DELETE FROM #{table_name}")
67
67
  elsif statements.empty?
@@ -71,7 +71,7 @@ module Cequel
71
71
  .append(statements.join(','), *bind_vars)
72
72
  .append(" FROM #{table_name}")
73
73
  end
74
- statement.append(generate_upsert_options)
74
+ statement.append(generate_upsert_options(options))
75
75
  end
76
76
 
77
77
  def empty?
@@ -35,10 +35,10 @@ module Cequel
35
35
 
36
36
  private
37
37
 
38
- def write_to_statement(statement)
38
+ def write_to_statement(statement, options)
39
39
  statement
40
40
  .append("UPDATE #{table_name}")
41
- .append(generate_upsert_options)
41
+ .append(generate_upsert_options(options))
42
42
  .append(
43
43
  " SET " << statements.join(', '),
44
44
  *bind_vars
@@ -11,7 +11,7 @@ module Cequel
11
11
  #
12
12
  # (see Writer#initialize)
13
13
  #
14
- def initialize(data_set, options = {})
14
+ def initialize(data_set)
15
15
  @row = {}
16
16
  super
17
17
  end
@@ -19,10 +19,12 @@ module Cequel
19
19
  #
20
20
  # (see Writer#execute)
21
21
  #
22
- def execute
22
+ def execute(options = {})
23
23
  statement = Statement.new
24
- write_to_statement(statement)
25
- data_set.write(*statement.args)
24
+ consistency = options.fetch(:consistency, data_set.query_consistency)
25
+ write_to_statement(statement, options)
26
+ data_set.write_with_consistency(
27
+ statement.cql, statement.bind_vars, consistency)
26
28
  end
27
29
 
28
30
  #
@@ -55,12 +57,12 @@ module Cequel
55
57
  end
56
58
  end
57
59
 
58
- def write_to_statement(statement)
60
+ def write_to_statement(statement, options)
59
61
  statement.append("INSERT INTO #{table_name}")
60
62
  statement.append(
61
63
  " (#{column_names.join(', ')}) VALUES (#{statements.join(', ')}) ",
62
64
  *bind_vars)
63
- statement.append(generate_upsert_options)
65
+ statement.append(generate_upsert_options(options))
64
66
  end
65
67
  end
66
68
  end
@@ -1,4 +1,6 @@
1
1
  # -*- encoding : utf-8 -*-
2
+ require 'set'
3
+
2
4
  module Cequel
3
5
  module Metal
4
6
  #
@@ -9,29 +11,72 @@ module Cequel
9
11
  class Keyspace
10
12
  extend Forwardable
11
13
  include Logging
14
+ include MonitorMixin
12
15
 
13
16
  # @return [Hash] configuration options for this keyspace
14
17
  attr_reader :configuration
15
18
  # @return [String] name of the keyspace
16
19
  attr_reader :name
20
+ # @return [Array<String>] list of hosts to connect to
21
+ attr_reader :hosts
22
+ # @return Integer port to connect to Cassandra nodes on
23
+ attr_reader :port
24
+ # @return [Symbol] the default consistency for queries in this keyspace
25
+ # @since 1.1.0
26
+ attr_writer :default_consistency
17
27
 
18
28
  #
19
29
  # @!method write(statement, *bind_vars)
20
30
  #
21
- # Write data to this keyspace using a CQL query. Will be included the
22
- # current batch operation if one is present.
31
+ # Write data to this keyspace using a CQL query. Will be included the
32
+ # current batch operation if one is present.
23
33
  #
24
- # @param (see #execute)
25
- # @return [void]
34
+ # @param (see #execute)
35
+ # @return [void]
26
36
  #
27
37
  def_delegator :write_target, :execute, :write
28
38
 
39
+ # @!method write_with_consistency(statement, bind_vars, consistency)
40
+ #
41
+ # Write data to this keyspace using a CQL query at the given
42
+ # consistency. Will be included the current batch operation if one is
43
+ # present.
44
+ #
45
+ # @param (see #execute_with_consistency)
46
+ # @return [void]
47
+ #
48
+ def_delegator :write_target, :execute_with_consistency,
49
+ :write_with_consistency
50
+
29
51
  #
30
52
  # @!method batch
31
53
  # (see Cequel::Metal::BatchManager#batch)
32
54
  #
33
55
  def_delegator :batch_manager, :batch
34
56
 
57
+ #
58
+ # Combine a statement with bind vars into a fully-fledged CQL query. This
59
+ # will no longer be needed once the CQL driver supports bound values
60
+ # natively.
61
+ #
62
+ # @param statement [String] CQL statement with ? placeholders for bind
63
+ # vars
64
+ # @param bind_vars [Array] bind variables corresponding to ? in the
65
+ # statement
66
+ # @return [String] CQL statement with quoted values in place of bind
67
+ # variables
68
+ #
69
+ def self.sanitize(statement, bind_vars)
70
+ each_bind_var = bind_vars.each
71
+ statement.gsub('?') { Type.quote(each_bind_var.next) }
72
+ end
73
+
74
+ #
75
+ # @!method sanitize
76
+ # (see Cequel::Metal::Keyspace.sanitize)
77
+ #
78
+ def_delegator 'self.class', :sanitize
79
+
35
80
  #
36
81
  # @api private
37
82
  # @param configuration [Options]
@@ -40,13 +85,14 @@ module Cequel
40
85
  #
41
86
  def initialize(configuration={})
42
87
  configure(configuration)
88
+ @lock = Monitor.new
43
89
  end
44
90
 
45
91
  #
46
92
  # Configure this keyspace from a hash of options
47
93
  #
48
94
  # @param configuration [Options] configuration options
49
- # @option configuration [String] :host ('127.0.0.1:9160') host/port of
95
+ # @option configuration [String] :host ('127.0.0.1:9042') host/port of
50
96
  # single Cassandra instance to connect to
51
97
  # @option configuration [Array<String>] :hosts list of Cassandra
52
98
  # instances to connect to
@@ -59,10 +105,14 @@ module Cequel
59
105
  # @return [void]
60
106
  #
61
107
  def configure(configuration = {})
108
+ if configuration.key?(:thrift)
109
+ warn "Cequel no longer uses the Thrift transport to communicate " \
110
+ "with Cassandra. The :thrift option is deprecated and ignored."
111
+ end
62
112
  @configuration = configuration
63
- @hosts = configuration.fetch(
64
- :host, configuration.fetch(:hosts, '127.0.0.1:9160'))
65
- @thrift_options = configuration[:thrift].try(:symbolize_keys) || {}
113
+
114
+ @hosts, @port = extract_hosts_and_port(configuration)
115
+
66
116
  @name = configuration[:keyspace]
67
117
  # reset the connections
68
118
  clear_active_connections!
@@ -83,18 +133,42 @@ module Cequel
83
133
  DataSet.new(table_name.to_sym, self)
84
134
  end
85
135
 
136
+ #
137
+ # @return [Cql::Client::Client] the low-level client provided by the
138
+ # adapter
139
+ # @api private
140
+ #
141
+ def client
142
+ synchronize { @client ||= build_client }
143
+ end
144
+
86
145
  #
87
146
  # Execute a CQL query in this keyspace
88
147
  #
89
148
  # @param statement [String] CQL string
90
149
  # @param bind_vars [Object] values for bind variables
91
- # @return [void]
150
+ # @return [Enumerable] the results of the query
151
+ #
152
+ # @see #execute_with_consistency
92
153
  #
93
154
  def execute(statement, *bind_vars)
155
+ execute_with_consistency(statement, bind_vars, default_consistency)
156
+ end
157
+
158
+ #
159
+ # Execute a CQL query in this keyspace with the given consistency
160
+ #
161
+ # @param statement [String] CQL string
162
+ # @param bind_vars [Array] array of values for bind variables
163
+ # @param consistency [Symbol] consistency at which to execute query
164
+ # @return [Enumerable] the results of the query
165
+ #
166
+ # @since 1.1.0
167
+ #
168
+ def execute_with_consistency(statement, bind_vars, consistency)
94
169
  log('CQL', statement, *bind_vars) do
95
- with_connection do |conn|
96
- conn.execute(statement, *bind_vars)
97
- end
170
+ client.execute(sanitize(statement, bind_vars),
171
+ consistency || default_consistency)
98
172
  end
99
173
  end
100
174
 
@@ -104,47 +178,66 @@ module Cequel
104
178
  # @return [void]
105
179
  #
106
180
  def clear_active_connections!
107
- if defined? @connection_pool
108
- remove_instance_variable(:@connection_pool)
181
+ if defined? @client
182
+ remove_instance_variable(:@client)
109
183
  end
110
184
  end
111
185
 
186
+ #
187
+ # @return [Symbol] the default consistency for queries in this keyspace
188
+ # @since 1.1.0
189
+ #
190
+ def default_consistency
191
+ @default_consistency || :quorum
192
+ end
193
+
112
194
  private
113
195
 
114
- def_delegator :connection_pool, :with, :with_connection
115
- private :with_connection
196
+ attr_reader :lock
116
197
 
117
198
  def_delegator :batch_manager, :current_batch
118
199
  private :current_batch
119
200
 
120
- def build_connection
121
- options = {cql_version: '3.0.0'}
122
- options[:keyspace] = name if name
123
- CassandraCQL::Database.new(
124
- @hosts,
125
- options,
126
- @thrift_options
127
- )
128
- end
201
+ def_delegator :lock, :synchronize
202
+ private :lock
129
203
 
130
- def connection_pool
131
- return @connection_pool if defined? @connection_pool
132
- options = {
133
- size: @configuration.fetch(:pool, 1),
134
- timeout: @configuration.fetch(:pool_timeout, 0)
135
- }
136
- @connection_pool = ConnectionPool.new(options) do
137
- build_connection
204
+ def build_client
205
+ Cql::Client.connect(hosts: hosts, port: port).tap do |client|
206
+ client.use(name) if name
138
207
  end
139
208
  end
140
209
 
141
210
  def batch_manager
142
- @batch_manager ||= BatchManager.new(self)
211
+ synchronize { @batch_manager ||= BatchManager.new(self) }
143
212
  end
144
213
 
145
214
  def write_target
146
215
  current_batch || self
147
216
  end
217
+
218
+ def extract_hosts_and_port(configuration)
219
+ hosts, ports = [], Set[]
220
+ ports << configuration[:port] if configuration.key?(:port)
221
+ Array.wrap(configuration.fetch(
222
+ :host, configuration.fetch(:hosts, '127.0.0.1'))).each do |host_port|
223
+
224
+ host, port = host_port.split(':')
225
+ hosts << host
226
+ if port
227
+ warn "Specifying a hostname as host:port is deprecated. Specify " \
228
+ "only the host IP or hostname in :hosts, and specify a " \
229
+ "port for all nodes using the :port option."
230
+ ports << port.to_i
231
+ end
232
+ end
233
+
234
+ if ports.size > 1
235
+ fail ArgumentError, "All Cassandra nodes must listen on the same " \
236
+ "port; specified multiple ports #{ports.join(', ')}"
237
+ end
238
+
239
+ [hosts, ports.first || 9042]
240
+ end
148
241
  end
149
242
  end
150
243
  end