believer 0.1.4 → 0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +168 -53
- data/lib/believer.rb +29 -26
- data/lib/believer/base.rb +2 -0
- data/lib/believer/column.rb +65 -0
- data/lib/believer/columns.rb +3 -62
- data/lib/believer/command.rb +14 -16
- data/lib/believer/connection.rb +25 -6
- data/lib/believer/cql_helper.rb +20 -0
- data/lib/believer/ddl.rb +7 -5
- data/lib/believer/delete.rb +4 -2
- data/lib/believer/environment.rb +43 -13
- data/lib/believer/environment/merb_env.rb +14 -0
- data/lib/believer/environment/rails_env.rb +3 -4
- data/lib/believer/insert.rb +1 -1
- data/lib/believer/model_schema.rb +10 -44
- data/lib/believer/order_by.rb +8 -1
- data/lib/believer/persistence.rb +6 -1
- data/lib/believer/query.rb +3 -3
- data/lib/believer/relation.rb +90 -0
- data/lib/believer/scoped_command.rb +11 -4
- data/lib/believer/test/test_run_life_cycle.rb +49 -0
- data/lib/believer/values.rb +2 -11
- data/lib/believer/version.rb +1 -1
- data/lib/believer/where_clause.rb +1 -1
- data/spec/believer/delete_spec.rb +2 -2
- data/spec/believer/environment_spec.rb +32 -0
- data/spec/believer/insert_spec.rb +2 -2
- data/spec/believer/query_spec.rb +14 -14
- data/spec/believer/relation_spec.rb +33 -0
- data/spec/spec_helper.rb +6 -20
- data/spec/support/setup_database.rb +1 -1
- data/spec/support/test_classes.rb +34 -21
- metadata +27 -7
- data/lib/believer/owner.rb +0 -48
- data/lib/believer/primary_key.rb +0 -5
- data/lib/believer/test/rspec/test_run_life_cycle.rb +0 -51
- data/spec/believer/environment/rails_env_spec.rb +0 -0
data/README.md
CHANGED
@@ -12,31 +12,34 @@ The Believer library is heavily inspired by ActiveRecord. Most patterns used in
|
|
12
12
|
### Define your class
|
13
13
|
An example:
|
14
14
|
|
15
|
-
|
15
|
+
``` ruby
|
16
|
+
require 'believer'
|
16
17
|
|
17
|
-
|
18
|
-
|
19
|
-
|
18
|
+
class Artist < Believer::Base
|
19
|
+
column :name
|
20
|
+
column :label
|
20
21
|
|
21
|
-
|
22
|
-
|
22
|
+
primary_key :name
|
23
|
+
end
|
23
24
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
25
|
+
class Album < Believer::Base
|
26
|
+
column :artist
|
27
|
+
column :name
|
28
|
+
column :release_date, :type => :timestamp
|
28
29
|
|
29
|
-
|
30
|
-
|
30
|
+
primary_key :artist, :name
|
31
|
+
end
|
31
32
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
33
|
+
class Song < Believer::Base
|
34
|
+
column :artist
|
35
|
+
column :album
|
36
|
+
column :name
|
37
|
+
column :track_number, :type => :integer
|
38
|
+
column :data, :cql_type => :blob
|
37
39
|
|
38
|
-
|
39
|
-
|
40
|
+
primary_key :artist, :album, :name
|
41
|
+
end
|
42
|
+
```
|
40
43
|
|
41
44
|
#### The Believer::Base class
|
42
45
|
This is the class you should extend from.
|
@@ -44,14 +47,25 @@ This is the class you should extend from.
|
|
44
47
|
#### The column class method
|
45
48
|
Defines the mapping between a Ruby object attribute and a Cassandra column. Also defines a getter and setter attribute with the same name.
|
46
49
|
The second argument is a Hash, which support the following keys:
|
47
|
-
* type: the data type. Supported are: :string, :integer, :float, :timestamp
|
48
|
-
|
50
|
+
* type: the data type. Supported values are: :string, :integer, :float, :timestamp, :time
|
51
|
+
* cql_type: the CQL data type.
|
52
|
+
For type determination, you must include either or both the :type or the :cql_type options.
|
49
53
|
|
50
54
|
#### The primary_key class method
|
51
55
|
Sets the primary key columns of the class.
|
52
56
|
In a situation where you're only querying data, you don't need to set this.
|
53
57
|
However, if you rely on object equality somewhere in your application, it is advisable to set the primary key, as the primary key values are used in the Believer::Base.eql? method.
|
54
58
|
|
59
|
+
If you wish to use a partition key consisting of multiple columns, use an array as the first part of the primary_key list:
|
60
|
+
|
61
|
+
``` ruby
|
62
|
+
class Song
|
63
|
+
...
|
64
|
+
primary_key [:artist, :album], :name, :...
|
65
|
+
...
|
66
|
+
end
|
67
|
+
```
|
68
|
+
|
55
69
|
### Query your class
|
56
70
|
The following methods can be used to query class instances.
|
57
71
|
* where: specify query filters
|
@@ -64,69 +78,170 @@ All methods are chainable, meaning you can
|
|
64
78
|
#### The where method
|
65
79
|
Use the where method to specify filters on the result set. These filters correspond to the expressions in the WHERE clause of a Cassandra query.
|
66
80
|
|
67
|
-
|
68
|
-
|
81
|
+
``` ruby
|
82
|
+
# Using a hash
|
83
|
+
Artist.where(:name => 'James Brown')
|
69
84
|
|
70
|
-
|
71
|
-
|
85
|
+
# Using a hash mapping key to an array of possible values. Maps to the CQL IN operator
|
86
|
+
Artist.where(:name => ['Coldplay', 'Depeche Mode', 'Eurythmics'])
|
72
87
|
|
73
|
-
|
74
|
-
|
88
|
+
# Using string with interpolation
|
89
|
+
Artist.where('name = ?', 'Foreigner')
|
90
|
+
```
|
75
91
|
|
76
92
|
#### The select method
|
77
93
|
Using the select method you can define the columns loaded in a query. These fields correspond to the expressions in the SELECT clause of a Cassandra query.
|
78
94
|
This might be handy in the case you have a table with a lot of columns, but only need a few.
|
79
95
|
|
80
|
-
|
81
|
-
|
96
|
+
``` ruby
|
97
|
+
# Select a single field
|
98
|
+
Artist.select(:name)
|
82
99
|
|
83
|
-
|
84
|
-
|
100
|
+
# Select a multiple fields
|
101
|
+
Artist.select(:name, :label)
|
102
|
+
```
|
85
103
|
|
86
104
|
#### The limit method
|
87
105
|
Limits the amount of records returned to the specified maximum
|
88
106
|
|
89
|
-
|
90
|
-
|
107
|
+
``` ruby
|
108
|
+
# Yield at most 20 class instances
|
109
|
+
Artist.limit(20)
|
110
|
+
```
|
91
111
|
|
92
112
|
#### The order method
|
93
113
|
Order the results in using the specified column
|
94
114
|
|
95
|
-
|
96
|
-
|
97
|
-
|
115
|
+
``` ruby
|
116
|
+
# Order ascending by name
|
117
|
+
Album.order(:name)
|
118
|
+
Album.order(:name, :asc)
|
98
119
|
|
99
|
-
|
100
|
-
|
120
|
+
# Order descending by name
|
121
|
+
Album.order(:name, :desc)
|
122
|
+
```
|
101
123
|
|
102
124
|
#### Method chaining
|
103
125
|
All query methods can be chained.
|
104
126
|
This is done by creating and returning a clone of the receiver. The clone is the receiver of the query method.
|
105
127
|
|
106
|
-
|
107
|
-
|
128
|
+
``` ruby
|
129
|
+
# 'Echoes'....
|
130
|
+
Song.where(:artist => 'Pink Floyd').where(:album => 'Meddle').order(:track_number, :desc).limit(1)
|
131
|
+
```
|
108
132
|
|
109
133
|
### Configuration
|
110
134
|
If using Rails, place a believer.yml file in the configuration directory of your application.
|
111
135
|
The file structure starts with the the environment name, followed by the connection configuration.
|
112
136
|
This is the client connection configuration passed to the cql-rb gem.
|
113
137
|
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
138
|
+
``` yaml
|
139
|
+
development:
|
140
|
+
host: 127.0.0.1
|
141
|
+
port: 9042
|
142
|
+
keyspace: my_keyspace
|
143
|
+
|
144
|
+
staging:
|
145
|
+
host: 'staging.mynetwork.local'
|
146
|
+
port: 9042
|
147
|
+
keyspace: my_keyspace
|
148
|
+
credentials:
|
149
|
+
username: john
|
150
|
+
password: $FDFD%@#&*
|
151
|
+
```
|
118
152
|
|
119
|
-
|
120
|
-
host: 'staging.mynetwork.local'
|
121
|
-
port: 9042
|
122
|
-
keyspace: my_keyspace
|
123
|
-
credentials:
|
124
|
-
username: john
|
125
|
-
password: $FDFD%@#&*
|
153
|
+
In other cases, you will have to programatically set the environment:
|
126
154
|
|
155
|
+
``` ruby
|
156
|
+
Believer::Base.environment = Believer::Environment::BaseEnv.new(:host => '127.0.0.1',
|
157
|
+
:keyspace => 'mykeyspace')
|
158
|
+
```
|
159
|
+
|
160
|
+
### Connection pooling
|
161
|
+
If you wish to use a pool of connections, include a :pool node to the configuration.
|
162
|
+
The pool library used is [connection_pool](https://github.com/mperham/connection_pool).
|
163
|
+
|
164
|
+
``` yaml
|
165
|
+
development:
|
166
|
+
host: 127.0.0.1
|
167
|
+
port: 9042
|
168
|
+
keyspace: my_keyspace
|
169
|
+
pool:
|
170
|
+
size: 10
|
171
|
+
timeout: 5
|
172
|
+
```
|
173
|
+
|
174
|
+
## Callbacks
|
175
|
+
The Believer::Base supports several callbacks to hook into the lifecycle of the models.
|
176
|
+
These callbacks can be included in the body of a Believer::Base subclass, like so:
|
177
|
+
|
178
|
+
``` ruby
|
179
|
+
class Song < Believer::Base
|
180
|
+
after_save :do_something
|
181
|
+
|
182
|
+
before_destroy do
|
183
|
+
puts "About to be destroyed: #{self}"
|
184
|
+
end
|
127
185
|
|
128
|
-
|
186
|
+
def do_something
|
187
|
+
puts "Just been saved: #{self}"
|
188
|
+
end
|
189
|
+
end
|
190
|
+
```
|
191
|
+
|
192
|
+
Supported callbacks are:
|
193
|
+
* after_initialize
|
194
|
+
* before_save
|
195
|
+
* after_save
|
196
|
+
* around_save
|
197
|
+
* before_destroy
|
198
|
+
* after_destroy
|
199
|
+
* around_destroy
|
200
|
+
|
201
|
+
## Relations
|
202
|
+
If you include Believer::Relation in any class, you can define a relation between the including class and the Believer::Base instances.
|
203
|
+
|
204
|
+
Supported relations are:
|
205
|
+
* one-to-one: use the has_single method in the referencing class
|
206
|
+
* one-to-many: use the has_some method in the referencing class
|
207
|
+
|
208
|
+
Options for both relations are:
|
209
|
+
* :class the name of the referenced class. If nil, it will be created from the relation name. Can be a constant or a String
|
210
|
+
* :foreign_key the name of the attribute of the referenced class which acts as the key to this object. Can also be an array, in which case the cardinality and order must match with the :key option
|
211
|
+
* :key the name of the attribute of the referencing class which acts as the key the referenced records. Can also be an array, in which case the cardinality and order must match with the :foreign_key option
|
212
|
+
* :filter a Proc or lambda which is called with a Believer::Query instance as a parameter to tweak the relation query
|
213
|
+
|
214
|
+
Example(s):
|
215
|
+
|
216
|
+
``` ruby
|
217
|
+
class Artist
|
218
|
+
include Believer::Relation
|
219
|
+
|
220
|
+
attr_accessor :name
|
221
|
+
|
222
|
+
has_some :albums, :class => 'Album', :key => :name, :foreign_key => :artist_name
|
223
|
+
end
|
224
|
+
|
225
|
+
class Album < Believer::Base
|
226
|
+
column :artist_name
|
227
|
+
column :name
|
228
|
+
end
|
229
|
+
|
230
|
+
class Song < Believer::Base
|
231
|
+
include Believer::Relation
|
232
|
+
|
233
|
+
column :artist_name
|
234
|
+
column :album_name
|
129
235
|
|
130
|
-
|
236
|
+
has_single :album, :class => 'Test::Album', :key => [:artist_name, :album_name], :foreign_key => [:artist_name, :name]
|
237
|
+
end
|
238
|
+
```
|
131
239
|
|
240
|
+
## Test support
|
241
|
+
An important aspect to note is that Cassandra does not support transactional rollbacks.
|
242
|
+
The consequence of this is that records persisted in a test case are not automatically deleted after a test has executed,
|
243
|
+
causing you to 'manually' delete all the garbage.
|
132
244
|
|
245
|
+
To make this a little less labor intensive, you can include the module Believer::Test::TestRunLifeCycle in your test.
|
246
|
+
This module will implement an after(:each) hook, which deletes all Believer::Base instance/records created in the span
|
247
|
+
of the test.
|
data/lib/believer.rb
CHANGED
@@ -13,31 +13,34 @@ require 'cql/client'
|
|
13
13
|
|
14
14
|
require 'yaml'
|
15
15
|
|
16
|
-
require 'believer/
|
16
|
+
require 'believer/cql_helper'
|
17
|
+
require 'believer/environment'
|
17
18
|
require 'believer/environment/rails_env'
|
18
|
-
require 'believer/
|
19
|
-
require 'believer/
|
20
|
-
require 'believer/
|
21
|
-
require 'believer/
|
22
|
-
require 'believer/
|
23
|
-
require 'believer/
|
24
|
-
require 'believer/
|
25
|
-
require 'believer/
|
26
|
-
require 'believer/
|
27
|
-
require 'believer/
|
28
|
-
require 'believer/
|
29
|
-
require 'believer/
|
30
|
-
require 'believer/
|
31
|
-
require 'believer/
|
32
|
-
require 'believer/
|
33
|
-
require 'believer/
|
34
|
-
require 'believer/
|
35
|
-
require 'believer/
|
36
|
-
require 'believer/
|
37
|
-
|
38
|
-
require 'believer/
|
39
|
-
|
40
|
-
|
41
|
-
require 'believer/
|
42
|
-
|
19
|
+
require 'believer/environment/merb_env'
|
20
|
+
require 'believer/connection'
|
21
|
+
require 'believer/values'
|
22
|
+
require 'believer/column'
|
23
|
+
require 'believer/columns'
|
24
|
+
require 'believer/model_schema'
|
25
|
+
require 'believer/persistence'
|
26
|
+
require 'believer/command'
|
27
|
+
require 'believer/querying'
|
28
|
+
require 'believer/where_clause'
|
29
|
+
require 'believer/empty_result'
|
30
|
+
require 'believer/limit'
|
31
|
+
require 'believer/order_by'
|
32
|
+
require 'believer/scoped_command'
|
33
|
+
require 'believer/query'
|
34
|
+
require 'believer/delete'
|
35
|
+
require 'believer/insert'
|
36
|
+
require 'believer/scoping'
|
37
|
+
require 'believer/batch'
|
38
|
+
require 'believer/batch_delete'
|
39
|
+
require 'believer/callbacks'
|
40
|
+
|
41
|
+
require 'believer/observer'
|
42
|
+
require 'believer/relation'
|
43
|
+
|
44
|
+
require 'believer/ddl'
|
45
|
+
require 'believer/base'
|
43
46
|
|
data/lib/believer/base.rb
CHANGED
@@ -0,0 +1,65 @@
|
|
1
|
+
module Believer
|
2
|
+
|
3
|
+
# Represents a Cassandra table column
|
4
|
+
class Column
|
5
|
+
include Values
|
6
|
+
|
7
|
+
CQL_TYPES = {
|
8
|
+
:ascii => {:ruby_type => :string}, # strings US-ASCII character string
|
9
|
+
:bigint => {:ruby_type => :integer}, # integers 64-bit signed long
|
10
|
+
:blob => {:ruby_type => :string}, # blobs Arbitrary bytes (no validation), expressed as hexadecimal
|
11
|
+
:boolean => {:ruby_type => :boolean}, # booleans true or false
|
12
|
+
:counter => {:ruby_type => :integer}, # integers Distributed counter value (64-bit long)
|
13
|
+
:decimal => {:ruby_type => :float}, # integers, floats Variable-precision decimal
|
14
|
+
:double => {:ruby_type => :float}, # integers 64-bit IEEE-754 floating point
|
15
|
+
:float => {:ruby_type => :float}, # integers, floats 32-bit IEEE-754 floating point
|
16
|
+
:inet => {:ruby_type => :string}, # strings IP address string in IPv4 or IPv6 format*
|
17
|
+
:int => {:ruby_type => :integer}, # integers 32-bit signed integer
|
18
|
+
:list => {:ruby_type => :array}, # n/a A collection of one or more ordered elements
|
19
|
+
:map => {:ruby_type => :hash}, # n/a A JSON-style array of literals: { literal : literal, literal : literal ... }
|
20
|
+
:set => {:ruby_type => :array}, # n/a A collection of one or more elements
|
21
|
+
:text => {:ruby_type => :string}, # strings UTF-8 encoded string
|
22
|
+
:timestamp => {:ruby_type => :time}, # integers, strings Date plus time, encoded as 8 bytes since epoch
|
23
|
+
:uuid => {:ruby_type => :string}, # uuids A UUID in standard UUID format
|
24
|
+
:timeuuid => {:ruby_type => :integer}, # uuids Type 1 UUID only (CQL 3)
|
25
|
+
:varchar => {:ruby_type => :string}, # strings UTF-8 encoded string
|
26
|
+
:varint => {:ruby_type => :integer}, # integers Arbitrary-precision integer
|
27
|
+
}
|
28
|
+
|
29
|
+
# Supported Ruby 'types'
|
30
|
+
RUBY_TYPES = {
|
31
|
+
:integer => {:default_cql_type => :int},
|
32
|
+
:string => {:default_cql_type => :varchar},
|
33
|
+
:time => {:default_cql_type => :timestamp},
|
34
|
+
:timestamp => {:default_cql_type => :timestamp},
|
35
|
+
:float => {:default_cql_type => :float}
|
36
|
+
}
|
37
|
+
|
38
|
+
attr_reader :name, :type, :cql_type
|
39
|
+
|
40
|
+
# Creates a new instance.
|
41
|
+
# @param opts [Hash] values options
|
42
|
+
# @option opts :name the column name
|
43
|
+
# @option opts :type the Ruby type. Can be :integer, :string, :time, :timestamp, :float
|
44
|
+
# @option opts :cql_type the CQL type. See Cassandra CQL documentation and {#CQL_TYPES} for supported types
|
45
|
+
def initialize(opts)
|
46
|
+
raise "Must specify either a :type and/or a :cql_type" if opts[:type].nil? && opts[:cql_type].nil?
|
47
|
+
raise "Invalid CQL column type #{opts[:cql_type]}" if opts[:cql_type] && !CQL_TYPES.has_key?(opts[:cql_type])
|
48
|
+
raise "Invalid type #{opts[:type]}" unless RUBY_TYPES.has_key?(opts[:type])
|
49
|
+
|
50
|
+
@name = opts[:name]
|
51
|
+
@type = opts[:type].nil? ? CQL_TYPES[opts[:cql_type]][:ruby_type] : opts[:type]
|
52
|
+
@cql_type = opts[:cql_type].nil? ? RUBY_TYPES[opts[:type]][:default_cql_type] : opts[:cql_type]
|
53
|
+
end
|
54
|
+
|
55
|
+
# Converts the value to a one that conforms to the type of this column
|
56
|
+
# @param v [Object] the value
|
57
|
+
def convert_to_type(v)
|
58
|
+
convert_method = "convert_to_#{@type}".to_sym
|
59
|
+
return self.send(convert_method, v) if respond_to?(convert_method)
|
60
|
+
v
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
data/lib/believer/columns.rb
CHANGED
@@ -19,61 +19,6 @@ module Believer
|
|
19
19
|
|
20
20
|
end
|
21
21
|
|
22
|
-
class Column
|
23
|
-
#TYPES = {
|
24
|
-
# :ascii strings US-ASCII character string
|
25
|
-
#bigint integers 64-bit signed long
|
26
|
-
#blob blobs Arbitrary bytes (no validation), expressed as hexadecimal
|
27
|
-
#boolean booleans true or false
|
28
|
-
#counter integers Distributed counter value (64-bit long)
|
29
|
-
#decimal integers, floats Variable-precision decimal
|
30
|
-
#double integers 64-bit IEEE-754 floating point
|
31
|
-
#float integers, floats 32-bit IEEE-754 floating point
|
32
|
-
#inet strings IP address string in IPv4 or IPv6 format*
|
33
|
-
# int integers 32-bit signed integer
|
34
|
-
#list n/a A collection of one or more ordered elements
|
35
|
-
#map n/a A JSON-style array of literals: { literal : literal, literal : literal ... }
|
36
|
-
#set n/a A collection of one or more elements
|
37
|
-
#text strings UTF-8 encoded string
|
38
|
-
#timestamp integers, strings Date plus time, encoded as 8 bytes since epoch
|
39
|
-
#uuid uuids A UUID in standard UUID format
|
40
|
-
#timeuuid uuids Type 1 UUID only (CQL 3)
|
41
|
-
#varchar strings UTF-8 encoded string
|
42
|
-
#varint integers Arbitrary-precision integer
|
43
|
-
#}
|
44
|
-
#
|
45
|
-
CQL_COL_TYPES = {
|
46
|
-
:integer => 'INT',
|
47
|
-
:string => 'VARCHAR',
|
48
|
-
:timestamp => 'TIMESTAMP',
|
49
|
-
:float => 'FlOAT'
|
50
|
-
}
|
51
|
-
|
52
|
-
attr_reader :name, :type
|
53
|
-
|
54
|
-
def initialize(opts)
|
55
|
-
@name = opts[:name]
|
56
|
-
@type = opts[:type]
|
57
|
-
raise "Invalid column type #{@type}" unless CQL_COL_TYPES.has_key?(@type)
|
58
|
-
@key = opts[:key] == true || !opts[:key].nil?
|
59
|
-
if @key && opts[:key].is_a?(Hash)
|
60
|
-
@partition_key = opts[:key][:partition_key]
|
61
|
-
end
|
62
|
-
end
|
63
|
-
|
64
|
-
def cql_column_type
|
65
|
-
CQL_COL_TYPES[@type]
|
66
|
-
end
|
67
|
-
|
68
|
-
def is_key?
|
69
|
-
@key
|
70
|
-
end
|
71
|
-
|
72
|
-
def is_partition_key?
|
73
|
-
@partition_key
|
74
|
-
end
|
75
|
-
|
76
|
-
end
|
77
22
|
|
78
23
|
module ClassMethods
|
79
24
|
|
@@ -83,7 +28,7 @@ module Believer
|
|
83
28
|
@columns ||= {}
|
84
29
|
end
|
85
30
|
|
86
|
-
|
31
|
+
# Defines a column on the model.
|
87
32
|
# The column name must correspond with the Cassandra column name
|
88
33
|
def column(name, opts = {})
|
89
34
|
defaults = {
|
@@ -91,7 +36,7 @@ module Believer
|
|
91
36
|
}
|
92
37
|
options = defaults.merge(opts).merge(:name => name)
|
93
38
|
|
94
|
-
columns[name] = Column.new(options)
|
39
|
+
columns[name] = ::Believer::Column.new(options)
|
95
40
|
|
96
41
|
self.redefine_method(name) do
|
97
42
|
read_attribute(name)
|
@@ -152,11 +97,7 @@ module Believer
|
|
152
97
|
def write_attribute(attr_name, value)
|
153
98
|
v = value
|
154
99
|
# Convert the value to the actual type
|
155
|
-
|
156
|
-
value_type = self.class.columns[attr_name].type
|
157
|
-
convert_method = "convert_to_#{value_type}".to_sym
|
158
|
-
v = self.send(convert_method, v) if respond_to?(convert_method)
|
159
|
-
end
|
100
|
+
v = self.class.columns[attr_name].convert_to_type(v) unless self.class.columns[attr_name].nil?
|
160
101
|
@attributes[attr_name] = v
|
161
102
|
end
|
162
103
|
|