0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Microsoft Access のクエリ SQL を Excel に一括エクスポート&シンタックスハイライト(前回のブラッシュアップ版)

Last updated at Posted at 2025-09-29

Pretty Print

主要節や JOIN/ON での改行規則、括弧に応じたインデント調整、三連以上改行の抑止で読みやすさ最適化。

色分け

文字列 '...' / "..."、日付 #...#、識別子 [...] の除外範囲を先に抽出 → その外側のみを対象に分類ハイライト。

主要句(太字濃色)/補助句/関数(直後に ( 条件)/数値/演算子などをレイヤー順に適用。

運用性

InputBox による開始位置指定、TOC 自動生成、既定シート削除で成果物を見やすく整理。

一時ファイル書込テストでフォルダ権限の問題を前倒し検知。


Option Compare Database
Option Explicit

' ========= ユーザー調整用オプション =========
Private Const OPT_INCLUDE_SYSTEM_QUERIES As Boolean = False  ' MSys/一時(~名)のクエリも含めるか
Private Const OPT_INCLUDE_ACTION_QUERIES As Boolean = False  ' INSERT/UPDATE/DELETE/DDL のクエリも含めるか
Private Const OPT_SHEET_FONT_NAME As String = "Consolas"     ' 出力Excelのフォント名(等幅フォント推奨)
Private Const OPT_SHEET_FONT_SIZE As Long = 10               ' フォントサイズ
Private Const OPT_SQL_COL_WIDTH As Double = 120              ' SQL表示列(B列)の幅
Private Const OPT_ENABLE_SYNTAX_COLOR As Boolean = True      ' シンタックスハイライトを適用するか
Private Const OPT_INDENT_SPACES As Long = 2                  ' SQL整形時のインデント幅(スペース数)
' ============================================

' ?? エントリポイント(このサブを実行)
Public Sub ExportAllQuerySQLs()
    On Error GoTo ErrHandler

    Dim dao_データベース As DAO.Database
    Dim qdf_定義 As DAO.QueryDef
    Dim app_Excel As Object, wbk_ブック As Object, wst_TOC As Object, wst_作業 As Object
    Dim str_保存先フォルダ As String, str_保存パス As String
    Dim col_対象クエリ名 As Collection
    Dim lng_インデックス As Long, lng_追加数 As Long
    Dim col_エラーログ As Collection
    Dim lng_開始位置 As Long, str_入力値 As String
    Dim lng_エラー番号 As Long, str_エラー説明 As String

    Set dao_データベース = CurrentDb
    Set col_対象クエリ名 = New Collection
    Set col_エラーログ = New Collection

    '  保存先フォルダ(Downloads)取得
    str_保存先フォルダ = GetDownloadsPath()
    If Len(str_保存先フォルダ) = 0 Then
        MsgBox "Downloads フォルダを特定できませんでした。USERPROFILE 環境変数をご確認ください。", vbExclamation
        Exit Sub
    End If
    str_保存パス = str_保存先フォルダ & "\QuerySQLExport_" & Format(Now, "yyyymmdd_hhnnss") & ".xlsx"
    ' 出力フォルダの存在と書き込み権限をチェック
    If Dir(str_保存先フォルダ, vbDirectory) = "" Then
        MsgBox "指定された保存先フォルダが存在しません: " & str_保存先フォルダ, vbCritical
        GoTo Cleanup
    End If
    Dim str_テストファイル As String
    str_テストファイル = str_保存先フォルダ & "\__AccessExportTest.tmp"
    On Error Resume Next
    Open str_テストファイル For Output As #1
    If Err.Number <> 0 Then
        On Error GoTo 0
        MsgBox "保存先フォルダに書き込むことができません。" & vbCrLf & "フォルダのアクセス権限をご確認ください: " & str_保存先フォルダ, vbCritical
        GoTo Cleanup
    End If
    Close #1
    Kill str_テストファイル
    On Error GoTo 0

    '  対象クエリ抽出(条件に合致するクエリをコレクションに追加)
    For Each qdf_定義 In dao_データベース.QueryDefs
        If IsTargetQuery(qdf_定義) Then
            col_対象クエリ名.Add qdf_定義.Name
        End If
    Next

    If col_対象クエリ名.Count = 0 Then
        MsgBox "出力対象のクエリ(SQLを持つもの)が見つかりませんでした。", vbInformation
        Exit Sub
    End If

    ' 開始位置の指定(オプション)
    lng_開始位置 = 1
    str_入力値 = InputBox("エクスポートを開始するクエリ番号 (1~" & col_対象クエリ名.Count & " ) を入力してください:", "開始位置の指定", "1")
    If str_入力値 = "" Then
        Exit Sub    ' ユーザーがキャンセル
    End If
    If Not IsNumeric(str_入力値) Then
        MsgBox "開始位置には数値を入力してください。", vbExclamation
        Exit Sub
    End If
    ' 小数や区切り文字が入力された場合は無効
    If InStr(str_入力値, ".") > 0 Or InStr(str_入力値, ",") > 0 Then
        MsgBox "開始位置には整数を入力してください。", vbExclamation
        Exit Sub
    End If
    On Error Resume Next
    lng_開始位置 = CLng(str_入力値)
    If Err.Number <> 0 Then
        Err.Clear: On Error GoTo 0
        MsgBox "開始位置の指定が不正です。", vbExclamation
        Exit Sub
    End If
    On Error GoTo 0
    If lng_開始位置 < 1 Or lng_開始位置 > col_対象クエリ名.Count Then
        MsgBox "1~" & col_対象クエリ名.Count & " の範囲で開始位置を指定してください。", vbExclamation
        Exit Sub
    End If
    ' 進捗バーを開始位置に合わせて設定
    SysCmd acSysCmdInitMeter, "クエリのSQLをExcelへ出力中…", (col_対象クエリ名.Count - lng_開始位置 + 1)

    '  Excel起動(遅延バインディング)
    On Error Resume Next
    Set app_Excel = CreateObject("Excel.Application")
    On Error GoTo ErrHandler
    If app_Excel Is Nothing Then
        MsgBox "Excel を起動できませんでした。インストール状況をご確認ください。", vbCritical
        GoTo Cleanup
    End If
    app_Excel.Visible = False
    Set wbk_ブック = app_Excel.Workbooks.Add

    '  TOC(目次)シート作成
    Set wst_TOC = wbk_ブック.Sheets(1)
    wst_TOC.Name = "TOC"
    InitSheetStyle wst_TOC
    WriteTOCHeader wst_TOC

    lng_追加数 = 0

    '  各クエリの処理ループ
    For lng_インデックス = lng_開始位置 To col_対象クエリ名.Count
        Dim str_クエリ名 As String, str_SQL本文 As String, str_SQL整形 As String
        Dim str_パラメータ一覧 As String, str_シート名 As String

        str_クエリ名 = CStr(col_対象クエリ名(lng_インデックス))
        On Error Resume Next
        str_SQL本文 = dao_データベース.QueryDefs(str_クエリ名).SQL
        lng_エラー番号 = Err.Number: str_エラー説明 = Err.Description
        On Error GoTo 0
        If lng_エラー番号 <> 0 Then
            LogError col_エラーログ, str_クエリ名, "SQL取得時にエラー発生: (" & lng_エラー番号 & ") " & str_エラー説明
            Err.Clear
            On Error GoTo ErrHandler
            GoTo NextLoop
        End If
        If Len(Trim$(str_SQL本文)) = 0 Then
            LogError col_エラーログ, str_クエリ名, "SQL が空、または取得できませんでした。"
            On Error GoTo ErrHandler
            GoTo NextLoop
        End If
        On Error GoTo ErrHandler

        '  整形(Pretty Print)
        str_SQL整形 = FormatSQLPretty(str_SQL本文)
        str_パラメータ一覧 = GetQueryParametersInfo(dao_データベース.QueryDefs(str_クエリ名))

        ' ? シート生成と初期化
        str_シート名 = MakeSafeUniqueSheetName(wbk_ブック, str_クエリ名)
        On Error Resume Next
        Set wst_作業 = wbk_ブック.Sheets.Add(After:=wbk_ブック.Sheets(wbk_ブック.Sheets.Count))
        lng_エラー番号 = Err.Number: str_エラー説明 = Err.Description
        On Error GoTo 0
        If lng_エラー番号 <> 0 Then
            LogError col_エラーログ, str_クエリ名, "シート追加時にエラー発生: (" & lng_エラー番号 & ") " & str_エラー説明
            Err.Clear
            On Error GoTo ErrHandler
            GoTo NextLoop
        End If
        On Error Resume Next
        wst_作業.Name = str_シート名
        lng_エラー番号 = Err.Number: str_エラー説明 = Err.Description
        On Error GoTo 0
        If lng_エラー番号 <> 0 Then
            LogError col_エラーログ, str_クエリ名, "シート名設定時にエラー発生: (" & lng_エラー番号 & ") " & str_エラー説明
            Err.Clear
            app_Excel.DisplayAlerts = False
            wst_作業.Delete
            app_Excel.DisplayAlerts = True
            On Error GoTo ErrHandler
            GoTo NextLoop
        End If
        On Error GoTo ErrHandler
        InitSheetStyle wst_作業

        '  書き込み(メタ情報 + 整形SQL)
        With wst_作業
            .Cells(1, 1).Value = "Query Name"
            .Cells(1, 2).Value = str_クエリ名
            .Cells(2, 1).Value = "Created"
            .Cells(2, 2).Value = Now
            .Cells(3, 1).Value = "Parameters"
            .Cells(3, 2).Value = IIf(str_パラメータ一覧 = "", "(none)", str_パラメータ一覧)

            .Cells(5, 1).Value = "SQL (formatted)"
            .Cells(6, 2).Value = str_SQL整形   ' フォーマット済みSQL本文はB6セルから表示
            .Columns("A:A").ColumnWidth = 25
            .Columns("B:B").ColumnWidth = OPT_SQL_COL_WIDTH
            .Columns("B:B").WrapText = True
            .Columns("B:B").VerticalAlignment = -4160   ' 上詰め (xlVAlignTop)
        End With

        '  エディタ風シンタックスハイライト(色分け)
        If OPT_ENABLE_SYNTAX_COLOR Then
            On Error Resume Next    ' 部分書式設定失敗を吸収
            ColorizeSqlEditorStyle wst_作業.Cells(6, 2)
            On Error GoTo ErrHandler
        End If

        '  TOCシートに追記(ハイパーリンクで目次追加)
        On Error Resume Next
        AppendTOC wst_TOC, str_クエリ名, wst_作業.Name
        If Err.Number <> 0 Then
            LogError col_エラーログ, str_クエリ名, "TOC追記時にエラー発生: (" & Err.Number & ") " & Err.Description
            Err.Clear
        End If
        On Error GoTo ErrHandler

        lng_追加数 = lng_追加数 + 1
        SysCmd acSysCmdUpdateMeter, lng_追加数
NextLoop:
    Next lng_インデックス

    ' 空シート削除(デフォルトの "Sheet*" シートを削除)
    On Error Resume Next
    Dim wst_削除候補 As Object
    app_Excel.DisplayAlerts = False
    For Each wst_削除候補 In wbk_ブック.Sheets
        If wst_削除候補.Name Like "Sheet*" Then wst_削除候補.Delete
    Next wst_削除候補
    app_Excel.DisplayAlerts = True
    On Error GoTo ErrHandler

    '  保存(正常完了時)
    wbk_ブック.SaveAs str_保存パス
    wbk_ブック.Close SaveChanges:=False
    app_Excel.Quit

    SysCmd acSysCmdClearStatus
    If col_エラーログ.Count > 0 Then
        MsgBox "一部のクエリでエラーが発生しました。" & vbCrLf & _
               "保存先: " & str_保存パス & vbCrLf & vbCrLf & JoinErrors(col_エラーログ), vbExclamation
    Else
        MsgBox "クエリのSQLを整形し、Excelに出力しました!" & vbCrLf & "保存先: " & str_保存パス, vbInformation
    End If
    Exit Sub

ErrHandler:
    SysCmd acSysCmdClearStatus
    On Error Resume Next
    If Not wbk_ブック Is Nothing Then
        If lng_追加数 > 0 Then wbk_ブック.SaveAs str_保存パス
        wbk_ブック.Close False
    End If
    If Not app_Excel Is Nothing Then app_Excel.Quit
    If lng_追加数 > 0 Then
        MsgBox "処理中にエラーが発生しました: " & Err.Number & " - " & Err.Description & vbCrLf & _
               "(途中までの結果を保存しました: " & str_保存パス & ")", vbCritical
    Else
        MsgBox "処理中にエラーが発生しました: " & Err.Number & " - " & Err.Description, vbCritical
    End If
Cleanup:
End Sub

' ====== ユーティリティ関数/判定処理 ======

' Downloadsフォルダのパス取得
Private Function GetDownloadsPath() As String
    Dim str_ユーザルート As String
    str_ユーザルート = Environ$("USERPROFILE")
    If Len(str_ユーザルート) = 0 Then
        GetDownloadsPath = ""
    Else
        GetDownloadsPath = str_ユーザルート & "\Downloads"
    End If
End Function

' 対象クエリ判定(SQLを持つ選択クエリか?)
Private Function IsTargetQuery(qdf_クエリ定義 As DAO.QueryDef) As Boolean
    On Error GoTo SafeNo   ' .SQLアクセスでエラーとなるものは対象外
    Dim str_名前 As String, str_sql小文字 As String
    str_名前 = Nz(qdf_クエリ定義.Name, "")
    ' システムテーブル系・一時クエリ除外
    If Not OPT_INCLUDE_SYSTEM_QUERIES Then
        If Left$(str_名前, 1) = "~" Or Left$(str_名前, 4) = "MSys" Then GoTo SafeNo
    End If
    ' SQL文が空のQueryDefは対象外
    str_sql小文字 = LCase$(Trim$(Nz(qdf_クエリ定義.SQL, "")))
    If Len(str_sql小文字) = 0 Then GoTo SafeNo
    ' アクション系(追加/更新/削除/DDLクエリ)は対象外(オプションによる)
    If Not OPT_INCLUDE_ACTION_QUERIES Then
        If InStr(str_sql小文字, " insert ") > 0 Or Left$(str_sql小文字, 6) = "insert" Then GoTo SafeNo
        If InStr(str_sql小文字, " update ") > 0 Or Left$(str_sql小文字, 6) = "update" Then GoTo SafeNo
        If InStr(str_sql小文字, " delete ") > 0 Or Left$(str_sql小文字, 6) = "delete" Then GoTo SafeNo
        If InStr(str_sql小文字, " create ") > 0 Or InStr(str_sql小文字, " alter ") > 0 Or InStr(str_sql小文字, " drop ") > 0 Then GoTo SafeNo
    End If
    ' SELECT/UNION/TRANSFORM/PARAMETERSクエリを対象とする
    If Left$(str_sql小文字, 6) = "select" _
       Or InStr(str_sql小文字, " union ") > 0 _
       Or Left$(str_sql小文字, 9) = "transform" _
       Or Left$(str_sql小文字, 10) = "parameters" Then
        IsTargetQuery = True
        Exit Function
    End If
SafeNo:
    IsTargetQuery = False
End Function

' シート名の禁止文字・長さ・重複に対応した安全な名前を生成
Private Function MakeSafeUniqueSheetName(obj_ブック As Object, str_基底名 As String) As String
    Dim str_作業名 As String, var_禁止文字 As Variant, lng_I As Long, str_候補 As String, lng_連番 As Long
    var_禁止文字 = Array("/", "\", "*", "[", "]", ":", "?", Chr$(34))  ' シート名に使えない文字
    str_作業名 = str_基底名
    For lng_I = LBound(var_禁止文字) To UBound(var_禁止文字)
        str_作業名 = Replace$(str_作業名, var_禁止文字(lng_I), "_")
    Next
    str_作業名 = Trim$(str_作業名)
    If Len(str_作業名) = 0 Then str_作業名 = "Query"
    If Len(str_作業名) > 31 Then str_作業名 = Left$(str_作業名, 31)   ' 長過ぎる場合はカット

    str_候補 = str_作業名: lng_連番 = 1
    Do While SheetExists(obj_ブック, str_候補)
        lng_連番 = lng_連番 + 1
        ' 重複があれば末尾に連番を付加(31文字制限内に収める)
        str_候補 = Left$(str_作業名, 31 - Len(CStr(lng_連番))) & CStr(lng_連番)
        ' 連番が3桁を超える場合の調整( "_001" の形式で付加)
        If lng_連番 > 999 Then str_候補 = Left$(str_作業名, 27) & "_" & Format(lng_連番, "000")
    Loop
    MakeSafeUniqueSheetName = str_候補
End Function

Private Function SheetExists(obj_ブック As Object, str_シート名 As String) As Boolean
    On Error Resume Next
    SheetExists = Not obj_ブック.Sheets(str_シート名) Is Nothing
    On Error GoTo 0
End Function

' クエリのパラメータ一覧文字列を取得(無い場合は空文字)
Private Function GetQueryParametersInfo(qdf_クエリ定義 As DAO.QueryDef) As String
    On Error GoTo NoParams
    Dim prm_パラメータ As DAO.Parameter, str_バッファ As String
    For Each prm_パラメータ In qdf_クエリ定義.Parameters
        str_バッファ = str_バッファ & prm_パラメータ.Name & " (Type=" & prm_パラメータ.Type & "); "
    Next
    If Len(str_バッファ) > 0 Then str_バッファ = Left$(str_バッファ, Len(str_バッファ) - 2)
    GetQueryParametersInfo = str_バッファ
    Exit Function
NoParams:
    GetQueryParametersInfo = ""
End Function

' 新規シートの初期スタイル設定
Private Sub InitSheetStyle(wst_対象 As Object)
    With wst_対象.Cells
        .Font.Name = OPT_SHEET_FONT_NAME
        .Font.Size = OPT_SHEET_FONT_SIZE
    End With
    wst_対象.Columns("A:B").HorizontalAlignment = -4131  ' xlLeft
    wst_対象.Columns("A:B").VerticalAlignment = -4160    ' xlTop
    wst_対象.Columns("A:A").ColumnWidth = 25
    wst_対象.Columns("B:B").ColumnWidth = OPT_SQL_COL_WIDTH
End Sub

' TOCシートのヘッダ行を書き込み
Private Sub WriteTOCHeader(wst_TOC対象 As Object)
    With wst_TOC対象
        InitSheetStyle wst_TOC対象
        .Cells(1, 1).Value = "Index"
        .Cells(1, 2).Value = "Query Name"
        .Cells(1, 3).Value = "Sheet"
        .Cells(1, 1).EntireRow.Font.Bold = True
        .Columns("A:C").ColumnWidth = 40
    End With
End Sub

' TOCシートへクエリ目次を追記(ハイパーリンクで該当シートA1へ)
Private Sub AppendTOC(wst_TOC対象 As Object, str_クエリ名 As String, str_シート名 As String)
    Dim lng_ As Long
    lng_ = wst_TOC対象.Cells(wst_TOC対象.Rows.Count, 1).End(-4162).Row + 1   ' xlUpで末尾を取得し次行へ
    If lng_ < 2 Then lng_ = 2
    wst_TOC対象.Cells(lng_, 2).Value = str_クエリ名
    wst_TOC対象.Cells(lng_, 3).Value = str_シート名
    wst_TOC対象.Hyperlinks.Add Anchor:=wst_TOC対象.Cells(lng_, 1), Address:="", _
                       SubAddress:="'" & str_シート名 & "'!A1", TextToDisplay:="→ " & str_クエリ名
End Sub

' エラーログにメッセージを追加
Private Sub LogError(ByRef col_ログ As Collection, ByVal str_クエリ名 As String, ByVal str_メッセージ As String)
    col_ログ.Add "[" & str_クエリ名 & "] " & str_メッセージ
End Sub

Private Function JoinErrors(col_ログ As Collection) As String
    Dim lng_I As Long, arr_文字列() As String
    If col_ログ.Count = 0 Then
        JoinErrors = ""
    Else
        ReDim arr_文字列(1 To col_ログ.Count)
        For lng_I = 1 To col_ログ.Count
            arr_文字列(lng_I) = CStr(col_ログ(lng_I))
        Next
        JoinErrors = Join(arr_文字列, vbCrLf)
    End If
End Function


Private Function FormatSQLPretty(ByVal str_S As String) As String
    Dim lng_I As Long
    Dim str_Ch As String * 1
    Dim str_Out As String, str_Tok As String, str_TokUp As String
    Dim bln_InSQ As Boolean, bln_InDQ As Boolean

    ' 句の状態
    Dim bln_InSelect As Boolean
    Dim bln_InGroup As Boolean
    Dim bln_InOrder As Boolean
    Dim bln_InTransform As Boolean
    Dim bln_InPivot As Boolean

    ' トップレベルのカンマ判定用:各句内の括弧深さ
    Dim lng_ParenDepthSelect As Long
    Dim lng_ParenDepthGroup As Long
    Dim lng_ParenDepthOrder As Long
    Dim lng_ParenDepthTransform As Long

    ' 2語句判定(GROUP BY / ORDER BY)
    Dim str_PendingWord As String
    Dim str_LastClause As String

    ' 見出し句でのみ使うインデント(JOINツリーを整える)
    Dim lng_Indent As Long

    ' 正規化
    str_S = Trim$(str_S)
    str_S = Replace$(Replace$(Replace$(str_S, vbCrLf, " "), vbCr, " "), vbLf, " ")
    Do While InStr(str_S, "  ") > 0: str_S = Replace$(str_S, "  ", " "): Loop

    For lng_I = 1 To Len(str_S)
        str_Ch = Mid$(str_S, lng_I, 1)

        ' クォート状態
        If str_Ch = "'" And Not bln_InDQ Then bln_InSQ = Not bln_InSQ
        If str_Ch = """" And Not bln_InSQ Then bln_InDQ = Not bln_InDQ

        ' 句ごとの括弧深さ(トップレベルカンマ制御用)
        If Not bln_InSQ And Not bln_InDQ Then
            If bln_InSelect Then
                If str_Ch = "(" Then lng_ParenDepthSelect = lng_ParenDepthSelect + 1
                If str_Ch = ")" And lng_ParenDepthSelect > 0 Then lng_ParenDepthSelect = lng_ParenDepthSelect - 1
            End If
            If bln_InGroup Then
                If str_Ch = "(" Then lng_ParenDepthGroup = lng_ParenDepthGroup + 1
                If str_Ch = ")" And lng_ParenDepthGroup > 0 Then lng_ParenDepthGroup = lng_ParenDepthGroup - 1
            End If
            If bln_InOrder Then
                If str_Ch = "(" Then lng_ParenDepthOrder = lng_ParenDepthOrder + 1
                If str_Ch = ")" And lng_ParenDepthOrder > 0 Then lng_ParenDepthOrder = lng_ParenDepthOrder - 1
            End If
            If bln_InTransform Then
                If str_Ch = "(" Then lng_ParenDepthTransform = lng_ParenDepthTransform + 1
                If str_Ch = ")" And lng_ParenDepthTransform > 0 Then lng_ParenDepthTransform = lng_ParenDepthTransform - 1
            End If

            ' 括弧でインデントは基本増やさない(SELECT~FROM間の( )に過反応しない設計)
            If str_Ch = ")" And Not (bln_InSelect Or bln_InGroup Or bln_InOrder Or bln_InTransform) Then
                If lng_Indent > 0 Then
                    lng_Indent = lng_Indent - 1
                    str_Out = RTrim$(str_Out) & vbCrLf & String$(lng_Indent * OPT_INDENT_SPACES, " ")
                End If
            End If
        End If

        str_Tok = str_Tok & str_Ch

        ' 区切り
        If (str_Ch = " " Or str_Ch = "," Or str_Ch = "(" Or str_Ch = ")" Or lng_I = Len(str_S)) Then
            str_TokUp = UCase$(Trim$(str_Tok))

            If Not bln_InSQ And Not bln_InDQ Then
                ' ===== 2語句:GROUP BY / ORDER BY =====
                If (str_PendingWord = "GROUP" And str_TokUp = "BY") Or (str_PendingWord = "ORDER" And str_TokUp = "BY") Then
                    str_Out = RTrim$(str_Out)
                    If Len(str_Out) > 0 Then str_Out = str_Out & vbCrLf
                    str_Out = str_Out & String$(lng_Indent * OPT_INDENT_SPACES, " ") & str_PendingWord & " BY"
                    str_LastClause = str_PendingWord & " BY"
                    bln_InGroup = (str_PendingWord = "GROUP")
                    bln_InOrder = (str_PendingWord = "ORDER")
                    bln_InSelect = False: bln_InTransform = False: bln_InPivot = False
                    str_PendingWord = ""
                ' ===== JOIN(INNER/LEFT/RIGHT の直後)=====
                ElseIf str_TokUp = "JOIN" Then
                    str_Out = RTrim$(str_Out)
                    If Len(str_Out) > 0 Then str_Out = str_Out & vbCrLf
                    str_Out = str_Out & String$(lng_Indent * OPT_INDENT_SPACES, " ") & "JOIN"
                    str_LastClause = "JOIN"
                ' ===== 句の開始(1語) =====
                ElseIf str_TokUp = "TRANSFORM" Or str_TokUp = "SELECT" Or str_TokUp = "FROM" Or _
                       str_TokUp = "WHERE" Or str_TokUp = "HAVING" Or _
                       str_TokUp = "UNION" Or str_TokUp = "UNION ALL" Or _
                       str_TokUp = "PIVOT" Or _
                       str_TokUp = "PARAMETERS" Or str_TokUp = "TRANSFORM" Or str_TokUp = "PIVOT" Then
                    str_Out = RTrim$(str_Out)
                    If Len(str_Out) > 0 Then str_Out = str_Out & vbCrLf
                    str_Out = str_Out & String$(lng_Indent * OPT_INDENT_SPACES, " ") & str_TokUp
                    str_LastClause = str_TokUp

                    ' 句フラグ
                    bln_InSelect = (str_TokUp = "SELECT")
                    bln_InTransform = (str_TokUp = "TRANSFORM")
                    bln_InPivot = (str_TokUp = "PIVOT")
                    If str_TokUp = "FROM" Or str_TokUp = "WHERE" Or str_TokUp = "HAVING" Or _
                       str_TokUp = "UNION" Or str_TokUp = "UNION ALL" Then
                        bln_InSelect = False: bln_InTransform = False: bln_InGroup = False: bln_InOrder = False: bln_InPivot = False
                    End If

                    ' 括弧深さリセット(句をまたぐ)
                    If bln_InSelect Then lng_ParenDepthSelect = 0
                    If bln_InTransform Then lng_ParenDepthTransform = 0
                    If bln_InGroup Then lng_ParenDepthGroup = 0
                    If bln_InOrder Then lng_ParenDepthOrder = 0

                ' ===== AND/OR は WHERE/HAVING で行頭 =====
                ElseIf (str_TokUp = "AND" Or str_TokUp = "OR") And (str_LastClause = "WHERE" Or str_LastClause = "HAVING") Then
                    str_Out = RTrim$(str_Out) & vbCrLf & String$(lng_Indent * OPT_INDENT_SPACES, " ") & str_TokUp

                ' ===== ON は JOIN条件の行頭 =====
                ElseIf str_TokUp = "ON" Then
                    str_Out = RTrim$(str_Out) & vbCrLf & String$(lng_Indent * OPT_INDENT_SPACES, " ") & "ON"

                ' ===== GROUP / ORDER は BY待ち =====
                ElseIf str_TokUp = "GROUP" Or str_TokUp = "ORDER" Then
                    str_PendingWord = str_TokUp
                    bln_InGroup = (str_TokUp = "GROUP") ' 仮置き(BY確定で正式ON)
                    bln_InOrder = (str_TokUp = "ORDER")

                ' ===== JOIN の前置詞(INNER/LEFT/RIGHT)はそのまま出力 =====
                ElseIf str_TokUp = "INNER" Or str_TokUp = "LEFT" Or str_TokUp = "RIGHT" Then
                    str_Out = AppendWithSpace(str_Out, str_TokUp)

                Else
                    ' 通常トークンはそのまま
                    str_Out = AppendWithSpace(str_Out, Trim$(str_Tok))
                End If
            Else
                ' クォート中は生出力
                str_Out = AppendWithSpace(str_Out, Trim$(str_Tok))
            End If

            ' 記号処理
            If str_Ch = "," Then
                Dim bln_Break As Boolean: bln_Break = True
                ' SELECT / GROUP BY / ORDER BY / TRANSFORM のトップレベルのみ改行
                If bln_InSelect And lng_ParenDepthSelect > 0 Then bln_Break = False
                If bln_InGroup And lng_ParenDepthGroup > 0 Then bln_Break = False
                If bln_InOrder And lng_ParenDepthOrder > 0 Then bln_Break = False
                If bln_InTransform And lng_ParenDepthTransform > 0 Then bln_Break = False

                If bln_Break And (bln_InSelect Or bln_InGroup Or bln_InOrder Or bln_InTransform) Then
                    str_Out = RTrim$(str_Out) & vbCrLf & String$(lng_Indent * OPT_INDENT_SPACES, " ")
                Else
                    str_Out = str_Out & " "
                End If

            ElseIf str_Ch = "(" Then
                ' 句中の( は改行を誘発しない(関数引数の集中を維持)
                str_Out = str_Out & " "

            ElseIf str_Ch = ")" Then
                str_Out = str_Out & " "

            Else
                str_Out = str_Out & " "
            End If

            ' 次へ
            str_Tok = ""
        End If
    Next lng_I

    ' 残りの保留(念のため)
    If Len(str_PendingWord) > 0 Then
        str_Out = RTrim$(str_Out) & vbCrLf & str_PendingWord
    End If

    ' 連続改行の圧縮
    str_Out = Trim$(str_Out)
    Do While InStr(str_Out, vbCrLf & vbCrLf & vbCrLf) > 0
        str_Out = Replace$(str_Out, vbCrLf & vbCrLf & vbCrLf, vbCrLf & vbCrLf)
    Loop

    FormatSQLPretty = str_Out
End Function


' 単語を末尾に追加(必要なときだけ空白を入れる)
Private Function AppendWithSpace(ByVal str_Base As String, ByVal str_Add As String) As String
    If Len(str_Add) = 0 Then
        AppendWithSpace = str_Base
    ElseIf Len(str_Base) = 0 Then
        AppendWithSpace = str_Add
    ElseIf Right$(str_Base, 1) = " " Or Right$(str_Base, 2) = vbCrLf Then
        AppendWithSpace = str_Base & str_Add
    Else
        AppendWithSpace = str_Base & " " & str_Add
    End If
End Function


' ====== エディタ風シンタックスハイライト(Excel上でSQLを色分け) ======

' 指定セル内のSQL文字列を色分け(主要句・補助句・関数・文字列リテラル・日付リテラル・識別子・数値・演算子)
Public Sub ColorizeSqlEditorStyle(ByVal obj_セル As Object)
    On Error GoTo SafeExit   ' 部分的な書式設定で稀に発生する例外に対処
    Dim str_本文 As String
    str_本文 = CStr(obj_セル.Value)
    If Len(str_本文) = 0 Then Exit Sub

    Dim col_除外範囲 As Collection
    Set col_除外範囲 = BuildExcludedRanges(str_本文)  ' クォート/日付/#囲み を色分け対象から除外

    ' 1) 主要句(濃い青+太字)
    Dim arr_主要句 As Variant
    arr_主要句 = Array("PARAMETERS", "TRANSFORM", "PIVOT", _
                       "SELECT", "FROM", "WHERE", "GROUP BY", "HAVING", "ORDER BY", _
                       "INNER JOIN", "LEFT JOIN", "RIGHT JOIN", "JOIN", "ON", _
                       "UNION", "UNION ALL", "WITH OWNERACCESS OPTION")
    HighlightList obj_セル, str_本文, arr_主要句, col_除外範囲, RGB(0, 0, 160), True, False, False

    ' 2) 補助キーワード(青)
    Dim arr_補助 As Variant
    arr_補助 = Array("DISTINCT", "DISTINCTROW", "TOP", "AS", _
                     "BETWEEN", "IN", "LIKE", "NOT", "IS NULL", "IS NOT NULL", _
                     "TRUE", "FALSE", "ALL")
    HighlightList obj_セル, str_本文, arr_補助, col_除外範囲, RGB(0, 0, 192), False, False, False

    ' 3) 関数名(紫:直後に"("があるもの)
    Dim arr_関数 As Variant
    arr_関数 = Array("SUM", "COUNT", "MIN", "MAX", "AVG", "ROUND", _
                     "NZ", "IIF", "SWITCH", "CHOOSE", _
                     "DATE", "TIME", "NOW", "DATEADD", "DATEDIFF", "DATESERIAL", "TIMESERIAL", _
                     "FORMAT", "LEN", "MID", "LEFT", "RIGHT", "VAL", "STR", "CSTR", "CLNG", "CDATE", "CBOOL", "ISNULL", "ABS")
    HighlightList obj_セル, str_本文, arr_関数, col_除外範囲, RGB(128, 0, 128), False, False, True

    ' 4) 文字列リテラル("..." および '...')→ 緑
    ColorQuotedLiterals obj_セル, str_本文, """", RGB(0, 128, 0)    ' ダブルクォート囲み
    ColorQuotedLiterals obj_セル, str_本文, "'", RGB(0, 128, 0)     ' シングルクォート囲み

    ' 5) 日付リテラル(#...#)→ ティール(深緑青色)
    ColorHashDateLiterals obj_セル, str_本文, RGB(0, 128, 128)

    ' 6) 角括弧囲み識別子 [Field] / [Table] → 茶色
    ColorBracketIdentifiers obj_セル, str_本文, RGB(128, 64, 0)

    ' 7) 数値リテラル(整数/小数/指数表記)→ 橙色
    ColorNumbers obj_セル, str_本文, col_除外範囲, RGB(255, 128, 0)

    ' 8) 演算子(=, <>, <=, >=, +, -, *, /, & など)→ グレー
    ColorOperators obj_セル, str_本文, col_除外範囲, RGB(96, 96, 96)
SafeExit:
End Sub

' --- 色分け処理の下請け ---

Private Sub HighlightList(ByVal obj_セル As Object, ByVal str_本文 As String, _
                          ByVal arr_対象 As Variant, ByVal col_除外範囲 As Collection, _
                          ByVal lng_ As Long, ByVal bln_太字 As Boolean, ByVal bln_イタリック As Boolean, _
                          ByVal bln_直後に括弧要件 As Boolean)
    Dim lng_I As Long
    For lng_I = LBound(arr_対象) To UBound(arr_対象)
        HighlightOne obj_セル, str_本文, CStr(arr_対象(lng_I)), col_除外範囲, lng_, bln_太字, bln_イタリック, bln_直後に括弧要件
    Next
End Sub

Private Sub HighlightOne(ByVal obj_セル As Object, ByVal str_本文 As String, ByVal str_キーワード As String, _
                         ByVal col_除外範囲 As Collection, ByVal lng_ As Long, _
                         ByVal bln_太字 As Boolean, ByVal bln_イタリック As Boolean, _
                         ByVal bln_直後に括弧要件 As Boolean)
    Dim lng_位置 As Long, lng_長さ As Long, lng_開始 As Long
    lng_長さ = Len(str_キーワード)
    lng_開始 = 1
    Do
        lng_位置 = InStr(lng_開始, str_本文, str_キーワード, vbTextCompare)
        If lng_位置 = 0 Then Exit Do

        If IsWholeWord(str_本文, lng_位置, lng_長さ) Then
            If Not IsInsideExcluded(lng_位置, lng_長さ, col_除外範囲) Then
                If Not bln_直後に括弧要件 Or HasParenAfter(str_本文, lng_位置 + lng_長さ) Then
                    With obj_セル.Characters(Start:=lng_位置, length:=lng_長さ).Font
                        .Color = lng_
                        .Bold = bln_太字
                        .Italic = bln_イタリック
                    End With
                End If
            End If
        End If
        lng_開始 = lng_位置 + lng_長さ
    Loop
End Sub

Private Function HasParenAfter(ByVal str_本文 As String, ByVal lng_開始 As Long) As Boolean
    Dim lng_j As Long, str_c As String
    For lng_j = lng_開始 To Len(str_本文)
        str_c = Mid$(str_本文, lng_j, 1)
        If str_c <> " " And str_c <> vbTab Then
            HasParenAfter = (str_c = "(")
            Exit Function
        End If
    Next
End Function

' --- 文字列リテラルの色付け ---
Private Sub ColorQuotedLiterals(ByVal obj_セル As Object, ByVal str_本文 As String, _
                                ByVal str_クォート As String, ByVal lng_ As Long)
    Dim lng_j As Long, lng_開始 As Long
    Dim bln_ As Boolean, str_c As String
    For lng_j = 1 To Len(str_本文)
        str_c = Mid$(str_本文, lng_j, 1)
        If str_c = str_クォート Then
            If Not bln_ Then
                lng_開始 = lng_j: bln_ = True
            Else
                Call ColorRange(obj_セル, lng_開始, lng_j - lng_開始 + 1, lng_)
                bln_ = False
            End If
        End If
    Next lng_j
End Sub

Private Sub ColorHashDateLiterals(ByVal obj_セル As Object, ByVal str_本文 As String, ByVal lng_ As Long)
    Dim lng_j As Long, lng_開始 As Long
    Dim bln_ As Boolean, str_c As String
    For lng_j = 1 To Len(str_本文)
        str_c = Mid$(str_本文, lng_j, 1)
        If str_c = "#" Then
            If Not bln_ Then
                lng_開始 = lng_j: bln_ = True
            Else
                Call ColorRange(obj_セル, lng_開始, lng_j - lng_開始 + 1, lng_)
                bln_ = False
            End If
        End If
    Next lng_j
End Sub

Private Sub ColorBracketIdentifiers(ByVal obj_セル As Object, ByVal str_本文 As String, ByVal lng_ As Long)
    Dim lng_j As Long, lng_開始 As Long
    Dim bln_角括弧中 As Boolean, str_c As String
    For lng_j = 1 To Len(str_本文)
        str_c = Mid$(str_本文, lng_j, 1)
        If str_c = "[" Then
            lng_開始 = lng_j: bln_角括弧中 = True
        End If
        If str_c = "]" And bln_角括弧中 Then
            Call ColorRange(obj_セル, lng_開始, lng_j - lng_開始 + 1, lng_)
            bln_角括弧中 = False
        End If
    Next lng_j
End Sub

Private Sub ColorNumbers(ByVal obj_セル As Object, ByVal str_本文 As String, ByVal col_除外範囲 As Collection, ByVal lng_ As Long)
    Dim obj_RegEx As Object
    Set obj_RegEx = CreateObject("VBScript.RegExp")
    obj_RegEx.Pattern = "(\b\d+(\.\d+)?([Ee][+-]?\d+)?\b)"
    obj_RegEx.Global = True
    Dim obj_Matches As Object, obj_m As Object
    If obj_RegEx.Test(str_本文) Then
        Set obj_Matches = obj_RegEx.Execute(str_本文)
        For Each obj_m In obj_Matches
            If Not IsInsideExcluded(obj_m.FirstIndex + 1, obj_m.length, col_除外範囲) Then   ' FirstIndexは0始まり
                Call ColorRange(obj_セル, obj_m.FirstIndex + 1, obj_m.length, lng_)
            End If
        Next
    End If
End Sub

Private Sub ColorOperators(ByVal obj_セル As Object, ByVal str_本文 As String, ByVal col_除外範囲 As Collection, ByVal lng_ As Long)
    Dim arr_演算子 As Variant
    arr_演算子 = Array("=", "<>", "<=", ">=", "<", ">", "+", "-", "*", "/", "&")
    Dim var_演算子 As Variant, lng_位置 As Long
    For Each var_演算子 In arr_演算子
        lng_位置 = 1
        Do
            lng_位置 = InStr(lng_位置, str_本文, var_演算子)
            If lng_位置 = 0 Then Exit Do
            ' 前後とも英数字でない(演算子として独立)場合のみ着色
            If lng_位置 > 1 And lng_位置 < Len(str_本文) Then
                If Not ((Mid$(str_本文, lng_位置 - 1, 1) Like "[0-9A-Za-z]") Or (Mid$(str_本文, lng_位置 + 1, 1) Like "[0-9A-Za-z]")) Then
                    If Not IsInsideExcluded(lng_位置, Len(var_演算子), col_除外範囲) Then
                        Call ColorRange(obj_セル, lng_位置, Len(var_演算子), lng_)
                    End If
                End If
            End If
            lng_位置 = lng_位置 + Len(var_演算子)
        Loop
    Next var_演算子
End Sub

' === 色付けユーティリティ ===

Private Function BuildExcludedRanges(ByVal str_本文 As String) As Collection
    Dim col_範囲 As New Collection
    ' シングル/ダブルクォート、#で囲まれた範囲、[]で囲まれた範囲を収集
    Dim lng_I As Long, str_c As String, bln_SQ As Boolean, bln_DQ As Boolean, bln_Hash As Boolean, bln_Bracket As Boolean
    Dim lng_SQ開始 As Long, lng_DQ開始 As Long, lng_Hash開始 As Long, lng_Bracket開始 As Long
    For lng_I = 1 To Len(str_本文)
        str_c = Mid$(str_本文, lng_I, 1)
        If str_c = "'" Then
            If Not bln_DQ Then
                If Not bln_SQ Then
                    lng_SQ開始 = lng_I: bln_SQ = True
                Else
                    col_範囲.Add Array(lng_SQ開始, lng_I - lng_SQ開始 + 1)
                    bln_SQ = False
                End If
            End If
        ElseIf str_c = """" Then
            If Not bln_SQ Then
                If Not bln_DQ Then
                    lng_DQ開始 = lng_I: bln_DQ = True
                Else
                    col_範囲.Add Array(lng_DQ開始, lng_I - lng_DQ開始 + 1)
                    bln_DQ = False
                End If
            End If
        ElseIf str_c = "#" Then
            If Not bln_SQ And Not bln_DQ Then
                If Not bln_Hash Then
                    lng_Hash開始 = lng_I: bln_Hash = True
                Else
                    col_範囲.Add Array(lng_Hash開始, lng_I - lng_Hash開始 + 1)
                    bln_Hash = False
                End If
            End If
        ElseIf str_c = "[" Then
            If Not bln_SQ And Not bln_DQ Then
                lng_Bracket開始 = lng_I: bln_Bracket = True
            End If
        ElseIf str_c = "]" Then
            If bln_Bracket And Not bln_SQ And Not bln_DQ Then
                col_範囲.Add Array(lng_Bracket開始, lng_I - lng_Bracket開始 + 1)
                bln_Bracket = False
            End If
        End If
    Next lng_I
    Set BuildExcludedRanges = col_範囲
End Function

Private Function IsInsideExcluded(ByVal lng_位置 As Long, ByVal lng_長さ As Long, ByVal col_除外範囲 As Collection) As Boolean
    Dim var_項目 As Variant
    For Each var_項目 In col_除外範囲
        Dim lng_開始 As Long, lng_ As Long
        lng_開始 = var_項目(1): lng_ = var_項目(2)
        If lng_位置 >= lng_開始 And lng_位置 < lng_開始 + lng_ _
           Or (lng_位置 + lng_長さ - 1 >= lng_開始 And lng_位置 + lng_長さ - 1 < lng_開始 + lng_) _
           Or (lng_位置 < lng_開始 And lng_位置 + lng_長さ - 1 >= lng_開始 + lng_ - 1) Then
            IsInsideExcluded = True
            Exit Function
        End If
    Next
    IsInsideExcluded = False
End Function

Private Function IsWholeWord(ByVal str_本文 As String, ByVal lng_位置 As Long, ByVal lng_長さ As Long) As Boolean
    Dim str_ As String, str_ As String
    str_ = ""
    str_ = ""
    If lng_位置 > 1 Then str_ = Mid$(str_本文, lng_位置 - 1, 1)
    If lng_位置 + lng_長さ <= Len(str_本文) Then str_ = Mid$(str_本文, lng_位置 + lng_長さ, 1)
    ' 前後が英数字またはアンダースコアでない場合、その単語は独立している
    If (str_ = "" Or str_ Like "[!0-9A-Za-z_]" Or str_ Like Space(1)) _
       And (str_ = "" Or str_ Like "[!0-9A-Za-z_]" Or str_ Like Space(1)) Then
        IsWholeWord = True
    Else
        IsWholeWord = False
    End If
End Function

Private Sub ColorRange(ByVal obj_セル As Object, ByVal lng_開始 As Long, ByVal lng_長さ As Long, ByVal lng_ As Long)
    With obj_セル.Characters(Start:=lng_開始, length:=lng_長さ).Font
        .Color = lng_
        .Bold = False
        .Italic = False
    End With
End Sub


' =========================
' 参照設定ユーティリティ
' =========================

' 入口:必要な参照を一括で有効化(既にあるものはスキップ)
Public Sub EnsureReferencesForExport()
    On Error GoTo ErrHandler
    Dim col_ログ As Collection: Set col_ログ = New Collection
    
    ' 必須:ACEDAO(DAOの型解決を安定させる)
    EnsureRefGUID "{4AC9E1DA-5BAD-4AC7-86E3-24F4CDCECA28}", 1, 0, _
                  "Microsoft Office Access database engine Object Library (ACEDAO)", col_ログ
    
    ' 推奨:VBScript 正規表現(将来早期バインドに切替える場合に有用)
    EnsureRefGUID "{3F4DACA7-160D-11D2-A8E9-00104B365C9F}", 5, 5, _
                  "Microsoft VBScript Regular Expressions 5.5", col_ログ
    
    ' 任意:Scripting Runtime(Dictionary/FSOを使う拡張がある場合)
    EnsureRefGUID "{420B2830-E718-11CF-893D-00A0C9054228}", 1, 0, _
                  "Microsoft Scripting Runtime", col_ログ
    
    ' 情報表示
    Dim str_メッセージ As String
    str_メッセージ = "参照設定の確認・追加が完了しました。" & vbCrLf & vbCrLf & JoinLog(col_ログ)
    MsgBox str_メッセージ, vbInformation, "EnsureReferencesForExport"
    Exit Sub
    
ErrHandler:
    MsgBox "参照設定の処理でエラーが発生しました: " & Err.Number & " - " & Err.Description, vbCritical
End Sub

' 指定GUID/バージョンの参照が無ければ追加
Private Sub EnsureRefGUID(ByVal str_GUID As String, ByVal lng_メジャー As Long, ByVal lng_マイナー As Long, _
                          ByVal str_名称 As String, ByRef col_ログ As Collection)
    On Error GoTo AddRef
    Dim ref_参照 As Reference
    For Each ref_参照 In Application.References
        If StrComp(ref_参照.guid, str_GUID, vbTextCompare) = 0 Then
            ' 既に同GUIDの参照がある(バージョン違いでも可とみなす)
            col_ログ.Add "?? 既に有効: " & str_名称 & "  (GUID=" & str_GUID & ")"
            Exit Sub
        End If
    Next ref_参照
    
AddRef:
    On Error GoTo Fail
    Application.References.AddFromGuid str_GUID, lng_メジャー, lng_マイナー
    col_ログ.Add "? 追加: " & str_名称 & "  (GUID=" & str_GUID & ", v" & lng_メジャー & "." & lng_マイナー & ")"
    Exit Sub
    
Fail:
    col_ログ.Add "? 追加失敗: " & str_名称 & "  (GUID=" & str_GUID & ", v" & lng_メジャー & "." & lng_マイナー & ") - " & Err.Description
    Err.Clear
End Sub

' ログ整形
Private Function JoinLog(col_ログ As Collection) As String
    If col_ログ Is Nothing Or col_ログ.Count = 0 Then
        JoinLog = "(変更なし)"
    Else
        Dim lng_I As Long, arr_文字列() As String
        ReDim arr_文字列(1 To col_ログ.Count)
        For lng_I = 1 To col_ログ.Count
            arr_文字列(lng_I) = CStr(col_ログ(lng_I))
        Next
        JoinLog = Join(arr_文字列, vbCrLf)
    End If
End Function

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?