activerecord-cockroachdb-adapter 6.0.0beta2 → 6.0.1

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
  SHA256:
3
- metadata.gz: f2d3e8ca47a966406dd46a2516ee8e28d4634109974b012604efa5eeef41661c
4
- data.tar.gz: 78f2720ce377db8b5d61c830c2f8819f4a928bceee3121668348293944b459ac
3
+ metadata.gz: a5cdb80a2174df8da1c60cd3b1bc52e70840e229d0cd87bb2e426921ba67f399
4
+ data.tar.gz: e9211907e6648c24ab5e15ae1b5213ec223560b9d0134be3a14db4770b6dae62
5
5
  SHA512:
6
- metadata.gz: 7928d308947df985eac1ecf4ac2338d8ed5e079c2436562a822b149699b0bc42e38411e90a73a5623c376613ca610060d68f9040d823b9fa65d2869baa98a6fd
7
- data.tar.gz: 5421bb06b3185d331a0a43e700e2475f6ce28ee38d433ba28e3e82c4a0e361a94518dd46fd41b54d1a83f1a217d19e098eef0a251ad7e79803263c08bfd7e3dc
6
+ metadata.gz: 2eaf028eb110e51d2e59521cc788115f34049fb3bba04077481082cb1040e072a0825bb6dead5f39428845a828f1363a0180808dc4bdcdf7b11c2b11ff91f7ae
7
+ data.tar.gz: dd9597f2f0f9578372d56a638eecabcd2d6f82e599e89e0ae88a408349c02b7961df5b3ed78fb8dcf3181835bb3442df2b03e67703efdc13c165e5502f97b2bb
data/CHANGELOG.md ADDED
@@ -0,0 +1,35 @@
1
+ # Changelog
2
+
3
+ ## 6.0.1 - 2021-05-14
4
+
5
+ - Fix a bug where starting the driver can result in a NoDatabaseError.
6
+
7
+ ## 6.0.0 - 2021-04-26
8
+
9
+ - Add a telemetry query on start-up. This helps the Cockroach Labs team
10
+ prioritize support for the adapter. It can be disabled by setting the
11
+ `disable_cockroachdb_telemetry` configuration option to false.
12
+
13
+ ## 6.0.0-beta.5 - 2021-04-02
14
+
15
+ - Added a configuration option named `use_follower_reads_for_type_introspection`.
16
+ If true, it improves the speed of type introspection by allowing potentially stale
17
+ type metadata to be read. Defaults to false.
18
+
19
+ ## 6.0.0-beta.4 - 2021-03-06
20
+
21
+ - Improved connection performance by refactoring an introspection
22
+ that loads types.
23
+ - Changed version numbers to semver.
24
+
25
+ ## 6.0.0beta3
26
+
27
+ - Added support for spatial features.
28
+
29
+ ## 6.0.0beta2
30
+
31
+ - Updated transaction retry logic to work with Rails 6.
32
+
33
+ ## 6.0.0beta1
34
+
35
+ - Initial support for Rails 6.
data/README.md CHANGED
@@ -22,6 +22,304 @@ development:
22
22
  user: <username>
23
23
  ```
24
24
 
25
+ ## Configuration
26
+
27
+ In addition to the standard adapter settings, CockroachDB also supports the following:
28
+
29
+ - `use_follower_reads_for_type_introspection`: Use follower reads on queries to the `pg_type` catalog when set to `true`. This helps to speed up initialization by reading historical data, but may not find recently created user-defined types.
30
+ - `disable_cockroachdb_telemetry`: Determines if a telemetry call is made to the database when the connection pool is initialized. Setting this to `true` will prevent the call from being made.
31
+
32
+ ## Working with Spatial Data
33
+
34
+ The adapter uses [RGeo](https://github.com/rgeo/rgeo) and [RGeo-ActiveRecord](https://github.com/rgeo/rgeo-activerecord) to represent geometric and geographic data as Ruby objects and easily interface them with the adapter. The following is a brief introduction to RGeo and tips to help setup your spatial application. More documentation about RGeo can be found in the [YARD Docs](https://rubydoc.info/github/rgeo/rgeo) and [wiki](https://github.com/rgeo/rgeo/wiki).
35
+
36
+ ### Installing RGeo
37
+
38
+ RGeo can be installed with the following command:
39
+
40
+ ```sh
41
+ gem install rgeo
42
+ ```
43
+
44
+ The best way to use RGeo is with GEOS support. If you have a version of libgeos installed, you can check that it was properly linked with RGeo by running the following commands:
45
+
46
+ ```rb
47
+ require 'rgeo'
48
+
49
+ RGeo::Geos.supported?
50
+ #=> true
51
+ ```
52
+
53
+ If this is `false`, you may need to specify the GEOS directory while installing. Here's an example linking it to the CockroachDB GEOS binary.
54
+
55
+ ```sh
56
+ gem install rgeo -- --with-geos-dir=/path/to/cockroach/lib/
57
+ ```
58
+
59
+ ### Working with RGeo
60
+
61
+ RGeo uses [factories](https://en.wikipedia.org/wiki/Factory_(object-oriented_programming)) to create geometry objects and define their properties. Different factories define their own implementations for standard methods. For instance, the `RGeo::Geographic.spherical_factory` accepts latitudes and longitues as its coordinates and does computations on a spherical surface, while `RGeo::Cartesian.factory` implements geometry objects on a plane.
62
+
63
+ The factory (or factories) you choose to use will depend on the requirements of your application and what you need to do with the geometries they produce. For example, if you are working with points or other simple geometries across long distances and need precise results, the spherical factory is a good choice. If you're working with polygons or multipolygons and analyzing complex relationships between them (`intersects?`, `difference`, etc.), then using a cartesian factory backed by GEOS is a much better option.
64
+
65
+ Once you've selected a factory, you need to create objects. RGeo supports geometry creation through standard constructors (`point`, `line_string`, `polygon`, etc.) or by WKT and WKB.
66
+
67
+ ```rb
68
+ require 'rgeo'
69
+ factory = RGeo::Cartesian.factory(srid: 3857)
70
+
71
+ # Create a line_string from points
72
+ pt1 = factory.point(0,0)
73
+ pt2 = factory.point(1,1)
74
+ pt3 = factory.point(2,2)
75
+ line_string = factory.line_string([pt1,pt2,pt3])
76
+
77
+ p line_string.length
78
+ #=> 2.8284271247461903
79
+
80
+ # check line_string equality
81
+ line_string2 = factory.parse_wkt("LINESTRING (0 0, 1 1, 2 2)")
82
+ p line_string == line_string2
83
+ #=> true
84
+
85
+ # create polygon and test intersection with line_string
86
+ pt4 = factory.point(0,2)
87
+ outer_ring = factory.linear_ring([pt1,pt2,pt3,pt4,pt1])
88
+ poly = factory.polygon(outer_ring)
89
+
90
+ p line_string.intersects? poly
91
+ #=> true
92
+ ```
93
+ ### Creating Spatial Tables
94
+
95
+ To store spatial data, you must create a column with a spatial type. PostGIS
96
+ provides a variety of spatial types, including point, linestring, polygon, and
97
+ different kinds of collections. These types are defined in a standard produced
98
+ by the Open Geospatial Consortium. You can specify options indicating the coordinate system and number of coordinates for the values you are storing.
99
+
100
+ The adapter extends ActiveRecord's migration syntax to
101
+ support these spatial types. The following example creates five spatial
102
+ columns in a table:
103
+
104
+ ```rb
105
+ create_table :my_spatial_table do |t|
106
+ t.column :shape1, :geometry
107
+ t.geometry :shape2
108
+ t.line_string :path, srid: 3857
109
+ t.st_point :lonlat, geographic: true
110
+ t.st_point :lonlatheight, geographic: true, has_z: true
111
+ end
112
+ ```
113
+
114
+ The first column, "shape1", is created with type "geometry". This is a general
115
+ "base class" for spatial types; the column declares that it can contain values
116
+ of _any_ spatial type.
117
+
118
+ The second column, "shape2", uses a shorthand syntax for the same type as the shape1 column.
119
+ You can create a column either by invoking `column` or invoking the name of the type directly.
120
+
121
+ The third column, "path", has a specific geometric type, `line_string`. It
122
+ also specifies an SRID (spatial reference ID) that indicates which coordinate
123
+ system it expects the data to be in. The column now has a "constraint" on it;
124
+ it will accept only LineString data, and only data whose SRID is 3857.
125
+
126
+ The fourth column, "lonlat", has the `st_point` type, and accepts only Point
127
+ data. Furthermore, it declares the column as "geographic", which means it
128
+ accepts longitude/latitude data, and performs calculations such as distances
129
+ using a spheroidal domain.
130
+
131
+ The fifth column, "lonlatheight", is a geographic (longitude/latitude) point
132
+ that also includes a third "z" coordinate that can be used to store height
133
+ information.
134
+
135
+ The following are the data types understood by PostGIS and exposed by
136
+ the adapter:
137
+
138
+ - `:geometry` -- Any geometric type
139
+ - `:st_point` -- Point data
140
+ - `:line_string` -- LineString data
141
+ - `:st_polygon` -- Polygon data
142
+ - `:geometry_collection` -- Any collection type
143
+ - `:multi_point` -- A collection of Points
144
+ - `:multi_line_string` -- A collection of LineStrings
145
+ - `:multi_polygon` -- A collection of Polygons
146
+
147
+ Following are the options understood by the adapter:
148
+
149
+ - `:geographic` -- If set to true, create a PostGIS geography column for
150
+ longitude/latitude data over a spheroidal domain; otherwise create a
151
+ geometry column in a flat coordinate system. Default is false. Also
152
+ implies :srid set to 4326.
153
+ - `:srid` -- Set a SRID constraint for the column. Default is 4326 for a
154
+ geography column, or 0 for a geometry column. Note that PostGIS currently
155
+ (as of version 2.0) requires geography columns to have SRID 4326, so this
156
+ constraint is of limited use for geography columns.
157
+ - `:has_z` -- Specify that objects in this column include a Z coordinate.
158
+ Default is false.
159
+ - `:has_m` -- Specify that objects in this column include an M coordinate.
160
+ Default is false.
161
+
162
+ To create a PostGIS spatial index, add `using: :gist` to your index:
163
+
164
+ ```rb
165
+ add_index :my_table, :lonlat, using: :gist
166
+
167
+ # or
168
+
169
+ change_table :my_table do |t|
170
+ t.index :lonlat, using: :gist
171
+ end
172
+ ```
173
+ ### Configuring ActiveRecord
174
+
175
+ ActiveRecord's usefulness stems from the way it automatically configures
176
+ classes based on the database structure and schema. If a column in the
177
+ database has an integer type, ActiveRecord automatically casts the data to a
178
+ Ruby Integer. In the same way, the adapter automatically
179
+ casts spatial data to a corresponding RGeo data type.
180
+
181
+ RGeo offers more flexibility in its type system than can be
182
+ interpreted solely from analyzing the database column. For example, you can
183
+ configure RGeo objects to exhibit certain behaviors related to their
184
+ serialization, validation, coordinate system, or computation. These settings
185
+ are embodied in the RGeo factory associated with the object.
186
+
187
+ You can configure the adapter to use a particular factory (i.e. a
188
+ particular combination of settings) for data associated with each type in
189
+ the database.
190
+
191
+ Here's an example using a Geos default factory:
192
+
193
+ ```ruby
194
+ RGeo::ActiveRecord::SpatialFactoryStore.instance.tap do |config|
195
+ # By default, use the GEOS implementation for spatial columns.
196
+ config.default = RGeo::Geos.factory_generator
197
+
198
+ # But use a geographic implementation for point columns.
199
+ config.register(RGeo::Geographic.spherical_factory(srid: 4326), geo_type: "point")
200
+ end
201
+ ```
202
+
203
+ The default spatial factory for geographic columns is `RGeo::Geographic.spherical_factory`.
204
+ The default spatial factory for cartesian columns is `RGeo::Cartesian.preferred_factory`.
205
+ You do not need to configure the `SpatialFactoryStore` if these defaults are ok.
206
+
207
+ More information about configuration options for the `SpatialFactoryStore` can be found in the [rgeo-activerecord](https://github.com/rgeo/rgeo-activerecord#spatial-factories-for-columns) docs.
208
+
209
+ ### Reading and Writing Spatial Columns
210
+
211
+ When you access a spatial attribute on your ActiveRecord model, it is given to
212
+ you as an RGeo geometry object (or nil, for attributes that allow null
213
+ values). You can then call the RGeo api on the object. For example, consider
214
+ the MySpatialTable class we worked with above:
215
+
216
+ ```rb
217
+ record = MySpatialTable.find(1)
218
+ point = record.lonlat # Returns an RGeo::Feature::Point
219
+ p point.x # displays the x coordinate
220
+ p point.geometry_type.type_name # displays "Point"
221
+ ```
222
+
223
+ The RGeo factory for the value is determined by how you configured the
224
+ ActiveRecord class, as described above. In this case, we explicitly set a
225
+ spherical factory for the `:lonlat` column:
226
+
227
+ ```rb
228
+ factory = point.factory # returns a spherical factory
229
+ ```
230
+
231
+ You can set a spatial attribute by providing an RGeo geometry object, or by
232
+ providing the WKT string representation of the geometry. If a string is
233
+ provided, the adapter will attempt to parse it as WKT and
234
+ set the value accordingly.
235
+
236
+ ```rb
237
+ record.lonlat = 'POINT(-122 47)' # sets the value to the given point
238
+ ```
239
+
240
+ If the WKT parsing fails, the value currently will be silently set to nil. In
241
+ the future, however, this will raise an exception.
242
+
243
+ ```rb
244
+ record.lonlat = 'POINT(x)' # sets the value to nil
245
+ ```
246
+
247
+ If you set the value to an RGeo object, the factory needs to match the factory
248
+ for the attribute. If the factories do not match, the adapter
249
+ will attempt to cast the value to the correct factory.
250
+
251
+ ```rb
252
+ p2 = factory.point(-122, 47) # p2 is a point in a spherical factory
253
+ record.lonlat = p2 # sets the value to the given point
254
+ record.shape1 = p2 # shape1 uses a flat geos factory, so it
255
+ # will cast p2 into that coordinate system
256
+ # before setting the value
257
+ record.save
258
+ ```
259
+
260
+ If you attempt to set the value to the wrong type, such as setting a linestring attribute to a point value, you will get an exception from the database when you attempt to save the record.
261
+
262
+ ```rb
263
+ record.path = p2 # This will appear to work, but...
264
+ record.save # This will raise an exception from the database
265
+ ```
266
+
267
+ ### Spatial Queries
268
+
269
+ You can create simple queries based on representational equality in the same
270
+ way you would on a scalar column:
271
+
272
+ ```ruby
273
+ record2 = MySpatialTable.where(:lonlat => factory.point(-122, 47)).first
274
+ ```
275
+
276
+ You can also use WKT:
277
+
278
+ ```ruby
279
+ record3 = MySpatialTable.where(:lonlat => 'POINT(-122 47)').first
280
+ ```
281
+
282
+ Note that these queries use representational equality, meaning they return
283
+ records where the lonlat value matches the given value exactly. A 0.00001
284
+ degree difference would not match, nor would a different representation of the
285
+ same geometry (like a multi_point with a single element). Equality queries
286
+ aren't generally all that useful in real world applications. Typically, if you
287
+ want to perform a spatial query, you'll look for, say, all the points within a
288
+ given area. For those queries, you'll need to use the standard spatial SQL
289
+ functions provided by PostGIS.
290
+
291
+ To perform more advanced spatial queries, you can use the extended Arel interface included in the adapter. The functions accept WKT strings or RGeo features.
292
+
293
+ ```rb
294
+ point = RGeo::Geos.factory(srid: 0).point(1,1)
295
+
296
+ # Example Building model where geom is a column of polygons.
297
+ buildings = Building.arel_table
298
+ containing_buiildings = Building.where(buildings[:geom].st_contains(point))
299
+ ```
300
+
301
+ See the [rgeo-activerecord YARD Docs](https://rubydoc.info/github/rgeo/rgeo-activerecord/RGeo/ActiveRecord/SpatialExpressions) for a list of available PostGIS functions.
302
+
303
+ ### Validation Issues
304
+
305
+ If you see an `RGeo::Error::InvalidGeometry (LinearRing failed ring test)` message while loading data or creating geometries, this means that the geometry you are trying to instantiate is not topologically valid. This is usually due to self-intersections in the geometry. The default behavior of RGeo factories is to raise this error when an invalid geometry is being instansiated, but this can be ignored by setting the `uses_lenient_assertions` flag to `true` when creating your factory.
306
+
307
+ ```rb
308
+ regular_fac = RGeo::Geographic.spherical_factory
309
+ modified_fac = RGeo::Geographic.spherical_factory(uses_lenient_assertions: true)
310
+
311
+ wkt = "POLYGON (0 0, 1 1, 0 1, 1 0, 0 0)" # closed ring with self intersection
312
+
313
+ regular_fac.parse_wkt(wkt)
314
+ #=> RGeo::Error::InvalidGeometry (LinearRing failed ring test)
315
+
316
+ p modified_fac.parse_wkt(wkt)
317
+ #=> #<RGeo::Geographic::SphericalPolygonImpl>
318
+ ```
319
+
320
+ Be careful when performing calculations on potentially invalid geometries, as the results might be nonsensical. For example, the area returned of an hourglass made of 2 equivalent triangles with a self-intersection in the middle is 0.
321
+
322
+ Note that when using the `spherical_factory`, there is a chance that valid geometries will be interpreted as invalid due to floating point issues with small geometries.
25
323
 
26
324
  ## Modifying the adapter?
27
325
 
@@ -4,7 +4,7 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
4
 
5
5
  Gem::Specification.new do |spec|
6
6
  spec.name = "activerecord-cockroachdb-adapter"
7
- spec.version = "6.0.0beta2"
7
+ spec.version = "6.0.1"
8
8
  spec.licenses = ["Apache-2.0"]
9
9
  spec.authors = ["Cockroach Labs"]
10
10
  spec.email = ["cockroach-db@googlegroups.com"]
@@ -15,6 +15,7 @@ Gem::Specification.new do |spec|
15
15
 
16
16
  spec.add_dependency "activerecord", "~> 6.0.3"
17
17
  spec.add_dependency "pg", ">= 0.20"
18
+ spec.add_dependency "rgeo-activerecord", "~> 7.0.0"
18
19
 
19
20
  # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
20
21
  # to allow pushing to a single host or delete this section to allow pushing to any host.
@@ -11,6 +11,7 @@ connections:
11
11
  user: root
12
12
  requiressl: disable
13
13
  min_messages: warning
14
+ disable_cockroachdb_telemetry: true
14
15
  arunit_without_prepared_statements:
15
16
  database: activerecord_unittest
16
17
  host: localhost
@@ -19,6 +20,7 @@ connections:
19
20
  requiressl: disable
20
21
  min_messages: warning
21
22
  prepared_statements: false
23
+ disable_cockroachdb_telemetry: true
22
24
  arunit2:
23
25
  database: activerecord_unittest2
24
26
  host: localhost
@@ -26,3 +28,4 @@ connections:
26
28
  user: root
27
29
  requiressl: disable
28
30
  min_messages: warning
31
+ disable_cockroachdb_telemetry: true
@@ -21,7 +21,7 @@ run_cockroach() {
21
21
  cockroach quit --insecure || true
22
22
  rm -rf cockroach-data
23
23
  # Start CockroachDB.
24
- cockroach start-single-node --max-sql-memory=25% --cache=25% --insecure --host=localhost --listening-url-file="$urlfile" >/dev/null 2>&1 &
24
+ cockroach start-single-node --max-sql-memory=25% --cache=25% --insecure --host=localhost --spatial-libs=./cockroach-$VERSION.linux-amd64/lib --listening-url-file="$urlfile" >/dev/null 2>&1 &
25
25
  # Ensure CockroachDB is stopped on script exit.
26
26
  trap "echo 'Exit routine: Killing CockroachDB.' && kill -9 $! &> /dev/null" EXIT
27
27
  # Wait until CockroachDB has started.
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RGeo
4
+ module ActiveRecord
5
+ ##
6
+ # Extend rgeo-activerecord visitors to use PostGIS specific functionality
7
+ module SpatialToPostGISSql
8
+ def visit_in_spatial_context(node, collector)
9
+ # Use ST_GeomFromEWKT for EWKT geometries
10
+ if node.is_a?(String) && node =~ /SRID=[\d+]{0,};/
11
+ collector << "#{st_func('ST_GeomFromEWKT')}(#{quote(node)})"
12
+ else
13
+ super(node, collector)
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ RGeo::ActiveRecord::SpatialToSql.prepend RGeo::ActiveRecord::SpatialToPostGISSql
20
+
21
+ module Arel # :nodoc:
22
+ module Visitors # :nodoc:
23
+ class CockroachDB < PostgreSQL # :nodoc:
24
+ include RGeo::ActiveRecord::SpatialToSql
25
+ end
26
+ end
27
+ end
@@ -2,8 +2,85 @@ module ActiveRecord
2
2
  module ConnectionAdapters
3
3
  module CockroachDB
4
4
  module PostgreSQLColumnMonkeyPatch
5
+ # most functions taken from activerecord-postgis-adapter spatial_column
6
+ # https://github.com/rgeo/activerecord-postgis-adapter/blob/master/lib/active_record/connection_adapters/postgis/spatial_column.rb
7
+ def initialize(name, default, sql_type_metadata = nil, null = true,
8
+ default_function = nil, collation: nil, comment: nil,
9
+ serial: nil, spatial: nil)
10
+ @sql_type_metadata = sql_type_metadata
11
+ @geographic = !!(sql_type_metadata.sql_type =~ /geography\(/i)
12
+
13
+ if spatial
14
+ # This case comes from an entry in the geometry_columns table
15
+ set_geometric_type_from_name(spatial[:type])
16
+ @srid = spatial[:srid].to_i
17
+ @has_z = !!spatial[:has_z]
18
+ @has_m = !!spatial[:has_m]
19
+ elsif @geographic
20
+ # Geographic type information is embedded in the SQL type
21
+ @srid = 4326
22
+ @has_z = @has_m = false
23
+ build_from_sql_type(sql_type_metadata.sql_type)
24
+ elsif sql_type =~ /geography|geometry|point|linestring|polygon/i
25
+ build_from_sql_type(sql_type_metadata.sql_type)
26
+ elsif sql_type_metadata.sql_type =~ /geography|geometry|point|linestring|polygon/i
27
+ # A geometry column with no geometry_columns entry.
28
+ # @geometric_type = geo_type_from_sql_type(sql_type)
29
+ build_from_sql_type(sql_type_metadata.sql_type)
30
+ end
31
+ super(name, default, sql_type_metadata, null, default_function,
32
+ collation: collation, comment: comment, serial: serial)
33
+ if spatial? && @srid
34
+ @limit = { srid: @srid, type: to_type_name(geometric_type) }
35
+ @limit[:has_z] = true if @has_z
36
+ @limit[:has_m] = true if @has_m
37
+ @limit[:geographic] = true if @geographic
38
+ end
39
+ end
40
+
41
+ attr_reader :geographic,
42
+ :geometric_type,
43
+ :has_m,
44
+ :has_z,
45
+ :srid
46
+
47
+ alias geographic? geographic
48
+ alias has_z? has_z
49
+ alias has_m? has_m
50
+
51
+ def limit
52
+ spatial? ? @limit : super
53
+ end
54
+
55
+ def spatial?
56
+ %i[geometry geography].include?(@sql_type_metadata.type)
57
+ end
58
+
5
59
  def serial?
6
- default_function == "unique_rowid()"
60
+ default_function == 'unique_rowid()'
61
+ end
62
+
63
+ private
64
+
65
+ def set_geometric_type_from_name(name)
66
+ @geometric_type = RGeo::ActiveRecord.geometric_type_from_name(name) || RGeo::Feature::Geometry
67
+ end
68
+
69
+ def build_from_sql_type(sql_type)
70
+ geo_type, @srid, @has_z, @has_m = OID::Spatial.parse_sql_type(sql_type)
71
+ set_geometric_type_from_name(geo_type)
72
+ end
73
+
74
+ def to_type_name(geometric_type)
75
+ name = geometric_type.type_name.underscore
76
+ case name
77
+ when 'point'
78
+ 'st_point'
79
+ when 'polygon'
80
+ 'st_polygon'
81
+ else
82
+ name
83
+ end
7
84
  end
8
85
  end
9
86
  end