bookmark_borderPostgres – Équivalence manquée sur les index d’Arrays

Expliquons la situation avec un exemple concret. Étant donné la structure de tableau suivante :

CREATE TABLE data (
    name TEXT PRIMARY KEY,
    categories TEXT[]
);

Les deux requêtes suivantes donnent exactement le même résultat.

  1. SELECT COUNT(*) FROM data WHERE 'cafes' = ANY(categories);
  2. SELECT COUNT(*) FROM data WHERE categories && ARRAY['cafes']::TEXT[];

Pour y parvenir, elles utilisent des opérateurs différents et, sous le capot, la différence n’est pas évidente. La première, qui utilise l’opérateur ANY, itère sur l’array des catégories et retourne vrai dès qu’une correspondance est trouvée. La seconde utilise « && », l’opérateur de chevauchement, et renvoie un résultat positif si les tableaux ont des éléments en commun. Dans notre cas, le tableau de droite n’a qu’un seul élément, la requête est donc équivalent à la première version, qui utilise l’opérateur ANY.

L’enjeu, c’est que si vous devez indexer des catégories spécifiques, l’index doit correspondre exactement aux requêtes. En suivant le même ordre, vous auriez besoin de

  1. CREATE INDEX idx_categories_2_any ON data (name) WHERE 'cafes' = ANY(categories); pour la première requête.
  2. CREATE INDEX idx_categories_2_intersect ON data (name) WHERE categories && ARRAY['cafes']::TEXT[]; pour la deuxième requête.

Même si les requêtes donnent des résultats identiques, il semble que le moteur reconnaisse pas l’équivalence. Si vous n’avez que l’index « ANY » et que vous utilisez la requête « && », le moteur n’utilisera pas l’index. L’inverse est également vrai : si vous n’avez que l’index « && » et que vous utilisez la requête « ANY », le moteur n’utilisera pas l’index non plus.

J’ai confirmé que c’est le cas pour les versions 15 à 17 de Postgres. Je n’ai pas essayé la version 14 ou les versions inférieures.

Si vous vous demandez si un formulation est plus performante que l’autre, il semble qu’elles soient identiques. Je l’ai testé sur des données de production chez Local Logic, sur notre table de points d’intérêts, qui a plus de 6 millions de lignes, avec 29 catégories. L’exécution de EXPLAIN (analyse, buffers) SELECT… sur les deux formes de la requête a donné exactement le même coût.

bookmark_borderMesures de l’impact de Sentry sur le démarrage des Lambdas AWS

Chez Local Logic, nous nous appuyons fortement sur les fonctions AWS lambda. Nous surveillons actuellement notre environnement avec Sentry via leur sdk et savons que nous pourrions alternativement utiliser la couche (layer) sentry. Comme un bon développeur, je me suis demandé quels étaient les avantages et les inconvénients. Évidemment, l’un d’entre eux est: quelle option est la plus rapide? Je les ai donc comparées et je partagerai les résultats dans ce billet.

Continue reading « Mesures de l’impact de Sentry sur le démarrage des Lambdas AWS »

bookmark_borderC’est un piège! Postgis Geometry avec un SRID 4326 n’est pas un type Geography

Longue histoire courte, vous créez une table avec une colonne geometry qui stocke des points avec srid 4326 (lire coordonnées géographiques lat/lng, ou devrais-je écrire lng/lat). Cela devrait être interprété comme geography, n’est-ce pas? Pas du tout!

Continue reading « C’est un piège! Postgis Geometry avec un SRID 4326 n’est pas un type Geography »

bookmark_borderModèle hybride Pydantic/FastAPI

Histoire courte, j’ai un API qui n’est pas sous FastAPI, mais je me sers quand même de FastAPI pour générer la documentation au format d’OpenAPI.

J’ai donc des modèles pour les points de terminaisons et je souhaite qu’ils fonctionnent avec et sans FastAPI, car dans leur version pure, FastAPI n’est pas déployé. (Pour faire une autre histoire courte, installer FastAPI alourdi un peu le paquet à déployer, mais surtout les démarrage à froid des lambdas AWS à cause du délai d’initialisation de Sentry. Sentry détecte que FastAPI est présent dans l’environnement et ajoute automatiquement du monitoring, ce qui prend un bon 500ms supplémentaire. Mesuré en décembre 2023.)

Bref, en m’inspirant fortement d’une réponse sur StackOverflow, j’ai produit ceci.

# models.py
from typing import Annotated, Any, cast

from pydantic import BaseModel, Field
from pydantic.fields import FieldInfo

try:
    from fastapi import Query  # type:ignore[import-not-found]

    def FieldQuery(*args: Any, **kwargs: Any) -> FieldInfo:  # noqa:N802
        return cast(FieldInfo, Field(Query(*args, **kwargs)))

except ImportError:

    def FieldQuery(*args: Any, **kwargs: Any) -> FieldInfo:  # noqa:N802
        return cast(FieldInfo, Field(*args, **kwargs))  # type: ignore[pydantic-field]


class QueryParams(BaseModel):
    limit: Annotated[
        int,
        FieldQuery(
            -1,
            description="The limit of values to fetch. -1 means no limit.",
        ),
    ]

Puis, pour la route

# routes.py
from fastapi import FastAPI

from models import QueryParams

app = FastAPI()

@app.get("/things")
def get_things(
    params: Annotated[QueryParams, Depends()],
) -> dict:
    ...

Est-ce parfait? Avec les « type: ignore », « noqa », « import try/except », pas dutout. Ça ressemble à un gros hack.

Est-ce que ça fonctionne? Oh que oui!

Note: Les tags « noqa » et « type: ignore » sont respectivement liés au linting avec ruff et mypy.

bookmark_borderMon coffre à outils Python, édition 2024

Ça fait longtemps que je souhaite publier annuellement mes outils python préférés. C’est pratique pour plein de gens et ça permet de voir l’évolution de ceux-ci au fil du temps et des projets sur lesquels je contribue.

Voici donc la première édition de cette liste.

Continue reading « Mon coffre à outils Python, édition 2024 »

bookmark_borderRecette: Migrer un blog WordPress vers AWS Lightsail

Le blog sur lequel vous vous trouvez actuellement était hébergé sur un serveur privé virtuel. Mon hébergeur a décidé de cesser ses activités et j’ai dû partir. Il m’a gentiment suggéré de considérer AWS Lightsail comme une option de migration. C’est ce que j’ai fait et c’est là que ce blog est maintenant hébergé.

Ce blog n’est pas nouveau. Il avait déjà du contenu. Il dispose également de personnalisations telles que des plugins et des thèmes. Cet article explique comment exporter le contenu et les personnalisations d’un ancien blog et les importer sur Lightsail. Sa précision peut varier en fonction du niveau de personnalisation de votre blog. Une connaissance de la ligne de commande (CLI) est requise.

Continue reading « Recette: Migrer un blog WordPress vers AWS Lightsail »

bookmark_borderModèle de FastAPI Stripe Webhook

L’équivalent FastAPI de vérifier-que-les-événements-proviennent-de-stripe

import os

from http import HTTPStatus
from typing import Annotated

import stripe
from fastapi import Depends, FastAPI, Header, HTTPException, Request


app = FastAPI()


async def get_body(request: Request) -> bytes:
    return await request.body()


@app.post("/webhook", status_code=HTTPStatus.NO_CONTENT)
def post_report(
    stripe_signature: Annotated[str, Header(alias="stripe-signature")],
    body: bytes = Depends(get_body),
) -> None:
    endpoint_secret = os.environ["ENDPOINT_SECRET"]

    try:
        # signature validation
        event = stripe.Webhook.construct_event(body, stripe_signature, endpoint_secret)
    except ValueError as e:
        # Invalid payload
        raise HTTPException(status_code=HTTPStatus.BAD_REQUEST) from e
    except stripe.error.SignatureVerificationError as e:
        # Invalid signature
        raise HTTPException(status_code=HTTPStatus.UNPROCESSABLE_ENTITY) from e

    print(event)

    return

Un problème possible, que j’ai rencontré, est que j’ai demandé à FastAPI de convertir request.body en un dictionnaire au niveau des paramètres de fonction (donc body: dict). Puis, je l’ai sérialisé en une chaîne de caractères pour l’étape de validation… et cela a échoué parce qu’il n’était plus identique à ce qui était entré.

bookmark_borderServerless: Comment partager les ids d’un API Gateway existant

La documentation de serverless indique comment partager les ids d’un API Gateway, mais la technique qui y est décrite ne semble fonctionner que pour un nouveau déploiement. (Dans mon cas, suivre cette technique a crée un 2è API Gateway vide dont les ids étaient partagés, ce qui, évidemment, ne répondait pas à mes besoins.)

Voici comment faire pour un déploiement existant.

Continue reading « Serverless: Comment partager les ids d’un API Gateway existant »

bookmark_borderAnnotations de type en python – Annoter ou convertir?

Lors d’une revue de code, un collègue, Zachary Paden, me demandait pourquoi j’appelais la fonction typing.cast sur mes variables plutôt que de créer des variables temporaires simplement pour typer. Eh bien, tout comme il ignorait l’existence de cast, j’ignorais que cette approche fonctionnait. En bon nerdz que nous sommes, il a décidé de mesurer la performance de chacune des approches.

Continue reading « Annotations de type en python – Annoter ou convertir? »