Skip to content

Commit 9368a15

Browse files
authored
[feature](fe) Add partition filter sql block rule (#62196)
Today SQL_BLOCK_RULE can block SQL by regex/sqlHash or by scan scale thresholds such as `partition_num`, `tablet_num`, and `cardinality`, but it cannot directly reject queries that scan a partitioned table without using any partition filter. This PR adds a new scan-based SQL block rule option: `require_partition_filter`. When this option is enabled on a rule, Doris rejects scans on supported partitioned tables if the query does not contain any usable partition predicate. This helps prevent accidental full partition scans caused by missing partition filters. The rule is intended for workloads where partition filters are mandatory for safety or cost control. ### User-visible behavior Users can now create a SQL block rule like this: ```sql CREATE SQL_BLOCK_RULE require_partition_filter_rule PROPERTIES( "require_partition_filter" = "true", "global" = "true", "enable" = "true" ); Or bind it to specific users: CREATE SQL_BLOCK_RULE require_partition_filter_rule PROPERTIES( "require_partition_filter" = "true", "global" = "false", "enable" = "true" ); SET PROPERTY FOR 'test_user' 'sql_block_rules' = 'require_partition_filter_rule'; If a supported partitioned table is scanned without any partition predicate, Doris returns an error like: sql hits sql block rule: require_partition_filter_rule, missing partition filter ### Scope This new rule currently applies to: - Partitioned internal tables - Partitioned Hive external tables This rule does not apply to: - Non-partitioned internal tables - Non-partitioned Hive tables - Iceberg tables - Other external table types not wired into this rule yet ### Rule semantics The new property is: - require_partition_filter = true|false Behavior: - The rule only takes effect when require_partition_filter=true - For supported partitioned tables, the scan is allowed if the query hits any partition column in partition pruning predicates - Filters on non-partition columns do not count - The rule applies to scan-producing statements, such as: - SELECT - INSERT INTO ... SELECT ... - EXPLAIN is not blocked Examples: Blocked: SELECT * FROM part_tbl; SELECT * FROM part_tbl WHERE non_partition_col = 1; INSERT INTO dst SELECT * FROM part_tbl; Allowed: SELECT * FROM part_tbl WHERE dt = '2026-04-09'; SELECT * FROM part_tbl WHERE dt = '2026-04-09' AND hh = '10'; INSERT INTO dst SELECT * FROM part_tbl WHERE dt = '2026-04-09'; ### Compatibility and validation require_partition_filter is treated as a scan-based SQL block condition. It can be used together with existing scan-based limits such as: - partition_num - tablet_num - cardinality It cannot be mixed with text-based block conditions such as: - sql - sqlHash
1 parent 6d7ba6a commit 9368a15

38 files changed

Lines changed: 1303 additions & 328 deletions

be/src/information_schema/schema_sql_block_rule_status_scanner.cpp

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@
3232

3333
namespace doris {
3434

35+
namespace {
36+
constexpr size_t LEGACY_SQL_BLOCK_RULE_STATUS_COLUMN_SIZE = 12;
37+
constexpr size_t REQUIRE_PARTITION_FILTER_INDEX = 8;
38+
} // namespace
39+
3540
std::vector<SchemaScanner::ColumnDesc>
3641
SchemaSqlBlockRuleStatusScanner::_s_sql_block_rule_status_columns = {
3742
{"NAME", TYPE_STRING, sizeof(StringRef), true},
@@ -42,6 +47,7 @@ std::vector<SchemaScanner::ColumnDesc>
4247
{"CARDINALITY", TYPE_BIGINT, sizeof(int64_t), true},
4348
{"GLOBAL", TYPE_BOOLEAN, sizeof(bool), true},
4449
{"ENABLE", TYPE_BOOLEAN, sizeof(bool), true},
50+
{"REQUIRE_PARTITION_FILTER", TYPE_BOOLEAN, sizeof(bool), true},
4551
{"BLOCKS", TYPE_BIGINT, sizeof(int64_t), true},
4652
{"AVERAGE_DURATION", TYPE_BIGINT, sizeof(int64_t), true},
4753
{"LONGEST_DURATION", TYPE_BIGINT, sizeof(int64_t), true},
@@ -65,11 +71,6 @@ Status SchemaSqlBlockRuleStatusScanner::_get_sql_block_rule_status_block_from_fe
6571
}
6672

6773
TSchemaTableRequestParams schema_table_request_params;
68-
for (int i = 0; i < _s_sql_block_rule_status_columns.size(); i++) {
69-
schema_table_request_params.__isset.columns_name = true;
70-
schema_table_request_params.columns_name.emplace_back(
71-
_s_sql_block_rule_status_columns[i].name);
72-
}
7374
schema_table_request_params.__set_current_user_ident(*_param->common_param->current_user_ident);
7475
schema_table_request_params.__set_frontend_conjuncts(*_param->common_param->frontend_conjuncts);
7576

@@ -112,16 +113,31 @@ Status SchemaSqlBlockRuleStatusScanner::_get_sql_block_rule_status_block_from_fe
112113

113114
if (result_data.size() > 0) {
114115
auto col_size = result_data[0].column_value.size();
115-
if (col_size != _s_sql_block_rule_status_columns.size()) {
116+
if (col_size != LEGACY_SQL_BLOCK_RULE_STATUS_COLUMN_SIZE &&
117+
col_size != _s_sql_block_rule_status_columns.size()) {
116118
return Status::InternalError("col size not equal");
117119
}
118120
}
119121

120122
// Add rows from this FE to the result block
121123
for (int i = 0; i < result_data.size(); i++) {
122-
TRow row = result_data[i];
124+
const TRow& row = result_data[i];
125+
auto col_size = row.column_value.size();
123126
for (int j = 0; j < _s_sql_block_rule_status_columns.size(); j++) {
124-
RETURN_IF_ERROR(insert_block_column(row.column_value[j], j,
127+
if (col_size == LEGACY_SQL_BLOCK_RULE_STATUS_COLUMN_SIZE &&
128+
j == REQUIRE_PARTITION_FILTER_INDEX) {
129+
TCell default_cell;
130+
default_cell.__set_boolVal(false);
131+
RETURN_IF_ERROR(insert_block_column(default_cell, j,
132+
_sql_block_rule_status_block.get(),
133+
_s_sql_block_rule_status_columns[j].type));
134+
continue;
135+
}
136+
int row_index = col_size == LEGACY_SQL_BLOCK_RULE_STATUS_COLUMN_SIZE &&
137+
j > REQUIRE_PARTITION_FILTER_INDEX
138+
? j - 1
139+
: j;
140+
RETURN_IF_ERROR(insert_block_column(row.column_value[row_index], j,
125141
_sql_block_rule_status_block.get(),
126142
_s_sql_block_rule_status_columns[j].type));
127143
}

fe/fe-core/src/main/java/org/apache/doris/blockrule/SqlBlockRule.java

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@ public class SqlBlockRule implements Writable, GsonPostProcessable {
7575
@SerializedName(value = "enable")
7676
private Boolean enable;
7777

78+
// whether partitioned table scans require partition filters
79+
@SerializedName(value = "requirePartitionFilter")
80+
private Boolean requirePartitionFilter;
81+
7882
private Pattern sqlPattern;
7983
private Histogram tryBlockHistogram;
8084
private LongCounterMetric blockCount;
@@ -83,13 +87,14 @@ public class SqlBlockRule implements Writable, GsonPostProcessable {
8387
* Create SqlBlockRule.
8488
**/
8589
public SqlBlockRule(String name, String sql, String sqlHash, Long partitionNum, Long tabletNum, Long cardinality,
86-
Boolean global, Boolean enable) {
90+
Boolean requirePartitionFilter, Boolean global, Boolean enable) {
8791
this.name = name;
8892
this.sql = sql;
8993
this.sqlHash = sqlHash;
9094
this.partitionNum = partitionNum;
9195
this.tabletNum = tabletNum;
9296
this.cardinality = cardinality;
97+
this.requirePartitionFilter = requirePartitionFilter;
9398
this.global = global;
9499
this.enable = enable;
95100
if (StringUtils.isNotEmpty(sql)) {
@@ -103,6 +108,7 @@ public SqlBlockRule(String name, String sql, String sqlHash, Long partitionNum,
103108
public SqlBlockRule() {
104109
this.tryBlockHistogram = new Histogram(new SlidingWindowReservoir(1000));
105110
this.blockCount = new LongCounterMetric("blocks", MetricUnit.ROWS, "");
111+
this.requirePartitionFilter = false;
106112
}
107113

108114
public String getName() {
@@ -141,6 +147,10 @@ public Boolean getEnable() {
141147
return enable;
142148
}
143149

150+
public Boolean getRequirePartitionFilter() {
151+
return requirePartitionFilter;
152+
}
153+
144154
public void setSql(String sql) {
145155
this.sql = sql;
146156
}
@@ -173,6 +183,10 @@ public void setEnable(Boolean enable) {
173183
this.enable = enable;
174184
}
175185

186+
public void setRequirePartitionFilter(Boolean requirePartitionFilter) {
187+
this.requirePartitionFilter = requirePartitionFilter;
188+
}
189+
176190
/**
177191
* Show SqlBlockRule info.
178192
**/
@@ -181,7 +195,11 @@ public List<String> getShowInfo() {
181195
this.partitionNum == null ? "0" : Long.toString(this.partitionNum),
182196
this.tabletNum == null ? "0" : Long.toString(this.tabletNum),
183197
this.cardinality == null ? "0" : Long.toString(this.cardinality), String.valueOf(this.global),
184-
String.valueOf(this.enable));
198+
String.valueOf(this.enable), toShowBoolean(this.requirePartitionFilter));
199+
}
200+
201+
private static String toShowBoolean(Boolean value) {
202+
return Boolean.TRUE.equals(value) ? "1" : "0";
185203
}
186204

187205
public Histogram getTryBlockHistogram() {
@@ -211,5 +229,8 @@ public void gsonPostProcess() {
211229
this.getSql())) {
212230
this.setSqlPattern(Pattern.compile(this.getSql()));
213231
}
232+
if (this.getRequirePartitionFilter() == null) {
233+
this.setRequirePartitionFilter(false);
234+
}
214235
}
215236
}

fe/fe-core/src/main/java/org/apache/doris/blockrule/SqlBlockRuleMgr.java

Lines changed: 40 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,9 @@ public void alterSqlBlockRule(SqlBlockRule sqlBlockRule) throws AnalysisExceptio
161161
if (sqlBlockRule.getEnable() == null) {
162162
sqlBlockRule.setEnable(originRule.getEnable());
163163
}
164+
if (sqlBlockRule.getRequirePartitionFilter() == null) {
165+
sqlBlockRule.setRequirePartitionFilter(originRule.getRequirePartitionFilter());
166+
}
164167
sqlBlockRule.setSqlPattern(Pattern.compile(sqlBlockRule.getSql()));
165168
verifyLimitations(sqlBlockRule);
166169
SqlBlockUtil.checkAlterValidate(sqlBlockRule);
@@ -269,13 +272,22 @@ private void matchSql(SqlBlockRule rule, String originSql, String sqlHash) throw
269272
**/
270273
public void checkLimitations(Long partitionNum, Long tabletNum, Long cardinality, String user)
271274
throws AnalysisException {
275+
checkLimitations(partitionNum, tabletNum, cardinality, false, false, user);
276+
}
277+
278+
/**
279+
* Check scan limitations whether legal by user.
280+
**/
281+
public void checkLimitations(Long partitionNum, Long tabletNum, Long cardinality,
282+
boolean isPartitionedTable, boolean hasPartitionPredicate, String user) throws AnalysisException {
272283
if (ConnectContext.get().getState().isInternal()) {
273284
return;
274285
}
275286
// match global rule
276287
for (SqlBlockRule rule : nameToSqlBlockRuleMap.values()) {
277288
if (rule.getGlobal()) {
278-
checkLimitations(rule, partitionNum, tabletNum, cardinality);
289+
checkLimitations(rule, partitionNum, tabletNum, cardinality,
290+
isPartitionedTable, hasPartitionPredicate);
279291
}
280292
}
281293
// match user rule
@@ -285,33 +297,42 @@ public void checkLimitations(Long partitionNum, Long tabletNum, Long cardinality
285297
if (rule == null) {
286298
continue;
287299
}
288-
checkLimitations(rule, partitionNum, tabletNum, cardinality);
300+
checkLimitations(rule, partitionNum, tabletNum, cardinality,
301+
isPartitionedTable, hasPartitionPredicate);
289302
}
290303
}
291304

292305
/**
293306
* Check number whether legal by SqlBlockRule.
294307
**/
295-
private void checkLimitations(SqlBlockRule rule, Long partitionNum, Long tabletNum, Long cardinality)
308+
@VisibleForTesting
309+
void checkLimitations(SqlBlockRule rule, Long partitionNum, Long tabletNum, Long cardinality,
310+
boolean isPartitionedTable, boolean hasPartitionPredicate)
296311
throws AnalysisException {
312+
if (!rule.getEnable()) {
313+
return;
314+
}
315+
if (Boolean.TRUE.equals(rule.getRequirePartitionFilter()) && isPartitionedTable && !hasPartitionPredicate) {
316+
MetricRepo.COUNTER_HIT_SQL_BLOCK_RULE.increase(1L);
317+
throw new AnalysisException("sql hits sql block rule: " + rule.getName() + ", missing partition filter");
318+
}
297319
if (rule.getPartitionNum() == 0 && rule.getTabletNum() == 0 && rule.getCardinality() == 0) {
298320
return;
299-
} else if (rule.getEnable()) {
300-
if ((rule.getPartitionNum() != 0 && rule.getPartitionNum() < partitionNum) || (rule.getTabletNum() != 0
301-
&& rule.getTabletNum() < tabletNum) || (rule.getCardinality() != 0
302-
&& rule.getCardinality() < cardinality)) {
303-
MetricRepo.COUNTER_HIT_SQL_BLOCK_RULE.increase(1L);
304-
if (rule.getPartitionNum() < partitionNum && rule.getPartitionNum() != 0) {
305-
throw new AnalysisException(
306-
"sql hits sql block rule: " + rule.getName() + ", reach partition_num : "
307-
+ rule.getPartitionNum());
308-
} else if (rule.getTabletNum() < tabletNum && rule.getTabletNum() != 0) {
309-
throw new AnalysisException("sql hits sql block rule: " + rule.getName() + ", reach tablet_num : "
310-
+ rule.getTabletNum());
311-
} else if (rule.getCardinality() < cardinality && rule.getCardinality() != 0) {
312-
throw new AnalysisException("sql hits sql block rule: " + rule.getName() + ", reach cardinality : "
313-
+ rule.getCardinality());
314-
}
321+
}
322+
if ((rule.getPartitionNum() != 0 && rule.getPartitionNum() < partitionNum) || (rule.getTabletNum() != 0
323+
&& rule.getTabletNum() < tabletNum) || (rule.getCardinality() != 0
324+
&& rule.getCardinality() < cardinality)) {
325+
MetricRepo.COUNTER_HIT_SQL_BLOCK_RULE.increase(1L);
326+
if (rule.getPartitionNum() < partitionNum && rule.getPartitionNum() != 0) {
327+
throw new AnalysisException(
328+
"sql hits sql block rule: " + rule.getName() + ", reach partition_num : "
329+
+ rule.getPartitionNum());
330+
} else if (rule.getTabletNum() < tabletNum && rule.getTabletNum() != 0) {
331+
throw new AnalysisException("sql hits sql block rule: " + rule.getName() + ", reach tablet_num : "
332+
+ rule.getTabletNum());
333+
} else if (rule.getCardinality() < cardinality && rule.getCardinality() != 0) {
334+
throw new AnalysisException("sql hits sql block rule: " + rule.getName() + ", reach cardinality : "
335+
+ rule.getCardinality());
315336
}
316337
}
317338
}

fe/fe-core/src/main/java/org/apache/doris/catalog/SchemaTable.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -805,6 +805,8 @@ public class SchemaTable extends Table {
805805
.column("CARDINALITY", ScalarType.createType(PrimitiveType.BIGINT))
806806
.column("GLOBAL", ScalarType.createType(PrimitiveType.BOOLEAN))
807807
.column("ENABLE", ScalarType.createType(PrimitiveType.BOOLEAN))
808+
.column("REQUIRE_PARTITION_FILTER",
809+
ScalarType.createType(PrimitiveType.BOOLEAN))
808810
.column("BLOCKS", ScalarType.createType(PrimitiveType.BIGINT),
809811
SchemaTableAggregateType.SUM, false)
810812
.column("AVERAGE_DURATION", ScalarType.createType(PrimitiveType.BIGINT),

0 commit comments

Comments
 (0)