pg 0.21.0 → 1.1.4
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 +5 -5
- checksums.yaml.gz.sig +0 -0
- data.tar.gz.sig +0 -0
- data/History.rdoc +98 -0
- data/Manifest.txt +5 -1
- data/README.rdoc +14 -4
- data/Rakefile +4 -5
- data/Rakefile.cross +17 -21
- data/ext/errorcodes.def +12 -0
- data/ext/errorcodes.rb +1 -1
- data/ext/errorcodes.txt +4 -1
- data/ext/extconf.rb +14 -32
- data/ext/gvl_wrappers.c +4 -0
- data/ext/gvl_wrappers.h +23 -39
- data/ext/pg.c +23 -50
- data/ext/pg.h +51 -81
- data/ext/pg_binary_decoder.c +73 -6
- data/ext/pg_coder.c +52 -3
- data/ext/pg_connection.c +369 -219
- data/ext/pg_copy_coder.c +10 -5
- data/ext/pg_result.c +343 -119
- data/ext/pg_text_decoder.c +597 -37
- data/ext/pg_text_encoder.c +6 -7
- data/ext/pg_tuple.c +541 -0
- data/ext/util.c +6 -6
- data/ext/util.h +2 -2
- data/lib/pg.rb +5 -7
- data/lib/pg/basic_type_mapping.rb +40 -7
- data/lib/pg/binary_decoder.rb +22 -0
- data/lib/pg/coder.rb +1 -1
- data/lib/pg/connection.rb +27 -3
- data/lib/pg/constants.rb +1 -1
- data/lib/pg/exceptions.rb +1 -1
- data/lib/pg/result.rb +1 -1
- data/lib/pg/text_decoder.rb +19 -23
- data/lib/pg/text_encoder.rb +35 -1
- data/lib/pg/tuple.rb +30 -0
- data/lib/pg/type_map_by_column.rb +1 -1
- data/spec/helpers.rb +49 -21
- data/spec/pg/basic_type_mapping_spec.rb +230 -27
- data/spec/pg/connection_spec.rb +473 -277
- data/spec/pg/connection_sync_spec.rb +41 -0
- data/spec/pg/result_spec.rb +48 -13
- data/spec/pg/tuple_spec.rb +280 -0
- data/spec/pg/type_map_by_class_spec.rb +1 -1
- data/spec/pg/type_map_by_column_spec.rb +1 -1
- data/spec/pg/type_map_by_mri_type_spec.rb +1 -1
- data/spec/pg/type_map_by_oid_spec.rb +1 -1
- 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 +184 -12
- data/spec/pg_spec.rb +2 -2
- metadata +37 -33
- metadata.gz.sig +0 -0
- data/lib/pg/deprecated_constants.rb +0 -21
data/ext/util.c
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
/*
|
2
2
|
* util.c - Utils for ruby-pg
|
3
|
-
* $Id: util.c,v
|
3
|
+
* $Id: util.c,v fc1c4deb1398 2018/06/25 12:02:09 kanis $
|
4
4
|
*
|
5
5
|
*/
|
6
6
|
|
@@ -15,9 +15,9 @@ static const char base64_encode_table[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijk
|
|
15
15
|
* in-place (with _out_ == _in_).
|
16
16
|
*/
|
17
17
|
void
|
18
|
-
base64_encode( char *out, char *in, int len)
|
18
|
+
base64_encode( char *out, const char *in, int len)
|
19
19
|
{
|
20
|
-
unsigned char *in_ptr = (unsigned char *)in + len;
|
20
|
+
const unsigned char *in_ptr = (const unsigned char *)in + len;
|
21
21
|
char *out_ptr = out + BASE64_ENCODED_SIZE(len);
|
22
22
|
int part_len = len % 3;
|
23
23
|
|
@@ -72,12 +72,12 @@ static const unsigned char base64_decode_table[] =
|
|
72
72
|
* It is possible to decode a string in-place (with _out_ == _in_).
|
73
73
|
*/
|
74
74
|
int
|
75
|
-
base64_decode( char *out, char *in, unsigned int len)
|
75
|
+
base64_decode( char *out, const char *in, unsigned int len)
|
76
76
|
{
|
77
77
|
unsigned char a, b, c, d;
|
78
|
-
unsigned char *in_ptr = (unsigned char *)in;
|
78
|
+
const unsigned char *in_ptr = (const unsigned char *)in;
|
79
79
|
unsigned char *out_ptr = (unsigned char *)out;
|
80
|
-
unsigned char *iend_ptr = (unsigned char *)in + len;
|
80
|
+
const unsigned char *iend_ptr = (unsigned char *)in + len;
|
81
81
|
|
82
82
|
for(;;){
|
83
83
|
if( in_ptr+3 < iend_ptr &&
|
data/ext/util.h
CHANGED
@@ -57,8 +57,8 @@
|
|
57
57
|
#define BASE64_ENCODED_SIZE(strlen) (((strlen) + 2) / 3 * 4)
|
58
58
|
#define BASE64_DECODED_SIZE(base64len) (((base64len) + 3) / 4 * 3)
|
59
59
|
|
60
|
-
void base64_encode( char *out, char *in, int len);
|
61
|
-
int base64_decode( char *out, char *in, unsigned int len);
|
60
|
+
void base64_encode( char *out, const char *in, int len);
|
61
|
+
int base64_decode( char *out, const char *in, unsigned int len);
|
62
62
|
|
63
63
|
int rbpg_strncasecmp(const char *s1, const char *s2, size_t n);
|
64
64
|
|
data/lib/pg.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
|
1
|
+
# -*- ruby -*-
|
2
2
|
|
3
3
|
begin
|
4
4
|
require 'pg_ext'
|
@@ -35,10 +35,10 @@ end
|
|
35
35
|
module PG
|
36
36
|
|
37
37
|
# Library version
|
38
|
-
VERSION = '
|
38
|
+
VERSION = '1.1.4'
|
39
39
|
|
40
40
|
# VCS revision
|
41
|
-
REVISION = %q$Revision:
|
41
|
+
REVISION = %q$Revision: 6f611e78845a $
|
42
42
|
|
43
43
|
class NotAllCopyDataRetrieved < PG::Error
|
44
44
|
end
|
@@ -60,17 +60,15 @@ module PG
|
|
60
60
|
require 'pg/exceptions'
|
61
61
|
require 'pg/constants'
|
62
62
|
require 'pg/coder'
|
63
|
+
require 'pg/binary_decoder'
|
63
64
|
require 'pg/text_encoder'
|
64
65
|
require 'pg/text_decoder'
|
65
66
|
require 'pg/basic_type_mapping'
|
66
67
|
require 'pg/type_map_by_column'
|
67
68
|
require 'pg/connection'
|
68
69
|
require 'pg/result'
|
70
|
+
require 'pg/tuple'
|
69
71
|
|
70
72
|
end # module PG
|
71
73
|
|
72
74
|
|
73
|
-
autoload :PGError, 'pg/deprecated_constants'
|
74
|
-
autoload :PGconn, 'pg/deprecated_constants'
|
75
|
-
autoload :PGresult, 'pg/deprecated_constants'
|
76
|
-
|
@@ -1,7 +1,27 @@
|
|
1
|
-
|
1
|
+
# -*- ruby -*-
|
2
2
|
|
3
3
|
require 'pg' unless defined?( PG )
|
4
4
|
|
5
|
+
# This module defines the mapping between OID and encoder/decoder classes for PG::BasicTypeMapForResults, PG::BasicTypeMapForQueries and PG::BasicTypeMapBasedOnResult.
|
6
|
+
#
|
7
|
+
# Additional types can be added like so:
|
8
|
+
#
|
9
|
+
# require 'pg'
|
10
|
+
# require 'ipaddr'
|
11
|
+
#
|
12
|
+
# class InetDecoder < PG::SimpleDecoder
|
13
|
+
# def decode(string, tuple=nil, field=nil)
|
14
|
+
# IPAddr.new(string)
|
15
|
+
# end
|
16
|
+
# end
|
17
|
+
# class InetEncoder < PG::SimpleEncoder
|
18
|
+
# def encode(ip_addr)
|
19
|
+
# ip_addr.to_s
|
20
|
+
# end
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# # 0 if for text format, can also be 1 for binary
|
24
|
+
# PG::BasicTypeRegistry.register_type(0, 'inet', InetEncoder, InetDecoder)
|
5
25
|
module PG::BasicTypeRegistry
|
6
26
|
# An instance of this class stores the coders that should be used for a given wire format (text or binary)
|
7
27
|
# and type cast direction (encoder or decoder).
|
@@ -137,6 +157,7 @@ module PG::BasicTypeRegistry
|
|
137
157
|
# Register an OID type named +name+ with a typecasting encoder and decoder object in
|
138
158
|
# +type+. +name+ should correspond to the `typname` column in
|
139
159
|
# the `pg_type` table.
|
160
|
+
# +format+ can be 0 for text format and 1 for binary.
|
140
161
|
def self.register_type(format, name, encoder_class, decoder_class)
|
141
162
|
CODERS_BY_NAME[format] ||= { encoder: {}, decoder: {} }
|
142
163
|
CODERS_BY_NAME[format][:encoder][name] = encoder_class.new(name: name, format: format) if encoder_class
|
@@ -145,8 +166,14 @@ module PG::BasicTypeRegistry
|
|
145
166
|
|
146
167
|
# Alias the +old+ type to the +new+ type.
|
147
168
|
def self.alias_type(format, new, old)
|
148
|
-
|
149
|
-
|
169
|
+
[:encoder, :decoder].each do |ende|
|
170
|
+
enc = CODERS_BY_NAME[format][ende][old]
|
171
|
+
if enc
|
172
|
+
CODERS_BY_NAME[format][ende][new] = enc
|
173
|
+
else
|
174
|
+
CODERS_BY_NAME[format][ende].delete(new)
|
175
|
+
end
|
176
|
+
end
|
150
177
|
end
|
151
178
|
|
152
179
|
register_type 0, 'int2', PG::TextEncoder::Integer, PG::TextDecoder::Integer
|
@@ -154,7 +181,7 @@ module PG::BasicTypeRegistry
|
|
154
181
|
alias_type 0, 'int8', 'int2'
|
155
182
|
alias_type 0, 'oid', 'int2'
|
156
183
|
|
157
|
-
|
184
|
+
register_type 0, 'numeric', PG::TextEncoder::Numeric, PG::TextDecoder::Numeric
|
158
185
|
register_type 0, 'text', PG::TextEncoder::String, PG::TextDecoder::String
|
159
186
|
alias_type 0, 'varchar', 'text'
|
160
187
|
alias_type 0, 'char', 'text'
|
@@ -193,8 +220,8 @@ module PG::BasicTypeRegistry
|
|
193
220
|
# register_type 'citext', OID::Text.new
|
194
221
|
# register_type 'ltree', OID::Text.new
|
195
222
|
#
|
196
|
-
|
197
|
-
|
223
|
+
register_type 0, 'inet', PG::TextEncoder::Inet, PG::TextDecoder::Inet
|
224
|
+
alias_type 0, 'cidr', 'inet'
|
198
225
|
|
199
226
|
|
200
227
|
|
@@ -213,6 +240,8 @@ module PG::BasicTypeRegistry
|
|
213
240
|
register_type 1, 'bool', PG::BinaryEncoder::Boolean, PG::BinaryDecoder::Boolean
|
214
241
|
register_type 1, 'float4', nil, PG::BinaryDecoder::Float
|
215
242
|
register_type 1, 'float8', nil, PG::BinaryDecoder::Float
|
243
|
+
register_type 1, 'timestamp', nil, PG::BinaryDecoder::TimestampUtc
|
244
|
+
register_type 1, 'timestamptz', nil, PG::BinaryDecoder::TimestampUtcToLocal
|
216
245
|
end
|
217
246
|
|
218
247
|
# Simple set of rules for type casting common PostgreSQL types to Ruby.
|
@@ -258,7 +287,7 @@ end
|
|
258
287
|
# This prints the rows with type casted columns:
|
259
288
|
# ["a", 123, [5, 4, 3]]
|
260
289
|
#
|
261
|
-
# See also PG::BasicTypeMapBasedOnResult for the encoder direction.
|
290
|
+
# See also PG::BasicTypeMapBasedOnResult for the encoder direction and PG::BasicTypeRegistry for the definition of additional types.
|
262
291
|
class PG::BasicTypeMapForResults < PG::TypeMapByOid
|
263
292
|
include PG::BasicTypeRegistry
|
264
293
|
|
@@ -412,6 +441,10 @@ class PG::BasicTypeMapForQueries < PG::TypeMapByClass
|
|
412
441
|
# to unnecessary type conversions on server side.
|
413
442
|
Integer => [0, 'int8'],
|
414
443
|
Float => [0, 'float8'],
|
444
|
+
BigDecimal => [0, 'numeric'],
|
445
|
+
# We use text format and no type OID for IPAddr, because setting the OID can lead
|
446
|
+
# to unnecessary inet/cidr conversions on the server side.
|
447
|
+
IPAddr => [0, 'inet'],
|
415
448
|
Array => :get_array_type,
|
416
449
|
}
|
417
450
|
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
|
3
|
+
module PG
|
4
|
+
module BinaryDecoder
|
5
|
+
# Convenience classes for timezone options
|
6
|
+
class TimestampUtc < Timestamp
|
7
|
+
def initialize(params={})
|
8
|
+
super(params.merge(flags: PG::Coder::TIMESTAMP_DB_UTC | PG::Coder::TIMESTAMP_APP_UTC))
|
9
|
+
end
|
10
|
+
end
|
11
|
+
class TimestampUtcToLocal < Timestamp
|
12
|
+
def initialize(params={})
|
13
|
+
super(params.merge(flags: PG::Coder::TIMESTAMP_DB_UTC | PG::Coder::TIMESTAMP_APP_LOCAL))
|
14
|
+
end
|
15
|
+
end
|
16
|
+
class TimestampLocal < Timestamp
|
17
|
+
def initialize(params={})
|
18
|
+
super(params.merge(flags: PG::Coder::TIMESTAMP_DB_LOCAL | PG::Coder::TIMESTAMP_APP_LOCAL))
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end # module PG
|
data/lib/pg/coder.rb
CHANGED
data/lib/pg/connection.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
|
1
|
+
# -*- ruby -*-
|
2
2
|
|
3
3
|
require 'pg' unless defined?( PG )
|
4
4
|
require 'uri'
|
@@ -47,7 +47,7 @@ class PG::Connection
|
|
47
47
|
|
48
48
|
if args.length == 1
|
49
49
|
case args.first
|
50
|
-
when URI, URI.regexp
|
50
|
+
when URI, /\A#{URI.regexp}\z/
|
51
51
|
uri = URI(args.first)
|
52
52
|
options.merge!( Hash[URI.decode_www_form( uri.query )] ) if uri.query
|
53
53
|
when /=/
|
@@ -85,7 +85,7 @@ class PG::Connection
|
|
85
85
|
|
86
86
|
|
87
87
|
# call-seq:
|
88
|
-
# conn.copy_data( sql ) {|sql_result| ... } -> PG::Result
|
88
|
+
# conn.copy_data( sql [, coder] ) {|sql_result| ... } -> PG::Result
|
89
89
|
#
|
90
90
|
# Execute a copy process for transfering data to or from the server.
|
91
91
|
#
|
@@ -109,6 +109,11 @@ class PG::Connection
|
|
109
109
|
# of blocking mode of operation, #copy_data is preferred to raw calls
|
110
110
|
# of #put_copy_data, #get_copy_data and #put_copy_end.
|
111
111
|
#
|
112
|
+
# _coder_ can be a PG::Coder derivation
|
113
|
+
# (typically PG::TextEncoder::CopyRow or PG::TextDecoder::CopyRow).
|
114
|
+
# This enables encoding of data fields given to #put_copy_data
|
115
|
+
# or decoding of fields received by #get_copy_data.
|
116
|
+
#
|
112
117
|
# Example with CSV input format:
|
113
118
|
# conn.exec "create table my_table (a text,b text,c text,d text)"
|
114
119
|
# conn.copy_data "COPY my_table FROM STDIN CSV" do
|
@@ -263,5 +268,24 @@ class PG::Connection
|
|
263
268
|
end
|
264
269
|
end
|
265
270
|
|
271
|
+
REDIRECT_METHODS = {
|
272
|
+
:exec => [:async_exec, :sync_exec],
|
273
|
+
:query => [:async_exec, :sync_exec],
|
274
|
+
:exec_params => [:async_exec_params, :sync_exec_params],
|
275
|
+
:prepare => [:async_prepare, :sync_prepare],
|
276
|
+
:exec_prepared => [:async_exec_prepared, :sync_exec_prepared],
|
277
|
+
:describe_portal => [:async_describe_portal, :sync_describe_portal],
|
278
|
+
:describe_prepared => [:async_describe_prepared, :sync_describe_prepared],
|
279
|
+
}
|
280
|
+
|
281
|
+
def self.async_api=(enable)
|
282
|
+
REDIRECT_METHODS.each do |ali, (async, sync)|
|
283
|
+
remove_method(ali) if method_defined?(ali)
|
284
|
+
alias_method( ali, enable ? async : sync )
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
# pg-1.1.0+ defaults to libpq's async API for query related blocking methods
|
289
|
+
self.async_api = true
|
266
290
|
end # class PG::Connection
|
267
291
|
|
data/lib/pg/constants.rb
CHANGED
data/lib/pg/exceptions.rb
CHANGED
data/lib/pg/result.rb
CHANGED
data/lib/pg/text_decoder.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
|
1
|
+
# -*- ruby -*-
|
2
2
|
|
3
3
|
require 'date'
|
4
4
|
require 'json'
|
@@ -17,35 +17,31 @@ module PG
|
|
17
17
|
end
|
18
18
|
end
|
19
19
|
|
20
|
-
class
|
21
|
-
ISO_DATETIME_WITHOUT_TIMEZONE = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?\z/
|
22
|
-
|
20
|
+
class JSON < SimpleDecoder
|
23
21
|
def decode(string, tuple=nil, field=nil)
|
24
|
-
|
25
|
-
Time.new $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, "#{$6}#{$7}".to_r
|
26
|
-
else
|
27
|
-
string
|
28
|
-
end
|
22
|
+
::JSON.parse(string, quirks_mode: true)
|
29
23
|
end
|
30
24
|
end
|
31
25
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
if string =~ ISO_DATETIME_WITH_TIMEZONE
|
37
|
-
Time.new $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, "#{$6}#{$7}".to_r, "#{$8}:#{$9 || '00'}:#{$10 || '00'}"
|
38
|
-
else
|
39
|
-
string
|
40
|
-
end
|
26
|
+
# Convenience classes for timezone options
|
27
|
+
class TimestampUtc < Timestamp
|
28
|
+
def initialize(params={})
|
29
|
+
super(params.merge(flags: PG::Coder::TIMESTAMP_DB_UTC | PG::Coder::TIMESTAMP_APP_UTC))
|
41
30
|
end
|
42
31
|
end
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
::JSON.parse(string, quirks_mode: true)
|
32
|
+
class TimestampUtcToLocal < Timestamp
|
33
|
+
def initialize(params={})
|
34
|
+
super(params.merge(flags: PG::Coder::TIMESTAMP_DB_UTC | PG::Coder::TIMESTAMP_APP_LOCAL))
|
47
35
|
end
|
48
36
|
end
|
37
|
+
class TimestampLocal < Timestamp
|
38
|
+
def initialize(params={})
|
39
|
+
super(params.merge(flags: PG::Coder::TIMESTAMP_DB_LOCAL | PG::Coder::TIMESTAMP_APP_LOCAL))
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# For backward compatibility:
|
44
|
+
TimestampWithoutTimeZone = TimestampLocal
|
45
|
+
TimestampWithTimeZone = Timestamp
|
49
46
|
end
|
50
47
|
end # module PG
|
51
|
-
|
data/lib/pg/text_encoder.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
|
-
|
1
|
+
# -*- ruby -*-
|
2
2
|
|
3
3
|
require 'json'
|
4
|
+
require 'ipaddr'
|
4
5
|
|
5
6
|
module PG
|
6
7
|
module TextEncoder
|
@@ -18,6 +19,13 @@ module PG
|
|
18
19
|
end
|
19
20
|
end
|
20
21
|
|
22
|
+
class TimestampUtc < SimpleEncoder
|
23
|
+
STRFTIME_ISO_DATETIME_WITHOUT_TIMEZONE_UTC = "%Y-%m-%d %H:%M:%S.%N".freeze
|
24
|
+
def encode(value)
|
25
|
+
value.respond_to?(:utc) ? value.utc.strftime(STRFTIME_ISO_DATETIME_WITHOUT_TIMEZONE_UTC) : value
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
21
29
|
class TimestampWithTimeZone < SimpleEncoder
|
22
30
|
STRFTIME_ISO_DATETIME_WITH_TIMEZONE = "%Y-%m-%d %H:%M:%S.%N %:z".freeze
|
23
31
|
def encode(value)
|
@@ -25,11 +33,37 @@ module PG
|
|
25
33
|
end
|
26
34
|
end
|
27
35
|
|
36
|
+
class Numeric < SimpleEncoder
|
37
|
+
def encode(value)
|
38
|
+
value.is_a?(BigDecimal) ? value.to_s('F') : value
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
28
42
|
class JSON < SimpleEncoder
|
29
43
|
def encode(value)
|
30
44
|
::JSON.generate(value, quirks_mode: true)
|
31
45
|
end
|
32
46
|
end
|
47
|
+
|
48
|
+
class Inet < SimpleEncoder
|
49
|
+
def encode(value)
|
50
|
+
case value
|
51
|
+
when IPAddr
|
52
|
+
default_prefix = (value.family == Socket::AF_INET ? 32 : 128)
|
53
|
+
s = value.to_s
|
54
|
+
if value.respond_to?(:prefix)
|
55
|
+
prefix = value.prefix
|
56
|
+
else
|
57
|
+
range = value.to_range
|
58
|
+
prefix = default_prefix - Math.log(((range.end.to_i - range.begin.to_i) + 1), 2).to_i
|
59
|
+
end
|
60
|
+
s << "/" << prefix.to_s if prefix != default_prefix
|
61
|
+
s
|
62
|
+
else
|
63
|
+
value
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
33
67
|
end
|
34
68
|
end # module PG
|
35
69
|
|
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
|