pg 0.18.4 → 1.2.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- checksums.yaml.gz.sig +0 -0
- data.tar.gz.sig +0 -0
- data/BSDL +2 -2
- data/ChangeLog +0 -5911
- data/History.rdoc +240 -0
- data/Manifest.txt +8 -20
- data/README-Windows.rdoc +4 -4
- data/README.ja.rdoc +1 -2
- data/README.rdoc +64 -15
- data/Rakefile +20 -21
- data/Rakefile.cross +67 -69
- data/ext/errorcodes.def +101 -0
- data/ext/errorcodes.rb +1 -1
- data/ext/errorcodes.txt +33 -2
- data/ext/extconf.rb +26 -36
- data/ext/gvl_wrappers.c +4 -0
- data/ext/gvl_wrappers.h +27 -39
- data/ext/pg.c +156 -145
- data/ext/pg.h +74 -98
- data/ext/pg_binary_decoder.c +82 -15
- data/ext/pg_binary_encoder.c +20 -19
- data/ext/pg_coder.c +103 -21
- data/ext/pg_connection.c +917 -523
- data/ext/pg_copy_coder.c +50 -12
- data/ext/pg_record_coder.c +491 -0
- data/ext/pg_result.c +590 -208
- data/ext/pg_text_decoder.c +606 -40
- data/ext/pg_text_encoder.c +245 -94
- data/ext/pg_tuple.c +549 -0
- data/ext/pg_type_map.c +14 -7
- data/ext/pg_type_map_all_strings.c +4 -4
- data/ext/pg_type_map_by_class.c +9 -4
- data/ext/pg_type_map_by_column.c +7 -6
- data/ext/pg_type_map_by_mri_type.c +1 -1
- data/ext/pg_type_map_by_oid.c +3 -2
- data/ext/pg_type_map_in_ruby.c +1 -1
- data/ext/{util.c → pg_util.c} +10 -10
- data/ext/{util.h → pg_util.h} +2 -2
- data/lib/pg.rb +23 -13
- data/lib/pg/basic_type_mapping.rb +155 -32
- data/lib/pg/binary_decoder.rb +23 -0
- data/lib/pg/coder.rb +23 -2
- data/lib/pg/connection.rb +73 -13
- data/lib/pg/constants.rb +2 -1
- data/lib/pg/exceptions.rb +2 -1
- data/lib/pg/result.rb +24 -7
- data/lib/pg/text_decoder.rb +24 -22
- data/lib/pg/text_encoder.rb +40 -8
- data/lib/pg/tuple.rb +30 -0
- data/lib/pg/type_map_by_column.rb +3 -2
- data/spec/helpers.rb +61 -36
- data/spec/pg/basic_type_mapping_spec.rb +415 -36
- data/spec/pg/connection_spec.rb +732 -327
- data/spec/pg/connection_sync_spec.rb +41 -0
- data/spec/pg/result_spec.rb +253 -21
- data/spec/pg/tuple_spec.rb +333 -0
- data/spec/pg/type_map_by_class_spec.rb +4 -4
- data/spec/pg/type_map_by_column_spec.rb +6 -2
- data/spec/pg/type_map_by_mri_type_spec.rb +2 -2
- data/spec/pg/type_map_by_oid_spec.rb +3 -3
- data/spec/pg/type_map_in_ruby_spec.rb +1 -1
- data/spec/pg/type_map_spec.rb +1 -1
- data/spec/pg/type_spec.rb +446 -20
- data/spec/pg_spec.rb +2 -2
- metadata +63 -72
- metadata.gz.sig +0 -0
- data/sample/array_insert.rb +0 -20
- data/sample/async_api.rb +0 -106
- data/sample/async_copyto.rb +0 -39
- data/sample/async_mixed.rb +0 -56
- data/sample/check_conn.rb +0 -21
- data/sample/copyfrom.rb +0 -81
- data/sample/copyto.rb +0 -19
- data/sample/cursor.rb +0 -21
- data/sample/disk_usage_report.rb +0 -186
- data/sample/issue-119.rb +0 -94
- data/sample/losample.rb +0 -69
- data/sample/minimal-testcase.rb +0 -17
- data/sample/notify_wait.rb +0 -72
- data/sample/pg_statistics.rb +0 -294
- data/sample/replication_monitor.rb +0 -231
- data/sample/test_binary_values.rb +0 -33
- data/sample/wal_shipper.rb +0 -434
- data/sample/warehouse_partitions.rb +0 -320
@@ -0,0 +1,23 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module PG
|
5
|
+
module BinaryDecoder
|
6
|
+
# Convenience classes for timezone options
|
7
|
+
class TimestampUtc < Timestamp
|
8
|
+
def initialize(params={})
|
9
|
+
super(params.merge(flags: PG::Coder::TIMESTAMP_DB_UTC | PG::Coder::TIMESTAMP_APP_UTC))
|
10
|
+
end
|
11
|
+
end
|
12
|
+
class TimestampUtcToLocal < Timestamp
|
13
|
+
def initialize(params={})
|
14
|
+
super(params.merge(flags: PG::Coder::TIMESTAMP_DB_UTC | PG::Coder::TIMESTAMP_APP_LOCAL))
|
15
|
+
end
|
16
|
+
end
|
17
|
+
class TimestampLocal < Timestamp
|
18
|
+
def initialize(params={})
|
19
|
+
super(params.merge(flags: PG::Coder::TIMESTAMP_DB_LOCAL | PG::Coder::TIMESTAMP_APP_LOCAL))
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end # module PG
|
data/lib/pg/coder.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
|
-
|
1
|
+
# -*- ruby -*-
|
2
|
+
# frozen_string_literal: true
|
2
3
|
|
3
4
|
module PG
|
4
5
|
|
@@ -28,6 +29,7 @@ module PG
|
|
28
29
|
{
|
29
30
|
oid: oid,
|
30
31
|
format: format,
|
32
|
+
flags: flags,
|
31
33
|
name: name,
|
32
34
|
}
|
33
35
|
end
|
@@ -52,6 +54,18 @@ module PG
|
|
52
54
|
str[-1,0] = "#{name_str} #{oid_str}#{format_str}"
|
53
55
|
str
|
54
56
|
end
|
57
|
+
|
58
|
+
def inspect_short
|
59
|
+
str = case format
|
60
|
+
when 0 then "T"
|
61
|
+
when 1 then "B"
|
62
|
+
else format.to_s
|
63
|
+
end
|
64
|
+
str += "E" if respond_to?(:encode)
|
65
|
+
str += "D" if respond_to?(:decode)
|
66
|
+
|
67
|
+
"#{name || self.class.name}:#{str}"
|
68
|
+
end
|
55
69
|
end
|
56
70
|
|
57
71
|
class CompositeCoder < Coder
|
@@ -79,5 +93,12 @@ module PG
|
|
79
93
|
})
|
80
94
|
end
|
81
95
|
end
|
82
|
-
end # module PG
|
83
96
|
|
97
|
+
class RecordCoder < Coder
|
98
|
+
def to_h
|
99
|
+
super.merge!({
|
100
|
+
type_map: type_map,
|
101
|
+
})
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end # module PG
|
data/lib/pg/connection.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
|
-
|
1
|
+
# -*- ruby -*-
|
2
|
+
# frozen_string_literal: true
|
2
3
|
|
3
4
|
require 'pg' unless defined?( PG )
|
4
5
|
require 'uri'
|
@@ -47,7 +48,7 @@ class PG::Connection
|
|
47
48
|
|
48
49
|
if args.length == 1
|
49
50
|
case args.first
|
50
|
-
when URI, URI
|
51
|
+
when URI, /\A#{URI::ABS_URI_REF}\z/
|
51
52
|
uri = URI(args.first)
|
52
53
|
options.merge!( Hash[URI.decode_www_form( uri.query )] ) if uri.query
|
53
54
|
when /=/
|
@@ -85,7 +86,7 @@ class PG::Connection
|
|
85
86
|
|
86
87
|
|
87
88
|
# call-seq:
|
88
|
-
# conn.copy_data( sql ) {|sql_result| ... } -> PG::Result
|
89
|
+
# conn.copy_data( sql [, coder] ) {|sql_result| ... } -> PG::Result
|
89
90
|
#
|
90
91
|
# Execute a copy process for transfering data to or from the server.
|
91
92
|
#
|
@@ -109,13 +110,26 @@ class PG::Connection
|
|
109
110
|
# of blocking mode of operation, #copy_data is preferred to raw calls
|
110
111
|
# of #put_copy_data, #get_copy_data and #put_copy_end.
|
111
112
|
#
|
113
|
+
# _coder_ can be a PG::Coder derivation
|
114
|
+
# (typically PG::TextEncoder::CopyRow or PG::TextDecoder::CopyRow).
|
115
|
+
# This enables encoding of data fields given to #put_copy_data
|
116
|
+
# or decoding of fields received by #get_copy_data.
|
117
|
+
#
|
112
118
|
# Example with CSV input format:
|
113
|
-
# conn.exec "create table my_table (a text,b text,c text,d text
|
119
|
+
# conn.exec "create table my_table (a text,b text,c text,d text)"
|
114
120
|
# conn.copy_data "COPY my_table FROM STDIN CSV" do
|
115
|
-
# conn.put_copy_data "some,
|
116
|
-
# conn.put_copy_data "more,
|
121
|
+
# conn.put_copy_data "some,data,to,copy\n"
|
122
|
+
# conn.put_copy_data "more,data,to,copy\n"
|
123
|
+
# end
|
124
|
+
# This creates +my_table+ and inserts two CSV rows.
|
125
|
+
#
|
126
|
+
# The same with text format encoder PG::TextEncoder::CopyRow
|
127
|
+
# and Array input:
|
128
|
+
# enco = PG::TextEncoder::CopyRow.new
|
129
|
+
# conn.copy_data "COPY my_table FROM STDIN", enco do
|
130
|
+
# conn.put_copy_data ['some', 'data', 'to', 'copy']
|
131
|
+
# conn.put_copy_data ['more', 'data', 'to', 'copy']
|
117
132
|
# end
|
118
|
-
# This creates +my_table+ and inserts two rows.
|
119
133
|
#
|
120
134
|
# Example with CSV output format:
|
121
135
|
# conn.copy_data "COPY my_table TO STDOUT CSV" do
|
@@ -124,8 +138,21 @@ class PG::Connection
|
|
124
138
|
# end
|
125
139
|
# end
|
126
140
|
# This prints all rows of +my_table+ to stdout:
|
127
|
-
# "some,
|
128
|
-
# "more,
|
141
|
+
# "some,data,to,copy\n"
|
142
|
+
# "more,data,to,copy\n"
|
143
|
+
#
|
144
|
+
# The same with text format decoder PG::TextDecoder::CopyRow
|
145
|
+
# and Array output:
|
146
|
+
# deco = PG::TextDecoder::CopyRow.new
|
147
|
+
# conn.copy_data "COPY my_table TO STDOUT", deco do
|
148
|
+
# while row=conn.get_copy_data
|
149
|
+
# p row
|
150
|
+
# end
|
151
|
+
# end
|
152
|
+
# This receives all rows of +my_table+ as ruby array:
|
153
|
+
# ["some", "data", "to", "copy"]
|
154
|
+
# ["more", "data", "to", "copy"]
|
155
|
+
|
129
156
|
def copy_data( sql, coder=nil )
|
130
157
|
res = exec( sql )
|
131
158
|
|
@@ -165,7 +192,7 @@ class PG::Connection
|
|
165
192
|
raise
|
166
193
|
else
|
167
194
|
res = get_last_result
|
168
|
-
if res.result_status != PGRES_COMMAND_OK
|
195
|
+
if !res || res.result_status != PGRES_COMMAND_OK
|
169
196
|
while get_copy_data
|
170
197
|
end
|
171
198
|
while get_result
|
@@ -224,8 +251,41 @@ class PG::Connection
|
|
224
251
|
end
|
225
252
|
end
|
226
253
|
|
227
|
-
|
254
|
+
# Method 'ssl_attribute' was introduced in PostgreSQL 9.5.
|
255
|
+
if self.instance_methods.find{|m| m.to_sym == :ssl_attribute }
|
256
|
+
# call-seq:
|
257
|
+
# conn.ssl_attributes -> Hash<String,String>
|
258
|
+
#
|
259
|
+
# Returns SSL-related information about the connection as key/value pairs
|
260
|
+
#
|
261
|
+
# The available attributes varies depending on the SSL library being used,
|
262
|
+
# and the type of connection.
|
263
|
+
#
|
264
|
+
# See also #ssl_attribute
|
265
|
+
def ssl_attributes
|
266
|
+
ssl_attribute_names.each.with_object({}) do |n,h|
|
267
|
+
h[n] = ssl_attribute(n)
|
268
|
+
end
|
269
|
+
end
|
270
|
+
end
|
228
271
|
|
229
|
-
|
230
|
-
|
272
|
+
REDIRECT_METHODS = {
|
273
|
+
:exec => [:async_exec, :sync_exec],
|
274
|
+
:query => [:async_exec, :sync_exec],
|
275
|
+
:exec_params => [:async_exec_params, :sync_exec_params],
|
276
|
+
:prepare => [:async_prepare, :sync_prepare],
|
277
|
+
:exec_prepared => [:async_exec_prepared, :sync_exec_prepared],
|
278
|
+
:describe_portal => [:async_describe_portal, :sync_describe_portal],
|
279
|
+
:describe_prepared => [:async_describe_prepared, :sync_describe_prepared],
|
280
|
+
}
|
231
281
|
|
282
|
+
def self.async_api=(enable)
|
283
|
+
REDIRECT_METHODS.each do |ali, (async, sync)|
|
284
|
+
remove_method(ali) if method_defined?(ali)
|
285
|
+
alias_method( ali, enable ? async : sync )
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
# pg-1.1.0+ defaults to libpq's async API for query related blocking methods
|
290
|
+
self.async_api = true
|
291
|
+
end # class PG::Connection
|
data/lib/pg/constants.rb
CHANGED
data/lib/pg/exceptions.rb
CHANGED
data/lib/pg/result.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
|
-
|
1
|
+
# -*- ruby -*-
|
2
|
+
# frozen_string_literal: true
|
2
3
|
|
3
4
|
require 'pg' unless defined?( PG )
|
4
5
|
|
@@ -9,18 +10,34 @@ class PG::Result
|
|
9
10
|
#
|
10
11
|
# +type_map+: a PG::TypeMap instance.
|
11
12
|
#
|
12
|
-
#
|
13
|
+
# This method is equal to #type_map= , but returns self, so that calls can be chained.
|
14
|
+
#
|
15
|
+
# See also PG::BasicTypeMapForResults
|
13
16
|
def map_types!(type_map)
|
14
17
|
self.type_map = type_map
|
15
|
-
self
|
18
|
+
return self
|
19
|
+
end
|
20
|
+
|
21
|
+
# Set the data type for all field name returning methods.
|
22
|
+
#
|
23
|
+
# +type+: a Symbol defining the field name type.
|
24
|
+
#
|
25
|
+
# This method is equal to #field_name_type= , but returns self, so that calls can be chained.
|
26
|
+
def field_names_as(type)
|
27
|
+
self.field_name_type = type
|
28
|
+
return self
|
16
29
|
end
|
17
30
|
|
31
|
+
### Return a String representation of the object suitable for debugging.
|
18
32
|
def inspect
|
19
33
|
str = self.to_s
|
20
|
-
str[-1,0] =
|
21
|
-
|
34
|
+
str[-1,0] = if cleared?
|
35
|
+
" cleared"
|
36
|
+
else
|
37
|
+
" status=#{res_status(result_status)} ntuples=#{ntuples} nfields=#{nfields} cmd_tuples=#{cmd_tuples}"
|
38
|
+
end
|
39
|
+
return str
|
22
40
|
end
|
41
|
+
|
23
42
|
end # class PG::Result
|
24
43
|
|
25
|
-
# Backward-compatible alias
|
26
|
-
PGresult = PG::Result
|
data/lib/pg/text_decoder.rb
CHANGED
@@ -1,14 +1,14 @@
|
|
1
|
-
|
1
|
+
# -*- ruby -*-
|
2
|
+
# frozen_string_literal: true
|
2
3
|
|
3
4
|
require 'date'
|
5
|
+
require 'json'
|
4
6
|
|
5
7
|
module PG
|
6
8
|
module TextDecoder
|
7
9
|
class Date < SimpleDecoder
|
8
|
-
ISO_DATE = /\A(\d{4})-(\d\d)-(\d\d)\z/
|
9
|
-
|
10
10
|
def decode(string, tuple=nil, field=nil)
|
11
|
-
if string =~
|
11
|
+
if string =~ /\A(\d{4})-(\d\d)-(\d\d)\z/
|
12
12
|
::Date.new $1.to_i, $2.to_i, $3.to_i
|
13
13
|
else
|
14
14
|
string
|
@@ -16,29 +16,31 @@ module PG
|
|
16
16
|
end
|
17
17
|
end
|
18
18
|
|
19
|
-
class
|
20
|
-
ISO_DATETIME_WITHOUT_TIMEZONE = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?\z/
|
21
|
-
|
19
|
+
class JSON < SimpleDecoder
|
22
20
|
def decode(string, tuple=nil, field=nil)
|
23
|
-
|
24
|
-
Time.new $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, "#{$6}#{$7}".to_r
|
25
|
-
else
|
26
|
-
string
|
27
|
-
end
|
21
|
+
::JSON.parse(string, quirks_mode: true)
|
28
22
|
end
|
29
23
|
end
|
30
24
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
25
|
+
# Convenience classes for timezone options
|
26
|
+
class TimestampUtc < Timestamp
|
27
|
+
def initialize(params={})
|
28
|
+
super(params.merge(flags: PG::Coder::TIMESTAMP_DB_UTC | PG::Coder::TIMESTAMP_APP_UTC))
|
29
|
+
end
|
30
|
+
end
|
31
|
+
class TimestampUtcToLocal < Timestamp
|
32
|
+
def initialize(params={})
|
33
|
+
super(params.merge(flags: PG::Coder::TIMESTAMP_DB_UTC | PG::Coder::TIMESTAMP_APP_LOCAL))
|
40
34
|
end
|
41
35
|
end
|
36
|
+
class TimestampLocal < Timestamp
|
37
|
+
def initialize(params={})
|
38
|
+
super(params.merge(flags: PG::Coder::TIMESTAMP_DB_LOCAL | PG::Coder::TIMESTAMP_APP_LOCAL))
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# For backward compatibility:
|
43
|
+
TimestampWithoutTimeZone = TimestampLocal
|
44
|
+
TimestampWithTimeZone = Timestamp
|
42
45
|
end
|
43
46
|
end # module PG
|
44
|
-
|
data/lib/pg/text_encoder.rb
CHANGED
@@ -1,27 +1,59 @@
|
|
1
|
-
|
1
|
+
# -*- ruby -*-
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'json'
|
5
|
+
require 'ipaddr'
|
2
6
|
|
3
7
|
module PG
|
4
8
|
module TextEncoder
|
5
9
|
class Date < SimpleEncoder
|
6
|
-
STRFTIME_ISO_DATE = "%Y-%m-%d".freeze
|
7
10
|
def encode(value)
|
8
|
-
value.respond_to?(:strftime) ? value.strftime(
|
11
|
+
value.respond_to?(:strftime) ? value.strftime("%Y-%m-%d") : value
|
9
12
|
end
|
10
13
|
end
|
11
14
|
|
12
15
|
class TimestampWithoutTimeZone < SimpleEncoder
|
13
|
-
STRFTIME_ISO_DATETIME_WITHOUT_TIMEZONE = "%Y-%m-%d %H:%M:%S.%N".freeze
|
14
16
|
def encode(value)
|
15
|
-
value.respond_to?(:strftime) ? value.strftime(
|
17
|
+
value.respond_to?(:strftime) ? value.strftime("%Y-%m-%d %H:%M:%S.%N") : value
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class TimestampUtc < SimpleEncoder
|
22
|
+
def encode(value)
|
23
|
+
value.respond_to?(:utc) ? value.utc.strftime("%Y-%m-%d %H:%M:%S.%N") : value
|
16
24
|
end
|
17
25
|
end
|
18
26
|
|
19
27
|
class TimestampWithTimeZone < SimpleEncoder
|
20
|
-
STRFTIME_ISO_DATETIME_WITH_TIMEZONE = "%Y-%m-%d %H:%M:%S.%N %:z".freeze
|
21
28
|
def encode(value)
|
22
|
-
value.respond_to?(:strftime) ? value.strftime(
|
29
|
+
value.respond_to?(:strftime) ? value.strftime("%Y-%m-%d %H:%M:%S.%N %:z") : value
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class JSON < SimpleEncoder
|
34
|
+
def encode(value)
|
35
|
+
::JSON.generate(value, quirks_mode: true)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class Inet < SimpleEncoder
|
40
|
+
def encode(value)
|
41
|
+
case value
|
42
|
+
when IPAddr
|
43
|
+
default_prefix = (value.family == Socket::AF_INET ? 32 : 128)
|
44
|
+
s = value.to_s
|
45
|
+
if value.respond_to?(:prefix)
|
46
|
+
prefix = value.prefix
|
47
|
+
else
|
48
|
+
range = value.to_range
|
49
|
+
prefix = default_prefix - Math.log(((range.end.to_i - range.begin.to_i) + 1), 2).to_i
|
50
|
+
end
|
51
|
+
s << "/" << prefix.to_s if prefix != default_prefix
|
52
|
+
s
|
53
|
+
else
|
54
|
+
value
|
55
|
+
end
|
23
56
|
end
|
24
57
|
end
|
25
58
|
end
|
26
59
|
end # module PG
|
27
|
-
|
data/lib/pg/tuple.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'pg' unless defined?( PG )
|
5
|
+
|
6
|
+
|
7
|
+
class PG::Tuple
|
8
|
+
|
9
|
+
### Return a String representation of the object suitable for debugging.
|
10
|
+
def inspect
|
11
|
+
"#<#{self.class} #{self.map{|k,v| "#{k}: #{v.inspect}" }.join(", ") }>"
|
12
|
+
end
|
13
|
+
|
14
|
+
def has_key?(key)
|
15
|
+
field_map.has_key?(key)
|
16
|
+
end
|
17
|
+
alias key? has_key?
|
18
|
+
|
19
|
+
def keys
|
20
|
+
field_names || field_map.keys.freeze
|
21
|
+
end
|
22
|
+
|
23
|
+
def each_key(&block)
|
24
|
+
if fn=field_names
|
25
|
+
fn.each(&block)
|
26
|
+
else
|
27
|
+
field_map.each_key(&block)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -1,4 +1,5 @@
|
|
1
|
-
|
1
|
+
# -*- ruby -*-
|
2
|
+
# frozen_string_literal: true
|
2
3
|
|
3
4
|
require 'pg' unless defined?( PG )
|
4
5
|
|
@@ -9,7 +10,7 @@ class PG::TypeMapByColumn
|
|
9
10
|
end
|
10
11
|
|
11
12
|
def inspect
|
12
|
-
type_strings = coders.map{|c| c ?
|
13
|
+
type_strings = coders.map{|c| c ? c.inspect_short : 'nil' }
|
13
14
|
"#<#{self.class} #{type_strings.join(' ')}>"
|
14
15
|
end
|
15
16
|
end
|