diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..2f8c46d --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,55 @@ +# Copilot instructions for `akfaiz/migris` + +## Build, test, and lint commands + +- Install deps: `make install` +- Install dev tools (`golangci-lint`, `gotestsum`): `make install-tools` +- Format: `make fmt` +- Lint: `make lint` +- Full test suite: `make test` +- Coverage run: `make testcov` +- CI-parity test run (race + coverage): `go test -v -cover -race ./...` + +Run a single test: + +- By exact test name across packages: `go test ./... -run '^TestUp$' -v` +- In one package/file area: `go test ./schema/grammars -run '^TestMysqlGrammar_CompileCreate$' -v` + +## High-level architecture + +Migris has two layers that work together: + +1. **Migration runner layer (`migris` package root)** + `Migrate` wraps Goose provider usage (`migrate.go`, `up.go`, `down.go`, `reset.go`, `status.go`), wires the selected dialect, registry, migration dir, and dry-run behavior. +2. **Schema builder layer (`schema/`)** + DDL generation follows **Builder → Blueprint → Grammar → SQL execution**: + - `schema/builders/*`: dialect builders orchestrate schema operations + - `schema/blueprint/*`: fluent API collects column/index/command definitions + - `schema/grammars/*`: dialect-specific SQL compilation + - `schema/core/*`: execution context abstraction (`Context`) and dry-run SQL capture + +Go migrations are registered in-memory through `AddMigrationContext` / `AddNamedMigrationContext` (`register.go`), then passed to Goose via `goose.WithGoMigrations(...)`. + +## Key repository-specific conventions + +- **Migration registration is filename-version driven.** + Version uniqueness and ordering come from numeric prefixes in migration source names (`goose.NumericComponent` in `register.go`). Keep migration filenames timestamp-prefixed and snake_case. + +- **Migrations are expected to register in `init()` and be imported for side effects.** + Templates generated by `Create` and examples use this pattern; CLIs import migrations with `_ "path/to/migrations"` so registration happens before execution. + +- **Schema operations should stay inside `schema.Context` (transaction-backed).** + Registered migration functions are wrapped into Goose `RunTx` handlers (`register.go`) that create `schema.NewContext(...)` with dialect/filename metadata. + +- **Blueprint `Change()` preserves unspecified column attributes intentionally.** + `Blueprint.Build()` hydrates existing column metadata and carries forward nullable/default/comment when those commands are not explicitly set (`schema/blueprint/blueprint.go`). + +- **When adding fluent schema features, update all dialect grammars and tests.** + New Blueprint/column modifiers must be implemented across MySQL, MariaDB, Postgres, and SQLite grammar paths, with corresponding grammar tests. + +- **MySQL modifier output order is a compatibility contract.** + Keep modifier order consistent with existing SQL expectations/tests: + `Unsigned`, `Charset`, `Collate`, `VirtualAs`, `StoredAs`, `Nullable`, `Default`, `Increment`, `OnUpdate`, `Comment`, `After`, `First`. + +- **Builder integration tests rely on Testcontainers.** + `schema/builders/*_test.go` spin real DB containers via `internal/testutil/db.go`; local runs need Docker available. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a10608a..f1182ed 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,7 @@ jobs: uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: - go-version: '1.24' + go-version: '1.25' - name: golangci-lint uses: golangci/golangci-lint-action@v8 with: @@ -28,32 +28,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - go-version: ['1.24', '1.25'] - - services: - postgres: - image: postgres:16-alpine - ports: - - 5432:5432 - env: - POSTGRES_USER: root - POSTGRES_PASSWORD: password - POSTGRES_DB: db_test - options: >- - --health-cmd pg_isready --health-interval 10s --health-timeout 5s - --health-retries 5 - mysql: - image: mysql:8.0.36 - ports: - - 3306:3306 - env: - MYSQL_ROOT_PASSWORD: password - MYSQL_DATABASE: db_test - options: >- - --health-cmd="mysqladmin ping -ppassword" - --health-interval=10s - --health-timeout=5s - --health-retries=5 + go-version: ['1.25', '1.26'] steps: - name: Checkout code @@ -66,10 +41,6 @@ jobs: run: | go mod download - name: Test - env: - DB_USER: root - DB_PASSWORD: password - DB_NAME: db_test run: | go test -v -cover -race ./... -coverprofile=coverage.txt - name: Upload coverage reports to Codecov diff --git a/.gitignore b/.gitignore index e999d01..671cf1e 100644 --- a/.gitignore +++ b/.gitignore @@ -31,4 +31,4 @@ go.work.sum # Editor/IDE .idea/ .vscode/ -.serena/ \ No newline at end of file +.serena/ diff --git a/.golangci.yml b/.golangci.yml index 5f56d70..e2924c0 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -475,3 +475,7 @@ linters: text: 'shadow' linters: - govet + - path: 'extra/(migriscli|migriscobra)/go\.mod' + text: 'local replacement are not allowed: github.com/akfaiz/migris' + linters: + - gomoddirectives diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..ec9e3c7 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,43 @@ +# Repository Guidelines + +## Project Structure & Module Organization +Core migration APIs live at the repository root (`up.go`, `down.go`, `migrate.go`, `create.go`, `reset.go`, `status.go`). +Schema builders and SQL grammar implementations are in `schema/` (MySQL, PostgreSQL, SQLite). +Internal helper packages are in `internal/` (`config`, `dialect`, `logger`, `parser`, `util`). +Runnable examples are in `examples/` (`basic`, `migriscli`, `migriscobra`), while optional integrations are in `extra/`. +Tests are colocated with source files as `*_test.go`. + +## Build, Test, and Development Commands +Use `make` targets: +- `make install`: download and tidy module dependencies. +- `make install-tools`: install `golangci-lint` and `gotestsum`. +- `make fmt`: run `go fmt ./...`. +- `make lint`: run strict lint checks (`golangci-lint run --timeout=2m --verbose`). +- `make test`: run all tests via `gotestsum`. +- `make testcov`: run tests with coverage output (`coverage.out`). + +For quick checks, `go test ./...` is acceptable, but prefer `make test` in PR validation. + +## Coding Style & Naming Conventions +Target Go `1.24` (see `go.mod`). Follow idiomatic Go and keep exported APIs stable. +Formatting is required before commit (`go fmt` and linter formatters like `goimports`/`golines` are enforced). +Use descriptive CamelCase for exported identifiers and short, clear lowerCamelCase for locals. +Migration filenames in examples follow timestamped snake case (for example, `20250904164848_create_users_table.go`). + +## Testing Guidelines +Use Go’s `testing` package with `testify` assertions where helpful. +Name tests clearly with `TestXxx` and colocate them with implementation. +Run `make test` locally before opening a PR; use `make testcov` when changing migration logic, schema builders, or SQL grammar behavior. +Add regression tests for bug fixes. + +## Commit & Pull Request Guidelines +Commit messages must follow Conventional Commits, enforced by `lefthook`: +`feat|fix|docs|style|refactor|test|chore(scope): summary`. +Examples from history include `feat: add sqlite3 support` and `fix: ...`. + +Before pushing, run: `make fmt && make lint && make test`. +PRs should include: +- A concise change summary and motivation. +- Linked issue(s) when applicable. +- Notes on behavior changes and test coverage updates. +- Example CLI output or screenshots only when UX/output changed. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 5e2f77e..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,100 +0,0 @@ -# Contributing to \[Migris] - -Thank you for considering contributing to this Go library! We welcome contributions of all kinds—bug reports, feature requests, documentation updates, tests, and code improvements. - -Please take a moment to review this guide before submitting an issue or pull request. - -## 🚀 Getting Started - -1. **Fork** the repository. - -2. **Clone** your fork: - - ```bash - git clone https://github.com/your-username/your-library.git - cd your-library - ``` - -3. **Install dependencies**: - - ```bash - go mod tidy - ``` - -4. **Run tests** to ensure everything is working: - - ```bash - go test ./... - ``` - -## 🧑‍💻 Development Guidelines - -* Go version: \[Specify Go version, e.g., `1.24+`] -* Use idiomatic Go and follow the official [Effective Go](https://golang.org/doc/effective_go.html) guide. -* All code should be tested. We use [Go's testing package](https://golang.org/pkg/testing/) and optionally [Testify](https://github.com/stretchr/testify). -* Keep the API stable and backward compatible when possible. - -## 🤝 How to Contribute - -### 🐛 Reporting Bugs - -* Use the [issue tracker](https://github.com/your-org/your-library/issues) to report bugs. -* Include steps to reproduce, expected behavior, and actual behavior. - -### 💡 Suggesting Enhancements - -* Propose enhancements via issues before working on a large feature. -* Provide context, use cases, and possible implementation ideas. - -### ✅ Submitting a Pull Request - -1. Create a new branch from `main`: - - ```bash - git checkout -b feat/your-feature-name - ``` - -2. Make your changes. - -3. Run tests and format your code: - - ```bash - go fmt ./... - go test ./... - ``` - -4. Commit with a clear message: - - ```bash - git commit -m "feat: add support for XYZ" - ``` - -5. Push and open a PR against the `main` branch. - -6. Follow the PR template and reference any related issues. - -## 🧼 Code Style - -* Run `go fmt ./...` before submitting. -* Use descriptive variable and function names. -* Use `golangci-lint` or similar tools to catch linting issues. - -## 🧪 Running Tests - -Use the standard testing command: - -```bash -go test -v ./... -``` - -To run with coverage: - -```bash -go test -cover ./... -``` - -## 📄 License - -By contributing, you agree that your contributions will be licensed under the [MIT License](LICENSE) of the project. - -Thank you again for your support! 🙌 diff --git a/GEMINI.md b/GEMINI.md new file mode 100644 index 0000000..fbe269e --- /dev/null +++ b/GEMINI.md @@ -0,0 +1,48 @@ +# Migris Project Instructions + +Migris is a database migration library for Go, inspired by Laravel's migrations. It combines the power of [pressly/goose](https://github.com/pressly/goose) with a fluent schema builder. + +## Project Overview + +- **Purpose:** Provide a fluent, Laravel-like API for defining and managing database schemas in Go. +- **Main Technologies:** Go (1.25+), [goose](https://github.com/pressly/goose), MySQL, PostgreSQL (pgx), SQLite3. +- **Architecture:** + - `schema/`: The core Schema Builder API. It uses a **Builder -> Blueprint -> Grammar -> DDL** pipeline. + - `Blueprint`: Collects column and index definitions. + - `Grammar`: Translates Blueprint definitions into dialect-specific SQL (DDL). + - `Builder`: Orchestrates the creation and modification of tables. + - `internal/`: Internal components including dialect management, logging, and configuration. + - `extra/`: Integration helpers for popular CLI frameworks like `Cobra` and `urfave/cli`. + - Root: High-level migration management (`Migrate`, `Registry`). + +## Building and Running + +The project uses a `Makefile` for common development tasks: + +- **Install Dependencies:** `make install` +- **Format Code:** `make fmt` +- **Lint Code:** `make lint` or `make lint-fix` +- **Run Tests:** `make test` or `go test ./...` +- **Generate Coverage:** `make testcov` or `make testcov-html` + +## Development Conventions + +- **Code Style:** Use idiomatic Go and follow [Effective Go](https://golang.org/doc/effective_go.html). Run `go fmt` before submitting. +- **Testing:** + - ALWAYS add or update tests when making changes. + - Grammar implementations (e.g., `mysql_grammar.go`) MUST have corresponding tests (e.g., `mysql_grammar_test.go`) to verify DDL generation. + - Use `github.com/stretchr/testify` for assertions. +- **Grammar Implementation:** + - When adding new fluent modifiers to `Blueprint`, ensure they are implemented in all supported grammars (MySQL, Postgres, SQLite). + - Maintain consistent modifier order in DDL generation to match established conventions and existing tests. + - Standard MySQL modifier order: `Unsigned`, `Charset`, `Collate`, `VirtualAs`, `StoredAs`, `Nullable`, `Default`, `Increment`, `OnUpdate`, `Comment`, `After`, `First`. +- **Backward Compatibility:** Keep the public API stable. Avoid breaking changes unless absolutely necessary. +- **Transaction Safety:** Ensure all schema operations are performed within the provided `schema.Context` (which typically wraps a database transaction). + +## Key Files + +- `schema/blueprint.go`: The central place for defining the fluent API. +- `schema/grammar.go`: The interface for dialect-specific SQL generation. +- `schema/column_definition.go`: Defines the attributes and modifiers for table columns. +- `migrate.go`: The main entry point for running migrations. +- `internal/dialect/dialect.go`: Handles database dialect identification and mapping to Goose dialects. diff --git a/Makefile b/Makefile index 92edeae..7fb6860 100644 --- a/Makefile +++ b/Makefile @@ -2,27 +2,46 @@ COVERAGE_FILE := coverage.out COVERAGE_HTML := coverage.html FORMAT ?= dots +VERSION ?= +PUSH ?= false +VERSION_STRIPPED := $(patsubst v%,%,$(VERSION)) +VERSION_TAG := v$(VERSION_STRIPPED) +BLUE := \033[0;34m +YELLOW := \033[1;33m +NC := \033[0m + +ROOT_MODULE := . +EXTRA_MODULES := extra/migriscli extra/migriscobra +MONOREPO_MODULES := $(ROOT_MODULE) $(EXTRA_MODULES) +RELEASE_TAGS := $(VERSION_TAG) extra/migriscli/$(VERSION_TAG) extra/migriscobra/$(VERSION_TAG) + +define run_in_modules + @set -e; \ + for mod in $(MONOREPO_MODULES); do \ + echo "==> [$${mod}] $(1)"; \ + (cd "$$mod" && $(2)); \ + done +endef .PHONY: fmt fmt: # Format code - @echo "Formatting code..." - @go fmt ./... + $(call run_in_modules,Formatting code...,go fmt ./...) .PHONY: lint lint: # Lint code - @echo "Linting code..." - @golangci-lint run --timeout=2m --verbose + $(call run_in_modules,Linting code...,golangci-lint run --timeout=2m --verbose) .PHONY: lint-fix lint-fix: # Fix lint issues - @echo "Fixing lint issues..." - @golangci-lint run --fix --timeout=2m --verbose + $(call run_in_modules,Fixing lint issues...,golangci-lint run --fix --timeout=2m --verbose) .PHONY: install install: # Install dependencies - @echo "Installing dependencies..." - @go mod download - @go mod tidy + $(call run_in_modules,Installing dependencies...,go mod download && go mod tidy) + +.PHONY: tidy +tidy: # Run go mod tidy in all modules + $(call run_in_modules,Tidying modules...,go mod tidy) install-tools: # Install tools @echo "Installing tools..." @@ -31,14 +50,16 @@ install-tools: # Install tools .PHONY: test test: # Run tests - @echo "Running tests..." - @gotestsum --format=$(FORMAT) -- ./... + $(call run_in_modules,Running tests...,gotestsum --format=$(FORMAT) -- ./...) .PHONY: testcov testcov: # Run tests with coverage - @echo "Running tests with coverage..." - @gotestsum --format=$(FORMAT) -- -coverprofile=$(COVERAGE_FILE) ./... - @echo "Total coverage is: " $(shell go tool cover -func=$(COVERAGE_FILE) | grep total | awk '{print $$3}') + @set -e; \ + for mod in $(MONOREPO_MODULES); do \ + echo "==> [$${mod}] Running tests with coverage..."; \ + (cd "$$mod" && gotestsum --format=$(FORMAT) -- -coverprofile=$(COVERAGE_FILE) ./...); \ + echo "==> [$${mod}] Total coverage is: $$(cd "$$mod" && go tool cover -func=$(COVERAGE_FILE) | grep total | awk '{print $$3}')"; \ + done .PHONY: testcov-html testcov-html: testcov # Generate coverage HTML report @@ -46,14 +67,229 @@ testcov-html: testcov # Generate coverage HTML report @echo "Coverage HTML report generated: $(COVERAGE_HTML)" @open $(COVERAGE_HTML) +.PHONY: sync-extra-deps +sync-extra-deps: # Sync extra modules to require github.com/akfaiz/migris@$(VERSION_TAG) + @set -e; \ + if [ -z "$(VERSION)" ]; then \ + echo "VERSION is required. Usage: make sync-extra-deps VERSION=0.5.0 (or v0.5.0)"; \ + exit 1; \ + fi; \ + if ! printf '%s' "$(VERSION_STRIPPED)" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$$'; then \ + echo "VERSION must be SemVer (example: 0.5.0 or v0.5.0)"; \ + exit 1; \ + fi; \ + for mod in $(EXTRA_MODULES); do \ + echo "Syncing $$mod to github.com/akfaiz/migris@$(VERSION_TAG)"; \ + (cd "$$mod" && go mod edit -require=github.com/akfaiz/migris@$(VERSION_TAG) && go mod tidy); \ + done + +.PHONY: release-core-preflight +release-core-preflight: # Validate core tag release prerequisites + @set -e; \ + if [ -z "$(VERSION)" ]; then \ + echo "VERSION is required. Usage: make release-core VERSION=0.5.0 (or v0.5.0)"; \ + exit 1; \ + fi; \ + if ! printf '%s' "$(VERSION_STRIPPED)" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$$'; then \ + echo "VERSION must be SemVer (example: 0.5.0 or v0.5.0)"; \ + exit 1; \ + fi; \ + if [ -n "$$(git status --porcelain)" ]; then \ + echo "Working tree must be clean before release."; \ + exit 1; \ + fi; \ + if ! git rev-parse --abbrev-ref --symbolic-full-name @{u} >/dev/null 2>&1; then \ + echo "No upstream branch configured."; \ + exit 1; \ + fi; \ + if [ "$$(git rev-list --count @{u}..HEAD)" -ne 0 ]; then \ + echo "You have unpushed commits. Push before release."; \ + exit 1; \ + fi; \ + if git rev-parse -q --verify "refs/tags/$(VERSION_TAG)" >/dev/null; then \ + echo "Tag already exists locally: $(VERSION_TAG)"; \ + exit 1; \ + fi; \ + if git ls-remote --exit-code --tags origin "refs/tags/$(VERSION_TAG)" >/dev/null 2>&1; then \ + echo "Tag already exists on origin: $(VERSION_TAG)"; \ + exit 1; \ + fi + +.PHONY: release-core-publish +release-core-publish: release-core-preflight # Create and push root module tag + @set -e; \ + echo "Creating core tag $(VERSION_TAG)"; \ + git tag -a "$(VERSION_TAG)" -m "release $(VERSION_TAG)"; \ + echo "Pushing core tag $(VERSION_TAG)"; \ + git push origin "$(VERSION_TAG)" + +.PHONY: release-extra-preflight +release-extra-preflight: # Validate extra module release prerequisites + @set -e; \ + if [ -z "$(VERSION)" ]; then \ + echo "VERSION is required. Usage: make release-extra VERSION=0.5.0 (or v0.5.0)"; \ + exit 1; \ + fi; \ + if ! printf '%s' "$(VERSION_STRIPPED)" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$$'; then \ + echo "VERSION must be SemVer (example: 0.5.0 or v0.5.0)"; \ + exit 1; \ + fi; \ + if [ -n "$$(git status --porcelain)" ]; then \ + echo "Working tree must be clean before publishing extra module tags."; \ + exit 1; \ + fi; \ + if ! git rev-parse --abbrev-ref --symbolic-full-name @{u} >/dev/null 2>&1; then \ + echo "No upstream branch configured."; \ + exit 1; \ + fi; \ + if [ "$$(git rev-list --count @{u}..HEAD)" -ne 0 ]; then \ + echo "You have unpushed commits. Push before publishing extra tags."; \ + exit 1; \ + fi; \ + for mod in $(EXTRA_MODULES); do \ + current="$$(cd "$$mod" && go list -m -f '{{.Version}}' github.com/akfaiz/migris)"; \ + if [ "$$current" != "$(VERSION_TAG)" ]; then \ + echo "$$mod requires github.com/akfaiz/migris@$$current (expected $(VERSION_TAG))."; \ + echo "Run: make sync-extra-deps VERSION=$(VERSION_TAG)"; \ + exit 1; \ + fi; \ + done; \ + for tag in extra/migriscli/$(VERSION_TAG) extra/migriscobra/$(VERSION_TAG); do \ + if git rev-parse -q --verify "refs/tags/$$tag" >/dev/null; then \ + echo "Tag already exists locally: $$tag"; \ + exit 1; \ + fi; \ + if git ls-remote --exit-code --tags origin "refs/tags/$$tag" >/dev/null 2>&1; then \ + echo "Tag already exists on origin: $$tag"; \ + exit 1; \ + fi; \ + done + +.PHONY: release-extra-publish +release-extra-publish: release-extra-preflight # Create and push extra module tags + @set -e; \ + tags="extra/migriscli/$(VERSION_TAG) extra/migriscobra/$(VERSION_TAG)"; \ + for tag in $$tags; do \ + echo "Creating extra tag $$tag"; \ + git tag -a "$$tag" -m "release $$tag"; \ + done; \ + echo "Pushing extra tags"; \ + git push origin $$tags + +.PHONY: release-core +release-core: # Stage 1: publish root tag and sync extra deps + @set -e; \ + $(MAKE) release-core-publish VERSION=$(VERSION_TAG); \ + $(MAKE) sync-extra-deps VERSION=$(VERSION_TAG); \ + echo "Stage 1 complete."; \ + echo "Commit and push extra dependency updates, then run:"; \ + echo " make release-extra VERSION=$(VERSION_TAG)" + +.PHONY: release-extra +release-extra: # Stage 2: publish extra module tags after sync commit is pushed + @set -e; \ + $(MAKE) release-extra-publish VERSION=$(VERSION_TAG) + +.PHONY: release-dry-run +release-dry-run: # Dry-run for staged monorepo release + @set -e; \ + if [ -z "$(VERSION)" ]; then \ + echo "VERSION is required. Usage: make release-dry-run VERSION=0.5.0 (or v0.5.0)"; \ + exit 1; \ + fi; \ + if ! printf '%s' "$(VERSION_STRIPPED)" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$$'; then \ + echo "VERSION must be SemVer (example: 0.5.0 or v0.5.0)"; \ + exit 1; \ + fi; \ + echo "Stage 1/2 (core tag): $(VERSION_TAG)"; \ + if git rev-parse -q --verify "refs/tags/$(VERSION_TAG)" >/dev/null; then \ + echo " local: conflict (exists)"; \ + else \ + echo " local: ok"; \ + fi; \ + if git ls-remote --exit-code --tags origin "refs/tags/$(VERSION_TAG)" >/dev/null 2>&1; then \ + echo " remote: conflict (exists)"; \ + else \ + echo " remote: ok"; \ + fi; \ + echo "Stage 1/2 also syncs extra deps to $(VERSION_TAG)"; \ + for mod in $(EXTRA_MODULES); do \ + current="$$(cd "$$mod" && go list -m -f '{{.Version}}' github.com/akfaiz/migris)"; \ + if [ "$$current" = "$(VERSION_TAG)" ]; then \ + echo " [ok] $$mod -> $$current"; \ + else \ + echo " [diff] $$mod -> $$current (expected $(VERSION_TAG))"; \ + fi; \ + done; \ + echo "Stage 2/2 (extra tags):"; \ + for tag in extra/migriscli/$(VERSION_TAG) extra/migriscobra/$(VERSION_TAG); do \ + local_status="ok"; \ + remote_status="ok"; \ + if git rev-parse -q --verify "refs/tags/$$tag" >/dev/null; then \ + local_status="conflict"; \ + fi; \ + if git ls-remote --exit-code --tags origin "refs/tags/$$tag" >/dev/null 2>&1; then \ + remote_status="conflict"; \ + fi; \ + echo " $$tag (local=$$local_status, remote=$$remote_status)"; \ + done; \ + echo "Flow:"; \ + echo " 1) make release-core VERSION=$(VERSION_TAG)"; \ + echo " 2) commit + push updated extra/go.mod and go.sum"; \ + echo " 3) make release-extra VERSION=$(VERSION_TAG)" + +.PHONY: delete-tag +delete-tag: # Delete a tag locally and from origin (TAG=v0.5.0) + @set -e; \ + if [ -z "$(TAG)" ]; then \ + echo "TAG is required. Usage: make delete-tag TAG=v0.5.0"; \ + exit 1; \ + fi; \ + local_exists=false; \ + remote_exists=false; \ + if git rev-parse -q --verify "refs/tags/$(TAG)" >/dev/null; then \ + local_exists=true; \ + fi; \ + if git ls-remote --exit-code --tags origin "refs/tags/$(TAG)" >/dev/null 2>&1; then \ + remote_exists=true; \ + fi; \ + if [ "$$local_exists" = "false" ] && [ "$$remote_exists" = "false" ]; then \ + echo "Tag not found locally or on origin: $(TAG)"; \ + exit 1; \ + fi; \ + if [ "$$local_exists" = "true" ]; then \ + echo "Deleting local tag: $(TAG)"; \ + git tag -d "$(TAG)"; \ + fi; \ + if [ "$$remote_exists" = "true" ]; then \ + echo "Deleting remote tag: $(TAG)"; \ + git push origin ":refs/tags/$(TAG)"; \ + fi + .PHONY: help help: - @echo "Available commands:" - @echo " fmt - Format code" - @echo " lint - Lint code" - @echo " install - Install dependencies" - @echo " install-tools- Install development tools" - @echo " test - Run tests" - @echo " testcov - Run tests with coverage" - @echo " testcov-html - Generate coverage HTML report" - @echo " help - Show this help message" + @printf "$(BLUE)Available commands$(NC)\n\n" + @printf "$(YELLOW)%-24s$(NC) %s\n" "fmt" "Format code" + @printf "$(YELLOW)%-24s$(NC) %s\n" "lint" "Lint code" + @printf "$(YELLOW)%-24s$(NC) %s\n" "lint-fix" "Fix lint issues" + @printf "$(YELLOW)%-24s$(NC) %s\n" "install" "Install dependencies" + @printf "$(YELLOW)%-24s$(NC) %s\n" "tidy" "Run go mod tidy in all modules" + @printf "$(YELLOW)%-24s$(NC) %s\n" "install-tools" "Install development tools" + @printf "$(YELLOW)%-24s$(NC) %s\n" "test" "Run tests" + @printf "$(YELLOW)%-24s$(NC) %s\n" "testcov" "Run tests with coverage" + @printf "$(YELLOW)%-24s$(NC) %s\n" "testcov-html" "Generate coverage HTML report" + @printf "\n$(BLUE)Release flow$(NC)\n\n" + @printf "$(YELLOW)%-24s$(NC) %s\n" "sync-extra-deps" "Sync extra module dep to root release version" + @printf "$(YELLOW)%-24s$(NC) %s\n" "release-core-preflight" "Validate root tag release prerequisites" + @printf "$(YELLOW)%-24s$(NC) %s\n" "release-core-publish" "Create and push root module tag" + @printf "$(YELLOW)%-24s$(NC) %s\n" "release-extra-preflight" "Validate extra tag release prerequisites" + @printf "$(YELLOW)%-24s$(NC) %s\n" "release-extra-publish" "Create and push extra module tags" + @printf "$(YELLOW)%-24s$(NC) %s\n" "release-core" "Stage 1: publish root tag and sync extra deps" + @printf "$(YELLOW)%-24s$(NC) %s\n" "release-extra" "Stage 2: publish extra module tags" + @printf "$(YELLOW)%-24s$(NC) %s\n" "release-dry-run" "Dry-run for staged release flow" + @printf "$(YELLOW)%-24s$(NC) %s\n" "delete-tag TAG=v0.5.0" "Delete local/remote tag by name" + @printf "\n$(BLUE)Examples$(NC)\n\n" + @printf " make release-core VERSION=0.5.0\n" + @printf " make release-extra VERSION=0.5.0\n" + @printf " make release-dry-run VERSION=v0.5.0\n" + @printf "\n$(YELLOW)%-24s$(NC) %s\n" "help" "Show this help message" diff --git a/README.md b/README.md index 585272c..1859a27 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ - **Migration management** - Run up, down, reset, status, and create operations - **Dry-run mode** - Preview migrations without executing them to see generated SQL - **Fluent schema builder** - Laravel-inspired API for defining database schemas -- **Multi-database support** - Works with PostgreSQL, MySQL, and MariaDB +- **Multi-database support** - Works with PostgreSQL, MySQL, MariaDB, and SQLite3 - **Transaction safety** - All migrations run within database transactions - **Native Go integration** - No external CLI tools required @@ -202,10 +202,7 @@ Currently supported databases: - **PostgreSQL** (via pgx driver) - **MySQL** - **MariaDB** - -## Roadmap - -- [ ] Advanced schema introspection +- **SQLite3** ## Contributing diff --git a/create_test.go b/create_test.go index 28c5ebf..fa8cbbe 100644 --- a/create_test.go +++ b/create_test.go @@ -1,6 +1,7 @@ package migris_test import ( + "database/sql" "os" "path/filepath" "testing" @@ -79,3 +80,26 @@ func TestCreate_InvalidDirectory(t *testing.T) { err := migris.Create("/nonexistent/invalid/path", "test_migration") assert.Error(t, err, "Create() should return error for invalid directory") } + +func TestMigrate_Create(t *testing.T) { + tmpDir := t.TempDir() + + db, err := sql.Open("sqlite3", ":memory:") + require.NoError(t, err) + defer db.Close() + + m, err := migris.New("sqlite3", migris.WithDB(db), migris.WithMigrationDir(tmpDir)) + require.NoError(t, err) + + err = m.Create("create_migrate_users_table") + require.NoError(t, err) + + entries, err := os.ReadDir(tmpDir) + require.NoError(t, err, "failed to read temp dir") + assert.NotEmpty(t, entries, "expected migration file to be created by Migrate.Create") + + filename := filepath.Join(tmpDir, entries[0].Name()) + content, err := os.ReadFile(filename) + require.NoError(t, err, "failed to read migration file") + assert.Contains(t, string(content), "schema.Create") +} diff --git a/docker-compose.yaml b/docker-compose.yaml deleted file mode 100644 index 0dcddd2..0000000 --- a/docker-compose.yaml +++ /dev/null @@ -1,31 +0,0 @@ -version: '3.8' - -services: - postgres: - image: postgres:16-alpine - container_name: postgres_test - ports: - - "5432:5432" - environment: - POSTGRES_USER: root - POSTGRES_PASSWORD: password - POSTGRES_DB: db_test - healthcheck: - test: ["CMD", "pg_isready"] - interval: 10s - timeout: 5s - retries: 5 - - mysql: - image: mysql:8.0.36 - container_name: mysql_test - ports: - - "3306:3306" - environment: - MYSQL_ROOT_PASSWORD: password - MYSQL_DATABASE: db_test - healthcheck: - test: ["CMD", "mysqladmin", "ping", "-ppassword"] - interval: 10s - timeout: 5s - retries: 5 \ No newline at end of file diff --git a/down.go b/down.go index e6c0ab4..1f4aced 100644 --- a/down.go +++ b/down.go @@ -136,6 +136,7 @@ func (m *Migrate) determineMigrationsToRollback(version, currentVersion int64) [ if version == -1 { // Rollback last applied migration only + registeredMigrations := m.registry.migrationsSnapshot() for i := len(registeredMigrations) - 1; i >= 0; i-- { migration := registeredMigrations[i] if migration.version <= currentVersion { @@ -145,6 +146,7 @@ func (m *Migrate) determineMigrationsToRollback(version, currentVersion int64) [ } } else { // Rollback migrations down to specified version (only applied ones) + registeredMigrations := m.registry.migrationsSnapshot() for i := len(registeredMigrations) - 1; i >= 0; i-- { migration := registeredMigrations[i] if migration.version > version && migration.version <= currentVersion { diff --git a/dryrun_state.go b/dryrun_state.go deleted file mode 100644 index cbe879e..0000000 --- a/dryrun_state.go +++ /dev/null @@ -1,26 +0,0 @@ -package migris - -import ( - "sync" -) - -var ( - // Global state for dry-run configuration - // This is used by migration contexts to determine if they should use dry-run mode. - globalDryRunMu sync.RWMutex - globalDryRunState bool -) - -// setGlobalDryRunState sets the global dry-run state. -func setGlobalDryRunState(enabled bool) { - globalDryRunMu.Lock() - defer globalDryRunMu.Unlock() - globalDryRunState = enabled -} - -// getGlobalDryRunState returns the current global dry-run state. -func getGlobalDryRunState() bool { - globalDryRunMu.RLock() - defer globalDryRunMu.RUnlock() - return globalDryRunState -} diff --git a/examples/basic/README.MD b/examples/basic/README.MD index 814872d..4cbae6c 100644 --- a/examples/basic/README.MD +++ b/examples/basic/README.MD @@ -1,6 +1,18 @@ # Migris Example This is a basic example of using Migris for database migrations in Go. +Start PostgreSQL with Docker Compose: + +```bash +docker compose up -d +``` + +Create `.env`: + +```bash +DATABASE_URL=postgres://root:password@localhost:5432/db_test?sslmode=disable +``` + ```bash go build -o migris ``` @@ -46,4 +58,4 @@ $ ./migris status 20250904165150_create_posts_table.go ................................... Applied 20250904165317_create_comments_table.go ................................ Applied 20250904165421_insert_data.go .......................................... Applied -``` \ No newline at end of file +``` diff --git a/examples/basic/docker-compose.yaml b/examples/basic/docker-compose.yaml new file mode 100644 index 0000000..ee79915 --- /dev/null +++ b/examples/basic/docker-compose.yaml @@ -0,0 +1,17 @@ +version: "3.8" + +services: + postgres: + image: postgres:16-alpine + container_name: migris_example_postgres + ports: + - "5432:5432" + environment: + POSTGRES_USER: root + POSTGRES_PASSWORD: password + POSTGRES_DB: db_test + healthcheck: + test: ["CMD-SHELL", "pg_isready -U root -d db_test"] + interval: 10s + timeout: 5s + retries: 5 diff --git a/examples/basic/go.mod b/examples/basic/go.mod index c5119b8..9dc1d0c 100644 --- a/examples/basic/go.mod +++ b/examples/basic/go.mod @@ -1,10 +1,10 @@ module github.com/akfaiz/migris/examples/basic -go 1.24.0 +go 1.25.7 require ( github.com/akfaiz/migris v0.0.0 - github.com/jackc/pgx/v5 v5.8.0 + github.com/jackc/pgx/v5 v5.9.2 github.com/joho/godotenv v1.5.1 github.com/urfave/cli/v3 v3.4.1 ) @@ -15,15 +15,15 @@ require ( github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-isatty v0.0.21 // indirect github.com/mfridman/interpolate v0.0.2 // indirect - github.com/pressly/goose/v3 v3.26.0 // indirect + github.com/pressly/goose/v3 v3.27.1 // indirect github.com/sethvargo/go-retry v0.3.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/sync v0.19.0 // indirect - golang.org/x/sys v0.35.0 // indirect - golang.org/x/term v0.34.0 // indirect - golang.org/x/text v0.33.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.43.0 // indirect + golang.org/x/term v0.42.0 // indirect + golang.org/x/text v0.36.0 // indirect ) replace github.com/akfaiz/migris => ../.. diff --git a/examples/basic/go.sum b/examples/basic/go.sum index b6aefdf..e35affd 100644 --- a/examples/basic/go.sum +++ b/examples/basic/go.sum @@ -1,12 +1,48 @@ -filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= -filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= +filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= +filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c= +github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU= +github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -15,59 +51,118 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo= -github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw= +github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw= +github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= +github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= +github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs= +github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= +github.com/mattn/go-sqlite3 v1.14.33 h1:A5blZ5ulQo2AtayQ9/limgHEkFreKj1Dv226a1K73s0= +github.com/mattn/go-sqlite3 v1.14.33/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY= github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg= -github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= -github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8= +github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU= +github.com/moby/moby/api v1.54.2 h1:wiat9QAhnDQjA7wk1kh/TqHz2I1uUA7M7t9SAl/JNXg= +github.com/moby/moby/api v1.54.2/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs= +github.com/moby/moby/client v0.4.1 h1:DMQgisVoMkmMs7fp3ROSdiBnoAu8+vo3GggFl06M/wY= +github.com/moby/moby/client v0.4.1/go.mod h1:z52C9O2POPOsnxZAy//WtKcQ32P+jT/NGeXu/7nfjGQ= +github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U= +github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= +github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= +github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= +github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= +github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pressly/goose/v3 v3.26.0 h1:KJakav68jdH0WDvoAcj8+n61WqOIaPGgH0bJWS6jpmM= -github.com/pressly/goose/v3 v3.26.0/go.mod h1:4hC1KrritdCxtuFsqgs1R4AU5bWtTAf+cnWvfhf2DNY= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/pressly/goose/v3 v3.27.1 h1:6uEvcprBybDmW4hcz3gYujhARhye+GoWKhEWyzD5sh4= +github.com/pressly/goose/v3 v3.27.1/go.mod h1:maruOxsPnIG2yHHyo8UqKWXYKFcH7Q76csUV7+7KYoM= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE= github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas= +github.com/shirou/gopsutil/v4 v4.26.3 h1:2ESdQt90yU3oXF/CdOlRCJxrP+Am1aBYubTMTfxJ1qc= +github.com/shirou/gopsutil/v4 v4.26.3/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= +github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/testcontainers/testcontainers-go v0.42.0 h1:He3IhTzTZOygSXLJPMX7n44XtK+qhjat1nI9cneBbUY= +github.com/testcontainers/testcontainers-go v0.42.0/go.mod h1:vZjdY1YmUA1qEForxOIOazfsrdyORJAbhi0bp8plN30= +github.com/testcontainers/testcontainers-go/modules/mariadb v0.42.0 h1:ZfWUJSIDnbNgoLAXMV1fc7lqcxBIX3zdnhwjaVUo7N0= +github.com/testcontainers/testcontainers-go/modules/mariadb v0.42.0/go.mod h1:0kV+yHee7zAgp0yccydxjNnHvlC1EOavTLCeg/lnRBY= +github.com/testcontainers/testcontainers-go/modules/mysql v0.42.0 h1:Yhv1k7vDpyzZePntg5R5Oj4ZMCyWpAfpJeRu1ROsgiU= +github.com/testcontainers/testcontainers-go/modules/mysql v0.42.0/go.mod h1:Z7SCTuiZlghAdRjkv3Ir0iXJKC2T2avbtxLR0DRe+ng= +github.com/testcontainers/testcontainers-go/modules/postgres v0.42.0 h1:GCbb1ndrF7OTDiIvxXyItaDab4qkzTFJ48LKFdM7EIo= +github.com/testcontainers/testcontainers-go/modules/postgres v0.42.0/go.mod h1:IRPBaI8jXdrNfD0e4Zm7Fbcgaz5shKxOQv4axiL09xs= +github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA= +github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= +github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw= +github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ= github.com/urfave/cli/v3 v3.4.1 h1:1M9UOCy5bLmGnuu1yn3t3CB4rG79Rtoxuv1sPhnm6qM= github.com/urfave/cli/v3 v3.4.1/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= -golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= -golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= +golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= -golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= -golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= -golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= -golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= +golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= +golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ= -modernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8= +modernc.org/libc v1.72.1 h1:db1xwJ6u1kE3KHTFTTbe2GCrczHPKzlURP0aDC4NGD0= +modernc.org/libc v1.72.1/go.mod h1:HRMiC/PhPGLIPM7GzAFCbI+oSgE3dhZ8FWftmRrHVlY= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= -modernc.org/sqlite v1.38.2 h1:Aclu7+tgjgcQVShZqim41Bbw9Cho0y/7WzYptXqkEek= -modernc.org/sqlite v1.38.2/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E= +modernc.org/sqlite v1.49.1 h1:dYGHTKcX1sJ+EQDnUzvz4TJ5GbuvhNJa8Fg6ElGx73U= +modernc.org/sqlite v1.49.1/go.mod h1:m0w8xhwYUVY3H6pSDwc3gkJ/irZT/0YEXwBlhaxQEew= diff --git a/examples/migriscli/go.mod b/examples/migriscli/go.mod index d9ac7d2..b746754 100644 --- a/examples/migriscli/go.mod +++ b/examples/migriscli/go.mod @@ -1,11 +1,11 @@ module github.com/akfaiz/migris/examples/migriscli -go 1.24.0 +go 1.25.7 require ( github.com/akfaiz/migris v0.4.0 github.com/akfaiz/migris/extra/migriscli v0.0.0 - github.com/jackc/pgx/v5 v5.8.0 + github.com/jackc/pgx/v5 v5.9.2 github.com/joho/godotenv v1.5.1 ) @@ -15,16 +15,16 @@ require ( github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/mattn/go-colorable v0.1.14 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-isatty v0.0.21 // indirect github.com/mfridman/interpolate v0.0.2 // indirect - github.com/pressly/goose/v3 v3.26.0 // indirect + github.com/pressly/goose/v3 v3.27.1 // indirect github.com/sethvargo/go-retry v0.3.0 // indirect github.com/urfave/cli/v3 v3.6.1 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/sync v0.19.0 // indirect - golang.org/x/sys v0.40.0 // indirect - golang.org/x/term v0.39.0 // indirect - golang.org/x/text v0.33.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.43.0 // indirect + golang.org/x/term v0.42.0 // indirect + golang.org/x/text v0.36.0 // indirect ) replace github.com/akfaiz/migris => ../.. diff --git a/examples/migriscli/go.sum b/examples/migriscli/go.sum index c142b9c..a616f54 100644 --- a/examples/migriscli/go.sum +++ b/examples/migriscli/go.sum @@ -1,12 +1,48 @@ -filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= -filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= +filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= +filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c= +github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU= +github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -15,57 +51,116 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo= -github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw= +github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw= +github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= +github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= +github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs= +github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= +github.com/mattn/go-sqlite3 v1.14.33 h1:A5blZ5ulQo2AtayQ9/limgHEkFreKj1Dv226a1K73s0= +github.com/mattn/go-sqlite3 v1.14.33/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY= github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg= -github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= -github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8= +github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU= +github.com/moby/moby/api v1.54.2 h1:wiat9QAhnDQjA7wk1kh/TqHz2I1uUA7M7t9SAl/JNXg= +github.com/moby/moby/api v1.54.2/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs= +github.com/moby/moby/client v0.4.1 h1:DMQgisVoMkmMs7fp3ROSdiBnoAu8+vo3GggFl06M/wY= +github.com/moby/moby/client v0.4.1/go.mod h1:z52C9O2POPOsnxZAy//WtKcQ32P+jT/NGeXu/7nfjGQ= +github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U= +github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= +github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= +github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= +github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= +github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pressly/goose/v3 v3.26.0 h1:KJakav68jdH0WDvoAcj8+n61WqOIaPGgH0bJWS6jpmM= -github.com/pressly/goose/v3 v3.26.0/go.mod h1:4hC1KrritdCxtuFsqgs1R4AU5bWtTAf+cnWvfhf2DNY= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/pressly/goose/v3 v3.27.1 h1:6uEvcprBybDmW4hcz3gYujhARhye+GoWKhEWyzD5sh4= +github.com/pressly/goose/v3 v3.27.1/go.mod h1:maruOxsPnIG2yHHyo8UqKWXYKFcH7Q76csUV7+7KYoM= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE= github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas= +github.com/shirou/gopsutil/v4 v4.26.3 h1:2ESdQt90yU3oXF/CdOlRCJxrP+Am1aBYubTMTfxJ1qc= +github.com/shirou/gopsutil/v4 v4.26.3/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= +github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/testcontainers/testcontainers-go v0.42.0 h1:He3IhTzTZOygSXLJPMX7n44XtK+qhjat1nI9cneBbUY= +github.com/testcontainers/testcontainers-go v0.42.0/go.mod h1:vZjdY1YmUA1qEForxOIOazfsrdyORJAbhi0bp8plN30= +github.com/testcontainers/testcontainers-go/modules/mariadb v0.42.0 h1:ZfWUJSIDnbNgoLAXMV1fc7lqcxBIX3zdnhwjaVUo7N0= +github.com/testcontainers/testcontainers-go/modules/mariadb v0.42.0/go.mod h1:0kV+yHee7zAgp0yccydxjNnHvlC1EOavTLCeg/lnRBY= +github.com/testcontainers/testcontainers-go/modules/mysql v0.42.0 h1:Yhv1k7vDpyzZePntg5R5Oj4ZMCyWpAfpJeRu1ROsgiU= +github.com/testcontainers/testcontainers-go/modules/mysql v0.42.0/go.mod h1:Z7SCTuiZlghAdRjkv3Ir0iXJKC2T2avbtxLR0DRe+ng= +github.com/testcontainers/testcontainers-go/modules/postgres v0.42.0 h1:GCbb1ndrF7OTDiIvxXyItaDab4qkzTFJ48LKFdM7EIo= +github.com/testcontainers/testcontainers-go/modules/postgres v0.42.0/go.mod h1:IRPBaI8jXdrNfD0e4Zm7Fbcgaz5shKxOQv4axiL09xs= +github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA= +github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= +github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw= +github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ= github.com/urfave/cli/v3 v3.6.1 h1:j8Qq8NyUawj/7rTYdBGrxcH7A/j7/G8Q5LhWEW4G3Mo= github.com/urfave/cli/v3 v3.6.1/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= -golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= -golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= -golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= -golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= -golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= -golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= +golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= +golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= +golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ= -modernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8= +modernc.org/libc v1.72.1 h1:db1xwJ6u1kE3KHTFTTbe2GCrczHPKzlURP0aDC4NGD0= +modernc.org/libc v1.72.1/go.mod h1:HRMiC/PhPGLIPM7GzAFCbI+oSgE3dhZ8FWftmRrHVlY= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= -modernc.org/sqlite v1.38.2 h1:Aclu7+tgjgcQVShZqim41Bbw9Cho0y/7WzYptXqkEek= -modernc.org/sqlite v1.38.2/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E= +modernc.org/sqlite v1.49.1 h1:dYGHTKcX1sJ+EQDnUzvz4TJ5GbuvhNJa8Fg6ElGx73U= +modernc.org/sqlite v1.49.1/go.mod h1:m0w8xhwYUVY3H6pSDwc3gkJ/irZT/0YEXwBlhaxQEew= diff --git a/examples/migriscobra/go.mod b/examples/migriscobra/go.mod index c8376ab..cf68788 100644 --- a/examples/migriscobra/go.mod +++ b/examples/migriscobra/go.mod @@ -1,11 +1,11 @@ module github.com/akfaiz/migris/examples/migriscobra -go 1.24.0 +go 1.25.7 require ( github.com/akfaiz/migris v0.4.0 github.com/akfaiz/migris/extra/migriscobra v0.0.0 - github.com/jackc/pgx/v5 v5.8.0 + github.com/jackc/pgx/v5 v5.9.2 github.com/joho/godotenv v1.5.1 ) @@ -16,17 +16,17 @@ require ( github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/mattn/go-colorable v0.1.14 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-isatty v0.0.21 // indirect github.com/mfridman/interpolate v0.0.2 // indirect - github.com/pressly/goose/v3 v3.26.0 // indirect + github.com/pressly/goose/v3 v3.27.1 // indirect github.com/sethvargo/go-retry v0.3.0 // indirect github.com/spf13/cobra v1.8.1 // indirect github.com/spf13/pflag v1.0.5 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/sync v0.19.0 // indirect - golang.org/x/sys v0.40.0 // indirect - golang.org/x/term v0.39.0 // indirect - golang.org/x/text v0.33.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.43.0 // indirect + golang.org/x/term v0.42.0 // indirect + golang.org/x/text v0.36.0 // indirect ) replace github.com/akfaiz/migris => ../.. diff --git a/examples/migriscobra/go.sum b/examples/migriscobra/go.sum index 07579e9..db2f929 100644 --- a/examples/migriscobra/go.sum +++ b/examples/migriscobra/go.sum @@ -1,13 +1,49 @@ -filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= -filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= +filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= +filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c= +github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU= +github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -18,29 +54,65 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo= -github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw= +github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw= +github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= +github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= +github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs= +github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= +github.com/mattn/go-sqlite3 v1.14.33 h1:A5blZ5ulQo2AtayQ9/limgHEkFreKj1Dv226a1K73s0= +github.com/mattn/go-sqlite3 v1.14.33/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY= github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg= -github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= -github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8= +github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU= +github.com/moby/moby/api v1.54.2 h1:wiat9QAhnDQjA7wk1kh/TqHz2I1uUA7M7t9SAl/JNXg= +github.com/moby/moby/api v1.54.2/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs= +github.com/moby/moby/client v0.4.1 h1:DMQgisVoMkmMs7fp3ROSdiBnoAu8+vo3GggFl06M/wY= +github.com/moby/moby/client v0.4.1/go.mod h1:z52C9O2POPOsnxZAy//WtKcQ32P+jT/NGeXu/7nfjGQ= +github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U= +github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= +github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= +github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= +github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= +github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pressly/goose/v3 v3.26.0 h1:KJakav68jdH0WDvoAcj8+n61WqOIaPGgH0bJWS6jpmM= -github.com/pressly/goose/v3 v3.26.0/go.mod h1:4hC1KrritdCxtuFsqgs1R4AU5bWtTAf+cnWvfhf2DNY= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/pressly/goose/v3 v3.27.1 h1:6uEvcprBybDmW4hcz3gYujhARhye+GoWKhEWyzD5sh4= +github.com/pressly/goose/v3 v3.27.1/go.mod h1:maruOxsPnIG2yHHyo8UqKWXYKFcH7Q76csUV7+7KYoM= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE= github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas= +github.com/shirou/gopsutil/v4 v4.26.3 h1:2ESdQt90yU3oXF/CdOlRCJxrP+Am1aBYubTMTfxJ1qc= +github.com/shirou/gopsutil/v4 v4.26.3/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= +github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= @@ -50,28 +122,51 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/testcontainers/testcontainers-go v0.42.0 h1:He3IhTzTZOygSXLJPMX7n44XtK+qhjat1nI9cneBbUY= +github.com/testcontainers/testcontainers-go v0.42.0/go.mod h1:vZjdY1YmUA1qEForxOIOazfsrdyORJAbhi0bp8plN30= +github.com/testcontainers/testcontainers-go/modules/mariadb v0.42.0 h1:ZfWUJSIDnbNgoLAXMV1fc7lqcxBIX3zdnhwjaVUo7N0= +github.com/testcontainers/testcontainers-go/modules/mariadb v0.42.0/go.mod h1:0kV+yHee7zAgp0yccydxjNnHvlC1EOavTLCeg/lnRBY= +github.com/testcontainers/testcontainers-go/modules/mysql v0.42.0 h1:Yhv1k7vDpyzZePntg5R5Oj4ZMCyWpAfpJeRu1ROsgiU= +github.com/testcontainers/testcontainers-go/modules/mysql v0.42.0/go.mod h1:Z7SCTuiZlghAdRjkv3Ir0iXJKC2T2avbtxLR0DRe+ng= +github.com/testcontainers/testcontainers-go/modules/postgres v0.42.0 h1:GCbb1ndrF7OTDiIvxXyItaDab4qkzTFJ48LKFdM7EIo= +github.com/testcontainers/testcontainers-go/modules/postgres v0.42.0/go.mod h1:IRPBaI8jXdrNfD0e4Zm7Fbcgaz5shKxOQv4axiL09xs= +github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA= +github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= +github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw= +github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= -golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= -golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= -golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= -golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= -golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= -golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= +golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= +golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= +golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ= -modernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8= +modernc.org/libc v1.72.1 h1:db1xwJ6u1kE3KHTFTTbe2GCrczHPKzlURP0aDC4NGD0= +modernc.org/libc v1.72.1/go.mod h1:HRMiC/PhPGLIPM7GzAFCbI+oSgE3dhZ8FWftmRrHVlY= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= -modernc.org/sqlite v1.38.2 h1:Aclu7+tgjgcQVShZqim41Bbw9Cho0y/7WzYptXqkEek= -modernc.org/sqlite v1.38.2/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E= +modernc.org/sqlite v1.49.1 h1:dYGHTKcX1sJ+EQDnUzvz4TJ5GbuvhNJa8Fg6ElGx73U= +modernc.org/sqlite v1.49.1/go.mod h1:m0w8xhwYUVY3H6pSDwc3gkJ/irZT/0YEXwBlhaxQEew= diff --git a/extra/migriscli/go.mod b/extra/migriscli/go.mod index 04e2b8c..0d8a0bd 100644 --- a/extra/migriscli/go.mod +++ b/extra/migriscli/go.mod @@ -1,6 +1,6 @@ module github.com/akfaiz/migris/extra/migriscli -go 1.24.0 +go 1.25.7 require ( github.com/akfaiz/migris v0.4.0 @@ -10,14 +10,14 @@ require ( require ( github.com/fatih/color v1.18.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-isatty v0.0.21 // indirect github.com/mfridman/interpolate v0.0.2 // indirect - github.com/pressly/goose/v3 v3.26.0 // indirect + github.com/pressly/goose/v3 v3.27.1 // indirect github.com/sethvargo/go-retry v0.3.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/sync v0.19.0 // indirect - golang.org/x/sys v0.40.0 // indirect - golang.org/x/term v0.39.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.43.0 // indirect + golang.org/x/term v0.42.0 // indirect ) replace github.com/akfaiz/migris => ../.. diff --git a/extra/migriscli/go.sum b/extra/migriscli/go.sum index 934604e..49ab4d4 100644 --- a/extra/migriscli/go.sum +++ b/extra/migriscli/go.sum @@ -1,31 +1,158 @@ +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= +filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= +filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c= +github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU= +github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= +github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw= +github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= +github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= +github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs= +github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= +github.com/mattn/go-sqlite3 v1.14.33 h1:A5blZ5ulQo2AtayQ9/limgHEkFreKj1Dv226a1K73s0= +github.com/mattn/go-sqlite3 v1.14.33/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY= github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8= +github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU= +github.com/moby/moby/api v1.54.2 h1:wiat9QAhnDQjA7wk1kh/TqHz2I1uUA7M7t9SAl/JNXg= +github.com/moby/moby/api v1.54.2/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs= +github.com/moby/moby/client v0.4.1 h1:DMQgisVoMkmMs7fp3ROSdiBnoAu8+vo3GggFl06M/wY= +github.com/moby/moby/client v0.4.1/go.mod h1:z52C9O2POPOsnxZAy//WtKcQ32P+jT/NGeXu/7nfjGQ= +github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U= +github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= +github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= +github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= +github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= +github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pressly/goose/v3 v3.26.0 h1:KJakav68jdH0WDvoAcj8+n61WqOIaPGgH0bJWS6jpmM= -github.com/pressly/goose/v3 v3.26.0/go.mod h1:4hC1KrritdCxtuFsqgs1R4AU5bWtTAf+cnWvfhf2DNY= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/pressly/goose/v3 v3.27.1 h1:6uEvcprBybDmW4hcz3gYujhARhye+GoWKhEWyzD5sh4= +github.com/pressly/goose/v3 v3.27.1/go.mod h1:maruOxsPnIG2yHHyo8UqKWXYKFcH7Q76csUV7+7KYoM= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE= github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas= +github.com/shirou/gopsutil/v4 v4.26.3 h1:2ESdQt90yU3oXF/CdOlRCJxrP+Am1aBYubTMTfxJ1qc= +github.com/shirou/gopsutil/v4 v4.26.3/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= +github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/testcontainers/testcontainers-go v0.42.0 h1:He3IhTzTZOygSXLJPMX7n44XtK+qhjat1nI9cneBbUY= +github.com/testcontainers/testcontainers-go v0.42.0/go.mod h1:vZjdY1YmUA1qEForxOIOazfsrdyORJAbhi0bp8plN30= +github.com/testcontainers/testcontainers-go/modules/mariadb v0.42.0 h1:ZfWUJSIDnbNgoLAXMV1fc7lqcxBIX3zdnhwjaVUo7N0= +github.com/testcontainers/testcontainers-go/modules/mariadb v0.42.0/go.mod h1:0kV+yHee7zAgp0yccydxjNnHvlC1EOavTLCeg/lnRBY= +github.com/testcontainers/testcontainers-go/modules/mysql v0.42.0 h1:Yhv1k7vDpyzZePntg5R5Oj4ZMCyWpAfpJeRu1ROsgiU= +github.com/testcontainers/testcontainers-go/modules/mysql v0.42.0/go.mod h1:Z7SCTuiZlghAdRjkv3Ir0iXJKC2T2avbtxLR0DRe+ng= +github.com/testcontainers/testcontainers-go/modules/postgres v0.42.0 h1:GCbb1ndrF7OTDiIvxXyItaDab4qkzTFJ48LKFdM7EIo= +github.com/testcontainers/testcontainers-go/modules/postgres v0.42.0/go.mod h1:IRPBaI8jXdrNfD0e4Zm7Fbcgaz5shKxOQv4axiL09xs= +github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA= +github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= +github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw= +github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ= github.com/urfave/cli/v3 v3.6.1 h1:j8Qq8NyUawj/7rTYdBGrxcH7A/j7/G8Q5LhWEW4G3Mo= github.com/urfave/cli/v3 v3.6.1/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= -golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= -golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= +golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= +golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= +golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= +golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +modernc.org/libc v1.72.1 h1:db1xwJ6u1kE3KHTFTTbe2GCrczHPKzlURP0aDC4NGD0= +modernc.org/libc v1.72.1/go.mod h1:HRMiC/PhPGLIPM7GzAFCbI+oSgE3dhZ8FWftmRrHVlY= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/sqlite v1.49.1 h1:dYGHTKcX1sJ+EQDnUzvz4TJ5GbuvhNJa8Fg6ElGx73U= +modernc.org/sqlite v1.49.1/go.mod h1:m0w8xhwYUVY3H6pSDwc3gkJ/irZT/0YEXwBlhaxQEew= diff --git a/extra/migriscli/migriscli.go b/extra/migriscli/migriscli.go index 41f9004..990f7fb 100644 --- a/extra/migriscli/migriscli.go +++ b/extra/migriscli/migriscli.go @@ -17,137 +17,146 @@ type Config struct { // NewCLI creates a new CLI interface for migris with subcommands. func NewCLI(cfg Config) *cli.Command { - cmd := &cli.Command{ - Name: "migrate", - Usage: "Database migration CLI tool", - Commands: []*cli.Command{ - { - Name: "create", - Usage: "Create a new migration file", - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "name", - Aliases: []string{"n"}, - Usage: "Name of the migration", - Required: true, - }, - }, - Action: func(ctx context.Context, c *cli.Command) error { - return migris.Create(cfg.MigrationsDir, c.String("name")) - }, - }, - { - Name: "up", - Usage: "Apply all up migrations", - Flags: []cli.Flag{ - &cli.BoolFlag{ - Name: "dry-run", - Usage: "Simulate the migration without applying changes", - }, - }, - Action: func(ctx context.Context, c *cli.Command) error { - migrator, err := createMigrator(c, cfg.DB, cfg) - if err != nil { - return err - } - return migrator.UpContext(ctx) - }, - }, - { - Name: "up-to", - Usage: "Apply migrations up to a specific version", - Flags: []cli.Flag{ - &cli.BoolFlag{ - Name: "dry-run", - Usage: "Simulate the migration without applying changes", - }, - &cli.Int64Flag{ - Name: "version", - Aliases: []string{"v"}, - Usage: "Target version to migrate up to", - Required: true, - }, - }, - Action: func(ctx context.Context, c *cli.Command) error { - migrator, err := createMigrator(c, cfg.DB, cfg) - if err != nil { - return err - } - return migrator.UpToContext(ctx, c.Int64("version")) - }, - }, - { - Name: "down", - Usage: "Rollback the last migration", - Flags: []cli.Flag{ - &cli.BoolFlag{ - Name: "dry-run", - Usage: "Simulate the migration without applying changes", - }, - }, - Action: func(ctx context.Context, c *cli.Command) error { - migrator, err := createMigrator(c, cfg.DB, cfg) - if err != nil { - return err - } - return migrator.DownContext(ctx) - }, - }, - { - Name: "down-to", - Usage: "Rollback migrations down to a specific version", - Flags: []cli.Flag{ - &cli.BoolFlag{ - Name: "dry-run", - Usage: "Simulate the migration without applying changes", - }, - &cli.Int64Flag{ - Name: "version", - Aliases: []string{"v"}, - Usage: "Target version to migrate down to", - Required: true, - }, - }, - Action: func(ctx context.Context, c *cli.Command) error { - migrator, err := createMigrator(c, cfg.DB, cfg) - if err != nil { - return err - } - return migrator.DownToContext(ctx, c.Int64("version")) - }, - }, - { - Name: "reset", - Usage: "Rollback all migrations", - Flags: []cli.Flag{ - &cli.BoolFlag{ - Name: "dry-run", - Usage: "Simulate the migration without applying changes", - }, - }, - Action: func(ctx context.Context, c *cli.Command) error { - migrator, err := createMigrator(c, cfg.DB, cfg) - if err != nil { - return err - } - return migrator.ResetContext(ctx) - }, - }, - { - Name: "status", - Usage: "Show the status of migrations", - Action: func(ctx context.Context, c *cli.Command) error { - migrator, err := createMigrator(c, cfg.DB, cfg) - if err != nil { - return err - } - return migrator.StatusContext(ctx) - }, + return &cli.Command{ + Name: "migrate", + Usage: "Database migration CLI tool", + Commands: buildCommands(cfg), + } +} + +func buildCommands(cfg Config) []*cli.Command { + return []*cli.Command{ + newCreateCommand(cfg), + newUpCommand(cfg), + newUpToCommand(cfg), + newDownCommand(cfg), + newDownToCommand(cfg), + newResetCommand(cfg), + newStatusCommand(cfg), + } +} + +func newCreateCommand(cfg Config) *cli.Command { + return &cli.Command{ + Name: "create", + Usage: "Create a new migration file", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "name", + Aliases: []string{"n"}, + Usage: "Name of the migration", + Required: true, }, }, + Action: func(_ context.Context, c *cli.Command) error { + return migris.Create(cfg.MigrationsDir, c.String("name")) + }, + } +} + +func newUpCommand(cfg Config) *cli.Command { + return &cli.Command{ + Name: "up", + Usage: "Apply all up migrations", + Flags: []cli.Flag{dryRunFlag()}, + Action: func(ctx context.Context, c *cli.Command) error { + migrator, err := createMigrator(c, cfg.DB, cfg) + if err != nil { + return err + } + return migrator.UpContext(ctx) + }, + } +} + +func newUpToCommand(cfg Config) *cli.Command { + return &cli.Command{ + Name: "up-to", + Usage: "Apply migrations up to a specific version", + Flags: []cli.Flag{dryRunFlag(), versionFlag("Target version to migrate up to")}, + Action: func(ctx context.Context, c *cli.Command) error { + migrator, err := createMigrator(c, cfg.DB, cfg) + if err != nil { + return err + } + return migrator.UpToContext(ctx, c.Int64("version")) + }, } +} + +func newDownCommand(cfg Config) *cli.Command { + return &cli.Command{ + Name: "down", + Usage: "Rollback the last migration", + Flags: []cli.Flag{dryRunFlag()}, + Action: func(ctx context.Context, c *cli.Command) error { + migrator, err := createMigrator(c, cfg.DB, cfg) + if err != nil { + return err + } + return migrator.DownContext(ctx) + }, + } +} - return cmd +func newDownToCommand(cfg Config) *cli.Command { + return &cli.Command{ + Name: "down-to", + Usage: "Rollback migrations down to a specific version", + Flags: []cli.Flag{dryRunFlag(), versionFlag("Target version to migrate down to")}, + Action: func(ctx context.Context, c *cli.Command) error { + migrator, err := createMigrator(c, cfg.DB, cfg) + if err != nil { + return err + } + return migrator.DownToContext(ctx, c.Int64("version")) + }, + } +} + +func newResetCommand(cfg Config) *cli.Command { + return &cli.Command{ + Name: "reset", + Usage: "Rollback all migrations", + Flags: []cli.Flag{dryRunFlag()}, + Action: func(ctx context.Context, c *cli.Command) error { + migrator, err := createMigrator(c, cfg.DB, cfg) + if err != nil { + return err + } + return migrator.ResetContext(ctx) + }, + } +} + +func newStatusCommand(cfg Config) *cli.Command { + return &cli.Command{ + Name: "status", + Usage: "Show the status of migrations", + Action: func(ctx context.Context, c *cli.Command) error { + migrator, err := createMigrator(c, cfg.DB, cfg) + if err != nil { + return err + } + return migrator.StatusContext(ctx) + }, + } +} + +func dryRunFlag() *cli.BoolFlag { + return &cli.BoolFlag{ + Name: "dry-run", + Usage: "Simulate the migration without applying changes", + } +} + +func versionFlag(usage string) *cli.Int64Flag { + return &cli.Int64Flag{ + Name: "version", + Aliases: []string{"v"}, + Usage: usage, + Required: true, + } } func createMigrator(c *cli.Command, db *sql.DB, cfg Config) (*migris.Migrate, error) { diff --git a/extra/migriscobra/go.mod b/extra/migriscobra/go.mod index 566aa60..941cfc1 100644 --- a/extra/migriscobra/go.mod +++ b/extra/migriscobra/go.mod @@ -1,6 +1,6 @@ module github.com/akfaiz/migris/extra/migriscobra -go 1.24.0 +go 1.25.7 require ( github.com/akfaiz/migris v0.4.0 @@ -11,15 +11,15 @@ require ( github.com/fatih/color v1.18.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-isatty v0.0.21 // indirect github.com/mfridman/interpolate v0.0.2 // indirect - github.com/pressly/goose/v3 v3.26.0 // indirect + github.com/pressly/goose/v3 v3.27.1 // indirect github.com/sethvargo/go-retry v0.3.0 // indirect github.com/spf13/pflag v1.0.5 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/sync v0.19.0 // indirect - golang.org/x/sys v0.35.0 // indirect - golang.org/x/term v0.34.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.43.0 // indirect + golang.org/x/term v0.42.0 // indirect ) replace github.com/akfaiz/migris => ../.. diff --git a/extra/migriscobra/go.sum b/extra/migriscobra/go.sum index f157642..80f847f 100644 --- a/extra/migriscobra/go.sum +++ b/extra/migriscobra/go.sum @@ -1,12 +1,48 @@ -filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= -filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= +filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= +filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c= +github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU= +github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -17,56 +53,115 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo= -github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw= +github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw= +github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= +github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= +github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs= +github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= +github.com/mattn/go-sqlite3 v1.14.33 h1:A5blZ5ulQo2AtayQ9/limgHEkFreKj1Dv226a1K73s0= +github.com/mattn/go-sqlite3 v1.14.33/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY= github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg= -github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= -github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8= +github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU= +github.com/moby/moby/api v1.54.2 h1:wiat9QAhnDQjA7wk1kh/TqHz2I1uUA7M7t9SAl/JNXg= +github.com/moby/moby/api v1.54.2/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs= +github.com/moby/moby/client v0.4.1 h1:DMQgisVoMkmMs7fp3ROSdiBnoAu8+vo3GggFl06M/wY= +github.com/moby/moby/client v0.4.1/go.mod h1:z52C9O2POPOsnxZAy//WtKcQ32P+jT/NGeXu/7nfjGQ= +github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U= +github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= +github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= +github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= +github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= +github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pressly/goose/v3 v3.26.0 h1:KJakav68jdH0WDvoAcj8+n61WqOIaPGgH0bJWS6jpmM= -github.com/pressly/goose/v3 v3.26.0/go.mod h1:4hC1KrritdCxtuFsqgs1R4AU5bWtTAf+cnWvfhf2DNY= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/pressly/goose/v3 v3.27.1 h1:6uEvcprBybDmW4hcz3gYujhARhye+GoWKhEWyzD5sh4= +github.com/pressly/goose/v3 v3.27.1/go.mod h1:maruOxsPnIG2yHHyo8UqKWXYKFcH7Q76csUV7+7KYoM= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE= github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas= +github.com/shirou/gopsutil/v4 v4.26.3 h1:2ESdQt90yU3oXF/CdOlRCJxrP+Am1aBYubTMTfxJ1qc= +github.com/shirou/gopsutil/v4 v4.26.3/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= +github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/testcontainers/testcontainers-go v0.42.0 h1:He3IhTzTZOygSXLJPMX7n44XtK+qhjat1nI9cneBbUY= +github.com/testcontainers/testcontainers-go v0.42.0/go.mod h1:vZjdY1YmUA1qEForxOIOazfsrdyORJAbhi0bp8plN30= +github.com/testcontainers/testcontainers-go/modules/mariadb v0.42.0 h1:ZfWUJSIDnbNgoLAXMV1fc7lqcxBIX3zdnhwjaVUo7N0= +github.com/testcontainers/testcontainers-go/modules/mariadb v0.42.0/go.mod h1:0kV+yHee7zAgp0yccydxjNnHvlC1EOavTLCeg/lnRBY= +github.com/testcontainers/testcontainers-go/modules/mysql v0.42.0 h1:Yhv1k7vDpyzZePntg5R5Oj4ZMCyWpAfpJeRu1ROsgiU= +github.com/testcontainers/testcontainers-go/modules/mysql v0.42.0/go.mod h1:Z7SCTuiZlghAdRjkv3Ir0iXJKC2T2avbtxLR0DRe+ng= +github.com/testcontainers/testcontainers-go/modules/postgres v0.42.0 h1:GCbb1ndrF7OTDiIvxXyItaDab4qkzTFJ48LKFdM7EIo= +github.com/testcontainers/testcontainers-go/modules/postgres v0.42.0/go.mod h1:IRPBaI8jXdrNfD0e4Zm7Fbcgaz5shKxOQv4axiL09xs= +github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA= +github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= +github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw= +github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= -golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= -golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= +golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= -golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= -golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= -golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= -golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= +golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= +golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ= -modernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8= +modernc.org/libc v1.72.1 h1:db1xwJ6u1kE3KHTFTTbe2GCrczHPKzlURP0aDC4NGD0= +modernc.org/libc v1.72.1/go.mod h1:HRMiC/PhPGLIPM7GzAFCbI+oSgE3dhZ8FWftmRrHVlY= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= -modernc.org/sqlite v1.38.2 h1:Aclu7+tgjgcQVShZqim41Bbw9Cho0y/7WzYptXqkEek= -modernc.org/sqlite v1.38.2/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E= +modernc.org/sqlite v1.49.1 h1:dYGHTKcX1sJ+EQDnUzvz4TJ5GbuvhNJa8Fg6ElGx73U= +modernc.org/sqlite v1.49.1/go.mod h1:m0w8xhwYUVY3H6pSDwc3gkJ/irZT/0YEXwBlhaxQEew= diff --git a/extra/migriscobra/migriscobra.go b/extra/migriscobra/migriscobra.go index 00f89d6..216e7cf 100644 --- a/extra/migriscobra/migriscobra.go +++ b/extra/migriscobra/migriscobra.go @@ -41,7 +41,7 @@ func createCreateCommand(cfg Config) *cobra.Command { cmd := &cobra.Command{ Use: "create", Short: "Create a new migration file", - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cobra.Command, _ []string) error { name, _ := cmd.Flags().GetString("name") if name == "" { return cmd.Help() @@ -50,7 +50,7 @@ func createCreateCommand(cfg Config) *cobra.Command { }, } cmd.Flags().StringP("name", "n", "", "Name of the migration (required)") - cmd.MarkFlagRequired("name") + mustMarkFlagRequired(cmd, "name") return cmd } @@ -58,7 +58,7 @@ func createUpCommand(cfg Config) *cobra.Command { cmd := &cobra.Command{ Use: "up", Short: "Apply all up migrations", - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cobra.Command, _ []string) error { migrator, err := createMigrator(cmd, cfg) if err != nil { return err @@ -74,7 +74,7 @@ func createUpToCommand(cfg Config) *cobra.Command { cmd := &cobra.Command{ Use: "up-to", Short: "Apply migrations up to a specific version", - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cobra.Command, _ []string) error { version, _ := cmd.Flags().GetInt64("version") migrator, err := createMigrator(cmd, cfg) if err != nil { @@ -85,7 +85,7 @@ func createUpToCommand(cfg Config) *cobra.Command { } cmd.Flags().Bool("dry-run", false, "Simulate the migration without applying changes") cmd.Flags().Int64P("version", "v", 0, "Target version to migrate up to (required)") - cmd.MarkFlagRequired("version") + mustMarkFlagRequired(cmd, "version") return cmd } @@ -93,7 +93,7 @@ func createDownCommand(cfg Config) *cobra.Command { cmd := &cobra.Command{ Use: "down", Short: "Rollback the last migration", - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cobra.Command, _ []string) error { migrator, err := createMigrator(cmd, cfg) if err != nil { return err @@ -109,7 +109,7 @@ func createDownToCommand(cfg Config) *cobra.Command { cmd := &cobra.Command{ Use: "down-to", Short: "Rollback migrations down to a specific version", - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cobra.Command, _ []string) error { version, _ := cmd.Flags().GetInt64("version") migrator, err := createMigrator(cmd, cfg) if err != nil { @@ -120,7 +120,7 @@ func createDownToCommand(cfg Config) *cobra.Command { } cmd.Flags().Bool("dry-run", false, "Simulate the migration without applying changes") cmd.Flags().Int64P("version", "v", 0, "Target version to migrate down to (required)") - cmd.MarkFlagRequired("version") + mustMarkFlagRequired(cmd, "version") return cmd } @@ -128,7 +128,7 @@ func createResetCommand(cfg Config) *cobra.Command { cmd := &cobra.Command{ Use: "reset", Short: "Rollback all migrations", - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cobra.Command, _ []string) error { migrator, err := createMigrator(cmd, cfg) if err != nil { return err @@ -144,7 +144,7 @@ func createStatusCommand(cfg Config) *cobra.Command { cmd := &cobra.Command{ Use: "status", Short: "Show the status of migrations", - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cobra.Command, _ []string) error { migrator, err := createMigrator(cmd, cfg) if err != nil { return err @@ -172,3 +172,9 @@ func createMigrator(cmd *cobra.Command, cfg Config) (*migris.Migrate, error) { return migrator, nil } + +func mustMarkFlagRequired(cmd *cobra.Command, name string) { + if err := cmd.MarkFlagRequired(name); err != nil { + panic(err) + } +} diff --git a/go.mod b/go.mod index 2dee9e1..deb0660 100644 --- a/go.mod +++ b/go.mod @@ -1,32 +1,80 @@ module github.com/akfaiz/migris -go 1.24.0 +go 1.25.7 require ( github.com/fatih/color v1.18.0 github.com/go-sql-driver/mysql v1.9.3 - github.com/jackc/pgx/v5 v5.8.0 + github.com/jackc/pgx/v5 v5.9.2 github.com/mattn/go-sqlite3 v1.14.33 - github.com/pressly/goose/v3 v3.26.0 + github.com/pressly/goose/v3 v3.27.1 github.com/stretchr/testify v1.11.1 - golang.org/x/term v0.34.0 + github.com/testcontainers/testcontainers-go v0.42.0 + github.com/testcontainers/testcontainers-go/modules/mariadb v0.42.0 + github.com/testcontainers/testcontainers-go/modules/mysql v0.42.0 + github.com/testcontainers/testcontainers-go/modules/postgres v0.42.0 + golang.org/x/term v0.42.0 ) require ( - filippo.io/edwards25519 v1.1.0 // indirect + dario.cat/mergo v1.0.2 // indirect + filippo.io/edwards25519 v1.2.0 // indirect + github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/containerd/errdefs v1.0.0 // indirect + github.com/containerd/errdefs/pkg v0.3.0 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/containerd/platforms v0.2.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/go-connections v0.7.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/ebitengine/purego v0.10.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect - github.com/kr/text v0.2.0 // indirect + github.com/klauspost/compress v1.18.5 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/magiconair/properties v1.8.10 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-isatty v0.0.21 // indirect github.com/mfridman/interpolate v0.0.2 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/go-archive v0.2.0 // indirect + github.com/moby/moby/api v1.54.2 // indirect + github.com/moby/moby/client v0.4.1 // indirect + github.com/moby/patternmatcher v0.6.1 // indirect + github.com/moby/sys/sequential v0.6.0 // indirect + github.com/moby/sys/user v0.4.0 // indirect + github.com/moby/sys/userns v0.1.0 // indirect + github.com/moby/term v0.5.2 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/sethvargo/go-retry v0.3.0 // indirect + github.com/shirou/gopsutil/v4 v4.26.3 // indirect + github.com/sirupsen/logrus v1.9.4 // indirect + github.com/tklauser/go-sysconf v0.3.16 // indirect + github.com/tklauser/numcpus v0.11.0 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect + go.opentelemetry.io/otel v1.43.0 // indirect + go.opentelemetry.io/otel/metric v1.43.0 // indirect + go.opentelemetry.io/otel/trace v1.43.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/sync v0.19.0 // indirect - golang.org/x/sys v0.35.0 // indirect - golang.org/x/text v0.33.0 // indirect + golang.org/x/crypto v0.50.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.43.0 // indirect + golang.org/x/text v0.36.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 7b8f53a..cc8f14e 100644 --- a/go.sum +++ b/go.sum @@ -1,80 +1,198 @@ -filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= -filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= +filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= +filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= +github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c= +github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU= +github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo= -github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw= +github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw= +github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= +github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= +github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs= +github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= github.com/mattn/go-sqlite3 v1.14.33 h1:A5blZ5ulQo2AtayQ9/limgHEkFreKj1Dv226a1K73s0= github.com/mattn/go-sqlite3 v1.14.33/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mdelapenya/tlscert v0.2.0 h1:7H81W6Z/4weDvZBNOfQte5GpIMo0lGYEeWbkGp5LJHI= +github.com/mdelapenya/tlscert v0.2.0/go.mod h1:O4njj3ELLnJjGdkN7M/vIVCpZ+Cf0L6muqOG4tLSl8o= github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY= github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg= -github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= -github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8= +github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU= +github.com/moby/moby/api v1.54.2 h1:wiat9QAhnDQjA7wk1kh/TqHz2I1uUA7M7t9SAl/JNXg= +github.com/moby/moby/api v1.54.2/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs= +github.com/moby/moby/client v0.4.1 h1:DMQgisVoMkmMs7fp3ROSdiBnoAu8+vo3GggFl06M/wY= +github.com/moby/moby/client v0.4.1/go.mod h1:z52C9O2POPOsnxZAy//WtKcQ32P+jT/NGeXu/7nfjGQ= +github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U= +github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= +github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= +github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= +github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= +github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pressly/goose/v3 v3.26.0 h1:KJakav68jdH0WDvoAcj8+n61WqOIaPGgH0bJWS6jpmM= -github.com/pressly/goose/v3 v3.26.0/go.mod h1:4hC1KrritdCxtuFsqgs1R4AU5bWtTAf+cnWvfhf2DNY= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/pressly/goose/v3 v3.27.1 h1:6uEvcprBybDmW4hcz3gYujhARhye+GoWKhEWyzD5sh4= +github.com/pressly/goose/v3 v3.27.1/go.mod h1:maruOxsPnIG2yHHyo8UqKWXYKFcH7Q76csUV7+7KYoM= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE= github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas= +github.com/shirou/gopsutil/v4 v4.26.3 h1:2ESdQt90yU3oXF/CdOlRCJxrP+Am1aBYubTMTfxJ1qc= +github.com/shirou/gopsutil/v4 v4.26.3/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= +github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= +github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/testcontainers/testcontainers-go v0.42.0 h1:He3IhTzTZOygSXLJPMX7n44XtK+qhjat1nI9cneBbUY= +github.com/testcontainers/testcontainers-go v0.42.0/go.mod h1:vZjdY1YmUA1qEForxOIOazfsrdyORJAbhi0bp8plN30= +github.com/testcontainers/testcontainers-go/modules/mariadb v0.42.0 h1:ZfWUJSIDnbNgoLAXMV1fc7lqcxBIX3zdnhwjaVUo7N0= +github.com/testcontainers/testcontainers-go/modules/mariadb v0.42.0/go.mod h1:0kV+yHee7zAgp0yccydxjNnHvlC1EOavTLCeg/lnRBY= +github.com/testcontainers/testcontainers-go/modules/mysql v0.42.0 h1:Yhv1k7vDpyzZePntg5R5Oj4ZMCyWpAfpJeRu1ROsgiU= +github.com/testcontainers/testcontainers-go/modules/mysql v0.42.0/go.mod h1:Z7SCTuiZlghAdRjkv3Ir0iXJKC2T2avbtxLR0DRe+ng= +github.com/testcontainers/testcontainers-go/modules/postgres v0.42.0 h1:GCbb1ndrF7OTDiIvxXyItaDab4qkzTFJ48LKFdM7EIo= +github.com/testcontainers/testcontainers-go/modules/postgres v0.42.0/go.mod h1:IRPBaI8jXdrNfD0e4Zm7Fbcgaz5shKxOQv4axiL09xs= +github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA= +github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= +github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw= +github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= +go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= -golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= -golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= +golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= -golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= -golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= -golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= -golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= +golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= +golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ= -modernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= +modernc.org/libc v1.72.1 h1:db1xwJ6u1kE3KHTFTTbe2GCrczHPKzlURP0aDC4NGD0= +modernc.org/libc v1.72.1/go.mod h1:HRMiC/PhPGLIPM7GzAFCbI+oSgE3dhZ8FWftmRrHVlY= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= -modernc.org/sqlite v1.38.2 h1:Aclu7+tgjgcQVShZqim41Bbw9Cho0y/7WzYptXqkEek= -modernc.org/sqlite v1.38.2/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E= +modernc.org/sqlite v1.49.1 h1:dYGHTKcX1sJ+EQDnUzvz4TJ5GbuvhNJa8Fg6ElGx73U= +modernc.org/sqlite v1.49.1/go.mod h1:m0w8xhwYUVY3H6pSDwc3gkJ/irZT/0YEXwBlhaxQEew= +pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk= +pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= diff --git a/internal/dialect/dialect.go b/internal/dialect/dialect.go index b9047e0..232b584 100644 --- a/internal/dialect/dialect.go +++ b/internal/dialect/dialect.go @@ -7,6 +7,7 @@ type Dialect string const ( MySQL Dialect = "mysql" + MariaDB Dialect = "mariadb" Postgres Dialect = "postgres" SQLite3 Dialect = "sqlite3" Unknown Dialect = "" @@ -18,7 +19,7 @@ func (d Dialect) String() string { func (d Dialect) GooseDialect() database.Dialect { switch d { - case MySQL: + case MySQL, MariaDB: return database.DialectMySQL case Postgres: return database.DialectPostgres @@ -33,8 +34,10 @@ func (d Dialect) GooseDialect() database.Dialect { func FromString(dialect string) Dialect { switch dialect { - case "mysql", "mariadb": + case "mysql": return MySQL + case "mariadb": + return MariaDB case "postgres", "pgx": return Postgres case "sqlite3", "sqlite": diff --git a/internal/dialect/dialect_test.go b/internal/dialect/dialect_test.go index 322b387..cec1ac4 100644 --- a/internal/dialect/dialect_test.go +++ b/internal/dialect/dialect_test.go @@ -49,7 +49,7 @@ func TestFromString(t *testing.T) { {"postgres", dialect.Postgres}, {"pgx", dialect.Postgres}, {"mysql", dialect.MySQL}, - {"mariadb", dialect.MySQL}, + {"mariadb", dialect.MariaDB}, {"sqlite3", dialect.SQLite3}, {"sqlite", dialect.SQLite3}, {"unknown", dialect.Unknown}, // default diff --git a/internal/testutil/db.go b/internal/testutil/db.go new file mode 100644 index 0000000..796509a --- /dev/null +++ b/internal/testutil/db.go @@ -0,0 +1,127 @@ +package testutil + +import ( + "context" + "database/sql" + "fmt" + "time" + + _ "github.com/go-sql-driver/mysql" // MySQL driver for testing + _ "github.com/jackc/pgx/v5/stdlib" // PostgreSQL driver for testing + "github.com/testcontainers/testcontainers-go" + tcmariadb "github.com/testcontainers/testcontainers-go/modules/mariadb" + tcmysql "github.com/testcontainers/testcontainers-go/modules/mysql" + tcpostgres "github.com/testcontainers/testcontainers-go/modules/postgres" +) + +const ( + TestDBName = "db_test" + TestDBUser = "root" + TestDBPassword = "password" +) + +func StartMySQLTestDB(ctx context.Context) (testcontainers.Container, *sql.DB, error) { + container, err := tcmysql.Run(ctx, "mysql:8.0.36", + tcmysql.WithDatabase(TestDBName), + tcmysql.WithUsername(TestDBUser), + tcmysql.WithPassword(TestDBPassword), + ) + if err != nil { + return nil, nil, err + } + + connectionString, err := container.ConnectionString(ctx, "parseTime=true", "loc=Local") + if err != nil { + _ = container.Terminate(ctx) + return nil, nil, err + } + db, err := sql.Open("mysql", connectionString) + if err != nil { + _ = container.Terminate(ctx) + return nil, nil, err + } + + err = WaitForDBPing(ctx, db, 30*time.Second) + if err != nil { + _ = db.Close() + _ = container.Terminate(ctx) + return nil, nil, err + } + + return container, db, nil +} + +func StartMariaDBTestDB(ctx context.Context) (testcontainers.Container, *sql.DB, error) { + container, err := tcmariadb.Run(ctx, "mariadb:11.0.3", + tcmariadb.WithDatabase(TestDBName), + tcmariadb.WithUsername(TestDBUser), + tcmariadb.WithPassword(TestDBPassword), + ) + if err != nil { + return nil, nil, err + } + + connectionString, err := container.ConnectionString(ctx, "parseTime=true", "loc=Local") + if err != nil { + _ = container.Terminate(ctx) + return nil, nil, err + } + db, err := sql.Open("mysql", connectionString) + if err != nil { + _ = container.Terminate(ctx) + return nil, nil, err + } + + err = WaitForDBPing(ctx, db, 30*time.Second) + if err != nil { + _ = db.Close() + _ = container.Terminate(ctx) + return nil, nil, err + } + + return container, db, nil +} + +func StartPostgresTestDB(ctx context.Context) (testcontainers.Container, *sql.DB, error) { + container, err := tcpostgres.Run(ctx, "postgres:16-alpine", + tcpostgres.WithDatabase(TestDBName), + tcpostgres.WithUsername(TestDBUser), + tcpostgres.WithPassword(TestDBPassword), + ) + if err != nil { + return nil, nil, err + } + + connectionString, err := container.ConnectionString(ctx, "sslmode=disable") + if err != nil { + _ = container.Terminate(ctx) + return nil, nil, err + } + db, err := sql.Open("pgx", connectionString) + if err != nil { + _ = container.Terminate(ctx) + return nil, nil, err + } + + err = WaitForDBPing(ctx, db, 30*time.Second) + if err != nil { + _ = db.Close() + _ = container.Terminate(ctx) + return nil, nil, err + } + + return container, db, nil +} + +func WaitForDBPing(ctx context.Context, db *sql.DB, timeout time.Duration) error { + deadline := time.Now().Add(timeout) + for { + if err := db.PingContext(ctx); err == nil { + return nil + } + if time.Now().After(deadline) { + return fmt.Errorf("database did not become ready within %s", timeout) + } + time.Sleep(500 * time.Millisecond) + } +} diff --git a/internal/util/util.go b/internal/util/util.go index f5b7a44..61a4ca3 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -1,4 +1,4 @@ -package util //nolint:revive // Helper functions for general purposes. +package util func Optional[T any](defaultValue T, values ...T) T { if len(values) > 0 { diff --git a/migrate.go b/migrate.go index da221e3..72e28d8 100644 --- a/migrate.go +++ b/migrate.go @@ -20,6 +20,7 @@ type Migrate struct { tableName string dryRun bool logger *logger.Logger + registry *Registry } // New creates a new Migrate instance. @@ -35,6 +36,7 @@ func New(dialectValue string, opts ...Option) (*Migrate, error) { migrationDir: "migrations", tableName: "schema_migrations", logger: logger.Get(), + registry: defaultRegistry, } for _, opt := range opts { opt(m) @@ -46,8 +48,7 @@ func New(dialectValue string, opts ...Option) (*Migrate, error) { } func (m *Migrate) newProvider() (*goose.Provider, error) { - val := config.GetDialect() - gooseDialect := val.GooseDialect() + gooseDialect := m.dialect.GooseDialect() store, err := database.NewStore(gooseDialect, m.tableName) if err != nil { return nil, err @@ -55,7 +56,7 @@ func (m *Migrate) newProvider() (*goose.Provider, error) { provider, err := goose.NewProvider(database.DialectCustom, m.db, os.DirFS(m.migrationDir), goose.WithStore(store), goose.WithDisableGlobalRegistry(true), - goose.WithGoMigrations(gooseMigrations()...), + goose.WithGoMigrations(m.registry.gooseMigrations(m.dialect)...), ) if err != nil { return nil, err diff --git a/migrate_test.go b/migrate_test.go index ae4fb7c..87db626 100644 --- a/migrate_test.go +++ b/migrate_test.go @@ -2,9 +2,12 @@ package migris_test import ( "database/sql" + "errors" + "fmt" "testing" "github.com/akfaiz/migris" + "github.com/akfaiz/migris/schema" _ "github.com/mattn/go-sqlite3" "github.com/stretchr/testify/require" ) @@ -35,3 +38,80 @@ func TestNew_UnknownDialect(t *testing.T) { require.Error(t, err) require.Nil(t, m) } + +func TestMigrator_WithRegistryUsesIsolatedMigrations(t *testing.T) { + migris.ResetRegisteredMigrations() + db, err := sql.Open("sqlite3", ":memory:") + require.NoError(t, err) + defer db.Close() + + migris.AddNamedMigrationContext("20250101000100_create_global_table.go", func(ctx schema.Context) error { + return schema.Create(ctx, "global_table", func(t *schema.Blueprint) { + t.Increments("id") + }) + }, func(ctx schema.Context) error { + return schema.DropIfExists(ctx, "global_table") + }) + + registry := migris.NewRegistry() + require.NoError( + t, + registry.AddNamedMigrationContext("20250101000101_create_registry_table.go", func(ctx schema.Context) error { + return schema.Create(ctx, "registry_table", func(t *schema.Blueprint) { + t.Increments("id") + }) + }, func(ctx schema.Context) error { + return schema.DropIfExists(ctx, "registry_table") + }), + ) + + m, err := migris.New("sqlite3", migris.WithDB(db), migris.WithRegistry(registry)) + require.NoError(t, err) + require.NoError(t, m.Up()) + + var name string + require.NoError( + t, + db.QueryRow("SELECT name FROM sqlite_master WHERE type='table' AND name='registry_table'").Scan(&name), + ) + require.Equal(t, "registry_table", name) + require.Error( + t, + db.QueryRow("SELECT name FROM sqlite_master WHERE type='table' AND name='global_table'").Scan(&name), + ) +} + +func TestMigrator_UsesInstanceDialectDuringMigrationExecution(t *testing.T) { + migris.ResetRegisteredMigrations() + db, err := sql.Open("sqlite3", ":memory:") + require.NoError(t, err) + defer db.Close() + + registry := migris.NewRegistry() + require.NoError( + t, + registry.AddNamedMigrationContext("20250101000102_create_context_table.go", func(ctx schema.Context) error { + dialectContext, ok := ctx.(interface{ Dialect() string }) + if !ok { + return errors.New("expected migration context to expose dialect") + } + if dialectContext.Dialect() != "sqlite3" { + return fmt.Errorf("expected sqlite3 dialect, got %q", dialectContext.Dialect()) + } + return schema.Create(ctx, "context_table", func(t *schema.Blueprint) { + t.Increments("id") + }) + }, func(ctx schema.Context) error { + return schema.DropIfExists(ctx, "context_table") + }), + ) + + mSQLite, err := migris.New("sqlite3", migris.WithDB(db), migris.WithRegistry(registry)) + require.NoError(t, err) + + // Constructing a second migrator must not affect the first migrator's schema context. + _, err = migris.New("postgres", migris.WithDB(db)) + require.NoError(t, err) + + require.NoError(t, mSQLite.Up()) +} diff --git a/options.go b/options.go index a4da5c4..580b1fa 100644 --- a/options.go +++ b/options.go @@ -33,3 +33,12 @@ func WithDryRun(enabled bool) Option { m.dryRun = enabled } } + +// WithRegistry sets the migration registry for the migrator. +func WithRegistry(registry *Registry) Option { + return func(m *Migrate) { + if registry != nil { + m.registry = registry + } + } +} diff --git a/register.go b/register.go index e2ce8f0..7afebe1 100644 --- a/register.go +++ b/register.go @@ -6,15 +6,21 @@ import ( "fmt" "path" "runtime" + "sort" + "sync" + "github.com/akfaiz/migris/internal/dialect" "github.com/akfaiz/migris/schema" "github.com/pressly/goose/v3" ) -var ( - registeredVersions = make(map[int64]string) - registeredMigrations = make([]*Migration, 0) -) +type Registry struct { + mu sync.RWMutex + versions map[int64]string + migrations []*Migration +} + +var defaultRegistry = NewRegistry() type Migration struct { version int64 @@ -26,80 +32,103 @@ type Migration struct { // context. type MigrationContext func(ctx schema.Context) error -func (m MigrationContext) runTxFunc(source string) func(ctx context.Context, tx *sql.Tx) error { +// NewRegistry creates an isolated migration registry. +func NewRegistry() *Registry { + return &Registry{ + versions: make(map[int64]string), + migrations: make([]*Migration, 0), + } +} + +func (m MigrationContext) runTxFunc( + source string, + dialectVal dialect.Dialect, +) func(ctx context.Context, tx *sql.Tx) error { return func(ctx context.Context, tx *sql.Tx) error { filename := path.Base(source) - - // Check if we're in dry-run mode - isDryRun := getGlobalDryRunState() - - var c schema.Context - if isDryRun { - // Create dry-run context - c = schema.NewDryRunContext(ctx) - } else { - // Create regular context - c = schema.NewContext(ctx, tx, schema.WithFilename(filename)) - } + c := schema.NewContext(ctx, tx, + schema.WithFilename(filename), + schema.WithDialect(dialectVal.String()), + ) return m(c) } } -// AddMigrationContext adds Go migrations. +// AddMigrationContext adds Go migrations to the default registry. func AddMigrationContext(up, down MigrationContext) { _, filename, _, _ := runtime.Caller(1) AddNamedMigrationContext(filename, up, down) } -// AddNamedMigrationContext adds named Go migrations. +// AddNamedMigrationContext adds named Go migrations to the default registry. func AddNamedMigrationContext(source string, up, down MigrationContext) { - if err := register( - source, - up, - down, - ); err != nil { + if err := defaultRegistry.AddNamedMigrationContext(source, up, down); err != nil { panic(err) } } -func register(source string, up, down MigrationContext) error { +// AddMigrationContext adds Go migrations to the registry. +func (r *Registry) AddMigrationContext(up, down MigrationContext) { + _, filename, _, _ := runtime.Caller(1) + if err := r.AddNamedMigrationContext(filename, up, down); err != nil { + panic(err) + } +} + +// AddNamedMigrationContext adds named Go migrations to the registry. +func (r *Registry) AddNamedMigrationContext(source string, up, down MigrationContext) error { v, _ := goose.NumericComponent(source) - if existing, ok := registeredVersions[v]; ok { + + r.mu.Lock() + defer r.mu.Unlock() + + if existing, ok := r.versions[v]; ok { return fmt.Errorf("failed to add migration %q: version %d conflicts with %q", source, v, existing, ) } - // Add to global as a registered migration. m := &Migration{ version: v, source: source, upFnContext: up, downFnContext: down, } - registeredVersions[v] = source - registeredMigrations = append(registeredMigrations, m) + r.versions[v] = source + r.migrations = append(r.migrations, m) return nil } -// ResetRegisteredMigrations clears the global registry of migrations. +func (r *Registry) migrationsSnapshot() []*Migration { + r.mu.RLock() + defer r.mu.RUnlock() + + migrations := make([]*Migration, len(r.migrations)) + copy(migrations, r.migrations) + sort.SliceStable(migrations, func(i, j int) bool { + return migrations[i].version < migrations[j].version + }) + return migrations +} + +// ResetRegisteredMigrations clears the default global registry of migrations. // This is primarily intended for tests to avoid cross-test interference. func ResetRegisteredMigrations() { - registeredVersions = make(map[int64]string) - registeredMigrations = make([]*Migration, 0) + defaultRegistry = NewRegistry() } -func gooseMigrations() []*goose.Migration { +func (r *Registry) gooseMigrations(dialectVal dialect.Dialect) []*goose.Migration { + registeredMigrations := r.migrationsSnapshot() migrations := make([]*goose.Migration, 0, len(registeredMigrations)) for _, m := range registeredMigrations { upFunc := &goose.GoFunc{ - RunTx: m.upFnContext.runTxFunc(m.source), + RunTx: m.upFnContext.runTxFunc(m.source, dialectVal), Mode: goose.TransactionEnabled, } downFunc := &goose.GoFunc{ - RunTx: m.downFnContext.runTxFunc(m.source), + RunTx: m.downFnContext.runTxFunc(m.source, dialectVal), Mode: goose.TransactionEnabled, } gm := goose.NewGoMigration(m.version, upFunc, downFunc) diff --git a/register_test.go b/register_test.go new file mode 100644 index 0000000..462c7d0 --- /dev/null +++ b/register_test.go @@ -0,0 +1,45 @@ +package migris_test + +import ( + "testing" + + "github.com/akfaiz/migris" + "github.com/akfaiz/migris/schema" + "github.com/stretchr/testify/require" +) + +func TestAddMigrationContext(t *testing.T) { + migris.ResetRegisteredMigrations() + + require.NotPanics(t, func() { + migris.AddMigrationContext( + func(_ schema.Context) error { return nil }, + func(_ schema.Context) error { return nil }, + ) + }) + + require.Panics(t, func() { + migris.AddMigrationContext( + func(_ schema.Context) error { return nil }, + func(_ schema.Context) error { return nil }, + ) + }) +} + +func TestRegistry_AddMigrationContext(t *testing.T) { + r := migris.NewRegistry() + + require.NotPanics(t, func() { + r.AddMigrationContext( + func(_ schema.Context) error { return nil }, + func(_ schema.Context) error { return nil }, + ) + }) + + require.Panics(t, func() { + r.AddMigrationContext( + func(_ schema.Context) error { return nil }, + func(_ schema.Context) error { return nil }, + ) + }) +} diff --git a/schema/blueprint.go b/schema/blueprint.go deleted file mode 100644 index 2846a7f..0000000 --- a/schema/blueprint.go +++ /dev/null @@ -1,724 +0,0 @@ -package schema - -import ( - "fmt" - - "github.com/akfaiz/migris/internal/dialect" - "github.com/akfaiz/migris/internal/util" -) - -const ( - columnTypeBoolean string = "boolean" - columnTypeChar string = "char" - columnTypeString string = "string" - columnTypeLongText string = "longText" - columnTypeMediumText string = "mediumText" - columnTypeText string = "text" - columnTypeTinyText string = "tinyText" - columnTypeBigInteger string = "bigInteger" - columnTypeInteger string = "integer" - columnTypeMediumInteger string = "mediumInteger" - columnTypeSmallInteger string = "smallInteger" - columnTypeTinyInteger string = "tinyInteger" - columnTypeDecimal string = "decimal" - columnTypeDouble string = "double" - columnTypeFloat string = "float" - columnTypeDateTime string = "dateTime" - columnTypeDateTimeTz string = "dateTimeTz" - columnTypeDate string = "date" - columnTypeTime string = "time" - columnTypeTimeTz string = "timeTz" - columnTypeTimestamp string = "timestamp" - columnTypeTimestampTz string = "timestampTz" - columnTypeYear string = "year" - columnTypeBinary string = "binary" - columnTypeJSON string = "json" - columnTypeJSONB string = "jsonb" - columnTypeGeography string = "geography" - columnTypeGeometry string = "geometry" - columnTypePoint string = "point" - columnTypeUUID string = "uuid" - columnTypeEnum string = "enum" -) - -const ( - defaultStringLength int = 255 - defaultTimePrecision int = 0 -) - -// Blueprint represents a schema blueprint for creating or altering a database table. -type Blueprint struct { - dialect dialect.Dialect - columns []*columnDefinition - commands []*command - grammar grammar - name string - charset string - collation string - engine string -} - -// Charset sets the character set for the table in the blueprint. -func (b *Blueprint) Charset(charset string) { - b.charset = charset -} - -// Collation sets the collation for the table in the blueprint. -func (b *Blueprint) Collation(collation string) { - b.collation = collation -} - -// Engine sets the storage engine for the table in the blueprint. -func (b *Blueprint) Engine(engine string) { - b.engine = engine -} - -// Column creates a new custom column definition in the blueprint with the specified name and type. -func (b *Blueprint) Column(name string, columnType string) ColumnDefinition { - return b.addColumn(columnType, name) -} - -// Boolean creates a new boolean column definition in the blueprint. -func (b *Blueprint) Boolean(name string) ColumnDefinition { - return b.addColumn(columnTypeBoolean, name) -} - -// Char creates a new char column definition in the blueprint. -func (b *Blueprint) Char(name string, length ...int) ColumnDefinition { - return b.addColumn(columnTypeChar, name, &columnDefinition{ - length: util.OptionalPtr(defaultStringLength, length...), - }) -} - -// String creates a new string column definition in the blueprint. -func (b *Blueprint) String(name string, length ...int) ColumnDefinition { - return b.addColumn(columnTypeString, name, &columnDefinition{ - length: util.OptionalPtr(defaultStringLength, length...), - }) -} - -// LongText creates a new long text column definition in the blueprint. -func (b *Blueprint) LongText(name string) ColumnDefinition { - return b.addColumn(columnTypeLongText, name) -} - -// Text creates a new text column definition in the blueprint. -func (b *Blueprint) Text(name string) ColumnDefinition { - return b.addColumn(columnTypeText, name) -} - -// MediumText creates a new medium text column definition in the blueprint. -func (b *Blueprint) MediumText(name string) ColumnDefinition { - return b.addColumn(columnTypeMediumText, name) -} - -// TinyText creates a new tiny text column definition in the blueprint. -func (b *Blueprint) TinyText(name string) ColumnDefinition { - return b.addColumn(columnTypeTinyText, name) -} - -// BigIncrements creates a new big increments column definition in the blueprint. -func (b *Blueprint) BigIncrements(name string) ColumnDefinition { - return b.UnsignedBigInteger(name).AutoIncrement() -} - -// BigInteger creates a new big integer column definition in the blueprint. -func (b *Blueprint) BigInteger(name string) ColumnDefinition { - return b.addColumn(columnTypeBigInteger, name) -} - -// Decimal creates a new decimal column definition in the blueprint. -// -// The total and places parameters are optional. -// -// Example: -// -// table.Decimal("price", 10, 2) // creates a decimal column with total 10 and places 2 -// -// table.Decimal("price") // creates a decimal column with default total 8 and places 2 -func (b *Blueprint) Decimal(name string, params ...int) ColumnDefinition { - defaultPlaces := 2 - if len(params) > 1 { - defaultPlaces = params[1] - } - return b.addColumn(columnTypeDecimal, name, &columnDefinition{ - total: util.OptionalPtr(8, params...), - places: util.PtrOf(defaultPlaces), - }) -} - -// Double creates a new double column definition in the blueprint. -func (b *Blueprint) Double(name string) ColumnDefinition { - return b.addColumn(columnTypeDouble, name) -} - -// Float creates a new float column definition in the blueprint. -func (b *Blueprint) Float(name string, precision ...int) ColumnDefinition { - return b.addColumn(columnTypeFloat, name, &columnDefinition{ - precision: util.OptionalPtr(53, precision...), - }) -} - -// ID creates a new big increments column definition in the blueprint with the name "id" or a custom name. -// -// If a name is provided, it will be used as the column name; otherwise, "id" will be used. -func (b *Blueprint) ID(name ...string) ColumnDefinition { - return b.BigIncrements(util.Optional("id", name...)).Primary() -} - -// Increments create a new increment column definition in the blueprint. -func (b *Blueprint) Increments(name string) ColumnDefinition { - return b.UnsignedInteger(name).AutoIncrement() -} - -// Integer creates a new integer column definition in the blueprint. -func (b *Blueprint) Integer(name string) ColumnDefinition { - return b.addColumn(columnTypeInteger, name) -} - -// MediumIncrements creates a new medium increments column definition in the blueprint. -func (b *Blueprint) MediumIncrements(name string) ColumnDefinition { - return b.UnsignedMediumInteger(name).AutoIncrement() -} - -func (b *Blueprint) MediumInteger(name string) ColumnDefinition { - return b.addColumn(columnTypeMediumInteger, name) -} - -// SmallIncrements creates a new small increments column definition in the blueprint. -func (b *Blueprint) SmallIncrements(name string) ColumnDefinition { - return b.UnsignedSmallInteger(name).AutoIncrement() -} - -// SmallInteger creates a new small integer column definition in the blueprint. -func (b *Blueprint) SmallInteger(name string) ColumnDefinition { - return b.addColumn(columnTypeSmallInteger, name) -} - -// TinyIncrements creates a new tiny increments column definition in the blueprint. -func (b *Blueprint) TinyIncrements(name string) ColumnDefinition { - return b.UnsignedTinyInteger(name).AutoIncrement() -} - -// TinyInteger creates a new tiny integer column definition in the blueprint. -func (b *Blueprint) TinyInteger(name string) ColumnDefinition { - return b.addColumn(columnTypeTinyInteger, name) -} - -// UnsignedBigInteger creates a new unsigned big integer column definition in the blueprint. -func (b *Blueprint) UnsignedBigInteger(name string) ColumnDefinition { - return b.BigInteger(name).Unsigned() -} - -// UnsignedInteger creates a new unsigned integer column definition in the blueprint. -func (b *Blueprint) UnsignedInteger(name string) ColumnDefinition { - return b.Integer(name).Unsigned() -} - -// UnsignedMediumInteger creates a new unsigned medium integer column definition in the blueprint. -func (b *Blueprint) UnsignedMediumInteger(name string) ColumnDefinition { - return b.MediumInteger(name).Unsigned() -} - -// UnsignedSmallInteger creates a new unsigned small integer column definition in the blueprint. -func (b *Blueprint) UnsignedSmallInteger(name string) ColumnDefinition { - return b.SmallInteger(name).Unsigned() -} - -// UnsignedTinyInteger creates a new unsigned tiny integer column definition in the blueprint. -func (b *Blueprint) UnsignedTinyInteger(name string) ColumnDefinition { - return b.TinyInteger(name).Unsigned() -} - -// DateTime creates a new date time column definition in the blueprint. -func (b *Blueprint) DateTime(name string, precision ...int) ColumnDefinition { - return b.addColumn(columnTypeDateTime, name, &columnDefinition{ - precision: util.OptionalPtr(defaultTimePrecision, precision...), - }) -} - -// DateTimeTz creates a new date time with a time zone column definition in the blueprint. -func (b *Blueprint) DateTimeTz(name string, precision ...int) ColumnDefinition { - return b.addColumn(columnTypeDateTimeTz, name, &columnDefinition{ - precision: util.OptionalPtr(defaultTimePrecision, precision...), - }) -} - -// Date creates a new date column definition in the blueprint. -func (b *Blueprint) Date(name string) ColumnDefinition { - return b.addColumn(columnTypeDate, name) -} - -// Time creates a new time column definition in the blueprint. -func (b *Blueprint) Time(name string, precision ...int) ColumnDefinition { - return b.addColumn(columnTypeTime, name, &columnDefinition{ - precision: util.OptionalPtr(defaultTimePrecision, precision...), - }) -} - -// TimeTz creates a new time with time zone column definition in the blueprint. -func (b *Blueprint) TimeTz(name string, precision ...int) ColumnDefinition { - return b.addColumn(columnTypeTimeTz, name, &columnDefinition{ - precision: util.OptionalPtr(defaultTimePrecision, precision...), - }) -} - -// Timestamp creates a new timestamp column definition in the blueprint. -// The precision parameter is optional and defaults to 0 if not provided. -func (b *Blueprint) Timestamp(name string, precision ...int) ColumnDefinition { - return b.addColumn(columnTypeTimestamp, name, &columnDefinition{ - precision: util.OptionalPtr(defaultTimePrecision, precision...), - }) -} - -// TimestampTz creates a new timestamp with time zone column definition in the blueprint. -// The precision parameter is optional and defaults to 0 if not provided. -func (b *Blueprint) TimestampTz(name string, precision ...int) ColumnDefinition { - return b.addColumn(columnTypeTimestampTz, name, &columnDefinition{ - precision: util.OptionalPtr(defaultTimePrecision, precision...), - }) -} - -// Timestamps adds created_at and updated_at timestamp columns to the blueprint. -func (b *Blueprint) Timestamps(precision ...int) { - b.Timestamp("created_at", precision...).UseCurrent() - b.Timestamp("updated_at", precision...).UseCurrent().UseCurrentOnUpdate() -} - -// TimestampsTz adds created_at and updated_at timestamp with time zone columns to the blueprint. -func (b *Blueprint) TimestampsTz(precision ...int) { - b.TimestampTz("created_at", precision...).UseCurrent() - b.TimestampTz("updated_at", precision...).UseCurrent().UseCurrentOnUpdate() -} - -// Year creates a new year column definition in the blueprint. -func (b *Blueprint) Year(name string) ColumnDefinition { - return b.addColumn(columnTypeYear, name) -} - -// Binary creates a new binary column definition in the blueprint. -func (b *Blueprint) Binary(name string, length ...int) ColumnDefinition { - return b.addColumn(columnTypeBinary, name, &columnDefinition{ - length: util.OptionalNil(length...), - }) -} - -// JSON creates a new JSON column definition in the blueprint. -func (b *Blueprint) JSON(name string) ColumnDefinition { - return b.addColumn(columnTypeJSON, name) -} - -// JSONB creates a new JSONB column definition in the blueprint. -func (b *Blueprint) JSONB(name string) ColumnDefinition { - return b.addColumn(columnTypeJSONB, name) -} - -// UUID creates a new UUID column definition in the blueprint. -func (b *Blueprint) UUID(name string) ColumnDefinition { - return b.addColumn(columnTypeUUID, name) -} - -// Geography creates a new geography column definition in the blueprint. -// The subType parameter is optional and can be used to specify the type of geography (e.g., "Point", "LineString", "Polygon"). -// The srid parameter is optional and specifies the Spatial Reference Identifier (SRID) for the geography type. -func (b *Blueprint) Geography(name string, subtype string, srid ...int) ColumnDefinition { - return b.addColumn(columnTypeGeography, name, &columnDefinition{ - subtype: util.OptionalPtr("", subtype), - srid: util.OptionalPtr(4326, srid...), - }) -} - -// Geometry creates a new geometry column definition in the blueprint. -// The subType parameter is optional and can be used to specify the type of geometry (e.g., "Point", "LineString", "Polygon"). -// The srid parameter is optional and specifies the Spatial Reference Identifier (SRID) for the geometry type. -func (b *Blueprint) Geometry(name string, subtype string, srid ...int) ColumnDefinition { - return b.addColumn(columnTypeGeometry, name, &columnDefinition{ - subtype: util.OptionalPtr("", subtype), - srid: util.OptionalNil(srid...), - }) -} - -// Point creates a new point column definition in the blueprint. -func (b *Blueprint) Point(name string, srid ...int) ColumnDefinition { - return b.addColumn(columnTypePoint, name, &columnDefinition{ - srid: util.OptionalPtr(4326, srid...), - }) -} - -// Enum creates a new enum column definition in the blueprint. -// The allowedEnums parameter is a slice of strings that defines the allowed values for the enum column. -// -// Example: -// -// table.Enum("status", []string{"active", "inactive", "pending"}) -// table.Enum("role", []string{"admin", "user", "guest"}).Comment("User role in the system") -func (b *Blueprint) Enum(name string, allowed []string) ColumnDefinition { - return b.addColumn(columnTypeEnum, name, &columnDefinition{ - allowed: allowed, - }) -} - -// DropTimestamps removes the created_at and updated_at timestamp columns from the blueprint. -func (b *Blueprint) DropTimestamps() { - b.DropColumn("created_at", "updated_at") -} - -// DropTimestampsTz removes the created_at and updated_at timestamp with time zone columns from the blueprint. -func (b *Blueprint) DropTimestampsTz() { - b.DropTimestamps() -} - -// Index creates a new index definition in the blueprint. -// -// Example: -// -// table.Index("email") -// table.Index("email", "username") // creates a composite index -// table.Index("email").Algorithm("btree") // creates a btree index -func (b *Blueprint) Index(column string, otherColumns ...string) IndexDefinition { - return b.indexCommand(commandIndex, append([]string{column}, otherColumns...)...) -} - -// Unique creates a new unique index definition in the blueprint. -// -// Example: -// -// table.Unique("email") -// table.Unique("email", "username") // creates a composite unique index -func (b *Blueprint) Unique(column string, otherColumns ...string) IndexDefinition { - return b.indexCommand(commandUnique, append([]string{column}, otherColumns...)...) -} - -// Primary creates a new primary key index definition in the blueprint. -// -// Example: -// -// table.Primary("id") -// table.Primary("id", "email") // creates a composite primary key -func (b *Blueprint) Primary(column string, otherColumns ...string) IndexDefinition { - return b.indexCommand(commandPrimary, append([]string{column}, otherColumns...)...) -} - -// FullText creates a new fulltext index definition in the blueprint. -func (b *Blueprint) FullText(column string, otherColumns ...string) IndexDefinition { - return b.indexCommand(commandFullText, append([]string{column}, otherColumns...)...) -} - -// Foreign creates a new foreign key definition in the blueprint. -// -// Example: -// -// table.Foreign("user_id").References("id").On("users").OnDelete("CASCADE").OnUpdate("CASCADE") -func (b *Blueprint) Foreign(column string) ForeignKeyDefinition { - command := b.addCommand(commandForeign, &command{ - columns: []string{column}, - }) - return &foreignKeyDefinition{command: command} -} - -// DropColumn adds a column to be dropped from the table. -// -// Example: -// -// table.DropColumn("old_column") -// table.DropColumn("old_column", "another_old_column") // drops multiple columns -func (b *Blueprint) DropColumn(column string, otherColumns ...string) { - b.addCommand(commandDropColumn, &command{ - columns: append([]string{column}, otherColumns...), - }) -} - -// RenameColumn changes the name of the table in the blueprint. -// -// Example: -// -// table.RenameColumn("old_table_name", "new_table_name") -func (b *Blueprint) RenameColumn(oldColumn string, newColumn string) { - b.addCommand(commandRenameColumn, &command{ - from: oldColumn, - to: newColumn, - }) -} - -// DropIndex adds an index to be dropped from the table. -func (b *Blueprint) DropIndex(index any) { - b.dropIndexCommand(commandDropIndex, commandIndex, index) -} - -// DropForeign adds a foreign key to be dropped from the table. -func (b *Blueprint) DropForeign(index any) { - b.dropIndexCommand(commandDropForeign, commandForeign, index) -} - -// DropPrimary adds a primary key to be dropped from the table. -func (b *Blueprint) DropPrimary(index any) { - b.dropIndexCommand(commandDropPrimary, commandPrimary, index) -} - -// DropUnique adds a unique key to be dropped from the table. -func (b *Blueprint) DropUnique(index any) { - b.dropIndexCommand(commandDropUnique, commandUnique, index) -} - -func (b *Blueprint) DropFulltext(index any) { - b.dropIndexCommand(commandDropFullText, commandFullText, index) -} - -// RenameIndex changes the name of an index in the blueprint. -// Example: -// -// table.RenameIndex("old_index_name", "new_index_name") -func (b *Blueprint) RenameIndex(oldIndexName string, newIndexName string) { - b.addCommand(commandRenameIndex, &command{ - from: oldIndexName, - to: newIndexName, - }) -} - -func (b *Blueprint) getAddedColumns() []*columnDefinition { - var addedColumns []*columnDefinition - for _, col := range b.columns { - if !col.change { - addedColumns = append(addedColumns, col) - } - } - return addedColumns -} - -func (b *Blueprint) getChangedColumns() []*columnDefinition { - var changedColumns []*columnDefinition - for _, col := range b.columns { - if col.change { - changedColumns = append(changedColumns, col) - } - } - return changedColumns -} - -func (b *Blueprint) create() { - b.addCommand(commandCreate) -} - -func (b *Blueprint) creating() bool { - for _, command := range b.commands { - if command.name == commandCreate { - return true - } - } - return false -} - -func (b *Blueprint) drop() { - b.addCommand(commandDrop) -} - -func (b *Blueprint) dropIfExists() { - b.addCommand(commandDropIfExists) -} - -func (b *Blueprint) rename(to string) { - b.addCommand(commandRename, &command{ - to: to, - }) -} - -func (b *Blueprint) addImpliedCommands() { - b.addFluentIndexes() - - if !b.creating() { - if len(b.getAddedColumns()) > 0 { - b.commands = append([]*command{{name: commandAdd}}, b.commands...) - } - if len(b.getChangedColumns()) > 0 { - changedCommands := make([]*command, 0, len(b.getChangedColumns())) - for _, col := range b.getChangedColumns() { - changedCommands = append(changedCommands, &command{name: commandChange, column: col}) - } - b.commands = append(changedCommands, b.commands...) - } - } -} - -func (b *Blueprint) addFluentIndexes() { - for _, col := range b.columns { - skipped := b.addFluentIndexPrimary(col) - if skipped { - continue - } - b.addFluentIndexIndex(col) - b.addFluentIndexUnique(col) - } -} - -func (b *Blueprint) addFluentIndexPrimary(col *columnDefinition) bool { - if col.primary != nil { - if b.dialect == dialect.MySQL { - return true - } - if !*col.primary && col.change { - b.DropPrimary([]string{col.name}) - col.primary = nil - } - } - return false -} - -func (b *Blueprint) addFluentIndexIndex(col *columnDefinition) { - if col.index != nil { - if *col.index { - b.Index(col.name).Name(col.indexName) - col.index = nil - } else if !*col.index && col.change { - b.DropIndex([]string{col.name}) - col.index = nil - } - } -} - -func (b *Blueprint) addFluentIndexUnique(col *columnDefinition) { - if col.unique != nil { - if *col.unique { - b.Unique(col.name).Name(col.uniqueName) - col.unique = nil - } else if !*col.unique && col.change { - b.DropUnique([]string{col.name}) - col.unique = nil - } - } -} - -func (b *Blueprint) getFluentStatements() []string { - var statements []string - for _, column := range b.columns { - for _, fluentCommand := range b.grammar.GetFluentCommands() { - if statement := fluentCommand(b, &command{column: column}); statement != "" { - statements = append(statements, statement) - } - } - } - return statements -} - -func (b *Blueprint) build(ctx Context) error { - statements, err := b.toSQL() - if err != nil { - return err - } - for _, statement := range statements { - if _, err = ctx.Exec(statement); err != nil { - return err - } - } - return nil -} - -func (b *Blueprint) toSQL() ([]string, error) { - b.addImpliedCommands() - - var statements []string - - mainCommandMap := map[string]func(blueprint *Blueprint) (string, error){ - commandCreate: b.grammar.CompileCreate, - commandAdd: b.grammar.CompileAdd, - commandDrop: b.grammar.CompileDrop, - commandDropIfExists: b.grammar.CompileDropIfExists, - } - secondaryCommandMap := map[string]func(blueprint *Blueprint, command *command) (string, error){ - commandChange: b.grammar.CompileChange, - commandDropColumn: b.grammar.CompileDropColumn, - commandDropIndex: b.grammar.CompileDropIndex, - commandDropForeign: b.grammar.CompileDropForeign, - commandDropFullText: b.grammar.CompileDropFulltext, - commandDropPrimary: b.grammar.CompileDropPrimary, - commandDropUnique: b.grammar.CompileDropUnique, - commandForeign: b.grammar.CompileForeign, - commandFullText: b.grammar.CompileFullText, - commandIndex: b.grammar.CompileIndex, - commandPrimary: b.grammar.CompilePrimary, - commandRename: b.grammar.CompileRename, - commandRenameColumn: b.grammar.CompileRenameColumn, - commandRenameIndex: b.grammar.CompileRenameIndex, - commandUnique: b.grammar.CompileUnique, - } - for _, cmd := range b.commands { - if compileFunc, exists := mainCommandMap[cmd.name]; exists { - sql, err := compileFunc(b) - if err != nil { - return nil, err - } - if sql != "" { - statements = append(statements, sql) - } - continue - } - if compileFunc, exists := secondaryCommandMap[cmd.name]; exists { - sql, err := compileFunc(b, cmd) - if err != nil { - return nil, err - } - if sql != "" { - statements = append(statements, sql) - } - continue - } - return nil, fmt.Errorf("unknown command: %s", cmd.name) - } - - statements = append(statements, b.getFluentStatements()...) - - return statements, nil -} - -func (b *Blueprint) addColumn(colType string, name string, columnDefs ...*columnDefinition) *columnDefinition { - var col *columnDefinition - if len(columnDefs) > 0 { - col = columnDefs[0] - } else { - col = &columnDefinition{} - } - col.columnType = colType - col.name = name - - return b.addColumnDefinition(col) -} - -func (b *Blueprint) addColumnDefinition(col *columnDefinition) *columnDefinition { - b.columns = append(b.columns, col) - return col -} - -func (b *Blueprint) indexCommand(name string, columns ...string) IndexDefinition { - command := b.addCommand(name, &command{ - columns: columns, - }) - return &indexDefinition{command} -} - -func (b *Blueprint) dropIndexCommand(name string, indexType string, index any) { - switch index := index.(type) { - case string: - b.addCommand(name, &command{ - index: index, - }) - case []string: - indexName := b.grammar.CreateIndexName(b, indexType, index...) - b.addCommand(name, &command{ - index: indexName, - }) - default: - panic(fmt.Sprintf("unsupported index type: %T", index)) - } -} - -func (b *Blueprint) addCommand(name string, parameters ...*command) *command { - var parameter *command - if len(parameters) > 0 { - parameter = parameters[0] - } else { - parameter = &command{} - } - parameter.name = name - b.commands = append(b.commands, parameter) - - return parameter -} diff --git a/schema/blueprint/blueprint.go b/schema/blueprint/blueprint.go new file mode 100644 index 0000000..7bafdc5 --- /dev/null +++ b/schema/blueprint/blueprint.go @@ -0,0 +1,760 @@ +package blueprint + +import ( + "fmt" + + "github.com/akfaiz/migris/internal/dialect" + "github.com/akfaiz/migris/internal/util" + "github.com/akfaiz/migris/schema/core" +) + +const ( + ColumnTypeBoolean string = "boolean" + ColumnTypeChar string = "char" + ColumnTypeString string = "string" + ColumnTypeLongText string = "longText" + ColumnTypeMediumText string = "mediumText" + ColumnTypeText string = "text" + ColumnTypeTinyText string = "tinyText" + ColumnTypeBigInteger string = "bigInteger" + ColumnTypeInteger string = "integer" + ColumnTypeMediumInteger string = "mediumInteger" + ColumnTypeSmallInteger string = "smallInteger" + ColumnTypeTinyInteger string = "tinyInteger" + ColumnTypeDecimal string = "decimal" + ColumnTypeDouble string = "double" + ColumnTypeFloat string = "float" + ColumnTypeDateTime string = "dateTime" + ColumnTypeDateTimeTz string = "dateTimeTz" + ColumnTypeDate string = "date" + ColumnTypeTime string = "time" + ColumnTypeTimeTz string = "timeTz" + ColumnTypeTimestamp string = "timestamp" + ColumnTypeTimestampTz string = "timestampTz" + ColumnTypeYear string = "year" + ColumnTypeBinary string = "binary" + ColumnTypeJSON string = "json" + ColumnTypeJSONB string = "jsonb" + ColumnTypeGeography string = "geography" + ColumnTypeGeometry string = "geometry" + ColumnTypePoint string = "point" + ColumnTypeUUID string = "uuid" + ColumnTypeULID string = "ulid" + ColumnTypeEnum string = "enum" + ColumnTypeSet string = "set" + ColumnTypeIPAddress string = "ipAddress" + ColumnTypeMacAddress string = "macAddress" + ColumnTypeVector string = "vector" + ColumnTypeTSVector string = "tsvector" + ColumnTypeCidr string = "cidr" + ColumnTypeInet string = "inet" + ColumnTypeMacaddr string = "macaddr" + ColumnTypeMacaddr8 string = "macaddr8" +) + +const ( + defaultStringLength int = 255 + defaultTimePrecision int = 0 +) + +// Blueprint represents a schema blueprint for creating or altering a database table. +type Blueprint struct { + Dialect dialect.Dialect + Columns []*Column + Commands []*Command + Grammar Grammar + Builder Builder + Name string + CharsetVal string + CollationVal string + EngineVal string + CommentVal string + AutoIncrementStartingValuesVal *int +} + +// Builder interface. +type Builder interface { + GetColumns(ctx core.Context, table string) ([]*core.Column, error) +} + +// NewBlueprintForTesting creates a new blueprint for testing purposes. +func NewBlueprintForTesting(name string, g Grammar) *Blueprint { + return &Blueprint{Name: name, Grammar: g} +} + +func (b *Blueprint) SetDialect(d dialect.Dialect) { + b.Dialect = d +} + +func (b *Blueprint) SetBuilder(builder Builder) { + b.Builder = builder +} + +func (b *Blueprint) SetGrammar(g Grammar) { + b.Grammar = g +} + +func (b *Blueprint) SetName(name string) { + b.Name = name +} + +func (b *Blueprint) Charset(charset string) { + b.CharsetVal = charset +} + +func (b *Blueprint) Collation(collation string) { + b.CollationVal = collation +} + +func (b *Blueprint) Engine(engine string) { + b.EngineVal = engine +} + +func (b *Blueprint) Comment(comment string) { + b.CommentVal = comment +} + +func (b *Blueprint) AutoIncrementStartingValues(value int) { + b.AutoIncrementStartingValuesVal = &value +} + +func (b *Blueprint) Column(name string, columnType string) ColumnDefinition { + return b.addColumn(columnType, name) +} + +func (b *Blueprint) Boolean(name string) ColumnDefinition { + return b.addColumn(ColumnTypeBoolean, name) +} + +func (b *Blueprint) Char(name string, length ...int) ColumnDefinition { + return b.addColumn(ColumnTypeChar, name, &Column{ + Length: util.OptionalPtr(defaultStringLength, length...), + }) +} + +func (b *Blueprint) String(name string, length ...int) ColumnDefinition { + return b.addColumn(ColumnTypeString, name, &Column{ + Length: util.OptionalPtr(defaultStringLength, length...), + }) +} + +func (b *Blueprint) LongText(name string) ColumnDefinition { + return b.addColumn(ColumnTypeLongText, name) +} + +func (b *Blueprint) Text(name string) ColumnDefinition { + return b.addColumn(ColumnTypeText, name) +} + +func (b *Blueprint) MediumText(name string) ColumnDefinition { + return b.addColumn(ColumnTypeMediumText, name) +} + +func (b *Blueprint) TinyText(name string) ColumnDefinition { + return b.addColumn(ColumnTypeTinyText, name) +} + +func (b *Blueprint) BigIncrements(name string) ColumnDefinition { + return b.UnsignedBigInteger(name).AutoIncrement() +} + +func (b *Blueprint) BigInteger(name string) ColumnDefinition { + return b.addColumn(ColumnTypeBigInteger, name) +} + +func (b *Blueprint) Decimal(name string, params ...int) ColumnDefinition { + defaultPlaces := 2 + if len(params) > 1 { + defaultPlaces = params[1] + } + return b.addColumn(ColumnTypeDecimal, name, &Column{ + Total: util.OptionalPtr(8, params...), + Places: util.PtrOf(defaultPlaces), + }) +} + +func (b *Blueprint) Double(name string) ColumnDefinition { + return b.addColumn(ColumnTypeDouble, name) +} + +func (b *Blueprint) Float(name string, precision ...int) ColumnDefinition { + return b.addColumn(ColumnTypeFloat, name, &Column{ + Precision: util.OptionalPtr(53, precision...), + }) +} + +func (b *Blueprint) ID(name ...string) ColumnDefinition { + return b.BigIncrements(util.Optional("id", name...)).Primary() +} + +func (b *Blueprint) Increments(name string) ColumnDefinition { + return b.UnsignedInteger(name).AutoIncrement() +} + +func (b *Blueprint) Integer(name string) ColumnDefinition { + return b.addColumn(ColumnTypeInteger, name) +} + +func (b *Blueprint) MediumIncrements(name string) ColumnDefinition { + return b.UnsignedMediumInteger(name).AutoIncrement() +} + +func (b *Blueprint) MediumInteger(name string) ColumnDefinition { + return b.addColumn(ColumnTypeMediumInteger, name) +} + +func (b *Blueprint) SmallIncrements(name string) ColumnDefinition { + return b.UnsignedSmallInteger(name).AutoIncrement() +} + +func (b *Blueprint) SmallInteger(name string) ColumnDefinition { + return b.addColumn(ColumnTypeSmallInteger, name) +} + +func (b *Blueprint) TinyIncrements(name string) ColumnDefinition { + return b.UnsignedTinyInteger(name).AutoIncrement() +} + +func (b *Blueprint) TinyInteger(name string) ColumnDefinition { + return b.addColumn(ColumnTypeTinyInteger, name) +} + +func (b *Blueprint) UnsignedBigInteger(name string) ColumnDefinition { + return b.BigInteger(name).Unsigned() +} + +func (b *Blueprint) UnsignedInteger(name string) ColumnDefinition { + return b.Integer(name).Unsigned() +} + +func (b *Blueprint) UnsignedMediumInteger(name string) ColumnDefinition { + return b.MediumInteger(name).Unsigned() +} + +func (b *Blueprint) UnsignedSmallInteger(name string) ColumnDefinition { + return b.SmallInteger(name).Unsigned() +} + +func (b *Blueprint) UnsignedTinyInteger(name string) ColumnDefinition { + return b.TinyInteger(name).Unsigned() +} + +func (b *Blueprint) DateTime(name string, precision ...int) ColumnDefinition { + return b.addColumn(ColumnTypeDateTime, name, &Column{ + Precision: util.OptionalPtr(defaultTimePrecision, precision...), + }) +} + +func (b *Blueprint) DateTimeTz(name string, precision ...int) ColumnDefinition { + return b.addColumn(ColumnTypeDateTimeTz, name, &Column{ + Precision: util.OptionalPtr(defaultTimePrecision, precision...), + }) +} + +func (b *Blueprint) Date(name string) ColumnDefinition { + return b.addColumn(ColumnTypeDate, name) +} + +func (b *Blueprint) Time(name string, precision ...int) ColumnDefinition { + return b.addColumn(ColumnTypeTime, name, &Column{ + Precision: util.OptionalPtr(defaultTimePrecision, precision...), + }) +} + +func (b *Blueprint) TimeTz(name string, precision ...int) ColumnDefinition { + return b.addColumn(ColumnTypeTimeTz, name, &Column{ + Precision: util.OptionalPtr(defaultTimePrecision, precision...), + }) +} + +func (b *Blueprint) Timestamp(name string, precision ...int) ColumnDefinition { + return b.addColumn(ColumnTypeTimestamp, name, &Column{ + Precision: util.OptionalPtr(defaultTimePrecision, precision...), + }) +} + +func (b *Blueprint) TimestampTz(name string, precision ...int) ColumnDefinition { + return b.addColumn(ColumnTypeTimestampTz, name, &Column{ + Precision: util.OptionalPtr(defaultTimePrecision, precision...), + }) +} + +func (b *Blueprint) Timestamps(precision ...int) { + b.Timestamp("created_at", precision...).UseCurrent() + b.Timestamp("updated_at", precision...).UseCurrent().UseCurrentOnUpdate() +} + +func (b *Blueprint) TimestampsTz(precision ...int) { + b.TimestampTz("created_at", precision...).UseCurrent() + b.TimestampTz("updated_at", precision...).UseCurrent().UseCurrentOnUpdate() +} + +func (b *Blueprint) Year(name string) ColumnDefinition { + return b.addColumn(ColumnTypeYear, name) +} + +func (b *Blueprint) Binary(name string, length ...int) ColumnDefinition { + return b.addColumn(ColumnTypeBinary, name, &Column{ + Length: util.OptionalNil(length...), + }) +} + +func (b *Blueprint) JSON(name string) ColumnDefinition { + return b.addColumn(ColumnTypeJSON, name) +} + +func (b *Blueprint) JSONB(name string) ColumnDefinition { + return b.addColumn(ColumnTypeJSONB, name) +} + +func (b *Blueprint) UUID(name string) ColumnDefinition { + return b.addColumn(ColumnTypeUUID, name) +} + +func (b *Blueprint) ULID(name string) ColumnDefinition { + return b.addColumn(ColumnTypeULID, name) +} + +func (b *Blueprint) Geography(name string, subtype string, srid ...int) ColumnDefinition { + return b.addColumn(ColumnTypeGeography, name, &Column{ + Subtype: util.OptionalPtr("", subtype), + Srid: util.OptionalPtr(4326, srid...), + }) +} + +func (b *Blueprint) Geometry(name string, subtype string, srid ...int) ColumnDefinition { + return b.addColumn(ColumnTypeGeometry, name, &Column{ + Subtype: util.OptionalPtr("", subtype), + Srid: util.OptionalNil(srid...), + }) +} + +func (b *Blueprint) Point(name string, srid ...int) ColumnDefinition { + return b.addColumn(ColumnTypePoint, name, &Column{ + Srid: util.OptionalPtr(4326, srid...), + }) +} + +func (b *Blueprint) Enum(name string, allowed []string) ColumnDefinition { + return b.addColumn(ColumnTypeEnum, name, &Column{ + Allowed: allowed, + }) +} + +func (b *Blueprint) Set(name string, allowed []string) ColumnDefinition { + return b.addColumn(ColumnTypeSet, name, &Column{ + Allowed: allowed, + }) +} + +func (b *Blueprint) IPAddress(name string) ColumnDefinition { + return b.addColumn(ColumnTypeIPAddress, name) +} + +func (b *Blueprint) MacAddress(name string) ColumnDefinition { + return b.addColumn(ColumnTypeMacAddress, name) +} + +func (b *Blueprint) Vector(name string, dimensions ...int) ColumnDefinition { + return b.addColumn(ColumnTypeVector, name, &Column{ + Places: util.OptionalNil(dimensions...), + }) +} + +func (b *Blueprint) TSVector(name string) ColumnDefinition { + return b.addColumn(ColumnTypeTSVector, name) +} + +func (b *Blueprint) Cidr(name string) ColumnDefinition { + return b.addColumn(ColumnTypeCidr, name) +} + +func (b *Blueprint) Inet(name string) ColumnDefinition { + return b.addColumn(ColumnTypeInet, name) +} + +func (b *Blueprint) MacAddr(name string) ColumnDefinition { + return b.addColumn(ColumnTypeMacaddr, name) +} + +func (b *Blueprint) MacAddr8(name string) ColumnDefinition { + return b.addColumn(ColumnTypeMacaddr8, name) +} + +func (b *Blueprint) DropTimestamps() { + b.DropColumn("created_at", "updated_at") +} + +func (b *Blueprint) DropTimestampsTz() { + b.DropTimestamps() +} + +func (b *Blueprint) Index(column string, otherColumns ...string) IndexDefinition { + return b.indexCommand(CommandIndex, append([]string{column}, otherColumns...)...) +} + +func (b *Blueprint) Unique(column string, otherColumns ...string) IndexDefinition { + return b.indexCommand(CommandUnique, append([]string{column}, otherColumns...)...) +} + +func (b *Blueprint) Primary(column string, otherColumns ...string) IndexDefinition { + return b.indexCommand(CommandPrimary, append([]string{column}, otherColumns...)...) +} + +func (b *Blueprint) FullText(column string, otherColumns ...string) IndexDefinition { + return b.indexCommand(CommandFullText, append([]string{column}, otherColumns...)...) +} + +func (b *Blueprint) Foreign(column string) ForeignKeyDefinition { + return b.ForeignColumns(column) +} + +func (b *Blueprint) ForeignColumns(columns ...string) ForeignKeyDefinition { + command := b.addCommand(CommandForeign, &Command{ + Columns: columns, + }) + return &foreignKeyDefinition{command: command} +} + +func (b *Blueprint) DropColumn(column string, otherColumns ...string) { + b.addCommand(CommandDropColumn, &Command{ + Columns: append([]string{column}, otherColumns...), + }) +} + +func (b *Blueprint) RenameColumn(oldColumn string, newColumn string) { + b.addCommand(CommandRenameColumn, &Command{ + From: oldColumn, + To: newColumn, + }) +} + +func (b *Blueprint) DropIndex(index any) { + b.dropIndexCommand(CommandDropIndex, CommandIndex, index) +} + +func (b *Blueprint) DropForeign(index any) { + b.dropIndexCommand(CommandDropForeign, CommandForeign, index) +} + +func (b *Blueprint) DropPrimary(index any) { + b.dropIndexCommand(CommandDropPrimary, CommandPrimary, index) +} + +func (b *Blueprint) DropUnique(index any) { + b.dropIndexCommand(CommandDropUnique, CommandUnique, index) +} + +func (b *Blueprint) DropFulltext(index any) { + b.dropIndexCommand(CommandDropFullText, CommandFullText, index) +} + +func (b *Blueprint) RenameIndex(oldIndexName string, newIndexName string) { + b.addCommand(CommandRenameIndex, &Command{ + From: oldIndexName, + To: newIndexName, + }) +} + +func (b *Blueprint) GetAddedColumns() []*Column { + var addedColumns []*Column + for _, col := range b.Columns { + if !col.ChangeVal { + addedColumns = append(addedColumns, col) + } + } + return addedColumns +} + +func (b *Blueprint) GetChangedColumns() []*Column { + var changedColumns []*Column + for _, col := range b.Columns { + if col.ChangeVal { + changedColumns = append(changedColumns, col) + } + } + return changedColumns +} + +func (b *Blueprint) Create() { + b.addCommand(CommandCreate) +} + +func (b *Blueprint) IsCreating() bool { + for _, command := range b.Commands { + if command.Name == CommandCreate { + return true + } + } + return false +} + +func (b *Blueprint) Drop() { + b.addCommand(CommandDrop) +} + +func (b *Blueprint) DropIfExists() { + b.addCommand(CommandDropIfExists) +} + +func (b *Blueprint) Rename(to string) { + b.addCommand(CommandRename, &Command{ + To: to, + }) +} + +func (b *Blueprint) AddImpliedCommands() { + b.addFluentIndexes() + + if !b.IsCreating() { + if len(b.GetAddedColumns()) > 0 { + b.Commands = append([]*Command{{Name: CommandAdd}}, b.Commands...) + } + if len(b.GetChangedColumns()) > 0 { + changedCommands := make([]*Command, 0, len(b.GetChangedColumns())) + for _, col := range b.GetChangedColumns() { + changedCommands = append(changedCommands, &Command{Name: CommandChange, Column: col}) + } + b.Commands = append(changedCommands, b.Commands...) + } + } +} + +func (b *Blueprint) addFluentIndexes() { + for _, col := range b.Columns { + skipped := b.addFluentIndexPrimary(col) + if skipped { + continue + } + b.addFluentIndexIndex(col) + b.addFluentIndexUnique(col) + } +} + +func (b *Blueprint) addFluentIndexPrimary(col *Column) bool { + if col.PrimaryVal != nil { + if b.Dialect == dialect.MySQL { + return true + } + if !*col.PrimaryVal && col.ChangeVal { + b.DropPrimary([]string{col.Name}) + col.PrimaryVal = nil + } + } + return false +} + +func (b *Blueprint) addFluentIndexIndex(col *Column) { + if col.IndexVal != nil { + if *col.IndexVal { + b.Index(col.Name).Name(col.IndexName) + col.IndexVal = nil + } else if !*col.IndexVal && col.ChangeVal { + b.DropIndex([]string{col.Name}) + col.IndexVal = nil + } + } +} + +func (b *Blueprint) addFluentIndexUnique(col *Column) { + if col.UniqueVal != nil { + if *col.UniqueVal { + b.Unique(col.Name).Name(col.UniqueName) + col.UniqueVal = nil + } else if !*col.UniqueVal && col.ChangeVal { + b.DropUnique([]string{col.Name}) + col.UniqueVal = nil + } + } +} + +func (b *Blueprint) GetFluentStatements() []string { + var statements []string + for _, column := range b.Columns { + for _, fluentCommand := range b.Grammar.GetFluentCommands() { + if statement := fluentCommand(b, &Command{Column: column}); statement != "" { + statements = append(statements, statement) + } + } + } + + for _, tableFluentCommand := range b.Grammar.GetTableFluentCommands() { + if statement := tableFluentCommand(b); statement != "" { + statements = append(statements, statement) + } + } + + return statements +} + +func (b *Blueprint) Build(ctx core.Context) error { + if err := b.hydrate(ctx); err != nil { + return err + } + + statements, err := b.ToSQL() + if err != nil { + return err + } + + for _, statement := range statements { + if _, execErr := ctx.Exec(statement); execErr != nil { + return execErr + } + } + + return nil +} + +func (b *Blueprint) hydrate(ctx core.Context) error { + changedColumns := b.GetChangedColumns() + if len(changedColumns) == 0 { + return nil + } + + if b.Builder == nil { + return nil + } + + existingColumns, err := b.Builder.GetColumns(ctx, b.Name) + if err != nil { + return err + } + + columnsMap := make(map[string]*core.Column) + for _, col := range existingColumns { + columnsMap[col.Name] = col + } + + for _, colDef := range changedColumns { + if existing, ok := columnsMap[colDef.Name]; ok { + b.mergeColumnMetadata(colDef, existing) + } + } + return nil +} + +func (b *Blueprint) mergeColumnMetadata(colDef *Column, existing *core.Column) { + if !colDef.HasCommand("nullable") { + colDef.Nullable(existing.Nullable) + } + + if !colDef.HasCommand("default") && existing.DefaultVal.Valid { + colDef.Default(Expression(existing.DefaultVal.String)) + } + + if !colDef.HasCommand("comment") && existing.Comment.Valid { + colDef.Comment(existing.Comment.String) + } +} + +func (b *Blueprint) ToSQL() ([]string, error) { + b.AddImpliedCommands() + + var statements []string + + mainCommandMap := map[string]func(blueprint *Blueprint) (string, error){ + CommandCreate: b.Grammar.CompileCreate, + CommandAdd: b.Grammar.CompileAdd, + CommandDrop: b.Grammar.CompileDrop, + CommandDropIfExists: b.Grammar.CompileDropIfExists, + } + secondaryCommandMap := map[string]func(blueprint *Blueprint, command *Command) (string, error){ + CommandChange: b.Grammar.CompileChange, + CommandDropColumn: b.Grammar.CompileDropColumn, + CommandDropIndex: b.Grammar.CompileDropIndex, + CommandDropForeign: b.Grammar.CompileDropForeign, + CommandDropFullText: b.Grammar.CompileDropFulltext, + CommandDropPrimary: b.Grammar.CompileDropPrimary, + CommandDropUnique: b.Grammar.CompileDropUnique, + CommandForeign: b.Grammar.CompileForeign, + CommandFullText: b.Grammar.CompileFullText, + CommandIndex: b.Grammar.CompileIndex, + CommandPrimary: b.Grammar.CompilePrimary, + CommandRename: b.Grammar.CompileRename, + CommandRenameColumn: b.Grammar.CompileRenameColumn, + CommandRenameIndex: b.Grammar.CompileRenameIndex, + CommandUnique: b.Grammar.CompileUnique, + } + for _, cmd := range b.Commands { + if compileFunc, exists := mainCommandMap[cmd.Name]; exists { + sql, err := compileFunc(b) + if err != nil { + return nil, err + } + if sql != "" { + statements = append(statements, sql) + } + continue + } + if compileFunc, exists := secondaryCommandMap[cmd.Name]; exists { + sql, err := compileFunc(b, cmd) + if err != nil { + return nil, err + } + if sql != "" { + statements = append(statements, sql) + } + continue + } + return nil, fmt.Errorf("unknown command: %s", cmd.Name) + } + + statements = append(statements, b.GetFluentStatements()...) + + return statements, nil +} + +func (b *Blueprint) addColumn(colType string, name string, columnDefs ...*Column) *Column { + var col *Column + if len(columnDefs) > 0 { + col = columnDefs[0] + } else { + col = &Column{} + } + col.ColumnType = colType + col.Name = name + + return b.addColumnDefinition(col) +} + +func (b *Blueprint) addColumnDefinition(col *Column) *Column { + b.Columns = append(b.Columns, col) + return col +} + +func (b *Blueprint) indexCommand(name string, columns ...string) IndexDefinition { + command := b.addCommand(name, &Command{ + Columns: columns, + }) + return &indexDefinition{command: command} +} + +func (b *Blueprint) dropIndexCommand(name string, indexType string, index any) { + switch index := index.(type) { + case string: + b.addCommand(name, &Command{ + Index: index, + }) + case []string: + indexName := b.Grammar.CreateIndexName(b, indexType, index...) + b.addCommand(name, &Command{ + Index: indexName, + }) + default: + panic(fmt.Sprintf("unsupported index type: %T", index)) + } +} + +func (b *Blueprint) addCommand(name string, parameters ...*Command) *Command { + var parameter *Command + if len(parameters) > 0 { + parameter = parameters[0] + } else { + parameter = &Command{} + } + parameter.Name = name + b.Commands = append(b.Commands, parameter) + + return parameter +} diff --git a/schema/blueprint/blueprint_test.go b/schema/blueprint/blueprint_test.go new file mode 100644 index 0000000..8b98ea2 --- /dev/null +++ b/schema/blueprint/blueprint_test.go @@ -0,0 +1,357 @@ +package blueprint_test + +import ( + "context" + "database/sql" + "testing" + + "github.com/akfaiz/migris/internal/dialect" + "github.com/akfaiz/migris/schema/blueprint" + "github.com/akfaiz/migris/schema/core" + _ "github.com/mattn/go-sqlite3" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type mockGrammar struct { + blueprint.BaseGrammar +} + +func (m *mockGrammar) CompileTableExists(_ string, _ string) (string, error) { return "", nil } +func (m *mockGrammar) CompileTables(_ string) (string, error) { return "", nil } +func (m *mockGrammar) CompileColumns(_, _ string) (string, error) { return "", nil } +func (m *mockGrammar) CompileIndexes(_, _ string) (string, error) { return "", nil } +func (m *mockGrammar) CompileCreate(_ *blueprint.Blueprint) (string, error) { return "SELECT 1", nil } +func (m *mockGrammar) CompileAdd(_ *blueprint.Blueprint) (string, error) { return "SELECT 1", nil } +func (m *mockGrammar) CompileDrop(_ *blueprint.Blueprint) (string, error) { return "SELECT 1", nil } +func (m *mockGrammar) CompileDropIfExists(_ *blueprint.Blueprint) (string, error) { + return "SELECT 1", nil +} +func (m *mockGrammar) CompileRename(_ *blueprint.Blueprint, _ *blueprint.Command) (string, error) { + return "SELECT 1", nil +} +func (m *mockGrammar) CompileChange(_ *blueprint.Blueprint, _ *blueprint.Command) (string, error) { + return "SELECT 1", nil +} +func (m *mockGrammar) GetType(_ *blueprint.Column) string { return "TEXT" } + +func TestBlueprint_TableSettings(t *testing.T) { + bp := blueprint.NewBlueprintForTesting("users", &mockGrammar{}) + + bp.Charset("utf8mb4") + bp.Collation("utf8mb4_unicode_ci") + bp.Engine("InnoDB") + bp.Comment("User table") + bp.AutoIncrementStartingValues(100) + + assert.Equal(t, "utf8mb4", bp.CharsetVal) + assert.Equal(t, "utf8mb4_unicode_ci", bp.CollationVal) + assert.Equal(t, "InnoDB", bp.EngineVal) + assert.Equal(t, "User table", bp.CommentVal) + assert.Equal(t, 100, *bp.AutoIncrementStartingValuesVal) +} + +func TestBlueprint_ColumnAddition(t *testing.T) { + bp := blueprint.NewBlueprintForTesting("users", &mockGrammar{}) + + bp.ID() + bp.String("email", 100).Unique().Nullable() + bp.Integer("age").Unsigned().Default(18) + bp.Boolean("active").Default(true) + bp.Timestamps() + + columns := bp.Columns + require.Len(t, columns, 6) + + assert.Equal(t, "id", columns[0].Name) + assert.Equal(t, blueprint.ColumnTypeBigInteger, columns[0].ColumnType) + assert.True(t, *columns[0].AutoIncrementVal) + assert.True(t, *columns[0].PrimaryVal) + + assert.Equal(t, "email", columns[1].Name) + assert.Equal(t, blueprint.ColumnTypeString, columns[1].ColumnType) + assert.Equal(t, 100, *columns[1].Length) + assert.True(t, *columns[1].UniqueVal) + assert.True(t, *columns[1].NullableVal) + + assert.Equal(t, "age", columns[2].Name) + assert.Equal(t, blueprint.ColumnTypeInteger, columns[2].ColumnType) + assert.True(t, *columns[2].UnsignedVal) + assert.Equal(t, 18, columns[2].DefaultValue) + + assert.Equal(t, "active", columns[3].Name) + assert.Equal(t, blueprint.ColumnTypeBoolean, columns[3].ColumnType) + assert.Equal(t, true, columns[3].DefaultValue) + + assert.Equal(t, "created_at", columns[4].Name) + assert.Equal(t, blueprint.ColumnTypeTimestamp, columns[4].ColumnType) + assert.True(t, columns[4].UseCurrentVal) + + assert.Equal(t, "updated_at", columns[5].Name) + assert.Equal(t, blueprint.ColumnTypeTimestamp, columns[5].ColumnType) + assert.True(t, columns[5].UseCurrentVal) + assert.True(t, columns[5].UseCurrentOnUpdateVal) +} + +func TestBlueprint_Indexes(t *testing.T) { + bp := blueprint.NewBlueprintForTesting("users", &mockGrammar{}) + + bp.Index("email", "username").Name("idx_email_username").Algorithm("btree") + bp.Unique("phone").Name("uk_phone") + bp.Primary("id") + bp.FullText("bio") + + commands := bp.Commands + require.Len(t, commands, 4) + + assert.Equal(t, blueprint.CommandIndex, commands[0].Name) + assert.Equal(t, []string{"email", "username"}, commands[0].Columns) + assert.Equal(t, "idx_email_username", commands[0].Index) + assert.Equal(t, "btree", commands[0].Algorithm) + + assert.Equal(t, blueprint.CommandUnique, commands[1].Name) + assert.Equal(t, []string{"phone"}, commands[1].Columns) + assert.Equal(t, "uk_phone", commands[1].Index) + + assert.Equal(t, blueprint.CommandPrimary, commands[2].Name) + assert.Equal(t, []string{"id"}, commands[2].Columns) + + assert.Equal(t, blueprint.CommandFullText, commands[3].Name) + assert.Equal(t, []string{"bio"}, commands[3].Columns) +} + +func TestBlueprint_ForeignKeys(t *testing.T) { + bp := blueprint.NewBlueprintForTesting("posts", &mockGrammar{}) + + bp.Foreign("user_id").References("id").On("users").OnDelete("CASCADE").OnUpdate("RESTRICT") + + commands := bp.Commands + require.Len(t, commands, 1) + + assert.Equal(t, blueprint.CommandForeign, commands[0].Name) + assert.Equal(t, []string{"user_id"}, commands[0].Columns) + assert.Equal(t, []string{"id"}, commands[0].References) + assert.Equal(t, "users", commands[0].On) + assert.Equal(t, "CASCADE", commands[0].OnDelete) + assert.Equal(t, "RESTRICT", commands[0].OnUpdate) +} + +func TestBlueprint_DropOperations(t *testing.T) { + bp := blueprint.NewBlueprintForTesting("users", &mockGrammar{}) + + bp.DropColumn("age", "active") + bp.DropIndex("idx_email") + bp.DropUnique("uk_phone") + bp.DropPrimary([]string{"id"}) + bp.DropForeign("fk_user_id") + + commands := bp.Commands + require.Len(t, commands, 5) + + assert.Equal(t, blueprint.CommandDropColumn, commands[0].Name) + assert.Equal(t, []string{"age", "active"}, commands[0].Columns) + + assert.Equal(t, blueprint.CommandDropIndex, commands[1].Name) + assert.Equal(t, "idx_email", commands[1].Index) + + assert.Equal(t, blueprint.CommandDropUnique, commands[2].Name) + assert.Equal(t, "uk_phone", commands[2].Index) + + assert.Equal(t, blueprint.CommandDropPrimary, commands[3].Name) + assert.Equal(t, "users_id_primary", commands[3].Index) + + assert.Equal(t, blueprint.CommandDropForeign, commands[4].Name) + assert.Equal(t, "fk_user_id", commands[4].Index) +} + +func TestBlueprint_ImpliedCommands(t *testing.T) { + t.Run("creates add command when not creating", func(t *testing.T) { + bp := blueprint.NewBlueprintForTesting("users", &mockGrammar{}) + bp.String("new_col") + bp.AddImpliedCommands() + + assert.Equal(t, blueprint.CommandAdd, bp.Commands[0].Name) + }) + + t.Run("does not create add command when creating", func(t *testing.T) { + bp := blueprint.NewBlueprintForTesting("users", &mockGrammar{}) + bp.Create() + bp.String("new_col") + bp.AddImpliedCommands() + + for _, cmd := range bp.Commands { + assert.NotEqual(t, blueprint.CommandAdd, cmd.Name) + } + }) + + t.Run("handles fluent indexes for MySQL", func(t *testing.T) { + bp := blueprint.NewBlueprintForTesting("users", &mockGrammar{}) + bp.SetDialect(dialect.MySQL) + bp.Integer("id").Primary() + bp.AddImpliedCommands() + + // MySQL primary key should be handled in-line, not as a command + for _, cmd := range bp.Commands { + assert.NotEqual(t, blueprint.CommandPrimary, cmd.Name) + } + }) + + t.Run("handles fluent indexes for non-MySQL", func(t *testing.T) { + bp := blueprint.NewBlueprintForTesting("users", &mockGrammar{}) + bp.SetDialect(dialect.Postgres) + bp.String("email").Unique() + bp.AddImpliedCommands() + + found := false + for _, cmd := range bp.Commands { + if cmd.Name == blueprint.CommandUnique { + found = true + assert.Equal(t, []string{"email"}, cmd.Columns) + } + } + assert.True(t, found) + }) +} + +func TestBlueprint_AllColumnTypes(t *testing.T) { + bp := blueprint.NewBlueprintForTesting("all_types", &mockGrammar{}) + + bp.Char("char_col", 10) + bp.LongText("long_text") + bp.Text("text_col") + bp.MediumText("medium_text") + bp.TinyText("tiny_text") + bp.Decimal("decimal_col", 10, 3) + bp.Double("double_col") + bp.Float("float_col", 20) + bp.Increments("inc") + bp.MediumIncrements("med_inc") + bp.SmallIncrements("small_inc") + bp.TinyIncrements("tiny_inc") + bp.Integer("int_col") + bp.MediumInteger("med_int") + bp.SmallInteger("small_int") + bp.TinyInteger("tiny_int") + bp.UnsignedInteger("u_int") + bp.UnsignedMediumInteger("u_med_int") + bp.UnsignedSmallInteger("u_small_int") + bp.UnsignedTinyInteger("u_tiny_int") + bp.DateTime("dt", 6) + bp.DateTimeTz("dt_tz", 6) + bp.Date("d") + bp.Time("t", 6) + bp.TimeTz("t_tz", 6) + bp.TimestampTz("ts_tz", 6) + bp.Year("y") + bp.Binary("bin", 100) + bp.JSON("j") + bp.JSONB("jb") + bp.UUID("u") + bp.ULID("ul") + bp.Geography("geog", "POINT", 4326) + bp.Geometry("geom", "POLYGON", 4326) + bp.Point("pt", 4326) + bp.Enum("e", []string{"a", "b"}) + bp.Set("s", []string{"x", "y"}) + bp.IPAddress("ip") + bp.MacAddress("mac") + bp.Vector("v", 128) + bp.TSVector("tsv") + bp.Cidr("cid") + bp.Inet("ine") + bp.MacAddr("ma") + bp.MacAddr8("ma8") + bp.Column("custom", "CUSTOM_TYPE") + + assert.Len(t, bp.Columns, 46) +} + +func TestBlueprint_Altering(t *testing.T) { + bp := blueprint.NewBlueprintForTesting("users", &mockGrammar{}) + bp.Drop() + bp.DropIfExists() + bp.Rename("new_users") + bp.RenameColumn("old", "new") + bp.DropTimestamps() + bp.DropTimestampsTz() + bp.RenameIndex("old_idx", "new_idx") + bp.DropFulltext("idx") + + assert.Len(t, bp.Commands, 8) +} + +type mockBuilder struct{} + +func (m *mockBuilder) GetColumns(_ core.Context, _ string) ([]*core.Column, error) { + return []*core.Column{ + { + Name: "bio", + Nullable: false, + DefaultVal: sql.NullString{String: "Hello", Valid: true}, + Comment: sql.NullString{String: "World", Valid: true}, + }, + }, nil +} + +func TestBlueprint_BuildAndHydrate(t *testing.T) { + db, _ := sql.Open("sqlite3", ":memory:") + defer db.Close() + ctx := context.Background() + tx, _ := db.BeginTx(ctx, nil) + defer tx.Rollback() + c := core.NewContext(ctx, tx, core.WithDialect("postgres")) + + bp := blueprint.NewBlueprintForTesting("users", &mockGrammar{}) + bp.SetBuilder(&mockBuilder{}) + bp.String("bio").Change() + + err := bp.Build(c) + require.NoError(t, err) + + col := bp.Columns[0] + assert.False(t, *col.NullableVal) + assert.Equal(t, "Hello", string(col.DefaultValue.(blueprint.Expression))) + assert.Equal(t, "World", *col.CommentVal) +} + +func TestBlueprint_DropIndex_Panic(t *testing.T) { + bp := blueprint.NewBlueprintForTesting("users", &mockGrammar{}) + assert.Panics(t, func() { + bp.DropIndex(123) + }) +} + +func (m *mockGrammar) GetFluentCommands() []func(blueprint *blueprint.Blueprint, command *blueprint.Command) string { + return []func(blueprint *blueprint.Blueprint, command *blueprint.Command) string{ + func(_ *blueprint.Blueprint, cmd *blueprint.Command) string { + if cmd.Column != nil && cmd.Column.Name == "fluent" { + return "SELECT 1" + } + return "" + }, + } +} + +func (m *mockGrammar) GetTableFluentCommands() []func(blueprint *blueprint.Blueprint) string { + return []func(blueprint *blueprint.Blueprint) string{ + func(_ *blueprint.Blueprint) string { + return "SELECT 1" + }, + } +} + +func TestBlueprint_FluentStatements(t *testing.T) { + bp := blueprint.NewBlueprintForTesting("users", &mockGrammar{}) + bp.String("fluent") + bp.String("normal") + + statements := bp.GetFluentStatements() + assert.Contains(t, statements, "SELECT 1") +} + +func TestBlueprint_ToSQL_Errors(t *testing.T) { + bp := blueprint.NewBlueprintForTesting("users", &mockGrammar{}) + bp.Commands = append(bp.Commands, &blueprint.Command{Name: "unknown"}) + _, err := bp.ToSQL() + assert.Error(t, err) +} diff --git a/schema/blueprint/column_definition.go b/schema/blueprint/column_definition.go new file mode 100644 index 0000000..6863cd1 --- /dev/null +++ b/schema/blueprint/column_definition.go @@ -0,0 +1,225 @@ +package blueprint + +import ( + "slices" + + "github.com/akfaiz/migris/internal/util" +) + +// ColumnDefinition defines the interface for defining a column in a database table. +type ColumnDefinition interface { + AutoIncrement() ColumnDefinition + Change() ColumnDefinition + Charset(charset string) ColumnDefinition + Collation(collation string) ColumnDefinition + Comment(comment string) ColumnDefinition + Default(value any) ColumnDefinition + Index(params ...any) ColumnDefinition + Nullable(value ...bool) ColumnDefinition + OnUpdate(value any) ColumnDefinition + Primary(value ...bool) ColumnDefinition + Unique(params ...any) ColumnDefinition + Unsigned() ColumnDefinition + UseCurrent() ColumnDefinition + UseCurrentOnUpdate() ColumnDefinition + After(column string) ColumnDefinition + First() ColumnDefinition + VirtualAs(expression string) ColumnDefinition + StoredAs(expression string) ColumnDefinition + Invisible() ColumnDefinition + GeneratedAs(expression ...string) ColumnDefinition + Always(value ...bool) ColumnDefinition + RenameTo(name string) ColumnDefinition +} + +// Column represents a database column definition in a blueprint. +type Column struct { + Commands []string + Name string + RenameToVal string + ColumnType string + CharsetVal *string + CollationVal *string + CommentVal *string + DefaultValue any + OnUpdateValue any + UseCurrentVal bool + UseCurrentOnUpdateVal bool + NullableVal *bool + AutoIncrementVal *bool + UnsignedVal *bool + PrimaryVal *bool + IndexVal *bool + IndexName string + UniqueVal *bool + UniqueName string + Length *int + Precision *int + Total *int + Places *int + ChangeVal bool + Allowed []string // for enum type columns + Subtype *string // for geography and geometry types + Srid *int // for geography and geometry types + AfterVal *string + FirstVal bool + VirtualAsVal *string + StoredAsVal *string + InvisibleVal *bool + GeneratedAsVal *string + AlwaysVal *bool +} + +// Expression is a type for expressions that can be used as default values for columns. +type Expression string + +func (e Expression) String() string { + return string(e) +} + +var _ ColumnDefinition = &Column{} + +func (c *Column) AddCommand(command string) { + c.Commands = append(c.Commands, command) +} + +func (c *Column) HasCommand(command string) bool { + return slices.Contains(c.Commands, command) +} + +func (c *Column) AutoIncrement() ColumnDefinition { + c.AutoIncrementVal = util.PtrOf(true) + return c +} + +func (c *Column) Charset(charset string) ColumnDefinition { + c.CharsetVal = &charset + return c +} + +func (c *Column) Change() ColumnDefinition { + c.ChangeVal = true + return c +} + +func (c *Column) Collation(collation string) ColumnDefinition { + c.CollationVal = &collation + return c +} + +func (c *Column) Comment(comment string) ColumnDefinition { + c.AddCommand("comment") + c.CommentVal = &comment + return c +} + +func (c *Column) Default(value any) ColumnDefinition { + c.AddCommand("default") + c.DefaultValue = value + return c +} + +func (c *Column) Index(params ...any) ColumnDefinition { + index := true + for _, param := range params { + switch v := param.(type) { + case bool: + index = v + case string: + c.IndexName = v + } + } + c.IndexVal = &index + return c +} + +func (c *Column) Nullable(value ...bool) ColumnDefinition { + c.AddCommand("nullable") + c.NullableVal = util.OptionalPtr(true, value...) + return c +} + +func (c *Column) OnUpdate(value any) ColumnDefinition { + c.AddCommand("onUpdate") + c.OnUpdateValue = value + return c +} + +func (c *Column) Primary(value ...bool) ColumnDefinition { + val := util.Optional(true, value...) + c.PrimaryVal = &val + return c +} + +func (c *Column) Unique(params ...any) ColumnDefinition { + unique := true + for _, param := range params { + switch v := param.(type) { + case bool: + unique = v + case string: + c.UniqueName = v + } + } + c.UniqueVal = &unique + return c +} + +func (c *Column) Unsigned() ColumnDefinition { + c.UnsignedVal = util.PtrOf(true) + return c +} + +func (c *Column) UseCurrent() ColumnDefinition { + c.UseCurrentVal = true + return c +} + +func (c *Column) UseCurrentOnUpdate() ColumnDefinition { + c.UseCurrentOnUpdateVal = true + return c +} + +func (c *Column) After(column string) ColumnDefinition { + c.AfterVal = &column + return c +} + +func (c *Column) First() ColumnDefinition { + c.FirstVal = true + return c +} + +func (c *Column) VirtualAs(expression string) ColumnDefinition { + c.VirtualAsVal = &expression + return c +} + +func (c *Column) StoredAs(expression string) ColumnDefinition { + c.StoredAsVal = &expression + return c +} + +func (c *Column) Invisible() ColumnDefinition { + c.InvisibleVal = util.PtrOf(true) + return c +} + +func (c *Column) GeneratedAs(expression ...string) ColumnDefinition { + if len(expression) > 0 { + c.GeneratedAsVal = &expression[0] + } else { + c.GeneratedAsVal = util.PtrOf("") + } + return c +} + +func (c *Column) Always(value ...bool) ColumnDefinition { + c.AlwaysVal = util.OptionalPtr(true, value...) + return c +} + +func (c *Column) RenameTo(name string) ColumnDefinition { + c.RenameToVal = name + return c +} diff --git a/schema/blueprint/column_definition_test.go b/schema/blueprint/column_definition_test.go new file mode 100644 index 0000000..fd31ff0 --- /dev/null +++ b/schema/blueprint/column_definition_test.go @@ -0,0 +1,74 @@ +package blueprint_test + +import ( + "testing" + + "github.com/akfaiz/migris/schema/blueprint" + "github.com/stretchr/testify/assert" +) + +func TestColumnDefinition_Modifiers(t *testing.T) { + col := &blueprint.Column{Name: "test"} + + col.AutoIncrement(). + Charset("utf8"). + Collation("utf8_bin"). + Comment("some comment"). + Default("val"). + Nullable(true). + Unsigned(). + UseCurrent(). + UseCurrentOnUpdate(). + After("other"). + First(). + VirtualAs("expr1"). + StoredAs("expr2"). + Invisible(). + Always(true). + RenameTo("new_name") + + assert.True(t, *col.AutoIncrementVal) + assert.Equal(t, "utf8", *col.CharsetVal) + assert.Equal(t, "utf8_bin", *col.CollationVal) + assert.Equal(t, "some comment", *col.CommentVal) + assert.Equal(t, "val", col.DefaultValue) + assert.True(t, *col.NullableVal) + assert.True(t, *col.UnsignedVal) + assert.True(t, col.UseCurrentVal) + assert.True(t, col.UseCurrentOnUpdateVal) + assert.Equal(t, "other", *col.AfterVal) + assert.True(t, col.FirstVal) + assert.Equal(t, "expr1", *col.VirtualAsVal) + assert.Equal(t, "expr2", *col.StoredAsVal) + assert.True(t, *col.InvisibleVal) + assert.True(t, *col.AlwaysVal) + assert.Equal(t, "new_name", col.RenameToVal) +} + +func TestColumnDefinition_IndexUnique(t *testing.T) { + t.Run("index with boolean", func(t *testing.T) { + col := &blueprint.Column{Name: "test"} + col.Index(true) + assert.True(t, *col.IndexVal) + }) + + t.Run("index with name", func(t *testing.T) { + col := &blueprint.Column{Name: "test"} + col.Index("idx_test") + assert.True(t, *col.IndexVal) + assert.Equal(t, "idx_test", col.IndexName) + }) + + t.Run("unique with boolean", func(t *testing.T) { + col := &blueprint.Column{Name: "test"} + col.Unique(true) + assert.True(t, *col.UniqueVal) + }) + + t.Run("unique with name", func(t *testing.T) { + col := &blueprint.Column{Name: "test"} + col.Unique("uk_test") + assert.True(t, *col.UniqueVal) + assert.Equal(t, "uk_test", col.UniqueName) + }) +} diff --git a/schema/blueprint/command.go b/schema/blueprint/command.go new file mode 100644 index 0000000..0774402 --- /dev/null +++ b/schema/blueprint/command.go @@ -0,0 +1,41 @@ +package blueprint + +const ( + CommandAdd string = "add" + CommandCreate string = "create" + CommandChange string = "change" + CommandDrop string = "drop" + CommandDropIfExists string = "dropIfExists" + CommandRename string = "rename" + CommandDropColumn string = "dropColumn" + CommandRenameColumn string = "renameColumn" + CommandIndex string = "index" + CommandUnique string = "unique" + CommandPrimary string = "primary" + CommandFullText string = "fulltext" + CommandDropIndex string = "dropIndex" + CommandDropUnique string = "dropUnique" + CommandDropPrimary string = "dropPrimary" + CommandDropFullText string = "dropFulltext" + CommandRenameIndex string = "renameIndex" + CommandForeign string = "foreign" + CommandDropForeign string = "dropForeign" +) + +// Command represents a database command to be executed in a blueprint. +type Command struct { + Name string + To string + From string + Column *Column + Columns []string + Algorithm string + On string + OnDelete string + OnUpdate string + References []string + Index string + Deferrable *bool + InitiallyImmediate *bool + Language string +} diff --git a/schema/blueprint/foreign_key_definition.go b/schema/blueprint/foreign_key_definition.go new file mode 100644 index 0000000..be11ab1 --- /dev/null +++ b/schema/blueprint/foreign_key_definition.go @@ -0,0 +1,98 @@ +package blueprint + +type ForeignKeyDefinition interface { + References(columns ...string) ForeignKeyDefinition + On(table string) ForeignKeyDefinition + OnDelete(action string) ForeignKeyDefinition + OnUpdate(action string) ForeignKeyDefinition + Name(name string) ForeignKeyDefinition + CascadeOnDelete() ForeignKeyDefinition + CascadeOnUpdate() ForeignKeyDefinition + RestrictOnDelete() ForeignKeyDefinition + RestrictOnUpdate() ForeignKeyDefinition + NullOnDelete() ForeignKeyDefinition + NullOnUpdate() ForeignKeyDefinition + NoActionOnDelete() ForeignKeyDefinition + NoActionOnUpdate() ForeignKeyDefinition + Deferrable(deferrable ...bool) ForeignKeyDefinition + InitiallyImmediate(immediate ...bool) ForeignKeyDefinition +} + +type foreignKeyDefinition struct { + command *Command +} + +func (f *foreignKeyDefinition) References(columns ...string) ForeignKeyDefinition { + f.command.References = columns + return f +} + +func (f *foreignKeyDefinition) On(table string) ForeignKeyDefinition { + f.command.On = table + return f +} + +func (f *foreignKeyDefinition) OnDelete(action string) ForeignKeyDefinition { + f.command.OnDelete = action + return f +} + +func (f *foreignKeyDefinition) OnUpdate(action string) ForeignKeyDefinition { + f.command.OnUpdate = action + return f +} + +func (f *foreignKeyDefinition) Name(name string) ForeignKeyDefinition { + f.command.Index = name + return f +} + +func (f *foreignKeyDefinition) CascadeOnDelete() ForeignKeyDefinition { + return f.OnDelete("CASCADE") +} + +func (f *foreignKeyDefinition) CascadeOnUpdate() ForeignKeyDefinition { + return f.OnUpdate("CASCADE") +} + +func (f *foreignKeyDefinition) RestrictOnDelete() ForeignKeyDefinition { + return f.OnDelete("RESTRICT") +} + +func (f *foreignKeyDefinition) RestrictOnUpdate() ForeignKeyDefinition { + return f.OnUpdate("RESTRICT") +} + +func (f *foreignKeyDefinition) NullOnDelete() ForeignKeyDefinition { + return f.OnDelete("SET NULL") +} + +func (f *foreignKeyDefinition) NullOnUpdate() ForeignKeyDefinition { + return f.OnUpdate("SET NULL") +} + +func (f *foreignKeyDefinition) NoActionOnDelete() ForeignKeyDefinition { + return f.OnDelete("NO ACTION") +} + +func (f *foreignKeyDefinition) NoActionOnUpdate() ForeignKeyDefinition { + return f.OnUpdate("NO ACTION") +} + +func (f *foreignKeyDefinition) Deferrable(deferrable ...bool) ForeignKeyDefinition { + val := true + if len(deferrable) > 0 { + val = deferrable[0] + } + f.command.Deferrable = &val + return f +} + +func (f *foreignKeyDefinition) InitiallyImmediate(immediate ...bool) ForeignKeyDefinition { + val := true + if len(immediate) > 0 { + val = immediate[0] + } + f.command.InitiallyImmediate = &val + return f +} diff --git a/schema/blueprint/foreign_key_definition_test.go b/schema/blueprint/foreign_key_definition_test.go new file mode 100644 index 0000000..784f26c --- /dev/null +++ b/schema/blueprint/foreign_key_definition_test.go @@ -0,0 +1,54 @@ +package blueprint_test + +import ( + "testing" + + "github.com/akfaiz/migris/schema/blueprint" + "github.com/stretchr/testify/assert" +) + +func TestForeignKeyDefinition_Modifiers(t *testing.T) { + bp := blueprint.NewBlueprintForTesting("posts", &mockGrammar{}) + fk := bp.Foreign("user_id") + + fk.References("id"). + On("users"). + OnDelete("CASCADE"). + OnUpdate("RESTRICT"). + Name("fk_posts_user_id"). + Deferrable(true). + InitiallyImmediate(true) + + // Since fk is an interface wrapping internal command, we check the bp.Commands + cmd := bp.Commands[0] + assert.Equal(t, blueprint.CommandForeign, cmd.Name) + assert.Equal(t, []string{"user_id"}, cmd.Columns) + assert.Equal(t, []string{"id"}, cmd.References) + assert.Equal(t, "users", cmd.On) + assert.Equal(t, "CASCADE", cmd.OnDelete) + assert.Equal(t, "RESTRICT", cmd.OnUpdate) + assert.Equal(t, "fk_posts_user_id", cmd.Index) + assert.True(t, *cmd.Deferrable) + assert.True(t, *cmd.InitiallyImmediate) +} + +func TestForeignKeyDefinition_HelperMethods(t *testing.T) { + bp := blueprint.NewBlueprintForTesting("posts", &mockGrammar{}) + + bp.Foreign("c1").CascadeOnDelete().CascadeOnUpdate() + bp.Foreign("c2").RestrictOnDelete().RestrictOnUpdate() + bp.Foreign("c3").NullOnDelete().NullOnUpdate() + bp.Foreign("c4").NoActionOnDelete().NoActionOnUpdate() + + assert.Equal(t, "CASCADE", bp.Commands[0].OnDelete) + assert.Equal(t, "CASCADE", bp.Commands[0].OnUpdate) + + assert.Equal(t, "RESTRICT", bp.Commands[1].OnDelete) + assert.Equal(t, "RESTRICT", bp.Commands[1].OnUpdate) + + assert.Equal(t, "SET NULL", bp.Commands[2].OnDelete) + assert.Equal(t, "SET NULL", bp.Commands[2].OnUpdate) + + assert.Equal(t, "NO ACTION", bp.Commands[3].OnDelete) + assert.Equal(t, "NO ACTION", bp.Commands[3].OnUpdate) +} diff --git a/schema/blueprint/grammar.go b/schema/blueprint/grammar.go new file mode 100644 index 0000000..9cbdc14 --- /dev/null +++ b/schema/blueprint/grammar.go @@ -0,0 +1,228 @@ +package blueprint + +import ( + "errors" + "fmt" + "regexp" + "slices" + "strings" + + "github.com/akfaiz/migris/internal/util" +) + +type Grammar interface { + CompileTableExists(schema string, table string) (string, error) + CompileTables(schema string) (string, error) + CompileColumns(schema, table string) (string, error) + CompileIndexes(schema, table string) (string, error) + CompileCreate(bp *Blueprint) (string, error) + CompileAdd(bp *Blueprint) (string, error) + CompileChange(bp *Blueprint, command *Command) (string, error) + CompileDrop(bp *Blueprint) (string, error) + CompileDropIfExists(bp *Blueprint) (string, error) + CompileRename(bp *Blueprint, command *Command) (string, error) + CompileDropColumn(blueprint *Blueprint, command *Command) (string, error) + CompileRenameColumn(blueprint *Blueprint, command *Command) (string, error) + CompileIndex(blueprint *Blueprint, command *Command) (string, error) + CompileUnique(blueprint *Blueprint, command *Command) (string, error) + CompilePrimary(blueprint *Blueprint, command *Command) (string, error) + CompileFullText(blueprint *Blueprint, command *Command) (string, error) + CompileDropIndex(blueprint *Blueprint, command *Command) (string, error) + CompileDropUnique(blueprint *Blueprint, command *Command) (string, error) + CompileDropFulltext(blueprint *Blueprint, command *Command) (string, error) + CompileDropPrimary(blueprint *Blueprint, command *Command) (string, error) + CompileRenameIndex(blueprint *Blueprint, command *Command) (string, error) + CompileForeign(blueprint *Blueprint, command *Command) (string, error) + CompileDropForeign(blueprint *Blueprint, command *Command) (string, error) + GetFluentCommands() []func(blueprint *Blueprint, command *Command) string + GetTableFluentCommands() []func(blueprint *Blueprint) string + CreateIndexName(blueprint *Blueprint, idxType string, columns ...string) string + GetType(column *Column) string +} + +type BaseGrammar struct{} + +func (g *BaseGrammar) CompileChange(_ *Blueprint, _ *Command) (string, error) { + return "", errors.New("change operation not supported by this grammar") +} + +func (g *BaseGrammar) CompileDropColumn(_ *Blueprint, _ *Command) (string, error) { + return "", errors.New("drop column operation not supported by this grammar") +} + +func (g *BaseGrammar) CompileRenameColumn(_ *Blueprint, _ *Command) (string, error) { + return "", errors.New("rename column operation not supported by this grammar") +} + +func (g *BaseGrammar) CompileIndex(_ *Blueprint, _ *Command) (string, error) { + return "", errors.New("index operation not supported by this grammar") +} + +func (g *BaseGrammar) CompileUnique(_ *Blueprint, _ *Command) (string, error) { + return "", errors.New("unique index operation not supported by this grammar") +} + +func (g *BaseGrammar) CompilePrimary(_ *Blueprint, _ *Command) (string, error) { + return "", errors.New("primary key operation not supported by this grammar") +} + +func (g *BaseGrammar) CompileFullText(_ *Blueprint, _ *Command) (string, error) { + return "", errors.New("fulltext index operation not supported by this grammar") +} + +func (g *BaseGrammar) CompileDropIndex(_ *Blueprint, _ *Command) (string, error) { + return "", errors.New("drop index operation not supported by this grammar") +} + +func (g *BaseGrammar) CompileDropUnique(_ *Blueprint, _ *Command) (string, error) { + return "", errors.New("drop unique operation not supported by this grammar") +} + +func (g *BaseGrammar) CompileDropFulltext(_ *Blueprint, _ *Command) (string, error) { + return "", errors.New("drop fulltext operation not supported by this grammar") +} + +func (g *BaseGrammar) CompileDropPrimary(_ *Blueprint, _ *Command) (string, error) { + return "", errors.New("drop primary operation not supported by this grammar") +} + +func (g *BaseGrammar) CompileRenameIndex(_ *Blueprint, _ *Command) (string, error) { + return "", errors.New("rename index operation not supported by this grammar") +} + +func (g *BaseGrammar) CompileDropForeign(_ *Blueprint, _ *Command) (string, error) { + return "", errors.New("drop foreign operation not supported by this grammar") +} + +func (g *BaseGrammar) CompileForeign(blueprint *Blueprint, command *Command) (string, error) { + if len(command.Columns) == 0 || slices.Contains(command.Columns, "") || command.On == "" || + len(command.References) == 0 || slices.Contains(command.References, "") { + return "", errors.New("foreign key definition is incomplete: column, on, and references must be set") + } + onDelete := "" + if command.OnDelete != "" { + onDelete = fmt.Sprintf(" ON DELETE %s", command.OnDelete) + } + onUpdate := "" + if command.OnUpdate != "" { + onUpdate = fmt.Sprintf(" ON UPDATE %s", command.OnUpdate) + } + index := command.Index + if index == "" { + index = g.CreateForeignKeyName(blueprint, command) + } + + return fmt.Sprintf("ALTER TABLE %s ADD CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s (%s)%s%s", + blueprint.Name, + index, + g.Columnize(command.Columns), + command.On, + g.Columnize(command.References), + onDelete, + onUpdate, + ), nil +} + +func (g *BaseGrammar) GetFluentCommands() []func(blueprint *Blueprint, command *Command) string { + return []func(blueprint *Blueprint, command *Command) string{} +} + +func (g *BaseGrammar) GetTableFluentCommands() []func(blueprint *Blueprint) string { + return []func(blueprint *Blueprint) string{} +} + +func (g *BaseGrammar) CreateIndexName(blueprint *Blueprint, idxType string, columns ...string) string { + parts := []string{blueprint.Name} + parts = append(parts, columns...) + parts = append(parts, idxType) + + index := strings.ToLower(strings.Join(parts, "_")) + return strings.NewReplacer("-", "_", ".", "_").Replace(index) +} + +func (g *BaseGrammar) CreateForeignKeyName(blueprint *Blueprint, command *Command) string { + return g.CreateIndexName(blueprint, "foreign", command.Columns...) +} + +func (g *BaseGrammar) QuoteString(s string) string { + return "'" + s + "'" +} + +func (g *BaseGrammar) PrefixArray(prefix string, items []string) []string { + prefixed := make([]string, len(items)) + for i, item := range items { + prefixed[i] = fmt.Sprintf("%s%s", prefix, item) + } + return prefixed +} + +func (g *BaseGrammar) Columnize(columns []string) string { + if len(columns) == 0 { + return "" + } + return strings.Join(columns, ", ") +} + +func (g *BaseGrammar) Wrap(ident string, quote string) string { + if ident == "" { + return ident + } + parts := strings.Split(ident, ".") + wrapped := make([]string, 0, len(parts)) + for _, part := range parts { + if part == "*" { + wrapped = append(wrapped, part) + continue + } + wrapped = append(wrapped, quote+part+quote) + } + return strings.Join(wrapped, ".") +} + +func (g *BaseGrammar) WrapTable(table string, quote string) string { + return g.Wrap(table, quote) +} + +func (g *BaseGrammar) WrapColumnize(columns []string, quote string) string { + if len(columns) == 0 { + return "" + } + wrapped := make([]string, 0, len(columns)) + for _, column := range columns { + wrapped = append(wrapped, g.Wrap(column, quote)) + } + return strings.Join(wrapped, ", ") +} + +func (g *BaseGrammar) WrapIndexName(index string, quote string) string { + if index == "" { + return index + } + if regexp.MustCompile(`[()\s]`).MatchString(index) { + return index + } + return g.Wrap(index, quote) +} + +func (g *BaseGrammar) GetValue(value any) string { + switch v := value.(type) { + case Expression: + return v.String() + default: + return fmt.Sprintf("'%v'", v) + } +} + +func (g *BaseGrammar) GetDefaultValue(value any) string { + if value == nil { + return "NULL" + } + switch v := value.(type) { + case Expression: + return v.String() + case bool: + return util.Ternary(v, "'1'", "'0'") + default: + return fmt.Sprintf("'%v'", v) + } +} diff --git a/schema/blueprint/grammar_test.go b/schema/blueprint/grammar_test.go new file mode 100644 index 0000000..6224b9e --- /dev/null +++ b/schema/blueprint/grammar_test.go @@ -0,0 +1,144 @@ +package blueprint_test + +import ( + "testing" + + "github.com/akfaiz/migris/schema/blueprint" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestBaseGrammar_UnsupportedOperations(t *testing.T) { + g := &blueprint.BaseGrammar{} + bp := &blueprint.Blueprint{Name: "users"} + cmd := &blueprint.Command{} + + tests := []struct { + name string + fn func() (string, error) + }{ + {"CompileChange", func() (string, error) { return g.CompileChange(bp, cmd) }}, + {"CompileDropColumn", func() (string, error) { return g.CompileDropColumn(bp, cmd) }}, + {"CompileRenameColumn", func() (string, error) { return g.CompileRenameColumn(bp, cmd) }}, + {"CompileIndex", func() (string, error) { return g.CompileIndex(bp, cmd) }}, + {"CompileUnique", func() (string, error) { return g.CompileUnique(bp, cmd) }}, + {"CompilePrimary", func() (string, error) { return g.CompilePrimary(bp, cmd) }}, + {"CompileFullText", func() (string, error) { return g.CompileFullText(bp, cmd) }}, + {"CompileDropIndex", func() (string, error) { return g.CompileDropIndex(bp, cmd) }}, + {"CompileDropUnique", func() (string, error) { return g.CompileDropUnique(bp, cmd) }}, + {"CompileDropFulltext", func() (string, error) { return g.CompileDropFulltext(bp, cmd) }}, + {"CompileDropPrimary", func() (string, error) { return g.CompileDropPrimary(bp, cmd) }}, + {"CompileRenameIndex", func() (string, error) { return g.CompileRenameIndex(bp, cmd) }}, + {"CompileDropForeign", func() (string, error) { return g.CompileDropForeign(bp, cmd) }}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := tt.fn() + require.Error(t, err) + assert.Contains(t, err.Error(), "not supported") + }) + } +} + +func TestBaseGrammar_CompileForeign(t *testing.T) { + g := &blueprint.BaseGrammar{} + bp := &blueprint.Blueprint{Name: "posts"} + + t.Run("incomplete command", func(t *testing.T) { + cmd := &blueprint.Command{} + _, err := g.CompileForeign(bp, cmd) + require.Error(t, err) + assert.Contains(t, err.Error(), "incomplete") + }) +} + +func TestBaseGrammar_GetFluentCommands(t *testing.T) { + g := &blueprint.BaseGrammar{} + cmds := g.GetFluentCommands() + assert.Empty(t, cmds) + + tableCmds := g.GetTableFluentCommands() + assert.Empty(t, tableCmds) +} + +func TestBaseGrammar_CreateIndexName(t *testing.T) { + g := &blueprint.BaseGrammar{} + bp := &blueprint.Blueprint{Name: "users"} + + idxName := g.CreateIndexName(bp, "index", "email") + assert.Equal(t, "users_email_index", idxName) +} + +func TestBaseGrammar_Helpers(t *testing.T) { + g := &blueprint.BaseGrammar{} + bp := &blueprint.Blueprint{Name: "users"} + + t.Run("CreateForeignKeyName", func(t *testing.T) { + cmd := &blueprint.Command{Columns: []string{"user_id"}} + name := g.CreateForeignKeyName(bp, cmd) + assert.Equal(t, "users_user_id_foreign", name) + }) + + t.Run("QuoteString", func(t *testing.T) { + assert.Equal(t, "'test'", g.QuoteString("test")) + }) + + t.Run("PrefixArray", func(t *testing.T) { + assert.Equal(t, []string{"prefix_a", "prefix_b"}, g.PrefixArray("prefix_", []string{"a", "b"})) + }) + + t.Run("Columnize", func(t *testing.T) { + assert.Equal(t, "a, b", g.Columnize([]string{"a", "b"})) + }) + + t.Run("Wrap", func(t *testing.T) { + assert.Equal(t, "\"test\"", g.Wrap("test", "\"")) + }) + + t.Run("WrapTable", func(t *testing.T) { + assert.Equal(t, "\"test\"", g.WrapTable("test", "\"")) + }) + + t.Run("WrapColumnize", func(t *testing.T) { + assert.Equal(t, "\"a\", \"b\"", g.WrapColumnize([]string{"a", "b"}, "\"")) + }) + + t.Run("WrapIndexName", func(t *testing.T) { + assert.Equal(t, "\"idx\"", g.WrapIndexName("idx", "\"")) + assert.Empty(t, g.WrapIndexName("", "\"")) + }) + + t.Run("GetValue", func(t *testing.T) { + assert.Equal(t, "'val'", g.GetValue("val")) + assert.Equal(t, "'1'", g.GetValue(1)) + assert.Equal(t, "'true'", g.GetValue(true)) + assert.Equal(t, "'false'", g.GetValue(false)) + }) + + t.Run("GetDefaultValue", func(t *testing.T) { + assert.Equal(t, "'val'", g.GetDefaultValue("val")) + assert.Equal(t, "NULL", g.GetDefaultValue(nil)) + }) +} + +func TestBaseGrammar_CompileForeignValid(t *testing.T) { + g := &blueprint.BaseGrammar{} + bp := &blueprint.Blueprint{Name: "posts"} + + cmd := &blueprint.Command{ + Columns: []string{"user_id"}, + On: "users", + References: []string{"id"}, + OnDelete: "CASCADE", + OnUpdate: "RESTRICT", + } + + sql, err := g.CompileForeign(bp, cmd) + require.NoError(t, err) + assert.Equal( + t, + "ALTER TABLE posts ADD CONSTRAINT posts_user_id_foreign FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE ON UPDATE RESTRICT", + sql, + ) +} diff --git a/schema/blueprint/index_definition.go b/schema/blueprint/index_definition.go new file mode 100644 index 0000000..94f557f --- /dev/null +++ b/schema/blueprint/index_definition.go @@ -0,0 +1,46 @@ +package blueprint + +type IndexDefinition interface { + Name(name string) IndexDefinition + Algorithm(algorithm string) IndexDefinition + Deferrable(deferrable ...bool) IndexDefinition + InitiallyImmediate(immediate ...bool) IndexDefinition + Language(language string) IndexDefinition +} + +type indexDefinition struct { + command *Command +} + +func (i *indexDefinition) Name(name string) IndexDefinition { + i.command.Index = name + return i +} + +func (i *indexDefinition) Algorithm(algorithm string) IndexDefinition { + i.command.Algorithm = algorithm + return i +} + +func (i *indexDefinition) Deferrable(deferrable ...bool) IndexDefinition { + val := true + if len(deferrable) > 0 { + val = deferrable[0] + } + i.command.Deferrable = &val + return i +} + +func (i *indexDefinition) InitiallyImmediate(immediate ...bool) IndexDefinition { + val := true + if len(immediate) > 0 { + val = immediate[0] + } + i.command.InitiallyImmediate = &val + return i +} + +func (i *indexDefinition) Language(language string) IndexDefinition { + i.command.Language = language + return i +} diff --git a/schema/blueprint/index_definition_test.go b/schema/blueprint/index_definition_test.go new file mode 100644 index 0000000..3d9b32f --- /dev/null +++ b/schema/blueprint/index_definition_test.go @@ -0,0 +1,36 @@ +package blueprint_test + +import ( + "testing" + + "github.com/akfaiz/migris/schema/blueprint" + "github.com/stretchr/testify/assert" +) + +func TestIndexDefinition_Modifiers(t *testing.T) { + bp := blueprint.NewBlueprintForTesting("users", &mockGrammar{}) + idx := bp.Index("email") + + idx.Name("idx_email_custom"). + Algorithm("hash"). + Deferrable(true). + InitiallyImmediate(false) + + cmd := bp.Commands[0] + assert.Equal(t, blueprint.CommandIndex, cmd.Name) + assert.Equal(t, "idx_email_custom", cmd.Index) + assert.Equal(t, "hash", cmd.Algorithm) + assert.True(t, *cmd.Deferrable) + assert.False(t, *cmd.InitiallyImmediate) +} + +func TestIndexDefinition_Language(t *testing.T) { + bp := blueprint.NewBlueprintForTesting("users", &mockGrammar{}) + idx := bp.FullText("bio") + + idx.Language("spanish") + + cmd := bp.Commands[0] + assert.Equal(t, blueprint.CommandFullText, cmd.Name) + assert.Equal(t, "spanish", cmd.Language) +} diff --git a/schema/builder.go b/schema/builder.go deleted file mode 100644 index 26f1eec..0000000 --- a/schema/builder.go +++ /dev/null @@ -1,139 +0,0 @@ -package schema - -import ( - "errors" - - "github.com/akfaiz/migris/internal/dialect" -) - -// Builder is an interface that defines methods for creating, dropping, and managing database tables. -type Builder interface { - // Create creates a new table with the given name and applies the provided blueprint. - Create(c Context, name string, blueprint func(table *Blueprint)) error - // Drop removes the table with the given name. - Drop(c Context, name string) error - // DropIfExists removes the table with the given name if it exists. - DropIfExists(c Context, name string) error - // GetColumns retrieves the columns of the specified table. - GetColumns(c Context, tableName string) ([]*Column, error) - // GetIndexes retrieves the indexes of the specified table. - GetIndexes(c Context, tableName string) ([]*Index, error) - // GetTables retrieves all tables in the database. - GetTables(c Context) ([]*TableInfo, error) - // HasColumn checks if the specified table has the given column. - HasColumn(c Context, tableName string, columnName string) (bool, error) - // HasColumns checks if the specified table has all the given columns. - HasColumns(c Context, tableName string, columnNames []string) (bool, error) - // HasIndex checks if the specified table has the given index. - HasIndex(c Context, tableName string, indexes []string) (bool, error) - // HasTable checks if a table with the given name exists. - HasTable(c Context, name string) (bool, error) - // Rename renames a table from oldName to newName. - Rename(c Context, oldName string, newName string) error - // Table applies the provided blueprint to the specified table. - Table(c Context, name string, blueprint func(table *Blueprint)) error -} - -// NewBuilder creates a new Builder instance based on the specified dialect. -// It returns an error if the dialect is not supported. -// -// Supported dialects are "postgres", "pgx", "mysql", and "mariadb". -func NewBuilder(dialectValue string) (Builder, error) { - dialectVal := dialect.FromString(dialectValue) - switch dialectVal { - case dialect.MySQL: - return newMysqlBuilder(), nil - case dialect.Postgres: - return newPostgresBuilder(), nil - case dialect.SQLite3: - return newSqliteBuilder(), nil - case dialect.Unknown: - return nil, errors.New("unsupported dialect: " + dialectValue) - default: - return nil, errors.New("unsupported dialect: " + dialectValue) - } -} - -type baseBuilder struct { - grammar grammar -} - -func (b *baseBuilder) newBlueprint(name string) *Blueprint { - return &Blueprint{name: name, grammar: b.grammar} -} - -func (b *baseBuilder) Create(c Context, name string, blueprint func(table *Blueprint)) error { - if c == nil || name == "" || blueprint == nil { - return errors.New("invalid arguments: context, name, or blueprint is nil/empty") - } - - bp := b.newBlueprint(name) - bp.create() - blueprint(bp) - - if err := bp.build(c); err != nil { - return err - } - - return nil -} - -func (b *baseBuilder) Drop(c Context, name string) error { - if c == nil || name == "" { - return errors.New("invalid arguments: context is nil or name is empty") - } - - bp := b.newBlueprint(name) - bp.drop() - - if err := bp.build(c); err != nil { - return err - } - - return nil -} - -func (b *baseBuilder) DropIfExists(c Context, name string) error { - if c == nil || name == "" { - return errors.New("invalid arguments: context is nil or name is empty") - } - - bp := b.newBlueprint(name) - bp.dropIfExists() - - if err := bp.build(c); err != nil { - return err - } - - return nil -} - -func (b *baseBuilder) Rename(c Context, oldName string, newName string) error { - if c == nil || oldName == "" || newName == "" { - return errors.New("invalid arguments: context is nil or old/new table name is empty") - } - - bp := b.newBlueprint(oldName) - bp.rename(newName) - - if err := bp.build(c); err != nil { - return err - } - - return nil -} - -func (b *baseBuilder) Table(c Context, name string, blueprint func(table *Blueprint)) error { - if c == nil || name == "" || blueprint == nil { - return errors.New("invalid arguments: context is nil or name/blueprint is empty") - } - - bp := b.newBlueprint(name) - blueprint(bp) - - if err := bp.build(c); err != nil { - return err - } - - return nil -} diff --git a/schema/builders/builder.go b/schema/builders/builder.go new file mode 100644 index 0000000..5351b4e --- /dev/null +++ b/schema/builders/builder.go @@ -0,0 +1,90 @@ +package builders + +import ( + "errors" + + "github.com/akfaiz/migris/internal/dialect" + "github.com/akfaiz/migris/schema/blueprint" + "github.com/akfaiz/migris/schema/core" +) + +// Builder is an interface that defines methods for creating, dropping, and managing database tables. +type Builder interface { + Create(c core.Context, name string, blueprint func(table *blueprint.Blueprint)) error + Table(c core.Context, name string, blueprint func(table *blueprint.Blueprint)) error + Drop(c core.Context, name string) error + DropIfExists(c core.Context, name string) error + Rename(c core.Context, from, to string) error + HasTable(c core.Context, name string) (bool, error) + HasColumn(c core.Context, table, column string) (bool, error) + HasColumns(c core.Context, table string, columns []string) (bool, error) + HasIndex(c core.Context, table string, columns []string) (bool, error) + GetColumns(c core.Context, table string) ([]*core.Column, error) + GetIndexes(c core.Context, table string) ([]*core.Index, error) + GetTables(c core.Context) ([]*core.TableInfo, error) +} + +func NewBuilder(dialectValue string) (Builder, error) { + dialectVal := dialect.FromString(dialectValue) + switch dialectVal { + case dialect.MySQL: + return NewMysqlBuilder(), nil + case dialect.MariaDB: + return NewMariadbBuilder(), nil + case dialect.Postgres: + return NewPostgresBuilder(), nil + case dialect.SQLite3: + return NewSqliteBuilder(), nil + case dialect.Unknown: + return nil, errors.New("unsupported dialect: " + dialectValue) + default: + return nil, errors.New("unsupported dialect: " + dialectValue) + } +} + +type baseBuilder struct { + Grammar blueprint.Grammar + Outer Builder +} + +func (b *baseBuilder) newBlueprint(name string) *blueprint.Blueprint { + bp := &blueprint.Blueprint{ + Name: name, + Grammar: b.Grammar, + Builder: b.Outer, + } + return bp +} + +func (b *baseBuilder) Create(c core.Context, name string, bp func(table *blueprint.Blueprint)) error { + if c == nil || name == "" || bp == nil { + return errors.New("invalid arguments") + } + + blueprint := b.newBlueprint(name) + blueprint.SetDialect(dialect.FromString(c.Dialect())) + blueprint.Create() + bp(blueprint) + + if err := blueprint.Build(c); err != nil { + return err + } + + return nil +} + +func (b *baseBuilder) Table(c core.Context, name string, bp func(table *blueprint.Blueprint)) error { + if c == nil || name == "" || bp == nil { + return errors.New("invalid arguments") + } + + blueprint := b.newBlueprint(name) + blueprint.SetDialect(dialect.FromString(c.Dialect())) + bp(blueprint) + + if err := blueprint.Build(c); err != nil { + return err + } + + return nil +} diff --git a/schema/builders/mariadb_builder.go b/schema/builders/mariadb_builder.go new file mode 100644 index 0000000..109023f --- /dev/null +++ b/schema/builders/mariadb_builder.go @@ -0,0 +1,18 @@ +package builders + +import ( + "github.com/akfaiz/migris/schema/grammars" +) + +type mariadbBuilder struct { + mysqlBuilder +} + +var _ Builder = (*mariadbBuilder)(nil) + +func NewMariadbBuilder() Builder { + grammar, _ := grammars.NewGrammar("mariadb") + b := &mariadbBuilder{} + b.baseBuilder = baseBuilder{Grammar: grammar, Outer: b} + return b +} diff --git a/schema/builders/mariadb_builder_test.go b/schema/builders/mariadb_builder_test.go new file mode 100644 index 0000000..af2ed2e --- /dev/null +++ b/schema/builders/mariadb_builder_test.go @@ -0,0 +1,117 @@ +package builders_test + +import ( + "context" + "database/sql" + "testing" + + "github.com/akfaiz/migris/internal/testutil" + "github.com/akfaiz/migris/schema" + "github.com/stretchr/testify/suite" + "github.com/testcontainers/testcontainers-go" +) + +func TestMariadbBuilderSuite(t *testing.T) { + suite.Run(t, new(mariadbBuilderSuite)) +} + +type mariadbBuilderSuite struct { + suite.Suite + + ctx context.Context + db *sql.DB + builder schema.Builder + tc testcontainers.Container +} + +func (s *mariadbBuilderSuite) SetupSuite() { + s.ctx = context.Background() + + container, db, err := testutil.StartMariaDBTestDB(s.ctx) + s.Require().NoError(err) + + s.tc = container + s.db = db + s.builder, err = schema.NewBuilder("mariadb") + s.Require().NoError(err) +} + +func (s *mariadbBuilderSuite) TearDownSuite() { + _ = s.db.Close() + if s.tc != nil { + _ = s.tc.Terminate(s.ctx) + } +} + +func (s *mariadbBuilderSuite) AfterTest(_, _ string) { + builder := s.builder + tx, err := s.db.BeginTx(s.ctx, nil) + s.Require().NoError(err) + c := schema.NewContext(s.ctx, tx) + tables, err := builder.GetTables(c) + s.Require().NoError(err) + for _, table := range tables { + err := builder.DropIfExists(c, table.Name) + if err != nil { + s.T().Logf("error dropping table %s: %v", table.Name, err) + } + } + err = tx.Commit() + s.Require().NoError(err, "expected no error when committing transaction after dropping tables") +} + +func (s *mariadbBuilderSuite) TestCreate() { + builder := s.builder + tx, err := s.db.BeginTx(s.ctx, nil) + s.Require().NoError(err) + defer tx.Rollback() + + c := schema.NewContext(s.ctx, tx) + + s.Run("when all parameters are valid, should create table successfully", func() { + err = builder.Create(c, "users", func(table *schema.Blueprint) { + table.ID() + table.String("name", 255) + table.String("email", 255).Unique() + table.UUID("uuid").Nullable() + table.Timestamps() + }) + s.Require().NoError(err) + }) +} + +func (s *mariadbBuilderSuite) TestGetColumns() { + builder := s.builder + tx, err := s.db.BeginTx(s.ctx, nil) + s.Require().NoError(err) + defer tx.Rollback() + + c := schema.NewContext(s.ctx, tx) + + s.Run("when table exists, should return columns successfully", func() { + tableName := "all_types" + err = builder.Create(c, tableName, func(table *schema.Blueprint) { + table.ID() + table.String("name", 255) + table.UUID("guid") + table.Point("location") + }) + s.Require().NoError(err) + + columns, err := builder.GetColumns(c, tableName) + s.Require().NoError(err) + s.Len(columns, 4) + + columnMap := make(map[string]*schema.Column) + for _, col := range columns { + columnMap[col.Name] = col + } + + s.Contains(columnMap, "id") + s.Contains(columnMap, "name") + s.Contains(columnMap, "guid") + s.Contains(columnMap, "location") + + s.Equal("uuid", columnMap["guid"].TypeName) + }) +} diff --git a/schema/builders/mysql_builder.go b/schema/builders/mysql_builder.go new file mode 100644 index 0000000..5fff55a --- /dev/null +++ b/schema/builders/mysql_builder.go @@ -0,0 +1,286 @@ +package builders + +import ( + "database/sql" + "errors" + "slices" + "strings" + + "github.com/akfaiz/migris/schema/blueprint" + "github.com/akfaiz/migris/schema/core" + "github.com/akfaiz/migris/schema/grammars" +) + +type mysqlBuilder struct { + baseBuilder +} + +var _ Builder = (*mysqlBuilder)(nil) + +func NewMysqlBuilder() Builder { + g, _ := grammars.NewGrammar("mysql") + b := &mysqlBuilder{} + b.baseBuilder = baseBuilder{Grammar: g, Outer: b} + return b +} + +func (b *mysqlBuilder) Drop(c core.Context, name string) error { + if c == nil || name == "" { + return errors.New("invalid arguments") + } + query, err := b.Grammar.CompileDrop(&blueprint.Blueprint{Name: name}) + if err != nil { + return err + } + _, err = c.Exec(query) + return err +} + +func (b *mysqlBuilder) DropIfExists(c core.Context, name string) error { + if c == nil || name == "" { + return errors.New("invalid arguments") + } + query, err := b.Grammar.CompileDropIfExists(&blueprint.Blueprint{Name: name}) + if err != nil { + return err + } + _, err = c.Exec(query) + return err +} + +func (b *mysqlBuilder) Rename(c core.Context, from, to string) error { + if c == nil || from == "" || to == "" { + return errors.New("invalid arguments") + } + query, err := b.Grammar.CompileRename(&blueprint.Blueprint{Name: from}, &blueprint.Command{To: to}) + if err != nil { + return err + } + _, err = c.Exec(query) + return err +} + +func (b *mysqlBuilder) GetColumns(c core.Context, tableName string) ([]*core.Column, error) { + if c == nil || tableName == "" { + return nil, errors.New("invalid arguments") + } + + exists, err := b.HasTable(c, tableName) + if err != nil { + return nil, err + } + if !exists { + return nil, nil + } + + query, err := b.Grammar.CompileColumns("", tableName) + if err != nil { + return nil, err + } + + rows, err := c.Query(query) + if err != nil { + return nil, err + } + defer rows.Close() + + var columns []*core.Column + for rows.Next() { + var col core.Column + var nullableStr, keyVal, privilegesVal string + if err = rows.Scan( + &col.Name, &col.TypeFull, &col.Collation, + &nullableStr, &keyVal, &col.DefaultVal, + &col.Extra, &privilegesVal, &col.Comment, + ); err != nil { + return nil, err + } + + // Extract TypeName from TypeFull (e.g., "varchar(255)" -> "varchar") + col.TypeName = col.TypeFull + if idx := strings.Index(col.TypeFull, "("); idx != -1 { + col.TypeName = col.TypeFull[:idx] + } + + if nullableStr == "YES" { + col.Nullable = true + } + columns = append(columns, &col) + } + return columns, rows.Err() +} + +func (b *mysqlBuilder) GetIndexes(c core.Context, tableName string) ([]*core.Index, error) { + if c == nil || tableName == "" { + return nil, errors.New("invalid arguments") + } + + exists, err := b.HasTable(c, tableName) + if err != nil { + return nil, err + } + if !exists { + return nil, nil + } + + query, err := b.Grammar.CompileIndexes("", tableName) + if err != nil { + return nil, err + } + rows, err := c.Query(query) + if err != nil { + return nil, err + } + defer rows.Close() + + indexMap := make(map[string]*core.Index) + var indexNames []string + + cols, err := rows.Columns() + if err != nil { + return nil, err + } + + for rows.Next() { + var nonUniqueVal int + var keyNameVal, columnNameVal string + + dest := make([]any, len(cols)) + for i, col := range cols { + switch strings.ToLower(col) { + case "non_unique": + dest[i] = &nonUniqueVal + case "key_name": + dest[i] = &keyNameVal + case "column_name": + dest[i] = &columnNameVal + default: + dest[i] = new(any) + } + } + + err = rows.Scan(dest...) + if err != nil { + return nil, err + } + + if idx, ok := indexMap[keyNameVal]; ok { + idx.Columns = append(idx.Columns, columnNameVal) + } else { + idx = &core.Index{ + Name: keyNameVal, + Columns: []string{columnNameVal}, + Unique: nonUniqueVal == 0, + Primary: keyNameVal == "PRIMARY", + } + indexMap[keyNameVal] = idx + indexNames = append(indexNames, keyNameVal) + } + } + + var result []*core.Index + for _, name := range indexNames { + result = append(result, indexMap[name]) + } + return result, rows.Err() +} + +func (b *mysqlBuilder) GetTables(c core.Context) ([]*core.TableInfo, error) { + if c == nil { + return nil, errors.New("invalid arguments") + } + query, err := b.Grammar.CompileTables("") + if err != nil { + return nil, err + } + rows, err := c.Query(query) + if err != nil { + return nil, err + } + defer rows.Close() + + var tables []*core.TableInfo + for rows.Next() { + var table core.TableInfo + if err = rows.Scan(&table.Name, &table.Comment); err != nil { + return nil, err + } + tables = append(tables, &table) + } + return tables, rows.Err() +} + +func (b *mysqlBuilder) HasTable(c core.Context, name string) (bool, error) { + if c == nil || name == "" { + return false, errors.New("invalid arguments") + } + query, err := b.Grammar.CompileTableExists("", name) + if err != nil { + return false, err + } + row := c.QueryRow(query) + var exists int + err = row.Scan(&exists) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return false, nil + } + return false, err + } + return true, nil +} + +func (b *mysqlBuilder) HasColumn(c core.Context, table, column string) (bool, error) { + if c == nil || table == "" || column == "" { + return false, errors.New("invalid arguments") + } + cols, err := b.GetColumns(c, table) + if err != nil { + return false, err + } + for _, col := range cols { + if col.Name == column { + return true, nil + } + } + return false, nil +} + +func (b *mysqlBuilder) HasColumns(c core.Context, table string, columns []string) (bool, error) { + if c == nil || table == "" || len(columns) == 0 { + return false, errors.New("invalid arguments") + } + cols, err := b.GetColumns(c, table) + if err != nil { + return false, err + } + colMap := make(map[string]bool) + for _, col := range cols { + colMap[col.Name] = true + } + for _, col := range columns { + if col == "" || !colMap[col] { + return false, nil + } + } + return true, nil +} + +func (b *mysqlBuilder) HasIndex(c core.Context, table string, columns []string) (bool, error) { + if c == nil || table == "" || len(columns) == 0 { + return false, errors.New("invalid arguments") + } + indexes, err := b.GetIndexes(c, table) + if err != nil { + return false, err + } + for _, idx := range indexes { + if slices.Equal(idx.Columns, columns) { + return true, nil + } + if len(columns) == 1 && idx.Name == columns[0] { + return true, nil + } + } + return false, nil +} diff --git a/schema/mysql_builder_test.go b/schema/builders/mysql_builder_test.go similarity index 97% rename from schema/mysql_builder_test.go rename to schema/builders/mysql_builder_test.go index dad104b..3146f0f 100644 --- a/schema/mysql_builder_test.go +++ b/schema/builders/mysql_builder_test.go @@ -1,14 +1,14 @@ -package schema_test +package builders_test import ( "context" "database/sql" - "fmt" "testing" + "github.com/akfaiz/migris/internal/testutil" "github.com/akfaiz/migris/schema" - _ "github.com/go-sql-driver/mysql" // MySQL driver "github.com/stretchr/testify/suite" + "github.com/testcontainers/testcontainers-go" ) func TestMysqlBuilderSuite(t *testing.T) { @@ -21,28 +21,16 @@ type mysqlBuilderSuite struct { ctx context.Context db *sql.DB builder schema.Builder + tc testcontainers.Container } func (s *mysqlBuilderSuite) SetupSuite() { s.ctx = context.Background() - config := parseTestConfig() - - dsn := fmt.Sprintf( - "%s:%s@tcp(%s:%d)/%s?parseTime=true&loc=Local", - config.Username, - config.Password, - "localhost", - 3306, - config.Database, - ) - - db, err := sql.Open("mysql", dsn) - s.Require().NoError(err) - - err = db.Ping() + container, db, err := testutil.StartMySQLTestDB(s.ctx) s.Require().NoError(err) + s.tc = container s.db = db s.builder, err = schema.NewBuilder("mysql") s.Require().NoError(err) @@ -50,6 +38,9 @@ func (s *mysqlBuilderSuite) SetupSuite() { func (s *mysqlBuilderSuite) TearDownSuite() { _ = s.db.Close() + if s.tc != nil { + _ = s.tc.Terminate(s.ctx) + } } func (s *mysqlBuilderSuite) AfterTest(_, _ string) { @@ -162,7 +153,8 @@ func (s *mysqlBuilderSuite) TestDrop() { s.Require().Error(err) }) s.Run("when all parameters are valid, should drop table successfully", func() { - err = builder.Create(c, "users", func(table *schema.Blueprint) { + _ = builder.DropIfExists(c, "users_drop_test") + err = builder.Create(c, "users_drop_test", func(table *schema.Blueprint) { table.ID() table.String("name", 255) table.String("email", 255).Unique() @@ -170,7 +162,7 @@ func (s *mysqlBuilderSuite) TestDrop() { table.Timestamps() }) s.Require().NoError(err, "expected no error when creating table before dropping it") - err = builder.Drop(c, "users") + err = builder.Drop(c, "users_drop_test") s.Require().NoError(err, "expected no error when dropping table with valid parameters") }) s.Run("when table does not exist, should return error", func() { @@ -327,7 +319,7 @@ func (s *mysqlBuilderSuite) TestTable() { }) s.Run("should drop fulltext index", func() { err = builder.Table(c, "users", func(table *schema.Blueprint) { - table.DropFulltext("ft_users_bio") + table.DropFulltext([]string{"bio"}) }) s.Require().NoError(err, "expected no error when dropping fulltext index from table") }) @@ -345,7 +337,7 @@ func (s *mysqlBuilderSuite) TestTable() { }) s.Run("should drop foreign key", func() { err = builder.Table(c, "users", func(table *schema.Blueprint) { - table.DropForeign("fk_users_roles") + table.DropForeign([]string{"role_id"}) }) s.Require().NoError(err, "expected no error when dropping foreign key from users table") }) diff --git a/schema/builders/postgres_builder.go b/schema/builders/postgres_builder.go new file mode 100644 index 0000000..2bf692a --- /dev/null +++ b/schema/builders/postgres_builder.go @@ -0,0 +1,253 @@ +package builders + +import ( + "database/sql" + "errors" + "slices" + + "github.com/akfaiz/migris/schema/blueprint" + "github.com/akfaiz/migris/schema/core" + "github.com/akfaiz/migris/schema/grammars" +) + +type postgresBuilder struct { + baseBuilder +} + +var _ Builder = (*postgresBuilder)(nil) + +func NewPostgresBuilder() Builder { + g, _ := grammars.NewGrammar("postgres") + b := &postgresBuilder{} + b.baseBuilder = baseBuilder{Grammar: g, Outer: b} + return b +} + +func (b *postgresBuilder) Drop(c core.Context, name string) error { + if c == nil || name == "" { + return errors.New("invalid arguments") + } + query, err := b.Grammar.CompileDrop(&blueprint.Blueprint{Name: name}) + if err != nil { + return err + } + _, err = c.Exec(query) + return err +} + +func (b *postgresBuilder) DropIfExists(c core.Context, name string) error { + if c == nil || name == "" { + return errors.New("invalid arguments") + } + query, err := b.Grammar.CompileDropIfExists(&blueprint.Blueprint{Name: name}) + if err != nil { + return err + } + _, err = c.Exec(query) + return err +} + +func (b *postgresBuilder) Rename(c core.Context, from, to string) error { + if c == nil || from == "" || to == "" { + return errors.New("invalid arguments") + } + query, err := b.Grammar.CompileRename(&blueprint.Blueprint{Name: from}, &blueprint.Command{To: to}) + if err != nil { + return err + } + _, err = c.Exec(query) + return err +} + +func (b *postgresBuilder) GetColumns(c core.Context, tableName string) ([]*core.Column, error) { + if c == nil || tableName == "" { + return nil, errors.New("invalid arguments") + } + + exists, err := b.HasTable(c, tableName) + if err != nil { + return nil, err + } + if !exists { + return nil, nil + } + + query, err := b.Grammar.CompileColumns("", tableName) + if err != nil { + return nil, err + } + + rows, err := c.Query(query) + if err != nil { + return nil, err + } + defer rows.Close() + + var columns []*core.Column + for rows.Next() { + var col core.Column + var isNullable string + if err = rows.Scan(&col.Name, &col.TypeName, &isNullable, &col.DefaultVal, &col.Comment); err != nil { + return nil, err + } + col.Nullable = (isNullable == "YES") + columns = append(columns, &col) + } + return columns, rows.Err() +} + +func (b *postgresBuilder) GetIndexes(c core.Context, tableName string) ([]*core.Index, error) { + if c == nil || tableName == "" { + return nil, errors.New("invalid arguments") + } + + exists, err := b.HasTable(c, tableName) + if err != nil { + return nil, err + } + if !exists { + return nil, nil + } + + query, err := b.Grammar.CompileIndexes("", tableName) + if err != nil { + return nil, err + } + rows, err := c.Query(query) + if err != nil { + return nil, err + } + defer rows.Close() + + indexMap := make(map[string]*core.Index) + var indexNames []string + + for rows.Next() { + var indexName string + var isUnique, isPrimary bool + var columnName string + + if err = rows.Scan(&indexName, &isUnique, &isPrimary, &columnName); err != nil { + return nil, err + } + + if idx, ok := indexMap[indexName]; ok { + idx.Columns = append(idx.Columns, columnName) + } else { + idx = &core.Index{ + Name: indexName, + Columns: []string{columnName}, + Unique: isUnique, + Primary: isPrimary, + } + indexMap[indexName] = idx + indexNames = append(indexNames, indexName) + } + } + + var result []*core.Index + for _, name := range indexNames { + result = append(result, indexMap[name]) + } + return result, rows.Err() +} + +func (b *postgresBuilder) GetTables(c core.Context) ([]*core.TableInfo, error) { + if c == nil { + return nil, errors.New("invalid arguments") + } + query, err := b.Grammar.CompileTables("") + if err != nil { + return nil, err + } + rows, err := c.Query(query) + if err != nil { + return nil, err + } + defer rows.Close() + + var tables []*core.TableInfo + for rows.Next() { + var table core.TableInfo + if err = rows.Scan(&table.Name, &table.Schema, &table.Comment); err != nil { + return nil, err + } + tables = append(tables, &table) + } + return tables, rows.Err() +} + +func (b *postgresBuilder) HasTable(c core.Context, name string) (bool, error) { + if c == nil || name == "" { + return false, errors.New("invalid arguments") + } + query, err := b.Grammar.CompileTableExists("", name) + if err != nil { + return false, err + } + row := c.QueryRow(query) + var exists int + err = row.Scan(&exists) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return false, nil + } + return false, err + } + return true, nil +} + +func (b *postgresBuilder) HasColumn(c core.Context, table, column string) (bool, error) { + if c == nil || table == "" || column == "" { + return false, errors.New("invalid arguments") + } + cols, err := b.GetColumns(c, table) + if err != nil { + return false, err + } + for _, col := range cols { + if col.Name == column { + return true, nil + } + } + return false, nil +} + +func (b *postgresBuilder) HasColumns(c core.Context, table string, columns []string) (bool, error) { + if c == nil || table == "" || len(columns) == 0 { + return false, errors.New("invalid arguments") + } + cols, err := b.GetColumns(c, table) + if err != nil { + return false, err + } + colMap := make(map[string]bool) + for _, col := range cols { + colMap[col.Name] = true + } + for _, col := range columns { + if col == "" || !colMap[col] { + return false, nil + } + } + return true, nil +} + +func (b *postgresBuilder) HasIndex(c core.Context, table string, columns []string) (bool, error) { + if c == nil || table == "" || len(columns) == 0 { + return false, errors.New("invalid arguments") + } + indexes, err := b.GetIndexes(c, table) + if err != nil { + return false, err + } + for _, idx := range indexes { + if slices.Equal(idx.Columns, columns) { + return true, nil + } + if len(columns) == 1 && idx.Name == columns[0] { + return true, nil + } + } + return false, nil +} diff --git a/schema/postgres_builder_test.go b/schema/builders/postgres_builder_test.go similarity index 96% rename from schema/postgres_builder_test.go rename to schema/builders/postgres_builder_test.go index e52cab4..c19bb52 100644 --- a/schema/postgres_builder_test.go +++ b/schema/builders/postgres_builder_test.go @@ -1,68 +1,36 @@ -package schema_test +package builders_test import ( "context" "database/sql" - "fmt" - "os" "testing" + "github.com/akfaiz/migris/internal/testutil" "github.com/akfaiz/migris/schema" - _ "github.com/jackc/pgx/v5/stdlib" "github.com/stretchr/testify/suite" + "github.com/testcontainers/testcontainers-go" ) func TestPostgresBuilderSuite(t *testing.T) { suite.Run(t, new(postgresBuilderSuite)) } -type dbConfig struct { - Database string - Username string - Password string -} - -func getStringFromEnv(envVar string, defaultValue string) string { - if value, exists := os.LookupEnv(envVar); exists && value != "" { - return value - } - return defaultValue -} - -func parseTestConfig() dbConfig { - return dbConfig{ - Database: getStringFromEnv("DB_NAME", "db_test"), - Username: getStringFromEnv("DB_USER", "root"), - Password: getStringFromEnv("DB_PASSWORD", "password"), - } -} - type postgresBuilderSuite struct { suite.Suite ctx context.Context db *sql.DB builder schema.Builder + tc testcontainers.Container } func (s *postgresBuilderSuite) SetupSuite() { s.ctx = context.Background() - config := parseTestConfig() - - dsn := fmt.Sprintf( - "host=localhost port=5432 user=%s password=%s dbname=%s sslmode=disable", - config.Username, - config.Password, - config.Database, - ) - - db, err := sql.Open("pgx", dsn) - s.Require().NoError(err) - - err = db.Ping() + container, db, err := testutil.StartPostgresTestDB(s.ctx) s.Require().NoError(err) + s.tc = container s.db = db s.builder, err = schema.NewBuilder("postgres") s.Require().NoError(err) @@ -70,6 +38,9 @@ func (s *postgresBuilderSuite) SetupSuite() { func (s *postgresBuilderSuite) TearDownSuite() { _ = s.db.Close() + if s.tc != nil { + _ = s.tc.Terminate(s.ctx) + } } func (s *postgresBuilderSuite) TestCreate() { @@ -337,13 +308,13 @@ func (s *postgresBuilderSuite) TestTable() { }) s.Run("should drop unique constraint", func() { err = builder.Table(c, "users", func(table *schema.Blueprint) { - table.DropUnique([]string{"email"}) + table.DropUnique("uk_users_email") }) s.Require().NoError(err, "expected no error when dropping unique constraint from table") }) s.Run("should drop fulltext index", func() { err = builder.Table(c, "users", func(table *schema.Blueprint) { - table.DropFulltext("ft_users_bio") + table.DropFulltext([]string{"bio"}) }) s.Require().NoError(err, "expected no error when dropping fulltext index from table") }) @@ -361,13 +332,13 @@ func (s *postgresBuilderSuite) TestTable() { }) s.Run("should drop foreign key", func() { err = builder.Table(c, "users", func(table *schema.Blueprint) { - table.DropForeign("fk_users_roles") + table.DropForeign([]string{"role_id"}) }) s.Require().NoError(err, "expected no error when dropping foreign key from users table") }) s.Run("should drop primary key", func() { err = builder.Table(c, "users", func(table *schema.Blueprint) { - table.DropPrimary("pk_users") + table.DropPrimary([]string{"id"}) }) s.Require().NoError(err, "expected no error when dropping primary key from users table") }) diff --git a/schema/builders/sqlite_builder.go b/schema/builders/sqlite_builder.go new file mode 100644 index 0000000..80f4700 --- /dev/null +++ b/schema/builders/sqlite_builder.go @@ -0,0 +1,313 @@ +package builders + +import ( + "database/sql" + "errors" + "fmt" + "slices" + + "github.com/akfaiz/migris/schema/blueprint" + "github.com/akfaiz/migris/schema/core" + "github.com/akfaiz/migris/schema/grammars" +) + +type sqliteBuilder struct { + baseBuilder +} + +var _ Builder = (*sqliteBuilder)(nil) + +func NewSqliteBuilder() Builder { + g, _ := grammars.NewGrammar("sqlite3") + b := &sqliteBuilder{} + b.baseBuilder = baseBuilder{Grammar: g, Outer: b} + return b +} + +func (b *sqliteBuilder) Drop(c core.Context, name string) error { + if c == nil || name == "" { + return errors.New("invalid arguments") + } + query, err := b.Grammar.CompileDrop(&blueprint.Blueprint{Name: name}) + if err != nil { + return err + } + _, err = c.Exec(query) + return err +} + +func (b *sqliteBuilder) DropIfExists(c core.Context, name string) error { + if c == nil || name == "" { + return errors.New("invalid arguments") + } + query, err := b.Grammar.CompileDropIfExists(&blueprint.Blueprint{Name: name}) + if err != nil { + return err + } + _, err = c.Exec(query) + return err +} + +func (b *sqliteBuilder) Rename(c core.Context, from, to string) error { + if c == nil || from == "" || to == "" { + return errors.New("invalid arguments") + } + query, err := b.Grammar.CompileRename(&blueprint.Blueprint{Name: from}, &blueprint.Command{To: to}) + if err != nil { + return err + } + _, err = c.Exec(query) + return err +} + +func (b *sqliteBuilder) GetColumns(c core.Context, tableName string) ([]*core.Column, error) { + if c == nil || tableName == "" { + return nil, errors.New("invalid arguments") + } + + exists, err := b.HasTable(c, tableName) + if err != nil { + return nil, err + } + if !exists { + return nil, nil + } + + query, err := b.Grammar.CompileColumns("", tableName) + if err != nil { + return nil, err + } + + rows, err := c.Query(query) + if err != nil { + return nil, err + } + defer rows.Close() + + var columns []*core.Column + for rows.Next() { + var col core.Column + var notNull int + var discard any + if err = rows.Scan(&discard, &col.Name, &col.TypeName, ¬Null, &col.DefaultVal, &discard); err != nil { + return nil, err + } + col.Nullable = (notNull == 0) + columns = append(columns, &col) + } + return columns, rows.Err() +} + +func (b *sqliteBuilder) GetIndexes(c core.Context, tableName string) ([]*core.Index, error) { + if c == nil || tableName == "" { + return nil, errors.New("invalid arguments") + } + + exists, err := b.HasTable(c, tableName) + if err != nil { + return nil, err + } + if !exists { + return nil, nil + } + + query, err := b.Grammar.CompileIndexes("", tableName) + if err != nil { + return nil, err + } + rows, err := c.Query(query) + if err != nil { + return nil, err + } + defer rows.Close() + + var indexes []*core.Index + for rows.Next() { + var idx core.Index + var unique int + var origin string + var discard any + if err = rows.Scan(&discard, &idx.Name, &unique, &origin, &discard); err != nil { + return nil, err + } + idx.Unique = (unique == 1) + idx.Primary = (origin == "pk") + + // Fetch columns for each index + idx.Columns = b.fetchIndexColumns(c, idx.Name) + indexes = append(indexes, &idx) + } + + // SQLite's PRAGMA index_list does not always include the primary key if it's an INTEGER PRIMARY KEY. + if !b.hasPrimaryIndex(indexes) { + if pkIdx := b.fetchPrimaryKey(c, tableName); pkIdx != nil { + indexes = append(indexes, pkIdx) + } + } + + return indexes, rows.Err() +} + +func (b *sqliteBuilder) fetchIndexColumns(c core.Context, indexName string) []string { + var columns []string + columnQuery := fmt.Sprintf("PRAGMA index_info(%q)", indexName) + colRows, colErr := c.Query(columnQuery) + if colErr != nil { + return columns + } + defer colRows.Close() + + for colRows.Next() { + var colName string + var discard any + if scanErr := colRows.Scan(&discard, &discard, &colName); scanErr == nil { + columns = append(columns, colName) + } + } + return columns +} + +func (b *sqliteBuilder) hasPrimaryIndex(indexes []*core.Index) bool { + for _, idx := range indexes { + if idx.Primary { + return true + } + } + return false +} + +func (b *sqliteBuilder) fetchPrimaryKey(c core.Context, tableName string) *core.Index { + tableInfoQuery := fmt.Sprintf("PRAGMA table_info(%q)", tableName) + tableInfoRows, infoErr := c.Query(tableInfoQuery) + if infoErr != nil { + return nil + } + defer tableInfoRows.Close() + + var pkColumns []string + for tableInfoRows.Next() { + var colName string + var pk int + var discard any + if scanErr := tableInfoRows.Scan( + &discard, + &colName, + &discard, + &discard, + &discard, + &pk, + ); scanErr == nil && pk > 0 { + pkColumns = append(pkColumns, colName) + } + } + + if len(pkColumns) > 0 { + return &core.Index{ + Name: "primary", + Columns: pkColumns, + Unique: true, + Primary: true, + } + } + return nil +} + +func (b *sqliteBuilder) GetTables(c core.Context) ([]*core.TableInfo, error) { + if c == nil { + return nil, errors.New("invalid arguments") + } + query, err := b.Grammar.CompileTables("") + if err != nil { + return nil, err + } + rows, err := c.Query(query) + if err != nil { + return nil, err + } + defer rows.Close() + + var tables []*core.TableInfo + for rows.Next() { + var table core.TableInfo + if err = rows.Scan(&table.Name); err != nil { + return nil, err + } + tables = append(tables, &table) + } + return tables, rows.Err() +} + +func (b *sqliteBuilder) HasTable(c core.Context, name string) (bool, error) { + if c == nil || name == "" { + return false, errors.New("invalid arguments") + } + query, err := b.Grammar.CompileTableExists("", name) + if err != nil { + return false, err + } + row := c.QueryRow(query) + var exists int + err = row.Scan(&exists) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return false, nil + } + return false, err + } + return true, nil +} + +func (b *sqliteBuilder) HasColumn(c core.Context, table, column string) (bool, error) { + if c == nil || table == "" || column == "" { + return false, errors.New("invalid arguments") + } + cols, err := b.GetColumns(c, table) + if err != nil { + return false, err + } + for _, col := range cols { + if col.Name == column { + return true, nil + } + } + return false, nil +} + +func (b *sqliteBuilder) HasColumns(c core.Context, table string, columns []string) (bool, error) { + if c == nil || table == "" || len(columns) == 0 { + return false, errors.New("invalid arguments") + } + cols, err := b.GetColumns(c, table) + if err != nil { + return false, err + } + colMap := make(map[string]bool) + for _, col := range cols { + colMap[col.Name] = true + } + for _, col := range columns { + if col == "" || !colMap[col] { + return false, nil + } + } + return true, nil +} + +func (b *sqliteBuilder) HasIndex(c core.Context, table string, columns []string) (bool, error) { + if c == nil || table == "" || len(columns) == 0 { + return false, errors.New("invalid arguments") + } + indexes, err := b.GetIndexes(c, table) + if err != nil { + return false, err + } + for _, idx := range indexes { + if slices.Equal(idx.Columns, columns) { + return true, nil + } + if len(columns) == 1 && idx.Name == columns[0] { + return true, nil + } + } + return false, nil +} diff --git a/schema/sqlite_builder_test.go b/schema/builders/sqlite_builder_test.go similarity index 93% rename from schema/sqlite_builder_test.go rename to schema/builders/sqlite_builder_test.go index 01e765c..2919cd7 100644 --- a/schema/sqlite_builder_test.go +++ b/schema/builders/sqlite_builder_test.go @@ -1,4 +1,4 @@ -package schema_test +package builders_test import ( "context" @@ -99,11 +99,6 @@ func (s *sqliteBuilderSuite) TestCreate() { }) s.Require().NoError(err, "expected no error when creating table with valid parameters") }) - s.Run("when have composite primary key should create it successfully", func() { - // Note: SQLite doesn't support composite primary keys at table level - // This test is skipped for SQLite as it's a known limitation - s.T().Skip("SQLite doesn't support composite primary keys at table level") - }) s.Run("when have custom index should create it successfully", func() { err = builder.Create(c, "orders_2", func(table *schema.Blueprint) { table.ID() @@ -273,38 +268,18 @@ func (s *sqliteBuilderSuite) TestTable() { } s.Require().NoError(err, "expected no error when adding column with valid parameters") }) - s.Run("should modify existing column", func() { - // SQLite doesn't support MODIFY COLUMN - s.T().Skip("SQLite does not support modifying columns") - }) - s.Run("should drop column and rename existing one", func() { - // SQLite has limited support for dropping columns (only since v3.35.0) - s.T().Skip("SQLite has limited support for dropping/renaming columns") - }) s.Run("should add index", func() { err = builder.Table(c, "users", func(table *schema.Blueprint) { table.Index("bio").Name("idx_users_bio") }) s.Require().NoError(err, "expected no error when adding index to table") }) - s.Run("should rename index", func() { - // SQLite doesn't support renaming indexes - s.T().Skip("SQLite does not support renaming indexes") - }) s.Run("should drop index", func() { err = builder.Table(c, "users", func(table *schema.Blueprint) { table.DropIndex("idx_users_bio") }) s.Require().NoError(err, "expected no error when dropping index from table") }) - s.Run("should drop unique constraint", func() { - // SQLite doesn't support dropping constraints individually - s.T().Skip("SQLite does not support dropping unique constraints individually") - }) - s.Run("should drop fulltext index", func() { - // Note: SQLite FullText requires FTS virtual tables, skip for regular table test - s.T().Skip("SQLite FullText requires special FTS virtual table setup") - }) }) } @@ -378,7 +353,11 @@ func (s *sqliteBuilderSuite) TestGetIndexes() { indexes, err := builder.GetIndexes(c, "users") s.Require().NoError(err, "expected no error when getting indexes with valid parameters") - s.Len(indexes, 2, "expected 2 indexes to be returned") // SQLite: one for email unique and one for name index + s.Len( + indexes, + 3, + "expected 3 indexes to be returned", + ) // SQLite: one for primary, one for email unique and one for name index }) s.Run("when table does not exist, should return empty indexes", func() { indexes, err := builder.GetIndexes(c, "non_existent_table") diff --git a/schema/column_definition.go b/schema/column_definition.go deleted file mode 100644 index 88cf3b2..0000000 --- a/schema/column_definition.go +++ /dev/null @@ -1,198 +0,0 @@ -package schema - -import ( - "slices" - - "github.com/akfaiz/migris/internal/util" -) - -// ColumnDefinition defines the interface for defining a column in a database table. -type ColumnDefinition interface { - // AutoIncrement sets the column to auto-increment. - // This is typically used for primary key columns. - AutoIncrement() ColumnDefinition - // Change changes the column definition. - Change() ColumnDefinition - // Charset sets the character set for the column. - Charset(charset string) ColumnDefinition - // Collation sets the collation for the column. - Collation(collation string) ColumnDefinition - // Comment adds a comment to the column definition. - Comment(comment string) ColumnDefinition - // Default sets a default value for the column. - Default(value any) ColumnDefinition - // Index adds an index to the column. - Index(params ...any) ColumnDefinition - // Nullable sets the column to be nullable or not. - Nullable(value ...bool) ColumnDefinition - // OnUpdate sets the value to be used when the column is updated. - OnUpdate(value any) ColumnDefinition - // Primary sets the column as a primary key. - Primary(value ...bool) ColumnDefinition - // Unique sets the column to be unique. - Unique(params ...any) ColumnDefinition - // Unsigned sets the column to be unsigned (applicable for numeric types). - Unsigned() ColumnDefinition - // UseCurrent sets the column to use the current timestamp as default. - UseCurrent() ColumnDefinition - // UseCurrentOnUpdate sets the column to use the current timestamp on update. - UseCurrentOnUpdate() ColumnDefinition -} - -type columnDefinition struct { - commands []string - name string - columnType string - charset *string - collation *string - comment *string - defaultValue any - onUpdateValue any - useCurrent bool - useCurrentOnUpdate bool - nullable *bool - autoIncrement *bool - unsigned *bool - primary *bool - index *bool - indexName string - unique *bool - uniqueName string - length *int - precision *int - total *int - places *int - change bool - allowed []string // for enum type columns - subtype *string // for geography and geometry types - srid *int // for geography and geometry types -} - -// Expression is a type for expressions that can be used as default values for columns. -// -// Example: -// -// schema.Timestamp("created_at").Default(schema.Expression("CURRENT_TIMESTAMP")). -type Expression string - -func (e Expression) String() string { - return string(e) -} - -var _ ColumnDefinition = &columnDefinition{} - -func (c *columnDefinition) addCommand(command string) { - c.commands = append(c.commands, command) -} - -func (c *columnDefinition) hasCommand(command string) bool { - return slices.Contains(c.commands, command) -} - -func (c *columnDefinition) SetDefault(value any) { - c.addCommand("default") - c.defaultValue = value -} - -func (c *columnDefinition) SetOnUpdate(value any) { - c.addCommand("onUpdate") - c.onUpdateValue = value -} - -func (c *columnDefinition) SetSubtype(value *string) { - c.subtype = value -} - -func (c *columnDefinition) AutoIncrement() ColumnDefinition { - c.autoIncrement = util.PtrOf(true) - return c -} - -func (c *columnDefinition) Charset(charset string) ColumnDefinition { - c.charset = &charset - return c -} - -func (c *columnDefinition) Change() ColumnDefinition { - c.change = true - return c -} - -func (c *columnDefinition) Collation(collation string) ColumnDefinition { - c.collation = &collation - return c -} - -func (c *columnDefinition) Comment(comment string) ColumnDefinition { - c.addCommand("comment") - c.comment = &comment - return c -} - -func (c *columnDefinition) Default(value any) ColumnDefinition { - c.addCommand("default") - c.defaultValue = value - - return c -} - -func (c *columnDefinition) Index(params ...any) ColumnDefinition { - index := true - for _, param := range params { - switch v := param.(type) { - case bool: - index = v - case string: - c.indexName = v - } - } - c.index = &index - return c -} - -func (c *columnDefinition) Nullable(value ...bool) ColumnDefinition { - c.addCommand("nullable") - c.nullable = util.OptionalPtr(true, value...) - return c -} - -func (c *columnDefinition) OnUpdate(value any) ColumnDefinition { - c.addCommand("onUpdate") - c.onUpdateValue = value - return c -} - -func (c *columnDefinition) Primary(value ...bool) ColumnDefinition { - val := util.Optional(true, value...) - c.primary = &val - return c -} - -func (c *columnDefinition) Unique(params ...any) ColumnDefinition { - unique := true - for _, param := range params { - switch v := param.(type) { - case bool: - unique = v - case string: - c.uniqueName = v - } - } - c.unique = &unique - return c -} - -func (c *columnDefinition) Unsigned() ColumnDefinition { - c.unsigned = util.PtrOf(true) - return c -} - -func (c *columnDefinition) UseCurrent() ColumnDefinition { - c.useCurrent = true - return c -} - -func (c *columnDefinition) UseCurrentOnUpdate() ColumnDefinition { - c.useCurrentOnUpdate = true - return c -} diff --git a/schema/command.go b/schema/command.go deleted file mode 100644 index 907798a..0000000 --- a/schema/command.go +++ /dev/null @@ -1,40 +0,0 @@ -package schema - -const ( - commandAdd string = "add" - commandChange string = "change" - commandCreate string = "create" - commandDrop string = "drop" - commandDropIfExists string = "dropIfExists" - commandDropColumn string = "dropColumn" - commandDropForeign string = "dropForeign" - commandDropFullText string = "dropFullText" - commandDropIndex string = "dropIndex" - commandDropPrimary string = "dropPrimary" - commandDropUnique string = "dropUnique" - commandForeign string = "foreign" - commandFullText string = "fullText" - commandIndex string = "index" - commandPrimary string = "primary" - commandRename string = "rename" - commandRenameColumn string = "renameColumn" - commandRenameIndex string = "renameIndex" - commandUnique string = "unique" -) - -type command struct { - column *columnDefinition - deferrable *bool - initiallyImmediate *bool - algorithm string - from string - index string - language string - name string - on string - onDelete string - onUpdate string - to string - columns []string - references []string -} diff --git a/schema/context.go b/schema/context.go deleted file mode 100644 index 3d659f0..0000000 --- a/schema/context.go +++ /dev/null @@ -1,52 +0,0 @@ -package schema - -import ( - "context" - "database/sql" -) - -// Context interface defines the contract for database operations -// This allows us to switch between normal execution and dry-run mode. -type Context interface { - Exec(query string, args ...any) (sql.Result, error) - Query(query string, args ...any) (*sql.Rows, error) - QueryRow(query string, args ...any) *sql.Row -} - -// RegularContext implements Context for normal database operations. -type RegularContext struct { - ctx context.Context - tx *sql.Tx - filename string -} - -type ContextOptions func(*RegularContext) - -func WithFilename(filename string) ContextOptions { - return func(c *RegularContext) { - c.filename = filename - } -} - -func NewContext(ctx context.Context, tx *sql.Tx, opts ...ContextOptions) Context { - c := &RegularContext{ - ctx: ctx, - tx: tx, - } - for _, opt := range opts { - opt(c) - } - return c -} - -func (c *RegularContext) Exec(query string, args ...any) (sql.Result, error) { - return c.tx.ExecContext(c.ctx, query, args...) -} - -func (c *RegularContext) Query(query string, args ...any) (*sql.Rows, error) { - return c.tx.QueryContext(c.ctx, query, args...) -} - -func (c *RegularContext) QueryRow(query string, args ...any) *sql.Row { - return c.tx.QueryRowContext(c.ctx, query, args...) -} diff --git a/schema/core/context.go b/schema/core/context.go new file mode 100644 index 0000000..4caaef8 --- /dev/null +++ b/schema/core/context.go @@ -0,0 +1,120 @@ +package core + +import ( + "context" + "database/sql" +) + +// Context interface defines the contract for database operations. +type Context interface { + Exec(query string, args ...any) (sql.Result, error) + Query(query string, args ...any) (Rows, error) + QueryRow(query string, args ...any) Row + Dialect() string +} + +// Rows interface defines the methods required for database rows. +type Rows interface { + Next() bool + Scan(dest ...any) error + Close() error + Err() error + Columns() ([]string, error) +} + +// Row interface defines the method required for a single database row. +type Row interface { + Scan(dest ...any) error +} + +// Column represents a database column with its properties. +type Column struct { + Name string // Name is the name of the column. + TypeName string // TypeName is the name of the column type (e.g., "VARCHAR", "INT"). + TypeFull string // TypeFull is the full type name including any modifiers (e.g., "VARCHAR(255)", "INT(11)"). + Collation sql.NullString // Collation is the collation of the column, if applicable. + Nullable bool // Nullable indicates whether the column can contain NULL values. + DefaultVal sql.NullString // DefaultVal is the default value for the column, if any. + Comment sql.NullString // Comment is an optional comment for the column. + Extra sql.NullString // Extra contains additional information about the column (e.g., "auto_increment"). +} + +// Index represents a database index with its properties. +type Index struct { + Name string // Name is the name of the index. + Columns []string // Columns is a slice of column names that are part of the index. + Type string // e.g., "btree", "hash" + Unique bool // Indicates if the index is unique + Primary bool // Indicates if the index is a primary key +} + +// TableInfo represents information about a database table. +type TableInfo struct { + Name string // Name is the name of the table. + Schema string // Schema is the schema where the table resides. + Size int64 // Size is the size of the table in bytes. + Comment sql.NullString // Comment is an optional comment for the table. + Engine sql.NullString // Engine is the storage engine used for the table (e.g., "InnoDB", "MyISAM"). + Collation sql.NullString // Collation is the collation used for the table (e.g., "utf8mb4_general_ci"). +} + +// RegularContext implements Context for normal database operations. +type RegularContext struct { + ctx context.Context + tx *sql.Tx + filename string + dialect string +} + +type ContextOptions func(*RegularContext) + +func WithFilename(filename string) ContextOptions { + return func(c *RegularContext) { + c.filename = filename + } +} + +func WithDialect(dialect string) ContextOptions { + return func(c *RegularContext) { + c.dialect = dialect + } +} + +func NewContext(ctx context.Context, tx *sql.Tx, opts ...ContextOptions) Context { + c := &RegularContext{ + ctx: ctx, + tx: tx, + } + for _, opt := range opts { + opt(c) + } + return c +} + +func (c *RegularContext) Exec(query string, args ...any) (sql.Result, error) { + return c.tx.ExecContext(c.ctx, query, args...) +} + +func (c *RegularContext) Query(query string, args ...any) (Rows, error) { + rows, err := c.tx.QueryContext(c.ctx, query, args...) + if err != nil { + return nil, err + } + defer func() { + if err != nil { + _ = rows.Close() + } + }() + if err = rows.Err(); err != nil { + return nil, err + } + return rows, nil +} + +func (c *RegularContext) QueryRow(query string, args ...any) Row { + return c.tx.QueryRowContext(c.ctx, query, args...) +} + +func (c *RegularContext) Dialect() string { + return c.dialect +} diff --git a/schema/core/context_test.go b/schema/core/context_test.go new file mode 100644 index 0000000..a8f634e --- /dev/null +++ b/schema/core/context_test.go @@ -0,0 +1,111 @@ +package core_test + +import ( + "context" + "database/sql" + "testing" + + "github.com/akfaiz/migris/schema/core" + _ "github.com/mattn/go-sqlite3" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRegularContext(t *testing.T) { + db, err := sql.Open("sqlite3", ":memory:") + require.NoError(t, err) + defer db.Close() + + ctx := context.Background() + tx, err := db.BeginTx(ctx, nil) + require.NoError(t, err) + defer tx.Rollback() + + c := core.NewContext(ctx, tx, core.WithDialect("sqlite3"), core.WithFilename("test.go")) + + t.Run("Exec", func(t *testing.T) { + res, err := c.Exec("CREATE TABLE test (id INTEGER)") + require.NoError(t, err) + assert.NotNil(t, res) + }) + + t.Run("Query", func(t *testing.T) { + _, err := c.Exec("INSERT INTO test (id) VALUES (1)") + require.NoError(t, err) + + rows, err := c.Query("SELECT id FROM test") + require.NoError(t, err) + defer rows.Close() + + assert.True(t, rows.Next()) + var id int + err = rows.Scan(&id) + require.NoError(t, err) + assert.Equal(t, 1, id) + }) + + t.Run("QueryRow", func(t *testing.T) { + row := c.QueryRow("SELECT id FROM test WHERE id = 1") + var id int + err := row.Scan(&id) + require.NoError(t, err) + assert.Equal(t, 1, id) + }) + + t.Run("Dialect", func(t *testing.T) { + assert.Equal(t, "sqlite3", c.Dialect()) + }) +} + +func TestDryRunContext_More(t *testing.T) { + ctx := context.Background() + drc := core.NewDryRunContext(ctx, core.WithDryRunDialect("postgres")) + + t.Run("Dialect", func(t *testing.T) { + assert.Equal(t, "postgres", drc.Dialect()) + }) + + t.Run("Exec", func(t *testing.T) { + res, err := drc.Exec("CREATE TABLE users") + require.NoError(t, err) + lastID, _ := res.LastInsertId() + rowsAff, _ := res.RowsAffected() + assert.Equal(t, int64(1), lastID) + assert.Equal(t, int64(1), rowsAff) + assert.Contains(t, drc.GetCapturedSQL(), "CREATE TABLE users") + }) + + t.Run("Query", func(t *testing.T) { + rows, err := drc.Query("SELECT * FROM users") + require.NoError(t, err) + assert.False(t, rows.Next()) + require.NoError(t, rows.Err()) + cols, _ := rows.Columns() + assert.Empty(t, cols) + assert.NoError(t, rows.Close()) + }) + + t.Run("QueryRow", func(t *testing.T) { + row := drc.QueryRow("SELECT 1") + err := row.Scan() + assert.ErrorIs(t, err, sql.ErrNoRows) + }) + + t.Run("PendingQueries", func(t *testing.T) { + assert.True(t, drc.HasPendingQuery()) + queries := drc.GetPendingQueries() + assert.Len(t, queries, 3) + assert.False(t, drc.HasPendingQuery()) + }) + + t.Run("GetPendingQuery", func(t *testing.T) { + drc.Exec("DROP TABLE users", 1, 2) + query, args := drc.GetPendingQuery() + assert.Equal(t, "DROP TABLE users", query) + assert.Equal(t, []any{1, 2}, args) + + q2, a2 := drc.GetPendingQuery() + assert.Empty(t, q2) + assert.Nil(t, a2) + }) +} diff --git a/schema/dryrun_context.go b/schema/core/dryrun_context.go similarity index 59% rename from schema/dryrun_context.go rename to schema/core/dryrun_context.go index 8eb3057..b0c459f 100644 --- a/schema/dryrun_context.go +++ b/schema/core/dryrun_context.go @@ -1,4 +1,4 @@ -package schema +package core import ( "context" @@ -10,6 +10,7 @@ import ( type DryRunContext struct { ctx context.Context capturedSQL []string + dialect string pendingQueries []QueryWithArgs } @@ -38,16 +39,17 @@ type MockRows struct { Closed bool } -func (m *MockRows) Close() { +func (m *MockRows) Close() error { m.Closed = true + return nil } func (m *MockRows) Next() bool { - return false // No rows in dry-run mode + return false } -func (m *MockRows) Scan(_ ...interface{}) error { - return nil // No data to scan in dry-run mode +func (m *MockRows) Scan(_ ...any) error { + return nil } func (m *MockRows) Columns() ([]string, error) { @@ -58,87 +60,82 @@ func (m *MockRows) Err() error { return nil } -// MockRow implements basic sql.Row functionality for dry-run mode. +// MockRow implements Row functionality for dry-run mode. type MockRow struct{} -func (m *MockRow) Scan(_ ...interface{}) error { - return sql.ErrNoRows // Always return no rows in dry-run mode +func (m *MockRow) Scan(_ ...any) error { + return sql.ErrNoRows +} + +// DryRunContextOptions is a function type for configuring DryRunContext. +type DryRunContextOptions func(*DryRunContext) + +func WithDryRunDialect(dialect string) DryRunContextOptions { + return func(c *DryRunContext) { + c.dialect = dialect + } } -// NewDryRunContext creates a new DryRunContext. -func NewDryRunContext(ctx context.Context) *DryRunContext { - return &DryRunContext{ +func NewDryRunContext(ctx context.Context, opts ...DryRunContextOptions) *DryRunContext { + c := &DryRunContext{ ctx: ctx, capturedSQL: make([]string, 0), } + for _, opt := range opts { + opt(c) + } + return c +} + +func (drc *DryRunContext) Dialect() string { + return drc.dialect } func (drc *DryRunContext) Exec(query string, args ...any) (sql.Result, error) { - // Clean up the query for display cleanQuery := strings.TrimSpace(query) drc.capturedSQL = append(drc.capturedSQL, cleanQuery) - drc.printSQL(cleanQuery, args...) - - // Return a mock result that simulates successful execution - return &MockResult{ - LastInsertID: 1, - RowsAffectedValue: 1, - }, nil + return &MockResult{LastInsertID: 1, RowsAffectedValue: 1}, nil } -func (drc *DryRunContext) Query(query string, args ...any) (*sql.Rows, error) { - // Capture the query but don't execute +func (drc *DryRunContext) Query(query string, args ...any) (Rows, error) { cleanQuery := strings.TrimSpace(query) drc.capturedSQL = append(drc.capturedSQL, cleanQuery) - drc.printSQL(cleanQuery, args...) - - // Return empty mock rows - return &sql.Rows{}, nil + return &MockRows{}, nil } -func (drc *DryRunContext) QueryRow(query string, args ...any) *sql.Row { - // Capture the query but don't execute +func (drc *DryRunContext) QueryRow(query string, args ...any) Row { cleanQuery := strings.TrimSpace(query) drc.capturedSQL = append(drc.capturedSQL, cleanQuery) - drc.printSQL(cleanQuery, args...) - - // Return a row that will return sql.ErrNoRows when scanned - return &sql.Row{} + return &MockRow{} } -// GetCapturedSQL returns all captured SQL statements. func (drc *DryRunContext) GetCapturedSQL() []string { return drc.capturedSQL } -// HasPendingQuery returns true if there's a pending SQL query to be printed. -func (drc *DryRunContext) HasPendingQuery() bool { - return len(drc.pendingQueries) > 0 -} - -// GetPendingQueries returns all pending queries and clears them. func (drc *DryRunContext) GetPendingQueries() []QueryWithArgs { queries := drc.pendingQueries drc.pendingQueries = nil return queries } -// GetPendingQuery returns the first pending query and arguments (for backward compatibility). +func (drc *DryRunContext) HasPendingQuery() bool { + return len(drc.pendingQueries) > 0 +} + func (drc *DryRunContext) GetPendingQuery() (string, []any) { if len(drc.pendingQueries) == 0 { return "", nil } - first := drc.pendingQueries[0] + q := drc.pendingQueries[0] drc.pendingQueries = drc.pendingQueries[1:] - return first.Query, first.Args + return q.Query, q.Args } -// printSQL stores the query for later printing by logger. func (drc *DryRunContext) printSQL(query string, args ...any) { - // Store query and args for later printing drc.pendingQueries = append(drc.pendingQueries, QueryWithArgs{ Query: query, Args: args, diff --git a/schema/core/dryrun_context_test.go b/schema/core/dryrun_context_test.go new file mode 100644 index 0000000..a8084d3 --- /dev/null +++ b/schema/core/dryrun_context_test.go @@ -0,0 +1,86 @@ +package core_test + +import ( + "context" + "database/sql" + "testing" + + "github.com/akfaiz/migris/schema/core" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewDryRunContext(t *testing.T) { + ctx := context.Background() + drc := core.NewDryRunContext(ctx) + + assert.NotNil(t, drc, "NewDryRunContext should not return nil") + assert.NotNil(t, drc.GetCapturedSQL(), "capturedSQL should be initialized") + assert.Empty(t, drc.GetCapturedSQL(), "capturedSQL should be empty initially") +} + +func TestDryRunContext_Exec(t *testing.T) { + drc := core.NewDryRunContext(context.Background()) + + query := "INSERT INTO users (name) VALUES ($1)" + args := []any{"John"} + + result, err := drc.Exec(query, args...) + require.NoError(t, err, "Exec should not return error") + assert.NotNil(t, result, "Exec should not return nil result") + + lastID, err := result.LastInsertId() + require.NoError(t, err, "LastInsertId should not return error") + assert.Equal(t, int64(1), lastID, "LastInsertId should be 1") + + rowsAffected, err := result.RowsAffected() + require.NoError(t, err, "RowsAffected should not return error") + assert.Equal(t, int64(1), rowsAffected, "RowsAffected should be 1") + + captured := drc.GetCapturedSQL() + assert.Len(t, captured, 1, "Should have 1 captured query") + assert.Equal(t, query, captured[0], "Captured query should match") +} + +func TestMockResult(t *testing.T) { + mock := &core.MockResult{ + LastInsertID: 42, + RowsAffectedValue: 10, + } + + lastID, err := mock.LastInsertId() + require.NoError(t, err, "LastInsertId should not return error") + assert.Equal(t, int64(42), lastID, "LastInsertId should be 42") + + rowsAffected, err := mock.RowsAffected() + require.NoError(t, err, "RowsAffected should not return error") + assert.Equal(t, int64(10), rowsAffected, "RowsAffected should be 10") +} + +func TestMockRows(t *testing.T) { + mock := &core.MockRows{} + + assert.False(t, mock.Closed, "MockRows should not be closed initially") + assert.False(t, mock.Next(), "MockRows.Next() should always return false") + + err := mock.Scan() + require.NoError(t, err, "MockRows.Scan() should return nil") + + columns, err := mock.Columns() + require.NoError(t, err, "MockRows.Columns() should not return error") + assert.Empty(t, columns, "MockRows.Columns() should return empty slice") + + err = mock.Err() + require.NoError(t, err, "MockRows.Err() should return nil") + + err = mock.Close() + require.NoError(t, err, "MockRows.Close() should return nil") + assert.True(t, mock.Closed, "MockRows should be closed after Close()") +} + +func TestMockRow(t *testing.T) { + mock := &core.MockRow{} + + err := mock.Scan() + assert.Equal(t, sql.ErrNoRows, err, "MockRow.Scan() should return sql.ErrNoRows") +} diff --git a/schema/dryrun_context_test.go b/schema/dryrun_context_test.go deleted file mode 100644 index 0079b93..0000000 --- a/schema/dryrun_context_test.go +++ /dev/null @@ -1,187 +0,0 @@ -package schema_test - -import ( - "context" - "database/sql" - "testing" - - "github.com/akfaiz/migris/schema" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestNewDryRunContext(t *testing.T) { - ctx := context.Background() - drc := schema.NewDryRunContext(ctx) - - assert.NotNil(t, drc, "NewDryRunContext should not return nil") - assert.NotNil(t, drc.GetCapturedSQL(), "capturedSQL should be initialized") - assert.Empty(t, drc.GetCapturedSQL(), "capturedSQL should be empty initially") -} - -func TestDryRunContext_Exec(t *testing.T) { - drc := schema.NewDryRunContext(context.Background()) - - query := "INSERT INTO users (name) VALUES ($1)" - args := []any{"John"} - - result, err := drc.Exec(query, args...) - require.NoError(t, err, "Exec should not return error") - assert.NotNil(t, result, "Exec should not return nil result") - - lastID, err := result.LastInsertId() - require.NoError(t, err, "LastInsertId should not return error") - assert.Equal(t, int64(1), lastID, "LastInsertId should be 1") - - rowsAffected, err := result.RowsAffected() - require.NoError(t, err, "RowsAffected should not return error") - assert.Equal(t, int64(1), rowsAffected, "RowsAffected should be 1") - - captured := drc.GetCapturedSQL() - assert.Len(t, captured, 1, "Should have 1 captured query") - assert.Equal(t, query, captured[0], "Captured query should match") -} - -func TestDryRunContext_Query(t *testing.T) { - drc := schema.NewDryRunContext(context.Background()) - - query := "SELECT * FROM users WHERE id = $1" - args := []any{1} - - rows, err := drc.Query(query, args...) //nolint:rowserrcheck // ignore roserrcheck for test - require.NoError(t, err, "Query should not return error") - assert.NotNil(t, rows, "Query should not return nil rows") - - captured := drc.GetCapturedSQL() - assert.Len(t, captured, 1, "Should have 1 captured query") - assert.Equal(t, query, captured[0], "Captured query should match") -} - -func TestDryRunContext_QueryRow(t *testing.T) { - drc := schema.NewDryRunContext(context.Background()) - - query := "SELECT name FROM users WHERE id = $1" - args := []any{1} - - row := drc.QueryRow(query, args...) - assert.NotNil(t, row, "QueryRow should not return nil row") - - captured := drc.GetCapturedSQL() - assert.Len(t, captured, 1, "Should have 1 captured query") - assert.Equal(t, query, captured[0], "Captured query should match") -} - -func TestDryRunContext_GetCapturedSQL(t *testing.T) { - drc := schema.NewDryRunContext(context.Background()) - - queries := []string{ - "INSERT INTO users (name) VALUES ($1)", - "UPDATE users SET name = $1 WHERE id = $2", - "DELETE FROM users WHERE id = $1", - } - - for _, query := range queries { - drc.Exec(query, "test") - } - - captured := drc.GetCapturedSQL() - assert.Len(t, captured, len(queries), "Should capture all queries") - assert.Equal(t, queries, captured, "Captured queries should match expected queries") -} - -func TestDryRunContext_PendingQueries(t *testing.T) { - drc := schema.NewDryRunContext(context.Background()) - - assert.False(t, drc.HasPendingQuery(), "Should not have pending queries initially") - - query := "INSERT INTO users (name) VALUES ($1)" - args := []any{"John"} - drc.Exec(query, args...) - - assert.True(t, drc.HasPendingQuery(), "Should have pending query after Exec") - - queries := drc.GetPendingQueries() - assert.Len(t, queries, 1, "Should have 1 pending query") - assert.Equal(t, query, queries[0].Query, "Query should match") - assert.Len(t, queries[0].Args, len(args), "Args length should match") - - assert.False(t, drc.HasPendingQuery(), "HasPendingQuery should return false after GetPendingQueries consumes them") -} - -func TestDryRunContext_GetPendingQuery(t *testing.T) { - drc := schema.NewDryRunContext(context.Background()) - - query1 := "INSERT INTO users (name) VALUES ($1)" - args1 := []any{"John"} - drc.Exec(query1, args1...) - - query2 := "UPDATE users SET name = $1" - args2 := []any{"Jane"} - drc.Exec(query2, args2...) - - gotQuery, gotArgs := drc.GetPendingQuery() - assert.Equal(t, query1, gotQuery, "First query should match") - assert.Len(t, gotArgs, len(args1), "First args length should match") - - gotQuery, _ = drc.GetPendingQuery() - assert.Equal(t, query2, gotQuery, "Second query should match") - - gotQuery, gotArgs = drc.GetPendingQuery() - assert.Empty(t, gotQuery, "Should return empty query when no more queries") - assert.Nil(t, gotArgs, "Should return nil args for empty query") -} - -func TestMockResult(t *testing.T) { - mock := &schema.MockResult{ - LastInsertID: 42, - RowsAffectedValue: 10, - } - - lastID, err := mock.LastInsertId() - require.NoError(t, err, "LastInsertId should not return error") - assert.Equal(t, int64(42), lastID, "LastInsertId should be 42") - - rowsAffected, err := mock.RowsAffected() - require.NoError(t, err, "RowsAffected should not return error") - assert.Equal(t, int64(10), rowsAffected, "RowsAffected should be 10") -} - -func TestMockRows(t *testing.T) { - mock := &schema.MockRows{} - - assert.False(t, mock.Closed, "MockRows should not be closed initially") - assert.False(t, mock.Next(), "MockRows.Next() should always return false") - - err := mock.Scan() - require.NoError(t, err, "MockRows.Scan() should return nil") - - columns, err := mock.Columns() - require.NoError(t, err, "MockRows.Columns() should not return error") - assert.Empty(t, columns, "MockRows.Columns() should return empty slice") - - err = mock.Err() - require.NoError(t, err, "MockRows.Err() should return nil") - - mock.Close() - assert.True(t, mock.Closed, "MockRows should be closed after Close()") -} - -func TestMockRow(t *testing.T) { - mock := &schema.MockRow{} - - err := mock.Scan() - assert.Equal(t, sql.ErrNoRows, err, "MockRow.Scan() should return sql.ErrNoRows") -} - -func TestDryRunContext_WhitespaceHandling(t *testing.T) { - drc := schema.NewDryRunContext(context.Background()) - - query := " INSERT INTO users (name) VALUES ($1) \n" - expected := "INSERT INTO users (name) VALUES ($1)" - - drc.Exec(query) - - captured := drc.GetCapturedSQL() - assert.Len(t, captured, 1, "Should have 1 captured query") - assert.Equal(t, expected, captured[0], "Query should be trimmed") -} diff --git a/schema/foreign_key_definition.go b/schema/foreign_key_definition.go deleted file mode 100644 index 49ad922..0000000 --- a/schema/foreign_key_definition.go +++ /dev/null @@ -1,111 +0,0 @@ -package schema - -import "github.com/akfaiz/migris/internal/util" - -// ForeignKeyDefinition defines the interface for defining a foreign key constraint in a database table. -type ForeignKeyDefinition interface { - // CascadeOnDelete sets the foreign key to cascade on delete. - CascadeOnDelete() ForeignKeyDefinition - // CascadeOnUpdate sets the foreign key to cascade on update. - CascadeOnUpdate() ForeignKeyDefinition - // Deferrable sets the foreign key as deferrable. - Deferrable(value ...bool) ForeignKeyDefinition - // InitiallyImmediate sets the foreign key to be initially immediate. - InitiallyImmediate(value ...bool) ForeignKeyDefinition - // Name sets the name of the foreign key constraint. - // This is optional and can be used to give a specific name to the foreign key. - Name(name string) ForeignKeyDefinition - // NoActionOnDelete set the foreign key to do nothing on delete. - NoActionOnDelete() ForeignKeyDefinition - // NoActionOnUpdate set the foreign key to do nothing on the update. - NoActionOnUpdate() ForeignKeyDefinition - // NullOnDelete set the foreign key to set the column to NULL on delete. - NullOnDelete() ForeignKeyDefinition - // NullOnUpdate set the foreign key to set the column to NULL on update. - NullOnUpdate() ForeignKeyDefinition - // On sets the table that these foreign key references. - On(table string) ForeignKeyDefinition - // OnDelete set the action to take when the referenced row is deleted. - OnDelete(action string) ForeignKeyDefinition - // OnUpdate set the action to take when the referenced row is updated. - OnUpdate(action string) ForeignKeyDefinition - // References set the column that this foreign key references in the other table. - References(column string) ForeignKeyDefinition - // RestrictOnDelete set the foreign key to restrict deletion of the referenced row. - RestrictOnDelete() ForeignKeyDefinition - // RestrictOnUpdate set the foreign key to restrict updating of the referenced row. - RestrictOnUpdate() ForeignKeyDefinition -} - -type foreignKeyDefinition struct { - *command -} - -func (fd *foreignKeyDefinition) CascadeOnDelete() ForeignKeyDefinition { - return fd.OnDelete("CASCADE") -} - -func (fd *foreignKeyDefinition) CascadeOnUpdate() ForeignKeyDefinition { - return fd.OnUpdate("CASCADE") -} - -func (fd *foreignKeyDefinition) Deferrable(value ...bool) ForeignKeyDefinition { - val := util.Optional(true, value...) - fd.deferrable = &val - return fd -} - -func (fd *foreignKeyDefinition) InitiallyImmediate(value ...bool) ForeignKeyDefinition { - val := util.Optional(true, value...) - fd.initiallyImmediate = &val - return fd -} - -func (fd *foreignKeyDefinition) Name(name string) ForeignKeyDefinition { - fd.index = name - return fd -} - -func (fd *foreignKeyDefinition) NoActionOnDelete() ForeignKeyDefinition { - return fd.OnDelete("NO ACTION") -} - -func (fd *foreignKeyDefinition) NoActionOnUpdate() ForeignKeyDefinition { - return fd.OnUpdate("NO ACTION") -} - -func (fd *foreignKeyDefinition) NullOnDelete() ForeignKeyDefinition { - return fd.OnDelete("SET NULL") -} - -func (fd *foreignKeyDefinition) NullOnUpdate() ForeignKeyDefinition { - return fd.OnUpdate("SET NULL") -} - -func (fd *foreignKeyDefinition) On(table string) ForeignKeyDefinition { - fd.on = table - return fd -} - -func (fd *foreignKeyDefinition) OnDelete(action string) ForeignKeyDefinition { - fd.onDelete = action - return fd -} - -func (fd *foreignKeyDefinition) OnUpdate(action string) ForeignKeyDefinition { - fd.onUpdate = action - return fd -} - -func (fd *foreignKeyDefinition) References(columns string) ForeignKeyDefinition { - fd.references = []string{columns} - return fd -} - -func (fd *foreignKeyDefinition) RestrictOnDelete() ForeignKeyDefinition { - return fd.OnDelete("RESTRICT") -} - -func (fd *foreignKeyDefinition) RestrictOnUpdate() ForeignKeyDefinition { - return fd.OnUpdate("RESTRICT") -} diff --git a/schema/grammar.go b/schema/grammar.go deleted file mode 100644 index e680586..0000000 --- a/schema/grammar.go +++ /dev/null @@ -1,146 +0,0 @@ -package schema - -import ( - "errors" - "fmt" - "slices" - "strings" - - "github.com/akfaiz/migris/internal/util" -) - -type grammar interface { - CompileTableExists(schema string, table string) (string, error) - CompileTables(schema string) (string, error) - CompileColumns(schema, table string) (string, error) - CompileIndexes(schema, table string) (string, error) - CompileCreate(bp *Blueprint) (string, error) - CompileAdd(bp *Blueprint) (string, error) - CompileChange(bp *Blueprint, command *command) (string, error) - CompileDrop(bp *Blueprint) (string, error) - CompileDropIfExists(bp *Blueprint) (string, error) - CompileRename(bp *Blueprint, command *command) (string, error) - CompileDropColumn(blueprint *Blueprint, command *command) (string, error) - CompileRenameColumn(blueprint *Blueprint, command *command) (string, error) - CompileIndex(blueprint *Blueprint, command *command) (string, error) - CompileUnique(blueprint *Blueprint, command *command) (string, error) - CompilePrimary(blueprint *Blueprint, command *command) (string, error) - CompileFullText(blueprint *Blueprint, command *command) (string, error) - CompileDropIndex(blueprint *Blueprint, command *command) (string, error) - CompileDropUnique(blueprint *Blueprint, command *command) (string, error) - CompileDropFulltext(blueprint *Blueprint, command *command) (string, error) - CompileDropPrimary(blueprint *Blueprint, command *command) (string, error) - CompileRenameIndex(blueprint *Blueprint, command *command) (string, error) - CompileForeign(blueprint *Blueprint, command *command) (string, error) - CompileDropForeign(blueprint *Blueprint, command *command) (string, error) - GetFluentCommands() []func(blueprint *Blueprint, command *command) string - CreateIndexName(blueprint *Blueprint, idxType string, columns ...string) string -} - -type baseGrammar struct{} - -func (g *baseGrammar) CompileForeign(blueprint *Blueprint, command *command) (string, error) { - if len(command.columns) == 0 || slices.Contains(command.columns, "") || command.on == "" || - len(command.references) == 0 || slices.Contains(command.references, "") { - return "", errors.New("foreign key definition is incomplete: column, on, and references must be set") - } - onDelete := "" - if command.onDelete != "" { - onDelete = fmt.Sprintf(" ON DELETE %s", command.onDelete) - } - onUpdate := "" - if command.onUpdate != "" { - onUpdate = fmt.Sprintf(" ON UPDATE %s", command.onUpdate) - } - index := command.index - if index == "" { - index = g.CreateForeignKeyName(blueprint, command) - } - - return fmt.Sprintf("ALTER TABLE %s ADD CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s(%s)%s%s", - blueprint.name, - index, - command.columns[0], - command.on, - command.references[0], - onDelete, - onUpdate, - ), nil -} - -func (g *baseGrammar) CreateIndexName(blueprint *Blueprint, idxType string, columns ...string) string { - tableName := blueprint.name - if strings.Contains(tableName, ".") { - parts := strings.Split(tableName, ".") - tableName = parts[len(parts)-1] // Use the last part as the table name - } - - switch idxType { - case "primary": - return fmt.Sprintf("pk_%s", tableName) - case "unique": - return fmt.Sprintf("uk_%s_%s", tableName, strings.Join(columns, "_")) - case "index": - return fmt.Sprintf("idx_%s_%s", tableName, strings.Join(columns, "_")) - case "fulltext": - return fmt.Sprintf("ft_%s_%s", tableName, strings.Join(columns, "_")) - default: - return "" - } -} - -func (g *baseGrammar) CreateForeignKeyName(blueprint *Blueprint, command *command) string { - tableName := blueprint.name - if strings.Contains(tableName, ".") { - parts := strings.Split(tableName, ".") - tableName = parts[len(parts)-1] // Use the last part as the table name - } - on := command.on - if strings.Contains(on, ".") { - parts := strings.Split(on, ".") - on = parts[len(parts)-1] // Use the last part as the column name - } - return fmt.Sprintf("fk_%s_%s", tableName, on) -} - -func (g *baseGrammar) QuoteString(s string) string { - return "'" + s + "'" -} - -func (g *baseGrammar) PrefixArray(prefix string, items []string) []string { - prefixed := make([]string, len(items)) - for i, item := range items { - prefixed[i] = fmt.Sprintf("%s%s", prefix, item) - } - return prefixed -} - -func (g *baseGrammar) Columnize(columns []string) string { - if len(columns) == 0 { - return "" - } - return strings.Join(columns, ", ") -} - -func (g *baseGrammar) GetValue(value any) string { - switch v := value.(type) { - case Expression: - return v.String() - default: - return fmt.Sprintf("'%v'", v) - } -} - -func (g *baseGrammar) GetDefaultValue(value any) string { - if value == nil { - return "NULL" - } - switch v := value.(type) { - case Expression: - return v.String() - case bool: - return util.Ternary(v, "'1'", "'0'") - default: - return fmt.Sprintf("'%v'", v) - } -} diff --git a/schema/grammars/factory.go b/schema/grammars/factory.go new file mode 100644 index 0000000..af694fd --- /dev/null +++ b/schema/grammars/factory.go @@ -0,0 +1,27 @@ +package grammars + +import ( + "fmt" + + "github.com/akfaiz/migris/internal/dialect" + "github.com/akfaiz/migris/schema/blueprint" +) + +// NewGrammar creates a new grammar instance for the given dialect. +func NewGrammar(dialectValue string) (blueprint.Grammar, error) { + dialectVal := dialect.FromString(dialectValue) + switch dialectVal { + case dialect.MySQL: + return newMysqlGrammar(), nil + case dialect.MariaDB: + return newMariadbGrammar(), nil + case dialect.Postgres: + return newPostgresGrammar(), nil + case dialect.SQLite3: + return newSqliteGrammar(), nil + case dialect.Unknown: + return nil, fmt.Errorf("unsupported Dialect: %s", dialectValue) + default: + return nil, fmt.Errorf("unsupported Dialect: %s", dialectValue) + } +} diff --git a/schema/grammars/mariadb_grammar.go b/schema/grammars/mariadb_grammar.go new file mode 100644 index 0000000..02ae9cb --- /dev/null +++ b/schema/grammars/mariadb_grammar.go @@ -0,0 +1,89 @@ +package grammars + +import ( + "fmt" + "slices" + "strings" + + "github.com/akfaiz/migris/internal/util" + "github.com/akfaiz/migris/schema/blueprint" +) + +type mariadbGrammar struct { + mysqlGrammar +} + +func newMariadbGrammar() *mariadbGrammar { + g := &mariadbGrammar{} + g.mysqlGrammar.serials = []string{ + blueprint.ColumnTypeBigInteger, + blueprint.ColumnTypeInteger, + blueprint.ColumnTypeMediumInteger, + blueprint.ColumnTypeSmallInteger, + blueprint.ColumnTypeTinyInteger, + } + g.mysqlGrammar.self = g + return g +} + +func (g *mariadbGrammar) GetType(col *blueprint.Column) string { + typeFuncMap := g.getTypeFuncMap() + if fn, ok := typeFuncMap[col.ColumnType]; ok { + return fn(col) + } + return g.mysqlGrammar.GetType(col) +} + +func (g *mariadbGrammar) getTypeFuncMap() map[string]func(*blueprint.Column) string { + m := g.mysqlGrammar.getTypeFuncMap() + m[blueprint.ColumnTypeUUID] = g.typeUUID + m[blueprint.ColumnTypeGeometry] = g.typeGeometry + m[blueprint.ColumnTypeGeography] = g.typeGeography + m[blueprint.ColumnTypePoint] = g.typePoint + return m +} + +func (g *mariadbGrammar) typeUUID(_ *blueprint.Column) string { + return "UUID" +} + +func (g *mariadbGrammar) typeGeography(col *blueprint.Column) string { + return g.typeGeometry(col) +} + +func (g *mariadbGrammar) typeGeometry(col *blueprint.Column) string { + subtype := util.Ternary(col.Subtype != nil, util.PtrOf(strings.ToUpper(*col.Subtype)), nil) + if subtype != nil { + if !slices.Contains( + []string{ + "POINT", + "LINESTRING", + "POLYGON", + "GEOMETRYCOLLECTION", + "MULTIPOINT", + "MULTILINESTRING", + "MULTIPOLYGON", + }, + *subtype, + ) { + subtype = nil + } + } + + if subtype == nil { + subtype = util.PtrOf("GEOMETRY") + } + + if col.Srid != nil && *col.Srid > 0 { + return fmt.Sprintf("%s REF_SYSTEM_ID=%d", *subtype, *col.Srid) + } + + return *subtype +} + +func (g *mariadbGrammar) typePoint(col *blueprint.Column) string { + if col.Srid != nil && *col.Srid > 0 { + return fmt.Sprintf("POINT REF_SYSTEM_ID=%d", *col.Srid) + } + return "POINT" +} diff --git a/schema/grammars/mariadb_grammar_test.go b/schema/grammars/mariadb_grammar_test.go new file mode 100644 index 0000000..b302793 --- /dev/null +++ b/schema/grammars/mariadb_grammar_test.go @@ -0,0 +1,68 @@ +package grammars_test + +import ( + "testing" + + "github.com/akfaiz/migris/schema" + "github.com/akfaiz/migris/schema/grammars" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestMariadbGrammar_CompileCreate(t *testing.T) { + g, err := grammars.NewGrammar("mariadb") + require.NoError(t, err) + + tests := []struct { + name string + table string + blueprint func(table *schema.Blueprint) + want string + wantErr bool + }{ + { + name: "basic table creation", + table: "users", + blueprint: func(table *schema.Blueprint) { + table.ID() + table.String("name", 255) + }, + want: "CREATE TABLE `users` (`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, `name` VARCHAR(255) NOT NULL, CONSTRAINT `users_id_primary` PRIMARY KEY (`id`))", + wantErr: false, + }, + { + name: "mariadb specific uuid type", + table: "users", + blueprint: func(table *schema.Blueprint) { + table.UUID("id") + }, + want: "CREATE TABLE `users` (`id` UUID NOT NULL)", + wantErr: false, + }, + { + name: "mariadb specific geometry types", + table: "locations", + blueprint: func(table *schema.Blueprint) { + table.Geometry("geom", "POINT") + table.Point("pt") + table.Geography("geog", "LINESTRING", 4326) + }, + want: "CREATE TABLE `locations` (`geom` POINT NOT NULL, `pt` POINT REF_SYSTEM_ID=4326 NOT NULL, `geog` LINESTRING REF_SYSTEM_ID=4326 NOT NULL)", + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bp := schema.NewBlueprintForTesting(tt.table, g) + tt.blueprint(bp) + got, err := g.CompileCreate(bp) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/schema/grammars/mysql_grammar.go b/schema/grammars/mysql_grammar.go new file mode 100644 index 0000000..0399811 --- /dev/null +++ b/schema/grammars/mysql_grammar.go @@ -0,0 +1,727 @@ +package grammars + +import ( + "errors" + "fmt" + "slices" + "strings" + + "github.com/akfaiz/migris/internal/util" + "github.com/akfaiz/migris/schema/blueprint" +) + +type mysqlGrammar struct { + blueprint.BaseGrammar + + serials []string + self blueprint.Grammar +} + +func newMysqlGrammar() *mysqlGrammar { + g := &mysqlGrammar{ + serials: []string{ + blueprint.ColumnTypeBigInteger, + blueprint.ColumnTypeInteger, + blueprint.ColumnTypeMediumInteger, + blueprint.ColumnTypeSmallInteger, + blueprint.ColumnTypeTinyInteger, + }, + } + g.self = g + return g +} + +func (g *mysqlGrammar) CompileTableExists(_, table string) (string, error) { + schema, table := g.parseTable(table) + schemaClause := "table_schema = DATABASE()" + if schema != "" { + schemaClause = "table_schema = " + g.QuoteString(schema) + } + return fmt.Sprintf( + "SELECT 1 FROM information_schema.tables WHERE %s AND table_name = %s", + schemaClause, + g.QuoteString(table), + ), nil +} + +func (g *mysqlGrammar) CompileTables(_ string) (string, error) { + return "SELECT table_name, table_comment FROM information_schema.tables WHERE table_schema = DATABASE() AND table_type = 'BASE TABLE'", nil +} + +func (g *mysqlGrammar) CompileColumns(_, table string) (string, error) { + return fmt.Sprintf("SHOW FULL COLUMNS FROM %s", g.WrapTable(table, "`")), nil +} + +func (g *mysqlGrammar) CompileIndexes(_, table string) (string, error) { + return fmt.Sprintf("SHOW INDEX FROM %s", g.WrapTable(table, "`")), nil +} + +func (g *mysqlGrammar) parseTable(table string) (string, string) { + parts := strings.Split(table, ".") + if len(parts) > 1 { + return parts[0], parts[1] + } + return "", table +} + +func (g *mysqlGrammar) CompileCreate(bp *blueprint.Blueprint) (string, error) { + columns, err := g.getColumns(bp) + if err != nil { + return "", err + } + columns = append(columns, g.getConstraints(bp)...) + + sql := fmt.Sprintf("CREATE TABLE %s (%s)", g.WrapTable(bp.Name, "`"), strings.Join(columns, ", ")) + sql += g.compileTableModifiers(bp) + + return sql, nil +} + +func (g *mysqlGrammar) CompileAdd(bp *blueprint.Blueprint) (string, error) { + columns, err := g.getColumns(bp) + if err != nil { + return "", err + } + if len(columns) == 0 { + return "", nil + } + for i, col := range columns { + columns[i] = "ADD COLUMN " + col + } + return fmt.Sprintf("ALTER TABLE %s %s", g.WrapTable(bp.Name, "`"), strings.Join(columns, ", ")), nil +} + +func (g *mysqlGrammar) CompileChange(bp *blueprint.Blueprint, command *blueprint.Command) (string, error) { + column := command.Column + if column.Name == "" { + return "", errors.New("column name cannot be empty for change operation") + } + + operation := "MODIFY" + name := g.Wrap(column.Name, "`") + if column.RenameToVal != "" { + operation = "CHANGE" + name = fmt.Sprintf("%s %s", name, g.Wrap(column.RenameToVal, "`")) + } + + sql := fmt.Sprintf( + "ALTER TABLE %s %s COLUMN %s %s", + g.WrapTable(bp.Name, "`"), + operation, + name, + g.self.GetType(column), + ) + var sqlBuilder strings.Builder + for _, modifier := range g.modifiers() { + sqlBuilder.WriteString(modifier(column)) + } + sql += sqlBuilder.String() + + return sql, nil +} + +func (g *mysqlGrammar) CompileRename(blueprint *blueprint.Blueprint, command *blueprint.Command) (string, error) { + return fmt.Sprintf( + "ALTER TABLE %s RENAME TO %s", + g.WrapTable(blueprint.Name, "`"), + g.WrapTable(command.To, "`"), + ), nil +} + +func (g *mysqlGrammar) CompileDrop(blueprint *blueprint.Blueprint) (string, error) { + if blueprint.Name == "" { + return "", errors.New("table name cannot be empty") + } + return fmt.Sprintf("DROP TABLE %s", g.WrapTable(blueprint.Name, "`")), nil +} + +func (g *mysqlGrammar) CompileDropIfExists(blueprint *blueprint.Blueprint) (string, error) { + if blueprint.Name == "" { + return "", errors.New("table name cannot be empty") + } + return fmt.Sprintf("DROP TABLE IF EXISTS %s", g.WrapTable(blueprint.Name, "`")), nil +} + +func (g *mysqlGrammar) CompileDropColumn(blueprint *blueprint.Blueprint, command *blueprint.Command) (string, error) { + var dropped []string + for _, col := range command.Columns { + if col == "" { + return "", errors.New("column name cannot be empty") + } + dropped = append(dropped, "DROP COLUMN "+g.Wrap(col, "`")) + } + return fmt.Sprintf("ALTER TABLE %s %s", g.WrapTable(blueprint.Name, "`"), strings.Join(dropped, ", ")), nil +} + +func (g *mysqlGrammar) CompileRenameColumn(blueprint *blueprint.Blueprint, command *blueprint.Command) (string, error) { + if command.From == "" || command.To == "" { + return "", errors.New("old and new column names cannot be empty") + } + return fmt.Sprintf( + "ALTER TABLE %s RENAME COLUMN %s TO %s", + g.WrapTable(blueprint.Name, "`"), + g.Wrap(command.From, "`"), + g.Wrap(command.To, "`"), + ), nil +} + +func (g *mysqlGrammar) CompileIndex(blueprint *blueprint.Blueprint, command *blueprint.Command) (string, error) { + if len(command.Columns) == 0 || slices.Contains(command.Columns, "") { + return "", errors.New("index columns cannot be empty") + } + return g.compileKey(blueprint, command, "INDEX"), nil +} + +func (g *mysqlGrammar) CompileUnique(blueprint *blueprint.Blueprint, command *blueprint.Command) (string, error) { + if len(command.Columns) == 0 || slices.Contains(command.Columns, "") { + return "", errors.New("unique index columns cannot be empty") + } + return g.compileKey(blueprint, command, "UNIQUE INDEX"), nil +} + +func (g *mysqlGrammar) CompileFullText(blueprint *blueprint.Blueprint, command *blueprint.Command) (string, error) { + if len(command.Columns) == 0 || slices.Contains(command.Columns, "") { + return "", errors.New("fulltext index columns cannot be empty") + } + return g.compileKey(blueprint, command, "FULLTEXT INDEX"), nil +} + +func (g *mysqlGrammar) CompilePrimary(blueprint *blueprint.Blueprint, command *blueprint.Command) (string, error) { + if len(command.Columns) == 0 || slices.Contains(command.Columns, "") { + return "", errors.New("primary key columns cannot be empty") + } + index := command.Index + if index == "" { + index = g.CreateIndexName(blueprint, "primary", command.Columns...) + } + + return fmt.Sprintf("ALTER TABLE %s ADD CONSTRAINT %s PRIMARY KEY (%s)", + g.WrapTable(blueprint.Name, "`"), + g.WrapIndexName(index, "`"), + g.WrapColumnize(command.Columns, "`"), + ), nil +} + +func (g *mysqlGrammar) compileKey(blueprint *blueprint.Blueprint, command *blueprint.Command, keyType string) string { + idxType := strings.ToLower(keyType) + switch { + case strings.Contains(idxType, "unique"): + idxType = "unique" + case strings.Contains(idxType, "fulltext"): + idxType = "fulltext" + case strings.Contains(idxType, "index"): + idxType = "index" + } + + index := g.WrapIndexName(command.Index, "`") + if index == "" { + index = g.WrapIndexName(g.CreateIndexName(blueprint, idxType, command.Columns...), "`") + } + + columns := g.WrapColumnize(command.Columns, "`") + algorithm := "" + if command.Algorithm != "" { + algorithm = " USING " + strings.ToUpper(command.Algorithm) + } + + return fmt.Sprintf( + "CREATE %s %s ON %s (%s)%s", + keyType, + index, + g.WrapTable(blueprint.Name, "`"), + columns, + algorithm, + ) +} + +func (g *mysqlGrammar) CompileDropIndex(blueprint *blueprint.Blueprint, command *blueprint.Command) (string, error) { + if command.Index == "" { + return "", errors.New("index name cannot be empty") + } + return fmt.Sprintf( + "ALTER TABLE %s DROP INDEX %s", + g.WrapTable(blueprint.Name, "`"), + g.WrapIndexName(command.Index, "`"), + ), nil +} + +func (g *mysqlGrammar) CompileDropUnique(blueprint *blueprint.Blueprint, command *blueprint.Command) (string, error) { + if command.Index == "" { + return "", errors.New("unique index name cannot be empty") + } + return fmt.Sprintf( + "ALTER TABLE %s DROP INDEX %s", + g.WrapTable(blueprint.Name, "`"), + g.WrapIndexName(command.Index, "`"), + ), nil +} + +func (g *mysqlGrammar) CompileDropFulltext(blueprint *blueprint.Blueprint, command *blueprint.Command) (string, error) { + if command.Index == "" { + return "", errors.New("fulltext index name cannot be empty") + } + return fmt.Sprintf( + "ALTER TABLE %s DROP INDEX %s", + g.WrapTable(blueprint.Name, "`"), + g.WrapIndexName(command.Index, "`"), + ), nil +} + +func (g *mysqlGrammar) CompileDropPrimary(blueprint *blueprint.Blueprint, _ *blueprint.Command) (string, error) { + return fmt.Sprintf("ALTER TABLE %s DROP PRIMARY KEY", g.WrapTable(blueprint.Name, "`")), nil +} + +func (g *mysqlGrammar) CompileRenameIndex(blueprint *blueprint.Blueprint, command *blueprint.Command) (string, error) { + if command.From == "" || command.To == "" { + return "", errors.New("old and new index names cannot be empty") + } + return fmt.Sprintf( + "ALTER TABLE %s RENAME INDEX %s TO %s", + g.WrapTable(blueprint.Name, "`"), + g.WrapIndexName(command.From, "`"), + g.WrapIndexName(command.To, "`"), + ), nil +} + +func (g *mysqlGrammar) CompileDropForeign(blueprint *blueprint.Blueprint, command *blueprint.Command) (string, error) { + if command.Index == "" { + return "", errors.New("foreign key name cannot be empty") + } + return fmt.Sprintf( + "ALTER TABLE %s DROP FOREIGN KEY %s", + g.WrapTable(blueprint.Name, "`"), + g.WrapIndexName(command.Index, "`"), + ), nil +} + +func (g *mysqlGrammar) CompileForeign(blueprint *blueprint.Blueprint, command *blueprint.Command) (string, error) { + if len(command.Columns) == 0 || slices.Contains(command.Columns, "") || command.On == "" || + len(command.References) == 0 || slices.Contains(command.References, "") { + return "", errors.New("foreign key definition is incomplete: column, on, and references must be set") + } + onDelete := "" + if command.OnDelete != "" { + onDelete = fmt.Sprintf(" ON DELETE %s", command.OnDelete) + } + onUpdate := "" + if command.OnUpdate != "" { + onUpdate = fmt.Sprintf(" ON UPDATE %s", command.OnUpdate) + } + index := command.Index + if index == "" { + index = g.CreateForeignKeyName(blueprint, command) + } + + return fmt.Sprintf("ALTER TABLE %s ADD CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s (%s)%s%s", + g.WrapTable(blueprint.Name, "`"), + g.WrapIndexName(index, "`"), + g.WrapColumnize(command.Columns, "`"), + g.WrapTable(command.On, "`"), + g.WrapColumnize(command.References, "`"), + onDelete, + onUpdate, + ), nil +} + +func (g *mysqlGrammar) getColumns(blueprint *blueprint.Blueprint) ([]string, error) { + var columns []string + for _, col := range blueprint.GetAddedColumns() { + if col.Name == "" { + return nil, errors.New("column name cannot be empty") + } + + sql := g.Wrap(col.Name, "`") + " " + g.self.GetType(col) + var sqlBuilder strings.Builder + for _, modifier := range g.modifiers() { + sqlBuilder.WriteString(modifier(col)) + } + sql += sqlBuilder.String() + columns = append(columns, sql) + } + return columns, nil +} + +func (g *mysqlGrammar) getConstraints(blueprint *blueprint.Blueprint) []string { + var constrains []string + for _, col := range blueprint.GetAddedColumns() { + if col.PrimaryVal != nil && *col.PrimaryVal { + pkConstraintName := g.CreateIndexName(blueprint, "primary", col.Name) + sql := "CONSTRAINT " + g.WrapIndexName( + pkConstraintName, + "`", + ) + " PRIMARY KEY (" + g.Wrap( + col.Name, + "`", + ) + ")" + constrains = append(constrains, sql) + continue + } + } + return constrains +} + +func (g *mysqlGrammar) GetType(col *blueprint.Column) string { + typeFuncMap := g.getTypeFuncMap() + if fn, ok := typeFuncMap[col.ColumnType]; ok { + return fn(col) + } + return col.ColumnType +} + +func (g *mysqlGrammar) getTypeFuncMap() map[string]func(*blueprint.Column) string { + return map[string]func(*blueprint.Column) string{ + blueprint.ColumnTypeChar: g.typeChar, + blueprint.ColumnTypeString: g.typeString, + blueprint.ColumnTypeTinyText: g.typeTinyText, + blueprint.ColumnTypeText: g.typeText, + blueprint.ColumnTypeMediumText: g.typeMediumText, + blueprint.ColumnTypeLongText: g.typeLongText, + blueprint.ColumnTypeInteger: g.typeInteger, + blueprint.ColumnTypeBigInteger: g.typeBigInteger, + blueprint.ColumnTypeMediumInteger: g.typeMediumInteger, + blueprint.ColumnTypeSmallInteger: g.typeSmallInteger, + blueprint.ColumnTypeTinyInteger: g.typeTinyInteger, + blueprint.ColumnTypeFloat: g.typeFloat, + blueprint.ColumnTypeDouble: g.typeDouble, + blueprint.ColumnTypeDecimal: g.typeDecimal, + blueprint.ColumnTypeBoolean: g.typeBoolean, + blueprint.ColumnTypeEnum: g.typeEnum, + blueprint.ColumnTypeJSON: g.typeJSON, + blueprint.ColumnTypeJSONB: g.typeJSONB, + blueprint.ColumnTypeDate: g.typeDate, + blueprint.ColumnTypeDateTime: g.typeDateTime, + blueprint.ColumnTypeDateTimeTz: g.typeDateTimeTz, + blueprint.ColumnTypeTime: g.typeTime, + blueprint.ColumnTypeTimeTz: g.typeTimeTz, + blueprint.ColumnTypeTimestamp: g.typeTimestamp, + blueprint.ColumnTypeTimestampTz: g.typeTimestampTz, + blueprint.ColumnTypeYear: g.typeYear, + blueprint.ColumnTypeBinary: g.typeBinary, + blueprint.ColumnTypeUUID: g.typeUUID, + blueprint.ColumnTypeULID: g.typeULID, + blueprint.ColumnTypeIPAddress: g.typeIPAddress, + blueprint.ColumnTypeMacAddress: g.typeMacAddress, + blueprint.ColumnTypeSet: g.typeSet, + blueprint.ColumnTypeVector: g.typeVector, + blueprint.ColumnTypeGeography: g.typeGeography, + blueprint.ColumnTypeGeometry: g.typeGeometry, + blueprint.ColumnTypePoint: g.typePoint, + } +} + +func (g *mysqlGrammar) typeChar(col *blueprint.Column) string { + return fmt.Sprintf("CHAR(%d)", *col.Length) +} + +func (g *mysqlGrammar) typeString(col *blueprint.Column) string { + return fmt.Sprintf("VARCHAR(%d)", *col.Length) +} + +func (g *mysqlGrammar) typeTinyText(_ *blueprint.Column) string { + return "TINYTEXT" +} + +func (g *mysqlGrammar) typeText(_ *blueprint.Column) string { + return "TEXT" +} + +func (g *mysqlGrammar) typeMediumText(_ *blueprint.Column) string { + return "MEDIUMTEXT" +} + +func (g *mysqlGrammar) typeLongText(_ *blueprint.Column) string { + return "LONGTEXT" +} + +func (g *mysqlGrammar) typeBigInteger(_ *blueprint.Column) string { + return "BIGINT" +} + +func (g *mysqlGrammar) typeInteger(_ *blueprint.Column) string { + return "INT" +} + +func (g *mysqlGrammar) typeMediumInteger(_ *blueprint.Column) string { + return "MEDIUMINT" +} + +func (g *mysqlGrammar) typeSmallInteger(_ *blueprint.Column) string { + return "SMALLINT" +} + +func (g *mysqlGrammar) typeTinyInteger(_ *blueprint.Column) string { + return "TINYINT" +} + +func (g *mysqlGrammar) typeFloat(col *blueprint.Column) string { + return fmt.Sprintf("FLOAT(%d)", *col.Precision) +} + +func (g *mysqlGrammar) typeDouble(_ *blueprint.Column) string { + return "DOUBLE" +} + +func (g *mysqlGrammar) typeDecimal(col *blueprint.Column) string { + return fmt.Sprintf("DECIMAL(%d, %d)", *col.Total, *col.Places) +} + +func (g *mysqlGrammar) typeBoolean(_ *blueprint.Column) string { + return "TINYINT(1)" +} + +func (g *mysqlGrammar) typeEnum(col *blueprint.Column) string { + return fmt.Sprintf("ENUM(%s)", g.QuoteString(strings.Join(col.Allowed, "', '"))) +} + +func (g *mysqlGrammar) typeSet(col *blueprint.Column) string { + return fmt.Sprintf("SET(%s)", g.QuoteString(strings.Join(col.Allowed, "', '"))) +} + +func (g *mysqlGrammar) typeJSON(_ *blueprint.Column) string { + return "JSON" +} + +func (g *mysqlGrammar) typeJSONB(_ *blueprint.Column) string { + return "JSON" +} + +func (g *mysqlGrammar) typeDate(_ *blueprint.Column) string { + return "DATE" +} + +func (g *mysqlGrammar) typeDateTime(col *blueprint.Column) string { + if col.Precision != nil && *col.Precision > 0 { + return fmt.Sprintf("DATETIME(%d)", *col.Precision) + } + return "DATETIME" +} + +func (g *mysqlGrammar) typeDateTimeTz(col *blueprint.Column) string { + return g.typeDateTime(col) +} + +func (g *mysqlGrammar) typeTime(col *blueprint.Column) string { + if col.Precision != nil && *col.Precision > 0 { + return fmt.Sprintf("TIME(%d)", *col.Precision) + } + return "TIME" +} + +func (g *mysqlGrammar) typeTimeTz(col *blueprint.Column) string { + return g.typeTime(col) +} + +func (g *mysqlGrammar) typeTimestamp(col *blueprint.Column) string { + if col.Precision != nil && *col.Precision > 0 { + return fmt.Sprintf("TIMESTAMP(%d)", *col.Precision) + } + return "TIMESTAMP" +} + +func (g *mysqlGrammar) typeTimestampTz(col *blueprint.Column) string { + return g.typeTimestamp(col) +} + +func (g *mysqlGrammar) typeYear(_ *blueprint.Column) string { + return "YEAR" +} + +func (g *mysqlGrammar) typeBinary(_ *blueprint.Column) string { + return "BLOB" +} + +func (g *mysqlGrammar) typeUUID(_ *blueprint.Column) string { + return "CHAR(36)" +} + +func (g *mysqlGrammar) typeULID(_ *blueprint.Column) string { + return "CHAR(26)" +} + +func (g *mysqlGrammar) typeIPAddress(_ *blueprint.Column) string { + return "VARCHAR(45)" +} + +func (g *mysqlGrammar) typeMacAddress(_ *blueprint.Column) string { + return "VARCHAR(17)" +} + +func (g *mysqlGrammar) typeVector(_ *blueprint.Column) string { + return "VECTOR" +} + +func (g *mysqlGrammar) typeGeography(col *blueprint.Column) string { + return g.typeGeometry(col) +} + +func (g *mysqlGrammar) typeGeometry(col *blueprint.Column) string { + subtype := util.Ternary(col.Subtype != nil, util.PtrOf(strings.ToUpper(*col.Subtype)), nil) + if subtype != nil { + if !slices.Contains( + []string{ + "POINT", + "LINESTRING", + "POLYGON", + "GEOMETRYCOLLECTION", + "MULTIPOINT", + "MULTILINESTRING", + "MULTIPOLYGON", + }, + *subtype, + ) { + subtype = nil + } + } + + if subtype == nil { + subtype = util.PtrOf("GEOMETRY") + } + + if col.Srid != nil && *col.Srid > 0 { + return fmt.Sprintf("%s SRID %d", *subtype, *col.Srid) + } + + return *subtype +} + +func (g *mysqlGrammar) typePoint(col *blueprint.Column) string { + if col.Srid != nil && *col.Srid > 0 { + return fmt.Sprintf("POINT SRID %d", *col.Srid) + } + return "POINT" +} + +func (g *mysqlGrammar) modifiers() []func(*blueprint.Column) string { + return []func(*blueprint.Column) string{ + g.modifyUnsigned, + g.modifyCharset, + g.modifyCollate, + g.modifyVirtualAs, + g.modifyStoredAs, + g.modifyNullable, + g.modifyDefault, + g.modifyIncrement, + g.modifyOnUpdate, + g.modifyComment, + g.modifyAfter, + g.modifyFirst, + } +} + +func (g *mysqlGrammar) compileTableModifiers(bp *blueprint.Blueprint) string { + var sql string + if bp.CharsetVal != "" { + sql += " DEFAULT CHARACTER SET " + bp.CharsetVal + } + if bp.CollationVal != "" { + sql += " COLLATE " + bp.CollationVal + } + if bp.EngineVal != "" { + sql += " ENGINE = " + bp.EngineVal + } + if bp.CommentVal != "" { + sql += " COMMENT = " + g.QuoteString(bp.CommentVal) + } + return sql +} + +func (g *mysqlGrammar) modifyNullable(col *blueprint.Column) string { + if col.NullableVal != nil && *col.NullableVal { + return " NULL" + } + return " NOT NULL" +} + +func (g *mysqlGrammar) modifyDefault(col *blueprint.Column) string { + if col.DefaultValue != nil { + return fmt.Sprintf(" DEFAULT %s", g.GetDefaultValue(col.DefaultValue)) + } + if col.HasCommand("default") { + return " DEFAULT NULL" + } + if col.UseCurrentVal { + return " DEFAULT CURRENT_TIMESTAMP" + } + return "" +} + +func (g *mysqlGrammar) modifyIncrement(col *blueprint.Column) string { + if slices.Contains(g.serials, col.ColumnType) && col.AutoIncrementVal != nil && *col.AutoIncrementVal { + return " AUTO_INCREMENT" + } + return "" +} + +func (g *mysqlGrammar) modifyComment(col *blueprint.Column) string { + if col.CommentVal != nil { + return fmt.Sprintf(" COMMENT %s", g.QuoteString(*col.CommentVal)) + } + return "" +} + +func (g *mysqlGrammar) modifyAfter(col *blueprint.Column) string { + if col.AfterVal != nil { + return fmt.Sprintf(" AFTER %s", g.Wrap(*col.AfterVal, "`")) + } + return "" +} + +func (g *mysqlGrammar) modifyFirst(col *blueprint.Column) string { + if col.FirstVal { + return " FIRST" + } + return "" +} + +func (g *mysqlGrammar) modifyOnUpdate(col *blueprint.Column) string { + if col.UseCurrentOnUpdateVal { + return " ON UPDATE CURRENT_TIMESTAMP" + } + if col.OnUpdateValue != nil { + return fmt.Sprintf(" ON UPDATE %s", g.GetValue(col.OnUpdateValue)) + } + return "" +} + +func (g *mysqlGrammar) modifyUnsigned(col *blueprint.Column) string { + if col.UnsignedVal != nil && *col.UnsignedVal { + return " UNSIGNED" + } + return "" +} + +func (g *mysqlGrammar) modifyCharset(col *blueprint.Column) string { + if col.CharsetVal != nil { + return fmt.Sprintf(" CHARACTER SET %s", *col.CharsetVal) + } + return "" +} + +func (g *mysqlGrammar) modifyCollate(col *blueprint.Column) string { + if col.CollationVal != nil { + return fmt.Sprintf(" COLLATE %s", *col.CollationVal) + } + return "" +} + +func (g *mysqlGrammar) modifyVirtualAs(col *blueprint.Column) string { + if col.VirtualAsVal != nil { + return fmt.Sprintf(" GENERATED ALWAYS AS (%s) VIRTUAL", *col.VirtualAsVal) + } + return "" +} + +func (g *mysqlGrammar) modifyStoredAs(col *blueprint.Column) string { + if col.StoredAsVal != nil { + return fmt.Sprintf(" GENERATED ALWAYS AS (%s) STORED", *col.StoredAsVal) + } + return "" +} + +func (g *mysqlGrammar) GetFluentCommands() []func(*blueprint.Blueprint, *blueprint.Command) string { + return []func(*blueprint.Blueprint, *blueprint.Command) string{} +} + +func (g *mysqlGrammar) GetTableFluentCommands() []func(*blueprint.Blueprint) string { + return []func(*blueprint.Blueprint) string{} +} diff --git a/schema/mysql_grammar_test.go b/schema/grammars/mysql_grammar_test.go similarity index 58% rename from schema/mysql_grammar_test.go rename to schema/grammars/mysql_grammar_test.go index 37eea9d..9028061 100644 --- a/schema/mysql_grammar_test.go +++ b/schema/grammars/mysql_grammar_test.go @@ -1,79 +1,82 @@ -package schema //nolint:testpackage // Need to access unexported members for testing +package grammars_test import ( "testing" "github.com/akfaiz/migris/internal/dialect" + "github.com/akfaiz/migris/schema/blueprint" + "github.com/akfaiz/migris/schema/grammars" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestMysqlGrammar_CompileCreate(t *testing.T) { - g := newMysqlGrammar() + g, err := grammars.NewGrammar("mysql") + require.NoError(t, err) tests := []struct { name string table string - blueprint func(table *Blueprint) + blueprint func(table *blueprint.Blueprint) want string wantErr bool }{ { name: "basic table creation", table: "users", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.ID() table.String("name", 255) }, - want: "CREATE TABLE users (id BIGINT UNSIGNED AUTO_INCREMENT NOT NULL, name VARCHAR(255) NOT NULL, CONSTRAINT pk_users PRIMARY KEY (id))", + want: "CREATE TABLE `users` (`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, `name` VARCHAR(255) NOT NULL, CONSTRAINT `users_id_primary` PRIMARY KEY (`id`))", wantErr: false, }, { name: "table with charset", table: "users", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.Charset("utf8mb4") table.ID() }, - want: "CREATE TABLE users (id BIGINT UNSIGNED AUTO_INCREMENT NOT NULL, CONSTRAINT pk_users PRIMARY KEY (id)) DEFAULT CHARACTER SET utf8mb4", + want: "CREATE TABLE `users` (`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, CONSTRAINT `users_id_primary` PRIMARY KEY (`id`)) DEFAULT CHARACTER SET utf8mb4", wantErr: false, }, { name: "table with collation", table: "users", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.Collation("utf8mb4_unicode_ci") table.ID() }, - want: "CREATE TABLE users (id BIGINT UNSIGNED AUTO_INCREMENT NOT NULL, CONSTRAINT pk_users PRIMARY KEY (id)) COLLATE utf8mb4_unicode_ci", + want: "CREATE TABLE `users` (`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, CONSTRAINT `users_id_primary` PRIMARY KEY (`id`)) COLLATE utf8mb4_unicode_ci", wantErr: false, }, { name: "table with engine", table: "users", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.Engine("InnoDB") table.ID() }, - want: "CREATE TABLE users (id BIGINT UNSIGNED AUTO_INCREMENT NOT NULL, CONSTRAINT pk_users PRIMARY KEY (id)) ENGINE = InnoDB", + want: "CREATE TABLE `users` (`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, CONSTRAINT `users_id_primary` PRIMARY KEY (`id`)) ENGINE = InnoDB", wantErr: false, }, { name: "table with charset, collation and engine", table: "users", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.Charset("utf8mb4") table.Collation("utf8mb4_unicode_ci") table.Engine("InnoDB") table.ID() }, - want: "CREATE TABLE users (id BIGINT UNSIGNED AUTO_INCREMENT NOT NULL, CONSTRAINT pk_users PRIMARY KEY (id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB", + want: "CREATE TABLE `users` (`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, CONSTRAINT `users_id_primary` PRIMARY KEY (`id`)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB", wantErr: false, }, { name: "table with empty column name should return error", table: "users", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.Integer("") }, wantErr: true, @@ -82,7 +85,11 @@ func TestMysqlGrammar_CompileCreate(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - bp := &Blueprint{name: tt.table} + tableName := tt.table + if tableName == "" && !tt.wantErr { + tableName = tt.name + } + bp := &blueprint.Blueprint{Name: tableName} tt.blueprint(bp) got, err := g.CompileCreate(bp) if tt.wantErr { @@ -95,66 +102,151 @@ func TestMysqlGrammar_CompileCreate(t *testing.T) { } } +func TestMysqlGrammar_CompileTableExists(t *testing.T) { + g, err := grammars.NewGrammar("mysql") + require.NoError(t, err) + + sql, err := g.CompileTableExists("", "db.users") + require.NoError(t, err) + assert.Equal(t, "SELECT 1 FROM information_schema.tables WHERE table_schema = 'db' AND table_name = 'users'", sql) + + sql2, err := g.CompileTableExists("", "users") + require.NoError(t, err) + assert.Equal( + t, + "SELECT 1 FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'users'", + sql2, + ) +} + +func TestMysqlGrammar_CompileTables(t *testing.T) { + g, err := grammars.NewGrammar("mysql") + require.NoError(t, err) + + sql, err := g.CompileTables("") + require.NoError(t, err) + assert.Equal( + t, + "SELECT table_name, table_comment FROM information_schema.tables WHERE table_schema = DATABASE() AND table_type = 'BASE TABLE'", + sql, + ) +} + +func TestMysqlGrammar_CompileColumns(t *testing.T) { + g, err := grammars.NewGrammar("mysql") + require.NoError(t, err) + + sql, err := g.CompileColumns("", "users") + require.NoError(t, err) + assert.Equal(t, "SHOW FULL COLUMNS FROM `users`", sql) +} + +func TestMysqlGrammar_CompileIndexes(t *testing.T) { + g, err := grammars.NewGrammar("mysql") + require.NoError(t, err) + + sql, err := g.CompileIndexes("", "users") + require.NoError(t, err) + assert.Equal(t, "SHOW INDEX FROM `users`", sql) +} + func TestMysqlGrammar_CompileAdd(t *testing.T) { - g := newMysqlGrammar() + g, err := grammars.NewGrammar("mysql") + require.NoError(t, err) tests := []struct { name string table string - blueprint func(table *Blueprint) + blueprint func(table *blueprint.Blueprint) want string wantErr bool }{ { name: "add single column", table: "users", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.String("email", 255) }, - want: "ALTER TABLE users ADD COLUMN email VARCHAR(255) NOT NULL", + want: "ALTER TABLE `users` ADD COLUMN `email` VARCHAR(255) NOT NULL", wantErr: false, }, { name: "add multiple columns", table: "users", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.String("email", 255) table.Integer("age") }, - want: "ALTER TABLE users ADD COLUMN email VARCHAR(255) NOT NULL, ADD COLUMN age INT NOT NULL", + want: "ALTER TABLE `users` ADD COLUMN `email` VARCHAR(255) NOT NULL, ADD COLUMN `age` INT NOT NULL", wantErr: false, }, { name: "add column with nullable", table: "users", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.String("email", 255).Nullable() }, - want: "ALTER TABLE users ADD COLUMN email VARCHAR(255) NULL", + want: "ALTER TABLE `users` ADD COLUMN `email` VARCHAR(255) NULL", wantErr: false, }, { name: "add column with default value", table: "users", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.String("status", 50).Default("active") }, - want: "ALTER TABLE users ADD COLUMN status VARCHAR(50) DEFAULT 'active' NOT NULL", + want: "ALTER TABLE `users` ADD COLUMN `status` VARCHAR(50) NOT NULL DEFAULT 'active'", wantErr: false, }, { name: "add column with comment", table: "users", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.String("name", 255).Comment("User full name") }, - want: "ALTER TABLE users ADD COLUMN name VARCHAR(255) NOT NULL COMMENT 'User full name'", + want: "ALTER TABLE `users` ADD COLUMN `name` VARCHAR(255) NOT NULL COMMENT 'User full name'", + wantErr: false, + }, + { + name: "add column with after modifier", + table: "users", + blueprint: func(table *blueprint.Blueprint) { + table.String("name", 255).After("id") + }, + want: "ALTER TABLE `users` ADD COLUMN `name` VARCHAR(255) NOT NULL AFTER `id`", + wantErr: false, + }, + { + name: "add column with first modifier", + table: "users", + blueprint: func(table *blueprint.Blueprint) { + table.String("name", 255).First() + }, + want: "ALTER TABLE `users` ADD COLUMN `name` VARCHAR(255) NOT NULL FIRST", + wantErr: false, + }, + { + name: "add column with virtual as", + table: "users", + blueprint: func(table *blueprint.Blueprint) { + table.String("full_name").VirtualAs("concat(first_name, ' ', last_name)") + }, + want: "ALTER TABLE `users` ADD COLUMN `full_name` VARCHAR(255) GENERATED ALWAYS AS (concat(first_name, ' ', last_name)) VIRTUAL NOT NULL", + wantErr: false, + }, + { + name: "add column with stored as", + table: "users", + blueprint: func(table *blueprint.Blueprint) { + table.String("full_name").StoredAs("concat(first_name, ' ', last_name)") + }, + want: "ALTER TABLE `users` ADD COLUMN `full_name` VARCHAR(255) GENERATED ALWAYS AS (concat(first_name, ' ', last_name)) STORED NOT NULL", wantErr: false, }, { name: "no columns to add returns empty string", table: "users", - blueprint: func(_ *Blueprint) { + blueprint: func(_ *blueprint.Blueprint) { // No columns added }, want: "", @@ -163,7 +255,7 @@ func TestMysqlGrammar_CompileAdd(t *testing.T) { { name: "add column with empty name should return error", table: "users", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.String("", 255) }, wantErr: true, @@ -172,7 +264,11 @@ func TestMysqlGrammar_CompileAdd(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - bp := &Blueprint{name: tt.table} + tableName := tt.table + if tableName == "" && !tt.wantErr { + tableName = tt.name + } + bp := &blueprint.Blueprint{Name: tableName} tt.blueprint(bp) got, err := g.CompileAdd(bp) if tt.wantErr { @@ -186,89 +282,90 @@ func TestMysqlGrammar_CompileAdd(t *testing.T) { } func TestMysqlGrammar_CompileChange(t *testing.T) { - g := newMysqlGrammar() + g, err := grammars.NewGrammar("mysql") + require.NoError(t, err) tests := []struct { name string table string - blueprint func(table *Blueprint) + blueprint func(table *blueprint.Blueprint) want []string wantErr bool }{ { name: "no changed columns returns nil", table: "users", - blueprint: func(_ *Blueprint) {}, + blueprint: func(_ *blueprint.Blueprint) {}, want: nil, wantErr: false, }, { name: "change single column type", table: "users", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.Integer("age").Change() }, - want: []string{"ALTER TABLE users MODIFY COLUMN age INT NOT NULL"}, + want: []string{"ALTER TABLE `users` MODIFY COLUMN `age` INT NOT NULL"}, wantErr: false, }, { - name: "change column with nullable command", + name: "change column with nullable blueprint.Command", table: "users", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.String("email", 255).Nullable().Change() }, - want: []string{"ALTER TABLE users MODIFY COLUMN email VARCHAR(255) NULL"}, + want: []string{"ALTER TABLE `users` MODIFY COLUMN `email` VARCHAR(255) NULL"}, wantErr: false, }, { - name: "change column with not nullable command", + name: "change column with not nullable blueprint.Command", table: "users", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.String("name", 100).Nullable(false).Change() }, - want: []string{"ALTER TABLE users MODIFY COLUMN name VARCHAR(100) NOT NULL"}, + want: []string{"ALTER TABLE `users` MODIFY COLUMN `name` VARCHAR(100) NOT NULL"}, wantErr: false, }, { name: "change column with default value", table: "users", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.String("status", 50).Default("active").Change() }, - want: []string{"ALTER TABLE users MODIFY COLUMN status VARCHAR(50) NOT NULL DEFAULT 'active'"}, + want: []string{"ALTER TABLE `users` MODIFY COLUMN `status` VARCHAR(50) NOT NULL DEFAULT 'active'"}, wantErr: false, }, { name: "change column with null default", table: "users", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.Text("description").Nullable().Default(nil).Change() }, - want: []string{"ALTER TABLE users MODIFY COLUMN description TEXT NULL DEFAULT NULL"}, + want: []string{"ALTER TABLE `users` MODIFY COLUMN `description` TEXT NULL DEFAULT NULL"}, wantErr: false, }, { name: "change column with comment", table: "users", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.Integer("age").Comment("User age in years").Change() }, - want: []string{"ALTER TABLE users MODIFY COLUMN age INT NOT NULL COMMENT 'User age in years'"}, + want: []string{"ALTER TABLE `users` MODIFY COLUMN `age` INT NOT NULL COMMENT 'User age in years'"}, wantErr: false, }, { name: "change column with empty comment", table: "users", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.Text("notes").Comment("").Change() }, - want: []string{"ALTER TABLE users MODIFY COLUMN notes TEXT NOT NULL COMMENT ''"}, + want: []string{"ALTER TABLE `users` MODIFY COLUMN `notes` TEXT NOT NULL COMMENT ''"}, wantErr: false, }, { - name: "change column with all commands", + name: "change column with all blueprint.Commands", table: "users", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.String("email", 255). Nullable(false). Default("example@test.com"). @@ -276,27 +373,27 @@ func TestMysqlGrammar_CompileChange(t *testing.T) { Change() }, want: []string{ - "ALTER TABLE users MODIFY COLUMN email VARCHAR(255) NOT NULL DEFAULT 'example@test.com' COMMENT 'User email address'", + "ALTER TABLE `users` MODIFY COLUMN `email` VARCHAR(255) NOT NULL DEFAULT 'example@test.com' COMMENT 'User email address'", }, wantErr: false, }, { name: "change multiple columns", table: "users", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.String("name", 200).Change() table.SmallInteger("age").Nullable().Change() }, want: []string{ - "ALTER TABLE users MODIFY COLUMN name VARCHAR(200) NOT NULL", - "ALTER TABLE users MODIFY COLUMN age SMALLINT NULL", + "ALTER TABLE `users` MODIFY COLUMN `name` VARCHAR(200) NOT NULL", + "ALTER TABLE `users` MODIFY COLUMN `age` SMALLINT NULL", }, wantErr: false, }, { name: "change column with empty name should return error", table: "users", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.Integer("").Change() }, wantErr: true, @@ -305,9 +402,13 @@ func TestMysqlGrammar_CompileChange(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - bp := &Blueprint{name: tt.table, grammar: g, dialect: dialect.MySQL} + tableName := tt.table + if tableName == "" && !tt.wantErr { + tableName = tt.name + } + bp := &blueprint.Blueprint{Name: tableName, Grammar: g, Dialect: dialect.MySQL} tt.blueprint(bp) - statements, err := bp.toSQL() + statements, err := bp.ToSQL() if tt.wantErr { require.Error(t, err, "Expected error for test case: %s", tt.name) return @@ -319,7 +420,8 @@ func TestMysqlGrammar_CompileChange(t *testing.T) { } func TestMysqlGrammar_CompileRename(t *testing.T) { - g := newMysqlGrammar() + g, err := grammars.NewGrammar("mysql") + require.NoError(t, err) tests := []struct { name string @@ -332,32 +434,36 @@ func TestMysqlGrammar_CompileRename(t *testing.T) { name: "rename table with valid names", table: "users", newName: "customers", - want: "ALTER TABLE users RENAME TO customers", + want: "ALTER TABLE `users` RENAME TO `customers`", wantErr: false, }, { name: "rename table with underscore names", table: "old_table_name", newName: "new_table_name", - want: "ALTER TABLE old_table_name RENAME TO new_table_name", + want: "ALTER TABLE `old_table_name` RENAME TO `new_table_name`", wantErr: false, }, { name: "rename table with numeric names", table: "table1", newName: "table2", - want: "ALTER TABLE table1 RENAME TO table2", + want: "ALTER TABLE `table1` RENAME TO `table2`", wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - bp := &Blueprint{ - name: tt.table, + tableName := tt.table + if tableName == "" && !tt.wantErr { + tableName = tt.name } - bp.rename(tt.newName) - got, err := g.CompileRename(bp, bp.commands[0]) + bp := &blueprint.Blueprint{ + Name: tableName, + } + bp.Rename(tt.newName) + got, err := g.CompileRename(bp, bp.Commands[0]) if tt.wantErr { require.Error(t, err, "Expected error for test case: %s", tt.name) return @@ -369,7 +475,8 @@ func TestMysqlGrammar_CompileRename(t *testing.T) { } func TestMysqlGrammar_CompileDrop(t *testing.T) { - g := newMysqlGrammar() + g, err := grammars.NewGrammar("mysql") + require.NoError(t, err) tests := []struct { name string @@ -380,25 +487,25 @@ func TestMysqlGrammar_CompileDrop(t *testing.T) { { name: "drop table with valid name", table: "users", - want: "DROP TABLE users", + want: "DROP TABLE `users`", wantErr: false, }, { name: "drop table with underscore name", table: "user_profiles", - want: "DROP TABLE user_profiles", + want: "DROP TABLE `user_profiles`", wantErr: false, }, { name: "drop table with numeric name", table: "table123", - want: "DROP TABLE table123", + want: "DROP TABLE `table123`", wantErr: false, }, { name: "drop table with mixed case name", table: "UserTable", - want: "DROP TABLE UserTable", + want: "DROP TABLE `UserTable`", wantErr: false, }, { @@ -410,7 +517,11 @@ func TestMysqlGrammar_CompileDrop(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - bp := &Blueprint{name: tt.table} + tableName := tt.table + if tableName == "" && !tt.wantErr { + tableName = tt.name + } + bp := &blueprint.Blueprint{Name: tableName} got, err := g.CompileDrop(bp) if tt.wantErr { require.Error(t, err, "Expected error for test case: %s", tt.name) @@ -423,7 +534,8 @@ func TestMysqlGrammar_CompileDrop(t *testing.T) { } func TestMysqlGrammar_CompileDropIfExists(t *testing.T) { - g := newMysqlGrammar() + g, err := grammars.NewGrammar("mysql") + require.NoError(t, err) tests := []struct { name string @@ -434,25 +546,25 @@ func TestMysqlGrammar_CompileDropIfExists(t *testing.T) { { name: "drop table if exists with valid name", table: "users", - want: "DROP TABLE IF EXISTS users", + want: "DROP TABLE IF EXISTS `users`", wantErr: false, }, { name: "drop table if exists with underscore name", table: "user_profiles", - want: "DROP TABLE IF EXISTS user_profiles", + want: "DROP TABLE IF EXISTS `user_profiles`", wantErr: false, }, { name: "drop table if exists with numeric name", table: "table123", - want: "DROP TABLE IF EXISTS table123", + want: "DROP TABLE IF EXISTS `table123`", wantErr: false, }, { name: "drop table if exists with mixed case name", table: "UserTable", - want: "DROP TABLE IF EXISTS UserTable", + want: "DROP TABLE IF EXISTS `UserTable`", wantErr: false, }, { @@ -464,7 +576,11 @@ func TestMysqlGrammar_CompileDropIfExists(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - bp := &Blueprint{name: tt.table} + tableName := tt.table + if tableName == "" && !tt.wantErr { + tableName = tt.name + } + bp := &blueprint.Blueprint{Name: tableName} got, err := g.CompileDropIfExists(bp) if tt.wantErr { require.Error(t, err, "Expected error for test case: %s", tt.name) @@ -477,37 +593,38 @@ func TestMysqlGrammar_CompileDropIfExists(t *testing.T) { } func TestMysqlGrammar_CompileDropColumn(t *testing.T) { - g := newMysqlGrammar() + g, err := grammars.NewGrammar("mysql") + require.NoError(t, err) tests := []struct { name string table string - blueprint func(table *Blueprint) + blueprint func(table *blueprint.Blueprint) want string wantErr bool }{ { name: "drop single column", table: "users", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.DropColumn("email") }, - want: "ALTER TABLE users DROP COLUMN email", + want: "ALTER TABLE `users` DROP COLUMN `email`", wantErr: false, }, { name: "drop multiple columns", table: "users", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.DropColumn("email", "phone", "address") }, - want: "ALTER TABLE users DROP COLUMN email, DROP COLUMN phone, DROP COLUMN address", + want: "ALTER TABLE `users` DROP COLUMN `email`, DROP COLUMN `phone`, DROP COLUMN `address`", wantErr: false, }, { name: "empty column name should return error", table: "users", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.DropColumn("email", "", "phone") }, wantErr: true, @@ -515,7 +632,7 @@ func TestMysqlGrammar_CompileDropColumn(t *testing.T) { { name: "single empty column name should return error", table: "users", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.DropColumn("") }, wantErr: true, @@ -524,11 +641,15 @@ func TestMysqlGrammar_CompileDropColumn(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - bp := &Blueprint{ - name: tt.table, + tableName := tt.table + if tableName == "" && !tt.wantErr { + tableName = tt.name + } + bp := &blueprint.Blueprint{ + Name: tableName, } tt.blueprint(bp) - got, err := g.CompileDropColumn(bp, bp.commands[0]) + got, err := g.CompileDropColumn(bp, bp.Commands[0]) if tt.wantErr { require.Error(t, err, "Expected error for test case: %s", tt.name) return @@ -540,7 +661,8 @@ func TestMysqlGrammar_CompileDropColumn(t *testing.T) { } func TestMysqlGrammar_CompileRenameColumn(t *testing.T) { - g := newMysqlGrammar() + g, err := grammars.NewGrammar("mysql") + require.NoError(t, err) tests := []struct { name string @@ -555,7 +677,7 @@ func TestMysqlGrammar_CompileRenameColumn(t *testing.T) { table: "users", oldName: "email", newName: "email_address", - want: "ALTER TABLE users RENAME COLUMN email TO email_address", + want: "ALTER TABLE `users` RENAME COLUMN `email` TO `email_address`", wantErr: false, }, { @@ -583,8 +705,12 @@ func TestMysqlGrammar_CompileRenameColumn(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - bp := &Blueprint{name: tt.table} - command := &command{from: tt.oldName, to: tt.newName} + tableName := tt.table + if tableName == "" && !tt.wantErr { + tableName = tt.name + } + bp := &blueprint.Blueprint{Name: tableName} + command := &blueprint.Command{From: tt.oldName, To: tt.newName} got, err := g.CompileRenameColumn(bp, command) if tt.wantErr { require.Error(t, err, "Expected error for test case: %s", tt.name) @@ -597,82 +723,83 @@ func TestMysqlGrammar_CompileRenameColumn(t *testing.T) { } func TestMysqlGrammar_CompileForeign(t *testing.T) { - g := newMysqlGrammar() + g, err := grammars.NewGrammar("mysql") + require.NoError(t, err) tests := []struct { name string table string - blueprint func(table *Blueprint) + blueprint func(table *blueprint.Blueprint) want string wantErr bool }{ { name: "basic foreign key", table: "users", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.Foreign("company_id").References("id").On("companies") }, - want: "ALTER TABLE users ADD CONSTRAINT fk_users_companies FOREIGN KEY (company_id) REFERENCES companies(id)", + want: "ALTER TABLE `users` ADD CONSTRAINT `users_company_id_foreign` FOREIGN KEY (`company_id`) REFERENCES `companies` (`id`)", wantErr: false, }, { name: "foreign key with on delete cascade", table: "users", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.Foreign("company_id").References("id").On("companies").CascadeOnDelete() }, - want: "ALTER TABLE users ADD CONSTRAINT fk_users_companies FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE", + want: "ALTER TABLE `users` ADD CONSTRAINT `users_company_id_foreign` FOREIGN KEY (`company_id`) REFERENCES `companies` (`id`) ON DELETE CASCADE", wantErr: false, }, { name: "foreign key with on update cascade", table: "users", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.Foreign("company_id").References("id").On("companies").CascadeOnUpdate() }, - want: "ALTER TABLE users ADD CONSTRAINT fk_users_companies FOREIGN KEY (company_id) REFERENCES companies(id) ON UPDATE CASCADE", + want: "ALTER TABLE `users` ADD CONSTRAINT `users_company_id_foreign` FOREIGN KEY (`company_id`) REFERENCES `companies` (`id`) ON UPDATE CASCADE", wantErr: false, }, { name: "foreign key with both on delete and on update", table: "users", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.Foreign("company_id").References("id").On("companies"). CascadeOnDelete().NullOnUpdate() }, - want: "ALTER TABLE users ADD CONSTRAINT fk_users_companies FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE ON UPDATE SET NULL", + want: "ALTER TABLE `users` ADD CONSTRAINT `users_company_id_foreign` FOREIGN KEY (`company_id`) REFERENCES `companies` (`id`) ON DELETE CASCADE ON UPDATE SET NULL", wantErr: false, }, { name: "foreign key with on delete restrict", table: "orders", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.Foreign("user_id").References("id").On("users").RestrictOnDelete() }, - want: "ALTER TABLE orders ADD CONSTRAINT fk_orders_users FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE RESTRICT", + want: "ALTER TABLE `orders` ADD CONSTRAINT `orders_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE RESTRICT", wantErr: false, }, { name: "foreign key with on delete set null", table: "posts", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.Foreign("author_id").References("id").On("users").NullOnDelete() }, - want: "ALTER TABLE posts ADD CONSTRAINT fk_posts_users FOREIGN KEY (author_id) REFERENCES users(id) ON DELETE SET NULL", + want: "ALTER TABLE `posts` ADD CONSTRAINT `posts_author_id_foreign` FOREIGN KEY (`author_id`) REFERENCES `users` (`id`) ON DELETE SET NULL", wantErr: false, }, { name: "foreign key with no actions on delete or update", table: "comments", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.Foreign("post_id").References("id").On("posts").NoActionOnDelete().NoActionOnUpdate() }, - want: "ALTER TABLE comments ADD CONSTRAINT fk_comments_posts FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE NO ACTION ON UPDATE NO ACTION", + want: "ALTER TABLE `comments` ADD CONSTRAINT `comments_post_id_foreign` FOREIGN KEY (`post_id`) REFERENCES `posts` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION", }, { name: "empty column should return error", table: "users", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.Foreign("").References("id").On("companies") }, wantErr: true, @@ -680,7 +807,7 @@ func TestMysqlGrammar_CompileForeign(t *testing.T) { { name: "empty on table should return error", table: "users", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.Foreign("company_id").References("id").On("") }, wantErr: true, @@ -688,7 +815,7 @@ func TestMysqlGrammar_CompileForeign(t *testing.T) { { name: "empty references should return error", table: "users", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.Foreign("company_id").References("").On("companies") }, wantErr: true, @@ -696,7 +823,7 @@ func TestMysqlGrammar_CompileForeign(t *testing.T) { { name: "all empty values should return error", table: "users", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.Foreign("").References("").On("") }, wantErr: true, @@ -705,9 +832,13 @@ func TestMysqlGrammar_CompileForeign(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - bp := &Blueprint{name: tt.table} + tableName := tt.table + if tableName == "" && !tt.wantErr { + tableName = tt.name + } + bp := &blueprint.Blueprint{Name: tableName} tt.blueprint(bp) - got, err := g.CompileForeign(bp, bp.commands[0]) + got, err := g.CompileForeign(bp, bp.Commands[0]) if tt.wantErr { require.Error(t, err, "Expected error for test case: %s", tt.name) return @@ -719,7 +850,8 @@ func TestMysqlGrammar_CompileForeign(t *testing.T) { } func TestMysqlGrammar_CompileDropForeign(t *testing.T) { - g := newMysqlGrammar() + g, err := grammars.NewGrammar("mysql") + require.NoError(t, err) tests := []struct { name string @@ -732,21 +864,24 @@ func TestMysqlGrammar_CompileDropForeign(t *testing.T) { name: "drop single foreign key", table: "users", fkName: "fk_users_company_id", - want: "ALTER TABLE users DROP FOREIGN KEY fk_users_company_id", + want: "ALTER TABLE `users` DROP FOREIGN KEY `fk_users_company_id`", wantErr: false, }, { name: "empty foreign key name should return error", table: "users", - fkName: "", wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - bp := &Blueprint{name: tt.table} - command := &command{index: tt.fkName} + tableName := tt.table + if tableName == "" && !tt.wantErr { + tableName = tt.name + } + bp := &blueprint.Blueprint{Name: tableName} + command := &blueprint.Command{Index: tt.fkName} got, err := g.CompileDropForeign(bp, command) if tt.wantErr { require.Error(t, err, "Expected error for test case: %s", tt.name) @@ -759,64 +894,65 @@ func TestMysqlGrammar_CompileDropForeign(t *testing.T) { } func TestMysqlGrammar_CompileIndex(t *testing.T) { - g := newMysqlGrammar() + g, err := grammars.NewGrammar("mysql") + require.NoError(t, err) tests := []struct { name string table string - blueprint func(table *Blueprint) + blueprint func(table *blueprint.Blueprint) want string wantErr bool }{ { name: "basic index on single column", table: "users", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.Index("email") }, - want: "CREATE INDEX idx_users_email ON users (email)", + want: "CREATE INDEX `users_email_index` ON `users` (`email`)", wantErr: false, }, { name: "index on multiple columns", table: "users", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.Index("first_name", "last_name") }, - want: "CREATE INDEX idx_users_first_name_last_name ON users (first_name, last_name)", + want: "CREATE INDEX `users_first_name_last_name_index` ON `users` (`first_name`, `last_name`)", wantErr: false, }, { name: "index with custom name", table: "products", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.Index("category_id").Name("idx_product_category") }, - want: "CREATE INDEX idx_product_category ON products (category_id)", + want: "CREATE INDEX `idx_product_category` ON `products` (`category_id`)", wantErr: false, }, { name: "index with algorithm", table: "logs", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.Index("created_at").Algorithm("BTREE") }, - want: "CREATE INDEX idx_logs_created_at ON logs (created_at) USING BTREE", + want: "CREATE INDEX `logs_created_at_index` ON `logs` (`created_at`) USING BTREE", wantErr: false, }, { name: "index with custom name and algorithm", table: "orders", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.Index("status", "created_at").Name("idx_order_status_date").Algorithm("HASH") }, - want: "CREATE INDEX idx_order_status_date ON orders (status, created_at) USING HASH", + want: "CREATE INDEX `idx_order_status_date` ON `orders` (`status`, `created_at`) USING HASH", wantErr: false, }, { name: "empty columns should return error", table: "users", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.Index("") }, wantErr: true, @@ -825,9 +961,13 @@ func TestMysqlGrammar_CompileIndex(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - bp := &Blueprint{name: tt.table} + tableName := tt.table + if tableName == "" && !tt.wantErr { + tableName = tt.name + } + bp := &blueprint.Blueprint{Name: tableName} tt.blueprint(bp) - got, err := g.CompileIndex(bp, bp.commands[0]) + got, err := g.CompileIndex(bp, bp.Commands[0]) if tt.wantErr { require.Error(t, err, "Expected error for test case: %s", tt.name) return @@ -839,64 +979,65 @@ func TestMysqlGrammar_CompileIndex(t *testing.T) { } func TestMysqlGrammar_CompileUnique(t *testing.T) { - g := newMysqlGrammar() + g, err := grammars.NewGrammar("mysql") + require.NoError(t, err) tests := []struct { name string table string - blueprint func(table *Blueprint) + blueprint func(table *blueprint.Blueprint) want string wantErr bool }{ { name: "basic unique index on single column", table: "users", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.Unique("email") }, - want: "CREATE UNIQUE INDEX uk_users_email ON users (email)", + want: "CREATE UNIQUE INDEX `users_email_unique` ON `users` (`email`)", wantErr: false, }, { name: "unique index on multiple columns", table: "users", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.Unique("first_name", "last_name") }, - want: "CREATE UNIQUE INDEX uk_users_first_name_last_name ON users (first_name, last_name)", + want: "CREATE UNIQUE INDEX `users_first_name_last_name_unique` ON `users` (`first_name`, `last_name`)", wantErr: false, }, { name: "unique index with custom name", table: "products", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.Unique("sku").Name("unique_product_sku") }, - want: "CREATE UNIQUE INDEX unique_product_sku ON products (sku)", + want: "CREATE UNIQUE INDEX `unique_product_sku` ON `products` (`sku`)", wantErr: false, }, { name: "unique index with algorithm", table: "logs", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.Unique("transaction_id").Algorithm("BTREE") }, - want: "CREATE UNIQUE INDEX uk_logs_transaction_id ON logs (transaction_id) USING BTREE", + want: "CREATE UNIQUE INDEX `logs_transaction_id_unique` ON `logs` (`transaction_id`) USING BTREE", wantErr: false, }, { name: "unique index with custom name and algorithm", table: "orders", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.Unique("order_number", "customer_id").Name("unique_order_customer").Algorithm("HASH") }, - want: "CREATE UNIQUE INDEX unique_order_customer ON orders (order_number, customer_id) USING HASH", + want: "CREATE UNIQUE INDEX `unique_order_customer` ON `orders` (`order_number`, `customer_id`) USING HASH", wantErr: false, }, { name: "empty column should return error", table: "users", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.Unique("") }, wantErr: true, @@ -904,7 +1045,7 @@ func TestMysqlGrammar_CompileUnique(t *testing.T) { { name: "one empty column among multiple should return error", table: "users", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.Unique("email", "", "username") }, wantErr: true, @@ -913,9 +1054,13 @@ func TestMysqlGrammar_CompileUnique(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - bp := &Blueprint{name: tt.table} + tableName := tt.table + if tableName == "" && !tt.wantErr { + tableName = tt.name + } + bp := &blueprint.Blueprint{Name: tableName} tt.blueprint(bp) - got, err := g.CompileUnique(bp, bp.commands[0]) + got, err := g.CompileUnique(bp, bp.Commands[0]) if tt.wantErr { require.Error(t, err, "Expected error for test case: %s", tt.name) return @@ -927,64 +1072,65 @@ func TestMysqlGrammar_CompileUnique(t *testing.T) { } func TestMysqlGrammar_CompilePrimary(t *testing.T) { - g := newMysqlGrammar() + g, err := grammars.NewGrammar("mysql") + require.NoError(t, err) tests := []struct { name string table string - blueprint func(table *Blueprint) + blueprint func(table *blueprint.Blueprint) want string wantErr bool }{ { name: "basic primary key on single column", table: "users", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.Primary("id") }, - want: "ALTER TABLE users ADD CONSTRAINT pk_users PRIMARY KEY (id)", + want: "ALTER TABLE `users` ADD CONSTRAINT `users_id_primary` PRIMARY KEY (`id`)", wantErr: false, }, { name: "composite primary key on multiple columns", table: "order_items", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.Primary("order_id", "product_id") }, - want: "ALTER TABLE order_items ADD CONSTRAINT pk_order_items PRIMARY KEY (order_id, product_id)", + want: "ALTER TABLE `order_items` ADD CONSTRAINT `order_items_order_id_product_id_primary` PRIMARY KEY (`order_id`, `product_id`)", wantErr: false, }, { name: "primary key with custom name", table: "products", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.Primary("sku").Name("primary_product_sku") }, - want: "ALTER TABLE products ADD CONSTRAINT primary_product_sku PRIMARY KEY (sku)", + want: "ALTER TABLE `products` ADD CONSTRAINT `primary_product_sku` PRIMARY KEY (`sku`)", wantErr: false, }, { name: "primary key on three columns", table: "user_permissions", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.Primary("user_id", "resource_id", "permission_id") }, - want: "ALTER TABLE user_permissions ADD CONSTRAINT pk_user_permissions PRIMARY KEY (user_id, resource_id, permission_id)", + want: "ALTER TABLE `user_permissions` ADD CONSTRAINT `user_permissions_user_id_resource_id_permission_id_primary` PRIMARY KEY (`user_id`, `resource_id`, `permission_id`)", wantErr: false, }, { name: "primary key with custom name on multiple columns", table: "audit_logs", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.Primary("timestamp", "user_id", "action").Name("pk_audit_composite") }, - want: "ALTER TABLE audit_logs ADD CONSTRAINT pk_audit_composite PRIMARY KEY (timestamp, user_id, action)", + want: "ALTER TABLE `audit_logs` ADD CONSTRAINT `pk_audit_composite` PRIMARY KEY (`timestamp`, `user_id`, `action`)", wantErr: false, }, { name: "empty column should return error", table: "users", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.Primary("") }, wantErr: true, @@ -992,7 +1138,7 @@ func TestMysqlGrammar_CompilePrimary(t *testing.T) { { name: "one empty column among multiple should return error", table: "users", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.Primary("id", "", "email") }, wantErr: true, @@ -1000,7 +1146,7 @@ func TestMysqlGrammar_CompilePrimary(t *testing.T) { { name: "empty column in the middle should return error", table: "orders", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.Primary("user_id", "", "order_number") }, wantErr: true, @@ -1009,9 +1155,13 @@ func TestMysqlGrammar_CompilePrimary(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - bp := &Blueprint{name: tt.table} + tableName := tt.table + if tableName == "" && !tt.wantErr { + tableName = tt.name + } + bp := &blueprint.Blueprint{Name: tableName} tt.blueprint(bp) - got, err := g.CompilePrimary(bp, bp.commands[0]) + got, err := g.CompilePrimary(bp, bp.Commands[0]) if tt.wantErr { require.Error(t, err, "Expected error for test case: %s", tt.name) return @@ -1023,64 +1173,65 @@ func TestMysqlGrammar_CompilePrimary(t *testing.T) { } func TestMysqlGrammar_CompileFullText(t *testing.T) { - g := newMysqlGrammar() + g, err := grammars.NewGrammar("mysql") + require.NoError(t, err) tests := []struct { name string table string - blueprint func(table *Blueprint) + blueprint func(table *blueprint.Blueprint) want string wantErr bool }{ { name: "basic fulltext index on single column", table: "articles", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.FullText("content") }, - want: "CREATE FULLTEXT INDEX ft_articles_content ON articles (content)", + want: "CREATE FULLTEXT INDEX `articles_content_fulltext` ON `articles` (`content`)", wantErr: false, }, { name: "fulltext index on multiple columns", table: "posts", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.FullText("title", "content") }, - want: "CREATE FULLTEXT INDEX ft_posts_title_content ON posts (title, content)", + want: "CREATE FULLTEXT INDEX `posts_title_content_fulltext` ON `posts` (`title`, `content`)", wantErr: false, }, { name: "fulltext index with custom name", table: "documents", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.FullText("body").Name("fulltext_document_body") }, - want: "CREATE FULLTEXT INDEX fulltext_document_body ON documents (body)", + want: "CREATE FULLTEXT INDEX `fulltext_document_body` ON `documents` (`body`)", wantErr: false, }, { name: "fulltext index on three columns", table: "news", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.FullText("title", "summary", "content") }, - want: "CREATE FULLTEXT INDEX ft_news_title_summary_content ON news (title, summary, content)", + want: "CREATE FULLTEXT INDEX `news_title_summary_content_fulltext` ON `news` (`title`, `summary`, `content`)", wantErr: false, }, { name: "fulltext index with custom name on multiple columns", table: "blog_posts", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.FullText("title", "excerpt", "body").Name("ft_blog_search") }, - want: "CREATE FULLTEXT INDEX ft_blog_search ON blog_posts (title, excerpt, body)", + want: "CREATE FULLTEXT INDEX `ft_blog_search` ON `blog_posts` (`title`, `excerpt`, `body`)", wantErr: false, }, { name: "empty column should return error", table: "articles", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.FullText("") }, wantErr: true, @@ -1088,7 +1239,7 @@ func TestMysqlGrammar_CompileFullText(t *testing.T) { { name: "one empty column among multiple should return error", table: "posts", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.FullText("title", "", "content") }, wantErr: true, @@ -1096,7 +1247,7 @@ func TestMysqlGrammar_CompileFullText(t *testing.T) { { name: "empty column in the middle should return error", table: "documents", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.FullText("title", "", "body") }, wantErr: true, @@ -1105,9 +1256,13 @@ func TestMysqlGrammar_CompileFullText(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - bp := &Blueprint{name: tt.table} + tableName := tt.table + if tableName == "" && !tt.wantErr { + tableName = tt.name + } + bp := &blueprint.Blueprint{Name: tableName} tt.blueprint(bp) - got, err := g.CompileFullText(bp, bp.commands[0]) + got, err := g.CompileFullText(bp, bp.Commands[0]) if tt.wantErr { require.Error(t, err, "Expected error for test case: %s", tt.name) return @@ -1119,7 +1274,8 @@ func TestMysqlGrammar_CompileFullText(t *testing.T) { } func TestMysqlGrammar_CompileDropIndex(t *testing.T) { - g := newMysqlGrammar() + g, err := grammars.NewGrammar("mysql") + require.NoError(t, err) tests := []struct { name string @@ -1132,7 +1288,7 @@ func TestMysqlGrammar_CompileDropIndex(t *testing.T) { name: "drop index with valid name", table: "users", indexName: "idx_users_email", - want: "ALTER TABLE users DROP INDEX idx_users_email", + want: "ALTER TABLE `users` DROP INDEX `idx_users_email`", wantErr: false, }, { @@ -1145,8 +1301,12 @@ func TestMysqlGrammar_CompileDropIndex(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - bp := &Blueprint{name: tt.table} - command := &command{index: tt.indexName} + tableName := tt.table + if tableName == "" && !tt.wantErr { + tableName = tt.name + } + bp := &blueprint.Blueprint{Name: tableName} + command := &blueprint.Command{Index: tt.indexName} got, err := g.CompileDropIndex(bp, command) if tt.wantErr { require.Error(t, err, "Expected error for test case: %s", tt.name) @@ -1159,7 +1319,8 @@ func TestMysqlGrammar_CompileDropIndex(t *testing.T) { } func TestMysqlGrammar_CompileDropUnique(t *testing.T) { - g := newMysqlGrammar() + g, err := grammars.NewGrammar("mysql") + require.NoError(t, err) tests := []struct { name string @@ -1172,7 +1333,7 @@ func TestMysqlGrammar_CompileDropUnique(t *testing.T) { name: "drop unique index with valid name", table: "users", indexName: "uk_users_email", - want: "ALTER TABLE users DROP INDEX uk_users_email", + want: "ALTER TABLE `users` DROP INDEX `uk_users_email`", wantErr: false, }, { @@ -1184,8 +1345,12 @@ func TestMysqlGrammar_CompileDropUnique(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - bp := &Blueprint{name: tt.table} - command := &command{index: tt.indexName} + tableName := tt.table + if tableName == "" && !tt.wantErr { + tableName = tt.name + } + bp := &blueprint.Blueprint{Name: tableName} + command := &blueprint.Command{Index: tt.indexName} got, err := g.CompileDropUnique(bp, command) if tt.wantErr { require.Error(t, err, "Expected error for test case: %s", tt.name) @@ -1198,7 +1363,8 @@ func TestMysqlGrammar_CompileDropUnique(t *testing.T) { } func TestMysqlGrammar_CompileDropFulltext(t *testing.T) { - g := newMysqlGrammar() + g, err := grammars.NewGrammar("mysql") + require.NoError(t, err) tests := []struct { name string @@ -1211,7 +1377,7 @@ func TestMysqlGrammar_CompileDropFulltext(t *testing.T) { name: "drop fulltext index with valid name", table: "articles", indexName: "ft_articles_content", - want: "ALTER TABLE articles DROP INDEX ft_articles_content", + want: "ALTER TABLE `articles` DROP INDEX `ft_articles_content`", wantErr: false, }, { @@ -1223,8 +1389,12 @@ func TestMysqlGrammar_CompileDropFulltext(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - bp := &Blueprint{name: tt.table} - command := &command{index: tt.indexName} + tableName := tt.table + if tableName == "" && !tt.wantErr { + tableName = tt.name + } + bp := &blueprint.Blueprint{Name: tableName} + command := &blueprint.Command{Index: tt.indexName} got, err := g.CompileDropFulltext(bp, command) if tt.wantErr { require.Error(t, err, "Expected error for test case: %s", tt.name) @@ -1237,7 +1407,8 @@ func TestMysqlGrammar_CompileDropFulltext(t *testing.T) { } func TestMysqlGrammar_CompileDropPrimary(t *testing.T) { - g := newMysqlGrammar() + g, err := grammars.NewGrammar("mysql") + require.NoError(t, err) tests := []struct { name string @@ -1250,43 +1421,47 @@ func TestMysqlGrammar_CompileDropPrimary(t *testing.T) { name: "drop primary key with valid name", table: "users", indexName: "pk_users", - want: "ALTER TABLE users DROP PRIMARY KEY", + want: "ALTER TABLE `users` DROP PRIMARY KEY", wantErr: false, }, { name: "drop primary key with underscore name", table: "user_profiles", indexName: "pk_user_profiles", - want: "ALTER TABLE user_profiles DROP PRIMARY KEY", + want: "ALTER TABLE `user_profiles` DROP PRIMARY KEY", wantErr: false, }, { name: "drop primary key with numeric name", table: "table123", indexName: "pk_123", - want: "ALTER TABLE table123 DROP PRIMARY KEY", + want: "ALTER TABLE `table123` DROP PRIMARY KEY", wantErr: false, }, { name: "drop primary key with mixed case name", table: "UserTable", indexName: "PkUserTable", - want: "ALTER TABLE UserTable DROP PRIMARY KEY", + want: "ALTER TABLE `UserTable` DROP PRIMARY KEY", wantErr: false, }, { name: "drop primary key with special characters in name", table: "orders", indexName: "pk_order$id", - want: "ALTER TABLE orders DROP PRIMARY KEY", + want: "ALTER TABLE `orders` DROP PRIMARY KEY", wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - bp := &Blueprint{name: tt.table} - command := &command{index: tt.indexName} + tableName := tt.table + if tableName == "" && !tt.wantErr { + tableName = tt.name + } + bp := &blueprint.Blueprint{Name: tableName} + command := &blueprint.Command{Index: tt.indexName} got, err := g.CompileDropPrimary(bp, command) if tt.wantErr { require.Error(t, err, "Expected error for test case: %s", tt.name) @@ -1299,7 +1474,8 @@ func TestMysqlGrammar_CompileDropPrimary(t *testing.T) { } func TestMysqlGrammar_CompileRenameIndex(t *testing.T) { - g := newMysqlGrammar() + g, err := grammars.NewGrammar("mysql") + require.NoError(t, err) tests := []struct { name string @@ -1314,7 +1490,7 @@ func TestMysqlGrammar_CompileRenameIndex(t *testing.T) { table: "users", oldName: "idx_users_email", newName: "idx_users_email_address", - want: "ALTER TABLE users RENAME INDEX idx_users_email TO idx_users_email_address", + want: "ALTER TABLE `users` RENAME INDEX `idx_users_email` TO `idx_users_email_address`", wantErr: false, }, { @@ -1322,7 +1498,7 @@ func TestMysqlGrammar_CompileRenameIndex(t *testing.T) { table: "user_profiles", oldName: "idx_user_profiles_name", newName: "idx_user_profiles_full_name", - want: "ALTER TABLE user_profiles RENAME INDEX idx_user_profiles_name TO idx_user_profiles_full_name", + want: "ALTER TABLE `user_profiles` RENAME INDEX `idx_user_profiles_name` TO `idx_user_profiles_full_name`", wantErr: false, }, { @@ -1330,7 +1506,7 @@ func TestMysqlGrammar_CompileRenameIndex(t *testing.T) { table: "orders", oldName: "idx_123", newName: "idx_456", - want: "ALTER TABLE orders RENAME INDEX idx_123 TO idx_456", + want: "ALTER TABLE `orders` RENAME INDEX `idx_123` TO `idx_456`", wantErr: false, }, { @@ -1338,7 +1514,7 @@ func TestMysqlGrammar_CompileRenameIndex(t *testing.T) { table: "Products", oldName: "IdxProductSku", newName: "IdxProductCode", - want: "ALTER TABLE Products RENAME INDEX IdxProductSku TO IdxProductCode", + want: "ALTER TABLE `Products` RENAME INDEX `IdxProductSku` TO `IdxProductCode`", wantErr: false, }, { @@ -1346,7 +1522,7 @@ func TestMysqlGrammar_CompileRenameIndex(t *testing.T) { table: "logs", oldName: "idx_log$date", newName: "idx_log$timestamp", - want: "ALTER TABLE logs RENAME INDEX idx_log$date TO idx_log$timestamp", + want: "ALTER TABLE `logs` RENAME INDEX `idx_log$date` TO `idx_log$timestamp`", wantErr: false, }, { @@ -1374,8 +1550,12 @@ func TestMysqlGrammar_CompileRenameIndex(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - bp := &Blueprint{name: tt.table} - command := &command{from: tt.oldName, to: tt.newName} + tableName := tt.table + if tableName == "" && !tt.wantErr { + tableName = tt.name + } + bp := &blueprint.Blueprint{Name: tableName} + command := &blueprint.Command{From: tt.oldName, To: tt.newName} got, err := g.CompileRenameIndex(bp, command) if tt.wantErr { require.Error(t, err, "Expected error for test case: %s", tt.name) @@ -1388,268 +1568,318 @@ func TestMysqlGrammar_CompileRenameIndex(t *testing.T) { } func TestMysqlGrammar_GetType(t *testing.T) { - g := newMysqlGrammar() + g, err := grammars.NewGrammar("mysql") + require.NoError(t, err) tests := []struct { name string - blueprint func(table *Blueprint) + blueprint func(table *blueprint.Blueprint) want string }{ { name: "custom column type", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.Column("name", "CUSTOM_TYPE") }, want: "CUSTOM_TYPE", }, { name: "boolean column type", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.Boolean("active") }, want: "TINYINT(1)", }, { name: "char column type", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.Char("code", 10) }, want: "CHAR(10)", }, { name: "string column type", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.String("name", 255) }, want: "VARCHAR(255)", }, { name: "decimal column type", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.Decimal("price", 10, 2) }, want: "DECIMAL(10, 2)", }, { name: "double column type", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.Double("value") }, want: "DOUBLE", }, { name: "float column type with precision", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.Float("value", 6) }, want: "FLOAT(6)", }, { name: "float column type without precision", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.Float("value") }, want: "FLOAT(53)", }, { name: "big integer column type", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.BigInteger("id") }, want: "BIGINT", }, { name: "integer column type", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.Integer("count") }, want: "INT", }, { name: "small integer column type", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.SmallInteger("status") }, want: "SMALLINT", }, { name: "medium integer column type", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.MediumInteger("value") }, want: "MEDIUMINT", }, { name: "small integer column type", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.SmallInteger("level") }, want: "SMALLINT", }, { name: "tiny integer column type", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.TinyInteger("flag") }, want: "TINYINT", }, { name: "time column type", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.Time("created_at") }, want: "TIME", }, { name: "datetime column type with precision", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.DateTime("created_at", 6) }, want: "DATETIME(6)", }, { name: "datetime column type without precision", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.DateTime("created_at", 0) }, want: "DATETIME", }, { name: "datetime tz column type with precision", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.DateTimeTz("created_at", 3) }, want: "DATETIME(3)", }, { name: "datetime tz column type without precision", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.DateTimeTz("created_at", 0) }, want: "DATETIME", }, { name: "timestamp column type with precision", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.Timestamp("created_at", 6) }, want: "TIMESTAMP(6)", }, { name: "timestamp column type without precision", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.Timestamp("created_at", 0) }, want: "TIMESTAMP", }, { name: "timestamp tz column type with precision", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.TimestampTz("created_at", 3) }, want: "TIMESTAMP(3)", }, { name: "timestamp tz column type without precision", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.TimestampTz("created_at", 0) }, want: "TIMESTAMP", }, + { + name: "time tz column type with precision", + blueprint: func(table *blueprint.Blueprint) { + table.TimeTz("created_at", 3) + }, + want: "TIME(3)", + }, + { + name: "time tz column type without precision", + blueprint: func(table *blueprint.Blueprint) { + table.TimeTz("created_at", 0) + }, + want: "TIME", + }, + { + name: "set column type", + blueprint: func(table *blueprint.Blueprint) { + table.Set("tags", []string{"a", "b", "c"}) + }, + want: "SET('a', 'b', 'c')", + }, + { + name: "ulid column type", + blueprint: func(table *blueprint.Blueprint) { + table.ULID("ulid") + }, + want: "CHAR(26)", + }, + { + name: "ip address column type", + blueprint: func(table *blueprint.Blueprint) { + table.IPAddress("ip") + }, + want: "VARCHAR(45)", + }, + { + name: "mac address column type", + blueprint: func(table *blueprint.Blueprint) { + table.MacAddress("mac") + }, + want: "VARCHAR(17)", + }, + { + name: "vector column type", + blueprint: func(table *blueprint.Blueprint) { + table.Vector("embedding") + }, + want: "VECTOR", + }, { name: "enum column type", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.Enum("status", []string{"active", "inactive", "pending"}) }, want: "ENUM('active', 'inactive', 'pending')", }, { - name: "long text column type", - blueprint: func(table *Blueprint) { + name: "`long` text column type", + blueprint: func(table *blueprint.Blueprint) { table.LongText("content") }, want: "LONGTEXT", }, { name: "text column type", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.Text("description") }, want: "TEXT", }, { - name: "medium text column type", - blueprint: func(table *Blueprint) { + name: "`medium` text column type", + blueprint: func(table *blueprint.Blueprint) { table.MediumText("summary") }, want: "MEDIUMTEXT", }, { - name: "tiny text column type", - blueprint: func(table *Blueprint) { + name: "`tiny` text column type", + blueprint: func(table *blueprint.Blueprint) { table.TinyText("notes") }, want: "TINYTEXT", }, { name: "date column type", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.Date("birth_date") }, want: "DATE", }, { name: "year column type", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.Year("graduation_year") }, want: "YEAR", }, { name: "json column type", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.JSON("metadata") }, want: "JSON", }, { name: "jsonb column type", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.JSONB("data") }, want: "JSON", }, { name: "uuid column type", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.UUID("uuid") }, want: "CHAR(36)", }, { name: "binary column type", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.Binary("data") }, want: "BLOB", }, { name: "geography column type", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.Geography("location", "LINESTRING", 4326) }, want: "LINESTRING SRID 4326", }, { name: "geometry column type", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.Geometry("shape", "", 4326) }, want: "GEOMETRY SRID 4326", }, { name: "point column type", - blueprint: func(table *Blueprint) { + blueprint: func(table *blueprint.Blueprint) { table.Point("location") }, want: "POINT SRID 4326", @@ -1658,151 +1888,10 @@ func TestMysqlGrammar_GetType(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - bp := &Blueprint{name: "test_table"} + bp := &blueprint.Blueprint{Name: "test_table"} tt.blueprint(bp) - got := g.getType(bp.columns[0]) + got := g.GetType(bp.Columns[0]) assert.Equal(t, tt.want, got, "Expected type to match for test case: %s", tt.name) }) } } - -func TestMysqlGrammar_GetColumns(t *testing.T) { - g := newMysqlGrammar() - - tests := []struct { - name string - blueprint func(table *Blueprint) - want []string - wantErr bool - }{ - { - name: "single basic column", - blueprint: func(table *Blueprint) { - table.String("name", 255) - }, - want: []string{"name VARCHAR(255) NOT NULL"}, - wantErr: false, - }, - { - name: "multiple basic columns", - blueprint: func(table *Blueprint) { - table.String("name", 255) - table.Integer("age") - }, - want: []string{"name VARCHAR(255) NOT NULL", "age INT NOT NULL"}, - wantErr: false, - }, - { - name: "column with default value", - blueprint: func(table *Blueprint) { - table.String("status", 50).Default("active") - }, - want: []string{"status VARCHAR(50) DEFAULT 'active' NOT NULL"}, - wantErr: false, - }, - { - name: "nullable column", - blueprint: func(table *Blueprint) { - table.String("email", 255).Nullable() - }, - want: []string{"email VARCHAR(255) NULL"}, - wantErr: false, - }, - { - name: "not nullable column", - blueprint: func(table *Blueprint) { - table.String("username", 100).Nullable(false) - }, - want: []string{"username VARCHAR(100) NOT NULL"}, - wantErr: false, - }, - { - name: "column with comment", - blueprint: func(table *Blueprint) { - table.String("name", 255).Comment("User full name") - }, - want: []string{"name VARCHAR(255) NOT NULL COMMENT 'User full name'"}, - wantErr: false, - }, - { - name: "primary key column", - blueprint: func(table *Blueprint) { - table.BigInteger("id").Primary() - }, - want: []string{"id BIGINT NOT NULL"}, - wantErr: false, - }, - { - name: "column with all attributes", - blueprint: func(table *Blueprint) { - table.String("email", 255). - Default("user@example.com"). - Nullable(). - Comment("User email address") - }, - want: []string{"email VARCHAR(255) DEFAULT 'user@example.com' NULL COMMENT 'User email address'"}, - wantErr: false, - }, - { - name: "auto increment primary key", - blueprint: func(table *Blueprint) { - table.BigInteger("id").Unsigned().AutoIncrement().Primary() - }, - want: []string{"id BIGINT UNSIGNED AUTO_INCREMENT NOT NULL"}, - wantErr: false, - }, - { - name: "multiple columns with different attributes", - blueprint: func(table *Blueprint) { - table.BigInteger("id").Unsigned().AutoIncrement().Primary() - table.String("name", 255).Comment("User name") - table.String("email", 255).Nullable() - table.Timestamp("created_at", 0).UseCurrent() - }, - want: []string{ - "id BIGINT UNSIGNED AUTO_INCREMENT NOT NULL", - "name VARCHAR(255) NOT NULL COMMENT 'User name'", - "email VARCHAR(255) NULL", - "created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL", - }, - wantErr: false, - }, - { - name: "column with null default value", - blueprint: func(table *Blueprint) { - table.Text("description").Nullable().Default(nil) - }, - want: []string{"description TEXT DEFAULT NULL NULL"}, - wantErr: false, - }, - { - name: "empty column name should return error", - blueprint: func(table *Blueprint) { - table.String("", 255) - }, - wantErr: true, - }, - { - name: "multiple columns with one empty name should return error", - blueprint: func(table *Blueprint) { - table.String("name", 255) - table.Integer("") - }, - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - bp := &Blueprint{name: "test_table"} - tt.blueprint(bp) - got, err := g.getColumns(bp) - if tt.wantErr { - require.Error(t, err, "Expected error for test case: %s", tt.name) - return - } - require.NoError(t, err, "Did not expect error for test case: %s", tt.name) - assert.Equal(t, tt.want, got, "Expected columns to match for test case: %s", tt.name) - }) - } -} diff --git a/schema/grammars/postgres_grammar.go b/schema/grammars/postgres_grammar.go new file mode 100644 index 0000000..ef02418 --- /dev/null +++ b/schema/grammars/postgres_grammar.go @@ -0,0 +1,695 @@ +package grammars + +import ( + "errors" + "fmt" + "slices" + "strings" + + "github.com/akfaiz/migris/schema/blueprint" +) + +type postgresGrammar struct { + blueprint.BaseGrammar +} + +func newPostgresGrammar() *postgresGrammar { + return &postgresGrammar{} +} + +func (g *postgresGrammar) CompileTableExists(_, table string) (string, error) { + schema, table := g.parseTable(table) + return fmt.Sprintf( + "SELECT 1 FROM information_schema.tables WHERE table_schema = %s AND table_name = %s", + g.QuoteString(schema), + g.QuoteString(table), + ), nil +} + +func (g *postgresGrammar) CompileTables(_ string) (string, error) { + return `SELECT + t.table_name, + t.table_schema, + pg_catalog.obj_description(c.oid, 'pg_class') as table_comment + FROM + information_schema.tables t + JOIN + pg_catalog.pg_class c ON c.relname = t.table_name + JOIN + pg_catalog.pg_namespace n ON n.oid = c.relnamespace AND n.nspname = t.table_schema + WHERE + t.table_schema = 'public' + AND t.table_type = 'BASE TABLE'`, nil +} + +func (g *postgresGrammar) CompileColumns(_, table string) (string, error) { + schema, table := g.parseTable(table) + return fmt.Sprintf(`SELECT + column_name, + data_type, + is_nullable, + column_default, + pg_catalog.col_description(c.oid, cols.ordinal_position::int) AS column_comment + FROM + information_schema.columns cols + JOIN + pg_catalog.pg_class c ON c.relname = cols.table_name + JOIN + pg_catalog.pg_namespace n ON n.oid = c.relnamespace AND n.nspname = cols.table_schema + WHERE + cols.table_schema = %s + AND cols.table_name = %s`, g.QuoteString(schema), g.QuoteString(table)), nil +} + +func (g *postgresGrammar) CompileIndexes(_, table string) (string, error) { + schema, table := g.parseTable(table) + return fmt.Sprintf(`SELECT + i.relname AS index_name, + ix.indisunique AS is_unique, + ix.indisprimary AS is_primary, + a.attname AS column_name + FROM + pg_class t + JOIN + pg_index ix ON t.oid = ix.indrelid + JOIN + pg_class i ON i.oid = ix.indexrelid + JOIN + pg_namespace n ON n.oid = t.relnamespace + CROSS JOIN + unnest(ix.indkey) WITH ORDINALITY AS k(attnum, ord) + JOIN + pg_attribute a ON a.attrelid = t.oid AND a.attnum = k.attnum + WHERE + t.relname = %s + AND n.nspname = %s + AND t.relkind = 'r' + ORDER BY + i.relname, k.ord`, g.QuoteString(table), g.QuoteString(schema)), nil +} + +func (g *postgresGrammar) parseTable(table string) (string, string) { + parts := strings.Split(table, ".") + if len(parts) > 1 { + return parts[0], parts[1] + } + return "public", table +} + +func (g *postgresGrammar) CompileCreate(bp *blueprint.Blueprint) (string, error) { + columns, err := g.getColumns(bp) + if err != nil { + return "", err + } + columns = append(columns, g.getConstraints(bp)...) + + sql := fmt.Sprintf("CREATE TABLE %s (%s)", g.WrapTable(bp.Name, `"`), strings.Join(columns, ", ")) + return sql, nil +} + +func (g *postgresGrammar) CompileAdd(bp *blueprint.Blueprint) (string, error) { + columns, err := g.getColumns(bp) + if err != nil { + return "", err + } + + for i, col := range columns { + columns[i] = "ADD COLUMN " + col + } + + for _, constraint := range g.getConstraints(bp) { + columns = append(columns, "ADD "+constraint) + } + + if len(columns) == 0 { + return "", nil + } + + return fmt.Sprintf("ALTER TABLE %s %s", g.WrapTable(bp.Name, `"`), strings.Join(columns, ", ")), nil +} + +func (g *postgresGrammar) CompileChange(bp *blueprint.Blueprint, command *blueprint.Command) (string, error) { + col := command.Column + if col == nil || col.Name == "" { + return "", errors.New("column name cannot be empty") + } + var sqls []string + + // Type change + sqls = append(sqls, fmt.Sprintf("ALTER COLUMN %s TYPE %s", g.Wrap(col.Name, `"`), g.GetType(col))) + + // Nullability + if col.NullableVal != nil { + if *col.NullableVal { + sqls = append(sqls, fmt.Sprintf("ALTER COLUMN %s DROP NOT NULL", g.Wrap(col.Name, `"`))) + } else { + sqls = append(sqls, fmt.Sprintf("ALTER COLUMN %s SET NOT NULL", g.Wrap(col.Name, `"`))) + } + } + + // Default + if col.HasCommand("default") { + sqls = append( + sqls, + fmt.Sprintf("ALTER COLUMN %s SET DEFAULT %s", g.Wrap(col.Name, `"`), g.GetDefaultValue(col.DefaultValue)), + ) + } else if col.UseCurrentVal { + sqls = append(sqls, fmt.Sprintf("ALTER COLUMN %s SET DEFAULT CURRENT_TIMESTAMP", g.Wrap(col.Name, `"`))) + } + + return fmt.Sprintf("ALTER TABLE %s %s", g.WrapTable(bp.Name, `"`), strings.Join(sqls, ", ")), nil +} + +func (g *postgresGrammar) CompileRename(blueprint *blueprint.Blueprint, command *blueprint.Command) (string, error) { + return fmt.Sprintf( + "ALTER TABLE %s RENAME TO %s", + g.WrapTable(blueprint.Name, `"`), + g.WrapTable(command.To, `"`), + ), nil +} + +func (g *postgresGrammar) CompileDrop(blueprint *blueprint.Blueprint) (string, error) { + return fmt.Sprintf("DROP TABLE %s", g.WrapTable(blueprint.Name, `"`)), nil +} + +func (g *postgresGrammar) CompileDropIfExists(blueprint *blueprint.Blueprint) (string, error) { + return fmt.Sprintf("DROP TABLE IF EXISTS %s", g.WrapTable(blueprint.Name, `"`)), nil +} + +func (g *postgresGrammar) CompileDropColumn( + blueprint *blueprint.Blueprint, + command *blueprint.Command, +) (string, error) { + var dropped []string + for _, col := range command.Columns { + dropped = append(dropped, "DROP COLUMN "+g.Wrap(col, `"`)) + } + return fmt.Sprintf("ALTER TABLE %s %s", g.WrapTable(blueprint.Name, `"`), strings.Join(dropped, ", ")), nil +} + +func (g *postgresGrammar) CompileRenameColumn( + blueprint *blueprint.Blueprint, + command *blueprint.Command, +) (string, error) { + if command.From == "" || command.To == "" { + return "", errors.New("old and new column names cannot be empty") + } + return fmt.Sprintf( + "ALTER TABLE %s RENAME COLUMN %s TO %s", + g.WrapTable(blueprint.Name, `"`), + g.Wrap(command.From, `"`), + g.Wrap(command.To, `"`), + ), nil +} + +func (g *postgresGrammar) CompileIndex(blueprint *blueprint.Blueprint, command *blueprint.Command) (string, error) { + if len(command.Columns) == 0 || slices.Contains(command.Columns, "") { + return "", errors.New("index columns cannot be empty") + } + return g.compileKey(blueprint, command, "INDEX"), nil +} + +func (g *postgresGrammar) CompileUnique(blueprint *blueprint.Blueprint, command *blueprint.Command) (string, error) { + if len(command.Columns) == 0 || slices.Contains(command.Columns, "") { + return "", errors.New("unique index columns cannot be empty") + } + index := command.Index + if index == "" { + index = g.CreateIndexName(blueprint, "unique", command.Columns...) + } + + deferrable := "" + if command.Deferrable != nil { + if *command.Deferrable { + deferrable = " DEFERRABLE" + } else { + deferrable = " NOT DEFERRABLE" + } + } + + if (command.Deferrable == nil || *command.Deferrable) && command.InitiallyImmediate != nil { + if *command.InitiallyImmediate { + deferrable += " INITIALLY IMMEDIATE" + } else { + deferrable += " INITIALLY DEFERRED" + } + } + + return fmt.Sprintf("ALTER TABLE %s ADD CONSTRAINT %s UNIQUE (%s)%s", + g.WrapTable(blueprint.Name, `"`), + g.WrapIndexName(index, `"`), + g.WrapColumnize(command.Columns, `"`), + deferrable, + ), nil +} + +func (g *postgresGrammar) CompilePrimary(blueprint *blueprint.Blueprint, command *blueprint.Command) (string, error) { + if len(command.Columns) == 0 || slices.Contains(command.Columns, "") { + return "", errors.New("primary key columns cannot be empty") + } + index := command.Index + if index == "" { + index = g.CreateIndexName(blueprint, "primary", command.Columns...) + } + return fmt.Sprintf( + "ALTER TABLE %s ADD CONSTRAINT %s PRIMARY KEY (%s)", + g.WrapTable(blueprint.Name, `"`), + g.WrapIndexName(index, `"`), + g.WrapColumnize(command.Columns, `"`), + ), nil +} + +func (g *postgresGrammar) CompileFullText(blueprint *blueprint.Blueprint, command *blueprint.Command) (string, error) { + if len(command.Columns) == 0 || slices.Contains(command.Columns, "") { + return "", errors.New("fulltext index columns cannot be empty") + } + language := command.Language + if language == "" { + language = "english" + } + + columns := make([]string, len(command.Columns)) + for i, column := range command.Columns { + columns[i] = fmt.Sprintf("to_tsvector('%s', %s)", language, column) + } + + index := command.Index + if index == "" { + index = g.CreateIndexName(blueprint, "fulltext", command.Columns...) + } + + return fmt.Sprintf("CREATE INDEX %s ON %s USING GIN (%s)", + g.WrapIndexName(index, `"`), + g.WrapTable(blueprint.Name, `"`), + strings.Join(columns, " || "), + ), nil +} + +func (g *postgresGrammar) CompileDropIndex(_ *blueprint.Blueprint, command *blueprint.Command) (string, error) { + if command.Index == "" { + return "", errors.New("index name cannot be empty") + } + return fmt.Sprintf("DROP INDEX %s", g.WrapIndexName(command.Index, `"`)), nil +} + +func (g *postgresGrammar) CompileDropUnique( + blueprint *blueprint.Blueprint, + command *blueprint.Command, +) (string, error) { + if command.Index == "" { + return "", errors.New("index name cannot be empty") + } + return fmt.Sprintf( + "ALTER TABLE %s DROP CONSTRAINT %s", + g.WrapTable(blueprint.Name, `"`), + g.WrapIndexName(command.Index, `"`), + ), nil +} + +func (g *postgresGrammar) CompileDropFulltext( + blueprint *blueprint.Blueprint, + command *blueprint.Command, +) (string, error) { + if command.Index == "" { + return "", errors.New("index name cannot be empty") + } + return g.CompileDropIndex(blueprint, command) +} + +func (g *postgresGrammar) CompileDropPrimary( + blueprint *blueprint.Blueprint, + command *blueprint.Command, +) (string, error) { + index := command.Index + if index == "" { + index = blueprint.Name + "_primary" + } + return fmt.Sprintf( + "ALTER TABLE %s DROP CONSTRAINT %s", + g.WrapTable(blueprint.Name, `"`), + g.WrapIndexName(index, `"`), + ), nil +} + +func (g *postgresGrammar) CompileRenameIndex( + _ *blueprint.Blueprint, + command *blueprint.Command, +) (string, error) { + if command.From == "" || command.To == "" { + return "", errors.New("old and new index names cannot be empty") + } + return fmt.Sprintf( + "ALTER INDEX %s RENAME TO %s", + g.WrapIndexName(command.From, `"`), + g.WrapIndexName(command.To, `"`), + ), nil +} + +func (g *postgresGrammar) CompileForeign(blueprint *blueprint.Blueprint, command *blueprint.Command) (string, error) { + if len(command.Columns) == 0 || slices.Contains(command.Columns, "") || command.On == "" || + len(command.References) == 0 || slices.Contains(command.References, "") { + return "", errors.New("foreign key definition is incomplete: column, on, and references must be set") + } + onDelete := "" + if command.OnDelete != "" { + onDelete = fmt.Sprintf(" ON DELETE %s", command.OnDelete) + } + onUpdate := "" + if command.OnUpdate != "" { + onUpdate = fmt.Sprintf(" ON UPDATE %s", command.OnUpdate) + } + index := command.Index + if index == "" { + index = g.CreateForeignKeyName(blueprint, command) + } + + deferrable := "" + if command.Deferrable != nil { + if *command.Deferrable { + deferrable = " DEFERRABLE" + } else { + deferrable = " NOT DEFERRABLE" + } + } + + if (command.Deferrable == nil || *command.Deferrable) && command.InitiallyImmediate != nil { + if *command.InitiallyImmediate { + deferrable += " INITIALLY IMMEDIATE" + } else { + deferrable += " INITIALLY DEFERRED" + } + } + + return fmt.Sprintf("ALTER TABLE %s ADD CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s (%s)%s%s%s", + g.WrapTable(blueprint.Name, `"`), + g.WrapIndexName(index, `"`), + g.WrapColumnize(command.Columns, `"`), + g.WrapTable(command.On, `"`), + g.WrapColumnize(command.References, `"`), + onDelete, + onUpdate, + deferrable, + ), nil +} + +func (g *postgresGrammar) CompileDropForeign( + blueprint *blueprint.Blueprint, + command *blueprint.Command, +) (string, error) { + if command.Index == "" { + return "", errors.New("index name cannot be empty") + } + return fmt.Sprintf( + "ALTER TABLE %s DROP CONSTRAINT %s", + g.WrapTable(blueprint.Name, `"`), + g.WrapIndexName(command.Index, `"`), + ), nil +} + +func (g *postgresGrammar) GetFluentCommands() []func(blueprint *blueprint.Blueprint, command *blueprint.Command) string { + return []func(blueprint *blueprint.Blueprint, command *blueprint.Command) string{ + g.compileFluentComment, + } +} + +func (g *postgresGrammar) compileFluentComment(bp *blueprint.Blueprint, command *blueprint.Command) string { + col := command.Column + if col != nil && col.CommentVal != nil { + return fmt.Sprintf( + "COMMENT ON COLUMN %s.%s IS '%s'", + g.WrapTable(bp.Name, `"`), + g.Wrap(col.Name, `"`), + *col.CommentVal, + ) + } + return "" +} + +func (g *postgresGrammar) compileKey( + blueprint *blueprint.Blueprint, + command *blueprint.Command, + keyType string, +) string { + index := g.WrapIndexName(command.Index, `"`) + if index == "" { + index = g.WrapIndexName(g.CreateIndexName(blueprint, strings.ToLower(keyType), command.Columns...), `"`) + } + algorithm := "" + if command.Algorithm != "" { + algorithm = " USING " + command.Algorithm + } + return fmt.Sprintf( + "CREATE %s %s ON %s%s (%s)", + keyType, + index, + g.WrapTable(blueprint.Name, `"`), + algorithm, + g.WrapColumnize(command.Columns, `"`), + ) +} + +func (g *postgresGrammar) GetType(col *blueprint.Column) string { + if col.AutoIncrementVal != nil && *col.AutoIncrementVal { + switch col.ColumnType { + case blueprint.ColumnTypeInteger, blueprint.ColumnTypeMediumInteger: + return "SERIAL" + case blueprint.ColumnTypeSmallInteger, blueprint.ColumnTypeTinyInteger: + return "SMALLSERIAL" + case blueprint.ColumnTypeBigInteger: + return "BIGSERIAL" + } + } + typeMapFunc := map[string]func(*blueprint.Column) string{ + blueprint.ColumnTypeChar: g.typeChar, + blueprint.ColumnTypeString: g.typeString, + blueprint.ColumnTypeTinyText: g.typeTinyText, + blueprint.ColumnTypeText: g.typeText, + blueprint.ColumnTypeMediumText: g.typeText, + blueprint.ColumnTypeLongText: g.typeText, + blueprint.ColumnTypeInteger: g.typeInteger, + blueprint.ColumnTypeBigInteger: g.typeBigInteger, + blueprint.ColumnTypeMediumInteger: g.typeInteger, + blueprint.ColumnTypeSmallInteger: g.typeSmallInteger, + blueprint.ColumnTypeTinyInteger: g.typeSmallInteger, + blueprint.ColumnTypeFloat: g.typeFloat, + blueprint.ColumnTypeDouble: g.typeDouble, + blueprint.ColumnTypeDecimal: g.typeDecimal, + blueprint.ColumnTypeBoolean: g.typeBoolean, + blueprint.ColumnTypeEnum: g.typeEnum, + blueprint.ColumnTypeJSON: g.typeJSON, + blueprint.ColumnTypeJSONB: g.typeJSONB, + blueprint.ColumnTypeDate: g.typeDate, + blueprint.ColumnTypeDateTime: g.typeTimestamp, + blueprint.ColumnTypeDateTimeTz: g.typeTimestampTz, + blueprint.ColumnTypeTime: g.typeTime, + blueprint.ColumnTypeTimeTz: g.typeTimeTz, + blueprint.ColumnTypeTimestamp: g.typeTimestamp, + blueprint.ColumnTypeTimestampTz: g.typeTimestampTz, + blueprint.ColumnTypeYear: g.typeInteger, + blueprint.ColumnTypeBinary: g.typeBinary, + blueprint.ColumnTypeUUID: g.typeUUID, + blueprint.ColumnTypeULID: g.typeUUID, + blueprint.ColumnTypeIPAddress: g.typeIPAddress, + blueprint.ColumnTypeMacAddress: g.typeMacAddress, + blueprint.ColumnTypeTSVector: g.typeTSVector, + blueprint.ColumnTypeGeography: g.typeGeography, + blueprint.ColumnTypeGeometry: g.typeGeometry, + blueprint.ColumnTypePoint: g.typePoint, + } + if fn, ok := typeMapFunc[col.ColumnType]; ok { + return fn(col) + } + return col.ColumnType +} + +func (g *postgresGrammar) typeChar(col *blueprint.Column) string { + return fmt.Sprintf("CHAR(%d)", *col.Length) +} + +func (g *postgresGrammar) typeString(col *blueprint.Column) string { + return fmt.Sprintf("VARCHAR(%d)", *col.Length) +} + +func (g *postgresGrammar) typeText(_ *blueprint.Column) string { + return "TEXT" +} + +func (g *postgresGrammar) typeTinyText(_ *blueprint.Column) string { + return "VARCHAR(255)" +} + +func (g *postgresGrammar) typeInteger(_ *blueprint.Column) string { + return "INTEGER" +} + +func (g *postgresGrammar) typeBigInteger(_ *blueprint.Column) string { + return "BIGINT" +} + +func (g *postgresGrammar) typeSmallInteger(_ *blueprint.Column) string { + return "SMALLINT" +} + +func (g *postgresGrammar) typeFloat(_ *blueprint.Column) string { + return "REAL" +} + +func (g *postgresGrammar) typeDouble(_ *blueprint.Column) string { + return "DOUBLE PRECISION" +} + +func (g *postgresGrammar) typeDecimal(col *blueprint.Column) string { + return fmt.Sprintf("DECIMAL(%d, %d)", *col.Total, *col.Places) +} + +func (g *postgresGrammar) typeBoolean(_ *blueprint.Column) string { + return "BOOLEAN" +} + +func (g *postgresGrammar) typeEnum(col *blueprint.Column) string { + if len(col.Allowed) > 0 { + return fmt.Sprintf("VARCHAR(255) CHECK (%s IN ('%s'))", col.Name, strings.Join(col.Allowed, "', '")) + } + return "VARCHAR(255)" +} + +func (g *postgresGrammar) typeJSON(_ *blueprint.Column) string { + return "JSON" +} + +func (g *postgresGrammar) typeJSONB(_ *blueprint.Column) string { + return "JSONB" +} + +func (g *postgresGrammar) typeDate(_ *blueprint.Column) string { + return "DATE" +} + +func (g *postgresGrammar) typeTimestamp(col *blueprint.Column) string { + return g.typeDateTime(col) +} + +func (g *postgresGrammar) typeTimestampTz(col *blueprint.Column) string { + return g.typeDateTimeTz(col) +} + +func (g *postgresGrammar) typeDateTime(col *blueprint.Column) string { + if col.Precision != nil && *col.Precision > 0 { + return fmt.Sprintf("TIMESTAMP(%d)", *col.Precision) + } + return "TIMESTAMP(0)" +} + +func (g *postgresGrammar) typeDateTimeTz(col *blueprint.Column) string { + if col.Precision != nil && *col.Precision > 0 { + return fmt.Sprintf("TIMESTAMPTZ(%d)", *col.Precision) + } + return "TIMESTAMPTZ(0)" +} + +func (g *postgresGrammar) typeTime(col *blueprint.Column) string { + if col.Precision != nil && *col.Precision > 0 { + return fmt.Sprintf("TIME(%d)", *col.Precision) + } + return "TIME(0)" +} + +func (g *postgresGrammar) typeTimeTz(col *blueprint.Column) string { + if col.Precision != nil && *col.Precision > 0 { + return fmt.Sprintf("TIMETZ(%d)", *col.Precision) + } + return "TIMETZ(0)" +} + +func (g *postgresGrammar) typeBinary(_ *blueprint.Column) string { + return "BYTEA" +} + +func (g *postgresGrammar) typeUUID(_ *blueprint.Column) string { + return "UUID" +} + +func (g *postgresGrammar) typeIPAddress(_ *blueprint.Column) string { + return "inet" +} + +func (g *postgresGrammar) typeMacAddress(_ *blueprint.Column) string { + return "MACADDR" +} + +func (g *postgresGrammar) typeTSVector(_ *blueprint.Column) string { + return "TSVECTOR" +} + +func (g *postgresGrammar) typeGeography(col *blueprint.Column) string { + if col.Subtype != nil && *col.Subtype != "" { + return fmt.Sprintf("GEOGRAPHY(%s, %d)", strings.ToUpper(*col.Subtype), *col.Srid) + } + return "GEOGRAPHY" +} + +func (g *postgresGrammar) typeGeometry(col *blueprint.Column) string { + if col.Subtype != nil && *col.Subtype != "" { + if col.Srid != nil { + return fmt.Sprintf("GEOMETRY(%s, %d)", strings.ToUpper(*col.Subtype), *col.Srid) + } + return fmt.Sprintf("GEOMETRY(%s)", strings.ToUpper(*col.Subtype)) + } + return "GEOMETRY" +} + +func (g *postgresGrammar) typePoint(col *blueprint.Column) string { + if col.Srid != nil { + return fmt.Sprintf("POINT(%d)", *col.Srid) + } + return "POINT" +} + +func (g *postgresGrammar) getColumns(bp *blueprint.Blueprint) ([]string, error) { + var columns []string + for _, col := range bp.GetAddedColumns() { + if col.Name == "" { + return nil, errors.New("column name cannot be empty") + } + sql := g.Wrap(col.Name, `"`) + " " + g.GetType(col) + sql += g.modifiers(col) + columns = append(columns, sql) + } + return columns, nil +} + +func (g *postgresGrammar) getConstraints(bp *blueprint.Blueprint) []string { + var constraints []string + for _, col := range bp.GetAddedColumns() { + if col.PrimaryVal != nil && *col.PrimaryVal { + constraints = append( + constraints, + fmt.Sprintf( + "CONSTRAINT %s PRIMARY KEY (%s)", + g.WrapIndexName(g.CreateIndexName(bp, "primary", col.Name), `"`), + g.Wrap(col.Name, `"`), + ), + ) + } + } + return constraints +} + +func (g *postgresGrammar) modifiers(col *blueprint.Column) string { + var sql string + if col.NullableVal != nil { + if *col.NullableVal { + sql += " NULL" + } else { + sql += " NOT NULL" + } + } else { + sql += " NOT NULL" + } + if col.HasCommand("default") { + sql += fmt.Sprintf(" DEFAULT %s", g.GetDefaultValue(col.DefaultValue)) + } else if col.UseCurrentVal { + sql += " DEFAULT CURRENT_TIMESTAMP" + } + return sql +} diff --git a/schema/grammars/postgres_grammar_test.go b/schema/grammars/postgres_grammar_test.go new file mode 100644 index 0000000..a7864eb --- /dev/null +++ b/schema/grammars/postgres_grammar_test.go @@ -0,0 +1,1831 @@ +package grammars_test + +import ( + "testing" + + "github.com/akfaiz/migris/schema/blueprint" + "github.com/akfaiz/migris/schema/grammars" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestPgGrammar_CompileCreate(t *testing.T) { + g, err := grammars.NewGrammar("postgres") + require.NoError(t, err) + + tests := []struct { + name string + table string + blueprint func(table *blueprint.Blueprint) + want string + wantErr bool + }{ + { + name: "users", + blueprint: func(table *blueprint.Blueprint) { + table.ID() + table.String("name") + table.String("email") + table.String("password").Nullable() + table.Timestamp("created_at").UseCurrent() + table.Timestamp("updated_at").UseCurrent() + }, + want: "CREATE TABLE \"users\" (\"id\" BIGSERIAL NOT NULL, \"name\" VARCHAR(255) NOT NULL, \"email\" VARCHAR(255) NOT NULL, \"password\" VARCHAR(255) NULL, \"created_at\" TIMESTAMP(0) NOT NULL DEFAULT CURRENT_TIMESTAMP, \"updated_at\" TIMESTAMP(0) NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT \"users_id_primary\" PRIMARY KEY (\"id\"))", + }, + { + name: "posts", + blueprint: func(table *blueprint.Blueprint) { + table.ID() + table.Integer("user_id") + table.String("title") + table.Text("content").Nullable() + table.Foreign("user_id").References("id").On("users").OnDelete("CASCADE").OnUpdate("CASCADE") + }, + want: "CREATE TABLE \"posts\" (\"id\" BIGSERIAL NOT NULL, \"user_id\" INTEGER NOT NULL, \"title\" VARCHAR(255) NOT NULL, \"content\" TEXT NULL, CONSTRAINT \"posts_id_primary\" PRIMARY KEY (\"id\"))", + }, + { + name: "empty_column_table", + blueprint: func(table *blueprint.Blueprint) { + table.String("") // Intentionally empty column name + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tableName := tt.table + if tableName == "" && !tt.wantErr { + tableName = tt.name + } + bp := &blueprint.Blueprint{Name: tableName} + tt.blueprint(bp) + got, err := g.CompileCreate(bp) + if tt.wantErr { + require.Error(t, err) + return + } + + require.NoError(t, err) + assert.Equal(t, tt.want, got, "SQL statement mismatch for %s", tt.name) + }) + } +} + +func TestPgGrammar_CompileTableExists(t *testing.T) { + g, err := grammars.NewGrammar("postgres") + require.NoError(t, err) + + sql, err := g.CompileTableExists("", "public.users") + require.NoError(t, err) + assert.Equal( + t, + "SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'users'", + sql, + ) +} + +func TestPgGrammar_CompileTables(t *testing.T) { + g, err := grammars.NewGrammar("postgres") + require.NoError(t, err) + + sql, err := g.CompileTables("") + require.NoError(t, err) + assert.Contains(t, sql, "SELECT \n\t\t\tt.table_name") +} + +func TestPgGrammar_CompileColumns(t *testing.T) { + g, err := grammars.NewGrammar("postgres") + require.NoError(t, err) + + sql, err := g.CompileColumns("", "public.users") + require.NoError(t, err) + assert.Contains(t, sql, "cols.table_schema = 'public' \n\t\t\tAND cols.table_name = 'users'") +} + +func TestPgGrammar_CompileIndexes(t *testing.T) { + g, err := grammars.NewGrammar("postgres") + require.NoError(t, err) + + sql, err := g.CompileIndexes("", "public.users") + require.NoError(t, err) + assert.Contains(t, sql, "t.relname = 'users'\n\t\t\tAND n.nspname = 'public'") +} + +func TestPgGrammar_CompileAdd(t *testing.T) { + g, err := grammars.NewGrammar("postgres") + require.NoError(t, err) + + tests := []struct { + name string + table string + blueprint func(table *blueprint.Blueprint) + want string + wantErr bool + }{ + { + name: "users", + blueprint: func(table *blueprint.Blueprint) { + table.String("phone", 20) + }, + want: "ALTER TABLE \"users\" ADD COLUMN \"phone\" VARCHAR(20) NOT NULL", + wantErr: false, + }, + { + name: "users", + blueprint: func(table *blueprint.Blueprint) { + table.String("phone", 20) + table.String("address", 255).Nullable() + table.Integer("age") + }, + want: "ALTER TABLE \"users\" ADD COLUMN \"phone\" VARCHAR(20) NOT NULL, ADD COLUMN \"address\" VARCHAR(255) NULL, ADD COLUMN \"age\" INTEGER NOT NULL", + wantErr: false, + }, + { + name: "users", + blueprint: func(table *blueprint.Blueprint) { + table.Boolean("active").Default(true) + }, + want: "ALTER TABLE \"users\" ADD COLUMN \"active\" BOOLEAN NOT NULL DEFAULT '1'", + wantErr: false, + }, + { + name: "users", + blueprint: func(table *blueprint.Blueprint) { + table.String("notes", 500).Comment("User notes") + }, + want: "ALTER TABLE \"users\" ADD COLUMN \"notes\" VARCHAR(500) NOT NULL", + wantErr: false, + }, + { + name: "categories", + blueprint: func(table *blueprint.Blueprint) { + table.Integer("id").Primary() + }, + want: "ALTER TABLE \"categories\" ADD COLUMN \"id\" INTEGER NOT NULL, ADD CONSTRAINT \"categories_id_primary\" PRIMARY KEY (\"id\")", + wantErr: false, + }, + { + name: "logs", + blueprint: func(table *blueprint.Blueprint) { + table.BigInteger("id").AutoIncrement() + }, + want: "ALTER TABLE \"logs\" ADD COLUMN \"id\" BIGSERIAL NOT NULL", + wantErr: false, + }, + { + name: "products", + blueprint: func(table *blueprint.Blueprint) { + table.Decimal("price", 10, 2).Default(0) + }, + want: "ALTER TABLE \"products\" ADD COLUMN \"price\" DECIMAL(10, 2) NOT NULL DEFAULT '0'", + wantErr: false, + }, + { + name: "orders", + blueprint: func(table *blueprint.Blueprint) { + table.Timestamp("created_at").UseCurrent() + table.Timestamp("updated_at").UseCurrent().Nullable() + }, + want: "ALTER TABLE \"orders\" ADD COLUMN \"created_at\" TIMESTAMP(0) NOT NULL DEFAULT CURRENT_TIMESTAMP, ADD COLUMN \"updated_at\" TIMESTAMP(0) NULL DEFAULT CURRENT_TIMESTAMP", + wantErr: false, + }, + { + name: "mixed_table", + blueprint: func(table *blueprint.Blueprint) { + table.Text("description") + table.JSON("metadata").Nullable() + table.UUID("reference_id") + table.Date("event_date") + }, + want: "ALTER TABLE \"mixed_table\" ADD COLUMN \"description\" TEXT NOT NULL, ADD COLUMN \"metadata\" JSON NULL, ADD COLUMN \"reference_id\" UUID NOT NULL, ADD COLUMN \"event_date\" DATE NOT NULL", + wantErr: false, + }, + { + name: "No columns to add", + table: "users", + blueprint: func(_ *blueprint.Blueprint) {}, + want: "", + wantErr: false, + }, + { + name: "users", + blueprint: func(table *blueprint.Blueprint) { + table.String("", 255) // Intentionally empty column name + }, + wantErr: true, + }, + { + name: "users", + blueprint: func(table *blueprint.Blueprint) { + table.Enum("status", []string{"active", "inactive", "pending"}) + }, + want: "ALTER TABLE \"users\" ADD COLUMN \"status\" VARCHAR(255) CHECK (status IN ('active', 'inactive', 'pending')) NOT NULL", + wantErr: false, + }, + { + name: "locations", + blueprint: func(table *blueprint.Blueprint) { + table.Geography("coordinates", "POINT", 4326) + }, + want: "ALTER TABLE \"locations\" ADD COLUMN \"coordinates\" GEOGRAPHY(POINT, 4326) NOT NULL", + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tableName := tt.table + if tableName == "" && !tt.wantErr { + tableName = tt.name + } + bp := &blueprint.Blueprint{Name: tableName} + tt.blueprint(bp) + got, err := g.CompileAdd(bp) + if tt.wantErr { + require.Error(t, err) + return + } + + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestPgGrammar_CompileChange(t *testing.T) { + g, err := grammars.NewGrammar("postgres") + require.NoError(t, err) + + tests := []struct { + name string + table string + blueprint func(blueprint *blueprint.Blueprint) + want []string + wantErr bool + }{ + { + name: "users", + blueprint: func(table *blueprint.Blueprint) { + table.String("email", 500).Nullable().Change() + }, + want: []string{ + "ALTER TABLE \"users\" ALTER COLUMN \"email\" TYPE VARCHAR(500), ALTER COLUMN \"email\" DROP NOT NULL", + }, + }, + { + name: "users", + blueprint: func(table *blueprint.Blueprint) { + table.String("email", 500).Default("user@mail.com").Change() + }, + want: []string{ + "ALTER TABLE \"users\" ALTER COLUMN \"email\" TYPE VARCHAR(500), ALTER COLUMN \"email\" SET DEFAULT 'user@mail.com'", + }, + }, + { + name: "users", + blueprint: func(table *blueprint.Blueprint) { + table.String("email", 500).Nullable().Change() + table.String("name", 255).Default("Anonymous").Change() + }, + want: []string{ + "ALTER TABLE \"users\" ALTER COLUMN \"email\" TYPE VARCHAR(500), ALTER COLUMN \"email\" DROP NOT NULL", + "ALTER TABLE \"users\" ALTER COLUMN \"name\" TYPE VARCHAR(255), ALTER COLUMN \"name\" SET DEFAULT 'Anonymous'", + }, + }, + { + name: "users", + blueprint: func(table *blueprint.Blueprint) { + table.String("email", 500).Default(nil).Change() + }, + want: []string{ + "ALTER TABLE \"users\" ALTER COLUMN \"email\" TYPE VARCHAR(500), ALTER COLUMN \"email\" SET DEFAULT NULL", + }, + }, + { + name: "users", + blueprint: func(table *blueprint.Blueprint) { + table.String("email", 500).Comment("User email address").Change() + }, + want: []string{ + "ALTER TABLE \"users\" ALTER COLUMN \"email\" TYPE VARCHAR(500)", + "COMMENT ON COLUMN \"users\".\"email\" IS 'User email address'", + }, + }, + { + name: "users", + blueprint: func(table *blueprint.Blueprint) { + table.String("email", 500).Comment("").Change() + }, + want: []string{ + "ALTER TABLE \"users\" ALTER COLUMN \"email\" TYPE VARCHAR(500)", + "COMMENT ON COLUMN \"users\".\"email\" IS ''", + }, + }, + { + name: "users", + blueprint: func(table *blueprint.Blueprint) { + table.String("email", 500).Nullable(false).Change() + }, + want: []string{ + "ALTER TABLE \"users\" ALTER COLUMN \"email\" TYPE VARCHAR(500), ALTER COLUMN \"email\" SET NOT NULL", + }, + }, + { + name: "users", + blueprint: func(table *blueprint.Blueprint) { + table.String("", 255).Change() // Intentionally empty column name + }, + wantErr: true, + }, + { + name: "No changes", + table: "users", + blueprint: func(_ *blueprint.Blueprint) {}, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tableName := tt.table + if tableName == "" && !tt.wantErr { + tableName = tt.name + } + bp := &blueprint.Blueprint{Name: tableName, Grammar: g} + tt.blueprint(bp) + got, err := bp.ToSQL() + if tt.wantErr { + require.Error(t, err) + return + } + + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestPgGrammar_CompileDrop(t *testing.T) { + g, err := grammars.NewGrammar("postgres") + require.NoError(t, err) + + tests := []struct { + name string + table string + want string + wantErr bool + }{ + { + name: "Drop table", + table: "users", + want: "DROP TABLE \"users\"", + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tableName := tt.table + if tableName == "" && !tt.wantErr { + tableName = tt.name + } + bp := &blueprint.Blueprint{Name: tableName, Grammar: g} + got, err := g.CompileDrop(bp) + if tt.wantErr { + require.Error(t, err) + return + } + + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestPgGrammar_CompileDropIfExists(t *testing.T) { + g, err := grammars.NewGrammar("postgres") + require.NoError(t, err) + + tests := []struct { + name string + table string + want string + wantErr bool + }{ + { + name: "Drop table if exists", + table: "users", + want: "DROP TABLE IF EXISTS \"users\"", + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tableName := tt.table + if tableName == "" && !tt.wantErr { + tableName = tt.name + } + bp := &blueprint.Blueprint{Name: tableName, Grammar: g} + got, err := g.CompileDropIfExists(bp) + if tt.wantErr { + require.Error(t, err) + return + } + + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestPgGrammar_CompileRename(t *testing.T) { + g, err := grammars.NewGrammar("postgres") + require.NoError(t, err) + + tests := []struct { + name string + oldName string + newName string + want string + wantErr bool + }{ + { + name: "Rename table", + oldName: "users", + newName: "people", + want: "ALTER TABLE \"users\" RENAME TO \"people\"", + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bp := &blueprint.Blueprint{Name: tt.oldName, Grammar: g} + bp.Rename(tt.newName) + got, err := g.CompileRename(bp, bp.Commands[0]) + if tt.wantErr { + require.Error(t, err) + return + } + + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestPgGrammar_CompileDropColumn(t *testing.T) { + g, err := grammars.NewGrammar("postgres") + require.NoError(t, err) + + tests := []struct { + name string + table string + blueprint func(table *blueprint.Blueprint) + wants []string + wantErr bool + }{ + { + name: "users", + blueprint: func(table *blueprint.Blueprint) { + table.DropColumn("email") + }, + wants: []string{"ALTER TABLE \"users\" DROP COLUMN \"email\""}, + wantErr: false, + }, + { + name: "users", + blueprint: func(table *blueprint.Blueprint) { + table.DropColumn("email", "phone") + table.DropColumn("address") + }, + wants: []string{ + "ALTER TABLE \"users\" DROP COLUMN \"email\", DROP COLUMN \"phone\"", + "ALTER TABLE \"users\" DROP COLUMN \"address\"", + }, + wantErr: false, + }, + { + name: "No columns to drop", + table: "users", + blueprint: func(_ *blueprint.Blueprint) {}, + wants: nil, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tableName := tt.table + if tableName == "" && !tt.wantErr { + tableName = tt.name + } + bp := &blueprint.Blueprint{Name: tableName, Grammar: g} + tt.blueprint(bp) + got, err := bp.ToSQL() + if tt.wantErr { + require.Error(t, err) + return + } + + require.NoError(t, err) + assert.Equal(t, tt.wants, got) + }) + } +} + +func TestPgGrammar_CompileRenameColumn(t *testing.T) { + g, err := grammars.NewGrammar("postgres") + require.NoError(t, err) + + tests := []struct { + name string + table string + oldName string + newName string + want string + wantErr bool + }{ + { + name: "Rename column", + table: "users", + oldName: "email", + newName: "user_email", + want: "ALTER TABLE \"users\" RENAME COLUMN \"email\" TO \"user_email\"", + wantErr: false, + }, + { + name: "Empty old name", + table: "users", + oldName: "", + newName: "user_email", + wantErr: true, + }, + { + name: "Empty new name", + table: "users", + oldName: "email", + newName: "", + wantErr: true, + }, + { + name: "Both names empty", + table: "users", + oldName: "", + newName: "", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tableName := tt.table + if tableName == "" && !tt.wantErr { + tableName = tt.name + } + bp := &blueprint.Blueprint{Name: tableName, Grammar: g} + command := &blueprint.Command{From: tt.oldName, To: tt.newName} + got, err := g.CompileRenameColumn(bp, command) + if tt.wantErr { + require.Error(t, err) + return + } + + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestPgGrammar_CompileDropIndex(t *testing.T) { + g, err := grammars.NewGrammar("postgres") + require.NoError(t, err) + + tests := []struct { + name string + indexName string + want string + wantErr bool + }{ + { + name: "Drop index with valid name", + indexName: "users_email_index", + want: "DROP INDEX \"users_email_index\"", + wantErr: false, + }, + { + name: "Drop index with complex name", + indexName: "idx_users_email_name", + want: "DROP INDEX \"idx_users_email_name\"", + wantErr: false, + }, + { + name: "Empty index name", + indexName: "", + want: "", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bp := &blueprint.Blueprint{Grammar: g} + command := &blueprint.Command{Index: tt.indexName} + got, err := g.CompileDropIndex(bp, command) + if tt.wantErr { + require.Error(t, err) + assert.Empty(t, got) + return + } + + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestPgGrammar_CompileDropPrimary(t *testing.T) { + g, err := grammars.NewGrammar("postgres") + require.NoError(t, err) + + tests := []struct { + name string + blueprint *blueprint.Blueprint + indexName string + want string + wantErr bool + }{ + { + name: "Drop primary key with specified name", + blueprint: func() *blueprint.Blueprint { + return &blueprint.Blueprint{Name: "users", Grammar: g} + }(), + indexName: "users_pkey", + want: "ALTER TABLE \"users\" DROP CONSTRAINT \"users_pkey\"", + wantErr: false, + }, + { + name: "Drop primary key with empty index name (should use default naming)", + blueprint: func() *blueprint.Blueprint { + return &blueprint.Blueprint{Name: "posts", Grammar: g} + }(), + indexName: "", + want: "ALTER TABLE \"posts\" DROP CONSTRAINT \"posts_primary\"", + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + command := &blueprint.Command{Index: tt.indexName} + got, err := g.CompileDropPrimary(tt.blueprint, command) + if tt.wantErr { + require.Error(t, err) + return + } + + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestPgGrammar_CompileRenameIndex(t *testing.T) { + g, err := grammars.NewGrammar("postgres") + require.NoError(t, err) + + tests := []struct { + name string + table string + oldName string + newName string + want string + wantErr bool + }{ + { + name: "Rename index with valid names", + table: "users", + oldName: "users_email_index", + newName: "users_email_unique", + want: "ALTER INDEX \"users_email_index\" RENAME TO \"users_email_unique\"", + wantErr: false, + }, + { + name: "Rename index with complex names", + table: "users", + oldName: "idx_users_email_name", + newName: "idx_users_email_name_unique", + want: "ALTER INDEX \"idx_users_email_name\" RENAME TO \"idx_users_email_name_unique\"", + wantErr: false, + }, + { + name: "Empty old name", + table: "users", + oldName: "", + newName: "users_email_unique", + wantErr: true, + }, + { + name: "Empty new name", + table: "users", + oldName: "users_email_index", + newName: "", + wantErr: true, + }, + { + name: "Both names empty", + table: "users", + oldName: "", + newName: "", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tableName := tt.table + if tableName == "" && !tt.wantErr { + tableName = tt.name + } + bp := &blueprint.Blueprint{Name: tableName, Grammar: g} + command := &blueprint.Command{From: tt.oldName, To: tt.newName} + got, err := g.CompileRenameIndex(bp, command) + if tt.wantErr { + require.Error(t, err) + return + } + + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestPgGrammar_CompileForeign(t *testing.T) { + g, err := grammars.NewGrammar("postgres") + require.NoError(t, err) + + tests := []struct { + name string + table string + blueprint func(table *blueprint.Blueprint) + want string + wantErr bool + }{ + { + name: "posts", + blueprint: func(table *blueprint.Blueprint) { + table.Foreign("user_id").References("id").On("users") + }, + want: "ALTER TABLE \"posts\" ADD CONSTRAINT \"posts_user_id_foreign\" FOREIGN KEY (\"user_id\") REFERENCES \"users\" (\"id\")", + wantErr: false, + }, + { + name: "orders", + blueprint: func(table *blueprint.Blueprint) { + table.Foreign("customer_id").References("id").On("customers").Name("fk_orders_customers") + }, + want: "ALTER TABLE \"orders\" ADD CONSTRAINT \"fk_orders_customers\" FOREIGN KEY (\"customer_id\") REFERENCES \"customers\" (\"id\")", + wantErr: false, + }, + { + name: "comments", + blueprint: func(table *blueprint.Blueprint) { + table.Foreign("post_id").References("id").On("posts").CascadeOnDelete() + }, + want: "ALTER TABLE \"comments\" ADD CONSTRAINT \"comments_post_id_foreign\" FOREIGN KEY (\"post_id\") REFERENCES \"posts\" (\"id\") ON DELETE CASCADE", + wantErr: false, + }, + { + name: "orders", + blueprint: func(table *blueprint.Blueprint) { + table.Foreign("customer_id").References("id").On("customers").NullOnUpdate() + }, + want: "ALTER TABLE \"orders\" ADD CONSTRAINT \"orders_customer_id_foreign\" FOREIGN KEY (\"customer_id\") REFERENCES \"customers\" (\"id\") ON UPDATE SET NULL", + wantErr: false, + }, + { + name: "order_items", + blueprint: func(table *blueprint.Blueprint) { + table.Foreign("order_id").References("id").On("orders").CascadeOnDelete().RestrictOnUpdate() + }, + want: "ALTER TABLE \"order_items\" ADD CONSTRAINT \"order_items_order_id_foreign\" FOREIGN KEY (\"order_id\") REFERENCES \"orders\" (\"id\") ON DELETE CASCADE ON UPDATE RESTRICT", + wantErr: false, + }, + { + name: "posts", + blueprint: func(table *blueprint.Blueprint) { + table.Foreign("user_id").References("id").On("users").Deferrable(true) + }, + want: "ALTER TABLE \"posts\" ADD CONSTRAINT \"posts_user_id_foreign\" FOREIGN KEY (\"user_id\") REFERENCES \"users\" (\"id\") DEFERRABLE", + wantErr: false, + }, + { + name: "posts", + blueprint: func(table *blueprint.Blueprint) { + table.Foreign("user_id").References("id").On("users").Deferrable(false) + }, + want: "ALTER TABLE \"posts\" ADD CONSTRAINT \"posts_user_id_foreign\" FOREIGN KEY (\"user_id\") REFERENCES \"users\" (\"id\") NOT DEFERRABLE", + wantErr: false, + }, + { + name: "posts", + blueprint: func(table *blueprint.Blueprint) { + table.Foreign("user_id").References("id").On("users").Deferrable().InitiallyImmediate(true) + }, + want: "ALTER TABLE \"posts\" ADD CONSTRAINT \"posts_user_id_foreign\" FOREIGN KEY (\"user_id\") REFERENCES \"users\" (\"id\") DEFERRABLE INITIALLY IMMEDIATE", + wantErr: false, + }, + { + name: "posts", + blueprint: func(table *blueprint.Blueprint) { + table.Foreign("user_id").References("id").On("users").Deferrable().InitiallyImmediate(false) + }, + want: "ALTER TABLE \"posts\" ADD CONSTRAINT \"posts_user_id_foreign\" FOREIGN KEY (\"user_id\") REFERENCES \"users\" (\"id\") DEFERRABLE INITIALLY DEFERRED", + wantErr: false, + }, + { + name: "posts", + blueprint: func(table *blueprint.Blueprint) { + table.Foreign("user_id").References("id").On("users").Deferrable(false).InitiallyImmediate(true) + }, + want: "ALTER TABLE \"posts\" ADD CONSTRAINT \"posts_user_id_foreign\" FOREIGN KEY (\"user_id\") REFERENCES \"users\" (\"id\") NOT DEFERRABLE", + wantErr: false, + }, + { + name: "user_roles", + blueprint: func(table *blueprint.Blueprint) { + table.Foreign("role_id").References("id").On("roles"). + CascadeOnDelete().RestrictOnUpdate(). + Deferrable().InitiallyImmediate(true) + }, + want: "ALTER TABLE \"user_roles\" ADD CONSTRAINT \"user_roles_role_id_foreign\" FOREIGN KEY (\"role_id\") REFERENCES \"roles\" (\"id\") ON DELETE CASCADE ON UPDATE RESTRICT DEFERRABLE INITIALLY IMMEDIATE", + wantErr: false, + }, + { + name: "posts", + blueprint: func(table *blueprint.Blueprint) { + table.Foreign("").References("id").On("users") + }, + wantErr: true, + }, + { + name: "posts", + blueprint: func(table *blueprint.Blueprint) { + table.Foreign("user_id").References("id").On("") + }, + wantErr: true, + }, + { + name: "posts", + blueprint: func(table *blueprint.Blueprint) { + table.Foreign("user_id").References("").On("users") + }, + wantErr: true, + }, + { + name: "posts", + blueprint: func(table *blueprint.Blueprint) { + table.Foreign("").References("").On("") + }, + wantErr: true, + }, + { + name: "invoices", + blueprint: func(table *blueprint.Blueprint) { + table.Foreign("customer_id").References("id").On("customers").RestrictOnDelete().RestrictOnUpdate() + }, + want: "ALTER TABLE \"invoices\" ADD CONSTRAINT \"invoices_customer_id_foreign\" FOREIGN KEY (\"customer_id\") REFERENCES \"customers\" (\"id\") ON DELETE RESTRICT ON UPDATE RESTRICT", + wantErr: false, + }, + { + name: "payments", + blueprint: func(table *blueprint.Blueprint) { + table.Foreign("invoice_id").References("id").On("invoices").NoActionOnDelete().NoActionOnUpdate() + }, + want: "ALTER TABLE \"payments\" ADD CONSTRAINT \"payments_invoice_id_foreign\" FOREIGN KEY (\"invoice_id\") REFERENCES \"invoices\" (\"id\") ON DELETE NO ACTION ON UPDATE NO ACTION", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tableName := tt.table + if tableName == "" && !tt.wantErr { + tableName = tt.name + } + bp := &blueprint.Blueprint{Name: tableName} + tt.blueprint(bp) + got, err := g.CompileForeign(bp, bp.Commands[0]) + if tt.wantErr { + require.Error(t, err) + return + } + + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestPgGrammar_CompileDropForeign(t *testing.T) { + g, err := grammars.NewGrammar("postgres") + require.NoError(t, err) + + tests := []struct { + name string + table string + foreignKeyName string + want string + wantErr bool + }{ + { + name: "Drop foreign key with valid name", + table: "posts", + foreignKeyName: "fk_posts_users", + want: "ALTER TABLE \"posts\" DROP CONSTRAINT \"fk_posts_users\"", + wantErr: false, + }, + { + name: "Drop foreign key with complex name", + table: "order_items", + foreignKeyName: "fk_order_items_products_cascade", + want: "ALTER TABLE \"order_items\" DROP CONSTRAINT \"fk_order_items_products_cascade\"", + wantErr: false, + }, + { + name: "Empty foreign key name", + table: "users", + foreignKeyName: "", + want: "", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tableName := tt.table + if tableName == "" && !tt.wantErr { + tableName = tt.name + } + bp := &blueprint.Blueprint{Name: tableName} + command := &blueprint.Command{Index: tt.foreignKeyName} + got, err := g.CompileDropForeign(bp, command) + if tt.wantErr { + require.Error(t, err) + return + } + + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestPgGrammar_CompileIndex(t *testing.T) { + g, err := grammars.NewGrammar("postgres") + require.NoError(t, err) + + tests := []struct { + name string + table string + blueprint func(table *blueprint.Blueprint) + want string + wantErr bool + }{ + { + name: "users", + blueprint: func(table *blueprint.Blueprint) { + table.Index("email").Name("users_email_index") + }, + want: "CREATE INDEX \"users_email_index\" ON \"users\" (\"email\")", + wantErr: false, + }, + { + name: "users", + blueprint: func(table *blueprint.Blueprint) { + table.Index("name", "email").Name("users_name_email_index") + }, + want: "CREATE INDEX \"users_name_email_index\" ON \"users\" (\"name\", \"email\")", + wantErr: false, + }, + { + name: "products", + blueprint: func(table *blueprint.Blueprint) { + table.Index("sku").Name("products_sku_index").Algorithm("btree") + }, + want: "CREATE INDEX \"products_sku_index\" ON \"products\" USING btree (\"sku\")", + wantErr: false, + }, + { + name: "orders", + blueprint: func(table *blueprint.Blueprint) { + table.Index("sku") + }, + want: "CREATE INDEX \"orders_sku_index\" ON \"orders\" (\"sku\")", + wantErr: false, + }, + { + name: "users", + blueprint: func(table *blueprint.Blueprint) { + table.Index("name", "", "email").Name("users_invalid_index") + }, + wantErr: true, + }, + { + name: "users", + blueprint: func(table *blueprint.Blueprint) { + table.Index("").Name("users_empty_index") + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tableName := tt.table + if tableName == "" && !tt.wantErr { + tableName = tt.name + } + bp := &blueprint.Blueprint{Name: tableName} + tt.blueprint(bp) + got, err := g.CompileIndex(bp, bp.Commands[0]) + if tt.wantErr { + require.Error(t, err) + return + } + + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestPgGrammar_CompileUnique(t *testing.T) { + g, err := grammars.NewGrammar("postgres") + require.NoError(t, err) + + tests := []struct { + name string + table string + blueprint func(table *blueprint.Blueprint) + want string + wantErr bool + }{ + { + name: "users", + blueprint: func(table *blueprint.Blueprint) { + table.Unique("email").Name("users_email_unique") + }, + want: "ALTER TABLE \"users\" ADD CONSTRAINT \"users_email_unique\" UNIQUE (\"email\")", + wantErr: false, + }, + { + name: "users", + blueprint: func(table *blueprint.Blueprint) { + table.Unique("name", "email").Name("users_name_email_unique") + }, + want: "ALTER TABLE \"users\" ADD CONSTRAINT \"users_name_email_unique\" UNIQUE (\"name\", \"email\")", + wantErr: false, + }, + { + name: "orders", + blueprint: func(table *blueprint.Blueprint) { + table.Unique("order_number") + }, + want: "ALTER TABLE \"orders\" ADD CONSTRAINT \"orders_order_number_unique\" UNIQUE (\"order_number\")", + wantErr: false, + }, + { + name: "users", + blueprint: func(table *blueprint.Blueprint) { + table.Unique("email").Name("users_email_unique").Deferrable(true) + }, + want: "ALTER TABLE \"users\" ADD CONSTRAINT \"users_email_unique\" UNIQUE (\"email\") DEFERRABLE", + wantErr: false, + }, + { + name: "users", + blueprint: func(table *blueprint.Blueprint) { + table.Unique("email").Name("users_email_unique").Deferrable(false) + }, + want: "ALTER TABLE \"users\" ADD CONSTRAINT \"users_email_unique\" UNIQUE (\"email\") NOT DEFERRABLE", + wantErr: false, + }, + { + name: "users", + blueprint: func(table *blueprint.Blueprint) { + table.Unique("email").Name("users_email_unique").Deferrable().InitiallyImmediate(true) + }, + want: "ALTER TABLE \"users\" ADD CONSTRAINT \"users_email_unique\" UNIQUE (\"email\") DEFERRABLE INITIALLY IMMEDIATE", + wantErr: false, + }, + { + name: "users", + blueprint: func(table *blueprint.Blueprint) { + table.Unique("email").Name("users_email_unique").Deferrable().InitiallyImmediate(false) + }, + want: "ALTER TABLE \"users\" ADD CONSTRAINT \"users_email_unique\" UNIQUE (\"email\") DEFERRABLE INITIALLY DEFERRED", + wantErr: false, + }, + { + name: "users", + blueprint: func(table *blueprint.Blueprint) { + table.Unique("email").Name("users_email_unique").Deferrable(false).InitiallyImmediate(true) + }, + want: "ALTER TABLE \"users\" ADD CONSTRAINT \"users_email_unique\" UNIQUE (\"email\") NOT DEFERRABLE", + wantErr: false, + }, + { + name: "users", + blueprint: func(table *blueprint.Blueprint) { + table.Unique("name", "", "email").Name("users_invalid_unique") + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tableName := tt.table + if tableName == "" && !tt.wantErr { + tableName = tt.name + } + bp := &blueprint.Blueprint{Name: tableName} + tt.blueprint(bp) + got, err := g.CompileUnique(bp, bp.Commands[0]) + if tt.wantErr { + require.Error(t, err) + return + } + + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestPgGrammar_CompileFullText(t *testing.T) { + g, err := grammars.NewGrammar("postgres") + require.NoError(t, err) + + tests := []struct { + name string + table string + blueprint func(table *blueprint.Blueprint) + want string + wantErr bool + }{ + { + name: "articles", + blueprint: func(table *blueprint.Blueprint) { + table.FullText("title").Name("articles_title_fulltext").Language("english") + }, + want: "CREATE INDEX \"articles_title_fulltext\" ON \"articles\" USING GIN (to_tsvector('english', title))", + wantErr: false, + }, + { + name: "documents", + blueprint: func(table *blueprint.Blueprint) { + table.FullText("title", "content").Name("documents_title_content_fulltext").Language("english") + }, + want: "CREATE INDEX \"documents_title_content_fulltext\" ON \"documents\" USING GIN (to_tsvector('english', title) || to_tsvector('english', content))", + wantErr: false, + }, + { + name: "posts", + blueprint: func(table *blueprint.Blueprint) { + table.FullText("content").Name("posts_content_spanish_fulltext").Language("spanish") + }, + want: "CREATE INDEX \"posts_content_spanish_fulltext\" ON \"posts\" USING GIN (to_tsvector('spanish', content))", + wantErr: false, + }, + { + name: "blogs", + blueprint: func(table *blueprint.Blueprint) { + table.FullText("body").Name("blogs_body_fulltext") + }, + want: "CREATE INDEX \"blogs_body_fulltext\" ON \"blogs\" USING GIN (to_tsvector('english', body))", + wantErr: false, + }, + { + name: "news", + blueprint: func(table *blueprint.Blueprint) { + table.FullText("headline") + }, + want: "CREATE INDEX \"news_headline_fulltext\" ON \"news\" USING GIN (to_tsvector('english', headline))", + wantErr: false, + }, + { + name: "products", + blueprint: func(table *blueprint.Blueprint) { + table.FullText("name", "description", "tags").Name("products_search_fulltext").Language("english") + }, + want: "CREATE INDEX \"products_search_fulltext\" ON \"products\" USING GIN (to_tsvector('english', name) || to_tsvector('english', description) || to_tsvector('english', tags))", + wantErr: false, + }, + { + name: "articles", + blueprint: func(table *blueprint.Blueprint) { + table.FullText("title", "", "content").Name("articles_invalid_fulltext") + }, + wantErr: true, + }, + { + name: "articles", + blueprint: func(table *blueprint.Blueprint) { + table.FullText("").Name("articles_empty_fulltext") + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tableName := tt.table + if tableName == "" && !tt.wantErr { + tableName = tt.name + } + bp := &blueprint.Blueprint{Name: tableName} + tt.blueprint(bp) + got, err := g.CompileFullText(bp, bp.Commands[0]) + if tt.wantErr { + require.Error(t, err) + return + } + + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestPgGrammar_CompileDropUnique(t *testing.T) { + g, err := grammars.NewGrammar("postgres") + require.NoError(t, err) + + tests := []struct { + name string + indexName string + want string + wantErr bool + }{ + { + name: "Drop unique index with valid name", + indexName: "users_email_unique", + want: "ALTER TABLE DROP CONSTRAINT \"users_email_unique\"", + wantErr: false, + }, + { + name: "Drop unique index with complex name", + indexName: "uk_users_email_name", + want: "ALTER TABLE DROP CONSTRAINT \"uk_users_email_name\"", + wantErr: false, + }, + { + name: "Drop unique index with numeric suffix", + indexName: "users_email_unique_2", + want: "ALTER TABLE DROP CONSTRAINT \"users_email_unique_2\"", + wantErr: false, + }, + { + name: "Empty index name", + indexName: "", + want: "", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bp := &blueprint.Blueprint{} + command := &blueprint.Command{Index: tt.indexName} + got, err := g.CompileDropUnique(bp, command) + if tt.wantErr { + require.Error(t, err) + assert.Empty(t, got) + return + } + + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestPgGrammar_CompileDropFulltext(t *testing.T) { + g, err := grammars.NewGrammar("postgres") + require.NoError(t, err) + + tests := []struct { + name string + indexName string + want string + wantErr bool + }{ + { + name: "Drop fulltext index with valid name", + indexName: "articles_title_fulltext", + want: "DROP INDEX \"articles_title_fulltext\"", + wantErr: false, + }, + { + name: "Drop fulltext index with complex name", + indexName: "documents_title_content_fulltext", + want: "DROP INDEX \"documents_title_content_fulltext\"", + wantErr: false, + }, + { + name: "Drop fulltext index with underscore prefix", + indexName: "idx_posts_content_fulltext", + want: "DROP INDEX \"idx_posts_content_fulltext\"", + wantErr: false, + }, + { + name: "Drop fulltext index with numeric suffix", + indexName: "search_index_fulltext_1", + want: "DROP INDEX \"search_index_fulltext_1\"", + wantErr: false, + }, + { + name: "Empty index name", + indexName: "", + want: "", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bp := &blueprint.Blueprint{} + command := &blueprint.Command{Index: tt.indexName} + got, err := g.CompileDropFulltext(bp, command) + if tt.wantErr { + require.Error(t, err) + assert.Empty(t, got) + return + } + + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestPgGrammar_CompilePrimary(t *testing.T) { + g, err := grammars.NewGrammar("postgres") + require.NoError(t, err) + + tests := []struct { + name string + table string + blueprint func(table *blueprint.Blueprint) + want string + wantErr bool + }{ + { + name: "users", + blueprint: func(table *blueprint.Blueprint) { + table.Primary("id").Name("users_id_primary") + }, + want: "ALTER TABLE \"users\" ADD CONSTRAINT \"users_id_primary\" PRIMARY KEY (\"id\")", + wantErr: false, + }, + { + name: "user_roles", + blueprint: func(table *blueprint.Blueprint) { + table.Primary("user_id", "role_id").Name("user_roles_primary") + }, + want: "ALTER TABLE \"user_roles\" ADD CONSTRAINT \"user_roles_primary\" PRIMARY KEY (\"user_id\", \"role_id\")", + wantErr: false, + }, + { + name: "orders", + blueprint: func(table *blueprint.Blueprint) { + table.Primary("order_id") + }, + want: "ALTER TABLE \"orders\" ADD CONSTRAINT \"orders_order_id_primary\" PRIMARY KEY (\"order_id\")", + wantErr: false, + }, + { + name: "order_items", + blueprint: func(table *blueprint.Blueprint) { + table.Primary("order_id", "product_id", "variant_id").Name("order_items_composite_pk") + }, + want: "ALTER TABLE \"order_items\" ADD CONSTRAINT \"order_items_composite_pk\" PRIMARY KEY (\"order_id\", \"product_id\", \"variant_id\")", + wantErr: false, + }, + { + name: "users", + blueprint: func(table *blueprint.Blueprint) { + table.Primary("id", "", "tenant_id").Name("users_invalid_primary") + }, + wantErr: true, + }, + { + name: "users", + blueprint: func(table *blueprint.Blueprint) { + table.Primary("").Name("users_empty_primary") + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tableName := tt.table + if tableName == "" && !tt.wantErr { + tableName = tt.name + } + bp := &blueprint.Blueprint{Name: tableName} + tt.blueprint(bp) + got, err := g.CompilePrimary(bp, bp.Commands[0]) + if tt.wantErr { + require.Error(t, err) + return + } + + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestPgGrammar_GetType(t *testing.T) { + g, err := grammars.NewGrammar("postgres") + require.NoError(t, err) + + tests := []struct { + name string + blueprint func(table *blueprint.Blueprint) + want string + }{ + { + name: "custom column type", + blueprint: func(table *blueprint.Blueprint) { + table.Column("name", "CUSTOM_TYPE") + }, + want: "CUSTOM_TYPE", + }, + { + name: "boolean column type", + blueprint: func(table *blueprint.Blueprint) { + table.Boolean("active") + }, + want: "BOOLEAN", + }, + { + name: "char column type", + blueprint: func(table *blueprint.Blueprint) { + table.Char("code", 10) + }, + want: "CHAR(10)", + }, + { + name: "char column type without length", + blueprint: func(table *blueprint.Blueprint) { + table.Char("code") + }, + want: "CHAR(255)", + }, + { + name: "string column type", + blueprint: func(table *blueprint.Blueprint) { + table.String("name", 255) + }, + want: "VARCHAR(255)", + }, + { + name: "decimal column type", + blueprint: func(table *blueprint.Blueprint) { + table.Decimal("price", 10, 2) + }, + want: "DECIMAL(10, 2)", + }, + { + name: "double column type", + blueprint: func(table *blueprint.Blueprint) { + table.Double("value") + }, + want: "DOUBLE PRECISION", + }, + { + name: "float column type with precision", + blueprint: func(table *blueprint.Blueprint) { + table.Float("value", 6, 2) + }, + want: "REAL", + }, + { + name: "float column type without precision", + blueprint: func(table *blueprint.Blueprint) { + table.Float("value", 0, 0) + }, + want: "REAL", + }, + { + name: "big integer column type", + blueprint: func(table *blueprint.Blueprint) { + table.BigInteger("id") + }, + want: "BIGINT", + }, + { + name: "big integer auto increment", + blueprint: func(table *blueprint.Blueprint) { + table.BigInteger("id").AutoIncrement() + }, + want: "BIGSERIAL", + }, + { + name: "integer column type", + blueprint: func(table *blueprint.Blueprint) { + table.Integer("count") + }, + want: "INTEGER", + }, + { + name: "integer auto increment", + blueprint: func(table *blueprint.Blueprint) { + table.Integer("id").AutoIncrement() + }, + want: "SERIAL", + }, + { + name: "small integer column type", + blueprint: func(table *blueprint.Blueprint) { + table.SmallInteger("status") + }, + want: "SMALLINT", + }, + { + name: "small integer auto increment", + blueprint: func(table *blueprint.Blueprint) { + table.SmallInteger("id").Unsigned().AutoIncrement() + }, + want: "SMALLSERIAL", + }, + { + name: "medium integer column type", + blueprint: func(table *blueprint.Blueprint) { + table.MediumInteger("value") + }, + want: "INTEGER", + }, + { + name: "medium auto increment", + blueprint: func(table *blueprint.Blueprint) { + table.MediumIncrements("id") + }, + want: "SERIAL", + }, + { + name: "tiny integer column type", + blueprint: func(table *blueprint.Blueprint) { + table.TinyInteger("flag") + }, + want: "SMALLINT", + }, + { + name: "tiny integer auto increment", + blueprint: func(table *blueprint.Blueprint) { + table.TinyInteger("id").AutoIncrement() + }, + want: "SMALLSERIAL", + }, + { + name: "time column type", + blueprint: func(table *blueprint.Blueprint) { + table.Time("created_at") + }, + want: "TIME(0)", + }, + { + name: "datetime column type with precision", + blueprint: func(table *blueprint.Blueprint) { + table.DateTime("created_at", 6) + }, + want: "TIMESTAMP(6)", + }, + { + name: "datetime column type without precision", + blueprint: func(table *blueprint.Blueprint) { + table.DateTime("created_at", 0) + }, + want: "TIMESTAMP(0)", + }, + { + name: "datetime tz column type with precision", + blueprint: func(table *blueprint.Blueprint) { + table.DateTimeTz("created_at", 3) + }, + want: "TIMESTAMPTZ(3)", + }, + { + name: "datetime tz column type without precision", + blueprint: func(table *blueprint.Blueprint) { + table.DateTimeTz("created_at", 0) + }, + want: "TIMESTAMPTZ(0)", + }, + { + name: "timestamp column type with precision", + blueprint: func(table *blueprint.Blueprint) { + table.Timestamp("created_at", 6) + }, + want: "TIMESTAMP(6)", + }, + { + name: "timestamp column type without precision", + blueprint: func(table *blueprint.Blueprint) { + table.Timestamp("created_at", 0) + }, + want: "TIMESTAMP(0)", + }, + { + name: "timestamp tz column type with precision", + blueprint: func(table *blueprint.Blueprint) { + table.TimestampTz("created_at", 3) + }, + want: "TIMESTAMPTZ(3)", + }, + { + name: "timestamp tz column type without precision", + blueprint: func(table *blueprint.Blueprint) { + table.TimestampTz("created_at", 0) + }, + want: "TIMESTAMPTZ(0)", + }, + { + name: "geography column type", + blueprint: func(table *blueprint.Blueprint) { + table.Geography("location", "POINT", 4326) + }, + want: "GEOGRAPHY(POINT, 4326)", + }, + { + name: "long text column type", + blueprint: func(table *blueprint.Blueprint) { + table.LongText("content") + }, + want: "TEXT", + }, + { + name: "text column type", + blueprint: func(table *blueprint.Blueprint) { + table.Text("description") + }, + want: "TEXT", + }, + { + name: "tiny text column type", + blueprint: func(table *blueprint.Blueprint) { + table.TinyText("notes") + }, + want: "VARCHAR(255)", + }, + { + name: "date column type", + blueprint: func(table *blueprint.Blueprint) { + table.Date("birth_date") + }, + want: "DATE", + }, + { + name: "year column type", + blueprint: func(table *blueprint.Blueprint) { + table.Year("graduation_year") + }, + want: "INTEGER", + }, + { + name: "json column type", + blueprint: func(table *blueprint.Blueprint) { + table.JSON("metadata") + }, + want: "JSON", + }, + { + name: "jsonb column type", + blueprint: func(table *blueprint.Blueprint) { + table.JSONB("data") + }, + want: "JSONB", + }, + { + name: "uuid column type", + blueprint: func(table *blueprint.Blueprint) { + table.UUID("uuid") + }, + want: "UUID", + }, + { + name: "binary column type", + blueprint: func(table *blueprint.Blueprint) { + table.Binary("data") + }, + want: "BYTEA", + }, + { + name: "point column type", + blueprint: func(table *blueprint.Blueprint) { + table.Point("location") + }, + want: "POINT(4326)", + }, + { + name: "Geography type", + blueprint: func(table *blueprint.Blueprint) { + table.Geography("location", "POINT", 4326) + }, + want: "GEOGRAPHY(POINT, 4326)", + }, + { + name: "Geometry type with SRID", + blueprint: func(table *blueprint.Blueprint) { + table.Geometry("shape", "POLYGON", 4326) + }, + want: "GEOMETRY(POLYGON, 4326)", + }, + { + name: "Geometry type without SRID", + blueprint: func(table *blueprint.Blueprint) { + table.Geometry("shape", "POLYGON") + }, + want: "GEOMETRY(POLYGON)", + }, + { + name: "Enum type", + blueprint: func(table *blueprint.Blueprint) { + table.Enum("status", []string{"active", "inactive"}) + }, + want: "VARCHAR(255) CHECK (status IN ('active', 'inactive'))", + }, + { + name: "Enum type without allowed values", + blueprint: func(table *blueprint.Blueprint) { + table.Enum("status", []string{}) + }, + want: "VARCHAR(255)", + }, + { + name: "TimeTz with precision", + blueprint: func(table *blueprint.Blueprint) { + table.TimeTz("created_at", 3) + }, + want: "TIMETZ(3)", + }, + { + name: "TimeTz without precision", + blueprint: func(table *blueprint.Blueprint) { + table.TimeTz("created_at") + }, + want: "TIMETZ(0)", + }, + { + name: "IP Address type", + blueprint: func(table *blueprint.Blueprint) { + table.IPAddress("ip") + }, + want: "inet", + }, + { + name: "Mac Address type", + blueprint: func(table *blueprint.Blueprint) { + table.MacAddress("mac") + }, + want: "MACADDR", + }, + { + name: "TSVector type", + blueprint: func(table *blueprint.Blueprint) { + table.TSVector("ts") + }, + want: "TSVECTOR", + }, + { + name: "Geography type without subtype", + blueprint: func(table *blueprint.Blueprint) { + table.Geography("location", "") + }, + want: "GEOGRAPHY", + }, + { + name: "Point type without srid", + blueprint: func(table *blueprint.Blueprint) { + table.Point("location") + }, + want: "POINT(4326)", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bp := &blueprint.Blueprint{Name: "test_table"} + tt.blueprint(bp) + got := g.GetType(bp.Columns[0]) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/schema/grammars/sqlite_grammar.go b/schema/grammars/sqlite_grammar.go new file mode 100644 index 0000000..72cefa9 --- /dev/null +++ b/schema/grammars/sqlite_grammar.go @@ -0,0 +1,346 @@ +package grammars + +import ( + "errors" + "fmt" + "strings" + + "github.com/akfaiz/migris/schema/blueprint" +) + +type sqliteGrammar struct { + blueprint.BaseGrammar +} + +func newSqliteGrammar() *sqliteGrammar { + return &sqliteGrammar{} +} + +func (g *sqliteGrammar) CompileTableExists(_, table string) (string, error) { + return fmt.Sprintf("SELECT 1 FROM sqlite_master WHERE type='table' AND name=%s", g.QuoteString(table)), nil +} + +func (g *sqliteGrammar) CompileTables(_ string) (string, error) { + return "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'", nil +} + +func (g *sqliteGrammar) CompileColumns(_, table string) (string, error) { + return fmt.Sprintf("PRAGMA table_info(%q)", table), nil +} + +func (g *sqliteGrammar) CompileIndexes(_, table string) (string, error) { + return fmt.Sprintf("PRAGMA index_list(%q)", table), nil +} + +func (g *sqliteGrammar) CompileCreate(bp *blueprint.Blueprint) (string, error) { + columns, err := g.getColumns(bp) + if err != nil { + return "", err + } + sql := fmt.Sprintf("CREATE TABLE %q (%s)", bp.Name, strings.Join(columns, ", ")) + return sql, nil +} + +func (g *sqliteGrammar) CompileAdd(bp *blueprint.Blueprint) (string, error) { + columns, err := g.getColumns(bp) + if err != nil { + return "", err + } + if len(columns) == 0 { + return "", nil + } + for i, col := range columns { + columns[i] = "ADD COLUMN " + col + } + return fmt.Sprintf("ALTER TABLE %q %s", bp.Name, strings.Join(columns, ", ")), nil +} + +func (g *sqliteGrammar) CompileChange(_ *blueprint.Blueprint, _ *blueprint.Command) (string, error) { + return "", errors.New("sqlite does not support changing columns directly") +} + +func (g *sqliteGrammar) CompileRename(blueprint *blueprint.Blueprint, command *blueprint.Command) (string, error) { + return fmt.Sprintf("ALTER TABLE %q RENAME TO %q", blueprint.Name, command.To), nil +} + +func (g *sqliteGrammar) CompileDrop(blueprint *blueprint.Blueprint) (string, error) { + return fmt.Sprintf("DROP TABLE %q", blueprint.Name), nil +} + +func (g *sqliteGrammar) CompileDropIfExists(blueprint *blueprint.Blueprint) (string, error) { + return fmt.Sprintf("DROP TABLE IF EXISTS %q", blueprint.Name), nil +} + +func (g *sqliteGrammar) CompileDropColumn(_ *blueprint.Blueprint, _ *blueprint.Command) (string, error) { + return "", errors.New("sqlite drop column has limited support") +} + +func (g *sqliteGrammar) CompileIndex(blueprint *blueprint.Blueprint, command *blueprint.Command) (string, error) { + index := g.WrapIndexName(command.Index, `"`) + if index == "" { + index = g.WrapIndexName(g.CreateIndexName(blueprint, "index", command.Columns...), `"`) + } + return fmt.Sprintf("CREATE INDEX %s ON %q (%s)", index, blueprint.Name, g.WrapColumnize(command.Columns, `"`)), nil +} + +func (g *sqliteGrammar) CompileUnique(blueprint *blueprint.Blueprint, command *blueprint.Command) (string, error) { + index := g.WrapIndexName(command.Index, `"`) + if index == "" { + index = g.WrapIndexName(g.CreateIndexName(blueprint, "unique", command.Columns...), `"`) + } + return fmt.Sprintf( + "CREATE UNIQUE INDEX %s ON %q (%s)", + index, + blueprint.Name, + g.WrapColumnize(command.Columns, `"`), + ), nil +} + +func (g *sqliteGrammar) CompilePrimary(_ *blueprint.Blueprint, _ *blueprint.Command) (string, error) { + return "", nil +} + +func (g *sqliteGrammar) CompileForeign(_ *blueprint.Blueprint, _ *blueprint.Command) (string, error) { + return "", nil +} + +func (g *sqliteGrammar) CompileDropIndex(_ *blueprint.Blueprint, command *blueprint.Command) (string, error) { + if command.Index == "" { + return "", errors.New("index name cannot be empty") + } + return fmt.Sprintf("DROP INDEX %s", g.WrapIndexName(command.Index, `"`)), nil +} + +func (g *sqliteGrammar) GetType(col *blueprint.Column) string { + typeFuncMap := map[string]func(*blueprint.Column) string{ + blueprint.ColumnTypeChar: g.typeChar, + blueprint.ColumnTypeString: g.typeString, + blueprint.ColumnTypeTinyText: g.typeTinyText, + blueprint.ColumnTypeText: g.typeText, + blueprint.ColumnTypeMediumText: g.typeMediumText, + blueprint.ColumnTypeLongText: g.typeLongText, + blueprint.ColumnTypeBigInteger: g.typeBigInteger, + blueprint.ColumnTypeInteger: g.typeInteger, + blueprint.ColumnTypeMediumInteger: g.typeMediumInteger, + blueprint.ColumnTypeSmallInteger: g.typeSmallInteger, + blueprint.ColumnTypeTinyInteger: g.typeTinyInteger, + blueprint.ColumnTypeFloat: g.typeFloat, + blueprint.ColumnTypeDouble: g.typeDouble, + blueprint.ColumnTypeDecimal: g.typeDecimal, + blueprint.ColumnTypeBoolean: g.typeBoolean, + blueprint.ColumnTypeEnum: g.typeEnum, + blueprint.ColumnTypeJSON: g.typeJSON, + blueprint.ColumnTypeJSONB: g.typeJSONB, + blueprint.ColumnTypeDate: g.typeDate, + blueprint.ColumnTypeDateTime: g.typeDateTime, + blueprint.ColumnTypeDateTimeTz: g.typeDateTimeTz, + blueprint.ColumnTypeTime: g.typeTime, + blueprint.ColumnTypeTimeTz: g.typeTimeTz, + blueprint.ColumnTypeTimestamp: g.typeTimestamp, + blueprint.ColumnTypeTimestampTz: g.typeTimestampTz, + blueprint.ColumnTypeYear: g.typeYear, + blueprint.ColumnTypeBinary: g.typeBinary, + blueprint.ColumnTypeUUID: g.typeUUID, + blueprint.ColumnTypeULID: g.typeULID, + blueprint.ColumnTypeIPAddress: g.typeIPAddress, + blueprint.ColumnTypeMacAddress: g.typeMacAddress, + blueprint.ColumnTypeGeometry: g.typeGeometry, + blueprint.ColumnTypeGeography: g.typeGeography, + blueprint.ColumnTypePoint: g.typePoint, + } + if fn, ok := typeFuncMap[col.ColumnType]; ok { + return fn(col) + } + return col.ColumnType +} + +func (g *sqliteGrammar) typeChar(_ *blueprint.Column) string { + return "TEXT" +} + +func (g *sqliteGrammar) typeString(_ *blueprint.Column) string { + return "TEXT" +} + +func (g *sqliteGrammar) typeTinyText(_ *blueprint.Column) string { + return "TEXT" +} + +func (g *sqliteGrammar) typeText(_ *blueprint.Column) string { + return "TEXT" +} + +func (g *sqliteGrammar) typeMediumText(_ *blueprint.Column) string { + return "TEXT" +} + +func (g *sqliteGrammar) typeLongText(_ *blueprint.Column) string { + return "TEXT" +} + +func (g *sqliteGrammar) typeBigInteger(_ *blueprint.Column) string { + return "INTEGER" +} + +func (g *sqliteGrammar) typeInteger(_ *blueprint.Column) string { + return "INTEGER" +} + +func (g *sqliteGrammar) typeMediumInteger(_ *blueprint.Column) string { + return "INTEGER" +} + +func (g *sqliteGrammar) typeSmallInteger(_ *blueprint.Column) string { + return "INTEGER" +} + +func (g *sqliteGrammar) typeTinyInteger(_ *blueprint.Column) string { + return "INTEGER" +} + +func (g *sqliteGrammar) typeFloat(_ *blueprint.Column) string { + return "REAL" +} + +func (g *sqliteGrammar) typeDouble(_ *blueprint.Column) string { + return "REAL" +} + +func (g *sqliteGrammar) typeDecimal(_ *blueprint.Column) string { + return "NUMERIC" +} + +func (g *sqliteGrammar) typeBoolean(_ *blueprint.Column) string { + return "INTEGER" +} + +func (g *sqliteGrammar) typeEnum(_ *blueprint.Column) string { + return "TEXT" +} + +func (g *sqliteGrammar) typeJSON(_ *blueprint.Column) string { + return "TEXT" +} + +func (g *sqliteGrammar) typeJSONB(_ *blueprint.Column) string { + return "TEXT" +} + +func (g *sqliteGrammar) typeDate(_ *blueprint.Column) string { + return "DATE" +} + +func (g *sqliteGrammar) typeDateTime(_ *blueprint.Column) string { + return "DATETIME" +} + +func (g *sqliteGrammar) typeDateTimeTz(_ *blueprint.Column) string { + return "DATETIME" +} + +func (g *sqliteGrammar) typeTime(_ *blueprint.Column) string { + return "TEXT" +} + +func (g *sqliteGrammar) typeTimeTz(_ *blueprint.Column) string { + return "TEXT" +} + +func (g *sqliteGrammar) typeTimestamp(_ *blueprint.Column) string { + return "DATETIME" +} + +func (g *sqliteGrammar) typeTimestampTz(_ *blueprint.Column) string { + return "DATETIME" +} + +func (g *sqliteGrammar) typeYear(_ *blueprint.Column) string { + return "INTEGER" +} + +func (g *sqliteGrammar) typeBinary(_ *blueprint.Column) string { + return "BLOB" +} + +func (g *sqliteGrammar) typeUUID(_ *blueprint.Column) string { + return "TEXT" +} + +func (g *sqliteGrammar) typeULID(_ *blueprint.Column) string { + return "TEXT" +} + +func (g *sqliteGrammar) typeIPAddress(_ *blueprint.Column) string { + return "TEXT" +} + +func (g *sqliteGrammar) typeMacAddress(_ *blueprint.Column) string { + return "TEXT" +} + +func (g *sqliteGrammar) typeGeometry(_ *blueprint.Column) string { + return "TEXT" +} + +func (g *sqliteGrammar) typeGeography(_ *blueprint.Column) string { + return "TEXT" +} + +func (g *sqliteGrammar) typePoint(_ *blueprint.Column) string { + return "TEXT" +} + +func (g *sqliteGrammar) GetDefaultValue(value any) string { + if value == nil { + return "NULL" + } + switch v := value.(type) { + case blueprint.Expression: + return v.String() + case bool: + if v { + return "1" + } + return "0" + case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64: + return fmt.Sprintf("%v", v) + default: + return fmt.Sprintf("'%v'", v) + } +} + +func (g *sqliteGrammar) getColumns(bp *blueprint.Blueprint) ([]string, error) { + var columns []string + for _, col := range bp.GetAddedColumns() { + if col.Name == "" { + return nil, errors.New("column name cannot be empty") + } + sql := fmt.Sprintf("%q %s", col.Name, g.GetType(col)) + sql += g.modifiers(col) + columns = append(columns, sql) + } + return columns, nil +} + +func (g *sqliteGrammar) modifiers(col *blueprint.Column) string { + var sql string + if col.PrimaryVal != nil && *col.PrimaryVal { + sql += " PRIMARY KEY" + if col.AutoIncrementVal != nil && *col.AutoIncrementVal { + sql += " AUTOINCREMENT" + } + } + if col.NullableVal != nil { + if !*col.NullableVal { + sql += " NOT NULL" + } + } else { + sql += " NOT NULL" + } + if col.DefaultValue != nil { + sql += fmt.Sprintf(" DEFAULT %s", g.GetDefaultValue(col.DefaultValue)) + } else if col.UseCurrentVal { + sql += " DEFAULT CURRENT_TIMESTAMP" + } + return sql +} diff --git a/schema/grammars/sqlite_grammar_test.go b/schema/grammars/sqlite_grammar_test.go new file mode 100644 index 0000000..9d712cb --- /dev/null +++ b/schema/grammars/sqlite_grammar_test.go @@ -0,0 +1,712 @@ +package grammars_test + +import ( + "testing" + + "github.com/akfaiz/migris/schema/blueprint" + "github.com/akfaiz/migris/schema/grammars" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSqliteGrammar_CompileCreate(t *testing.T) { + g, err := grammars.NewGrammar("sqlite3") + require.NoError(t, err) + + tests := []struct { + name string + table string + blueprint func(table *blueprint.Blueprint) + want string + wantErr bool + }{ + { + name: "basic table creation", + table: "users", + blueprint: func(table *blueprint.Blueprint) { + table.ID() + table.String("name", 255) + }, + want: "CREATE TABLE \"users\" (\"id\" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, \"name\" TEXT NOT NULL)", + wantErr: false, + }, + { + name: "table with nullable column", + table: "posts", + blueprint: func(table *blueprint.Blueprint) { + table.ID() + table.String("title", 255) + table.Text("content").Nullable() + }, + want: "CREATE TABLE \"posts\" (\"id\" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, \"title\" TEXT NOT NULL, \"content\" TEXT)", + wantErr: false, + }, + { + name: "table with default values", + table: "settings", + blueprint: func(table *blueprint.Blueprint) { + table.ID() + table.String("key", 255) + table.Boolean("enabled").Default(true) + table.Integer("count").Default(0) + }, + want: "CREATE TABLE \"settings\" (\"id\" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, \"key\" TEXT NOT NULL, \"enabled\" INTEGER NOT NULL DEFAULT 1, \"count\" INTEGER NOT NULL DEFAULT 0)", + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tableName := tt.table + if tableName == "" && !tt.wantErr { + tableName = tt.name + } + bp := &blueprint.Blueprint{Name: tableName, Grammar: g} + tt.blueprint(bp) + + got, err := g.CompileCreate(bp) + if tt.wantErr { + assert.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, tt.want, got) + } + }) + } +} + +func TestSqliteGrammar_CompileAdd(t *testing.T) { + g, err := grammars.NewGrammar("sqlite3") + require.NoError(t, err) + + tests := []struct { + name string + table string + blueprint func(table *blueprint.Blueprint) + want string + wantErr bool + }{ + { + name: "add single column", + table: "users", + blueprint: func(table *blueprint.Blueprint) { + table.String("email", 255) + }, + want: "ALTER TABLE \"users\" ADD COLUMN \"email\" TEXT NOT NULL", + wantErr: false, + }, + { + name: "add multiple columns", + table: "posts", + blueprint: func(table *blueprint.Blueprint) { + table.String("slug", 255) + table.Timestamp("published_at").Nullable() + }, + want: "ALTER TABLE \"posts\" ADD COLUMN \"slug\" TEXT NOT NULL, ADD COLUMN \"published_at\" DATETIME", + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tableName := tt.table + if tableName == "" && !tt.wantErr { + tableName = tt.name + } + bp := &blueprint.Blueprint{Name: tableName, Grammar: g} + tt.blueprint(bp) + + got, err := g.CompileAdd(bp) + if tt.wantErr { + assert.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, tt.want, got) + } + }) + } +} + +func TestSqliteGrammar_CompileDrop(t *testing.T) { + g, err := grammars.NewGrammar("sqlite3") + require.NoError(t, err) + + tests := []struct { + name string + table string + want string + wantErr bool + }{ + { + name: "drop table", + table: "users", + want: "DROP TABLE \"users\"", + wantErr: false, + }, + { + name: "drop table with special chars", + table: "user_profiles", + want: "DROP TABLE \"user_profiles\"", + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tableName := tt.table + if tableName == "" && !tt.wantErr { + tableName = tt.name + } + bp := &blueprint.Blueprint{Name: tableName, Grammar: g} + + got, err := g.CompileDrop(bp) + if tt.wantErr { + assert.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, tt.want, got) + } + }) + } +} + +func TestSqliteGrammar_CompileDropIfExists(t *testing.T) { + g, err := grammars.NewGrammar("sqlite3") + require.NoError(t, err) + + tests := []struct { + name string + table string + want string + wantErr bool + }{ + { + name: "drop table if exists", + table: "users", + want: "DROP TABLE IF EXISTS \"users\"", + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tableName := tt.table + if tableName == "" && !tt.wantErr { + tableName = tt.name + } + bp := &blueprint.Blueprint{Name: tableName, Grammar: g} + + got, err := g.CompileDropIfExists(bp) + if tt.wantErr { + assert.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, tt.want, got) + } + }) + } +} + +func TestSqliteGrammar_CompileRename(t *testing.T) { + g, err := grammars.NewGrammar("sqlite3") + require.NoError(t, err) + + tests := []struct { + name string + table string + newName string + want string + wantErr bool + }{ + { + name: "rename table", + table: "users", + newName: "customers", + want: "ALTER TABLE \"users\" RENAME TO \"customers\"", + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tableName := tt.table + if tableName == "" && !tt.wantErr { + tableName = tt.name + } + bp := &blueprint.Blueprint{Name: tableName, Grammar: g} + bp.Rename(tt.newName) + + got, err := g.CompileRename(bp, bp.Commands[0]) + if tt.wantErr { + assert.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, tt.want, got) + } + }) + } +} + +func TestSqliteGrammar_CompileIndex(t *testing.T) { + g, err := grammars.NewGrammar("sqlite3") + require.NoError(t, err) + + tests := []struct { + name string + table string + blueprint func(table *blueprint.Blueprint) + want string + wantErr bool + }{ + { + name: "create index", + table: "users", + blueprint: func(table *blueprint.Blueprint) { + table.Index("email") + }, + want: "CREATE INDEX \"users_email_index\" ON \"users\" (\"email\")", + wantErr: false, + }, + { + name: "create composite index", + table: "posts", + blueprint: func(table *blueprint.Blueprint) { + table.Index("user_id", "created_at") + }, + want: "CREATE INDEX \"posts_user_id_created_at_index\" ON \"posts\" (\"user_id\", \"created_at\")", + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tableName := tt.table + if tableName == "" && !tt.wantErr { + tableName = tt.name + } + bp := &blueprint.Blueprint{Name: tableName, Grammar: g} + tt.blueprint(bp) + + got, err := g.CompileIndex(bp, bp.Commands[0]) + if tt.wantErr { + assert.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, tt.want, got) + } + }) + } +} + +func TestSqliteGrammar_CompileUnique(t *testing.T) { + g, err := grammars.NewGrammar("sqlite3") + require.NoError(t, err) + + tests := []struct { + name string + table string + blueprint func(table *blueprint.Blueprint) + want string + wantErr bool + }{ + { + name: "create unique index", + table: "users", + blueprint: func(table *blueprint.Blueprint) { + table.Unique("email") + }, + want: "CREATE UNIQUE INDEX \"users_email_unique\" ON \"users\" (\"email\")", + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tableName := tt.table + if tableName == "" && !tt.wantErr { + tableName = tt.name + } + bp := &blueprint.Blueprint{Name: tableName, Grammar: g} + tt.blueprint(bp) + + got, err := g.CompileUnique(bp, bp.Commands[0]) + if tt.wantErr { + assert.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, tt.want, got) + } + }) + } +} + +func TestSqliteGrammar_CompileTableExists(t *testing.T) { + g, err := grammars.NewGrammar("sqlite3") + require.NoError(t, err) + + sql, err := g.CompileTableExists("", "users") + require.NoError(t, err) + assert.Equal(t, "SELECT 1 FROM sqlite_master WHERE type='table' AND name='users'", sql) +} + +func TestSqliteGrammar_CompileTables(t *testing.T) { + g, err := grammars.NewGrammar("sqlite3") + require.NoError(t, err) + + sql, err := g.CompileTables("") + require.NoError(t, err) + assert.Equal(t, "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'", sql) +} + +func TestSqliteGrammar_CompileColumns(t *testing.T) { + g, err := grammars.NewGrammar("sqlite3") + require.NoError(t, err) + + sql, err := g.CompileColumns("", "users") + require.NoError(t, err) + assert.Equal(t, "PRAGMA table_info(\"users\")", sql) +} + +func TestSqliteGrammar_CompileIndexes(t *testing.T) { + g, err := grammars.NewGrammar("sqlite3") + require.NoError(t, err) + + sql, err := g.CompileIndexes("", "users") + require.NoError(t, err) + assert.Equal(t, "PRAGMA index_list(\"users\")", sql) +} + +func TestSqliteGrammar_CompileDropIndex(t *testing.T) { + g, err := grammars.NewGrammar("sqlite3") + require.NoError(t, err) + + bp := &blueprint.Blueprint{Name: "users", Grammar: g} + bp.DropIndex("users_email_index") + + sql, err := g.CompileDropIndex(bp, bp.Commands[0]) + require.NoError(t, err) + assert.Equal(t, "DROP INDEX \"users_email_index\"", sql) + + // test empty index name + cmd := &blueprint.Command{Index: ""} + _, err = g.CompileDropIndex(bp, cmd) + require.Error(t, err) +} + +func TestSqliteGrammar_UnsupportedOperations(t *testing.T) { + g, err := grammars.NewGrammar("sqlite3") + require.NoError(t, err) + bp := &blueprint.Blueprint{Name: "test_table", Grammar: g} + cmd := &blueprint.Command{} // Empty command for testing + + tests := []struct { + name string + fn func() (string, error) + }{ + {"CompileChange", func() (string, error) { return g.CompileChange(bp, cmd) }}, + {"CompileDropColumn", func() (string, error) { return g.CompileDropColumn(bp, cmd) }}, + {"CompileRenameColumn", func() (string, error) { return g.CompileRenameColumn(bp, cmd) }}, + {"CompileFullText", func() (string, error) { return g.CompileFullText(bp, cmd) }}, + {"CompileDropPrimary", func() (string, error) { return g.CompileDropPrimary(bp, cmd) }}, + {"CompileRenameIndex", func() (string, error) { return g.CompileRenameIndex(bp, cmd) }}, + {"CompileDropForeign", func() (string, error) { return g.CompileDropForeign(bp, cmd) }}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sql, err := tt.fn() + require.Error(t, err) + assert.Empty(t, sql) + }) + } +} + +func TestSqliteGrammar_SupportedButDelegated(t *testing.T) { + g, err := grammars.NewGrammar("sqlite3") + require.NoError(t, err) + bp := &blueprint.Blueprint{Name: "test_table", Grammar: g} + cmd := &blueprint.Command{} // Empty command for testing + + // Test operations that are supported but handled at table creation time (not as separate commands) + tests := []struct { + name string + fn func() (string, error) + }{ + {"CompilePrimary", func() (string, error) { return g.CompilePrimary(bp, cmd) }}, + {"CompileForeign", func() (string, error) { return g.CompileForeign(bp, cmd) }}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sql, err := tt.fn() + require.NoError(t, err, "Should not return error as these are handled at table creation time") + assert.Empty(t, sql, "Should return empty SQL as these are handled elsewhere") + }) + } +} + +func TestSqliteGrammar_GetType(t *testing.T) { + g, err := grammars.NewGrammar("sqlite3") + require.NoError(t, err) + + tests := []struct { + name string + blueprint func(table *blueprint.Blueprint) + want string + }{ + { + name: "custom column type", + blueprint: func(table *blueprint.Blueprint) { + table.Column("name", "CUSTOM_TYPE") + }, + want: "CUSTOM_TYPE", + }, + { + name: "char column type", + blueprint: func(table *blueprint.Blueprint) { + table.Char("code", 10) + }, + want: "TEXT", + }, + { + name: "string column type", + blueprint: func(table *blueprint.Blueprint) { + table.String("name", 255) + }, + want: "TEXT", + }, + { + name: "tiny text column type", + blueprint: func(table *blueprint.Blueprint) { + table.TinyText("content") + }, + want: "TEXT", + }, + { + name: "text column type", + blueprint: func(table *blueprint.Blueprint) { + table.Text("content") + }, + want: "TEXT", + }, + { + name: "medium text column type", + blueprint: func(table *blueprint.Blueprint) { + table.MediumText("content") + }, + want: "TEXT", + }, + { + name: "long text column type", + blueprint: func(table *blueprint.Blueprint) { + table.LongText("content") + }, + want: "TEXT", + }, + { + name: "big integer column type", + blueprint: func(table *blueprint.Blueprint) { + table.BigInteger("id") + }, + want: "INTEGER", + }, + { + name: "integer column type", + blueprint: func(table *blueprint.Blueprint) { + table.Integer("id") + }, + want: "INTEGER", + }, + { + name: "medium integer column type", + blueprint: func(table *blueprint.Blueprint) { + table.MediumInteger("id") + }, + want: "INTEGER", + }, + { + name: "small integer column type", + blueprint: func(table *blueprint.Blueprint) { + table.SmallInteger("id") + }, + want: "INTEGER", + }, + { + name: "tiny integer column type", + blueprint: func(table *blueprint.Blueprint) { + table.TinyInteger("id") + }, + want: "INTEGER", + }, + { + name: "float column type", + blueprint: func(table *blueprint.Blueprint) { + table.Float("val") + }, + want: "REAL", + }, + { + name: "double column type", + blueprint: func(table *blueprint.Blueprint) { + table.Double("val") + }, + want: "REAL", + }, + { + name: "decimal column type", + blueprint: func(table *blueprint.Blueprint) { + table.Decimal("val", 8, 2) + }, + want: "NUMERIC", + }, + { + name: "boolean column type", + blueprint: func(table *blueprint.Blueprint) { + table.Boolean("val") + }, + want: "INTEGER", + }, + { + name: "enum column type", + blueprint: func(table *blueprint.Blueprint) { + table.Enum("val", []string{"a", "b"}) + }, + want: "TEXT", + }, + { + name: "json column type", + blueprint: func(table *blueprint.Blueprint) { + table.JSON("val") + }, + want: "TEXT", + }, + { + name: "jsonb column type", + blueprint: func(table *blueprint.Blueprint) { + table.JSONB("val") + }, + want: "TEXT", + }, + { + name: "date column type", + blueprint: func(table *blueprint.Blueprint) { + table.Date("val") + }, + want: "DATE", + }, + { + name: "datetime column type", + blueprint: func(table *blueprint.Blueprint) { + table.DateTime("val", 0) + }, + want: "DATETIME", + }, + { + name: "datetime tz column type", + blueprint: func(table *blueprint.Blueprint) { + table.DateTimeTz("val", 0) + }, + want: "DATETIME", + }, + { + name: "time column type", + blueprint: func(table *blueprint.Blueprint) { + table.Time("val") + }, + want: "TEXT", + }, + { + name: "time tz column type", + blueprint: func(table *blueprint.Blueprint) { + table.TimeTz("val", 0) + }, + want: "TEXT", + }, + { + name: "timestamp column type", + blueprint: func(table *blueprint.Blueprint) { + table.Timestamp("val", 0) + }, + want: "DATETIME", + }, + { + name: "timestamp tz column type", + blueprint: func(table *blueprint.Blueprint) { + table.TimestampTz("val", 0) + }, + want: "DATETIME", + }, + { + name: "year column type", + blueprint: func(table *blueprint.Blueprint) { + table.Year("val") + }, + want: "INTEGER", + }, + { + name: "binary column type", + blueprint: func(table *blueprint.Blueprint) { + table.Binary("val") + }, + want: "BLOB", + }, + { + name: "uuid column type", + blueprint: func(table *blueprint.Blueprint) { + table.UUID("val") + }, + want: "TEXT", + }, + { + name: "ulid column type", + blueprint: func(table *blueprint.Blueprint) { + table.ULID("val") + }, + want: "TEXT", + }, + { + name: "ip address column type", + blueprint: func(table *blueprint.Blueprint) { + table.IPAddress("val") + }, + want: "TEXT", + }, + { + name: "mac address column type", + blueprint: func(table *blueprint.Blueprint) { + table.MacAddress("val") + }, + want: "TEXT", + }, + { + name: "geometry column type", + blueprint: func(table *blueprint.Blueprint) { + table.Geometry("val", "POLYGON", 4326) + }, + want: "TEXT", + }, + { + name: "geography column type", + blueprint: func(table *blueprint.Blueprint) { + table.Geography("val", "POLYGON", 4326) + }, + want: "TEXT", + }, + { + name: "point column type", + blueprint: func(table *blueprint.Blueprint) { + table.Point("val") + }, + want: "TEXT", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bp := &blueprint.Blueprint{Name: "test_table"} + tt.blueprint(bp) + got := g.GetType(bp.Columns[0]) + assert.Equal(t, tt.want, got, "Expected type to match for test case: %s", tt.name) + }) + } +} diff --git a/schema/index_definition.go b/schema/index_definition.go deleted file mode 100644 index cbcb14d..0000000 --- a/schema/index_definition.go +++ /dev/null @@ -1,49 +0,0 @@ -package schema - -import "github.com/akfaiz/migris/internal/util" - -// IndexDefinition defines the interface for defining an index in a database table. -type IndexDefinition interface { - // Algorithm sets the algorithm for the index. - Algorithm(algorithm string) IndexDefinition - // Deferrable sets the index as deferrable. - Deferrable(value ...bool) IndexDefinition - // InitiallyImmediate sets the index to be initially immediate. - InitiallyImmediate(value ...bool) IndexDefinition - // Language sets the language for the index. - // Used for full-text indexes in PostgreSQL. - Language(language string) IndexDefinition - // Name sets the name of the index. - Name(name string) IndexDefinition -} - -type indexDefinition struct { - *command -} - -func (id *indexDefinition) Algorithm(algorithm string) IndexDefinition { - id.algorithm = algorithm - return id -} - -func (id *indexDefinition) Deferrable(value ...bool) IndexDefinition { - val := util.Optional(true, value...) - id.deferrable = &val - return id -} - -func (id *indexDefinition) InitiallyImmediate(value ...bool) IndexDefinition { - val := util.Optional(true, value...) - id.initiallyImmediate = &val - return id -} - -func (id *indexDefinition) Language(language string) IndexDefinition { - id.language = language - return id -} - -func (id *indexDefinition) Name(name string) IndexDefinition { - id.index = name - return id -} diff --git a/schema/mysql_builder.go b/schema/mysql_builder.go deleted file mode 100644 index f606532..0000000 --- a/schema/mysql_builder.go +++ /dev/null @@ -1,221 +0,0 @@ -package schema - -import ( - "database/sql" - "errors" - "strings" -) - -type mysqlBuilder struct { - baseBuilder -} - -var _ Builder = (*mysqlBuilder)(nil) - -func newMysqlBuilder() Builder { - grammar := newMysqlGrammar() - - return &mysqlBuilder{ - baseBuilder: baseBuilder{grammar: grammar}, - } -} - -func (b *mysqlBuilder) GetColumns(c Context, tableName string) ([]*Column, error) { - if c == nil || tableName == "" { - return nil, errors.New("invalid arguments: context is nil or table name is empty") - } - - query, err := b.grammar.CompileColumns("", tableName) - if err != nil { - return nil, err - } - - rows, err := c.Query(query) - if err != nil { - return nil, err - } - defer rows.Close() - - var columns []*Column - for rows.Next() { - var col Column - var nullableStr string - if err = rows.Scan( - &col.Name, &col.TypeName, &col.TypeFull, - &col.Collation, &nullableStr, - &col.DefaultVal, &col.Comment, - &col.Extra, - ); err != nil { - return nil, err - } - if nullableStr == "YES" { - col.Nullable = true - } - columns = append(columns, &col) - } - if err = rows.Err(); err != nil { - return nil, err - } - - return columns, nil -} - -func (b *mysqlBuilder) GetIndexes(c Context, tableName string) ([]*Index, error) { - if c == nil || tableName == "" { - return nil, errors.New("invalid arguments: context is nil or table name is empty") - } - - query, err := b.grammar.CompileIndexes("", tableName) - if err != nil { - return nil, err - } - - rows, err := c.Query(query) - if err != nil { - return nil, err - } - defer rows.Close() - - var indexes []*Index - for rows.Next() { - var idx Index - var columnsStr string - if err = rows.Scan(&idx.Name, &columnsStr, &idx.Type, &idx.Unique); err != nil { - return nil, err - } - idx.Columns = strings.Split(columnsStr, ",") - indexes = append(indexes, &idx) - } - if err = rows.Err(); err != nil { - return nil, err - } - - return indexes, nil -} - -func (b *mysqlBuilder) GetTables(c Context) ([]*TableInfo, error) { - if c == nil { - return nil, errors.New("invalid arguments: context is nil") - } - - query, err := b.grammar.CompileTables("") - if err != nil { - return nil, err - } - rows, err := c.Query(query) - if err != nil { - return nil, err - } - defer rows.Close() - - var tables []*TableInfo - for rows.Next() { - var table TableInfo - if err = rows.Scan(&table.Name, &table.Size, &table.Comment, &table.Engine, &table.Collation); err != nil { - return nil, err - } - tables = append(tables, &table) - } - if err = rows.Err(); err != nil { - return nil, err - } - - return tables, nil -} - -func (b *mysqlBuilder) HasColumn(c Context, tableName string, columnName string) (bool, error) { - if c == nil || columnName == "" { - return false, errors.New("invalid arguments: context is nil or column name is empty") - } - return b.HasColumns(c, tableName, []string{columnName}) -} - -func (b *mysqlBuilder) HasColumns(c Context, tableName string, columnNames []string) (bool, error) { - if c == nil || tableName == "" { - return false, errors.New("invalid arguments: context is nil or table name is empty") - } - if len(columnNames) == 0 { - return false, errors.New("no column names provided") - } - - columns, err := b.GetColumns(c, tableName) - if err != nil { - return false, err - } - columnsMap := make(map[string]bool) - for _, col := range columns { - columnsMap[col.Name] = true - } - for _, colName := range columnNames { - if _, exists := columnsMap[colName]; !exists { - return false, nil // If any column does not exist, return false - } - } - return true, nil // All specified columns exist -} - -//nolint:dupl // Similar code exists in other builder files -func (b *mysqlBuilder) HasIndex(c Context, tableName string, indexes []string) (bool, error) { - if c == nil || tableName == "" { - return false, errors.New("invalid arguments: context is nil or table name is empty") - } - - existingIndexes, err := b.GetIndexes(c, tableName) - if err != nil { - return false, err - } - if len(existingIndexes) == 0 { - return false, nil // No indexes found, so the specified indexes cannot exist - } - if len(indexes) == 0 { - return true, nil // No specific indexes to check, so we assume they exist - } - if len(indexes) == 1 { - for _, idx := range existingIndexes { - if idx.Name == indexes[0] { - return true, nil // If any specified index exists, return true - } - } - } - indexColumns := make(map[string]bool) - for _, idx := range indexes { - indexColumns[idx] = true // Create a map of specified indexes for quick lookup - } - - for _, index := range existingIndexes { - found := true - for _, indexCol := range index.Columns { - if _, exists := indexColumns[indexCol]; !exists { - found = false // If any column in the index does not match the specified indexes, set found to false - break - } - } - // If all columns in the index match the specified indexes, we found a match - if found { - return true, nil - } - } - - return false, nil // If no specified index exists, return false -} - -func (b *mysqlBuilder) HasTable(c Context, name string) (bool, error) { - if c == nil || name == "" { - return false, errors.New("invalid arguments: context is nil or table name is empty") - } - - query, err := b.grammar.CompileTableExists("", name) - if err != nil { - return false, err - } - - row := c.QueryRow(query) - var exists bool - if err = row.Scan(&exists); err != nil { - if errors.Is(err, sql.ErrNoRows) { - return false, nil // Table does not exist - } - return false, err // Other error occurred - } - return exists, nil // Return true if the table exists -} diff --git a/schema/mysql_grammar.go b/schema/mysql_grammar.go deleted file mode 100644 index 3964e04..0000000 --- a/schema/mysql_grammar.go +++ /dev/null @@ -1,620 +0,0 @@ -package schema - -import ( - "errors" - "fmt" - "slices" - "strings" - - "github.com/akfaiz/migris/internal/util" -) - -type mysqlGrammar struct { - baseGrammar - - serials []string -} - -func newMysqlGrammar() *mysqlGrammar { - return &mysqlGrammar{ - serials: []string{ - "bigInteger", "integer", "mediumInteger", "smallInteger", - "tinyInteger", - }, - } -} - -func (g *mysqlGrammar) CompileTableExists(schema string, table string) (string, error) { - return fmt.Sprintf( - "SELECT 1 FROM information_schema.tables WHERE table_schema = %s AND table_name = %s AND table_type = 'BASE TABLE'", - util.Ternary(schema != "", g.QuoteString(schema), "schema()"), - g.QuoteString(table), - ), nil -} - -func (g *mysqlGrammar) CompileTables(schema string) (string, error) { - return fmt.Sprintf( - "select table_name as `name`, (data_length + index_length) as `size`, "+ - "table_comment as `comment`, engine as `engine`, table_collation as `collation` "+ - "from information_schema.tables where table_schema = %s and table_type in ('BASE TABLE', 'SYSTEM VERSIONED') "+ - "order by table_name", - util.Ternary(schema != "", g.QuoteString(schema), "schema()"), - ), nil -} - -func (g *mysqlGrammar) CompileColumns(schema, table string) (string, error) { - return fmt.Sprintf( - "select column_name as `name`, data_type as `type_name`, column_type as `type`, "+ - "collation_name as `collation`, is_nullable as `nullable`, "+ - "column_default as `default`, column_comment as `comment`, extra as `extra` "+ - "from information_schema.columns where table_schema = %s and table_name = %s "+ - "order by ordinal_position asc", - util.Ternary(schema != "", g.QuoteString(schema), "schema()"), - g.QuoteString(table), - ), nil -} - -func (g *mysqlGrammar) CompileIndexes(schema, table string) (string, error) { - return fmt.Sprintf( - "select index_name as `name`, group_concat(column_name order by seq_in_index) as `columns`, "+ - "index_type as `type`, not non_unique as `unique` "+ - "from information_schema.statistics where table_schema = %s and table_name = %s "+ - "group by index_name, index_type, non_unique", - util.Ternary(schema != "", g.QuoteString(schema), "schema()"), - g.QuoteString(table), - ), nil -} - -func (g *mysqlGrammar) CompileCreate(blueprint *Blueprint) (string, error) { - sql, err := g.compileCreateTable(blueprint) - if err != nil { - return "", err - } - sql = g.compileCreateEncoding(sql, blueprint) - - return g.compileCreateEngine(sql, blueprint), nil -} - -func (g *mysqlGrammar) compileCreateTable(blueprint *Blueprint) (string, error) { - columns, err := g.getColumns(blueprint) - if err != nil { - return "", err - } - - constraints := g.getConstraints(blueprint) - columns = append(columns, constraints...) - - return fmt.Sprintf("CREATE TABLE %s (%s)", blueprint.name, strings.Join(columns, ", ")), nil -} - -func (g *mysqlGrammar) compileCreateEncoding(sql string, blueprint *Blueprint) string { - if blueprint.charset != "" { - sql += fmt.Sprintf(" DEFAULT CHARACTER SET %s", blueprint.charset) - } - if blueprint.collation != "" { - sql += fmt.Sprintf(" COLLATE %s", blueprint.collation) - } - - return sql -} - -func (g *mysqlGrammar) compileCreateEngine(sql string, blueprint *Blueprint) string { - if blueprint.engine != "" { - sql += fmt.Sprintf(" ENGINE = %s", blueprint.engine) - } - return sql -} - -func (g *mysqlGrammar) CompileAdd(blueprint *Blueprint) (string, error) { - if len(blueprint.getAddedColumns()) == 0 { - return "", nil - } - - columns, err := g.getColumns(blueprint) - if err != nil { - return "", err - } - columns = g.PrefixArray("ADD COLUMN ", columns) - constraints := g.getConstraints(blueprint) - constraints = g.PrefixArray("ADD ", constraints) - columns = append(columns, constraints...) - - return fmt.Sprintf("ALTER TABLE %s %s", - blueprint.name, - strings.Join(columns, ", "), - ), nil -} - -func (g *mysqlGrammar) CompileChange(bp *Blueprint, command *command) (string, error) { - column := command.column - if column.name == "" { - return "", errors.New("column name cannot be empty for change operation") - } - - sql := fmt.Sprintf("ALTER TABLE %s MODIFY COLUMN %s %s", bp.name, column.name, g.getType(column)) - var sqlBuilder strings.Builder - for _, modifier := range g.modifiers() { - sqlBuilder.WriteString(modifier(column)) - } - sql += sqlBuilder.String() - return sql, nil -} - -func (g *mysqlGrammar) CompileRename(blueprint *Blueprint, command *command) (string, error) { - return fmt.Sprintf("ALTER TABLE %s RENAME TO %s", blueprint.name, command.to), nil -} - -func (g *mysqlGrammar) CompileDrop(blueprint *Blueprint) (string, error) { - if blueprint.name == "" { - return "", errors.New("table name cannot be empty") - } - return fmt.Sprintf("DROP TABLE %s", blueprint.name), nil -} - -func (g *mysqlGrammar) CompileDropIfExists(blueprint *Blueprint) (string, error) { - if blueprint.name == "" { - return "", errors.New("table name cannot be empty") - } - return fmt.Sprintf("DROP TABLE IF EXISTS %s", blueprint.name), nil -} - -func (g *mysqlGrammar) CompileDropColumn(blueprint *Blueprint, command *command) (string, error) { - if len(command.columns) == 0 { - return "", errors.New("no columns to drop") - } - columns := make([]string, len(command.columns)) - for i, col := range command.columns { - if col == "" { - return "", errors.New("column name cannot be empty") - } - columns[i] = col - } - columns = g.PrefixArray("DROP COLUMN ", columns) - return fmt.Sprintf("ALTER TABLE %s %s", blueprint.name, strings.Join(columns, ", ")), nil -} - -func (g *mysqlGrammar) CompileRenameColumn(blueprint *Blueprint, command *command) (string, error) { - if command.from == "" || command.to == "" { - return "", errors.New("old and new column names cannot be empty") - } - return fmt.Sprintf("ALTER TABLE %s RENAME COLUMN %s TO %s", blueprint.name, command.from, command.to), nil -} - -func (g *mysqlGrammar) CompileIndex(blueprint *Blueprint, command *command) (string, error) { - if slices.Contains(command.columns, "") { - return "", errors.New("index column cannot be empty") - } - - indexName := command.index - if indexName == "" { - indexName = g.CreateIndexName(blueprint, "index", command.columns...) - } - - sql := fmt.Sprintf("CREATE INDEX %s ON %s (%s)", indexName, blueprint.name, g.Columnize(command.columns)) - if command.algorithm != "" { - sql += fmt.Sprintf(" USING %s", command.algorithm) - } - - return sql, nil -} - -func (g *mysqlGrammar) CompileUnique(blueprint *Blueprint, command *command) (string, error) { - if slices.Contains(command.columns, "") { - return "", errors.New("unique column cannot be empty") - } - - indexName := command.index - if indexName == "" { - indexName = g.CreateIndexName(blueprint, "unique", command.columns...) - } - sql := fmt.Sprintf("CREATE UNIQUE INDEX %s ON %s (%s)", indexName, blueprint.name, g.Columnize(command.columns)) - if command.algorithm != "" { - sql += fmt.Sprintf(" USING %s", command.algorithm) - } - - return sql, nil -} - -func (g *mysqlGrammar) CompileFullText(blueprint *Blueprint, command *command) (string, error) { - if slices.Contains(command.columns, "") { - return "", errors.New("fulltext index column cannot be empty") - } - - indexName := command.index - if indexName == "" { - indexName = g.CreateIndexName(blueprint, "fulltext", command.columns...) - } - - return fmt.Sprintf( - "CREATE FULLTEXT INDEX %s ON %s (%s)", - indexName, - blueprint.name, - g.Columnize(command.columns), - ), nil -} - -func (g *mysqlGrammar) CompilePrimary(blueprint *Blueprint, command *command) (string, error) { - if slices.Contains(command.columns, "") { - return "", errors.New("primary key column cannot be empty") - } - - indexName := command.index - if indexName == "" { - indexName = g.CreateIndexName(blueprint, "primary", command.columns...) - } - - return fmt.Sprintf( - "ALTER TABLE %s ADD CONSTRAINT %s PRIMARY KEY (%s)", - blueprint.name, - indexName, - g.Columnize(command.columns), - ), nil -} - -func (g *mysqlGrammar) CompileDropIndex(blueprint *Blueprint, command *command) (string, error) { - if command.index == "" { - return "", errors.New("index name cannot be empty") - } - return fmt.Sprintf("ALTER TABLE %s DROP INDEX %s", blueprint.name, command.index), nil -} - -func (g *mysqlGrammar) CompileDropUnique(blueprint *Blueprint, command *command) (string, error) { - if command.index == "" { - return "", errors.New("unique index name cannot be empty") - } - return fmt.Sprintf("ALTER TABLE %s DROP INDEX %s", blueprint.name, command.index), nil -} - -func (g *mysqlGrammar) CompileDropFulltext(blueprint *Blueprint, command *command) (string, error) { - return g.CompileDropIndex(blueprint, command) -} - -func (g *mysqlGrammar) CompileDropPrimary(blueprint *Blueprint, _ *command) (string, error) { - return fmt.Sprintf("ALTER TABLE %s DROP PRIMARY KEY", blueprint.name), nil -} - -func (g *mysqlGrammar) CompileRenameIndex(blueprint *Blueprint, command *command) (string, error) { - if command.from == "" || command.to == "" { - return "", errors.New("old and new index names cannot be empty") - } - return fmt.Sprintf("ALTER TABLE %s RENAME INDEX %s TO %s", blueprint.name, command.from, command.to), nil -} - -func (g *mysqlGrammar) CompileDropForeign(blueprint *Blueprint, command *command) (string, error) { - if command.index == "" { - return "", errors.New("foreign key name cannot be empty") - } - return fmt.Sprintf("ALTER TABLE %s DROP FOREIGN KEY %s", blueprint.name, command.index), nil -} - -func (g *mysqlGrammar) GetFluentCommands() []func(*Blueprint, *command) string { - return []func(*Blueprint, *command) string{} -} - -func (g *mysqlGrammar) getColumns(blueprint *Blueprint) ([]string, error) { - var columns []string - for _, col := range blueprint.getAddedColumns() { - if col.name == "" { - return nil, errors.New("column name cannot be empty") - } - sql := col.name + " " + g.getType(col) - sql += g.modifyUnsigned(col) - sql += g.modifyIncrement(col) - sql += g.modifyDefault(col) - sql += g.modifyOnUpdate(col) - sql += g.modifyCharset(col) - sql += g.modifyCollate(col) - sql += g.modifyNullable(col) - sql += g.modifyComment(col) - - columns = append(columns, sql) - } - - return columns, nil -} - -func (g *mysqlGrammar) getConstraints(blueprint *Blueprint) []string { - var constrains []string - for _, col := range blueprint.getAddedColumns() { - if col.primary != nil && *col.primary { - pkConstraintName := g.CreateIndexName(blueprint, "primary") - sql := "CONSTRAINT " + pkConstraintName + " PRIMARY KEY (" + col.name + ")" - constrains = append(constrains, sql) - continue - } - } - - return constrains -} - -//nolint:dupl // Similar code exists in other grammar files -func (g *mysqlGrammar) getType(col *columnDefinition) string { - typeFuncMap := map[string]func(*columnDefinition) string{ - columnTypeChar: g.typeChar, - columnTypeString: g.typeString, - columnTypeTinyText: g.typeTinyText, - columnTypeText: g.typeText, - columnTypeMediumText: g.typeMediumText, - columnTypeLongText: g.typeLongText, - columnTypeInteger: g.typeInteger, - columnTypeBigInteger: g.typeBigInteger, - columnTypeMediumInteger: g.typeMediumInteger, - columnTypeSmallInteger: g.typeSmallInteger, - columnTypeTinyInteger: g.typeTinyInteger, - columnTypeFloat: g.typeFloat, - columnTypeDouble: g.typeDouble, - columnTypeDecimal: g.typeDecimal, - columnTypeBoolean: g.typeBoolean, - columnTypeEnum: g.typeEnum, - columnTypeJSON: g.typeJSON, - columnTypeJSONB: g.typeJSONB, - columnTypeDate: g.typeDate, - columnTypeDateTime: g.typeDateTime, - columnTypeDateTimeTz: g.typeDateTimeTz, - columnTypeTime: g.typeTime, - columnTypeTimeTz: g.typeTimeTz, - columnTypeTimestamp: g.typeTimestamp, - columnTypeTimestampTz: g.typeTimestampTz, - columnTypeYear: g.typeYear, - columnTypeBinary: g.typeBinary, - columnTypeUUID: g.typeUUID, - columnTypeGeography: g.typeGeography, - columnTypeGeometry: g.typeGeometry, - columnTypePoint: g.typePoint, - } - if fn, ok := typeFuncMap[col.columnType]; ok { - return fn(col) - } - return col.columnType -} - -func (g *mysqlGrammar) typeChar(col *columnDefinition) string { - return fmt.Sprintf("CHAR(%d)", *col.length) -} - -func (g *mysqlGrammar) typeString(col *columnDefinition) string { - return fmt.Sprintf("VARCHAR(%d)", *col.length) -} - -func (g *mysqlGrammar) typeTinyText(_ *columnDefinition) string { - return "TINYTEXT" -} - -func (g *mysqlGrammar) typeText(_ *columnDefinition) string { - return "TEXT" -} - -func (g *mysqlGrammar) typeMediumText(_ *columnDefinition) string { - return "MEDIUMTEXT" -} - -func (g *mysqlGrammar) typeLongText(_ *columnDefinition) string { - return "LONGTEXT" -} - -func (g *mysqlGrammar) typeBigInteger(_ *columnDefinition) string { - return "BIGINT" -} - -func (g *mysqlGrammar) typeInteger(_ *columnDefinition) string { - return "INT" -} - -func (g *mysqlGrammar) typeMediumInteger(_ *columnDefinition) string { - return "MEDIUMINT" -} - -func (g *mysqlGrammar) typeSmallInteger(_ *columnDefinition) string { - return "SMALLINT" -} - -func (g *mysqlGrammar) typeTinyInteger(_ *columnDefinition) string { - return "TINYINT" -} - -func (g *mysqlGrammar) typeFloat(col *columnDefinition) string { - if col.precision != nil && *col.precision > 0 { - return fmt.Sprintf("FLOAT(%d)", *col.precision) - } - return "FLOAT" -} - -func (g *mysqlGrammar) typeDouble(_ *columnDefinition) string { - return "DOUBLE" -} - -func (g *mysqlGrammar) typeDecimal(col *columnDefinition) string { - return fmt.Sprintf("DECIMAL(%d, %d)", *col.total, *col.places) -} - -func (g *mysqlGrammar) typeBoolean(_ *columnDefinition) string { - return "TINYINT(1)" -} - -func (g *mysqlGrammar) typeEnum(col *columnDefinition) string { - allowedValues := make([]string, len(col.allowed)) - for i, e := range col.allowed { - allowedValues[i] = g.QuoteString(e) - } - return fmt.Sprintf("ENUM(%s)", strings.Join(allowedValues, ", ")) -} - -func (g *mysqlGrammar) typeJSON(_ *columnDefinition) string { - return "JSON" -} - -func (g *mysqlGrammar) typeJSONB(_ *columnDefinition) string { - return "JSON" -} - -func (g *mysqlGrammar) typeDate(_ *columnDefinition) string { - return "DATE" -} - -func (g *mysqlGrammar) typeDateTime(col *columnDefinition) string { - current := "CURRENT_TIMESTAMP" - if col.precision != nil && *col.precision > 0 { - current = fmt.Sprintf("CURRENT_TIMESTAMP(%d)", *col.precision) - } - if col.useCurrent { - col.SetDefault(Expression(current)) - } - if col.useCurrentOnUpdate { - col.SetOnUpdate(Expression(current)) - } - if col.precision != nil && *col.precision > 0 { - return fmt.Sprintf("DATETIME(%d)", *col.precision) - } - return "DATETIME" -} - -func (g *mysqlGrammar) typeDateTimeTz(col *columnDefinition) string { - return g.typeDateTime(col) -} - -func (g *mysqlGrammar) typeTime(col *columnDefinition) string { - if col.precision != nil && *col.precision > 0 { - return fmt.Sprintf("TIME(%d)", *col.precision) - } - return "TIME" -} - -func (g *mysqlGrammar) typeTimeTz(col *columnDefinition) string { - return g.typeTime(col) -} - -func (g *mysqlGrammar) typeTimestamp(col *columnDefinition) string { - current := "CURRENT_TIMESTAMP" - if col.precision != nil && *col.precision > 0 { - current = fmt.Sprintf("CURRENT_TIMESTAMP(%d)", *col.precision) - } - if col.useCurrent { - col.SetDefault(Expression(current)) - } - if col.useCurrentOnUpdate { - col.SetOnUpdate(Expression(current)) - } - if col.precision != nil && *col.precision > 0 { - return fmt.Sprintf("TIMESTAMP(%d)", *col.precision) - } - return "TIMESTAMP" -} - -func (g *mysqlGrammar) typeTimestampTz(col *columnDefinition) string { - return g.typeTimestamp(col) -} - -func (g *mysqlGrammar) typeYear(_ *columnDefinition) string { - return "YEAR" -} - -func (g *mysqlGrammar) typeBinary(col *columnDefinition) string { - if col.length != nil && *col.length > 0 { - return fmt.Sprintf("BINARY(%d)", *col.length) - } - return "BLOB" -} - -func (g *mysqlGrammar) typeUUID(_ *columnDefinition) string { - return "CHAR(36)" // Default UUID length -} - -func (g *mysqlGrammar) typeGeometry(col *columnDefinition) string { - subtype := util.Ternary(col.subtype != nil, util.PtrOf(strings.ToUpper(*col.subtype)), nil) - if subtype != nil { - if !slices.Contains( - []string{"POINT", "LINESTRING", "POLYGON", "GEOMETRYCOLLECTION", "MULTIPOINT", "MULTILINESTRING"}, - *subtype, - ) { - subtype = nil - } - } - - if subtype == nil { - subtype = util.PtrOf("GEOMETRY") - } - if col.srid != nil && *col.srid > 0 { - return fmt.Sprintf("%s SRID %d", *subtype, *col.srid) - } - return *subtype -} - -func (g *mysqlGrammar) typeGeography(col *columnDefinition) string { - return g.typeGeometry(col) -} - -func (g *mysqlGrammar) typePoint(col *columnDefinition) string { - col.SetSubtype(util.PtrOf("POINT")) - return g.typeGeometry(col) -} - -func (g *mysqlGrammar) modifiers() []func(*columnDefinition) string { - return []func(*columnDefinition) string{ - g.modifyUnsigned, - g.modifyCharset, - g.modifyCollate, - g.modifyNullable, - g.modifyDefault, - g.modifyOnUpdate, - g.modifyIncrement, - g.modifyComment, - } -} - -func (g *mysqlGrammar) modifyCharset(col *columnDefinition) string { - if col.charset != nil && *col.charset != "" { - return fmt.Sprintf(" CHARACTER SET %s", *col.charset) - } - return "" -} - -func (g *mysqlGrammar) modifyCollate(col *columnDefinition) string { - if col.collation != nil && *col.collation != "" { - return fmt.Sprintf(" COLLATE %s", *col.collation) - } - return "" -} - -func (g *mysqlGrammar) modifyComment(col *columnDefinition) string { - if col.comment != nil { - return fmt.Sprintf(" COMMENT '%s'", *col.comment) - } - return "" -} - -func (g *mysqlGrammar) modifyDefault(col *columnDefinition) string { - if col.hasCommand("default") { - return fmt.Sprintf(" DEFAULT %s", g.GetDefaultValue(col.defaultValue)) - } - return "" -} - -func (g *mysqlGrammar) modifyIncrement(col *columnDefinition) string { - if slices.Contains(g.serials, col.columnType) && - col.autoIncrement != nil && *col.autoIncrement && - col.primary != nil && *col.primary { - return " AUTO_INCREMENT" - } - return "" -} - -func (g *mysqlGrammar) modifyNullable(col *columnDefinition) string { - if col.nullable != nil && *col.nullable { - return " NULL" - } - return " NOT NULL" -} - -func (g *mysqlGrammar) modifyOnUpdate(col *columnDefinition) string { - if col.hasCommand("onUpdate") { - return fmt.Sprintf(" ON UPDATE %s", g.GetValue(col.onUpdateValue)) - } - return "" -} - -func (g *mysqlGrammar) modifyUnsigned(col *columnDefinition) string { - if col.unsigned != nil && *col.unsigned { - return " UNSIGNED" - } - return "" -} diff --git a/schema/postgres_builder.go b/schema/postgres_builder.go deleted file mode 100644 index 342ad62..0000000 --- a/schema/postgres_builder.go +++ /dev/null @@ -1,238 +0,0 @@ -package schema - -import ( - "database/sql" - "errors" - "strings" -) - -type postgresBuilder struct { - baseBuilder -} - -func newPostgresBuilder() Builder { - grammar := newPostgresGrammar() - - return &postgresBuilder{ - baseBuilder: baseBuilder{grammar: grammar}, - } -} - -func (b *postgresBuilder) parseSchemaAndTable(name string) (string, string) { - names := strings.Split(name, ".") - if len(names) == 2 { - return names[0], names[1] - } - return "", names[0] -} - -const defaultPostgresSchema = "public" - -func (b *postgresBuilder) GetColumns(c Context, tableName string) ([]*Column, error) { - if c == nil || tableName == "" { - return nil, errors.New("invalid arguments: context is nil or table name is empty") - } - - schema, name := b.parseSchemaAndTable(tableName) - if schema == "" { - schema = defaultPostgresSchema - } - query, err := b.grammar.CompileColumns(schema, name) - if err != nil { - return nil, err - } - - rows, err := c.Query(query) - if err != nil { - return nil, err - } - defer rows.Close() - - var columns []*Column - for rows.Next() { - var col Column - if err = rows.Scan( - &col.Name, &col.TypeName, &col.TypeFull, &col.Collation, - &col.Nullable, &col.DefaultVal, &col.Comment, - ); err != nil { - return nil, err - } - columns = append(columns, &col) - } - if err = rows.Err(); err != nil { - return nil, err - } - - return columns, nil -} - -func (b *postgresBuilder) GetIndexes(c Context, tableName string) ([]*Index, error) { - if c == nil || tableName == "" { - return nil, errors.New("invalid arguments: context is nil or table name is empty") - } - schema, name := b.parseSchemaAndTable(tableName) - if schema == "" { - schema = "public" // Default schema for PostgreSQL - } - query, err := b.grammar.CompileIndexes(schema, name) - if err != nil { - return nil, err - } - rows, err := c.Query(query) - if err != nil { - return nil, err - } - defer rows.Close() - - var indexes []*Index - for rows.Next() { - var index Index - var columnsStr string - if err = rows.Scan(&index.Name, &columnsStr, &index.Type, &index.Unique, &index.Primary); err != nil { - return nil, err - } - index.Columns = strings.Split(columnsStr, ",") - indexes = append(indexes, &index) - } - if err = rows.Err(); err != nil { - return nil, err - } - - return indexes, nil -} - -func (b *postgresBuilder) GetTables(c Context) ([]*TableInfo, error) { - if c == nil { - return nil, errors.New("invalid arguments: context is nil") - } - - query, err := b.grammar.CompileTables("") - if err != nil { - return nil, err - } - - rows, err := c.Query(query) - if err != nil { - return nil, err - } - defer rows.Close() - - var tables []*TableInfo - for rows.Next() { - var table TableInfo - if err = rows.Scan(&table.Name, &table.Schema, &table.Size, &table.Comment); err != nil { - return nil, err - } - tables = append(tables, &table) - } - if err = rows.Err(); err != nil { - return nil, err - } - - return tables, nil -} - -func (b *postgresBuilder) HasColumn(c Context, tableName string, columnName string) (bool, error) { - return b.HasColumns(c, tableName, []string{columnName}) -} - -func (b *postgresBuilder) HasColumns(c Context, tableName string, columnNames []string) (bool, error) { - if c == nil || tableName == "" { - return false, errors.New("invalid arguments: context is nil or table name is empty") - } - if len(columnNames) == 0 { - return false, errors.New("no column names provided") - } - existingColumns, err := b.GetColumns(c, tableName) - if err != nil { - return false, err - } - if len(existingColumns) == 0 { - return false, nil // No columns found, so the specified columns cannot exist - } - if len(columnNames) == 0 { - return true, nil // No specific columns to check, so we assume they exist - } - existingColumnMap := make(map[string]bool) - for _, col := range existingColumns { - existingColumnMap[col.Name] = true - } - for _, colName := range columnNames { - if colName == "" { - return false, errors.New("column name is empty") - } - if _, exists := existingColumnMap[colName]; !exists { - return false, nil // If any specified column does not exist, return false - } - } - return true, nil // All specified columns exist -} - -//nolint:dupl // Similar code exists in other builder files -func (b *postgresBuilder) HasIndex(c Context, tableName string, indexes []string) (bool, error) { - if c == nil || tableName == "" { - return false, errors.New("invalid arguments: context is nil or table name is empty") - } - - existingIndexes, err := b.GetIndexes(c, tableName) - if err != nil { - return false, err - } - if len(existingIndexes) == 0 { - return false, nil // No indexes found, so the specified indexes cannot exist - } - if len(indexes) == 0 { - return true, nil // No specific indexes to check, so we assume they exist - } - if len(indexes) == 1 { - for _, idx := range existingIndexes { - if idx.Name == indexes[0] { - return true, nil // If any specified index exists, return true - } - } - } - indexColumns := make(map[string]bool) - for _, idx := range indexes { - indexColumns[idx] = true // Create a map of specified indexes for quick lookup - } - - for _, index := range existingIndexes { - found := true - for _, indexCol := range index.Columns { - if _, exists := indexColumns[indexCol]; !exists { - found = false // If any column in the index does not match the specified indexes, set found to false - break - } - } - // If all columns in the index match the specified indexes, we found a match - if found { - return true, nil - } - } - - return false, nil // If no specified index exists, return false -} - -func (b *postgresBuilder) HasTable(c Context, name string) (bool, error) { - if c == nil || name == "" { - return false, errors.New("invalid arguments: context is nil or table name is empty") - } - - schema, name := b.parseSchemaAndTable(name) - if schema == "" { - schema = "public" // Default schema for PostgreSQL - } - query, err := b.grammar.CompileTableExists(schema, name) - if err != nil { - return false, err - } - - var exists bool - if err = c.QueryRow(query).Scan(&exists); err != nil { - if errors.Is(err, sql.ErrNoRows) { - return false, nil // Table does not exist - } - return false, err // Other error - } - return exists, nil -} diff --git a/schema/postgres_grammar.go b/schema/postgres_grammar.go deleted file mode 100644 index c9af09b..0000000 --- a/schema/postgres_grammar.go +++ /dev/null @@ -1,598 +0,0 @@ -package schema - -import ( - "errors" - "fmt" - "slices" - "strings" -) - -type postgresGrammar struct { - baseGrammar -} - -func newPostgresGrammar() *postgresGrammar { - return &postgresGrammar{} -} - -func (g *postgresGrammar) CompileTableExists(schema string, table string) (string, error) { - return fmt.Sprintf( - "SELECT 1 FROM information_schema.tables WHERE table_schema = %s AND table_name = %s AND table_type = 'BASE TABLE'", - g.QuoteString(schema), - g.QuoteString(table), - ), nil -} - -func (g *postgresGrammar) CompileTables(_ string) (string, error) { - return "select c.relname as name, n.nspname as schema, pg_total_relation_size(c.oid) as size, " + - "obj_description(c.oid, 'pg_class') as comment from pg_class c, pg_namespace n " + - "where c.relkind in ('r', 'p') and n.oid = c.relnamespace and n.nspname not in ('pg_catalog', 'information_schema') " + - "order by c.relname", nil -} - -func (g *postgresGrammar) CompileColumns(schema, table string) (string, error) { - return fmt.Sprintf( - "select a.attname as name, t.typname as type_name, format_type(a.atttypid, a.atttypmod) as type, "+ - "(select tc.collcollate from pg_catalog.pg_collation tc where tc.oid = a.attcollation) as collation, "+ - "not a.attnotnull as nullable, "+ - "(select pg_get_expr(adbin, adrelid) from pg_attrdef where c.oid = pg_attrdef.adrelid and pg_attrdef.adnum = a.attnum) as default, "+ - "col_description(c.oid, a.attnum) as comment "+ - "from pg_attribute a, pg_class c, pg_type t, pg_namespace n "+ - "where c.relname = %s and n.nspname = %s and a.attnum > 0 and a.attrelid = c.oid and a.atttypid = t.oid and n.oid = c.relnamespace "+ - "order by a.attnum", - g.QuoteString(table), - g.QuoteString(schema), - ), nil -} - -func (g *postgresGrammar) CompileIndexes(schema, table string) (string, error) { - return fmt.Sprintf( - "select ic.relname as name, string_agg(a.attname, ',' order by indseq.ord) as columns, "+ - "am.amname as \"type\", i.indisunique as \"unique\", i.indisprimary as \"primary\" "+ - "from pg_index i "+ - "join pg_class tc on tc.oid = i.indrelid "+ - "join pg_namespace tn on tn.oid = tc.relnamespace "+ - "join pg_class ic on ic.oid = i.indexrelid "+ - "join pg_am am on am.oid = ic.relam "+ - "join lateral unnest(i.indkey) with ordinality as indseq(num, ord) on true "+ - "left join pg_attribute a on a.attrelid = i.indrelid and a.attnum = indseq.num "+ - "where tc.relname = %s and tn.nspname = %s "+ - "group by ic.relname, am.amname, i.indisunique, i.indisprimary", - g.QuoteString(table), - g.QuoteString(schema), - ), nil -} - -func (g *postgresGrammar) CompileCreate(blueprint *Blueprint) (string, error) { - columns, err := g.getColumns(blueprint) - if err != nil { - return "", err - } - columns = append(columns, g.getConstraints(blueprint)...) - return fmt.Sprintf("CREATE TABLE %s (%s)", blueprint.name, strings.Join(columns, ", ")), nil -} - -func (g *postgresGrammar) CompileAdd(blueprint *Blueprint) (string, error) { - if len(blueprint.getAddedColumns()) == 0 { - return "", nil - } - - columns, err := g.getColumns(blueprint) - if err != nil { - return "", err - } - columns = g.PrefixArray("ADD COLUMN ", columns) - constraints := g.getConstraints(blueprint) - if len(constraints) > 0 { - constraints = g.PrefixArray("ADD ", constraints) - columns = append(columns, constraints...) - } - - return fmt.Sprintf("ALTER TABLE %s %s", - blueprint.name, - strings.Join(columns, ", "), - ), nil -} - -func (g *postgresGrammar) CompileChange(bp *Blueprint, command *command) (string, error) { - column := command.column - if column.name == "" { - return "", errors.New("column name cannot be empty for change operation") - } - - var changes []string - changes = append(changes, fmt.Sprintf("TYPE %s", g.getType(command.column))) - for _, modifier := range g.modifiers() { - change := modifier(command.column) - if change != "" { - changes = append(changes, strings.TrimSpace(change)) - } - } - - return fmt.Sprintf("ALTER TABLE %s %s", - bp.name, - strings.Join(g.PrefixArray(fmt.Sprintf("ALTER COLUMN %s ", column.name), changes), ", "), - ), nil -} - -func (g *postgresGrammar) CompileDrop(blueprint *Blueprint) (string, error) { - return fmt.Sprintf("DROP TABLE %s", blueprint.name), nil -} - -func (g *postgresGrammar) CompileDropIfExists(blueprint *Blueprint) (string, error) { - return fmt.Sprintf("DROP TABLE IF EXISTS %s", blueprint.name), nil -} - -func (g *postgresGrammar) CompileRename(blueprint *Blueprint, command *command) (string, error) { - return fmt.Sprintf("ALTER TABLE %s RENAME TO %s", blueprint.name, command.to), nil -} - -func (g *postgresGrammar) CompileDropColumn(blueprint *Blueprint, command *command) (string, error) { - if len(command.columns) == 0 { - return "", nil - } - columns := g.PrefixArray("DROP COLUMN ", command.columns) - - return fmt.Sprintf("ALTER TABLE %s %s", blueprint.name, strings.Join(columns, ", ")), nil -} - -func (g *postgresGrammar) CompileRenameColumn(blueprint *Blueprint, command *command) (string, error) { - if command.from == "" || command.to == "" { - return "", errors.New("table name, old column name, and new column name cannot be empty for rename operation") - } - return fmt.Sprintf("ALTER TABLE %s RENAME COLUMN %s TO %s", blueprint.name, command.from, command.to), nil -} - -func (g *postgresGrammar) CompileFullText(blueprint *Blueprint, command *command) (string, error) { - if slices.Contains(command.columns, "") { - return "", errors.New("fulltext index column cannot be empty") - } - indexName := command.index - if indexName == "" { - indexName = g.CreateIndexName(blueprint, "fulltext", command.columns...) - } - language := command.language - if language == "" { - language = "english" // Default language for full-text search - } - var columns []string - for _, col := range command.columns { - columns = append(columns, fmt.Sprintf("to_tsvector(%s, %s)", g.QuoteString(language), col)) - } - - return fmt.Sprintf( - "CREATE INDEX %s ON %s USING GIN (%s)", - indexName, - blueprint.name, - strings.Join(columns, " || "), - ), nil -} - -func (g *postgresGrammar) CompileIndex(blueprint *Blueprint, command *command) (string, error) { - if slices.Contains(command.columns, "") { - return "", errors.New("index column cannot be empty") - } - indexName := command.index - if indexName == "" { - indexName = g.CreateIndexName(blueprint, "index", command.columns...) - } - - sql := fmt.Sprintf("CREATE INDEX %s ON %s", indexName, blueprint.name) - if command.algorithm != "" { - sql += fmt.Sprintf(" USING %s", command.algorithm) - } - return fmt.Sprintf("%s (%s)", sql, g.Columnize(command.columns)), nil -} - -func (g *postgresGrammar) CompileUnique(blueprint *Blueprint, command *command) (string, error) { - if slices.Contains(command.columns, "") { - return "", errors.New("unique index column cannot be empty") - } - indexName := command.index - if indexName == "" { - indexName = g.CreateIndexName(blueprint, "unique", command.columns...) - } - sql := fmt.Sprintf("ALTER TABLE %s ADD CONSTRAINT %s UNIQUE (%s)", - blueprint.name, - indexName, - g.Columnize(command.columns), - ) - - if command.deferrable != nil { - if *command.deferrable { - sql += " DEFERRABLE" - } else { - sql += " NOT DEFERRABLE" - } - } - if command.deferrable != nil && *command.deferrable && command.initiallyImmediate != nil { - if *command.initiallyImmediate { - sql += " INITIALLY IMMEDIATE" - } else { - sql += " INITIALLY DEFERRED" - } - } - - return sql, nil -} - -func (g *postgresGrammar) CompilePrimary(blueprint *Blueprint, command *command) (string, error) { - if slices.Contains(command.columns, "") { - return "", errors.New("primary key index column cannot be empty") - } - indexName := command.index - if indexName == "" { - indexName = g.CreateIndexName(blueprint, "primary", command.columns...) - } - return fmt.Sprintf( - "ALTER TABLE %s ADD CONSTRAINT %s PRIMARY KEY (%s)", - blueprint.name, - indexName, - g.Columnize(command.columns), - ), nil -} - -func (g *postgresGrammar) CompileDropIndex(_ *Blueprint, command *command) (string, error) { - if command.index == "" { - return "", errors.New("index name cannot be empty for drop operation") - } - return fmt.Sprintf("DROP INDEX %s", command.index), nil -} - -func (g *postgresGrammar) CompileDropFulltext(blueprint *Blueprint, command *command) (string, error) { - return g.CompileDropIndex(blueprint, command) -} - -func (g *postgresGrammar) CompileDropUnique(blueprint *Blueprint, command *command) (string, error) { - if command.index == "" { - return "", errors.New("index name cannot be empty for drop operation") - } - return fmt.Sprintf("ALTER TABLE %s DROP CONSTRAINT %s", blueprint.name, command.index), nil -} - -func (g *postgresGrammar) CompileDropPrimary(blueprint *Blueprint, command *command) (string, error) { - index := command.index - if index == "" { - index = g.CreateIndexName(blueprint, "primary", command.columns...) - } - return fmt.Sprintf("ALTER TABLE %s DROP CONSTRAINT %s", blueprint.name, index), nil -} - -func (g *postgresGrammar) CompileRenameIndex(_ *Blueprint, command *command) (string, error) { - if command.from == "" || command.to == "" { - return "", fmt.Errorf( - "index names for rename operation cannot be empty: oldName=%s, newName=%s", - command.from, - command.to, - ) - } - return fmt.Sprintf("ALTER INDEX %s RENAME TO %s", command.from, command.to), nil -} - -func (g *postgresGrammar) CompileForeign(blueprint *Blueprint, command *command) (string, error) { - sql, err := g.baseGrammar.CompileForeign(blueprint, command) - if err != nil { - return "", err - } - - if command.deferrable != nil { - if *command.deferrable { - sql += " DEFERRABLE" - } else { - sql += " NOT DEFERRABLE" - } - } - if command.deferrable != nil && *command.deferrable && command.initiallyImmediate != nil { - if *command.initiallyImmediate { - sql += " INITIALLY IMMEDIATE" - } else { - sql += " INITIALLY DEFERRED" - } - } - - return sql, nil -} - -func (g *postgresGrammar) CompileDropForeign(blueprint *Blueprint, command *command) (string, error) { - if command.index == "" { - return "", errors.New("foreign key name cannot be empty for drop operation") - } - return fmt.Sprintf("ALTER TABLE %s DROP CONSTRAINT %s", blueprint.name, command.index), nil -} - -func (g *postgresGrammar) GetFluentCommands() []func(blueprint *Blueprint, command *command) string { - return []func(blueprint *Blueprint, command *command) string{ - g.CompileComment, - } -} - -func (g *postgresGrammar) CompileComment(blueprint *Blueprint, command *command) string { - if command.column.comment != nil { - sql := fmt.Sprintf("COMMENT ON COLUMN %s.%s IS ", blueprint.name, command.column.name) - if command.column.comment == nil { - return sql + "NULL" - } - return sql + fmt.Sprintf("'%s'", *command.column.comment) - } - return "" -} - -func (g *postgresGrammar) getColumns(blueprint *Blueprint) ([]string, error) { - var columns []string - for _, col := range blueprint.getAddedColumns() { - if col.name == "" { - return nil, errors.New("column name cannot be empty") - } - sql := col.name + " " + g.getType(col) - var sqlBuilder strings.Builder - for _, modifier := range g.modifiers() { - sqlBuilder.WriteString(modifier(col)) - } - sql += sqlBuilder.String() - columns = append(columns, sql) - } - - return columns, nil -} - -func (g *postgresGrammar) getConstraints(blueprint *Blueprint) []string { - var constrains []string - for _, col := range blueprint.getAddedColumns() { - if col.primary != nil && *col.primary { - pkConstraintName := g.CreateIndexName(blueprint, "primary") - sql := "CONSTRAINT " + pkConstraintName + " PRIMARY KEY (" + col.name + ")" - constrains = append(constrains, sql) - continue - } - } - - return constrains -} - -//nolint:dupl // Similar code exists in other grammar files -func (g *postgresGrammar) getType(col *columnDefinition) string { - typeMapFunc := map[string]func(*columnDefinition) string{ - columnTypeChar: g.typeChar, - columnTypeString: g.typeString, - columnTypeTinyText: g.typeTinyText, - columnTypeText: g.typeText, - columnTypeMediumText: g.typeMediumText, - columnTypeLongText: g.typeLongText, - columnTypeInteger: g.typeInteger, - columnTypeBigInteger: g.typeBigInteger, - columnTypeMediumInteger: g.typeMediumInteger, - columnTypeSmallInteger: g.typeSmallInteger, - columnTypeTinyInteger: g.typeTinyInteger, - columnTypeFloat: g.typeFloat, - columnTypeDouble: g.typeDouble, - columnTypeDecimal: g.typeDecimal, - columnTypeBoolean: g.typeBoolean, - columnTypeEnum: g.typeEnum, - columnTypeJSON: g.typeJSON, - columnTypeJSONB: g.typeJSONB, - columnTypeDate: g.typeDate, - columnTypeDateTime: g.typeDateTime, - columnTypeDateTimeTz: g.typeDateTimeTz, - columnTypeTime: g.typeTime, - columnTypeTimeTz: g.typeTimeTz, - columnTypeTimestamp: g.typeTimestamp, - columnTypeTimestampTz: g.typeTimestampTz, - columnTypeYear: g.typeYear, - columnTypeBinary: g.typeBinary, - columnTypeUUID: g.typeUUID, - columnTypeGeography: g.typeGeography, - columnTypeGeometry: g.typeGeometry, - columnTypePoint: g.typePoint, - } - if fn, ok := typeMapFunc[col.columnType]; ok { - return fn(col) - } - return col.columnType -} - -func (g *postgresGrammar) typeChar(col *columnDefinition) string { - if col.length != nil && *col.length > 0 { - return fmt.Sprintf("CHAR(%d)", *col.length) - } - return "CHAR" -} - -func (g *postgresGrammar) typeString(col *columnDefinition) string { - if col.length != nil && *col.length > 0 { - return fmt.Sprintf("VARCHAR(%d)", *col.length) - } - return "VARCHAR" -} - -func (g *postgresGrammar) typeTinyText(_ *columnDefinition) string { - return "VARCHAR(255)" -} - -func (g *postgresGrammar) typeText(_ *columnDefinition) string { - return "TEXT" -} - -func (g *postgresGrammar) typeMediumText(_ *columnDefinition) string { - return "TEXT" -} - -func (g *postgresGrammar) typeLongText(_ *columnDefinition) string { - return "TEXT" -} - -func (g *postgresGrammar) typeBigInteger(col *columnDefinition) string { - if col.autoIncrement != nil && *col.autoIncrement { - return "BIGSERIAL" - } - return "BIGINT" -} - -func (g *postgresGrammar) typeInteger(col *columnDefinition) string { - if col.autoIncrement != nil && *col.autoIncrement { - return "SERIAL" - } - return "INTEGER" -} - -func (g *postgresGrammar) typeMediumInteger(col *columnDefinition) string { - return g.typeInteger(col) -} - -func (g *postgresGrammar) typeSmallInteger(col *columnDefinition) string { - if col.autoIncrement != nil && *col.autoIncrement { - return "SMALLSERIAL" - } - return "SMALLINT" -} - -func (g *postgresGrammar) typeTinyInteger(col *columnDefinition) string { - return g.typeSmallInteger(col) -} - -func (g *postgresGrammar) typeFloat(_ *columnDefinition) string { - return "REAL" -} - -func (g *postgresGrammar) typeDouble(_ *columnDefinition) string { - return "DOUBLE PRECISION" -} - -func (g *postgresGrammar) typeDecimal(col *columnDefinition) string { - return fmt.Sprintf("DECIMAL(%d, %d)", *col.total, *col.places) -} - -func (g *postgresGrammar) typeBoolean(_ *columnDefinition) string { - return "BOOLEAN" -} - -func (g *postgresGrammar) typeEnum(col *columnDefinition) string { - enumValues := make([]string, len(col.allowed)) - for i, v := range col.allowed { - enumValues[i] = g.QuoteString(v) - } - return "VARCHAR(255) CHECK (" + col.name + " IN (" + strings.Join(enumValues, ", ") + "))" -} - -func (g *postgresGrammar) typeJSON(_ *columnDefinition) string { - return "JSON" -} - -func (g *postgresGrammar) typeJSONB(_ *columnDefinition) string { - return "JSONB" -} - -func (g *postgresGrammar) typeDate(_ *columnDefinition) string { - return "DATE" -} - -func (g *postgresGrammar) typeDateTime(col *columnDefinition) string { - return g.typeTimestamp(col) -} - -func (g *postgresGrammar) typeDateTimeTz(col *columnDefinition) string { - return g.typeTimestampTz(col) -} - -func (g *postgresGrammar) typeTime(col *columnDefinition) string { - if col.precision != nil { - return fmt.Sprintf("TIME(%d)", *col.precision) - } - return "TIME" -} - -func (g *postgresGrammar) typeTimeTz(col *columnDefinition) string { - if col.precision != nil { - return fmt.Sprintf("TIMETZ(%d)", *col.precision) - } - return "TIMETZ" -} - -func (g *postgresGrammar) typeTimestamp(col *columnDefinition) string { - if col.useCurrent { - col.SetDefault(Expression("CURRENT_TIMESTAMP")) - } - if col.precision != nil { - return fmt.Sprintf("TIMESTAMP(%d)", *col.precision) - } - return "TIMESTAMP" -} - -func (g *postgresGrammar) typeTimestampTz(col *columnDefinition) string { - if col.useCurrent { - col.SetDefault(Expression("CURRENT_TIMESTAMP")) - } - if col.precision != nil { - return fmt.Sprintf("TIMESTAMPTZ(%d)", *col.precision) - } - return "TIMESTAMPTZ" -} - -func (g *postgresGrammar) typeYear(_ *columnDefinition) string { - return "INTEGER" -} - -func (g *postgresGrammar) typeBinary(_ *columnDefinition) string { - return "BYTEA" -} - -func (g *postgresGrammar) typeUUID(_ *columnDefinition) string { - return "UUID" -} - -func (g *postgresGrammar) typeGeography(col *columnDefinition) string { - if col.subtype != nil && col.srid != nil { - return fmt.Sprintf("GEOGRAPHY(%s, %d)", *col.subtype, *col.srid) - } else if col.subtype != nil { - return fmt.Sprintf("GEOGRAPHY(%s)", *col.subtype) - } - return "GEOGRAPHY" -} - -func (g *postgresGrammar) typeGeometry(col *columnDefinition) string { - if col.subtype != nil && col.srid != nil { - return fmt.Sprintf("GEOMETRY(%s, %d)", *col.subtype, *col.srid) - } else if col.subtype != nil { - return fmt.Sprintf("GEOMETRY(%s)", *col.subtype) - } - return "GEOMETRY" -} - -func (g *postgresGrammar) typePoint(col *columnDefinition) string { - if col.srid != nil { - return fmt.Sprintf("POINT(%d)", *col.srid) - } - return "POINT" -} - -func (g *postgresGrammar) modifiers() []func(*columnDefinition) string { - return []func(*columnDefinition) string{ - g.modifyDefault, - g.modifyNullable, - } -} - -func (g *postgresGrammar) modifyNullable(col *columnDefinition) string { - if col.change { - if col.nullable == nil { - return "" - } - if *col.nullable { - return " DROP NOT NULL" - } - return " SET NOT NULL" - } - if col.nullable != nil && *col.nullable { - return " NULL" - } - return " NOT NULL" -} - -func (g *postgresGrammar) modifyDefault(col *columnDefinition) string { - if col.hasCommand("default") { - if col.change { - return fmt.Sprintf(" SET DEFAULT %s", g.GetDefaultValue(col.defaultValue)) - } - return fmt.Sprintf(" DEFAULT %s", g.GetDefaultValue(col.defaultValue)) - } - return "" -} diff --git a/schema/postgres_grammar_test.go b/schema/postgres_grammar_test.go deleted file mode 100644 index adbaf44..0000000 --- a/schema/postgres_grammar_test.go +++ /dev/null @@ -1,1791 +0,0 @@ -package schema //nolint:testpackage // Need to access unexported members for testing - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestPgGrammar_CompileCreate(t *testing.T) { - grammar := newPostgresGrammar() - - tests := []struct { - name string - table string - blueprint func(table *Blueprint) - want string - wantErr bool - }{ - { - name: "Create simple table", - table: "users", - blueprint: func(table *Blueprint) { - table.ID() - table.String("name") - table.String("email") - table.String("password").Nullable() - table.Timestamp("created_at").UseCurrent() - table.Timestamp("updated_at").UseCurrent() - }, - want: "CREATE TABLE users (id BIGSERIAL NOT NULL, name VARCHAR(255) NOT NULL, email VARCHAR(255) NOT NULL, password VARCHAR(255) NULL, created_at TIMESTAMP(0) DEFAULT CURRENT_TIMESTAMP NOT NULL, updated_at TIMESTAMP(0) DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT pk_users PRIMARY KEY (id))", - }, - { - name: "Create table with foreign key", - table: "posts", - blueprint: func(table *Blueprint) { - table.ID() - table.Integer("user_id") - table.String("title") - table.Text("content").Nullable() - table.Foreign("user_id").References("id").On("users").OnDelete("CASCADE").OnUpdate("CASCADE") - }, - want: "CREATE TABLE posts (id BIGSERIAL NOT NULL, user_id INTEGER NOT NULL, title VARCHAR(255) NOT NULL, content TEXT NULL, CONSTRAINT pk_posts PRIMARY KEY (id))", - }, - { - name: "Create table with column name is empty", - table: "empty_column_table", - blueprint: func(table *Blueprint) { - table.String("") // Intentionally empty column name - }, - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - bp := &Blueprint{name: tt.table} - tt.blueprint(bp) - got, err := grammar.CompileCreate(bp) - if tt.wantErr { - require.Error(t, err) - return - } - - require.NoError(t, err) - assert.Equal(t, tt.want, got, "SQL statement mismatch for %s", tt.name) - }) - } -} - -func TestPgGrammar_CompileAdd(t *testing.T) { - grammar := newPostgresGrammar() - - tests := []struct { - name string - table string - blueprint func(table *Blueprint) - want string - wantErr bool - }{ - { - name: "Add single column", - table: "users", - blueprint: func(table *Blueprint) { - table.String("phone", 20) - }, - want: "ALTER TABLE users ADD COLUMN phone VARCHAR(20) NOT NULL", - wantErr: false, - }, - { - name: "Add multiple columns", - table: "users", - blueprint: func(table *Blueprint) { - table.String("phone", 20) - table.String("address", 255).Nullable() - table.Integer("age") - }, - want: "ALTER TABLE users ADD COLUMN phone VARCHAR(20) NOT NULL, ADD COLUMN address VARCHAR(255) NULL, ADD COLUMN age INTEGER NOT NULL", - wantErr: false, - }, - { - name: "Add column with default value", - table: "users", - blueprint: func(table *Blueprint) { - table.Boolean("active").Default(true) - }, - want: "ALTER TABLE users ADD COLUMN active BOOLEAN DEFAULT '1' NOT NULL", - wantErr: false, - }, - { - name: "Add column with comment", - table: "users", - blueprint: func(table *Blueprint) { - table.String("notes", 500).Comment("User notes") - }, - want: "ALTER TABLE users ADD COLUMN notes VARCHAR(500) NOT NULL", - wantErr: false, - }, - { - name: "Add primary key column", - table: "categories", - blueprint: func(table *Blueprint) { - table.Integer("id").Primary() - }, - want: "ALTER TABLE categories ADD COLUMN id INTEGER NOT NULL, ADD CONSTRAINT pk_categories PRIMARY KEY (id)", - wantErr: false, - }, - { - name: "Add auto increment column", - table: "logs", - blueprint: func(table *Blueprint) { - table.BigInteger("id").AutoIncrement() - }, - want: "ALTER TABLE logs ADD COLUMN id BIGSERIAL NOT NULL", - wantErr: false, - }, - { - name: "Add complex column with all attributes", - table: "products", - blueprint: func(table *Blueprint) { - table.Decimal("price", 10, 2).Default(0) - }, - want: "ALTER TABLE products ADD COLUMN price DECIMAL(10, 2) DEFAULT '0' NOT NULL", - wantErr: false, - }, - { - name: "Add timestamp columns", - table: "orders", - blueprint: func(table *Blueprint) { - table.Timestamp("created_at").UseCurrent() - table.Timestamp("updated_at").UseCurrent().Nullable() - }, - want: "ALTER TABLE orders ADD COLUMN created_at TIMESTAMP(0) DEFAULT CURRENT_TIMESTAMP NOT NULL, ADD COLUMN updated_at TIMESTAMP(0) DEFAULT CURRENT_TIMESTAMP NULL", - wantErr: false, - }, - { - name: "Add different data types", - table: "mixed_table", - blueprint: func(table *Blueprint) { - table.Text("description") - table.JSON("metadata").Nullable() - table.UUID("reference_id") - table.Date("event_date") - }, - want: "ALTER TABLE mixed_table ADD COLUMN description TEXT NOT NULL, ADD COLUMN metadata JSON NULL, ADD COLUMN reference_id UUID NOT NULL, ADD COLUMN event_date DATE NOT NULL", - wantErr: false, - }, - { - name: "No columns to add", - table: "users", - blueprint: func(_ *Blueprint) {}, - want: "", - wantErr: false, - }, - { - name: "Error on empty column name", - table: "users", - blueprint: func(table *Blueprint) { - table.String("", 255) // Intentionally empty column name - }, - wantErr: true, - }, - { - name: "Add enum column", - table: "users", - blueprint: func(table *Blueprint) { - table.Enum("status", []string{"active", "inactive", "pending"}) - }, - want: "ALTER TABLE users ADD COLUMN status VARCHAR(255) CHECK (status IN ('active', 'inactive', 'pending')) NOT NULL", - wantErr: false, - }, - { - name: "Add geography column", - table: "locations", - blueprint: func(table *Blueprint) { - table.Geography("coordinates", "POINT", 4326) - }, - want: "ALTER TABLE locations ADD COLUMN coordinates GEOGRAPHY(POINT, 4326) NOT NULL", - wantErr: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - bp := &Blueprint{name: tt.table} - tt.blueprint(bp) - got, err := grammar.CompileAdd(bp) - if tt.wantErr { - require.Error(t, err) - return - } - - require.NoError(t, err) - assert.Equal(t, tt.want, got) - }) - } -} - -func TestPgGrammar_CompileChange(t *testing.T) { - grammar := newPostgresGrammar() - - tests := []struct { - name string - table string - blueprint func(blueprint *Blueprint) - want []string - wantErr bool - }{ - { - name: "Change single column type", - table: "users", - blueprint: func(table *Blueprint) { - table.String("email", 500).Nullable().Change() - }, - want: []string{"ALTER TABLE users ALTER COLUMN email TYPE VARCHAR(500), ALTER COLUMN email DROP NOT NULL"}, - }, - { - name: "Change column with default value", - table: "users", - blueprint: func(table *Blueprint) { - table.String("email", 500).Default("user@mail.com").Change() - }, - want: []string{ - "ALTER TABLE users ALTER COLUMN email TYPE VARCHAR(500), ALTER COLUMN email SET DEFAULT 'user@mail.com'", - }, - }, - { - name: "Change multiple columns", - table: "users", - blueprint: func(table *Blueprint) { - table.String("email", 500).Nullable().Change() - table.String("name", 255).Default("Anonymous").Change() - }, - want: []string{ - "ALTER TABLE users ALTER COLUMN email TYPE VARCHAR(500), ALTER COLUMN email DROP NOT NULL", - "ALTER TABLE users ALTER COLUMN name TYPE VARCHAR(255), ALTER COLUMN name SET DEFAULT 'Anonymous'", - }, - }, - { - name: "Drop default value from column", - table: "users", - blueprint: func(table *Blueprint) { - table.String("email", 500).Default(nil).Change() - }, - want: []string{ - "ALTER TABLE users ALTER COLUMN email TYPE VARCHAR(500), ALTER COLUMN email SET DEFAULT NULL", - }, - }, - { - name: "Add comment to column", - table: "users", - blueprint: func(table *Blueprint) { - table.String("email", 500).Comment("User email address").Change() - }, - want: []string{ - "ALTER TABLE users ALTER COLUMN email TYPE VARCHAR(500)", - "COMMENT ON COLUMN users.email IS 'User email address'", - }, - }, - { - name: "Remove comment from column", - table: "users", - blueprint: func(table *Blueprint) { - table.String("email", 500).Comment("").Change() - }, - want: []string{ - "ALTER TABLE users ALTER COLUMN email TYPE VARCHAR(500)", - "COMMENT ON COLUMN users.email IS ''", - }, - }, - { - name: "Set column to not nullable", - table: "users", - blueprint: func(table *Blueprint) { - table.String("email", 500).Nullable(false).Change() - }, - want: []string{"ALTER TABLE users ALTER COLUMN email TYPE VARCHAR(500), ALTER COLUMN email SET NOT NULL"}, - }, - { - name: "Column name with empty string", - table: "users", - blueprint: func(table *Blueprint) { - table.String("", 255).Change() // Intentionally empty column name - }, - wantErr: true, - }, - { - name: "No changes", - table: "users", - blueprint: func(_ *Blueprint) {}, - wantErr: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - bp := &Blueprint{name: tt.table, grammar: grammar} - tt.blueprint(bp) - got, err := bp.toSQL() - if tt.wantErr { - require.Error(t, err) - return - } - - require.NoError(t, err) - assert.Equal(t, tt.want, got) - }) - } -} - -func TestPgGrammar_CompileDrop(t *testing.T) { - grammar := newPostgresGrammar() - - tests := []struct { - name string - table string - want string - wantErr bool - }{ - { - name: "Drop table", - table: "users", - want: "DROP TABLE users", - wantErr: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - bp := &Blueprint{name: tt.table} - got, err := grammar.CompileDrop(bp) - if tt.wantErr { - require.Error(t, err) - return - } - - require.NoError(t, err) - assert.Equal(t, tt.want, got) - }) - } -} - -func TestPgGrammar_CompileDropIfExists(t *testing.T) { - grammar := newPostgresGrammar() - - tests := []struct { - name string - table string - want string - wantErr bool - }{ - { - name: "Drop table if exists", - table: "users", - want: "DROP TABLE IF EXISTS users", - wantErr: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - bp := &Blueprint{name: tt.table} - got, err := grammar.CompileDropIfExists(bp) - if tt.wantErr { - require.Error(t, err) - return - } - - require.NoError(t, err) - assert.Equal(t, tt.want, got) - }) - } -} - -func TestPgGrammar_CompileRename(t *testing.T) { - grammar := newPostgresGrammar() - - tests := []struct { - name string - oldName string - newName string - want string - wantErr bool - }{ - { - name: "Rename table", - oldName: "users", - newName: "people", - want: "ALTER TABLE users RENAME TO people", - wantErr: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - bp := &Blueprint{name: tt.oldName} - bp.rename(tt.newName) - got, err := grammar.CompileRename(bp, bp.commands[0]) - if tt.wantErr { - require.Error(t, err) - return - } - - require.NoError(t, err) - assert.Equal(t, tt.want, got) - }) - } -} - -func TestPgGrammar_GetColumns(t *testing.T) { - grammar := newPostgresGrammar() - - tests := []struct { - name string - blueprint func(table *Blueprint) - want []string - wantErr bool - }{ - { - name: "Simple column", - blueprint: func(table *Blueprint) { - table.String("name", 255) - }, - want: []string{"name VARCHAR(255) NOT NULL"}, - }, - { - name: "Nullable column", - blueprint: func(table *Blueprint) { - table.String("email", 255).Nullable() - }, - want: []string{"email VARCHAR(255) NULL"}, - }, - { - name: "Nullable column with default null", - blueprint: func(table *Blueprint) { - table.Text("description").Nullable().Default(nil) - }, - want: []string{"description TEXT DEFAULT NULL NULL"}, - }, - { - name: "Column with default value", - blueprint: func(table *Blueprint) { - table.Boolean("active").Default(true) - }, - want: []string{"active BOOLEAN DEFAULT '1' NOT NULL"}, - }, - { - name: "Primary key column", - blueprint: func(table *Blueprint) { - table.Integer("id").Primary() - }, - want: []string{"id INTEGER NOT NULL"}, - }, - { - name: "Error on empty column", - blueprint: func(table *Blueprint) { - table.String("", 255) // Intentionally empty column name - }, - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - bp := &Blueprint{name: "test_table"} - tt.blueprint(bp) - got, err := grammar.getColumns(bp) - if tt.wantErr { - assert.Error(t, err) - return - } - assert.Equal(t, tt.want, got) - }) - } -} - -func TestPgGrammar_CompileDropColumn(t *testing.T) { - grammar := newPostgresGrammar() - - tests := []struct { - name string - table string - blueprint func(table *Blueprint) - wants []string - wantErr bool - }{ - { - name: "Drop single column", - table: "users", - blueprint: func(table *Blueprint) { - table.DropColumn("email") - }, - wants: []string{"ALTER TABLE users DROP COLUMN email"}, - wantErr: false, - }, - { - name: "Drop multiple columns", - table: "users", - blueprint: func(table *Blueprint) { - table.DropColumn("email", "phone") - table.DropColumn("address") - }, - wants: []string{ - "ALTER TABLE users DROP COLUMN email, DROP COLUMN phone", - "ALTER TABLE users DROP COLUMN address", - }, - wantErr: false, - }, - { - name: "No columns to drop", - table: "users", - blueprint: func(_ *Blueprint) {}, - wants: nil, - wantErr: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - bp := &Blueprint{name: tt.table, grammar: grammar} - tt.blueprint(bp) - got, err := bp.toSQL() - if tt.wantErr { - require.Error(t, err) - return - } - - require.NoError(t, err) - assert.Equal(t, tt.wants, got) - }) - } -} - -func TestPgGrammar_CompileRenameColumn(t *testing.T) { - grammar := newPostgresGrammar() - - tests := []struct { - name string - table string - oldName string - newName string - want string - wantErr bool - }{ - { - name: "Rename column", - table: "users", - oldName: "email", - newName: "user_email", - want: "ALTER TABLE users RENAME COLUMN email TO user_email", - wantErr: false, - }, - { - name: "Empty old name", - table: "users", - oldName: "", - newName: "user_email", - wantErr: true, - }, - { - name: "Empty new name", - table: "users", - oldName: "email", - newName: "", - wantErr: true, - }, - { - name: "Both names empty", - table: "users", - oldName: "", - newName: "", - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - bp := &Blueprint{name: tt.table} - command := &command{from: tt.oldName, to: tt.newName} - got, err := grammar.CompileRenameColumn(bp, command) - if tt.wantErr { - require.Error(t, err) - return - } - - require.NoError(t, err) - assert.Equal(t, tt.want, got) - }) - } -} - -func TestPgGrammar_CompileDropIndex(t *testing.T) { - grammar := newPostgresGrammar() - - tests := []struct { - name string - indexName string - want string - wantErr bool - }{ - { - name: "Drop index with valid name", - indexName: "users_email_index", - want: "DROP INDEX users_email_index", - wantErr: false, - }, - { - name: "Drop index with complex name", - indexName: "idx_users_email_name", - want: "DROP INDEX idx_users_email_name", - wantErr: false, - }, - { - name: "Empty index name", - indexName: "", - want: "", - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - bp := &Blueprint{} - command := &command{index: tt.indexName} - got, err := grammar.CompileDropIndex(bp, command) - if tt.wantErr { - require.Error(t, err) - assert.Empty(t, got) - return - } - - require.NoError(t, err) - assert.Equal(t, tt.want, got) - }) - } -} - -func TestPgGrammar_CompileDropPrimary(t *testing.T) { - grammar := newPostgresGrammar() - - tests := []struct { - name string - blueprint *Blueprint - indexName string - want string - wantErr bool - }{ - { - name: "Drop primary key with specified name", - blueprint: func() *Blueprint { - return &Blueprint{name: "users"} - }(), - indexName: "users_pkey", - want: "ALTER TABLE users DROP CONSTRAINT users_pkey", - wantErr: false, - }, - { - name: "Drop primary key with empty index name (should use default naming)", - blueprint: func() *Blueprint { - return &Blueprint{name: "posts"} - }(), - indexName: "", - want: "ALTER TABLE posts DROP CONSTRAINT pk_posts", - wantErr: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - command := &command{index: tt.indexName} - got, err := grammar.CompileDropPrimary(tt.blueprint, command) - if tt.wantErr { - require.Error(t, err) - return - } - - require.NoError(t, err) - assert.Equal(t, tt.want, got) - }) - } -} - -func TestPgGrammar_CompileRenameIndex(t *testing.T) { - grammar := newPostgresGrammar() - - tests := []struct { - name string - table string - oldName string - newName string - want string - wantErr bool - }{ - { - name: "Rename index with valid names", - table: "users", - oldName: "users_email_index", - newName: "users_email_unique", - want: "ALTER INDEX users_email_index RENAME TO users_email_unique", - wantErr: false, - }, - { - name: "Rename index with complex names", - table: "users", - oldName: "idx_users_email_name", - newName: "idx_users_email_name_unique", - want: "ALTER INDEX idx_users_email_name RENAME TO idx_users_email_name_unique", - wantErr: false, - }, - { - name: "Empty old name", - table: "users", - oldName: "", - newName: "users_email_unique", - wantErr: true, - }, - { - name: "Empty new name", - table: "users", - oldName: "users_email_index", - newName: "", - wantErr: true, - }, - { - name: "Both names empty", - table: "users", - oldName: "", - newName: "", - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - bp := &Blueprint{name: tt.table} - command := &command{from: tt.oldName, to: tt.newName} - got, err := grammar.CompileRenameIndex(bp, command) - if tt.wantErr { - require.Error(t, err) - return - } - - require.NoError(t, err) - assert.Equal(t, tt.want, got) - }) - } -} - -func TestPgGrammar_CompileForeign(t *testing.T) { - grammar := newPostgresGrammar() - - tests := []struct { - name string - table string - blueprint func(table *Blueprint) - want string - wantErr bool - }{ - { - name: "Basic foreign key", - table: "posts", - blueprint: func(table *Blueprint) { - table.Foreign("user_id").References("id").On("users") - }, - want: "ALTER TABLE posts ADD CONSTRAINT fk_posts_users FOREIGN KEY (user_id) REFERENCES users(id)", - wantErr: false, - }, - { - name: "Foreign key with custom constraint name", - table: "orders", - blueprint: func(table *Blueprint) { - table.Foreign("customer_id").References("id").On("customers").Name("fk_orders_customers") - }, - want: "ALTER TABLE orders ADD CONSTRAINT fk_orders_customers FOREIGN KEY (customer_id) REFERENCES customers(id)", - wantErr: false, - }, - { - name: "Foreign key with ON DELETE CASCADE", - table: "comments", - blueprint: func(table *Blueprint) { - table.Foreign("post_id").References("id").On("posts").CascadeOnDelete() - }, - want: "ALTER TABLE comments ADD CONSTRAINT fk_comments_posts FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE", - wantErr: false, - }, - { - name: "Foreign key with ON UPDATE SET NULL", - table: "orders", - blueprint: func(table *Blueprint) { - table.Foreign("customer_id").References("id").On("customers").NullOnUpdate() - }, - want: "ALTER TABLE orders ADD CONSTRAINT fk_orders_customers FOREIGN KEY (customer_id) REFERENCES customers(id) ON UPDATE SET NULL", - wantErr: false, - }, - { - name: "Foreign key with both ON DELETE and ON UPDATE", - table: "order_items", - blueprint: func(table *Blueprint) { - table.Foreign("order_id").References("id").On("orders").CascadeOnDelete().RestrictOnUpdate() - }, - want: "ALTER TABLE order_items ADD CONSTRAINT fk_order_items_orders FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE ON UPDATE RESTRICT", - wantErr: false, - }, - { - name: "Foreign key with deferrable true", - table: "posts", - blueprint: func(table *Blueprint) { - table.Foreign("user_id").References("id").On("users").Deferrable(true) - }, - want: "ALTER TABLE posts ADD CONSTRAINT fk_posts_users FOREIGN KEY (user_id) REFERENCES users(id) DEFERRABLE", - wantErr: false, - }, - { - name: "Foreign key with deferrable false", - table: "posts", - blueprint: func(table *Blueprint) { - table.Foreign("user_id").References("id").On("users").Deferrable(false) - }, - want: "ALTER TABLE posts ADD CONSTRAINT fk_posts_users FOREIGN KEY (user_id) REFERENCES users(id) NOT DEFERRABLE", - wantErr: false, - }, - { - name: "Foreign key with deferrable true and initially immediate true", - table: "posts", - blueprint: func(table *Blueprint) { - table.Foreign("user_id").References("id").On("users").Deferrable().InitiallyImmediate(true) - }, - want: "ALTER TABLE posts ADD CONSTRAINT fk_posts_users FOREIGN KEY (user_id) REFERENCES users(id) DEFERRABLE INITIALLY IMMEDIATE", - wantErr: false, - }, - { - name: "Foreign key with deferrable true and initially immediate false", - table: "posts", - blueprint: func(table *Blueprint) { - table.Foreign("user_id").References("id").On("users").Deferrable().InitiallyImmediate(false) - }, - want: "ALTER TABLE posts ADD CONSTRAINT fk_posts_users FOREIGN KEY (user_id) REFERENCES users(id) DEFERRABLE INITIALLY DEFERRED", - wantErr: false, - }, - { - name: "Foreign key with deferrable false and initially immediate true (should be ignored)", - table: "posts", - blueprint: func(table *Blueprint) { - table.Foreign("user_id").References("id").On("users").Deferrable(false).InitiallyImmediate(true) - }, - want: "ALTER TABLE posts ADD CONSTRAINT fk_posts_users FOREIGN KEY (user_id) REFERENCES users(id) NOT DEFERRABLE", - wantErr: false, - }, - { - name: "Complex foreign key with all options", - table: "user_roles", - blueprint: func(table *Blueprint) { - table.Foreign("role_id").References("id").On("roles"). - CascadeOnDelete().RestrictOnUpdate(). - Deferrable().InitiallyImmediate(true) - }, - want: "ALTER TABLE user_roles ADD CONSTRAINT fk_user_roles_roles FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE ON UPDATE RESTRICT DEFERRABLE INITIALLY IMMEDIATE", - wantErr: false, - }, - { - name: "Empty column name", - table: "posts", - blueprint: func(table *Blueprint) { - table.Foreign("").References("id").On("users") - }, - wantErr: true, - }, - { - name: "Empty on table name", - table: "posts", - blueprint: func(table *Blueprint) { - table.Foreign("user_id").References("id").On("") - }, - wantErr: true, - }, - { - name: "Empty references column name", - table: "posts", - blueprint: func(table *Blueprint) { - table.Foreign("user_id").References("").On("users") - }, - wantErr: true, - }, - { - name: "All required fields empty", - table: "posts", - blueprint: func(table *Blueprint) { - table.Foreign("").References("").On("") - }, - wantErr: true, - }, - { - name: "Foreign key with RESTRICT actions", - table: "invoices", - blueprint: func(table *Blueprint) { - table.Foreign("customer_id").References("id").On("customers").RestrictOnDelete().RestrictOnUpdate() - }, - want: "ALTER TABLE invoices ADD CONSTRAINT fk_invoices_customers FOREIGN KEY (customer_id) REFERENCES customers(id) ON DELETE RESTRICT ON UPDATE RESTRICT", - wantErr: false, - }, - { - name: "Foreign key with no actions", - table: "payments", - blueprint: func(table *Blueprint) { - table.Foreign("invoice_id").References("id").On("invoices").NoActionOnDelete().NoActionOnUpdate() - }, - want: "ALTER TABLE payments ADD CONSTRAINT fk_payments_invoices FOREIGN KEY (invoice_id) REFERENCES invoices(id) ON DELETE NO ACTION ON UPDATE NO ACTION", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - bp := &Blueprint{name: tt.table} - tt.blueprint(bp) - got, err := grammar.CompileForeign(bp, bp.commands[0]) - if tt.wantErr { - require.Error(t, err) - return - } - - require.NoError(t, err) - assert.Equal(t, tt.want, got) - }) - } -} - -func TestPgGrammar_CompileDropForeign(t *testing.T) { - grammar := newPostgresGrammar() - - tests := []struct { - name string - table string - foreignKeyName string - want string - wantErr bool - }{ - { - name: "Drop foreign key with valid name", - table: "posts", - foreignKeyName: "fk_posts_users", - want: "ALTER TABLE posts DROP CONSTRAINT fk_posts_users", - wantErr: false, - }, - { - name: "Drop foreign key with complex name", - table: "order_items", - foreignKeyName: "fk_order_items_products_cascade", - want: "ALTER TABLE order_items DROP CONSTRAINT fk_order_items_products_cascade", - wantErr: false, - }, - { - name: "Empty foreign key name", - table: "users", - foreignKeyName: "", - want: "", - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - bp := &Blueprint{name: tt.table} - command := &command{index: tt.foreignKeyName} - got, err := grammar.CompileDropForeign(bp, command) - if tt.wantErr { - require.Error(t, err) - return - } - - require.NoError(t, err) - assert.Equal(t, tt.want, got) - }) - } -} - -func TestPgGrammar_CompileIndex(t *testing.T) { - grammar := newPostgresGrammar() - - tests := []struct { - name string - table string - blueprint func(table *Blueprint) - want string - wantErr bool - }{ - { - name: "Basic index with single column", - table: "users", - blueprint: func(table *Blueprint) { - table.Index("email").Name("users_email_index") - }, - want: "CREATE INDEX users_email_index ON users (email)", - wantErr: false, - }, - { - name: "Basic index with multiple columns", - table: "users", - blueprint: func(table *Blueprint) { - table.Index("name", "email").Name("users_name_email_index") - }, - want: "CREATE INDEX users_name_email_index ON users (name, email)", - wantErr: false, - }, - { - name: "Index with algorithm", - table: "products", - blueprint: func(table *Blueprint) { - table.Index("sku").Name("products_sku_index").Algorithm("btree") - }, - want: "CREATE INDEX products_sku_index ON products USING btree (sku)", - wantErr: false, - }, - { - name: "Index without name (should use generated name)", - table: "orders", - blueprint: func(table *Blueprint) { - table.Index("sku") - }, - want: "CREATE INDEX idx_orders_sku ON orders (sku)", - wantErr: false, - }, - { - name: "Index with empty column in list", - table: "users", - blueprint: func(table *Blueprint) { - table.Index("name", "", "email").Name("users_invalid_index") - }, - wantErr: true, - }, - { - name: "Index with only empty column", - table: "users", - blueprint: func(table *Blueprint) { - table.Index("").Name("users_empty_index") - }, - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - bp := &Blueprint{name: tt.table} - tt.blueprint(bp) - got, err := grammar.CompileIndex(bp, bp.commands[0]) - if tt.wantErr { - require.Error(t, err) - return - } - - require.NoError(t, err) - assert.Equal(t, tt.want, got) - }) - } -} - -func TestPgGrammar_CompileUnique(t *testing.T) { - grammar := newPostgresGrammar() - - tests := []struct { - name string - table string - blueprint func(table *Blueprint) - want string - wantErr bool - }{ - { - name: "Basic unique index with single column", - table: "users", - blueprint: func(table *Blueprint) { - table.Unique("email").Name("users_email_unique") - }, - want: "ALTER TABLE users ADD CONSTRAINT users_email_unique UNIQUE (email)", - wantErr: false, - }, - { - name: "Basic unique index with multiple columns", - table: "users", - blueprint: func(table *Blueprint) { - table.Unique("name", "email").Name("users_name_email_unique") - }, - want: "ALTER TABLE users ADD CONSTRAINT users_name_email_unique UNIQUE (name, email)", - wantErr: false, - }, - { - name: "Unique index without name (should use generated name)", - table: "orders", - blueprint: func(table *Blueprint) { - table.Unique("order_number") - }, - want: "ALTER TABLE orders ADD CONSTRAINT uk_orders_order_number UNIQUE (order_number)", - wantErr: false, - }, - { - name: "Unique index with deferrable true", - table: "users", - blueprint: func(table *Blueprint) { - table.Unique("email").Name("users_email_unique").Deferrable(true) - }, - want: "ALTER TABLE users ADD CONSTRAINT users_email_unique UNIQUE (email) DEFERRABLE", - wantErr: false, - }, - { - name: "Unique index with deferrable false", - table: "users", - blueprint: func(table *Blueprint) { - table.Unique("email").Name("users_email_unique").Deferrable(false) - }, - want: "ALTER TABLE users ADD CONSTRAINT users_email_unique UNIQUE (email) NOT DEFERRABLE", - wantErr: false, - }, - { - name: "Unique index with deferrable and initially immediate true", - table: "users", - blueprint: func(table *Blueprint) { - table.Unique("email").Name("users_email_unique").Deferrable().InitiallyImmediate(true) - }, - want: "ALTER TABLE users ADD CONSTRAINT users_email_unique UNIQUE (email) DEFERRABLE INITIALLY IMMEDIATE", - wantErr: false, - }, - { - name: "Unique index with deferrable false and initially immediate false", - table: "users", - blueprint: func(table *Blueprint) { - table.Unique("email").Name("users_email_unique").Deferrable().InitiallyImmediate(false) - }, - want: "ALTER TABLE users ADD CONSTRAINT users_email_unique UNIQUE (email) DEFERRABLE INITIALLY DEFERRED", - wantErr: false, - }, - { - name: "Unique index with deferrable false and initially immediate true (should be ignored)", - table: "users", - blueprint: func(table *Blueprint) { - table.Unique("email").Name("users_email_unique").Deferrable(false).InitiallyImmediate(true) - }, - want: "ALTER TABLE users ADD CONSTRAINT users_email_unique UNIQUE (email) NOT DEFERRABLE", - wantErr: false, - }, - { - name: "Unique index with empty column in list", - table: "users", - blueprint: func(table *Blueprint) { - table.Unique("name", "", "email").Name("users_invalid_unique") - }, - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - bp := &Blueprint{name: tt.table} - tt.blueprint(bp) - got, err := grammar.CompileUnique(bp, bp.commands[0]) - if tt.wantErr { - require.Error(t, err) - return - } - - require.NoError(t, err) - assert.Equal(t, tt.want, got) - }) - } -} - -func TestPgGrammar_CompileFullText(t *testing.T) { - grammar := newPostgresGrammar() - - tests := []struct { - name string - table string - blueprint func(table *Blueprint) - want string - wantErr bool - }{ - { - name: "Basic fulltext index with single column", - table: "articles", - blueprint: func(table *Blueprint) { - table.FullText("title").Name("articles_title_fulltext").Language("english") - }, - want: "CREATE INDEX articles_title_fulltext ON articles USING GIN (to_tsvector('english', title))", - wantErr: false, - }, - { - name: "Fulltext index with multiple columns", - table: "documents", - blueprint: func(table *Blueprint) { - table.FullText("title", "content").Name("documents_title_content_fulltext").Language("english") - }, - want: "CREATE INDEX documents_title_content_fulltext ON documents USING GIN (to_tsvector('english', title) || to_tsvector('english', content))", - wantErr: false, - }, - { - name: "Fulltext index with different language", - table: "posts", - blueprint: func(table *Blueprint) { - table.FullText("content").Name("posts_content_spanish_fulltext").Language("spanish") - }, - want: "CREATE INDEX posts_content_spanish_fulltext ON posts USING GIN (to_tsvector('spanish', content))", - wantErr: false, - }, - { - name: "Fulltext index without language (should use default english)", - table: "blogs", - blueprint: func(table *Blueprint) { - table.FullText("body").Name("blogs_body_fulltext") - }, - want: "CREATE INDEX blogs_body_fulltext ON blogs USING GIN (to_tsvector('english', body))", - wantErr: false, - }, - { - name: "Fulltext index without name (should use generated name)", - table: "news", - blueprint: func(table *Blueprint) { - table.FullText("headline") - }, - want: "CREATE INDEX ft_news_headline ON news USING GIN (to_tsvector('english', headline))", - wantErr: false, - }, - { - name: "Fulltext index with three columns", - table: "products", - blueprint: func(table *Blueprint) { - table.FullText("name", "description", "tags").Name("products_search_fulltext").Language("english") - }, - want: "CREATE INDEX products_search_fulltext ON products USING GIN (to_tsvector('english', name) || to_tsvector('english', description) || to_tsvector('english', tags))", - wantErr: false, - }, - { - name: "Fulltext index with empty column in list", - table: "articles", - blueprint: func(table *Blueprint) { - table.FullText("title", "", "content").Name("articles_invalid_fulltext") - }, - wantErr: true, - }, - { - name: "Fulltext index with only empty column", - table: "articles", - blueprint: func(table *Blueprint) { - table.FullText("").Name("articles_empty_fulltext") - }, - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - bp := &Blueprint{name: tt.table} - tt.blueprint(bp) - got, err := grammar.CompileFullText(bp, bp.commands[0]) - if tt.wantErr { - require.Error(t, err) - return - } - - require.NoError(t, err) - assert.Equal(t, tt.want, got) - }) - } -} - -func TestPgGrammar_CompileDropUnique(t *testing.T) { - grammar := newPostgresGrammar() - - tests := []struct { - name string - indexName string - want string - wantErr bool - }{ - { - name: "Drop unique index with valid name", - indexName: "users_email_unique", - want: "ALTER TABLE DROP CONSTRAINT users_email_unique", - wantErr: false, - }, - { - name: "Drop unique index with complex name", - indexName: "uk_users_email_name", - want: "ALTER TABLE DROP CONSTRAINT uk_users_email_name", - wantErr: false, - }, - { - name: "Drop unique index with numeric suffix", - indexName: "users_email_unique_2", - want: "ALTER TABLE DROP CONSTRAINT users_email_unique_2", - wantErr: false, - }, - { - name: "Empty index name", - indexName: "", - want: "", - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - bp := &Blueprint{} - command := &command{index: tt.indexName} - got, err := grammar.CompileDropUnique(bp, command) - if tt.wantErr { - require.Error(t, err) - assert.Empty(t, got) - return - } - - require.NoError(t, err) - assert.Equal(t, tt.want, got) - }) - } -} - -func TestPgGrammar_CompileDropFulltext(t *testing.T) { - grammar := newPostgresGrammar() - - tests := []struct { - name string - indexName string - want string - wantErr bool - }{ - { - name: "Drop fulltext index with valid name", - indexName: "articles_title_fulltext", - want: "DROP INDEX articles_title_fulltext", - wantErr: false, - }, - { - name: "Drop fulltext index with complex name", - indexName: "documents_title_content_fulltext", - want: "DROP INDEX documents_title_content_fulltext", - wantErr: false, - }, - { - name: "Drop fulltext index with underscore prefix", - indexName: "idx_posts_content_fulltext", - want: "DROP INDEX idx_posts_content_fulltext", - wantErr: false, - }, - { - name: "Drop fulltext index with numeric suffix", - indexName: "search_index_fulltext_1", - want: "DROP INDEX search_index_fulltext_1", - wantErr: false, - }, - { - name: "Empty index name", - indexName: "", - want: "", - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - bp := &Blueprint{} - command := &command{index: tt.indexName} - got, err := grammar.CompileDropFulltext(bp, command) - if tt.wantErr { - require.Error(t, err) - assert.Empty(t, got) - return - } - - require.NoError(t, err) - assert.Equal(t, tt.want, got) - }) - } -} - -func TestPgGrammar_CompilePrimary(t *testing.T) { - grammar := newPostgresGrammar() - - tests := []struct { - name string - table string - blueprint func(table *Blueprint) - want string - wantErr bool - }{ - { - name: "Basic primary key with single column", - table: "users", - blueprint: func(table *Blueprint) { - table.Primary("id").Name("users_id_primary") - }, - want: "ALTER TABLE users ADD CONSTRAINT users_id_primary PRIMARY KEY (id)", - wantErr: false, - }, - { - name: "Primary key with multiple columns", - table: "user_roles", - blueprint: func(table *Blueprint) { - table.Primary("user_id", "role_id").Name("user_roles_primary") - }, - want: "ALTER TABLE user_roles ADD CONSTRAINT user_roles_primary PRIMARY KEY (user_id, role_id)", - wantErr: false, - }, - { - name: "Primary key without name (should use generated name)", - table: "orders", - blueprint: func(table *Blueprint) { - table.Primary("order_id") - }, - want: "ALTER TABLE orders ADD CONSTRAINT pk_orders PRIMARY KEY (order_id)", - wantErr: false, - }, - { - name: "Primary key with three columns", - table: "order_items", - blueprint: func(table *Blueprint) { - table.Primary("order_id", "product_id", "variant_id").Name("order_items_composite_pk") - }, - want: "ALTER TABLE order_items ADD CONSTRAINT order_items_composite_pk PRIMARY KEY (order_id, product_id, variant_id)", - wantErr: false, - }, - { - name: "Primary key with empty column in list", - table: "users", - blueprint: func(table *Blueprint) { - table.Primary("id", "", "tenant_id").Name("users_invalid_primary") - }, - wantErr: true, - }, - { - name: "Primary key with only empty column", - table: "users", - blueprint: func(table *Blueprint) { - table.Primary("").Name("users_empty_primary") - }, - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - bp := &Blueprint{name: tt.table} - tt.blueprint(bp) - got, err := grammar.CompilePrimary(bp, bp.commands[0]) - if tt.wantErr { - require.Error(t, err) - return - } - - require.NoError(t, err) - assert.Equal(t, tt.want, got) - }) - } -} - -func TestPgGrammar_GetType(t *testing.T) { - grammar := newPostgresGrammar() - - tests := []struct { - name string - blueprint func(table *Blueprint) - want string - }{ - { - name: "custom column type", - blueprint: func(table *Blueprint) { - table.Column("name", "CUSTOM_TYPE") - }, - want: "CUSTOM_TYPE", - }, - { - name: "boolean column type", - blueprint: func(table *Blueprint) { - table.Boolean("active") - }, - want: "BOOLEAN", - }, - { - name: "char column type", - blueprint: func(table *Blueprint) { - table.Char("code", 10) - }, - want: "CHAR(10)", - }, - { - name: "char column type without length", - blueprint: func(table *Blueprint) { - table.Char("code") - }, - want: "CHAR(255)", - }, - { - name: "string column type", - blueprint: func(table *Blueprint) { - table.String("name", 255) - }, - want: "VARCHAR(255)", - }, - { - name: "decimal column type", - blueprint: func(table *Blueprint) { - table.Decimal("price", 10, 2) - }, - want: "DECIMAL(10, 2)", - }, - { - name: "double column type", - blueprint: func(table *Blueprint) { - table.Double("value") - }, - want: "DOUBLE PRECISION", - }, - { - name: "float column type with precision", - blueprint: func(table *Blueprint) { - table.Float("value", 6, 2) - }, - want: "REAL", - }, - { - name: "float column type without precision", - blueprint: func(table *Blueprint) { - table.Float("value", 0, 0) - }, - want: "REAL", - }, - { - name: "big integer column type", - blueprint: func(table *Blueprint) { - table.BigInteger("id") - }, - want: "BIGINT", - }, - { - name: "big integer auto increment", - blueprint: func(table *Blueprint) { - table.BigInteger("id").AutoIncrement() - }, - want: "BIGSERIAL", - }, - { - name: "integer column type", - blueprint: func(table *Blueprint) { - table.Integer("count") - }, - want: "INTEGER", - }, - { - name: "integer auto increment", - blueprint: func(table *Blueprint) { - table.Integer("id").AutoIncrement() - }, - want: "SERIAL", - }, - { - name: "small integer column type", - blueprint: func(table *Blueprint) { - table.SmallInteger("status") - }, - want: "SMALLINT", - }, - { - name: "small integer auto increment", - blueprint: func(table *Blueprint) { - table.SmallInteger("id").Unsigned().AutoIncrement() - }, - want: "SMALLSERIAL", - }, - { - name: "medium integer column type", - blueprint: func(table *Blueprint) { - table.MediumInteger("value") - }, - want: "INTEGER", - }, - { - name: "medium auto increment", - blueprint: func(table *Blueprint) { - table.MediumIncrements("id") - }, - want: "SERIAL", - }, - { - name: "tiny integer column type", - blueprint: func(table *Blueprint) { - table.TinyInteger("flag") - }, - want: "SMALLINT", - }, - { - name: "tiny integer auto increment", - blueprint: func(table *Blueprint) { - table.TinyInteger("id").AutoIncrement() - }, - want: "SMALLSERIAL", - }, - { - name: "time column type", - blueprint: func(table *Blueprint) { - table.Time("created_at") - }, - want: "TIME(0)", - }, - { - name: "datetime column type with precision", - blueprint: func(table *Blueprint) { - table.DateTime("created_at", 6) - }, - want: "TIMESTAMP(6)", - }, - { - name: "datetime column type without precision", - blueprint: func(table *Blueprint) { - table.DateTime("created_at", 0) - }, - want: "TIMESTAMP(0)", - }, - { - name: "datetime tz column type with precision", - blueprint: func(table *Blueprint) { - table.DateTimeTz("created_at", 3) - }, - want: "TIMESTAMPTZ(3)", - }, - { - name: "datetime tz column type without precision", - blueprint: func(table *Blueprint) { - table.DateTimeTz("created_at", 0) - }, - want: "TIMESTAMPTZ(0)", - }, - { - name: "timestamp column type with precision", - blueprint: func(table *Blueprint) { - table.Timestamp("created_at", 6) - }, - want: "TIMESTAMP(6)", - }, - { - name: "timestamp column type without precision", - blueprint: func(table *Blueprint) { - table.Timestamp("created_at", 0) - }, - want: "TIMESTAMP(0)", - }, - { - name: "timestamp tz column type with precision", - blueprint: func(table *Blueprint) { - table.TimestampTz("created_at", 3) - }, - want: "TIMESTAMPTZ(3)", - }, - { - name: "timestamp tz column type without precision", - blueprint: func(table *Blueprint) { - table.TimestampTz("created_at", 0) - }, - want: "TIMESTAMPTZ(0)", - }, - { - name: "geography column type", - blueprint: func(table *Blueprint) { - table.Geography("location", "POINT", 4326) - }, - want: "GEOGRAPHY(POINT, 4326)", - }, - { - name: "long text column type", - blueprint: func(table *Blueprint) { - table.LongText("content") - }, - want: "TEXT", - }, - { - name: "text column type", - blueprint: func(table *Blueprint) { - table.Text("description") - }, - want: "TEXT", - }, - { - name: "tiny text column type", - blueprint: func(table *Blueprint) { - table.TinyText("notes") - }, - want: "VARCHAR(255)", - }, - { - name: "date column type", - blueprint: func(table *Blueprint) { - table.Date("birth_date") - }, - want: "DATE", - }, - { - name: "year column type", - blueprint: func(table *Blueprint) { - table.Year("graduation_year") - }, - want: "INTEGER", - }, - { - name: "json column type", - blueprint: func(table *Blueprint) { - table.JSON("metadata") - }, - want: "JSON", - }, - { - name: "jsonb column type", - blueprint: func(table *Blueprint) { - table.JSONB("data") - }, - want: "JSONB", - }, - { - name: "uuid column type", - blueprint: func(table *Blueprint) { - table.UUID("uuid") - }, - want: "UUID", - }, - { - name: "binary column type", - blueprint: func(table *Blueprint) { - table.Binary("data") - }, - want: "BYTEA", - }, - { - name: "point column type", - blueprint: func(table *Blueprint) { - table.Point("location") - }, - want: "POINT(4326)", - }, - { - name: "Geography type", - blueprint: func(table *Blueprint) { - table.Geography("location", "POINT", 4326) - }, - want: "GEOGRAPHY(POINT, 4326)", - }, - { - name: "Geometry type with SRID", - blueprint: func(table *Blueprint) { - table.Geometry("shape", "POLYGON", 4326) - }, - want: "GEOMETRY(POLYGON, 4326)", - }, - { - name: "Geometry type without SRID", - blueprint: func(table *Blueprint) { - table.Geometry("shape", "POLYGON") - }, - want: "GEOMETRY(POLYGON)", - }, - { - name: "Enum type", - blueprint: func(table *Blueprint) { - table.Enum("status", []string{"active", "inactive"}) - }, - want: "VARCHAR(255) CHECK (status IN ('active', 'inactive'))", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - bp := &Blueprint{name: "test_table"} - tt.blueprint(bp) - got := grammar.getType(bp.columns[0]) - assert.Equal(t, tt.want, got) - }) - } -} diff --git a/schema/schema.go b/schema/schema.go index 6da0c88..3cfc9de 100644 --- a/schema/schema.go +++ b/schema/schema.go @@ -1,255 +1,283 @@ package schema import ( + "context" "database/sql" "errors" "github.com/akfaiz/migris/internal/config" "github.com/akfaiz/migris/internal/dialect" + "github.com/akfaiz/migris/schema/blueprint" + "github.com/akfaiz/migris/schema/builders" + "github.com/akfaiz/migris/schema/core" + "github.com/akfaiz/migris/schema/grammars" ) -// Column represents a database column with its properties. -type Column struct { - Name string // Name is the name of the column. - TypeName string // TypeName is the name of the column type (e.g., "VARCHAR", "INT"). - TypeFull string // TypeFull is the full type name including any modifiers (e.g., "VARCHAR(255)", "INT(11)"). - Collation sql.NullString // Collation is the collation of the column, if applicable. - Nullable bool // Nullable indicates whether the column can contain NULL values. - DefaultVal sql.NullString // DefaultVal is the default value for the column, if any. - Comment sql.NullString // Comment is an optional comment for the column. - Extra sql.NullString // Extra contains additional information about the column (e.g., "auto_increment"). -} - -// Index represents a database index with its properties. -type Index struct { - Name string // Name is the name of the index. - Columns []string // Columns is a slice of column names that are part of the index. - Type string // e.g., "btree", "hash" - Unique bool // Indicates if the index is unique - Primary bool // Indicates if the index is a primary key -} - -// TableInfo represents information about a database table. -// It includes the table name, schema, size, and an optional comment. -type TableInfo struct { - Name string // Name is the name of the table. - Schema string // Schema is the schema where the table resides. - Size int64 // Size is the size of the table in bytes. - Comment sql.NullString // Comment is an optional comment for the table. - Engine sql.NullString // Engine is the storage engine used for the table (e.g., "InnoDB", "MyISAM"). - Collation sql.NullString // Collation is the collation used for the table (e.g., "utf8mb4_general_ci"). -} - -func newBuilder() (Builder, error) { - dialectVal := config.GetDialect() +// Type aliases for the public API. + +type ( + Blueprint = blueprint.Blueprint + ColumnDefinition = blueprint.ColumnDefinition + IndexDefinition = blueprint.IndexDefinition + ForeignKeyDefinition = blueprint.ForeignKeyDefinition + Context = core.Context + Rows = core.Rows + Row = core.Row + Builder = builders.Builder + Expression = blueprint.Expression + Column = core.Column + Index = core.Index + TableInfo = core.TableInfo +) + +func newBuilder(c Context) (Builder, error) { + dialectVal := dialect.Unknown + if c != nil { + dialectVal = dialect.FromString(c.Dialect()) + } if dialectVal == dialect.Unknown { - return nil, errors.New( - "schema dialect is not set, please call schema.SetDialect() before using schema functions", - ) + dialectVal = config.GetDialect() } - - builder, err := NewBuilder(dialectVal.String()) - if err != nil { - return nil, err + if dialectVal == dialect.Unknown { + return nil, errors.New("schema dialect is not set") } - return builder, nil + return builders.NewBuilder(dialectVal.String()) } // Create creates a new table with the given name and blueprint. -// The blueprint function is used to define the structure of the table. -// It returns an error if the table creation fails. // // Example: // -// err := schema.Create(ctx, tx, "users", func(table *schema.Blueprint) { -// table.ID() -// table.String("name").Nullable(false) -// table.String("email").Unique().Nullable(false) -// table.String("password").Nullable() -// table.Timestamp("created_at").Default("CURRENT_TIMESTAMP").Nullable(false) -// table.Timestamp("updated_at").Default("CURRENT_TIMESTAMP").Nullable(false) +// schema.Create(ctx, "users", func(table *schema.Blueprint) { +// table.String("name") +// table.Integer("age") // }) -func Create(c Context, name string, blueprint func(table *Blueprint)) error { - builder, err := newBuilder() +func Create(c Context, name string, bp func(table *Blueprint)) error { + if c == nil || name == "" || bp == nil { + return errors.New("invalid arguments") + } + builder, err := newBuilder(c) if err != nil { return err } - - return builder.Create(c, name, blueprint) + return builder.Create(c, name, bp) } -// Drop removes the table with the given name. -// It returns an error if the table removal fails. +// Drop drops the table with the given name. // // Example: // -// err := schema.Drop(ctx, tx, "users") +// schema.Drop(ctx, "users") func Drop(c Context, name string) error { - builder, err := newBuilder() + if c == nil || name == "" { + return errors.New("invalid arguments") + } + builder, err := newBuilder(c) if err != nil { return err } - return builder.Drop(c, name) } -// DropIfExists removes the table with the given name if it exists. -// It returns an error if the table removal fails. +// DropIfExists drops the table with the given name if it exists. // // Example: // -// err := schema.DropIfExists(ctx, tx, "users") +// schema.DropIfExists(ctx, "users") func DropIfExists(c Context, name string) error { - builder, err := newBuilder() + if c == nil || name == "" { + return errors.New("invalid arguments") + } + builder, err := newBuilder(c) if err != nil { return err } - return builder.DropIfExists(c, name) } -// GetColumns retrieves the columns of the specified table. -// It returns a slice of Column structs representing the columns in the table. +// Table modifies the table with the given name using the provided blueprint. // // Example: // -// columns, err := schema.GetColumns(ctx, tx, "users") -func GetColumns(c Context, tableName string) ([]*Column, error) { - builder, err := newBuilder() +// schema.Table(ctx, "users", func(table *schema.Blueprint) { +// table.String("email").Unique() +// }) +func Table(c Context, name string, bp func(table *Blueprint)) error { + if c == nil || name == "" || bp == nil { + return errors.New("invalid arguments") + } + builder, err := newBuilder(c) if err != nil { - return nil, err + return err } - - return builder.GetColumns(c, tableName) + return builder.Table(c, name, bp) } -// GetIndexes retrieves the indexes of the specified table. -// It returns a slice of Index structs representing the indexes in the table. +// Rename renames a table from the given name to the new name. // // Example: // -// indexes, err := schema.GetIndexes(ctx, tx, "users") -func GetIndexes(c Context, tableName string) ([]*Index, error) { - builder, err := newBuilder() +// schema.Rename(ctx, "users", "app_users") +func Rename(c Context, from, to string) error { + if c == nil || from == "" || to == "" { + return errors.New("invalid arguments") + } + builder, err := newBuilder(c) if err != nil { - return nil, err + return err } - - return builder.GetIndexes(c, tableName) + return builder.Rename(c, from, to) } -// GetTables retrieves all tables in the database. -// It returns a slice of TableInfo structs containing information about each table. +// HasTable checks if a table with the given name exists. // // Example: // -// tables, err := schema.GetTables(ctx, tx) -func GetTables(c Context) ([]*TableInfo, error) { - builder, err := newBuilder() +// exists, err := schema.HasTable(ctx, "users") +func HasTable(c Context, name string) (bool, error) { + if c == nil || name == "" { + return false, errors.New("invalid arguments") + } + builder, err := newBuilder(c) if err != nil { - return nil, err + return false, err } - - return builder.GetTables(c) + return builder.HasTable(c, name) } // HasColumn checks if a column with the given name exists in the specified table. -// It returns true if the column exists, false otherwise. // // Example: // -// exists, err := schema.HasColumn(ctx, tx, "users", "email") -func HasColumn(c Context, tableName string, columnName string) (bool, error) { - builder, err := newBuilder() +// exists, err := schema.HasColumn(ctx, "users", "email") +func HasColumn(c Context, table, column string) (bool, error) { + if c == nil || table == "" || column == "" { + return false, errors.New("invalid arguments") + } + builder, err := newBuilder(c) if err != nil { return false, err } - - return builder.HasColumn(c, tableName, columnName) + return builder.HasColumn(c, table, column) } -// HasColumns checks if the specified columns exist in the given table. -// It returns true if all specified columns exist, false otherwise. +// HasColumns checks if all specified columns exist in the given table. // // Example: // -// exists, err := schema.HasColumns(ctx, tx, "users", []string{"email", "name"}) -// -// If any of the specified columns do not exist, it returns false. -func HasColumns(c Context, tableName string, columnNames []string) (bool, error) { - builder, err := newBuilder() +// exists, err := schema.HasColumns(ctx, "users", []string{"email", "name"}) +func HasColumns(c Context, table string, columns []string) (bool, error) { + if c == nil || table == "" || len(columns) == 0 { + return false, errors.New("invalid arguments") + } + builder, err := newBuilder(c) if err != nil { return false, err } - - return builder.HasColumns(c, tableName, columnNames) + return builder.HasColumns(c, table, columns) } -// HasIndex checks if an index with the given name exists in the specified table. -// It returns true if the index exists, false otherwise. +// HasIndex checks if an index exists on the specified table for the given columns. // // Example: // -// exists, err := schema.HasIndex(ctx, tx, "users", []string{"uk_users_email"}) // Checks if the index with name "uk_users_email" exists in the "users" table. -// -// exists, err := schema.HasIndex(ctx, tx, "users", []string{"email", "name"}) // Checks if a composite index exists on the "email" and "name" columns in the "users" table. -func HasIndex(c Context, tableName string, indexes []string) (bool, error) { - builder, err := newBuilder() +// exists, err := schema.HasIndex(ctx, "users", []string{"email"}) +func HasIndex(c Context, table string, columns []string) (bool, error) { + if c == nil || table == "" || len(columns) == 0 { + return false, errors.New("invalid arguments") + } + builder, err := newBuilder(c) if err != nil { return false, err } - - return builder.HasIndex(c, tableName, indexes) + return builder.HasIndex(c, table, columns) } -// HasTable checks if a table with the given name exists in the database. -// It returns true if the table exists, false otherwise. -// It returns an error if the check fails. +// GetColumns retrieves the columns of the specified table. // // Example: // -// exists, err := schema.HasTable(ctx, tx, "users") -func HasTable(c Context, name string) (bool, error) { - builder, err := newBuilder() +// columns, err := schema.GetColumns(ctx, "users") +func GetColumns(c Context, table string) ([]*Column, error) { + if c == nil || table == "" { + return nil, errors.New("invalid arguments") + } + builder, err := newBuilder(c) if err != nil { - return false, err + return nil, err } - - return builder.HasTable(c, name) + return builder.GetColumns(c, table) } -// Rename changes the name of the table from name to newName. -// It returns an error if the renaming fails. +// GetIndexes retrieves the indexes of the specified table. // // Example: // -// err := schema.Rename(ctx, tx, "users", "people") -func Rename(c Context, name string, newName string) error { - builder, err := newBuilder() +// indexes, err := schema.GetIndexes(ctx, "users") +func GetIndexes(c Context, table string) ([]*Index, error) { + if c == nil || table == "" { + return nil, errors.New("invalid arguments") + } + builder, err := newBuilder(c) if err != nil { - return err + return nil, err } - - return builder.Rename(c, name, newName) + return builder.GetIndexes(c, table) } -// Table modifies an existing table with the given name and blueprint. -// The blueprint function is used to define the modifications to the table. -// It returns an error if the table modification fails. +// GetTables retrieves the list of tables in the database. // // Example: // -// err := schema.Table(ctx, tx, "users", func(table *schema.Blueprint) { -// table.Column("name").String().Nullable(false) -// table.DropColumn("password") -// table.RenameColumn("email", "contact_email") -// }) -func Table(c Context, name string, blueprint func(table *Blueprint)) error { - builder, err := newBuilder() +// tables, err := schema.GetTables(ctx) +func GetTables(c Context) ([]*TableInfo, error) { + if c == nil { + return nil, errors.New("invalid arguments") + } + builder, err := newBuilder(c) if err != nil { - return err + return nil, err } + return builder.GetTables(c) +} + +// NewBuilder creates a new Builder instance for the specified dialect. +func NewBuilder(dialectValue string) (Builder, error) { + return builders.NewBuilder(dialectValue) +} + +// NewGrammar creates a new Grammar instance for the specified dialect. +func NewGrammar(dialectValue string) (blueprint.Grammar, error) { + return grammars.NewGrammar(dialectValue) +} + +// NewContext creates a new Context with the given base context, transaction, and options. +func NewContext(ctx context.Context, tx *sql.Tx, opts ...core.ContextOptions) Context { + return core.NewContext(ctx, tx, opts...) +} + +// NewDryRunContext creates a new DryRunContext with the given base context and options. +func NewDryRunContext(ctx context.Context, opts ...core.DryRunContextOptions) *core.DryRunContext { + return core.NewDryRunContext(ctx, opts...) +} + +// WithFilename returns a ContextOption that sets the filename for the context. +func WithFilename(filename string) core.ContextOptions { + return core.WithFilename(filename) +} + +// WithDialect returns a ContextOption that sets the dialect for the context. +func WithDialect(dialect string) core.ContextOptions { + return core.WithDialect(dialect) +} + +// WithDryRunDialect returns a DryRunContextOption that sets the dialect for the dry run context. +func WithDryRunDialect(dialect string) core.DryRunContextOptions { + return core.WithDryRunDialect(dialect) +} - return builder.Table(c, name, blueprint) +// NewBlueprintForTesting creates a new Blueprint instance for testing purposes with the given name and grammar. +func NewBlueprintForTesting(name string, g blueprint.Grammar) *Blueprint { + return blueprint.NewBlueprintForTesting(name, g) } diff --git a/schema/schema_test.go b/schema/schema_test.go index 016e92e..5d9112b 100644 --- a/schema/schema_test.go +++ b/schema/schema_test.go @@ -3,13 +3,14 @@ package schema_test import ( "context" "database/sql" - "fmt" "testing" "github.com/akfaiz/migris/internal/config" "github.com/akfaiz/migris/internal/dialect" + "github.com/akfaiz/migris/internal/testutil" "github.com/akfaiz/migris/schema" "github.com/stretchr/testify/suite" + "github.com/testcontainers/testcontainers-go" ) func TestSchema(t *testing.T) { @@ -21,6 +22,7 @@ type schemaTestSuite struct { ctx context.Context db *sql.DB + tc testcontainers.Container } func (s *schemaTestSuite) SetupSuite() { @@ -28,25 +30,17 @@ func (s *schemaTestSuite) SetupSuite() { ctx := context.Background() s.ctx = ctx - config := parseTestConfig() - - dsn := fmt.Sprintf( - "host=localhost port=5432 user=%s password=%s dbname=%s sslmode=disable", - config.Username, - config.Password, - config.Database, - ) - - db, err := sql.Open("pgx", dsn) - s.Require().NoError(err) - - err = db.Ping() + container, db, err := testutil.StartPostgresTestDB(s.ctx) s.Require().NoError(err) + s.tc = container s.db = db } func (s *schemaTestSuite) TearDownSuite() { _ = s.db.Close() + if s.tc != nil { + _ = s.tc.Terminate(s.ctx) + } } func (s *schemaTestSuite) TestCreate() { @@ -349,6 +343,21 @@ func (s *schemaTestSuite) TestHasIndex() { c := schema.NewContext(s.ctx, tx) + s.Run("when context is nil should return error", func() { + exists, err := schema.HasIndex(nil, "users", []string{"email"}) + s.Require().Error(err) + s.False(exists) + }) + s.Run("when table name is empty should return error", func() { + exists, err := schema.HasIndex(c, "", []string{"email"}) + s.Require().Error(err) + s.False(exists) + }) + s.Run("when columns is empty should return error", func() { + exists, err := schema.HasIndex(c, "users", nil) + s.Require().Error(err) + s.False(exists) + }) s.Run("when index exists should return true", func() { err := schema.Create(c, "users", func(table *schema.Blueprint) { table.ID() @@ -506,3 +515,129 @@ func (s *schemaTestSuite) TestTable() { s.Require().Error(err) }) } + +func (s *schemaTestSuite) TestChangeStateAware() { + tx, err := s.db.BeginTx(s.ctx, nil) + s.Require().NoError(err) + defer tx.Rollback() + + c := schema.NewContext(s.ctx, tx) + + // Create a table with a nullable column and a default value + err = schema.Create(c, "test_change", func(table *schema.Blueprint) { + table.ID() + table.String("bio").Nullable().Default("Hello") + }) + s.Require().NoError(err) + + // Change the column type but don't specify Nullable() or Default() + // It should REMAIN nullable and keep its default because of our hydration logic + err = schema.Table(c, "test_change", func(table *schema.Blueprint) { + table.Text("bio").Change() + }) + s.Require().NoError(err) + + // Verify it is still nullable + builder, err := schema.NewBuilder("postgres") + s.Require().NoError(err) + columns, err := builder.GetColumns(c, "test_change") + s.Require().NoError(err) + + var bioCol *schema.Column + for _, col := range columns { + if col.Name == "bio" { + bioCol = col + break + } + } + s.Require().NotNil(bioCol) + s.True(bioCol.Nullable, "bio column should still be nullable after type change") + s.True(bioCol.DefaultVal.Valid) + s.Contains(bioCol.DefaultVal.String, "Hello") +} + +type mockContext struct { + schema.Context + + dialect string +} + +func (m *mockContext) Dialect() string { + return m.dialect +} + +func (s *schemaTestSuite) TestConstructors() { + s.Run("NewBuilder", func() { + b, err := schema.NewBuilder("mysql") + s.Require().NoError(err) + s.Require().NotNil(b) + }) + + s.Run("NewGrammar", func() { + g, err := schema.NewGrammar("postgres") + s.Require().NoError(err) + s.Require().NotNil(g) + }) + + s.Run("NewContext", func() { + c := schema.NewContext(context.Background(), nil, schema.WithDialect("sqlite3"), schema.WithFilename("test.go")) + s.Require().NotNil(c) + s.Require().Equal("sqlite3", c.Dialect()) + }) + + s.Run("NewDryRunContext", func() { + c := schema.NewDryRunContext(context.Background(), schema.WithDryRunDialect("mysql")) + s.Require().NotNil(c) + s.Require().Equal("mysql", c.Dialect()) + }) + + s.Run("NewBlueprintForTesting", func() { + g, _ := schema.NewGrammar("sqlite3") + bp := schema.NewBlueprintForTesting("test", g) + s.Require().NotNil(bp) + s.Require().Equal("test", bp.Name) + }) + + s.Run("newBuilder error", func() { + originalDialect := config.GetDialect() + config.SetDialect(dialect.Unknown) + defer config.SetDialect(originalDialect) + + mc := &mockContext{dialect: "unknown_dialect"} + + err := schema.Create(mc, "users", func(_ *schema.Blueprint) {}) + s.Require().Error(err) + + err = schema.Drop(mc, "users") + s.Require().Error(err) + + err = schema.DropIfExists(mc, "users") + s.Require().Error(err) + + err = schema.Table(mc, "users", func(_ *schema.Blueprint) {}) + s.Require().Error(err) + err = schema.Rename(mc, "users", "new_users") + s.Require().Error(err) + + _, err = schema.HasTable(mc, "users") + s.Require().Error(err) + + _, err = schema.HasColumn(mc, "users", "id") + s.Require().Error(err) + + _, err = schema.HasColumns(mc, "users", []string{"id"}) + s.Require().Error(err) + + _, err = schema.HasIndex(mc, "users", []string{"id"}) + s.Require().Error(err) + + _, err = schema.GetColumns(mc, "users") + s.Require().Error(err) + + _, err = schema.GetIndexes(mc, "users") + s.Require().Error(err) + + _, err = schema.GetTables(mc) + s.Require().Error(err) + }) +} diff --git a/schema/sqlite_builder.go b/schema/sqlite_builder.go deleted file mode 100644 index e0cad41..0000000 --- a/schema/sqlite_builder.go +++ /dev/null @@ -1,273 +0,0 @@ -package schema - -import ( - "database/sql" - "errors" - "fmt" - "strings" -) - -type sqliteBuilder struct { - baseBuilder -} - -var _ Builder = (*sqliteBuilder)(nil) - -func newSqliteBuilder() Builder { - grammar := newSqliteGrammar() - - return &sqliteBuilder{ - baseBuilder: baseBuilder{grammar: grammar}, - } -} - -func (b *sqliteBuilder) GetColumns(c Context, tableName string) ([]*Column, error) { - if c == nil || tableName == "" { - return nil, errors.New("invalid arguments: context is nil or table name is empty") - } - - query, err := b.grammar.CompileColumns("", tableName) - if err != nil { - return nil, err - } - - rows, err := c.Query(query) - if err != nil { - return nil, err - } - defer rows.Close() - - var columns []*Column - for rows.Next() { - var col Column - var cid int - var notNullInt int - var pk int - if err = rows.Scan( - &cid, &col.Name, &col.TypeFull, - ¬NullInt, &col.DefaultVal, &pk, - ); err != nil { - return nil, err - } - - // SQLite PRAGMA table_info returns: cid, name, type, notnull, dflt_value, pk - col.TypeName = col.TypeFull - col.Nullable = notNullInt == 0 // In SQLite, notnull=1 means NOT NULL, notnull=0 means nullable - // pk > 0 indicates this column is part of the primary key, but we don't store it in Column struct - - columns = append(columns, &col) - } - if err = rows.Err(); err != nil { - return nil, err - } - - return columns, nil -} - -//nolint:gocognit -func (b *sqliteBuilder) GetIndexes(c Context, tableName string) ([]*Index, error) { - if c == nil || tableName == "" { - return nil, errors.New("invalid arguments: context is nil or table name is empty") - } - - query, err := b.grammar.CompileIndexes("", tableName) - if err != nil { - return nil, err - } - - rows, err := c.Query(query) - if err != nil { - return nil, err - } - defer rows.Close() - - var indexes []*Index - for rows.Next() { - var idx Index - var uniqueFlag int - var columnsStr string // Change to string for scanning - if err = rows.Scan(&idx.Name, &uniqueFlag, &columnsStr); err != nil { - return nil, err - } - idx.Unique = uniqueFlag == 1 - // Parse the columns string if it's not empty - if columnsStr != "" { - // Split comma-separated column names (basic parsing) - idx.Columns = strings.Split(columnsStr, ",") - for i, col := range idx.Columns { - idx.Columns[i] = strings.TrimSpace(col) - } - } - indexes = append(indexes, &idx) - } - if err = rows.Err(); err != nil { - return nil, err - } - - // For each index, get detailed column information - for _, idx := range indexes { - func() { - columnQuery := fmt.Sprintf("PRAGMA index_info(%q)", idx.Name) - var columnRows *sql.Rows - columnRows, err = c.Query(columnQuery) - if err != nil { - return // Skip if we can't get column info - } - - // Ensure rows are closed when this anonymous function returns - defer func() { - if closeErr := columnRows.Close(); closeErr != nil { - _ = closeErr // Acknowledge the error - } - }() - - var columns []string - for columnRows.Next() { - var seqno, cid int - var name string - if err = columnRows.Scan(&seqno, &cid, &name); err != nil { - return - } - columns = append(columns, name) - } - - // Check for iteration errors - if err = columnRows.Err(); err != nil { - return - } - - if len(columns) > 0 { - idx.Columns = columns - } - }() - } - - return indexes, nil -} - -func (b *sqliteBuilder) GetTables(c Context) ([]*TableInfo, error) { - if c == nil { - return nil, errors.New("invalid arguments: context is nil") - } - - query, err := b.grammar.CompileTables("") - if err != nil { - return nil, err - } - rows, err := c.Query(query) - if err != nil { - return nil, err - } - defer rows.Close() - - var tables []*TableInfo - for rows.Next() { - var table TableInfo - if err = rows.Scan(&table.Name, &table.Size, &table.Comment); err != nil { - return nil, err - } - tables = append(tables, &table) - } - if err = rows.Err(); err != nil { - return nil, err - } - - return tables, nil -} - -func (b *sqliteBuilder) HasColumn(c Context, tableName string, columnName string) (bool, error) { - if c == nil || columnName == "" { - return false, errors.New("invalid arguments: context is nil or column name is empty") - } - return b.HasColumns(c, tableName, []string{columnName}) -} - -func (b *sqliteBuilder) HasColumns(c Context, tableName string, columnNames []string) (bool, error) { - if c == nil || tableName == "" { - return false, errors.New("invalid arguments: context is nil or table name is empty") - } - if len(columnNames) == 0 { - return false, errors.New("no column names provided") - } - - columns, err := b.GetColumns(c, tableName) - if err != nil { - return false, err - } - columnsMap := make(map[string]bool) - for _, col := range columns { - columnsMap[col.Name] = true - } - for _, colName := range columnNames { - if _, exists := columnsMap[colName]; !exists { - return false, nil // If any column does not exist, return false - } - } - return true, nil // All specified columns exist -} - -//nolint:dupl // Similar code exists in other builder files -func (b *sqliteBuilder) HasIndex(c Context, tableName string, indexes []string) (bool, error) { - if c == nil || tableName == "" { - return false, errors.New("invalid arguments: context is nil or table name is empty") - } - - existingIndexes, err := b.GetIndexes(c, tableName) - if err != nil { - return false, err - } - if len(existingIndexes) == 0 { - return false, nil // No indexes found, so the specified indexes cannot exist - } - if len(indexes) == 0 { - return true, nil // No specific indexes to check, so we assume they exist - } - if len(indexes) == 1 { - for _, idx := range existingIndexes { - if idx.Name == indexes[0] { - return true, nil // If any specified index exists, return true - } - } - } - indexColumns := make(map[string]bool) - for _, idx := range indexes { - indexColumns[idx] = true // Create a map of specified indexes for quick lookup - } - - for _, index := range existingIndexes { - found := true - for _, indexCol := range index.Columns { - if _, exists := indexColumns[indexCol]; !exists { - found = false // If any column in the index does not match the specified indexes, set found to false - break - } - } - // If all columns in the index match the specified indexes, we found a match - if found { - return true, nil - } - } - - return false, nil // If no specified index exists, return false -} - -func (b *sqliteBuilder) HasTable(c Context, name string) (bool, error) { - if c == nil || name == "" { - return false, errors.New("invalid arguments: context is nil or table name is empty") - } - - query, err := b.grammar.CompileTableExists("", name) - if err != nil { - return false, err - } - - row := c.QueryRow(query) - var exists bool - if err = row.Scan(&exists); err != nil { - if errors.Is(err, sql.ErrNoRows) { - return false, nil // Table does not exist - } - return false, err // Other error occurred - } - return exists, nil // Return true if the table exists -} diff --git a/schema/sqlite_grammar.go b/schema/sqlite_grammar.go deleted file mode 100644 index 8fd8a95..0000000 --- a/schema/sqlite_grammar.go +++ /dev/null @@ -1,553 +0,0 @@ -package schema - -import ( - "errors" - "fmt" - "slices" - "strings" -) - -type sqliteGrammar struct { - baseGrammar -} - -func newSqliteGrammar() *sqliteGrammar { - return &sqliteGrammar{} -} - -// QuoteString overrides the base implementation to use double quotes for SQLite identifiers. -func (g *sqliteGrammar) QuoteString(s string) string { - return "\"" + s + "\"" -} - -// GetDefaultValue overrides the base implementation for SQLite-specific default value formatting. -func (g *sqliteGrammar) GetDefaultValue(value any) string { - if value == nil { - return "NULL" - } - switch v := value.(type) { - case Expression: - return v.String() - case bool: - // SQLite boolean values as integers without quotes - if v { - return "1" - } - return "0" - case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: - // Numeric values without quotes - return fmt.Sprintf("%v", v) - case float32, float64: - // Numeric values without quotes - return fmt.Sprintf("%v", v) - default: - // String values with single quotes (for SQL literals, not identifiers) - return fmt.Sprintf("'%v'", v) - } -} - -// CreateIndexName overrides the base implementation for SQLite-specific naming conventions. -func (g *sqliteGrammar) CreateIndexName(blueprint *Blueprint, idxType string, columns ...string) string { - tableName := blueprint.name - if strings.Contains(tableName, ".") { - parts := strings.Split(tableName, ".") - tableName = parts[len(parts)-1] // Use the last part as the table name - } - - switch idxType { - case "primary": - return fmt.Sprintf("pk_%s", tableName) - case "unique": - return fmt.Sprintf("uq_%s_%s", tableName, strings.Join(columns, "_")) - case "index": - return fmt.Sprintf("idx_%s_%s", tableName, strings.Join(columns, "_")) - case "fulltext": - return fmt.Sprintf("ft_%s_%s", tableName, strings.Join(columns, "_")) - default: - return "" - } -} - -func (g *sqliteGrammar) CompileTableExists(_ string, table string) (string, error) { - return fmt.Sprintf( - "SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = %s", - g.QuoteString(table), - ), nil -} - -func (g *sqliteGrammar) CompileTables(_ string) (string, error) { - return "SELECT name, 0 as size, '' as comment FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' ORDER BY name", nil -} - -func (g *sqliteGrammar) CompileColumns(_, table string) (string, error) { - return fmt.Sprintf("PRAGMA table_info(%s)", g.QuoteString(table)), nil -} - -func (g *sqliteGrammar) CompileIndexes(_, table string) (string, error) { - return fmt.Sprintf( - "SELECT name, 0 as unique_flag, '' as columns FROM sqlite_master WHERE type = 'index' AND tbl_name = %s", - g.QuoteString(table), - ), nil -} - -func (g *sqliteGrammar) CompileCreate(blueprint *Blueprint) (string, error) { - sql := g.compileCreateTable(blueprint) - return sql, nil -} - -func (g *sqliteGrammar) compileCreateTable(blueprint *Blueprint) string { - columns := g.getColumns(blueprint) - constraints := g.getTableConstraints(blueprint) - - var items []string - items = append(items, columns...) - items = append(items, constraints...) - - return fmt.Sprintf("CREATE TABLE %s (%s)", - g.QuoteString(blueprint.name), - strings.Join(items, ", "), - ) -} - -// getTableConstraints extracts table-level constraints that should be included in CREATE TABLE. -func (g *sqliteGrammar) getTableConstraints(blueprint *Blueprint) []string { - var constraints []string - - // Process commands to find constraints that should be included in CREATE TABLE - for _, cmd := range blueprint.commands { - switch cmd.name { - case commandForeign: - // Foreign keys must be defined at table creation time in SQLite - if sql := g.compileForeignConstraint(blueprint, cmd); sql != "" { - constraints = append(constraints, sql) - } - case commandPrimary: - // Composite primary keys should be defined at table level - if sql := g.compilePrimaryConstraint(blueprint, cmd); sql != "" { - constraints = append(constraints, sql) - } - } - } - - return constraints -} - -// compileForeignConstraint compiles foreign key constraint for inclusion in CREATE TABLE. -func (g *sqliteGrammar) compileForeignConstraint(_ *Blueprint, command *command) string { - if len(command.columns) == 0 || command.on == "" { - return "" - } - - foreign := fmt.Sprintf("FOREIGN KEY (%s) REFERENCES %s", - g.QuoteColumnize(command.columns), - g.QuoteString(command.on), - ) - - if len(command.references) > 0 { - foreign += fmt.Sprintf(" (%s)", g.QuoteColumnize(command.references)) - } - - if command.onUpdate != "" { - foreign += " ON UPDATE " + strings.ToUpper(command.onUpdate) - } - - if command.onDelete != "" { - foreign += " ON DELETE " + strings.ToUpper(command.onDelete) - } - - return foreign -} - -// compilePrimaryConstraint compiles composite primary key constraint for inclusion in CREATE TABLE. -func (g *sqliteGrammar) compilePrimaryConstraint(_ *Blueprint, command *command) string { - if len(command.columns) <= 1 { - // Single column primary keys should be handled at column level - return "" - } - - return fmt.Sprintf("PRIMARY KEY (%s)", g.QuoteColumnize(command.columns)) -} - -func (g *sqliteGrammar) CompileAdd(blueprint *Blueprint) (string, error) { - columns := g.getColumns(blueprint) - - return fmt.Sprintf("ALTER TABLE %s %s", - g.QuoteString(blueprint.name), - strings.Join(g.PrefixArray("ADD COLUMN ", columns), ", "), - ), nil -} - -func (g *sqliteGrammar) CompileChange(_ *Blueprint, _ *command) (string, error) { - return "", errors.New("SQLite does not support modifying columns") -} - -func (g *sqliteGrammar) CompileRename(bp *Blueprint, command *command) (string, error) { - return fmt.Sprintf("ALTER TABLE %s RENAME TO %s", - g.QuoteString(bp.name), - g.QuoteString(command.to), - ), nil -} - -func (g *sqliteGrammar) CompileDrop(blueprint *Blueprint) (string, error) { - return fmt.Sprintf("DROP TABLE %s", g.QuoteString(blueprint.name)), nil -} - -func (g *sqliteGrammar) CompileDropIfExists(blueprint *Blueprint) (string, error) { - return fmt.Sprintf("DROP TABLE IF EXISTS %s", g.QuoteString(blueprint.name)), nil -} - -func (g *sqliteGrammar) CompileDropColumn(_ *Blueprint, _ *command) (string, error) { - return "", errors.New("SQLite does not support dropping columns") -} - -func (g *sqliteGrammar) CompileRenameColumn(_ *Blueprint, _ *command) (string, error) { - return "", errors.New("SQLite does not support renaming columns") -} - -func (g *sqliteGrammar) CompileIndex(blueprint *Blueprint, command *command) (string, error) { - if slices.Contains(command.columns, "") { - return "", errors.New("index column cannot be empty") - } - - indexName := command.index - if indexName == "" { - indexName = g.CreateIndexName(blueprint, "index", command.columns...) - } - - return fmt.Sprintf("CREATE INDEX %s ON %s (%s)", - g.QuoteString(indexName), - g.QuoteString(blueprint.name), - g.QuoteColumnize(command.columns), - ), nil -} - -func (g *sqliteGrammar) CompileUnique(blueprint *Blueprint, command *command) (string, error) { - if slices.Contains(command.columns, "") { - return "", errors.New("unique index column cannot be empty") - } - - indexName := command.index - if indexName == "" { - indexName = g.CreateIndexName(blueprint, "unique", command.columns...) - } - - return fmt.Sprintf("CREATE UNIQUE INDEX %s ON %s (%s)", - g.QuoteString(indexName), - g.QuoteString(blueprint.name), - g.QuoteColumnize(command.columns), - ), nil -} - -// QuoteColumnize quotes individual column names for SQLite. -func (g *sqliteGrammar) QuoteColumnize(columns []string) string { - if len(columns) == 0 { - return "" - } - quoted := make([]string, len(columns)) - for i, col := range columns { - quoted[i] = g.QuoteString(col) - } - return strings.Join(quoted, ", ") -} - -func (g *sqliteGrammar) CompileFullText(_ *Blueprint, _ *command) (string, error) { - return "", errors.New("SQLite full-text search requires special setup") -} - -func (g *sqliteGrammar) CompilePrimary(_ *Blueprint, _ *command) (string, error) { - // SQLite composite primary keys are handled at table creation time via getTableConstraints - // Single column primary keys are handled at column level via modifiers - return "", nil -} - -func (g *sqliteGrammar) CompileDropIndex(_ *Blueprint, command *command) (string, error) { - indexName := command.index - if indexName == "" { - return "", errors.New("index name cannot be empty") - } - - return fmt.Sprintf("DROP INDEX %s", g.QuoteString(indexName)), nil -} - -func (g *sqliteGrammar) CompileDropUnique(blueprint *Blueprint, command *command) (string, error) { - return g.CompileDropIndex(blueprint, command) -} - -func (g *sqliteGrammar) CompileDropFulltext(blueprint *Blueprint, command *command) (string, error) { - return g.CompileDropIndex(blueprint, command) -} - -func (g *sqliteGrammar) CompileDropPrimary(_ *Blueprint, _ *command) (string, error) { - return "", errors.New("SQLite does not support dropping primary keys") -} - -func (g *sqliteGrammar) CompileRenameIndex(_ *Blueprint, _ *command) (string, error) { - return "", errors.New("SQLite does not support renaming indexes") -} - -func (g *sqliteGrammar) CompileForeign(_ *Blueprint, _ *command) (string, error) { - // SQLite foreign keys must be defined at table creation time, not as separate statements - // They are handled in compileCreateTable via getTableConstraints - return "", nil -} - -func (g *sqliteGrammar) CompileDropForeign(_ *Blueprint, _ *command) (string, error) { - return "", errors.New("SQLite does not support dropping foreign keys") -} - -func (g *sqliteGrammar) GetFluentCommands() []func(blueprint *Blueprint, command *command) string { - return []func(blueprint *Blueprint, command *command) string{ - // Add fluent command handlers here if needed - } -} - -func (g *sqliteGrammar) getColumns(blueprint *Blueprint) []string { - var columns []string - - for _, column := range blueprint.getAddedColumns() { - sql := strings.TrimSpace(strings.Join(g.modifiers(column, blueprint), " ")) - columns = append(columns, sql) - } - - return columns -} - -func (g *sqliteGrammar) getType(column *columnDefinition) string { - typeFuncMap := map[string]func(*columnDefinition) string{ - "char": g.typeChar, - "string": g.typeString, - "tinyText": g.typeTinyText, - "text": g.typeText, - "mediumText": g.typeMediumText, - "longText": g.typeLongText, - "bigInteger": g.typeBigInteger, - "integer": g.typeInteger, - "mediumInteger": g.typeMediumInteger, - "smallInteger": g.typeSmallInteger, - "tinyInteger": g.typeTinyInteger, - "float": g.typeFloat, - "double": g.typeDouble, - "decimal": g.typeDecimal, - "boolean": g.typeBoolean, - "enum": g.typeEnum, - "json": g.typeJSON, - "jsonb": g.typeJSONB, - "date": g.typeDate, - "dateTime": g.typeDateTime, - "dateTimeTz": g.typeDateTimeTz, - "time": g.typeTime, - "timeTz": g.typeTimeTz, - "timestamp": g.typeTimestamp, - "timestampTz": g.typeTimestampTz, - "year": g.typeYear, - "binary": g.typeBinary, - "uuid": g.typeUUID, - "geometry": g.typeGeometry, - "geography": g.typeGeography, - "point": g.typePoint, - } - if fn, ok := typeFuncMap[column.columnType]; ok { - return fn(column) - } - return "TEXT" -} - -// SQLite type mappings. -func (g *sqliteGrammar) typeChar(_ *columnDefinition) string { - return "TEXT" -} - -func (g *sqliteGrammar) typeString(_ *columnDefinition) string { - return "TEXT" -} - -func (g *sqliteGrammar) typeTinyText(_ *columnDefinition) string { - return "TEXT" -} - -func (g *sqliteGrammar) typeText(_ *columnDefinition) string { - return "TEXT" -} - -func (g *sqliteGrammar) typeMediumText(_ *columnDefinition) string { - return "TEXT" -} - -func (g *sqliteGrammar) typeLongText(_ *columnDefinition) string { - return "TEXT" -} - -func (g *sqliteGrammar) typeBigInteger(_ *columnDefinition) string { - return "INTEGER" -} - -func (g *sqliteGrammar) typeInteger(_ *columnDefinition) string { - return "INTEGER" -} - -func (g *sqliteGrammar) typeMediumInteger(_ *columnDefinition) string { - return "INTEGER" -} - -func (g *sqliteGrammar) typeSmallInteger(_ *columnDefinition) string { - return "INTEGER" -} - -func (g *sqliteGrammar) typeTinyInteger(_ *columnDefinition) string { - return "INTEGER" -} - -func (g *sqliteGrammar) typeFloat(_ *columnDefinition) string { - return "REAL" -} - -func (g *sqliteGrammar) typeDouble(_ *columnDefinition) string { - return "REAL" -} - -func (g *sqliteGrammar) typeDecimal(_ *columnDefinition) string { - return "NUMERIC" -} - -func (g *sqliteGrammar) typeBoolean(_ *columnDefinition) string { - return "INTEGER" -} - -func (g *sqliteGrammar) typeEnum(_ *columnDefinition) string { - return "TEXT" -} - -func (g *sqliteGrammar) typeJSON(_ *columnDefinition) string { - return "TEXT" -} - -func (g *sqliteGrammar) typeJSONB(_ *columnDefinition) string { - return "TEXT" -} - -func (g *sqliteGrammar) typeDate(_ *columnDefinition) string { - return "DATE" -} - -func (g *sqliteGrammar) typeDateTime(col *columnDefinition) string { - if col.useCurrent { - col.SetDefault(Expression("CURRENT_TIMESTAMP")) - } - return "DATETIME" -} - -func (g *sqliteGrammar) typeDateTimeTz(col *columnDefinition) string { - if col.useCurrent { - col.SetDefault(Expression("CURRENT_TIMESTAMP")) - } - return "DATETIME" -} - -func (g *sqliteGrammar) typeTime(_ *columnDefinition) string { - return "TIME" -} - -func (g *sqliteGrammar) typeTimeTz(_ *columnDefinition) string { - return "TIME" -} - -func (g *sqliteGrammar) typeTimestamp(col *columnDefinition) string { - if col.useCurrent { - col.SetDefault(Expression("CURRENT_TIMESTAMP")) - } - return "DATETIME" -} - -func (g *sqliteGrammar) typeTimestampTz(col *columnDefinition) string { - if col.useCurrent { - col.SetDefault(Expression("CURRENT_TIMESTAMP")) - } - return "DATETIME" -} - -func (g *sqliteGrammar) typeYear(_ *columnDefinition) string { - return "INTEGER" -} - -func (g *sqliteGrammar) typeBinary(_ *columnDefinition) string { - return "BLOB" -} - -func (g *sqliteGrammar) typeUUID(_ *columnDefinition) string { - return "TEXT" -} - -func (g *sqliteGrammar) typeGeometry(_ *columnDefinition) string { - return "TEXT" -} - -func (g *sqliteGrammar) typeGeography(_ *columnDefinition) string { - return "TEXT" -} - -func (g *sqliteGrammar) typePoint(_ *columnDefinition) string { - return "TEXT" -} - -func (g *sqliteGrammar) modifiers(column *columnDefinition, blueprint *Blueprint) []string { - var modifiers []string - - // Add the column name and type first - modifiers = append(modifiers, g.QuoteString(column.name), g.getType(column)) - - // SQLite modifier order: [CONSTRAINT name] [PRIMARY KEY | UNIQUE] [NOT NULL] [DEFAULT value] - for _, method := range []string{"primary", "unique", "nullable", "default"} { - if modifier := g.getModifier(method, column, blueprint); modifier != "" { - modifiers = append(modifiers, modifier) - } - } - - return modifiers -} - -func (g *sqliteGrammar) getModifier(name string, column *columnDefinition, blueprint *Blueprint) string { - switch name { - case "primary": - return g.modifyPrimary(column, blueprint) - case "unique": - return g.modifyUnique(column, blueprint) - case "nullable": - return g.modifyNullable(column, blueprint) - case "default": - return g.modifyDefault(column, blueprint) - default: - return "" - } -} - -func (g *sqliteGrammar) modifyPrimary(column *columnDefinition, _ *Blueprint) string { - if column.primary != nil && *column.primary { - primary := "PRIMARY KEY" - if column.autoIncrement != nil && *column.autoIncrement { - primary += " AUTOINCREMENT" - } - return primary - } - return "" -} - -func (g *sqliteGrammar) modifyUnique(column *columnDefinition, _ *Blueprint) string { - if column.unique != nil && *column.unique { - return "UNIQUE" - } - return "" -} - -func (g *sqliteGrammar) modifyNullable(column *columnDefinition, _ *Blueprint) string { - if column.nullable == nil || !*column.nullable { - return "NOT NULL" - } - return "" -} - -func (g *sqliteGrammar) modifyDefault(column *columnDefinition, _ *Blueprint) string { - if column.defaultValue != nil { - return fmt.Sprintf("DEFAULT %s", g.GetDefaultValue(column.defaultValue)) - } - return "" -} diff --git a/schema/sqlite_grammar_test.go b/schema/sqlite_grammar_test.go deleted file mode 100644 index 56f53da..0000000 --- a/schema/sqlite_grammar_test.go +++ /dev/null @@ -1,388 +0,0 @@ -package schema //nolint:testpackage // Need to access unexported members for testing - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestSqliteGrammar_CompileCreate(t *testing.T) { - g := newSqliteGrammar() - - tests := []struct { - name string - table string - blueprint func(table *Blueprint) - want string - wantErr bool - }{ - { - name: "basic table creation", - table: "users", - blueprint: func(table *Blueprint) { - table.ID() - table.String("name", 255) - }, - want: "CREATE TABLE \"users\" (\"id\" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, \"name\" TEXT NOT NULL)", - wantErr: false, - }, - { - name: "table with nullable column", - table: "posts", - blueprint: func(table *Blueprint) { - table.ID() - table.String("title", 255) - table.Text("content").Nullable() - }, - want: "CREATE TABLE \"posts\" (\"id\" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, \"title\" TEXT NOT NULL, \"content\" TEXT)", - wantErr: false, - }, - { - name: "table with default values", - table: "settings", - blueprint: func(table *Blueprint) { - table.ID() - table.String("key", 255) - table.Boolean("enabled").Default(true) - table.Integer("count").Default(0) - }, - want: "CREATE TABLE \"settings\" (\"id\" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, \"key\" TEXT NOT NULL, \"enabled\" INTEGER NOT NULL DEFAULT 1, \"count\" INTEGER NOT NULL DEFAULT 0)", - wantErr: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - bp := &Blueprint{name: tt.table, grammar: g} - tt.blueprint(bp) - - got, err := g.CompileCreate(bp) - if tt.wantErr { - assert.Error(t, err) - } else { - require.NoError(t, err) - assert.Equal(t, tt.want, got) - } - }) - } -} - -func TestSqliteGrammar_CompileAdd(t *testing.T) { - g := newSqliteGrammar() - - tests := []struct { - name string - table string - blueprint func(table *Blueprint) - want string - wantErr bool - }{ - { - name: "add single column", - table: "users", - blueprint: func(table *Blueprint) { - table.String("email", 255) - }, - want: "ALTER TABLE \"users\" ADD COLUMN \"email\" TEXT NOT NULL", - wantErr: false, - }, - { - name: "add multiple columns", - table: "posts", - blueprint: func(table *Blueprint) { - table.String("slug", 255) - table.Timestamp("published_at").Nullable() - }, - want: "ALTER TABLE \"posts\" ADD COLUMN \"slug\" TEXT NOT NULL, ADD COLUMN \"published_at\" DATETIME", - wantErr: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - bp := &Blueprint{name: tt.table, grammar: g} - tt.blueprint(bp) - - got, err := g.CompileAdd(bp) - if tt.wantErr { - assert.Error(t, err) - } else { - require.NoError(t, err) - assert.Equal(t, tt.want, got) - } - }) - } -} - -func TestSqliteGrammar_CompileDrop(t *testing.T) { - g := newSqliteGrammar() - - tests := []struct { - name string - table string - want string - wantErr bool - }{ - { - name: "drop table", - table: "users", - want: "DROP TABLE \"users\"", - wantErr: false, - }, - { - name: "drop table with special chars", - table: "user_profiles", - want: "DROP TABLE \"user_profiles\"", - wantErr: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - bp := &Blueprint{name: tt.table, grammar: g} - - got, err := g.CompileDrop(bp) - if tt.wantErr { - assert.Error(t, err) - } else { - require.NoError(t, err) - assert.Equal(t, tt.want, got) - } - }) - } -} - -func TestSqliteGrammar_CompileDropIfExists(t *testing.T) { - g := newSqliteGrammar() - - tests := []struct { - name string - table string - want string - wantErr bool - }{ - { - name: "drop table if exists", - table: "users", - want: "DROP TABLE IF EXISTS \"users\"", - wantErr: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - bp := &Blueprint{name: tt.table, grammar: g} - - got, err := g.CompileDropIfExists(bp) - if tt.wantErr { - assert.Error(t, err) - } else { - require.NoError(t, err) - assert.Equal(t, tt.want, got) - } - }) - } -} - -func TestSqliteGrammar_CompileRename(t *testing.T) { - g := newSqliteGrammar() - - tests := []struct { - name string - table string - newName string - want string - wantErr bool - }{ - { - name: "rename table", - table: "users", - newName: "customers", - want: "ALTER TABLE \"users\" RENAME TO \"customers\"", - wantErr: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - bp := &Blueprint{name: tt.table, grammar: g} - bp.rename(tt.newName) - - got, err := g.CompileRename(bp, bp.commands[0]) - if tt.wantErr { - assert.Error(t, err) - } else { - require.NoError(t, err) - assert.Equal(t, tt.want, got) - } - }) - } -} - -func TestSqliteGrammar_CompileIndex(t *testing.T) { - g := newSqliteGrammar() - - tests := []struct { - name string - table string - blueprint func(table *Blueprint) - want string - wantErr bool - }{ - { - name: "create index", - table: "users", - blueprint: func(table *Blueprint) { - table.Index("email") - }, - want: "CREATE INDEX \"idx_users_email\" ON \"users\" (\"email\")", - wantErr: false, - }, - { - name: "create composite index", - table: "posts", - blueprint: func(table *Blueprint) { - table.Index("user_id", "created_at") - }, - want: "CREATE INDEX \"idx_posts_user_id_created_at\" ON \"posts\" (\"user_id\", \"created_at\")", - wantErr: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - bp := &Blueprint{name: tt.table, grammar: g} - tt.blueprint(bp) - - got, err := g.CompileIndex(bp, bp.commands[0]) - if tt.wantErr { - assert.Error(t, err) - } else { - require.NoError(t, err) - assert.Equal(t, tt.want, got) - } - }) - } -} - -func TestSqliteGrammar_CompileUnique(t *testing.T) { - g := newSqliteGrammar() - - tests := []struct { - name string - table string - blueprint func(table *Blueprint) - want string - wantErr bool - }{ - { - name: "create unique index", - table: "users", - blueprint: func(table *Blueprint) { - table.Unique("email") - }, - want: "CREATE UNIQUE INDEX \"uq_users_email\" ON \"users\" (\"email\")", - wantErr: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - bp := &Blueprint{name: tt.table, grammar: g} - tt.blueprint(bp) - - got, err := g.CompileUnique(bp, bp.commands[0]) - if tt.wantErr { - assert.Error(t, err) - } else { - require.NoError(t, err) - assert.Equal(t, tt.want, got) - } - }) - } -} - -func TestSqliteGrammar_TypeMethods(t *testing.T) { - g := newSqliteGrammar() - - tests := []struct { - name string - fn func(*columnDefinition) string - want string - }{ - {"typeChar", g.typeChar, "TEXT"}, - {"typeString", g.typeString, "TEXT"}, - {"typeText", g.typeText, "TEXT"}, - {"typeInteger", g.typeInteger, "INTEGER"}, - {"typeBigInteger", g.typeBigInteger, "INTEGER"}, - {"typeFloat", g.typeFloat, "REAL"}, - {"typeDouble", g.typeDouble, "REAL"}, - {"typeDecimal", g.typeDecimal, "NUMERIC"}, - {"typeBoolean", g.typeBoolean, "INTEGER"}, - {"typeDate", g.typeDate, "DATE"}, - {"typeDateTime", g.typeDateTime, "DATETIME"}, - {"typeTimestamp", g.typeTimestamp, "DATETIME"}, - {"typeBinary", g.typeBinary, "BLOB"}, - {"typeUUID", g.typeUUID, "TEXT"}, - {"typeJSON", g.typeJSON, "TEXT"}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - col := &columnDefinition{} // Empty column definition for testing - got := tt.fn(col) - assert.Equal(t, tt.want, got) - }) - } -} - -func TestSqliteGrammar_UnsupportedOperations(t *testing.T) { - g := newSqliteGrammar() - bp := &Blueprint{name: "test_table", grammar: g} - cmd := &command{} // Empty command for testing - - tests := []struct { - name string - fn func() (string, error) - }{ - {"CompileChange", func() (string, error) { return g.CompileChange(bp, cmd) }}, - {"CompileDropColumn", func() (string, error) { return g.CompileDropColumn(bp, cmd) }}, - {"CompileRenameColumn", func() (string, error) { return g.CompileRenameColumn(bp, cmd) }}, - {"CompileFullText", func() (string, error) { return g.CompileFullText(bp, cmd) }}, - {"CompileDropPrimary", func() (string, error) { return g.CompileDropPrimary(bp, cmd) }}, - {"CompileRenameIndex", func() (string, error) { return g.CompileRenameIndex(bp, cmd) }}, - {"CompileDropForeign", func() (string, error) { return g.CompileDropForeign(bp, cmd) }}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - sql, err := tt.fn() - require.Error(t, err) - assert.Empty(t, sql) - }) - } -} - -func TestSqliteGrammar_SupportedButDelegated(t *testing.T) { - g := newSqliteGrammar() - bp := &Blueprint{name: "test_table", grammar: g} - cmd := &command{} // Empty command for testing - - // Test operations that are supported but handled at table creation time (not as separate commands) - tests := []struct { - name string - fn func() (string, error) - }{ - {"CompilePrimary", func() (string, error) { return g.CompilePrimary(bp, cmd) }}, - {"CompileForeign", func() (string, error) { return g.CompileForeign(bp, cmd) }}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - sql, err := tt.fn() - require.NoError(t, err, "Should not return error as these are handled at table creation time") - assert.Empty(t, sql, "Should return empty SQL as these are handled elsewhere") - }) - } -} diff --git a/up.go b/up.go index 65abb35..e323af6 100644 --- a/up.go +++ b/up.go @@ -30,10 +30,6 @@ func (m *Migrate) UpTo(version int64) error { // UpToContext applies the migrations up to the specified version. func (m *Migrate) UpToContext(ctx context.Context, version int64) error { - // Set global dry-run state for migration execution - setGlobalDryRunState(m.dryRun) - defer setGlobalDryRunState(false) // Reset after execution - if m.dryRun { return m.executeDryRunUp(ctx, version) } @@ -112,7 +108,7 @@ func (m *Migrate) determineMigrationsToApply(version, currentVersion int64) []*M var migrationsToApply []*Migration // Get all registered migrations that need to be applied (only pending ones) - for _, migration := range registeredMigrations { + for _, migration := range m.registry.migrationsSnapshot() { // Skip migrations that are already applied if migration.version <= currentVersion { continue @@ -145,7 +141,7 @@ func (m *Migrate) processDryRunMigrations( m.logger.DryRunMigrationStart(filepath.Base(migration.source), migration.version) // Create dry-run context for this migration - dryRunCtx := schema.NewDryRunContext(ctx) + dryRunCtx := schema.NewDryRunContext(ctx, schema.WithDryRunDialect(m.dialect.String())) // Execute the migration in dry-run mode var migrationFunc MigrationContext