PL/iSQL函数与存储过程
1. 目的
PostgreSQL支持函数(FUNCTION)和存储过程(PROCEDURE),但是在语法上和Oracle有差异,为了让Oracle的PLSQL语句可以在IvorySQL上执行,也就是“语法兼容”,IvorySQL采取这样的解决方案:如果Oracle的子句在IvorySQL中存在相同功能的子句,则直接映射为对应的IvorySQL子句,否则只实现其语法,不实现其功能。
2. 实现说明
PL/iSQL是IvorySQL中的过程语言名称,专门用来兼容Oracle的PLSQL语句。为了兼容Oracle风格的函数与存储过程语法,需要对psql客户端工具、SQL端以及PL/iSQL端做相应处理。
2.1. 客户端工具psql
Oracle的sqlplus工具使用斜线(/)来结束函数和存储过程,IvorySQL的客户端工具psql需要兼容同样的语法,也就是说我们常规的遇到分号发送语句给服务端的机制,当遇到Oracle风格的函数和存储过程命令时失效,改为使用斜线(/)发送命令。
为此在 PsqlScanStateData 结构体中增加如下字段:
bool cancel_semicolon_terminator; /* not send command when semicolon found */
/*
* State to track boundaries of Oracle ANONYMOUS BLOCK.
* Case 1: Statements starting with << ident >> is Oracle anonymous block.
*/
int token_count; /* # of tokens, not blank or newline since start of statement */
bool anonymous_label_start; /* T if the first token is "<<" */
bool anonymous_label_ident; /* T if the second token is an identifier */
bool anonymous_label_end; /* T if the third token is ">>" */
/*
* Case 2: DECLARE BEGIN ... END is Oracle anonymous block syntax.
* DECLARE can also be a PostgreSQL cursor declaration statement, we need to tell this.
*/
bool maybe_anonymous_declare_start; /* T if the first token is DECLARE */
int token_cursor_idx; /* the position of keyword CURSOR in SQL statement */
/*
* Case 3: DECLARE BEGIN ... END is Oracle anonymous block syntax.
* BEGIN can also be a PostgreSQL transaction statement.
*/
bool maybe_anonymous_begin_start; /* T if the first token is BEGIN */
同时修改 ora_psqlscan.l,添加和修改相应的词法规则, 以下是代码片段示例:
{
if (is_oracle_slash(cur_state, cur_state->scanline))
{
/* Terminate lexing temporarily */
cur_state->cancel_semicolon_terminator = false;
cur_state->maybe_anonymous_declare_start = false;
cur_state->maybe_anonymous_begin_start = false;
cur_state->anonymous_label_start = false;
cur_state->anonymous_label_ident = false;
cur_state->anonymous_label_end = false;
cur_state->start_state = YY_START;
cur_state->token_count = 0;
cur_state->token_cursor_idx = 0;
cur_state->identifier_count = 0;
cur_state->begin_depth = 0;
cur_state->ora_plsql_expect_end_symbol = END_SYMBOL_INVALID;
return LEXRES_SEMI;
}
ECHO;
避免将注释等场景中的斜线误判为结束符,为此在 oracle_fe_utils/ora_psqlscan.l 文件中新增 is_oracle_slash 函数接口用于检测:
bool
is_oracle_slash(PsqlScanState state, const char *line)
{
bool result = false;
switch (state->start_state)
{
case INITIAL:
case xqs: /* treat these like INITIAL */
{
int len, i;
bool has_slash = false;
len = strlen(line);
for (i = 0; i < len; i++)
{
/* allow special char */
if (line[i] == '\t' ||
line[i] == '\n' ||
line[i] == '\r' ||
line[i] == ' ')
continue;
if (line[i] == '/')
{
if (has_slash)
break;
has_slash = true;
continue;
}
/* others */
break;
}
if (i == len && has_slash)
result = true;
}
break;
default:
break;
}
return result;
}
2.2. SQL端
SQL端要能够识别函数和存储过程的创建语法,这是通过修改ora_base_yylex来实现的。这个函数预取并缓存token,如果是Oracle语法格式则组织一个SCONST发送给PLSQL端,否则从堆栈中获取之前预读的token,按照原生PG的逻辑进行处理。
在ora_base_yy_extra_type数据结构中增加如下字段:
/*
* The native PG only cache one-token info include yylloc, yylval and token
* number in yyextra, IvorySQL cache multiple tokens info using two arrays.
*/
int max_pushbacks; /* the max size of cache array */
int loc_pushback; /* # of used tokens */
int num_pushbacks; /* # of cached tokens */
int *pushback_token; /* token number array */
TokenAuxData *pushback_auxdata; /* auxdata array */
OraBodyStyle body_style;
int body_start;
int body_level;
增加token堆栈的操作接口:
push_back_token |
forward_token |
ora_internal_yylex |
internal_yylex |
ora_base_yylex函数中在创建函数、过程、匿名块时会预读部分token,使用上述结构缓存到堆栈中,是为了构造一个符合Oracle PL/SQL语法的SCONST发送给PL/iSQL端去处理。具体请参考源代码。
2.3. PL/iSQL端
该部分主要修改了pl_gram.y文件,以兼容PLSQL的函数和存储过程语法,在不影响PG原生的PL/pgSQL的前提下去兼容Oracle PL/SQL语法形式,如下是DECLARE部分兼容的代码示例,更多请参考IvorySQL源代码。
/*
* The declaration section of the outermost block in Oracle does not have the DECLARE keyword.
*/
ora_outermost_pl_block: ora_decl_sect K_BEGIN proc_sect exception_sect K_END opt_label
{
PLiSQL_stmt_block *new;
new = palloc0(sizeof(PLiSQL_stmt_block));
new->cmd_type = PLISQL_STMT_BLOCK;
new->lineno = plisql_location_to_lineno(@2);
new->stmtid = ++plisql_curr_compile->nstatements;
new->label = $1.label;
new->n_initvars = $1.n_initvars;
new->initvarnos = $1.initvarnos;
new->body = $3;
new->exceptions = $4;
check_labels($1.label, $6, @6);
plisql_ns_pop();
$$ = (PLiSQL_stmt *)new;
}
;
ora_decl_sect: opt_block_label opt_ora_decl_start opt_ora_decl_stmts
{
if ($2)
{
if ($1 == NULL)
{
plisql_ns_push(NULL, PLISQL_LABEL_BLOCK);
}
}
}
opt_ora_decl_stmts
{
if ($4)
{
plisql_IdentifierLookup = IDENTIFIER_LOOKUP_NORMAL;
$$.label = ($1 == NULL ? plisql_curr_compile->namelabel : $1);
if ($2 && $1 == NULL)
$$.popname = true;
else
$$.popname = false;
/* Remember variables declared in decl_stmts */
$$.n_initvars = plisql_add_initdatums(&($$.initvarnos));
}
else
{
plisql_IdentifierLookup = IDENTIFIER_LOOKUP_NORMAL;
$$.label = ($1 == NULL ? plisql_curr_compile->namelabel : $1);
$$.n_initvars = 0;
if ($2 && $1 == NULL)
$$.popname = true;
else
$$.popname = false;
$$.initvarnos = NULL;
}
}
;
opt_ora_decl_start: K_DECLARE
{
/* Forget any variables created before block */
plisql_add_initdatums(NULL);
/*
* Disable scanner lookup of identifiers while
* we process the decl_stmts
*/
plisql_IdentifierLookup = IDENTIFIER_LOOKUP_DECLARE;
$$ = true;
}
| /*EMPTY*/
{
/* Forget any variables created before block */
plisql_add_initdatums(NULL);
/*
* Disable scanner lookup of identifiers while
* we process the decl_stmts
*/
plisql_IdentifierLookup = IDENTIFIER_LOOKUP_DECLARE;
$$ = false;
}
;
opt_ora_decl_stmts:
ora_decl_stmts
{
$$ = true;
}
| /*EMPTY*/
{
$$ = false;
}
ora_decl_stmts: ora_decl_stmts ora_decl_stmt
| ora_decl_stmt
;
ora_decl_stmt: decl_statement
;