ydbd-pg 0.5.1
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.
- checksums.yaml +7 -0
- data/ChangeLog +3703 -0
- data/LICENSE +25 -0
- data/lib/dbd/Pg.rb +188 -0
- data/lib/dbd/pg/database.rb +516 -0
- data/lib/dbd/pg/exec.rb +47 -0
- data/lib/dbd/pg/statement.rb +160 -0
- data/lib/dbd/pg/tuples.rb +121 -0
- data/lib/dbd/pg/type.rb +209 -0
- data/readme.md +274 -0
- data/test/DBD_TESTS +50 -0
- data/test/dbd/general/test_database.rb +206 -0
- data/test/dbd/general/test_statement.rb +326 -0
- data/test/dbd/general/test_types.rb +296 -0
- data/test/dbd/postgresql/base.rb +31 -0
- data/test/dbd/postgresql/down.sql +31 -0
- data/test/dbd/postgresql/test_arrays.rb +179 -0
- data/test/dbd/postgresql/test_async.rb +121 -0
- data/test/dbd/postgresql/test_blob.rb +36 -0
- data/test/dbd/postgresql/test_bytea.rb +87 -0
- data/test/dbd/postgresql/test_ping.rb +10 -0
- data/test/dbd/postgresql/test_timestamp.rb +77 -0
- data/test/dbd/postgresql/test_transactions.rb +58 -0
- data/test/dbd/postgresql/testdbipg.rb +307 -0
- data/test/dbd/postgresql/up.sql +60 -0
- data/test/ts_dbd.rb +131 -0
- metadata +100 -0
data/lib/dbd/pg/exec.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
module DBI::DBD::Pg
|
2
|
+
################################################################
|
3
|
+
# Convenience adaptor to hide details command execution API calls.
|
4
|
+
# See PgExecutorAsync subclass
|
5
|
+
class PgExecutor
|
6
|
+
def initialize(pg_conn)
|
7
|
+
@pg_conn = pg_conn
|
8
|
+
end
|
9
|
+
|
10
|
+
def exec(sql, parameters = nil)
|
11
|
+
@pg_conn.exec(sql, parameters)
|
12
|
+
end
|
13
|
+
|
14
|
+
def exec_prepared(stmt_name, parameters = nil)
|
15
|
+
@pg_conn.exec_prepared(stmt_name, parameters)
|
16
|
+
end
|
17
|
+
|
18
|
+
def prepare(stmt_name, sql)
|
19
|
+
@pg_conn.prepare(stmt_name, sql)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Asynchronous implementation of PgExecutor, useful for 'green
|
24
|
+
# thread' implementations (e.g., MRI <= 1.8.x) which would otherwise
|
25
|
+
# suspend other threads while awaiting query results.
|
26
|
+
#--
|
27
|
+
# FIXME: PQsetnonblocking + select/poll would make the exec*
|
28
|
+
# methods truly 'async', though this is rarely needed in
|
29
|
+
# practice.
|
30
|
+
class PgExecutorAsync < PgExecutor
|
31
|
+
def exec(sql, parameters = nil)
|
32
|
+
@pg_conn.async_exec(sql, parameters)
|
33
|
+
end
|
34
|
+
|
35
|
+
def exec_prepared(stmt_name, parameters = nil)
|
36
|
+
@pg_conn.send_query_prepared(stmt_name, parameters)
|
37
|
+
@pg_conn.block()
|
38
|
+
@pg_conn.get_last_result()
|
39
|
+
end
|
40
|
+
|
41
|
+
def prepare(stmt_name, sql)
|
42
|
+
@pg_conn.send_prepare(stmt_name, sql)
|
43
|
+
@pg_conn.block()
|
44
|
+
@pg_conn.get_last_result()
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,160 @@
|
|
1
|
+
#
|
2
|
+
# See DBI::BaseStatement, and DBI::DBD::Pg::Tuples.
|
3
|
+
#
|
4
|
+
#--
|
5
|
+
# Peculiar Statement responsibilities:
|
6
|
+
# - Translate dbi params (?, ?, ...) to Pg params ($1, $2, ...)
|
7
|
+
# - Translate DBI::Binary objects to Pg large objects (lo_*)
|
8
|
+
|
9
|
+
class DBI::DBD::Pg::Statement < DBI::BaseStatement
|
10
|
+
|
11
|
+
PG_STMT_NAME_PREFIX = 'ruby-dbi:Pg:'
|
12
|
+
|
13
|
+
def initialize(db, sql)
|
14
|
+
super(db)
|
15
|
+
@db = db
|
16
|
+
@sql = sql
|
17
|
+
@stmt_name = PG_STMT_NAME_PREFIX + self.object_id.to_s + Time.now.to_f.to_s
|
18
|
+
@result = nil
|
19
|
+
@bindvars = []
|
20
|
+
@prepared = false
|
21
|
+
rescue PGError => err
|
22
|
+
raise DBI::ProgrammingError.new(err.message)
|
23
|
+
end
|
24
|
+
|
25
|
+
def bind_param(index, value, options)
|
26
|
+
@bindvars[index-1] = value
|
27
|
+
end
|
28
|
+
|
29
|
+
#
|
30
|
+
# See DBI::BaseDatabase#execute.
|
31
|
+
#
|
32
|
+
# This method will make use of PostgreSQL's native BLOB support if
|
33
|
+
# DBI::Binary objects are passed in.
|
34
|
+
#
|
35
|
+
def execute
|
36
|
+
# replace DBI::Binary object by oid returned by lo_import
|
37
|
+
@bindvars.collect! do |var|
|
38
|
+
if var.is_a? DBI::Binary then
|
39
|
+
oid = @db.__blob_create(PGconn::INV_WRITE)
|
40
|
+
@db.__blob_write(oid, var.to_s)
|
41
|
+
oid
|
42
|
+
else
|
43
|
+
var
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
internal_prepare
|
48
|
+
|
49
|
+
if not @db['AutoCommit'] then
|
50
|
+
# if not SQL.query?(boundsql) and not @db['AutoCommit'] then
|
51
|
+
@db.start_transaction unless @db.in_transaction?
|
52
|
+
end
|
53
|
+
|
54
|
+
if @db["pg_native_binding"]
|
55
|
+
pg_result = @db._exec_prepared(@stmt_name, *@bindvars)
|
56
|
+
else
|
57
|
+
pg_result = @db._exec_prepared(@stmt_name)
|
58
|
+
end
|
59
|
+
|
60
|
+
@result = DBI::DBD::Pg::Tuples.new(@db, pg_result)
|
61
|
+
rescue PGError, RuntimeError => err
|
62
|
+
raise DBI::ProgrammingError.new(err.message)
|
63
|
+
end
|
64
|
+
|
65
|
+
def fetch
|
66
|
+
@result.fetchrow
|
67
|
+
end
|
68
|
+
|
69
|
+
def fetch_scroll(direction, offset)
|
70
|
+
@result.fetch_scroll(direction, offset)
|
71
|
+
end
|
72
|
+
|
73
|
+
def finish
|
74
|
+
internal_finish
|
75
|
+
@result = nil
|
76
|
+
@db = nil
|
77
|
+
end
|
78
|
+
|
79
|
+
#
|
80
|
+
# See DBI::DBD::Pg::Tuples#column_info.
|
81
|
+
#
|
82
|
+
def column_info
|
83
|
+
@result.column_info
|
84
|
+
end
|
85
|
+
|
86
|
+
def rows
|
87
|
+
if @result
|
88
|
+
@result.rows_affected
|
89
|
+
else
|
90
|
+
nil
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
#
|
95
|
+
# Attributes:
|
96
|
+
#
|
97
|
+
# If +pg_row_count+ is requested and the statement has already executed,
|
98
|
+
# postgres will return what it believes is the row count.
|
99
|
+
#
|
100
|
+
def [](attr)
|
101
|
+
case attr
|
102
|
+
when 'pg_row_count'
|
103
|
+
if @result
|
104
|
+
@result.row_count
|
105
|
+
else
|
106
|
+
nil
|
107
|
+
end
|
108
|
+
else
|
109
|
+
@attr[attr]
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
private
|
114
|
+
|
115
|
+
#
|
116
|
+
# A native binding helper.
|
117
|
+
#
|
118
|
+
class DummyQuoter
|
119
|
+
# dummy to substitute ?-style parameter markers by :1 :2 etc.
|
120
|
+
def quote(str)
|
121
|
+
str
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
# finish the statement at a lower level
|
126
|
+
def internal_finish
|
127
|
+
@result.finish if @result
|
128
|
+
@db._exec("DEALLOCATE \"#{@stmt_name}\"") if @prepared rescue nil
|
129
|
+
end
|
130
|
+
|
131
|
+
# prepare the statement at a lower level.
|
132
|
+
def internal_prepare
|
133
|
+
if @db["pg_native_binding"]
|
134
|
+
unless @prepared
|
135
|
+
@stmt = @db._prepare(@stmt_name, translate_param_markers(@sql))
|
136
|
+
end
|
137
|
+
else
|
138
|
+
internal_finish
|
139
|
+
@stmt = @db._prepare(@stmt_name, DBI::SQL::PreparedStatement.new(DBI::DBD::Pg, @sql).bind(@bindvars))
|
140
|
+
end
|
141
|
+
@prepared = true
|
142
|
+
end
|
143
|
+
|
144
|
+
# Prepare the given SQL statement, returning its PostgreSQL string
|
145
|
+
# handle. ?-style parameters are translated to $1, $2, etc.
|
146
|
+
#--
|
147
|
+
# TESTME do ?::TYPE qualifers work?
|
148
|
+
# FIXME: DBI ought to supply a generic param converter, e.g.:
|
149
|
+
# sql = DBI::Utils::convert_placeholders(sql) do |i|
|
150
|
+
# '$' + i.to_s
|
151
|
+
# end
|
152
|
+
def translate_param_markers(sql)
|
153
|
+
translator = DBI::SQL::PreparedStatement.new(DummyQuoter.new, sql)
|
154
|
+
if translator.unbound.size > 0
|
155
|
+
arr = (1..(translator.unbound.size)).collect{|i| "$#{i}"}
|
156
|
+
sql = translator.bind( arr )
|
157
|
+
end
|
158
|
+
sql
|
159
|
+
end
|
160
|
+
end # Statement
|
@@ -0,0 +1,121 @@
|
|
1
|
+
#
|
2
|
+
# Tuples is a class to represent result sets.
|
3
|
+
#
|
4
|
+
# Many of these methods are extremely similar to the methods that deal with
|
5
|
+
# result sets in DBI::BaseStatement and are wrapped by the StatementHandle.
|
6
|
+
# Unless you plan on working on this driver, these methods should never be
|
7
|
+
# called directly.
|
8
|
+
#
|
9
|
+
class DBI::DBD::Pg::Tuples
|
10
|
+
|
11
|
+
def initialize(db, pg_result)
|
12
|
+
@db = db
|
13
|
+
@pg_result = pg_result
|
14
|
+
@index = -1
|
15
|
+
@row = []
|
16
|
+
end
|
17
|
+
|
18
|
+
# See DBI::BaseStatement#column_info. Additional attributes:
|
19
|
+
#
|
20
|
+
# * array_of_type: True if this is actually an array of this type. In this
|
21
|
+
# case, +dbi_type+ will be the type authority for conversion.
|
22
|
+
#
|
23
|
+
def column_info
|
24
|
+
a = []
|
25
|
+
0.upto(@pg_result.num_fields-1) do |i|
|
26
|
+
str = @pg_result.fname(i)
|
27
|
+
|
28
|
+
typeinfo = nil
|
29
|
+
|
30
|
+
begin
|
31
|
+
typmod = @pg_result.fmod(i)
|
32
|
+
rescue
|
33
|
+
end
|
34
|
+
|
35
|
+
if typmod and typ = @pg_result.ftype(i)
|
36
|
+
res = @db._exec("select format_type(#{typ}, #{typmod})")
|
37
|
+
typeinfo = DBI::DBD::Pg.parse_type(res[0].values[0])
|
38
|
+
end
|
39
|
+
|
40
|
+
map = @db.type_map[@pg_result.ftype(i)] || { }
|
41
|
+
h = { "name" => str }.merge(map)
|
42
|
+
|
43
|
+
if typeinfo
|
44
|
+
h["precision"] = typeinfo[:size]
|
45
|
+
h["scale"] = typeinfo[:decimal]
|
46
|
+
h["type"] = typeinfo[:type]
|
47
|
+
h["array_of_type"] = typeinfo[:array]
|
48
|
+
|
49
|
+
if typeinfo[:array]
|
50
|
+
h['dbi_type'] =
|
51
|
+
DBI::DBD::Pg::Type::Array.new(
|
52
|
+
DBI::TypeUtil.type_name_to_module(typeinfo[:type])
|
53
|
+
)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
a.push h
|
58
|
+
end
|
59
|
+
|
60
|
+
return a
|
61
|
+
end
|
62
|
+
|
63
|
+
def fetchrow
|
64
|
+
@index += 1
|
65
|
+
if @index < @pg_result.num_tuples && @index >= 0
|
66
|
+
@row = Array.new
|
67
|
+
0.upto(@pg_result.num_fields-1) do |x|
|
68
|
+
@row.push(@pg_result.getvalue(@index, x))
|
69
|
+
end
|
70
|
+
@row
|
71
|
+
else
|
72
|
+
nil
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
#
|
77
|
+
# Just don't use this method. It'll be fixed soon.
|
78
|
+
#
|
79
|
+
def fetch_scroll(direction, offset)
|
80
|
+
# Exact semantics aren't too closely defined. I attempted to follow the DBI:Mysql example.
|
81
|
+
case direction
|
82
|
+
when SQL_FETCH_NEXT
|
83
|
+
# Nothing special to do, besides the fetchrow
|
84
|
+
when SQL_FETCH_PRIOR
|
85
|
+
@index -= 2
|
86
|
+
when SQL_FETCH_FIRST
|
87
|
+
@index = -1
|
88
|
+
when SQL_FETCH_LAST
|
89
|
+
@index = @pg_result.num_tuples - 2
|
90
|
+
when SQL_FETCH_ABSOLUTE
|
91
|
+
# Note: if you go "out of range", all fetches will give nil until you get back
|
92
|
+
# into range, this doesn't raise an error.
|
93
|
+
@index = offset-1
|
94
|
+
when SQL_FETCH_RELATIVE
|
95
|
+
# Note: if you go "out of range", all fetches will give nil until you get back
|
96
|
+
# into range, this doesn't raise an error.
|
97
|
+
@index += offset - 1
|
98
|
+
else
|
99
|
+
raise NotSupportedError
|
100
|
+
end
|
101
|
+
self.fetchrow
|
102
|
+
end
|
103
|
+
|
104
|
+
#
|
105
|
+
# The number of rows returned.
|
106
|
+
#
|
107
|
+
def row_count
|
108
|
+
@pg_result.num_tuples
|
109
|
+
end
|
110
|
+
|
111
|
+
#
|
112
|
+
# The row processed count. This is analogue to DBI::StatementHandle#rows.
|
113
|
+
#
|
114
|
+
def rows_affected
|
115
|
+
@pg_result.cmdtuples
|
116
|
+
end
|
117
|
+
|
118
|
+
def finish
|
119
|
+
@pg_result.clear
|
120
|
+
end
|
121
|
+
end # Tuples
|
data/lib/dbd/pg/type.rb
ADDED
@@ -0,0 +1,209 @@
|
|
1
|
+
#
|
2
|
+
# Type Management for PostgreSQL-specific types.
|
3
|
+
#
|
4
|
+
# See DBI::Type and DBI::TypeUtil for more information.
|
5
|
+
#
|
6
|
+
module DBI::DBD::Pg::Type
|
7
|
+
#
|
8
|
+
# ByteA is a special escaped form of binary data, suitable for inclusion in queries.
|
9
|
+
#
|
10
|
+
# This class is an attempt to abstract that type so you do not have to
|
11
|
+
# concern yourself with the conversion issues.
|
12
|
+
#
|
13
|
+
class ByteA
|
14
|
+
|
15
|
+
attr_reader :original
|
16
|
+
attr_reader :escaped
|
17
|
+
|
18
|
+
#
|
19
|
+
# Build a new ByteA object.
|
20
|
+
#
|
21
|
+
# The data supplied is the unescaped binary data you wish to put in the
|
22
|
+
# database.
|
23
|
+
#
|
24
|
+
def initialize(obj)
|
25
|
+
@original = obj
|
26
|
+
@escaped = escape_bytea(obj)
|
27
|
+
@original.freeze
|
28
|
+
@escaped.freeze
|
29
|
+
end
|
30
|
+
|
31
|
+
#
|
32
|
+
# Escapes the supplied data. Has no effect on the object.
|
33
|
+
#
|
34
|
+
def escape_bytea(str)
|
35
|
+
PGconn.escape_bytea(str)
|
36
|
+
end
|
37
|
+
|
38
|
+
#
|
39
|
+
# Returns the original data.
|
40
|
+
#
|
41
|
+
def to_s
|
42
|
+
return @original.dup
|
43
|
+
end
|
44
|
+
|
45
|
+
#
|
46
|
+
# Class method to escape the data into ByteA format.
|
47
|
+
#
|
48
|
+
def self.escape_bytea(str)
|
49
|
+
self.new(str).escaped
|
50
|
+
end
|
51
|
+
|
52
|
+
#
|
53
|
+
# Class method to unescape the ByteA data and present it as a string.
|
54
|
+
#
|
55
|
+
def self.parse(obj)
|
56
|
+
|
57
|
+
return nil if obj.nil?
|
58
|
+
|
59
|
+
# FIXME there's a bug in the upstream 'pg' driver that does not
|
60
|
+
# properly decode bytea, leaving in an extra slash for each decoded
|
61
|
+
# character.
|
62
|
+
#
|
63
|
+
# Fix this for now, but beware that we'll have to unfix this as
|
64
|
+
# soon as they fix their end.
|
65
|
+
ret = PGconn.unescape_bytea(obj)
|
66
|
+
|
67
|
+
# XXX
|
68
|
+
# String#split does not properly create a full array if the the
|
69
|
+
# string ENDS in the split regex, unless this oddball -1 argument is supplied.
|
70
|
+
#
|
71
|
+
# Another way of saying this:
|
72
|
+
# if foo = "foo\\\\\" and foo.split(/\\\\/), the result will be
|
73
|
+
# ["foo"]. You can add as many delimiters to the end of the string
|
74
|
+
# as you'd like - the result is no different.
|
75
|
+
#
|
76
|
+
|
77
|
+
ret = ret.split(/\\\\/, -1).collect { |x| x.length > 0 ? x.gsub(/\\[0-7]{3}/) { |y| y[1..3].oct.chr } : "" }.join("\\")
|
78
|
+
ret.gsub!(/''/, "'")
|
79
|
+
return ret
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
#
|
84
|
+
# PostgreSQL arrays are simply a specification that sits on top of normal
|
85
|
+
# types. They have a specialized string grammar and this class facilitates
|
86
|
+
# converting that syntax and the types within those arrays.
|
87
|
+
#
|
88
|
+
class Array
|
89
|
+
|
90
|
+
attr_reader :base_type
|
91
|
+
|
92
|
+
#
|
93
|
+
# +base_type+ is a DBI::Type that is used to parse the inner types when
|
94
|
+
# a non-array one is found.
|
95
|
+
#
|
96
|
+
# For instance, if you had an array of integer, one would pass
|
97
|
+
# DBI::Type::Integer here.
|
98
|
+
#
|
99
|
+
def initialize(base_type)
|
100
|
+
@base_type = base_type
|
101
|
+
end
|
102
|
+
|
103
|
+
#
|
104
|
+
# Object method. Please note that this is different than most DBI::Type
|
105
|
+
# classes! One must initialize an Array object with an appropriate
|
106
|
+
# DBI::Type used to convert the indices of the array before this method
|
107
|
+
# can be called.
|
108
|
+
#
|
109
|
+
# Returns an appropriately converted array.
|
110
|
+
#
|
111
|
+
def parse(obj)
|
112
|
+
if obj.nil?
|
113
|
+
nil
|
114
|
+
elsif obj.index('{') == 0 and obj.rindex('}') == (obj.length - 1)
|
115
|
+
convert_array(obj)
|
116
|
+
else
|
117
|
+
raise "Not an array"
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
#
|
122
|
+
# Parse a PostgreSQL-Array output and convert into ruby array. This
|
123
|
+
# does the real parsing work.
|
124
|
+
#
|
125
|
+
def convert_array(str)
|
126
|
+
|
127
|
+
array_nesting = 0 # nesting level of the array
|
128
|
+
in_string = false # currently inside a quoted string ?
|
129
|
+
escaped = false # if the character is escaped
|
130
|
+
sbuffer = '' # buffer for the current element
|
131
|
+
result_array = ::Array.new # the resulting Array
|
132
|
+
|
133
|
+
str.each_byte { |char| # parse character by character
|
134
|
+
char = char.chr # we need the Character, not it's Integer
|
135
|
+
|
136
|
+
if escaped then # if this character is escaped, just add it to the buffer
|
137
|
+
sbuffer += char
|
138
|
+
escaped = false
|
139
|
+
next
|
140
|
+
end
|
141
|
+
|
142
|
+
case char # let's see what kind of character we have
|
143
|
+
#------------- {: beginning of an array ----#
|
144
|
+
when '{'
|
145
|
+
if in_string then # ignore inside a string
|
146
|
+
sbuffer += char
|
147
|
+
next
|
148
|
+
end
|
149
|
+
|
150
|
+
if array_nesting >= 1 then # if it's an nested array, defer for recursion
|
151
|
+
sbuffer += char
|
152
|
+
end
|
153
|
+
array_nesting += 1 # inside another array
|
154
|
+
|
155
|
+
#------------- ": string deliminator --------#
|
156
|
+
when '"'
|
157
|
+
in_string = !in_string
|
158
|
+
|
159
|
+
#------------- \: escape character, next is regular character #
|
160
|
+
when "\\" # single \, must be extra escaped in Ruby
|
161
|
+
if array_nesting > 1
|
162
|
+
sbuffer += char
|
163
|
+
else
|
164
|
+
escaped = true
|
165
|
+
end
|
166
|
+
|
167
|
+
#------------- ,: element separator ---------#
|
168
|
+
when ','
|
169
|
+
if in_string or array_nesting > 1 then # don't care if inside string or
|
170
|
+
sbuffer += char # nested array
|
171
|
+
else
|
172
|
+
if !sbuffer.is_a? ::Array then
|
173
|
+
sbuffer = @base_type.parse(sbuffer)
|
174
|
+
end
|
175
|
+
result_array << sbuffer # otherwise, here ends an element
|
176
|
+
sbuffer = ''
|
177
|
+
end
|
178
|
+
|
179
|
+
#------------- }: End of Array --------------#
|
180
|
+
when '}'
|
181
|
+
if in_string then # ignore if inside quoted string
|
182
|
+
sbuffer += char
|
183
|
+
next
|
184
|
+
end
|
185
|
+
|
186
|
+
array_nesting -=1 # decrease nesting level
|
187
|
+
|
188
|
+
if array_nesting == 1 # must be the end of a nested array
|
189
|
+
sbuffer += char
|
190
|
+
sbuffer = convert_array( sbuffer ) # recurse, using the whole nested array
|
191
|
+
elsif array_nesting > 1 # inside nested array, keep it for later
|
192
|
+
sbuffer += char
|
193
|
+
else # array_nesting = 0, must be the last }
|
194
|
+
if !sbuffer.is_a? ::Array then
|
195
|
+
sbuffer = @base_type.parse( sbuffer )
|
196
|
+
end
|
197
|
+
|
198
|
+
result_array << sbuffer unless sbuffer.nil? # upto here was the last element
|
199
|
+
end
|
200
|
+
|
201
|
+
#------------- all other characters ---------#
|
202
|
+
else
|
203
|
+
sbuffer += char # simply append
|
204
|
+
end
|
205
|
+
}
|
206
|
+
return result_array
|
207
|
+
end # convert_array()
|
208
|
+
end
|
209
|
+
end
|