| 1 | -- SPDX-License-Identifier: AGPL-3.0-or-later |
| 2 | -- |
| 3 | -- Generic rate-limit counter queries (S35). Two write paths: |
| 4 | -- * BumpRateLimit — atomic UPSERT that rolls the window forward |
| 5 | -- when stale, increments hits otherwise. Returns |
| 6 | -- the post-update hits + window_started_at so |
| 7 | -- the caller can compute Retry-After without a |
| 8 | -- second round trip. |
| 9 | -- * BumpSignupIPThrottle — same shape against signup_ip_throttle |
| 10 | -- keyed by inet/CIDR. |
| 11 | -- |
| 12 | -- Reads (PeekRateLimit, PeekSignupIPThrottle) are kept around for |
| 13 | -- the admin observability surface; the hot path uses Bump-and-decide. |
| 14 | |
| 15 | -- name: BumpRateLimit :one |
| 16 | -- Roll-or-increment in one statement. The CASE in the UPDATE branch |
| 17 | -- handles the window roll: when the existing window started before |
| 18 | -- (now - $3 interval), we treat it as a new window and reset hits |
| 19 | -- to 1; otherwise we increment in place. |
| 20 | INSERT INTO rate_limits (scope, key, hits, window_started_at) |
| 21 | VALUES (sqlc.arg(scope), sqlc.arg(key), 1, now()) |
| 22 | ON CONFLICT (scope, key) |
| 23 | DO UPDATE SET |
| 24 | hits = CASE |
| 25 | WHEN rate_limits.window_started_at < now() - sqlc.arg(ttl)::interval |
| 26 | THEN 1 |
| 27 | ELSE rate_limits.hits + 1 |
| 28 | END, |
| 29 | window_started_at = CASE |
| 30 | WHEN rate_limits.window_started_at < now() - sqlc.arg(ttl)::interval |
| 31 | THEN now() |
| 32 | ELSE rate_limits.window_started_at |
| 33 | END |
| 34 | RETURNING hits, window_started_at; |
| 35 | |
| 36 | -- name: PeekRateLimit :one |
| 37 | SELECT scope, key, hits, window_started_at |
| 38 | FROM rate_limits |
| 39 | WHERE scope = $1 AND key = $2; |
| 40 | |
| 41 | -- name: PruneRateLimits :execrows |
| 42 | DELETE FROM rate_limits |
| 43 | WHERE window_started_at < now() - sqlc.arg(retention)::interval; |
| 44 | |
| 45 | -- name: BumpSignupIPThrottle :one |
| 46 | -- Same UPSERT shape against the inet-keyed signup throttle. |
| 47 | INSERT INTO signup_ip_throttle (cidr, hits, window_started_at) |
| 48 | VALUES (sqlc.arg(cidr), 1, now()) |
| 49 | ON CONFLICT (cidr) |
| 50 | DO UPDATE SET |
| 51 | hits = CASE |
| 52 | WHEN signup_ip_throttle.window_started_at < now() - sqlc.arg(ttl)::interval |
| 53 | THEN 1 |
| 54 | ELSE signup_ip_throttle.hits + 1 |
| 55 | END, |
| 56 | window_started_at = CASE |
| 57 | WHEN signup_ip_throttle.window_started_at < now() - sqlc.arg(ttl)::interval |
| 58 | THEN now() |
| 59 | ELSE signup_ip_throttle.window_started_at |
| 60 | END |
| 61 | RETURNING hits, window_started_at; |
| 62 | |
| 63 | -- name: PruneSignupIPThrottle :execrows |
| 64 | DELETE FROM signup_ip_throttle |
| 65 | WHERE window_started_at < now() - sqlc.arg(retention)::interval; |
| 66 |