Skip to main content
where is a method that specifies conditions to filter query results. It supports various operators and condition combinations, enabling type-safe filtering.

Basic Usage

Simple Equality Condition

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

// WHERE users.status = 'active'

Comparison Operators

// Greater than
await puri.table("users")
  .where("users.age", ">", 18);

// Less than or equal
await puri.table("orders")
  .where("orders.amount", "<=", 1000);

// Not equal
await puri.table("posts")
  .where("posts.status", "!=", "deleted");
Supported Operators:
  • = - Equal (default)
  • != - Not equal
  • > - Greater than
  • >= - Greater than or equal
  • < - Less than
  • <= - Less than or equal
  • like - Pattern matching
  • not like - Pattern mismatch
  • ilike - Case-insensitive pattern matching (PostgreSQL)
  • not ilike - Case-insensitive pattern mismatch (PostgreSQL)

Object-Style Conditions

You can specify multiple AND conditions as an object.
const users = await puri.table("users")
  .where({
    "users.status": "active",
    "users.role": "admin",
    "users.verified": true
  })
  .select({ id: "users.id", name: "users.name" });

// WHERE users.status = 'active'
//   AND users.role = 'admin'
//   AND users.verified = true

NULL Handling

IS NULL

// Explicit null check
await puri.table("users")
  .where("users.deleted_at", null);

// WHERE users.deleted_at IS NULL

// Using operator
await puri.table("users")
  .where("users.deleted_at", "=", null);

// WHERE users.deleted_at IS NULL

IS NOT NULL

await puri.table("users")
  .where("users.email", "!=", null);

// WHERE users.email IS NOT NULL

Pattern Matching (LIKE)

Partial Matching

// Contains
await puri.table("users")
  .where("users.email", "like", "%@gmail.com");

// Starts with
await puri.table("users")
  .where("users.name", "like", "John%");

// Ends with
await puri.table("users")
  .where("users.phone", "like", "%1234");

// Contains anywhere
await puri.table("posts")
  .where("posts.title", "like", "%typescript%");

NOT LIKE

await puri.table("users")
  .where("users.email", "not like", "%spam%");

// WHERE users.email NOT LIKE '%spam%'

ILIKE (Case-Insensitive, PostgreSQL)

Performs case-insensitive pattern matching in PostgreSQL.
// Search regardless of case
await puri.table("users")
  .where("users.name", "ilike", "%john%");

// WHERE users.name ILIKE '%john%'
// Matches 'John', 'JOHN', 'john', etc.

NOT ILIKE (PostgreSQL)

await puri.table("users")
  .where("users.email", "not ilike", "%SPAM%");

// WHERE users.email NOT ILIKE '%SPAM%'
// Excludes 'spam', 'Spam', 'SPAM', etc.
ilike and not ilike are PostgreSQL-specific operators. In MySQL, you can achieve similar results by combining LOWER() function with like.

whereIn / whereNotIn

One of Multiple Values

// IN
const userIds = [1, 2, 3, 4, 5];
await puri.table("users")
  .whereIn("users.id", userIds);

// WHERE users.id IN (1, 2, 3, 4, 5)

// NOT IN
await puri.table("users")
  .whereNotIn("users.status", ["banned", "suspended"]);

// WHERE users.status NOT IN ('banned', 'suspended')

Empty Array Handling

// Empty array always returns false
await puri.table("users")
  .whereIn("users.id", []);

// WHERE 1 = 0 (returns nothing)

Complex Condition Groups

whereGroup (AND Group)

Creates conditions wrapped in parentheses.
await puri.table("users")
  .where("users.role", "admin")
  .whereGroup((group) => {
    group.where("users.status", "active")
         .orWhere("users.status", "pending");
  });

// WHERE users.role = 'admin'
//   AND (users.status = 'active' OR users.status = 'pending')

orWhereGroup (OR Group)

await puri.table("products")
  .where("products.published", true)
  .orWhereGroup((group) => {
    group.where("products.featured", true)
         .where("products.stock", ">", 0);
  });

// WHERE products.published = true
//   OR (products.featured = true AND products.stock > 0)

Nested Groups

await puri.table("orders")
  .whereGroup((outer) => {
    outer.where("orders.status", "completed")
         .whereGroup((inner) => {
           inner.where("orders.amount", ">", 1000)
                .orWhere("orders.vip", true);
         });
  });

// WHERE (
//   orders.status = 'completed'
//   AND (orders.amount > 1000 OR orders.vip = true)
// )

whereMatch (MySQL)

Uses MySQL’s FULLTEXT index.
await puri.table("posts")
  .whereMatch("posts.content", "typescript programming");

// WHERE MATCH (posts.content) AGAINST ('typescript programming')
To use whereMatch, the column must have a FULLTEXT index.

whereTsSearch (PostgreSQL)

Uses PostgreSQL’s tsvector.
await puri.table("posts")
  .whereTsSearch("posts.content_tsv", "typescript programming");

// WHERE posts.content_tsv @@ websearch_to_tsquery('simple', 'typescript programming')
Options:
// Specify parser
await puri.table("posts")
  .whereTsSearch("posts.content_tsv", "typescript", {
    parser: "plainto_tsquery",  // or "to_tsquery", "phraseto_tsquery"
    config: "english"  // Language configuration
  });

// Specify config only
await puri.table("posts")
  .whereTsSearch("posts.content_tsv", "typescript", "english");

whereSearch (PGroonga)

Full-text search using the PGroonga extension.
// Single column search
await puri.table("posts")
  .whereSearch("posts.title", "타입스크립트");

// WHERE posts.title &@~ pgroonga_condition('타입스크립트')

// Multiple column search (array)
await puri.table("posts")
  .whereSearch(
    ["posts.title", "posts.content"],
    "타입스크립트"
  );

// WHERE ARRAY[posts.title::text, posts.content::text]
//   &@~ pgroonga_condition('타입스크립트')

// Specify weights
await puri.table("posts")
  .whereSearch(
    ["posts.title", "posts.content"],
    "타입스크립트",
    { weights: [10, 1] }  // title is 10x more important than content
  );
PGroonga search must use the same column configuration as the index to be used.

Raw SQL Conditions

whereRaw

You can write complex SQL conditions directly.
// No parameters
await puri.table("users")
  .whereRaw("YEAR(users.created_at) = YEAR(CURRENT_DATE)");

// Parameter binding
await puri.table("users")
  .whereRaw("users.age BETWEEN ? AND ?", [18, 65]);

// Multiple conditions
await puri.table("orders")
  .whereRaw("DATE(orders.created_at) = ?", ["2024-01-01"])
  .whereRaw("orders.amount > ?", [1000]);
Always use parameter binding to prevent SQL injection.

Real-World Examples

async function getActiveAdmins() {
  return puri.table("users")
    .where({
      "users.role": "admin",
      "users.status": "active",
      "users.email_verified": true
    })
    .where("users.last_login", ">", new Date(Date.now() - 30 * 24 * 60 * 60 * 1000))
    .select({
      id: "users.id",
      email: "users.email",
      name: "users.name",
      last_login: "users.last_login"
    })
    .orderBy("users.last_login", "desc");
}

Condition Chaining

All where methods are chainable and are connected with AND by default.
const result = await puri.table("users")
  .where("users.role", "admin")         // AND
  .where("users.status", "active")       // AND
  .where("users.age", ">", 18)          // AND
  .whereIn("users.country", ["KR", "US"]) // AND
  .where("users.verified", true);        // AND

OR Conditions

orWhere within WhereGroup

await puri.table("users")
  .whereGroup((group) => {
    group.where("users.role", "admin")
         .orWhere("users.role", "moderator")
         .orWhere("users.role", "editor");
  });

// WHERE (
//   users.role = 'admin'
//   OR users.role = 'moderator'
//   OR users.role = 'editor'
// )
There is no orWhere at the top level. Use OR conditions within whereGroup or orWhereGroup.

Type Safety

WHERE conditions are validated type-safely.
// ✅ Valid column
await puri.table("users")
  .where("users.email", "like", "%@gmail.com");

// ❌ Type error: Non-existent column
await puri.table("users")
  .where("users.unknown_field", "value");

// ✅ Valid type
await puri.table("users")
  .where("users.age", 25);  // number

// ❌ Type error: Wrong type
await puri.table("users")
  .where("users.age", "25");  // string (age is number)

Performance Optimization

1. Utilizing Indexes

// ✅ Good: Use indexed column
await puri.table("users")
  .where("users.email", "user@example.com");  // email needs index

// ❌ Bad: Using functions (invalidates index)
await puri.table("users")
  .whereRaw("LOWER(users.email) = ?", ["user@example.com"]);

2. LIKE/ILIKE Patterns

// ✅ Good: Fixed prefix (can use index)
.where("users.name", "like", "John%")
.where("users.name", "ilike", "john%")  // PostgreSQL

// ❌ Bad: Leading wildcard (cannot use index)
.where("users.name", "like", "%John")
.where("users.name", "ilike", "%john")  // PostgreSQL
In PostgreSQL, ilike is convenient for case-insensitive search, but it can affect performance on large datasets. Consider using citext type or pg_trgm extension indexes for such cases.

3. IN vs Multiple ORs

// ✅ Good: Use IN
.whereIn("users.id", [1, 2, 3, 4, 5])

// ❌ Bad: Multiple ORs (inefficient)
.whereGroup((g) => {
  g.where("users.id", 1)
   .orWhere("users.id", 2)
   .orWhere("users.id", 3)
   .orWhere("users.id", 4)
   .orWhere("users.id", 5)
})

Cautions

1. NULL Handling

// ✅ Correct: NULL check
.where("users.deleted_at", null)           // IS NULL
.where("users.deleted_at", "=", null)      // IS NULL
.where("users.deleted_at", "!=", null)     // IS NOT NULL

// ❌ Wrong: Regular comparison (doesn't work)
.where("users.deleted_at", "=", undefined)

2. Empty Arrays

// whereIn with empty array returns no results
const ids = [];
await puri.table("users")
  .whereIn("users.id", ids);  // Returns nothing

// Handle conditionally
if (ids.length > 0) {
  query = query.whereIn("users.id", ids);
}

3. Raw SQL Injection

// ❌ Dangerous: SQL injection possible
const userInput = "'; DROP TABLE users; --";
.whereRaw(`users.name = '${userInput}'`)

// ✅ Safe: Parameter binding
.whereRaw("users.name = ?", [userInput])

4. Condition Order

// WHERE order can affect performance

// ✅ Good: Highly selective conditions first
.where("users.id", 123)        // Very selective
.where("users.status", "active") // Less selective

// ❌ Inefficient: Low selectivity first
.where("users.status", "active") // Many rows
.where("users.id", 123)        // Few rows

Next Steps