๋ฉ”์ธ ์ฝ˜ํ…์ธ ๋กœ ๊ฑด๋„ˆ๋›ฐ๊ธฐ
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

having

๊ทธ๋ฃนํ™”๋œ ๊ฒฐ๊ณผ๋ฅผ ํ•„ํ„ฐ๋งํ•ฉ๋‹ˆ๋‹ค.
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

๋ฐ์ดํ„ฐ ์ˆ˜์ •

insert

์ƒˆ ๋ ˆ์ฝ”๋“œ๋ฅผ ์‚ฝ์ž…ํ•ฉ๋‹ˆ๋‹ค.
// ๋‹จ์ผ ์‚ฝ์ž…
const [id] = await puri.table("users")
  .insert({
    email: "[email protected]",
    name: "John Doe"
  });

// ๋ฐฐ์น˜ ์‚ฝ์ž…
const ids = await puri.table("users")
  .insert([
    { email: "[email protected]", name: "John" },
    { email: "[email protected]", name: "Jane" }
  ]);

update

๋ ˆ์ฝ”๋“œ๋ฅผ ์—…๋ฐ์ดํŠธํ•ฉ๋‹ˆ๋‹ค.
const count = await puri.table("users")
  .where("users.id", 1)
  .update({
    name: "John Smith",
    updated_at: new Date()
  });

console.log(`${count} rows updated`);

delete

๋ ˆ์ฝ”๋“œ๋ฅผ ์‚ญ์ œํ•ฉ๋‹ˆ๋‹ค.
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);

๊ฒฐ๊ณผ ๊ฐ€์ ธ์˜ค๊ธฐ

first

์ฒซ ๋ฒˆ์งธ ๋ ˆ์ฝ”๋“œ๋งŒ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.
const user = await puri.table("users")
  .where("users.email", "[email protected]")
  .select({ id: "users.id", name: "users.name" })
  .first();

// user: { id: number, name: string } | undefined

pluck

ํŠน์ • ์ปฌ๋Ÿผ ๊ฐ’๋“ค๋งŒ ๋ฐฐ์—ด๋กœ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.
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[]
// ["[email protected]", "[email protected]", ...]

๋ฒกํ„ฐ ์œ ์‚ฌ๋„ ๊ฒ€์ƒ‰

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
๋ฉ”์„œ๋“œ:
  • cosine: ์ฝ”์‚ฌ์ธ ์œ ์‚ฌ๋„ (0~1, ๋†’์„์ˆ˜๋ก ์œ ์‚ฌ)
  • l2: ์œ ํด๋ฆฌ๋“œ ๊ฑฐ๋ฆฌ (๋‚ฎ์„์ˆ˜๋ก ์œ ์‚ฌ)
  • inner_product: ๋‚ด์  (๋†’์„์ˆ˜๋ก ์œ ์‚ฌ)

Upsert (INSERT or UPDATE)

onConflict

์ถฉ๋Œ ์‹œ ๋™์ž‘์„ ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค.
// DO NOTHING
await puri.table("users")
  .insert({ email: "[email protected]", name: "John" })
  .onConflict(["email"], "nothing");

// INSERT ... ON CONFLICT (email) DO NOTHING

// DO UPDATE
await puri.table("users")
  .insert({ email: "[email protected]", name: "John Smith" })
  .onConflict(["email"], {
    update: ["name"]  // name๋งŒ ์—…๋ฐ์ดํŠธ
  });

// INSERT ... ON CONFLICT (email) DO UPDATE SET name = EXCLUDED.name

// ๊ฐ์ฒด ํ˜•ํƒœ
await puri.table("users")
  .insert({ email: "[email protected]", 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: "[email protected]", name: "John" })
  .returning("*");

// ํŠน์ • ์ปฌ๋Ÿผ
const [user] = await puri.table("users")
  .insert({ email: "[email protected]", name: "John" })
  .returning(["id", "email"]);

console.log(user.id, user.email);

์œ ํ‹ธ๋ฆฌํ‹ฐ ๋ฉ”์„œ๋“œ

clone

์ฟผ๋ฆฌ๋ฅผ ๋ณต์ œํ•ฉ๋‹ˆ๋‹ค.
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" });

debug

์ƒ์„ฑ๋œ 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

Raw SQL์„ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค.
const result = await puri.raw(`
  SELECT * FROM users WHERE status = ?
`, ["active"]);

์‹ค์ „ ์˜ˆ์‹œ

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");
}

์„ฑ๋Šฅ ์ตœ์ ํ™”

๋ฐฐ์น˜ INSERT

// โœ… ์ข‹์Œ: ๋ฐฐ์น˜๋กœ ํ•œ ๋ฒˆ์—
await puri.table("users")
  .insert([
    { email: "[email protected]", name: "User 1" },
    { email: "[email protected]", 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);

๋‹ค์Œ ๋‹จ๊ณ„