minor
This commit is contained in:
3
db/.dockerignore
Normal file
3
db/.dockerignore
Normal file
@@ -0,0 +1,3 @@
|
||||
*
|
||||
!*.sql
|
||||
!Dockerfile
|
||||
1
db/00_extensions.sql
Normal file
1
db/00_extensions.sql
Normal file
@@ -0,0 +1 @@
|
||||
CREATE EXTENSION IF NOT EXISTS pg_trgm;
|
||||
9
db/05_common.sql
Normal file
9
db/05_common.sql
Normal file
@@ -0,0 +1,9 @@
|
||||
CREATE OR REPLACE FUNCTION set_updated_at()
|
||||
RETURNS TRIGGER
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = NOW();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
7
db/10_categories.sql
Normal file
7
db/10_categories.sql
Normal file
@@ -0,0 +1,7 @@
|
||||
CREATE TABLE IF NOT EXISTS categories (
|
||||
id UUID PRIMARY KEY,
|
||||
parent_id UUID NULL REFERENCES categories(id) ON DELETE SET NULL,
|
||||
name TEXT NOT NULL,
|
||||
slug TEXT NOT NULL UNIQUE,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE
|
||||
);
|
||||
5
db/20_brands.sql
Normal file
5
db/20_brands.sql
Normal file
@@ -0,0 +1,5 @@
|
||||
CREATE TABLE IF NOT EXISTS brands (
|
||||
id UUID PRIMARY KEY,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
slug TEXT NOT NULL UNIQUE
|
||||
);
|
||||
26
db/30_products.sql
Normal file
26
db/30_products.sql
Normal file
@@ -0,0 +1,26 @@
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'product_unit') THEN
|
||||
CREATE TYPE product_unit AS ENUM ('pcs', 'kg', 'm', 'm2', 'm3', 'bag', 'pack');
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS products (
|
||||
id UUID PRIMARY KEY,
|
||||
sku TEXT NOT NULL UNIQUE,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
category_id UUID NOT NULL REFERENCES categories(id),
|
||||
brand_id UUID NULL REFERENCES brands(id),
|
||||
unit product_unit NOT NULL,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_products_updated_at ON products;
|
||||
CREATE TRIGGER trg_products_updated_at
|
||||
BEFORE UPDATE ON products
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION set_updated_at();
|
||||
6
db/40_product_images.sql
Normal file
6
db/40_product_images.sql
Normal file
@@ -0,0 +1,6 @@
|
||||
CREATE TABLE IF NOT EXISTS product_images (
|
||||
id UUID PRIMARY KEY,
|
||||
product_id UUID NOT NULL REFERENCES products(id) ON DELETE CASCADE,
|
||||
url TEXT NOT NULL,
|
||||
sort_order INTEGER NOT NULL DEFAULT 1
|
||||
);
|
||||
7
db/50_product_attributes.sql
Normal file
7
db/50_product_attributes.sql
Normal file
@@ -0,0 +1,7 @@
|
||||
CREATE TABLE IF NOT EXISTS product_attributes (
|
||||
id UUID PRIMARY KEY,
|
||||
product_id UUID NOT NULL REFERENCES products(id) ON DELETE CASCADE,
|
||||
key TEXT NOT NULL,
|
||||
value TEXT NOT NULL,
|
||||
unit TEXT NULL
|
||||
);
|
||||
5
db/60_stocks.sql
Normal file
5
db/60_stocks.sql
Normal file
@@ -0,0 +1,5 @@
|
||||
CREATE TABLE IF NOT EXISTS stocks (
|
||||
product_id UUID PRIMARY KEY REFERENCES products(id) ON DELETE CASCADE,
|
||||
qty NUMERIC(12, 3) NOT NULL DEFAULT 0 CHECK (qty >= 0),
|
||||
reserved_qty NUMERIC(12, 3) NOT NULL DEFAULT 0 CHECK (reserved_qty >= 0)
|
||||
);
|
||||
6
db/70_prices.sql
Normal file
6
db/70_prices.sql
Normal file
@@ -0,0 +1,6 @@
|
||||
CREATE TABLE IF NOT EXISTS prices (
|
||||
product_id UUID PRIMARY KEY REFERENCES products(id) ON DELETE CASCADE,
|
||||
currency CHAR(3) NOT NULL DEFAULT 'RUB',
|
||||
price NUMERIC(12, 2) NOT NULL CHECK (price >= 0),
|
||||
old_price NUMERIC(12, 2) NULL CHECK (old_price IS NULL OR old_price >= 0)
|
||||
);
|
||||
7
db/80_indexes.sql
Normal file
7
db/80_indexes.sql
Normal file
@@ -0,0 +1,7 @@
|
||||
CREATE INDEX IF NOT EXISTS idx_products_name_trgm ON products USING GIN (name gin_trgm_ops);
|
||||
CREATE INDEX IF NOT EXISTS idx_products_category_id ON products (category_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_products_brand_id ON products (brand_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_products_is_active ON products (is_active);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_product_attributes_product_key ON product_attributes (product_id, key);
|
||||
CREATE INDEX IF NOT EXISTS idx_product_attributes_key_value ON product_attributes (key, value);
|
||||
48
db/90_seed.sql
Normal file
48
db/90_seed.sql
Normal file
@@ -0,0 +1,48 @@
|
||||
INSERT INTO categories (id, parent_id, name, slug, is_active)
|
||||
VALUES
|
||||
('11111111-1111-1111-1111-111111111111', NULL, 'Сухие смеси', 'dry-mixes', TRUE),
|
||||
('22222222-2222-2222-2222-222222222222', '11111111-1111-1111-1111-111111111111', 'Цемент', 'cement', TRUE),
|
||||
('33333333-3333-3333-3333-333333333333', '11111111-1111-1111-1111-111111111111', 'Шпаклевки', 'putty', TRUE)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
INSERT INTO brands (id, name, slug)
|
||||
VALUES
|
||||
('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'Holcim', 'holcim'),
|
||||
('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', 'Knauf', 'knauf')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
INSERT INTO products (
|
||||
id, sku, name, description, category_id, brand_id, unit, is_active
|
||||
)
|
||||
VALUES (
|
||||
'99999999-9999-9999-9999-999999999999',
|
||||
'CEM-500-50',
|
||||
'Цемент М500, 50 кг',
|
||||
'Для кладочных и бетонных работ',
|
||||
'22222222-2222-2222-2222-222222222222',
|
||||
'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
|
||||
'bag',
|
||||
TRUE
|
||||
)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
INSERT INTO prices (product_id, currency, price, old_price)
|
||||
VALUES ('99999999-9999-9999-9999-999999999999', 'RUB', 489.90, 529.90)
|
||||
ON CONFLICT (product_id) DO NOTHING;
|
||||
|
||||
INSERT INTO stocks (product_id, qty, reserved_qty)
|
||||
VALUES ('99999999-9999-9999-9999-999999999999', 120.000, 5.000)
|
||||
ON CONFLICT (product_id) DO NOTHING;
|
||||
|
||||
INSERT INTO product_attributes (id, product_id, key, value, unit)
|
||||
VALUES
|
||||
('aaaa1111-1111-1111-1111-111111111111', '99999999-9999-9999-9999-999999999999', 'weight', '50', 'kg'),
|
||||
('aaaa2222-2222-2222-2222-222222222222', '99999999-9999-9999-9999-999999999999', 'strength', 'M500', NULL),
|
||||
('aaaa3333-3333-3333-3333-333333333333', '99999999-9999-9999-9999-999999999999', 'color', 'gray', NULL)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
INSERT INTO product_images (id, product_id, url, sort_order)
|
||||
VALUES
|
||||
('bbbb1111-1111-1111-1111-111111111111', '99999999-9999-9999-9999-999999999999', 'https://images.unsplash.com/photo-1519751138087-5bf79df62d5b', 1),
|
||||
('bbbb2222-2222-2222-2222-222222222222', '99999999-9999-9999-9999-999999999999', 'https://images.unsplash.com/photo-1581093458791-9d15482442f2', 2)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
19
db/Dockerfile
Normal file
19
db/Dockerfile
Normal file
@@ -0,0 +1,19 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
FROM postgres:16
|
||||
|
||||
COPY 00_*.sql /docker-entrypoint-initdb.d/
|
||||
COPY 05_*.sql /docker-entrypoint-initdb.d/
|
||||
COPY 10_*.sql /docker-entrypoint-initdb.d/
|
||||
COPY 20_*.sql /docker-entrypoint-initdb.d/
|
||||
COPY 30_*.sql /docker-entrypoint-initdb.d/
|
||||
COPY 40_*.sql /docker-entrypoint-initdb.d/
|
||||
COPY 50_*.sql /docker-entrypoint-initdb.d/
|
||||
COPY 60_*.sql /docker-entrypoint-initdb.d/
|
||||
COPY 70_*.sql /docker-entrypoint-initdb.d/
|
||||
COPY 80_*.sql /docker-entrypoint-initdb.d/
|
||||
COPY 90_*.sql /docker-entrypoint-initdb.d/
|
||||
|
||||
RUN chmod -R a+r /docker-entrypoint-initdb.d
|
||||
|
||||
HEALTHCHECK --interval=5s --timeout=5s --retries=10 \
|
||||
CMD pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB" || exit 1
|
||||
28
db/README.md
Normal file
28
db/README.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# Building Catalog DB
|
||||
|
||||
Схема разбита на отдельные файлы по таблицам и служебным объектам.
|
||||
|
||||
## Запуск сборки
|
||||
|
||||
```bash
|
||||
chmod +x ./build.sh
|
||||
./build.sh
|
||||
```
|
||||
|
||||
Переменные окружения:
|
||||
|
||||
- `DATABASE_URL` (приоритетный вариант)
|
||||
- или `PGHOST`, `PGPORT`, `PGUSER`, `PGPASSWORD`, `PGDATABASE`
|
||||
|
||||
Скрипт `build.sh` автоматически создаёт целевую базу, если её ещё нет (по умолчанию `building_catalog`).
|
||||
Для Docker-образа в `Dockerfile` задан `POSTGRES_DB=building_catalog` как значение по умолчанию.
|
||||
|
||||
## Описание таблиц
|
||||
|
||||
- `tables/categories.md`
|
||||
- `tables/brands.md`
|
||||
- `tables/products.md`
|
||||
- `tables/product_images.md`
|
||||
- `tables/product_attributes.md`
|
||||
- `tables/stocks.md`
|
||||
- `tables/prices.md`
|
||||
61
db/build.sh
Executable file
61
db/build.sh
Executable file
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
DEFAULT_DB_NAME="building_catalog"
|
||||
|
||||
if [[ -n "${DATABASE_URL:-}" ]]; then
|
||||
URL_WITHOUT_QUERY="${DATABASE_URL%%\?*}"
|
||||
URL_QUERY=""
|
||||
if [[ "$DATABASE_URL" == *\?* ]]; then
|
||||
URL_QUERY="?${DATABASE_URL#*\?}"
|
||||
fi
|
||||
|
||||
URL_PATH_PART="${URL_WITHOUT_QUERY#*://}"
|
||||
URL_DB_NAME=""
|
||||
URL_PREFIX="$URL_WITHOUT_QUERY"
|
||||
if [[ "$URL_PATH_PART" == */* ]]; then
|
||||
URL_DB_NAME="${URL_PATH_PART#*/}"
|
||||
URL_DB_NAME="${URL_DB_NAME%%/*}"
|
||||
URL_PREFIX="${URL_WITHOUT_QUERY%/*}"
|
||||
fi
|
||||
TARGET_DB="${PGDATABASE:-${URL_DB_NAME:-$DEFAULT_DB_NAME}}"
|
||||
|
||||
PSQL_MAINT_CMD=(psql "${URL_PREFIX}/postgres${URL_QUERY}" -v ON_ERROR_STOP=1)
|
||||
PSQL_CMD=(psql "${URL_PREFIX}/${TARGET_DB}${URL_QUERY}" -v ON_ERROR_STOP=1)
|
||||
else
|
||||
TARGET_DB="${PGDATABASE:-$DEFAULT_DB_NAME}"
|
||||
PSQL_MAINT_CMD=(
|
||||
psql
|
||||
-h "${PGHOST:-127.0.0.1}"
|
||||
-p "${PGPORT:-5432}"
|
||||
-U "${PGUSER:-postgres}"
|
||||
-d postgres
|
||||
-v ON_ERROR_STOP=1
|
||||
)
|
||||
PSQL_CMD=(
|
||||
psql
|
||||
-h "${PGHOST:-127.0.0.1}"
|
||||
-p "${PGPORT:-5432}"
|
||||
-U "${PGUSER:-postgres}"
|
||||
-d "$TARGET_DB"
|
||||
-v ON_ERROR_STOP=1
|
||||
)
|
||||
fi
|
||||
|
||||
if [[ ! "$TARGET_DB" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]]; then
|
||||
printf 'Unsupported database name: %s\n' "$TARGET_DB" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
DB_EXISTS="$("${PSQL_MAINT_CMD[@]}" -Atc "SELECT 1 FROM pg_database WHERE datname = '$TARGET_DB'")"
|
||||
if [[ "$DB_EXISTS" != "1" ]]; then
|
||||
printf 'Creating database: %s\n' "$TARGET_DB"
|
||||
"${PSQL_MAINT_CMD[@]}" -c "CREATE DATABASE \"$TARGET_DB\""
|
||||
fi
|
||||
|
||||
for sql_file in "$ROOT_DIR"/*.sql; do
|
||||
if [[ -f "$sql_file" ]]; then
|
||||
"${PSQL_CMD[@]}" -f "$sql_file"
|
||||
fi
|
||||
done
|
||||
5
db/tables/brands.md
Normal file
5
db/tables/brands.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# brands
|
||||
|
||||
- Справочник брендов.
|
||||
- `name` и `slug` уникальны.
|
||||
- Используется связью `products.brand_id`.
|
||||
6
db/tables/categories.md
Normal file
6
db/tables/categories.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# categories
|
||||
|
||||
- Хранит дерево категорий каталога.
|
||||
- `parent_id` ссылается на родительскую категорию.
|
||||
- `slug` уникальный, используется в URL и фильтрах.
|
||||
- `is_active` позволяет отключать категорию без удаления.
|
||||
5
db/tables/prices.md
Normal file
5
db/tables/prices.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# prices
|
||||
|
||||
- Текущая цена товара.
|
||||
- `currency` — код ISO 4217 (например, RUB).
|
||||
- `old_price` опционально хранит цену до скидки.
|
||||
5
db/tables/product_attributes.md
Normal file
5
db/tables/product_attributes.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# product_attributes
|
||||
|
||||
- EAV-таблица характеристик товара.
|
||||
- Хранит пары `key/value` и опциональную единицу `unit`.
|
||||
- Поддерживает фильтрацию каталога по атрибутам.
|
||||
5
db/tables/product_images.md
Normal file
5
db/tables/product_images.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# product_images
|
||||
|
||||
- Список изображений товара.
|
||||
- `sort_order` определяет порядок показа.
|
||||
- Внешний ключ на `products` с каскадным удалением.
|
||||
7
db/tables/products.md
Normal file
7
db/tables/products.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# products
|
||||
|
||||
- Основная таблица товаров.
|
||||
- Хранит SKU, описание, ссылки на категорию и бренд.
|
||||
- `unit` задан enum `product_unit`.
|
||||
- `is_active` используется для мягкого удаления.
|
||||
- `created_at` и `updated_at` ведут аудит изменений.
|
||||
5
db/tables/stocks.md
Normal file
5
db/tables/stocks.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# stocks
|
||||
|
||||
- Остатки по товарам.
|
||||
- `qty` — общее количество, `reserved_qty` — зарезервированное.
|
||||
- Доступный остаток считается как `qty - reserved_qty`.
|
||||
Reference in New Issue
Block a user