>**__RI_HEADER!@ Do NOT REMOVE or MODIFY this line!!!! @!__RI_HEADER** >* Steve Sawyer - Updated 11/22/1999 >* Carl Karsten - Updated 12/15/1999 >* Doug Hennig - Updated 02/18/2000 > >* Program...........: NEWRI.PRG >* Author............: Jzanus Dev Team >* Project...........: Visual Collections >* Created...........: 04/04/2005 14:28:19 >* Copyright.........: (c) Jzanus LTD, 2005 >*) Description.......: >* Calling Samples...: >* Parameter List....: >* Major change list.: Switched from scanning array of relations to table based > >* Known limitations: >* - Cascading updates and deletes for self-joins cause an "illegal recursion" >* error because the RI code tries to update or delete records in the same >* table the trigger is fired for, which VFP doesn't allow (all other >* rules for self-joins are handled correctly) >* - Compound keys aren't supported > >function __RI_Handler >lparameters tcTriggerType > >**-- CHANGE - NN - April 04, 2005 - 14:30:12 - added a new global variable >if vartype(m.glDontUseTriggers)="L" and m.glDontUseTriggers > return && Don't do any validations >endif > >local lcTable, ; > lcAlias, ; > lcRecordState, ; > lnTriggerType, ; > lcDBC, ; > lcParent, ; > lcChild, ; > lcParentKeyExp, ; > lcChildKeyExp, ; > lnRIInfo, ; > luOldKey, ; > luNewKey, ; > luOldFK, ; > luNewFK, luKey, llReturn > >* Get the name of the table the trigger was fired for and determine what type >* of trigger was fired. > >lcTable = upper(cursorgetprop('SourceName')) >lcAlias = alias() >lcRecordState = getfldstate(-1) >llReturn = .t. > >** This command is not supported by Ole DB >#define cnERR_TRIGGER_FAILED 1539 >#define cnDeleteCode 10 >#define cnInsertCode 20 >#define cnUpdateCode 30 >#define cL_USE_AGAIN .t. >* Trigger failed error number > >* If the trigger was passed, use it. >if vartype(m.tcTriggerType) = 'C' and not empty(m.tcTriggerType) > lnTriggerType = ICASE( ; > upper(m.tcTriggerType)="DELETE", cnDeleteCode, ; > upper(m.tcTriggerType)="INSERT", cnInsertCode, ; > upper(m.tcTriggerType)="UPDATE", cnUpdateCode ) >else >* If the deletion status was changed and the record is deleted, this is a >* "DELETE" trigger. >* Define some constants that'll make the code easier to read. > local ccFLDSTATE_UNCHANGED, ccFLDSTATE_EDITDEL, ccFLDSTATE_NEWUNCHANGED, ccFLDSTATE_NEWCHANGED > ccFLDSTATE_UNCHANGED = '1' >* GETFLDSTATE() 1 = the field is unchanged > ccFLDSTATE_EDITDEL = '2' >* GETFLDSTATE() 2 = the record was edited or deletion status changed > ccFLDSTATE_NEWUNCHANGED = '3' >* GETFLDSTATE() 3 = a new unchanged field or new undeleted record > ccFLDSTATE_NEWCHANGED = '4' >* GETFLDSTATE() 3 = a new changed field or new deleted record > do case > case left(m.lcRecordState, 1) = ccFLDSTATE_EDITDEL and deleted() > lnTriggerType = cnDeleteCode > >* If the deletion status was changed and the record is not deleted, it was >* just recalled, so this is an "INSERT" trigger. > > case left(m.lcRecordState, 1) = ccFLDSTATE_EDITDEL > lnTriggerType = cnInsertCode > >* If this is a new record, this is an "INSERT" trigger. > > case ccFLDSTATE_NEWUNCHANGED $ m.lcRecordState or ; > ccFLDSTATE_NEWCHANGED $ m.lcRecordState > lnTriggerType = cnInsertCode > >* Some field in the table has been changed, so this is an "UPDATE" trigger. > > case ccFLDSTATE_EDITDEL $ m.lcRecordState > lnTriggerType = cnUpdateCode > >* Carl Karsten found a weird bug in VFP: if you have a table with a memo field >* and delete a record such that the subsequent records have to be physically >* moved when you PACK, the "UPDATE" trigger for the table fires when you move >* the record pointer or close the table. In that case, we'll ignore it. > > case m.lcRecordState = replicate(ccFLDSTATE_NEWUNCHANGED, len(m.lcRecordState)) > return > endcase >endif > >* If we're at the top trigger level, start a transaction, create an error flag >* and array variables, get a snapshot of open tables, and set up the >* environment the way we need it. > >if _triggerlevel = 1 > private plError > plError = .f. > release gaErrors > public gaErrors[1, 12] > > private paUsed[1], ; > pcExact, ; > pcANSI, ; > pcDeleted, ; > pcOnEscape, ; > pcError, ; > pcOldDBC, plError > > aused(paUsed) > pcExact = set('EXACT') > pcANSI = set('ANSI') > pcDeleted = set('DELETED') > pcError = on('ERROR') > pcOnEscape = on('escape') > pcOldDBC = iif(empty(dbc()), '', '"' + dbc() + '"') > >* this command is not supported in Ole Db > on escape RICleanUp(.t.) > > set exact on > set ansi on > set deleted on > on error LogRIError(error(),message()) &&plError = .t. > #if cL_USE_AGAIN > SELECT 0 > use RIDefinitions AGAIN > LOCAL lnSelRIDefi > lnSelRIDefi = SELECT() > #else > if not used('RIDefinitions') > use RIDefinitions in 0 && Open table with RI Definitions > ENDIF > #endif > begin transaction >endif > >if not m.plError > lcDBC = cursorgetprop('Database') > set database to (m.lcDBC) > local lcParentTag, lcSearch > store "" to lcParentKeyExp, lcChildKeyExp, lcParentTag > > lcSearch = padr(m.lcTable,128) > lcParent = m.lcTable > #if cL_USE_AGAIN > SELECT (m.lnSelRIDefi) > #else > select RIDefinitions > #endif > scan for upper(ParentTB) = m.lcSearch and alltrim(ParentTag)<> m.lcParentTag and not m.plError > > lcParentTag = alltrim(ParentTag) > > do case > >* If this is an update trigger and this relation has our table as the parent, >* let's process it. We'll ignore it if the RI rule is empty or "ignore". > > case m.lnTriggerType = cnUpdateCode > if UpdTrigger > 0 && not ignore > if not alltrim(ParentKey) == m.lcParentKeyExp && So it would calculate it only once > lcParentKeyExp = alltrim(ParentKey) > luOldKey = oldval(m.lcParentKeyExp, m.lcAlias) > luNewKey = evaluate(IIF("."$m.lcParentKeyExp, "", m.lcAlias + ".") + m.lcParentKeyExp ) > endif > >* If this is a self-join, we may have an update trigger because the FK field >* in the "child" copy of the table was changed (which really is an insert >* trigger), so let's handle it if we have a restrict insert rule. The reason >* we don't handle it in a CASE below is that the user may have changed both >* the parent and foreign key fields. > > if m.lcParent == alltrim(ChildTB) && very rare case > luOldFK = oldval(m.lcChildKeyExp, m.lcAlias) > luNewFK = evaluate(IIF("."$m.lcChildKeyExp, "", m.lcAlias + ".") + m.lcChildKeyExp > >** Restrict ? > if InsTrigger = 1 ; > and not isnull(m.luOldFK) ; > and not isnull(m.luNewFK) ; > and m.luOldFK <> m.luNewFK > Restrict_Insert(m.luNewFK, "UPDATE") > endif luOldFK <> luNewFK ... > endif lcParent = lcChild > > do case > >* The parent key wasn't changed or an error occurred, so we have nothing to do. > > case m.plError or m.luOldKey = m.luNewKey > >* If the parent key has been changed, call the appropriate function, depending >* on whether this is a cascade or restrict rule. > > case m.lnRIInfo = 2 AND not isnull(m.luOldKey) && 'Cascade' > Cascade_Update(m.luOldKey, m.luNewKey, alltrim(ChildKey)) > > case m.lnRIInfo = 1 AND not isnull(m.luOldKey) && Restrict > Restrict_Update(m.luOldKey, m.luNewKey) > > endcase > endif > >* If this is a delete trigger and this relation has our table as the parent, >* let's process it. We'll ignore it if the RI rule is empty or "ignore". > > case m.lnTriggerType = cnDeleteCode > if DelTrigger > 0 > if not alltrim(ParentKey) == m.lcParentKeyExp && So it would calculate it only once > lcParentKeyExp = alltrim(ParentKey) > *-- iif can be eliminated/replaced by concat if no alias is ever present in parentkey... > luKey = evaluate(IIF("."$m.lcParentKeyExp, "", m.lcAlias + ".") + m.lcParentKeyExp ) > endif > >* Call the appropriate function, depending on whether this is a cascade or >* restrict rule. > = ICASE(isnull(m.luKey), .f., ; > DelTrigger = 2, Cascade_Delete(m.luKey, alltrim(ChildKey)), ; > DelTrigger = 1, Restrict_Delete(m.luKey) ) > endif > endcase > > endscan > >** Now do the same for the child > if inlist(m.lnTriggerType,cnInsertCode,cnUpdateCode) && there are no restrictions on delete, if it's a child table > #if cL_USE_AGAIN > SELECT (m.lnSelRIDefi) > #else > select RIDefinitions > #endif > lcChild = m.lcTable > lcChildKeyExp = "" > > scan for upper(ChildTB) = m.lcSearch and InsTrigger = 1 and not m.plError > > if vartype(m.pcCascadeParent) = "C" and m.pcCascadeParent = alltrim(ParentTB) >** In this case we do nothing - otherwise we're burried in recursion > else > >* If this relation has our table as the child, let's process it. We'll only >* process a "restrict" rule in either an insert or update trigger. > if not m.lcChildKeyExp == alltrim(ChildKey) && we don't want to evaluate more times than needed > lcChildKeyExp = alltrim(ChildKey) > luNewKey = evaluate(IIF("."$m.lcChildKeyExp, "", m.lcAlias + ".") + m.lcChildKeyExp > luOldKey = oldval(m.lcChildKeyExp, m.lcAlias) > endif > >* If this is an insert trigger or if it's an update trigger and the foreign key >* has changed, call the Restrict_Insert function to ensure the foreign key >* exists in the parent table. > > if !isnull(m.luNewKey) AND (m.lnTriggerType = cnInsertCode or m.luOldKey <> m.luNewKey) > Restrict_Insert(m.luNewKey, "INSERT") > endif > endif > endscan > endif >endif > >* If we're at the top trigger level, either end the transaction or roll it >* back, depending on whether the trigger succeeded or not, close any tables we >* opened, restore the things we changed, and return whether we succeeded or >* not. >if _triggerlevel = 1 > =RICleanUp(.f.) >* If we're not at the top trigger level, return .T. so we don't trigger an >* error yet. > llReturn = not m.plError >endif _triggerlevel = 1 >return m.llReturn > >* CleanUp procedure >function RICleanUp(tlEscaped) >if m.tlEscaped and messagebox("Are you sure you want to stop the trigger's execution?",4+16,"Stop execution") = 7 >** > retry >else > > #if cL_USE_AGAIN > USE IN (m.lnSelRIDefi) > #endif > local lnTables, laTables[1], lnI > if not empty(m.pcOldDBC) > set database to (m.pcOldDBC) > endif > if _triggerlevel = 1 > if m.plError or m.tlEscaped > rollback > else > end transaction > endif plError > >** Gregory Adam suggested in the thread #<A HREF="/wconnect/wc.dll?LevelExtreme~2,15,1002645">1002645</A> to only close tables in Default DS > if set("Datasession") > 1 or vartype(m.glLeaveTablesOpen)="L" and m.glLeaveTablesOpen && we're dealing with private DS, don't need to close > else > lnTables = aused(laTables) > for lnI = 1 to m.lnTables > lcTable = laTables[m.lnI, 1] > if not empty(m.lcTable) and ascan(paUsed, m.lcTable) = 0 > use in (m.lcTable) > endif not empty(lcTable) ... > next lnI > > endif > > if m.pcExact = "OFF" > set exact off > endif > > if m.pcANSI = "OFF" > set ansi off > endif > > if m.pcDeleted = "OFF" > set deleted off > endif > on error &pcError >** this command is not supported in Ole Db > on escape &pcOnEscape > endif >endif >return > >* Cascade update function: change the foreign key field in all child records >* that belong to the parent record. > >function Cascade_Update >lparameters tuOldKey, tuNewKey, tcChildKey >* call 3. parameter with alltrim(ChildKey) > >*-- necessary ? if so, replace further down with this private... >private pcCascadeParent >pcCascadeParent = alltrim(ParentTB) >* Do the cascading update. Log any error that occurred. >update (m.pcCascadeParent) ; > set &tcChildKey = m.tuNewKey ; > where &tcChildKey = m.tuOldKey > >if m.plError > local laError[1], tcParentKey > aerror(laError) > > LogRIError(laError[1], laError[2], 'Cascade Update', ; > m.pcCascadeParent, .f., alltrim(ParentKey), m.tuNewKey, ; > alltrim(ChildTB), 0, m.tcChildKey, m.tuOldKey) >endif && plError >return > >* Cascade delete function: delete all child records that belong to the parent >* record. > >function Cascade_Delete >lparameters tuOldKey, tcChildKey > >delete ; > from (ChildTB) ; > where &tcChildKey = m.tuOldKey > >if m.plError > local laError[1] > aerror(laError) > > LogRIError(laError[1], laError[2], 'Cascade Delete', ; > alltrim(ParentTB), .f., alltrim(ParentKey), m.tuOldKey, ; > alltrim(ChildTB), 0, m.tcChildKey, m.tuOldKey) >endif && plError >RETURN > >* Restrict delete function: see if there are any records in the specified child >* table matching the specified key in the parent table. > >function Restrict_Delete >lparameters tuKey > >local lcTable >lcTable = strtran(m.tcChildTable, ' ', '_') > >if not used(m.lcTable) > use (m.tcChildTable) again in 0 shared alias (m.lcTable) >endif &¬ used(lcTable) > >**** WHERE IS m.tcTag comin from now ??? was commented out but not in the new code ?? >if indexseek(m.tuKey, .f., m.lcTable, m.tcTag) && Record exists >* if keymatch(m.tuKey,tagno(m.tcTag, '', m.lcTable), m.lcTable) && Record exists > plError = .t. > LogRIError(cnERR_TRIGGER_FAILED, 'Trigger Failed', 'Restrict Delete', ; > alltrim(ParentTB), .f., alltrim(ParentKey), m.tuKey, ; > alltrim(ChildTB), 0, alltrim(ChildKey), m.tuKey) >endif &&_tally > 0 >RETURN > >* Restrict update function: see if there are any records in the specified child >* table matching the specified key in the parent table. > >function Restrict_Update >lparameters tuOldKey, tuNewKey > >local lcTable >lcTable = strtran(alltrim(ChildTB), ' ', '_') > >if not used(m.lcTable) > use (alltrim(ChildTB)) again in 0 shared alias (m.lcTable) >endif && not used(lcTable) > >if indexseek(m.tuOldKey, .f., m.lcTable, alltrim(ChildTag)) && Record exists > plError = .t. > LogRIError(cnERR_TRIGGER_FAILED, 'Trigger Failed', 'Restrict Update', ; > alltrim(ParentTB), .f., alltrim(ParentKey), m.tuNewKey, ; > alltrim(ChildTB), 0, alltrim(ChildKey), m.tuOldKey) >endif >RETURN > >* Restrict insert function: ensure a record in the parent table for the foreign >* key exists. >* CFK - 5/28 >* Or if the foreign key is null, no parent record is needed. >* The logic is this: >* if you specify in the dbc that a child cannot exist with out a parent, >* you can laxly enforce that by allowing nulls in that feild >* and this code will allow that setting to be overridden >* by setting the foreign key to null >* or, >* you can strictly enforce it by not allowing nulls, and then it will >* pass this test, but fail the "allow nulls" test that the >* database engine enforces. > >function Restrict_Insert >lparameters tuKey, tcTriggerType > >* CFK - 5/28 >* If the key is Null, don't check for a parent, let it pass. > >local lcTable >lcTable = strtran(alltrim(ParentTB), ' ', '_') > >if not used(m.lcTable) > use (alltrim(ParentTB)) again in 0 shared alias (m.lcTable) >ENDIF && not used(lcTable) > >if not indexseek(m.tuKey,.f.,m.lcTable,alltrim(ParentTag)) > plError = .t. > LogRIError(cnERR_TRIGGER_FAILED, 'Trigger Failed', 'Restrict ' + proper(m.tcTriggerType), ; > alltrim(ParentTB), 0, alltrim(ParentKey), 'Not Found', ; > alltrim(ChildTB), .f., alltrim(ChildKey), m.tuKey) >endif >RETURN > > >* Log errors to the public gaError array. >procedure LogRIError(tnErrNo, ; > tcMessage, ; > tcCode, ; > tcParentTable, ; > tnParentRec, ; > tcParentExp, ; > tuParentKey, ; > tcChildTable, ; > tnChildRec, ; > tcChildExp, ; > tuChildKey) > >IF VARTYPE(m.tnParentRec)!="N" > tnParentRec = iif(used(m.tcParentTable),recno(m.tcParentTable), 0) >ENDIF > >IF VARTYPE(m.tnChildRec)!="N" > tnChildRec = iif(used(m.tcChildTable),recno(m.tcChildTable), 0) >ENDIF > >local lnErrorRows, ; > lnLevel > >plError = .t. >* Add another row to the error array if necessary. > >lnErrorRows = alen(gaErrors, 1) >if type('gaErrors[lnErrorRows, 1]') <> 'L' > lnErrorRows = lnErrorRows + 1 > dimension gaErrors[lnErrorRows, alen(gaErrors, 2)] >endif type('gaErrors[lnErrorRows, 1]') <> 'L' > >* Log the error information, including the parameters passed to us and the >* program stack. > >gaErrors[lnErrorRows, 1] = tnErrNo >gaErrors[lnErrorRows, 2] = tcMessage >gaErrors[lnErrorRows, 3] = evl(tcCode,"") >gaErrors[lnErrorRows, 4] = '' >for lnLevel = 1 to program(-1) > gaErrors[lnErrorRows, 4] = gaErrors[lnErrorRows, 4] + ',' + ; > program(lnLevel) >next lnLevel >gaErrors[lnErrorRows, 5] = evl(tcParentTable,"") >gaErrors[lnErrorRows, 6] = evl(tnParentRec,0) >gaErrors[lnErrorRows, 7] = evl(tuParentKey,"") >gaErrors[lnErrorRows, 8] = evl(tcParentExp,"") >gaErrors[lnErrorRows, 9] = evl(tcChildTable,"") >gaErrors[lnErrorRows, 10] = evl(tnChildRec,0) >gaErrors[lnErrorRows, 11] = evl(tuChildKey,"") >gaErrors[lnErrorRows, 12] = evl(tcChildExp,"") >return tnErrNo > >**__RI_FOOTER!@ Do NOT REMOVE or MODIFY this line!!!! @!__RI_FOOTER**