duckdb 0.0.12 → 0.2.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/test_on_macos.yml +2 -8
- data/.github/workflows/test_on_ubuntu.yml +28 -28
- data/.github/workflows/test_on_windows.yml +37 -0
- data/.travis.yml +6 -6
- data/CHANGELOG.md +31 -0
- data/Gemfile.lock +3 -3
- data/README.md +81 -1
- data/ext/duckdb/appender.c +315 -0
- data/ext/duckdb/appender.h +17 -0
- data/ext/duckdb/config.c +80 -0
- data/ext/duckdb/config.h +18 -0
- data/ext/duckdb/database.c +48 -2
- data/ext/duckdb/duckdb.c +12 -0
- data/ext/duckdb/extconf.rb +5 -0
- data/ext/duckdb/prepared_statement.c +17 -2
- data/ext/duckdb/result.c +23 -3
- data/ext/duckdb/ruby-duckdb.h +18 -0
- data/lib/duckdb/appender.rb +102 -0
- data/lib/duckdb/config.rb +65 -0
- data/lib/duckdb/connection.rb +29 -0
- data/lib/duckdb/database.rb +15 -2
- data/lib/duckdb/prepared_statement.rb +20 -2
- data/lib/duckdb/version.rb +1 -1
- data/lib/duckdb.rb +2 -0
- metadata +13 -6
@@ -0,0 +1,17 @@
|
|
1
|
+
#ifndef RUBY_DUCKDB_APPENDER_H
|
2
|
+
#define RUBY_DUCKDB_APPENDER_H
|
3
|
+
|
4
|
+
#ifdef HAVE_DUCKDB_APPENDER_CREATE
|
5
|
+
|
6
|
+
struct _rubyDuckDBAppender {
|
7
|
+
duckdb_appender appender;
|
8
|
+
};
|
9
|
+
|
10
|
+
typedef struct _rubyDuckDBAppender rubyDuckDBAppender;
|
11
|
+
|
12
|
+
void init_duckdb_appender(void);
|
13
|
+
|
14
|
+
#endif
|
15
|
+
|
16
|
+
#endif
|
17
|
+
|
data/ext/duckdb/config.c
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
#include "ruby-duckdb.h"
|
2
|
+
|
3
|
+
#ifdef HAVE_DUCKDB_CREATE_CONFIG
|
4
|
+
|
5
|
+
VALUE cDuckDBConfig;
|
6
|
+
|
7
|
+
static void deallocate(void *);
|
8
|
+
static VALUE allocate(VALUE klass);
|
9
|
+
static VALUE config_s_size(VALUE klass);
|
10
|
+
static VALUE config_s_get_config_flag(VALUE self, VALUE value);
|
11
|
+
static VALUE config_initialize(VALUE self);
|
12
|
+
static VALUE config_set_config(VALUE self, VALUE key, VALUE value);
|
13
|
+
|
14
|
+
static void deallocate(void * ctx)
|
15
|
+
{
|
16
|
+
rubyDuckDBConfig *p = (rubyDuckDBConfig *)ctx;
|
17
|
+
|
18
|
+
duckdb_destroy_config(&(p->config));
|
19
|
+
xfree(p);
|
20
|
+
}
|
21
|
+
|
22
|
+
static VALUE allocate(VALUE klass)
|
23
|
+
{
|
24
|
+
rubyDuckDBConfig *ctx = xcalloc((size_t)1, sizeof(rubyDuckDBConfig));
|
25
|
+
return Data_Wrap_Struct(klass, NULL, deallocate, ctx);
|
26
|
+
}
|
27
|
+
|
28
|
+
static VALUE config_initialize(VALUE self) {
|
29
|
+
rubyDuckDBConfig *ctx;
|
30
|
+
|
31
|
+
Data_Get_Struct(self, rubyDuckDBConfig, ctx);
|
32
|
+
|
33
|
+
if (duckdb_create_config(&(ctx->config)) == DuckDBError) {
|
34
|
+
rb_raise(eDuckDBError, "failed to create config");
|
35
|
+
}
|
36
|
+
return self;
|
37
|
+
}
|
38
|
+
|
39
|
+
static VALUE config_s_size(VALUE self) {
|
40
|
+
return INT2NUM(duckdb_config_count());
|
41
|
+
}
|
42
|
+
|
43
|
+
static VALUE config_s_get_config_flag(VALUE klass, VALUE value) {
|
44
|
+
const char *pkey;
|
45
|
+
const char *pdesc;
|
46
|
+
|
47
|
+
size_t i = NUM2INT(value);
|
48
|
+
|
49
|
+
if (duckdb_get_config_flag(i, &pkey, &pdesc) == DuckDBError) {
|
50
|
+
rb_raise(eDuckDBError, "failed to get config information of index %ld", i);
|
51
|
+
}
|
52
|
+
|
53
|
+
return rb_ary_new3(2, rb_str_new2(pkey), rb_str_new2(pdesc));
|
54
|
+
}
|
55
|
+
|
56
|
+
static VALUE config_set_config(VALUE self, VALUE key, VALUE value) {
|
57
|
+
const char *pkey = StringValuePtr(key);
|
58
|
+
const char *pval = StringValuePtr(value);
|
59
|
+
|
60
|
+
rubyDuckDBConfig *ctx;
|
61
|
+
Data_Get_Struct(self, rubyDuckDBConfig, ctx);
|
62
|
+
|
63
|
+
if (duckdb_set_config(ctx->config, pkey, pval) == DuckDBError) {
|
64
|
+
rb_raise(eDuckDBError, "failed to set config %s => %s", pkey, pval);
|
65
|
+
}
|
66
|
+
return self;
|
67
|
+
}
|
68
|
+
|
69
|
+
void init_duckdb_config(void) {
|
70
|
+
cDuckDBConfig = rb_define_class_under(mDuckDB, "Config", rb_cObject);
|
71
|
+
rb_define_alloc_func(cDuckDBConfig, allocate);
|
72
|
+
rb_define_singleton_method(cDuckDBConfig, "size", config_s_size, 0);
|
73
|
+
rb_define_singleton_method(cDuckDBConfig, "get_config_flag", config_s_get_config_flag, 1);
|
74
|
+
|
75
|
+
rb_define_method(cDuckDBConfig, "initialize", config_initialize, 0);
|
76
|
+
rb_define_method(cDuckDBConfig, "set_config", config_set_config, 2);
|
77
|
+
}
|
78
|
+
|
79
|
+
#endif
|
80
|
+
|
data/ext/duckdb/config.h
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
#ifndef RUBY_DUCKDB_CONFIG_H
|
2
|
+
#define RUBY_DUCKDB_CONFIG_H
|
3
|
+
|
4
|
+
#ifdef HAVE_DUCKDB_CREATE_CONFIG
|
5
|
+
|
6
|
+
struct _rubyDuckDBConfig {
|
7
|
+
duckdb_config config;
|
8
|
+
};
|
9
|
+
|
10
|
+
typedef struct _rubyDuckDBConfig rubyDuckDBConfig;
|
11
|
+
|
12
|
+
void init_duckdb_config(void);
|
13
|
+
|
14
|
+
#endif
|
15
|
+
|
16
|
+
#endif
|
17
|
+
|
18
|
+
|
data/ext/duckdb/database.c
CHANGED
@@ -2,6 +2,12 @@
|
|
2
2
|
|
3
3
|
VALUE cDuckDBDatabase;
|
4
4
|
|
5
|
+
static void close_database(rubyDuckDB *p);
|
6
|
+
static void deallocate(void * ctx);
|
7
|
+
static VALUE allocate(VALUE klass);
|
8
|
+
static VALUE duckdb_database_s_open(int argc, VALUE *argv, VALUE cDuckDBDatabase);
|
9
|
+
static VALUE duckdb_database_s_open_ext(int argc, VALUE *argv, VALUE cDuckDBDatabase);
|
10
|
+
|
5
11
|
static void close_database(rubyDuckDB *p)
|
6
12
|
{
|
7
13
|
duckdb_close(&(p->db));
|
@@ -25,6 +31,7 @@ static VALUE duckdb_database_s_open(int argc, VALUE *argv, VALUE cDuckDBDatabase
|
|
25
31
|
{
|
26
32
|
rubyDuckDB *ctx;
|
27
33
|
VALUE obj;
|
34
|
+
|
28
35
|
char *pfile = NULL;
|
29
36
|
VALUE file = Qnil;
|
30
37
|
|
@@ -36,13 +43,49 @@ static VALUE duckdb_database_s_open(int argc, VALUE *argv, VALUE cDuckDBDatabase
|
|
36
43
|
|
37
44
|
obj = allocate(cDuckDBDatabase);
|
38
45
|
Data_Get_Struct(obj, rubyDuckDB, ctx);
|
39
|
-
if (duckdb_open(pfile, &(ctx->db)) == DuckDBError)
|
40
|
-
{
|
46
|
+
if (duckdb_open(pfile, &(ctx->db)) == DuckDBError) {
|
41
47
|
rb_raise(eDuckDBError, "Failed to open database"); /* FIXME */
|
42
48
|
}
|
43
49
|
return obj;
|
44
50
|
}
|
45
51
|
|
52
|
+
#ifdef HAVE_DUCKDB_OPEN_EXT
|
53
|
+
static VALUE duckdb_database_s_open_ext(int argc, VALUE *argv, VALUE cDuckDBDatabase)
|
54
|
+
{
|
55
|
+
rubyDuckDB *ctx;
|
56
|
+
VALUE obj;
|
57
|
+
rubyDuckDBConfig *ctx_config;
|
58
|
+
char *perror;
|
59
|
+
|
60
|
+
char *pfile = NULL;
|
61
|
+
VALUE file = Qnil;
|
62
|
+
VALUE config = Qnil;
|
63
|
+
|
64
|
+
rb_scan_args(argc, argv, "02", &file, &config);
|
65
|
+
|
66
|
+
if (!NIL_P(file)) {
|
67
|
+
pfile = StringValuePtr(file);
|
68
|
+
}
|
69
|
+
|
70
|
+
obj = allocate(cDuckDBDatabase);
|
71
|
+
Data_Get_Struct(obj, rubyDuckDB, ctx);
|
72
|
+
if (!NIL_P(config)) {
|
73
|
+
if (!rb_obj_is_kind_of(config, cDuckDBConfig)) {
|
74
|
+
rb_raise(rb_eTypeError, "The second argument must be DuckDB::Config object.");
|
75
|
+
}
|
76
|
+
Data_Get_Struct(config, rubyDuckDBConfig, ctx_config);
|
77
|
+
if (duckdb_open_ext(pfile, &(ctx->db), ctx_config->config, &perror) == DuckDBError) {
|
78
|
+
rb_raise(eDuckDBError, "Failed to open database %s", perror);
|
79
|
+
}
|
80
|
+
} else {
|
81
|
+
if (duckdb_open(pfile, &(ctx->db)) == DuckDBError) {
|
82
|
+
rb_raise(eDuckDBError, "Failed to open database"); /* FIXME */
|
83
|
+
}
|
84
|
+
}
|
85
|
+
return obj;
|
86
|
+
}
|
87
|
+
#endif /* HAVE_DUCKDB_OPEN_EXT */
|
88
|
+
|
46
89
|
static VALUE duckdb_database_connect(VALUE self)
|
47
90
|
{
|
48
91
|
return create_connection(self);
|
@@ -67,6 +110,9 @@ void init_duckdb_database(void)
|
|
67
110
|
cDuckDBDatabase = rb_define_class_under(mDuckDB, "Database", rb_cObject);
|
68
111
|
rb_define_alloc_func(cDuckDBDatabase, allocate);
|
69
112
|
rb_define_singleton_method(cDuckDBDatabase, "_open", duckdb_database_s_open, -1);
|
113
|
+
#ifdef HAVE_DUCKDB_OPEN_EXT
|
114
|
+
rb_define_singleton_method(cDuckDBDatabase, "_open_ext", duckdb_database_s_open_ext, -1);
|
115
|
+
#endif
|
70
116
|
rb_define_private_method(cDuckDBDatabase, "_connect", duckdb_database_connect, 0);
|
71
117
|
rb_define_method(cDuckDBDatabase, "close", duckdb_database_close, 0);
|
72
118
|
}
|
data/ext/duckdb/duckdb.c
CHANGED
@@ -18,4 +18,16 @@ Init_duckdb_native(void)
|
|
18
18
|
init_duckdb_blob();
|
19
19
|
|
20
20
|
#endif /* HAVE_DUCKDB_VALUE_BLOB */
|
21
|
+
|
22
|
+
#ifdef HAVE_DUCKDB_APPENDER_CREATE
|
23
|
+
|
24
|
+
init_duckdb_appender();
|
25
|
+
|
26
|
+
#endif /* HAVE_DUCKDB_APPENDER_CREATE */
|
27
|
+
|
28
|
+
#ifdef HAVE_DUCKDB_CREATE_CONFIG
|
29
|
+
|
30
|
+
init_duckdb_config();
|
31
|
+
|
32
|
+
#endif /* HAVE_DUCKDB_CREATE_CONFIG */
|
21
33
|
}
|
data/ext/duckdb/extconf.rb
CHANGED
@@ -3,5 +3,10 @@ require 'mkmf'
|
|
3
3
|
dir_config('duckdb')
|
4
4
|
if have_library('duckdb')
|
5
5
|
have_func('duckdb_value_blob', 'duckdb.h')
|
6
|
+
have_func('duckdb_bind_blob', 'duckdb.h')
|
7
|
+
have_func('duckdb_appender_create', 'duckdb.h')
|
8
|
+
have_func('duckdb_free', 'duckdb.h')
|
9
|
+
have_func('duckdb_create_config', 'duckdb.h')
|
10
|
+
have_func('duckdb_open_ext', 'duckdb.h')
|
6
11
|
create_makefile('duckdb/duckdb_native')
|
7
12
|
end
|
@@ -70,7 +70,7 @@ static idx_t check_index(VALUE vidx)
|
|
70
70
|
return idx;
|
71
71
|
}
|
72
72
|
|
73
|
-
static VALUE
|
73
|
+
static VALUE duckdb_prepared_statement_bind_bool(VALUE self, VALUE vidx, VALUE val)
|
74
74
|
{
|
75
75
|
rubyDuckDBPreparedStatement *ctx;
|
76
76
|
idx_t idx = check_index(vidx);
|
@@ -86,6 +86,20 @@ static VALUE duckdb_prepared_statement_bind_boolean(VALUE self, VALUE vidx, VALU
|
|
86
86
|
return self;
|
87
87
|
}
|
88
88
|
|
89
|
+
static VALUE duckdb_prepared_statement_bind_int8(VALUE self, VALUE vidx, VALUE val)
|
90
|
+
{
|
91
|
+
rubyDuckDBPreparedStatement *ctx;
|
92
|
+
idx_t idx = check_index(vidx);
|
93
|
+
int8_t i8val = (int8_t)NUM2INT(val);
|
94
|
+
|
95
|
+
Data_Get_Struct(self, rubyDuckDBPreparedStatement, ctx);
|
96
|
+
|
97
|
+
if (duckdb_bind_int8(ctx->prepared_statement, idx, i8val) == DuckDBError) {
|
98
|
+
rb_raise(eDuckDBError, "fail to bind %llu parameter", (unsigned long long)idx);
|
99
|
+
}
|
100
|
+
return self;
|
101
|
+
}
|
102
|
+
|
89
103
|
static VALUE duckdb_prepared_statement_bind_int16(VALUE self, VALUE vidx, VALUE val)
|
90
104
|
{
|
91
105
|
rubyDuckDBPreparedStatement *ctx;
|
@@ -203,7 +217,8 @@ void init_duckdb_prepared_statement(void)
|
|
203
217
|
rb_define_method(cDuckDBPreparedStatement, "initialize", duckdb_prepared_statement_initialize, 2);
|
204
218
|
rb_define_method(cDuckDBPreparedStatement, "execute", duckdb_prepared_statement_execute, 0);
|
205
219
|
rb_define_method(cDuckDBPreparedStatement, "nparams", duckdb_prepared_statement_nparams, 0);
|
206
|
-
rb_define_method(cDuckDBPreparedStatement, "
|
220
|
+
rb_define_method(cDuckDBPreparedStatement, "bind_bool", duckdb_prepared_statement_bind_bool, 2);
|
221
|
+
rb_define_method(cDuckDBPreparedStatement, "bind_int8", duckdb_prepared_statement_bind_int8, 2);
|
207
222
|
rb_define_method(cDuckDBPreparedStatement, "bind_int16", duckdb_prepared_statement_bind_int16, 2);
|
208
223
|
rb_define_method(cDuckDBPreparedStatement, "bind_int32", duckdb_prepared_statement_bind_int32, 2);
|
209
224
|
rb_define_method(cDuckDBPreparedStatement, "bind_int64", duckdb_prepared_statement_bind_int64, 2);
|
data/ext/duckdb/result.c
CHANGED
@@ -55,8 +55,19 @@ static VALUE to_ruby_obj_double(duckdb_result *result, idx_t col_idx, idx_t row_
|
|
55
55
|
#ifdef HAVE_DUCKDB_VALUE_BLOB
|
56
56
|
static VALUE to_ruby_obj_string_from_blob(duckdb_result *result, idx_t col_idx, idx_t row_idx)
|
57
57
|
{
|
58
|
+
VALUE str;
|
58
59
|
duckdb_blob bval = duckdb_value_blob(result, col_idx, row_idx);
|
59
|
-
|
60
|
+
str = rb_str_new(bval.data, bval.size);
|
61
|
+
|
62
|
+
if (bval.data) {
|
63
|
+
#ifdef HAVE_DUCKDB_FREE
|
64
|
+
duckdb_free(bval.data);
|
65
|
+
#else
|
66
|
+
free(bval.data);
|
67
|
+
#endif
|
68
|
+
}
|
69
|
+
|
70
|
+
return str;
|
60
71
|
}
|
61
72
|
#endif /* HAVE_DUCKDB_VALUE_BLOB */
|
62
73
|
|
@@ -86,8 +97,17 @@ static VALUE to_ruby_obj(duckdb_result *result, idx_t col_idx, idx_t row_idx)
|
|
86
97
|
#endif /* HAVE_DUCKDB_VALUE_BLOB */
|
87
98
|
default:
|
88
99
|
p = duckdb_value_varchar(result, col_idx, row_idx);
|
89
|
-
|
90
|
-
|
100
|
+
if (p) {
|
101
|
+
obj = rb_str_new2(p);
|
102
|
+
#ifdef HAVE_DUCKDB_FREE
|
103
|
+
duckdb_free(p);
|
104
|
+
#else
|
105
|
+
free(p);
|
106
|
+
#endif /* HAVE_DUCKDB_FREE */
|
107
|
+
if (result->columns[col_idx].type == DUCKDB_TYPE_HUGEINT) {
|
108
|
+
obj = rb_funcall(obj, rb_intern("to_i"), 0);
|
109
|
+
}
|
110
|
+
}
|
91
111
|
}
|
92
112
|
return obj;
|
93
113
|
}
|
data/ext/duckdb/ruby-duckdb.h
CHANGED
@@ -15,6 +15,18 @@
|
|
15
15
|
|
16
16
|
#endif /* HAVE_DUCKDB_VALUE_BLOB */
|
17
17
|
|
18
|
+
#ifdef HAVE_DUCKDB_APPENDER_CREATE
|
19
|
+
|
20
|
+
#include "./appender.h"
|
21
|
+
|
22
|
+
#endif /* HAVE_DUCKDB_APPENDER_CREATE */
|
23
|
+
|
24
|
+
#ifdef HAVE_DUCKDB_CREATE_CONFIG
|
25
|
+
|
26
|
+
#include "./config.h"
|
27
|
+
|
28
|
+
#endif /* HAVE_DUCKDB_CREATE_CONFIG */
|
29
|
+
|
18
30
|
extern VALUE mDuckDB;
|
19
31
|
extern VALUE cDuckDBDatabase;
|
20
32
|
extern VALUE cDuckDBConnection;
|
@@ -25,6 +37,12 @@ extern VALUE cDuckDBBlob;
|
|
25
37
|
|
26
38
|
#endif /* HAVE_DUCKDB_VALUE_BLOB */
|
27
39
|
|
40
|
+
#ifdef HAVE_DUCKDB_CREATE_CONFIG
|
41
|
+
|
42
|
+
extern VALUE cDuckDBConfig;
|
43
|
+
|
44
|
+
#endif /* HAVE_DUCKDB_CREATE_CONFIG */
|
45
|
+
|
28
46
|
extern VALUE eDuckDBError;
|
29
47
|
|
30
48
|
#endif
|
@@ -0,0 +1,102 @@
|
|
1
|
+
require 'date'
|
2
|
+
|
3
|
+
module DuckDB
|
4
|
+
if defined?(DuckDB::Appender)
|
5
|
+
# The DuckDB::Appender encapsulates DuckDB Appender.
|
6
|
+
#
|
7
|
+
# require 'duckdb'
|
8
|
+
# db = DuckDB::Database.open
|
9
|
+
# con = db.connect
|
10
|
+
# con.query('CREATE TABLE users (id INTEGER, name VARCHAR)')
|
11
|
+
# appender = con.appender('users')
|
12
|
+
# appender.append_row(1, 'Alice')
|
13
|
+
#
|
14
|
+
class Appender
|
15
|
+
RANGE_INT16 = -32_768..32_767
|
16
|
+
RANGE_INT32 = -2_147_483_648..2_147_483_647
|
17
|
+
RANGE_INT64 = -9_223_372_036_854_775_808..9_223_372_036_854_775_807
|
18
|
+
|
19
|
+
def append_hugeint(value)
|
20
|
+
case value
|
21
|
+
when Integer
|
22
|
+
append_varchar(value.to_s)
|
23
|
+
else
|
24
|
+
rb_raise(ArgumentError, "2nd argument `#{value}` must be Integer.")
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
#
|
29
|
+
# appends value.
|
30
|
+
#
|
31
|
+
# require 'duckdb'
|
32
|
+
# db = DuckDB::Database.open
|
33
|
+
# con = db.connect
|
34
|
+
# con.query('CREATE TABLE users (id INTEGER, name VARCHAR)')
|
35
|
+
# appender = con.appender('users')
|
36
|
+
# appender.begin_row
|
37
|
+
# appender.append(1)
|
38
|
+
# appender.append('Alice')
|
39
|
+
# appender.end_row
|
40
|
+
#
|
41
|
+
def append(value)
|
42
|
+
case value
|
43
|
+
when NilClass
|
44
|
+
append_null
|
45
|
+
when Float
|
46
|
+
append_double(value)
|
47
|
+
when Integer
|
48
|
+
case value
|
49
|
+
when RANGE_INT16
|
50
|
+
append_int16(value)
|
51
|
+
when RANGE_INT32
|
52
|
+
append_int32(value)
|
53
|
+
when RANGE_INT64
|
54
|
+
append_int64(value)
|
55
|
+
else
|
56
|
+
append_hugeint(value)
|
57
|
+
end
|
58
|
+
when String
|
59
|
+
if defined?(DuckDB::Blob)
|
60
|
+
blob?(value) ? append_blob(value) : append_varchar(value)
|
61
|
+
else
|
62
|
+
append_varchar(value)
|
63
|
+
end
|
64
|
+
when TrueClass, FalseClass
|
65
|
+
append_bool(value)
|
66
|
+
when Time
|
67
|
+
append_varchar(value.strftime('%Y-%m-%d %H:%M:%S.%N'))
|
68
|
+
when Date
|
69
|
+
append_varchar(value.strftime('%Y-%m-%d'))
|
70
|
+
else
|
71
|
+
rb_raise(DuckDB::Error, "not supported type #{value} (value.class)")
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
#
|
76
|
+
# append a row.
|
77
|
+
#
|
78
|
+
# appender.append_row(1, 'Alice')
|
79
|
+
#
|
80
|
+
# is same as:
|
81
|
+
#
|
82
|
+
# appender.begin_row
|
83
|
+
# appender.append(1)
|
84
|
+
# appender.append('Alice')
|
85
|
+
# appender.end_row
|
86
|
+
#
|
87
|
+
def append_row(*args)
|
88
|
+
begin_row
|
89
|
+
args.each do |arg|
|
90
|
+
append(arg)
|
91
|
+
end
|
92
|
+
end_row
|
93
|
+
end
|
94
|
+
|
95
|
+
private
|
96
|
+
|
97
|
+
def blob?(value)
|
98
|
+
value.instance_of?(DuckDB::Blob) || value.encoding == Encoding::BINARY
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|