Re: Open 7.4 items - Mailing list pgsql-hackers

From Jan Wieck
Subject Re: Open 7.4 items
Date
Msg-id 3F816F60.4090802@Yahoo.com
Whole thread Raw
In response to Re: Open 7.4 items  (Bruce Momjian <pgman@candle.pha.pa.us>)
Responses Re: Open 7.4 items
List pgsql-hackers
Bruce Momjian wrote:
> Wow, that's a heap of code --- that's my only comment.  :-)

Not really.

I'm not sure what conditions could possibley cause SPI_prepare to return 
NULL, but it'd be certainly better to check that. Other than that, looks 
good to me.


Jan

> 
> ---------------------------------------------------------------------------
> 
> Tom Lane wrote:
>> Stephan Szabo <sszabo@megazone.bigpanda.com> writes:
>> > It's not cleaned up, but yes. It appears to work for the simple tests I've
>> > done and should fall back if the permissions don't work to do a single
>> > query on both tables.
>> 
>> Here's my code-reviewed version of the patch.  Anyone else want to take
>> a look?
>> 
>> > I wasn't sure what to do about some of the spi error conditions.  For many
>> > of them I'm just returning false now so that it will try the other
>> > mechanism in case that might work. I'm not really sure if that's worth it,
>> > or if I should just elog(ERROR) and give up.
>> 
>> I think you may as well keep it the same as the other RI routines and
>> just elog() on SPI error.  If SPI is broken, the trigger procedure is
>> gonna fail too.
>> 
>> I changed that, consolidated the error-reporting code, and fixed a couple
>> other little issues, notably:
>> 
>> * The generated query applied ONLY to the FK table but not the PK table.
>>   I assume this was just an oversight.
>> 
>> * The query is now run using SPI_execp_current and selecting "current"
>>   snapshot.  Without this, we could fail in a serializable transaction
>>   if someone else has already committed changes to either relation.
>>   For example:
>> 
>>     create pk and fk tables;
>> 
>>     begin serializable xact;
>>     insert into pk values(1);
>>     insert into fk values(1);
>> 
>>                         begin;
>>                         insert into fk values(2);
>>                         commit;
>> 
>>     alter table fk add foreign key ...;
>> 
>>   The ALTER will not be blocked from acquiring exclusive lock, since
>>   T2 already committed.  But if we run the query in the serializable
>>   snapshot, it won't see the violating row fk=2.
>> 
>> The old trigger-based check avoids this error because the scan loop uses
>> SnapshotNow to select live rows from the FK table.  There is a dual race
>> condition where T2 deletes a row from the PK table.  In current CVS tip
>> this will be detected and reported as a serialization failure, because
>> T1 won't be able to get SELECT FOR UPDATE lock on the deleted row.  With
>> the proposed patch you'll instead see a "no such key" failure, which I
>> think is fine, even though it nominally violates serializability.
>> 
>> Comments?  Can anyone else do a code review (Jan??)?
>> 
>>             regards, tom lane
>> 
> 
> Content-Description: RIcheck.patch
> 
>> *** src/backend/commands/tablecmds.c.orig    Thu Oct  2 15:24:52 2003
>> --- src/backend/commands/tablecmds.c    Sun Oct  5 16:29:51 2003
>> ***************
>> *** 3455,3460 ****
>> --- 3455,3467 ----
>>       int            count;
>>   
>>       /*
>> +      * See if we can do it with a single LEFT JOIN query.  A FALSE result
>> +      * indicates we must proceed with the fire-the-trigger method.
>> +      */
>> +     if (RI_Initial_Check(fkconstraint, rel, pkrel))
>> +         return;
>> + 
>> +     /*
>>        * Scan through each tuple, calling RI_FKey_check_ins (insert trigger)
>>        * as if that tuple had just been inserted.  If any of those fail, it
>>        * should ereport(ERROR) and that's that.
>> *** src/backend/utils/adt/ri_triggers.c.orig    Wed Oct  1 17:30:52 2003
>> --- src/backend/utils/adt/ri_triggers.c    Sun Oct  5 16:42:37 2003
>> ***************
>> *** 40,45 ****
>> --- 40,46 ----
>>   #include "rewrite/rewriteHandler.h"
>>   #include "utils/lsyscache.h"
>>   #include "utils/typcache.h"
>> + #include "utils/acl.h"
>>   #include "miscadmin.h"
>>   
>>   
>> ***************
>> *** 164,170 ****
>>                    Datum *vals, char *nulls);
>>   static void ri_ReportViolation(RI_QueryKey *qkey, const char *constrname,
>>                      Relation pk_rel, Relation fk_rel,
>> !                    HeapTuple violator, bool spi_err);
>>   
>>   
>>   /* ----------
>> --- 165,172 ----
>>                    Datum *vals, char *nulls);
>>   static void ri_ReportViolation(RI_QueryKey *qkey, const char *constrname,
>>                      Relation pk_rel, Relation fk_rel,
>> !                    HeapTuple violator, TupleDesc tupdesc,
>> !                    bool spi_err);
>>   
>>   
>>   /* ----------
>> ***************
>> *** 2540,2546 ****
>> --- 2542,2743 ----
>>   }
>>   
>>   
>> + /* ----------
>> +  * RI_Initial_Check -
>> +  *
>> +  *    Check an entire table for non-matching values using a single query.
>> +  *    This is not a trigger procedure, but is called during ALTER TABLE
>> +  *    ADD FOREIGN KEY to validate the initial table contents.
>> +  *
>> +  *    We expect that an exclusive lock has been taken on rel and pkrel;
>> +  *    hence, we do not need to lock individual rows for the check.
>> +  *
>> +  *    If the check fails because the current user doesn't have permissions
>> +  *    to read both tables, return false to let our caller know that they will
>> +  *    need to do something else to check the constraint.
>> +  * ----------
>> +  */
>> + bool
>> + RI_Initial_Check(FkConstraint *fkconstraint, Relation rel, Relation pkrel)
>> + {
>> +     const char *constrname = fkconstraint->constr_name;
>> +     char        querystr[MAX_QUOTED_REL_NAME_LEN * 2 + 250 +
>> +                         (MAX_QUOTED_NAME_LEN + 32) * ((RI_MAX_NUMKEYS * 4)+1)];
>> +     char        pkrelname[MAX_QUOTED_REL_NAME_LEN];
>> +     char        relname[MAX_QUOTED_REL_NAME_LEN];
>> +     char        attname[MAX_QUOTED_NAME_LEN];
>> +     char        fkattname[MAX_QUOTED_NAME_LEN];
>> +     const char *sep;
>> +     List        *list;
>> +     List        *list2;
>> +     int            spi_result;
>> +     void        *qplan;
>> + 
>> +     /*
>> +      * Check to make sure current user has enough permissions to do the
>> +      * test query.  (If not, caller can fall back to the trigger method,
>> +      * which works because it changes user IDs on the fly.)
>> +      *
>> +      * XXX are there any other show-stopper conditions to check?
>> +      */
>> +     if (pg_class_aclcheck(RelationGetRelid(rel), GetUserId(), ACL_SELECT) != ACLCHECK_OK)
>> +         return false;
>> +     if (pg_class_aclcheck(RelationGetRelid(pkrel), GetUserId(), ACL_SELECT) != ACLCHECK_OK) 
>> +         return false;
>> + 
>> +     /*----------
>> +      * The query string built is:
>> +      *  SELECT fk.keycols FROM ONLY relname fk 
>> +      *   LEFT OUTER JOIN ONLY pkrelname pk 
>> +      *   ON (pk.pkkeycol1=fk.keycol1 [AND ...])
>> +      *   WHERE pk.pkkeycol1 IS NULL AND
>> +      * For MATCH unspecified:
>> +      *   (fk.keycol1 IS NOT NULL [AND ...])
>> +      * For MATCH FULL:
>> +      *   (fk.keycol1 IS NOT NULL [OR ...])
>> +      *----------
>> +      */
>> + 
>> +     sprintf(querystr, "SELECT ");
>> +     sep="";
>> +     foreach(list, fkconstraint->fk_attrs)
>> +     {
>> +         quoteOneName(attname, strVal(lfirst(list)));
>> +         snprintf(querystr + strlen(querystr), sizeof(querystr) - strlen(querystr),
>> +                  "%sfk.%s", sep, attname);
>> +         sep = ", ";
>> +     }
>> + 
>> +     quoteRelationName(pkrelname, pkrel);
>> +     quoteRelationName(relname, rel);
>> +     snprintf(querystr + strlen(querystr), sizeof(querystr) - strlen(querystr),
>> +              " FROM ONLY %s fk LEFT OUTER JOIN ONLY %s pk ON (",
>> +              relname, pkrelname);
>> + 
>> +     sep="";
>> +     for (list=fkconstraint->pk_attrs, list2=fkconstraint->fk_attrs; 
>> +          list != NIL && list2 != NIL; 
>> +          list=lnext(list), list2=lnext(list2))
>> +     {
>> +         quoteOneName(attname, strVal(lfirst(list)));
>> +         quoteOneName(fkattname, strVal(lfirst(list2)));
>> +         snprintf(querystr + strlen(querystr), sizeof(querystr) - strlen(querystr),
>> +                  "%spk.%s=fk.%s",
>> +                  sep, attname, fkattname);
>> +         sep = " AND ";
>> +     }
>> +     /*
>> +      * It's sufficient to test any one pk attribute for null to detect a
>> +      * join failure.
>> +      */
>> +     quoteOneName(attname, strVal(lfirst(fkconstraint->pk_attrs)));
>> +     snprintf(querystr + strlen(querystr), sizeof(querystr) - strlen(querystr),
>> +              ") WHERE pk.%s IS NULL AND (", attname);
>> + 
>> +     sep="";
>> +     foreach(list, fkconstraint->fk_attrs)
>> +     {
>> +         quoteOneName(attname, strVal(lfirst(list)));
>> +         snprintf(querystr + strlen(querystr), sizeof(querystr) - strlen(querystr),
>> +                  "%sfk.%s IS NOT NULL",
>> +                  sep, attname);
>> +         switch (fkconstraint->fk_matchtype)
>> +         {
>> +             case FKCONSTR_MATCH_UNSPECIFIED:
>> +                 sep=" AND ";
>> +                 break;
>> +             case FKCONSTR_MATCH_FULL:
>> +                 sep=" OR ";
>> +                 break;
>> +             case FKCONSTR_MATCH_PARTIAL:
>> +                 ereport(ERROR,
>> +                         (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
>> +                          errmsg("MATCH PARTIAL not yet implemented")));
>> +                 break;
>> +             default:
>> +                 elog(ERROR, "unrecognized match type: %d",
>> +                      fkconstraint->fk_matchtype);
>> +                 break;
>> +         }
>> +     }
>> +     snprintf(querystr + strlen(querystr), sizeof(querystr) - strlen(querystr),
>> +              ")");
>> + 
>> +     if (SPI_connect() != SPI_OK_CONNECT)
>> +         elog(ERROR, "SPI_connect failed");
>> + 
>> +     /*
>> +      * Generate the plan.  We don't need to cache it, and there are no
>> +      * arguments to the plan. 
>> +      */
>> +     qplan = SPI_prepare(querystr, 0, NULL);
>> + 
>> +     /*
>> +      * Run the plan.  For safety we force a current query snapshot to be
>> +      * used.  (In serializable mode, this arguably violates serializability,
>> +      * but we really haven't got much choice.)  We need at most one tuple
>> +      * returned, so pass limit = 1.
>> +      */
>> +     spi_result = SPI_execp_current(qplan, NULL, NULL, true, 1);
>>   
>> +     /* Check result */
>> +     if (spi_result != SPI_OK_SELECT)
>> +         elog(ERROR, "SPI_execp_current returned %d", spi_result);
>> + 
>> +     /* Did we find a tuple violating the constraint? */
>> +     if (SPI_processed > 0)
>> +     {
>> +         HeapTuple    tuple = SPI_tuptable->vals[0];
>> +         TupleDesc    tupdesc = SPI_tuptable->tupdesc;
>> +         int            nkeys = length(fkconstraint->fk_attrs);
>> +         int            i;
>> +         RI_QueryKey    qkey;
>> + 
>> +         /*
>> +          * If it's MATCH FULL, and there are any nulls in the FK keys,
>> +          * complain about that rather than the lack of a match.  MATCH FULL
>> +          * disallows partially-null FK rows.
>> +          */
>> +         if (fkconstraint->fk_matchtype == FKCONSTR_MATCH_FULL)
>> +         {
>> +             bool    isnull = false;
>> + 
>> +             for (i = 1; i <= nkeys; i++)
>> +             {
>> +                 (void) SPI_getbinval(tuple, tupdesc, i, &isnull);
>> +                 if (isnull)
>> +                     break;
>> +             }
>> +             if (isnull)
>> +                 ereport(ERROR,
>> +                         (errcode(ERRCODE_FOREIGN_KEY_VIOLATION),
>> +                          errmsg("insert or update on table \"%s\" violates foreign key constraint \"%s\"",
>> +                                 RelationGetRelationName(rel),
>> +                                 constrname),
>> +                          errdetail("MATCH FULL does not allow mixing of null and nonnull key values.")));
>> +         }
>> + 
>> +         /*
>> +          * Although we didn't cache the query, we need to set up a fake
>> +          * query key to pass to ri_ReportViolation.
>> +          */
>> +         MemSet(&qkey, 0, sizeof(qkey));
>> +         qkey.constr_queryno = RI_PLAN_CHECK_LOOKUPPK;
>> +         qkey.nkeypairs = nkeys;
>> +         for (i = 0; i < nkeys; i++)
>> +             qkey.keypair[i][RI_KEYPAIR_FK_IDX] = i + 1;
>> + 
>> +         ri_ReportViolation(&qkey, constrname,
>> +                            pkrel, rel,
>> +                            tuple, tupdesc,
>> +                            false);
>> +     }
>> + 
>> +     if (SPI_finish() != SPI_OK_FINISH)
>> +         elog(ERROR, "SPI_finish failed");
>> + 
>> +     return true;
>> + }
>>   
>>   
>>   /* ----------
>> ***************
>> *** 2905,2910 ****
>> --- 3102,3108 ----
>>           ri_ReportViolation(qkey, constrname ? constrname : "",
>>                              pk_rel, fk_rel,
>>                              new_tuple ? new_tuple : old_tuple,
>> +                            NULL,
>>                              true);
>>   
>>       /* XXX wouldn't it be clearer to do this part at the caller? */
>> ***************
>> *** 2913,2918 ****
>> --- 3111,3117 ----
>>           ri_ReportViolation(qkey, constrname,
>>                              pk_rel, fk_rel,
>>                              new_tuple ? new_tuple : old_tuple,
>> +                            NULL,
>>                              false);
>>   
>>       return SPI_processed != 0;
>> ***************
>> *** 2950,2956 ****
>>   static void
>>   ri_ReportViolation(RI_QueryKey *qkey, const char *constrname,
>>                      Relation pk_rel, Relation fk_rel,
>> !                    HeapTuple violator, bool spi_err)
>>   {
>>   #define BUFLENGTH    512
>>       char        key_names[BUFLENGTH];
>> --- 3149,3156 ----
>>   static void
>>   ri_ReportViolation(RI_QueryKey *qkey, const char *constrname,
>>                      Relation pk_rel, Relation fk_rel,
>> !                    HeapTuple violator, TupleDesc tupdesc,
>> !                    bool spi_err)
>>   {
>>   #define BUFLENGTH    512
>>       char        key_names[BUFLENGTH];
>> ***************
>> *** 2958,2964 ****
>>       char       *name_ptr = key_names;
>>       char       *val_ptr = key_values;
>>       bool        onfk;
>> -     Relation    rel;
>>       int            idx,
>>                   key_idx;
>>   
>> --- 3158,3163 ----
>> ***************
>> *** 2972,2989 ****
>>                    errhint("This is most likely due to a rule having rewritten the query.")));
>>   
>>       /*
>> !      * rel is set to where the tuple description is coming from.
>>        */
>>       onfk = (qkey->constr_queryno == RI_PLAN_CHECK_LOOKUPPK);
>>       if (onfk)
>>       {
>> -         rel = fk_rel;
>>           key_idx = RI_KEYPAIR_FK_IDX;
>>       }
>>       else
>>       {
>> -         rel = pk_rel;
>>           key_idx = RI_KEYPAIR_PK_IDX;
>>       }
>>   
>>       /*
>> --- 3171,3191 ----
>>                    errhint("This is most likely due to a rule having rewritten the query.")));
>>   
>>       /*
>> !      * Determine which relation to complain about.  If tupdesc wasn't
>> !      * passed by caller, assume the violator tuple came from there.
>>        */
>>       onfk = (qkey->constr_queryno == RI_PLAN_CHECK_LOOKUPPK);
>>       if (onfk)
>>       {
>>           key_idx = RI_KEYPAIR_FK_IDX;
>> +         if (tupdesc == NULL)
>> +             tupdesc = fk_rel->rd_att;
>>       }
>>       else
>>       {
>>           key_idx = RI_KEYPAIR_PK_IDX;
>> +         if (tupdesc == NULL)
>> +             tupdesc = pk_rel->rd_att;
>>       }
>>   
>>       /*
>> ***************
>> *** 3008,3015 ****
>>           char       *name,
>>                      *val;
>>   
>> !         name = SPI_fname(rel->rd_att, fnum);
>> !         val = SPI_getvalue(violator, rel->rd_att, fnum);
>>           if (!val)
>>               val = "null";
>>   
>> --- 3210,3217 ----
>>           char       *name,
>>                      *val;
>>   
>> !         name = SPI_fname(tupdesc, fnum);
>> !         val = SPI_getvalue(violator, tupdesc, fnum);
>>           if (!val)
>>               val = "null";
>>   
>> *** src/include/commands/trigger.h.orig    Sun Aug  3 23:01:31 2003
>> --- src/include/commands/trigger.h    Sun Oct  5 16:11:44 2003
>> ***************
>> *** 197,201 ****
>> --- 197,204 ----
>>    * in utils/adt/ri_triggers.c
>>    */
>>   extern bool RI_FKey_keyequal_upd(TriggerData *trigdata);
>> + extern bool RI_Initial_Check(FkConstraint *fkconstraint, 
>> +                              Relation rel, 
>> +                              Relation pkrel);
>>   
>>   #endif   /* TRIGGER_H */
> 
>> 
>> ---------------------------(end of broadcast)---------------------------
>> TIP 9: the planner will ignore your desire to choose an index scan if your
>>       joining column's datatypes do not match
> 

-- 
#======================================================================#
# It's easier to get forgiveness for being wrong than for being right. #
# Let's break this rule - forgive me.                                  #
#================================================== JanWieck@Yahoo.com #



pgsql-hackers by date:

Previous
From: Andrew Dunstan
Date:
Subject: Re: more on initdb
Next
From: Tom Lane
Date:
Subject: Re: Open 7.4 items