From 53d529e09a5319c1259969887822e1d4dd490cc7 Mon Sep 17 00:00:00 2001 From: "Sotaro.Hikosaka" Date: Sat, 11 Jul 2026 20:48:38 +0900 Subject: [PATCH 1/5] =?UTF-8?q?docs:=20README=E3=81=AEDB=E8=A8=98=E8=BF=B0?= =?UTF-8?q?=E3=82=92PostgreSQL=E7=A7=BB=E8=A1=8C=E6=96=B9=E9=87=9D(#62)?= =?UTF-8?q?=E3=81=AB=E6=95=B4=E5=90=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d979b01..061634c 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ ## コンセプト -- 個人〜少人数、PC1台での運用を前提とした最小構成(SQLite・単一サーバー) +- 個人〜少人数、PC1台での運用を前提とした最小構成(単一サーバー。現行はSQLite、PostgreSQLをprimary DBへ移行予定: [#62](https://github.com/strnh/quotebuilder/issues/62)) - 過去の見積ファイル(xlsx/ods、quotebuilder 出力PDF)を取り込んで移行できる - データは利用者自身の手元に置き、バックアップ・復元も自己完結できる @@ -16,7 +16,7 @@ |---|---| | Backend | PHP 8.3 / Laravel 13 | | Frontend | React 19 + TypeScript / Vite / Tailwind CSS 4(SPA、同一オリジン配信) | -| DB | SQLite(PostgreSQL対応は検討中: [#27](https://github.com/strnh/quotebuilder/issues/27)) | +| DB | SQLite(PostgreSQLをprimary DBへ移行予定: [#62](https://github.com/strnh/quotebuilder/issues/62)。SQLiteは移行元/legacy扱いへ) | | 取込 | PhpSpreadsheet(xlsx / ods) | | 集計表示 | Recharts | | テスト | PHPUnit(Feature)/ Playwright(E2E)/ tsc | @@ -70,7 +70,7 @@ | バックアップ管理画面の拡充 | [#22](https://github.com/strnh/quotebuilder/issues/22) | 画面からの復元操作 | | 差分バックアップ | [#23](https://github.com/strnh/quotebuilder/issues/23) | 方式設計とMVP範囲の確定 | | 見積一覧のTAB化 | [#17](https://github.com/strnh/quotebuilder/issues/17) | ステータス別TABでの一覧表示 | -| PostgreSQL対応 | [#27](https://github.com/strnh/quotebuilder/issues/27) | DB pluginの検討 | +| PostgreSQL移行 | [#62](https://github.com/strnh/quotebuilder/issues/62) | PostgreSQLをprimary DB化([#27](https://github.com/strnh/quotebuilder/issues/27) の検討を包含) | | Windows簡易アプリ化 | [#2](https://github.com/strnh/quotebuilder/issues/2) | NativePHPによる非技術者向け配布 | さらに先の構想として、OAuth2 / MFA(メール)による認証強化があります(#5 に記載)。 From 2e8daadf0993575a406aee531c9aca3966c2b54a Mon Sep 17 00:00:00 2001 From: "Sotaro.Hikosaka" Date: Sat, 11 Jul 2026 20:58:35 +0900 Subject: [PATCH 2/5] =?UTF-8?q?feat:=20PostgreSQL=E9=96=8B=E7=99=BA?= =?UTF-8?q?=E7=92=B0=E5=A2=83=E3=81=AE.env=20example=E3=81=A8=E8=B5=B7?= =?UTF-8?q?=E5=8B=95=E3=82=B9=E3=82=AF=E3=83=AA=E3=83=97=E3=83=88=E3=82=92?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0=20(#62)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - .env.pgsql.example: pgsql接続のローカル開発既定値 - scripts/pgsql-dev.sh: podman/docker でのコンテナ起動/停止/破棄。 起動時にテスト用DB quotebuilder_test も作成する Co-Authored-By: Claude Fable 5 --- .env.pgsql.example | 78 ++++++++++++++++++++++++++++++ scripts/pgsql-dev.sh | 111 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 189 insertions(+) create mode 100644 .env.pgsql.example create mode 100755 scripts/pgsql-dev.sh diff --git a/.env.pgsql.example b/.env.pgsql.example new file mode 100644 index 0000000..01f6bce --- /dev/null +++ b/.env.pgsql.example @@ -0,0 +1,78 @@ +APP_NAME=Laravel +APP_ENV=local +APP_KEY= +APP_DEBUG=true +APP_URL=http://localhost + +# dev: local/testing環境限定でbackup/seed等をrole制限なしで許可。 +# production(未設定時のデフォルト、安全側): 常に厳格。ローカル開発で緩めたい場合のみ明示的に dev に変更する。 +APP_MODE=production + +# test/reset(デモDB初期化)を叩くための共有secret。X-Test-Reset-Tokenヘッダがこの値と +# 一致しないと403で拒否。未設定なら常に拒否(fail-closed)。dev環境でE2E等から使う場合のみ設定する。 +TEST_RESET_TOKEN= + +APP_LOCALE=en +APP_FALLBACK_LOCALE=en +APP_FAKER_LOCALE=en_US + +APP_MAINTENANCE_DRIVER=file +# APP_MAINTENANCE_STORE=database + +# PHP_CLI_SERVER_WORKERS=4 + +BCRYPT_ROUNDS=12 + +LOG_CHANNEL=stack +LOG_STACK=single +LOG_DEPRECATIONS_CHANNEL=null +LOG_LEVEL=debug + +# PostgreSQL接続 (scripts/pgsql-dev.sh が起動するローカルコンテナの既定値) +DB_CONNECTION=pgsql +DB_HOST=127.0.0.1 +DB_PORT=5432 +DB_DATABASE=quotebuilder +DB_USERNAME=quotebuilder +DB_PASSWORD=quotebuilder + +SESSION_DRIVER=database +SESSION_LIFETIME=120 +SESSION_ENCRYPT=false +SESSION_PATH=/ +SESSION_DOMAIN=null + +BROADCAST_CONNECTION=log +FILESYSTEM_DISK=local +BACKUP_DISK=local +BACKUP_DIRECTORY=backups +BACKUP_RETENTION_DAYS=30 +BACKUP_MAX_GENERATIONS=20 +QUEUE_CONNECTION=database + +CACHE_STORE=database +# CACHE_PREFIX= + +MEMCACHED_HOST=127.0.0.1 + +REDIS_CLIENT=phpredis +REDIS_HOST=127.0.0.1 +REDIS_PASSWORD=null +REDIS_PORT=6379 + +MAIL_MAILER=log +MAIL_SCHEME=null +MAIL_HOST=127.0.0.1 +MAIL_PORT=2525 +MAIL_USERNAME=null +MAIL_PASSWORD=null +MAIL_FROM_ADDRESS="hello@example.com" +MAIL_FROM_NAME="${APP_NAME}" + +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_DEFAULT_REGION=us-east-1 +AWS_BUCKET= +AWS_USE_PATH_STYLE_ENDPOINT=false + +VITE_APP_NAME="${APP_NAME}" diff --git a/scripts/pgsql-dev.sh b/scripts/pgsql-dev.sh new file mode 100755 index 0000000..2d1f551 --- /dev/null +++ b/scripts/pgsql-dev.sh @@ -0,0 +1,111 @@ +#!/usr/bin/env bash +# 開発用 PostgreSQL コンテナの起動/停止/破棄 (podman 優先、なければ docker) +# 使い方: scripts/pgsql-dev.sh [start|stop|destroy|status] +set -euo pipefail + +cd "$(dirname "$0")/.." + +RED='\033[0;31m'; GREEN='\033[0;32m'; CYAN='\033[0;36m'; RESET='\033[0m' +step() { echo -e "${CYAN}==> $*${RESET}"; } +ok() { echo -e "${GREEN}OK: $*${RESET}"; } +die() { echo -e "${RED}ERROR: $*${RESET}" >&2; exit 1; } + +CONTAINER_NAME="${PGSQL_CONTAINER_NAME:-quotebuilder-pgsql}" +VOLUME_NAME="${PGSQL_VOLUME_NAME:-quotebuilder-pgsql-data}" +IMAGE="${PGSQL_IMAGE:-docker.io/library/postgres:17-alpine}" +HOST_PORT="${PGSQL_PORT:-5432}" +DB_NAME="${PGSQL_DB:-quotebuilder}" +DB_TEST_NAME="${PGSQL_TEST_DB:-quotebuilder_test}" +DB_USER="${PGSQL_USER:-quotebuilder}" +DB_PASSWORD="${PGSQL_PASSWORD:-quotebuilder}" + +if command -v podman &>/dev/null; then + ENGINE=podman +elif command -v docker &>/dev/null; then + ENGINE=docker +else + die "podman / docker が見つかりません" +fi + +container_exists() { "$ENGINE" container exists "$CONTAINER_NAME" 2>/dev/null || "$ENGINE" inspect "$CONTAINER_NAME" &>/dev/null; } +container_running() { [ "$("$ENGINE" inspect -f '{{.State.Running}}' "$CONTAINER_NAME" 2>/dev/null)" = "true" ]; } + +wait_ready() { + step "PostgreSQL の起動待ち" + for _ in $(seq 1 30); do + if "$ENGINE" exec "$CONTAINER_NAME" pg_isready -U "$DB_USER" -d "$DB_NAME" &>/dev/null; then + ok "PostgreSQL が応答しています (port ${HOST_PORT})" + return 0 + fi + sleep 1 + done + die "PostgreSQL が 30 秒以内に起動しませんでした ($ENGINE logs $CONTAINER_NAME を確認)" +} + +ensure_test_db() { + step "テスト用 DB (${DB_TEST_NAME}) の確認" + if "$ENGINE" exec "$CONTAINER_NAME" psql -U "$DB_USER" -d "$DB_NAME" -tAc \ + "SELECT 1 FROM pg_database WHERE datname = '${DB_TEST_NAME}'" | grep -q 1; then + ok "${DB_TEST_NAME} は既に存在します" + else + "$ENGINE" exec "$CONTAINER_NAME" psql -U "$DB_USER" -d "$DB_NAME" -c \ + "CREATE DATABASE ${DB_TEST_NAME} OWNER ${DB_USER}" >/dev/null + ok "${DB_TEST_NAME} を作成しました" + fi +} + +cmd="${1:-start}" +case "$cmd" in + start) + if container_running; then + ok "コンテナ ${CONTAINER_NAME} は起動済みです" + elif container_exists; then + step "既存コンテナ ${CONTAINER_NAME} を起動" + "$ENGINE" start "$CONTAINER_NAME" >/dev/null + else + step "コンテナ ${CONTAINER_NAME} を作成して起動 (${IMAGE})" + "$ENGINE" run -d \ + --name "$CONTAINER_NAME" \ + -e POSTGRES_DB="$DB_NAME" \ + -e POSTGRES_USER="$DB_USER" \ + -e POSTGRES_PASSWORD="$DB_PASSWORD" \ + -p "${HOST_PORT}:5432" \ + -v "${VOLUME_NAME}:/var/lib/postgresql/data" \ + "$IMAGE" >/dev/null + fi + wait_ready + ensure_test_db + echo "" + echo "接続設定は .env.pgsql.example を参照 (cp .env.pgsql.example .env)" + ;; + stop) + if container_running; then + "$ENGINE" stop "$CONTAINER_NAME" >/dev/null + ok "コンテナ ${CONTAINER_NAME} を停止しました" + else + ok "コンテナ ${CONTAINER_NAME} は起動していません" + fi + ;; + destroy) + if container_exists; then + "$ENGINE" rm -f "$CONTAINER_NAME" >/dev/null + ok "コンテナ ${CONTAINER_NAME} を削除しました" + fi + if "$ENGINE" volume exists "$VOLUME_NAME" 2>/dev/null || "$ENGINE" volume inspect "$VOLUME_NAME" &>/dev/null; then + "$ENGINE" volume rm "$VOLUME_NAME" >/dev/null + ok "ボリューム ${VOLUME_NAME} を削除しました (データは失われます)" + fi + ;; + status) + if container_running; then + ok "コンテナ ${CONTAINER_NAME} は起動中 (port ${HOST_PORT})" + elif container_exists; then + echo "コンテナ ${CONTAINER_NAME} は停止中" + else + echo "コンテナ ${CONTAINER_NAME} は未作成" + fi + ;; + *) + die "不明なサブコマンド: $cmd (start|stop|destroy|status)" + ;; +esac From be12394ba8e25d951860bcf1973a176ac8a55c26 Mon Sep 17 00:00:00 2001 From: "Sotaro.Hikosaka" Date: Sat, 11 Jul 2026 23:03:10 +0900 Subject: [PATCH 3/5] =?UTF-8?q?fix:=20customer=5Fsignatures=E7=A7=BB?= =?UTF-8?q?=E8=A1=8C=E3=81=AE=E9=87=8D=E8=A4=87=E6=A4=9C=E5=87=BA=E3=82=92?= =?UTF-8?q?PostgreSQL=E4=BA=92=E6=8F=9B=E3=81=AB=E4=BF=AE=E6=AD=A3=20(#62)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PostgreSQLはHAVINGでSELECT別名を参照できないため、 having('n','>',1) を havingRaw('count(*) > 1') に変更。 pgsqlでmigrate:fresh/migrate:resetの全件通過を確認、 SQLite側もFeature test 201件で回帰なし。 Co-Authored-By: Claude Fable 5 --- .../2026_07_01_000000_create_customer_signatures_table.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/database/migrations/2026_07_01_000000_create_customer_signatures_table.php b/database/migrations/2026_07_01_000000_create_customer_signatures_table.php index c509b52..1936200 100644 --- a/database/migrations/2026_07_01_000000_create_customer_signatures_table.php +++ b/database/migrations/2026_07_01_000000_create_customer_signatures_table.php @@ -19,7 +19,8 @@ public function up(): void ->whereNotNull('customer_signature') ->where('customer_signature', '!=', '') ->groupBy('sig') - ->having('n', '>', 1) + // PostgreSQL は HAVING で SELECT の別名(n)を参照できないため集約式で書く + ->havingRaw('count(*) > 1') ->pluck('sig'); if ($duplicates->isNotEmpty()) { From bf97bedc0d1a1d5c1dc49a1d4b867b73c9990208 Mon Sep 17 00:00:00 2001 From: "Sotaro.Hikosaka" Date: Sun, 12 Jul 2026 11:59:34 +0900 Subject: [PATCH 4/5] =?UTF-8?q?test:=20PostgreSQL=E5=AE=9F=E8=A1=8C?= =?UTF-8?q?=E7=94=A8=E3=81=AEFeature=20test=E3=83=97=E3=83=AD=E3=83=95?= =?UTF-8?q?=E3=82=A1=E3=82=A4=E3=83=AB=E3=82=92=E8=BF=BD=E5=8A=A0=20(#62)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - phpunit.pgsql.xml: scripts/pgsql-dev.sh の quotebuilder_test へ force接続するプロファイル。composer run test:pgsql で実行 - BackupStorageTest を pgsql プロファイル対応に修正 - acting user を default接続切替前に作成(切替後だと手書き スキーマの一時DBへinsertされ失敗) - 手書き backup_audit_logs を実migrationと同じ列構成に更新 - tearDown で database.default を復元。切替したままだと RefreshDatabase のロールバックが一時SQLiteへ向かい、pgsql側の トランザクション残置で次テストの migrate がロック待ちハング - pgsql / sqlite 両プロファイルで 201件 pass を確認 Co-Authored-By: Claude Fable 5 --- composer.json | 4 +++ phpunit.pgsql.xml | 44 +++++++++++++++++++++++++++++ tests/Feature/BackupStorageTest.php | 15 ++++++++-- 3 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 phpunit.pgsql.xml diff --git a/composer.json b/composer.json index 1b0a51d..892cd87 100644 --- a/composer.json +++ b/composer.json @@ -50,6 +50,10 @@ "@php artisan config:clear --ansi @no_additional_args", "@php artisan test" ], + "test:pgsql": [ + "@php artisan config:clear --ansi @no_additional_args", + "vendor/bin/phpunit -c phpunit.pgsql.xml" + ], "post-autoload-dump": [ "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump", "@php artisan package:discover --ansi" diff --git a/phpunit.pgsql.xml b/phpunit.pgsql.xml new file mode 100644 index 0000000..54ffb44 --- /dev/null +++ b/phpunit.pgsql.xml @@ -0,0 +1,44 @@ + + + + + + tests/Unit + + + tests/Feature + + + + + app + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/Feature/BackupStorageTest.php b/tests/Feature/BackupStorageTest.php index de6555f..56a5ee9 100644 --- a/tests/Feature/BackupStorageTest.php +++ b/tests/Feature/BackupStorageTest.php @@ -19,13 +19,19 @@ class BackupStorageTest extends TestCase private string $databasePath; + private string $originalDefaultConnection; + protected function setUp(): void { parent::setUp(); Storage::fake('backups-test'); config()->set('backup.disk', 'backups-test'); config()->set('backup.directory', 'private-backups'); + // default接続を一時SQLiteへ切り替える前に、migrate済みのプロファイル既定接続で作る + // (切替後だと手書きスキーマの一時DBへinsertされ、pgsqlプロファイルで失敗する)。 + $this->actingAs(User::factory()->create(['role' => 'admin'])); $this->databasePath = sys_get_temp_dir().'/backup-storage-'.bin2hex(random_bytes(6)).'.sqlite'; + $this->originalDefaultConnection = config('database.default'); config()->set('database.default', 'sqlite'); config()->set('database.connections.sqlite.database', $this->databasePath); $database = new SQLite3($this->databasePath); @@ -33,15 +39,20 @@ protected function setUp(): void $database->exec("INSERT INTO migrations VALUES (1, 'create_quotes', 1)"); $database->exec('CREATE TABLE quotes (id INTEGER PRIMARY KEY, title TEXT)'); $database->exec('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT, password TEXT, role TEXT, created_at TEXT, updated_at TEXT)'); - $database->exec('CREATE TABLE backup_audit_logs (id INTEGER PRIMARY KEY, actor_type TEXT, actor_identifier TEXT, action TEXT, target_type TEXT, target_identifier TEXT, outcome TEXT, failure_reason TEXT, created_at TEXT, updated_at TEXT)'); + // 実 migration (backup_audit_logs + note 追加) と同じ列構成にする。pgsql プロファイルでは + // request 中の監査ログ書き込みがこの一時DBへ向かうため、乖離すると 500 になる。 + $database->exec('CREATE TABLE backup_audit_logs (id INTEGER PRIMARY KEY, actor_type TEXT, actor_identifier TEXT, user_id INTEGER, operation TEXT, target TEXT, result TEXT, failure_reason TEXT, note TEXT, created_at TEXT)'); $database->close(); - $this->actingAs(User::factory()->create(['role' => 'admin'])); } protected function tearDown(): void { Carbon::setTestNow(); @unlink($this->databasePath); + // RefreshDatabase のロールバックは「この時点の default 接続」に向かうため、 + // 切り替えたままだとプロファイル既定接続(pgsql等)のトランザクションが残置され、 + // 次のテストの migrate がロック待ちでハングする。必ず元に戻してから破棄する。 + config()->set('database.default', $this->originalDefaultConnection); parent::tearDown(); } From a49cd5bcd71bd96b7ff8fce0146ef05e0e4bfa81 Mon Sep 17 00:00:00 2001 From: "Sotaro.Hikosaka" Date: Sun, 12 Jul 2026 18:16:52 +0900 Subject: [PATCH 5/5] =?UTF-8?q?fix:=20phpunit.pgsql.xml=20=E3=81=AE?= =?UTF-8?q?=E6=8E=A5=E7=B6=9A=E3=83=91=E3=83=A9=E3=83=A1=E3=83=BC=E3=82=BF?= =?UTF-8?q?=E3=82=92=E7=92=B0=E5=A2=83=E5=A4=89=E6=95=B0=E3=81=A7=E4=B8=8A?= =?UTF-8?q?=E6=9B=B8=E3=81=8D=E5=8F=AF=E8=83=BD=E3=81=AB=20(#62)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Copilot レビュー指摘対応。DB_HOST/DB_PORT/DB_USERNAME/DB_PASSWORD の force="true" を外し、CI やローカルで接続先を環境変数で差し替え可能にする。 DB_CONNECTION と DB_DATABASE は誤って開発DBへ向けて RefreshDatabase が 実データを消さないよう force 固定を維持する。 Co-Authored-By: Claude Fable 5 --- phpunit.pgsql.xml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/phpunit.pgsql.xml b/phpunit.pgsql.xml index 54ffb44..ffdccac 100644 --- a/phpunit.pgsql.xml +++ b/phpunit.pgsql.xml @@ -1,7 +1,9 @@ + 使い方: composer run test:pgsql + DB_HOST/DB_PORT/DB_USERNAME/DB_PASSWORD は環境変数で上書き可。 + DB_CONNECTION と DB_DATABASE は誤って開発DBへ向けないよう force で固定する。 --> - - + + - - + +