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?

StylezAdvent Calendar 2024

Day 19

SQL LinterのsqruffをDocker環境で試してみた

Last updated at Posted at 2024-12-18

はじめに

SQLのLinter Toolのsqruffを試してみました

今回は、誰でも簡単に試せるようにDocdkr上にExpress.jsを起動することで、ブラウザアクセスして、結果がわかるようにしてみました。

構成イメージ

architecture.png

  • 画面は最低限のシンプルなHTML、js、CSSを用意
  • バックエンドはExpressでシンプル実装
  • ExpressからsqruffはCLIで実行

構成しているファイル

全体構成

├─config
│  └─ .sqruff ※ここは今回は未作成
├─public
│  │  index.html
│  └─ styles.css
│─src
│  └─ server.js
│─ compose.yml
└─ Dockerfile

Docker関連

まずはDockerFileとcomseposeの用意

# ベースイメージとしてNode.js 20を選択
FROM node:20

# sqruffのインストール
RUN apt-get update && \
    apt-get install -y curl

RUN curl -fsSL https://raw.githubusercontent.com/quarylabs/sqruff/main/install.sh | bash

# 作業ディレクトリの設定
WORKDIR /app

# expressのインストール
RUN npm install express

# アプリケーションコードをコピー
COPY src ./src
COPY public ./public

# sqruff設定ファイルをコピー
COPY config .

# サーバーを起動
CMD ["node", "src/server.js"]

comseposeは特に何もしていないけど、こっちの方が起動シンプルなので定義してます。

compose.yml
services:
  sqruff:
    container_name: sqruff
    build: .
    ports:
      - "3000:3000"

サーバーのソース(Expressで実行するファイル)

src/server.js
const express = require('express');
const { spawn } = require('child_process');

const app = express();
const PORT = 3000;

// 静的ファイルを提供するための設定
app.use(express.static('public'));

// JSONデータのパース用(Expressの組み込み機能)
app.use(express.json());

app.post('/lint', (req, res) => {
    const { sql, dbEngine } = req.body;

    if (!sql || !dbEngine) {
        return res.status(400).json({ error: 'SQLとDBエンジンの両方を指定してください' });
    }

    // sqruffをCLIで実行するプロセスを生成(標準入力にSQLを直接渡す)
    const lintProcess = spawn('sqruff', ['lint', '-']);

    let output = '';
    let errorOutput = '';

    // 標準出力からのデータを取得
    lintProcess.stdout.on('data', (data) => {
        output += data.toString();
    });

    // 標準エラーからのデータを取得
    lintProcess.stderr.on('data', (data) => {
        errorOutput += data.toString();
    });

    // プロセスの終了時の処理
    lintProcess.on('close', (code) => {
        if (code !== 0) {
            console.error(`code: ${code}`);
            console.error(`stderr: ${errorOutput}`);
            return res.json({ code, result: errorOutput });
        }

        try {
            console.error(`stdout: ${output}`);
            res.json({ code: 0, result: output });
        } catch (parseError) {
            res.status(500).json({ error: 'Lint結果の解析に失敗しました' });
        }
    });

    // 標準入力にSQLを送信(末尾に改行を追加)
    lintProcess.stdin.write(sql + '\n');
    lintProcess.stdin.end();
});

app.listen(PORT, () => {
    console.log(`サーバーがポート ${PORT} で起動しました`);
});

画面のソース

DBドライバーの選択は、よく使いそうなのだけに絞ってます。

public/index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>SQL Linter</title>
    <link rel="stylesheet" href="styles.css">
</head>
<body>
    <div class="container">
        <header>
            <h1>SQL Linter</h1>
            <p>Validate your SQL syntax and style effortlessly</p>
        </header>
        <main>
            <form id="sqlForm" class="form">
                <div class="form-group">
                    <label for="dbEngine">DB Engine:</label>
                    <select id="dbEngine" name="dbEngine" class="input">
                        <option value="mysql">MySQL</option>
                        <option value="postgresql">PostgreSQL</option>
                        <option value="sqlite">SQLite</option>
                    </select>
                </div>
                <div class="form-group">
                    <label for="sql">Enter SQL:</label>
                    <textarea id="sql" name="sql" class="input textarea" rows="8" placeholder="Write your SQL here..."></textarea>
                </div>
                <button type="button" onclick="lintSql()" class="btn">Lint SQL</button>
            </form>
            <div id="result" class="result"></div>
        </main>
    </div>
    <script>
        function lintSql() {
            const dbEngine = document.getElementById("dbEngine").value;
            const sql = document.getElementById("sql").value;
            fetch('/lint', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify({ sql, dbEngine })
            })
            .then(response => response.json())
            .then(data => {
                const resultDiv = document.getElementById("result");
                resultDiv.innerHTML = `<pre>code: ${data.code}<br><br>${data.result}</pre>`;
            })
            .catch(error => {
                const resultDiv = document.getElementById("result");
                resultDiv.innerHTML = `<p class="error">Error: ${error.message}</p>`;
            });
        }
    </script>
</body>
</html>
public/style.css
/* Reset */
body, h1, p, textarea, select, button {
    margin: 0;
    padding: 0;
    font-family: 'Arial', sans-serif;
    box-sizing: border-box;
}

body {
    background: linear-gradient(135deg, #1e3c72, #2a5298);
    color: #ffffff;
    line-height: 1.6;
    font-size: 16px;
    min-height: 100vh;
    display: flex;
    justify-content: center;
    align-items: center;
}

/* Container */
.container {
    width: 90%;
    max-width: 800px;
    background: #ffffff;
    color: #333333;
    border-radius: 10px;
    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
    overflow: hidden;
    padding: 20px;
}

/* Header */
header {
    text-align: center;
    margin-bottom: 20px;
}

header h1 {
    font-size: 2rem;
    color: #1e3c72;
}

header p {
    color: #666666;
    font-size: 1rem;
}

/* Form */
.form {
    display: flex;
    flex-direction: column;
    gap: 15px;
}

.form-group {
    display: flex;
    flex-direction: column;
}

label {
    font-weight: bold;
    margin-bottom: 5px;
}

.input {
    padding: 10px;
    border: 1px solid #ccc;
    border-radius: 5px;
    font-size: 1rem;
}

.textarea {
    resize: vertical;
    min-height: 150px;
}

.btn {
    padding: 10px;
    background: #1e3c72;
    color: #ffffff;
    border: none;
    border-radius: 5px;
    font-size: 1rem;
    cursor: pointer;
    transition: background 0.3s ease;
}

.btn:hover {
    background: #2a5298;
}

/* Result */
.result {
    margin-top: 20px;
    padding: 10px;
    background: #f7f7f7;
    border: 1px solid #ddd;
    border-radius: 5px;
    color: #333;
    overflow: auto;
    max-height: 200px;
    white-space: pre-wrap;
    font-family: 'Courier New', Courier, monospace;
}

.error {
    color: #e74c3c;
}

実行

以下のコマンドでコンテナを起動します。

docker compose up

compose.ymlを作成していない場合は、以下のようにビルドして起動すれば大丈夫なはず(こっちは試していないので少し心配ですが・・)

# ビルド (少し時間かかる)
docker build -t sqruff-express .

# 起動
docker run -p 3000:3000 -d sqruff-express

実行イメージ

Dockerが起動したらhttp://localhost:3000でアクセスします。
👇こんな画面が表示されたらSQLを入力して試してみましょう。

image.png

構文エラーがある場合は、↓の枠にエラーが表示されました。

  • 1つ目:3行目のAsのキーワードを大文字のASに修正すること
  • 2つ目:4行目のインデントはスペース4つにすること

image.png

最後に

今回は、お試しなので構文チェックのルールはデフォルトで設定していますが、設定ファイルを用意することでインデントのサイズなど調整することができそうです。

ちなみに、ソースコードの8割ぐらいChatGPT先生に作成してもらいました。(便利ですねー

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?