๋ฉ”์ธ ์ฝ˜ํ…์ธ ๋กœ ๊ฑด๋„ˆ๋›ฐ๊ธฐ
Puri๋Š” ํƒ€์ž… ์•ˆ์ „ํ•œ ์ฟผ๋ฆฌ ๋นŒ๋”์ด์ง€๋งŒ, ๋ณต์žกํ•œ SQL ํ‘œํ˜„์‹์ด ํ•„์š”ํ•  ๋•Œ๋Š” Raw SQL์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Raw ํ•จ์ˆ˜ ๊ฐœ์š”

Raw ํƒ€์ž… ํ•จ์ˆ˜

ํƒ€์ž…๋ณ„ Raw ํ•จ์ˆ˜ rawString, rawNumber

WHERE Raw

๋ณต์žกํ•œ ์กฐ๊ฑด๋ฌธ whereRaw

CASE WHEN

์กฐ๊ฑด๋ถ€ ๊ฐ’ ์„ ํƒ CASE ํ‘œํ˜„์‹

์„œ๋ธŒ์ฟผ๋ฆฌ

์ค‘์ฒฉ ์ฟผ๋ฆฌ ์ž‘์„ฑ Subquery

Raw ํƒ€์ž… ํ•จ์ˆ˜

rawString - ๋ฌธ์ž์—ด ๋ฐ˜ํ™˜

const results = await db.table("users").select({
  id: "id",
  fullName: Puri.rawString("CONCAT(first_name, ' ', last_name)"),
  upperName: Puri.upper("username"),
  lowerEmail: Puri.lower("email"),
});

// ํƒ€์ž…: { id: number; fullName: string; upperName: string; lowerEmail: string; }[]

rawNumber - ์ˆซ์ž ๋ฐ˜ํ™˜

const results = await db.table("employees").select({
  id: "id",
  salary: "salary",
  yearsSince: Puri.rawNumber("EXTRACT(YEAR FROM AGE(NOW(), hire_date))"),
  roundedSalary: Puri.rawNumber("ROUND(salary, -3)"),
});

// ํƒ€์ž…: { id: number; salary: string; yearsSince: number; roundedSalary: number; }[]

rawBoolean - ๋ถˆ๋ฆฐ ๋ฐ˜ํ™˜

const results = await db.table("users").select({
  id: "id",
  isActive: "is_active",
  isAdmin: Puri.rawBoolean("role = 'admin'"),
  hasEmail: Puri.rawBoolean("email IS NOT NULL"),
});

// ํƒ€์ž…: { id: number; isActive: boolean; isAdmin: boolean; hasEmail: boolean; }[]

rawDate - ๋‚ ์งœ ๋ฐ˜ํ™˜

const results = await db.table("users").select({
  id: "id",
  createdAt: "created_at",
  nextWeek: Puri.rawDate("created_at + INTERVAL '7 days'"),
});

// ํƒ€์ž…: { id: number; createdAt: Date; nextWeek: Date; }[]

rawStringArray - ๋ฌธ์ž์—ด ๋ฐฐ์—ด ๋ฐ˜ํ™˜

const results = await db
  .table("projects")
  .join("projects__employees", "projects.id", "projects__employees.project_id")
  .join("users", "projects__employees.employee_id", "users.id")
  .select({
    projectId: "projects.id",
    memberNames: Puri.rawStringArray("ARRAY_AGG(users.username)"),
  })
  .groupBy("projects.id");

// ํƒ€์ž…: { projectId: number; memberNames: string[]; }[]

Static SQL ํ•จ์ˆ˜

Puri๊ฐ€ ์ œ๊ณตํ•˜๋Š” ๋‚ด์žฅ SQL ํ•จ์ˆ˜๋“ค์ž…๋‹ˆ๋‹ค.

๋ฌธ์ž์—ด ํ•จ์ˆ˜

const results = await db
  .table("users")
  .select({
    fullName: Puri.concat("first_name", "' '", "last_name"),
  });

์ง‘๊ณ„ ํ•จ์ˆ˜

const results = await db.table("employees").select({
  total: Puri.count("id"),
  totalSalary: Puri.sum("salary"),
  avgSalary: Puri.avg("salary"),
  maxSalary: Puri.max("salary"),
  minSalary: Puri.min("salary"),
});

WHERE Raw

๋ณต์žกํ•œ WHERE ์กฐ๊ฑด์„ ์ง์ ‘ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ธฐ๋ณธ WHERE Raw

const results = await db
  .table("employees")
  .select({ id: "id", salary: "salary" })
  .whereRaw("salary > ?", [50000])
  .whereRaw("EXTRACT(YEAR FROM hire_date) = ?", [2023]);
SQL Injection ์ฃผ์˜: whereRaw์—์„œ๋Š” ๋ฐ˜๋“œ์‹œ ๋ฐ”์ธ๋”ฉ(?)์„ ์‚ฌ์šฉํ•˜์„ธ์š”. ์‚ฌ์šฉ์ž ์ž…๋ ฅ์„ ์ง์ ‘ ๋ฌธ์ž์—ด์— ๋„ฃ์œผ๋ฉด ์•ˆ ๋ฉ๋‹ˆ๋‹ค.

๋ณต์žกํ•œ ์กฐ๊ฑด

const results = await db
  .table("employees")
  .select({ id: "id", name: "username" })
  .whereRaw(
    `
    (department_id = ? AND salary > ?)
    OR (department_id = ? AND salary > ?)
  `,
    [1, 60000, 2, 70000]
  );

๋‚ ์งœ ํ•จ์ˆ˜

// ์ตœ๊ทผ 30์ผ ๋ฐ์ดํ„ฐ
const results = await db
  .table("users")
  .select({ id: "id" })
  .whereRaw("created_at > NOW() - INTERVAL '30 days'");

// ํŠน์ • ์—ฐ๋„
const results = await db
  .table("users")
  .select({ id: "id" })
  .whereRaw("EXTRACT(YEAR FROM created_at) = ?", [2024]);

CASE WHEN - ์กฐ๊ฑด๋ถ€ ๊ฐ’

CASE WHEN ํ‘œํ˜„์‹์œผ๋กœ ์กฐ๊ฑด์— ๋”ฐ๋ผ ๋‹ค๋ฅธ ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ธฐ๋ณธ CASE WHEN

const results = await db.table("employees").select({
  id: "id",
  name: "username",
  salaryLevel: Puri.rawString(`
      CASE
        WHEN salary < 50000 THEN 'Junior'
        WHEN salary < 70000 THEN 'Mid'
        ELSE 'Senior'
      END
    `),
});

์ˆซ์ž ๊ณ„์‚ฐ

const results = await db.table("products").select({
  id: "id",
  name: "name",
  price: "price",
  discountedPrice: Puri.rawNumber(`
      CASE
        WHEN category = 'sale' THEN price * 0.8
        WHEN category = 'clearance' THEN price * 0.5
        ELSE price
      END
    `),
});

Boolean ๊ฒฐ๊ณผ

const results = await db.table("users").select({
  id: "id",
  name: "username",
  isPremium: Puri.rawBoolean(`
      CASE
        WHEN subscription_tier IN ('gold', 'platinum') THEN TRUE
        ELSE FALSE
      END
    `),
});

์„œ๋ธŒ์ฟผ๋ฆฌ์™€ Raw SQL

Scalar ์„œ๋ธŒ์ฟผ๋ฆฌ

const results = await db.table("users").select({
  id: "id",
  name: "username",
  postCount: Puri.rawNumber(`
      (SELECT COUNT(*) FROM posts WHERE posts.user_id = users.id)
    `),
});

COALESCE - NULL ์ฒ˜๋ฆฌ

const results = await db.table("employees").select({
  id: "id",
  departmentName: Puri.rawString(`
      COALESCE(
        (SELECT name FROM departments WHERE id = employees.department_id),
        'No Department'
      )
    `),
});

์‹ค์ „ ์˜ˆ์ œ

์‚ฌ์šฉ์ž ํ†ต๊ณ„ ๋Œ€์‹œ๋ณด๋“œ

async getUserStats(userId: number) {
  const stats = await this.getPuri("r")
    .table("users")
    .select({
      userId: "users.id",
      username: "users.username",

      // ๊ฒŒ์‹œ๊ธ€ ํ†ต๊ณ„
      totalPosts: Puri.rawNumber(`
        (SELECT COUNT(*) FROM posts WHERE posts.user_id = users.id)
      `),

      recentPosts: Puri.rawNumber(`
        (SELECT COUNT(*)
         FROM posts
         WHERE posts.user_id = users.id
         AND posts.created_at > NOW() - INTERVAL '30 days')
      `),

      // ํ™œ๋™ ๋ ˆ๋ฒจ
      activityLevel: Puri.rawString(`
        CASE
          WHEN (SELECT COUNT(*) FROM posts WHERE user_id = users.id) > 100 THEN 'High'
          WHEN (SELECT COUNT(*) FROM posts WHERE user_id = users.id) > 10 THEN 'Medium'
          ELSE 'Low'
        END
      `),

      // ๊ฐ€์ž… ๊ฒฝ๊ณผ ์ผ์ˆ˜
      daysSinceJoined: Puri.rawNumber(`
        EXTRACT(DAY FROM AGE(NOW(), users.created_at))
      `),
    })
    .where("users.id", userId)
    .first();

  return stats;
}

์‹œ๊ฐ„๋Œ€๋ณ„ ์ง‘๊ณ„

async getHourlyStats(date: string) {
  const stats = await this.getPuri("r")
    .table("events")
    .select({
      hour: Puri.rawNumber("EXTRACT(HOUR FROM created_at)"),
      date: Puri.rawDate("DATE(created_at)"),
      eventCount: Puri.count("id"),
      uniqueUsers: Puri.rawNumber("COUNT(DISTINCT user_id)"),
    })
    .whereRaw("DATE(created_at) = ?", [date])
    .groupBy("hour", "date")
    .orderBy("hour", "asc");

  return stats;
}

์ˆœ์œ„ ๊ณ„์‚ฐ

async getTopUsers() {
  const results = await this.getPuri("r")
    .table("users")
    .select({
      userId: "users.id",
      username: "users.username",
      postCount: Puri.rawNumber(`
        (SELECT COUNT(*) FROM posts WHERE posts.user_id = users.id)
      `),
      rank: Puri.rawNumber(`
        RANK() OVER (ORDER BY
          (SELECT COUNT(*) FROM posts WHERE posts.user_id = users.id) DESC
        )
      `),
    })
    .whereRaw(`
      (SELECT COUNT(*) FROM posts WHERE posts.user_id = users.id) > 0
    `)
    .orderBy("rank", "asc")
    .limit(10);

  return results;
}

์œˆ๋„์šฐ ํ•จ์ˆ˜

ROW_NUMBER

const results = await db.table("employees").select({
  id: "id",
  name: "username",
  salary: "salary",
  rowNumber: Puri.rawNumber(`
      ROW_NUMBER() OVER (ORDER BY salary DESC)
    `),
});

RANK / DENSE_RANK

const results = await db.table("employees").select({
  id: "id",
  departmentId: "department_id",
  salary: "salary",
  rankInDept: Puri.rawNumber(`
      RANK() OVER (PARTITION BY department_id ORDER BY salary DESC)
    `),
});

LAG / LEAD - ์ด์ „/๋‹ค์Œ ํ–‰

const results = await db
  .table("sales")
  .select({
    id: "id",
    month: "month",
    amount: "amount",
    previousMonth: Puri.rawNumber(`
      LAG(amount, 1) OVER (ORDER BY month)
    `),
    nextMonth: Puri.rawNumber(`
      LEAD(amount, 1) OVER (ORDER BY month)
    `),
  })
  .orderBy("month", "asc");

JSON ํ•จ์ˆ˜ (PostgreSQL)

JSON ํ•„๋“œ ์ถ”์ถœ

const results = await db.table("users").select({
  id: "id",
  city: Puri.rawString("metadata->>'city'"),
  age: Puri.rawNumber("(metadata->>'age')::integer"),
  tags: Puri.rawStringArray(
    "ARRAY(SELECT jsonb_array_elements_text(metadata->'tags'))"
  ),
});

JSON ์ง‘๊ณ„

const results = await db
  .table("employees")
  .select({
    departmentId: "department_id",
    employees: Puri.rawString(`
      JSON_AGG(JSON_BUILD_OBJECT(
        'id', id,
        'name', username,
        'salary', salary
      ))
    `),
  })
  .groupBy("department_id");

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

EXPLAIN ์‚ฌ์šฉ

// ์ฟผ๋ฆฌ ์‹คํ–‰ ๊ณ„ํš ํ™•์ธ
const plan = await db
  .table("employees")
  .select({ id: "id" })
  .where("department_id", 1)
  .rawQuery()
  .explain();

console.log(plan);

์ธ๋ฑ์Šค ํžŒํŠธ (PostgreSQL์€ ์ง€์› ์•ˆํ•จ)

PostgreSQL์€ ์˜ตํ‹ฐ๋งˆ์ด์ €๊ฐ€ ์ž๋™์œผ๋กœ ์ธ๋ฑ์Šค๋ฅผ ์„ ํƒํ•ฉ๋‹ˆ๋‹ค. ๋Œ€์‹  ํ†ต๊ณ„ ์—…๋ฐ์ดํŠธ:
ANALYZE employees;

ํƒ€์ž… ์•ˆ์ „์„ฑ

Raw ํ•จ์ˆ˜๋“ค์€ ๋ฐ˜ํ™˜ ํƒ€์ž…์„ ๋ช…์‹œํ•ฉ๋‹ˆ๋‹ค.
const results = await db.table("users").select({
  stringValue: Puri.rawString("'test'"), // string
  numberValue: Puri.rawNumber("123"), // number
  boolValue: Puri.rawBoolean("TRUE"), // boolean
  dateValue: Puri.rawDate("NOW()"), // Date
  arrayValue: Puri.rawStringArray("'{}'"), // string[]
});

// ํƒ€์ž…์ด ์ž๋™์œผ๋กœ ์ถ”๋ก ๋จ
results[0].stringValue; // string
results[0].numberValue; // number
results[0].boolValue; // boolean

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