ydbi 0.5.2 → 0.5.7

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 (93) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/ruby.yml +35 -0
  3. data/.gitignore +8 -0
  4. data/.travis.yml +15 -0
  5. data/ChangeLog +339 -314
  6. data/Gemfile +5 -0
  7. data/Rakefile +10 -0
  8. data/TODO +44 -0
  9. data/bench/bench.rb +79 -0
  10. data/build/rake_task_lib.rb +186 -0
  11. data/doc/DBD_SPEC.rdoc +88 -0
  12. data/doc/DBI_SPEC.rdoc +157 -0
  13. data/doc/homepage/contact.html +62 -0
  14. data/doc/homepage/development.html +124 -0
  15. data/doc/homepage/index.html +83 -0
  16. data/doc/homepage/ruby-dbi.css +91 -0
  17. data/lib/dbd/Mysql.rb +137 -0
  18. data/lib/dbd/ODBC.rb +89 -0
  19. data/lib/dbd/Pg.rb +188 -0
  20. data/lib/dbd/SQLite.rb +97 -0
  21. data/lib/dbd/SQLite3.rb +124 -0
  22. data/lib/dbd/mysql/database.rb +405 -0
  23. data/lib/dbd/mysql/driver.rb +125 -0
  24. data/lib/dbd/mysql/statement.rb +188 -0
  25. data/lib/dbd/odbc/database.rb +128 -0
  26. data/lib/dbd/odbc/driver.rb +38 -0
  27. data/lib/dbd/odbc/statement.rb +137 -0
  28. data/lib/dbd/pg/database.rb +504 -0
  29. data/lib/dbd/pg/exec.rb +47 -0
  30. data/lib/dbd/pg/statement.rb +160 -0
  31. data/lib/dbd/pg/tuples.rb +121 -0
  32. data/lib/dbd/pg/type.rb +209 -0
  33. data/lib/dbd/sqlite/database.rb +151 -0
  34. data/lib/dbd/sqlite/statement.rb +125 -0
  35. data/lib/dbd/sqlite3/database.rb +201 -0
  36. data/lib/dbd/sqlite3/statement.rb +78 -0
  37. data/lib/dbi.rb +14 -17
  38. data/lib/dbi/utils/date.rb +7 -3
  39. data/lib/dbi/version.rb +1 -1
  40. data/prototypes/types2.rb +237 -0
  41. data/readme.md +15 -0
  42. data/setup.rb +1585 -0
  43. data/test/DBD_TESTS +50 -0
  44. data/test/TESTING +16 -0
  45. data/test/dbd/general/test_database.rb +206 -0
  46. data/test/dbd/general/test_statement.rb +326 -0
  47. data/test/dbd/general/test_types.rb +296 -0
  48. data/test/dbd/mysql/base.rb +26 -0
  49. data/test/dbd/mysql/down.sql +19 -0
  50. data/test/dbd/mysql/test_blob.rb +18 -0
  51. data/test/dbd/mysql/test_new_methods.rb +7 -0
  52. data/test/dbd/mysql/test_patches.rb +111 -0
  53. data/test/dbd/mysql/up.sql +28 -0
  54. data/test/dbd/odbc/base.rb +30 -0
  55. data/test/dbd/odbc/down.sql +19 -0
  56. data/test/dbd/odbc/test_new_methods.rb +12 -0
  57. data/test/dbd/odbc/test_ping.rb +10 -0
  58. data/test/dbd/odbc/test_statement.rb +44 -0
  59. data/test/dbd/odbc/test_transactions.rb +58 -0
  60. data/test/dbd/odbc/up.sql +33 -0
  61. data/test/dbd/postgresql/base.rb +31 -0
  62. data/test/dbd/postgresql/down.sql +31 -0
  63. data/test/dbd/postgresql/test_arrays.rb +179 -0
  64. data/test/dbd/postgresql/test_async.rb +121 -0
  65. data/test/dbd/postgresql/test_blob.rb +36 -0
  66. data/test/dbd/postgresql/test_bytea.rb +87 -0
  67. data/test/dbd/postgresql/test_ping.rb +10 -0
  68. data/test/dbd/postgresql/test_timestamp.rb +77 -0
  69. data/test/dbd/postgresql/test_transactions.rb +58 -0
  70. data/test/dbd/postgresql/testdbipg.rb +307 -0
  71. data/test/dbd/postgresql/up.sql +60 -0
  72. data/test/dbd/sqlite/base.rb +32 -0
  73. data/test/dbd/sqlite/test_database.rb +30 -0
  74. data/test/dbd/sqlite/test_driver.rb +68 -0
  75. data/test/dbd/sqlite/test_statement.rb +112 -0
  76. data/test/dbd/sqlite/up.sql +25 -0
  77. data/test/dbd/sqlite3/base.rb +32 -0
  78. data/test/dbd/sqlite3/test_database.rb +77 -0
  79. data/test/dbd/sqlite3/test_driver.rb +67 -0
  80. data/test/dbd/sqlite3/test_statement.rb +88 -0
  81. data/test/dbd/sqlite3/up.sql +33 -0
  82. data/test/dbi/tc_columninfo.rb +4 -9
  83. data/test/dbi/tc_date.rb +2 -9
  84. data/test/dbi/tc_dbi.rb +3 -9
  85. data/test/dbi/tc_row.rb +17 -23
  86. data/test/dbi/tc_sqlbind.rb +6 -7
  87. data/test/dbi/tc_statementhandle.rb +3 -4
  88. data/test/dbi/tc_time.rb +2 -8
  89. data/test/dbi/tc_timestamp.rb +2 -16
  90. data/test/dbi/tc_types.rb +5 -11
  91. data/test/ts_dbd.rb +131 -0
  92. data/ydbi.gemspec +23 -0
  93. metadata +128 -10
@@ -0,0 +1,504 @@
1
+ #
2
+ # See DBI::BaseDatabase.
3
+ #
4
+ class DBI::DBD::Pg::Database < DBI::BaseDatabase
5
+
6
+ # type map
7
+ POSTGRESQL_to_XOPEN = {
8
+ "boolean" => [DBI::SQL_CHAR, 1, nil],
9
+ "character" => [DBI::SQL_CHAR, 1, nil],
10
+ "char" => [DBI::SQL_CHAR, 1, nil],
11
+ "real" => [DBI::SQL_REAL, 4, 6],
12
+ "double precision" => [DBI::SQL_DOUBLE, 8, 15],
13
+ "smallint" => [DBI::SQL_SMALLINT, 2],
14
+ "integer" => [DBI::SQL_INTEGER, 4],
15
+ "bigint" => [DBI::SQL_BIGINT, 8],
16
+ "numeric" => [DBI::SQL_NUMERIC, nil, nil],
17
+ "time with time zone" => [DBI::SQL_TIME, nil, nil],
18
+ "timestamp with time zone" => [DBI::SQL_TIMESTAMP, nil, nil],
19
+ "bit varying" => [DBI::SQL_BINARY, nil, nil], #huh??
20
+ "character varying" => [DBI::SQL_VARCHAR, nil, nil],
21
+ "bit" => [DBI::SQL_TINYINT, nil, nil],
22
+ "text" => [DBI::SQL_VARCHAR, nil, nil],
23
+ nil => [DBI::SQL_OTHER, nil, nil]
24
+ }
25
+
26
+ attr_reader :type_map
27
+
28
+ #
29
+ # See DBI::BaseDatabase#new. These attributes are also supported:
30
+ #
31
+ # * pg_async: boolean or strings 'true' or 'false'. Indicates if we're to
32
+ # use PostgreSQL's asyncrohonous support. 'NonBlocking' is a synonym for
33
+ # this.
34
+ # * AutoCommit: 'unchained' mode in PostgreSQL. Commits after each
35
+ # statement execution.
36
+ # * pg_client_encoding: set the encoding for the client.
37
+ # * pg_native_binding: Boolean. Indicates whether to use libpq native
38
+ # binding or DBI's inline binding. Defaults to true.
39
+ #
40
+ def initialize(dbname, user, auth, attr)
41
+ hash = DBI::Utils.parse_params(dbname)
42
+
43
+ if hash['dbname'].nil? and hash['database'].nil?
44
+ raise DBI::InterfaceError, "must specify database"
45
+ end
46
+
47
+ hash['options'] ||= nil
48
+ hash['tty'] ||= ''
49
+ hash['host'] ||= 'localhost'
50
+ hash['port'] = hash['port'].to_i unless hash['port'].nil?
51
+
52
+ @connection = PG::Connection.new(hash['host'], hash['port'], hash['options'], hash['tty'],
53
+ hash['dbname'] || hash['database'], user, auth)
54
+
55
+ @exec_method = :exec
56
+ @in_transaction = false
57
+
58
+ # set attribute defaults, and look for pg_* attrs in the DSN
59
+ @attr = { 'AutoCommit' => true, 'pg_async' => false }
60
+ hash.each do |key, value|
61
+ @attr[key] = value if key =~ /^pg_./
62
+ end
63
+ @attr.merge!(attr || {})
64
+ if @attr['pg_async'].is_a?(String)
65
+ case @attr['pg_async'].downcase
66
+ when 'true'
67
+ @attr['pg_async'] = true
68
+ when 'false'
69
+ @attr['pg_async'] = false
70
+ else
71
+ raise InterfaceError, %q{'pg_async' must be 'true' or 'false'}
72
+ end
73
+ end
74
+
75
+ @attr.each { |k,v| self[k] = v}
76
+ @attr["pg_native_binding"] = true unless @attr.has_key? "pg_native_binding"
77
+
78
+ load_type_map
79
+
80
+ self['AutoCommit'] = true # Postgres starts in unchained mode (AutoCommit=on) by default
81
+
82
+ rescue PG::Error => err
83
+ raise DBI::OperationalError.new(err.message)
84
+ end
85
+
86
+ def disconnect
87
+ if not @attr['AutoCommit'] and @in_transaction
88
+ _exec("ROLLBACK") # rollback outstanding transactions
89
+ end
90
+ @connection.close
91
+ end
92
+
93
+ def ping
94
+ answer = _exec("SELECT 1")
95
+ if answer
96
+ return answer.num_tuples == 1
97
+ else
98
+ return false
99
+ end
100
+ rescue PG::Error
101
+ return false
102
+ ensure
103
+ answer.clear if answer
104
+ end
105
+
106
+ def database_name
107
+ @connection.db
108
+ end
109
+
110
+ def tables
111
+ stmt = execute("SELECT c.relname FROM pg_catalog.pg_class c WHERE c.relkind IN ('r','v') and pg_catalog.pg_table_is_visible(c.oid)")
112
+ res = stmt.fetch_all.collect {|row| row[0]}
113
+ stmt.finish
114
+ res
115
+ end
116
+
117
+ #
118
+ # See DBI::BaseDatabase.
119
+ #
120
+ # These additional attributes are also supported:
121
+ #
122
+ # * nullable: true if NULL values are allowed in this column.
123
+ # * indexed: true if this column is a part of an index.
124
+ # * primary: true if this column is a part of a primary key.
125
+ # * unique: true if this column is a part of a unique key.
126
+ # * default: what will be insert if this column is left out of an insert query.
127
+ # * array_of_type: true if this is actually an array of this type.
128
+ # +dbi_type+ will be the type authority if this is the case.
129
+ #
130
+ def columns(table)
131
+ sql1 = %[
132
+ select a.attname, i.indisprimary, i.indisunique
133
+ from pg_class bc inner join pg_index i
134
+ on bc.oid = i.indrelid
135
+ inner join pg_class c
136
+ on c.oid = i.indexrelid
137
+ inner join pg_attribute a
138
+ on c.oid = a.attrelid
139
+ where bc.relname = ?
140
+ and bc.relkind in ('r', 'v')
141
+ and pg_catalog.pg_table_is_visible(bc.oid);
142
+ ]
143
+
144
+ sql2 = %[
145
+ SELECT a.attname, a.atttypid, a.attnotnull, a.attlen, format_type(a.atttypid, a.atttypmod)
146
+ FROM pg_catalog.pg_class c, pg_attribute a, pg_type t
147
+ WHERE a.attnum > 0 AND a.attrelid = c.oid AND a.atttypid = t.oid AND c.relname = ?
148
+ AND c.relkind IN ('r','v')
149
+ AND pg_catalog.pg_table_is_visible(c.oid)
150
+ ]
151
+
152
+ # by Michael Neumann (get default value)
153
+ # corrected by Joseph McDonald
154
+ sql3 = %[
155
+ SELECT pg_attrdef.adsrc, pg_attribute.attname
156
+ FROM pg_attribute, pg_attrdef, pg_catalog.pg_class
157
+ WHERE pg_catalog.pg_class.relname = ? AND
158
+ pg_attribute.attrelid = pg_catalog.pg_class.oid AND
159
+ pg_attrdef.adrelid = pg_catalog.pg_class.oid AND
160
+ pg_attrdef.adnum = pg_attribute.attnum
161
+ AND pg_catalog.pg_class.relkind IN ('r','v')
162
+ AND pg_catalog.pg_table_is_visible(pg_catalog.pg_class.oid)
163
+ ]
164
+
165
+ dbh = DBI::DatabaseHandle.new(self)
166
+ dbh.driver_name = DBI::DBD::Pg.driver_name
167
+ indices = {}
168
+ default_values = {}
169
+
170
+ dbh.select_all(sql3, table) do |default, name|
171
+ default_values[name] = default
172
+ end
173
+
174
+ dbh.select_all(sql1, table) do |name, primary, unique|
175
+ indices[name] = [primary, unique]
176
+ end
177
+
178
+ ##########
179
+
180
+ ret = []
181
+ dbh.execute(sql2, table) do |sth|
182
+ ret = sth.collect do |row|
183
+ name, pg_type, notnullable, len, ftype = row
184
+ #name = row[2]
185
+ indexed = false
186
+ primary = nil
187
+ unique = nil
188
+ if indices.has_key?(name)
189
+ indexed = true
190
+ primary, unique = indices[name]
191
+ end
192
+
193
+ typeinfo = DBI::DBD::Pg.parse_type(ftype)
194
+ typeinfo[:size] ||= len
195
+
196
+ if POSTGRESQL_to_XOPEN.has_key?(typeinfo[:type])
197
+ sql_type = POSTGRESQL_to_XOPEN[typeinfo[:type]][0]
198
+ else
199
+ sql_type = POSTGRESQL_to_XOPEN[nil][0]
200
+ end
201
+
202
+ row = {}
203
+ row['name'] = name
204
+ row['sql_type'] = sql_type
205
+ row['type_name'] = typeinfo[:type]
206
+ row['nullable'] = ! notnullable
207
+ row['indexed'] = indexed
208
+ row['primary'] = primary
209
+ row['unique'] = unique
210
+ row['precision'] = typeinfo[:size]
211
+ row['scale'] = typeinfo[:decimal]
212
+ row['default'] = default_values[name]
213
+ row['array_of_type'] = typeinfo[:array]
214
+
215
+ if typeinfo[:array]
216
+ row['dbi_type'] =
217
+ DBI::DBD::Pg::Type::Array.new(
218
+ DBI::TypeUtil.type_name_to_module(typeinfo[:type])
219
+ )
220
+ end
221
+ row
222
+ end # collect
223
+ end # execute
224
+
225
+ return ret
226
+ end
227
+
228
+ def prepare(statement)
229
+ DBI::DBD::Pg::Statement.new(self, statement)
230
+ end
231
+
232
+ def [](attr)
233
+ case attr
234
+ when 'pg_client_encoding'
235
+ @connection.client_encoding
236
+ when 'NonBlocking'
237
+ @attr['pg_async']
238
+ else
239
+ @attr[attr]
240
+ end
241
+ end
242
+
243
+ def []=(attr, value)
244
+ case attr
245
+ when 'AutoCommit'
246
+ if @attr['AutoCommit'] != value then
247
+ if value # turn AutoCommit ON
248
+ if @in_transaction
249
+ # TODO: commit outstanding transactions?
250
+ _exec("COMMIT")
251
+ @in_transaction = false
252
+ end
253
+ else # turn AutoCommit OFF
254
+ @in_transaction = false
255
+ end
256
+ end
257
+ # value is assigned below
258
+ when 'NonBlocking', 'pg_async'
259
+ # booleanize input
260
+ value = value ? true : false
261
+ @pgexec = (value ? DBI::DBD::Pg::PgExecutorAsync : DBI::DBD::Pg::PgExecutor).new(@connection)
262
+ # value is assigned to @attr below
263
+ when 'pg_client_encoding'
264
+ @connection.set_client_encoding(value)
265
+ when 'pg_native_binding'
266
+ @attr[attr] = value
267
+ else
268
+ if attr =~ /^pg_/ or attr != /_/
269
+ raise DBI::NotSupportedError, "Option '#{attr}' not supported"
270
+ else # option for some other driver - quitly ignore
271
+ return
272
+ end
273
+ end
274
+ @attr[attr] = value
275
+ end
276
+
277
+ def commit
278
+ if @in_transaction
279
+ _exec("COMMIT")
280
+ @in_transaction = false
281
+ else
282
+ # TODO: Warn?
283
+ end
284
+ end
285
+
286
+ def rollback
287
+ if @in_transaction
288
+ _exec("ROLLBACK")
289
+ @in_transaction = false
290
+ else
291
+ # TODO: Warn?
292
+ end
293
+ end
294
+
295
+ #
296
+ # Are we in an transaction?
297
+ #
298
+ def in_transaction?
299
+ @in_transaction
300
+ end
301
+
302
+ #
303
+ # Forcibly initializes a new transaction.
304
+ #
305
+ def start_transaction
306
+ _exec("BEGIN")
307
+ @in_transaction = true
308
+ end
309
+
310
+ def _exec(sql, *parameters)
311
+ @pgexec.exec(sql, parameters)
312
+ end
313
+
314
+ def _exec_prepared(stmt_name, *parameters)
315
+ @pgexec.exec_prepared(stmt_name, parameters)
316
+ end
317
+
318
+ def _prepare(stmt_name, sql)
319
+ @pgexec.prepare(stmt_name, sql)
320
+ end
321
+
322
+ private
323
+
324
+ def parse_type_name(type_name)
325
+ case type_name
326
+ when 'bool' then DBI::Type::Boolean
327
+ when 'int8', 'int4', 'int2' then DBI::Type::Integer
328
+ when 'varchar' then DBI::Type::Varchar
329
+ when 'float4','float8' then DBI::Type::Float
330
+ when 'time', 'timetz' then DBI::Type::Timestamp
331
+ when 'timestamp', 'timestamptz' then DBI::Type::Timestamp
332
+ when 'date' then DBI::Type::Timestamp
333
+ when 'decimal', 'numeric' then DBI::Type::Decimal
334
+ when 'bytea' then DBI::DBD::Pg::Type::ByteA
335
+ when 'enum' then DBI::Type::Varchar
336
+ end
337
+ end
338
+
339
+ #
340
+ # Gathers the types from the postgres database and attempts to
341
+ # locate matching DBI::Type objects for them.
342
+ #
343
+ def load_type_map
344
+ @type_map = Hash.new
345
+
346
+ res = _exec("SELECT oid, typname, typelem FROM pg_type WHERE typtype IN ('b', 'e')")
347
+
348
+ res.each do |row|
349
+ rowtype = parse_type_name(row["typname"])
350
+ @type_map[row["oid"].to_i] =
351
+ {
352
+ "type_name" => row["typname"],
353
+ "dbi_type" =>
354
+ if rowtype
355
+ rowtype
356
+ elsif row["typname"] =~ /^_/ and row["typelem"].to_i > 0 then
357
+ # arrays are special and have a subtype, as an
358
+ # oid held in the "typelem" field.
359
+ # Since we may not have a mapping for the
360
+ # subtype yet, defer by storing the typelem
361
+ # integer as a base type in a constructed
362
+ # Type::Array object. dirty, i know.
363
+ #
364
+ # These array objects will be reconstructed
365
+ # after all rows are processed and therefore
366
+ # the oid -> type mapping is complete.
367
+ #
368
+ DBI::DBD::Pg::Type::Array.new(row["typelem"].to_i)
369
+ else
370
+ DBI::Type::Varchar
371
+ end
372
+ }
373
+ end
374
+ # additional conversions
375
+ @type_map[705] ||= DBI::Type::Varchar # select 'hallo'
376
+ @type_map[1114] ||= DBI::Type::Timestamp # TIMESTAMP WITHOUT TIME ZONE
377
+
378
+ # remap array subtypes
379
+ @type_map.each_key do |key|
380
+ if @type_map[key]["dbi_type"].class == DBI::DBD::Pg::Type::Array
381
+ oid = @type_map[key]["dbi_type"].base_type
382
+ if @type_map[oid]
383
+ @type_map[key]["dbi_type"] = DBI::DBD::Pg::Type::Array.new(@type_map[oid]["dbi_type"])
384
+ else
385
+ # punt
386
+ @type_map[key] = DBI::DBD::Pg::Type::Array.new(DBI::Type::Varchar)
387
+ end
388
+ end unless key.is_a?(Integer)
389
+ end
390
+ end
391
+
392
+ public
393
+
394
+ # return the postgresql types for this session. returns an oid -> type name mapping.
395
+ def __types(force=nil)
396
+ load_type_map if (!@type_map or force)
397
+ @type_map
398
+ end
399
+
400
+ # deprecated.
401
+ def __types_old
402
+ h = { }
403
+
404
+ _exec('select oid, typname from pg_type').each do |row|
405
+ h[row["oid"].to_i] = row["typname"]
406
+ end
407
+
408
+ return h
409
+ end
410
+
411
+ #
412
+ # Import a BLOB from a file.
413
+ #
414
+ def __blob_import(file)
415
+ start_transaction unless @in_transaction
416
+ @connection.lo_import(file)
417
+ rescue PG::Error => err
418
+ raise DBI::DatabaseError.new(err.message)
419
+ end
420
+
421
+ #
422
+ # Export a BLOB to a file.
423
+ #
424
+ def __blob_export(oid, file)
425
+ start_transaction unless @in_transaction
426
+ @connection.lo_export(oid.to_i, file)
427
+ rescue PG::Error => err
428
+ raise DBI::DatabaseError.new(err.message)
429
+ end
430
+
431
+ #
432
+ # Create a BLOB.
433
+ #
434
+ def __blob_create(mode=PG::Connection::INV_READ)
435
+ start_transaction unless @in_transaction
436
+ @connection.lo_creat(mode)
437
+ rescue PG::Error => err
438
+ raise DBI::DatabaseError.new(err.message)
439
+ end
440
+
441
+ #
442
+ # Open a BLOB.
443
+ #
444
+ def __blob_open(oid, mode=PG::Connection::INV_READ)
445
+ start_transaction unless @in_transaction
446
+ @connection.lo_open(oid.to_i, mode)
447
+ rescue PG::Error => err
448
+ raise DBI::DatabaseError.new(err.message)
449
+ end
450
+
451
+ #
452
+ # Remove a BLOB.
453
+ #
454
+ def __blob_unlink(oid)
455
+ start_transaction unless @in_transaction
456
+ @connection.lo_unlink(oid.to_i)
457
+ rescue PG::Error => err
458
+ raise DBI::DatabaseError.new(err.message)
459
+ end
460
+
461
+ #
462
+ # Read a BLOB and return the data.
463
+ #
464
+ def __blob_read(oid, length)
465
+ blob = @connection.lo_open(oid.to_i, PG::Connection::INV_READ)
466
+
467
+ if length.nil?
468
+ data = @connection.lo_read(blob)
469
+ else
470
+ data = @connection.lo_read(blob, length)
471
+ end
472
+
473
+ # FIXME it doesn't like to close here either.
474
+ # @connection.lo_close(blob)
475
+ data
476
+ rescue PG::Error => err
477
+ raise DBI::DatabaseError.new(err.message)
478
+ end
479
+
480
+ #
481
+ # Write the value to the BLOB.
482
+ #
483
+ def __blob_write(oid, value)
484
+ start_transaction unless @in_transaction
485
+ blob = @connection.lo_open(oid.to_i, PG::Connection::INV_WRITE)
486
+ res = @connection.lo_write(blob, value)
487
+ # FIXME not sure why PG doesn't like to close here -- seems to be
488
+ # working but we should make sure it's not eating file descriptors
489
+ # up before release.
490
+ # @connection.lo_close(blob)
491
+ return res
492
+ rescue PG::Error => err
493
+ raise DBI::DatabaseError.new(err.message)
494
+ end
495
+
496
+ #
497
+ # FIXME DOCUMENT
498
+ #
499
+ def __set_notice_processor(proc)
500
+ @connection.set_notice_processor proc
501
+ rescue PG::Error => err
502
+ raise DBI::DatabaseError.new(err.message)
503
+ end
504
+ end # Database