---
name: step-05-db-validation
description: Validate database schema, migrations, seed data, and multi-tenant isolation against real SQL Server
prev_step: steps/step-04-api-smoke.md
next_step: null
---

# Step 5: Database Validation (SQL Server LocalDB)

> **Purpose:** Verify that migrations apply correctly, seed data inserts, LINQ queries execute real SQL, and multi-tenant isolation works on a REAL SQL Server instance — not SQLite.

---

## 1. Verify LocalDB Availability

```bash
sqllocaldb info MSSQLLocalDB
```

**If LocalDB is NOT available:**
- Display warning: `LocalDB not found — skipping DB validation`
- Set `DB_VALIDATION = SKIPPED`
- Skip to section 8 (Summary)

**If LocalDB is available:**
- Continue to section 2

---

## 2. Locate Infrastructure Project

```bash
INFRA_PROJECT=$(ls src/*Infrastructure*/*.csproj 2>/dev/null | head -1)
API_PROJECT=$(ls src/*Api*/*.csproj 2>/dev/null | head -1)

if [ -z "$INFRA_PROJECT" ] || [ -z "$API_PROJECT" ]; then
  echo "WARNING: Infrastructure or API project not found — skipping DB validation"
  DB_VALIDATION="SKIPPED"
  # Skip to section 8
fi
```

---

## 3. Check Pending Model Changes

```bash
dotnet ef migrations has-pending-model-changes \
  --project "$INFRA_PROJECT" \
  --startup-project "$API_PROJECT"
```

| Exit Code | Meaning | Action |
|-----------|---------|--------|
| 0 | No pending changes | PASS — continue |
| Non-zero | Model has changes not in a migration | **FAIL — BLOCKING** |

**If FAIL:**
- Display: `MIGRATION MISSING: EF Core model has changes not captured in a migration`
- Provide fix: `dotnet ef migrations add {SuggestedName} --project {InfraProject} --startup-project {ApiProject} -o Persistence/Migrations`
- **BLOCK** — do NOT continue until migration is created

---

## 4. Apply Migrations on Temp Database

```bash
# Create unique test database name
DB_NAME="SmartStack_Validate_$(date +%s)"
CONN_STRING="Server=(localdb)\\MSSQLLocalDB;Database=$DB_NAME;Integrated Security=true;TrustServerCertificate=true;Connect Timeout=120;"

# Apply ALL migrations from scratch
dotnet ef database update \
  --connection "$CONN_STRING" \
  --project "$INFRA_PROJECT" \
  --startup-project "$API_PROJECT"
```

| Result | Meaning | Action |
|--------|---------|--------|
| Success | All migrations apply cleanly | PASS — continue |
| Failure | Migration chain is broken | **FAIL — BLOCKING** |

**If FAIL:**
- Parse the error output
- Common issues:
  - `Invalid column name` → migration references a column that doesn't exist yet (ordering issue)
  - `There is already an object named` → duplicate migration or missing Down() method
  - `Cannot insert duplicate key` → seed data in migration conflicting
- **BLOCK** — fix the migration before continuing

---

## 5. Run Integration Tests Against Real SQL Server

```bash
# Find integration test project
TEST_PROJECT=$(ls tests/*Tests.Integration*/*.csproj 2>/dev/null | head -1)

if [ -n "$TEST_PROJECT" ]; then
  # Integration tests use DatabaseFixture which creates its own LocalDB instance
  # and applies real migrations — this validates LINQ → SQL translation
  dotnet test "$TEST_PROJECT" --no-build --verbosity normal
else
  echo "INFO: No integration test project found — running unit tests only"
  TEST_PROJECT=$(ls tests/*Tests*/*.csproj 2>/dev/null | head -1)
  if [ -n "$TEST_PROJECT" ]; then
    dotnet test "$TEST_PROJECT" --filter "FullyQualifiedName~Integration" --no-build --verbosity normal
  fi
fi
```

**What this validates:**
- **LINQ → SQL**: Queries execute against real SQL Server, not SQLite
- **Multi-tenant isolation**: Global query filters enforced at SQL Server level
- **Soft delete**: SQL Server handles IsDeleted filter correctly
- **EF Core configuration**: Indexes, relationships, constraints verified
- **Repository operations**: CRUD operations hit real SQL Server

| Result | Meaning |
|--------|---------|
| All tests pass | PASS — LINQ→SQL, isolation, and EF configs work on real SQL Server |
| Test failures | **FAIL** — investigate: some queries may not translate to SQL Server |

**Common SQLite → SQL Server differences caught:**
- `LIKE` is case-insensitive in SQLite, case-sensitive in SQL Server
- Date functions differ (`date('now')` vs `GETUTCDATE()`)
- String concatenation (`||` vs `+`)
- `LIMIT` vs `TOP`
- `AUTOINCREMENT` vs `IDENTITY`
- `COLLATE` behavior differences

---

## 5b. Verify Dev Seeding Configuration

Before checking seed data, verify that dev seeding is actually enabled:

```bash
# Check 1: EnableDevSeeding in appsettings
APPSETTINGS_DEV=$(ls appsettings.Development.json 2>/dev/null || ls src/*Api*/appsettings.Development.json 2>/dev/null)
APPSETTINGS_BASE=$(ls appsettings.json 2>/dev/null || ls src/*Api*/appsettings.json 2>/dev/null)

SEEDING_EXPLICIT_TRUE=false
SEEDING_EXPLICIT_FALSE=false

# Check Development appsettings first (overrides base)
if [ -n "$APPSETTINGS_DEV" ]; then
  if grep -q '"EnableDevSeeding".*true' "$APPSETTINGS_DEV" 2>/dev/null; then
    SEEDING_EXPLICIT_TRUE=true
  fi
fi

# Check base appsettings
if [ -n "$APPSETTINGS_BASE" ] && [ "$SEEDING_EXPLICIT_TRUE" = "false" ]; then
  if grep -q '"EnableDevSeeding".*false' "$APPSETTINGS_BASE" 2>/dev/null; then
    SEEDING_EXPLICIT_FALSE=true
  fi
fi

# Check 2: Program.cs override pattern
PROGRAM_CS=$(find . -name "Program.cs" -path "*/Api/*" 2>/dev/null | head -1)
PROGRAM_OVERRIDE=false
if [ -n "$PROGRAM_CS" ]; then
  if grep -q 'EnableDevSeeding.*IsDevelopment\|IsDevelopment.*EnableDevSeeding' "$PROGRAM_CS" 2>/dev/null; then
    PROGRAM_OVERRIDE=true
  fi
fi

# Evaluate
if [ "$SEEDING_EXPLICIT_FALSE" = "true" ] && [ "$PROGRAM_OVERRIDE" = "false" ]; then
  echo "⚠️ WARNING: EnableDevSeeding is FALSE in appsettings.json and NO Program.cs override detected"
  echo "DevDataSeeder will NOT execute — test data will be empty"
  echo "Fix option 1: Add to appsettings.Development.json: \"EnableDevSeeding\": true"
  echo "Fix option 2: Add to Program.cs: options.EnableDevSeeding = builder.Environment.IsDevelopment();"
  SEEDING_ENABLED=false
elif [ "$PROGRAM_OVERRIDE" = "true" ]; then
  echo "✓ EnableDevSeeding: Program.cs overrides to IsDevelopment() — active in Development"
  SEEDING_ENABLED=true
else
  echo "✓ EnableDevSeeding: explicitly enabled"
  SEEDING_ENABLED=true
fi
```

| Result | Meaning | Action |
|--------|---------|--------|
| Program.cs override detected | PASS | Dev seeding active in Development environment |
| Explicitly enabled | PASS | Dev seeding active |
| FALSE + no override | **WARNING** | DevDataSeeder won't run — seed data checks in section 6 will likely fail |

---

## 6. Verify Seed Data Execution

```bash
# Use the test database from step 4 to verify seed data
# Start the API against the temp database
dotnet run --project "$API_PROJECT" \
  --urls "http://localhost:5097" \
  -- --ConnectionStrings:DefaultConnection="$CONN_STRING" > /tmp/validate-seed-check.log 2>&1 &
SEED_PID=$!

# Wait for startup
STARTED=false
for i in $(seq 1 15); do
  if ! kill -0 $SEED_PID 2>/dev/null; then
    echo "API crashed during startup with seed data"
    break
  fi
  HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:5097/health 2>/dev/null)
  if [ "$HTTP_CODE" != "000" ]; then
    STARTED=true
    break
  fi
  sleep 1
done

if [ "$STARTED" = "true" ]; then
  # Verify seed data exists by checking API endpoints
  # Login with test credentials
  TOKEN=$(curl -s -X POST http://localhost:5097/api/auth/login \
    -H "Content-Type: application/json" \
    -d '{"email":"admin@smartstack.io","password":"Admin123!"}' \
    | grep -o '"accessToken":"[^"]*' | cut -d'"' -f4)

  if [ -n "$TOKEN" ]; then
    # Check navigation modules exist (core seed data)
    NAV_STATUS=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:5097/api/platform/navigation/modules \
      -H "Authorization: Bearer $TOKEN" \
      -H "X-Tenant-Id: 11111111-1111-1111-1111-111111111111")

    if [ "$NAV_STATUS" = "200" ]; then
      echo "PASS: Core seed data verified (navigation modules accessible)"
    else
      echo "WARNING: Core seed data may not be seeded (HTTP $NAV_STATUS)"
    fi
  else
    echo "WARNING: Could not authenticate — seed data verification via API skipped"
    echo "INFO: Integration tests already verify seed data at DB level via DatabaseFixture"
  fi
fi

# --- DefaultTenantId cross-schema FK validation ---
# Verify that SeedConstants.DefaultTenantId exists in core.tenant_Tenants
SEED_CONSTANTS=$(find . -path "*/SeedConstants.cs" 2>/dev/null | head -1)
if [ -n "$SEED_CONSTANTS" ]; then
  TENANT_GUID=$(grep -oP 'DefaultTenantId\s*=\s*Guid\.Parse\("([^"]+)"\)' "$SEED_CONSTANTS" | grep -oP '"[^"]+"' | tr -d '"')

  if [ -n "$TENANT_GUID" ] && [ -n "$DB_NAME" ]; then
    TENANT_EXISTS=$(sqlcmd -S "(localdb)\MSSQLLocalDB" -d "$DB_NAME" -Q "
      SET NOCOUNT ON;
      SELECT COUNT(*) FROM core.tenant_Tenants WHERE Id = '$TENANT_GUID'
    " -h -1 2>/dev/null | tr -d ' ')

    if [ "$TENANT_EXISTS" = "0" ]; then
      echo "❌ FAIL: SeedConstants.DefaultTenantId ($TENANT_GUID) does NOT exist in core.tenant_Tenants"
      echo "DevDataSeeder inserts rows with an invalid FK — causes runtime 500 errors"
      echo "Fix: Use a TenantId that is seeded by InitializeSmartStackAsync()"
    else
      echo "✓ DefaultTenantId ($TENANT_GUID) exists in core.tenant_Tenants"
    fi
  fi
fi

# Cleanup
kill $SEED_PID 2>/dev/null
wait $SEED_PID 2>/dev/null
rm -f /tmp/validate-seed-check.log
```

---

## 7. Cleanup Temp Database

```bash
# Drop the temp database created in step 4
sqlcmd -S "(localdb)\MSSQLLocalDB" -Q "
  IF DB_ID('$DB_NAME') IS NOT NULL
  BEGIN
    ALTER DATABASE [$DB_NAME] SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
    DROP DATABASE [$DB_NAME];
  END
" 2>/dev/null

# Alternative if sqlcmd is not available
if [ $? -ne 0 ]; then
  dotnet ef database drop --force \
    --connection "$CONN_STRING" \
    --project "$INFRA_PROJECT" \
    --startup-project "$API_PROJECT" 2>/dev/null
fi
```

---

## 8. Validation Summary

```
## DB Validation: {EntityName}

| Check                          | Result    | Details                                    |
|--------------------------------|-----------|--------------------------------------------|
| LocalDB available              | PASS/SKIP | sqllocaldb info MSSQLLocalDB               |
| Pending model changes          | PASS/FAIL | dotnet ef migrations has-pending           |
| Migrations apply cleanly       | PASS/FAIL | dotnet ef database update on temp DB       |
| Integration tests (SQL Server) | PASS/FAIL | dotnet test --filter Integration           |
| EnableDevSeeding active        | PASS/WARN | Program.cs override or appsettings check   |
| DefaultTenantId FK valid       | PASS/FAIL | SeedConstants GUID exists in tenant_Tenants|
| Seed data accessible           | PASS/WARN | API endpoints return seeded data           |
| Temp DB cleanup                | PASS/WARN | Database dropped                           |

DB Validation Result: {PASS / FAIL / SKIPPED}
```

**Interpretation:**
- **PASS**: All 8 checks covered — migrations, SQL Server, LINQ→SQL, isolation, seeding config, tenant FK, seed data verified
- **FAIL**: One or more checks failed — fix before committing
- **SKIPPED**: LocalDB not available — tests ran on SQLite only (partial coverage)

---

## Error Classification (DB-specific)

| Error Pattern | Category | Fix |
|---------------|----------|-----|
| `Invalid column name '{Column}'` | Migration ordering | Reorder migration or split into multiple |
| `Cannot insert duplicate key` | Seed data conflict | Make seed data idempotent (check before insert) |
| `String or binary data would be truncated` | Column length | Increase `MaxLength` in EF config |
| `The INSERT statement conflicted with the FOREIGN KEY constraint` | Missing parent data | Ensure parent entities are seeded first |
| `Cannot insert the value NULL into column` | Missing NOT NULL | Add default value or make column nullable |
| `Login failed for user` | LocalDB auth | Run `sqllocaldb start MSSQLLocalDB` |
| `A network-related or instance-specific error` | LocalDB not running | Run `sqllocaldb start MSSQLLocalDB` |
| `DefaultTenantId does NOT exist in core.tenant_Tenants` | Phantom FK | Use a TenantId seeded by `InitializeSmartStackAsync()` |
| `EnableDevSeeding is FALSE` + empty tables | Seeding disabled | Add `options.EnableDevSeeding = builder.Environment.IsDevelopment()` in Program.cs |
