Stream Triggers¶
BDRは、ダウンストリーム/ターゲットノードでの追加のデータ処理に使用できる新しいタイプのトリガーを導入します。
競合のトリガー
トリガーの変換
これらのタイプのトリガーを合わせて、ストリームトリガーと呼びます。
!!! Note * 現在、この機能はEDB Postgres ExtendedおよびEDB Postgres Advancedでのみ利用可能です。
ストリームトリガーは、構文がトリガーのように設計されており、PostgreSQL BEFOREトリガーアーキテクチャを活用し、 PostgreSQL BEFOREトリガーと同様のパフォーマンス特性を持つ可能性があります。
通常のPostgreSQLトリガーと同様に、1つのトリガーファンクションをマルチプルのトリガー定義で使用できます。トリガーファンクションは、このフォームで定義されたプログラムです:CREATE FUNCTION ... RETURNS TRIGGER。実際のトリガーを作成するには、CREATE
TRIGGERコマンドを使用する必要はありません。代わりに、特別なBDR関数bdr.create_conflict_trigger()およびbdr.create_transform_trigger()を使用してストリームトリガーが作成されます。
作成されると、トリガーはカタログテーブルpg_triggerに可視されます。ストリームトリガーはtgisinternal = trueとtgenabled = 'D'としてマークされ、名前のサフィックス「 _bdrc」または「 _bdrt」が付きます。
visibleb_tran_4は、テーブルにリレーションするトリガーに関する情報、実行されているプロシージャの名前、トリガーをトリガーするイベント、およびトリガータイプを提供します。
そのため、通常のSQL処理ではストリームSQLが有効にならないことに注意して名前。
これらのトリガーは、ダウンストリームまたはターゲットノードで実行されることに注意してください。オリジンノードで実行オプションはありませんが、オリジンでrow_filter式の使用を検討することもできます。
また、streamtriggerの実行中に適用されるDMLは他のBDRノードに複製されず、標準のローカルトリガーの実行をトリガーしません。これは意図的なものであり、インスタンス、ストリームトリガーによってキャプチャされた変更または競合を、そのノード固有のクラッシュセーフテーブルにログするために使用できます。この章の最後に実用例を示します。
適用中のトリガー実行¶
トランスフォームトリガーは、トリガーテーブルの着信変更ごとに1回、最初に実行されます。これらのトリガーは、一致するターゲット行を見つけようとする前に起動し、非常にレンジの変換を効率的かつ一貫して適用できます。
次に、UPDATEおよびDELETEの変更について、ターゲット行を見つけます。ターゲット行がない場合、それらの変更タイプに対するそれ以上の処理はありません。
次に、以前にテーブルレベルでレプリカトリガーとして明示的に有効にされていた通常のトリガーを実行します。
ALTER TABLE tablename
ENABLE REPLICA TRIGGER trigger_name;
次に、潜在的な競合が存在するかどうかを判断し、存在する場合は、そのテーブルに存在する競合トリガーを呼び出します。
列の競合解決がありません¶
変換トリガーが実行される前に、 PostgreSQLは着信タプルをターゲットテーブルの行タイプとマッチしようとします。
入力行に存在するがターゲットテーブルには存在しない列は、target_column_missing型の競合をトリガーします。逆に、ターゲットテーブルに存在する列が受信行に存在しない場合、source_column_missingの競合が発生します。これら2つの競合タイプのデフォルトの解像度は、それぞれignore_if_nullとuse_default_valueです。
これは、ローリングスキーマアップグレードのコンテキストに関連しています。
forinstance、スキーマの新しいバージョンが新しい列を導入する場合。スキーマの古いバージョンから新しいバージョンに複製する場合、ソース列が欠落しており、use_default_valuestrategyが適切です。これは、新しく導入された列にデフォルト値を設定するためです。
ただし、新しいスキーマバージョンのノードから古いスキーマバージョンのノードにレプリケーションする場合、列はアップグレードにありません。ignore_if_nullリゾルバは、ユーザーがアップグレードされたノードのいずれか、新しい列にNULL以外の値を持つタプル。
この例をビューして、ローリングスキーマアップグレードの適切な設定は、target_column_missingの競合の場合にignoreリゾルバを適用するように各ノードを構成することです。
これは、node1を実際のノード名前に置き換えた後、各ノードで別々に実行する必要がある次のクエリーで行われます:
SELECT bdr.alter_node_set_conflict_resolver('node1',
* 'target_column_missing', 'ignore');
データ損失と発散リスク¶
このセクションでは、競合リゾルバをignoreに設定すると、データロスとクラスターの発散がどのように発生するかを示します。
次の例を考えてみましょう。テーブルtはノード1と2に存在しますが、その列colはノード1にのみ存在します。
コンフリクトリゾルバがignoreに設定されている場合、ノード1にcがnullではない行((pk=1, col=100)など)が存在する可能性があります。その行はノード2に複製され、c列の値は破棄されます(例:(pk=1))。
次に、列cがノード2のテーブルに追加されると、既存のすべての行で最初にNULLに設定され、上記で考慮された行は(pk=1, col=NULL)になります:pk=1を持つ行はすべてのノードで同一ではなくなり、したがってクラスターは分岐します。
デフォルトのignore_if_nullリゾルバは、このリスクの影響を受けないことに注意してください。ノード2に複製される行にはcol=NULLがあるためです。
この例に基づいて、ignoreリゾルバが使用されたローリングスキーマアップグレードの最後に、クラスタ全体に対してLiveCompareを実行して、相違を検出して修正makeことをお勧めします。
行タイプの用語¶
このドキュメントでは、次の行タイプを使用しています。
SOURCE_OLDは、更新前の行、つまりキーです。SOURCE_NEWは、別のノードからの新しい行です。TARGETは、ノード上にすでに存在する行、つまり競合する行です。
競合のトリガー¶
競合トリガーは、 BDRによって競合が検出されたときに実行され、競合が発生したときに何が起こるかを決定するために使用されます。
トリガーファンクションが行を返す場合、アクションはターゲットに適用されます。
トリガーファンクションがNULL行を返す場合、アクションはスキップされたされます。
明確にするために、DELETEに対してトリガーが呼び出された場合、DELETEをスキップしたい場合、トリガーはNULLを返す必要があります。
DELETEを続行する場合は、行の値を結果ます-SOURCE_OLDまたはTARGETのいずれかが機能します。競合するオペレーションがINSERTまたはUPDATEのいずれかであり、選択された解決が競合する行の削除である場合、トリガーは明示的に削除を実行して結果なければなりませんNULL。トリガーファンクションは、選択に応じて他のSQLアクションを実行できますが、これらのアクションはレプリケートではなくローカルにのみ適用されます。
2つ以上のノード間で実際のデータの競合が発生すると、同時に2つ以上の変更が発生します。これらの変更を適用すると、競合解決は各ノードで独立して発生します。これは、conflictresolutionが各ノードで1回発生し、その間に大幅な時間差で発生する可能性があることを意味します。その結果、conflicttriggerのマルチプルの実行間で通信する可能性はありません。トリガーが関連するすべてのイベントに対してまったく同じ結果を与えるようにすることは、競合トリガーの作成者の責任です。そうしないと、データの相違が発生します。テクニカルサポートは、BDRに付属のisolationtesterツールを使用して、すべての競合トリガーを正式にテストすることをお勧めします。
!!!警告-複数の競合トリガーを単一のテーブルに指定できますが、それらは個別のイベントにマッチする必要があります。つまり、各競合は単一の競合トリガーにのみマッチする必要があります。同じテーブルの同じイベントにマッチングする複数のトリガーは推奨されません。一貫性のない動作が発生する可能性があり、将来のリリースでは禁止されます。
同じ競合トリガーが複数のイベントに一致する場合、トリガー内でTG_OP変数を使用して、競合を引き起こしたオペレーションを識別できます。
デフォルトでは、 BDR行のレプリケーション元の変更を監視することで競合を検出します。そのため、変更が1つしか発生していない場合でも競合トリガーを呼び出すことができます。この場合、実際の競合は存在しないため、この競合検出メカニズムは偽陽性の競合を生成する可能性があると言います。前述のように、競合トリガーはそれらすべてを同一にハンドルする必要があります。
場合によっては、タイムスタンプの競合の検出では競合がまったく検出されないことに注意してください。例、UPDATEの直後にDELETEが発生する同時UPDATE / DELETEでは、最初にUPDATEを表示し、次にDELETEを表示するノードでは競合は発生しません。競合が見られない場合、競合トリガーは呼び出されません。同じシチュエーションですが、行バージョンの競合検出を使用すると、競合が表示され、競合トリガーによって処理できます。
トリガーファンクションは、オペレーションの種類に応じて、競合に関係するデータ行だけでなく、追加の状態情報にもアクセスできます。
INSERTでは、競合トリガーはSOURCE_NEW行にアクセスできます ソースおよびTARGET行UPDATEでは、競合トリガーはSOURCE_OLDおよび ソースからのSOURCE_NEW行およびTARGET行DELETEでは、競合トリガーはSOURCE_OLD行にアクセスできます ソースおよびTARGET行
ファンクションbdr.trigger_get_row()は、そのオペレーションの値が存在する場合、SOURCE_OLD、SOURCE_NEW、またはTARGET行を取得するために使用できます。
競合トリガーの変更はトランザクション的に発生し、ALTER TABLEの一部のバリアントの処理方法と同様に、構成変更のレプリケーション中にグローバルDMLロックによって保護されます。
プライマリキーが競合トリガー内で更新されると、実行のタイミングの違いにより一意性制約違反エラーが発生することがあります。したがって、ユーザーは競合トリガー内でプライマリキーを更新しないでください。
変換トリガー¶
これらのトリガーは、特定のテーブルに対するデータストリームのすべての行に対して実行されることを除いて、競合トリガーに似ています。戻り値と公開された変数の動作は似ていますが、変換トリガーはターゲット行が特定される前に実行されるため、TARGET行はありません。
必要に応じて、 BDRの各テーブルでマルチプルの変換トリガーを指定します。変換トリガーはアルファベット順に実行されます。
変換トリガーは行をフィルターで除去し、必要に応じて追加の操作を実行できます。任意の列の値を変更したり、NULLに設定したりできます。戻り値は、さらに実行されるアクションを決定します。
トリガーファンクションが行を返す場合、ターゲットに適用されます。
トリガーファンクションが
NULL行を返す場合、それ以上のアクションはありません 実行され、まだ実行されていないトリガーは実行されません。トリガーファンクションは、選択に応じて他のアクションを実行できます。
トリガーファンクションは、競合に関係する行だけでなく、追加の状態情報にもアクセスできます。
INSERTでは、変換トリガーはソースからSOURCE_NEW行にアクセスできます。UPDATEでは、変換トリガーはソースからSOURCE_OLDおよびSOURCE_NEW行にアクセスできます。DELETEでは、変換トリガーはソースからSOURCE_OLD行にアクセスできます。
ファンクションbdr.trigger_get_row()を使用して、SOURCE_OLDまたはSOURCE_NEWrowsを取得できます。
TARGET行は使用できません。このタイプのトリガーは、ターゲット行が存在する場合、その行が識別される前に実行されるためです。
変換トリガーは、通常のBEFORE行トリガーと非常によく似ていますが、次の重要な違いがあります。
すべての着信変更に対して変換トリガーが呼び出されます。 BEFOREトリガーは、UPDATEおよびDELETEの変更に対してまったく呼び出されません テーブルにマッチング行が見つからない場合。
パーティションテーブルのルーティングが発生する前に、変換トリガーが呼び出されます。
変換トリガーは、SOURCE_OLDを介してルックアップキーにアクセスできます。 通常のSQLトリガーでは使用できません。
ストリームトリガー変数¶
競合トリガーと変換トリガーの両方は、トリガーAPIが提供する定義済み変数とBDRが提供する追加情報関数を介して、行とメタデータに関する情報にアクセスできます。
PL / pgSQLには、次の定義済み変数が存在します。
TG_NAME¶
データ型名前;実際にトリガーされたトリガーの名前を含む変数。実際のトリガー名前には、トリガーの作成時に指定された名前と比較して、「 _ bdrt」または「 _bdrc」サフィックス(トリガータイプに応じて)が付けられます。
TG_WHEN¶
データ型テキスト。これは、競合トリガーと変換トリガーの両方に対してBEFOREと表示されます。ストリームトリガータイプは、bdr.trigger_get_type()informationファンクションを呼び出して取得できます(以下を参照)。
TG_LEVEL¶
データ型テキスト。 ROWの文字列。
TG_OP¶
データ型テキスト。トリガーが起動されたオペレーションを示すINSERT、UPDATEまたはDELETEの文字列。
TG_RELID¶
データ型oid;トリガー呼び出しを引き起こしたテーブルのオブジェクトID。
TG_TABLE_NAME¶
データ型名前;トリガー呼び出しを引き起こしたテーブルの名前。
TG_TABLE_SCHEMA¶
データ型名前;トリガー呼び出しを引き起こしたテーブルのスキーマの名前。テーブルパーティションの場合、これはルート表の名前です。
TG_NARGS¶
データ型整数;
bdr.create_conflict_trigger()またはbdr.create_transform_trigger()ステートメントでトリガーファンクションに与えられた引数の数。
TG_ARGV []¶
テキストのデータ型配列。
bdr.create_conflict_trigger()またはbdr.create_transform_trigger()ステートメントからの引数。インデックスは0からカウントされます。無効なインデックス(0より小さい以上)は、NULL値になります。
情報関数¶
bdr.trigger_get_row¶
このファンクションは、識別子としてRECORDとして指定されたトリガー行の内容を返します。このファンクションは、不適切に呼び出された場合、つまりオペレーションタイプ(TG_OP)がDELETEの場合にSOURCE_NEWで呼び出された場合、NULLを返します。
あらすじ¶
bdr.trigger_get_row(row_id text)
パラメーター¶
row_id-行の識別子。 SOURCE_NEW、SOURCE_OLD、および TARGET、トリガーのタイプとオペレーションに応じて(の文書を参照 個々のトリガータイプ)。
bdr.trigger_get_committs¶
このファンクションは、識別子によって指定されたトリガー行のコミットタイムスタンプを返します。行が凍結されて行か利用できないため利用できない場合、これはNULLを結果ます。行識別子SOURCE_OLDに対して常にNULLを返します。
あらすじ¶
bdr.trigger_get_committs(row_id text)
パラメーター¶
row_id-行の識別子。 SOURCE_NEW、SOURCE_OLD、および TARGET、トリガーのタイプとオペレーションに応じて(の文書を参照 個々のトリガータイプ)。
bdr.trigger_get_xid¶
このファンクションは、識別子で指定されたTARGET行のローカルトランザクションIDを返します。行が凍結されて行か利用できないため利用できない場合、これはNULLを結果ます。 SOURCE_OLDおよびSOURCE_NEW行識別子に対しては常にNULLを返します。
これは、競合トリガーでのみ使用可能です。
あらすじ¶
bdr.trigger_get_xid(row_id text)
パラメーター¶
row_id-行の識別子。 SOURCE_NEW、SOURCE_OLD、および TARGET、トリガーのタイプとオペレーションに応じて(の文書を参照 個々のトリガータイプ)。
bdr.trigger_get_type¶
このファンクションは、CONFLICTまたはTRANSFORMのいずれかの現在のトリガータイプを返します。ストリームトリガーの外部で呼び出された場合はnullを返します。
あらすじ¶
bdr.trigger_get_type()
bdr.trigger_get_conflict_type¶
このファンクションは、conflicttrigger内で呼び出された場合は現在の競合タイプを返し、そうでない場合はNULLを返します。
このファンクションの可能な結果値については、[競合タイプ] (conflicts.md#競合タイプのリスト)を参照してください。
あらすじ¶
bdr.trigger_get_conflict_type()
bdr.trigger_get_origin_node_id¶
このファンクションは、引数として渡されたtriggerrow_idのオリジンに対応するノードIDを返します。オリジンが有効でない場合(つまり、行がローカルで生成されていることを意味します)、トリガー行引数に応じて、ソースノードまたはターゲットノードのノードIDを結果ます。行識別子SOURCE_OLDに対して常にNULLを返します。これを使用して、常に信頼されたソースノードを優先する競合トリガーを定義できます。以下の例を参照してください。
あらすじ¶
bdr.trigger_get_origin_node_id(row_id text)
パラメーター¶
row_id-行の識別子。 SOURCE_NEW、SOURCE_OLD、および TARGET、トリガーのタイプとオペレーションに応じて(の文書を参照 個々のトリガータイプ)。
bdr.ri_fkey_on_del_trigger¶
BEFOREトリガーとして呼び出されると、このファンクションは外部キー情報を使用してFKの異常を回避します。
あらすじ¶
bdr.ri_fkey_on_del_trigger()
行の内容¶
SOURCE_NEW、SOURCE_OLD、およびTARGETの内容は、オペレーション、テーブルのREPLICAIDENTITY設定、およびターゲットテーブルの内容に依存します。
TARGET行は、競合トリガーでのみ使用できます。 TARGET行には、targettableでUPDATEまたはDELETEを適用するときに行が見つかった場合にのみデータが含まれます。行が見つからない場合、TARGETはNULLになります。
トリガーノート¶
トリガーの実行オーダー:
トリガーの変換-ターゲットの入力行ごとに1回実行
通常のトリガー-行ごとに1回実行
競合トリガー-競合が存在する行ごとに1回実行
ストリームトリガー操作インターフェイス¶
ストリームトリガーは、bdr-enterprise拡張のパートとして提供されるSQLインターフェイスを使用して管理されます。
ストリームトリガーは、REPLICA IDENTITY FULLableテーブルまたはTOASTable列のないテーブルでのみ作成できます。
bdr.create_conflict_trigger¶
このファンクションは、新しい競合トリガーを作成します。
あらすじ¶
bdr.create_conflict_trigger(trigger_name text,
* events text[],
* relation regclass,
* function regprocedure,
* args text[] DEFAULT '{}')
パラメーター¶
trigger_name-新しいトリガーの名前events-このトリガーを起動するイベントの配列。有効な値は 「INSERT」、「UPDATE」および「DELETE」relation-このトリガーを起動するリレーションfunction-実行ファンクションargs-オプショナル。トリガーファンクションが使用するパラメーターの配列を指定します 実行時に受け取る(TG_ARGV変数の内容)
注¶
このファンクションは、DDLステートメントと同じレプリケーションメカニズムを使用します。これは、レプリケーションがddl
filters構成の影響を受けることを意味します。
このファンクションは、トリガーが作成されているリレーションでグローバルDMLロックを取得します。
このファンクションはトランザクションです-トランザクションのROLLBACKで効果をロールバックでき、変更は現在のトランザクションに可視されます。
通常のPostgreSQLトリガーと同様に、bdr.create_conflict_trigger関数には、ファンクションのrelationおよびEXECUTEprivilegeに対するTRIGGER権限が必要です。これは、30619以上のabdr.backwards_compatibilityに適用されます。
BDRでは、競合トリガーを含むすべてのトリガーに追加のセキュリティルールが適用されます。
security chapter on
triggersを参照してください。
bdr.create_transform_trigger¶
このファンクションは、新しい変換トリガーを作成します。
あらすじ¶
bdr.create_transform_trigger(trigger_name text,
* events text[],
* relation regclass,
* function regprocedure,
* args text[] DEFAULT '{}')
パラメーター¶
trigger_name-新しいトリガーの名前events-このトリガーを起動するイベントの配列。有効な値は 「INSERT」、「UPDATE」および「DELETE」relation-このトリガーを起動するリレーションfunction-実行ファンクションargs-オプショナル、トリガーファンクションが行うパラメーターの配列を指定 実行時に受け取る(TG_ARGV変数の内容)
注¶
このファンクションは、DDLステートメントと同じレプリケーションメカニズムを使用します。これは、レプリケーションがddl
filters構成の影響を受けることを意味します。
このファンクションは、トリガーが作成されているリレーションでグローバルDMLロックを取得します。
このファンクションはトランザクションです-トランザクションのROLLBACKで効果をロールバックでき、変更は現在のトランザクションに可視されます。
通常のPostgreSQLトリガーと同様に、bdr.create_transform_trigger関数には、ファンクションのrelationおよびEXECUTEprivilegeに対するTRIGGER権限が必要です。トランスフォームトリガーを含むすべてのトリガーには、
BDRで追加のセキュリティルールが適用されます。 thesecurity chapter on
triggersを参照してください。
bdr.drop_trigger¶
このファンクションは、既存のストリームトリガー(競合と変換の両方)を削除します。
あらすじ¶
bdr.drop_trigger(trigger_name text,
* relation regclass,
* ifexists boolean DEFAULT false)
パラメーター¶
trigger_name-既存のトリガーの名前relation-どのリレーションが定義されたトリガーかifexists-truetrueに設定すると、このコマンドは欠落を無視します トリガー
注¶
このファンクションは、DDLステートメントと同じレプリケーションメカニズムを使用します。これは、レプリケーションがddl
filters構成の影響を受けることを意味します。
このファンクションは、トリガーが作成されているリレーションでグローバルDMLロックを取得します。
このファンクションはトランザクションです-トランザクションのROLLBACKで効果をロールバックでき、変更は現在のトランザクションに可視されます。
bdr.drop_triggerファンクションは、relationの所有者のみが実行できます。
ストリームトリガーの例¶
update_if_newerconflictリゾルバと同様の動作を提供する競合トリガー:
CREATE OR REPLACE FUNCTION update_if_newer_trig_func
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
* IF (bdr.trigger_get_committs('TARGET') >
* bdr.trigger_get_committs('SOURCE_NEW')) THEN
* RETURN TARGET;
* ELSIF
* RETURN SOURCE;
* END IF;
END;
$$;
カウンタ列にデルタ変更を適用し、他のすべての列にSOURCE_NEWを使用する競合トリガー:
CREATE OR REPLACE FUNCTION delta_count_trg_func
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
DECLARE
* DELTA bigint;
* SOURCE_OLD record;
* SOURCE_NEW record;
* TARGET record;
BEGIN
* SOURCE_OLD := bdr.trigger_get_row('SOURCE_OLD');
* SOURCE_NEW := bdr.trigger_get_row('SOURCE_NEW');
* TARGET := bdr.trigger_get_row('TARGET');
* DELTA := SOURCE_NEW.counter - SOURCE_OLD.counter;
* SOURCE_NEW.counter = TARGET.counter + DELTA;
* RETURN SOURCE_NEW;
END;
$$;
すべての変更を適用する代わりにログテーブルに記録する変換トリガー:
CREATE OR REPLACE FUNCTION log_change
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
DECLARE
* SOURCE_NEW record;
* SOURCE_OLD record;
* COMMITTS timestamptz;
BEGIN
* SOURCE_NEW := bdr.trigger_get_row('SOURCE_NEW');
* SOURCE_OLD := bdr.trigger_get_row('SOURCE_OLD');
* COMMITTS := bdr.trigger_get_committs('SOURCE_NEW');
* IF (TG_OP = 'INSERT') THEN
* INSERT INTO log SELECT 'I', COMMITTS, row_to_json(SOURCE_NEW);
* ELSIF (TG_OP = 'UPDATE') THEN
* INSERT INTO log SELECT 'U', COMMITTS, row_to_json(SOURCE_NEW);
* ELSIF (TG_OP = 'DELETE') THEN
* INSERT INTO log SELECT 'D', COMMITTS, row_to_json(SOURCE_OLD);
* END IF;
* RETURN NULL; -- do not apply the change
END;
$$;
以下の例は、信頼できるサイト、優先ノード、または常にWinsresolutionとも呼ばれる、信頼できるソースの競合検出を実装する競合トリガーを示しています。これは、bdr.trigger_get_origin_node_id()ファンクションを使用して、3つ以上のノードで機能するソリューションを提供します。
CREATE OR REPLACE FUNCTION test_conflict_trigger()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
DECLARE
SOURCE record;
TARGET record;
* TRUSTED_NODE bigint;
SOURCE_NODE bigint;
TARGET_NODE bigint;
BEGIN
TARGET := bdr.trigger_get_row('TARGET');
IF (TG_OP = 'DELETE')
* SOURCE := bdr.trigger_get_row('SOURCE_OLD');
ELSE
* SOURCE := bdr.trigger_get_row('SOURCE_NEW');
END IF;
* TRUSTED_NODE := current_setting('customer.trusted_node_id');
* SOURCE_NODE := bdr.trigger_get_origin_node_id('SOURCE_NEW');
TARGET_NODE := bdr.trigger_get_origin_node_id('TARGET');
* IF (TRUSTED_NODE = SOURCE_NODE) THEN
* RETURN SOURCE;
ELSIF (TRUSTED_NODE = TARGET_NODE) THEN
* RETURN TARGET;
ELSE
* RETURN NULL; -- do not apply the change
END IF;
END;
$$;