postgres-pr 0.0.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.
- data/examples/client.rb +33 -0
- data/examples/server.rb +11 -0
- data/examples/test_connection.rb +17 -0
- data/lib/binary_reader.rb +120 -0
- data/lib/binary_writer.rb +100 -0
- data/lib/buffer.rb +87 -0
- data/lib/byteorder.rb +32 -0
- data/lib/postgres-pr/connection.rb +101 -0
- data/lib/postgres-pr/message.rb +473 -0
- data/lib/postgres-pr/typeconv/TC_conv.rb +18 -0
- data/lib/postgres-pr/typeconv/array.rb +46 -0
- data/lib/postgres-pr/typeconv/bytea.rb +26 -0
- data/lib/postgres-pr/typeconv/conv.rb +5 -0
- data/test/TC_message.rb +101 -0
- metadata +51 -0
data/examples/client.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
$LOAD_PATH.unshift "../lib"
|
2
|
+
require 'postgres-pr/message'
|
3
|
+
require 'socket'
|
4
|
+
|
5
|
+
s = UNIXSocket.new(ARGV.shift || "/tmp/.s.PGSQL.5432")
|
6
|
+
|
7
|
+
msg = StartupMessage.new(196608, "user" => "mneumann", "database" => "mneumann")
|
8
|
+
s << msg.dump
|
9
|
+
|
10
|
+
Thread.start(s) { |s|
|
11
|
+
sleep 2
|
12
|
+
s << Query.new("drop table test").dump
|
13
|
+
s << Query.new("create table test (i int, v varchar(100))").dump
|
14
|
+
s << Parse.new("insert into test (i, v) values ($1, $2)", "blah").dump
|
15
|
+
s << Query.new("EXECUTE blah(1, 'hallo')").dump
|
16
|
+
|
17
|
+
while not (line = gets.chomp).empty?
|
18
|
+
s << Query.new(line).dump
|
19
|
+
end
|
20
|
+
exit
|
21
|
+
}
|
22
|
+
|
23
|
+
loop do
|
24
|
+
msg = Message.read(s)
|
25
|
+
p msg
|
26
|
+
|
27
|
+
case msg
|
28
|
+
when AuthentificationOk
|
29
|
+
p "OK"
|
30
|
+
when ErrorResponse
|
31
|
+
p "FAILED"
|
32
|
+
end
|
33
|
+
end
|
data/examples/server.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
$LOAD_PATH.unshift '../lib'
|
2
|
+
require 'postgres-pr/connection'
|
3
|
+
|
4
|
+
conn = Connection.new('mneumann', 'mneumann')
|
5
|
+
p conn.query("DROP TABLE test; CREATE TABLE test (a VARCHAR(100))")
|
6
|
+
p conn.query("INSERT INTO test VALUES ('hallo')")
|
7
|
+
p conn.query("INSERT INTO test VALUES ('leute')")
|
8
|
+
conn.query("COMMIT")
|
9
|
+
|
10
|
+
conn.query("BEGIN")
|
11
|
+
10000.times do |i|
|
12
|
+
p i
|
13
|
+
conn.query("INSERT INTO test VALUES ('#{i}')")
|
14
|
+
end
|
15
|
+
conn.query("COMMIT")
|
16
|
+
|
17
|
+
p conn.query("SELECT * FROM test")
|
@@ -0,0 +1,120 @@
|
|
1
|
+
require 'byteorder'
|
2
|
+
|
3
|
+
# This mixin solely depends on method read(n), which must be defined
|
4
|
+
# in the class/module where you mixin this module.
|
5
|
+
module BinaryReaderMixin
|
6
|
+
|
7
|
+
# == 8 bit
|
8
|
+
|
9
|
+
# no byteorder for 8 bit!
|
10
|
+
|
11
|
+
def read_word8
|
12
|
+
ru(1, 'C')
|
13
|
+
end
|
14
|
+
|
15
|
+
def read_int8
|
16
|
+
ru(1, 'c')
|
17
|
+
end
|
18
|
+
|
19
|
+
alias read_byte read_word8
|
20
|
+
|
21
|
+
# == 16 bit
|
22
|
+
|
23
|
+
# === Unsigned
|
24
|
+
|
25
|
+
def read_word16_native
|
26
|
+
ru(2, 'S')
|
27
|
+
end
|
28
|
+
|
29
|
+
def read_word16_little
|
30
|
+
ru(2, 'v')
|
31
|
+
end
|
32
|
+
|
33
|
+
def read_word16_big
|
34
|
+
ru(2, 'n')
|
35
|
+
end
|
36
|
+
|
37
|
+
# === Signed
|
38
|
+
|
39
|
+
def read_int16_native
|
40
|
+
ru(2, 's')
|
41
|
+
end
|
42
|
+
|
43
|
+
def read_int16_little
|
44
|
+
# swap bytes if native=big (but we want little)
|
45
|
+
ru_swap(2, 's', ByteOrder::Big)
|
46
|
+
end
|
47
|
+
|
48
|
+
def read_int16_big
|
49
|
+
# swap bytes if native=little (but we want big)
|
50
|
+
ru_swap(2, 's', ByteOrder::Little)
|
51
|
+
end
|
52
|
+
|
53
|
+
# == 32 bit
|
54
|
+
|
55
|
+
# === Unsigned
|
56
|
+
|
57
|
+
def read_word32_native
|
58
|
+
ru(4, 'L')
|
59
|
+
end
|
60
|
+
|
61
|
+
def read_word32_little
|
62
|
+
ru(4, 'V')
|
63
|
+
end
|
64
|
+
|
65
|
+
def read_word32_big
|
66
|
+
ru(4, 'N')
|
67
|
+
end
|
68
|
+
|
69
|
+
# === Signed
|
70
|
+
|
71
|
+
def read_int32_native
|
72
|
+
ru(4, 'l')
|
73
|
+
end
|
74
|
+
|
75
|
+
def read_int32_little
|
76
|
+
# swap bytes if native=big (but we want little)
|
77
|
+
ru_swap(4, 'l', ByteOrder::Big)
|
78
|
+
end
|
79
|
+
|
80
|
+
def read_int32_big
|
81
|
+
# swap bytes if native=little (but we want big)
|
82
|
+
ru_swap(4, 'l', ByteOrder::Little)
|
83
|
+
end
|
84
|
+
|
85
|
+
# == Aliases
|
86
|
+
|
87
|
+
alias read_uint8 read_word8
|
88
|
+
|
89
|
+
# add some short-cut functions
|
90
|
+
%w(word16 int16 word32 int32).each do |typ|
|
91
|
+
alias_method "read_#{typ}_network", "read_#{typ}_big"
|
92
|
+
end
|
93
|
+
|
94
|
+
{:word16 => :uint16, :word32 => :uint32}.each do |old, new|
|
95
|
+
['_native', '_little', '_big', '_network'].each do |bo|
|
96
|
+
alias_method "read_#{new}#{bo}", "read_#{old}#{bo}"
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# read exactly n characters, otherwise raise an exception.
|
101
|
+
def readn(n)
|
102
|
+
str = read(n)
|
103
|
+
raise "couldn't read #{n} characters" if str.nil? or str.size != n
|
104
|
+
str
|
105
|
+
end
|
106
|
+
|
107
|
+
private
|
108
|
+
|
109
|
+
# shortcut method for readn+unpack
|
110
|
+
def ru(size, template)
|
111
|
+
readn(size).unpack(template).first
|
112
|
+
end
|
113
|
+
|
114
|
+
# same as method +ru+, but swap bytes if native byteorder == _byteorder_
|
115
|
+
def ru_swap(size, template, byteorder)
|
116
|
+
str = readn(size)
|
117
|
+
str.reverse! if ByteOrder.byteorder == byteorder
|
118
|
+
str.unpack(template).first
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
require 'byteorder'
|
2
|
+
|
3
|
+
module BinaryWriterMixin
|
4
|
+
|
5
|
+
# == 8 bit
|
6
|
+
|
7
|
+
# no byteorder for 8 bit!
|
8
|
+
|
9
|
+
def write_word8(val)
|
10
|
+
pw(val, 'C')
|
11
|
+
end
|
12
|
+
|
13
|
+
def write_int8(val)
|
14
|
+
pw(val, 'c')
|
15
|
+
end
|
16
|
+
|
17
|
+
alias write_byte write_word8
|
18
|
+
|
19
|
+
# == 16 bit
|
20
|
+
|
21
|
+
# === Unsigned
|
22
|
+
|
23
|
+
def write_word16_native(val)
|
24
|
+
pw(val, 'S')
|
25
|
+
end
|
26
|
+
|
27
|
+
def write_word16_little(val)
|
28
|
+
str = [val].pack('S')
|
29
|
+
str.reverse! if ByteOrder.network? # swap bytes as native=network (and we want little)
|
30
|
+
write(str)
|
31
|
+
end
|
32
|
+
|
33
|
+
def write_word16_network(val)
|
34
|
+
str = [val].pack('S')
|
35
|
+
str.reverse! if ByteOrder.little? # swap bytes as native=little (and we want network)
|
36
|
+
write(str)
|
37
|
+
end
|
38
|
+
|
39
|
+
# === Signed
|
40
|
+
|
41
|
+
def write_int16_native(val)
|
42
|
+
pw(val, 's')
|
43
|
+
end
|
44
|
+
|
45
|
+
def write_int16_little(val)
|
46
|
+
pw(val, 'v')
|
47
|
+
end
|
48
|
+
|
49
|
+
def write_int16_network(val)
|
50
|
+
pw(val, 'n')
|
51
|
+
end
|
52
|
+
|
53
|
+
# == 32 bit
|
54
|
+
|
55
|
+
# === Unsigned
|
56
|
+
|
57
|
+
def write_word32_native(val)
|
58
|
+
pw(val, 'L')
|
59
|
+
end
|
60
|
+
|
61
|
+
def write_word32_little(val)
|
62
|
+
str = [val].pack('L')
|
63
|
+
str.reverse! if ByteOrder.network? # swap bytes as native=network (and we want little)
|
64
|
+
write(str)
|
65
|
+
end
|
66
|
+
|
67
|
+
def write_word32_network(val)
|
68
|
+
str = [val].pack('L')
|
69
|
+
str.reverse! if ByteOrder.little? # swap bytes as native=little (and we want network)
|
70
|
+
write(str)
|
71
|
+
end
|
72
|
+
|
73
|
+
# === Signed
|
74
|
+
|
75
|
+
def write_int32_native(val)
|
76
|
+
pw(val, 'l')
|
77
|
+
end
|
78
|
+
|
79
|
+
def write_int32_little(val)
|
80
|
+
pw(val, 'V')
|
81
|
+
end
|
82
|
+
|
83
|
+
def write_int32_network(val)
|
84
|
+
pw(val, 'N')
|
85
|
+
end
|
86
|
+
|
87
|
+
# add some short-cut functions
|
88
|
+
%w(word16 int16 word32 int32).each do |typ|
|
89
|
+
alias_method "write_#{typ}_big", "write_#{typ}_network"
|
90
|
+
end
|
91
|
+
|
92
|
+
# == Other methods
|
93
|
+
|
94
|
+
private
|
95
|
+
|
96
|
+
# shortcut for pack and write
|
97
|
+
def pw(val, template)
|
98
|
+
write([val].pack(template))
|
99
|
+
end
|
100
|
+
end
|
data/lib/buffer.rb
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
require 'binary_writer'
|
2
|
+
require 'binary_reader'
|
3
|
+
|
4
|
+
# Fixed size buffer.
|
5
|
+
class Buffer
|
6
|
+
|
7
|
+
class Error < RuntimeError; end
|
8
|
+
class EOF < Error; end
|
9
|
+
|
10
|
+
def initialize(size)
|
11
|
+
raise ArgumentError if size < 0
|
12
|
+
|
13
|
+
@size = size
|
14
|
+
@position = 0
|
15
|
+
@content = "#" * @size
|
16
|
+
end
|
17
|
+
|
18
|
+
def size
|
19
|
+
@size
|
20
|
+
end
|
21
|
+
|
22
|
+
def position
|
23
|
+
@position
|
24
|
+
end
|
25
|
+
|
26
|
+
def position=(new_pos)
|
27
|
+
raise ArgumentError if new_pos < 0 or new_pos > @size
|
28
|
+
@position = new_pos
|
29
|
+
end
|
30
|
+
|
31
|
+
def at_end?
|
32
|
+
@position == @size
|
33
|
+
end
|
34
|
+
|
35
|
+
def content
|
36
|
+
@content
|
37
|
+
end
|
38
|
+
|
39
|
+
def read(n)
|
40
|
+
raise EOF, 'cannot read beyond the end of buffer' if @position + n > @size
|
41
|
+
str = @content[@position, n]
|
42
|
+
@position += n
|
43
|
+
str
|
44
|
+
end
|
45
|
+
|
46
|
+
def write(str)
|
47
|
+
sz = str.size
|
48
|
+
raise EOF, 'cannot write beyond the end of buffer' if @position + sz > @size
|
49
|
+
@content[@position, sz] = str
|
50
|
+
@position += sz
|
51
|
+
self
|
52
|
+
end
|
53
|
+
|
54
|
+
def copy_from_stream(stream, n)
|
55
|
+
raise ArgumentError if n < 0
|
56
|
+
while n > 0
|
57
|
+
str = stream.read(n)
|
58
|
+
write(str)
|
59
|
+
n -= str.size
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def write_cstring(cstr)
|
64
|
+
raise ArgumentError, "Invalid Ruby/cstring" if cstr.include?("\000")
|
65
|
+
write(cstr)
|
66
|
+
write("\000")
|
67
|
+
end
|
68
|
+
|
69
|
+
# returns a Ruby string without the trailing NUL character
|
70
|
+
def read_cstring
|
71
|
+
nul_pos = @content.index(0, @position)
|
72
|
+
raise Error, "no cstring found!" unless nul_pos
|
73
|
+
|
74
|
+
sz = nul_pos - @position
|
75
|
+
str = @content[@position, sz]
|
76
|
+
@position += sz + 1
|
77
|
+
return str
|
78
|
+
end
|
79
|
+
|
80
|
+
# read till the end of the buffer
|
81
|
+
def read_rest
|
82
|
+
read(self.size-@position)
|
83
|
+
end
|
84
|
+
|
85
|
+
include BinaryWriterMixin
|
86
|
+
include BinaryReaderMixin
|
87
|
+
end
|
data/lib/byteorder.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
module ByteOrder
|
2
|
+
Native = :Native
|
3
|
+
BigEndian = Big = Network = :BigEndian
|
4
|
+
LittleEndian = Little = :LittleEndian
|
5
|
+
|
6
|
+
# examines the byte order of the underlying machine
|
7
|
+
def byte_order
|
8
|
+
if [0x12345678].pack("L") == "\x12\x34\x56\x78"
|
9
|
+
BigEndian
|
10
|
+
else
|
11
|
+
LittleEndian
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
alias byteorder byte_order
|
16
|
+
|
17
|
+
def little_endian?
|
18
|
+
byte_order == LittleEndian
|
19
|
+
end
|
20
|
+
|
21
|
+
def big_endian?
|
22
|
+
byte_order == BigEndian
|
23
|
+
end
|
24
|
+
|
25
|
+
alias little? little_endian?
|
26
|
+
alias big? big_endian?
|
27
|
+
alias network? big_endian?
|
28
|
+
|
29
|
+
module_function :byte_order, :byteorder
|
30
|
+
module_function :little_endian?, :little?
|
31
|
+
module_function :big_endian?, :big?, :network?
|
32
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
#
|
2
|
+
# Author:: Michael Neumann
|
3
|
+
# Copyright:: (c) 2004 by Michael Neumann
|
4
|
+
#
|
5
|
+
|
6
|
+
require 'postgres-pr/message'
|
7
|
+
require 'uri'
|
8
|
+
require 'socket'
|
9
|
+
require 'thread'
|
10
|
+
|
11
|
+
PROTO_VERSION = 196608
|
12
|
+
|
13
|
+
class Connection
|
14
|
+
|
15
|
+
# sync
|
16
|
+
|
17
|
+
def initialize(database, user, auth=nil, uri = "unix:/tmp/.s.PGSQL.5432")
|
18
|
+
raise unless @mutex.nil?
|
19
|
+
|
20
|
+
@mutex = Mutex.new
|
21
|
+
|
22
|
+
@mutex.synchronize {
|
23
|
+
@params = {}
|
24
|
+
establish_connection(uri)
|
25
|
+
|
26
|
+
@conn << StartupMessage.new(PROTO_VERSION, 'user' => user, 'database' => database).dump
|
27
|
+
|
28
|
+
loop do
|
29
|
+
msg = Message.read(@conn)
|
30
|
+
case msg
|
31
|
+
when AuthentificationOk
|
32
|
+
when ErrorResponse
|
33
|
+
raise
|
34
|
+
when NoticeResponse
|
35
|
+
# TODO
|
36
|
+
when ParameterStatus
|
37
|
+
@params[msg.key] = msg.value
|
38
|
+
when BackendKeyData
|
39
|
+
# TODO
|
40
|
+
#p msg
|
41
|
+
when ReadyForQuery
|
42
|
+
# TODO: use transaction status
|
43
|
+
break
|
44
|
+
else
|
45
|
+
raise "unhandled message type"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
}
|
49
|
+
end
|
50
|
+
|
51
|
+
def query(sql)
|
52
|
+
@mutex.synchronize {
|
53
|
+
@conn << Query.dump(sql)
|
54
|
+
|
55
|
+
rows = []
|
56
|
+
|
57
|
+
loop do
|
58
|
+
msg = Message.read(@conn)
|
59
|
+
case msg
|
60
|
+
when DataRow
|
61
|
+
rows << msg.columns
|
62
|
+
when CommandComplete
|
63
|
+
when ReadyForQuery
|
64
|
+
break
|
65
|
+
when RowDescription
|
66
|
+
# TODO
|
67
|
+
when CopyInResponse
|
68
|
+
when CopyOutResponse
|
69
|
+
when EmptyQueryResponse
|
70
|
+
when ErrorResponse
|
71
|
+
p msg
|
72
|
+
raise
|
73
|
+
when NoticeResponse
|
74
|
+
# TODO
|
75
|
+
else
|
76
|
+
raise
|
77
|
+
end
|
78
|
+
end
|
79
|
+
rows
|
80
|
+
}
|
81
|
+
end
|
82
|
+
|
83
|
+
DEFAULT_PORT = 5432
|
84
|
+
DEFAULT_HOST = 'localhost'
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
# tcp://localhost:5432
|
89
|
+
# unix:/tmp/.s.PGSQL.5432
|
90
|
+
def establish_connection(uri)
|
91
|
+
u = URI.parse(uri)
|
92
|
+
case u.scheme
|
93
|
+
when 'tcp'
|
94
|
+
@conn = TCPSocket.new(u.host || DEFAULT_HOST, u.port || DEFAULT_PORT)
|
95
|
+
when 'unix'
|
96
|
+
@conn = UNIXSocket.new(u.path)
|
97
|
+
else
|
98
|
+
raise 'unrecognized uri scheme format (must be tcp or unix)'
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,473 @@
|
|
1
|
+
#
|
2
|
+
# Author:: Michael Neumann
|
3
|
+
# Copyright:: (c) 2004 by Michael Neumann
|
4
|
+
#
|
5
|
+
|
6
|
+
require 'buffer'
|
7
|
+
require 'readbytes'
|
8
|
+
|
9
|
+
class ParseError < RuntimeError; end
|
10
|
+
class DumpError < RuntimeError; end
|
11
|
+
|
12
|
+
|
13
|
+
# Base class representing a PostgreSQL protocol message
|
14
|
+
class Message
|
15
|
+
# One character message-typecode to class map
|
16
|
+
MsgTypeMap = Hash.new { UnknownMessageType }
|
17
|
+
|
18
|
+
def self.register_message_type(type)
|
19
|
+
raise ArgumentError if type < 0 or type > 255
|
20
|
+
raise "duplicate message type registration" if MsgTypeMap.has_key? type
|
21
|
+
|
22
|
+
MsgTypeMap[type] = self
|
23
|
+
|
24
|
+
self.const_set(:MsgType, type)
|
25
|
+
class_eval "def message_type; MsgType end"
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.read(stream, startup=false)
|
29
|
+
type = stream.readbytes(1).unpack('C').first unless startup
|
30
|
+
length = stream.readbytes(4).unpack('N').first # FIXME: length should be signed, not unsigned
|
31
|
+
|
32
|
+
raise ParseError unless length >= 4
|
33
|
+
|
34
|
+
# initialize buffer
|
35
|
+
buffer = Buffer.new(startup ? length : 1+length)
|
36
|
+
buffer.write_byte(type) unless startup
|
37
|
+
buffer.write_int32_network(length)
|
38
|
+
buffer.copy_from_stream(stream, length-4)
|
39
|
+
|
40
|
+
(startup ? StartupMessage : MsgTypeMap[type]).create(buffer)
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.create(buffer)
|
44
|
+
obj = allocate
|
45
|
+
obj.parse(buffer)
|
46
|
+
obj
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.dump(*args)
|
50
|
+
new(*args).dump
|
51
|
+
end
|
52
|
+
|
53
|
+
def dump(body_size=0)
|
54
|
+
buffer = Buffer.new(5 + body_size)
|
55
|
+
buffer.write_byte(self.message_type)
|
56
|
+
buffer.write_int32_network(4 + body_size)
|
57
|
+
yield buffer if block_given?
|
58
|
+
raise DumpError unless buffer.at_end?
|
59
|
+
return buffer.content
|
60
|
+
end
|
61
|
+
|
62
|
+
def parse(buffer)
|
63
|
+
buffer.position = 5
|
64
|
+
yield buffer if block_given?
|
65
|
+
raise ParseError, buffer.inspect unless buffer.at_end?
|
66
|
+
end
|
67
|
+
|
68
|
+
def self.fields(*attribs)
|
69
|
+
names = attribs.map {|name, type| name.to_s}
|
70
|
+
arg_list = names.join(", ")
|
71
|
+
ivar_list = names.map {|name| "@" + name }.join(", ")
|
72
|
+
sym_list = names.map {|name| ":" + name }.join(", ")
|
73
|
+
class_eval %[
|
74
|
+
attr_accessor #{ sym_list }
|
75
|
+
def initialize(#{ arg_list })
|
76
|
+
#{ ivar_list } = #{ arg_list }
|
77
|
+
end
|
78
|
+
]
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
class UnknownMessageType < Message
|
83
|
+
def dump
|
84
|
+
raise
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
class Authentification < Message
|
89
|
+
register_message_type ?R
|
90
|
+
|
91
|
+
AuthTypeMap = Hash.new { UnknownAuthType }
|
92
|
+
|
93
|
+
def self.create(buffer)
|
94
|
+
buffer.position = 5
|
95
|
+
authtype = buffer.read_int32_network
|
96
|
+
klass = AuthTypeMap[authtype]
|
97
|
+
obj = klass.allocate
|
98
|
+
obj.parse(buffer)
|
99
|
+
obj
|
100
|
+
end
|
101
|
+
|
102
|
+
def self.register_auth_type(type)
|
103
|
+
raise "duplicate auth type registration" if AuthTypeMap.has_key?(type)
|
104
|
+
AuthTypeMap[type] = self
|
105
|
+
self.const_set(:AuthType, type)
|
106
|
+
class_eval "def auth_type() AuthType end"
|
107
|
+
end
|
108
|
+
|
109
|
+
# the dump method of class Message
|
110
|
+
alias message__dump dump
|
111
|
+
|
112
|
+
def dump
|
113
|
+
super(4) do |buffer|
|
114
|
+
buffer.write_int32_network(self.auth_type)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def parse(buffer)
|
119
|
+
super do
|
120
|
+
auth_t = buffer.read_int32_network
|
121
|
+
raise ParseError unless auth_t == self.auth_type
|
122
|
+
yield if block_given?
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
class UnknownAuthType < Authentification
|
128
|
+
end
|
129
|
+
|
130
|
+
class AuthentificationOk < Authentification
|
131
|
+
register_auth_type 0
|
132
|
+
end
|
133
|
+
|
134
|
+
class AuthentificationKerberosV4 < Authentification
|
135
|
+
register_auth_type 1
|
136
|
+
end
|
137
|
+
|
138
|
+
class AuthentificationKerberosV5 < Authentification
|
139
|
+
register_auth_type 2
|
140
|
+
end
|
141
|
+
|
142
|
+
class AuthentificationClearTextPassword < Authentification
|
143
|
+
register_auth_type 3
|
144
|
+
end
|
145
|
+
|
146
|
+
module SaltedAuthentificationMixin
|
147
|
+
attr_accessor :salt
|
148
|
+
|
149
|
+
def initialize(salt)
|
150
|
+
@salt = salt
|
151
|
+
end
|
152
|
+
|
153
|
+
def dump
|
154
|
+
raise DumpError unless @salt.size == self.salt_size
|
155
|
+
|
156
|
+
message__dump(4 + self.salt_size) do |buffer|
|
157
|
+
buffer.write_int32_network(self.auth_type)
|
158
|
+
buffer.write(@salt)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def parse(buffer)
|
163
|
+
super do
|
164
|
+
@salt = buffer.read(self.salt_size)
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
class AuthentificationCryptPassword < Authentification
|
170
|
+
register_auth_type 4
|
171
|
+
include SaltedAuthentificationMixin
|
172
|
+
def salt_size; 2 end
|
173
|
+
end
|
174
|
+
|
175
|
+
|
176
|
+
class AuthentificationMD5Password < Authentification
|
177
|
+
register_auth_type 5
|
178
|
+
include SaltedAuthentificationMixin
|
179
|
+
def salt_size; 4 end
|
180
|
+
end
|
181
|
+
|
182
|
+
class AuthentificationSCMCredential < Authentification
|
183
|
+
register_auth_type 6
|
184
|
+
end
|
185
|
+
|
186
|
+
class ParameterStatus < Message
|
187
|
+
register_message_type ?S
|
188
|
+
fields :key, :value
|
189
|
+
|
190
|
+
def dump
|
191
|
+
super(@key.size + 1 + @value.size + 1) do |buffer|
|
192
|
+
buffer.write_cstring(@key)
|
193
|
+
buffer.write_cstring(@value)
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
def parse(buffer)
|
198
|
+
super do
|
199
|
+
@key = buffer.read_cstring
|
200
|
+
@value = buffer.read_cstring
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
class BackendKeyData < Message
|
206
|
+
register_message_type ?K
|
207
|
+
fields :process_id, :secret_key
|
208
|
+
|
209
|
+
def dump
|
210
|
+
super(4 + 4) do |buffer|
|
211
|
+
buffer.write_int32_network(@process_id)
|
212
|
+
buffer.write_int32_network(@secret_key)
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
def parse(buffer)
|
217
|
+
super do
|
218
|
+
@process_id = buffer.read_int32_network
|
219
|
+
@secret_key = buffer.read_int32_network
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
class ReadyForQuery < Message
|
225
|
+
register_message_type ?Z
|
226
|
+
fields :backend_transaction_status_indicator
|
227
|
+
|
228
|
+
def dump
|
229
|
+
super(1) do |buffer|
|
230
|
+
buffer.write_byte(@backend_transaction_status_indicator)
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
def parse(buffer)
|
235
|
+
super do
|
236
|
+
@backend_transaction_status_indicator = buffer.read_byte
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
class DataRow < Message
|
242
|
+
register_message_type ?D
|
243
|
+
fields :columns
|
244
|
+
|
245
|
+
def dump
|
246
|
+
sz = @columns.inject(2) {|sum, col| sum + 4 + (col ? col.size : 0)}
|
247
|
+
super(sz) do |buffer|
|
248
|
+
buffer.write_int16_network(@columns.size)
|
249
|
+
@columns.each {|col|
|
250
|
+
buffer.write_int32_network(col ? col.size : -1)
|
251
|
+
buffer.write(col) if col
|
252
|
+
}
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
def parse(buffer)
|
257
|
+
super do
|
258
|
+
n_cols = buffer.read_int16_network
|
259
|
+
@columns = (1..n_cols).collect {
|
260
|
+
len = buffer.read_int32_network
|
261
|
+
if len == -1
|
262
|
+
nil
|
263
|
+
else
|
264
|
+
buffer.read(len)
|
265
|
+
end
|
266
|
+
}
|
267
|
+
end
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
class CommandComplete < Message
|
272
|
+
register_message_type ?C
|
273
|
+
fields :cmd_tag
|
274
|
+
|
275
|
+
def dump
|
276
|
+
super(@cmd_tag.size + 1) do |buffer|
|
277
|
+
buffer.write_cstring(@cmd_tag)
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
def parse(buffer)
|
282
|
+
super do
|
283
|
+
@cmd_tag = buffer.read_cstring
|
284
|
+
end
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
class EmptyQueryResponse < Message
|
289
|
+
register_message_type ?I
|
290
|
+
end
|
291
|
+
|
292
|
+
module NoticeErrorMixin
|
293
|
+
attr_accessor :field_type, :field_values
|
294
|
+
|
295
|
+
def initialize(field_type=0, field_values=[])
|
296
|
+
raise ArgumentError if field_type == 0 and not field_values.empty?
|
297
|
+
@field_type, @field_values = field_type, field_values
|
298
|
+
end
|
299
|
+
|
300
|
+
def dump
|
301
|
+
raise ArgumentError if @field_type == 0 and not @field_values.empty?
|
302
|
+
|
303
|
+
sz = 1
|
304
|
+
sz += @field_values.inject(1) {|sum, fld| sum + fld.size + 1} unless @field_type == 0
|
305
|
+
|
306
|
+
super(sz) do |buffer|
|
307
|
+
buffer.write_byte(@field_type)
|
308
|
+
break if @field_type == 0
|
309
|
+
@field_values.each {|fld| buffer.write_cstring(fld) }
|
310
|
+
buffer.write_byte(0)
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
def parse(buffer)
|
315
|
+
super do
|
316
|
+
@field_type = buffer.read_byte
|
317
|
+
break if @field_type == 0
|
318
|
+
@field_values = []
|
319
|
+
while buffer.position < buffer.size-1
|
320
|
+
@field_values << buffer.read_cstring
|
321
|
+
end
|
322
|
+
terminator = buffer.read_byte
|
323
|
+
raise ParseError unless terminator == 0
|
324
|
+
end
|
325
|
+
end
|
326
|
+
end
|
327
|
+
|
328
|
+
class NoticeResponse < Message
|
329
|
+
register_message_type ?N
|
330
|
+
include NoticeErrorMixin
|
331
|
+
end
|
332
|
+
|
333
|
+
class ErrorResponse < Message
|
334
|
+
register_message_type ?E
|
335
|
+
include NoticeErrorMixin
|
336
|
+
end
|
337
|
+
|
338
|
+
# TODO
|
339
|
+
class CopyInResponse < Message
|
340
|
+
register_message_type ?G
|
341
|
+
end
|
342
|
+
|
343
|
+
# TODO
|
344
|
+
class CopyOutResponse < Message
|
345
|
+
register_message_type ?H
|
346
|
+
end
|
347
|
+
|
348
|
+
class Parse < Message
|
349
|
+
register_message_type ?P
|
350
|
+
fields :query, :stmt_name, :parameter_oids
|
351
|
+
|
352
|
+
def initialize(query, stmt_name="", parameter_oids=[])
|
353
|
+
@query, @stmt_name, @parameter_oids = query, stmt_name, parameter_oids
|
354
|
+
end
|
355
|
+
|
356
|
+
def dump
|
357
|
+
sz = @stmt_name.size + 1 + @query.size + 1 + 2 + (4 * @parameter_oids.size)
|
358
|
+
super(sz) do |buffer|
|
359
|
+
buffer.write_cstring(@stmt_name)
|
360
|
+
buffer.write_cstring(@query)
|
361
|
+
buffer.write_int16_network(@parameter_oids.size)
|
362
|
+
@parameter_oids.each {|oid| buffer.write_int32_network(oid) }
|
363
|
+
end
|
364
|
+
end
|
365
|
+
|
366
|
+
def parse(buffer)
|
367
|
+
super do
|
368
|
+
@stmt_name = buffer.read_cstring
|
369
|
+
@query = buffer.read_cstring
|
370
|
+
n_oids = buffer.read_int16_network
|
371
|
+
@parameter_oids = (1..n_oids).collect {
|
372
|
+
# TODO: zero means unspecified. map to nil?
|
373
|
+
buffer.read_int32_network
|
374
|
+
}
|
375
|
+
end
|
376
|
+
end
|
377
|
+
end
|
378
|
+
|
379
|
+
class ParseComplete < Message
|
380
|
+
register_message_type ?1
|
381
|
+
end
|
382
|
+
|
383
|
+
class Query < Message
|
384
|
+
register_message_type ?Q
|
385
|
+
fields :query
|
386
|
+
|
387
|
+
def dump
|
388
|
+
super(@query.size + 1) do |buffer|
|
389
|
+
buffer.write_cstring(@query)
|
390
|
+
end
|
391
|
+
end
|
392
|
+
|
393
|
+
def parse(buffer)
|
394
|
+
super do
|
395
|
+
@query = buffer.read_cstring
|
396
|
+
end
|
397
|
+
end
|
398
|
+
end
|
399
|
+
|
400
|
+
class RowDescription < Message
|
401
|
+
register_message_type ?T
|
402
|
+
|
403
|
+
class FieldInfo < Struct.new(:name, :oid, :attr_nr, :type_oid, :typlen, :atttypmod, :formatcode); end
|
404
|
+
|
405
|
+
def dump
|
406
|
+
sz = @fields.inject(2) {|sum, fld| sum + 18 + fld.name.size + 1 }
|
407
|
+
super(sz) do |buffer|
|
408
|
+
buffer.write_int16_network(@fields.size)
|
409
|
+
@fields.each { |f|
|
410
|
+
buffer.write_cstring(f.name)
|
411
|
+
buffer.write_int32_network(f.oid)
|
412
|
+
buffer.write_int16_network(f.attr_nr)
|
413
|
+
buffer.write_int32_network(f.type_oid)
|
414
|
+
buffer.write_int16_network(f.typlen)
|
415
|
+
buffer.write_int32_network(f.atttypmod)
|
416
|
+
buffer.write_int16_network(f.formatcode)
|
417
|
+
}
|
418
|
+
end
|
419
|
+
end
|
420
|
+
|
421
|
+
def parse(buffer)
|
422
|
+
super do
|
423
|
+
n_fields = buffer.read_int16_network
|
424
|
+
@fields = (1..n_fields).collect {
|
425
|
+
f = FieldInfo.new
|
426
|
+
f.name = buffer.read_cstring
|
427
|
+
f.oid = buffer.read_int32_network
|
428
|
+
f.attr_nr = buffer.read_int16_network
|
429
|
+
f.type_oid = buffer.read_int32_network
|
430
|
+
f.typlen = buffer.read_int16_network
|
431
|
+
f.atttypmod = buffer.read_int32_network
|
432
|
+
f.formatcode = buffer.read_int16_network
|
433
|
+
}
|
434
|
+
end
|
435
|
+
end
|
436
|
+
end
|
437
|
+
|
438
|
+
class StartupMessage < Message
|
439
|
+
fields :proto_version, :params
|
440
|
+
|
441
|
+
def dump
|
442
|
+
sz = @params.inject(4 + 4) {|sum, kv| sum + kv[0].size + 1 + kv[1].size + 1} + 1
|
443
|
+
|
444
|
+
buffer = Buffer.new(sz)
|
445
|
+
buffer.write_int32_network(sz)
|
446
|
+
buffer.write_int32_network(@proto_version)
|
447
|
+
@params.each_pair {|key, value|
|
448
|
+
buffer.write_cstring(key)
|
449
|
+
buffer.write_cstring(value)
|
450
|
+
}
|
451
|
+
buffer.write_byte(0)
|
452
|
+
|
453
|
+
raise DumpError unless buffer.at_end?
|
454
|
+
return buffer.content
|
455
|
+
end
|
456
|
+
|
457
|
+
def parse(buffer)
|
458
|
+
buffer.position = 4
|
459
|
+
|
460
|
+
@proto_version = buffer.read_int32_network
|
461
|
+
@params = {}
|
462
|
+
|
463
|
+
while buffer.position < buffer.size-1
|
464
|
+
key = buffer.read_cstring
|
465
|
+
val = buffer.read_cstring
|
466
|
+
@params[key] = val
|
467
|
+
end
|
468
|
+
|
469
|
+
nul = buffer.read_byte
|
470
|
+
raise ParseError unless nul == 0
|
471
|
+
raise ParseError unless buffer.at_end?
|
472
|
+
end
|
473
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'conv'
|
3
|
+
require 'array'
|
4
|
+
require 'bytea'
|
5
|
+
|
6
|
+
class TC_Conversion < Test::Unit::TestCase
|
7
|
+
def test_decode_array
|
8
|
+
assert_equal ["abcdef ", "hallo", ["1", "2"]], decode_array("{ abcdef , hallo, { 1, 2} }")
|
9
|
+
assert_equal [""], decode_array("{ }") # TODO: Correct?
|
10
|
+
assert_equal [], decode_array("{}")
|
11
|
+
assert_equal ["hallo", ""], decode_array("{hallo,}")
|
12
|
+
end
|
13
|
+
|
14
|
+
def test_bytea
|
15
|
+
end
|
16
|
+
|
17
|
+
include Postgres::Conversion
|
18
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'strscan'
|
2
|
+
|
3
|
+
module Postgres::Conversion
|
4
|
+
|
5
|
+
def decode_array(str, delim=',', &conv_proc)
|
6
|
+
delim = Regexp.escape(delim)
|
7
|
+
buf = StringScanner.new(str)
|
8
|
+
return parse_arr(buf, delim, &conv_proc)
|
9
|
+
ensure
|
10
|
+
raise ConversionError, "end of string expected (#{buf.rest})" unless buf.empty?
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def parse_arr(buf, delim, &conv_proc)
|
16
|
+
# skip whitespace
|
17
|
+
buf.skip(/\s*/)
|
18
|
+
|
19
|
+
raise ConversionError, "'{' expected" unless buf.get_byte == '{'
|
20
|
+
|
21
|
+
elems = []
|
22
|
+
unless buf.scan(/\}/) # array is not empty
|
23
|
+
loop do
|
24
|
+
# skip whitespace
|
25
|
+
buf.skip(/\s+/)
|
26
|
+
|
27
|
+
elems <<
|
28
|
+
if buf.check(/\{/)
|
29
|
+
parse_arr(buf, delim, &conv_proc)
|
30
|
+
else
|
31
|
+
e = buf.scan(/("((\\.)|[^"])*"|\\.|[^\}#{ delim }])*/) || raise(ConversionError)
|
32
|
+
if conv_proc then conv_proc.call(e) else e end
|
33
|
+
end
|
34
|
+
|
35
|
+
break if buf.scan(/\}/)
|
36
|
+
break unless buf.scan(/#{ delim }/)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# skip whitespace
|
41
|
+
buf.skip(/\s*/)
|
42
|
+
|
43
|
+
elems
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Postgres::Conversion
|
2
|
+
|
3
|
+
#
|
4
|
+
# Encodes a string as bytea value.
|
5
|
+
#
|
6
|
+
# for encoding rules see:
|
7
|
+
# http://www.postgresql.org/docs/7.4/static/datatype-binary.html
|
8
|
+
#
|
9
|
+
|
10
|
+
def encode_bytea(str)
|
11
|
+
str.gsub(/[\000-\037\047\134\177-\377]/) {|b| "\\#{ b[0].to_s(8).rjust(3, '0') }" }
|
12
|
+
end
|
13
|
+
|
14
|
+
#
|
15
|
+
# Decodes a bytea encoded string.
|
16
|
+
#
|
17
|
+
# for decoding rules see:
|
18
|
+
# http://www.postgresql.org/docs/7.4/static/datatype-binary.html
|
19
|
+
#
|
20
|
+
def decode_bytea(str)
|
21
|
+
str.gsub(/\\(\\|'|[0-3][0-7][0-7])/) {|s|
|
22
|
+
if s.size == 2 then s[1,1] else s[1,3].oct.chr end
|
23
|
+
}
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
data/test/TC_message.rb
ADDED
@@ -0,0 +1,101 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'stringio'
|
3
|
+
|
4
|
+
class Module
|
5
|
+
def attr_accessor(*attrs)
|
6
|
+
@@attrs = [] unless defined?(@@attrs)
|
7
|
+
@@attrs += attrs
|
8
|
+
|
9
|
+
x = @@attrs.map {|a| "self.#{a} == o.#{a}"}.join(" && ")
|
10
|
+
class_eval %{
|
11
|
+
def ==(o)
|
12
|
+
#{ x }
|
13
|
+
end
|
14
|
+
}
|
15
|
+
|
16
|
+
@@attrs.each do |a|
|
17
|
+
class_eval %{
|
18
|
+
def #{a}() @#{a} end
|
19
|
+
def #{a}=(v) @#{a}=v end
|
20
|
+
}
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
$LOAD_PATH.unshift '../lib'
|
26
|
+
require 'postgres-pr/message'
|
27
|
+
class Buffer
|
28
|
+
alias old_content content
|
29
|
+
def content
|
30
|
+
self
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class Message
|
35
|
+
attr_accessor :buffer
|
36
|
+
|
37
|
+
class << self
|
38
|
+
alias old_create create
|
39
|
+
def create(buffer)
|
40
|
+
obj = old_create(buffer)
|
41
|
+
obj.buffer = buffer
|
42
|
+
obj
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
alias old_dump dump
|
47
|
+
|
48
|
+
def dump(body_size=0, &block)
|
49
|
+
buf = old_dump(body_size, &block)
|
50
|
+
self.buffer = buf
|
51
|
+
buf.old_content
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
class StartupMessage
|
56
|
+
alias old_dump dump
|
57
|
+
def dump
|
58
|
+
buf = old_dump
|
59
|
+
self.buffer = buf
|
60
|
+
buf.old_content
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
class StringIO
|
65
|
+
alias readbytes read
|
66
|
+
end
|
67
|
+
|
68
|
+
class TC_Message < Test::Unit::TestCase
|
69
|
+
|
70
|
+
CASES = [
|
71
|
+
#[AuthentificationOk],
|
72
|
+
#[ErrorResponse],
|
73
|
+
[ParameterStatus, "key", "value"],
|
74
|
+
[BackendKeyData, 234234234, 213434],
|
75
|
+
[ReadyForQuery, ?T],
|
76
|
+
# TODO: RowDescription
|
77
|
+
[DataRow, ["a", "bbbbbb", "ccc", nil, nil, "ddddd", "e" * 10_000]],
|
78
|
+
[DataRow, []],
|
79
|
+
[CommandComplete, "INSERT"],
|
80
|
+
[StartupMessage, 196608, {"user" => "mneumann", "database" => "mneumann"}],
|
81
|
+
[Parse, "INSERT INTO blah values (?, ?)", ""],
|
82
|
+
[Query, "SELECT * FROM test\nWHERE a='test'"]
|
83
|
+
]
|
84
|
+
|
85
|
+
def test_pack_unpack_feature
|
86
|
+
assert_equal ['a', 'b'], "a\000b\000".unpack('Z*Z*')
|
87
|
+
end
|
88
|
+
|
89
|
+
def test_marshal_unmarshal
|
90
|
+
CASES.each do |klass, *params|
|
91
|
+
msg = klass.new(*params)
|
92
|
+
new_msg = Message.read(StringIO.new(msg.dump), klass == StartupMessage)
|
93
|
+
assert_equal(msg, new_msg)
|
94
|
+
|
95
|
+
msg1, msg2 = klass.new(*params), klass.new(*params)
|
96
|
+
msg1.dump
|
97
|
+
msg2.dump; msg2.parse(msg2.buffer)
|
98
|
+
assert_equal(msg1, msg2)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
metadata
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
rubygems_version: 0.8.1
|
3
|
+
specification_version: 1
|
4
|
+
name: postgres-pr
|
5
|
+
version: !ruby/object:Gem::Version
|
6
|
+
version: 0.0.1
|
7
|
+
date: 2004-11-18
|
8
|
+
summary: A pure Ruby interface to the PostgreSQL database
|
9
|
+
require_paths:
|
10
|
+
- lib
|
11
|
+
author: Michael Neumann
|
12
|
+
email: mneumann@ntecs.de
|
13
|
+
homepage: ruby-dbi.rubyforge.org
|
14
|
+
rubyforge_project: ruby-dbi
|
15
|
+
description:
|
16
|
+
autorequire:
|
17
|
+
default_executable:
|
18
|
+
bindir: bin
|
19
|
+
has_rdoc: false
|
20
|
+
required_ruby_version: !ruby/object:Gem::Version::Requirement
|
21
|
+
requirements:
|
22
|
+
-
|
23
|
+
- ">"
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: 0.0.0
|
26
|
+
version:
|
27
|
+
platform: ruby
|
28
|
+
files:
|
29
|
+
- lib/postgres-pr
|
30
|
+
- lib/binary_writer.rb
|
31
|
+
- lib/byteorder.rb
|
32
|
+
- lib/binary_reader.rb
|
33
|
+
- lib/buffer.rb
|
34
|
+
- lib/postgres-pr/typeconv
|
35
|
+
- lib/postgres-pr/connection.rb
|
36
|
+
- lib/postgres-pr/message.rb
|
37
|
+
- lib/postgres-pr/typeconv/array.rb
|
38
|
+
- lib/postgres-pr/typeconv/bytea.rb
|
39
|
+
- lib/postgres-pr/typeconv/conv.rb
|
40
|
+
- lib/postgres-pr/typeconv/TC_conv.rb
|
41
|
+
- test/TC_message.rb
|
42
|
+
- examples/client.rb
|
43
|
+
- examples/server.rb
|
44
|
+
- examples/test_connection.rb
|
45
|
+
test_files: []
|
46
|
+
rdoc_options: []
|
47
|
+
extra_rdoc_files: []
|
48
|
+
executables: []
|
49
|
+
extensions: []
|
50
|
+
requirements: []
|
51
|
+
dependencies: []
|