isamd write and read functions ok, except when diff block full.
[idzebra-moved-to-github.git] / isamc / isamd.c
1 /*
2  * Copyright (c) 1995-1998, Index Data.
3  * See the file LICENSE for details.
4  * Heikki Levanto
5  * 
6  * Isamd - isam with diffs 
7  *
8  * todo: Move read_pp into merge-d
9  *       get it to work
10  *
11  */
12
13
14
15
16 #include <stdlib.h>
17 #include <assert.h>
18 #include <string.h>
19 #include <stdio.h>
20
21 #include <log.h>
22 #include "../index/index.h"  /* isamd uses the internal structure of it_key */
23 #include "isamd-p.h"
24
25 static void flush_block (ISAMD is, int cat);
26 static void release_fc (ISAMD is, int cat);
27 static void init_fc (ISAMD is, int cat);
28
29 #define ISAMD_FREELIST_CHUNK 1
30
31 #define SMALL_TEST 1
32
33 ISAMD_M isamd_getmethod (ISAMD_M me)
34 {
35     static struct ISAMD_filecat_s def_cat[] = {
36 #if SMALL_TEST
37 /*        blocksz,   max keys before switching size */
38         {    32,   40 },
39         {   128,    0 },
40 #else
41         {    24,   40 },
42         {  2048, 2048 },
43         { 16384,    0 },
44
45 #endif 
46
47 /* old values from isamc, long time ago...
48         {    24,   40 },
49         {   128,  256 },
50         {   512, 1024 },
51         {  2048, 4096 },
52         {  8192,16384 },
53         { 32768,   0  },
54 */
55
56 /* assume about 2 bytes per pointer, when compressed. The head uses */
57 /* 16 bytes, and other blocks use 8 for header info... If you want 3 */
58 /* blocks of 32 bytes, say max 16+24+24 = 64 keys */
59
60     };
61     ISAMD_M m = (ISAMD_M) xmalloc (sizeof(*m));
62     m->filecat = def_cat;
63
64     m->code_start = NULL;
65     m->code_item = NULL;
66     m->code_stop = NULL;
67     m->code_reset = NULL;
68
69     m->compare_item = NULL;
70
71     m->debug = 1;
72
73     m->max_blocks_mem = 10;
74
75     return m;
76 }
77
78
79
80 ISAMD isamd_open (BFiles bfs, const char *name, int writeflag, ISAMD_M method)
81 {
82     ISAMD is;
83     ISAMD_filecat filecat;
84     int i = 0;
85
86     is = (ISAMD) xmalloc (sizeof(*is));
87
88     is->method = (ISAMD_M) xmalloc (sizeof(*is->method));
89     memcpy (is->method, method, sizeof(*method));
90     filecat = is->method->filecat;
91     assert (filecat);
92
93     /* determine number of block categories */
94     if (is->method->debug)
95         logf (LOG_LOG, "isamd: bsize  maxkeys");
96     do
97     {
98         if (is->method->debug)
99             logf (LOG_LOG, "isamd:%6d %6d",
100                   filecat[i].bsize, filecat[i].mblocks);
101     } while (filecat[i++].mblocks);
102     is->no_files = i;
103     is->max_cat = --i;
104
105     assert (is->no_files > 0);
106     is->files = (ISAMD_file) xmalloc (sizeof(*is->files)*is->no_files);
107     if (writeflag)
108     {
109       /* TODO - what ever needs to be done here... */
110     }
111     else
112     {
113     }
114
115     for (i = 0; i<is->no_files; i++)
116     {
117         char fname[512];
118
119         sprintf (fname, "%s%c", name, i+'A');
120         is->files[i].bf = bf_open (bfs, fname, is->method->filecat[i].bsize,
121                                    writeflag);
122         is->files[i].head_is_dirty = 0;
123         if (!bf_read (is->files[i].bf, 0, 0, sizeof(ISAMD_head),
124                      &is->files[i].head))
125         {
126             is->files[i].head.lastblock = 1;
127             is->files[i].head.freelist = 0;
128         }
129         is->files[i].alloc_entries_num = 0;
130         is->files[i].alloc_entries_max =
131             is->method->filecat[i].bsize / sizeof(int) - 1;
132         is->files[i].alloc_buf = (char *)
133             xmalloc (is->method->filecat[i].bsize);
134         is->files[i].no_writes = 0; /* clear statistics */
135         is->files[i].no_reads = 0;
136         is->files[i].no_skip_writes = 0;
137         is->files[i].no_allocated = 0;
138         is->files[i].no_released = 0;
139         is->files[i].no_remap = 0;
140         is->files[i].no_forward = 0;
141         is->files[i].no_backward = 0;
142         is->files[i].sum_forward = 0;
143         is->files[i].sum_backward = 0;
144         is->files[i].no_next = 0;
145         is->files[i].no_prev = 0;
146
147         init_fc (is, i);
148     }
149     return is;
150 }
151
152 int isamd_block_used (ISAMD is, int type)
153 {
154     if (type < 0 || type >= is->no_files)
155         return -1;
156     return is->files[type].head.lastblock-1;
157 }
158
159 int isamd_block_size (ISAMD is, int type)
160 {
161     ISAMD_filecat filecat = is->method->filecat;
162     if (type < 0 || type >= is->no_files)
163         return -1;
164     return filecat[type].bsize;
165 }
166
167 int isamd_close (ISAMD is)
168 {
169     int i;
170
171     if (is->method->debug)
172     {
173         logf (LOG_LOG, "isamd:    next    forw   mid-f    prev   backw   mid-b");
174         for (i = 0; i<is->no_files; i++)
175             logf (LOG_LOG, "isamd:%8d%8d%8.1f%8d%8d%8.1f",
176                   is->files[i].no_next,
177                   is->files[i].no_forward,
178                   is->files[i].no_forward ?
179                   (double) is->files[i].sum_forward/is->files[i].no_forward
180                   : 0.0,
181                   is->files[i].no_prev,
182                   is->files[i].no_backward,
183                   is->files[i].no_backward ?
184                   (double) is->files[i].sum_backward/is->files[i].no_backward
185                   : 0.0);
186     }
187     if (is->method->debug)
188         logf (LOG_LOG, "isamd:  writes   reads skipped   alloc released  remap");
189     for (i = 0; i<is->no_files; i++)
190     {
191         release_fc (is, i);
192         assert (is->files[i].bf);
193         if (is->files[i].head_is_dirty)
194             bf_write (is->files[i].bf, 0, 0, sizeof(ISAMD_head),
195                  &is->files[i].head);
196         if (is->method->debug)
197             logf (LOG_LOG, "isamd:%8d%8d%8d%8d%8d%8d",
198                   is->files[i].no_writes,
199                   is->files[i].no_reads,
200                   is->files[i].no_skip_writes,
201                   is->files[i].no_allocated,
202                   is->files[i].no_released,
203                   is->files[i].no_remap);
204         xfree (is->files[i].fc_list);
205         flush_block (is, i);
206         bf_close (is->files[i].bf);
207     }
208     xfree (is->files);
209     xfree (is->method);
210     xfree (is);
211     return 0;
212 }
213
214 int isamd_read_block (ISAMD is, int cat, int pos, char *dst)
215 {
216     ++(is->files[cat].no_reads);
217     return bf_read (is->files[cat].bf, pos, 0, 0, dst);
218 }
219
220 int isamd_write_block (ISAMD is, int cat, int pos, char *src)
221 {
222     ++(is->files[cat].no_writes);
223     if (is->method->debug > 2)
224         logf (LOG_LOG, "isamd: write_block %d %d", cat, pos);
225     return bf_write (is->files[cat].bf, pos, 0, 0, src);
226 }
227
228 int isamd_write_dblock (ISAMD is, int cat, int pos, char *src,
229                       int nextpos, int offset)
230 {
231     ISAMD_BLOCK_SIZE size = offset + ISAMD_BLOCK_OFFSET_N;
232     if (is->method->debug > 2)
233         logf (LOG_LOG, "isamd: write_dblock. size=%d nextpos=%d",
234               (int) size, nextpos);
235     src -= ISAMD_BLOCK_OFFSET_N;
236     assert( ISAMD_BLOCK_OFFSET_N == sizeof(int)+sizeof(int) );
237     memcpy (src, &nextpos, sizeof(int));
238     memcpy (src + sizeof(int), &size, sizeof(size));
239     return isamd_write_block (is, cat, pos, src);
240 }
241
242 #if ISAMD_FREELIST_CHUNK
243 static void flush_block (ISAMD is, int cat)
244 {
245     char *abuf = is->files[cat].alloc_buf;
246     int block = is->files[cat].head.freelist;
247     if (block && is->files[cat].alloc_entries_num)
248     {
249         memcpy (abuf, &is->files[cat].alloc_entries_num, sizeof(int));
250         bf_write (is->files[cat].bf, block, 0, 0, abuf);
251         is->files[cat].alloc_entries_num = 0;
252     }
253     xfree (abuf);
254 }
255
256 static int alloc_block (ISAMD is, int cat)
257 {
258     int block = is->files[cat].head.freelist;
259     char *abuf = is->files[cat].alloc_buf;
260
261     (is->files[cat].no_allocated)++;
262
263     if (!block)
264     {
265         block = (is->files[cat].head.lastblock)++;   /* no free list */
266         is->files[cat].head_is_dirty = 1;
267     }
268     else
269     {
270         if (!is->files[cat].alloc_entries_num) /* read first time */
271         {
272             bf_read (is->files[cat].bf, block, 0, 0, abuf);
273             memcpy (&is->files[cat].alloc_entries_num, abuf,
274                     sizeof(is->files[cat].alloc_entries_num));
275             assert (is->files[cat].alloc_entries_num > 0);
276         }
277         /* have some free blocks now */
278         assert (is->files[cat].alloc_entries_num > 0);
279         is->files[cat].alloc_entries_num--;
280         if (!is->files[cat].alloc_entries_num)  /* last one in block? */
281         {
282             memcpy (&is->files[cat].head.freelist, abuf + sizeof(int),
283                     sizeof(int));
284             is->files[cat].head_is_dirty = 1;
285
286             if (is->files[cat].head.freelist)
287             {
288                 bf_read (is->files[cat].bf, is->files[cat].head.freelist,
289                          0, 0, abuf);
290                 memcpy (&is->files[cat].alloc_entries_num, abuf,
291                         sizeof(is->files[cat].alloc_entries_num));
292                 assert (is->files[cat].alloc_entries_num);
293             }
294         }
295         else
296             memcpy (&block, abuf + sizeof(int) + sizeof(int) *
297                     is->files[cat].alloc_entries_num, sizeof(int));
298     }
299     return block;
300 }
301
302 static void release_block (ISAMD is, int cat, int pos)
303 {
304     char *abuf = is->files[cat].alloc_buf;
305     int block = is->files[cat].head.freelist;
306
307     (is->files[cat].no_released)++;
308
309     if (block && !is->files[cat].alloc_entries_num) /* must read block */
310     {
311         bf_read (is->files[cat].bf, block, 0, 0, abuf);
312         memcpy (&is->files[cat].alloc_entries_num, abuf,
313                 sizeof(is->files[cat].alloc_entries_num));
314         assert (is->files[cat].alloc_entries_num > 0);
315     }
316     assert (is->files[cat].alloc_entries_num <= is->files[cat].alloc_entries_max);
317     if (is->files[cat].alloc_entries_num == is->files[cat].alloc_entries_max)
318     {
319         assert (block);
320         memcpy (abuf, &is->files[cat].alloc_entries_num, sizeof(int));
321         bf_write (is->files[cat].bf, block, 0, 0, abuf);
322         is->files[cat].alloc_entries_num = 0;
323     }
324     if (!is->files[cat].alloc_entries_num) /* make new buffer? */
325     {
326         memcpy (abuf + sizeof(int), &block, sizeof(int));
327         is->files[cat].head.freelist = pos;
328         is->files[cat].head_is_dirty = 1; 
329     }
330     else
331     {
332         memcpy (abuf + sizeof(int) +
333                 is->files[cat].alloc_entries_num*sizeof(int),
334                 &pos, sizeof(int));
335     }
336     is->files[cat].alloc_entries_num++;
337 }
338 #else
339 static void flush_block (ISAMD is, int cat)
340 {
341     char *abuf = is->files[cat].alloc_buf;
342     xfree (abuf);
343 }
344
345 static int alloc_block (ISAMD is, int cat)
346 {
347     int block;
348     char buf[sizeof(int)];
349
350     is->files[cat].head_is_dirty = 1;
351     (is->files[cat].no_allocated)++;
352     if ((block = is->files[cat].head.freelist))
353     {
354         bf_read (is->files[cat].bf, block, 0, sizeof(int), buf);
355         memcpy (&is->files[cat].head.freelist, buf, sizeof(int));
356     }
357     else
358         block = (is->files[cat].head.lastblock)++;
359     return block;
360 }
361
362 static void release_block (ISAMD is, int cat, int pos)
363 {
364     char buf[sizeof(int)];
365    
366     (is->files[cat].no_released)++;
367     is->files[cat].head_is_dirty = 1; 
368     memcpy (buf, &is->files[cat].head.freelist, sizeof(int));
369     is->files[cat].head.freelist = pos;
370     bf_write (is->files[cat].bf, pos, 0, sizeof(int), buf);
371 }
372 #endif
373
374 int isamd_alloc_block (ISAMD is, int cat)
375 {
376     int block = 0;
377
378     if (is->files[cat].fc_list)
379     {
380         int j, nb;
381         for (j = 0; j < is->files[cat].fc_max; j++)
382             if ((nb = is->files[cat].fc_list[j]) && (!block || nb < block))
383             {
384                 is->files[cat].fc_list[j] = 0;
385                 block = nb;
386                 break;
387             }
388     }
389     if (!block)
390         block = alloc_block (is, cat);
391     if (is->method->debug > 3)
392         logf (LOG_LOG, "isamd: alloc_block in cat %d: %d", cat, block);
393     return block;
394 }
395
396 void isamd_release_block (ISAMD is, int cat, int pos)
397 {
398     if (is->method->debug > 3)
399         logf (LOG_LOG, "isamd: release_block in cat %d: %d", cat, pos);
400     if (is->files[cat].fc_list)
401     {
402         int j;
403         for (j = 0; j<is->files[cat].fc_max; j++)
404             if (!is->files[cat].fc_list[j])
405             {
406                 is->files[cat].fc_list[j] = pos;
407                 return;
408             }
409     }
410     release_block (is, cat, pos);
411 }
412
413 static void init_fc (ISAMD is, int cat)
414 {
415     int j = 100;
416         
417     is->files[cat].fc_max = j;
418     is->files[cat].fc_list = (int *)
419         xmalloc (sizeof(*is->files[0].fc_list) * j);
420     while (--j >= 0)
421         is->files[cat].fc_list[j] = 0;
422 }
423
424 static void release_fc (ISAMD is, int cat)
425 {
426     int b, j = is->files[cat].fc_max;
427
428     while (--j >= 0)
429         if ((b = is->files[cat].fc_list[j]))
430         {
431             release_block (is, cat, b);
432             is->files[cat].fc_list[j] = 0;
433         }
434 }
435
436 void isamd_pp_close (ISAMD_PP pp)
437 {
438     ISAMD is = pp->is;
439
440     (*is->method->code_stop)(ISAMD_DECODE, pp->decodeClientData);
441     isamd_free_diffs(pp);  /* see merge-d.h */
442     xfree (pp->buf);
443     xfree (pp);
444 }
445
446
447
448 ISAMD_PP isamd_pp_open (ISAMD is, ISAMD_P ipos)
449 {
450     ISAMD_PP pp = (ISAMD_PP) xmalloc (sizeof(*pp));
451     char *src;
452    
453     pp->cat = isamd_type(ipos);
454     pp->pos = isamd_block(ipos); 
455
456     src = pp->buf = (char *) xmalloc (is->method->filecat[is->max_cat].bsize);
457                  /* always allocate for the largest blocks, saves trouble */
458     pp->next = 0;
459     pp->size = 0;
460     pp->offset = 0;
461     pp->is = is;
462     pp->decodeClientData = (*is->method->code_start)(ISAMD_DECODE);
463     //pp->deleteFlag = 0;
464     pp->numKeys = 0;
465     pp->diffs=0;
466   
467     pp->diffbuf=0;
468     pp->diffinfo=0;
469     
470     if (pp->pos)
471     {
472         src = pp->buf;
473         isamd_read_block (is, pp->cat, pp->pos, src);
474         memcpy (&pp->next, src, sizeof(pp->next));
475         src += sizeof(pp->next);
476         memcpy (&pp->size, src, sizeof(pp->size));
477         src += sizeof(pp->size);
478         memcpy (&pp->numKeys, src, sizeof(pp->numKeys));
479         src += sizeof(pp->numKeys);
480         memcpy (&pp->diffs, src, sizeof(pp->diffs));
481         src += sizeof(pp->diffs);
482         assert (pp->next != pp->pos);
483         pp->offset = src - pp->buf; 
484         assert (pp->offset == ISAMD_BLOCK_OFFSET_1);
485         if (is->method->debug > 2)
486             logf (LOG_LOG, "isamd_pp_open sz=%d c=%d p=%d n=%d",
487                  pp->size, pp->cat, pp->pos, isamd_block(pp->next));
488     }
489       
490     return pp;
491 }
492
493
494
495 void isamd_buildfirstblock(ISAMD_PP pp){
496   char *dst=pp->buf;
497   assert(pp->buf);
498   assert(pp->next != pp->pos); 
499   memcpy(dst, &pp->next, sizeof(pp->next) );
500   dst += sizeof(pp->next);
501   memcpy(dst, &pp->size,sizeof(pp->size));
502   dst += sizeof(pp->size);
503   memcpy(dst, &pp->numKeys, sizeof(pp->numKeys));
504   dst += sizeof(pp->numKeys);
505   memcpy(dst, &pp->diffs, sizeof(pp->diffs));
506   dst += sizeof(pp->diffs);  
507   assert (dst - pp->buf  == ISAMD_BLOCK_OFFSET_1);
508   if (pp->is->method->debug > 2)
509      logf (LOG_LOG, "isamd: first: sz=%d  p=%d/%d>%d/%d nk=%d d=%d",
510            pp->size, 
511            pp->cat, pp->pos, 
512            isamd_type(pp->next), isamd_block(pp->next),
513            pp->numKeys, pp->diffs);
514 }
515
516 void isamd_buildlaterblock(ISAMD_PP pp){
517   char *dst=pp->buf;
518   assert(pp->buf);
519   assert(pp->next != isamd_addr(pp->pos,pp->cat)); 
520   memcpy(dst, &pp->next, sizeof(pp->next) );
521   dst += sizeof(pp->next);
522   memcpy(dst, &pp->size,sizeof(pp->size));
523   dst += sizeof(pp->size);
524   assert (dst - pp->buf  == ISAMD_BLOCK_OFFSET_N);
525   if (pp->is->method->debug > 2)
526      logf (LOG_LOG, "isamd: l8r: sz=%d  p=%d/%d>%d/%d",
527            pp->size, 
528            pp->pos, pp->cat, 
529            isamd_block(pp->next), isamd_type(pp->next) );
530 }
531
532
533
534 /* returns non-zero if item could be read; 0 otherwise */
535 int isamd_pp_read (ISAMD_PP pp, void *buf)
536 {
537     return isamd_read_item (pp, (char **) &buf);
538     /* note: isamd_read_item is in merge-d.c, because it is so */
539     /* convoluted with the merge process */
540 }
541
542 /* read one main item from file - decode and store it in *dst.
543    Does not worry about diffs
544    Returns
545      0 if end-of-file
546      1 if item could be read ok
547 */
548 int isamd_read_main_item (ISAMD_PP pp, char **dst)
549 {
550     ISAMD is = pp->is;
551     char *src = pp->buf + pp->offset;
552     int newcat;
553
554     if (pp->offset >= pp->size)
555     {
556         if (!pp->next)
557         {
558             pp->pos = 0;
559             return 0; /* end of file */
560         }
561         if (pp->next > pp->pos)
562         {
563             if (pp->next == pp->pos + 1)
564                 is->files[pp->cat].no_next++;
565             else
566             {
567                 is->files[pp->cat].no_forward++;
568                 is->files[pp->cat].sum_forward += pp->next - pp->pos;
569             }
570         }
571         else
572         {
573             if (pp->next + 1 == pp->pos)
574                 is->files[pp->cat].no_prev++;
575             else
576             {
577                 is->files[pp->cat].no_backward++;
578                 is->files[pp->cat].sum_backward += pp->pos - pp->next;
579             }
580         }
581         /* out new block position */
582         newcat = isamd_type(pp->next);
583         pp->pos = isamd_block(pp->next);
584         pp->cat = isamd_type(pp->next);
585         
586         src = pp->buf;
587         /* read block and save 'next' and 'size' entry */
588         isamd_read_block (is, pp->cat, pp->pos, src);
589         memcpy (&pp->next, src, sizeof(pp->next));
590         src += sizeof(pp->next);
591         memcpy (&pp->size, src, sizeof(pp->size));
592         src += sizeof(pp->size);
593         /* assume block is non-empty */
594         assert (src - pp->buf == ISAMD_BLOCK_OFFSET_N);
595         assert (pp->next != isamd_addr(pp->pos,pp->cat));
596         //if (pp->deleteFlag)
597         //    isamd_release_block (is, pp->cat, pp->pos);
598         (*is->method->code_reset)(pp->decodeClientData);
599         (*is->method->code_item)(ISAMD_DECODE, pp->decodeClientData, dst, &src);
600         pp->offset = src - pp->buf; 
601         if (is->method->debug > 2)
602             logf (LOG_LOG, "isamd: read_block size=%d %d %d next=%d",
603                  pp->size, pp->cat, pp->pos, pp->next);
604         return 2;
605     }
606     (*is->method->code_item)(ISAMD_DECODE, pp->decodeClientData, dst, &src);
607     pp->offset = src - pp->buf; 
608     return 1;
609 }
610
611 int isamd_pp_num (ISAMD_PP pp)
612 {
613     return pp->numKeys;
614 }
615
616 static char *hexdump(unsigned char *p, int len, char *buff) {
617   static char localbuff[128];
618   char bytebuff[8];
619   if (!buff) buff=localbuff;
620   *buff='\0';
621   while (len--) {
622     sprintf(bytebuff,"%02x",*p);
623     p++;
624     strcat(buff,bytebuff);
625     if (len) strcat(buff," ");
626   }
627   return buff;
628 }
629
630
631 void isamd_pp_dump (ISAMD is, ISAMD_P ipos)
632 {
633   ISAMD_PP pp;
634   ISAMD_P oldaddr=0;
635   struct it_key key;
636   int i,n;
637   int occur =0;
638   int oldoffs;
639   char hexbuff[64];
640   
641   logf(LOG_LOG,"dumping isamd block %d (%d:%d)",
642                   (int)ipos, isamd_type(ipos), isamd_block(ipos) );
643   pp=isamd_pp_open(is,ipos);
644   logf(LOG_LOG,"numKeys=%d,  ofs=%d d=%d",
645        pp->numKeys, 
646        pp->offset, pp->diffs);
647   oldoffs= pp->offset;
648   while(isamd_pp_read(pp, &key))
649   {
650      if (oldaddr != isamd_addr(pp->pos,pp->cat) )
651      {
652         oldaddr = isamd_addr(pp->pos,pp->cat); 
653         logf(LOG_LOG,"block %d (%d:%d) sz=%d nx=%d (%d:%d) ofs=%d",
654                   isamd_addr(pp->pos,pp->cat), 
655                   pp->cat, pp->pos, pp->size,
656                   pp->next, isamd_type(pp->next), isamd_block(pp->next),
657                   pp->offset);
658         i=0;      
659         while (i<pp->size) {
660           n=pp->size-i;
661           if (n>8) n=8;
662           logf(LOG_LOG,"  %05x: %s",i,hexdump(pp->buf+i,n,hexbuff));
663           i+=n;
664         }
665         if (oldoffs >  ISAMD_BLOCK_OFFSET_N)
666            oldoffs=ISAMD_BLOCK_OFFSET_N;
667      } /* new block */
668      occur++;
669      logf (LOG_LOG,"    got %d:%d=%x:%x from %s at %d=%x",
670                   key.sysno, key.seqno,
671                   key.sysno, key.seqno,
672                   hexdump(pp->buf+oldoffs, pp->offset-oldoffs, hexbuff),
673                   oldoffs, oldoffs);
674      oldoffs = pp->offset;
675   }
676   /*!*/ /*TODO: dump diffs too!!! */
677   isamd_pp_close(pp);
678 } /* dump */
679
680 /*
681  * $Log: isamd.c,v $
682  * Revision 1.3  1999-07-21 14:24:50  heikki
683  * isamd write and read functions ok, except when diff block full.
684  * (merge not yet done)
685  *
686  * Revision 1.1  1999/07/14 12:34:43  heikki
687  * Copied from isamh, starting to change things...
688  *
689  *
690  */