๋ฉ”์ธ ์ฝ˜ํ…์ธ ๋กœ ๊ฑด๋„ˆ๋›ฐ๊ธฐ
select๋Š” ์กฐํšŒํ•  ํ•„๋“œ๋ฅผ ์ง€์ •ํ•˜๋Š” ๋ฉ”์„œ๋“œ์ž…๋‹ˆ๋‹ค. ํƒ€์ž… ์•ˆ์ „ํ•˜๊ฒŒ ํ•„๋“œ๋ฅผ ์„ ํƒํ•˜๊ณ , SQL ์ง‘๊ณ„ ํ•จ์ˆ˜ ๋ฐ ํ‘œํ˜„์‹์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ธฐ๋ณธ ์‚ฌ์šฉ๋ฒ•

์ „์ฒด ํ•„๋“œ ์กฐํšŒ

const users = await puri.table("users").selectAll();

// SELECT * FROM users

ํŠน์ • ํ•„๋“œ ์„ ํƒ

const users = await puri.table("users").select({
  id: "users.id",
  email: "users.email",
  name: "users.name"
});

// SELECT users.id, users.email, users.name FROM users

ํ•„๋“œ ๋ณ„์นญ (Alias)

const users = await puri.table("users").select({
  userId: "users.id",
  userName: "users.name"
});

// SELECT users.id as userId, users.name as userName FROM users

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

Puri๋Š” ํƒ€์ž… ์•ˆ์ „ํ•œ SQL ํ•จ์ˆ˜๋“ค์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

COUNT

const result = await puri.table("users")
  .select({
    total: Puri.count()  // COUNT(*)
  })
  .first();

console.log(result.total);  // number

// ํŠน์ • ์ปฌ๋Ÿผ ์นด์šดํŠธ
const result = await puri.table("posts")
  .select({
    published: Puri.count("posts.published")
  })
  .first();

SUM, AVG, MAX, MIN

const stats = await puri.table("orders")
  .select({
    total: Puri.sum("orders.amount"),
    average: Puri.avg("orders.amount"),
    max: Puri.max("orders.amount"),
    min: Puri.min("orders.amount")
  })
  .first();

console.log(stats.total);    // number
console.log(stats.average);  // number

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

const users = await puri.table("users").select({
  id: "users.id",
  full_name: Puri.concat("users.first_name", "' '", "users.last_name"),
  email_upper: Puri.upper("users.email"),
  email_lower: Puri.lower("users.email")
});

Raw SQL ํ‘œํ˜„์‹

๋ณต์žกํ•œ SQL ํ‘œํ˜„์‹์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

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

const result = await puri.table("users").select({
  id: "users.id",
  
  // ๋ฌธ์ž์—ด ๋ฐ˜ํ™˜
  display_name: Puri.rawString("CONCAT(users.first_name, ' ', users.last_name)"),
  
  // ์ˆซ์ž ๋ฐ˜ํ™˜
  age: Puri.rawNumber("YEAR(CURRENT_DATE) - YEAR(users.birth_date)"),
  
  // ๋ถˆ๋ฆฌ์–ธ ๋ฐ˜ํ™˜
  is_adult: Puri.rawBoolean("YEAR(CURRENT_DATE) - YEAR(users.birth_date) >= 18"),
  
  // ๋‚ ์งœ ๋ฐ˜ํ™˜
  signup_date: Puri.rawDate("DATE(users.created_at)"),
  
  // ๋ฌธ์ž์—ด ๋ฐฐ์—ด ๋ฐ˜ํ™˜
  tags: Puri.rawStringArray("ARRAY_AGG(tags.name)")
});

// ํƒ€์ž…์ด ์ž๋™์œผ๋กœ ์ถ”๋ก ๋จ
result[0].display_name;  // string
result[0].age;           // number
result[0].is_adult;      // boolean
result[0].signup_date;   // Date
result[0].tags;          // string[]

์ค‘์ฒฉ ๊ฐ์ฒด ์„ ํƒ

์ค‘์ฒฉ ๊ฐ์ฒด ๊ตฌ์กฐ๋กœ SELECT๋ฅผ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
const users = await puri.table("users")
  .leftJoin("posts", "users.id", "posts.user_id")
  .select({
    id: "users.id",
    email: "users.email",
    post: {
      id: "posts.id",
      title: "posts.title"
    }
  });

// ์ž๋™์œผ๋กœ flatํ•˜๊ฒŒ ๋ณ€ํ™˜๋จ:
// SELECT 
//   users.id,
//   users.email,
//   posts.id as post__id,
//   posts.title as post__title
// FROM users LEFT JOIN posts ...

// ๊ฒฐ๊ณผ๋Š” hydrate๋˜์–ด ์ค‘์ฒฉ ๊ฐ์ฒด๋กœ ๋ฐ˜ํ™˜
console.log(users[0].post.title);

appendSelect

๊ธฐ์กด SELECT์— ํ•„๋“œ๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.
let query = puri.table("users").select({
  id: "users.id",
  email: "users.email"
});

// ํ•„๋“œ ์ถ”๊ฐ€
query = query.appendSelect({
  name: "users.name",
  created_at: "users.created_at"
});

// SELECT users.id, users.email, users.name, users.created_at FROM users
select()๋Š” ๊ธฐ์กด SELECT๋ฅผ ๋ฎ์–ด์“ฐ๊ณ , appendSelect()๋Š” ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.

Full-Text Search ํ•จ์ˆ˜

ts_highlight (PostgreSQL)

PostgreSQL์˜ tsvector๋ฅผ ์‚ฌ์šฉํ•œ ํ•˜์ด๋ผ์ดํŒ…์ž…๋‹ˆ๋‹ค.
const posts = await puri.table("posts")
  .whereTsSearch("posts.content_tsv", "typescript")
  .select({
    id: "posts.id",
    title: Puri.tsHighlight("posts.title", "typescript"),
    content: Puri.tsHighlight("posts.content", "typescript", {
      startSel: "<mark>",
      stopSel: "</mark>",
      maxFragments: 3,
      maxWords: 50
    })
  });
์˜ต์…˜:
  • parser: ์ฟผ๋ฆฌ ํŒŒ์„œ (๊ธฐ๋ณธ: "websearch_to_tsquery")
  • config: ํ…์ŠคํŠธ ๊ฒ€์ƒ‰ ์„ค์ • (๊ธฐ๋ณธ: "simple")
  • startSel: ์‹œ์ž‘ ํƒœ๊ทธ (๊ธฐ๋ณธ: "<b>")
  • stopSel: ์ข…๋ฃŒ ํƒœ๊ทธ (๊ธฐ๋ณธ: "</b>")
  • maxWords: ์ตœ๋Œ€ ๋‹จ์–ด ์ˆ˜
  • minWords: ์ตœ์†Œ ๋‹จ์–ด ์ˆ˜
  • maxFragments: ์ตœ๋Œ€ ์กฐ๊ฐ ์ˆ˜

ts_rank (PostgreSQL)

๊ฒ€์ƒ‰ ๊ด€๋ จ๋„ ์ ์ˆ˜๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค.
const posts = await puri.table("posts")
  .whereTsSearch("posts.content_tsv", "typescript")
  .select({
    id: "posts.id",
    title: "posts.title",
    
    // ๊ธฐ๋ณธ rank
    rank: Puri.tsRank("posts.content_tsv", "typescript"),
    
    // ๊ฐ€์ค‘์น˜ ์ ์šฉ
    weighted_rank: Puri.tsRank("posts.content_tsv", "typescript", {
      weights: [0.1, 0.2, 0.4, 1.0],  // D, C, B, A ๊ฐ€์ค‘์น˜
      normalization: 1  // ๊ธธ์ด ์ •๊ทœํ™”
    }),
    
    // Cover Density rank
    rank_cd: Puri.tsRankCd("posts.content_tsv", "typescript")
  })
  .orderBy("rank", "desc");
์˜ต์…˜:
  • parser: ์ฟผ๋ฆฌ ํŒŒ์„œ (๊ธฐ๋ณธ: "websearch_to_tsquery")
  • config: ํ…์ŠคํŠธ ๊ฒ€์ƒ‰ ์„ค์ • (๊ธฐ๋ณธ: "simple")
  • weights: [D, C, B, A] ๊ฐ€์ค‘์น˜ ๋ฐฐ์—ด
  • normalization: ์ •๊ทœํ™” ์˜ต์…˜ (0-32)

PGroonga ํ•จ์ˆ˜

PGroonga ํ™•์žฅ์„ ์‚ฌ์šฉํ•œ ์ „๋ฌธ ๊ฒ€์ƒ‰์ž…๋‹ˆ๋‹ค.
const posts = await puri.table("posts")
  .whereSearch("posts.title", "ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ")
  .select({
    id: "posts.id",
    
    // ๋‹จ์ผ ์ปฌ๋Ÿผ ํ•˜์ด๋ผ์ดํŒ…
    title: Puri.highlight("posts.title", "ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ"),
    
    // ์—ฌ๋Ÿฌ ๊ฒ€์ƒ‰์–ด
    content: Puri.highlight("posts.content", ["ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ", "์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ"]),
    
    // ์—ฌ๋Ÿฌ ์ปฌ๋Ÿผ ํ•˜์ด๋ผ์ดํŒ… (๋ฐฐ์—ด ๋ฐ˜ํ™˜)
    highlights: Puri.highlight(
      ["posts.title", "posts.content"],
      ["ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ", "Node.js"]
    ),
    
    // ๊ฒ€์ƒ‰ ์ ์ˆ˜
    score: Puri.score()
  })
  .orderBy("score", "desc");

// ํƒ€์ž… ์ž๋™ ์ถ”๋ก 
console.log(posts[0].title);       // string
console.log(posts[0].highlights);  // string[]
console.log(posts[0].score);       // number

์‹ค์ „ ์˜ˆ์‹œ

const stats = await puri.table("users")
  .select({
    total_users: Puri.count(),
    active_users: Puri.rawNumber(
      "SUM(CASE WHEN status = 'active' THEN 1 ELSE 0 END)"
    ),
    avg_age: Puri.avg("users.age"),
    oldest: Puri.max("users.age"),
    youngest: Puri.min("users.age")
  })
  .first();

console.log(stats);
// {
//   total_users: 1000,
//   active_users: 850,
//   avg_age: 32.5,
//   oldest: 65,
//   youngest: 18
// }

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

SELECT์˜ ๋ฐ˜ํ™˜ ํƒ€์ž…์€ ์ž๋™์œผ๋กœ ์ถ”๋ก ๋ฉ๋‹ˆ๋‹ค.
const result = await puri.table("users")
  .select({
    id: "users.id",
    email: "users.email",
    count: Puri.count(),
    upper_name: Puri.upper("users.name")
  })
  .first();

// ํƒ€์ž… ์ž๋™ ์ถ”๋ก 
result.id;         // number
result.email;      // string
result.count;      // number
result.upper_name; // string

result.unknown;    // โŒ ํƒ€์ž… ์—๋Ÿฌ

์ฃผ์˜์‚ฌํ•ญ

1. select vs appendSelect

// โŒ ๋‚˜์œ ์˜ˆ: ์ด์ „ select๊ฐ€ ๋ฎ์–ด์”Œ์›Œ์ง
query.select({ id: "users.id" })
     .select({ name: "users.name" });  // id๊ฐ€ ์‚ฌ๋ผ์ง!

// โœ… ์ข‹์€ ์˜ˆ: appendSelect๋กœ ์ถ”๊ฐ€
query.select({ id: "users.id" })
     .appendSelect({ name: "users.name" });

2. ํ…Œ์ด๋ธ” ์ ‘๋‘์‚ฌ ํ•„์ˆ˜

// โŒ ์ž˜๋ชป๋จ
puri.table("users").select({
  id: "id"  // ์–ด๋–ค ํ…Œ์ด๋ธ”์˜ id์ธ์ง€ ๋ถˆ๋ช…ํ™•
});

// โœ… ์˜ฌ๋ฐ”๋ฆ„
puri.table("users").select({
  id: "users.id"  // ๋ช…ํ™•ํ•œ ํ…Œ์ด๋ธ” ์ง€์ •
});

3. Raw SQL ํƒ€์ž… ์ง€์ •

// Raw SQL ์‚ฌ์šฉ ์‹œ ๋ฐ˜ํ™˜ ํƒ€์ž… ๋ช…์‹œ
const result = await puri.table("users").select({
  // โœ… ํƒ€์ž… ๋ช…์‹œ
  age: Puri.rawNumber("YEAR(CURRENT_DATE) - YEAR(birth_date)"),
  
  // โŒ ํƒ€์ž… ์—†์Œ (any)
  unknown: puri.raw("SOME_COMPLEX_SQL")
});

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