Skip to Content

データベースアクション

dbアクションはMySQL、PostgreSQL、SQLiteデータベースでSQLクエリを実行し、包括的な結果処理とエラーレポートを提供します。

基本的な構文

データベースのステップでは、接続文字列と実行する文を指定します。

steps: - name: "Database Query" uses: db with: dsn: "mysql://user:password@localhost:3306/database" query: "SELECT * FROM users WHERE active = ?" params: [true] test: res.code == 0 && res.rows_affected > 0

パラメータ

データベースステップは4つのパラメータで決まります。接続先、実行する文、バインドする値、待ち時間の上限です。

dsn (必須)

型: String
説明: 自動ドライバー検出付きのデータベース接続文字列
サポート: テンプレート式

# MySQL vars: db_pass: "{{DB_PASS}}" with: dsn: "mysql://user:password@localhost:3306/database" dsn: "mysql://{{vars.db_user}}:{{vars.db_pass}}@{{vars.db_host}}/{{vars.db_name}}" # PostgreSQL vars: pg_user: "{{PG_USER}}" pg_pass: "{{PG_PASS}}" pg_host: "{{PG_HOST}}" pg_db: "{{PG_DB}}" with: dsn: "postgres://user:password@localhost:5432/database?sslmode=disable" dsn: "postgres://{{vars.pg_user}}:{{vars.pg_pass}}@{{vars.pg_host}}/{{vars.pg_db}}" # SQLite with: dsn: "file:./testdata/sqlite.db" dsn: "file:/absolute/path/to/database.db" dsn: "file:{{vars.data_dir}}/app.db"

query (必須)

型: String
説明: 実行するSQLクエリ
サポート: テンプレート式と複数行文字列

with: query: "SELECT * FROM users" query: "INSERT INTO logs (message, timestamp) VALUES (?, NOW())" query: | SELECT u.name, u.email, p.title FROM users u JOIN profiles p ON u.id = p.user_id WHERE u.active = ? AND u.created_at > ?

params (オプション)

型: 混合値の配列 (String, Number, Boolean)
説明: プリペアドステートメント用のクエリパラメータ
サポート: テンプレート式

with: query: "SELECT * FROM users WHERE id = ? AND active = ?" params: [123, true, "{{vars.user_email}}"]

timeout (オプション)

型: Duration
デフォルト: 30s
説明: 接続とクエリの実行にかける制限時間。"60s"のようなGoのduration形式か秒数で、0より大きい値

with: query: "SELECT COUNT(*) FROM large_table" timeout: "60s"

制限時間を過ぎたクエリは止められ、ステップはそのままテストに進みます。このときres.codeは1になり、res.errorはtimed out after 60sで始まります。

レスポンスオブジェクト

データベースアクションは次のプロパティを持つresオブジェクトを提供します:

プロパティ型説明
codeInteger操作結果 (0 = 成功, 1 = エラー)
rows_affectedIntegerクエリによって影響を受けた行数
rowsArraySELECTステートメントのクエリ結果(オブジェクトとして)
errorString操作が失敗した場合のエラーメッセージ

resのほかに、テストでは次の値も使えます。

フィールド型説明
statusIntegerクエリが成功すれば0
rt.durationStringクエリにかかった時間。"1.3ms"など
rt.secFloat同じ時間を秒で表した値

レスポンス例

レスポンスの中身は実行した文によって変わります。SELECTでは行が返り、INSERTやUPDATEでは件数が返ります。

SELECTクエリレスポンス

取得した行はres.rowsに、その件数はres.rows_affectedに入ります。

steps: - name: "Fetch Users" id: fetch_users uses: db with: dsn: "mysql://user:pass@localhost/db" query: "SELECT id, name, email FROM users WHERE active = ?" params: [true] test: res.code == 0 && res.rows_affected > 0 outputs: user_count: res.rows_affected first_user_id: res.rows[0].id first_user_name: res.rows[0].name

INSERT/UPDATEクエリレスポンス

書き込みでは行が返らないため、結果は変更された行数で確認します。

steps: - name: "Insert User" uses: db with: dsn: "postgres://user:pass@localhost/db" query: "INSERT INTO users (name, email) VALUES ($1, $2)" params: ["John Doe", "john@example.com"] test: res.code == 0 && res.rows_affected == 1

データベース固有の機能

dsnとqueryの書き方はどのドライバでも共通ですが、クエリの中の構文は共通ではありません。以下では対応しているデータベースごとの書き方を示します。

MySQL例

接続オプションはDSNに含めます。ストアドプロシージャも通常の文と同じように呼び出せます。

# 接続オプション付きMySQL - name: "MySQL Query" uses: db with: dsn: "mysql://user:pass@tcp(localhost:3306)/database?charset=utf8mb4&parseTime=true" query: "SELECT VERSION() as mysql_version, NOW() as current_time" test: res.code == 0 # MySQLストアドプロシージャ - name: "Call Procedure" uses: db with: dsn: "mysql://user:pass@localhost:3306/database" query: "CALL GetUsersByDepartment(?)" params: ["Engineering"] test: res.code == 0

PostgreSQL例

PostgreSQLではJSONや配列の演算子をクエリの中で直接使えます。

# JSON操作付きPostgreSQL - name: "JSON Query" uses: db with: dsn: "postgres://user:pass@localhost:5432/database?sslmode=disable" query: | SELECT name, data->>'role' as role, data->'preferences' as prefs FROM users WHERE data ? 'role' AND data->>'role' = $1 params: ["admin"] test: res.code == 0 # PostgreSQL配列操作 - name: "Array Query" uses: db with: dsn: "postgres://user:pass@localhost:5432/database" query: "SELECT name FROM users WHERE tags && $1" params: ['{"admin","moderator"}'] test: res.code == 0

SQLite例

SQLiteにはファイルパスを渡します。:memory:を指定すると、そのステップの間だけ存在するデータベースになります。

# ファイル作成付きSQLite - name: "SQLite Query" uses: db with: dsn: "file:./testdata/sqlite.db" query: | CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, email TEXT UNIQUE, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) test: res.code == 0 # インメモリデータベースSQLite - name: "Memory Database" uses: db with: dsn: "file::memory:" query: "CREATE TABLE temp_data (id INTEGER, value TEXT)" test: res.code == 0

一般的なクエリパターン

ワークフロー中のデータベースステップの用途は、おおむね3つに分かれます。データが期待する状態にあることの検証、データベース自体の挙動の監視、後続のステップが読むレコードの一括投入です。

データ検証クエリ

存在してはならないものを数えるクエリを書けば、データの不変条件をそのままテストにできます。

- name: "Check Data Integrity" uses: db with: dsn: "mysql://user:pass@localhost/db" query: | SELECT COUNT(*) as total_users, COUNT(CASE WHEN active = 1 THEN 1 END) as active_users, COUNT(CASE WHEN email IS NULL THEN 1 END) as missing_emails FROM users test: | res.code == 0 && res.rows[0].total_users > 0 && res.rows[0].missing_emails == 0

パフォーマンス監視

データベース自身の統計テーブルから、接続数やスロークエリを取得できます。

- name: "Database Performance Check" uses: db with: dsn: "postgres://user:pass@localhost/db" query: | SELECT schemaname, tablename, seq_scan, seq_tup_read, idx_scan, idx_tup_fetch FROM pg_stat_user_tables WHERE seq_scan > 1000 timeout: "10s" test: res.code == 0 outputs: high_seq_scan_tables: res.rows_affected

バッチ操作

1つの文で複数行を挿入し、後続のステップが読むデータを用意します。

- name: "Batch Insert" uses: db with: dsn: "mysql://user:pass@localhost/db" query: | INSERT INTO audit_log (action, table_name, record_id, timestamp) VALUES ('CREATE', 'users', 123, NOW()), ('UPDATE', 'profiles', 456, NOW()), ('DELETE', 'sessions', 789, NOW()) test: res.code == 0 && res.rows_affected == 3

セキュリティ機能

データベースアクションはいくつかのセキュリティ対策を実装しています:

  • プリペアドステートメント: すべてのパラメータ化クエリでプリペアドステートメントを使用してSQLインジェクションを防止
  • 接続文字列マスキング: ログと出力でパスワードをマスク
  • タイムアウト保護: 長時間実行されるクエリのハングを防止
  • ドライバー検証: 承認されたデータベースドライバーのみをサポート
  • DSN検証: 実行前に接続文字列形式を検証

ガードの下での動作

--read-onlyを指定して実行すると、SELECT、SHOW、DESCRIBE、DESC、EXPLAIN、WITHのいずれかで始まる1文だけを実行し、それ以外の文は接続する前に拒否します。末尾以外にセミコロンを含むクエリも、文字列やコメントの中であっても拒否します。文字列やコメントの終わり方はデータベースや接続の設定によって異なり、文字列のつもりのセミコロンが2つ目の文を始めることがあるからです。読み取り専用トランザクションの前、接続を開くときにドライバに文を実行させるDSNも拒否します。MySQLではmultiStatementsと、go-sql-driverが独自の文で設定するシステム変数、SQLiteではmode、cache、immutable、_txlock、_time_format以外のパラメータ(_pragmaなど)です。その文は読み取り専用トランザクション(SQLiteでは問い合わせしかできない接続)で実行するため、WITH x AS (DELETE ...) SELECT ...のように文が書き込みを隠していても、データベース自身が拒否します。その場合、ステップはデータベースが報告するとおりに失敗します。--allow-hostを指定して実行すると、ドライバが接続しうる各サーバーがすべて許可されていない限り、DSNを拒否します。サーバーは、ドライバ自身がDSNを解釈した結果で確かめます。ポートがなければドライバの既定のポートとして扱います。MySQLでは、URLを変換したDSNからgo-sql-driverが読む、接続先のアドレスです。PostgreSQLでは、lib/pqがDSNを解決した結果を使います。これは接続に使うのと同じDSNです。パラメータ、サービスファイル、PGHOST、PGHOSTADDR、PGPORTなども考慮し、ホストのリストはすべて確かめ、hostaddrがあればそのアドレスを確かめます。lib/pqが読めないDSNは拒否します。SQLiteのファイルはホストを持ちません。拒否されたステップは種類refusedで失敗します。--read-onlyを参照してください。

エラーハンドリング

データベースがクエリを拒否した場合や接続に失敗した場合も、ステップは止まりません。res.codeが1になり、データベースのメッセージがres.errorに入るので、想定した失敗かどうかをテストで確かめられます。メッセージはデータベースによって異なり、SQLiteは存在しないテーブルをno such tableと報告します。

- name: "Query a Table That Does Not Exist" uses: db with: dsn: "file:./testdata/app.db" query: "SELECT * FROM missing_table" test: res.code == 1 && res.error contains "no such table" outputs: error: res.error

トランザクション例

アクションは直接トランザクションをサポートしませんが、データベース固有のトランザクション構文を使用できます:

# PostgreSQLトランザクション - name: "Begin Transaction" uses: db with: dsn: "postgres://user:pass@localhost/db" query: "BEGIN" test: res.code == 0 - name: "Insert Data" uses: db with: dsn: "postgres://user:pass@localhost/db" query: "INSERT INTO users (name) VALUES ($1)" params: ["Test User"] test: res.code == 0 - name: "Commit Transaction" uses: db with: dsn: "postgres://user:pass@localhost/db" query: "COMMIT" test: res.code == 0
更新日時