skiima 0.1.000 → 0.2.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/.gitignore +9 -0
- data/.travis.yml +11 -3
- data/Gemfile +12 -6
- data/Guardfile +13 -11
- data/LICENSE +20 -0
- data/Procfile.example +2 -0
- data/README.md +170 -23
- data/Rakefile +26 -22
- data/lib/skiima.rb +61 -240
- data/lib/skiima/config.rb +60 -0
- data/lib/skiima/config/struct.rb +87 -0
- data/lib/skiima/db/connector.rb +195 -0
- data/lib/skiima/db/connector/active_record.rb +11 -0
- data/lib/skiima/db/connector/active_record/base_connector.rb +34 -0
- data/lib/skiima/db/connector/active_record/mysql2_connector.rb +147 -0
- data/lib/skiima/db/connector/active_record/mysql_connector.rb +177 -0
- data/lib/skiima/db/connector/active_record/postgresql_connector.rb +39 -0
- data/lib/skiima/db/helpers/mysql.rb +230 -0
- data/lib/skiima/db/helpers/postgresql.rb +221 -0
- data/lib/skiima/db/resolver.rb +62 -0
- data/lib/skiima/dependency/reader.rb +55 -0
- data/lib/skiima/dependency/script.rb +63 -0
- data/lib/skiima/i18n.rb +24 -0
- data/lib/skiima/loader.rb +108 -0
- data/lib/skiima/locales/en.yml +2 -2
- data/lib/skiima/logger.rb +54 -0
- data/lib/skiima/railtie.rb +10 -0
- data/lib/skiima/railties/skiima.rake +31 -0
- data/lib/skiima/version.rb +2 -2
- data/skiima.gemspec +5 -5
- data/spec/config/{database.yml → database.yml.example} +16 -0
- data/spec/config/database.yml.travis +69 -0
- data/spec/db/skiima/{depends.yml → dependencies.yml} +7 -2
- data/spec/db/skiima/{empty_depends.yml → empty_dependencies.yml} +0 -0
- data/spec/db/skiima/init_test_db/database.skiima_test.mysql.current.sql +7 -0
- data/spec/db/skiima/init_test_db/database.skiima_test.postgresql.current.sql +7 -0
- data/spec/mysql2_spec.rb +61 -12
- data/spec/mysql_spec.rb +66 -27
- data/spec/postgresql_spec.rb +55 -34
- data/spec/shared_examples/config_shared_example.rb +40 -0
- data/spec/skiima/config/struct_spec.rb +78 -0
- data/spec/skiima/config_spec.rb +6 -0
- data/spec/skiima/db/connector/active_record/base_connector_spec.rb +0 -0
- data/spec/skiima/db/connector/active_record/mysql2_connector_spec.rb +3 -0
- data/spec/skiima/db/connector/active_record/mysql_connector_spec.rb +3 -0
- data/spec/skiima/db/connector/active_record/postgresql_connector_spec.rb +7 -0
- data/spec/skiima/db/connector_spec.rb +6 -0
- data/spec/skiima/db/resolver_spec.rb +54 -0
- data/spec/skiima/dependency/reader_spec.rb +52 -0
- data/spec/skiima/{dependency_spec.rb → dependency/script_spec.rb} +3 -41
- data/spec/skiima/i18n_spec.rb +29 -0
- data/spec/skiima/loader_spec.rb +102 -0
- data/spec/skiima/logger_spec.rb +0 -0
- data/spec/skiima_spec.rb +43 -64
- data/spec/spec_helper.rb +38 -4
- metadata +144 -100
- data/lib/skiima/db_adapters.rb +0 -187
- data/lib/skiima/db_adapters/base_mysql_adapter.rb +0 -308
- data/lib/skiima/db_adapters/mysql2_adapter.rb +0 -114
- data/lib/skiima/db_adapters/mysql_adapter.rb +0 -287
- data/lib/skiima/db_adapters/postgresql_adapter.rb +0 -509
- data/lib/skiima/dependency.rb +0 -84
- data/lib/skiima_helpers.rb +0 -49
- data/spec/skiima/db_adapters/mysql_adapter_spec.rb +0 -38
- data/spec/skiima/db_adapters/postgresql_adapter_spec.rb +0 -20
- data/spec/skiima/db_adapters_spec.rb +0 -31
data/lib/skiima/db_adapters.rb
DELETED
@@ -1,187 +0,0 @@
|
|
1
|
-
# encoding: utf-8
|
2
|
-
module Skiima
|
3
|
-
module DbAdapters
|
4
|
-
class Base
|
5
|
-
attr_accessor :version
|
6
|
-
|
7
|
-
def initialize(connection, logger = nil) #:nodoc:
|
8
|
-
super()
|
9
|
-
|
10
|
-
@active = nil
|
11
|
-
@connection = connection
|
12
|
-
@in_use = false
|
13
|
-
@last_use = false
|
14
|
-
@logger = logger
|
15
|
-
@visitor = nil
|
16
|
-
end
|
17
|
-
|
18
|
-
def adapter_name
|
19
|
-
'Base'
|
20
|
-
end
|
21
|
-
|
22
|
-
def supports_ddl_transactions?
|
23
|
-
false
|
24
|
-
end
|
25
|
-
|
26
|
-
def supported_objects
|
27
|
-
[] # this should be overridden by concrete adapters
|
28
|
-
end
|
29
|
-
|
30
|
-
def drop(type, name, opts = {})
|
31
|
-
send("drop_#{type}", name, opts) if supported_objects.include? type.to_sym
|
32
|
-
end
|
33
|
-
|
34
|
-
def object_exists?(type, name, opts = {})
|
35
|
-
send("#{type}_exists?", name, opts) if supported_objects.include? type.to_sym
|
36
|
-
end
|
37
|
-
|
38
|
-
# Does this adapter support savepoints? PostgreSQL and MySQL do,
|
39
|
-
# SQLite < 3.6.8 does not.
|
40
|
-
def supports_savepoints?
|
41
|
-
false
|
42
|
-
end
|
43
|
-
|
44
|
-
def active?
|
45
|
-
@active != false
|
46
|
-
end
|
47
|
-
|
48
|
-
# Disconnects from the database if already connected, and establishes a
|
49
|
-
# new connection with the database.
|
50
|
-
def reconnect!
|
51
|
-
@active = true
|
52
|
-
end
|
53
|
-
|
54
|
-
# Disconnects from the database if already connected. Otherwise, this
|
55
|
-
# method does nothing.
|
56
|
-
def disconnect!
|
57
|
-
@active = false
|
58
|
-
end
|
59
|
-
|
60
|
-
# Reset the state of this connection, directing the DBMS to clear
|
61
|
-
# transactions and other connection-related server-side state. Usually a
|
62
|
-
# database-dependent operation.
|
63
|
-
#
|
64
|
-
# The default implementation does nothing; the implementation should be
|
65
|
-
# overridden by concrete adapters.
|
66
|
-
def reset!
|
67
|
-
# this should be overridden by concrete adapters
|
68
|
-
end
|
69
|
-
|
70
|
-
###
|
71
|
-
# Clear any caching the database adapter may be doing, for example
|
72
|
-
# clearing the prepared statement cache. This is database specific.
|
73
|
-
def clear_cache!
|
74
|
-
# this should be overridden by concrete adapters
|
75
|
-
end
|
76
|
-
|
77
|
-
# Returns true if its required to reload the connection between requests for development mode.
|
78
|
-
# This is not the case for Ruby/MySQL and it's not necessary for any adapters except SQLite.
|
79
|
-
def requires_reloading?
|
80
|
-
false
|
81
|
-
end
|
82
|
-
|
83
|
-
# Checks whether the connection to the database is still active (i.e. not stale).
|
84
|
-
# This is done under the hood by calling <tt>active?</tt>. If the connection
|
85
|
-
# is no longer active, then this method will reconnect to the database.
|
86
|
-
def verify!(*ignored)
|
87
|
-
reconnect! unless active?
|
88
|
-
end
|
89
|
-
|
90
|
-
# Provides access to the underlying database driver for this adapter. For
|
91
|
-
# example, this method returns a Mysql object in case of MysqlAdapter,
|
92
|
-
# and a PGconn object in case of PostgreSQLAdapter.
|
93
|
-
#
|
94
|
-
# This is useful for when you need to call a proprietary method such as
|
95
|
-
# PostgreSQL's lo_* methods.
|
96
|
-
def raw_connection
|
97
|
-
@connection
|
98
|
-
end
|
99
|
-
|
100
|
-
attr_reader :open_transactions
|
101
|
-
|
102
|
-
def increment_open_transactions
|
103
|
-
@open_transactions += 1
|
104
|
-
end
|
105
|
-
|
106
|
-
def decrement_open_transactions
|
107
|
-
@open_transactions -= 1
|
108
|
-
end
|
109
|
-
|
110
|
-
def transaction_joinable=(joinable)
|
111
|
-
@transaction_joinable = joinable
|
112
|
-
end
|
113
|
-
|
114
|
-
def create_savepoint
|
115
|
-
end
|
116
|
-
|
117
|
-
def rollback_to_savepoint
|
118
|
-
end
|
119
|
-
|
120
|
-
def release_savepoint
|
121
|
-
end
|
122
|
-
|
123
|
-
def current_savepoint_name
|
124
|
-
"active_record_#{open_transactions}"
|
125
|
-
end
|
126
|
-
|
127
|
-
# Check the connection back in to the connection pool
|
128
|
-
def close
|
129
|
-
disconnect!
|
130
|
-
end
|
131
|
-
|
132
|
-
# Disconnects from the database if already connected. Otherwise, this
|
133
|
-
# method does nothing.
|
134
|
-
def disconnect!
|
135
|
-
clear_cache!
|
136
|
-
@connection.close rescue nil
|
137
|
-
end
|
138
|
-
|
139
|
-
protected
|
140
|
-
|
141
|
-
def log(sql, name = "SQL", binds = [])
|
142
|
-
@logger.debug("Executing SQL Statement: #{name}")
|
143
|
-
@logger.debug(sql)
|
144
|
-
result = yield
|
145
|
-
@logger.debug("SUCCESS!")
|
146
|
-
result
|
147
|
-
rescue Exception => e
|
148
|
-
message = "#{e.class.name}: #{e.message}: #{sql}"
|
149
|
-
@logger.debug message if @logger
|
150
|
-
exception = translate_exception(e, message)
|
151
|
-
exception.set_backtrace e.backtrace
|
152
|
-
raise exception
|
153
|
-
end
|
154
|
-
|
155
|
-
def translate_exception(e, message)
|
156
|
-
# override in derived class
|
157
|
-
raise "override in derived class"
|
158
|
-
end
|
159
|
-
|
160
|
-
end
|
161
|
-
|
162
|
-
class Resolver
|
163
|
-
attr_accessor :db, :adapter_method
|
164
|
-
|
165
|
-
def initialize(db_config)
|
166
|
-
@db = Skiima.symbolize_keys(db_config)
|
167
|
-
adapter_specified?
|
168
|
-
load_adapter
|
169
|
-
@adapter_method = "#{db[:adapter]}_connection"
|
170
|
-
end
|
171
|
-
|
172
|
-
private
|
173
|
-
|
174
|
-
def adapter_specified?
|
175
|
-
raise(AdapterNotSpecified, "database configuration does not specify adapter") unless db.key?(:adapter)
|
176
|
-
end
|
177
|
-
|
178
|
-
def load_adapter
|
179
|
-
begin
|
180
|
-
require "skiima/db_adapters/#{db[:adapter]}_adapter"
|
181
|
-
rescue => e
|
182
|
-
raise LoadError, "Adapter does not exist: #{db[:adapter]} - (#{e.message})", e.backtrace
|
183
|
-
end
|
184
|
-
end
|
185
|
-
end
|
186
|
-
end
|
187
|
-
end
|
@@ -1,308 +0,0 @@
|
|
1
|
-
# encoding: utf-8
|
2
|
-
module Skiima
|
3
|
-
module DbAdapters
|
4
|
-
class BaseMysqlAdapter < Base
|
5
|
-
attr_accessor :version
|
6
|
-
|
7
|
-
LOST_CONNECTION_ERROR_MESSAGES = [
|
8
|
-
"Server shutdown in progress",
|
9
|
-
"Broken pipe",
|
10
|
-
"Lost connection to MySQL server during query",
|
11
|
-
"MySQL server has gone away" ]
|
12
|
-
|
13
|
-
# FIXME: Make the first parameter more similar for the two adapters
|
14
|
-
def initialize(connection, logger, connection_options, config)
|
15
|
-
super(connection, logger)
|
16
|
-
@connection_options, @config = connection_options, config
|
17
|
-
@quoted_column_names, @quoted_table_names = {}, {}
|
18
|
-
end
|
19
|
-
|
20
|
-
def version
|
21
|
-
@version ||= @connection.info[:version].scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i }
|
22
|
-
end
|
23
|
-
|
24
|
-
def adapter_name #:nodoc:
|
25
|
-
self.class::ADAPTER_NAME
|
26
|
-
end
|
27
|
-
|
28
|
-
# Returns true, since this connection adapter supports migrations.
|
29
|
-
def supports_migrations?
|
30
|
-
true
|
31
|
-
end
|
32
|
-
|
33
|
-
def supports_primary_key?
|
34
|
-
true
|
35
|
-
end
|
36
|
-
|
37
|
-
# Returns true, since this connection adapter supports savepoints.
|
38
|
-
def supports_savepoints?
|
39
|
-
true
|
40
|
-
end
|
41
|
-
|
42
|
-
# Must return the Mysql error number from the exception, if the exception has an
|
43
|
-
# error number.
|
44
|
-
def error_number(exception) # :nodoc:
|
45
|
-
raise NotImplementedError
|
46
|
-
end
|
47
|
-
|
48
|
-
def disable_referential_integrity(&block) #:nodoc:
|
49
|
-
old = select_value("SELECT @@FOREIGN_KEY_CHECKS")
|
50
|
-
|
51
|
-
begin
|
52
|
-
update("SET FOREIGN_KEY_CHECKS = 0")
|
53
|
-
yield
|
54
|
-
ensure
|
55
|
-
update("SET FOREIGN_KEY_CHECKS = #{old}")
|
56
|
-
end
|
57
|
-
end
|
58
|
-
|
59
|
-
# MysqlAdapter has to free a result after using it, so we use this method to write
|
60
|
-
# stuff in a abstract way without concerning ourselves about whether it needs to be
|
61
|
-
# explicitly freed or not.
|
62
|
-
def execute_and_free(sql, name = nil) #:nodoc:
|
63
|
-
yield execute(sql, name)
|
64
|
-
end
|
65
|
-
|
66
|
-
# Executes the SQL statement in the context of this connection.
|
67
|
-
def execute(sql, name = nil)
|
68
|
-
if name == :skip_logging
|
69
|
-
@connection.query(sql)
|
70
|
-
else
|
71
|
-
log(sql, name) { @connection.query(sql) }
|
72
|
-
end
|
73
|
-
rescue StatementInvalid => exception
|
74
|
-
if exception.message.split(":").first =~ /Packets out of order/
|
75
|
-
raise StatementInvalid, "'Packets out of order' error was received from the database. Please update your mysql bindings (gem install mysql) and read http://dev.mysql.com/doc/mysql/en/password-hashing.html for more information. If you're on Windows, use the Instant Rails installer to get the updated mysql bindings."
|
76
|
-
else
|
77
|
-
raise
|
78
|
-
end
|
79
|
-
end
|
80
|
-
|
81
|
-
def begin_db_transaction
|
82
|
-
execute "BEGIN"
|
83
|
-
rescue Exception
|
84
|
-
# Transactions aren't supported
|
85
|
-
end
|
86
|
-
|
87
|
-
def commit_db_transaction #:nodoc:
|
88
|
-
execute "COMMIT"
|
89
|
-
rescue Exception
|
90
|
-
# Transactions aren't supported
|
91
|
-
end
|
92
|
-
|
93
|
-
def rollback_db_transaction #:nodoc:
|
94
|
-
execute "ROLLBACK"
|
95
|
-
rescue Exception
|
96
|
-
# Transactions aren't supported
|
97
|
-
end
|
98
|
-
|
99
|
-
def create_savepoint
|
100
|
-
execute("SAVEPOINT #{current_savepoint_name}")
|
101
|
-
end
|
102
|
-
|
103
|
-
def rollback_to_savepoint
|
104
|
-
execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}")
|
105
|
-
end
|
106
|
-
|
107
|
-
def release_savepoint
|
108
|
-
execute("RELEASE SAVEPOINT #{current_savepoint_name}")
|
109
|
-
end
|
110
|
-
|
111
|
-
def supported_objects
|
112
|
-
[:database, :table, :view, :index]
|
113
|
-
end
|
114
|
-
|
115
|
-
def tables(name = nil, database = nil, like = nil)
|
116
|
-
sql = "SHOW FULL TABLES "
|
117
|
-
sql << "IN #{database} " if database
|
118
|
-
sql << "WHERE table_type = 'BASE TABLE' "
|
119
|
-
sql << "LIKE '#{like}' " if like
|
120
|
-
|
121
|
-
execute_and_free(sql, 'SCHEMA') do |result|
|
122
|
-
result.collect { |field| field.first }
|
123
|
-
end
|
124
|
-
end
|
125
|
-
|
126
|
-
def views(name = nil, database = nil, like = nil)
|
127
|
-
sql = "SHOW FULL TABLES "
|
128
|
-
sql << "IN #{database} " if database
|
129
|
-
sql << "WHERE table_type = 'VIEW' "
|
130
|
-
sql << "LIKE '#{like}' " if like
|
131
|
-
|
132
|
-
execute_and_free(sql, 'SCHEMA') do |result|
|
133
|
-
result.collect { |field| field.first }
|
134
|
-
end
|
135
|
-
end
|
136
|
-
|
137
|
-
def indexes(name = nil, database = nil, table = nil)
|
138
|
-
sql = "SHOW INDEX "
|
139
|
-
sql << "IN #{table} "
|
140
|
-
sql << "IN #{database} " if database
|
141
|
-
sql << "WHERE key_name = '#{name}'" if name
|
142
|
-
|
143
|
-
execute_and_free(sql, 'SCHEMA') do |result|
|
144
|
-
result.collect { |field| field[2] }
|
145
|
-
end
|
146
|
-
end
|
147
|
-
|
148
|
-
"select routine_schema, routine_name, routine_type from routines;"
|
149
|
-
|
150
|
-
def procs(name = nil, database = nil, like = nil)
|
151
|
-
sql = "SELECT r.routine_name "
|
152
|
-
sql << "FROM information_schema.routines r "
|
153
|
-
sql << "WHERE r.routine_type = 'PROCEDURE' "
|
154
|
-
sql << "AND r.routine_name LIKE '#{like}' " if like
|
155
|
-
sql << "AND r.routine_schema = #{database} " if database
|
156
|
-
|
157
|
-
execute_and_free(sql, 'SCHEMA') do |result|
|
158
|
-
result.collect { |field| field.first }
|
159
|
-
end
|
160
|
-
end
|
161
|
-
|
162
|
-
def database_exists?(name)
|
163
|
-
#stub
|
164
|
-
end
|
165
|
-
|
166
|
-
def table_exists?(name)
|
167
|
-
return false unless name
|
168
|
-
return true if tables(nil, nil, name).any?
|
169
|
-
|
170
|
-
name = name.to_s
|
171
|
-
schema, table = name.split('.', 2)
|
172
|
-
|
173
|
-
unless table # A table was provided without a schema
|
174
|
-
table = schema
|
175
|
-
schema = nil
|
176
|
-
end
|
177
|
-
|
178
|
-
tables(nil, schema, table).any?
|
179
|
-
end
|
180
|
-
|
181
|
-
def view_exists?(name)
|
182
|
-
return false unless name
|
183
|
-
return true if views(nil, nil, name).any?
|
184
|
-
|
185
|
-
name = name.to_s
|
186
|
-
schema, view = name.split('.', 2)
|
187
|
-
|
188
|
-
unless view # A table was provided without a schema
|
189
|
-
view = schema
|
190
|
-
schema = nil
|
191
|
-
end
|
192
|
-
|
193
|
-
views(nil, schema, view).any?
|
194
|
-
end
|
195
|
-
|
196
|
-
def index_exists?(name, opts = {})
|
197
|
-
target = opts[:attr] ? opts[:attr][0] : nil
|
198
|
-
raise "requires target object" unless target
|
199
|
-
|
200
|
-
return false unless table_exists?(target) #mysql blows up when table doesn't exist
|
201
|
-
return false unless name
|
202
|
-
return true if indexes(name, nil, target).any?
|
203
|
-
|
204
|
-
name = name.to_s
|
205
|
-
schema, target = name.split('.', 2)
|
206
|
-
|
207
|
-
unless target # A table was provided without a schema
|
208
|
-
target = schema
|
209
|
-
schema = nil
|
210
|
-
end
|
211
|
-
|
212
|
-
indexes(name, schema, target).any?
|
213
|
-
end
|
214
|
-
|
215
|
-
def proc_exists?(name, opts = {})
|
216
|
-
return false unless name
|
217
|
-
return true if procs(nil, nil, name).any?
|
218
|
-
|
219
|
-
name = name.to_s
|
220
|
-
schema, proc = name.split('.', 2)
|
221
|
-
|
222
|
-
unless proc # A table was provided without a schema
|
223
|
-
proc = schema
|
224
|
-
schema = nil
|
225
|
-
end
|
226
|
-
|
227
|
-
procs(name, schema, proc).any?
|
228
|
-
end
|
229
|
-
|
230
|
-
def drop_database(name, opts = {})
|
231
|
-
"DROP DATABASE IF EXISTS #{name}"
|
232
|
-
end
|
233
|
-
|
234
|
-
def drop_table(name, opts = {})
|
235
|
-
"DROP TABLE IF EXISTS #{name}"
|
236
|
-
end
|
237
|
-
|
238
|
-
def drop_view(name, opts = {})
|
239
|
-
"DROP VIEW IF EXISTS #{name}"
|
240
|
-
end
|
241
|
-
|
242
|
-
def drop_index(name, opts = {})
|
243
|
-
target = opts[:attr].first if opts[:attr]
|
244
|
-
raise "requires target object" unless target
|
245
|
-
|
246
|
-
"DROP INDEX #{name} ON #{target}"
|
247
|
-
end
|
248
|
-
|
249
|
-
def column_definitions(table_name)
|
250
|
-
# "SHOW FULL FIELDS FROM #{quote_table_name(table_name)}"
|
251
|
-
end
|
252
|
-
|
253
|
-
def column_names(table_name)
|
254
|
-
sql = "SHOW FULL FIELDS FROM #{quote_table_name(table_name)}"
|
255
|
-
execute_and_free(sql, 'SCHEMA') do |result|
|
256
|
-
result.collect { |field| field.first }
|
257
|
-
end
|
258
|
-
end
|
259
|
-
|
260
|
-
def quote_column_name(name) #:nodoc:
|
261
|
-
@quoted_column_names[name] ||= "`#{name.to_s.gsub('`', '``')}`"
|
262
|
-
end
|
263
|
-
|
264
|
-
def quote_table_name(name) #:nodoc:
|
265
|
-
@quoted_table_names[name] ||= quote_column_name(name).gsub('.', '`.`')
|
266
|
-
end
|
267
|
-
|
268
|
-
def current_database
|
269
|
-
select_value 'SELECT DATABASE() as db'
|
270
|
-
end
|
271
|
-
|
272
|
-
# Returns the database character set.
|
273
|
-
def charset
|
274
|
-
show_variable 'character_set_database'
|
275
|
-
end
|
276
|
-
|
277
|
-
# Returns the database collation strategy.
|
278
|
-
def collation
|
279
|
-
show_variable 'collation_database'
|
280
|
-
end
|
281
|
-
|
282
|
-
def show_variable(name)
|
283
|
-
# variables = select_all("SHOW VARIABLES LIKE '#{name}'")
|
284
|
-
# variables.first['Value'] unless variables.empty?
|
285
|
-
end
|
286
|
-
|
287
|
-
protected
|
288
|
-
|
289
|
-
def translate_exception(exception, message)
|
290
|
-
exception
|
291
|
-
# case error_number(exception)
|
292
|
-
# when 1062
|
293
|
-
# RecordNotUnique.new(message, exception)
|
294
|
-
# when 1452
|
295
|
-
# InvalidForeignKey.new(message, exception)
|
296
|
-
# else
|
297
|
-
# super
|
298
|
-
# end
|
299
|
-
end
|
300
|
-
|
301
|
-
private
|
302
|
-
|
303
|
-
def supports_views?
|
304
|
-
version[0] >= 5
|
305
|
-
end
|
306
|
-
end
|
307
|
-
end
|
308
|
-
end
|