|
| 1 | +:sectnums: |
| 2 | +:sectnumlevels: 5 |
| 3 | + |
| 4 | +:imagesdir: ./_images |
| 5 | + |
| 6 | += PL/iSQL函数与存储过程 |
| 7 | + |
| 8 | +== 目的 |
| 9 | + |
| 10 | +PostgreSQL支持函数(FUNCTION)和存储过程(PROCEDURE),但是在语法上和Oracle有差异,为了让Oracle的PLSQL语句可以在IvorySQL上执行,也就是“语法兼容”,IvorySQL采取这样的解决方案:如果Oracle的子句在IvorySQL中存在相同功能的子句,则直接映射为对应的IvorySQL子句,否则只实现其语法,不实现其功能。 |
| 11 | + |
| 12 | +== 实现说明 |
| 13 | + |
| 14 | +PL/iSQL是IvorySQL中的过程语言名称,专门用来兼容Oracle的PLSQL语句。为了兼容Oracle风格的函数与存储过程语法,需要对psql客户端工具、SQL端以及PL/iSQL端做相应处理。 |
| 15 | + |
| 16 | +=== 客户端工具psql |
| 17 | + |
| 18 | +Oracle的sqlplus工具使用斜线(/)来结束函数和存储过程,IvorySQL的客户端工具psql需要兼容同样的语法,也就是说我们常规的遇到分号发送语句给服务端的机制,当遇到Oracle风格的函数和存储过程命令时失效,改为使用斜线(/)发送命令。 |
| 19 | + |
| 20 | +为此在 PsqlScanStateData 结构体中增加如下字段: |
| 21 | +``` |
| 22 | + bool cancel_semicolon_terminator; /* not send command when semicolon found */ |
| 23 | + |
| 24 | + /* |
| 25 | + * State to track boundaries of Oracle ANONYMOUS BLOCK. |
| 26 | + * Case 1: Statements starting with << ident >> is Oracle anonymous block. |
| 27 | + */ |
| 28 | + int token_count; /* # of tokens, not blank or newline since start of statement */ |
| 29 | + bool anonymous_label_start; /* T if the first token is "<<" */ |
| 30 | + bool anonymous_label_ident; /* T if the second token is an identifier */ |
| 31 | + bool anonymous_label_end; /* T if the third token is ">>" */ |
| 32 | + |
| 33 | + /* |
| 34 | + * Case 2: DECLARE BEGIN ... END is Oracle anonymous block syntax. |
| 35 | + * DECLARE can also be a PostgreSQL cursor declaration statement, we need to tell this. |
| 36 | + */ |
| 37 | + bool maybe_anonymous_declare_start; /* T if the first token is DECLARE */ |
| 38 | + int token_cursor_idx; /* the position of keyword CURSOR in SQL statement */ |
| 39 | + |
| 40 | + /* |
| 41 | + * Case 3: DECLARE BEGIN ... END is Oracle anonymous block syntax. |
| 42 | + * BEGIN can also be a PostgreSQL transaction statement. |
| 43 | + */ |
| 44 | + bool maybe_anonymous_begin_start; /* T if the first token is BEGIN */ |
| 45 | + |
| 46 | +``` |
| 47 | + |
| 48 | +同时修改 ora_psqlscan.l,添加和修改相应的词法规则, 以下是代码片段示例: |
| 49 | +``` |
| 50 | +{ |
| 51 | + if (is_oracle_slash(cur_state, cur_state->scanline)) |
| 52 | + { |
| 53 | + /* Terminate lexing temporarily */ |
| 54 | + cur_state->cancel_semicolon_terminator = false; |
| 55 | + cur_state->maybe_anonymous_declare_start = false; |
| 56 | + cur_state->maybe_anonymous_begin_start = false; |
| 57 | + cur_state->anonymous_label_start = false; |
| 58 | + cur_state->anonymous_label_ident = false; |
| 59 | + cur_state->anonymous_label_end = false; |
| 60 | + cur_state->start_state = YY_START; |
| 61 | + cur_state->token_count = 0; |
| 62 | + cur_state->token_cursor_idx = 0; |
| 63 | + cur_state->identifier_count = 0; |
| 64 | + cur_state->begin_depth = 0; |
| 65 | + cur_state->ora_plsql_expect_end_symbol = END_SYMBOL_INVALID; |
| 66 | + return LEXRES_SEMI; |
| 67 | + } |
| 68 | + ECHO; |
| 69 | +``` |
| 70 | + |
| 71 | +Psql工具需要检测斜线/的含义,避免将注释等部分的斜线判定为结束符,为此在oracle_fe_utils/ora_psqlscan.l文件中增加一个单独的接口is_oracle_slash来检测: |
| 72 | +``` |
| 73 | +bool |
| 74 | +is_oracle_slash(PsqlScanState state, const char *line) |
| 75 | +{ |
| 76 | + bool result = false; |
| 77 | + |
| 78 | + switch (state->start_state) |
| 79 | + { |
| 80 | + case INITIAL: |
| 81 | + case xqs: /* treat these like INITIAL */ |
| 82 | + { |
| 83 | + int len, i; |
| 84 | + bool has_slash = false; |
| 85 | + |
| 86 | + len = strlen(line); |
| 87 | + for (i = 0; i < len; i++) |
| 88 | + { |
| 89 | + /* allow special char */ |
| 90 | + if (line[i] == '\t' || |
| 91 | + line[i] == '\n' || |
| 92 | + line[i] == '\r' || |
| 93 | + line[i] == ' ') |
| 94 | + continue; |
| 95 | + |
| 96 | + if (line[i] == '/') |
| 97 | + { |
| 98 | + if (has_slash) |
| 99 | + break; |
| 100 | + has_slash = true; |
| 101 | + continue; |
| 102 | + } |
| 103 | + /* others */ |
| 104 | + break; |
| 105 | + } |
| 106 | + |
| 107 | + if (i == len && has_slash) |
| 108 | + result = true; |
| 109 | + } |
| 110 | + break; |
| 111 | + default: |
| 112 | + break; |
| 113 | + } |
| 114 | + |
| 115 | + return result; |
| 116 | +} |
| 117 | + |
| 118 | +``` |
| 119 | + |
| 120 | +=== SQL端 |
| 121 | + |
| 122 | +SQL端要能够识别函数和存储过程的创建语法,这是通过修改ora_base_yylex来实现的。这个函数预取并缓存token,如果是Oracle语法格式则组织一个SCONST发送给PLSQL端,否则从堆栈中获取之前预读的token,按照原生PG的逻辑进行处理。 |
| 123 | + |
| 124 | +在ora_base_yy_extra_type数据结构中增加如下字段: |
| 125 | +``` |
| 126 | + /* |
| 127 | + * The native PG only cache one-token info include yylloc, yylval and token |
| 128 | + * number in yyextra, IvorySQL cache multiple tokens info using two arrays. |
| 129 | + */ |
| 130 | + int max_pushbacks; /* the max size of cache array */ |
| 131 | + int loc_pushback; /* # of used tokens */ |
| 132 | + int num_pushbacks; /* # of cached tokens */ |
| 133 | + int *pushback_token; /* token number array */ |
| 134 | + TokenAuxData *pushback_auxdata; /* auxdata array */ |
| 135 | + |
| 136 | + OraBodyStyle body_style; |
| 137 | + int body_start; |
| 138 | + int body_level; |
| 139 | +``` |
| 140 | + |
| 141 | +增加token堆栈的操作接口: |
| 142 | +|==== |
| 143 | +| push_back_token |
| 144 | +| forward_token |
| 145 | +| ora_internal_yylex |
| 146 | +| internal_yylex |
| 147 | +|==== |
| 148 | + |
| 149 | +ora_base_yylex函数中在创建函数、过程、匿名块时会预读部分token,使用上述结构缓存到堆栈中,是为了构造一个符合Oracle PL/SQL语法的SCONST发送给PL/iSQL端去处理。具体请参考源代码。 |
| 150 | + |
| 151 | + |
| 152 | +=== PL/iSQL端 |
| 153 | + |
| 154 | +该部分主要修改了pl_gram.y文件,以兼容PLSQL的函数和存储过程语法,在不影响PG原生的PL/pgSQL的前提下去兼容Oracle PL/SQL语法形式,如下是DECLARE部分兼容的代码示例,更多请参考IvorySQL源代码。 |
| 155 | + |
| 156 | +``` |
| 157 | +/* |
| 158 | + * The declaration section of the outermost block in Oracle does not have the DECLARE keyword. |
| 159 | + */ |
| 160 | +ora_outermost_pl_block: ora_decl_sect K_BEGIN proc_sect exception_sect K_END opt_label |
| 161 | + { |
| 162 | + PLiSQL_stmt_block *new; |
| 163 | + |
| 164 | + new = palloc0(sizeof(PLiSQL_stmt_block)); |
| 165 | + |
| 166 | + new->cmd_type = PLISQL_STMT_BLOCK; |
| 167 | + new->lineno = plisql_location_to_lineno(@2); |
| 168 | + new->stmtid = ++plisql_curr_compile->nstatements; |
| 169 | + new->label = $1.label; |
| 170 | + new->n_initvars = $1.n_initvars; |
| 171 | + new->initvarnos = $1.initvarnos; |
| 172 | + new->body = $3; |
| 173 | + new->exceptions = $4; |
| 174 | + |
| 175 | + check_labels($1.label, $6, @6); |
| 176 | + plisql_ns_pop(); |
| 177 | + |
| 178 | + $$ = (PLiSQL_stmt *)new; |
| 179 | + } |
| 180 | + ; |
| 181 | + |
| 182 | +ora_decl_sect: opt_block_label opt_ora_decl_start opt_ora_decl_stmts |
| 183 | + { |
| 184 | + if ($2) |
| 185 | + { |
| 186 | + if ($1 == NULL) |
| 187 | + { |
| 188 | + plisql_ns_push(NULL, PLISQL_LABEL_BLOCK); |
| 189 | + } |
| 190 | + } |
| 191 | + } |
| 192 | + opt_ora_decl_stmts |
| 193 | + { |
| 194 | + if ($4) |
| 195 | + { |
| 196 | + plisql_IdentifierLookup = IDENTIFIER_LOOKUP_NORMAL; |
| 197 | + $$.label = ($1 == NULL ? plisql_curr_compile->namelabel : $1); |
| 198 | + if ($2 && $1 == NULL) |
| 199 | + $$.popname = true; |
| 200 | + else |
| 201 | + $$.popname = false; |
| 202 | + /* Remember variables declared in decl_stmts */ |
| 203 | + $$.n_initvars = plisql_add_initdatums(&($$.initvarnos)); |
| 204 | + } |
| 205 | + else |
| 206 | + { |
| 207 | + plisql_IdentifierLookup = IDENTIFIER_LOOKUP_NORMAL; |
| 208 | + $$.label = ($1 == NULL ? plisql_curr_compile->namelabel : $1); |
| 209 | + $$.n_initvars = 0; |
| 210 | + if ($2 && $1 == NULL) |
| 211 | + $$.popname = true; |
| 212 | + else |
| 213 | + $$.popname = false; |
| 214 | + $$.initvarnos = NULL; |
| 215 | + } |
| 216 | + } |
| 217 | + ; |
| 218 | + |
| 219 | +opt_ora_decl_start: K_DECLARE |
| 220 | + { |
| 221 | + /* Forget any variables created before block */ |
| 222 | + plisql_add_initdatums(NULL); |
| 223 | + /* |
| 224 | + * Disable scanner lookup of identifiers while |
| 225 | + * we process the decl_stmts |
| 226 | + */ |
| 227 | + plisql_IdentifierLookup = IDENTIFIER_LOOKUP_DECLARE; |
| 228 | + $$ = true; |
| 229 | + } |
| 230 | + | /*EMPTY*/ |
| 231 | + { |
| 232 | + /* Forget any variables created before block */ |
| 233 | + plisql_add_initdatums(NULL); |
| 234 | + /* |
| 235 | + * Disable scanner lookup of identifiers while |
| 236 | + * we process the decl_stmts |
| 237 | + */ |
| 238 | + plisql_IdentifierLookup = IDENTIFIER_LOOKUP_DECLARE; |
| 239 | + $$ = false; |
| 240 | + } |
| 241 | + ; |
| 242 | + |
| 243 | +opt_ora_decl_stmts: |
| 244 | + ora_decl_stmts |
| 245 | + { |
| 246 | + $$ = true; |
| 247 | + } |
| 248 | + | /*EMPTY*/ |
| 249 | + { |
| 250 | + $$ = false; |
| 251 | + } |
| 252 | + |
| 253 | +ora_decl_stmts: ora_decl_stmts ora_decl_stmt |
| 254 | + | ora_decl_stmt |
| 255 | + ; |
| 256 | + |
| 257 | +ora_decl_stmt: decl_statement |
| 258 | + ; |
| 259 | + |
| 260 | +``` |
| 261 | + |
0 commit comments