Skip to main content

N/query Module

Complete guide to executing SuiteQL queries in SuiteScript using the N/query module.


Module Overview

The N/query module provides methods to run SuiteQL queries and process results in SuiteScript.

/**
* @NApiVersion 2.1
*/
define(['N/query'], (query) => {
// query module is now available
});

Core Methods

MethodDescriptionReturns
query.runSuiteQL()Execute SuiteQL queryResultSet
query.runSuiteQLPaged()Execute with pagingPagedData
query.create()Build query programmaticallyQuery object

runSuiteQL: Basic Usage

Simple Query

/**
* @NApiVersion 2.1
* @NScriptType Suitelet
*/
define(['N/query'], (query) => {

const onRequest = (context) => {
// Execute simple query
const results = query.runSuiteQL({
query: `
SELECT id, companyname, email
FROM customer
WHERE isinactive = 'F'
LIMIT 10
`
});

// Get all results as array of objects
const customers = results.asMappedResults();

customers.forEach(cust => {
log.debug('Customer', `${cust.companyname} - ${cust.email}`);
});
};

return { onRequest };
});

Result Processing Methods

const resultSet = query.runSuiteQL({ query: sqlString });

// Method 1: As mapped results (array of objects)
const mapped = resultSet.asMappedResults();
// [ { id: 1, companyname: 'Acme', email: 'a@a.com' }, ... ]

// Method 2: Iterate with each()
resultSet.each(result => {
log.debug('Row', result);
return true; // Continue iteration
});

// Method 3: Get column metadata
const columns = resultSet.columns;
columns.forEach(col => {
log.debug('Column', `${col.name} (${col.type})`);
});

Parameterized Queries

Always use parameters to prevent SQL injection.

Test Case: Safe Parameter Usage

/**
* Search customers by name and state
*/
const searchCustomers = (searchName, stateCode) => {
// CORRECT: Use parameterized query
const results = query.runSuiteQL({
query: `
SELECT id, companyname, email, state
FROM customer
WHERE companyname LIKE ?
AND state = ?
AND isinactive = 'F'
ORDER BY companyname
`,
params: [`%${searchName}%`, stateCode]
});

return results.asMappedResults();
};

// Usage
const caCustomers = searchCustomers('Tech', 'CA');

Multiple Parameters Example

/**
* Get transactions in date range for specific customer
*/
const getCustomerOrders = (customerId, startDate, endDate, minAmount) => {
const results = query.runSuiteQL({
query: `
SELECT
t.id,
t.tranid,
t.trandate,
t.amount,
t.status
FROM transaction t
WHERE t.entity = ?
AND t.type = 'SalesOrd'
AND t.trandate BETWEEN TO_DATE(?, 'YYYY-MM-DD')
AND TO_DATE(?, 'YYYY-MM-DD')
AND t.amount >= ?
ORDER BY t.trandate DESC
`,
params: [customerId, startDate, endDate, minAmount]
});

return results.asMappedResults();
};

// Usage
const orders = getCustomerOrders(123, '2024-01-01', '2024-12-31', 1000);

Pagination with runSuiteQLPaged

For large result sets, use paged queries to manage memory and governance.

Test Case: Process Large Dataset

/**
* Process all customers in batches
*/
const processAllCustomers = () => {
const pagedData = query.runSuiteQLPaged({
query: `
SELECT id, companyname, email, balance
FROM customer
WHERE isinactive = 'F'
ORDER BY id
`,
pageSize: 100 // Process 100 at a time
});

log.audit('Total', `${pagedData.count} customers to process`);

let processed = 0;

// Iterate through pages
pagedData.pageRanges.forEach(pageRange => {
const page = pagedData.fetch({ index: pageRange.index });

page.data.asMappedResults().forEach(customer => {
// Process each customer
processCustomer(customer);
processed++;
});

log.debug('Progress', `Processed ${processed} of ${pagedData.count}`);
});

return processed;
};

Paged Query with Iterator

/**
* Alternative: Use iterator pattern
*/
const processWithIterator = () => {
const pagedData = query.runSuiteQLPaged({
query: `SELECT id, companyname FROM customer WHERE isinactive = 'F'`,
pageSize: 50
});

const pageIterator = pagedData.iterator();

let pageNum = 0;
while (pageIterator.hasNext()) {
const page = pageIterator.next();
pageNum++;

log.debug('Page', `Processing page ${pageNum}`);

page.value.data.asMappedResults().forEach(row => {
// Process row
});
}
};

Error Handling

Test Case: Robust Error Handling

/**
* Execute query with proper error handling
*/
const safeQuery = (sqlQuery, params = []) => {
try {
const results = query.runSuiteQL({
query: sqlQuery,
params: params
});

return {
success: true,
data: results.asMappedResults()
};

} catch (e) {
log.error('Query Error', {
query: sqlQuery,
params: params,
error: e.message,
stack: e.stack
});

// Check for specific error types
if (e.message.includes('Invalid search filter')) {
return { success: false, error: 'Invalid column name' };
}

if (e.message.includes('SSS_USAGE_LIMIT_EXCEEDED')) {
return { success: false, error: 'Governance limit reached' };
}

return { success: false, error: e.message };
}
};

// Usage
const result = safeQuery(
'SELECT id, companyname FROM customer WHERE id = ?',
[123]
);

if (result.success) {
log.debug('Data', result.data);
} else {
log.error('Failed', result.error);
}

Complete Script Examples

Example 1: Suitelet with SuiteQL Report

/**
* @NApiVersion 2.1
* @NScriptType Suitelet
*/
define(['N/query', 'N/ui/serverWidget'], (query, serverWidget) => {

const onRequest = (context) => {
if (context.request.method === 'GET') {
showForm(context);
} else {
showResults(context);
}
};

const showForm = (context) => {
const form = serverWidget.createForm({ title: 'Customer Report' });

form.addField({
id: 'custpage_state',
type: serverWidget.FieldType.SELECT,
label: 'State',
source: 'state'
});

form.addField({
id: 'custpage_min_balance',
type: serverWidget.FieldType.CURRENCY,
label: 'Minimum Balance'
});

form.addSubmitButton({ label: 'Run Report' });

context.response.writePage(form);
};

const showResults = (context) => {
const state = context.request.parameters.custpage_state;
const minBalance = parseFloat(context.request.parameters.custpage_min_balance) || 0;

// Build dynamic query
let sql = `
SELECT id, companyname, email, state, balance
FROM customer
WHERE isinactive = 'F'
AND balance >= ?
`;
const params = [minBalance];

if (state) {
sql += ` AND state = ?`;
params.push(state);
}

sql += ` ORDER BY balance DESC LIMIT 100`;

// Execute query
const results = query.runSuiteQL({
query: sql,
params: params
}).asMappedResults();

// Build output form
const form = serverWidget.createForm({ title: 'Customer Report Results' });

const sublist = form.addSublist({
id: 'custpage_results',
type: serverWidget.SublistType.LIST,
label: `Results (${results.length} customers)`
});

sublist.addField({ id: 'custpage_id', type: serverWidget.FieldType.TEXT, label: 'ID' });
sublist.addField({ id: 'custpage_name', type: serverWidget.FieldType.TEXT, label: 'Company' });
sublist.addField({ id: 'custpage_email', type: serverWidget.FieldType.TEXT, label: 'Email' });
sublist.addField({ id: 'custpage_state', type: serverWidget.FieldType.TEXT, label: 'State' });
sublist.addField({ id: 'custpage_balance', type: serverWidget.FieldType.CURRENCY, label: 'Balance' });

results.forEach((row, idx) => {
sublist.setSublistValue({ id: 'custpage_id', line: idx, value: row.id });
sublist.setSublistValue({ id: 'custpage_name', line: idx, value: row.companyname || '' });
sublist.setSublistValue({ id: 'custpage_email', line: idx, value: row.email || '' });
sublist.setSublistValue({ id: 'custpage_state', line: idx, value: row.state || '' });
sublist.setSublistValue({ id: 'custpage_balance', line: idx, value: row.balance || 0 });
});

form.addButton({ id: 'custpage_back', label: 'Back', functionName: 'history.back()' });

context.response.writePage(form);
};

return { onRequest };
});

Example 2: Map/Reduce with SuiteQL

/**
* @NApiVersion 2.1
* @NScriptType MapReduceScript
*/
define(['N/query', 'N/record', 'N/email', 'N/runtime'], (query, record, email, runtime) => {

const getInputData = () => {
// Use SuiteQL to get records to process
return query.runSuiteQL({
query: `
SELECT
c.id,
c.companyname,
c.email,
SUM(t.amountremaining) AS total_ar
FROM customer c
INNER JOIN transaction t ON c.id = t.entity
WHERE t.type = 'CustInvc'
AND t.amountremaining > 0
AND c.isinactive = 'F'
GROUP BY c.id, c.companyname, c.email
HAVING SUM(t.amountremaining) > 10000
`
}).asMappedResults();
};

const map = (context) => {
const customer = JSON.parse(context.value);

log.debug('Processing', customer.companyname);

// Get detailed invoice list
const invoices = query.runSuiteQL({
query: `
SELECT tranid, trandate, amountremaining, duedate
FROM transaction
WHERE entity = ?
AND type = 'CustInvc'
AND amountremaining > 0
ORDER BY duedate
`,
params: [customer.id]
}).asMappedResults();

context.write({
key: customer.id,
value: {
customer: customer,
invoices: invoices
}
});
};

const reduce = (context) => {
const data = JSON.parse(context.values[0]);

// Send reminder email
// ... email logic ...

context.write({
key: context.key,
value: { sent: true, customer: data.customer.companyname }
});
};

const summarize = (context) => {
let sent = 0;
context.output.iterator().each((key, value) => {
sent++;
return true;
});

log.audit('Complete', `Sent ${sent} reminder emails`);
};

return { getInputData, map, reduce, summarize };
});

Example 3: RESTlet with SuiteQL

/**
* @NApiVersion 2.1
* @NScriptType Restlet
*/
define(['N/query'], (query) => {

const get = (params) => {
try {
// Validate parameters
if (!params.type) {
return { error: 'Missing required parameter: type' };
}

const allowedTypes = ['SalesOrd', 'CustInvc', 'PurchOrd'];
if (!allowedTypes.includes(params.type)) {
return { error: 'Invalid transaction type' };
}

// Build query
let sql = `
SELECT
t.id,
t.tranid,
t.trandate,
t.amount,
t.status,
c.companyname AS customer_name
FROM transaction t
LEFT JOIN customer c ON t.entity = c.id
WHERE t.type = ?
`;
const queryParams = [params.type];

// Optional filters
if (params.startDate) {
sql += ` AND t.trandate >= TO_DATE(?, 'YYYY-MM-DD')`;
queryParams.push(params.startDate);
}

if (params.endDate) {
sql += ` AND t.trandate <= TO_DATE(?, 'YYYY-MM-DD')`;
queryParams.push(params.endDate);
}

if (params.customerId) {
sql += ` AND t.entity = ?`;
queryParams.push(parseInt(params.customerId));
}

sql += ` ORDER BY t.trandate DESC LIMIT ?`;
queryParams.push(parseInt(params.limit) || 100);

// Execute
const results = query.runSuiteQL({
query: sql,
params: queryParams
}).asMappedResults();

return {
success: true,
count: results.length,
data: results
};

} catch (e) {
log.error('RESTlet Error', e.message);
return {
success: false,
error: e.message
};
}
};

return { get };
});

Governance Considerations

Query Governance Cost

OperationGovernance Units
runSuiteQL10 units
runSuiteQLPaged10 units + 5 per page fetch

Test Case: Monitor Governance

/**
* Track governance usage
*/
const runQueryWithGovernance = (sql, params) => {
const script = runtime.getCurrentScript();
const startUsage = script.getRemainingUsage();

const results = query.runSuiteQL({
query: sql,
params: params
}).asMappedResults();

const endUsage = script.getRemainingUsage();
const used = startUsage - endUsage;

log.debug('Governance', `Query used ${used} units, ${endUsage} remaining`);

return results;
};

Best Practices

Do's

// DO: Use parameterized queries
query.runSuiteQL({
query: 'SELECT * FROM customer WHERE id = ?',
params: [customerId]
});

// DO: Select only needed columns
query.runSuiteQL({
query: 'SELECT id, companyname FROM customer' // Not SELECT *
});

// DO: Use LIMIT
query.runSuiteQL({
query: 'SELECT id FROM transaction ORDER BY id LIMIT 1000'
});

// DO: Handle errors
try {
const results = query.runSuiteQL({ query: sql });
} catch (e) {
log.error('Query failed', e.message);
}

Don'ts

// DON'T: Concatenate user input (SQL injection!)
query.runSuiteQL({
query: 'SELECT * FROM customer WHERE name = \'' + userName + '\'' // WRONG!
});

// DON'T: Query inside loops
records.forEach(rec => {
query.runSuiteQL({ query: '...' }); // WRONG - query once outside loop
});

// DON'T: Ignore pagination for large sets
const all = query.runSuiteQL({
query: 'SELECT * FROM transactionline' // WRONG - could be millions
});

Quick Reference

// Basic query
query.runSuiteQL({ query: 'SELECT ...', params: [] }).asMappedResults();

// Paged query
const paged = query.runSuiteQLPaged({ query: 'SELECT ...', pageSize: 100 });
paged.pageRanges.forEach(range => {
const page = paged.fetch({ index: range.index });
page.data.asMappedResults().forEach(row => { /* process */ });
});

// With iterator
resultSet.each(row => { /* process */ return true; });