Skip to content

SQL Helper Functions Guide ​

This document provides detailed information about SQL helper functions provided by Foggy Dataset for safely building dynamic SQL queries in FSScript.

1. Overview ​

1.1 Why Do We Need SQL Helper Functions? ​

In dynamic SQL concatenation, a common security vulnerability is SQL injection attacks. Traditional string concatenation is susceptible to injection attacks:

javascript
// ❌ Unsafe approach
let sql = `SELECT * FROM users WHERE name = '${userName}'`;
// If userName = "admin' OR '1'='1", it will cause SQL injection

SQL helper functions provided by Foggy Dataset solve this problem through PreparedStatement parameterized queries:

javascript
// ✅ Safe approach
let sql = `SELECT * FROM users WHERE 1=1
    ${sqlExp(userName, 'AND name = ?')}
`;
// userName will be safely added to PreparedStatement parameter list

1.2 How It Works ​

SQL helper functions work with QueryExpEvaluator:

  1. When FSScript script executes, use QueryExpEvaluator instead of regular DefaultExpEvaluator
  2. SQL helper functions (like sqlExp) add parameter values to QueryExpEvaluator's parameter list
  3. Return SQL fragment containing ? placeholders
  4. Final generated SQL and parameter list are passed to JDBC PreparedStatement
FSScript template string
        ↓
QueryExpEvaluator execution
        ↓
SQL helper functions collect parameters
        ↓
Generate SQL + parameter list
        ↓
PreparedStatement execution

2. QueryExpEvaluator ​

2.1 Creating QueryExpEvaluator ​

QueryExpEvaluator is an expression evaluator specifically designed for SQL queries:

java
import com.foggyframework.dataset.model.QueryExpEvaluator;
import org.springframework.context.ApplicationContext;

// Create QueryExpEvaluator with Spring context
QueryExpEvaluator evaluator = QueryExpEvaluator.newInstance(applicationContext);

// Execute FSScript
Exp exp = parser.compileEl(sqlScript);
String sql = (String) exp.evalResult(evaluator);

// Get parameter list
List<Object> args = evaluator.getArgs();

// Use JdbcTemplate to execute query
List<Map<String, Object>> results = jdbcTemplate.queryForList(sql, args.toArray());

2.2 QueryExpEvaluator Features ​

In addition to collecting SQL parameters, QueryExpEvaluator also supports:

  • Pagination parameters: start, limit
  • Query configuration: QueryConfig
  • Result mapping: beanCls
  • Return total count: returnTotal
java
QueryExpEvaluator evaluator = QueryExpEvaluator.newInstance(applicationContext);

// Set pagination
evaluator.setStart(0);
evaluator.setLimit(20);

// Set result mapping class
evaluator.setBeanCls(UserDTO.class);

// Whether to return total record count
evaluator.setReturnTotal(true);

3. Core Functions ​

3.1 sqlExp - Conditional Parameter ​

Signature: sqlExp(value, sqlFragment[, force])

Function: When value is not null, add it to parameter list and return SQL fragment; otherwise return empty string.

Parameters:

ParameterTypeRequiredDescription
valueanyYesParameter value, can be any type
sqlFragmentStringYesSQL fragment, use ? as placeholder
forceBooleanNoWhether to force add even if value is null (default false)

Examples:

javascript
// Basic usage
${sqlExp(form.param.teamId, 'AND team_id = ?')}
// If teamId has value, returns: "AND team_id = ?", and adds parameter
// If teamId is null, returns: ""

// Multiple placeholders
${sqlExp(form.param.startTime, 'AND ? <= create_time')}
${sqlExp(form.param.endTime, 'AND create_time < ?')}

// Force mode
${sqlExp(form.param.status, 'AND status = ?', true)}
// Even if status is null, will return SQL fragment and add null parameter

3.2 sqlInExp - IN Query ​

Signature: sqlInExp(array, sqlPrefix[, force])

Function: Handle IN queries, automatically generate multiple placeholders.

Parameters:

ParameterTypeRequiredDescription
arrayArray/CollectionYesArray or collection
sqlPrefixStringYesSQL prefix, such as "AND status IN "
forceBooleanNoWhether to force add (default false)

Examples:

javascript
// Array parameter
${sqlInExp(form.param.statusList, 'AND status IN ')}
// If statusList = [10, 20, 30], returns: "AND status IN (?,?,?)"
// And adds 3 parameters: 10, 20, 30

// Empty array handling
${sqlInExp([], 'AND status IN ')}
// Returns: ""

// Force mode - empty array
${sqlInExp([], 'AND status IN ', true)}
// Returns: "AND status IN (1) and 1=2"  (ensures query returns no results)

Notes:

  • Automatically handles Collection, Object[] and single values
  • When array is empty, by default returns empty string (doesn't add condition)
  • When force=true, empty array returns always-false condition

3.3 toLikeStr / str2Like - Fuzzy Query (Both Sides) ​

Signature: toLikeStr(str) or str2Like(str)

Function: Add % on both sides of string for LIKE fuzzy query.

Examples:

javascript
${sqlExp(toLikeStr(form.param.userName), 'AND name LIKE ?')}
// If userName = "张三", generates parameter: "%张三%"
// SQL: "AND name LIKE ?"

3.4 toLikeStrL - Fuzzy Query (Left Side) ​

Signature: toLikeStrL(str)

Function: Add % on left side of string for suffix matching.

Examples:

javascript
${sqlExp(toLikeStrL(form.param.email), 'AND email LIKE ?')}
// If email = "gmail.com", generates parameter: "%gmail.com"
// Matches all emails ending with gmail.com

3.5 toLikeStrR - Fuzzy Query (Right Side) ​

Signature: toLikeStrR(str)

Function: Add % on right side of string for prefix matching.

Examples:

javascript
${sqlExp(toLikeStrR(form.param.mobile), 'AND mobile LIKE ?')}
// If mobile = "138", generates parameter: "138%"
// Matches all mobile numbers starting with 138

3.6 iif - Conditional Expression ​

Signature: iif(condition, trueValue, falseValue)

Function: Ternary expression, returns different values based on condition.

Examples:

javascript
// Simple condition
${iif(
    form.param.includeDeleted,
    '',
    'AND deleted = 0'
)}

// Complex condition concatenation
${iif(
    form.param.tranChannel == 'OFFLINE',
    "AND (tran_channel='OFFLINE' OR tran_channel='INNER' OR tran_channel IS NULL)",
    sqlExp(form.param.tranChannel, 'AND tran_channel = ?')
)}

// Range query
${iif(
    form.param.priceRange?.length == 2,
    sqlExp(form.param.priceRange[0], 'AND price >= ?') +
    sqlExp(form.param.priceRange[1], 'AND price <= ?'),
    ''
)}

4. Complete Examples ​

4.1 Simple Query Example ​

javascript
// user_query.fsscript
import {getCurrentUser} from '@authService';

export const sql = `
    SELECT
        id,
        name,
        email,
        status,
        create_time
    FROM users
    WHERE tenant_id = '${getCurrentUser().tenantId}'
        ${sqlExp(form.param.keyword, 'AND name LIKE ?', toLikeStr)}
        ${sqlInExp(form.param.statusList, 'AND status IN ')}
        ${sqlExp(form.param.startDate, 'AND create_time >= ?')}
    ORDER BY create_time DESC
`;
java
// Java calling code
@Service
public class UserQueryService {

    @Autowired
    private ApplicationContext applicationContext;

    @Autowired
    private JdbcTemplate jdbcTemplate;

    public List<Map<String, Object>> queryUsers(Map<String, Object> params) {
        // Load FSScript
        String scriptPath = "classpath:scripts/user_query.fsscript";
        String scriptContent = loadScript(scriptPath);

        // Compile script
        ExpParser parser = new ExpParser();
        Exp exp = parser.compileEl(scriptContent);

        // Create query evaluator
        QueryExpEvaluator evaluator = QueryExpEvaluator.newInstance(applicationContext);

        // Set parameters
        Map<String, Object> form = new HashMap<>();
        form.put("param", params);
        evaluator.setVar("form", form);

        // Execute script to generate SQL
        String sql = (String) exp.evalResult(evaluator);

        // Get parameter list
        List<Object> args = evaluator.getArgs();

        // Execute query
        return jdbcTemplate.queryForList(sql, args.toArray());
    }
}

4.2 Complex Query Example ​

javascript
// order_detail_query.fsscript
import {workonSessionTokenUsingCache as token} from '@saasBasicWebUtils';

/**
 * Order detail query
 * Supports multi-table JOIN, complex conditions, pagination
 */
export const sql = `
    SELECT
        o.order_id,
        o.order_no,
        o.amount,
        o.status,
        o.create_time,
        c.customer_name,
        c.customer_mobile,
        t.team_name,
        p.product_name
    FROM orders o
    LEFT JOIN customers c ON o.customer_id = c.id
    LEFT JOIN teams t ON o.team_id = t.id
    LEFT JOIN order_items oi ON o.order_id = oi.order_id
    LEFT JOIN products p ON oi.product_id = p.id
    WHERE o.tenant_id = '${token.tenantId}'
        AND o.deleted = 0
        ${sqlExp(form.param.teamId, 'AND o.team_id = ?')}
        ${sqlInExp(form.param.statusList, 'AND o.status IN ')}
        ${sqlExp(form.param.orderNo, 'AND o.order_no LIKE ?', toLikeStr)}
        ${sqlExp(form.param.customerMobile, 'AND c.customer_mobile LIKE ?', toLikeStrR)}
        ${sqlExp(form.param.startTime, 'AND ? <= o.create_time')}
        ${sqlExp(form.param.endTime, 'AND o.create_time < ?')}
        ${iif(
            form.param.amountRange?.length == 2,
            sqlExp(form.param.amountRange[0], 'AND o.amount >= ?') +
            sqlExp(form.param.amountRange[1], 'AND o.amount <= ?'),
            ''
        )}
        ${iif(
            form.param.paymentMethod == 'CASH',
            "AND (o.payment_method = 'CASH' OR o.payment_method IS NULL)",
            sqlExp(form.param.paymentMethod, 'AND o.payment_method = ?')
        )}
    ORDER BY o.create_time DESC
`;

4.3 Query with Pagination ​

java
@Service
public class OrderQueryService {

    public PageResult<Order> queryOrdersWithPaging(QueryParams params) {
        ExpParser parser = new ExpParser();
        Exp exp = parser.compileEl(scriptContent);

        QueryExpEvaluator evaluator = QueryExpEvaluator.newInstance(applicationContext);

        // Set pagination parameters
        evaluator.setStart(params.getStart());
        evaluator.setLimit(params.getLimit());
        evaluator.setReturnTotal(true);

        // Set query parameters
        Map<String, Object> form = new HashMap<>();
        form.put("param", params.getConditions());
        evaluator.setVar("form", form);

        // Generate SQL
        String sql = (String) exp.evalResult(evaluator);
        List<Object> args = evaluator.getArgs();

        // Query total count
        String countSql = "SELECT COUNT(*) FROM (" + sql + ") tmp";
        Long total = jdbcTemplate.queryForObject(countSql, Long.class, args.toArray());

        // Query paginated data
        String pageSql = sql + " LIMIT ? OFFSET ?";
        List<Object> pageArgs = new ArrayList<>(args);
        pageArgs.add(evaluator.getLimit());
        pageArgs.add(evaluator.getStart());

        List<Order> data = jdbcTemplate.query(pageSql,
            new BeanPropertyRowMapper<>(Order.class),
            pageArgs.toArray());

        return new PageResult<>(data, total, params.getStart(), params.getLimit());
    }
}

5. Best Practices ​

5.1 Security ​

Always use SQL helper functions

javascript
// ✅ Correct
${sqlExp(form.param.userId, 'AND user_id = ?')}

// ❌ Wrong - SQL injection risk
AND user_id = '${form.param.userId}'

Don't concatenate variables in SQL fragments

javascript
// ✅ Correct
${sqlExp(form.param.fieldName, 'AND field_name = ?')}

// ❌ Wrong - Still has injection risk
${sqlExp(form.param.value, `AND ${form.param.field} = ?`)}

5.2 Performance Optimization ​

Avoid using SQL helper functions in loops

javascript
// ❌ Poor performance
let conditions = '';
for (let field of form.param.fields) {
    conditions += sqlExp(field.value, `AND ${field.name} = ?`);
}

// ✅ Use sqlInExp or pre-process instead

Use indexes reasonably

javascript
// ✅ Use prefix matching (can use index)
${sqlExp(toLikeStrR(form.param.mobile), 'AND mobile LIKE ?')}

// ⚠️ Left-side fuzzy matching cannot use index
${sqlExp(toLikeStrL(form.param.email), 'AND email LIKE ?')}

5.3 Readability ​

Use comments to explain complex logic

javascript
/**
 * Special channel handling:
 * OFFLINE channel includes OFFLINE/INNER/NULL three cases
 */
${iif(
    form.param.channel == 'OFFLINE',
    "AND (channel='OFFLINE' OR channel='INNER' OR channel IS NULL)",
    sqlExp(form.param.channel, 'AND channel = ?')
)}

Extract complex conditions as variables

javascript
let hasDateRange = form.param.startDate && form.param.endDate;
let hasPriceRange = form.param.priceRange?.length == 2;

export const sql = `
    SELECT * FROM products
    WHERE 1=1
        ${iif(hasDateRange,
            sqlExp(form.param.startDate, 'AND create_time >= ?') +
            sqlExp(form.param.endDate, 'AND create_time <= ?'),
            ''
        )}
        ${iif(hasPriceRange,
            sqlExp(form.param.priceRange[0], 'AND price >= ?') +
            sqlExp(form.param.priceRange[1], 'AND price <= ?'),
            ''
        )}
`;

5.4 Testing ​

java
@Test
public void testSqlGeneration() {
    // Prepare test data
    Map<String, Object> params = new HashMap<>();
    params.put("teamId", "team001");
    params.put("statusList", Arrays.asList(10, 20, 30));

    Map<String, Object> form = Map.of("param", params);

    // Execute script
    QueryExpEvaluator evaluator = QueryExpEvaluator.newInstance(applicationContext);
    evaluator.setVar("form", form);

    String sql = (String) exp.evalResult(evaluator);
    List<Object> args = evaluator.getArgs();

    // Verify results
    assertThat(sql).contains("AND team_id = ?");
    assertThat(sql).contains("AND status IN (?,?,?)");
    assertThat(args).hasSize(4);
    assertThat(args.get(0)).isEqualTo("team001");
}

6. Function Summary Table ​

FunctionFunctionPurpose
sqlExp(value, sql[, force])Conditional parameterSingle value condition query
sqlInExp(array, sql[, force])IN queryMulti-value IN query
toLikeStr(str)Both sides fuzzyLIKE '%value%'
toLikeStrL(str)Left side fuzzyLIKE '%value'
toLikeStrR(str)Right side fuzzyLIKE 'value%'
iif(cond, true, false)Conditional expressionComplex condition concatenation

7. Common Questions ​

Q: What's the difference between sqlExp and direct string concatenation?

A: sqlExp uses PreparedStatement parameterized queries to prevent SQL injection; string concatenation has injection risk.

Q: When do I need to use the force parameter?

A: Use when you need to explicitly pass null value to database, or in certain special business scenarios where you need to keep the condition.

Q: Can sqlInExp handle empty arrays?

A: Yes. By default empty arrays return empty string (doesn't add condition); when force=true, returns always-false condition.

Q: How to use database functions in FSScript?

A: Use directly in SQL fragments:

javascript
${sqlExp(form.param.date, 'AND DATE(create_time) = ?')}
AND YEAR(create_time) = 2024

Q: Is dynamic table name supported?

A: Dynamic table names are not recommended due to security risks. If necessary, validate through whitelist:

javascript
// Java side validation
String tableName = validateTableName(params.get("tableName"));
evaluator.setVar("tableName", tableName);

// Use in FSScript
SELECT * FROM ${tableName} WHERE ...