9.10 SQL Rules
This extension depends on the SQL executor, provided by dataql-sqlproc. Configure Dataway data sources first.
SqlRule implements custom @{ruleName, expression, content} behavior, such as shared filters and argument transformations. Rules run before SQL execution and append SQL and bound values to SqlBuilder.
Implement a fixed condition
Encapsulate the active-record filter as manualEnabled:
package com.example.sql;
import net.hasor.dataql.sqlproc.dynamic.QueryContext;
import net.hasor.dataql.sqlproc.dynamic.SqlBuilder;
import net.hasor.dataql.sqlproc.dynamic.rule.SqlRule;
import net.hasor.dataql.sqlproc.types.SqlArgSource;
public class EnabledRule implements SqlRule {
@Override
public boolean test(SqlArgSource data, QueryContext context, String expression) {
return true;
}
@Override
public void executeRule(SqlArgSource data, QueryContext context,
SqlBuilder sql, String expression, String value) {
sql.appendSql("enabled = 1");
}
}
executeRule runs only if test returns true. Returning false omits the entire rule. This rule has no business arguments and always emits a fixed condition.
Register in Dataway
Register rules through the application's DatawayConfig. The framework integration creates Dataway. Here connectionProvider is the application's configured SQL connection provider:
import com.example.sql.EnabledRule;
import net.hasor.dataql.sqlproc.dynamic.rule.RuleRegistry;
import net.hasor.dataql.sqlproc.execute.support.ConnectionProvider;
import net.hasor.dataway.service.DatawayConfig;
public class SqlConfiguration {
public DatawayConfig datawayConfig(ConnectionProvider connectionProvider) {
DatawayConfig config = new DatawayConfig();
config.attachment(ConnectionProvider.class, connectionProvider);
config.configureHost(host -> {
RuleRegistry.DEFAULT.register("manualEnabled", new EnabledRule());
});
return config;
}
}
Expose this configuration through the Spring, Solon or Hasor integration. For an existing DatawayConfig, add the configureHost callback. With a standalone DataQL engine, call the same RuleRegistry.DEFAULT.register(...) before creating queries.
RuleRegistry.DEFAULT is shared. Register once during application startup. Names are case-insensitive; registering the same name replaces the previous rule. Use application-specific names and stateless or thread-safe implementations. Do not store per-request data in rule fields.
Invoke from a script
Initialize the people table from SQL Execution, then run:
var find = @@selectSql()<%
SELECT count(*) FROM people WHERE @{manualEnabled}
%>;
return find();
This generates SELECT count(*) FROM people WHERE enabled = 1. In Dataway, a SQL-type API accepts the SQL body directly; a DataQL-type API accepts the complete script. See Dynamic Rules for built-in rules.
Generate bound values
The appEquals rule binds a named argument to an allowed column. Column names come from a fixed application allowlist, while values always use JDBC placeholders.
package com.example.sql;
import java.sql.SQLException;
import java.util.Set;
import net.hasor.dataql.sqlproc.dynamic.QueryContext;
import net.hasor.dataql.sqlproc.dynamic.SqlBuilder;
import net.hasor.dataql.sqlproc.dynamic.rule.SqlRule;
import net.hasor.dataql.sqlproc.types.SqlArgSource;
public class EqualsRule implements SqlRule {
private static final Set<String> COLUMNS = Set.of("id", "name", "age");
@Override
public boolean test(SqlArgSource data, QueryContext context, String expression) {
return true;
}
@Override
public void executeRule(SqlArgSource data, QueryContext context,
SqlBuilder sql, String expression, String value) throws SQLException {
String parameter = expression == null ? "" : expression.trim();
String column = value == null ? "" : value.trim();
if (!COLUMNS.contains(column)) {
throw new SQLException("Unsupported filter column: " + column);
}
if (!data.hasValue(parameter)) {
throw new SQLException("Missing filter parameter: " + parameter);
}
Object parameterValue = data.getValue(parameter);
if (parameterValue == null) {
sql.appendSql(column + " IS NULL");
} else {
sql.appendSql(column + " = ?", parameterValue);
}
}
}
Add this registration to the same initialization callback:
RuleRegistry.DEFAULT.register("appEquals", new EqualsRule());
Import com.example.sql.EqualsRule. The second rule section names the argument and the third names the column:
hint FRAGMENT_SQL_OPEN_PACKAGE = 'off';
hint FRAGMENT_SQL_COLUMN_CASE = 'lower';
var find = @@selectSql(searchName)<%
SELECT id, name FROM people WHERE @{appEquals, searchName, name}
%>;
return find('Alice');
It produces WHERE name = ? with ["Alice"]. A null input produces WHERE name IS NULL without an argument. Quotes inside argument values remain data passed to JDBC.
Interface and parsing contract
test(data, context, expression): decides whether to run the rule. The rule defines how expression is interpreted; OGNL evaluation is not automatic.executeRule(data, context, sql, expression, value): appends SQL and arguments; may throwSQLException.SqlArgSource: supplies current fragment arguments. The example reads by name. For expressions, useOgnlUtils.evalOgnl(expression, data).QueryContext: finds rules, shared SQL fragments, type handlers and the class loader.SqlBuilder.appendSql(text, args...): appends SQL and arguments. Each appended?must correspond to one argument.
The parser uses the first top-level comma to separate the rule name and the second to separate the expression. Remaining content becomes value. Commas inside quotes or nested rules do not split these sections. For @{appEquals, searchName, name}, the expression is searchName and the value is name.
To parse inner parameters and rules, call this inside executeRule:
DynamicParsed.getParsedSql(value).buildQuery(data, context, sql);
Import net.hasor.dataql.sqlproc.dynamic.segment.DynamicParsed. Calling appendSql(value) instead appends literal text without parsing; choose the behavior appropriate to the rule. See Nested Rules.