/** * WPML compatibility functions * * @global array $duplicated_posts Array to store the posts being duplicated. * * @package Yoast\WP\Duplicate_Post * @since 3.2 */ add_action( 'admin_init', 'duplicate_post_wpml_init' ); /** * Add handlers for WPML compatibility. */ function duplicate_post_wpml_init() { if ( defined( 'ICL_SITEPRESS_VERSION' ) ) { add_action( 'dp_duplicate_page', 'duplicate_post_wpml_copy_translations', 10, 3 ); add_action( 'dp_duplicate_post', 'duplicate_post_wpml_copy_translations', 10, 3 ); add_action( 'shutdown', 'duplicate_wpml_string_packages', 11 ); } } global $duplicated_posts; // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- Reason: Renaming a global variable is a BC break. $duplicated_posts = []; /** * Copy post translations. * * @global SitePress $sitepress Instance of the Main WPML class. * @global array $duplicated_posts Array of duplicated posts. * * @param int $post_id ID of the copy. * @param WP_Post $post Original post object. * @param string $status Status of the new post. */ function duplicate_post_wpml_copy_translations( $post_id, $post, $status = '' ) { global $sitepress; global $duplicated_posts; remove_action( 'dp_duplicate_page', 'duplicate_post_wpml_copy_translations', 10 ); remove_action( 'dp_duplicate_post', 'duplicate_post_wpml_copy_translations', 10 ); $current_language = $sitepress->get_current_language(); $trid = $sitepress->get_element_trid( $post->ID ); if ( ! empty( $trid ) ) { $translations = $sitepress->get_element_translations( $trid ); $new_trid = $sitepress->get_element_trid( $post_id ); foreach ( $translations as $code => $details ) { if ( $code !== $current_language ) { if ( $details->element_id ) { $translation = get_post( $details->element_id ); if ( ! $translation ) { continue; } $new_post_id = duplicate_post_create_duplicate( $translation, $status ); if ( ! is_wp_error( $new_post_id ) ) { $sitepress->set_element_language_details( $new_post_id, 'post_' . $translation->post_type, $new_trid, $code, $current_language ); } } } } // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- Reason: see above. $duplicated_posts[ $post->ID ] = $post_id; } } /** * Duplicate string packages. * * @global array() $duplicated_posts Array of duplicated posts. */ function duplicate_wpml_string_packages() { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- Reason: renaming the function would be a BC-break. global $duplicated_posts; foreach ( $duplicated_posts as $original_post_id => $duplicate_post_id ) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- Reason: using WPML native filter. $original_string_packages = apply_filters( 'wpml_st_get_post_string_packages', false, $original_post_id ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- Reason: using WPML native filter. $new_string_packages = apply_filters( 'wpml_st_get_post_string_packages', false, $duplicate_post_id ); if ( is_array( $original_string_packages ) ) { foreach ( $original_string_packages as $original_string_package ) { $translated_original_strings = $original_string_package->get_translated_strings( [] ); foreach ( $new_string_packages as $new_string_package ) { $cache = new WPML_WP_Cache( 'WPML_Package' ); $cache->flush_group_cache(); $new_strings = $new_string_package->get_package_strings(); foreach ( $new_strings as $new_string ) { if ( isset( $translated_original_strings[ $new_string->name ] ) ) { foreach ( $translated_original_strings[ $new_string->name ] as $language => $translated_string ) { do_action( // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- Reason: using WPML native filter. 'wpml_add_string_translation', $new_string->id, $language, $translated_string['value'], $translated_string['status'] ); } } } } } } } } **工程6(改訂)の「決定事項」**として固定します(=実装が迷わない最小セット)。 – Raqqa

**工程6(改訂)の「決定事項」**として固定します(=実装が迷わない最小セット)。

1) history に「必ず保存」する項目(必須フィールド)

  • route(例: SQL_ONLY / CYPHER_ONLY / CYPHER_THEN_SQL
  • sql_params(JSON。SQL生成/実行に使った“安全な”パラメータのみ。秘密情報は入れない)
  • decikg_params(JSON。同上)
  • execution_kind(後述)
  • providers(後述。devportaldecikg の mode を必ず持つ)
  • timings_ms(後述。dummy/stubでも必ず計測して入れる)
  • error_summary(失敗時に必須。成功時は null か空)

✅ “接続なし”でも、Stub呼び出しとして上記は全部埋まるのが合格条件。


2) execution_kind(固定 enum)

以下の3値に固定します(将来拡張しても互換が壊れにくい)。

  • STUB:外部接続なし/禁止/未設定 → stub結果を返しつつ履歴は残す
  • REAL:外部接続ありで実実行(devportal/decikg いずれか、または両方)
  • DRY_RUN:外部には投げないが、生成物(SQL/Cypher/params)までは作って履歴に残す

3) providers(固定スキーマ)

必ずこの形で保存します(devportal/decikg 両方必須)。

{
  "devportal": {"mode": "dummy" | "http"},
  "decikg":    {"mode": "dummy" | "http"}
}
  • “接続なしでOK”の工程6では、最低でも mode="dummy" を使えること。
  • mode は 実際にそのリクエストで使った mode を保存(設定値ではなく実績)。

4) timings_ms(固定スキーマ + dummyでも計測)

必ず msの整数で保存し、最低限このキーを持ちます(増やすのはOK)。

{
  "total": 1234,
  "route": 12,
  "build_sql": 210,
  "build_decikg": 0,
  "exec_devportal": 0,
  "exec_decikg": 0,
  "analysis": 350,
  "proposal": 200
}
  • total は必須(全体)
  • 未実行ステージは 0(nullにしない:集計しやすくするため)

5) error_summary(失敗時の固定スキーマ)

失敗時は必ず保存。DBには要約のみ(スタックトレースはログへ)。

{
  "stage": "route|build_sql|exec_devportal|exec_decikg|analysis|proposal",
  "provider": "devportal|decikg|null",
  "type": "TimeoutError|HTTPError|ValidationError|...",
  "message": "sanitized short message",
  "retryable": true
}
  • stage は必須(どこで死んだか)
  • provider は外部絡みなら必須(devportal/decikg)
  • message秘匿情報を含めない(URLのクエリやトークン等は落とす)

6) 「外部結果」についての扱い(工程6の最小決定)

工程6の目的にある「外部結果/失敗追跡」を満たすため、history には以下を最小で残します(接続なしでもOK)。

  • 実行した/実行予定だったものの要約
    • sql_text(生成したSQL。可能なら保存)
    • decikg_query(生成したCypher等。可能なら保存)
    • result_summary(例:行数、カラム、先頭N行のサンプル、ノード/リレーション件数など)
  • 巨大な生データは保存しない(上限を決めてトランケート or 別ストレージ。工程6では“要約”で十分)

これで、工程6の狙い(「後から改善できる追跡」+「接続なしでもStubとして記録」)を、最小変更で満たせます。

新規ファイル

  • 基本:なし
  • 例外(どちらか)
    • 既存DDLに追記できない運用なら:sql/0xx_nlq_history.sql などの migration追加 が必要
    • 既存SQLで管理しているなら:新規不要(既存DDLファイルを修正)

変更予定ファイル(工程6の最小セット)

1) api/app/config.py

  • 工程6で必要な保存のため、すでに追加予定のENV(前回のやつ)を含めて providerのmode判定に使う
    • DEVPORTAL_URL, DEVPORTAL_SQL_PATH
    • DECIGK_URL, DECIGK_MODE=dummy|http
    • NLQ_EXECUTE_STUB_ENABLED
  • (必要なら)history保存のサイズ上限などの定数をここに置く

2) DBスキーマ定義(SQL or Alembic)

  • nlq_sessions_* もしくは history 相当テーブルに以下を追加(工程6の必須)
    • route
    • sql_params / decikg_params(JSON)
    • execution_kind
    • providers(JSON)
    • timings_ms(JSON)
    • error_summary(JSON, nullable)

3) api/app/schemas/*(Pydantic)

  • history保存/返却用のスキーマに、上の必須フィールドを反映
    • Providers / TimingsMs / ErrorSummary を型として切る(既存の書き方に合わせる)

4) api/app/repos/*(history/session upsert箇所)

  • 履歴保存を1箇所に集約して、成功/失敗/Stubのどれでも同じ形で upsert できるようにする
  • 既存で nlq_sessions_* に upsert しているなら、そこに項目追加が最短

5) api/app/services/router.py

  • すでに route/params を返している前提で、工程6の保存要件に合わせて
    • route を必ず返す
    • sql_params / decikg_params を “保存できる形” で必ず返す(空でも {}

6) api/app/services/*executor*.py(SQL/DeciKG実行層)

  • provider mode 実績(dummy/http)を返せるようにする
  • 実行時間を計測して timings_ms.exec_devportal / timings_ms.exec_decikg を埋める
  • 接続なしの場合も stub扱いで timings を 0以外で埋める(開始/終了の差分でOK)

7) api/app/services/orchestrator.py(または相当の集約サービス)

  • ここが工程6のコア:
    • execution_kind 決定(STUB/REAL/DRY_RUN)
    • providers を確定
    • timings_ms.total 含む全体タイムを集約
    • 例外時に error_summary を作って 必ず履歴保存(finallyで呼ぶ)

8) api/app/routers/*(NLQ実行エンドポイント)

  • orchestrator から返ってきた history をレスポンスに含める/もしくは history_id を返す(既存設計に合わせる)
  • 「接続なしでも stub として記録」をここで保証する

9) tests/*

  • 既存テストに追記(新規ファイルなしでいけるなら追記)
    • Stub時でも history に必須フィールドが全て入る
    • 失敗時に error_summary が入る
    • timings_ms が常に入る

直す順番(おすすめ)

  1. DBスキーマ(保存先の形を先に確定)
  2. schemas(Pydantic)(API/Repo/Serviceの型を固定)
  3. repo(upsert)(保存処理を1箇所に集約)
  4. orchestrator(finallyで必ず保存)
  5. executor(providers/timing/error取り回し)
  6. router.py(route/paramsを必ず返す形に)
  7. API router(エンドポイントの返却整合)
  8. tests(Stub/失敗/成功の3パターン)

直す順番(この順が一番事故りにくい)

  1. DBスキーマ(DDL):保存先を先に確定
  2. nlq_history_repo.py:新カラムに書けるようにする
  3. history_service.py:成功/失敗/Stub でも必ず保存できる形にする
  4. nlq_execute.py:execution_kind/providers/timings/error_summary を組み立てる
  5. executors(route_executorsql_executor/cypher_executorhybrid_executor:route/params を欠損させない
  6. routers(nlq_execute.py / history.py:レスポンスに反映
  7. tests(もしあれば):Stubでも必須項目が全部入る、失敗時 error_summary が入る

必要なら次の一手として、nlq_history_repo.pyhistory_service.py の中身(今の insert/update の引数と保存してる列)を貼ってくれれば、「変更が確定」リストを100%にして、どの関数に何を追加するかまで具体化できます。_


Comments

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です