shapecraft 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +227 -0
- package/README.md +22 -0
- package/apps/cli/node_modules/.bin/prettier +21 -0
- package/apps/cli/node_modules/.bin/tsc +21 -0
- package/apps/cli/node_modules/.bin/tsserver +21 -0
- package/apps/cli/node_modules/.bin/tsx +21 -0
- package/apps/cli/node_modules/.bin/vitest +21 -0
- package/apps/cli/package.json +47 -0
- package/apps/cli/src/index.ts +98 -0
- package/apps/cli/tsconfig.cjs.json +10 -0
- package/apps/cli/tsconfig.esm.json +10 -0
- package/apps/cli/tsconfig.json +22 -0
- package/package.json +16 -0
- package/packages/core/node_modules/.bin/prettier +21 -0
- package/packages/core/node_modules/.bin/tsc +21 -0
- package/packages/core/node_modules/.bin/tsserver +21 -0
- package/packages/core/node_modules/.bin/tsx +21 -0
- package/packages/core/node_modules/.bin/vitest +21 -0
- package/packages/core/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/packages/core/package.json +44 -0
- package/packages/core/src/common/array.test.ts +19 -0
- package/packages/core/src/common/array.ts +15 -0
- package/packages/core/src/common/index.ts +5 -0
- package/packages/core/src/common/is.ts +23 -0
- package/packages/core/src/common/object.ts +35 -0
- package/packages/core/src/common/phantom.ts +1 -0
- package/packages/core/src/common/result.ts +43 -0
- package/packages/core/src/common/string.ts +28 -0
- package/packages/core/src/common/types.ts +34 -0
- package/packages/core/src/index.ts +1 -0
- package/packages/core/src/shape/annotate.ts +139 -0
- package/packages/core/src/shape/annotation.ts +47 -0
- package/packages/core/src/shape/base.ts +71 -0
- package/packages/core/src/shape/builder.test.ts +728 -0
- package/packages/core/src/shape/builder.ts +475 -0
- package/packages/core/src/shape/error.ts +4 -0
- package/packages/core/src/shape/index.ts +3 -0
- package/packages/core/src/shape/number.ts +118 -0
- package/packages/core/src/shape/shape.test.ts +792 -0
- package/packages/core/src/shape/shape.ts +377 -0
- package/packages/core/src/shape/tags.ts +14 -0
- package/packages/core/src/shape/transforms/index.ts +3 -0
- package/packages/core/src/shape/transforms/json-schema/index.ts +2 -0
- package/packages/core/src/shape/transforms/json-schema/transform.test.ts +850 -0
- package/packages/core/src/shape/transforms/json-schema/transform.ts +882 -0
- package/packages/core/src/shape/transforms/json-schema/types.ts +132 -0
- package/packages/core/src/shape/transforms/sql/dialects/dialect.ts +89 -0
- package/packages/core/src/shape/transforms/sql/dialects/index.ts +14 -0
- package/packages/core/src/shape/transforms/sql/dialects/postgres.ts +392 -0
- package/packages/core/src/shape/transforms/sql/dialects/sqlite.ts +333 -0
- package/packages/core/src/shape/transforms/sql/from-sql.test.ts +704 -0
- package/packages/core/src/shape/transforms/sql/from-sql.ts +210 -0
- package/packages/core/src/shape/transforms/sql/index.ts +3 -0
- package/packages/core/src/shape/transforms/sql/options.ts +6 -0
- package/packages/core/src/shape/transforms/sql/parser/check-decoder.ts +457 -0
- package/packages/core/src/shape/transforms/sql/parser/create-domain.ts +105 -0
- package/packages/core/src/shape/transforms/sql/parser/create-table.ts +809 -0
- package/packages/core/src/shape/transforms/sql/parser/create-type.ts +91 -0
- package/packages/core/src/shape/transforms/sql/parser/cursor.ts +179 -0
- package/packages/core/src/shape/transforms/sql/parser/default-decoder.ts +129 -0
- package/packages/core/src/shape/transforms/sql/parser/lexer.ts +289 -0
- package/packages/core/src/shape/transforms/sql/parser/pg-types.ts +247 -0
- package/packages/core/src/shape/transforms/sql/parser/sqlite-types.ts +103 -0
- package/packages/core/src/shape/transforms/sql/parser/statements.ts +127 -0
- package/packages/core/src/shape/transforms/sql/parser/type-spec.ts +159 -0
- package/packages/core/src/shape/transforms/sql/transform.sqlite.test.ts +448 -0
- package/packages/core/src/shape/transforms/sql/transform.test.ts +880 -0
- package/packages/core/src/shape/transforms/sql/transform.ts +295 -0
- package/packages/core/src/shape/transforms/typescript/index.ts +1 -0
- package/packages/core/src/shape/transforms/typescript/transform.ts +211 -0
- package/packages/core/src/shape/tuple.test.ts +171 -0
- package/packages/core/src/shape/validate.ts +413 -0
- package/packages/core/tsconfig.cjs.json +11 -0
- package/packages/core/tsconfig.esm.json +10 -0
- package/packages/core/tsconfig.json +23 -0
- package/packages/samples/node_modules/.bin/prettier +21 -0
- package/packages/samples/node_modules/.bin/tsc +21 -0
- package/packages/samples/node_modules/.bin/tsserver +21 -0
- package/packages/samples/node_modules/.bin/tsx +21 -0
- package/packages/samples/node_modules/.bin/vitest +21 -0
- package/packages/samples/package.json +47 -0
- package/packages/samples/src/blog.ts +49 -0
- package/packages/samples/src/config.ts +50 -0
- package/packages/samples/src/ecommerce.ts +65 -0
- package/packages/samples/src/embeddings.ts +43 -0
- package/packages/samples/src/events.ts +52 -0
- package/packages/samples/src/geometry.ts +62 -0
- package/packages/samples/src/index.ts +9 -0
- package/packages/samples/src/relational.ts +17 -0
- package/packages/samples/src/tuples.ts +67 -0
- package/packages/samples/src/user.ts +9 -0
- package/packages/samples/tsconfig.cjs.json +11 -0
- package/packages/samples/tsconfig.esm.json +10 -0
- package/packages/samples/tsconfig.json +23 -0
- package/pnpm-workspace.yaml +3 -0
- package/test-data/json-schema/address.json +35 -0
- package/test-data/json-schema/array-of-things.json +36 -0
- package/test-data/json-schema/basic.json +21 -0
- package/test-data/json-schema/blog-post.json +29 -0
- package/test-data/json-schema/calendar.json +48 -0
- package/test-data/json-schema/complex-object-with-nested-properties.json +41 -0
- package/test-data/json-schema/ecommerce-complex.json +344 -0
- package/test-data/json-schema/ecommerce-system.json +27 -0
- package/test-data/json-schema/enumerated-values.json +11 -0
- package/test-data/json-schema/fstab-entry.json +92 -0
- package/test-data/json-schema/geographical-location.json +20 -0
- package/test-data/json-schema/health-record.json +41 -0
- package/test-data/json-schema/job-posting.json +33 -0
- package/test-data/json-schema/movie.json +35 -0
- package/test-data/json-schema/regular-expression-pattern.json +12 -0
- package/test-data/json-schema/user-profile.json +33 -0
- package/test-data/sql/ecommerce.sql +641 -0
|
@@ -0,0 +1,641 @@
|
|
|
1
|
+
-- ============================================================================
|
|
2
|
+
-- Mock E-commerce Database Schema (PostgreSQL)
|
|
3
|
+
-- ============================================================================
|
|
4
|
+
-- Exercises: extensions, custom types, enums, domains, schemas, inheritance,
|
|
5
|
+
-- partitioning, constraints, indexes (B-tree, GIN, GiST, HNSW), triggers,
|
|
6
|
+
-- functions, views, materialized views, foreign keys, JSONB, arrays, full-text
|
|
7
|
+
-- search, pgvector for semantic similarity, row-level security, and more.
|
|
8
|
+
-- ============================================================================
|
|
9
|
+
|
|
10
|
+
-- ----------------------------------------------------------------------------
|
|
11
|
+
-- Extensions
|
|
12
|
+
-- ----------------------------------------------------------------------------
|
|
13
|
+
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
|
14
|
+
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
|
|
15
|
+
CREATE EXTENSION IF NOT EXISTS "citext";
|
|
16
|
+
CREATE EXTENSION IF NOT EXISTS "pg_trgm";
|
|
17
|
+
CREATE EXTENSION IF NOT EXISTS "btree_gin";
|
|
18
|
+
CREATE EXTENSION IF NOT EXISTS "vector"; -- pgvector
|
|
19
|
+
CREATE EXTENSION IF NOT EXISTS "postgis"; -- for shipping zone geometry (optional)
|
|
20
|
+
|
|
21
|
+
-- ----------------------------------------------------------------------------
|
|
22
|
+
-- Schemas (logical separation)
|
|
23
|
+
-- ----------------------------------------------------------------------------
|
|
24
|
+
CREATE SCHEMA IF NOT EXISTS shop;
|
|
25
|
+
CREATE SCHEMA IF NOT EXISTS audit;
|
|
26
|
+
CREATE SCHEMA IF NOT EXISTS analytics;
|
|
27
|
+
|
|
28
|
+
SET search_path TO shop, public;
|
|
29
|
+
|
|
30
|
+
-- ----------------------------------------------------------------------------
|
|
31
|
+
-- Custom Types, Domains, and Enums
|
|
32
|
+
-- ----------------------------------------------------------------------------
|
|
33
|
+
CREATE TYPE shop.order_status AS ENUM (
|
|
34
|
+
'pending', 'paid', 'processing', 'shipped',
|
|
35
|
+
'delivered', 'cancelled', 'refunded', 'returned'
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
CREATE TYPE shop.payment_method AS ENUM (
|
|
39
|
+
'credit_card', 'debit_card', 'paypal', 'stripe', 'crypto', 'bank_transfer'
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
CREATE TYPE shop.user_role AS ENUM ('customer', 'vendor', 'admin', 'support');
|
|
43
|
+
|
|
44
|
+
CREATE TYPE shop.address_kind AS ENUM ('billing', 'shipping', 'both');
|
|
45
|
+
|
|
46
|
+
CREATE TYPE shop.money AS (
|
|
47
|
+
amount NUMERIC(14, 4),
|
|
48
|
+
currency CHAR(3)
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
CREATE DOMAIN shop.email AS CITEXT
|
|
52
|
+
CHECK (VALUE ~ '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$');
|
|
53
|
+
|
|
54
|
+
CREATE DOMAIN shop.positive_numeric AS NUMERIC(14, 4)
|
|
55
|
+
CHECK (VALUE >= 0);
|
|
56
|
+
|
|
57
|
+
CREATE DOMAIN shop.sku AS VARCHAR(64)
|
|
58
|
+
CHECK (VALUE ~ '^[A-Z0-9][A-Z0-9-]{2,63}$');
|
|
59
|
+
|
|
60
|
+
-- ----------------------------------------------------------------------------
|
|
61
|
+
-- Reusable trigger function: updated_at
|
|
62
|
+
-- ----------------------------------------------------------------------------
|
|
63
|
+
CREATE OR REPLACE FUNCTION shop.tg_set_updated_at()
|
|
64
|
+
RETURNS TRIGGER
|
|
65
|
+
LANGUAGE plpgsql AS $$
|
|
66
|
+
BEGIN
|
|
67
|
+
NEW.updated_at := CURRENT_TIMESTAMP;
|
|
68
|
+
RETURN NEW;
|
|
69
|
+
END;
|
|
70
|
+
$$;
|
|
71
|
+
|
|
72
|
+
-- ============================================================================
|
|
73
|
+
-- Core tables
|
|
74
|
+
-- ============================================================================
|
|
75
|
+
|
|
76
|
+
-- ---- users -----------------------------------------------------------------
|
|
77
|
+
CREATE TABLE shop.users (
|
|
78
|
+
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
79
|
+
email shop.email NOT NULL UNIQUE,
|
|
80
|
+
password_hash TEXT NOT NULL,
|
|
81
|
+
full_name VARCHAR(200) NOT NULL,
|
|
82
|
+
role shop.user_role NOT NULL DEFAULT 'customer',
|
|
83
|
+
phone VARCHAR(32),
|
|
84
|
+
date_of_birth DATE,
|
|
85
|
+
preferences JSONB NOT NULL DEFAULT '{}'::JSONB,
|
|
86
|
+
tags TEXT[] NOT NULL DEFAULT '{}',
|
|
87
|
+
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
|
88
|
+
last_login_at TIMESTAMPTZ,
|
|
89
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
90
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
91
|
+
CONSTRAINT users_dob_reasonable CHECK (date_of_birth IS NULL OR date_of_birth > '1900-01-01'),
|
|
92
|
+
CONSTRAINT users_phone_format CHECK (phone IS NULL OR phone ~ '^\+?[0-9\s\-()]{7,32}$')
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
CREATE INDEX users_role_active_idx ON shop.users (role) WHERE is_active;
|
|
96
|
+
CREATE INDEX users_tags_gin_idx ON shop.users USING GIN (tags);
|
|
97
|
+
CREATE INDEX users_preferences_gin_idx ON shop.users USING GIN (preferences jsonb_path_ops);
|
|
98
|
+
CREATE INDEX users_email_trgm_idx ON shop.users USING GIN (email gin_trgm_ops);
|
|
99
|
+
|
|
100
|
+
CREATE TRIGGER users_set_updated_at
|
|
101
|
+
BEFORE UPDATE ON shop.users
|
|
102
|
+
FOR EACH ROW EXECUTE FUNCTION shop.tg_set_updated_at();
|
|
103
|
+
|
|
104
|
+
-- ---- addresses (self-contained, linked to users) ---------------------------
|
|
105
|
+
CREATE TABLE shop.addresses (
|
|
106
|
+
id BIGSERIAL PRIMARY KEY,
|
|
107
|
+
user_id UUID NOT NULL REFERENCES shop.users(id) ON DELETE CASCADE,
|
|
108
|
+
kind shop.address_kind NOT NULL DEFAULT 'both',
|
|
109
|
+
line1 VARCHAR(200) NOT NULL,
|
|
110
|
+
line2 VARCHAR(200),
|
|
111
|
+
city VARCHAR(120) NOT NULL,
|
|
112
|
+
region VARCHAR(120),
|
|
113
|
+
postal_code VARCHAR(20) NOT NULL,
|
|
114
|
+
country_code CHAR(2) NOT NULL,
|
|
115
|
+
is_default BOOLEAN NOT NULL DEFAULT FALSE,
|
|
116
|
+
geo GEOGRAPHY(POINT, 4326),
|
|
117
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
-- only one default address of a given kind per user
|
|
121
|
+
CREATE UNIQUE INDEX addresses_one_default_per_kind
|
|
122
|
+
ON shop.addresses (user_id, kind)
|
|
123
|
+
WHERE is_default;
|
|
124
|
+
|
|
125
|
+
CREATE INDEX addresses_geo_gix ON shop.addresses USING GIST (geo);
|
|
126
|
+
|
|
127
|
+
-- ---- vendors (specialization of user) --------------------------------------
|
|
128
|
+
CREATE TABLE shop.vendors (
|
|
129
|
+
user_id UUID PRIMARY KEY REFERENCES shop.users(id) ON DELETE CASCADE,
|
|
130
|
+
business_name VARCHAR(200) NOT NULL,
|
|
131
|
+
tax_id VARCHAR(64),
|
|
132
|
+
rating NUMERIC(3, 2) NOT NULL DEFAULT 0
|
|
133
|
+
CHECK (rating BETWEEN 0 AND 5),
|
|
134
|
+
approved_at TIMESTAMPTZ,
|
|
135
|
+
metadata JSONB NOT NULL DEFAULT '{}'::JSONB
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
-- ---- categories (recursive / nested set via parent_id) ---------------------
|
|
139
|
+
CREATE TABLE shop.categories (
|
|
140
|
+
id SERIAL PRIMARY KEY,
|
|
141
|
+
parent_id INTEGER REFERENCES shop.categories(id) ON DELETE CASCADE,
|
|
142
|
+
name VARCHAR(120) NOT NULL,
|
|
143
|
+
slug VARCHAR(140) NOT NULL UNIQUE,
|
|
144
|
+
path LTREE, -- materialized hierarchy if ltree is enabled; otherwise leave NULL
|
|
145
|
+
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
146
|
+
CONSTRAINT categories_no_self_parent CHECK (parent_id IS NULL OR parent_id <> id)
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
CREATE INDEX categories_parent_idx ON shop.categories (parent_id);
|
|
150
|
+
|
|
151
|
+
-- ---- products ---------------------------------------------------------------
|
|
152
|
+
CREATE TABLE shop.products (
|
|
153
|
+
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
154
|
+
vendor_id UUID NOT NULL REFERENCES shop.vendors(user_id) ON DELETE RESTRICT,
|
|
155
|
+
category_id INTEGER REFERENCES shop.categories(id) ON DELETE SET NULL,
|
|
156
|
+
sku shop.sku NOT NULL UNIQUE,
|
|
157
|
+
name VARCHAR(300) NOT NULL,
|
|
158
|
+
description TEXT,
|
|
159
|
+
price shop.positive_numeric NOT NULL,
|
|
160
|
+
currency CHAR(3) NOT NULL DEFAULT 'USD',
|
|
161
|
+
weight_grams INTEGER CHECK (weight_grams IS NULL OR weight_grams > 0),
|
|
162
|
+
dimensions JSONB, -- e.g. {"length_cm": 10, "width_cm": 5, "height_cm": 2}
|
|
163
|
+
attributes JSONB NOT NULL DEFAULT '{}'::JSONB,
|
|
164
|
+
tags TEXT[] NOT NULL DEFAULT '{}',
|
|
165
|
+
is_published BOOLEAN NOT NULL DEFAULT FALSE,
|
|
166
|
+
-- pgvector: 1536-dim OpenAI-style embedding for semantic search
|
|
167
|
+
description_embedding VECTOR(1536),
|
|
168
|
+
-- generated column for full-text search
|
|
169
|
+
search_doc TSVECTOR GENERATED ALWAYS AS (
|
|
170
|
+
setweight(to_tsvector('english', coalesce(name, '')), 'A') ||
|
|
171
|
+
setweight(to_tsvector('english', coalesce(description, '')), 'B') ||
|
|
172
|
+
setweight(to_tsvector('english', array_to_string(tags, ' ')), 'C')
|
|
173
|
+
) STORED,
|
|
174
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
175
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
CREATE INDEX products_vendor_idx ON shop.products (vendor_id);
|
|
179
|
+
CREATE INDEX products_category_idx ON shop.products (category_id);
|
|
180
|
+
CREATE INDEX products_price_idx ON shop.products (price) WHERE is_published;
|
|
181
|
+
CREATE INDEX products_tags_gin_idx ON shop.products USING GIN (tags);
|
|
182
|
+
CREATE INDEX products_attrs_gin_idx ON shop.products USING GIN (attributes jsonb_path_ops);
|
|
183
|
+
CREATE INDEX products_search_idx ON shop.products USING GIN (search_doc);
|
|
184
|
+
CREATE INDEX products_name_trgm_idx ON shop.products USING GIN (name gin_trgm_ops);
|
|
185
|
+
|
|
186
|
+
-- pgvector HNSW index for fast approximate nearest neighbour search.
|
|
187
|
+
-- Using cosine distance; alternatives are vector_l2_ops and vector_ip_ops.
|
|
188
|
+
CREATE INDEX products_embedding_hnsw_idx
|
|
189
|
+
ON shop.products
|
|
190
|
+
USING hnsw (description_embedding vector_cosine_ops)
|
|
191
|
+
WITH (m = 16, ef_construction = 64);
|
|
192
|
+
|
|
193
|
+
CREATE TRIGGER products_set_updated_at
|
|
194
|
+
BEFORE UPDATE ON shop.products
|
|
195
|
+
FOR EACH ROW EXECUTE FUNCTION shop.tg_set_updated_at();
|
|
196
|
+
|
|
197
|
+
-- ---- product variants -------------------------------------------------------
|
|
198
|
+
CREATE TABLE shop.product_variants (
|
|
199
|
+
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
200
|
+
product_id UUID NOT NULL REFERENCES shop.products(id) ON DELETE CASCADE,
|
|
201
|
+
sku shop.sku NOT NULL UNIQUE,
|
|
202
|
+
option_values JSONB NOT NULL, -- e.g. {"size":"L","color":"red"}
|
|
203
|
+
price_override shop.positive_numeric,
|
|
204
|
+
stock_qty INTEGER NOT NULL DEFAULT 0 CHECK (stock_qty >= 0),
|
|
205
|
+
UNIQUE (product_id, option_values)
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
-- ---- inventory ledger (append-only) -----------------------------------------
|
|
209
|
+
CREATE TABLE shop.inventory_movements (
|
|
210
|
+
id BIGSERIAL PRIMARY KEY,
|
|
211
|
+
variant_id UUID NOT NULL REFERENCES shop.product_variants(id) ON DELETE CASCADE,
|
|
212
|
+
delta INTEGER NOT NULL,
|
|
213
|
+
reason VARCHAR(64) NOT NULL,
|
|
214
|
+
reference_id UUID,
|
|
215
|
+
occurred_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
216
|
+
CONSTRAINT inventory_delta_nonzero CHECK (delta <> 0)
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
CREATE INDEX inv_movements_variant_time_idx
|
|
220
|
+
ON shop.inventory_movements (variant_id, occurred_at DESC);
|
|
221
|
+
|
|
222
|
+
-- ---- carts and cart items ---------------------------------------------------
|
|
223
|
+
CREATE TABLE shop.carts (
|
|
224
|
+
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
225
|
+
user_id UUID REFERENCES shop.users(id) ON DELETE SET NULL,
|
|
226
|
+
session_token TEXT,
|
|
227
|
+
currency CHAR(3) NOT NULL DEFAULT 'USD',
|
|
228
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
229
|
+
expires_at TIMESTAMPTZ,
|
|
230
|
+
CONSTRAINT cart_owner_required CHECK (
|
|
231
|
+
user_id IS NOT NULL OR session_token IS NOT NULL
|
|
232
|
+
)
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
CREATE TABLE shop.cart_items (
|
|
236
|
+
cart_id UUID NOT NULL REFERENCES shop.carts(id) ON DELETE CASCADE,
|
|
237
|
+
variant_id UUID NOT NULL REFERENCES shop.product_variants(id) ON DELETE RESTRICT,
|
|
238
|
+
quantity INTEGER NOT NULL CHECK (quantity > 0),
|
|
239
|
+
unit_price shop.positive_numeric NOT NULL,
|
|
240
|
+
added_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
241
|
+
PRIMARY KEY (cart_id, variant_id)
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
-- ---- orders (range-partitioned by created_at) -------------------------------
|
|
245
|
+
CREATE TABLE shop.orders (
|
|
246
|
+
id UUID NOT NULL DEFAULT uuid_generate_v4(),
|
|
247
|
+
user_id UUID NOT NULL REFERENCES shop.users(id) ON DELETE RESTRICT,
|
|
248
|
+
status shop.order_status NOT NULL DEFAULT 'pending',
|
|
249
|
+
subtotal shop.positive_numeric NOT NULL,
|
|
250
|
+
tax shop.positive_numeric NOT NULL DEFAULT 0,
|
|
251
|
+
shipping shop.positive_numeric NOT NULL DEFAULT 0,
|
|
252
|
+
total shop.positive_numeric NOT NULL,
|
|
253
|
+
currency CHAR(3) NOT NULL DEFAULT 'USD',
|
|
254
|
+
billing_address_id BIGINT REFERENCES shop.addresses(id) ON DELETE SET NULL,
|
|
255
|
+
shipping_address_id BIGINT REFERENCES shop.addresses(id) ON DELETE SET NULL,
|
|
256
|
+
metadata JSONB NOT NULL DEFAULT '{}'::JSONB,
|
|
257
|
+
placed_at TIMESTAMPTZ,
|
|
258
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
259
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
260
|
+
PRIMARY KEY (id, created_at),
|
|
261
|
+
CONSTRAINT orders_total_matches CHECK (total = subtotal + tax + shipping)
|
|
262
|
+
) PARTITION BY RANGE (created_at);
|
|
263
|
+
|
|
264
|
+
-- Example partitions (extend as needed)
|
|
265
|
+
CREATE TABLE shop.orders_2024 PARTITION OF shop.orders
|
|
266
|
+
FOR VALUES FROM ('2024-01-01') TO ('2025-01-01');
|
|
267
|
+
CREATE TABLE shop.orders_2025 PARTITION OF shop.orders
|
|
268
|
+
FOR VALUES FROM ('2025-01-01') TO ('2026-01-01');
|
|
269
|
+
CREATE TABLE shop.orders_2026 PARTITION OF shop.orders
|
|
270
|
+
FOR VALUES FROM ('2026-01-01') TO ('2027-01-01');
|
|
271
|
+
|
|
272
|
+
CREATE INDEX orders_user_idx ON shop.orders (user_id, created_at DESC);
|
|
273
|
+
CREATE INDEX orders_status_idx ON shop.orders (status) WHERE status <> 'delivered';
|
|
274
|
+
CREATE INDEX orders_metadata_idx ON shop.orders USING GIN (metadata jsonb_path_ops);
|
|
275
|
+
|
|
276
|
+
CREATE TRIGGER orders_set_updated_at
|
|
277
|
+
BEFORE UPDATE ON shop.orders
|
|
278
|
+
FOR EACH ROW EXECUTE FUNCTION shop.tg_set_updated_at();
|
|
279
|
+
|
|
280
|
+
-- ---- order items ------------------------------------------------------------
|
|
281
|
+
CREATE TABLE shop.order_items (
|
|
282
|
+
id BIGSERIAL PRIMARY KEY,
|
|
283
|
+
order_id UUID NOT NULL,
|
|
284
|
+
order_created_at TIMESTAMPTZ NOT NULL,
|
|
285
|
+
variant_id UUID NOT NULL REFERENCES shop.product_variants(id) ON DELETE RESTRICT,
|
|
286
|
+
quantity INTEGER NOT NULL CHECK (quantity > 0),
|
|
287
|
+
unit_price shop.positive_numeric NOT NULL,
|
|
288
|
+
line_total shop.positive_numeric GENERATED ALWAYS AS (quantity * unit_price) STORED,
|
|
289
|
+
FOREIGN KEY (order_id, order_created_at)
|
|
290
|
+
REFERENCES shop.orders(id, created_at) ON DELETE CASCADE
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
CREATE INDEX order_items_order_idx ON shop.order_items (order_id);
|
|
294
|
+
|
|
295
|
+
-- ---- payments ---------------------------------------------------------------
|
|
296
|
+
CREATE TABLE shop.payments (
|
|
297
|
+
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
298
|
+
order_id UUID NOT NULL,
|
|
299
|
+
order_created_at TIMESTAMPTZ NOT NULL,
|
|
300
|
+
method shop.payment_method NOT NULL,
|
|
301
|
+
amount shop.positive_numeric NOT NULL,
|
|
302
|
+
currency CHAR(3) NOT NULL DEFAULT 'USD',
|
|
303
|
+
provider_ref TEXT,
|
|
304
|
+
succeeded BOOLEAN NOT NULL DEFAULT FALSE,
|
|
305
|
+
raw_response JSONB,
|
|
306
|
+
paid_at TIMESTAMPTZ,
|
|
307
|
+
FOREIGN KEY (order_id, order_created_at)
|
|
308
|
+
REFERENCES shop.orders(id, created_at) ON DELETE CASCADE
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
CREATE INDEX payments_order_idx ON shop.payments (order_id);
|
|
312
|
+
|
|
313
|
+
-- ---- reviews ----------------------------------------------------------------
|
|
314
|
+
CREATE TABLE shop.reviews (
|
|
315
|
+
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
316
|
+
product_id UUID NOT NULL REFERENCES shop.products(id) ON DELETE CASCADE,
|
|
317
|
+
user_id UUID NOT NULL REFERENCES shop.users(id) ON DELETE CASCADE,
|
|
318
|
+
rating SMALLINT NOT NULL CHECK (rating BETWEEN 1 AND 5),
|
|
319
|
+
title VARCHAR(200),
|
|
320
|
+
body TEXT,
|
|
321
|
+
body_embedding VECTOR(768), -- smaller embedding for review text
|
|
322
|
+
helpful_count INTEGER NOT NULL DEFAULT 0,
|
|
323
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
324
|
+
UNIQUE (product_id, user_id)
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
CREATE INDEX reviews_product_idx ON shop.reviews (product_id, created_at DESC);
|
|
328
|
+
CREATE INDEX reviews_embedding_ivfflat_idx
|
|
329
|
+
ON shop.reviews
|
|
330
|
+
USING ivfflat (body_embedding vector_l2_ops)
|
|
331
|
+
WITH (lists = 100);
|
|
332
|
+
|
|
333
|
+
-- ---- wishlists (many-to-many) -----------------------------------------------
|
|
334
|
+
CREATE TABLE shop.wishlists (
|
|
335
|
+
user_id UUID NOT NULL REFERENCES shop.users(id) ON DELETE CASCADE,
|
|
336
|
+
product_id UUID NOT NULL REFERENCES shop.products(id) ON DELETE CASCADE,
|
|
337
|
+
added_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
338
|
+
note TEXT,
|
|
339
|
+
PRIMARY KEY (user_id, product_id)
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
-- ---- coupons / discounts ----------------------------------------------------
|
|
343
|
+
CREATE TABLE shop.coupons (
|
|
344
|
+
code VARCHAR(40) PRIMARY KEY,
|
|
345
|
+
description TEXT,
|
|
346
|
+
percent_off NUMERIC(5, 2) CHECK (percent_off IS NULL OR percent_off BETWEEN 0 AND 100),
|
|
347
|
+
amount_off shop.positive_numeric,
|
|
348
|
+
valid_from TIMESTAMPTZ NOT NULL,
|
|
349
|
+
valid_until TIMESTAMPTZ NOT NULL,
|
|
350
|
+
max_redemptions INTEGER,
|
|
351
|
+
redeemed_count INTEGER NOT NULL DEFAULT 0,
|
|
352
|
+
constraints JSONB NOT NULL DEFAULT '{}'::JSONB,
|
|
353
|
+
CONSTRAINT coupon_one_discount CHECK (
|
|
354
|
+
(percent_off IS NOT NULL)::INT + (amount_off IS NOT NULL)::INT = 1
|
|
355
|
+
),
|
|
356
|
+
CONSTRAINT coupon_validity CHECK (valid_until > valid_from)
|
|
357
|
+
);
|
|
358
|
+
|
|
359
|
+
-- exclusion constraint: no overlapping global "everyone" coupons with same code prefix
|
|
360
|
+
-- (illustrative use of EXCLUDE)
|
|
361
|
+
CREATE TABLE shop.promotions (
|
|
362
|
+
id SERIAL PRIMARY KEY,
|
|
363
|
+
name VARCHAR(120) NOT NULL,
|
|
364
|
+
category_id INTEGER REFERENCES shop.categories(id) ON DELETE CASCADE,
|
|
365
|
+
during TSTZRANGE NOT NULL,
|
|
366
|
+
discount_pct NUMERIC(5, 2) NOT NULL CHECK (discount_pct BETWEEN 0 AND 100),
|
|
367
|
+
EXCLUDE USING gist (
|
|
368
|
+
category_id WITH =,
|
|
369
|
+
during WITH &&
|
|
370
|
+
)
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
-- ============================================================================
|
|
374
|
+
-- Audit / history
|
|
375
|
+
-- ============================================================================
|
|
376
|
+
CREATE TABLE audit.event_log (
|
|
377
|
+
id BIGSERIAL PRIMARY KEY,
|
|
378
|
+
table_name TEXT NOT NULL,
|
|
379
|
+
row_pk TEXT NOT NULL,
|
|
380
|
+
operation CHAR(1) NOT NULL CHECK (operation IN ('I','U','D')),
|
|
381
|
+
actor UUID,
|
|
382
|
+
diff JSONB,
|
|
383
|
+
occurred_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
CREATE OR REPLACE FUNCTION audit.tg_record_change()
|
|
387
|
+
RETURNS TRIGGER LANGUAGE plpgsql AS $$
|
|
388
|
+
DECLARE
|
|
389
|
+
v_pk TEXT;
|
|
390
|
+
v_op CHAR(1);
|
|
391
|
+
v_diff JSONB;
|
|
392
|
+
BEGIN
|
|
393
|
+
IF (TG_OP = 'INSERT') THEN
|
|
394
|
+
v_op := 'I';
|
|
395
|
+
v_pk := COALESCE(NEW.id::TEXT, '');
|
|
396
|
+
v_diff := to_jsonb(NEW);
|
|
397
|
+
ELSIF (TG_OP = 'UPDATE') THEN
|
|
398
|
+
v_op := 'U';
|
|
399
|
+
v_pk := COALESCE(NEW.id::TEXT, '');
|
|
400
|
+
v_diff := jsonb_build_object('old', to_jsonb(OLD), 'new', to_jsonb(NEW));
|
|
401
|
+
ELSE
|
|
402
|
+
v_op := 'D';
|
|
403
|
+
v_pk := COALESCE(OLD.id::TEXT, '');
|
|
404
|
+
v_diff := to_jsonb(OLD);
|
|
405
|
+
END IF;
|
|
406
|
+
|
|
407
|
+
INSERT INTO audit.event_log (table_name, row_pk, operation, diff)
|
|
408
|
+
VALUES (TG_TABLE_SCHEMA || '.' || TG_TABLE_NAME, v_pk, v_op, v_diff);
|
|
409
|
+
|
|
410
|
+
RETURN COALESCE(NEW, OLD);
|
|
411
|
+
END;
|
|
412
|
+
$$;
|
|
413
|
+
|
|
414
|
+
CREATE TRIGGER orders_audit
|
|
415
|
+
AFTER INSERT OR UPDATE OR DELETE ON shop.orders
|
|
416
|
+
FOR EACH ROW EXECUTE FUNCTION audit.tg_record_change();
|
|
417
|
+
|
|
418
|
+
CREATE TRIGGER products_audit
|
|
419
|
+
AFTER INSERT OR UPDATE OR DELETE ON shop.products
|
|
420
|
+
FOR EACH ROW EXECUTE FUNCTION audit.tg_record_change();
|
|
421
|
+
|
|
422
|
+
-- ============================================================================
|
|
423
|
+
-- Views and materialized views
|
|
424
|
+
-- ============================================================================
|
|
425
|
+
CREATE VIEW shop.v_product_listing AS
|
|
426
|
+
SELECT
|
|
427
|
+
p.id,
|
|
428
|
+
p.sku,
|
|
429
|
+
p.name,
|
|
430
|
+
p.price,
|
|
431
|
+
p.currency,
|
|
432
|
+
c.name AS category,
|
|
433
|
+
v.business_name AS vendor,
|
|
434
|
+
COALESCE(AVG(r.rating), 0)::NUMERIC(3,2) AS avg_rating,
|
|
435
|
+
COUNT(r.id) AS review_count
|
|
436
|
+
FROM shop.products p
|
|
437
|
+
LEFT JOIN shop.categories c ON c.id = p.category_id
|
|
438
|
+
LEFT JOIN shop.vendors v ON v.user_id = p.vendor_id
|
|
439
|
+
LEFT JOIN shop.reviews r ON r.product_id = p.id
|
|
440
|
+
WHERE p.is_published
|
|
441
|
+
GROUP BY p.id, c.name, v.business_name;
|
|
442
|
+
|
|
443
|
+
CREATE MATERIALIZED VIEW analytics.mv_daily_sales AS
|
|
444
|
+
SELECT
|
|
445
|
+
DATE_TRUNC('day', o.created_at)::DATE AS sale_day,
|
|
446
|
+
COUNT(*) AS order_count,
|
|
447
|
+
SUM(o.total) AS gross_revenue,
|
|
448
|
+
SUM(o.tax) AS total_tax,
|
|
449
|
+
SUM(o.shipping) AS total_shipping
|
|
450
|
+
FROM shop.orders o
|
|
451
|
+
WHERE o.status NOT IN ('cancelled', 'refunded')
|
|
452
|
+
GROUP BY 1
|
|
453
|
+
WITH NO DATA;
|
|
454
|
+
|
|
455
|
+
CREATE UNIQUE INDEX mv_daily_sales_day_uidx
|
|
456
|
+
ON analytics.mv_daily_sales (sale_day);
|
|
457
|
+
|
|
458
|
+
-- ============================================================================
|
|
459
|
+
-- Functions: business logic and vector search helpers
|
|
460
|
+
-- ============================================================================
|
|
461
|
+
|
|
462
|
+
-- Compute live stock from the ledger
|
|
463
|
+
CREATE OR REPLACE FUNCTION shop.fn_variant_stock(p_variant_id UUID)
|
|
464
|
+
RETURNS INTEGER LANGUAGE sql STABLE AS $$
|
|
465
|
+
SELECT COALESCE(SUM(delta), 0)::INTEGER
|
|
466
|
+
FROM shop.inventory_movements
|
|
467
|
+
WHERE variant_id = p_variant_id;
|
|
468
|
+
$$;
|
|
469
|
+
|
|
470
|
+
-- Semantic product search using pgvector
|
|
471
|
+
CREATE OR REPLACE FUNCTION shop.fn_search_products_by_embedding(
|
|
472
|
+
p_query_embedding VECTOR(1536),
|
|
473
|
+
p_limit INTEGER DEFAULT 10,
|
|
474
|
+
p_min_price NUMERIC DEFAULT NULL,
|
|
475
|
+
p_max_price NUMERIC DEFAULT NULL
|
|
476
|
+
)
|
|
477
|
+
RETURNS TABLE (
|
|
478
|
+
product_id UUID,
|
|
479
|
+
name VARCHAR,
|
|
480
|
+
price NUMERIC,
|
|
481
|
+
similarity REAL
|
|
482
|
+
)
|
|
483
|
+
LANGUAGE sql STABLE AS $$
|
|
484
|
+
SELECT
|
|
485
|
+
p.id,
|
|
486
|
+
p.name,
|
|
487
|
+
p.price,
|
|
488
|
+
1 - (p.description_embedding <=> p_query_embedding) AS similarity
|
|
489
|
+
FROM shop.products p
|
|
490
|
+
WHERE p.is_published
|
|
491
|
+
AND p.description_embedding IS NOT NULL
|
|
492
|
+
AND (p_min_price IS NULL OR p.price >= p_min_price)
|
|
493
|
+
AND (p_max_price IS NULL OR p.price <= p_max_price)
|
|
494
|
+
ORDER BY p.description_embedding <=> p_query_embedding
|
|
495
|
+
LIMIT p_limit;
|
|
496
|
+
$$;
|
|
497
|
+
|
|
498
|
+
-- Hybrid search: combine full-text rank and vector similarity
|
|
499
|
+
CREATE OR REPLACE FUNCTION shop.fn_hybrid_search(
|
|
500
|
+
p_query_text TEXT,
|
|
501
|
+
p_query_embedding VECTOR(1536),
|
|
502
|
+
p_limit INTEGER DEFAULT 20,
|
|
503
|
+
p_alpha REAL DEFAULT 0.5 -- weight on vector similarity
|
|
504
|
+
)
|
|
505
|
+
RETURNS TABLE (
|
|
506
|
+
product_id UUID,
|
|
507
|
+
name VARCHAR,
|
|
508
|
+
score REAL
|
|
509
|
+
)
|
|
510
|
+
LANGUAGE sql STABLE AS $$
|
|
511
|
+
WITH ranked AS (
|
|
512
|
+
SELECT
|
|
513
|
+
p.id,
|
|
514
|
+
p.name,
|
|
515
|
+
ts_rank(p.search_doc, plainto_tsquery('english', p_query_text)) AS text_rank,
|
|
516
|
+
1 - (p.description_embedding <=> p_query_embedding) AS vec_sim
|
|
517
|
+
FROM shop.products p
|
|
518
|
+
WHERE p.is_published
|
|
519
|
+
AND p.description_embedding IS NOT NULL
|
|
520
|
+
)
|
|
521
|
+
SELECT
|
|
522
|
+
id,
|
|
523
|
+
name,
|
|
524
|
+
((1 - p_alpha) * text_rank + p_alpha * vec_sim)::REAL AS score
|
|
525
|
+
FROM ranked
|
|
526
|
+
ORDER BY score DESC
|
|
527
|
+
LIMIT p_limit;
|
|
528
|
+
$$;
|
|
529
|
+
|
|
530
|
+
-- Recommend similar products by a seed product's embedding
|
|
531
|
+
CREATE OR REPLACE FUNCTION shop.fn_similar_products(
|
|
532
|
+
p_product_id UUID,
|
|
533
|
+
p_limit INTEGER DEFAULT 5
|
|
534
|
+
)
|
|
535
|
+
RETURNS TABLE (product_id UUID, name VARCHAR, distance REAL)
|
|
536
|
+
LANGUAGE sql STABLE AS $$
|
|
537
|
+
WITH seed AS (
|
|
538
|
+
SELECT description_embedding FROM shop.products WHERE id = p_product_id
|
|
539
|
+
)
|
|
540
|
+
SELECT p.id, p.name, (p.description_embedding <=> seed.description_embedding)::REAL
|
|
541
|
+
FROM shop.products p, seed
|
|
542
|
+
WHERE p.id <> p_product_id
|
|
543
|
+
AND p.is_published
|
|
544
|
+
AND p.description_embedding IS NOT NULL
|
|
545
|
+
ORDER BY p.description_embedding <=> seed.description_embedding
|
|
546
|
+
LIMIT p_limit;
|
|
547
|
+
$$;
|
|
548
|
+
|
|
549
|
+
-- ============================================================================
|
|
550
|
+
-- Row Level Security: customers can only see their own orders
|
|
551
|
+
-- ============================================================================
|
|
552
|
+
ALTER TABLE shop.orders ENABLE ROW LEVEL SECURITY;
|
|
553
|
+
|
|
554
|
+
CREATE POLICY orders_owner_select ON shop.orders
|
|
555
|
+
FOR SELECT
|
|
556
|
+
USING (user_id::TEXT = current_setting('app.current_user_id', TRUE));
|
|
557
|
+
|
|
558
|
+
CREATE POLICY orders_admin_all ON shop.orders
|
|
559
|
+
FOR ALL
|
|
560
|
+
USING (current_setting('app.current_user_role', TRUE) = 'admin');
|
|
561
|
+
|
|
562
|
+
-- ============================================================================
|
|
563
|
+
-- Sample data
|
|
564
|
+
-- ============================================================================
|
|
565
|
+
INSERT INTO shop.users (id, email, password_hash, full_name, role) VALUES
|
|
566
|
+
('11111111-1111-1111-1111-111111111111', '[email protected]', crypt('admin', gen_salt('bf')), 'Site Admin', 'admin'),
|
|
567
|
+
('22222222-2222-2222-2222-222222222222', '[email protected]', crypt('hunter2', gen_salt('bf')), 'Alice Andersson', 'customer'),
|
|
568
|
+
('33333333-3333-3333-3333-333333333333', '[email protected]', crypt('secret', gen_salt('bf')), 'Bob Bergman', 'vendor');
|
|
569
|
+
|
|
570
|
+
INSERT INTO shop.vendors (user_id, business_name, approved_at)
|
|
571
|
+
VALUES ('33333333-3333-3333-3333-333333333333', 'Bergman Gadgets AB', CURRENT_TIMESTAMP);
|
|
572
|
+
|
|
573
|
+
INSERT INTO shop.categories (id, parent_id, name, slug) VALUES
|
|
574
|
+
(1, NULL, 'Electronics', 'electronics'),
|
|
575
|
+
(2, 1, 'Audio', 'audio'),
|
|
576
|
+
(3, 1, 'Wearables', 'wearables'),
|
|
577
|
+
(4, NULL, 'Home & Kitchen', 'home-kitchen');
|
|
578
|
+
|
|
579
|
+
SELECT setval(pg_get_serial_sequence('shop.categories', 'id'), 4);
|
|
580
|
+
|
|
581
|
+
INSERT INTO shop.products (id, vendor_id, category_id, sku, name, description, price, tags, is_published)
|
|
582
|
+
VALUES
|
|
583
|
+
('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
|
|
584
|
+
'33333333-3333-3333-3333-333333333333', 2, 'AUD-HP-001',
|
|
585
|
+
'Noise-Cancelling Headphones',
|
|
586
|
+
'Over-ear wireless headphones with adaptive noise cancellation and 40-hour battery life.',
|
|
587
|
+
249.00, ARRAY['audio','bluetooth','wireless'], TRUE),
|
|
588
|
+
('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb',
|
|
589
|
+
'33333333-3333-3333-3333-333333333333', 3, 'WEA-WATCH-7',
|
|
590
|
+
'Smart Fitness Watch',
|
|
591
|
+
'Heart rate, GPS, and sleep tracking with a 7-day battery in a lightweight titanium body.',
|
|
592
|
+
399.00, ARRAY['fitness','wearable','gps'], TRUE);
|
|
593
|
+
|
|
594
|
+
INSERT INTO shop.product_variants (id, product_id, sku, option_values, stock_qty) VALUES
|
|
595
|
+
('cccccccc-cccc-cccc-cccc-cccccccccccc',
|
|
596
|
+
'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'AUD-HP-001-BLK',
|
|
597
|
+
'{"color":"black"}'::JSONB, 25),
|
|
598
|
+
('dddddddd-dddd-dddd-dddd-dddddddddddd',
|
|
599
|
+
'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'AUD-HP-001-WHT',
|
|
600
|
+
'{"color":"white"}'::JSONB, 10),
|
|
601
|
+
('eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee',
|
|
602
|
+
'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', 'WEA-WATCH-7-42',
|
|
603
|
+
'{"size":"42mm"}'::JSONB, 15);
|
|
604
|
+
|
|
605
|
+
INSERT INTO shop.inventory_movements (variant_id, delta, reason)
|
|
606
|
+
VALUES
|
|
607
|
+
('cccccccc-cccc-cccc-cccc-cccccccccccc', 25, 'initial_stock'),
|
|
608
|
+
('dddddddd-dddd-dddd-dddd-dddddddddddd', 10, 'initial_stock'),
|
|
609
|
+
('eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee', 15, 'initial_stock');
|
|
610
|
+
|
|
611
|
+
-- Example order using the partition for 2026
|
|
612
|
+
INSERT INTO shop.orders (id, user_id, status, subtotal, tax, shipping, total, currency, created_at, placed_at)
|
|
613
|
+
VALUES
|
|
614
|
+
('99999999-9999-9999-9999-999999999999',
|
|
615
|
+
'22222222-2222-2222-2222-222222222222',
|
|
616
|
+
'paid', 249.00, 24.90, 9.00, 282.90, 'USD',
|
|
617
|
+
'2026-03-15 10:00:00+00', '2026-03-15 10:01:00+00');
|
|
618
|
+
|
|
619
|
+
INSERT INTO shop.order_items (order_id, order_created_at, variant_id, quantity, unit_price)
|
|
620
|
+
VALUES ('99999999-9999-9999-9999-999999999999', '2026-03-15 10:00:00+00',
|
|
621
|
+
'cccccccc-cccc-cccc-cccc-cccccccccccc', 1, 249.00);
|
|
622
|
+
|
|
623
|
+
-- ============================================================================
|
|
624
|
+
-- Example queries (kept as comments)
|
|
625
|
+
-- ============================================================================
|
|
626
|
+
-- 1. Nearest-neighbour semantic search:
|
|
627
|
+
-- SELECT * FROM shop.fn_search_products_by_embedding('[0.01, 0.02, ...]'::vector, 10);
|
|
628
|
+
--
|
|
629
|
+
-- 2. Hybrid lexical + vector:
|
|
630
|
+
-- SELECT * FROM shop.fn_hybrid_search('wireless headphones', '[0.01,...]'::vector, 20, 0.6);
|
|
631
|
+
--
|
|
632
|
+
-- 3. Recursive category tree:
|
|
633
|
+
-- WITH RECURSIVE tree AS (
|
|
634
|
+
-- SELECT id, parent_id, name, 1 AS depth FROM shop.categories WHERE parent_id IS NULL
|
|
635
|
+
-- UNION ALL
|
|
636
|
+
-- SELECT c.id, c.parent_id, c.name, t.depth + 1
|
|
637
|
+
-- FROM shop.categories c JOIN tree t ON c.parent_id = t.id
|
|
638
|
+
-- ) SELECT * FROM tree ORDER BY depth, name;
|
|
639
|
+
--
|
|
640
|
+
-- 4. Refresh sales rollup:
|
|
641
|
+
-- REFRESH MATERIALIZED VIEW CONCURRENTLY analytics.mv_daily_sales;
|