ivorysql 源码分析-双port兼容

ivorysql oracle兼容框架

ivorysql采用双端口的方式实现oracle兼容,默认的5432端口为pg语法,1532用来支持oracle的语法与功能,从官方的流程图上看是支持oracle应用连接的,但是貌似没有支持oracle的协议,以下为支持oracle兼容模式的提交的commit

实现双端口主要需要实现

  1. postmaster进程新增监听的端口1521(或其他可设置的端口号)
  2. 1521监听到的连接对应为支持oracle兼容的连接
  3. 添加参数控制隔离默认的pg行为
  4. 新增参数
  5. 新增系统函数
postmaster进程新增监听的端口
c 复制代码
void
PostmasterMain(int argc, char *argv[]){
    //...
//创建oracle tcp/ip socket, database_mode用于隔离pg默认行为
if (DB_ORACLE == database_mode && OraListenAddresses)
	{
		char	   *rawstring;
		List	   *elemlist;
		ListCell   *l;
		int 		success = 0;

		/* Need a modifiable copy of ListenAddresses */
		rawstring = pstrdup(OraListenAddresses);

		/* Parse string into list of hostnames */
		if (!SplitGUCList(rawstring, ',', &elemlist))
		{
			/* syntax error in list */
			ereport(FATAL,
					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
					 errmsg("invalid list syntax in parameter \"%s\"",
							"listen_addresses")));
		}

		foreach(l, elemlist)
		{
			char	   *curhost = (char *) lfirst(l);

			if (strcmp(curhost, "*") == 0)
				status = StreamServerPort(AF_UNSPEC, NULL,
										  (unsigned short) OraPortNumber,
										  NULL,
										  ListenSocket, MAXLISTEN);
			else
				status = StreamServerPort(AF_UNSPEC, curhost,
										  (unsigned short) OraPortNumber,
										  NULL,
										  ListenSocket, MAXLISTEN);

			if (status == STATUS_OK)
			{
				success++;
				/* record the first successful host addr in lockfile */
				if (!listen_addr_saved)
				{
					AddToDataDirLockFile(LOCK_FILE_LINE_LISTEN_ADDR, curhost);
					listen_addr_saved = true;
				}
			}
			else
				ereport(WARNING,
						(errmsg("could not create listen socket for \"%s\"",
								curhost)));
		}

		if (!success && elemlist != NIL)
			ereport(FATAL,
					(errmsg("could not create any Oracle TCP/IP sockets")));

		list_free(elemlist);
		pfree(rawstring);
	}
   //...
//创建oracle本地Unix Domain Socket
		if (DB_ORACLE == database_mode)
		{
			success = 0;
			foreach(l, elemlist)
			{
				char	   *socketdir = (char *) lfirst(l);

				status = StreamServerPort(AF_UNIX, NULL,
										  (unsigned short) OraPortNumber,
										  socketdir,
										  ListenSocket, MAXLISTEN);

				if (status == STATUS_OK)
				{
					success++;
					/* record the first successful Unix socket in lockfile */
					if (success == 1)
						AddToDataDirLockFile(LOCK_FILE_LINE_SOCKET_DIR, socketdir);
				}
				else
					ereport(WARNING,
							(errmsg("could not create Oracle Unix-domain socket in directory \"%s\"",
									socketdir)));
			}

			if (!success && elemlist != NIL)
				ereport(FATAL,
						(errmsg("could not create any Oracle Unix-domain sockets")));
		}
    //...
   }
新增端口监听到的连接对应为支持oracle兼容的连接
c 复制代码
//Port新增字段connmode,标识当前连接是什么兼容模式
typedef struct Port
{
	pgsocket	sock;			/* File descriptor */
	//...

	/*
	 * Connection Mode.
	 *
	 * Use single-byte characters to identify the mode information of the
	 * current connection. The valid characters are as follows:
	 *	'p' =  PostgreSQL mode,
	 *	'o' =  Oracle mode,
	 *	'u' = Unkonw mode, shouldn't happen.
	 */
	char	connmode;

} Port;

//connmode字段赋值
PostmasterMain
    ServerLoop
    	BackendStartup
    		BackendInitialize //子进程自己赋值connmode字段

BackendInitialize(Port *port){
 //...
 //对比localport是否为oracle端口号,是就给port->connmode赋值'o'
 	service[0] = '\0';
	if ((ret = pg_getnameinfo_all(&port->laddr.addr, port->laddr.salen,
									NULL, 0,
									service, sizeof(service),
									NI_NUMERICSERV)) != 0)
		ereport(WARNING,
				(errmsg_internal("pg_getnameinfo_all() failed: %s",
								 gai_strerror(ret))));

	localport = atoi(service);
    //atoi,异常情况直接用strstr对比字符串
	if (localport == 0)
	{
		char	PostPortNumberStr[32];
		char	OraPortNumberStr[32];

		snprintf(PostPortNumberStr, sizeof(PostPortNumberStr), "%d", PostPortNumber);
		snprintf(OraPortNumberStr, sizeof(OraPortNumberStr), "%d", OraPortNumber);

		if (strstr(service, PostPortNumberStr) != NULL)
		{
			port->connmode = 'p';
		}
		else if (strstr(service, OraPortNumberStr) != NULL)
		{
			port->connmode = 'o';
		}
		else
			port->connmode = 'u';
	}
	else
	{
		if (localport == PostPortNumber)
		{
			port->connmode = 'p';
		}
		else if (localport == OraPortNumber)
		{
			port->connmode = 'o';
		}
		else
			port->connmode = 'u';
	}
}
隔离原生pg的行为

数据库初始化时可指定database_mode,database_mode==oracle时监听oracle端口,用于隔离原生pg的行为

c 复制代码
//initdb 新增参数-m [oracle/pg],指定时 bootstrap转为 -y oracle/pg ,默认为oracle
main
    initialize_data_directory
    	bootstrap_template1
            snprintf(cmd, sizeof(cmd),
                         "\"%s\" --boot -C ivorysql.identifier_case_switch=%d -X %d %s %s %s %s %s",
                         backend_exec,
                         caseswitchmode,
                         wal_segment_size_mb * (1024 * 1024),
                         pg_strcasecmp(dbmode, "pg") ? "-y oracle" : "-y pg",
                         data_checksums ? "-k" : "",
                         boot_options, extra_options,
                         debug ? "-d 5" : "");
//database_mode持久化
BootstrapModeMain
    //Bootstrap 新增 "-y" 指定pg或者oracle,为bootstrap_database_mode
    //bootstrap_database_mode = DB_ORACLE;
	//bootstrap_database_mode = DB_PG;
    BootStrapXLOG
    	ControlFile->dbmode = bootstrap_database_mode;	//后续start,restart,或者crash恢复 会从controlfile中读取dbmode字段,实现双端口
		WriteControlFile

LocalProcessControlFile
            ReadControlFile		//读取dbmode字段 并赋值ivorysql.database_mode
新增区别于原生pg的一些参数
参数名 解释
ivorysql.port 等同于pg端口的参数port,监听oracle兼容连接的端口号,对应内核参数OraPortNumber
ivorysql.listen_addresses 等同于pg端口的参数listen_addresses,监听oracle兼容连接的ip地址,对应内核参数OraListenAddresses
ivorysql.database_mode 值为oracle(1),pg(0),对应内核参数为database_mode, PGC_INTERNAL级别, 用户不可修改,可以视为整个db级别的参数,创建后就不可修改, 默认为DB_PG, 1. 是否监听oracle端口 2. 当前库是否包含oracle兼容的数据类型等
c 复制代码
//新增宏,默认的port
DEF_ORAPORT
DEF_ORAPORT_STR
    
//新增环境变量IYHOST,IYPORT,在设置环境变量 不指定port和host时,使用环境变量的host和port连接数据库?只留了定义没有使用 最新的代码已经删掉了
	{"ivoryhost", "IYHOST", NULL, NULL,
		"IvorySQL-Database-Host", "", 40,
		offsetof(struct pg_conn, iyhost)},

	{"ivoryport", "IYPORT", DEF_ORAPORT_STR, NULL,
		"IvorySQL-Database-Port", "", 6,
		offsetof(struct pg_conn, iyport)},
//iyhost,iyport 字段还在
struct pg_conn
{
	/* Saved values of connection options */
	char	   *pghost;			/* the machine on which the server is running,
								 * or a path to a UNIX-domain socket, or a
								 * comma-separated list of machines and/or
								 * paths; if NULL, use DEFAULT_PGSOCKET_DIR */
	char       *iyhost;			/* Oracle compatible mode ENV VARIABLE */
	char	   *pghostaddr;		/* the numeric IP address of the machine on
								 * which the server is running, or a
								 * comma-separated list of same.  Takes
								 * precedence over pghost. */
	char       *iyport;          /* Oracle compatible mdoe ENV VARIABLE */
	char	   *pgport;			/* the server's communication port number, or
//...
}
新增系统函数
函数名 解释
pg_get_connection_mode 返回当前连接的方式是oracle或者pg
babelfish 协议层的抽象

双端口的实现类似于babelfish或是mysql兼容上对协议层功能的裁剪

c 复制代码
//连接对应的接口
typedef struct ProtocolExtensionConfig {
	int		(*fn_accept)(pgsocket server_fd, struct Port *port);
	void	(*fn_close)(pgsocket server_fd);
	void	(*fn_init)(void);
	int		(*fn_start)(struct Port *port);
	void	(*fn_authenticate)(struct Port *port, const char **username);
	void	(*fn_mainfunc)(struct Port *port) pg_attribute_noreturn();
	void	(*fn_send_message)(ErrorData *edata);
	void	(*fn_send_cancel_key)(int pid, int32 key);
	void	(*fn_comm_reset)(void);
	bool	(*fn_is_reading_msg)(void);
	void	(*fn_send_ready_for_query)(CommandDest dest);
	int		(*fn_read_command)(StringInfo inBuf);
	void	(*fn_end_command)(QueryCompletion *qc, CommandDest dest);
	bool	(*fn_printtup)(TupleTableSlot *slot, DestReceiver *self);
	void	(*fn_printtup_startup)(DestReceiver *self, int operation,
								   TupleDesc typeinfo);
	void	(*fn_printtup_shutdown)(DestReceiver *self);
	void	(*fn_printtup_destroy)(DestReceiver *self);
	int		(*fn_process_command)(void);
	void	(*fn_report_param_status)(const char *name, char *val);
} ProtocolExtensionConfig;
//pg默认的libpq连接
static ProtocolExtensionConfig default_protocol_config = {
	libpq_accept,
	libpq_close,
	libpq_init,
	libpq_start,
	libpq_authenticate,
	libpq_mainfunc,
	libpq_send_message,
	libpq_send_cancel_key,
	libpq_comm_reset,
	libpq_is_reading_msg,
	libpq_send_ready_for_query,
	libpq_read_command,
	libpq_end_command,
	NULL, NULL, NULL, NULL,		/* use libpq defaults for printtup*() */
	NULL,
	libpq_report_param_status
};
//支持tds协议
static ProtocolExtensionConfig pe_config = {
	pe_accept,
	pe_close,
	pe_tds_init,
	pe_start,
	pe_authenticate,
	pe_mainfunc,
	pe_send_message,
	NULL,						/* not interested in cancel key */
	NULL,
	NULL,
	pe_send_ready_for_query,
	pe_read_command,
	pe_end_command,
	TdsPrintTup,
	TdsPrinttupStartup,
	TdsShutdown,
	TdsDestroy,
	pe_process_command,
	pe_report_param_status
};

typedef struct Port
{
	//... 
    //port新增字段protocol_config,is_tds_conn 
	ProtocolExtensionConfig *protocol_config;	/* wire protocol functions */
	bool		is_tds_conn;	//当前是否为tsql连接
} Port;

PostmasterMain
    ServerLoop
		ConnCreate	//赋值字段protocol_config

在双端口的基础上实现双解析等

以下大部分为对原先功能的复制,拆分为两份代码文件,提供不同的接口,后续oracle新增语法在oracle文件中补充。因为是对原有功能的复制,所以oracle兼容支持所有pg原有的语法与功能:

复制代码
1. 双parser,oracle兼容走ora_parser, pg走原生的parser。oracle兼容下语法解析后转为pg的语法树,走pg的语义分析,重写,优化执行等
2. 新增plisql
3. 双测试框架
  1. 新增参数用来控制
    5. oracle的config文件等
双parser
  1. 拷贝原有parser,新增oracle_parser文件夹,以及对应文件,实现双词法解析以及双parser需要对gram.y以及scan.l做接口名做部分修改

    生成不同的词法解析函数:%option prefix="core_yy" 通过加入前缀,可以将原来的yylex等函数 变成core_yylex.这样可以在一个程序中建立多个词法分析器。用来分析不同的输入流。

    生成不同语法解析函数:%name-prefix="base_yy" 代表生成的函数和变量名从yy改成base_yy,同flex,为了在一个产品里使用多个语法分析器,分析不同的数据类。

ivorysql的oracle parser也是以插件形式编译加载,需要通过参数shared_preload_libraries 加载到db中

shared_preload_libraries = 'liboracle_parser, ivorysql_ora'

c 复制代码
//加载和 unload时 做函数指针的赋值和置空
void
_PG_init(void)
{
	prev_raw_parser 			  = ora_raw_parser;
	prev_pg_get_keywords 		  = get_keywords_hook;
	prev_fill_in_contant_lengths  = fill_in_constant_lengths_hook;
	prev_quote_identifier 		  = quote_identifier_hook;

	ora_raw_parser 			  	  = oracle_raw_parser;
	get_keywords_hook 			  = oracle_pg_get_keywords;
	fill_in_constant_lengths_hook = oracle_fill_in_constant_lengths;
	quote_identifier_hook		  = oracle_quote_identifier;
}

void
_PG_fini(void)
{
	ora_raw_parser 			  = prev_raw_parser;
	get_keywords_hook 			  = prev_pg_get_keywords;
	fill_in_constant_lengths_hook = prev_fill_in_contant_lengths;
	quote_identifier_hook		  = prev_quote_identifier;
}
  1. 原有parser接口改为钩子函数sql_raw_parser,通过guc参数来ivorysql.compatible_mode设置值
c 复制代码
//parser.c
raw_parser_hook_type sql_raw_parser= standard_raw_parser;
raw_parser_hook_type ora_raw_parser= NULL;

List *
raw_parser(const char *str, RawParseMode mode)
{
	if (sql_raw_parser == NULL)
		ereport(ERROR,
				(errcode(ERRCODE_SYSTEM_ERROR),
				 errmsg("liboracle_parser not found!"),
				 errhint("You must load liboracle_parser to use oracle parser.")));

	return (*sql_raw_parser)(str, mode);
}
//原生pg parser接口改为standard_raw_parser
List *
standard_raw_parser(const char *str, RawParseMode mode)
{
    //。。。
}

默认的ivorysql.compatible_mode参数和database_mode

c 复制代码
PostmasterMain	//postmaster进程
	ServerLoop
		BackendStartup
			BackendRun //Backend进程
				PostgresMain
    				InitIvorysql
//根据双端口连接设置的参数connmode 设置新会话默认的ivorysql.compatible_mode参数
static void
InitIvorysql(void)
{
	if (MyProcPort)
	{
		if (MyProcPort->connmode == 'p')
		{
			SetConfigOption("ivorysql.compatible_mode", "pg", PGC_USERSET, PGC_S_OVERRIDE);
		}
		else if (MyProcPort->connmode == 'o')
		{
			SetConfigOption("ivorysql.compatible_mode", "oracle", PGC_USERSET, PGC_S_OVERRIDE);
			if (NULL == ora_raw_parser)
				ereport(ERROR,
						(errmsg("Invalid Oracle compatibility mode syntax library.")));
		}
	}

}

//sql_raw_parser 在对应ivorysql.compatible_mode参数赋值时赋值
//ivorysql.compatible_mode的assign函数
void
assign_compatible_mode(int newval, void *extra)
{
	if(DB_ORACLE == database_mode
		&&  (IsNormalProcessingMode() || (IsUnderPostmaster && MyProcPort)))
	{
		if (newval == DB_ORACLE)
		{
			sql_raw_parser = ora_raw_parser;


			pg_transform_merge_stmt_hook = ora_transform_merge_stmt_hook;
			pg_exec_merge_matched_hook = ora_exec_merge_matched_hook;

			assign_search_path(NULL, NULL);
		}
		else if (newval == DB_PG)
		{
			sql_raw_parser = standard_raw_parser;


			pg_transform_merge_stmt_hook = transformMergeStmt;
			pg_exec_merge_matched_hook = ExecMergeMatched;

			assign_search_path(NULL, NULL);
		}
	}
}
//ivorysql.compatible_mode的check函数,判断当前database_mode等
static bool
check_compatible_mode(int *newval, void **extra, GucSource source)
{
	int		newmode = *newval;

	if (DB_PG == database_mode && newmode == DB_ORACLE)
			ereport(ERROR,
				(errcode(ERRCODE_CANT_CHANGE_RUNTIME_PARAM),
				errmsg("parameter ivorysql.compatible_mode cannot be changed in native PG mode.")));

	if(DB_ORACLE == database_mode
       //正常状态(非init 非bootstrap) 或者 postmaster的子进程 并且 MyProcPort!=NULL
		&&  (IsNormalProcessingMode() || (IsUnderPostmaster && MyProcPort)))
	{
		if (newmode == DB_ORACLE)
		{
			if (ora_raw_parser == NULL)
				ereport(ERROR,
					(errcode(ERRCODE_SYSTEM_ERROR),
					errmsg("liboracle_parser not found!"),
					errhint("You must load liboracle_parser to use oracle parser.")));
			if (!ISLOADIVORYSQL_ORA)
				ereport(ERROR,
					(errcode(ERRCODE_SYSTEM_ERROR),
					errmsg("IVORYSQL_ORA library not found!"),
					errhint("You must load IVORYSQL_ORA to use oracle parser..")));
		}
	}
	return true;
}
  1. initdb阶段的适配

    c 复制代码
    static const char *ora_options = "-c session_preload_libraries=liboracle_parser ";
    void
    initialize_data_directory(void)
    {
    	//...
    
    	/* Bootstrap template1 */
    	bootstrap_template1();
    
    	/*
    	 * Make the per-database PG_VERSION for template1 only after init'ing it
    	 */
    	write_version_file("base/1");
    
    	//single模式下 创建系统对象时 oracledb 要加载liboracle_parser 动态库
    	fputs(_("performing post-bootstrap initialization ... "), stdout);
    	fflush(stdout);
    
    	if (strcmp(dbmode, "pg") == 0)
    		snprintf(cmd, sizeof(cmd),
    				 "\"%s\" %s %s template1 >%s",
    				 backend_exec, backend_options, extra_options,
    				 DEVNULL);
    	else
    		snprintf(cmd, sizeof(cmd),
    				 "\"%s\" %s %s %s template1 >%s",
    				 backend_exec, backend_options, extra_options, ora_options,
    				 DEVNULL);
    
    	PG_CMD_OPEN;
    
    	setup_auth(cmdfd);
    
    	setup_run_file(cmdfd, system_constraints_file);
    	// pg原有文件,以pg语法执行, 有oracle兼容语法文件时 切换到oracle
    	if (database_mode == DB_ORACLE)
    		PG_CMD_PUTS("set ivorysql.compatible_mode to pg;\n\n");
    	//加载system_functions,system_views, dictionary_file, schema, plpgsql插件。。。
    
      //加载ivory_ora(兼容性功能 包含type,function等的插件),需要切换compatible_mode到oracle
    	if (database_mode == DB_ORACLE)
    	{
    		load_plisql(cmdfd);
    		load_ivorysql_ora(cmdfd);
    	}
    
    	vacuum_db(cmdfd);
    
    	make_template0(cmdfd);
    
    	make_postgres(cmdfd);
    
    	PG_CMD_CLOSE;
    
    	check_ok();
    }
  2. 其他工具适配

    c 复制代码
    // 查询兼容性的接口, 赋值全局变量db_mode, 在对应工具中做判断
    void
    getDbCompatibleMode(PGconn *conn)
    {
    	PGresult *res;
    
    	char *db_style = NULL;
    
    	res = PQexec(conn, "show ivorysql.compatible_mode;");
    
    	if (PQresultStatus(res) != PGRES_TUPLES_OK)
    		db_mode = DB_PG;
    	else
    	{
    		db_style = PQgetvalue(res, 0, 0);
    
    		if (0 == pg_strcasecmp(db_style, "oracle"))
    			db_mode = DB_ORACLE;
    		else
    			db_mode = DB_PG;
    	}
    
    	if (res)
    		PQclear(res);
    }


  3. 新增参数

    参数名 解释
    ivorysql.compatible_mode 用户级别的参数,会话级别可修改,对应参数compatible_db,默认为PG_PARSER,用来控制当前会话是否支持oracle语法与功能

参考 Postgresql源码学习之词法和语法分析 - 墨天轮

新增plisql

​ 原文件基本复制的plpgsql,做了部分文件名等修改

c 复制代码
//initdb阶段,创建插件到template1
initialize_data_directory(void)
{
	//...

  //加载ivory_ora(兼容性功能 包含type,function等的插件),需要切换compatible_mode到oracle
	if (database_mode == DB_ORACLE)
	{
		load_plisql(cmdfd);
		load_ivorysql_ora(cmdfd);
	}
}


//匿名块默认用plisql
void
ExecuteDoStmt(ParseState *pstate, DoStmt *stmt, bool atomic)
{
	//...

	/* if LANGUAGE option wasn't specified, use the default */
	if (language_item)
		language = strVal(language_item->arg);
	else
		{
			/* anonymous block's language default value is plsql
			 * in oracle compatibility mode
			 */
			if (DB_ORACLE == compatible_db)
				language = "plisql";
			else
				language = "plpgsql";
		}
}
oracle的config文件等

新增ivorysql.conf.sample,initdb阶段加载

c 复制代码
//include_if_exists 相当于在原有的postgres.conf后追加内容,理论上ivorysql.conf中的参数优先级比postgres.conf中的高
//pgmode 不包含ivorysql.conf
if (database_mode == DB_PG)
	{
		conflines = replace_token(conflines,
							  "include_if_exists = 'ivorysql.conf'",
							  "#include_if_exists = 'ivorysql.conf'");
	}
相关推荐
北杳同学1 小时前
前端一些用得上的有意思网站
前端·javascript·vue.js·学习
真上帝的左手1 小时前
4. 关系型数据库-MySQL-架构
数据库·mysql·架构
haiyu柠檬1 小时前
迁移redis 集群从Ubuntu到Red Hat
数据库·redis·缓存
小帅学编程1 小时前
JVM学习记录
jvm·学习
xian_wwq1 小时前
【学习笔记】威胁情报
网络·笔记·学习
小糊涂加油1 小时前
TypeScript学习笔记
笔记·学习
7哥♡ۣۖᝰꫛꫀꪝۣℋ1 小时前
Spring WebMVC及常用注释
java·数据库·spring
@游子1 小时前
Python学习笔记-Day6
笔记·python·学习
脸大是真的好~1 小时前
尚硅谷-mysql专项训练-索引
数据库·mysql