SQL文の種類とCRUD対応表

SQL文CRUDCOBOLでの登場頻度注意点
SELECTR非常に多いJOIN・サブクエリで複数テーブル
INSERT INTOC多い基本的にシンプル
UPDATEU多いサブクエリの場合は複数テーブル
DELETE FROMD中程度サブクエリの場合は複数テーブル
MERGE INTOCU少ない(DB2等)条件によってCもUも発生
DECLARE CURSORR多い(カーソル処理)SELECTとして扱う

正規表現によるCRUD分類(シンプル版)

JSqlParserが使えない場合やフォールバック用の軽量実装です。

Python — 正規表現CRUD分類
import re
from dataclasses import dataclass

@dataclass
class CrudEntry:
    table_name: str
    operations: set  # {'C', 'R', 'U', 'D'} の部分集合

# CRUD 操作を判別するパターン
_CRUD_PATTERNS = [
    ('C', re.compile(r'^\s*INSERT\s+INTO\s+([A-Z][A-Z0-9_-]*)', re.IGNORECASE)),
    ('R', re.compile(r'^\s*SELECT\b.*?\bFROM\s+([A-Z][A-Z0-9_-]*)', re.IGNORECASE | re.DOTALL)),
    ('R', re.compile(r'^\s*DECLARE\s+\w+\s+CURSOR\b.*?\bFROM\s+([A-Z][A-Z0-9_-]*)', re.IGNORECASE | re.DOTALL)),
    ('U', re.compile(r'^\s*UPDATE\s+([A-Z][A-Z0-9_-]*)', re.IGNORECASE)),
    ('D', re.compile(r'^\s*DELETE\s+FROM\s+([A-Z][A-Z0-9_-]*)', re.IGNORECASE)),
]

def classify_crud_regex(sql: str) -> list[CrudEntry]:
    """正規表現でSQL文のCRUD種別とテーブル名を判別(フォールバック用)"""
    results = {}
    for op, pattern in _CRUD_PATTERNS:
        m = pattern.match(sql)
        if m:
            table = m.group(1).upper()
            if table not in results:
                results[table] = CrudEntry(table_name=table, operations=set())
            results[table].operations.add(op)
    return list(results.values())

JSqlParserによるCRUD分類(高精度版)

JSqlParserはJavaライブラリですが、Pythonからsubprocessを使って呼び出すか、 JPype を経由して JVM を起動する方法があります。ここでは薄いJavaラッパーを介する方法を採ります。

Java — JSqlParser ラッパー(CrudAnalyzer.java)
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.statement.*;
import net.sf.jsqlparser.statement.select.*;
import net.sf.jsqlparser.statement.insert.Insert;
import net.sf.jsqlparser.statement.update.Update;
import net.sf.jsqlparser.statement.delete.Delete;
import net.sf.jsqlparser.util.TablesNamesFinder;
import java.util.*;

/**
 * 標準入力から SQL を読み込み、テーブル名と操作種別を
 * "TABLE_NAME:OPERATION" 形式で標準出力に書き出すラッパー。
 * Python から subprocess で呼び出して使う。
 */
public class CrudAnalyzer {
    public static void main(String[] args) throws Exception {
        Scanner sc = new Scanner(System.in);
        while (sc.hasNextLine()) {
            String sql = sc.nextLine().trim();
            if (sql.isEmpty()) continue;
            try {
                Statement stmt = CCJSqlParserUtil.parse(sql);
                TablesNamesFinder finder = new TablesNamesFinder();
                List<String> tables = finder.getTableList(stmt);
                String op = detectOperation(stmt);
                for (String t : tables) {
                    System.out.println(t.toUpperCase() + ":" + op);
                }
            } catch (Exception e) {
                System.err.println("PARSE_ERROR: " + sql.substring(0, Math.min(60, sql.length())));
            }
        }
    }

    private static String detectOperation(Statement stmt) {
        if (stmt instanceof Select)  return "R";
        if (stmt instanceof Insert)  return "C";
        if (stmt instanceof Update)  return "U";
        if (stmt instanceof Delete)  return "D";
        return "?";
    }
}
Python — JSqlParser ラッパー呼び出し
import subprocess

def classify_crud_jsqlparser(sql_list: list[str], jar_path: str) -> dict[str, list[CrudEntry]]:
    """
    JSqlParser ラッパーJARを呼び出してSQL群を一括解析する。
    戻り値: {sql文字列: [CrudEntry, ...]} の辞書
    """
    proc = subprocess.run(
        ['java', '-cp', jar_path, 'CrudAnalyzer'],
        input='\n'.join(sql_list),
        capture_output=True,
        text=True,
        encoding='utf-8'
    )

    results = {sql: {} for sql in sql_list}
    lines = proc.stdout.strip().splitlines()
    idx = 0
    for sql in sql_list:
        entries = {}
        # 各SQL の出力行数はテーブル数に依存するため、
        # PARSE_ERROR が出るまで or 次のSQLまで読む(簡易実装)
        while idx < len(lines) and not lines[idx].startswith('PARSE_ERROR'):
            parts = lines[idx].split(':')
            if len(parts) == 2:
                table, op = parts[0].strip(), parts[1].strip()
                if table not in entries:
                    entries[table] = CrudEntry(table_name=table, operations=set())
                entries[table].operations.add(op)
            idx += 1
        results[sql] = list(entries.values())

    return results

MERGE文・UPSERT の扱い

⚠️ MERGE文(DB2・Oracle)

DB2 や Oracle では MERGE INTO 文が使われます。この文は WHEN MATCHED THEN UPDATE(U)と WHEN NOT MATCHED THEN INSERT(C)の両方を含むため、同一テーブルに C と U の両方を記録する必要があります。JSqlParser の Merge クラスで判別できます。

正規表現とJSqlParserの組み合わせ

Python — 組み合わせ分類
def classify_crud(sql: str, jar_path: str | None = None) -> list[CrudEntry]:
    """
    JSqlParser(利用可能な場合)で解析し、失敗時は正規表現でフォールバック。
    """
    if jar_path:
        try:
            result = classify_crud_jsqlparser([sql], jar_path)
            entries = result.get(sql, [])
            if entries:
                return entries
        except Exception:
            pass
    # フォールバック
    return classify_crud_regex(sql)

分類結果の構造

分類結果の例
SQL: SELECT TOKUI-CD, TOKUI-NM FROM TOKUI WHERE TOKUI-CD = ?
  → CrudEntry(table_name='TOKUI', operations={'R'})

SQL: SELECT U.URIAGE-NO, M.SHOHIN-CD FROM URIAGE U JOIN MEISAI M ON ...
  → CrudEntry(table_name='URIAGE', operations={'R'})
  → CrudEntry(table_name='MEISAI', operations={'R'})

SQL: INSERT INTO URIAGE (URIAGE-NO, TOKUI-CD, KIN-GAK) VALUES (?, ?, ?)
  → CrudEntry(table_name='URIAGE', operations={'C'})

次の章では…

PART 05 では PART 03・04 の解析結果をプロジェクト全体で集約し、プログラム × テーブル → CRUD 操作セット という行列データ構造を完成させます。

→ PART 05 — CRUD行列の構築へ