1#!/usr/local/bin/ruby
2
3require 'socket'
4
5=begin
6= FastDB API
7  This module contains Ruby API to FastDB
8=end
9module FastDB
10
11
12=begin
13== Connection to the FastDB server
14=end
15class Connection
16=begin
17=== Open connection with server
18==== host_address - string with server host name
19==== host_port    - integer number with server port
20=end                     
21    def open(host_address, host_port)
22        @socket = TCPSocket.new(host_address, host_port)
23        @socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, true)
24        @n_statements = 0
25        loadSchema()
26    end
27
28=begin
29=== Close connection with server
30=end                     
31    def close()
32        sendCommand(CliCmdCloseSession)
33        @socket.close()
34        @socket = nil
35    end
36
37=begin
38=== Create select statement.
39==== sql - SubSQL select statement with parameters. Parameters should be started with % character.
40     Each used parameter should be set before execution of the statement.
41=end
42    def createStatement(sql)
43        @n_statements += 1
44        return Statement.new(self, sql, @n_statements)
45    end
46
47=begin
48=== Execute select statement
49==== sql - SubSQL select statement with parameters. Parameters should be started with % character.
50==== param - hash specifying qyery parameters
51==== for_update - if cursor is opened in for update mode
52=end
53    def select(sql, params = {}, for_update = false)
54        stmt = createStatement(stmt)
55        params.each_pair {|key, value| stmt[key] = value}
56        return stmt.fetch(for_update)
57    end
58
59=begin
60=== Commit current transaction
61=end
62    def commit()
63        sendReceiveCommand(CliCmdCommit)
64    end
65
66=begin
67=== Exclusively lock database (FastDB set locks implicitely, explicit exclusive lock may be needed to avoid deadlock caused by lock upgrade)
68=end
69    def lock()
70        sendCommand(CliCmdLock)
71    end
72
73=begin
74=== Release all locks set by the current transaction
75=end
76    def unlock()
77        sendReceiveCommand(CliCmdPrecommit)
78    end
79
80=begin
81=== Rollback curent transaction. All changes made by current transaction are lost.
82=end
83    def rollback()
84        sendReceiveCommand(CliCmdAbort)
85    end
86
87=begin
88=== Execute block in the context of transaction
89=end
90    def transaction
91      commited = false
92      yield
93      commit
94      commited = true
95    ensure
96      rollback unless commited
97    end
98
99=begin
100=== Insert object in the database. There is should be table in the database with
101    name equal to the full class name of the inserted object (comparison is
102    case sensitive). FastDB will store to the database all non-static and
103    non-transient fields from the class.
104==== obj - object to be inserted in the database
105==== table - name of the table in which object should be inserted (by default - table corresponding to the object class)
106==== Returns reference to the inserted object
107=end
108    def insert(obj, table=obj.class.name)
109        column_defs=""
110        column_values=""
111        n_columns=0
112        table_desc = @tables[table]
113        if obj.is_a?(Struct)
114            vars = obj.members
115            is_struct=true
116        else
117            vars = obj.instance_variables
118            is_struct=false
119        end
120        if table_desc.nil?
121            raise CliError, "Class #{table} is not found in the database"
122        end
123        for var in vars
124            if is_struct
125                field = var
126            else
127                field = var[1...var.length] # ignore @ prefix
128            end
129            field_desc = table_desc[field]
130            if field_desc != nil
131                n_columns += 1
132                if is_struct
133                    value = obj[var]
134                else
135		    value = obj.instance_variable_get(var)
136                end
137                type = value.class
138                if type == Fixnum
139                    column_defs << CliInt4 << field << 0
140                    column_values << [value].pack("N")
141                elsif type == Bignum
142                    column_defs << CliInt8 << field << 0
143                    column_values << [value >> 32, value & 0xffffffff].pack("NN")
144                elsif type == Float
145                    column_defs << CliReal8 << field << 0
146                    column_values << [value].pack("G")
147                elsif type == String
148                    if field_desc.type == CliArrayOfInt1
149                        column_defs << CliArrayOfInt1 << field << 0
150                        column_values << [value.length].pack("N") << value
151                    else
152                        column_defs << CliAsciiz << field << 0
153                        column_values << [value.length+1].pack("N") << value << 0
154                    end
155                elsif type == Reference
156                    column_defs << CliOid << field << 0
157                    column_values << [value.oid].pack("N")
158                elsif type == TrueClass
159                    column_defs << CliBool << field << 0
160                    column_values << 1
161                elsif type == FalseClass
162                    column_defs << CliBool << field << 0
163                    column_values << 0
164                elsif type == Rectangle
165                    column_defs << CliRectangle << field << 0
166                    column_values << [value.left,value.top,value.right,value.bottom].pack("NNNN")
167                elsif type == NilClass
168                    case field_desc.type
169                        when CliInt1, CliInt2, CliInt4, CliInt8
170                           column_defs << CliInt1 << field << 0
171                           column_values << 0
172                        when CliReal4, CliReal8
173                           column_defs << CliReal4 << field << 0
174                           column_values << [0.0].pack("g")
175                        when CliAsciiz
176                            column_defs << CliAsciiz << field << 0
177                            column_values << [1].pack("N") << 0
178                        when CliOid
179                            column_defs << CliOid << field << 0
180                            column_values << [0].pack("N")
181                        else
182                            raise CliError, "Null value is not allowed for field #{var}"
183                    end
184                elsif type == Array
185                    column_defs << field_desc.type << field << 0
186                    column_values << [value.length].pack("N")
187                    case field_desc.type
188                        when CliArrayOfInt1
189                            column_values << value.pack("c*")
190                        when CliArrayOfBool
191                            for elem in value
192                                column_values << elem ? 1 : 0
193                            end
194                        when CliArrayOfInt2
195                            column_values << value.pack("n*")
196                        when CliArrayOfInt4
197                            column_values << value.pack("N*")
198                        when CliArrayOfInt8
199                            for elem in value
200                                column_values << [elem >> 32, elem & 0xffffffff].pack("NN")
201                            end
202                        when CliArrayOfReal4
203                            column_values << value.pack("g*")
204                        when CliArrayOfReal8
205                            column_values << value.pack("G*")
206                        when CliArrayOfOid
207                            for elem in value
208                                column_values << [elem.to_i].pack("N")
209                            end
210                        when CliArrayOfString
211                            for elem in value
212                                column_values << elem << 0
213                            end
214                        else
215                            raise CliError, "Unsupported element type #{field_desc.type}"
216                    end
217                else
218                    raise CliError, "Unsupported type #{type.name}"
219                end
220            end
221        end
222
223        req = [CliRequestSize + 14 + table.length + column_defs.length + column_values.length, CliCmdPrepareAndInsert, 0].pack("NNN")
224        req << "insert into " << table << 0 << n_columns << column_defs << column_values
225        @socket.send(req, 0)
226        rc=@socket.recv(CliRequestSize).unpack("NNN")
227        raiseError(rc[0]) unless rc[0] == CliOk
228        Reference.new(rc[2]) if rc[2] != 0
229    end
230
231    def loadSchema()
232        sendCommand(CliCmdShowTables)
233        ret = @socket.recv(8).unpack("NN")
234        len = ret[0]
235        n_tables = ret[1]
236        table_names = @socket.recv(len)
237        @tables = {}
238        j = 0
239        for i in 0...n_tables
240            k = table_names.index(0, j)
241            table = table_names[j...k]
242            j = k + 1
243            if Object::const_defined?(table)
244                cls=Object::const_get(table)
245            else
246                cls=Class::new
247                Object::const_set(table, cls)
248            end
249            @socket.send([CliRequestSize + table.length + 1, CliCmdDescribeTable, 0].pack("NNN") << table << 0, 0)
250            ret = @socket.recv(8).unpack("NN")
251            len = ret[0]
252            n_fields = ret[1]
253            field_info = @socket.recv(len)
254            fields = Array.new(n_fields)
255            j = 0
256            for k in 0...n_fields
257                type = field_info[j]
258                j += 1
259                flags = field_info[j]
260                j += 1
261
262                z = field_info.index(0, j)
263                name = field_info[j...z]
264                j = z + 1
265
266                z = field_info.index(0, j)
267                if z != j
268                    ref_table = field_info[j...z]
269                else
270                    ref_table = nil
271                end
272                j = z + 1
273
274                z = field_info.index(0, j)
275                if z != j
276                    inverse_field = field_info[j...z]
277                else
278                    inverse_field = nil
279                end
280                j = z + 1
281
282                fields[k] = FieldDescriptor.new(name, ref_table, inverse_field, type, flags)
283            end
284            @tables[cls.name] = TableDescriptor.new(cls, fields)
285        end
286    end
287
288    def sendCommand(cmd, id=0)
289        @socket.send([CliRequestSize, cmd, id].pack("NNN"), 0)
290    end
291
292    def receive(len)
293        return @socket.recv(len)
294    end
295
296    def sendReceiveCommand(cmd, id=0)
297        sendCommand(cmd, id)
298        rc = @socket.recv(4).unpack("N")[0].hash
299        raiseError(rc, "Send command failed: ") if rc < 0
300        return rc
301    end
302
303    def sendReceiveRequest(req)
304        @socket.send(req, 0)
305        rc = @socket.recv(4).unpack("N")[0].hash
306        raiseError(rc, "Send request failed: ") if rc < 0
307        return rc
308    end
309
310=begin
311=== Close connection with server
312=end                     
313    def close()
314        sendCommand(CliCmdCloseSession)
315        @socket.close()
316        @socket = nil
317    end
318
319    def raiseError(error_code, prefix = "")
320      raise CliError, "#{prefix}#{ERROR_DESCRIPTIONS[error_code]}" if ERROR_DESCRIPTIONS[error_code]
321      raise CliError, "#{prefix}Unknown error code #{error_code}"
322    end
323
324
325    CliRequestSize = 12
326    AtChar = 64
327
328=begin
329== Field flag
330=end
331    CliHashed           = 1  # field should be indexed usnig hash table
332    CliIndexed          = 2  # field should be indexed using B-Tree
333    CliCascadeDelete    = 8   # perfrom cascade delete for for reference or array of reference fields
334    CliAutoincremented  = 16 # field is assigned automaticall incremented value
335
336=begin
337== Operation result codes
338=end
339    CliOk=0
340    CliBadAddress = -1
341    CliConnectionRefused = -2
342    CliDatabaseNotFound = -3
343    CliBadStatement = -4
344    CliParameterNotFound = -5
345    CliUnboundParameter = -6
346
347    CliColumnNotFound = -7
348    CliIncompatibleType = -8
349    CliNetworkError = -9
350    CliRuntimeError = -10
351    CliClosedStatement = -11
352    CliUnsupportedType = -12
353    CliNotFound = -13
354    CliNotUpdateMode = -14
355    CliTableNotFound = -15
356    CliNotAllColumnsSpecified = -16
357    CliNotFetched = -17
358    CliAlreadyUpdated = -18
359    CliTableAlreadyExists = -19
360    CliNotImplemented = -20
361    CliLoginFailed = -21
362    CliEmptyParameter = -22
363    CliClosedConnection = -23
364
365    ERROR_DESCRIPTIONS = {
366      CliBadAddress => "Bad address",
367      CliConnectionRefused => "Connection refused",
368      CliDatabaseNotFound => "Database not found",
369      CliBadStatement => "Bad statement",
370      CliParameterNotFound => "Parameter not found",
371      CliUnboundParameter => "Unbound parameter",
372      CliColumnNotFound => "Column not found",
373      CliIncompatibleType => "Incomptaible type",
374      CliNetworkError => "Network error",
375      CliRuntimeError => "Runtime error",
376      CliClosedStatement => "Closed statement",
377      CliUnsupportedType => "Unsupported type",
378      CliNotFound => "Not found",
379      CliNotUpdateMode => "Not update mode",
380      CliTableNotFound => "Table not found",
381      CliNotAllColumnsSpecified => "Not all columns specified",
382      CliNotFetched => "Not fetched",
383      CliAlreadyUpdated => "Already updated",
384      CliTableAlreadyExists => "Table already exists",
385      CliNotImplemented => "Not implemented",
386      CliLoginFailed => "Login failed",
387      CliEmptyParameter => "Empty parameter",
388      CliClosedConnection => "Closed connection"}
389
390=begin
391== Command codes
392=end
393    CliCmdCloseSession = 0
394    CliCmdPrepareAndExecute = 1
395    CliCmdExecute = 2
396    CliCmdGetFirst = 3
397    CliCmdGetLast = 4
398    CliCmdGetNext = 5
399    CliCmdGetPrev = 6
400    CliCmdFreeStatement = 7
401    CliCmdAbort = 8
402    CliCmdCommit = 9
403    CliCmdUpdate = 10
404    CliCmdRemove = 11
405    CliCmdRemoveCurrent = 12
406    CliCmdInsert = 13
407    CliCmdPrepareAndInsert = 14
408    CliCmdDescribeTable = 15
409    CliCmdShowTables = 16
410    CliCmdPrecommit = 17
411    CliCmdSkip = 18
412    CliCmdCreateTable = 19
413    CliCmdDropTable = 20
414    CliCmdAlterIndex = 21
415    CliCmdFreeze = 22
416    CliCmdUnfreeze = 23
417    CliCmdSeek = 24
418    CliCmdAlterTable = 25
419    CliCmdLock = 26
420
421=begin
422== Field type codes
423=end
424    CliOid =  0
425    CliBool = 1
426    CliInt1 = 2
427    CliInt2 = 3
428    CliInt4 = 4
429    CliInt8 = 5
430    CliReal4 = 6
431    CliReal8 = 7
432    CliDecimal = 8
433    CliAsciiz = 9
434    CliPasciiz = 10
435    CliCstring = 11
436    CliArrayOfOid =  12
437    CliArrayOfBool = 13
438    CliArrayOfInt1 = 14
439    CliArrayOfInt2 = 15
440    CliArrayOfInt4 = 16
441    CliArrayOfInt8 = 17
442    CliArrayOfReal4 = 18
443    CliArrayOfReal8 = 19
444    CliArrayOfDecimal = 20
445    CliArrayOfString = 21
446    CliAny =           22
447    CliDatetime  =     23
448    CliAutoincrement  = 24
449    CliRectangle =     25
450    CliUndefined =     26
451
452    attr_reader :tables
453end
454
455
456=begin
457== Statement class is used to prepare and execute select statement
458=end
459class Statement
460  private
461    PERCENT=37
462    QUOTE=39
463    LETTER_A=65
464    LETTER_Z=90
465    LETTER_a=97
466    LETTER_z=122
467    DIGIT_0=48
468    DIGIT_9=57
469    UNDERSCORE=95
470
471  public
472=begin
473=== Statement constructor called by Connection class
474=end                     
475    def initialize(con, sql, stmt_id)
476        @con = con
477        @stmt_id = stmt_id
478        r = sql.match(/\s+from\s+([^\s]+)/i)
479        raise CliError, "Bad statement: table name is expected after FROM" unless r
480        table_name = r[1]
481        @table = con.tables[table_name]
482        in_quotes = false
483        param_name = nil
484        @param_hash = {}
485        @param_list = []
486        req_str=""
487        sql.each_byte do |ch|
488            if ch == QUOTE
489                in_quotes = !in_quotes
490                req_str << ch
491            elsif ch == PERCENT and !in_quotes
492                param_name=""
493            elsif param_name != nil and ((ch >= LETTER_a and ch <= LETTER_z) or (ch >= LETTER_A and ch <= LETTER_Z) or (ch >= DIGIT_0 and ch <= DIGIT_9) or ch == UNDERSCORE)
494                param_name << ch
495            else
496                if param_name != nil
497                    p = Parameter.new(param_name)
498                    @param_list << p
499                    @param_hash[param_name] = p
500                    param_name = nil
501                    req_str << 0
502                end
503                req_str << ch
504            end
505        end
506        if param_name != nil
507            p = Parameter.new(param_name)
508            @param_list << p
509            @param_hash[param_name] = p
510            req_str << 0
511        end
512        if req_str.length == 0 or req_str[-1] != 0
513            req_str << 0
514        end
515        @stmt = req_str
516        @prepared = false
517    end
518
519=begin
520=== Get parameter value
521=end
522    def [](param_name)
523        return @param_hash[param_name].value
524    end
525
526
527=begin
528=== Assign value to the statement parameter
529=end
530    def []=(param_name, value)
531        @param_hash[param_name].value = value
532    end
533
534
535=begin
536=== Prepare (if needed) and execute select statement
537    Only object set returned by the select for updated statement allows
538    update and deletion of the objects.
539==== for_update - if cursor is opened in for update mode
540==== Rerturns object set with the selected objects
541=end        
542    def fetch(for_update = false)
543        cmd=Connection::CliCmdExecute
544        req=""
545        if !@prepared
546            cmd=Connection::CliCmdPrepareAndExecute
547            @prepared=true
548            req << @param_list.length << @table.fields.length << [@stmt.length + @param_list.length].pack("n")
549            param_no=0
550            @stmt.each_byte do |ch|
551                req << ch
552                if ch == 0 and param_no < @param_list.length
553                    param = @param_list[param_no]
554                    if param.type == Connection::CliUndefined
555                        raise CliError, "Unbound parameter #{param.name}"
556                    end
557                    param_no += 1
558                    req << param.type
559                end
560            end
561            for field in @table.fields
562                 req << field.type << field.name << 0
563            end
564        end
565        @for_update = for_update
566        req << for_update ? 1 : 0
567        for param in @param_list
568            case param.type
569                when Connection::CliOid
570                    req << [param.value.to_i].pack("N")
571                when Connection::CliBool
572                    req << param.value ? 1 : 0
573                when Connection::CliInt4
574                    req << [param.value].pack("N")
575                when Connection::CliInt8
576                    req << [param.value >> 32, param.value & 0xffffffff].pack("NN")
577                when Connection::CliReal8
578                    req << [param.value].pack("G")
579                when Connection::CliAsciiz
580                    req << param.value << 0
581                when Connection::CliRectangle
582                    req << [param.value.left, param.value.top, param.value.right, param.value.bottom].pack("NNNN")
583                else
584                    raise CliError, "Unsupported parameter type #{param.type}"
585            end
586        end
587        req = [req.length + Connection::CliRequestSize, cmd, @stmt_id].pack("NNN") + req
588        return ResultSet.new(self, con.sendReceiveRequest(req))
589    end
590
591=begin
592=== Close statement
593=end                     
594    def close()
595	if con.nil?
596	    raise CliError, "Statement already closed"
597	end
598        @con.sendCommand(Connection::CliCmdFreeStatement)
599        @con = nil
600    end
601
602    attr_reader :for_update, :con, :stmt_id, :table
603end
604
605=begin
606== Rectangle class for spatial coordinates
607=end                     
608class Rectangle
609=begin
610=== Rectangle constructor
611=end                     
612    def initialize(left,top,right,bottom)
613       @left = left
614       @right = right
615       @top = top
616       @bottom = bottom
617    end
618
619    def to_s()
620       return "<#{@left}, #{@top}, #{@right}, #{@bottom}>"
621    end
622
623    def inspect
624       return "<Rectangle:0x#{"%x" % object_id} #{@left.inspect}, #{@top.inspect}, #{@right.inspect}, #{@bottom.inspect}>"
625    end
626
627    attr_reader :left, :right, :top, :bottom
628end
629
630=begin
631== Descriptor of database table
632=end                     
633class TableDescriptor
634=begin
635=== Class descriptor constructor
636==== cls - class
637==== fields - array of FieldDescriptor
638=end                     
639    def initialize(cls, fields)
640        @cls=cls
641        @fields=fields
642        @fields_map={}
643        for field in fields
644            @fields_map[field.name]=field
645        end
646    end
647
648    def [](field_name)
649        return @fields_map[field_name]
650    end
651
652    attr_reader :cls, :fields
653end
654
655=begin
656== Descriptor of database table field
657=end                     
658class FieldDescriptor
659    def initialize(name, ref_table, inverse_field, type, flags)
660       @name = name
661       @type = type
662       @flags = flags
663       @ref_table = ref_table
664       @inverse_field = inverse_field
665    end
666
667    attr_reader :name, :ref_table, :inverse_field, :type, :flags
668end
669
670=begin
671== Reference to the persistent object
672=end                     
673class Reference
674    def initialize(oid)
675        @oid=oid
676    end
677
678    def to_i
679        return @oid
680    end
681
682    def to_s()
683        return "##{@oid}"
684    end
685
686    attr_reader :oid
687end
688
689=begin
690== Statement parameter
691=end                     
692class Parameter
693   def initialize(name)
694       @name = name
695       @type = Connection::CliUndefined
696   end
697
698   def value=(v)
699       @value = v
700       type = value.class
701       if type == Fixnum
702           @type = Connection::CliInt4
703       elsif type == Bignum
704           @type = Connection::CliInt8
705       elsif type == Float
706           @type = Connection::CliReal8
707       elsif type == String
708           @type = Connection::CliAsciiz
709       elsif type == Reference
710           @type = Connection::CliOid
711       elsif type == TrueClass or type == FalseClass
712           @type = Connection::CliBool
713       elsif type == Rectangle
714           @type = Connection::CliRectangle
715       else
716           raise CliError, "Unsupported parameter type #{value.class.name}"
717       end
718   end
719
720   attr_reader :name, :type, :value
721end
722
723=begin
724== CLI exception class
725=end    
726class CliError < RuntimeError
727end
728
729=begin
730== Set of objects returned by select. This class allows navigation though the selected objects in orward or backward direction
731=end    
732class ResultSet
733    def initialize(stmt, n_objects)
734        @stmt = stmt
735        @n_objects = n_objects
736        @updated = false
737        @curr_oid = 0
738        @curr_obj = nil
739    end
740
741=begin
742=== Get first selected object
743==== Returns first object in the set or nil if no objects were selected
744=end
745    def first()
746        return getObject(Connection::CliCmdGetFirst)
747    end
748
749=begin
750=== Get last selected object
751==== Returns last object in the set or nil if no objects were selected
752=end
753    def last()
754        return getObject(Connection::CliCmdGetLast)
755    end
756
757=begin
758=== Get next selected object
759==== Returns next object in the set or nil if current object is the last one in the
760    set or no objects were selected
761=end
762    def next()
763        return getObject(Connection::CliCmdGetNext)
764    end
765
766=begin
767=== Get previous selected object
768==== Returns previous object in the set or nil if the current object is the first
769    one in the set or no objects were selected
770=end
771    def prev()
772        return getObject(Connection::CliCmdGetPrev)
773    end
774
775=begin
776=== Skip specified number of objects.
777=== if ((|n|)|)) is positive, then this method has the same effect as
778    executing getNext() mehod ((|n|)) times.
779=== if ((|n|)) is negative, then this method has the same effect of
780    executing getPrev() mehod ((|-n|)) times.
781=== if ((|n|)) is zero, this method has no effect
782==== n - number of objects to be skipped
783==== Returns object ((|n|)) positions relative to the current position
784=end
785    def skip(n)
786        return getObject(Connection::CliCmdSkip, n)
787    end
788
789=begin
790=== Get reference to the current object
791==== Return return reference to the current object or nil if no objects were selected
792=end
793    def ref()
794        if @curr_oid != 0
795            return Reference.new(@curr_oid)
796        end
797        return nil
798    end
799
800=begin
801=== Update the current object in the set. Changes made in the current object
802    are saved in the database
803=end
804    def update()
805        if @stmt.nil?
806            raise CliError, "ResultSet was aleady closed"
807        end
808        if @stmt.con.nil?
809            raise CliError, "Statement was closed"
810        end
811        if @curr_oid == 0
812            raise CliError, "No object was selected"
813        end
814        if !@stmt.for_update
815            raise CliError, "Updates not allowed"
816        end
817        if @updated
818            raise CliError, "Record was already updated"
819        end
820        @updated=true
821        column_values=""
822        obj=@curr_obj
823        is_struct=obj.is_a?(Struct)
824
825        for field in @stmt.table.fields
826            if is_struct
827                value = obj[field.name]
828            else
829                value = obj.instance_variable_get("@#{field.name}")
830            end
831            case field.type
832                when Connection::CliBool
833                    column_values << value ? 1 : 0
834                when Connection::CliInt1
835                    column_values << value.to_i
836                when Connection::CliInt2
837                    column_values << [value.to_i].pack("n")
838                when Connection::CliInt4
839                    column_values << [value.to_i].pack("N")
840                when Connection::CliInt8
841                    column_values << [value.to_i >> 32, value.to_i & 0xffffffff].pack("NN")
842                when Connection::CliReal4
843                    column_values << [value.to_f].pack("g")
844                when Connection::CliReal8
845                    column_values << [value.to_f].pack("G")
846                when Connection::CliAsciiz
847                    if value.nil?
848                        column_values << [1].pack("N") << 0
849                    else
850                        column_values << [value.length+1].pack("N") << value << 0
851                    end
852                when Connection::CliOid
853                    if value.nil?
854                        column_values << [0].pack("N")
855                    else
856                        column_values << [value.to_i].pack("N")
857                    end
858                when Connection::CliRectangle
859                    column_values << [value.left,value.top,value.right,value.bottom].pack("NNNN")
860                when Connection::CliArrayOfInt1
861                    column_values << [value.length].pack("N") << value.pack("c*")
862                when Connection::CliArrayOfBool
863                    column_values << [value.length].pack("N")
864                    for elem in value
865                        column_values << elem ? 1 : 0
866                    end
867                when Connection::CliArrayOfInt2
868                    column_values << [value.length].pack("N") << value.pack("n*")
869                when Connection::CliArrayOfInt4
870                    column_values << [value.length].pack("N") << value.pack("N*")
871                when Connection::CliArrayOfInt8
872                    column_values << [value.length].pack("N")
873                    for elem in value
874                        column_values << [elem >> 32, elem & 0xffffffff].pack("NN")
875                    end
876                when Connection::CliArrayOfReal4
877                    column_values << [value.length].pack("N") << value.pack("g*")
878                when Connection::CliArrayOfReal8
879                    column_values << [value.length].pack("N") << value.pack("G*")
880                when Connection::CliArrayOfOid
881                    column_values << [value.length].pack("N")
882                    for elem in value
883                        column_values << [elem.to_i].pack("N")
884                    end
885                when Connection::CliArrayOfString
886                    column_values << [value.length].pack("N")
887                    for elem in value
888                        column_values << elem << 0
889                    end
890                else
891                    raise CliError, "Unsupported type #{field.type}"
892             end
893        end
894        req = [Connection::CliRequestSize + column_values.length, Connection::CliCmdUpdate, @stmt.stmt_id].pack("NNN") + column_values
895        @stmt.con.sendReceiveRequest(req)
896   end
897
898=begin
899=== Remove all selected objects.
900    All objects in the object set are removed from the database.
901=end
902    def removeAll()
903        if @stmt.nil?
904            raise CliError, "ResultSet was aleady closed"
905        end
906        if @stmt.con.nil?
907            raise CliError, "Statement was closed"
908        end
909        if !@stmt.for_update
910            raise CliError, "Updates not allowed"
911        end
912        @stmt.con.sendReceiveCommand(Connection::CliCmdRemove, @stmt.stmt_id)
913    end
914
915=begin
916=== Remove current object.
917    All objects in the object set are removed from the database.
918=end
919    def removeCurrent()
920        if @stmt.nil?
921            raise CliError, "ResultSet was aleady closed"
922        end
923        if @stmt.con.nil?
924            raise CliError, "Statement was closed"
925        end
926        if !@stmt.for_update
927            raise CliError, "Updates not allowed"
928        end
929        if @curr_oid == 0
930            raise CliError, "No object was selected"
931        end
932        @stmt.con.sendReceiveCommand(Connection::CliCmdRemoveCurrent, @stmt.stmt_id)
933    end
934
935=begin
936=== Get the number of objects in the object set.
937==== Returna number of the selected objects
938=end
939    def size()
940        return @n_objects
941    end
942
943=begin
944=== Close result set and statement. Any followin operation with this result set or statement will raise an exception.
945=end
946    def close()
947        @stmt.close()
948        @stmt = nil
949    end
950
951=begin
952=== Iterator through result set
953=end
954    def each()
955       if first != nil
956           yield @curr_obj
957           while self.next != nil
958               yield @curr_obj
959           end
960       end
961    end
962
963
964    def getObject(cmd, n=0)
965        if @stmt.nil?
966            raise CliError, "ResultSet was aleady closed"
967        end
968        if @stmt.con.nil?
969            raise CliError, "Statement was closed"
970        end
971        if cmd == Connection::CliCmdSkip
972            @socket.send([16, cmd, @stmt.stmt_id, n].pack("NNNN"), 0)
973        else
974            @stmt.con.sendCommand(cmd, @stmt.stmt_id)
975        end
976        rc = @stmt.con.receive(4).unpack("N")[0].hash
977        if rc == Connection::CliNotFound
978            return nil
979        elsif rc <= 0
980            @stmt.con.raiseError(rc, "Failed to get object: ")
981        end
982        resp = @stmt.con.receive(rc-4)
983        @curr_oid = resp.unpack("N")[0]
984        @updated = false
985        @curr_obj = nil
986        if @curr_oid == 0
987            return nil
988        end
989        obj = @stmt.table.cls.allocate
990        @curr_obj = obj
991        is_struct = obj.is_a?(Struct)
992        i = 4
993        for field in @stmt.table.fields
994            type = resp[i]
995            if field.type != type
996                raise CliError, "Unexpected type of column: #{type} instead of #{field.type}"
997            end
998            i += 1
999            case type
1000                when Connection::CliBool
1001                    value = (resp[i] != 0)
1002                    i += 1
1003                when Connection::CliInt1
1004                    value = resp[i]
1005                    i += 1
1006                when Connection::CliInt2
1007                    value = resp[i,2].unpack("n")[0]
1008                    i += 2
1009                when Connection::CliInt4
1010                    value = resp[i,4].unpack("N")[0]
1011                    i += 4
1012                when Connection::CliInt8
1013                    word = resp[i,8].unpack("NN")
1014                    value = (word[0] << 32) | (word[1] & 0xffffffff)
1015                    i += 8
1016                when Connection::CliReal4
1017                    value = resp[i,4].unpack("g")[0]
1018                    i += 4
1019                when Connection::CliReal8
1020                    value = resp[i,8].unpack("G")[0]
1021                    i += 8
1022                when Connection::CliAsciiz
1023                    len = resp[i,4].unpack("N")[0]
1024                    value = resp[i+4, len-1]
1025                    i += len + 4
1026                when Connection::CliOid
1027                    value = Reference.new(resp[i,4].unpack("N")[0])
1028                    i += 4
1029                when Connection::CliRectangle
1030                    coord = resp[i, 16].unpack("NNNN")
1031                    value = Rectangle.new(coord[0], coord[1], coord[2], coord[3])
1032                when Connection::CliArrayOfInt1
1033                    len = resp[i,4].unpack("N")[0]
1034                    i += 4
1035                    value = resp[i, len]
1036                    i += len
1037                when Connection::CliArrayOfBool
1038                    value = Array.new(resp[i,4].unpack("N")[0])
1039                    i += 4
1040                    for j in 0...value.length
1041                        value[j] = resp[i] != 0
1042                        i += 1
1043                    end
1044                when Connection::CliArrayOfInt2
1045                    len = resp[i,4].unpack("N")[0]
1046                    i += 4
1047                    value = resp[i, len*2].unpack("n*")
1048                    i += len*2
1049                when Connection::CliArrayOfInt4
1050                    len = resp[i,4].unpack("N")[0]
1051                    i += 4
1052                    value = resp[i, len*4].unpack("N*")
1053                    i += len*4
1054                when Connection::CliArrayOfInt8
1055                    len = resp[i,4].unpack("N")[0]
1056                    i += 4
1057                    word = resp[i, len*8].unpack("N*")
1058                    value = Array.new(len)
1059                    for j in 0...value.length
1060                        value[j] = (word[j*2] << 32) | (word[j*2+1] & 0xffffffff)
1061                    end
1062                    i += len*8
1063                when Connection::CliArrayOfReal4
1064                    len = resp[i,4].unpack("N")[0]
1065                    i += 4
1066                    value = resp[i, len*4].unpack("g*")
1067                    i += len*4
1068                when Connection::CliArrayOfReal8
1069                    len = resp[i,4].unpack("N")[0]
1070                    i += 4
1071                    value = resp[i, len*8].unpack("G*")
1072                    i += len*8
1073                when Connection::CliArrayOfOid
1074                    len = resp[i,4].unpack("N")[0]
1075                    value = Array.new(len)
1076                    i += 4
1077                    oid = resp[i, len*4].unpack("N*")
1078                    for j in 0...len
1079                        value[j] = Reference.new(oid[j])
1080                    end
1081                    i += len*4
1082                when Connection::CliArrayOfString
1083                    value = Array.new(resp[i,4].unpack("N")[0])
1084                    i += 4
1085                    for j in 0...value.length
1086                        k = resp.index(0, i)
1087                        value[j] = resp[i...k]
1088                        i = k + 1
1089                    end
1090                else
1091                    raise CliError, "Unsupported type #{type}"
1092            end
1093            if is_struct
1094                obj[field.name] = value
1095            else
1096                obj.instance_variable_set("@#{field.name}", value)
1097            end
1098        end
1099        return obj
1100    end
1101end
1102
1103end
1104