~ubuntu-branches/ubuntu/hardy/postgresql-8.4/hardy-backports

« back to all changes in this revision

Viewing changes to contrib/spi/timetravel.c

  • Committer: Bazaar Package Importer
  • Author(s): Martin Pitt
  • Date: 2009-03-20 12:00:13 UTC
  • Revision ID: james.westby@ubuntu.com-20090320120013-hogj7egc5mjncc5g
Tags: upstream-8.4~0cvs20090328
ImportĀ upstreamĀ versionĀ 8.4~0cvs20090328

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
/*
 
2
 * $PostgreSQL$ 
 
3
 *
 
4
 *
 
5
 * timetravel.c --      function to get time travel feature
 
6
 *              using general triggers.
 
7
 *
 
8
 * Modified by Bļæ½JTHE Zoltļæ½n, Hungary, mailto:urdesobt@axelero.hu
 
9
 */
 
10
#include "postgres.h"
 
11
 
 
12
#include <ctype.h>
 
13
 
 
14
#include "catalog/pg_type.h"
 
15
#include "commands/trigger.h"
 
16
#include "executor/spi.h"
 
17
#include "miscadmin.h"
 
18
#include "utils/builtins.h"
 
19
#include "utils/nabstime.h"
 
20
 
 
21
PG_MODULE_MAGIC;
 
22
 
 
23
/* AbsoluteTime currabstime(void); */
 
24
Datum           timetravel(PG_FUNCTION_ARGS);
 
25
Datum           set_timetravel(PG_FUNCTION_ARGS);
 
26
Datum           get_timetravel(PG_FUNCTION_ARGS);
 
27
 
 
28
typedef struct
 
29
{
 
30
        char       *ident;
 
31
        SPIPlanPtr      splan;
 
32
}       EPlan;
 
33
 
 
34
static EPlan *Plans = NULL;             /* for UPDATE/DELETE */
 
35
static int      nPlans = 0;
 
36
 
 
37
typedef struct _TTOffList
 
38
{
 
39
        struct _TTOffList *next;
 
40
        char            name[1];
 
41
}       TTOffList;
 
42
 
 
43
static TTOffList TTOff = {NULL, {0}};
 
44
 
 
45
static int      findTTStatus(char *name);
 
46
static EPlan *find_plan(char *ident, EPlan ** eplan, int *nplans);
 
47
 
 
48
/*
 
49
 * timetravel () --
 
50
 *              1.      IF an update affects tuple with stop_date eq INFINITY
 
51
 *                      then form (and return) new tuple with start_date eq current date
 
52
 *                      and stop_date eq INFINITY [ and update_user eq current user ]
 
53
 *                      and all other column values as in new tuple, and insert tuple
 
54
 *                      with old data and stop_date eq current date
 
55
 *                      ELSE - skip updation of tuple.
 
56
 *              2.      IF an delete affects tuple with stop_date eq INFINITY
 
57
 *                      then insert the same tuple with stop_date eq current date
 
58
 *                      [ and delete_user eq current user ]
 
59
 *                      ELSE - skip deletion of tuple.
 
60
 *              3.      On INSERT, if start_date is NULL then current date will be
 
61
 *                      inserted, if stop_date is NULL then INFINITY will be inserted.
 
62
 *                      [ and insert_user eq current user, update_user and delete_user
 
63
 *                      eq NULL ]
 
64
 *
 
65
 * In CREATE TRIGGER you are to specify start_date and stop_date column
 
66
 * names:
 
67
 * EXECUTE PROCEDURE
 
68
 * timetravel ('date_on', 'date_off' [,'insert_user', 'update_user', 'delete_user' ] ).
 
69
 */
 
70
 
 
71
#define MaxAttrNum      5
 
72
#define MinAttrNum      2
 
73
 
 
74
#define a_time_on       0
 
75
#define a_time_off      1
 
76
#define a_ins_user      2
 
77
#define a_upd_user      3
 
78
#define a_del_user      4
 
79
 
 
80
PG_FUNCTION_INFO_V1(timetravel);
 
81
 
 
82
Datum                                                   /* have to return HeapTuple to Executor */
 
83
timetravel(PG_FUNCTION_ARGS)
 
84
{
 
85
        TriggerData *trigdata = (TriggerData *) fcinfo->context;
 
86
        Trigger    *trigger;            /* to get trigger name */
 
87
        int                     argc;
 
88
        char      **args;                       /* arguments */
 
89
        int                     attnum[MaxAttrNum];             /* fnumbers of start/stop columns */
 
90
        Datum           oldtimeon,
 
91
                                oldtimeoff;
 
92
        Datum           newtimeon,
 
93
                                newtimeoff,
 
94
                                newuser,
 
95
                                nulltext;
 
96
        Datum      *cvals;                      /* column values */
 
97
        char       *cnulls;                     /* column nulls */
 
98
        char       *relname;            /* triggered relation name */
 
99
        Relation        rel;                    /* triggered relation */
 
100
        HeapTuple       trigtuple;
 
101
        HeapTuple       newtuple = NULL;
 
102
        HeapTuple       rettuple;
 
103
        TupleDesc       tupdesc;                /* tuple description */
 
104
        int                     natts;                  /* # of attributes */
 
105
        EPlan      *plan;                       /* prepared plan */
 
106
        char            ident[2 * NAMEDATALEN];
 
107
        bool            isnull;                 /* to know is some column NULL or not */
 
108
        bool            isinsert = false;
 
109
        int                     ret;
 
110
        int                     i;
 
111
 
 
112
        /*
 
113
         * Some checks first...
 
114
         */
 
115
 
 
116
        /* Called by trigger manager ? */
 
117
        if (!CALLED_AS_TRIGGER(fcinfo))
 
118
                elog(ERROR, "timetravel: not fired by trigger manager");
 
119
 
 
120
        /* Should be called for ROW trigger */
 
121
        if (TRIGGER_FIRED_FOR_STATEMENT(trigdata->tg_event))
 
122
                elog(ERROR, "timetravel: cannot process STATEMENT events");
 
123
 
 
124
        /* Should be called BEFORE */
 
125
        if (TRIGGER_FIRED_AFTER(trigdata->tg_event))
 
126
                elog(ERROR, "timetravel: must be fired before event");
 
127
 
 
128
        /* INSERT ? */
 
129
        if (TRIGGER_FIRED_BY_INSERT(trigdata->tg_event))
 
130
                isinsert = true;
 
131
 
 
132
        if (TRIGGER_FIRED_BY_UPDATE(trigdata->tg_event))
 
133
                newtuple = trigdata->tg_newtuple;
 
134
 
 
135
        trigtuple = trigdata->tg_trigtuple;
 
136
 
 
137
        rel = trigdata->tg_relation;
 
138
        relname = SPI_getrelname(rel);
 
139
 
 
140
        /* check if TT is OFF for this relation */
 
141
        if (0 == findTTStatus(relname))
 
142
        {
 
143
                /* OFF - nothing to do */
 
144
                pfree(relname);
 
145
                return PointerGetDatum((newtuple != NULL) ? newtuple : trigtuple);
 
146
        }
 
147
 
 
148
        trigger = trigdata->tg_trigger;
 
149
 
 
150
        argc = trigger->tgnargs;
 
151
        if (argc != MinAttrNum && argc != MaxAttrNum)
 
152
                elog(ERROR, "timetravel (%s): invalid (!= %d or %d) number of arguments %d",
 
153
                         relname, MinAttrNum, MaxAttrNum, trigger->tgnargs);
 
154
 
 
155
        args = trigger->tgargs;
 
156
        tupdesc = rel->rd_att;
 
157
        natts = tupdesc->natts;
 
158
 
 
159
        for (i = 0; i < MinAttrNum; i++)
 
160
        {
 
161
                attnum[i] = SPI_fnumber(tupdesc, args[i]);
 
162
                if (attnum[i] < 0)
 
163
                        elog(ERROR, "timetravel (%s): there is no attribute %s", relname, args[i]);
 
164
                if (SPI_gettypeid(tupdesc, attnum[i]) != ABSTIMEOID)
 
165
                        elog(ERROR, "timetravel (%s): attribute %s must be of abstime type",
 
166
                                 relname, args[i]);
 
167
        }
 
168
        for (; i < argc; i++)
 
169
        {
 
170
                attnum[i] = SPI_fnumber(tupdesc, args[i]);
 
171
                if (attnum[i] < 0)
 
172
                        elog(ERROR, "timetravel (%s): there is no attribute %s", relname, args[i]);
 
173
                if (SPI_gettypeid(tupdesc, attnum[i]) != TEXTOID)
 
174
                        elog(ERROR, "timetravel (%s): attribute %s must be of text type",
 
175
                                 relname, args[i]);
 
176
        }
 
177
 
 
178
        /* create fields containing name */
 
179
        newuser = CStringGetTextDatum(GetUserNameFromId(GetUserId()));
 
180
 
 
181
        nulltext = (Datum) NULL;
 
182
 
 
183
        if (isinsert)
 
184
        {                                                       /* INSERT */
 
185
                int                     chnattrs = 0;
 
186
                int                     chattrs[MaxAttrNum];
 
187
                Datum           newvals[MaxAttrNum];
 
188
                char            newnulls[MaxAttrNum];
 
189
 
 
190
                oldtimeon = SPI_getbinval(trigtuple, tupdesc, attnum[a_time_on], &isnull);
 
191
                if (isnull)
 
192
                {
 
193
                        newvals[chnattrs] = GetCurrentAbsoluteTime();
 
194
                        newnulls[chnattrs] = ' ';
 
195
                        chattrs[chnattrs] = attnum[a_time_on];
 
196
                        chnattrs++;
 
197
                }
 
198
 
 
199
                oldtimeoff = SPI_getbinval(trigtuple, tupdesc, attnum[a_time_off], &isnull);
 
200
                if (isnull)
 
201
                {
 
202
                        if ((chnattrs == 0 && DatumGetInt32(oldtimeon) >= NOEND_ABSTIME) ||
 
203
                                (chnattrs > 0 && DatumGetInt32(newvals[a_time_on]) >= NOEND_ABSTIME))
 
204
                                elog(ERROR, "timetravel (%s): %s is infinity", relname, args[a_time_on]);
 
205
                        newvals[chnattrs] = NOEND_ABSTIME;
 
206
                        newnulls[chnattrs] = ' ';
 
207
                        chattrs[chnattrs] = attnum[a_time_off];
 
208
                        chnattrs++;
 
209
                }
 
210
                else
 
211
                {
 
212
                        if ((chnattrs == 0 && DatumGetInt32(oldtimeon) > DatumGetInt32(oldtimeoff)) ||
 
213
                                (chnattrs > 0 && DatumGetInt32(newvals[a_time_on]) > DatumGetInt32(oldtimeoff)))
 
214
                                elog(ERROR, "timetravel (%s): %s gt %s", relname, args[a_time_on], args[a_time_off]);
 
215
                }
 
216
 
 
217
                pfree(relname);
 
218
                if (chnattrs <= 0)
 
219
                        return PointerGetDatum(trigtuple);
 
220
 
 
221
                if (argc == MaxAttrNum)
 
222
                {
 
223
                        /* clear update_user value */
 
224
                        newvals[chnattrs] = nulltext;
 
225
                        newnulls[chnattrs] = 'n';
 
226
                        chattrs[chnattrs] = attnum[a_upd_user];
 
227
                        chnattrs++;
 
228
                        /* clear delete_user value */
 
229
                        newvals[chnattrs] = nulltext;
 
230
                        newnulls[chnattrs] = 'n';
 
231
                        chattrs[chnattrs] = attnum[a_del_user];
 
232
                        chnattrs++;
 
233
                        /* set insert_user value */
 
234
                        newvals[chnattrs] = newuser;
 
235
                        newnulls[chnattrs] = ' ';
 
236
                        chattrs[chnattrs] = attnum[a_ins_user];
 
237
                        chnattrs++;
 
238
                }
 
239
                rettuple = SPI_modifytuple(rel, trigtuple, chnattrs, chattrs, newvals, newnulls);
 
240
                return PointerGetDatum(rettuple);
 
241
                /* end of INSERT */
 
242
        }
 
243
 
 
244
        /* UPDATE/DELETE: */
 
245
        oldtimeon = SPI_getbinval(trigtuple, tupdesc, attnum[a_time_on], &isnull);
 
246
        if (isnull)
 
247
                elog(ERROR, "timetravel (%s): %s must be NOT NULL", relname, args[a_time_on]);
 
248
 
 
249
        oldtimeoff = SPI_getbinval(trigtuple, tupdesc, attnum[a_time_off], &isnull);
 
250
        if (isnull)
 
251
                elog(ERROR, "timetravel (%s): %s must be NOT NULL", relname, args[a_time_off]);
 
252
 
 
253
        /*
 
254
         * If DELETE/UPDATE of tuple with stop_date neq INFINITY then say upper
 
255
         * Executor to skip operation for this tuple
 
256
         */
 
257
        if (newtuple != NULL)
 
258
        {                                                       /* UPDATE */
 
259
                newtimeon = SPI_getbinval(newtuple, tupdesc, attnum[a_time_on], &isnull);
 
260
                if (isnull)
 
261
                        elog(ERROR, "timetravel (%s): %s must be NOT NULL", relname, args[a_time_on]);
 
262
 
 
263
                newtimeoff = SPI_getbinval(newtuple, tupdesc, attnum[a_time_off], &isnull);
 
264
                if (isnull)
 
265
                        elog(ERROR, "timetravel (%s): %s must be NOT NULL", relname, args[a_time_off]);
 
266
 
 
267
                if (oldtimeon != newtimeon || oldtimeoff != newtimeoff)
 
268
                        elog(ERROR, "timetravel (%s): you cannot change %s and/or %s columns (use set_timetravel)",
 
269
                                 relname, args[a_time_on], args[a_time_off]);
 
270
        }
 
271
        if (oldtimeoff != NOEND_ABSTIME)
 
272
        {                                                       /* current record is a deleted/updated record */
 
273
                pfree(relname);
 
274
                return PointerGetDatum(NULL);
 
275
        }
 
276
 
 
277
        newtimeoff = GetCurrentAbsoluteTime();
 
278
 
 
279
        /* Connect to SPI manager */
 
280
        if ((ret = SPI_connect()) < 0)
 
281
                elog(ERROR, "timetravel (%s): SPI_connect returned %d", relname, ret);
 
282
 
 
283
        /* Fetch tuple values and nulls */
 
284
        cvals = (Datum *) palloc(natts * sizeof(Datum));
 
285
        cnulls = (char *) palloc(natts * sizeof(char));
 
286
        for (i = 0; i < natts; i++)
 
287
        {
 
288
                cvals[i] = SPI_getbinval(trigtuple, tupdesc, i + 1, &isnull);
 
289
                cnulls[i] = (isnull) ? 'n' : ' ';
 
290
        }
 
291
 
 
292
        /* change date column(s) */
 
293
        cvals[attnum[a_time_off] - 1] = newtimeoff; /* stop_date eq current date */
 
294
        cnulls[attnum[a_time_off] - 1] = ' ';
 
295
 
 
296
        if (!newtuple)
 
297
        {                                                       /* DELETE */
 
298
                if (argc == MaxAttrNum)
 
299
                {
 
300
                        cvals[attnum[a_del_user] - 1] = newuser;        /* set delete user */
 
301
                        cnulls[attnum[a_del_user] - 1] = ' ';
 
302
                }
 
303
        }
 
304
 
 
305
        /*
 
306
         * Construct ident string as TriggerName $ TriggeredRelationId and try to
 
307
         * find prepared execution plan.
 
308
         */
 
309
        snprintf(ident, sizeof(ident), "%s$%u", trigger->tgname, rel->rd_id);
 
310
        plan = find_plan(ident, &Plans, &nPlans);
 
311
 
 
312
        /* if there is no plan ... */
 
313
        if (plan->splan == NULL)
 
314
        {
 
315
                SPIPlanPtr      pplan;
 
316
                Oid                *ctypes;
 
317
                char            sql[8192];
 
318
                char            separ = ' ';
 
319
 
 
320
                /* allocate ctypes for preparation */
 
321
                ctypes = (Oid *) palloc(natts * sizeof(Oid));
 
322
 
 
323
                /*
 
324
                 * Construct query: INSERT INTO _relation_ VALUES ($1, ...)
 
325
                 */
 
326
                snprintf(sql, sizeof(sql), "INSERT INTO %s VALUES (", relname);
 
327
                for (i = 1; i <= natts; i++)
 
328
                {
 
329
                        ctypes[i - 1] = SPI_gettypeid(tupdesc, i);
 
330
                        if (!(tupdesc->attrs[i - 1]->attisdropped)) /* skip dropped columns */
 
331
                        {
 
332
                                snprintf(sql + strlen(sql), sizeof(sql) - strlen(sql), "%c$%d", separ, i);
 
333
                                separ = ',';
 
334
                        }
 
335
                }
 
336
                snprintf(sql + strlen(sql), sizeof(sql) - strlen(sql), ")");
 
337
 
 
338
                elog(DEBUG4, "timetravel (%s) update: sql: %s", relname, sql);
 
339
 
 
340
                /* Prepare plan for query */
 
341
                pplan = SPI_prepare(sql, natts, ctypes);
 
342
                if (pplan == NULL)
 
343
                        elog(ERROR, "timetravel (%s): SPI_prepare returned %d", relname, SPI_result);
 
344
 
 
345
                /*
 
346
                 * Remember that SPI_prepare places plan in current memory context -
 
347
                 * so, we have to save plan in Top memory context for latter use.
 
348
                 */
 
349
                pplan = SPI_saveplan(pplan);
 
350
                if (pplan == NULL)
 
351
                        elog(ERROR, "timetravel (%s): SPI_saveplan returned %d", relname, SPI_result);
 
352
 
 
353
                plan->splan = pplan;
 
354
        }
 
355
 
 
356
        /*
 
357
         * Ok, execute prepared plan.
 
358
         */
 
359
        ret = SPI_execp(plan->splan, cvals, cnulls, 0);
 
360
 
 
361
        if (ret < 0)
 
362
                elog(ERROR, "timetravel (%s): SPI_execp returned %d", relname, ret);
 
363
 
 
364
        /* Tuple to return to upper Executor ... */
 
365
        if (newtuple)
 
366
        {                                                       /* UPDATE */
 
367
                int                     chnattrs = 0;
 
368
                int                     chattrs[MaxAttrNum];
 
369
                Datum           newvals[MaxAttrNum];
 
370
                char            newnulls[MaxAttrNum];
 
371
 
 
372
                newvals[chnattrs] = newtimeoff;
 
373
                newnulls[chnattrs] = ' ';
 
374
                chattrs[chnattrs] = attnum[a_time_on];
 
375
                chnattrs++;
 
376
 
 
377
                newvals[chnattrs] = NOEND_ABSTIME;
 
378
                newnulls[chnattrs] = ' ';
 
379
                chattrs[chnattrs] = attnum[a_time_off];
 
380
                chnattrs++;
 
381
 
 
382
                if (argc == MaxAttrNum)
 
383
                {
 
384
                        /* set update_user value */
 
385
                        newvals[chnattrs] = newuser;
 
386
                        newnulls[chnattrs] = ' ';
 
387
                        chattrs[chnattrs] = attnum[a_upd_user];
 
388
                        chnattrs++;
 
389
                        /* clear delete_user value */
 
390
                        newvals[chnattrs] = nulltext;
 
391
                        newnulls[chnattrs] = 'n';
 
392
                        chattrs[chnattrs] = attnum[a_del_user];
 
393
                        chnattrs++;
 
394
                        /* set insert_user value */
 
395
                        newvals[chnattrs] = nulltext;
 
396
                        newnulls[chnattrs] = 'n';
 
397
                        chattrs[chnattrs] = attnum[a_ins_user];
 
398
                        chnattrs++;
 
399
                }
 
400
 
 
401
                rettuple = SPI_modifytuple(rel, newtuple, chnattrs, chattrs, newvals, newnulls);
 
402
 
 
403
                /*
 
404
                 * SPI_copytuple allocates tmptuple in upper executor context - have
 
405
                 * to free allocation using SPI_pfree
 
406
                 */
 
407
                /* SPI_pfree(tmptuple); */
 
408
        }
 
409
        else
 
410
                /* DELETE case */
 
411
                rettuple = trigtuple;
 
412
 
 
413
        SPI_finish();                           /* don't forget say Bye to SPI mgr */
 
414
 
 
415
        pfree(relname);
 
416
        return PointerGetDatum(rettuple);
 
417
}
 
418
 
 
419
/*
 
420
 * set_timetravel (relname, on) --
 
421
 *                                      turn timetravel for specified relation ON/OFF
 
422
 */
 
423
PG_FUNCTION_INFO_V1(set_timetravel);
 
424
 
 
425
Datum
 
426
set_timetravel(PG_FUNCTION_ARGS)
 
427
{
 
428
        Name            relname = PG_GETARG_NAME(0);
 
429
        int32           on = PG_GETARG_INT32(1);
 
430
        char       *rname;
 
431
        char       *d;
 
432
        char       *s;
 
433
        int32           ret;
 
434
        TTOffList  *p,
 
435
                           *pp;
 
436
 
 
437
        for (pp = (p = &TTOff)->next; pp; pp = (p = pp)->next)
 
438
        {
 
439
                if (namestrcmp(relname, pp->name) == 0)
 
440
                        break;
 
441
        }
 
442
        if (pp)
 
443
        {
 
444
                /* OFF currently */
 
445
                if (on != 0)
 
446
                {
 
447
                        /* turn ON */
 
448
                        p->next = pp->next;
 
449
                        free(pp);
 
450
                }
 
451
                ret = 0;
 
452
        }
 
453
        else
 
454
        {
 
455
                /* ON currently */
 
456
                if (on == 0)
 
457
                {
 
458
                        /* turn OFF */
 
459
                        s = rname = DatumGetCString(DirectFunctionCall1(nameout, NameGetDatum(relname)));
 
460
                        if (s)
 
461
                        {
 
462
                                pp = malloc(sizeof(TTOffList) + strlen(rname));
 
463
                                if (pp)
 
464
                                {
 
465
                                        pp->next = NULL;
 
466
                                        p->next = pp;
 
467
                                        d = pp->name;
 
468
                                        while (*s)
 
469
                                                *d++ = tolower((unsigned char) *s++);
 
470
                                        *d = '\0';
 
471
                                }
 
472
                                pfree(rname);
 
473
                        }
 
474
                }
 
475
                ret = 1;
 
476
        }
 
477
        PG_RETURN_INT32(ret);
 
478
}
 
479
 
 
480
/*
 
481
 * get_timetravel (relname) --
 
482
 *      get timetravel status for specified relation (ON/OFF)
 
483
 */
 
484
PG_FUNCTION_INFO_V1(get_timetravel);
 
485
 
 
486
Datum
 
487
get_timetravel(PG_FUNCTION_ARGS)
 
488
{
 
489
        Name            relname = PG_GETARG_NAME(0);
 
490
        TTOffList  *pp;
 
491
 
 
492
        for (pp = TTOff.next; pp; pp = pp->next)
 
493
        {
 
494
                if (namestrcmp(relname, pp->name) == 0)
 
495
                        PG_RETURN_INT32(0);
 
496
        }
 
497
        PG_RETURN_INT32(1);
 
498
}
 
499
 
 
500
static int
 
501
findTTStatus(char *name)
 
502
{
 
503
        TTOffList  *pp;
 
504
 
 
505
        for (pp = TTOff.next; pp; pp = pp->next)
 
506
                if (pg_strcasecmp(name, pp->name) == 0)
 
507
                        return 0;
 
508
        return 1;
 
509
}
 
510
 
 
511
/*
 
512
AbsoluteTime
 
513
currabstime()
 
514
{
 
515
        return (GetCurrentAbsoluteTime());
 
516
}
 
517
*/
 
518
 
 
519
static EPlan *
 
520
find_plan(char *ident, EPlan ** eplan, int *nplans)
 
521
{
 
522
        EPlan      *newp;
 
523
        int                     i;
 
524
 
 
525
        if (*nplans > 0)
 
526
        {
 
527
                for (i = 0; i < *nplans; i++)
 
528
                {
 
529
                        if (strcmp((*eplan)[i].ident, ident) == 0)
 
530
                                break;
 
531
                }
 
532
                if (i != *nplans)
 
533
                        return (*eplan + i);
 
534
                *eplan = (EPlan *) realloc(*eplan, (i + 1) * sizeof(EPlan));
 
535
                newp = *eplan + i;
 
536
        }
 
537
        else
 
538
        {
 
539
                newp = *eplan = (EPlan *) malloc(sizeof(EPlan));
 
540
                (*nplans) = i = 0;
 
541
        }
 
542
 
 
543
        newp->ident = (char *) malloc(strlen(ident) + 1);
 
544
        strcpy(newp->ident, ident);
 
545
        newp->splan = NULL;
 
546
        (*nplans)++;
 
547
 
 
548
        return (newp);
 
549
}