# ADR-011 : ORM et Accès Données

**Statut** : Accepté
**Date** : 2025-01-20

## Contexte

RoadWave nécessite des requêtes SQL complexes (PostGIS géospatiales) avec performance optimale et type safety. Le choix entre ORM, query builder ou SQL brut impacte maintenabilité et performance.

## Décision

**sqlc** pour génération de code Go type-safe depuis SQL.

## Alternatives considérées

| Solution | Performance | Type Safety | Contrôle SQL | Courbe apprentissage |
|----------|-------------|-------------|--------------|----------------------|
| **sqlc** | Excellente | Très haute | Total | Faible |
| GORM | Moyenne | Moyenne | Limité | Faible |
| pgx + SQL brut | Excellente | Faible | Total | Moyenne |
| sqlx | Bonne | Faible | Total | Faible |

## Justification

- **Performance** : Génération compile-time, zero overhead runtime
- **Type safety** : Structs Go générées automatiquement, erreurs détectées à la compilation
- **Contrôle SQL** : Requêtes PostGIS complexes écrites en pur SQL (pas de limitations ORM)
- **Maintenabilité** : Modifications SQL → `sqlc generate` → code mis à jour
- **Simplicité** : Pas de magic, code généré lisible et debuggable

## Workflow

```sql
-- queries/content.sql
-- name: GetContentNearby :many
SELECT id, title, ST_Distance(location, $1::geography) as distance
FROM contents
WHERE ST_DWithin(location, $1::geography, $2)
ORDER BY distance
LIMIT $3;
```

```bash
sqlc generate
```

```go
// Code Go type-safe généré automatiquement
contents, err := q.GetContentNearby(ctx, location, radius, limit)
```

## Gestion des Types PostGIS

**Problème** : sqlc génère du code depuis SQL, mais les types PostGIS (`geography`, `geometry`) ne mappent pas naturellement en Go → types opaques (`[]byte`, `interface{}`), perte de type safety.

### Solution : Wrappers Typés + Fonctions de Conversion

**Architecture** :

```
SQL (PostGIS)
    ↓ ST_AsGeoJSON() / ST_AsText() / pgtype.Point
    ↓
Code Go (wrapper types)
    ↓
Business Logic (strongly-typed)
```

**Implémentation** :

```go
// backend/internal/geo/types.go
package geo

import (
    "database/sql/driver"
    "encoding/json"
    "github.com/jackc/pgx/v5/pgtype"
)

// GeoJSON représente un point géographique en format JSON
type GeoJSON struct {
    Type        string    `json:"type"` // "Point"
    Coordinates [2]float64 `json:"coordinates"` // [lon, lat]
}

// Value() et Scan() implémentent sql.Valuer et sql.Scanner
// pour conversion automatique avec sqlc
func (g GeoJSON) Value() (driver.Value, error) {
    return json.Marshal(g)
}

func (g *GeoJSON) Scan(value interface{}) error {
    bytes, _ := value.([]byte)
    return json.Unmarshal(bytes, g)
}

// Distance calcule la distance entre 2 points (Haversine)
func (g GeoJSON) Distance(other GeoJSON) float64 {
    // Implémentation Haversine formula
    // ...
}

// WKT représente un point en Well-Known Text
type WKT string // "POINT(2.3522 48.8566)"

func (w WKT) Scan(value interface{}) error {
    if str, ok := value.(string); ok {
        *w = WKT(str)
    }
    return nil
}
```

### Patterns SQL Recommandés

**Pattern 1 : GeoJSON (recommandé pour frontend)**

```sql
-- queries/poi.sql
-- name: GetPOIsNearby :many
SELECT
    id,
    name,
    ST_AsGeoJSON(location)::jsonb as location,  -- ← Conversion en JSON
    ST_Distance(location, $1::geography) as distance_meters
FROM points_of_interest
WHERE ST_DWithin(location, $1::geography, $2)
ORDER BY distance_meters
LIMIT $3;
```

```go
// Code généré par sqlc
type GetPOIsNearbyRow struct {
    ID                int64
    Name              string
    Location          json.RawMessage // ← Peut être parsé en GeoJSON
    DistanceMeters    float64
}

// Utilisation
rows, err := q.GetPOIsNearby(ctx, userLocation, radius, limit)
for _, row := range rows {
    var poi geo.GeoJSON
    json.Unmarshal(row.Location, &poi)
    // poi est maintenant strongly-typed
}
```

**Pattern 2 : WKT (pour debug/logging)**

```sql
-- name: GetPOILocation :one
SELECT
    id,
    ST_AsText(location) as location_wkt  -- ← "POINT(2.3522 48.8566)"
FROM points_of_interest
WHERE id = $1;
```

**Pattern 3 : Utiliser pgtype pour types natifs**

```sql
-- name: GetDistanceBetweenPOIs :one
SELECT
    ST_Distance(
        (SELECT location FROM points_of_interest WHERE id = $1),
        (SELECT location FROM points_of_interest WHERE id = $2)
    )::float8 as distance_meters;  -- ← Force conversion en float64
```

### Index PostGIS pour Performance

Créer un index GIST pour optimiser les requêtes ST_DWithin :

```sql
-- migrations/002_add_postgis_indexes.up.sql
CREATE INDEX idx_poi_location_gist
ON points_of_interest USING GIST(location);

CREATE INDEX idx_user_last_position_gist
ON user_locations USING GIST(last_position);
```

### Checklist d'Implémentation

- [ ] Créer package `backend/internal/geo/types.go` avec wrappers
- [ ] Implémenter `Scan/Value` pour conversion automatique
- [ ] Écrire requêtes SQL avec `ST_AsGeoJSON()` / `ST_AsText()`
- [ ] Ajouter index GIST sur colonnes géographiques
- [ ] Documenter patterns dans `backend/README.md`
- [ ] Tests d'intégration avec Testcontainers (PostGIS réel)

### Impact

- ✅ **Type safety** retrouvée : Plus de `interface{}` opaques
- ✅ **Performance** : Index GIST + conversion optimisée
- ✅ **Maintenabilité** : Patterns clairs, réutilisables
- ❌ **Complexité** : Une couche de plus, mais justifiée

**Référence** : Résout incohérence #4 dans [INCONSISTENCIES-ANALYSIS.md](../INCONSISTENCIES-ANALYSIS.md#4--orm-sqlc-vs-types-postgis)

## Conséquences

- Dépendance : `github.com/sqlc-dev/sqlc`
- Fichier `sqlc.yaml` à la racine pour configuration
- Migrations gérées séparément avec `golang-migrate`
- CI doit exécuter `sqlc generate` pour valider cohérence SQL/Go

**Librairies** : Voir [ADR-018](018-librairies-go.md) pour stack complet (sqlc + golang-migrate + pgx)
