├── lib ├── postgres-pr │ ├── version.rb │ ├── typeconv │ │ ├── conv.rb │ │ ├── TC_conv.rb │ │ ├── bytea.rb │ │ └── array.rb │ ├── postgres-compat.rb │ ├── connection.rb │ └── message.rb ├── postgres.rb ├── byteorder.rb ├── buffer.rb ├── binary_writer.rb └── binary_reader.rb ├── examples ├── server.rb ├── test_connection.rb ├── client.rb └── og │ └── test.rb ├── TODO ├── postgres-pr.gemspec ├── README └── test └── TC_message.rb /lib/postgres-pr/version.rb: -------------------------------------------------------------------------------- 1 | module PostgresPR 2 | Version = "0.6.0" 3 | end 4 | -------------------------------------------------------------------------------- /lib/postgres-pr/typeconv/conv.rb: -------------------------------------------------------------------------------- 1 | module Postgres 2 | module Conversion 3 | class ConversionError < Exception; end 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /lib/postgres.rb: -------------------------------------------------------------------------------- 1 | # This is a compatibility layer for using the pure Ruby postgres-pr instead of 2 | # the C interface of postgres. 3 | 4 | begin 5 | require 'postgres.so' 6 | rescue LoadError 7 | require 'postgres-pr/postgres-compat' 8 | end 9 | -------------------------------------------------------------------------------- /examples/server.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift "../lib" 2 | require 'postgres-pr/message' 3 | require 'socket' 4 | include PostgresPR 5 | 6 | s = UNIXServer.open(ARGV.shift || raise).accept 7 | startup = true 8 | loop do 9 | msg = Message.read(s, startup) 10 | p msg 11 | startup = false 12 | end 13 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | * Message types 2 | 3 | Bind, BindComplete, CancelRequest, Close, CloseComplete, CommandComplete, 4 | CopyData, CopyDone, CopyFail, CopyInResponse, CopyOutResponse, Describe, 5 | Execute, Flush, FunctionCall, FunctionCallResponse, NoData, 6 | NotificationResponse, ParameterDescription, PortalSuspended 7 | 8 | * SSLRequest instead of StartupMessage 9 | -------------------------------------------------------------------------------- /lib/postgres-pr/typeconv/TC_conv.rb: -------------------------------------------------------------------------------- 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 19 | -------------------------------------------------------------------------------- /examples/test_connection.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift '../lib' 2 | require 'postgres-pr/connection' 3 | 4 | conn = PostgresPR::Connection.new('mneumann', 'mneumann', nil, 'unix:/var/run/postgresql/.s.PGSQL.5432') 5 | p conn.query("DROP TABLE test") rescue nil 6 | p conn.query("CREATE TABLE test (a VARCHAR(100))") 7 | p conn.query("INSERT INTO test VALUES ('hallo')") 8 | p conn.query("INSERT INTO test VALUES ('leute')") 9 | conn.query("COMMIT") 10 | 11 | conn.query("BEGIN") 12 | 10000.times do |i| 13 | p i 14 | conn.query("INSERT INTO test VALUES ('#{i}')") 15 | end 16 | conn.query("COMMIT") 17 | 18 | p conn.query("SELECT * FROM test") 19 | -------------------------------------------------------------------------------- /lib/postgres-pr/typeconv/bytea.rb: -------------------------------------------------------------------------------- 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 27 | -------------------------------------------------------------------------------- /postgres-pr.gemspec: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | 3 | if File.read('lib/postgres-pr/version.rb') =~ /Version\s+=\s+"(\d+\.\d+\.\d+)"/ 4 | version = $1 5 | else 6 | raise "no version" 7 | end 8 | 9 | spec = Gem::Specification.new do |s| 10 | s.name = 'postgres-pr' 11 | s.version = version 12 | s.summary = 'A pure Ruby interface to the PostgreSQL (>= 7.4) database' 13 | s.requirements << 'PostgreSQL >= 7.4' 14 | 15 | s.files = (Dir['lib/**/*'] + Dir['test/**/*'] + 16 | Dir['examples/**/*']) 17 | 18 | s.require_path = 'lib' 19 | 20 | s.author = "Michael Neumann" 21 | s.email = "mneumann@ntecs.de" 22 | s.homepage = "postgres-pr.rubyforge.org" 23 | s.rubyforge_project = "postgres-pr" 24 | end 25 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | = Pure Ruby PostgreSQL interface 2 | 3 | This is a library to access PostgreSQL (>= 7.4) from Ruby without the need of 4 | any C library. 5 | 6 | == Author and Copyright 7 | 8 | Copyright (c) 2005, 2008 by Michael Neumann (mneumann@ntecs.de). 9 | Released under the same terms of license as Ruby. 10 | 11 | == Homepage 12 | 13 | http://rubyforge.org/projects/ruby-dbi 14 | 15 | == Quick Example 16 | 17 | > gem install postgres-pr 18 | > irb -r rubygems 19 | 20 | Then in the interactive Ruby interpreter type (replace DBNAME and DBUSER 21 | accordingly): 22 | 23 | require 'postgres-pr/connection' 24 | c = PostgresPR::Connection.new('DBNAME', 'DBUSER') 25 | c.query('SELECT 1+2').rows # => [["3"]] 26 | -------------------------------------------------------------------------------- /lib/byteorder.rb: -------------------------------------------------------------------------------- 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 33 | -------------------------------------------------------------------------------- /examples/client.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift "../lib" 2 | require 'postgres-pr/message' 3 | require 'socket' 4 | include PostgresPR 5 | 6 | s = UNIXSocket.new(ARGV.shift || "/tmp/.s.PGSQL.5432") 7 | 8 | msg = StartupMessage.new(196608, "user" => "mneumann", "database" => "mneumann") 9 | s << msg.dump 10 | 11 | Thread.start(s) { |s| 12 | sleep 2 13 | s << Query.new("drop table test").dump 14 | s << Query.new("create table test (i int, v varchar(100))").dump 15 | s << Parse.new("insert into test (i, v) values ($1, $2)", "blah").dump 16 | s << Query.new("EXECUTE blah(1, 'hallo')").dump 17 | 18 | while not (line = gets.chomp).empty? 19 | s << Query.new(line).dump 20 | end 21 | exit 22 | } 23 | 24 | loop do 25 | msg = Message.read(s) 26 | p msg 27 | 28 | case msg 29 | when AuthentificationOk 30 | p "OK" 31 | when ErrorResponse 32 | p "FAILED" 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /examples/og/test.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift '../../lib' 2 | require 'rubygems' 3 | require 'og' 4 | require 'glue/logger' 5 | 6 | $DBG = true 7 | 8 | class User 9 | end 10 | 11 | class Comment 12 | prop_accessor :body, String 13 | belongs_to :user, User 14 | end 15 | 16 | class User 17 | prop_accessor :name, String 18 | has_many :comments, Comment 19 | end 20 | 21 | if __FILE__ == $0 22 | config = { 23 | :address => "localhost", 24 | :database => "mneumann", 25 | :backend => "psql", 26 | :user => "mneumann", 27 | :password => "", 28 | :connection_count => 1 29 | } 30 | $log = Logger.new(STDERR) 31 | $og = Og::Database.new(config) 32 | 33 | $og.get_connection 34 | 35 | u1 = User.new 36 | u1.name = "Michael Neumann" 37 | u1.save! 38 | 39 | u2 = User.new 40 | u2.name = "John User" 41 | u2.save! 42 | 43 | c1 = Comment.new 44 | c1.body = "og test" 45 | c1.user = u1 46 | c1.save! 47 | 48 | p User.all 49 | p Comment.all 50 | end 51 | -------------------------------------------------------------------------------- /lib/postgres-pr/typeconv/array.rb: -------------------------------------------------------------------------------- 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 47 | -------------------------------------------------------------------------------- /lib/buffer.rb: -------------------------------------------------------------------------------- 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 self.from_string(str) 11 | new(str) 12 | end 13 | 14 | def self.of_size(size) 15 | raise ArgumentError if size < 0 16 | new('#' * size) 17 | end 18 | 19 | def initialize(content) 20 | @size = content.size 21 | @content = content 22 | @position = 0 23 | end 24 | 25 | def size 26 | @size 27 | end 28 | 29 | def position 30 | @position 31 | end 32 | 33 | def position=(new_pos) 34 | raise ArgumentError if new_pos < 0 or new_pos > @size 35 | @position = new_pos 36 | end 37 | 38 | def at_end? 39 | @position == @size 40 | end 41 | 42 | def content 43 | @content 44 | end 45 | 46 | def read(n) 47 | raise EOF, 'cannot read beyond the end of buffer' if @position + n > @size 48 | str = @content[@position, n] 49 | @position += n 50 | str 51 | end 52 | 53 | def write(str) 54 | sz = str.size 55 | raise EOF, 'cannot write beyond the end of buffer' if @position + sz > @size 56 | @content[@position, sz] = str 57 | @position += sz 58 | self 59 | end 60 | 61 | def copy_from_stream(stream, n) 62 | raise ArgumentError if n < 0 63 | while n > 0 64 | str = stream.read(n) 65 | write(str) 66 | n -= str.size 67 | end 68 | raise if n < 0 69 | end 70 | 71 | NUL = "\000" 72 | 73 | def write_cstring(cstr) 74 | raise ArgumentError, "Invalid Ruby/cstring" if cstr.include?(NUL) 75 | write(cstr) 76 | write(NUL) 77 | end 78 | 79 | # returns a Ruby string without the trailing NUL character 80 | def read_cstring 81 | nul_pos = @content.index(NUL, @position) 82 | raise Error, "no cstring found!" unless nul_pos 83 | 84 | sz = nul_pos - @position 85 | str = @content[@position, sz] 86 | @position += sz + 1 87 | return str 88 | end 89 | 90 | # read till the end of the buffer 91 | def read_rest 92 | read(self.size-@position) 93 | end 94 | 95 | include BinaryWriterMixin 96 | include BinaryReaderMixin 97 | end 98 | -------------------------------------------------------------------------------- /lib/binary_writer.rb: -------------------------------------------------------------------------------- 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 101 | -------------------------------------------------------------------------------- /test/TC_message.rb: -------------------------------------------------------------------------------- 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 | include PostgresPR 28 | 29 | class Buffer 30 | alias old_content content 31 | def content 32 | self 33 | end 34 | end 35 | 36 | class Message 37 | attr_accessor :buffer 38 | 39 | class << self 40 | alias old_create create 41 | def create(buffer) 42 | obj = old_create(buffer) 43 | obj.buffer = buffer 44 | obj 45 | end 46 | end 47 | 48 | alias old_dump dump 49 | 50 | def dump(body_size=0, &block) 51 | buf = old_dump(body_size, &block) 52 | self.buffer = buf 53 | buf.old_content 54 | end 55 | end 56 | 57 | class StartupMessage 58 | alias old_dump dump 59 | def dump 60 | buf = old_dump 61 | self.buffer = buf 62 | buf.old_content 63 | end 64 | end 65 | 66 | class StringIO 67 | alias readbytes read 68 | end 69 | 70 | class TC_Message < Test::Unit::TestCase 71 | 72 | CASES = [ 73 | #[AuthentificationOk], 74 | #[ErrorResponse], 75 | [ParameterStatus, "key", "value"], 76 | [BackendKeyData, 234234234, 213434], 77 | [ReadyForQuery, ?T], 78 | # TODO: RowDescription 79 | [DataRow, ["a", "bbbbbb", "ccc", nil, nil, "ddddd", "e" * 10_000]], 80 | [DataRow, []], 81 | [CommandComplete, "INSERT"], 82 | [StartupMessage, 196608, {"user" => "mneumann", "database" => "mneumann"}], 83 | [Parse, "INSERT INTO blah values (?, ?)", ""], 84 | [Query, "SELECT * FROM test\nWHERE a='test'"] 85 | ] 86 | 87 | def test_pack_unpack_feature 88 | assert_equal ['a', 'b'], "a\000b\000".unpack('Z*Z*') 89 | end 90 | 91 | def test_marshal_unmarshal 92 | CASES.each do |klass, *params| 93 | msg = klass.new(*params) 94 | new_msg = Message.read(StringIO.new(msg.dump), klass == StartupMessage) 95 | assert_equal(msg, new_msg) 96 | 97 | msg1, msg2 = klass.new(*params), klass.new(*params) 98 | msg1.dump 99 | msg2.dump; msg2.parse(msg2.buffer) 100 | assert_equal(msg1, msg2) 101 | end 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /lib/binary_reader.rb: -------------------------------------------------------------------------------- 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 121 | -------------------------------------------------------------------------------- /lib/postgres-pr/postgres-compat.rb: -------------------------------------------------------------------------------- 1 | # This is a compatibility layer for using the pure Ruby postgres-pr instead of 2 | # the C interface of postgres. 3 | 4 | require 'postgres-pr/connection' 5 | 6 | class PGconn 7 | class << self 8 | alias connect new 9 | end 10 | 11 | def initialize(host, port, options, tty, database, user, auth) 12 | uri = 13 | if host.nil? 14 | nil 15 | elsif host[0] != ?/ 16 | "tcp://#{ host }:#{ port }" 17 | else 18 | "unix:#{ host }/.s.PGSQL.#{ port }" 19 | end 20 | @host = host 21 | @db = database 22 | @user = user 23 | @conn = PostgresPR::Connection.new(database, user, auth, uri) 24 | end 25 | 26 | def close 27 | @conn.close 28 | end 29 | 30 | attr_reader :host, :db, :user 31 | 32 | def query(sql) 33 | PGresult.new(@conn.query(sql)) 34 | end 35 | 36 | alias exec query 37 | 38 | def transaction_status 39 | @conn.transaction_status 40 | end 41 | 42 | def self.escape(str) 43 | str.gsub("'","''").gsub("\\","\\\\\\\\") 44 | end 45 | 46 | end 47 | 48 | class PGresult 49 | include Enumerable 50 | 51 | EMPTY_QUERY = 0 52 | COMMAND_OK = 1 53 | TUPLES_OK = 2 54 | COPY_OUT = 3 55 | COPY_IN = 4 56 | BAD_RESPONSE = 5 57 | NONFATAL_ERROR = 6 58 | FATAL_ERROR = 7 59 | 60 | def each(&block) 61 | @result.each(&block) 62 | end 63 | 64 | def [](index) 65 | @result[index] 66 | end 67 | 68 | def initialize(res) 69 | @res = res 70 | @fields = @res.fields.map {|f| f.name} 71 | @result = @res.rows 72 | end 73 | 74 | # TODO: status, getlength, cmdstatus 75 | 76 | attr_reader :result, :fields 77 | 78 | def num_tuples 79 | @result.size 80 | end 81 | 82 | def num_fields 83 | @fields.size 84 | end 85 | 86 | def fieldname(index) 87 | @fields[index] 88 | end 89 | 90 | def fieldnum(name) 91 | @fields.index(name) 92 | end 93 | 94 | def type(index) 95 | # TODO: correct? 96 | @res.fields[index].type_oid 97 | end 98 | 99 | def size(index) 100 | raise 101 | # TODO: correct? 102 | @res.fields[index].typlen 103 | end 104 | 105 | def getvalue(tup_num, field_num) 106 | @result[tup_num][field_num] 107 | end 108 | 109 | def status 110 | if num_tuples > 0 111 | TUPLES_OK 112 | else 113 | COMMAND_OK 114 | end 115 | end 116 | 117 | def cmdstatus 118 | @res.cmd_tag || '' 119 | end 120 | 121 | # free the result set 122 | def clear 123 | @res = @fields = @result = nil 124 | end 125 | 126 | # Returns the number of rows affected by the SQL command 127 | def cmdtuples 128 | case @res.cmd_tag 129 | when nil 130 | return nil 131 | when /^INSERT\s+(\d+)\s+(\d+)$/, /^(DELETE|UPDATE|MOVE|FETCH)\s+(\d+)$/ 132 | $2.to_i 133 | else 134 | nil 135 | end 136 | end 137 | 138 | end 139 | 140 | class PGError < Exception 141 | end 142 | -------------------------------------------------------------------------------- /lib/postgres-pr/connection.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Author:: Michael Neumann 3 | # Copyright:: (c) 2005 by Michael Neumann 4 | # License:: Same as Ruby's or BSD 5 | # 6 | 7 | require 'postgres-pr/message' 8 | require 'postgres-pr/version' 9 | require 'uri' 10 | require 'socket' 11 | 12 | module PostgresPR 13 | 14 | PROTO_VERSION = 3 << 16 #196608 15 | 16 | class Connection 17 | 18 | # A block which is called with the NoticeResponse object as parameter. 19 | attr_accessor :notice_processor 20 | 21 | # 22 | # Returns one of the following statuses: 23 | # 24 | # PQTRANS_IDLE = 0 (connection idle) 25 | # PQTRANS_INTRANS = 2 (idle, within transaction block) 26 | # PQTRANS_INERROR = 3 (idle, within failed transaction) 27 | # PQTRANS_UNKNOWN = 4 (cannot determine status) 28 | # 29 | # Not yet implemented is: 30 | # 31 | # PQTRANS_ACTIVE = 1 (command in progress) 32 | # 33 | def transaction_status 34 | case @transaction_status 35 | when ?I 36 | 0 37 | when ?T 38 | 2 39 | when ?E 40 | 3 41 | else 42 | 4 43 | end 44 | end 45 | 46 | def initialize(database, user, password=nil, uri = nil) 47 | uri ||= DEFAULT_URI 48 | 49 | @transaction_status = nil 50 | @params = {} 51 | establish_connection(uri) 52 | 53 | @conn << StartupMessage.new(PROTO_VERSION, 'user' => user, 'database' => database).dump 54 | 55 | loop do 56 | msg = Message.read(@conn) 57 | 58 | case msg 59 | when AuthentificationClearTextPassword 60 | raise ArgumentError, "no password specified" if password.nil? 61 | @conn << PasswordMessage.new(password).dump 62 | 63 | when AuthentificationCryptPassword 64 | raise ArgumentError, "no password specified" if password.nil? 65 | @conn << PasswordMessage.new(password.crypt(msg.salt)).dump 66 | 67 | when AuthentificationMD5Password 68 | raise ArgumentError, "no password specified" if password.nil? 69 | require 'digest/md5' 70 | 71 | m = Digest::MD5.hexdigest(password + user) 72 | m = Digest::MD5.hexdigest(m + msg.salt) 73 | m = 'md5' + m 74 | @conn << PasswordMessage.new(m).dump 75 | 76 | when AuthentificationKerberosV4, AuthentificationKerberosV5, AuthentificationSCMCredential 77 | raise "unsupported authentification" 78 | 79 | when AuthentificationOk 80 | when ErrorResponse 81 | raise msg.field_values.join("\t") 82 | when NoticeResponse 83 | @notice_processor.call(msg) if @notice_processor 84 | when ParameterStatus 85 | @params[msg.key] = msg.value 86 | when BackendKeyData 87 | # TODO 88 | #p msg 89 | when ReadyForQuery 90 | @transaction_status = msg.backend_transaction_status_indicator 91 | break 92 | else 93 | raise "unhandled message type" 94 | end 95 | end 96 | end 97 | 98 | def close 99 | raise "connection already closed" if @conn.nil? 100 | @conn.shutdown 101 | @conn = nil 102 | end 103 | 104 | class Result 105 | attr_accessor :rows, :fields, :cmd_tag 106 | def initialize(rows=[], fields=[]) 107 | @rows, @fields = rows, fields 108 | end 109 | end 110 | 111 | def query(sql) 112 | @conn << Query.dump(sql) 113 | 114 | result = Result.new 115 | errors = [] 116 | 117 | loop do 118 | msg = Message.read(@conn) 119 | case msg 120 | when DataRow 121 | result.rows << msg.columns 122 | when CommandComplete 123 | result.cmd_tag = msg.cmd_tag 124 | when ReadyForQuery 125 | @transaction_status = msg.backend_transaction_status_indicator 126 | break 127 | when RowDescription 128 | result.fields = msg.fields 129 | when CopyInResponse 130 | when CopyOutResponse 131 | when EmptyQueryResponse 132 | when ErrorResponse 133 | # TODO 134 | errors << msg 135 | when NoticeResponse 136 | @notice_processor.call(msg) if @notice_processor 137 | else 138 | # TODO 139 | end 140 | end 141 | 142 | raise errors.map{|e| e.field_values.join("\t") }.join("\n") unless errors.empty? 143 | 144 | result 145 | end 146 | 147 | DEFAULT_PORT = 5432 148 | DEFAULT_HOST = 'localhost' 149 | DEFAULT_PATH = '/tmp' 150 | DEFAULT_URI = 151 | if RUBY_PLATFORM.include?('win') 152 | 'tcp://' + DEFAULT_HOST + ':' + DEFAULT_PORT.to_s 153 | else 154 | 'unix:' + File.join(DEFAULT_PATH, '.s.PGSQL.' + DEFAULT_PORT.to_s) 155 | end 156 | 157 | private 158 | 159 | # tcp://localhost:5432 160 | # unix:/tmp/.s.PGSQL.5432 161 | def establish_connection(uri) 162 | u = URI.parse(uri) 163 | case u.scheme 164 | when 'tcp' 165 | @conn = TCPSocket.new(u.host || DEFAULT_HOST, u.port || DEFAULT_PORT) 166 | when 'unix' 167 | @conn = UNIXSocket.new(u.path) 168 | else 169 | raise 'unrecognized uri scheme format (must be tcp or unix)' 170 | end 171 | end 172 | end 173 | 174 | end # module PostgresPR 175 | -------------------------------------------------------------------------------- /lib/postgres-pr/message.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Author:: Michael Neumann 3 | # Copyright:: (c) 2005 by Michael Neumann 4 | # License:: Same as Ruby's or BSD 5 | # 6 | 7 | require 'buffer' 8 | class IO 9 | def read_exactly_n_bytes(n) 10 | buf = read(n) 11 | raise EOFError if buf == nil 12 | return buf if buf.size == n 13 | 14 | n -= buf.size 15 | 16 | while n > 0 17 | str = read(n) 18 | raise EOFError if str == nil 19 | buf << str 20 | n -= str.size 21 | end 22 | return buf 23 | end 24 | end 25 | 26 | module PostgresPR 27 | 28 | class ParseError < RuntimeError; end 29 | class DumpError < RuntimeError; end 30 | 31 | 32 | # Base class representing a PostgreSQL protocol message 33 | class Message 34 | # One character message-typecode to class map 35 | MsgTypeMap = Hash.new { UnknownMessageType } 36 | 37 | def self.register_message_type(type) 38 | raise "duplicate message type registration" if MsgTypeMap.has_key?(type) 39 | 40 | MsgTypeMap[type] = self 41 | 42 | self.const_set(:MsgType, type) 43 | class_eval "def message_type; MsgType end" 44 | end 45 | 46 | def self.read(stream, startup=false) 47 | type = stream.read_exactly_n_bytes(1) unless startup 48 | length = stream.read_exactly_n_bytes(4).unpack('N').first # FIXME: length should be signed, not unsigned 49 | 50 | raise ParseError unless length >= 4 51 | 52 | # initialize buffer 53 | buffer = Buffer.of_size(startup ? length : 1+length) 54 | buffer.write(type) unless startup 55 | buffer.write_int32_network(length) 56 | buffer.copy_from_stream(stream, length-4) 57 | 58 | (startup ? StartupMessage : MsgTypeMap[type]).create(buffer) 59 | end 60 | 61 | def self.create(buffer) 62 | obj = allocate 63 | obj.parse(buffer) 64 | obj 65 | end 66 | 67 | def self.dump(*args) 68 | new(*args).dump 69 | end 70 | 71 | def dump(body_size=0) 72 | buffer = Buffer.of_size(5 + body_size) 73 | buffer.write(self.message_type) 74 | buffer.write_int32_network(4 + body_size) 75 | yield buffer if block_given? 76 | raise DumpError unless buffer.at_end? 77 | return buffer.content 78 | end 79 | 80 | def parse(buffer) 81 | buffer.position = 5 82 | yield buffer if block_given? 83 | raise ParseError, buffer.inspect unless buffer.at_end? 84 | end 85 | 86 | def self.fields(*attribs) 87 | names = attribs.map {|name, type| name.to_s} 88 | arg_list = names.join(", ") 89 | ivar_list = names.map {|name| "@" + name }.join(", ") 90 | sym_list = names.map {|name| ":" + name }.join(", ") 91 | class_eval %[ 92 | attr_accessor #{ sym_list } 93 | def initialize(#{ arg_list }) 94 | #{ ivar_list } = #{ arg_list } 95 | end 96 | ] 97 | end 98 | end 99 | 100 | class UnknownMessageType < Message 101 | def dump 102 | raise 103 | end 104 | end 105 | 106 | class Authentification < Message 107 | register_message_type 'R' 108 | 109 | AuthTypeMap = Hash.new { UnknownAuthType } 110 | 111 | def self.create(buffer) 112 | buffer.position = 5 113 | authtype = buffer.read_int32_network 114 | klass = AuthTypeMap[authtype] 115 | obj = klass.allocate 116 | obj.parse(buffer) 117 | obj 118 | end 119 | 120 | def self.register_auth_type(type) 121 | raise "duplicate auth type registration" if AuthTypeMap.has_key?(type) 122 | AuthTypeMap[type] = self 123 | self.const_set(:AuthType, type) 124 | class_eval "def auth_type() AuthType end" 125 | end 126 | 127 | # the dump method of class Message 128 | alias message__dump dump 129 | 130 | def dump 131 | super(4) do |buffer| 132 | buffer.write_int32_network(self.auth_type) 133 | end 134 | end 135 | 136 | def parse(buffer) 137 | super do 138 | auth_t = buffer.read_int32_network 139 | raise ParseError unless auth_t == self.auth_type 140 | yield if block_given? 141 | end 142 | end 143 | end 144 | 145 | class UnknownAuthType < Authentification 146 | end 147 | 148 | class AuthentificationOk < Authentification 149 | register_auth_type 0 150 | end 151 | 152 | class AuthentificationKerberosV4 < Authentification 153 | register_auth_type 1 154 | end 155 | 156 | class AuthentificationKerberosV5 < Authentification 157 | register_auth_type 2 158 | end 159 | 160 | class AuthentificationClearTextPassword < Authentification 161 | register_auth_type 3 162 | end 163 | 164 | module SaltedAuthentificationMixin 165 | attr_accessor :salt 166 | 167 | def initialize(salt) 168 | @salt = salt 169 | end 170 | 171 | def dump 172 | raise DumpError unless @salt.size == self.salt_size 173 | 174 | message__dump(4 + self.salt_size) do |buffer| 175 | buffer.write_int32_network(self.auth_type) 176 | buffer.write(@salt) 177 | end 178 | end 179 | 180 | def parse(buffer) 181 | super do 182 | @salt = buffer.read(self.salt_size) 183 | end 184 | end 185 | end 186 | 187 | class AuthentificationCryptPassword < Authentification 188 | register_auth_type 4 189 | include SaltedAuthentificationMixin 190 | def salt_size; 2 end 191 | end 192 | 193 | 194 | class AuthentificationMD5Password < Authentification 195 | register_auth_type 5 196 | include SaltedAuthentificationMixin 197 | def salt_size; 4 end 198 | end 199 | 200 | class AuthentificationSCMCredential < Authentification 201 | register_auth_type 6 202 | end 203 | 204 | class PasswordMessage < Message 205 | register_message_type 'p' 206 | fields :password 207 | 208 | def dump 209 | super(@password.size + 1) do |buffer| 210 | buffer.write_cstring(@password) 211 | end 212 | end 213 | 214 | def parse(buffer) 215 | super do 216 | @password = buffer.read_cstring 217 | end 218 | end 219 | end 220 | 221 | class ParameterStatus < Message 222 | register_message_type 'S' 223 | fields :key, :value 224 | 225 | def dump 226 | super(@key.size + 1 + @value.size + 1) do |buffer| 227 | buffer.write_cstring(@key) 228 | buffer.write_cstring(@value) 229 | end 230 | end 231 | 232 | def parse(buffer) 233 | super do 234 | @key = buffer.read_cstring 235 | @value = buffer.read_cstring 236 | end 237 | end 238 | end 239 | 240 | class BackendKeyData < Message 241 | register_message_type 'K' 242 | fields :process_id, :secret_key 243 | 244 | def dump 245 | super(4 + 4) do |buffer| 246 | buffer.write_int32_network(@process_id) 247 | buffer.write_int32_network(@secret_key) 248 | end 249 | end 250 | 251 | def parse(buffer) 252 | super do 253 | @process_id = buffer.read_int32_network 254 | @secret_key = buffer.read_int32_network 255 | end 256 | end 257 | end 258 | 259 | class ReadyForQuery < Message 260 | register_message_type 'Z' 261 | fields :backend_transaction_status_indicator 262 | 263 | def dump 264 | super(1) do |buffer| 265 | buffer.write_byte(@backend_transaction_status_indicator) 266 | end 267 | end 268 | 269 | def parse(buffer) 270 | super do 271 | @backend_transaction_status_indicator = buffer.read_byte 272 | end 273 | end 274 | end 275 | 276 | class DataRow < Message 277 | register_message_type 'D' 278 | fields :columns 279 | 280 | def dump 281 | sz = @columns.inject(2) {|sum, col| sum + 4 + (col ? col.size : 0)} 282 | super(sz) do |buffer| 283 | buffer.write_int16_network(@columns.size) 284 | @columns.each {|col| 285 | buffer.write_int32_network(col ? col.size : -1) 286 | buffer.write(col) if col 287 | } 288 | end 289 | end 290 | 291 | def parse(buffer) 292 | super do 293 | n_cols = buffer.read_int16_network 294 | @columns = (1..n_cols).collect { 295 | len = buffer.read_int32_network 296 | if len == -1 297 | nil 298 | else 299 | buffer.read(len) 300 | end 301 | } 302 | end 303 | end 304 | end 305 | 306 | class CommandComplete < Message 307 | register_message_type 'C' 308 | fields :cmd_tag 309 | 310 | def dump 311 | super(@cmd_tag.size + 1) do |buffer| 312 | buffer.write_cstring(@cmd_tag) 313 | end 314 | end 315 | 316 | def parse(buffer) 317 | super do 318 | @cmd_tag = buffer.read_cstring 319 | end 320 | end 321 | end 322 | 323 | class EmptyQueryResponse < Message 324 | register_message_type 'I' 325 | end 326 | 327 | module NoticeErrorMixin 328 | attr_accessor :field_type, :field_values 329 | 330 | def initialize(field_type=0, field_values=[]) 331 | raise ArgumentError if field_type == 0 and not field_values.empty? 332 | @field_type, @field_values = field_type, field_values 333 | end 334 | 335 | def dump 336 | raise ArgumentError if @field_type == 0 and not @field_values.empty? 337 | 338 | sz = 1 339 | sz += @field_values.inject(1) {|sum, fld| sum + fld.size + 1} unless @field_type == 0 340 | 341 | super(sz) do |buffer| 342 | buffer.write_byte(@field_type) 343 | break if @field_type == 0 344 | @field_values.each {|fld| buffer.write_cstring(fld) } 345 | buffer.write_byte(0) 346 | end 347 | end 348 | 349 | def parse(buffer) 350 | super do 351 | @field_type = buffer.read_byte 352 | break if @field_type == 0 353 | @field_values = [] 354 | while buffer.position < buffer.size-1 355 | @field_values << buffer.read_cstring 356 | end 357 | terminator = buffer.read_byte 358 | raise ParseError unless terminator == 0 359 | end 360 | end 361 | end 362 | 363 | class NoticeResponse < Message 364 | register_message_type 'N' 365 | include NoticeErrorMixin 366 | end 367 | 368 | class ErrorResponse < Message 369 | register_message_type 'E' 370 | include NoticeErrorMixin 371 | end 372 | 373 | # TODO 374 | class CopyInResponse < Message 375 | register_message_type 'G' 376 | end 377 | 378 | # TODO 379 | class CopyOutResponse < Message 380 | register_message_type 'H' 381 | end 382 | 383 | class Parse < Message 384 | register_message_type 'P' 385 | fields :query, :stmt_name, :parameter_oids 386 | 387 | def initialize(query, stmt_name="", parameter_oids=[]) 388 | @query, @stmt_name, @parameter_oids = query, stmt_name, parameter_oids 389 | end 390 | 391 | def dump 392 | sz = @stmt_name.size + 1 + @query.size + 1 + 2 + (4 * @parameter_oids.size) 393 | super(sz) do |buffer| 394 | buffer.write_cstring(@stmt_name) 395 | buffer.write_cstring(@query) 396 | buffer.write_int16_network(@parameter_oids.size) 397 | @parameter_oids.each {|oid| buffer.write_int32_network(oid) } 398 | end 399 | end 400 | 401 | def parse(buffer) 402 | super do 403 | @stmt_name = buffer.read_cstring 404 | @query = buffer.read_cstring 405 | n_oids = buffer.read_int16_network 406 | @parameter_oids = (1..n_oids).collect { 407 | # TODO: zero means unspecified. map to nil? 408 | buffer.read_int32_network 409 | } 410 | end 411 | end 412 | end 413 | 414 | class ParseComplete < Message 415 | register_message_type '1' 416 | end 417 | 418 | class Query < Message 419 | register_message_type 'Q' 420 | fields :query 421 | 422 | def dump 423 | super(@query.size + 1) do |buffer| 424 | buffer.write_cstring(@query) 425 | end 426 | end 427 | 428 | def parse(buffer) 429 | super do 430 | @query = buffer.read_cstring 431 | end 432 | end 433 | end 434 | 435 | class RowDescription < Message 436 | register_message_type 'T' 437 | fields :fields 438 | 439 | class FieldInfo < Struct.new(:name, :oid, :attr_nr, :type_oid, :typlen, :atttypmod, :formatcode); end 440 | 441 | def dump 442 | sz = @fields.inject(2) {|sum, fld| sum + 18 + fld.name.size + 1 } 443 | super(sz) do |buffer| 444 | buffer.write_int16_network(@fields.size) 445 | @fields.each { |f| 446 | buffer.write_cstring(f.name) 447 | buffer.write_int32_network(f.oid) 448 | buffer.write_int16_network(f.attr_nr) 449 | buffer.write_int32_network(f.type_oid) 450 | buffer.write_int16_network(f.typlen) 451 | buffer.write_int32_network(f.atttypmod) 452 | buffer.write_int16_network(f.formatcode) 453 | } 454 | end 455 | end 456 | 457 | def parse(buffer) 458 | super do 459 | n_fields = buffer.read_int16_network 460 | @fields = (1..n_fields).collect { 461 | f = FieldInfo.new 462 | f.name = buffer.read_cstring 463 | f.oid = buffer.read_int32_network 464 | f.attr_nr = buffer.read_int16_network 465 | f.type_oid = buffer.read_int32_network 466 | f.typlen = buffer.read_int16_network 467 | f.atttypmod = buffer.read_int32_network 468 | f.formatcode = buffer.read_int16_network 469 | f 470 | } 471 | end 472 | end 473 | end 474 | 475 | class StartupMessage < Message 476 | fields :proto_version, :params 477 | 478 | def dump 479 | sz = @params.inject(4 + 4) {|sum, kv| sum + kv[0].size + 1 + kv[1].size + 1} + 1 480 | 481 | buffer = Buffer.of_size(sz) 482 | buffer.write_int32_network(sz) 483 | buffer.write_int32_network(@proto_version) 484 | @params.each_pair {|key, value| 485 | buffer.write_cstring(key) 486 | buffer.write_cstring(value) 487 | } 488 | buffer.write_byte(0) 489 | 490 | raise DumpError unless buffer.at_end? 491 | return buffer.content 492 | end 493 | 494 | def parse(buffer) 495 | buffer.position = 4 496 | 497 | @proto_version = buffer.read_int32_network 498 | @params = {} 499 | 500 | while buffer.position < buffer.size-1 501 | key = buffer.read_cstring 502 | val = buffer.read_cstring 503 | @params[key] = val 504 | end 505 | 506 | nul = buffer.read_byte 507 | raise ParseError unless nul == 0 508 | raise ParseError unless buffer.at_end? 509 | end 510 | end 511 | 512 | class SSLRequest < Message 513 | fields :ssl_request_code 514 | 515 | def dump 516 | sz = 4 + 4 517 | buffer = Buffer.of_size(sz) 518 | buffer.write_int32_network(sz) 519 | buffer.write_int32_network(@ssl_request_code) 520 | raise DumpError unless buffer.at_end? 521 | return buffer.content 522 | end 523 | 524 | def parse(buffer) 525 | buffer.position = 4 526 | @ssl_request_code = buffer.read_int32_network 527 | raise ParseError unless buffer.at_end? 528 | end 529 | end 530 | 531 | =begin 532 | # TODO: duplicate message-type, split into client/server messages 533 | class Sync < Message 534 | register_message_type 'S' 535 | end 536 | =end 537 | 538 | class Terminate < Message 539 | register_message_type 'X' 540 | end 541 | 542 | end # module PostgresPR 543 | --------------------------------------------------------------------------------