sedna 0.1.0 → 0.2.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.
- data/CHANGES +7 -1
- data/README +1 -1
- data/ext/sedna.c +77 -12
- data/test/{test_sedna.rb → sedna_test.rb} +144 -18
- metadata +4 -4
data/CHANGES
CHANGED
@@ -1,6 +1,12 @@
|
|
1
|
+
=== 0.2.0
|
2
|
+
|
3
|
+
* Released on December 15th, 2008.
|
4
|
+
* Added support for importing documents from a string or file (Sedna#load_document).
|
5
|
+
* Fixed garbage collection error for unsuccessful connections.
|
6
|
+
|
1
7
|
=== 0.1.0
|
2
8
|
|
3
|
-
* Released on
|
9
|
+
* Released on December 8th, 2008.
|
4
10
|
* Development preview (alpha) release.
|
5
11
|
* Simple, high-level API.
|
6
12
|
* Tested with Ruby 1.8.7 and Ruby 1.9.0.
|
data/README
CHANGED
@@ -32,7 +32,7 @@ limitations under the License.
|
|
32
32
|
|
33
33
|
=== Current version
|
34
34
|
|
35
|
-
The current version of this library is 0.
|
35
|
+
The current version of this library is 0.2.0. This version is a <b>development
|
36
36
|
preview</b>. The API may change significantly between minor versions.
|
37
37
|
For a complete overview all recent and previous changes, see CHANGES.
|
38
38
|
|
data/ext/sedna.c
CHANGED
@@ -24,7 +24,8 @@
|
|
24
24
|
#include "libsedna.h"
|
25
25
|
|
26
26
|
// Size of the read buffer.
|
27
|
-
#define
|
27
|
+
#define RESULT_BUF_LEN 8192
|
28
|
+
#define LOAD_BUF_LEN 8192
|
28
29
|
|
29
30
|
// Use macros to fool RDoc and hide some of our methods/aliases.
|
30
31
|
#define rb_define_undocumented_alias(kl, new, old) rb_define_alias(kl, new, old)
|
@@ -83,7 +84,7 @@ static void sedna_err(SC *conn, int res)
|
|
83
84
|
}
|
84
85
|
if(details != NULL) {
|
85
86
|
details += 10;
|
86
|
-
|
87
|
+
while((p = strstr(details, "\n")) != NULL) strncpy(p, " ", 1);
|
87
88
|
rb_raise(exception, "%s (%s)", err, details);
|
88
89
|
} else {
|
89
90
|
rb_raise(exception, "%s", err);
|
@@ -114,10 +115,10 @@ static void sedna_mark(SC *conn)
|
|
114
115
|
static VALUE sedna_read(SC *conn, int strip_n)
|
115
116
|
{
|
116
117
|
int bytes_read = 0;
|
117
|
-
char buffer[
|
118
|
+
char buffer[RESULT_BUF_LEN];
|
118
119
|
VALUE str = rb_str_buf_new(0);
|
119
120
|
do {
|
120
|
-
bytes_read = SEgetData(conn, buffer,
|
121
|
+
bytes_read = SEgetData(conn, buffer, RESULT_BUF_LEN - 1);
|
121
122
|
if(bytes_read == SEDNA_ERROR) {
|
122
123
|
sedna_err(conn, SEDNA_ERROR);
|
123
124
|
} else {
|
@@ -215,7 +216,16 @@ static VALUE cSedna_initialize(VALUE self, VALUE options)
|
|
215
216
|
if(NIL_P(pw_v = rb_hash_aref(options, pw_k))) pw = "MANAGER"; else pw = STR2CSTR(pw_v);
|
216
217
|
|
217
218
|
res = SEconnect(conn, host, db, user, pw);
|
218
|
-
|
219
|
+
if(res != SEDNA_SESSION_OPEN) {
|
220
|
+
// We have to set the connection status to closed explicitly here,
|
221
|
+
// because the GC routine sedna_free() will test for this status, but
|
222
|
+
// the socket is already closed by SEconnect(). If we do not change the
|
223
|
+
// status, sedna_free() will attempt to close the connection again by
|
224
|
+
// calling SEclose(), which will definitely lead to unpredictable
|
225
|
+
// results.
|
226
|
+
conn->isConnectionOk = SEDNA_CONNECTION_CLOSED;
|
227
|
+
sedna_err(conn, res);
|
228
|
+
}
|
219
229
|
|
220
230
|
// Initialize @autocommit to true (default argument).
|
221
231
|
rb_iv_set(self, "@autocommit", Qtrue);
|
@@ -297,7 +307,7 @@ static VALUE cSedna_s_connect(VALUE klass, VALUE options)
|
|
297
307
|
* sedna.execute(query) -> array or nil
|
298
308
|
* sedna.query(query) -> array or nil
|
299
309
|
*
|
300
|
-
*
|
310
|
+
* Executes the given +query+ against a \Sedna database. Returns an array if the
|
301
311
|
* given query is a select query. The elements of the array are strings that
|
302
312
|
* correspond to each result in the result set. If the query is an update query
|
303
313
|
* or a (bulk) load query, +nil+ is returned. When attempting to \execute a
|
@@ -340,6 +350,60 @@ static VALUE cSedna_execute(VALUE self, VALUE query)
|
|
340
350
|
}
|
341
351
|
}
|
342
352
|
|
353
|
+
/*
|
354
|
+
* call-seq:
|
355
|
+
* sedna.load_document(document, doc_name, col_name = nil) -> nil
|
356
|
+
*
|
357
|
+
* Creates a new document named +doc_name+ in collection +col_name+, or as a
|
358
|
+
* stand-alone document if +col_name+ is +nil+. The string +document+ is
|
359
|
+
* subsequently loaded into the newly created document. As an alternative, the
|
360
|
+
* argument +document+ may be an IO object (or any descendant, such as a File
|
361
|
+
* object).
|
362
|
+
*
|
363
|
+
* If the document was successfully loaded, this method returns +nil+. If an
|
364
|
+
* error occurs, a Sedna::Exception is raised.
|
365
|
+
*
|
366
|
+
* ==== Examples
|
367
|
+
*
|
368
|
+
* Create a new standalone document and retrieve it.
|
369
|
+
*
|
370
|
+
* sedna.load_document "<my_document>Hello world!</my_document>", "my_doc"
|
371
|
+
* #=> nil
|
372
|
+
* sedna.execute "doc('my_doc')"
|
373
|
+
* #=> ["<?xml version=\"1.0\" standalone=\"yes\"?><my_document>Hello world!</my_document>"]
|
374
|
+
*
|
375
|
+
* Open a file and import its contents into a new document in an existing collection.
|
376
|
+
*
|
377
|
+
* File.open "document.xml" do |file|
|
378
|
+
* sedna.load_document file, "my_doc", "my_col"
|
379
|
+
* end
|
380
|
+
*/
|
381
|
+
static VALUE cSedna_load_document(int argc, VALUE *argv, VALUE self)
|
382
|
+
{
|
383
|
+
int res = 0;
|
384
|
+
SC *conn = sedna_struct(self);
|
385
|
+
VALUE document, doc_name, col_name, buf;
|
386
|
+
char *doc_name_c, *col_name_c;
|
387
|
+
if(SEconnectionStatus(conn) != SEDNA_CONNECTION_OK) rb_raise(cSednaConnError, "Connection is closed.");
|
388
|
+
rb_scan_args(argc, argv, "21", &document, &doc_name, &col_name); // 2 mandatory args, 1 optional.
|
389
|
+
doc_name_c = STR2CSTR(doc_name);
|
390
|
+
col_name_c = NIL_P(col_name) ? NULL : STR2CSTR(col_name);
|
391
|
+
if(TYPE(document) == T_FILE) {
|
392
|
+
while(!NIL_P(buf = rb_funcall(document, rb_intern("read"), 1, INT2NUM(LOAD_BUF_LEN)))) {
|
393
|
+
res = SEloadData(conn, STR2CSTR(buf), RSTRING_LEN(buf), doc_name_c, col_name_c);
|
394
|
+
verify_res(SEDNA_DATA_CHUNK_LOADED, res, conn);
|
395
|
+
}
|
396
|
+
if(res == 0) rb_raise(cSednaException, "Document is empty.");
|
397
|
+
} else {
|
398
|
+
if(RSTRING_LEN(document) == 0) rb_raise(cSednaException, "Document is empty.");
|
399
|
+
res = SEloadData(conn, STR2CSTR(document), RSTRING_LEN(document), doc_name_c, col_name_c);
|
400
|
+
verify_res(SEDNA_DATA_CHUNK_LOADED, res, conn);
|
401
|
+
}
|
402
|
+
res = SEendLoadData(conn);
|
403
|
+
verify_res(SEDNA_BULK_LOAD_SUCCEEDED, res, conn);
|
404
|
+
return Qnil;
|
405
|
+
}
|
406
|
+
|
343
407
|
/* :nodoc:
|
344
408
|
*
|
345
409
|
* Turn autocommit on or off.
|
@@ -364,7 +428,7 @@ static VALUE cSedna_autocommit_get(VALUE self)
|
|
364
428
|
|
365
429
|
/*
|
366
430
|
* call-seq:
|
367
|
-
* sedna.transaction { ... } ->
|
431
|
+
* sedna.transaction { ... } -> nil
|
368
432
|
*
|
369
433
|
* Wraps the given block in a \transaction. If the block runs
|
370
434
|
* completely, the \transaction is committed. If the stack is unwinded
|
@@ -373,7 +437,7 @@ static VALUE cSedna_autocommit_get(VALUE self)
|
|
373
437
|
* invoking +throw+. Note that Exceptions will not be rescued -- they will be
|
374
438
|
* re-raised after rolling back the \transaction.
|
375
439
|
*
|
376
|
-
* This method returns +
|
440
|
+
* This method returns +nil+ if the \transaction is successfully committed
|
377
441
|
* to the database. If the given block completes successfully, but the
|
378
442
|
* \transaction fails to be committed, a Sedna::TransactionError will
|
379
443
|
* be raised.
|
@@ -381,14 +445,14 @@ static VALUE cSedna_autocommit_get(VALUE self)
|
|
381
445
|
* ==== Examples
|
382
446
|
*
|
383
447
|
* sedna.transaction do
|
384
|
-
* count = sedna.execute "count(collection('
|
385
|
-
* sedna.execute "update insert <total>#{count}</total> into doc('
|
448
|
+
* count = sedna.execute "count(collection('my_col')/items)" #=> 0
|
449
|
+
* sedna.execute "update insert <total>#{count}</total> into doc('my_doc')"
|
386
450
|
* # ...
|
387
451
|
* end
|
388
452
|
* # Transaction is committed.
|
389
453
|
*
|
390
454
|
* sedna.transaction do
|
391
|
-
* count = sedna.execute "count(collection('
|
455
|
+
* count = sedna.execute "count(collection('my_col')/items)" #=> 0
|
392
456
|
* throw :no_items if count == 0
|
393
457
|
* # ... never get here
|
394
458
|
* end
|
@@ -412,7 +476,7 @@ static VALUE cSedna_transaction(VALUE self)
|
|
412
476
|
switch_sedna_autocommit(conn, rb_iv_get(self, "@autocommit"));
|
413
477
|
rb_jump_tag(status); // Re-raise exception.
|
414
478
|
}
|
415
|
-
return
|
479
|
+
return Qnil;
|
416
480
|
}
|
417
481
|
|
418
482
|
void Init_sedna()
|
@@ -440,6 +504,7 @@ void Init_sedna()
|
|
440
504
|
rb_define_singleton_method(cSedna, "connect", cSedna_s_connect, 1);
|
441
505
|
rb_define_method(cSedna, "initialize", cSedna_initialize, 1);
|
442
506
|
rb_define_method(cSedna, "execute", cSedna_execute, 1);
|
507
|
+
rb_define_method(cSedna, "load_document", cSedna_load_document, -1);
|
443
508
|
rb_define_undocumented_alias(cSedna, "query", "execute");
|
444
509
|
rb_define_method(cSedna, "close", cSedna_close, 0);
|
445
510
|
|
@@ -20,11 +20,13 @@
|
|
20
20
|
# This file contains the test suite to verify the client library is working
|
21
21
|
# correctly.
|
22
22
|
|
23
|
+
$:.unshift(File.dirname(__FILE__) + '/../ext')
|
24
|
+
|
23
25
|
require 'test/unit'
|
24
|
-
require '
|
26
|
+
require 'sedna'
|
25
27
|
require 'socket'
|
26
28
|
|
27
|
-
class
|
29
|
+
class SednaTest < Test::Unit::TestCase
|
28
30
|
def setup
|
29
31
|
@connection = {
|
30
32
|
:database => "test",
|
@@ -36,7 +38,9 @@ class TestSedna < Test::Unit::TestCase
|
|
36
38
|
|
37
39
|
def teardown
|
38
40
|
end
|
39
|
-
|
41
|
+
|
42
|
+
# Faux test that just checks if we can connect, otherwise the test
|
43
|
+
# suite is aborted.
|
40
44
|
def test_aaa_connection
|
41
45
|
port = 5050
|
42
46
|
begin
|
@@ -59,19 +63,19 @@ class TestSedna < Test::Unit::TestCase
|
|
59
63
|
|
60
64
|
def test_connect_should_raise_exception_when_host_not_found
|
61
65
|
assert_raises Sedna::ConnectionError do
|
62
|
-
Sedna.connect @connection.merge(:host => "non-
|
66
|
+
Sedna.connect @connection.merge(:host => "non-existent-host")
|
63
67
|
end
|
64
68
|
end
|
65
69
|
|
66
70
|
def test_connect_should_raise_exception_when_credentials_are_incorrect
|
67
71
|
assert_raises Sedna::AuthenticationError do
|
68
|
-
Sedna.connect @connection.merge(:username => "non-
|
72
|
+
Sedna.connect @connection.merge(:username => "non-existent-user")
|
69
73
|
end
|
70
74
|
end
|
71
75
|
|
72
76
|
def test_connect_should_return_nil_on_error
|
73
77
|
begin
|
74
|
-
sedna = Sedna.connect @connection.merge(:username => "non-
|
78
|
+
sedna = Sedna.connect @connection.merge(:username => "non-existent-user")
|
75
79
|
rescue
|
76
80
|
end
|
77
81
|
assert_nil sedna
|
@@ -126,16 +130,6 @@ class TestSedna < Test::Unit::TestCase
|
|
126
130
|
end
|
127
131
|
end
|
128
132
|
end
|
129
|
-
|
130
|
-
# TODO: Fix the following strangely-failing test case.
|
131
|
-
#def test_zzz_connect_should_not_fail_to_close_connection_when_require_called_inside_block
|
132
|
-
# # Squash a strange bug -- only appears to work if this test is run last and nothing else fails.
|
133
|
-
# assert_nothing_raised do
|
134
|
-
# Sedna.connect @connection do |sedna|
|
135
|
-
# require 'pp'
|
136
|
-
# end
|
137
|
-
# end
|
138
|
-
#end
|
139
133
|
|
140
134
|
# Test sedna.close.
|
141
135
|
def test_close_should_return_nil
|
@@ -221,6 +215,138 @@ class TestSedna < Test::Unit::TestCase
|
|
221
215
|
assert_equal ["<test/>"], sedna.query("<test/>")
|
222
216
|
end
|
223
217
|
end
|
218
|
+
|
219
|
+
# Test sedna.load_document.
|
220
|
+
def test_load_document_should_create_document_in_given_collection
|
221
|
+
Sedna.connect @connection do |sedna|
|
222
|
+
name = "test_load_document_should_create_document_in_given_collection"
|
223
|
+
col = "test_collection"
|
224
|
+
doc = "<?xml version=\"1.0\" standalone=\"yes\"?><document>\n <node/>\n</document>"
|
225
|
+
|
226
|
+
sedna.execute "create collection '#{col}'" rescue Sedna::Exception
|
227
|
+
sedna.execute "drop document '#{name}' in collection '#{col}'" rescue Sedna::Exception
|
228
|
+
sedna.load_document doc, name, col
|
229
|
+
assert_equal doc, sedna.execute("doc('#{name}', '#{col}')").first
|
230
|
+
sedna.execute "drop document '#{name}' in collection '#{col}'" rescue Sedna::Exception
|
231
|
+
sedna.execute "drop collection '#{col}'" rescue Sedna::Exception
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
def test_load_document_should_create_standalone_document_if_collection_is_unspecified
|
236
|
+
Sedna.connect @connection do |sedna|
|
237
|
+
name = "test_load_document_should_create_standalone_document_if_collection_is_unspecified"
|
238
|
+
doc = "<?xml version=\"1.0\" standalone=\"yes\"?><document>\n <node/>\n</document>"
|
239
|
+
|
240
|
+
sedna.execute "drop document '#{name}'" rescue Sedna::Exception
|
241
|
+
sedna.load_document doc, name
|
242
|
+
assert_equal doc, sedna.execute("doc('#{name}')").first
|
243
|
+
sedna.execute "drop document '#{name}'" rescue Sedna::Exception
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
def test_load_document_should_create_standalone_document_if_collection_is_nil
|
248
|
+
Sedna.connect @connection do |sedna|
|
249
|
+
name = "test_load_document_should_create_standalone_document_if_collection_is_unspecified"
|
250
|
+
doc = "<?xml version=\"1.0\" standalone=\"yes\"?><document>\n <node/>\n</document>"
|
251
|
+
|
252
|
+
sedna.execute "drop document '#{name}'" rescue Sedna::Exception
|
253
|
+
sedna.load_document doc, name, nil
|
254
|
+
assert_equal doc, sedna.execute("doc('#{name}')").first
|
255
|
+
sedna.execute "drop document '#{name}'" rescue Sedna::Exception
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
def test_load_document_should_return_nil_if_standalone_document_loaded_successfully
|
260
|
+
Sedna.connect @connection do |sedna|
|
261
|
+
name = "test_load_document_should_return_nil_if_document_loaded_successfully"
|
262
|
+
sedna.execute "drop document '#{name}'" rescue Sedna::Exception
|
263
|
+
assert_nil sedna.load_document("<document><node/></document>", name)
|
264
|
+
sedna.execute "drop document '#{name}'" rescue Sedna::Exception
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
def test_load_document_should_fail_if_autocommit_is_false
|
269
|
+
Sedna.connect @connection do |sedna|
|
270
|
+
sedna.autocommit = false
|
271
|
+
assert_raises Sedna::Exception do
|
272
|
+
sedna.load_document "<test/>", "some_doc"
|
273
|
+
end
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
def test_load_document_should_fail_with_sedna_exception_for_invalid_documents
|
278
|
+
Sedna.connect @connection do |sedna|
|
279
|
+
assert_raises Sedna::Exception do
|
280
|
+
sedna.load_document "<doc/> this is an invalid document", "some_doc"
|
281
|
+
end
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
def test_load_document_should_raise_exception_with_complete_details_for_invalid_documents
|
286
|
+
Sedna.connect @connection do |sedna|
|
287
|
+
e = nil
|
288
|
+
begin
|
289
|
+
sedna.load_document "<doc/> junk here", "some_doc"
|
290
|
+
rescue Sedna::Exception => e
|
291
|
+
end
|
292
|
+
assert_match /junk after document element/, e.message
|
293
|
+
end
|
294
|
+
end
|
295
|
+
|
296
|
+
def test_load_document_should_fail_with_sedna_connection_error_if_connection_is_closed
|
297
|
+
Sedna.connect @connection do |sedna|
|
298
|
+
sedna.close
|
299
|
+
assert_raises Sedna::ConnectionError do
|
300
|
+
sedna.load_document "<doc/>", "some_doc"
|
301
|
+
end
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
def test_load_document_should_create_document_if_given_document_is_io_object
|
306
|
+
Sedna.connect @connection do |sedna|
|
307
|
+
name = "test_load_document_should_create_document_if_given_document_is_io_object"
|
308
|
+
doc = "<?xml version=\"1.0\" standalone=\"yes\"?><document>" << ("\n <some_very_often_repeated_node/>" * 800) << "\n</document>"
|
309
|
+
p_out, p_in = IO.pipe
|
310
|
+
p_in.write doc
|
311
|
+
p_in.close
|
312
|
+
|
313
|
+
sedna.execute "drop document '#{name}'" rescue Sedna::Exception
|
314
|
+
sedna.load_document p_out, name, nil
|
315
|
+
assert_equal doc.length, sedna.execute("doc('#{name}')").first.length
|
316
|
+
sedna.execute "drop document '#{name}'" rescue Sedna::Exception
|
317
|
+
end
|
318
|
+
end
|
319
|
+
|
320
|
+
def test_load_document_should_raise_sedna_exception_if_given_document_is_empty_io_object
|
321
|
+
Sedna.connect @connection do |sedna|
|
322
|
+
name = "test_load_document_should_raise_sedna_exception_if_given_document_is_empty_io_object"
|
323
|
+
p_out, p_in = IO.pipe
|
324
|
+
p_in.close
|
325
|
+
|
326
|
+
sedna.execute "drop document '#{name}'" rescue Sedna::Exception
|
327
|
+
e = nil
|
328
|
+
begin
|
329
|
+
sedna.load_document p_out, name, nil
|
330
|
+
rescue Sedna::Exception => e
|
331
|
+
end
|
332
|
+
assert_equal "Document is empty.", e.message
|
333
|
+
sedna.execute "drop document '#{name}'" rescue Sedna::Exception
|
334
|
+
end
|
335
|
+
end
|
336
|
+
|
337
|
+
def test_load_document_should_raise_sedna_exception_if_given_document_is_empty_string
|
338
|
+
Sedna.connect @connection do |sedna|
|
339
|
+
name = "test_load_document_should_raise_sedna_exception_if_given_document_is_empty_string"
|
340
|
+
sedna.execute "drop document '#{name}'" rescue Sedna::Exception
|
341
|
+
e = nil
|
342
|
+
begin
|
343
|
+
sedna.load_document "", name, nil
|
344
|
+
rescue Sedna::Exception => e
|
345
|
+
end
|
346
|
+
assert_equal "Document is empty.", e.message
|
347
|
+
sedna.execute "drop document '#{name}'" rescue Sedna::Exception
|
348
|
+
end
|
349
|
+
end
|
224
350
|
|
225
351
|
# Test sedna.autocommit= / sedna.autocommit.
|
226
352
|
def test_autocommit_should_return_true_by_default
|
@@ -276,9 +402,9 @@ class TestSedna < Test::Unit::TestCase
|
|
276
402
|
end
|
277
403
|
|
278
404
|
# Test sedna.transaction.
|
279
|
-
def
|
405
|
+
def test_transaction_should_return_nil_if_committed
|
280
406
|
Sedna.connect @connection do |sedna|
|
281
|
-
|
407
|
+
assert_nil sedna.transaction(){}
|
282
408
|
end
|
283
409
|
end
|
284
410
|
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sedna
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Rolf Timmermans
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2008-12-
|
12
|
+
date: 2008-12-15 00:00:00 +01:00
|
13
13
|
default_executable:
|
14
14
|
dependencies: []
|
15
15
|
|
@@ -26,7 +26,7 @@ extra_rdoc_files:
|
|
26
26
|
files:
|
27
27
|
- ext/extconf.rb
|
28
28
|
- ext/sedna.c
|
29
|
-
- test/
|
29
|
+
- test/sedna_test.rb
|
30
30
|
- CHANGES
|
31
31
|
- README
|
32
32
|
has_rdoc: true
|
@@ -59,4 +59,4 @@ signing_key:
|
|
59
59
|
specification_version: 2
|
60
60
|
summary: Sedna XML DBMS client library.
|
61
61
|
test_files:
|
62
|
-
- test/
|
62
|
+
- test/sedna_test.rb
|