目标:
给定一查询SQL.
1. 如果隐私字段出现在最终的查询结果中,不论中间经过多少次别名变换,仍然能够识别。
2. 如果隐私字段出现在最终的查询结果中,中间经过的所有处理函数,都记录下来。
3. 如果隐私字段仅出现在中间查询中,或者仅用隐私字段作为关联条件(如用手机号判断两个商城的重合度),则允许。
一句话:出现在最终结果的查询字段,如果是隐私字段,则判断处理函数是否允许,如果没有处理函数,或者函数不允许,则抛出异常。
数据字典
要定义表的哪些字段是隐私字段,我们需要有数据字典,给定一个SQL,我们从数据字典中判断是否有隐私字段,和判断隐私字段上的函数是否允许。
通过MetaManager.getTable(String dbName, String tableName)得到SQL中Table的定义。
ITable是一个存储根据查询的表名从数据字段中得到的表的定义, 定义了一下方法:
Interface ITable {
String getDBName(); //返回数据库名
String getTableName(); // 返回表名
List<IField> getFieldList(); // 返回字段列表
}
IField的定义如下:
Interface IField {
String getName(); // 返回字段名
Boolean canBeDirectlySelect(); // 返回该字段是否可以直接选择
// 如果该字段不可以直接选择,是否可以使用参数functionName对应的函数。
Boolean canBeInFunction(String functionName);
}
词法分析
HiveParser.g
HiveParser.g has 2395 lines and import 5 other anltr file.
HiveLexer.g
KW_HOUR: 'HOUR';
KW_MINUTE: 'MINUTE';
KW_SECOND: 'SECOND';
// Operators
// NOTE: if you add a new function/operator, add it to sysFuncNames so that describe function _FUNC_ will work.
DOT : '.'; // generated as a part of Number rule
COLON : ':' ;
COMMA : ',' ;
SEMICOLON : ';' ;
LPAREN : '(' ;
RPAREN : ')' ;
LSQUARE : '[' ;
RSQUARE : ']' ;
LCURLY : '{';
RCURLY : '}';
EQUAL : '=' | '==';
EQUAL_NS : '<=>';
NOTEQUAL : '<>' | '!=';
LESSTHANOREQUALTO : '<=';
LESSTHAN : '<';
GREATERTHANOREQUALTO : '>=';
GREATERTHAN : '>';
DIVIDE : '/';
PLUS : '+';
MINUS : '-';
STAR : '*';
MOD : '%';
DIV : 'DIV';
AMPERSAND : '&';
TILDE : '~';
BITWISEOR : '|';
BITWISEXOR : '^';
QUESTION : '?';
DOLLAR : '$';
// LITERALS
fragment
Letter
: 'a'..'z' | 'A'..'Z'
;
fragment
HexDigit
: 'a'..'f' | 'A'..'F'
;
fragment
Digit
:
'0'..'9'
;
fragment
Exponent
:
('e' | 'E') ( PLUS|MINUS )? (Digit)+
;
fragment
RegexComponent
: 'a'..'z' | 'A'..'Z' | '0'..'9' | '_'
| PLUS | STAR | QUESTION | MINUS | DOT
| LPAREN | RPAREN | LSQUARE | RSQUARE | LCURLY | RCURLY
| BITWISEXOR | BITWISEOR | DOLLAR
;
StringLiteral
:
( '\'' ( ~('\''|'\\') | ('\\' .) )* '\''
| '\"' ( ~('\"'|'\\') | ('\\' .) )* '\"'
)+
;
CharSetLiteral
:
StringLiteral
| '0' 'X' (HexDigit|Digit)+
;
BigintLiteral
:
(Digit)+ 'L'
;
SmallintLiteral
:
(Digit)+ 'S'
;
TinyintLiteral
:
(Digit)+ 'Y'
;
DecimalLiteral
:
Number 'B' 'D'
;
ByteLengthLiteral
:
(Digit)+ ('b' | 'B' | 'k' | 'K' | 'm' | 'M' | 'g' | 'G')
;
Number
:
(Digit)+ ( DOT (Digit)* (Exponent)? | Exponent)?
;
/*
An Identifier can be:
- tableName
- columnName
- select expr alias
- lateral view aliases
- database name
- view name
- subquery alias
- function name
- ptf argument identifier
- index name
- property name for: db,tbl,partition...
- fileFormat
- role name
- privilege name
- principal name
- macro name
- hint name
- window name
*/
Identifier
:
(Letter | Digit) (Letter | Digit | '_')*
| {allowQuotedId()}? QuotedIdentifier /* though at the language level we allow all Identifiers to be QuotedIdentifiers;
at the API level only columns are allowed to be of this form */
| '`' RegexComponent+ '`'
;
fragment
QuotedIdentifier
:
'`' ( '``' | ~('`') )* '`' { setText(getText().substring(1, getText().length() -1 ).replaceAll("``", "`")); }
;
CharSetName
:
'_' (Letter | Digit | '_' | '-' | '.' | ':' )+
;
WS : (' '|'\r'|'\t'|'\n') {$channel=HIDDEN;}
;
COMMENT
: '--' (~('\n'|'\r'))*
{ $channel=HIDDEN; }
;
statement
parser grammar HiveParser;
// starting rule
statement
: explainStatement EOF
| execStatement EOF
;
execStatement
execStatement
@init { pushMsg("statement", state); }
@after { popMsg(state); }
: queryStatementExpression[true]
| loadStatement
| exportStatement
| importStatement
| ddlStatement
| deleteStatement
| updateStatement
;
以这个SQL为例:
select
dept.dept_no,
dept.dept_name,
salary_sum
from (
select
dept_emp.dept_no,
sum(dept_emp.salary) salary_sum
from sql_parser.employee
join sql_parser.dept_emp
on employee.id = dept_emp.emp_no
group by dept_emp.dept_no
) t
join department dept
on dept.dept_no = t.dept_no
order by salary_sum desc
limits 10
这个SQL生成的AST树如下:
TOK_QUERY
TOK_FROM
TOK_JOIN
TOK_SUBQUERY
TOK_QUERY
TOK_FROM
TOK_JOIN
TOK_TABREF
TOK_TABNAME
sql_parser
employee
TOK_TABREF
TOK_TABNAME
sql_parser
dept_emp
=
.
TOK_TABLE_OR_COL
employee
id
.
TOK_TABLE_OR_COL
dept_emp
emp_no
TOK_INSERT
TOK_DESTINATION
TOK_DIR
TOK_TMP_FILE
TOK_SELECT
TOK_SELEXPR
.
TOK_TABLE_OR_COL
dept_emp
dept_no
TOK_SELEXPR
TOK_FUNCTION
sum
.
TOK_TABLE_OR_COL
dept_emp
salary
salary_sum
TOK_GROUPBY
.
TOK_TABLE_OR_COL
dept_emp
dept_no
t
TOK_TABREF
TOK_TABNAME
department
dept
=
.
TOK_TABLE_OR_COL
dept
dept_no
.
TOK_TABLE_OR_COL
t
dept_no
TOK_INSERT
TOK_DESTINATION
TOK_DIR
TOK_TMP_FILE
TOK_SELECT
TOK_SELEXPR
.
TOK_TABLE_OR_COL
dept
dept_no
TOK_SELEXPR
.
TOK_TABLE_OR_COL
dept
dept_name
TOK_SELEXPR
TOK_TABLE_OR_COL
salary_sum
TOK_ORDERBY
TOK_TABSORTCOLNAMEDESC
TOK_TABLE_OR_COL
salary_sum
AST 结点的结构如下:
每个结点都有一个名称,如“TOK_QUERY”。每个结点都可能有子节点。如“TOK_QUERY”的子节点是“TOK_FROM”和“TOK_INSERT”。
第二部分是逻辑分析
逻辑分析根据AST生成QueryBlock对象。
QueryBlock的内容如下:
Map
(
select
dept_emp.dept_no,
sum(employee.salary) salary_sum
from sql_parser.employee
join sql_parser.dept_emp
on employee.id = dept_emp.emp_no
group by dept_emp.dept_no ) t
nameToDest的生成规则:anmeToDest是SQL语句中检索的内容。把整个TOK_SELECT 放到放到关键字为随机的nameToDest映射中。
如select dept.dept_no, dept.dept_name, salary_sum
TOK_SELECT
TOK_SELEXPR
.
TOK_TABLE_OR_COL
dept
dept_no
TOK_SELEXPR
.
TOK_TABLE_OR_COL
dept
dept_name
TOK_SELEXPR
TOK_TABLE_OR_COL
salary_sum
第三部分 语义分析和判断
从第二部分的逻辑分析,我们知道这一层的查询内容,以及这些表和子查询。我们的最终的目的是找到查询用的所有字段,判断是否是隐私字段,如果是隐私字段,我们判断用的方法是否允许。但是我们从QueryBlock得到一个字段,如salary_sum, 我们无法判断它对应哪个表的哪个字段。因此我们要有语义分析。语义分析根据QueryBlock生成SelectExprDependencyRelation对象。
语义分析的定义如下:
class SelectExprDependencyRelation {
List<SelectItem> itemList; // 查询的字段
Map<String, ITable> tableMap; // 查询用的表映射
// 子查询对应的语义分析对象
Map<Strng, SelectExprDependencyRelation> subRelation;
}
SelectItem的定义如下:
Class SelectItem {
String dbName; // 查询内容对应的库名;
String tableName; // 查询内容对应的表名
String fieldname; // 查询内容对应的字段名
Set<String> funcSet; // 作用在字段上的函数名的集合
String alias; // 查询别名,如果不配置,则查询别名为字段名fieldname。
}
itemList得到真正的字段列表。
语义分析过程如下:
1. 生成tableMap。每个QueryBlock的aliasToTabs中的一个映射对象生成tableMap的一个映射,key值相同。从aliasToTabs的对应的值找到数据库名和表名,根据数据库名和表名从元数据管理系统中得到ITable对象,放到tableMap中。
数据库名和表名的规则如下:
如果ASTNode有两个子结点,则第一个子节点是数据库名,第二个子节点是表名。如果ASTNode有一个子结点,则该子结点是表名,数据库名则是当前数据库名。
2. 生成subRelation。由于每个子查询又是一个QueryBlock,每个都生成对应的SelectExprDependencyRelation。这是一个递归过程,因为子查询可能还有子查询。每个子查询生成后放到subRelation中。
3. 生成itemList。由于这是最后一步,由于递归,所以最先生成最内部子查询的itemList,再生成上级查询的itemList。由于最内部的子查询是没有子查询的,可以找到查询内容对应的表的字段,把字段,表名,函数以及查询内容的别名组成一个个SelectItem,放到子查询对应的itemList中。解析上一层查询内容是,其对应的字段要么在当前层对应的表中,要么在当前层对应的子查询中。生成对应的SelectItem,放在本层的itemList,一直到最上层,从itemList就知道本查询的所有内容是从哪些表的哪些字段查询出来的。
由于到某一层时,所有的表和子查询对应的字段都已经清楚了,我们开发了两个个工具方法,名称为getSelectItemList。其中一个有一个参数为字段别名,返回从表或子查询的所有用到的字段信息列表。另外一个有两个参数,第一个参数为表别名或子查询别名,第二个参数为字段别名。用于从对应的表和子查询中找到对应的字段别名对应的字段列表。为什么上层的一个查询内容对应是一个列表呢?因为上层的查询内容,可能在某级子查询中,对应的是某级子查询的一个函数的输出,而这个函数用到了多个列,则这个查询内容对应的所有列的信息都应该放到本机查询中。
详细过程如下:
从QueryBlock的nameToDest对象,遍历所有子结点,子结点命名为selectNode.
如果selectNode有两个子结点,则第二个子结点为内容别名。
第一个子节点为查询内容,依次执行以下判断。
1. 是否为“TOK_ALLCOLREF”, 对应“select ”中的’’。再判断是否有限定,对应于“select t.*”。如果没有限定,则代表从所有表中,和子查询的所有查询内容都放到本层查询的查询结果中。如果有限定,则代表,则先根据限定名从tableMap找到对应的表。如果能找到,则检索出所有字段,每个字段组成一个SelectItem对象,放到itemList中。如果找到对应的表,则根据限定名从子查询中找到所有的itemList,全部加到当前层查询的itemList中。
2. 是否是“TOK_TABLE_OR_COL”,代表查询是字段。“TOK_TABLE_OR_COL”代表查询字段,用getSelectItemList方法找到该字段对应的所有SelectItem。每个SelectItem对象做拷贝,然后把函数集加上当前的函数,并且别名改为当前别名。
3. 是否是“.”,代表查询有限定。如“select dept.dept_no”, 限定了dept_no从表别名dept或者子查询dept中。用getSelectItemList方法找到该字段对应的所有SelectItem。每个SelectItem对象做拷贝,然后把函数集加上当前的函数,并且别名改为当前别名。
4. 其他情况,用于表达式,或函数,如“select func1(func2(column1), column2) ”, 由于一个函数的参数可能是函数或者是检索的字段,所以函数是一个嵌套的结构。如示例所述column1对应的函数为func1和func2,而column2对应的函数只有func1。如果参数为字段,则用以上的方法得到。而从该检索内容找到所有的字段和函数的方法getFieldsFromTree是一个递归式,这个递归式有四个参数,第一个参数为ASTNode node, 第二个参数为返回结果集List itemList,第三个参数为函数集Set functionSet。
从检索内容找到所有的字段和函数的方法getFieldsFromTree的执行步骤:
1. 生成新的函数集newFunctionSet;
2. 把所有的functionSet的内容添加到newFunctionSet。不能直接往functionSet中添加函数,因为递归式中里面的层级添加的函数上级应该看不到。
3. 判断node的内容:
3.1 如果是“字段”,则用getSelectItemList取出对应的字段,把当前然newFunctionSet的内容添加到字段的functionSet中,然后把字段添加到itemList中。然后返回。
3.2 如果是函数,则第一个子结点存储的是函数名,把函数名放到newFunctionSet中。从第二个子结点到最后一个子结点,依次调用getFieldsFromTree。
3.3 其他情况,则从第一个子结点到最后一个子结点,依次调用getFieldsFromTree。
我们以“select func1(func2(column1), column2) ”为例,结果为
column1的functionset 为func1,func2。
column2的functionset 为func1。
验证
从SelectExprDependencyRelation 对象取出itemList,从数据字典中找到表和字段,判断是否是隐私字段,如果是,从字段的functionSet取出函数,依次判断函数是否允许。