SQLフォーマッタ

  1. Alt + F11 で VBA エディタを開く
  2. 挿入 → 標準モジュール → 上記コードを貼り付け
  3. Excel に戻って、整形したい SQL が入ったセル(または範囲)を選択
  4. Alt + F8 → FormatSqlInSelection を実行
Option Explicit

' ==== 公開関数 ============================================

' 単一文字列の SQL を整形
Public Function FormatSql(ByVal sql As String) As String
    Dim tokens As Collection
    Dim result As String
    
    Set tokens = TokenizeSql(sql)
    result = BuildFormattedSql(tokens)
    
    FormatSql = result
End Function

' 選択セル内の SQL をまとめて整形
Public Sub FormatSqlInSelection()
    Dim c As Range
    
    If Selection Is Nothing Then Exit Sub
    
    For Each c In Selection.Cells
        If Not IsEmpty(c.Value) Then
            On Error Resume Next
            c.Value = FormatSql(CStr(c.Value))
            On Error GoTo 0
        End If
    Next c
End Sub

' ==== トークナイザ ========================================

' SQL をトークン列に分解(文字列リテラルも考慮)
' SQL をトークン列に分解(文字列リテラル & ブロックコメントも考慮)
Private Function TokenizeSql(ByVal sql As String) As Collection
    Dim tokens As New Collection
    Dim i As Long
    Dim ch As String
    Dim token As String
    Dim inSingle As Boolean
    Dim inDouble As Boolean
    Dim j As Long
    
    i = 1
    Do While i <= Len(sql)
        ch = Mid$(sql, i, 1)
        
        ' ==== ブロックコメント /* ... */ の検出 ====
        If Not inSingle And Not inDouble Then
            If ch = "/" And i < Len(sql) And Mid$(sql, i + 1, 1) = "*" Then
                ' 直前までのトークンを確定
                If token <> "" Then
                    tokens.Add token
                    token = ""
                End If
                
                j = i + 2
                Do While j <= Len(sql) - 1
                    If Mid$(sql, j, 2) = "*/" Then
                        j = j + 2
                        Exit Do
                    End If
                    j = j + 1
                Loop
                If j > Len(sql) Then j = Len(sql) + 1   ' 万一 */ が無かった場合の保険
                
                ' コメント全体を 1 トークンとして追加
                tokens.Add Mid$(sql, i, j - i)
                
                i = j
                GoTo NextChar
            End If
        End If
        
        ' ==== ここから元々のロジック ====
        If inSingle Then
            ' 単一引用符内('...')
            token = token & ch
            If ch = "'" Then
                ' '' のエスケープを考慮
                If i < Len(sql) And Mid$(sql, i + 1, 1) = "'" Then
                    token = token & "'"
                    i = i + 1
                Else
                    tokens.Add token
                    token = ""
                    inSingle = False
                End If
            End If
        
        ElseIf inDouble Then
            ' 二重引用符内("...")
            token = token & ch
            If ch = """" Then
                ' "" のエスケープを考慮
                If i < Len(sql) And Mid$(sql, i + 1, 1) = """" Then
                    token = token & """"
                    i = i + 1
                Else
                    tokens.Add token
                    token = ""
                    inDouble = False
                End If
            End If
        
        Else
            ' 通常領域
            Select Case ch
                Case " ", vbTab, vbCr, vbLf
                    If token <> "" Then
                        tokens.Add token
                        token = ""
                    End If
                Case "("
                    If token <> "" Then
                        tokens.Add token
                        token = ""
                    End If
                    tokens.Add ch
                Case ")"
                    If token <> "" Then
                        tokens.Add token
                        token = ""
                    End If
                    tokens.Add ch
                Case ","
                    If token <> "" Then
                        tokens.Add token
                        token = ""
                    End If
                    tokens.Add ch
                Case "'"
                    If token <> "" Then
                        tokens.Add token
                        token = ""
                    End If
                    inSingle = True
                    token = "'"
                Case """"
                    If token <> "" Then
                        tokens.Add token
                        token = ""
                    End If
                    inDouble = True
                    token = """"
                Case Else
                    token = token & ch
            End Select
        End If
        
NextChar:
        i = i + 1
    Loop
    
    If token <> "" Then
        tokens.Add token
    End If
    
    Set TokenizeSql = tokens
End Function


' ==== フォーマッタ本体 ====================================

Private Function BuildFormattedSql(tokens As Collection) As String
    Dim sb As String
    Dim indentLevel As Long          ' () のネストレベル
    Dim nextIndentLevel As Long      ' キーワード行の次の行のインデント
    Dim i As Long
    Dim tok As String
    Dim lowerTok As String
    Dim dispTok As String
    
    indentLevel = 0
    nextIndentLevel = 0
    sb = ""
    
    i = 1
    Do While i <= tokens.Count
        tok = tokens(i)
        dispTok = tok
        lowerTok = LCase$(tok)
        
        ' -------------------------------------------------
        ' 1. 構造トークン(カッコとカンマ)
        ' -------------------------------------------------
        If tok = "(" Then
            ' "(" を書いて改行 → インデントレベル+1
            If EndsWithNewline(sb) Then
                sb = sb & IndentString(indentLevel)
            ElseIf Len(sb) > 0 Then
                If Right$(sb, 1) <> " " Then
                    sb = sb & " "
                End If
            End If
            sb = sb & "(" & vbCrLf
            indentLevel = indentLevel + 1
            nextIndentLevel = indentLevel
        
        ElseIf tok = ")" Then
            ' 閉じカッコの前にインデントレベルを戻す
            indentLevel = IIf(indentLevel > 0, indentLevel - 1, 0)
            sb = RTrim$(sb) & vbCrLf & IndentString(indentLevel) & ")"
            nextIndentLevel = indentLevel
        
        ElseIf tok = "," Then
            ' カンマの後は改行+同じインデント
            sb = sb & ","
            sb = sb & vbCrLf
            ' nextIndentLevel はそのまま(SELECT句のカラムを揃える)
        
        Else
            ' -------------------------------------------------
            ' 2. キーワード結合(GROUP BY / ORDER BY / UNION ALL など)
            ' -------------------------------------------------
            If (lowerTok = "order" Or lowerTok = "group") And i < tokens.Count Then
                If LCase$(tokens(i + 1)) = "by" Then
                    dispTok = tok & " " & tokens(i + 1)
                    lowerTok = lowerTok & " by"
                    i = i + 1
                End If
            ElseIf lowerTok = "union" And i < tokens.Count Then
                If LCase$(tokens(i + 1)) = "all" Then
                    dispTok = tok & " " & tokens(i + 1)
                    lowerTok = lowerTok & " all"
                    i = i + 1
                End If
            End If
            
            ' -------------------------------------------------
            ' 3. キーワード行かどうか
            ' -------------------------------------------------
            If IsSqlKeyword(lowerTok) Then
                ' 直前の余分な空白カット
                sb = RTrim$(sb)
                If Len(sb) > 0 Then
                    sb = sb & vbCrLf
                End If
                
                ' キーワード自体を表示
                sb = sb & IndentString(indentLevel) & dispTok & vbCrLf
                
                ' 次の行から一段深くインデント
                nextIndentLevel = indentLevel + 1
            
            Else
                ' -------------------------------------------------
                ' 4. 通常トークン
                ' -------------------------------------------------
                If EndsWithNewline(sb) Then
                    ' 改行直後ならインデントを入れる
                    sb = sb & IndentString(nextIndentLevel) & dispTok
                ElseIf Len(sb) = 0 Then
                    sb = dispTok
                Else
                    Dim lastChar As String
                    lastChar = Right$(sb, 1)
                    If lastChar = " " Or lastChar = "(" Then
                        sb = sb & dispTok
                    Else
                        sb = sb & " " & dispTok
                    End If
                End If
            End If
        End If
        
        i = i + 1
    Loop
    
    BuildFormattedSql = RTrim$(sb)
End Function

' ==== 補助関数 ============================================

Private Function IndentString(ByVal level As Long) As String
    If level < 0 Then level = 0
    IndentString = String(level * 4, " ") ' 4スペースインデント
End Function

Private Function EndsWithNewline(ByVal s As String) As Boolean
    If Len(s) >= 2 Then
        EndsWithNewline = (Right$(s, 2) = vbCrLf)
    Else
        EndsWithNewline = False
    End If
End Function

' SQL キーワード判定
Private Function IsSqlKeyword(ByVal key As String) As Boolean
    Select Case key
        Case "select", "from", "where", "group", "group by", "order", "order by", _
             "having", "join", "inner", "left", "right", "full", "cross", _
             "union", "union all", "on", "and", "or", _
             "case", "when", "then", "else", "end", _
             "update", "insert", "into", "values", "delete", "set"
            IsSqlKeyword = True
        Case Else
            IsSqlKeyword = False
    End Select
End Function