File Coverage

blib/lib/PGObject.pm
Criterion Covered Total %
statement 37 128 28.9
branch 1 44 2.2
condition 2 24 8.3
subroutine 11 14 78.5
pod 6 6 100.0
total 57 216 26.3


line stmt bran cond sub pod time code
1              
2             =head1 NAME
3              
4             PGObject - A toolkit integrating intelligent PostgreSQL dbs into Perl objects
5              
6             =cut
7              
8             package PGObject;
9              
10 6     6   959027 use strict;
  6         15  
  6         234  
11 6     6   42 use warnings;
  6         11  
  6         606  
12              
13 6     6   3458 use Carp::Clan qr/^PGObject\b/;
  6         27022  
  6         59  
14 6     6   3935 use Log::Any qw($log);
  6         58542  
  6         36  
15 6     6   18382 use Memoize;
  6         16121  
  6         465  
16 6     6   3520 use PGObject::Util::DBException;
  6         21  
  6         251  
17              
18 6     6   3290 use PGObject::Type::Registry;
  6         21  
  6         11668  
19              
20             =head1 VERSION
21              
22             Version 2.4.0
23              
24             =cut
25              
26             our $VERSION = '2.4.0';
27              
28             =head1 SYNPOSIS
29              
30             To use without caching:
31              
32             use PGObject;
33              
34             To use with caching:
35              
36             use PGObject ':cache';
37              
38             To get basic info from a function
39              
40             my $f_info = PGObject->function_info(
41             dbh => $dbh,
42             funcname => $funcname,
43             funcschema => 'public',
44             );
45              
46             To get info about a function, filtered by first argument type
47              
48             my $f_info = PGObject->function_info(
49             dbh => $dbh,
50             funcname => $funcname,
51             funcschema => 'public',
52             funcprefix => 'test__',
53             objtype => 'invoice',
54             objschema => 'public',
55             );
56              
57             To call a function with enumerated arguments
58              
59             my @results = PGObject->call_procedure(
60             dbh => $dbh,
61             funcname => $funcname,
62             funcprefix => 'test__',
63             funcschema => $funcname,
64             args => [$arg1, $arg2, $arg3],
65             );
66              
67             To do the same with a running total
68              
69             my @results = PGObject->call_procedure(
70             dbh => $dbh,
71             funcname => $funcname,
72             funcschema => $funcname,
73             args => [$arg1, $arg2, $arg3],
74             running_funcs => [{agg => 'sum(amount)', alias => 'running_total'}],
75             );
76              
77             =cut
78              
79             sub import {
80 5     5   77 my @directives = @_;
81 5 50       11 memoize 'function_info' if grep { $_ eq ':cache' } @directives;
  7         36  
82             PGObject::Type::Registry->new_registry($_)
83 5         11 for grep { $_ !~ /^\:/; } @directives;
  7         42  
84             }
85              
86             =head1 DESCRIPTION
87              
88             PGObject contains the base routines for object management using discoverable
89             stored procedures in PostgreSQL databases. This module contains only common
90             functionality and support structures, and low-level API's. Most developers will
91             want to use more functional modules which add to these functions.
92              
93             The overall approach here is to provide the basics for a toolkit that other
94             modules can extend. This is thus intended to be a component for building
95             integration between PostgreSQL user defined functions and Perl objects.
96              
97             Because decisions such as state handling are largely outside of the scope of
98             this module, this module itself does not do any significant state handling.
99             Database handles (using DBD::Pg 2.0 or later) must be passed in on every call.
100             This decision was made in order to allow for diversity in this area, with the
101             idea that wrapper classes would be written to implement this.
102              
103             =head1 FUNCTIONS
104              
105             =head2 clear_info_cache
106              
107             This function clears the info cache if this was loaded with caching enabled.
108              
109             The cache is also automatically cleared when a function that was run could not
110             be found (this could be caused by updating the db).
111              
112             =cut
113              
114             sub clear_info_cache {
115 0     0 1 0 local ($@);
116 0         0 eval { Memoize::flush_cache('function_info') };
  0         0  
117             }
118              
119             =head2 function_info(%args)
120              
121             Arguments:
122              
123             =over
124              
125             =item dbh (required)
126              
127             Database handle
128              
129             =item funcname (required)
130              
131             function name
132              
133             =item funcschema (optional, default 'public')
134              
135             function schema
136              
137             =item funcprefix (optiona, default '')
138              
139             Prefix for the function. This can be useful for separating functions by class.
140              
141             =item argtype1 (optional)
142              
143             Name of first argument type. If not provided, does not filter on this criteria.
144              
145             =item argschema (optional)
146              
147             Name of first argument type's schema. If not provided defaults to 'public'
148              
149             =back
150              
151             This function looks up basic mapping information for a function. If more than
152             one function is found, an exception is raised. This function is primarily
153             intended to be used by packages which extend this one, in order to accomplish
154             stored procedure to object mapping.
155              
156             Return data is a hashref containing the following elements:
157              
158             =over
159              
160             =item args
161              
162             This is an arrayref of hashrefs, each of which contains 'name' and 'type'
163              
164             =item name
165              
166             The name of the function
167              
168             =item num_args
169              
170             The number of arguments
171              
172             =back
173              
174             =cut
175              
176             sub _dup_func() { '42A01' }
177             sub _no_func() { '26A01' }
178              
179             sub function_info {
180 0     0 1 0 my ( $self, %args ) = @_;
181 0   0     0 $args{funcschema} ||= 'public';
182 0   0     0 $args{funcprefix} ||= '';
183 0         0 $args{funcname} = $args{funcprefix} . $args{funcname};
184 0   0     0 $args{argschema} ||= 'public';
185              
186 0   0     0 my $dbh = $args{dbh} || croak $log->error( 'No dbh provided' );
187              
188 0         0 my $query = qq|
189             SELECT proname, pronargs, proargnames,
190             string_to_array(array_to_string(proargtypes::regtype[], ' '),
191             ' ') as argtypes
192             FROM pg_proc
193             JOIN pg_namespace pgn ON pgn.oid = pronamespace
194             WHERE proname = ? AND nspname = ?
195             |;
196 0         0 my @queryargs = ( $args{funcname}, $args{funcschema} );
197 0 0       0 if ( $args{argtype1} ) {
198 0         0 $query .= qq|
199             AND (proargtypes::int[])[0] IN (select t.oid
200             from pg_type t
201             join pg_namespace n
202             ON n.oid = typnamespace
203             where typname = ?
204             AND n.nspname = ?
205             )|;
206 0         0 push @queryargs, $args{argtype1};
207 0         0 push @queryargs, $args{argschema};
208             }
209              
210 0   0     0 my $sth = $dbh->prepare($query) || die PGObject::Util::DBException->new(
211             $dbh, $query, @queryargs);
212 0 0       0 $sth->execute(@queryargs) || die PGObject::Util::DBException->new(
213             $dbh, $query, @queryargs);
214 0         0 my $rows = $sth->rows;
215 0 0       0 if ($rows > 1) {
    0          
216 0 0       0 if ($args{argtype1}) {
217             my $e = PGObject::Util::DBException->internal(_dup_func,
218             sprintf('Ambiguous criteria discovering function %s.%s (with first argument type %s)',
219             $args{funcschema}, $args{funcname}, $args{argtype1}
220 0         0 ), $query, @queryargs);
221 0         0 croak $e;
222             }
223             else {
224             my $e = PGObject::Util::DBException->internal(_dup_func,
225             sprintf('Ambiguous criteria discovering function %s.%s',
226             $args{funcschema}, $args{funcname}
227 0         0 ), $query, @queryargs);
228 0         0 croak $e;
229             }
230             }
231             elsif ($rows == 0) {
232             my $e = PGObject::Util::DBException->internal(_no_func,
233             sprintf('No such function: %s.%s',
234 0         0 $args{funcschema}, $args{funcname} ),
235             $query, @queryargs);
236              
237 0         0 croak $e;
238             }
239 0         0 my $ref = $sth->fetchrow_hashref('NAME_lc');
240              
241 0         0 my $f_args;
242 0         0 for my $n ( @{ $ref->{proargnames} } ) {
  0         0  
243 0         0 push @$f_args, { name => $n, type => shift @{ $ref->{argtypes} } };
  0         0  
244             }
245              
246             return {
247             name => $ref->{proname},
248             num_args => $ref->{pronargs},
249 0         0 args => $f_args,
250             };
251              
252             }
253              
254             =head2 call_procedure(%args)
255              
256             Arguments:
257              
258             =over
259              
260             =item funcname
261              
262             The function name
263              
264             =item funcschema
265              
266             The schema in which the function resides
267              
268             =item funcprefix (optiona, default '')
269              
270             Prefix for the function. This can be useful for separating functions by class.
271              
272             =item args
273              
274             This is an arrayref. Each item is either a literal value, an arrayref, or a
275             hashref of extended information. In the hashref case, the type key specifies
276             the string to use to cast the type in, and value is the value.
277              
278             =item orderby
279              
280             The list (arrayref) of columns on output for ordering.
281              
282             =item running_funcs
283              
284             An arrayref of running windowed aggregates. Each contains two keys, namely 'agg' for the aggregate and 'alias' for the function name.
285              
286             These are aggregates, each one has appended 'OVER (ROWS UNBOUNDED PRECEDING)'
287             to it.
288              
289             =item registry
290              
291             This is the name of the registry used for type conversion. It can be omitted
292             and defaults to 'default.' Note that use of a non-standard registry currently
293             does *not* merge changes from the default registry, so you need to reregister
294             types in non-default registries when you create them.
295              
296             Please note, these aggregates are not intended to be user-supplied. Please only
297             allow whitelisted values here or construct in a tested framework elsewhere.
298             Because of the syntax here, there is no sql injection prevention possible at
299             the framework level for this parameter.
300              
301             =back
302              
303             =cut
304              
305             sub call_procedure {
306 0     0 1 0 my ( $self, %args ) = @_;
307 0         0 local $@;
308 0   0     0 $args{funcschema} ||= 'public';
309 0   0     0 $args{funcprefix} ||= '';
310 0         0 $args{funcname} = $args{funcprefix} . $args{funcname};
311 0   0     0 $args{registry} ||= 'default';
312              
313 0         0 my $dbh = $args{dbh};
314 0 0       0 croak $log->error( "No database handle provided" )
315             unless $dbh;
316             croak $log->error( "dbh not a database handle" )
317 0 0       0 unless eval { $dbh->isa('DBI::db') };
  0         0  
318              
319 0         0 my $wf_string = '';
320              
321             $wf_string = join ', ', map {
322             $_->{agg}
323             . ' OVER (ROWS UNBOUNDED PRECEDING) AS '
324             . $_->{alias}
325 0 0       0 } @{ $args{running_funcs} } if $args{running_funcs};
  0         0  
  0         0  
326 0 0       0 $wf_string = ', ' . $wf_string if $wf_string;
327              
328             my @qargs = map {
329 0         0 my $arg = $_;
330 0         0 local ($@);
331 0 0       0 $arg = $arg->to_db if eval { $arg->can('to_db') };
  0         0  
332 0 0       0 $arg = $arg->pgobject_to_db if eval { $arg->can('pgobject_to_db') };
  0         0  
333 0         0 $arg;
334 0         0 } @{ $args{args} };
  0         0  
335              
336             my $argstr = join ', ', map {
337 0 0 0     0 ( ref $_ and eval { $_->{cast} } ) ? "?::$_->{cast}" : '?';
338 0         0 } @{ $args{args} };
  0         0  
339              
340 0         0 my $order = '';
341 0 0       0 if ( $args{orderby} ) {
342             $order = join(
343             ', ',
344             map {
345 0         0 my $dir = undef;
346 0 0       0 if (s/\s+(ASC|DESC)\s*$//i) {
347 0         0 $dir = $1;
348             }
349 0 0       0 defined $dir
350             ? $dbh->quote_identifier($_) . " $dir"
351             : $dbh->quote_identifier($_);
352 0         0 } @{ $args{orderby} }
  0         0  
353             );
354             }
355             my $query = qq|
356             SELECT * $wf_string
357             FROM |
358             . $dbh->quote_identifier( $args{funcschema} ) . '.'
359             . $dbh->quote_identifier( $args{funcname} )
360 0         0 . qq|($argstr) |;
361 0 0       0 if ($order) {
362 0         0 $query .= qq|
363             ORDER BY $order |;
364             }
365              
366 0   0     0 my $sth = $dbh->prepare($query) || die PGObject::Util::DBException(
367             $dbh, $query, @qargs);
368              
369 0         0 my $place = 1;
370              
371 0         0 foreach my $carg (@qargs) {
372 0 0       0 if ( ref($carg) =~ /HASH/ ) {
373             $sth->bind_param( $place, $carg->{value},
374 0         0 { pg_type => $carg->{type} } );
375             }
376             else {
377              
378             # This is used to support arrays of db-aware types. Long-run
379             # I think we should merge bytea support into this framework. --CT
380 0 0       0 if ( ref($carg) =~ /ARRAY/ ) {
381 0         0 local ($@);
382 0 0       0 if ( eval { $carg->[0]->can('to_db') } ) {
  0         0  
383 0         0 for my $ref (@$carg) {
384 0         0 $ref = $ref->to_db;
385             }
386             }
387             }
388              
389 0         0 $sth->bind_param( $place, $carg );
390             }
391 0         0 ++$place;
392             }
393              
394 0 0       0 $sth->execute() || die PGObject::Util::DBException->new($dbh, $query, @qargs);
395              
396 0 0       0 clear_info_cache() if $dbh->state eq '42883'; # (No Such Function)
397              
398 0         0 my @rows = ();
399             my $row_deserializer =
400             PGObject::Type::Registry->rowhash_deserializer(
401             registry => $args{registry},
402             types => $sth->{pg_type},
403             columns => $sth->{NAME_lc},
404 0         0 );
405 0         0 while (my $row = $sth->fetchrow_hashref('NAME_lc')) {
406 0         0 push @rows, $row_deserializer->( $row );
407             }
408 0         0 return @rows;
409             }
410              
411             =head2 new_registry($registry_name)
412              
413             Creates a new registry if it does not exist. This is useful when segments of
414             an application must override existing type mappings.
415              
416             This is deprecated and throws a warning.
417              
418             Use PGObject::Type::Registry->new_registry($registry_name) instead.
419              
420             This no longer returns anything of significance.
421              
422             =cut
423              
424             sub new_registry {
425 5     5 1 259 my ( $self, $registry_name ) = @_;
426 5         31 carp $log->warn( "Deprecated use of PGObject->new_registry()" );
427 5         1256 PGObject::Type::Registry->new_registry($registry_name);
428             }
429              
430             =head2 register_type(pgtype => $tname, registry => $regname, perl_class => $pm)
431              
432             DEPRECATED
433              
434             Registers a type as a class. This means that when an attribute of type $pg_type
435             is returned, that PGObject will automatically return whatever
436             $perl_class->from_db returns. This allows you to have a db-specific constructor
437             for such types.
438              
439             The registry argument is optional and defaults to 'default'
440              
441             If the registry does not exist, an error is raised. if the pg_type is already
442             registered to a different type, this returns 0. Returns 1 on success.
443              
444             Use PGObject::Type::Registry->register_type() instead.
445              
446             =cut
447              
448             sub register_type {
449 11     11 1 202697 carp $log->warn( 'Use of deprecated method register_type of PGObject module' );
450 11         1843 my ( $self, %args ) = @_;
451              
452             PGObject::Type::Registry->register_type(
453             registry => $args{registry},
454             dbtype => $args{pg_type},
455             apptype => $args{perl_class}
456 11         102 );
457 7         57 return 1;
458             }
459              
460             =head2 unregister_type(pgtype => $tname, registry => $regname)
461              
462             Deprecated.
463              
464             Tries to unregister the type. If the type does not exist, returns 0, otherwise
465             returns 1. This is mostly useful for when a specific type must make sure it has
466             the slot. This is rarely desirable. It is usually better to use a subregistry
467             instead.
468              
469             =cut
470              
471             sub unregister_type {
472 3     3 1 683 carp $log->warn( 'Use of deprecated method unregister_type of PGObject' );
473 3         441 my ( $self, %args ) = @_;
474              
475 3   100     20 $args{registry} ||= 'default';
476             PGObject::Type::Registry->unregister_type(
477             registry => $args{registry},
478             dbtype => $args{pg_type}
479 3         16 );
480             }
481              
482             =head1 WRITING PGOBJECT-AWARE HELPER CLASSES
483              
484             One of the powerful features of PGObject is the ability to declare methods in
485             types which can be dynamically detected and used to serialize data for query
486             purposes. Objects which contain a pgobject_to_db() or a to_db() method, that
487             method will be called and the return value used in place of the object. This
488             can allow arbitrary types to serialize themselves in arbitrary ways.
489              
490             For example a date object could be set up with such a method which would export
491             a string in yyyy-mm-dd format. An object could look up its own definition and
492             return something like :
493              
494             { cast => 'dbtypename', value => '("A","List","Of","Properties")'}
495              
496             If a scalar is returned that is used as the serialized value. If a hashref is
497             returned, it must follow the type format:
498              
499             type => variable binding type,
500             cast => db cast type
501             value => literal representation of type, as intelligible by DBD::Pg
502              
503             =head2 REQUIRED INTERFACES
504              
505             Registered types MUST implement a $class->from_db function accepts the string
506             from the database as its only argument, and returns the object of the desired
507             type.
508              
509             Any type MAY present an $object->to_db() interface, requiring no arguments, and returning a valid value. These can be hashrefs as specified above, arrayrefs
510             (converted to PostgreSQL arrays by DBD::Pg) or scalar text values.
511              
512             =head2 UNDERSTANDING THE REGISTRY SYSTEM
513              
514             Note that 2.0 moves the registry to a service module which handles both
515             registry and deserialization of database types. This is intended to be both
516             cleaner and more flexible than the embedded system in 1.x.
517              
518             The registry system allows Perl classes to "claim" PostgreSQL types within a
519             certain domain. For example, if I want to ensure that all numeric types are
520             turned into Math::BigFloat objects, I can build a wrapper class with appropriate
521             interfaces, but PGObject won't know to convert numeric types to this new class,
522             so this is what registration is for.
523              
524             By default, these mappings are fully global. Once a class claims a type, unless
525             another type goes through the trouble of unregisterign the first type and making
526             sure it gets the authoritative spot, all items of that type get turned into the
527             appropriate Perl object types. While this is sufficient for the vast number of
528             applications, however, there may be cases where names conflict across schemas or
529             the like. To address this application components may create their own
530             registries. Each registry is fully global, but application components can
531             specify non-standard registries when calling procedures, and PGObject will use
532             only those components registered on the non-standard registry when checking rows
533             before output.
534              
535             =head3 Backwards Incompatibilities from 1.x
536              
537             Deserialization occurs in a context which specifies a registry. In 1.x there
538             were no concerns about default mappings but now this triggers a warning. The
539             most basic and frequently used portions of this have been kept but return values
540             for registering types has changed. We no longer provide a return variable but
541             throw an exception if the type cannot be safely registered.
542              
543             This follows a philosophy of throwing exceptions when guarantees cannot be met.
544              
545             We now throw warnings when the default registry is used.
546              
547             Longer-run, deserializers should use the PGObject::Type::Registry interface
548             directly.
549              
550             =head1 WRITING TOP-HALF OBJECT FRAMEWORKS FOR PGOBJECT
551              
552             PGObject is intended to be the database-facing side of a framework for objects.
553             The intended structure is for three tiers of logic:
554              
555             =over
556              
557             =item Database facing, low-level API's
558              
559             =item Object management modules
560              
561             =item Application handlers with things like database connection management.
562              
563             =back
564              
565             By top half, we are referring to the second tier. The third tier exists in the
566             client application.
567              
568             The PGObject module provides only low-level API's in that first tier. The job
569             of this module is to provide database function information to the upper level
570             modules.
571              
572             We do not supply type information, If your top-level module needs this, please
573             check out https://code.google.com/p/typeutils/ which could then be used via our
574             function mapping APIs here.
575              
576             =head2 Safely Handling Memoization of Catalog Lookups
577              
578             It is important to remember, when writing PGObject top half frameworks that the
579             catalog lookups may be memoized and may come back as a data structure. This
580             means that changes to the structures returned from get_function_info() in this
581             module and similar functions in other catalog-bound modules may not be safe to
582             modify in arbitrary ways. Therefore we recommend that the return values from
583             catalog-lookup functions are treated as immutable.
584              
585             Normalizing output is safe provided there are no conflicts between naming
586             conventions. This is usually true since different naming conventions would
587             interfere withmapping. However, there could be cases where it is not true, for
588             example, where two different mapping modules agree on a subset of normalization
589             conventions but differ on some details. The two might safely handle the same
590             conventions but normalize differently resulting in conflicts of both were used.
591              
592             =head1 A BRIEF GUIDE TO THE NAMESPACE LAYOUT
593              
594             Most names underneath PGObject can be assumed to be top-half modules and modules
595             under those can be generally assumed to be variants on those. There are,
596             however, a few reserved names:
597              
598             =over
599              
600             =item ::Debug is reserved for debugging information. For example, functions
601             which retrieve sources of functions, or grab diagnostics, or the like would go
602             here.
603              
604             =item ::Test is reserved for test framework extensions applible only here
605              
606             =item ::Type is reserved for PG aware type classes.
607              
608             For example, one might have PGObject::Type::BigFloat for a Math::Bigfloat
609             wrapper, or PGObject::Type::DateTime for a DateTime wrapper.
610              
611             =item ::Util is reserved for utility functions and classes.
612              
613             =back
614              
615             =head1 AUTHOR
616              
617             Chris Travers, C<< >>
618              
619             =head1 BUGS
620              
621             Please report any bugs or feature requests to C, or through
622             the web interface at L. I will be notified, and then you'll
623             automatically be notified of progress on your bug as I make changes.
624              
625             =head1 SUPPORT
626              
627             You can find documentation for this module with the perldoc command.
628              
629             perldoc PGObject
630              
631              
632             You can also look for information at:
633              
634             =over
635              
636             =item * RT: CPAN's request tracker (report bugs here)
637              
638             L
639              
640             =item * MetaCPAN
641              
642             L
643              
644             =back
645              
646             =head1 ACKNOWLEDGEMENTS
647              
648             This code has been loosely based on code written for the LedgerSMB open source
649             accounting and ERP project. While that software uses the GNU GPL v2 or later,
650             this is my own reimplementation, based on my original contributions to that
651             project alone, and it differs in significant ways. This being said, without
652             LedgerSMB, this module wouldn't exist, and without the lessons learned there,
653             and the great people who have helped make this possible, this framework would
654             not be half of what it is today.
655              
656              
657             =head1 SEE ALSO
658              
659             =over
660              
661             =item PGObject::Simple - Simple mapping of object properties to stored proc args
662              
663             =item PGObject::Simple::Role - Moose-enabled wrapper for PGObject::Simple
664              
665             =back
666              
667             =head1 COPYRIGHT
668              
669             COPYRIGHT (C) 2013-2014 Chris Travers
670             COPYRIGHT (C) 2014-2021 The LedgerSMB Core Team
671              
672             Redistribution and use in source and compiled forms with or without
673             modification, are permitted provided that the following conditions are met:
674              
675             =over
676              
677             =item
678              
679             Redistributions of source code must retain the above
680             copyright notice, this list of conditions and the following disclaimer as the
681             first lines of this file unmodified.
682              
683             =item
684              
685             Redistributions in compiled form must reproduce the above copyright
686             notice, this list of conditions and the following disclaimer in the
687             source code, documentation, and/or other materials provided with the
688             distribution.
689              
690             =back
691              
692             =head1 LICENSE
693              
694             THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) "AS IS" AND
695             ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
696             WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
697             DISCLAIMED. IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR
698             ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
699             (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
700             LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
701             ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
702             (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
703             SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
704              
705             =cut
706              
707             1;