diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index ab5931d..165036f 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -11,21 +11,6 @@ jobs: name: Integration Tests runs-on: ubuntu-latest - services: - postgres: - image: postgres:16-alpine - env: - POSTGRES_USER: relspec - POSTGRES_PASSWORD: relspec_test_password - POSTGRES_DB: relspec_test - ports: - - 5432:5432 - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 - steps: - name: Checkout code uses: actions/checkout@v4 @@ -46,6 +31,29 @@ jobs: - name: Download dependencies run: go mod download + - name: Start PostgreSQL container + run: | + docker run -d \ + --name relspec-test-postgres \ + -e POSTGRES_USER=relspec \ + -e POSTGRES_PASSWORD=relspec_test_password \ + -e POSTGRES_DB=relspec_test \ + -p 5432:5432 \ + postgres:16-alpine + + - name: Wait for PostgreSQL to be ready + run: | + echo "Waiting for PostgreSQL to start..." + for i in {1..30}; do + if docker exec relspec-test-postgres pg_isready -U relspec -d relspec_test > /dev/null 2>&1; then + echo "PostgreSQL is ready!" + break + fi + echo "Waiting... ($i/30)" + sleep 1 + done + sleep 2 + - name: Install PostgreSQL client run: | sudo apt-get update @@ -75,8 +83,14 @@ jobs: RELSPEC_TEST_PG_CONN: postgres://relspec:relspec_test_password@localhost:5432/relspec_test run: make test-integration + - name: Stop PostgreSQL container + if: always() + run: | + docker stop relspec-test-postgres || true + docker rm relspec-test-postgres || true + - name: Summary if: always() run: | echo "Integration tests completed." - echo "PostgreSQL service was running on localhost:5432" + echo "PostgreSQL container has been cleaned up." diff --git a/Makefile b/Makefile index 7a609b5..a1ddaf9 100644 --- a/Makefile +++ b/Makefile @@ -26,7 +26,7 @@ test: test-unit ## Run all unit tests (alias for test-unit) test-unit: ## Run unit tests (excludes integration tests) @echo "Running unit tests..." - $(GOTEST) -v -race -coverprofile=coverage.out -covermode=atomic $$(go list ./... | grep -v '/tests/integration' | grep -v '/pkg/readers/pgsql') + $(GOTEST) -v -race -coverprofile=coverage.out -covermode=atomic $$(go list ./... | grep -v '/tests/integration' | grep -v '/tests/assets' | grep -v '/pkg/readers/pgsql') test-integration: ## Run integration tests (requires RELSPEC_TEST_PG_CONN environment variable) @echo "Running integration tests..." diff --git a/pkg/readers/bun/reader.go b/pkg/readers/bun/reader.go index 7b77bed..5cd5636 100644 --- a/pkg/readers/bun/reader.go +++ b/pkg/readers/bun/reader.go @@ -382,6 +382,23 @@ func (r *Reader) isRelationship(tag string) bool { return strings.Contains(tag, "bun:\"rel:") || strings.Contains(tag, ",rel:") } +// getRelationType extracts the relationship type from a bun tag +func (r *Reader) getRelationType(bunTag string) string { + if strings.Contains(bunTag, "rel:has-many") { + return "has-many" + } + if strings.Contains(bunTag, "rel:belongs-to") { + return "belongs-to" + } + if strings.Contains(bunTag, "rel:has-one") { + return "has-one" + } + if strings.Contains(bunTag, "rel:many-to-many") { + return "many-to-many" + } + return "" +} + // parseRelationshipConstraints parses relationship fields to extract foreign key constraints func (r *Reader) parseRelationshipConstraints(table *models.Table, structType *ast.StructType, structMap map[string]*models.Table) { for _, field := range structType.Fields.List { @@ -409,27 +426,50 @@ func (r *Reader) parseRelationshipConstraints(table *models.Table, structType *a } // Parse the join information: join:user_id=id - // This means: referencedTable.user_id = thisTable.id + // This means: thisTable.user_id = referencedTable.id joinInfo := r.parseJoinInfo(bunTag) if joinInfo == nil { continue } - // The FK is on the referenced table + // Determine which table gets the FK based on relationship type + relType := r.getRelationType(bunTag) + + var fkTable *models.Table + var fkColumn, refTable, refColumn string + + if relType == "belongs-to" { + // For belongs-to: FK is on the current table + // join:user_id=id means table.user_id references referencedTable.id + fkTable = table + fkColumn = joinInfo.ForeignKey + refTable = referencedTable.Name + refColumn = joinInfo.ReferencedKey + } else if relType == "has-many" { + // For has-many: FK is on the referenced table + // join:id=user_id means referencedTable.user_id references table.id + fkTable = referencedTable + fkColumn = joinInfo.ReferencedKey + refTable = table.Name + refColumn = joinInfo.ForeignKey + } else { + continue + } + constraint := &models.Constraint{ - Name: fmt.Sprintf("fk_%s_%s", referencedTable.Name, table.Name), + Name: fmt.Sprintf("fk_%s_%s", fkTable.Name, refTable), Type: models.ForeignKeyConstraint, - Table: referencedTable.Name, - Schema: referencedTable.Schema, - Columns: []string{joinInfo.ForeignKey}, - ReferencedTable: table.Name, - ReferencedSchema: table.Schema, - ReferencedColumns: []string{joinInfo.ReferencedKey}, + Table: fkTable.Name, + Schema: fkTable.Schema, + Columns: []string{fkColumn}, + ReferencedTable: refTable, + ReferencedSchema: fkTable.Schema, + ReferencedColumns: []string{refColumn}, OnDelete: "NO ACTION", // Bun doesn't specify this in tags OnUpdate: "NO ACTION", } - referencedTable.Constraints[constraint.Name] = constraint + fkTable.Constraints[constraint.Name] = constraint } }