Advanced Features Overview
Transaction
Ensure data consistency with transactionsACID properties
Subqueries
Express complex conditions with nested queriesFROM, WHERE, SELECT
Upsert
INSERT OR UPDATE at onceON CONFLICT
Vector Search
AI embedding similarity searchpgvector
Transaction
Transactions bundle multiple queries into one unit to ensure data consistency.Basic Transaction
Copy
await db.transaction(async (trx) => {
// 1. Create user
const [userId] = await trx
.table("users")
.insert({
username: "john",
email: "john@test.com",
password: "hashed",
role: "normal",
})
.returning("id");
// 2. Create profile
await trx.table("profiles").insert({
user_id: userId.id,
bio: "Hello world",
});
// 3. Create initial settings
await trx.table("user_settings").insert({
user_id: userId.id,
theme: "dark",
language: "ko",
});
// Auto-commit if all operations succeed
// Auto-rollback if any fails
});
ACID properties of transactions:
- Atomicity: All succeed or all fail
- Consistency: Maintain data consistency
- Isolation: Isolate concurrent executions
- Durability: Permanent storage
Explicit Rollback
Copy
await db.transaction(async (trx) => {
// Insert data
await trx.table("users").insert({
username: "test",
email: "test@test.com",
password: "pass",
role: "normal",
});
// Rollback based on condition
const existingUser = await trx
.table("users")
.where("email", "test@test.com")
.first();
if (existingUser) {
// Manual rollback
await trx.rollback();
return;
}
// Auto-commit if not rolled back
});
Transaction Error Handling
Copy
try {
await db.transaction(async (trx) => {
await trx.table("users").insert({
username: "john",
email: "john@test.com",
password: "pass",
role: "normal",
});
// Auto-rollback on error
throw new Error("Something went wrong");
});
} catch (error) {
console.error("Transaction failed:", error);
// All changes are rolled back
}
Subqueries
Use subqueries to write complex queries.FROM Clause Subquery
Copy
// Define subquery
const activeUsers = db
.table("users")
.where("is_active", true)
.select({
id: "id",
username: "username",
email: "email",
});
// Use subquery as a table
const results = await db
.table({ active: activeUsers })
.join("posts", "active.id", "posts.user_id")
.select({
userId: "active.id",
username: "active.username",
postCount: Puri.count("posts.id"),
})
.groupBy("active.id", "active.username");
WHERE Clause Subquery
Copy
// Employees with salary above average
const results = await db
.table("employees")
.select({
id: "id",
name: "username",
salary: "salary",
})
.whereRaw(`
salary > (SELECT AVG(salary) FROM employees)
`);
SELECT Clause Subquery
Copy
const results = await db
.table("users")
.select({
id: "id",
username: "username",
postCount: Puri.rawNumber(`
(SELECT COUNT(*) FROM posts WHERE posts.user_id = users.id)
`),
commentCount: Puri.rawNumber(`
(SELECT COUNT(*) FROM comments WHERE comments.user_id = users.id)
`),
});
WHERE Groups - Complex Conditions
whereGroup - AND/OR Combination
Copy
const results = await db
.table("users")
.select({ id: "id", name: "username" })
.whereGroup((g) =>
g
.where("role", "admin")
.orWhere("role", "moderator")
)
.where("is_active", true);
// SQL: WHERE (role = 'admin' OR role = 'moderator') AND is_active = true
Nested Groups
Copy
const results = await db
.table("employees")
.select({ id: "id", name: "username" })
.whereGroup((g) =>
g
.whereGroup((g2) =>
g2
.where("department_id", 1)
.where("salary", ">", 50000)
)
.orWhereGroup((g2) =>
g2
.where("department_id", 2)
.where("salary", ">", 60000)
)
);
// SQL: WHERE (
// (department_id = 1 AND salary > 50000)
// OR
// (department_id = 2 AND salary > 60000)
// )
Practical Example - Search Filter
Copy
async searchUsers(params: {
search?: string;
role?: string[];
isActive?: boolean;
minAge?: number;
}) {
let query = this.getPuri("r")
.table("users")
.select({
id: "id",
username: "username",
email: "email",
});
// Search term (multiple fields)
if (params.search) {
query = query.whereGroup((g) =>
g
.where("username", "like", `%${params.search}%`)
.orWhere("email", "like", `%${params.search}%`)
.orWhere("bio", "like", `%${params.search}%`)
);
}
// Role filter
if (params.role && params.role.length > 0) {
query = query.whereIn("role", params.role);
}
// Active status
if (params.isActive !== undefined) {
query = query.where("is_active", params.isActive);
}
// Age filter
if (params.minAge !== undefined) {
query = query.whereRaw(
"EXTRACT(YEAR FROM AGE(birth_date)) >= ?",
[params.minAge]
);
}
return await query.orderBy("created_at", "desc");
}
UPSERT - ON CONFLICT
Perform INSERT and UPDATE at once.Basic UPSERT
Copy
await db
.table("users")
.insert({
email: "john@test.com",
username: "john",
password: "hashed",
role: "normal",
})
.onConflict("email", {
update: ["username", "password"],
});
// If email already exists, update only username and password
// If not exists, insert new row
Selective Update
Copy
await db
.table("user_stats")
.insert({
user_id: 1,
login_count: 1,
last_login: new Date(),
})
.onConflict("user_id", {
update: {
login_count: Puri.rawNumber("user_stats.login_count + 1"),
last_login: new Date(),
},
});
// login_count is existing value + 1
// last_login is updated to new value
DO NOTHING
Copy
await db
.table("users")
.insert({
email: "john@test.com",
username: "john",
password: "pass",
role: "normal",
})
.onConflict("email", "nothing");
// Do nothing if email already exists
Multiple Column UNIQUE
Copy
await db
.table("user_preferences")
.insert({
user_id: 1,
key: "theme",
value: "dark",
})
.onConflict(["user_id", "key"], {
update: ["value"],
});
// If (user_id, key) combination exists, update only value
UpsertBuilder - Complex Relationship Saving
Use when saving relationships across multiple tables at once.Basic Usage
Copy
await db.transaction(async (trx) => {
// 1. Register Company
const companyRef = trx.ubRegister("companies", {
name: "Tech Corp",
});
// 2. Register Department (reference Company)
const deptRef = trx.ubRegister("departments", {
name: "Engineering",
company_id: companyRef, // Use UBRef
});
// 3. Register User
const userRef = trx.ubRegister("users", {
email: "dev@tech.com",
username: "developer",
password: "hashed",
role: "normal",
});
// 4. Register Employee (reference User, Department)
trx.ubRegister("employees", {
user_id: userRef,
department_id: deptRef,
employee_number: "E001",
salary: "70000",
});
// 5. Save in order
await trx.ubUpsert("companies");
await trx.ubUpsert("departments");
await trx.ubUpsert("users");
await trx.ubUpsert("employees");
});
Benefits of UBRef:
- Define relationships without worrying about foreign keys
- Automatically resolves dependency order
- Auto-assigns IDs within transaction
Many-to-Many Relationships
Copy
await db.transaction(async (trx) => {
// Create Project
const projectRef = trx.ubRegister("projects", {
name: "New Feature",
status: "in_progress",
});
// Create multiple Employees
const emp1Ref = trx.ubRegister("employees", {
employee_number: "E100",
salary: "60000",
});
const emp2Ref = trx.ubRegister("employees", {
employee_number: "E101",
salary: "65000",
});
// Set M:N relationships
trx.ubRegister("projects__employees", {
project_id: projectRef,
employee_id: emp1Ref,
});
trx.ubRegister("projects__employees", {
project_id: projectRef,
employee_id: emp2Ref,
});
// Save
await trx.ubUpsert("projects");
await trx.ubUpsert("employees");
await trx.ubUpsert("projects__employees");
});
Bulk Registration
Copy
await db.transaction(async (trx) => {
const companyRef = trx.ubRegister("companies", {
name: "Startup Inc",
});
// Register 10 employees
for (let i = 0; i < 10; i++) {
const userRef = trx.ubRegister("users", {
email: `user${i}@startup.com`,
username: `user${i}`,
password: "pass",
role: "normal",
});
trx.ubRegister("employees", {
user_id: userRef,
employee_number: `E${1000 + i}`,
salary: String(50000 + i * 5000),
});
}
// Batch save
await trx.ubUpsert("companies");
await trx.ubUpsert("users");
await trx.ubUpsert("employees");
});
Vector Search - Vector Similarity Search
Semantic search using AI embeddings.Cosine Similarity (Default)
Copy
const queryEmbedding = [0.1, 0.2, 0.3, ...]; // 1536-dimensional vector
const results = await db
.table("documents")
.vectorSimilarity("embedding", queryEmbedding, {
method: "cosine",
threshold: 0.7, // 70%+ similarity
})
.select({
id: "id",
title: "title",
similarity: "similarity", // Auto-added
})
.limit(10);
// Results sorted by similarity descending
results[0].similarity; // 0.95 (higher = more similar)
L2 Distance
Copy
const results = await db
.table("documents")
.vectorSimilarity("embedding", queryEmbedding, {
method: "l2",
threshold: 1.0, // Distance 1.0 or less
})
.select({
id: "id",
title: "title",
similarity: "similarity", // Distance value (lower = more similar)
})
.limit(10);
Inner Product
Copy
const results = await db
.table("documents")
.vectorSimilarity("embedding", queryEmbedding, {
method: "inner_product",
threshold: 0.5,
})
.select({
id: "id",
content: "content",
similarity: "similarity",
})
.limit(20);
Vector Search method selection:
- cosine: General embedding search (OpenAI, Cohere, etc.)
- l2: Distance-based search
- inner_product: Suitable for normalized vectors
Full-text Search
PGroonga (Excellent for CJK languages)
Copy
// Single column search
const results = await db
.table("posts")
.whereSearch("title", "search term")
.select({
id: "id",
title: "title",
score: Puri.score(), // Search score
})
.orderBy("score", "desc");
// Multi-column search (with weights)
const results = await db
.table("posts")
.whereSearch(["title", "content"], "search term", {
weights: [10, 1], // title is 10x more important
})
.select({
id: "id",
title: "title",
highlightedTitle: Puri.highlight("title", "search term"),
score: Puri.score(),
})
.orderBy("score", "desc");
PostgreSQL tsvector
Copy
// Basic search
const results = await db
.table("documents")
.whereTsSearch("title", "search query", "simple")
.select({
id: "id",
title: "title",
highlighted: Puri.tsHighlight("title", "search query"),
rank: Puri.tsRank("to_tsvector('simple', title)", "search query"),
})
.orderBy("rank", "desc");
// Advanced search options
const results = await db
.table("documents")
.whereTsSearch("content", "important keywords", {
config: "english",
parser: "plainto_tsquery",
})
.select({
id: "id",
content: Puri.tsHighlight("content", "important keywords", {
startSel: "<mark>",
stopSel: "</mark>",
maxFragments: 3,
}),
rank: Puri.tsRankCd("to_tsvector('english', content)", "important keywords"),
})
.orderBy("rank", "desc");
Batch Operations
Batch Insert
Copy
const users = [
{ username: "user1", email: "user1@test.com", password: "pass", role: "normal" },
{ username: "user2", email: "user2@test.com", password: "pass", role: "normal" },
{ username: "user3", email: "user3@test.com", password: "pass", role: "normal" },
];
await db.table("users").insert(users);
Batch Update
Copy
await db.transaction(async (trx) => {
for (const userId of userIds) {
await trx
.table("users")
.where("id", userId)
.update({ is_active: true });
}
});
Batch Delete
Copy
// Using IN
await db
.table("users")
.whereIn("id", [1, 2, 3, 4, 5])
.delete();
// With condition
await db
.table("users")
.where("last_login", "<", "2023-01-01")
.delete();
Performance Optimization
Index Usage
Copy
-- Index frequently queried columns
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_posts_user_id ON posts(user_id);
-- Composite index
CREATE INDEX idx_users_role_active ON users(role, is_active);
-- Vector index (HNSW)
CREATE INDEX idx_documents_embedding ON documents
USING hnsw (embedding vector_cosine_ops);
-- Full-text index
CREATE INDEX idx_posts_content_pgroonga ON posts
USING pgroonga (content);
SELECT Optimization
Copy
// ❌ Bad: Query unnecessary columns
await db.table("users").selectAll();
// ✅ Good: Only needed columns
await db
.table("users")
.select({
id: "id",
name: "username",
});
JOIN Optimization
Copy
// ✅ Small table → Large table order
await db
.table("departments") // 10 rows
.join("employees", ...) // 100 rows
.join("user_logs", ...); // 10000 rows
Pagination Optimization
Copy
// ✅ Cursor-based pagination (for large data)
async function getCursorPage(lastId: number, limit: number) {
return await db
.table("posts")
.select({ id: "id", title: "title" })
.where("id", ">", lastId)
.orderBy("id", "asc")
.limit(limit);
}
// Usage
const page1 = await getCursorPage(0, 20);
const page2 = await getCursorPage(page1[page1.length - 1].id, 20);