Best Practices
This guide covers production-ready patterns and recommendations for working with databases in Ductape. Following these practices will help you build reliable, performant, and maintainable applications.
Connection Management
Use Environment-Specific Configurations
Always configure separate database credentials for each environment:
- TypeScript
- Java
- Go
- .NET
// Development - local database
await ductape.database.connect({
database: 'main-db',
});
// Production - production credentials
await ductape.database.connect({
database: 'main-db',
});
// Development - local database
ductape.database.connect(Map.of(
"database", "main-db"
));
// Production - production credentials
ductape.database.connect(Map.of(
"database", "main-db"
));
// Development - local database
client.database.connect({
"database": "main-db",
});
// Production - production credentials
client.database.connect({
"database": "main-db",
});
// Development - local database
await ductape.database.connect({
["database"] = "main-db",
});
// Production - production credentials
await ductape.database.connect({
["database"] = "main-db",
});
Configure environment URLs in the Ductape console to keep credentials secure and separate.
Connect Once, Reuse Connection
Establish the connection once and reuse it for multiple operations:
- TypeScript
- Java
- Go
- .NET
// Good: Connect once at startup
await ductape.database.connect({
database: 'main-db',
});
// All subsequent queries inherit the connection
await ductape.database.query({ table: 'users' });
await ductape.database.query({ table: 'orders' });
await ductape.database.query({ table: 'products' });
// Good: Connect once at startup
ductape.database.connect(Map.of(
"database", "main-db"
));
// All subsequent queries inherit the connection
ductape.database.query(Map.of( "table", "users" ));
ductape.database.query(Map.of( "table", "orders" ));
ductape.database.query(Map.of( "table", "products" ));
// Good: Connect once at startup
client.database.connect({
"database": "main-db",
});
// All subsequent queries inherit the connection
client.database.query({ "table": "users" });
client.database.query({ "table": "orders" });
client.database.query({ "table": "products" });
// Good: Connect once at startup
await ductape.database.connect({
["database"] = "main-db",
});
// All subsequent queries inherit the connection
await ductape.database.query({ ["table"] = "users" });
await ductape.database.query({ ["table"] = "orders" });
await ductape.database.query({ ["table"] = "products" });
- TypeScript
- Java
- Go
- .NET
// Bad: Connecting for each query
async function getUser(id: number) {
await ductape.database.connect({ ... }); // Unnecessary reconnection
return ductape.database.query({ table: 'users', where: { id } });
}
// Bad: Connecting for each query
async function getUser(id: number) Map.of(
ductape.database.connect(Map.of( ... )); // Unnecessary reconnection
return ductape.database.query(Map.of( "table", "users", where: Map.of( id ) ));
)
// Bad: Connecting for each query
async function getUser(id: number) {
client.database.connect({ ... }); // Unnecessary reconnection
return client.database.query({ "table": "users", where: { id } });
}
// Bad: Connecting for each query
async function getUser(id: number) {
await ductape.database.connect({ ... }); // Unnecessary reconnection
return ductape.database.query({ ["table"] = "users", where: { id } });
}
Disconnect When Done
Clean up connections when your application shuts down:
- TypeScript
- Java
- Go
- .NET
process.on('SIGTERM', async () => {
await ductape.database.disconnect();
process.exit(0);
});
process.on('SIGTERM', async () => Map.of(
ductape.database.disconnect();
process.exit(0);
));
process.on('SIGTERM', async () => {
client.database.disconnect();
process.exit(0);
});
process.on('SIGTERM', async () => {
await ductape.database.disconnect();
process.exit(0);
});
Query Optimization
Select Only Needed Columns
Reduce data transfer by selecting only required fields:
- TypeScript
- Java
- Go
- .NET
// Good: Select specific columns
const users = await ductape.database.query({
table: 'users',
select: ['id', 'name', 'email'],
});
// Bad: Selecting all columns when you only need a few
const users = await ductape.database.query({
table: 'users',
// Returns all columns including large text fields
});
// Good: Select specific columns
Map<String, Object> users = ductape.database.query(Map.of(
"table", "users",
select: ['id', 'name', 'email']
));
// Bad: Selecting all columns when you only need a few
Map<String, Object> users = ductape.database.query(Map.of(
"table", "users",
// Returns all columns including large text fields
));
// Good: Select specific columns
users := client.database.query({
"table": "users",
select: ['id', 'name', 'email'],
});
// Bad: Selecting all columns when you only need a few
users := client.database.query({
"table": "users",
// Returns all columns including large text fields
});
// Good: Select specific columns
var users = await ductape.database.query({
["table"] = "users",
select: ['id', 'name', 'email'],
});
// Bad: Selecting all columns when you only need a few
var users = await ductape.database.query({
["table"] = "users",
// Returns all columns including large text fields
});
Use Appropriate Indexes
Create indexes for frequently queried columns:
- TypeScript
- Java
- Go
- .NET
// Create index for email lookups
await ductape.database.createIndex({
database: 'main-db',
table: 'users',
index: {
name: 'idx_users_email',
table: 'users',
columns: [{ name: 'email' }],
unique: true,
},
});
// Create composite index for common queries
await ductape.database.createIndex({
database: 'main-db',
table: 'orders',
index: {
name: 'idx_orders_customer_status',
table: 'orders',
columns: [{ name: 'customer_id' }, { name: 'status' }],
},
});
// Create index for email lookups
ductape.database.createIndex(Map.of(
"database", "main-db",
"table", "users",
index: Map.of(
"name", "idx_users_email",
"table", "users",
columns: [Map.of( "name", "email" )],
"unique", true
)
));
// Create composite index for common queries
ductape.database.createIndex(Map.of(
"database", "main-db",
"table", "orders",
index: Map.of(
"name", "idx_orders_customer_status",
"table", "orders",
columns: [Map.of( "name", "customer_id" ), Map.of( "name", "status" )]
)
));
// Create index for email lookups
client.database.createIndex({
"database": "main-db",
"table": "users",
index: {
"name": "idx_users_email",
"table": "users",
columns: [{ "name": "email" }],
"unique": true,
},
});
// Create composite index for common queries
client.database.createIndex({
"database": "main-db",
"table": "orders",
index: {
"name": "idx_orders_customer_status",
"table": "orders",
columns: [{ "name": "customer_id" }, { "name": "status" }],
},
});
// Create index for email lookups
await ductape.database.createIndex({
["database"] = "main-db",
["table"] = "users",
index: {
["name"] = "idx_users_email",
["table"] = "users",
columns: [{ ["name"] = "email" }],
["unique"] = true,
},
});
// Create composite index for common queries
await ductape.database.createIndex({
["database"] = "main-db",
["table"] = "orders",
index: {
["name"] = "idx_orders_customer_status",
["table"] = "orders",
columns: [{ ["name"] = "customer_id" }, { ["name"] = "status" }],
},
});
Paginate Large Result Sets
Never fetch unlimited results. Always use pagination:
- TypeScript
- Java
- Go
- .NET
// Good: Paginated queries
const pageSize = 20;
const page = 1;
const result = await ductape.database.query({
table: 'orders',
limit: pageSize,
offset: (page - 1) * pageSize,
orderBy: { column: 'created_at', order: 'DESC' },
});
// Bad: Unbounded query
const allOrders = await ductape.database.query({
table: 'orders',
// Could return millions of rows!
});
// Good: Paginated queries
Map<String, Object> pageSize = 20;
Map<String, Object> page = 1;
Map<String, Object> result = ductape.database.query(Map.of(
"table", "orders",
limit: pageSize,
offset: (page - 1) * pageSize,
orderBy: Map.of( "column", "created_at", "order", "DESC" )
));
// Bad: Unbounded query
Map<String, Object> allOrders = ductape.database.query(Map.of(
"table", "orders",
// Could return millions of rows!
));
// Good: Paginated queries
pageSize := 20;
page := 1;
result := client.database.query({
"table": "orders",
limit: pageSize,
offset: (page - 1) * pageSize,
orderBy: { "column": "created_at", "order": "DESC" },
});
// Bad: Unbounded query
allOrders := client.database.query({
"table": "orders",
// Could return millions of rows!
});
// Good: Paginated queries
var pageSize = 20;
var page = 1;
var result = await ductape.database.query({
["table"] = "orders",
limit: pageSize,
offset: (page - 1) * pageSize,
orderBy: { ["column"] = "created_at", ["order"] = "DESC" },
});
// Bad: Unbounded query
var allOrders = await ductape.database.query({
["table"] = "orders",
// Could return millions of rows!
});
Use Count for Totals
Get total counts efficiently without fetching all data:
- TypeScript
- Java
- Go
- .NET
// Get count separately for pagination metadata
const total = await ductape.database.count({
table: 'orders',
where: { status: 'pending' },
});
const orders = await ductape.database.query({
table: 'orders',
where: { status: 'pending' },
limit: 20,
offset: 0,
});
return {
data: orders.data,
total,
totalPages: Math.ceil(total / 20),
};
// Get count separately for pagination metadata
Map<String, Object> total = ductape.database.count(Map.of(
"table", "orders",
where: Map.of( "status", "pending" )
));
Map<String, Object> orders = ductape.database.query(Map.of(
"table", "orders",
where: Map.of( "status", "pending" ),
"limit", 20,
"offset", 0
));
return Map.of(
data: orders.data,
total,
totalPages: Math.ceil(total / 20)
);
// Get count separately for pagination metadata
total := client.database.count({
"table": "orders",
where: { "status": "pending" },
});
orders := client.database.query({
"table": "orders",
where: { "status": "pending" },
"limit": 20,
"offset": 0,
});
return {
data: orders.data,
total,
totalPages: Math.ceil(total / 20),
};
// Get count separately for pagination metadata
var total = await ductape.database.count({
["table"] = "orders",
where: { ["status"] = "pending" },
});
var orders = await ductape.database.query({
["table"] = "orders",
where: { ["status"] = "pending" },
["limit"] = 20,
["offset"] = 0,
});
return {
data: orders.data,
total,
totalPages: Math.ceil(total / 20),
};
Data Integrity
Use Transactions for Related Operations
Always wrap related operations in transactions:
- TypeScript
- Java
- Go
- .NET
// Good: Atomic operations
await ductape.database.transaction({
database: 'main-db',
}, async (transaction) => {
const order = await ductape.database.insert({
table: 'orders',
data: orderData,
transaction,
});
await ductape.database.insert({
table: 'order_items',
data: items.map(item => ({ order_id: order.insertedIds[0], ...item })),
transaction,
});
await ductape.database.update({
table: 'inventory',
data: { stock: { $dec: quantity } },
where: { product_id: productId },
transaction,
});
});
// Good: Atomic operations
ductape.database.transaction(Map.of(
"database", "main-db"
), async (transaction) => Map.of(
Map<String, Object> order = ductape.database.insert(Map.of(
"table", "orders",
data: orderData,
transaction
));
ductape.database.insert(Map.of(
"table", "order_items",
data: items.map(item => (Map.of( order_id: order.insertedIds[0], ...item ))),
transaction
));
ductape.database.update(Map.of(
"table", "inventory",
data: Map.of( stock: Map.of( $dec: quantity ) ),
where: Map.of( product_id: productId ),
transaction
));
));
// Good: Atomic operations
client.database.transaction({
"database": "main-db",
}, async (transaction) => {
order := client.database.insert({
"table": "orders",
data: orderData,
transaction,
});
client.database.insert({
"table": "order_items",
data: items.map(item => ({ order_id: order.insertedIds[0], ...item })),
transaction,
});
client.database.update({
"table": "inventory",
data: { stock: { $dec: quantity } },
where: { product_id: productId },
transaction,
});
});
// Good: Atomic operations
await ductape.database.transaction({
["database"] = "main-db",
}, async (transaction) => {
var order = await ductape.database.insert({
["table"] = "orders",
data: orderData,
transaction,
});
await ductape.database.insert({
["table"] = "order_items",
data: items.map(item => ({ order_id: order.insertedIds[0], ...item })),
transaction,
});
await ductape.database.update({
["table"] = "inventory",
data: { stock: { $dec: quantity } },
where: { product_id: productId },
transaction,
});
});
Validate Input Data
Use Database Actions with validation for user input:
- TypeScript
- Java
- Go
- .NET
// Good: Validated action
const action = await ductape.database.createAction({
tag: 'create-user',
type: DatabaseActionType.INSERT,
table: 'users',
input: {
email: '{{email:EMAIL}}',
name: '{{name:STRING:2:100}}',
age: '{{age:NUMBER:18:120}}',
},
});
// Good: Validated action
Map<String, Object> action = ductape.database.createAction(Map.of(
"tag", "create-user",
type: DatabaseActionType.INSERT,
"table", "users",
input: Map.of(
"email", "Map.of(Map.of(email:EMAIL))",
"name", "Map.of(Map.of(name:"STRING", 2:100))",
"age", "Map.of(Map.of(age:"NUMBER", 18:120))"
)
));
// Good: Validated action
action := client.database.createAction({
"tag": "create-user",
type: DatabaseActionType.INSERT,
"table": "users",
input: {
"email": "{{email:EMAIL}}",
"name": "{{name:"STRING": 2:100}}",
"age": "{{age:"NUMBER": 18:120}}",
},
});
// Good: Validated action
var action = await ductape.database.createAction({
["tag"] = "create-user",
type: DatabaseActionType.INSERT,
["table"] = "users",
input: {
["email"] = "{{email:EMAIL}}",
["name"] = "{{name:["STRING"] = 2:100}}",
["age"] = "{{age:["NUMBER"] = 18:120}}",
},
});
For direct queries, validate before executing:
- TypeScript
- Java
- Go
- .NET
// Validate before insert
function createUser(data: { email: string; name: string }) {
if (!isValidEmail(data.email)) {
throw new Error('Invalid email format');
}
if (data.name.length < 2) {
throw new Error('Name too short');
}
return ductape.database.insert({
table: 'users',
data,
});
}
// Validate before insert
function createUser(data: Map.of( email: string; name: string )) Map.of(
if (!isValidEmail(data.email)) Map.of(
throw new Error('Invalid email format');
)
if (data.name.length < 2) Map.of(
throw new Error('Name too short');
)
return ductape.database.insert(Map.of(
"table", "users",
data
));
)
// Validate before insert
function createUser(data: { email: string; name: string }) {
if (!isValidEmail(data.email)) {
throw new Error('Invalid email format');
}
if (data.name.length < 2) {
throw new Error('Name too short');
}
return client.database.insert({
"table": "users",
data,
});
}
// Validate before insert
function createUser(data: { email: string; name: string }) {
if (!isValidEmail(data.email)) {
throw new Error('Invalid email format');
}
if (data.name.length < 2) {
throw new Error('Name too short');
}
return ductape.database.insert({
["table"] = "users",
data,
});
}
Use Upsert for Idempotent Operations
Prevent duplicate key errors with upsert:
- TypeScript
- Java
- Go
- .NET
// Good: Upsert for settings
await ductape.database.upsert({
table: 'user_settings',
data: {
user_id: userId,
theme: 'dark',
notifications: true,
},
conflictKeys: ['user_id'],
});
// Bad: Insert that fails on duplicate
try {
await ductape.database.insert({
table: 'user_settings',
data: { user_id: userId, theme: 'dark' },
});
} catch (error) {
// Handle duplicate key error
await ductape.database.update({
table: 'user_settings',
data: { theme: 'dark' },
where: { user_id: userId },
});
}
// Good: Upsert for settings
ductape.database.upsert(Map.of(
"table", "user_settings",
data: Map.of(
user_id: userId,
"theme", "dark",
"notifications", true
),
conflictKeys: ['user_id']
));
// Bad: Insert that fails on duplicate
try Map.of(
ductape.database.insert(Map.of(
"table", "user_settings",
data: Map.of( user_id: userId, "theme", "dark" )
));
) catch (error) Map.of(
// Handle duplicate key error
ductape.database.update(Map.of(
"table", "user_settings",
data: Map.of( "theme", "dark" ),
where: Map.of( user_id: userId )
));
)
// Good: Upsert for settings
client.database.upsert({
"table": "user_settings",
data: {
user_id: userId,
"theme": "dark",
"notifications": true,
},
conflictKeys: ['user_id'],
});
// Bad: Insert that fails on duplicate
try {
client.database.insert({
"table": "user_settings",
data: { user_id: userId, "theme": "dark" },
});
} catch (error) {
// Handle duplicate key error
client.database.update({
"table": "user_settings",
data: { "theme": "dark" },
where: { user_id: userId },
});
}
// Good: Upsert for settings
await ductape.database.upsert({
["table"] = "user_settings",
data: {
user_id: userId,
["theme"] = "dark",
["notifications"] = true,
},
conflictKeys: ['user_id'],
});
// Bad: Insert that fails on duplicate
try {
await ductape.database.insert({
["table"] = "user_settings",
data: { user_id: userId, ["theme"] = "dark" },
});
} catch (error) {
// Handle duplicate key error
await ductape.database.update({
["table"] = "user_settings",
data: { ["theme"] = "dark" },
where: { user_id: userId },
});
}
Error Handling
Catch and Handle Specific Errors
- TypeScript
- Java
- Go
- .NET
import { DatabaseError, DatabaseErrorType } from '@ductape/sdk';
async function createUser(data: UserData) {
try {
return await ductape.database.insert({
table: 'users',
data,
});
} catch (error) {
if (error instanceof DatabaseError) {
switch (error.type) {
case DatabaseErrorType.UNIQUE_VIOLATION:
throw new Error('Email already exists');
case DatabaseErrorType.FOREIGN_KEY_VIOLATION:
throw new Error('Invalid reference');
case DatabaseErrorType.CONNECTION_ERROR:
// Log and retry or fail gracefully
console.error('Database connection failed');
throw new Error('Service temporarily unavailable');
default:
throw error;
}
}
throw error;
}
}
import Map.of( DatabaseError, DatabaseErrorType ) from '@ductape/sdk';
async function createUser(data: UserData) Map.of(
try Map.of(
return ductape.database.insert(Map.of(
"table", "users",
data
));
) catch (error) Map.of(
if (error instanceof DatabaseError) Map.of(
switch (error.type) Map.of(
case DatabaseErrorType.UNIQUE_VIOLATION:
throw new Error('Email already exists');
case DatabaseErrorType.FOREIGN_KEY_VIOLATION:
throw new Error('Invalid reference');
case DatabaseErrorType.CONNECTION_ERROR:
// Log and retry or fail gracefully
console.error('Database connection failed');
throw new Error('Service temporarily unavailable');
default:
throw error;
)
)
throw error;
)
)
import { DatabaseError, DatabaseErrorType } from '@ductape/sdk';
async function createUser(data: UserData) {
try {
return client.database.insert({
"table": "users",
data,
});
} catch (error) {
if (error instanceof DatabaseError) {
switch (error.type) {
case DatabaseErrorType.UNIQUE_VIOLATION:
throw new Error('Email already exists');
case DatabaseErrorType.FOREIGN_KEY_VIOLATION:
throw new Error('Invalid reference');
case DatabaseErrorType.CONNECTION_ERROR:
// Log and retry or fail gracefully
console.error('Database connection failed');
throw new Error('Service temporarily unavailable');
default:
throw error;
}
}
throw error;
}
}
import { DatabaseError, DatabaseErrorType } from '@ductape/sdk';
async function createUser(data: UserData) {
try {
return await ductape.database.insert({
["table"] = "users",
data,
});
} catch (error) {
if (error instanceof DatabaseError) {
switch (error.type) {
case DatabaseErrorType.UNIQUE_VIOLATION:
throw new Error('Email already exists');
case DatabaseErrorType.FOREIGN_KEY_VIOLATION:
throw new Error('Invalid reference');
case DatabaseErrorType.CONNECTION_ERROR:
// Log and retry or fail gracefully
console.error('Database connection failed');
throw new Error('Service temporarily unavailable');
default:
throw error;
}
}
throw error;
}
}
Use Savepoints for Partial Failures
When some operations can fail without affecting others:
- TypeScript
- Java
- Go
- .NET
await ductape.database.transaction({ ... }, async (transaction) => {
// Critical: Must succeed
await ductape.database.insert({
table: 'orders',
data: orderData,
transaction,
});
// Optional: Can fail
const savepoint = await transaction.savepoint('notifications');
try {
await ductape.database.insert({
table: 'notifications',
data: notificationData,
transaction,
});
await savepoint.release();
} catch {
await savepoint.rollback();
// Order still created, notification skipped
}
});
ductape.database.transaction(Map.of( ... ), async (transaction) => Map.of(
// Critical: Must succeed
ductape.database.insert(Map.of(
"table", "orders",
data: orderData,
transaction
));
// Optional: Can fail
Map<String, Object> savepoint = transaction.savepoint('notifications');
try Map.of(
ductape.database.insert(Map.of(
"table", "notifications",
data: notificationData,
transaction
));
savepoint.release();
) catch Map.of(
savepoint.rollback();
// Order still created, notification skipped
)
));
client.database.transaction({ ... }, async (transaction) => {
// Critical: Must succeed
client.database.insert({
"table": "orders",
data: orderData,
transaction,
});
// Optional: Can fail
savepoint := transaction.savepoint('notifications');
try {
client.database.insert({
"table": "notifications",
data: notificationData,
transaction,
});
savepoint.release();
} catch {
savepoint.rollback();
// Order still created, notification skipped
}
});
await ductape.database.transaction({ ... }, async (transaction) => {
// Critical: Must succeed
await ductape.database.insert({
["table"] = "orders",
data: orderData,
transaction,
});
// Optional: Can fail
var savepoint = await transaction.savepoint('notifications');
try {
await ductape.database.insert({
["table"] = "notifications",
data: notificationData,
transaction,
});
await savepoint.release();
} catch {
await savepoint.rollback();
// Order still created, notification skipped
}
});
Performance Patterns
Batch Operations
Insert multiple records in a single operation:
- TypeScript
- Java
- Go
- .NET
// Good: Batch insert
await ductape.database.insert({
table: 'logs',
data: logEntries, // Array of records
});
// Bad: Individual inserts
for (const entry of logEntries) {
await ductape.database.insert({
table: 'logs',
data: entry,
});
}
// Good: Batch insert
ductape.database.insert(Map.of(
"table", "logs",
data: logEntries, // Array of records
));
// Bad: Individual inserts
for (Map<String, Object> entry of logEntries) Map.of(
ductape.database.insert(Map.of(
"table", "logs",
data: entry
));
)
// Good: Batch insert
client.database.insert({
"table": "logs",
data: logEntries, // Array of records
});
// Bad: Individual inserts
for (const entry of logEntries) {
client.database.insert({
"table": "logs",
data: entry,
});
}
// Good: Batch insert
await ductape.database.insert({
["table"] = "logs",
data: logEntries, // Array of records
});
// Bad: Individual inserts
for (var entry of logEntries) {
await ductape.database.insert({
["table"] = "logs",
data: entry,
});
}
Use Aggregations Instead of Fetching All Data
- TypeScript
- Java
- Go
- .NET
// Good: Database-side aggregation
const stats = await ductape.database.aggregate({
table: 'orders',
operations: {
total_revenue: { $SUM: 'total' },
order_count: { $COUNT: '*' },
avg_order_value: { $AVG: 'total' },
},
where: { status: 'completed' },
});
// Bad: Fetching all data and calculating in app
const orders = await ductape.database.query({
table: 'orders',
where: { status: 'completed' },
});
const totalRevenue = orders.data.reduce((sum, o) => sum + o.total, 0);
const avgOrderValue = totalRevenue / orders.data.length;
// Good: Database-side aggregation
Map<String, Object> stats = ductape.database.aggregate(Map.of(
"table", "orders",
operations: Map.of(
total_revenue: Map.of( $"SUM", "total" ),
order_count: Map.of( $"COUNT", "*" ),
avg_order_value: Map.of( $"AVG", "total" )
),
where: Map.of( "status", "completed" )
));
// Bad: Fetching all data and calculating in app
Map<String, Object> orders = ductape.database.query(Map.of(
"table", "orders",
where: Map.of( "status", "completed" )
));
Map<String, Object> totalRevenue = orders.data.reduce((sum, o) => sum + o.total, 0);
Map<String, Object> avgOrderValue = totalRevenue / orders.data.length;
// Good: Database-side aggregation
stats := client.database.aggregate({
"table": "orders",
operations: {
total_revenue: { $"SUM": "total" },
order_count: { $"COUNT": "*" },
avg_order_value: { $"AVG": "total" },
},
where: { "status": "completed" },
});
// Bad: Fetching all data and calculating in app
orders := client.database.query({
"table": "orders",
where: { "status": "completed" },
});
totalRevenue := orders.data.reduce((sum, o) => sum + o.total, 0);
avgOrderValue := totalRevenue / orders.data.length;
// Good: Database-side aggregation
var stats = await ductape.database.aggregate({
["table"] = "orders",
operations: {
total_revenue: { $["SUM"] = "total" },
order_count: { $["COUNT"] = "*" },
avg_order_value: { $["AVG"] = "total" },
},
where: { ["status"] = "completed" },
});
// Bad: Fetching all data and calculating in app
var orders = await ductape.database.query({
["table"] = "orders",
where: { ["status"] = "completed" },
});
var totalRevenue = orders.data.reduce((sum, o) => sum + o.total, 0);
var avgOrderValue = totalRevenue / orders.data.length;
Avoid N+1 Queries
Use relationships to fetch related data in fewer queries:
- TypeScript
- Java
- Go
- .NET
// Good: Include relationships
const orders = await ductape.database.query({
table: 'orders',
where: { customer_id: customerId },
include: {
items: {
type: 'one-to-many',
table: 'order_items',
foreignKey: 'order_id',
},
},
});
// Bad: N+1 pattern
const orders = await ductape.database.query({
table: 'orders',
where: { customer_id: customerId },
});
for (const order of orders.data) {
// Separate query for each order!
const items = await ductape.database.query({
table: 'order_items',
where: { order_id: order.id },
});
order.items = items.data;
}
// Good: Include relationships
Map<String, Object> orders = ductape.database.query(Map.of(
"table", "orders",
where: Map.of( customer_id: customerId ),
include: Map.of(
items: Map.of(
"type", "one-to-many",
"table", "order_items",
"foreignKey", "order_id"
)
)
));
// Bad: N+1 pattern
Map<String, Object> orders = ductape.database.query(Map.of(
"table", "orders",
where: Map.of( customer_id: customerId )
));
for (Map<String, Object> order of orders.data) Map.of(
// Separate query for each order!
Map<String, Object> items = ductape.database.query(Map.of(
"table", "order_items",
where: Map.of( order_id: order.id )
));
order.items = items.data;
)
// Good: Include relationships
orders := client.database.query({
"table": "orders",
where: { customer_id: customerId },
include: {
items: {
"type": "one-to-many",
"table": "order_items",
"foreignKey": "order_id",
},
},
});
// Bad: N+1 pattern
orders := client.database.query({
"table": "orders",
where: { customer_id: customerId },
});
for (const order of orders.data) {
// Separate query for each order!
items := client.database.query({
"table": "order_items",
where: { order_id: order.id },
});
order.items = items.data;
}
// Good: Include relationships
var orders = await ductape.database.query({
["table"] = "orders",
where: { customer_id: customerId },
include: {
items: {
["type"] = "one-to-many",
["table"] = "order_items",
["foreignKey"] = "order_id",
},
},
});
// Bad: N+1 pattern
var orders = await ductape.database.query({
["table"] = "orders",
where: { customer_id: customerId },
});
for (var order of orders.data) {
// Separate query for each order!
var items = await ductape.database.query({
["table"] = "order_items",
where: { order_id: order.id },
});
order.items = items.data;
}
Security Practices
Never Expose Raw Database Errors
- TypeScript
- Java
- Go
- .NET
// Good: Sanitized error messages
try {
await ductape.database.insert({ table: 'users', data });
} catch (error) {
console.error('Database error:', error); // Log full error
throw new Error('Failed to create user'); // Return safe message
}
// Bad: Exposing internal details
try {
await ductape.database.insert({ table: 'users', data });
} catch (error) {
throw error; // Exposes table names, column names, etc.
}
// Good: Sanitized error messages
try Map.of(
ductape.database.insert(Map.of( "table", "users", data ));
) catch (error) Map.of(
console.error('Database "error", ", error); // Log full error
throw new Error("Failed to create user'); // Return safe message
)
// Bad: Exposing internal details
try Map.of(
ductape.database.insert(Map.of( "table", "users", data ));
) catch (error) Map.of(
throw error; // Exposes table names, column names, etc.
)
// Good: Sanitized error messages
try {
client.database.insert({ "table": "users", data });
} catch (error) {
console.error('Database "error": ", error); // Log full error
throw new Error("Failed to create user'); // Return safe message
}
// Bad: Exposing internal details
try {
client.database.insert({ "table": "users", data });
} catch (error) {
throw error; // Exposes table names, column names, etc.
}
// Good: Sanitized error messages
try {
await ductape.database.insert({ ["table"] = "users", data });
} catch (error) {
console.error('Database ["error"] = ", error); // Log full error
throw new Error("Failed to create user'); // Return safe message
}
// Bad: Exposing internal details
try {
await ductape.database.insert({ ["table"] = "users", data });
} catch (error) {
throw error; // Exposes table names, column names, etc.
}
Use Parameterized Queries
Raw queries use parameterized inputs by default:
- TypeScript
- Java
- Go
- .NET
// Good: Parameterized (safe)
await ductape.database.raw({
query: 'SELECT * FROM users WHERE email = $1',
params: [userEmail],
});
// Bad: String concatenation (SQL injection risk)
await ductape.database.raw({
query: `SELECT * FROM users WHERE email = '${userEmail}'`, // DANGEROUS!
});
// Good: Parameterized (safe)
ductape.database.raw(Map.of(
"query", "SELECT * FROM users WHERE email = $1",
params: [userEmail]
));
// Bad: String concatenation (SQL injection risk)
ductape.database.raw(Map.of(
query: `SELECT * FROM users WHERE email = '$Map.of(userEmail)'`, // DANGEROUS!
));
// Good: Parameterized (safe)
client.database.raw({
"query": "SELECT * FROM users WHERE email = $1",
params: [userEmail],
});
// Bad: String concatenation (SQL injection risk)
client.database.raw({
query: `SELECT * FROM users WHERE email = '${userEmail}'`, // DANGEROUS!
});
// Good: Parameterized (safe)
await ductape.database.raw({
["query"] = "SELECT * FROM users WHERE email = $1",
params: [userEmail],
});
// Bad: String concatenation (SQL injection risk)
await ductape.database.raw({
query: `SELECT * FROM users WHERE email = '${userEmail}'`, // DANGEROUS!
});
Limit Query Scope
Restrict queries to prevent accidental data exposure:
- TypeScript
- Java
- Go
- .NET
// Good: Always filter by tenant/owner
async function getUserOrders(userId: number, tenantId: number) {
return ductape.database.query({
table: 'orders',
where: {
user_id: userId,
tenant_id: tenantId, // Always include tenant filter
},
});
}
// Good: Always filter by tenant/owner
async function getUserOrders(userId: number, tenantId: number) Map.of(
return ductape.database.query(Map.of(
"table", "orders",
where: Map.of(
user_id: userId,
tenant_id: tenantId, // Always include tenant filter
)
));
)
// Good: Always filter by tenant/owner
async function getUserOrders(userId: number, tenantId: number) {
return client.database.query({
"table": "orders",
where: {
user_id: userId,
tenant_id: tenantId, // Always include tenant filter
},
});
}
// Good: Always filter by tenant/owner
async function getUserOrders(userId: number, tenantId: number) {
return ductape.database.query({
["table"] = "orders",
where: {
user_id: userId,
tenant_id: tenantId, // Always include tenant filter
},
});
}
Transaction Best Practices
Keep Transactions Short
Long transactions hold locks and reduce concurrency:
- TypeScript
- Java
- Go
- .NET
// Good: Short transaction
await ductape.database.transaction({ ... }, async (trx) => {
await ductape.database.insert({ table: 'orders', data, transaction: trx });
await ductape.database.update({ table: 'inventory', data: update, transaction: trx });
});
// Bad: External I/O inside transaction
await ductape.database.transaction({ ... }, async (trx) => {
await ductape.database.insert({ table: 'orders', data, transaction: trx });
await sendEmail(order); // DON'T: Holds transaction open
await callExternalAPI(order); // DON'T: Network latency
});
// Good: Short transaction
ductape.database.transaction(Map.of( ... ), async (trx) => Map.of(
ductape.database.insert(Map.of( "table", "orders", data, transaction: trx ));
ductape.database.update(Map.of( "table", "inventory", data: update, transaction: trx ));
));
// Bad: External I/O inside transaction
ductape.database.transaction(Map.of( ... ), async (trx) => Map.of(
ductape.database.insert(Map.of( "table", "orders", data, transaction: trx ));
sendEmail(order); // DON'T: Holds transaction open
callExternalAPI(order); // DON'T: Network latency
));
// Good: Short transaction
client.database.transaction({ ... }, async (trx) => {
client.database.insert({ "table": "orders", data, transaction: trx });
client.database.update({ "table": "inventory", data: update, transaction: trx });
});
// Bad: External I/O inside transaction
client.database.transaction({ ... }, async (trx) => {
client.database.insert({ "table": "orders", data, transaction: trx });
sendEmail(order); // DON'T: Holds transaction open
callExternalAPI(order); // DON'T: Network latency
});
// Good: Short transaction
await ductape.database.transaction({ ... }, async (trx) => {
await ductape.database.insert({ ["table"] = "orders", data, transaction: trx });
await ductape.database.update({ ["table"] = "inventory", data: update, transaction: trx });
});
// Bad: External I/O inside transaction
await ductape.database.transaction({ ... }, async (trx) => {
await ductape.database.insert({ ["table"] = "orders", data, transaction: trx });
await sendEmail(order); // DON'T: Holds transaction open
await callExternalAPI(order); // DON'T: Network latency
});
Move External Operations Outside
- TypeScript
- Java
- Go
- .NET
// Good: External I/O after transaction
const order = await ductape.database.transaction({ ... }, async (trx) => {
return await ductape.database.insert({ table: 'orders', data, transaction: trx });
});
// After commit - safe to do external operations
await sendEmailNotification(order);
await callExternalAPI(order);
// Good: External I/O after transaction
Map<String, Object> order = ductape.database.transaction(Map.of( ... ), async (trx) => Map.of(
return ductape.database.insert(Map.of( "table", "orders", data, transaction: trx ));
));
// After commit - safe to do external operations
sendEmailNotification(order);
callExternalAPI(order);
// Good: External I/O after transaction
order := client.database.transaction({ ... }, async (trx) => {
return client.database.insert({ "table": "orders", data, transaction: trx });
});
// After commit - safe to do external operations
sendEmailNotification(order);
callExternalAPI(order);
// Good: External I/O after transaction
var order = await ductape.database.transaction({ ... }, async (trx) => {
return await ductape.database.insert({ ["table"] = "orders", data, transaction: trx });
});
// After commit - safe to do external operations
await sendEmailNotification(order);
await callExternalAPI(order);
Choose Appropriate Isolation Levels
| Scenario | Recommended Level |
|---|---|
| Standard CRUD | READ_COMMITTED (default) |
| Report generation | REPEATABLE_READ |
| Financial transactions | SERIALIZABLE |
| High-throughput writes | READ_COMMITTED |
Actions vs Direct Queries
Use Actions For
- Standard CRUD operations with consistent structure
- Operations that need input validation
- Reusable operations across your application
- Operations that benefit from centralized configuration
- TypeScript
- Java
- Go
- .NET
// Action: Validated, reusable
await ductape.database.execute({
action: 'main-db:create-user',
input: { email, name, role },
});
// Action: Validated, reusable
ductape.database.execute(Map.of(
"action", "main-db:create-user",
input: Map.of( email, name, role )
));
// Action: Validated, reusable
client.database.execute({
"action": "main-db:create-user",
input: { email, name, role },
});
// Action: Validated, reusable
await ductape.database.execute({
["action"] = "main-db:create-user",
input: { email, name, role },
});
Use Direct Queries For
- Dynamic queries with variable conditions
- Complex aggregations and analytics
- Ad-hoc data exploration
- Operations not fitting action templates
- TypeScript
- Java
- Go
- .NET
// Direct query: Dynamic filters
const where: any = {};
if (filters.status) where.status = filters.status;
if (filters.minPrice) where.price = { $GTE: filters.minPrice };
await ductape.database.query({
table: 'products',
where: Object.keys(where).length > 0 ? where : undefined,
});
// Direct query: Dynamic filters
Map<String, Object> where: any = Map.of();
if (filters.status) where.status = filters.status;
if (filters.minPrice) where.price = Map.of( $GTE: filters.minPrice );
ductape.database.query(Map.of(
"table", "products",
where: Object.keys(where).length > 0 ? where : undefined
));
// Direct query: Dynamic filters
const where: any = {};
if (filters.status) where.status = filters.status;
if (filters.minPrice) where.price = { $GTE: filters.minPrice };
client.database.query({
"table": "products",
where: Object.keys(where).length > 0 ? where : undefined,
});
// Direct query: Dynamic filters
var where: any = {};
if (filters.status) where.status = filters.status;
if (filters.minPrice) where.price = { $GTE: filters.minPrice };
await ductape.database.query({
["table"] = "products",
where: Object.keys(where).length > 0 ? where : undefined,
});
Database-Specific Tips
PostgreSQL
- Use
SERIALIZABLEisolation for financial operations - Leverage
RETURNINGclause for insert/update results - Use
ARRAY_AGGfor grouping into arrays
MySQL
- Default isolation is
REPEATABLE_READ - Use
GROUP_CONCATfor string aggregation - Be aware of implicit commits with DDL statements
MongoDB
- Transactions require replica set or sharded cluster
- Savepoints are not supported
- Use aggregation pipeline for complex queries
DynamoDB
- Transactions limited to 100 items
- Savepoints not supported
- 2x write cost for transactional writes
Checklist
Before deploying to production, verify:
- Environment-specific database configurations
- Appropriate indexes for frequent queries
- Pagination on all list endpoints
- Transactions for related operations
- Input validation on user-facing operations
- Error handling with sanitized messages
- No N+1 query patterns
- Connection cleanup on shutdown
- Appropriate isolation levels for critical operations
Next Steps
- Database Overview - Start from the beginning
- Transactions - Deep dive into transactions
- Direct Queries - Advanced query patterns