feat: replace UUID v4 with UUID v7 across backend

Define uuid_generate_v7() in the first migration and switch all table
primary key defaults to it. Remove the uuid-ossp extension dependency.
Update refresh token and request ID generation in Go code to use
uuid.NewV7() from the existing google/uuid v1.6.0 library.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
dbastrikin
2026-02-27 13:19:55 +02:00
parent 92d40618cd
commit ea4a6301ea
9 changed files with 43 additions and 12 deletions

View File

@@ -42,7 +42,7 @@ func (j *JWTManager) GenerateAccessToken(userID, plan string) (string, error) {
} }
func (j *JWTManager) GenerateRefreshToken() (string, time.Time) { func (j *JWTManager) GenerateRefreshToken() (string, time.Time) {
token := uuid.NewString() token := uuid.Must(uuid.NewV7()).String()
expiresAt := time.Now().Add(j.refreshDuration) expiresAt := time.Now().Add(j.refreshDuration)
return token, expiresAt return token, expiresAt
} }

View File

@@ -15,7 +15,7 @@ func RequestID(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id := r.Header.Get("X-Request-ID") id := r.Header.Get("X-Request-ID")
if id == "" { if id == "" {
id = uuid.NewString() id = uuid.Must(uuid.NewV7()).String()
} }
ctx := context.WithValue(r.Context(), requestIDKey, id) ctx := context.WithValue(r.Context(), requestIDKey, id)
w.Header().Set("X-Request-ID", id) w.Header().Set("X-Request-ID", id)

View File

@@ -1,5 +1,35 @@
-- +goose Up -- +goose Up
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- Generate UUID v7 (time-ordered, millisecond precision).
-- Structure: 48-bit unix_ts_ms | 4-bit version (0111) | 12-bit rand_a | 2-bit variant (10) | 62-bit rand_b
CREATE OR REPLACE FUNCTION uuid_generate_v7()
RETURNS uuid
AS $$
DECLARE
unix_ts_ms bytea;
uuid_bytes bytea;
BEGIN
-- 48-bit Unix timestamp in milliseconds.
-- int8send produces 8 big-endian bytes; skip the first 2 zero bytes to get 6.
unix_ts_ms = substring(int8send(floor(extract(epoch from clock_timestamp()) * 1000)::bigint) from 3);
-- Use a random v4 UUID as the source of random bits for rand_a and rand_b.
uuid_bytes = uuid_send(gen_random_uuid());
-- Overwrite bytes 05 with the timestamp (positions 16 in 1-indexed bytea).
uuid_bytes = overlay(uuid_bytes placing unix_ts_ms from 1 for 6);
-- Set version nibble (bits 4851) to 0111 (7).
uuid_bytes = set_bit(uuid_bytes, 48, 0);
uuid_bytes = set_bit(uuid_bytes, 49, 1);
uuid_bytes = set_bit(uuid_bytes, 50, 1);
uuid_bytes = set_bit(uuid_bytes, 51, 1);
-- Variant bits (6465) stay at 10 as inherited from gen_random_uuid().
RETURN encode(uuid_bytes, 'hex')::uuid;
END
$$ LANGUAGE plpgsql VOLATILE;
CREATE TYPE user_plan AS ENUM ('free', 'paid'); CREATE TYPE user_plan AS ENUM ('free', 'paid');
CREATE TYPE user_gender AS ENUM ('male', 'female'); CREATE TYPE user_gender AS ENUM ('male', 'female');
@@ -7,7 +37,7 @@ CREATE TYPE user_goal AS ENUM ('lose', 'maintain', 'gain');
CREATE TYPE activity_level AS ENUM ('low', 'moderate', 'high'); CREATE TYPE activity_level AS ENUM ('low', 'moderate', 'high');
CREATE TABLE users ( CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), id UUID PRIMARY KEY DEFAULT uuid_generate_v7(),
firebase_uid VARCHAR(128) NOT NULL UNIQUE, firebase_uid VARCHAR(128) NOT NULL UNIQUE,
email VARCHAR(255) NOT NULL, email VARCHAR(255) NOT NULL,
name VARCHAR(255) NOT NULL DEFAULT '', name VARCHAR(255) NOT NULL DEFAULT '',
@@ -47,3 +77,4 @@ DROP TYPE IF EXISTS activity_level;
DROP TYPE IF EXISTS user_goal; DROP TYPE IF EXISTS user_goal;
DROP TYPE IF EXISTS user_gender; DROP TYPE IF EXISTS user_gender;
DROP TYPE IF EXISTS user_plan; DROP TYPE IF EXISTS user_plan;
DROP FUNCTION IF EXISTS uuid_generate_v7();

View File

@@ -1,6 +1,6 @@
-- +goose Up -- +goose Up
CREATE TABLE ingredient_mappings ( CREATE TABLE ingredient_mappings (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), id UUID PRIMARY KEY DEFAULT uuid_generate_v7(),
canonical_name VARCHAR(255) NOT NULL, canonical_name VARCHAR(255) NOT NULL,
canonical_name_ru VARCHAR(255), canonical_name_ru VARCHAR(255),
spoonacular_id INTEGER UNIQUE, spoonacular_id INTEGER UNIQUE,

View File

@@ -3,7 +3,7 @@ CREATE TYPE recipe_source AS ENUM ('spoonacular', 'ai', 'user');
CREATE TYPE recipe_difficulty AS ENUM ('easy', 'medium', 'hard'); CREATE TYPE recipe_difficulty AS ENUM ('easy', 'medium', 'hard');
CREATE TABLE recipes ( CREATE TABLE recipes (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), id UUID PRIMARY KEY DEFAULT uuid_generate_v7(),
source recipe_source NOT NULL DEFAULT 'spoonacular', source recipe_source NOT NULL DEFAULT 'spoonacular',
spoonacular_id INTEGER UNIQUE, spoonacular_id INTEGER UNIQUE,

View File

@@ -1,6 +1,6 @@
-- +goose Up -- +goose Up
CREATE TABLE saved_recipes ( CREATE TABLE saved_recipes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT uuid_generate_v7(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
title TEXT NOT NULL, title TEXT NOT NULL,
description TEXT, description TEXT,

View File

@@ -1,6 +1,6 @@
-- +goose Up -- +goose Up
CREATE TABLE products ( CREATE TABLE products (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT uuid_generate_v7(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
mapping_id UUID REFERENCES ingredient_mappings(id), mapping_id UUID REFERENCES ingredient_mappings(id),
name TEXT NOT NULL, name TEXT NOT NULL,

View File

@@ -1,7 +1,7 @@
-- +goose Up -- +goose Up
CREATE TABLE menu_plans ( CREATE TABLE menu_plans (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT uuid_generate_v7(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
week_start DATE NOT NULL, week_start DATE NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(), created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
@@ -9,7 +9,7 @@ CREATE TABLE menu_plans (
); );
CREATE TABLE menu_items ( CREATE TABLE menu_items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT uuid_generate_v7(),
menu_plan_id UUID NOT NULL REFERENCES menu_plans(id) ON DELETE CASCADE, menu_plan_id UUID NOT NULL REFERENCES menu_plans(id) ON DELETE CASCADE,
day_of_week INT NOT NULL CHECK (day_of_week BETWEEN 1 AND 7), day_of_week INT NOT NULL CHECK (day_of_week BETWEEN 1 AND 7),
meal_type TEXT NOT NULL CHECK (meal_type IN ('breakfast','lunch','dinner')), meal_type TEXT NOT NULL CHECK (meal_type IN ('breakfast','lunch','dinner')),
@@ -21,7 +21,7 @@ CREATE TABLE menu_items (
-- Stores the generated shopping list for a menu plan. -- Stores the generated shopping list for a menu plan.
-- items is a JSONB array of {name, category, amount, unit, checked, in_stock}. -- items is a JSONB array of {name, category, amount, unit, checked, in_stock}.
CREATE TABLE shopping_lists ( CREATE TABLE shopping_lists (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT uuid_generate_v7(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
menu_plan_id UUID REFERENCES menu_plans(id) ON DELETE CASCADE, menu_plan_id UUID REFERENCES menu_plans(id) ON DELETE CASCADE,
items JSONB NOT NULL DEFAULT '[]', items JSONB NOT NULL DEFAULT '[]',

View File

@@ -1,7 +1,7 @@
-- +goose Up -- +goose Up
CREATE TABLE meal_diary ( CREATE TABLE meal_diary (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT uuid_generate_v7(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
date DATE NOT NULL, date DATE NOT NULL,
meal_type TEXT NOT NULL, meal_type TEXT NOT NULL,