大家好,欢迎来到IT知识分享网。
邵大明
中国PG分会认证专家
PostgreSQL资深内核研发工程师
文章内容较长,建议先收藏,PC端阅读效果更佳。
目录
1 实现文件
2 概述
3 数据结构
4 Flex
4.1 scan.l 中涉及的Flex基本语法
5 Bison
5.1 gram.y中Bison基本语法
6 raw_parser
7 案例解析
7.1 函数调用关系
7.2 分析过程
实现文件
src/backend/parser 目录;
scan.l gram.y
概述
词法分析和语法分析主要通过Flex和Bison配合实现的。Flex负责做sql中关键字的事别,然后转换成token给Bison使用。Bison 根据token,定义语法并匹配语法,转换成parsetree.
数据结构
typedef struct RawStmt //parsetree 节点类型, parsetree 就是有RawStmt类型的节点构成的链表. RawStmt 是各类型节点在parsetree中的包装节点。 { NodeTagtype; //类型定义在了gram.y中,如下stmt所示. Node *stmt;/* raw parse tree */stmt值内容 intstmt_location;/* start location, or -1 if unknown */ intstmt_len;/* length in bytes; 0 means "rest of string" */ } RawStmt;
IT知识分享网
Flex
scan.l 中涉及的Flex基本语法
概述:
Flex 文件即scan.l,由%%分为三段,定义段(此段可以通过%{… %}包含C的代码,这部分代码也会被原样copy到生成的C文件中,还有一些参数项通过%option来设置,还有一些代理项,通过代理可以简化规则段的书写),规则段(此段的规则会对应到生成的yylex函数中),代码段(此段被原样copy到生成的C源码文件中即scan.c)
定义段:
1) %top{ … } 这部分,括号中内容将被原样copy到生成的scan.c中,并且位于C文件的最顶部。此部分的作用就是加入一些此文件描述注释,以及需要include的头文件。
2) %{ … %}这部分是的代码会被原样copy到生成的C文件中。在这里可以重定义一些Flex中的宏,如YYSTYPE, 以及一些在规则段使用的函数声明,和结构体声明和定义。
3) %option 此部分是Flex支持的一些参数,通过%option 来设置
%option reentrant 可重入词法分析器,传统词法分析器只能一次处理一个输入流,所以很多变量都定义的为静态变量这样分析器才能记住上次分析的地方继而可以继续分析。但是不能同时处理多个输入流。为了解决这个问题引入了可重入词法分析器。通过参数reentrant来控制。这样通过一个yyscan_t类型变量来保存中间状态。yyscan_t为void *,但是在yylex_init初始化的时候是分配了sizeof(yyguts_t)大小的空间,yyguts_t 是一个结构体,保存了所有需要记录的中间的状态。通过这个结构体里的变量替换了原来的静态变量。即在yylex中这个yygut_t类型的变量被传入,这样就通过这个结构体保存了所有原来很多静态变量保存的值。实现了多个输入流同时被分析。只要针对每个输入流,创建一个yyscan_t类型的scanner.传入到yylex中。
%option prefix=”core_yy” 通过加入前缀,可以将原来的yylex等函数 变成core_yylex.这样可以在一个程序中建立多个词法分析器。用来分析不同的输入流。
%option bison-bridge ,bison桥模式,为什么会有这个模式呢,因为bison的发展和flex的发展沟通并不是很密切,导致了一个不好的情况,即在bison调用yylex的时候是yylex(YYSTYPE *yylvalp); 即必须传入一个yylval的指针,但是flex中定义的yylex函数为int yylex(yyscan_t scaninfo).这样两者就不一样了。就无法互相协作的工作了。所以在flex中提拱了桥模式,如果按%option bison-bridge做了声明,那么在flex中yylex将被声明为int yylex(YYSTYPE* lvalp, yyscan_t scaninfo);这样就兼容了bison.
%option bison-locations 此模式同上面参数同时使用,如果做了此声明,yylex 将被声明为int yylex (YYSTYPE* lvalp, YYLTYPE* llocp, yyscan_t scaninfo);加入了location参数。而在flex中yylex 中宏yylval 和 yylloc其实就是lvalp 和llocp的一个拷贝。
4) %x 定义 开始状态,开始状态代表进入一个特定的状态,在规则段只有定义了特定状态的规则才会匹配,这种规则通过<start stat>来标识。例如 定义段定义了 %x xb 则在规则段只有<xb>开头的规则才会匹配,其他的的规则则不会被匹配。
5) 代理器, 代理器,可以为一些要匹配的表达式命名,这样在规则段可以用这个代理名子,来代替这个表达式。例如space [ \t\n\r\f],给[ \t\n\r\f] 命名为space, 后面在规则段即可使用{space}来代替[ \t\n\r\f],同时代理也可以被嵌套,如whitespace ({space}+|{comment}) 这里定义了新的代理whitespace, 它代理了 ({space}+|{comment}) 其中{space}就被嵌套代理了。
规则段:
1) 通过{代理器} 来表示要匹配的字符, 后面跟着{ … }大括号中的内容就是要生成的C代码,即前面的字符配匹配后,则会对应调用后面大括号内的C代码。同时后面大括号内容可以为空,即表示忽略这个匹配的字符。这些规则将被生成scan.c中的yylex函数。例如:
IT知识分享网 {whitespace} {}//这里的意思是凡是whitespace, 则忽略 {xcstart} { SET_YYLLOC(); yyextra->xcdepth = 0; BEGIN(xc);yyless(2);}//这里的意思是当匹配xcstart时,要做后面的操作。
2) BEGIN, Flex 初始状态为 INITIAL, BEGIN (start stat) 意思是开始进入一个新的状态. 进入这个状态后,只有定义了对应状态的规则才会被匹配。例如:
上面调用BEGIN(xc)之后,即匹配xcstart之后,进入xc状态,所以只有<xc>{xcstart} { … }等开头带<xc>的代理器才会被匹配。其他的任何规则都不会被匹配。
3) yyless(), 此函数意思是保留参数个数的字符流,其他的返回给输入流。但是匹配的字符流长度,为匹配的总长度。例如:
如果yyless(2),这个时候匹配的字符是5个字符,则保留前两个字符,后面三个字符返回给输入流。
4) yyextra, 此变量是用户传入的参数数据,其类型为YY_EXTRA_TYPE定义,其值默认为void*, 但是可以根据需要自定义,这里定义为了core_yy_extra_type *, 此参数是通过两个途径传入,通过yylex_init_extra(yyext, &scanner)定义,或者调用yylex_init(&scanner)之后,调用yyset_extra(yyext, scanner);设置进去,其实就是把这个extra data 赋给了scanner中的成员变量yyextra_r。这里yyext即是extra data.此方法是在yylex函数外调用的。在yylex中可以直接通过yyextra宏(这个宏展开后就是scanner的成员变量yyextra_r)直接读取这个参数值,也可以把结果作为此参数的一部分返回。此参数必须是 Reentrant Scanner中使用即打开了参数%option reentrant.
5) yylval yylex(在Bison yyparse 函数中调用)返回值可以理解分为两部分,一个是在规则中的return 值,此为返回的token, 另一个是与之一一对应的yylval.yylval 的类型为YYSTYPE, 可以根据用户需要重新定义。一般定义为一个union类型,在Bison中将token和这个union中的一个变量绑定。然后在scan.l中直接将对应值设置到yylval->(union 的成员变量).如此这般,在yyparse中即可获取到,token 和 其对应的值。
6) yytext 其为一个字符数组,里面存放的是匹配的字符串。
7) yylloc 其为位置信息,即匹配的token在整个输入流中的位置。通过这个值反馈给Bison中的yyparse.
8) yyalloc 默认是malloc,这里通过%opiton noyylloc 屏蔽掉了,并定义了自己的版本为palloc
9) yyrealloc 默认realloc, 通过%option noyyrealloc屏蔽,并定义了自己的版本 repalloc
10) yyfree 默认为free, 通过%option noyyfree 屏蔽,并定义了自己的版本pfree
11) yyleng 是匹配token对应字符串的长度。
Bison
gram.y中Bison基本语法
概述:
Bison是yacc在GNU的实现。用来做语法分析,可以同Flex一起合作实现对sql语句的解析。同样分为三段,定义段,规则段和代码段。也是通过%%做三个段的分割。源码文件为gram.y, 最后通过Bison 编译源文件生成 gram.c.
Bison的工作原理主要通过两个堆栈,和shift,reduce操作来完成。一个堆栈是负责处理符号的包括终结符号,非终结符符号。另一个堆栈是处理与之一一对应的值堆栈。
shift操纵,是从一个规则中冒号右侧的终结符号一个一个移动。例如 stmt1 : stmt2 OR stmt3 { … }; 这个规则中先匹配stmt2,然后做shift,匹配OR,直到最后shift到最后一个终结符号stmt3.每次shift操作,会将对应终结符号存入符号堆栈,匹配的值存入值堆栈。
reduce 规约操作,当匹配到最后一个终结符号后,将符号堆栈和值堆栈中的所有值一起出栈,根据后面{ … }对应的运算处理,做完运算,将运算结果压入值堆栈,与之对应的符号压入符号堆栈。例如上面的例子中将{ … }中的运算结果压入值堆栈,将stmt1 压入符号堆栈。代表的意思就是stmt1 的值 为{ … }对应的运算结果。
定义段:
1) {% … %}中的代码将被原样copy到生成的文件gram.c中.其中包含头文件包含,结构体定义和函数声明等
2) %pure-parser 声明此语法分析器是纯语法分析器。这样可以实现可重入。同时需要%parse-param {core_yyscan_t yyscanner} %lex-param {core_yyscan_t yyscanner}配合使用,即为了调用纯词法分析器flex,需要scanner实例,即需要传入这个参数.通过定义%parse-param 即可给yyparse()函数传入参数。定义%lex-param.即可把parse-param中定义的参数传递给yylex.
3) %expect 0 ,意思是期待0个冲突。即不希望有任何冲突出现
4) %name-prefix=”base_yy” 代表生成的函数和变量名从yy改成base_yy,同flex,为了在一个产品里使用多个语法分析器,分析不同的数据类。
5) %locations 声明使用位置信息。
6) %union{} 定义yylval类型,在flex中通过yylval的返回匹配的值。
7) %type< union中的变量名 > 非终结符 ,此语法是定义非终结符(在规则段,: 左边是非终结符,右边是终结符,终结符)和union中变量的绑定。在Bison中,每个符号(终结符和非终结符)都有一个值与之对应。默认是一个整数值,为了扩展,可以定义union类型。$代表非终结符的值。这里的意思就是把$和union中某一个变量绑定。
8) %token< union 中的变量名 > 总结符, 此语法是定义终结符和union中变量的绑定。这样就可以在flex中直接通过yylval->(union中的变量名)返回匹配的值
9) %nonassoc symbol, 用来定义有限集的。同%prec 联合使用可以定义某个表达式的优先级。例如
%nonassoc UMINUS 定义了个UMINUS.%%exp: '-' exp %prec UMINUS{ ... }这里的意思就是的('-' exp)运算优先级同UMINUS的优先级。
10) %left 代表操作符左匹配 例如:%left AND
11) %right 代表操作符右匹配 例如:%right NOT
12) 优先级是通过由低到高,例如:
IT知识分享网 %nonassoc SET /* see relation_expr_opt_alias */ %left UNION EXCEPT//在同一行代表优先级相同。 %left INTERSECT %left OR %left AND//这里优先级最高 依次大于上面的OR INTERSECT... SET 优先级最低。
规则段:
1)
stmtblock: stmtmulti //stmtblock是非终结符号,冒号语法分隔符, stmtmulti代表终结符号,其值通过位置获取,因只有一个终结符,所以其值为$1. stmtblock的值,用$表示。 { pg_yyget_extra(yyscanner)->parsetree = $1; //在生成的C文件中,用{}括起来的内容是用来替换前面匹配的此规则, } ;//使用; 作为此规则的结束符。
2) 通过@来获取终结符在规则中的位置信息. 例如:上面的规则中@1,代表的是stmtmulti对应的位置信息。这个位置信息是flex设置的。
3) $ 代表非终结符对应的值,即规则中冒号左边符号对应的值。例如:
CreateOptRoleElem: AlterOptRoleElem { $ = $1; }//$代表冒号左边非终结符号的值,即CreateOptRoleElem的值。
4) $NUM 代表终结符对应的值.例如:
CreateOptRoleElem: AlterOptRoleElem { $ = $1; }//$1代表冒号右边第一个终结符的值即AlterOptRoleElem的值。
5) ‘|’ 代表 or 的关系, 例如:
opt_unique: UNIQUE { $ = TRUE; }//如果匹配到UNIQUE 这个token,会reduce为opt_unique | /*EMPTY*/ { $ = FALSE; }//如果匹配到空,也会reduce为opt_unique. ;
6) token 都是大写字母表示(不是必须,但是一般采用大写表示token.)。
raw_parser
List * raw_parser(const char *str) { core_yyscan_t yyscanner;// void * 指针 base_yy_extra_type yyextra; //定义一个extra data. intyyresult; /* initialize the flex scanner */ yyscanner = scanner_init(str, &yyextra.core_yy_extra,ScanKeywords, NumScanKeywords);//core_yy_extra为YY_EXTRA_TYPE类型, YY_EXTRA_TYPE 宏被define为core_yy_extra_type *类型。 //ScanKewWords,是一个const ScanKeyword ScanKeywords[] = { #include "parser/kwlist.h"};kwlist.h中是预先定义的关键字,NumScanKeywords是这个数组的长度。 //这个yyscanner会被传进flex中用于保存全局状态。即在yylex中可以通过这个scanner保存解析的状态。以及使用scanner中传入的用户的数据即extra data. { Sizeslen = strlen(str); yyscan_tscanner; if (core_yylex_init(&scanner) != 0)//初始化scanner, scanner被初始化为yyguts_t类型的结构体大小。并初始化结构体中各变量。 elog(ERROR, "core_yylex_init() failed: %m"); core_yyset_extra(yyext, scanner);把yyext赋给scanner->yyextra_r yyext->keywords = keywords;//赋值到extra data中,这样就可以在yylex中使用这个数组。 yyext->num_keywords = num_keywords; //GUC参数 yyext->backslash_quote = backslash_quote; yyext->escape_string_warning = escape_string_warning; yyext->standard_conforming_strings = standard_conforming_strings; yyext->scanbuf = (char *) palloc(slen + 2);//用户存放要解析的字符串 yyext->scanbuflen = slen; memcpy(yyext->scanbuf, str, slen); yyext->scanbuf[slen] = yyext->scanbuf[slen + 1] = YY_END_OF_BUFFER_CHAR; core_yy_scan_buffer(yyext->scanbuf,slen + 2,scanner);//把这个buffer设置为yylex的输入。 yyext->literalalloc = 1024; yyext->literalbuf = (char *) palloc(yyext->literalalloc); yyext->literallen = 0; } /* base_yylex() only needs this much initialization */ yyextra.have_lookahead = false; /* initialize the bison parser */ parser_init(&yyextra);//yyextra->parsetree = NIL;清空parsetree,parsetree,及我们要返回的结果。 /* Parse! */ yyresult = base_yyparse(yyscanner);//调用Bison的yyparse做词法和语法分析,并把结果放到yyextra.parsetree中 /*释放使用的内存资源*/ scanner_finish(yyscanner); if (yyresult)/* error */ return NIL; return yyextra.parsetree; }
案例解析
解析语句:
INSERT INTO films (code, title, did, date_prod, kind) VALUES ('T_601', 'Yojimbo', 106, '1961-06-16', 'Drama');
函数调用关系:
PostgresMain->exec_simple_query->pg_parse_query->raw_parser->base_yyparse(yyscanner)
分析过程:
分析过程采用直接分析gram.y和scan.l 的方式。
我们先从scan.l中开始分析。
1) 在yyparse中通过调用yylex来分析第一个单词这需要注意一下:在 在gram.y中已经使用了%name-prefix=”base_yy”,所以这里的yyparse,和yylex,实际上最后都是调用的base_yyparse,和base_yylex,这里需要关心的是base_yylex.因为在scan.l中定义了%option prefix=”core_yy”,所以在scan.c中生成的yylex的函数名被重定义为来了core_yylex.这里出现了不匹配,PG,是通过自己写了base_yylex函数,其中调用了core_yylex. 那为什么要这么做呢,因为Flex解析的时候默认只是向前多看一个token,来做匹配的, 但是在SQL中有些语句匹配是需要向前多看多于一个token的,为了实现这点,PG采用在base_yylex中读取token,然后做替换的方式实现匹配多个token.例如 如果是 WITH TIME,那么这两个token 将被替换成WITH_LA.
在定义段有如下代理器。identifier 可以匹配INSERT.
digit [0-9] ident_start [A-Za-z\200-\377_] ident_cont [A-Za-z\200-\377_0-9\$] identifier {ident_start}{ident_cont}*
2) 所以在规则段,查找{identifier} 如下:
{identifier} { const ScanKeyword *keyword; char *ident; SET_YYLLOC(); /* 在关键字列表yyextra->keywords中查找匹配的字符串是否为关键字 */ScanKeywordLookup函数被声明在keywords.h中keywords.h被包含在scanner.h中,scanner.h被包含在gramparse.h中,gramparse.h被scan.l引用. keyword = ScanKeywordLookup(yytext, yyextra->keywords,这变量是我们传入的kwlist.h中的所有关键字. yyextra->num_keywords); if (keyword != NULL)//如果找到 在kwlist.h中存在PG_KEYWORD("insert", INSERT, UNRESERVED_KEYWORD) { yylval->keyword = keyword->name;//token 对应的值这里为 “insert” return keyword->value;//返回token, 这里为宏“INSERT” } /* * No. Convert the identifier to lower case, and truncate * if necessary. */ ident = downcase_truncate_identifier(yytext, yyleng, true); yylval->str = ident; return IDENT; }
3) 在yylex返回INSERT 这个token.然后分析gram.y中这个token 对应的规则 由于flex 默认向前查看一个token, 根据第二部可知第二个token 为INTO.在规则段中找到如下规则
InsertStmt: opt_with_clause INSERT INTO insert_target insert_rest opt_on_conflict returning_clause { $5->relation = $4;//将insert_target值赋给了insert_rest返回值InsertStmt Node中的relation. $5->onConflictClause = $6;//$6为空 $5->returningList = $7;//$7为空. $5->withClause = $1;//$1为空 $ = (Node *) $5;//将填好的InsertStmt Node, 赋值给InsertStmt } ;
opt_with_clause 可以为空,并且后面跟着一个INSERT INTO, 所以即匹配上这个规则。
4) 继续调用yylex分析 films, films 根据第二部,返回token IDENT.查看insert_target 的规则,
insert_target-->qualified_name-->ColId-->IDENT
那顺着IDENT,找到ColId,这一步规则的reduce, 采用了如下置换:
insert_target: qualified_name { $ = $1;//将RangeVar类型node赋给了insert_target } qualified_name: ColId { $ = makeRangeVar(NULL, $1, @1);//创建了一个RangeVar类型node,参考RangeVar结构体。并赋给了$即qualified_name. }
5) 继续调用yylex分析,在gram.y中规则InsertStmt,指示下一个需要匹配的项是insert_rest.继续分析输入流,发现后面是一个(Column list).
insert_rest: SelectStmt { $ = makeNode(InsertStmt); $->cols = NIL; $->selectStmt = $1; } | OVERRIDING override_kind VALUE_P SelectStmt { $ = makeNode(InsertStmt); $->cols = NIL; $->override = $2; $->selectStmt = $4; } | '(' insert_column_list ')' SelectStmt // 根据输入流匹配这一项, 建立InsertStmt结构,并赋值给insert_rest. { $ = makeNode(InsertStmt); $->cols = $2; $->selectStmt = $4; } | '(' insert_column_list ')' OVERRIDING override_kind VALUE_P SelectStmt { $ = makeNode(InsertStmt); $->cols = $2; $->override = $5; $->selectStmt = $7; } | DEFAULT VALUES { $ = makeNode(InsertStmt); $->cols = NIL; $->selectStmt = NULL; } ;
6) 继续分析(code, title, did, date_prod, kind)括号内对应的字段应该最后匹配到insert_column_list.所以这里要沿着insert_column_list 规则继续分析。
insert_column_list: insert_column_item { $ = list_make1($1); }//建立链表并赋给insert_column_list。 | insert_column_list ',' insert_column_item { $ = lappend($1, $3); }//如果是多个,都添加到列表内并赋给insert_column_list。 ; insert_column_item: ColId opt_indirection { $ = makeNode(ResTarget);//创建ResTarget节点,参考ResTarget结构体。最后把结果值赋给insert_column_item. $->name = $1; $->indirection = check_indirection($2, yyscanner); $->val = NULL; $->location = @1; } ; ColId: IDENT { $ = $1; }//这里按照步骤2.中匹配返回IDENT,并赋值给ColId. | unreserved_keyword { $ = pstrdup($1); } | col_name_keyword { $ = pstrdup($1); } ; opt_indirection: /*EMPTY*/ { $ = NIL; }//这里匹配空 | opt_indirection indirection_el { $ = lappend($1, $2); } ;
7) 继续分析,后面跟着一个关键字VALUES, 沿着规则继续查找,最后匹配到values_clause规则。
SelectStmt: select_no_parens %prec UMINUS //将select_no_parens 赋给SelectStmt select_no_parens: simple_select { $ = $1; } ///将simple_select赋给 select_no_parens simple_select: values_clause { $ = $1; }//将values_clause赋给simple_select values_clause: VALUES '(' expr_list ')' { SelectStmt *n = makeNode(SelectStmt);//建立SelectStmt节点,参考SelectStmt结构体。 n->valuesLists = list_make1($3);//将后面匹配的value list 加入到SelectStmt结构体中。 $ = (Node *) n;//将结果赋给values_clause. }
8) 继续分析, 后面是跟着的插入值(‘T_601’, ‘Yojimbo’, 106, ‘1961-06-16’, ‘Drama’)。
expr_list: a_expr //通用表达式 { $ = list_make1($1);//建立列表,并赋值给expr_list } | expr_list ',' a_expr//递归嵌套,多个通用表达式 { $ = lappend($1, $3);//把新匹配的值加入到列表中,并赋值给expr_list } ; a_expr: c_expr { $ = $1; }//将c_expr 的值赋值给a_expr c_expr: AexprConst { $ = $1; }//将AexprConst 的值赋值给c_expr AexprConst: Sconst { $ = makeStringConst($1, @1);//根据Sconst的值,和对应位置信息。建立A_Const节点,类型为T_String,并赋值给AexprConst } ; Sconst: SCONST { $ = $1; };//将token SCONST匹配的对应值赋值给Sconst。 在scan.l中通过如下规则段匹配对应的SCONST. quote '//单引号代理器. quotestop {quote}{whitespace}* xqstart {quote}//引号代理器 xqdouble {quote}{quote} xqinside [^']+//引号内部的内容 %% {xqstart} { yyextra->warn_on_first_escape = true; yyextra->saw_non_ascii = false; SET_YYLLOC();//设置位置信息,以便yyparse()使用。 if (yyextra->standard_conforming_strings) BEGIN(xq);//如果没配置standard_conforming_strings,默认值为true else BEGIN(xe); startlit(); } <xq,xus>{xqinside} { addlit(yytext, yyleng, yyscanner);//读取yytext值到yyextra->literalbuf中 } <xq,xe>{quotestop} | <xq,xe>{quotefail} { yyless(1);只读取‘其他字符退回输入流 BEGIN(INITIAL); /* * check that the data remains valid if it might have been * made invalid by unescaping any chars. */ if (yyextra->saw_non_ascii) pg_verifymbstr(yyextra->literalbuf, yyextra->literallen, false); yylval->str = litbufdup(yyscanner);//读取字符串 return SCONST;//返回token }
9) 到目前为止,已经分析完输入的sql 语句,结果已经放到InsertStmt中,后面继续根据以下规则做规约处理,由于在规则段中第一个出现的非终结符号,stmtblock是我们要的结果。通过不断的规约即reduce,最后的分析结果即剩下stmtblock这一个符号。
stmtblock: stmtmulti { pg_yyget_extra(yyscanner)->parsetree = $1;//从这条语句可知,就是把stmtmulti的值直接付给了scanner(是一开始在raw_parser中生成并传入到base_yyparse的变量)里的extra data中的parsetree。到这里代表整个分析过程结束。 }; stmtmulti: stmtmulti ';' stmt { if ($1 != NIL) { /* update length of previous stmt */ updateRawStmtEnd(llast_node(RawStmt, $1), @2); } if ($3 != NULL) $ = lappend($1, makeRawStmt($3, @2 + 1)); else $ = $1; } | stmt { if ($1 != NULL) $ = list_make1(makeRawStmt($1, 0));//将stmt中的值,即InsertStmt中的值 $1,作为参数生成RawStmt并加入到列表中。 else $ = NIL; }; stmt : AlterEventTrigStmt | AlterCollationStmt | AlterDatabaseStmt | AlterDatabaseSetStmt .... |InsertStmt//这里要重点分析的语句从前面分析的结果已经放到InsertStmt中
原文链接:http://www.postgresqlchina.com/tecdocdetail/1
免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://yundeesoft.com/6562.html