Puri๋ ์ง๊ณ, ์์ , ๋ฒกํฐ ๊ฒ์ ๋ฑ ๋ค์ํ ๊ณ ๊ธ ์ฟผ๋ฆฌ ๋ฉ์๋๋ฅผ ์ ๊ณตํฉ๋๋ค.
์ง๊ณ ์ฟผ๋ฆฌ
groupBy
๊ฒฐ๊ณผ๋ฅผ ๊ทธ๋ฃนํํฉ๋๋ค.
const stats = await puri.table("orders")
.select({
user_id: "orders.user_id",
total_orders: Puri.count(),
total_amount: Puri.sum("orders.amount")
})
.groupBy("orders.user_id");
// GROUP BY orders.user_id
๊ทธ๋ฃนํ๋ ๊ฒฐ๊ณผ๋ฅผ ํํฐ๋งํฉ๋๋ค.
const activeUsers = await puri.table("orders")
.select({
user_id: "orders.user_id",
order_count: Puri.count()
})
.groupBy("orders.user_id")
.having("order_count", ">", 10);
// HAVING order_count > 10
๋ฐ์ดํฐ ์์
์ ๋ ์ฝ๋๋ฅผ ์ฝ์
ํฉ๋๋ค.
// ๋จ์ผ ์ฝ์
const [id] = await puri.table("users")
.insert({
email: "john@example.com",
name: "John Doe"
});
// ๋ฐฐ์น ์ฝ์
const ids = await puri.table("users")
.insert([
{ email: "john@example.com", name: "John" },
{ email: "jane@example.com", name: "Jane" }
]);
๋ ์ฝ๋๋ฅผ ์
๋ฐ์ดํธํฉ๋๋ค.
const count = await puri.table("users")
.where("users.id", 1)
.update({
name: "John Smith",
updated_at: new Date()
});
console.log(`${count} rows updated`);
๋ ์ฝ๋๋ฅผ ์ญ์ ํฉ๋๋ค.
const count = await puri.table("users")
.where("users.status", "inactive")
.delete();
console.log(`${count} rows deleted`);
increment / decrement
์ซ์ ์ปฌ๋ผ์ ์ฆ๊ฐ/๊ฐ์์ํต๋๋ค.
// ์กฐํ์ ์ฆ๊ฐ
await puri.table("posts")
.where("posts.id", 123)
.increment("posts.views", 1);
// ์ฌ๊ณ ๊ฐ์
await puri.table("products")
.where("products.id", 456)
.decrement("products.stock", 5);
๊ฒฐ๊ณผ ๊ฐ์ ธ์ค๊ธฐ
์ฒซ ๋ฒ์งธ ๋ ์ฝ๋๋ง ๊ฐ์ ธ์ต๋๋ค.
const user = await puri.table("users")
.where("users.email", "john@example.com")
.select({ id: "users.id", name: "users.name" })
.first();
// user: { id: number, name: string } | undefined
ํน์ ์ปฌ๋ผ ๊ฐ๋ค๋ง ๋ฐฐ์ด๋ก ๊ฐ์ ธ์ต๋๋ค.
const userIds = await puri.table("users")
.where("users.status", "active")
.pluck("users.id");
// userIds: number[]
// [1, 2, 3, 4, 5]
const emails = await puri.table("users")
.pluck("users.email");
// emails: string[]
// ["john@example.com", "jane@example.com", ...]
๋ฒกํฐ ์ ์ฌ๋ ๊ฒ์
vectorSimilarity
pgvector๋ฅผ ์ฌ์ฉํ ๋ฒกํฐ ์ ์ฌ๋ ๊ฒ์์
๋๋ค.
const queryEmbedding = [0.1, 0.2, 0.3, /* ... */]; // 1536 ์ฐจ์
const results = await puri.table("documents")
.vectorSimilarity("documents.embedding", queryEmbedding, {
method: "cosine", // ๋๋ "l2", "inner_product"
threshold: 0.7 // ์ ์ฌ๋ 0.7 ์ด์๋ง
})
.select({
id: "documents.id",
title: "documents.title",
similarity: "similarity" // ์๋ ์ถ๊ฐ๋จ
})
.limit(10);
// ORDER BY documents.embedding <=> '[0.1,0.2,0.3,...]' ASC
์ต์
:
method: ์ ์ฌ๋ ์ธก์ ๋ฐฉ์
cosine: ์ฝ์ฌ์ธ ์ ์ฌ๋ (0~1, ๋์์๋ก ์ ์ฌ)
l2: ์ ํด๋ฆฌ๋ ๊ฑฐ๋ฆฌ (๋ฎ์์๋ก ์ ์ฌ)
inner_product: ๋ด์ (๋์์๋ก ์ ์ฌ)
threshold: ์ ์ฌ๋ ํํฐ๋ง ๊ธฐ์ค๊ฐ
distinctOn: ํน์ ์ปฌ๋ผ ๊ฐ๋ณ๋ก ๊ฐ์ฅ ์ ์ฌํ ๊ฒฐ๊ณผ ํ๋์ฉ๋ง ๋ฐํ
distinctOn ์ต์
distinctOn ์ต์
์ ์ฌ์ฉํ๋ฉด ํน์ ์ปฌ๋ผ ๊ฐ๋ณ๋ก ๊ฐ์ฅ ์ ์ฌํ ๊ฒฐ๊ณผ๋ฅผ ํ๋์ฉ๋ง ๊ฐ์ ธ์ฌ ์ ์์ต๋๋ค. PostgreSQL์ DISTINCT ON ์ ์ ํ์ฉํฉ๋๋ค.
// ์นดํ
๊ณ ๋ฆฌ๋ณ๋ก ๊ฐ์ฅ ์ ์ฌํ ๋ฌธ์ ํ๋์ฉ๋ง ์กฐํ
const results = await puri.table("documents")
.vectorSimilarity("documents.embedding", queryEmbedding, {
method: "cosine",
distinctOn: "documents.category_id" // ์นดํ
๊ณ ๋ฆฌ๋ณ ๊ฐ์ฅ ์ ์ฌํ ๋ฌธ์ 1๊ฐ์ฉ
})
.select({
id: "documents.id",
title: "documents.title",
category_id: "documents.category_id",
similarity: "similarity"
})
.limit(10);
distinctOn๊ณผ threshold๋ฅผ ํจ๊ป ์ฌ์ฉํ ์ ์์ต๋๋ค:
// ์์ฑ์๋ณ๋ก ์ ์ฌ๋ 0.6 ์ด์์ธ ๋ฌธ์ ์ค ๊ฐ์ฅ ์ ์ฌํ ๊ฒ๋ง ์กฐํ
const results = await puri.table("documents")
.vectorSimilarity("documents.embedding", queryEmbedding, {
method: "cosine",
distinctOn: "documents.author_id",
threshold: 0.6
})
.limit(5);
distinctOn ์ต์
์ฌ์ฉ ์ ๋ด๋ถ์ ์ผ๋ก ์๋ธ์ฟผ๋ฆฌ๋ก ๊ฐ์ธ์ง๋ฉฐ, ์ธ๋ถ์์ similarity ๊ธฐ์ค ๋ด๋ฆผ์ฐจ์์ผ๋ก ์ ๋ ฌ๋ฉ๋๋ค.
Upsert (INSERT or UPDATE)
onConflict
์ถฉ๋ ์ ๋์์ ์ง์ ํฉ๋๋ค.
// DO NOTHING
await puri.table("users")
.insert({ email: "john@example.com", name: "John" })
.onConflict(["email"], "nothing");
// INSERT ... ON CONFLICT (email) DO NOTHING
// DO UPDATE
await puri.table("users")
.insert({ email: "john@example.com", name: "John Smith" })
.onConflict(["email"], {
update: ["name"] // name๋ง ์
๋ฐ์ดํธ
});
// INSERT ... ON CONFLICT (email) DO UPDATE SET name = EXCLUDED.name
// ๊ฐ์ฒด ํํ
await puri.table("users")
.insert({ email: "john@example.com", count: 1 })
.onConflict(["email"], {
update: {
count: Puri.rawNumber("users.count + 1"), // SQL ํํ์
updated_at: new Date()
}
});
returning
์ฝ์
/์
๋ฐ์ดํธ๋ ๋ ์ฝ๋๋ฅผ ๋ฐํํฉ๋๋ค.
// ์ ์ฒด ์ปฌ๋ผ
const users = await puri.table("users")
.insert({ email: "john@example.com", name: "John" })
.returning("*");
// ํน์ ์ปฌ๋ผ
const [user] = await puri.table("users")
.insert({ email: "john@example.com", name: "John" })
.returning(["id", "email"]);
console.log(user.id, user.email);
์ ํธ๋ฆฌํฐ ๋ฉ์๋
์ฟผ๋ฆฌ๋ฅผ ๋ณต์ ํฉ๋๋ค.
const baseQuery = puri.table("users")
.where("users.status", "active");
// ๋ณต์ ํ์ฌ ๋ค๋ฅธ ์กฐ๊ฑด ์ถ๊ฐ
const admins = await baseQuery.clone()
.where("users.role", "admin")
.select({ id: "users.id", name: "users.name" });
const editors = await baseQuery.clone()
.where("users.role", "editor")
.select({ id: "users.id", name: "users.name" });
์์ฑ๋ SQL์ ์ฝ์์ ์ถ๋ ฅํฉ๋๋ค.
const users = await puri.table("users")
.where("users.status", "active")
.select({ id: "users.id", name: "users.name" })
.debug(); // SQL ์ถ๋ ฅ
// [Puri Debug] SELECT users.id, users.name FROM users WHERE users.status = 'active'
toQuery
SQL ์ฟผ๋ฆฌ ๋ฌธ์์ด์ ๋ฐํํฉ๋๋ค.
const query = puri.table("users")
.where("users.status", "active")
.select({ id: "users.id", name: "users.name" });
const sql = query.toQuery();
console.log(sql);
// "SELECT users.id, users.name FROM users WHERE users.status = 'active'"
Raw SQL์ ์คํํฉ๋๋ค.
const result = await puri.raw(`
SELECT * FROM users WHERE status = ?
`, ["active"]);
์ค์ ์์
ํต๊ณ ์ฟผ๋ฆฌ
๋ฐฐ์น ์
๋ฐ์ดํธ
Upsert ํจํด
๋ฒกํฐ ๊ฒ์
ํธ๋์ญ์
์กฐ๊ฑด๋ถ ์ฟผ๋ฆฌ
async function getUserStatistics() {
return puri.table("users")
.leftJoin("orders", "users.id", "orders.user_id")
.select({
user_id: "users.id",
user_name: "users.name",
// ์ง๊ณ
order_count: Puri.count("orders.id"),
total_spent: Puri.sum("orders.amount"),
avg_order: Puri.avg("orders.amount"),
// ์ฒซ/๋ง์ง๋ง ์ฃผ๋ฌธ
first_order: Puri.min("orders.created_at"),
last_order: Puri.max("orders.created_at")
})
.groupBy("users.id", "users.name")
.having("order_count", ">", 0)
.orderBy("total_spent", "desc");
}
async function updateInactivUsers(days: number = 30) {
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - days);
const count = await puri.table("users")
.where("users.last_login", "<", cutoffDate)
.where("users.status", "active")
.update({
status: "inactive",
updated_at: new Date()
});
console.log(`${count} users marked as inactive`);
return count;
}
async function upsertUserStats(userId: number, stats: {
login_count: number;
last_login: Date;
}) {
await puri.table("user_stats")
.insert({
user_id: userId,
login_count: stats.login_count,
last_login: stats.last_login,
created_at: new Date()
})
.onConflict(["user_id"], {
update: {
login_count: Puri.rawNumber("user_stats.login_count + 1"),
last_login: stats.last_login,
updated_at: new Date()
}
})
.returning("*");
}
async function semanticSearch(query: string, limit: number = 10) {
// ์ฟผ๋ฆฌ๋ฅผ ์๋ฒ ๋ฉ์ผ๋ก ๋ณํ (์: OpenAI API)
const queryEmbedding = await getEmbedding(query);
return puri.table("documents")
// ๋ฒกํฐ ์ ์ฌ๋ ๊ฒ์
.vectorSimilarity("documents.embedding", queryEmbedding, {
method: "cosine",
threshold: 0.5 // ์ ์ฌ๋ 0.5 ์ด์
})
.where("documents.published", true)
.select({
id: "documents.id",
title: "documents.title",
content: "documents.content",
similarity: "similarity"
})
.limit(limit);
}
async function transferPoints(
fromUserId: number,
toUserId: number,
points: number
) {
const wdb = getPuri("w");
await wdb.transaction(async (trx) => {
// ํฌ์ธํธ ์ฐจ๊ฐ
await trx.table("users")
.where("users.id", fromUserId)
.decrement("users.points", points);
// ํฌ์ธํธ ์ฆ๊ฐ
await trx.table("users")
.where("users.id", toUserId)
.increment("users.points", points);
// ํ์คํ ๋ฆฌ ๊ธฐ๋ก
await trx.table("point_history")
.insert({
from_user_id: fromUserId,
to_user_id: toUserId,
points,
created_at: new Date()
});
});
}
async function searchProducts(filters: {
category?: string;
minPrice?: number;
maxPrice?: number;
inStock?: boolean;
query?: string;
}) {
let query = puri.table("products")
.select({
id: "products.id",
name: "products.name",
price: "products.price",
stock: "products.stock"
});
// ์กฐ๊ฑด๋ถ ํํฐ๋ง
if (filters.category) {
query = query.where("products.category", filters.category);
}
if (filters.minPrice !== undefined) {
query = query.where("products.price", ">=", filters.minPrice);
}
if (filters.maxPrice !== undefined) {
query = query.where("products.price", "<=", filters.maxPrice);
}
if (filters.inStock) {
query = query.where("products.stock", ">", 0);
}
if (filters.query) {
query = query.where("products.name", "like", `%${filters.query}%`);
}
return query
.orderBy("products.created_at", "desc")
.limit(50);
}
์ฑ๋ฅ ์ต์ ํ
๋ฐฐ์น INSERT
// โ
์ข์: ๋ฐฐ์น๋ก ํ ๋ฒ์
await puri.table("users")
.insert([
{ email: "user1@example.com", name: "User 1" },
{ email: "user2@example.com", name: "User 2" },
// ... 1000๊ฐ
]);
// โ ๋์จ: ๋ฐ๋ณต๋ฌธ์ผ๋ก ํ๋์ฉ
for (const user of users) {
await puri.table("users").insert(user);
}
increment vs UPDATE
// โ
์ข์: increment (์์์ , ๋น ๋ฆ)
await puri.table("posts")
.where("posts.id", 123)
.increment("posts.views", 1);
// โ ๋์จ: SELECT โ UPDATE (๊ฒฝ์ ์กฐ๊ฑด, ๋๋ฆผ)
const post = await puri.table("posts").where("posts.id", 123).first();
await puri.table("posts")
.where("posts.id", 123)
.update({ views: post.views + 1 });
HAVING vs WHERE
// โ
์ข์: WHERE๋ก ๋จผ์ ํํฐ๋ง
await puri.table("orders")
.where("orders.status", "completed") // GROUP BY ์ ํํฐ
.select({
user_id: "orders.user_id",
total: Puri.sum("orders.amount")
})
.groupBy("orders.user_id")
.having("total", ">", 1000); // GROUP BY ํ ํํฐ
// โ ๋นํจ์จ: ๋ชจ๋ HAVING
await puri.table("orders")
.select({
user_id: "orders.user_id",
total: Puri.sum("orders.amount")
})
.groupBy("orders.user_id")
.having("total", ">", 1000)
.having(/* status ์กฐ๊ฑด */); // ๋นํจ์จ์
์ฃผ์์ฌํญ
1. increment/decrement ๊ฐ
// โ
์ฌ๋ฐ๋ฆ: ์์๋ง
.increment("column", 1)
.decrement("column", 5)
// โ ์๋ฌ: 0 ์ดํ
.increment("column", 0) // Error
.increment("column", -1) // Error
2. onConflict ์ ์ฝ์กฐ๊ฑด
// ํ
์ด๋ธ์ UNIQUE ์ ์ฝ์กฐ๊ฑด ๋๋ PRIMARY KEY ํ์
CREATE UNIQUE INDEX idx_users_email ON users(email);
// ๊ทธ๋์ผ onConflict ๋์
.onConflict(["email"], ...)
3. returning์ PostgreSQL๋ง
// โ
PostgreSQL
.insert({ ... })
.returning("*")
// โ MySQL: RETURNING ์ง์ ์ ํจ
// ๋์ insert ํ ID ๋ฐํ
const [id] = await puri.table("users").insert({ ... });
4. vectorSimilarity๋ pgvector ํ์
-- PostgreSQL ํ์ฅ ์ค์น ํ์
CREATE EXTENSION vector;
-- ๋ฒกํฐ ์ปฌ๋ผ ์์ฑ
ALTER TABLE documents
ADD COLUMN embedding vector(1536);
-- HNSW ์ธ๋ฑ์ค ์์ฑ
CREATE INDEX ON documents
USING hnsw (embedding vector_cosine_ops);
๋ค์ ๋จ๊ณ