diff --git a/README.md b/README.md index 042b664..f89ef9c 100644 --- a/README.md +++ b/README.md @@ -47,20 +47,42 @@ option | description `dsn` | The Database Source Name of the foreign database system you're connecting to. `driver` | The name of the ODBC driver to use (needed if no dsn is used) -These additional ODBC connection options are supported and can be defined -either in the server or foreign table definition (or in an IMPORT FOREIGN SCHEMA statement): +Any other ODBC connection attribute is driver-dependent, and should be defined by +an option named as the attribute prepended by the prefix `odbc_`. +For example `odbc_server`, `odbc_port`, `odbc_uid`, `odbc_pwd`, etc. + +The DSN and Driver can also be defined by the prefixed options +`odbc_DSN` and `odbc_DRIVER` repectively. + +The odbc_ prefixed options can be defined either in the server, user mapping +or foreign table statements. + +If the ODBC driver requires case-sensitive attribute names, the +`odbc_` option names will have to be quoted with double quotes (`""`), +for example `OPTIONS ( "odbc_SERVER" '127.0.0.1' )`. +Attributes `DSN`, `DRIVER`, `UID` and `PWD` are automatically uppercased +and don't need quoting. + +If an ODBC attribute value contains special characters such as `=` or `;` +it will require quoting with curly braces (`{}`), for example: +for example `OPTIONS ( "odbc_PWD" '{xyz=abc}' )`. + +odbc_ option names may need to be quoted with "" if the driver +requires case-sensitive names (otherwise the names are passed as lowercase, +except for UID & PWD) +odbc_ option values may need to be quoted with {} if they contain +characters such as =; ... +(but PG driver doesn't seem to support them) +(the driver name and DNS should always support this quoting, since they aren't +handled by the driver) -option | description ----------- | ----------- -`host` | The address (hostname or ip) of the database server. -`port` | The server port to connect to. -`database` | The name of the database to query. -`username` | The username to authenticate in the foreign server with. -`password` | The password to authenticate in the foreign server with. -The `username` and `password` options can also be defined +Usually you'll want to define authentication-related attributes in a `CREATE USER MAPPING` statement, so that they are determined by -the connected PostgreSQL role. +the connected PostgreSQL role, but that's not a requirement: any attribute +can be define in any of the statements; when a foreign table is access +the SERVER, USER MAPPING and FOREIGN TABLE options will be combined +to produce an ODBC connection string. The next options are used to define the table or query to connect a foreign table to. They should be defined either in `CREATE FOREIGN TABLE` @@ -97,7 +119,7 @@ CREATE FOREIGN TABLE ) SERVER odbc_server OPTIONS ( - database 'myplace', + odbc_DATABASE 'myplace', schema 'test', sql_query 'select description,id,name,created_datetime,sd,users from `test`.`dblist`', sql_count 'select count(id) from `test`.`dblist`' @@ -105,7 +127,7 @@ CREATE FOREIGN TABLE CREATE USER MAPPING FOR postgres SERVER odbc_server - OPTIONS (username 'root', password ''); + OPTIONS (odbc_UID 'root', odbc_PWD ''); ``` Note that no DSN is required; we can define connection attributes, @@ -115,8 +137,8 @@ including the name of the ODBC driver, individually: CREATE SERVER odbc_server FOREIGN DATA WRAPPER odbc_fdw OPTIONS ( - driver 'MySQL', - host '192.168.1.17', + odbc_DRIVER 'MySQL', + odbc_SERVER '192.168.1.17', encoding 'iso88591' ); ``` @@ -132,7 +154,7 @@ IMPORT FOREIGN SCHEMA test FROM SERVER odbc_server INTO public OPTIONS ( - database 'myplace', + odbc_DATABASE 'myplace', table 'odbc_table', -- this will be the name of the created foreign table sql_query 'select description,id,name,created_datetime,sd,users from `test`.`dblist`' ); @@ -164,15 +186,6 @@ LIMITATIONS - SQL_TIME - SQL_TIMESTAMP - SQL_GUID -* Option names must be lower-case. -* Only the ODBC connection attributes mentioned above can be provided via options: - - `DSN` - - `DRIVER` - - `UID` (`username` option) - - `PWD` (`password` option) - - `SERVER` (`host` option) - - `PORT` - - `DATABASE` * Foreign encodings are supported with the `encoding` option for any enconding supported by PostgreSQL and compatible with the local database. The encoding must be identified with the diff --git a/odbc_fdw.c b/odbc_fdw.c index 00f5fbb..5161a4d 100644 --- a/odbc_fdw.c +++ b/odbc_fdw.c @@ -66,16 +66,6 @@ PG_MODULE_MAGIC; typedef struct odbcFdwOptions { - /* ODBC common attributes */ - char *dsn; /* Data Source Name */ - char *driver; /* ODBC driver name */ - char *host; /* server address (SERVER) */ - char *port; /* server port */ - char *database; /* Database name */ - char *username; /* Username (UID) */ - char *password; /* Password (PWD) */ - - /* table specification */ char *schema; /* Foreign schema name */ char *table; /* Foreign table */ char *prefix; /* Prefix for imported foreign table names */ @@ -83,6 +73,8 @@ typedef struct odbcFdwOptions char *sql_count; /* SQL query for counting results */ char *encoding; /* Character encoding name */ + List *connection_list; /* ODBC connection attributes */ + List *mapping_list; /* Column name mapping */ } odbcFdwOptions; @@ -109,19 +101,19 @@ struct odbcFdwOption /* * Array of valid options - * + * In addition to this, any option with a name prefixed + * by odbc_ is accepted as an ODBC connection attribute + * and can be defined in foreign servier, user mapping or + * table statements. + * Note that dsn and driver can be defined by + * prefixed or non-prefixed options. */ static struct odbcFdwOption valid_options[] = { /* Foreign server options */ { "dsn", ForeignServerRelationId }, { "driver", ForeignServerRelationId }, - { "host", ForeignServerRelationId }, - { "port", ForeignServerRelationId }, - { "database", ForeignServerRelationId }, { "encoding", ForeignServerRelationId }, - { "username", ForeignServerRelationId }, - { "password", ForeignServerRelationId }, /* Foreign table options */ { "schema", ForeignTableRelationId }, @@ -130,10 +122,6 @@ static struct odbcFdwOption valid_options[] = { "sql_query", ForeignTableRelationId }, { "sql_count", ForeignTableRelationId }, - /* User mapping options */ - { "username", UserMappingRelationId }, - { "password", UserMappingRelationId }, - /* Sentinel */ { NULL, InvalidOid} }; @@ -177,11 +165,13 @@ static void odbcGetOptions(Oid server_oid, List *add_options, odbcFdwOptions *ex static void odbcGetTableOptions(Oid foreigntableid, odbcFdwOptions *extracted_options); static void check_return(SQLRETURN ret, char *msg, SQLHANDLE handle, SQLSMALLINT type); static void odbcConnStr(StringInfoData *conn_str, odbcFdwOptions* options); +static char* get_schema_name(odbcFdwOptions *options); +static inline bool is_blank_string(const char *s); /* * Check if string pointer is NULL or points to empty string */ -inline bool is_blank_string(const char *s) +static inline bool is_blank_string(const char *s) { return s == NULL || s[0] == '\0'; } @@ -229,11 +219,42 @@ empty_string_if_null(char *string) return string == NULL ? empty_string : string; } +static const char odbc_attribute_prefix[] = "odbc_"; +static const int odbc_attribute_prefix_len = sizeof(odbc_attribute_prefix) - 1; /* strlen(odbc_attribute_prefix); */ + +static bool +is_odbc_attribute(const char* defname) +{ + return (strlen(defname) > odbc_attribute_prefix_len && strncmp(defname, odbc_attribute_prefix, odbc_attribute_prefix_len) == 0); +} + +/* These ODBC attributes names are always uppercase */ +static const char *normalized_attributes[] = { "DRIVER", "DSN", "UID", "PWD" }; +static const char *normalized_attribute(const char* attribute_name) +{ + int i; + for (i=0; i < sizeof(normalized_attributes)/sizeof(normalized_attributes[0]); i++) + { + if (strcasecmp(attribute_name, normalized_attributes[i])==0) + { + attribute_name = normalized_attributes[i]; + break; + } + } + return attribute_name; +} + +static const char* +get_odbc_attribute_name(const char* defname) +{ + int offset = is_odbc_attribute(defname) ? odbc_attribute_prefix_len : 0; + return normalized_attribute(defname + offset); +} + static void extract_odbcFdwOptions(List *options_list, odbcFdwOptions *extracted_options) { ListCell *lc; - List *mapping_list; #ifdef DEBUG elog(DEBUG1, "extract_init_odbcFdwOptions"); @@ -248,31 +269,13 @@ extract_odbcFdwOptions(List *options_list, odbcFdwOptions *extracted_options) if (strcmp(def->defname, "dsn") == 0) { - extracted_options->dsn = defGetString(def); + extracted_options->connection_list = lappend(extracted_options->connection_list, def); continue; } if (strcmp(def->defname, "driver") == 0) { - extracted_options->driver = defGetString(def); - continue; - } - - if (strcmp(def->defname, "host") == 0) - { - extracted_options->host = defGetString(def); - continue; - } - - if (strcmp(def->defname, "port") == 0) - { - extracted_options->port = defGetString(def); - continue; - } - - if (strcmp(def->defname, "database") == 0) - { - extracted_options->database = defGetString(def); + extracted_options->connection_list = lappend(extracted_options->connection_list, def); continue; } @@ -306,21 +309,15 @@ extract_odbcFdwOptions(List *options_list, odbcFdwOptions *extracted_options) continue; } - if (strcmp(def->defname, "username") == 0) - { - extracted_options->username = defGetString(def); - continue; - } - - if (strcmp(def->defname, "password") == 0) + if (strcmp(def->defname, "encoding") == 0) { - extracted_options->password = defGetString(def); + extracted_options->encoding = defGetString(def); continue; } - if (strcmp(def->defname, "encoding") == 0) + if (is_odbc_attribute(def->defname)) { - extracted_options->encoding = defGetString(def); + extracted_options->connection_list = lappend(extracted_options->connection_list, def); continue; } @@ -334,15 +331,9 @@ extract_odbcFdwOptions(List *options_list, odbcFdwOptions *extracted_options) /* * Get the schema name from the options */ -char* get_schema_name(odbcFdwOptions *options) +static char* get_schema_name(odbcFdwOptions *options) { - char* schema_name = options->schema; - if (is_blank_string(schema_name)) - { - /* TODO: this is just a MySQL convenience; should remove it? */ - schema_name = options->database; - } - return schema_name; + return options->schema; } /* @@ -379,18 +370,11 @@ odbc_fdw_validator(PG_FUNCTION_ARGS) { List *options_list = untransformRelOptions(PG_GETARG_DATUM(0)); Oid catalog = PG_GETARG_OID(1); - char *dsn = NULL; - char *driver = NULL; - char *svr_host = NULL; - char *svr_port = NULL; - char *svr_database = NULL; char *svr_schema = NULL; char *svr_table = NULL; char *svr_prefix = NULL; char *sql_query = NULL; char *sql_count = NULL; - char *username = NULL; - char *password = NULL; ListCell *cell; #ifdef DEBUG @@ -429,54 +413,10 @@ odbc_fdw_validator(PG_FUNCTION_ARGS) )); } - /* Complain about redundent options */ - if (strcmp(def->defname, "dsn") == 0) - { - if (!is_blank_string(dsn)) - ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), - errmsg("conflicting or redundant options: dsn (%s)", defGetString(def)) - )); - - dsn = defGetString(def); - } - else if (strcmp(def->defname, "driver") == 0) - { - if (!is_blank_string(driver)) - ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), - errmsg("conflicting or redundant options: driver (%s)", defGetString(def)) - )); - - driver = defGetString(def); - } - else if (strcmp(def->defname, "host") == 0) - { - if (!is_blank_string(svr_host)) - ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), - errmsg("conflicting or redundant options: host (%s)", defGetString(def)) - )); - - svr_host = defGetString(def); - } - else if (strcmp(def->defname, "port") == 0) - { - if (!is_blank_string(svr_port)) - ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), - errmsg("conflicting or redundant options: port (%s)", defGetString(def)) - )); - - svr_port = defGetString(def); - } - else if (strcmp(def->defname, "database") == 0) - { - if (!is_blank_string(svr_database)) - ereport(ERROR, - (errcode(ERRCODE_SYNTAX_ERROR), - errmsg("conflicting or redundant options: database (%s)", defGetString(def)) - )); - - svr_database = defGetString(def); - } - else if (strcmp(def->defname, "schema") == 0) + /* TODO: detect redundant connection attributes and missing required attributs (dsn or driver) + * Complain about redundent options + */ + if (strcmp(def->defname, "schema") == 0) { if (!is_blank_string(svr_schema)) ereport(ERROR, @@ -526,35 +466,8 @@ odbc_fdw_validator(PG_FUNCTION_ARGS) sql_count = defGetString(def); } - else if (strcmp(def->defname, "username") == 0) - { - if (!is_blank_string(username)) - ereport(ERROR, - (errcode(ERRCODE_SYNTAX_ERROR), - errmsg("conflicting or redundant options: username (%s)", defGetString(def)) - )); - - username = defGetString(def); - } - else if (strcmp(def->defname, "password") == 0) - { - if (!is_blank_string(password)) - ereport(ERROR, - (errcode(ERRCODE_SYNTAX_ERROR), - errmsg("conflicting or redundant options: password (%s)", defGetString(def)) - )); - - password = defGetString(def); - } } - /* Complain about missing essential options: dsn */ - if (is_blank_string(dsn) && is_blank_string(driver) && catalog == ForeignServerRelationId) - ereport(ERROR, - (errcode(ERRCODE_SYNTAX_ERROR), - errmsg("missing essential information: dsn (Database Source Name) or driver") - )); - PG_RETURN_VOID(); } @@ -654,7 +567,6 @@ odbcGetOptions(Oid server_oid, List *add_options, odbcFdwOptions *extracted_opti ForeignServer *server; UserMapping *mapping; List *options; - ListCell *lc; #ifdef DEBUG elog(DEBUG1, "odbcGetOptions"); @@ -678,7 +590,6 @@ static void odbcGetTableOptions(Oid foreigntableid, odbcFdwOptions *extracted_options) { ForeignTable *table; - ForeignServer *server; #ifdef DEBUG elog(DEBUG1, "odbcGetTableOptions"); @@ -776,7 +687,7 @@ static bool appendConnAttribute(bool sep, StringInfoData *conn_str, const char* { if (sep) appendStringInfoString(conn_str, sep_str); - appendStringInfo(conn_str, "%s={%s}", name, value); + appendStringInfo(conn_str, "%s=%s", name, value); sep = TRUE; } return sep; @@ -785,14 +696,15 @@ static bool appendConnAttribute(bool sep, StringInfoData *conn_str, const char* static void odbcConnStr(StringInfoData *conn_str, odbcFdwOptions* options) { bool sep = FALSE; + ListCell *lc; + initStringInfo(conn_str); - sep = appendConnAttribute(sep, conn_str, "DSN", options->dsn); - sep = appendConnAttribute(sep, conn_str, "DRIVER", options->driver); - sep = appendConnAttribute(sep, conn_str, "SERVER", options->host); /* TODO: "HOST" in some cases */ - sep = appendConnAttribute(sep, conn_str, "PORT", options->port); - sep = appendConnAttribute(sep, conn_str, "DATABASE", options->database); - sep = appendConnAttribute(sep, conn_str, "UID", options->username); /* TODO: "USER" in some cases */ - sep = appendConnAttribute(sep, conn_str, "PWD", options->password); /* TODO: "PASSWORD" in some cases */ + + foreach(lc, options->connection_list) + { + DefElem *def = (DefElem *) lfirst(lc); + sep = appendConnAttribute(sep, conn_str, get_odbc_attribute_name(def->defname), defGetString(def)); + } #ifdef DEBUG elog(DEBUG1,"CONN STR: %s", conn_str->data); #endif @@ -1006,6 +918,12 @@ odbcIsValidOption(const char *option, Oid context) return true; } + /* ODBC attributes are valid in any context */ + if (is_odbc_attribute(option)) + { + return true; + } + /* Foreign table may have anything as a mapping option */ if (context == ForeignTableRelationId) return true; @@ -1322,7 +1240,6 @@ odbcIterateForeignScan(ForeignScanState *node) StringInfoData *table_columns = festate->table_columns; List *col_position_mask = NIL; List *col_size_array = NIL; - int encoding = 0; #ifdef DEBUG elog(DEBUG1, "odbcIterateForeignScan"); @@ -1350,14 +1267,6 @@ odbcIterateForeignScan(ForeignScanState *node) StringInfoData sql_type; - SQLPOINTER CharacterAttributePtr; - SQLSMALLINT BufferLength; - SQLSMALLINT ActualLengthPtr; - SQLULEN NumericAttribute; - SQLCHAR *buffer; - BufferLength = 1024; - buffer = (SQLCHAR*)malloc( BufferLength*sizeof(char) ); - /* Allocate memory for the masks in a memory context that persists between IterateForeignScan calls */ prev_context = MemoryContextSwitchTo(executor_state->es_query_cxt); @@ -1626,13 +1535,16 @@ odbcImportForeignSchema(ImportForeignSchemaStmt *stmt, Oid serverOid) odbcGetOptions(serverOid, stmt->options, &options); schema_name = get_schema_name(&options); + if (is_blank_string(schema_name)) + { + schema_name = stmt->remote_schema; + } if (!is_blank_string(options.sql_query)) { /* Generate foreign table for a query */ if (is_blank_string(options.table)) { - /* TODO: error */ elog(ERROR, "Must provide 'table' option to name the foreign table"); } @@ -1686,6 +1598,8 @@ odbcImportForeignSchema(ImportForeignSchemaStmt *stmt, Oid serverOid) { /* Will obtain the foreign tables with SQLTables() */ + SQLCHAR *table_schema = (SQLCHAR *) palloc(sizeof(SQLCHAR) * MAXIMUM_SCHEMA_NAME_LEN); + odbc_connection(&options, &env, &dbc); /* Allocate a statement handle */ @@ -1701,8 +1615,6 @@ odbcImportForeignSchema(ImportForeignSchemaStmt *stmt, Oid serverOid) check_return(ret, "Obtaining ODBC tables", tables_stmt, SQL_HANDLE_STMT); initStringInfo(&col_str); - SQLCHAR *table_catalog = (SQLCHAR *) palloc(sizeof(SQLCHAR) * MAXIMUM_CATALOG_NAME_LEN); - SQLCHAR *table_schema = (SQLCHAR *) palloc(sizeof(SQLCHAR) * MAXIMUM_SCHEMA_NAME_LEN); while (SQL_SUCCESS == ret) { ret = SQLFetch(tables_stmt); @@ -1721,26 +1633,29 @@ odbcImportForeignSchema(ImportForeignSchemaStmt *stmt, Oid serverOid) blank and different from the desired schema: */ ret = SQLGetData(tables_stmt, 2, SQL_C_CHAR, table_schema, MAXIMUM_SCHEMA_NAME_LEN, &indicator); - if (!is_blank_string(table_schema) && strcmp(table_schema, schema_name) ) + if (SQL_SUCCESS == ret) + { + if (!is_blank_string((char*)table_schema) && strcmp((char*)table_schema, schema_name) ) + { + excluded = TRUE; + } + } + else { - excluded = TRUE; + /* Some drivers don't support schemas and may return an error code here; + * in that case we must avoid using an schema to query the table columns. + */ + schema_name = NULL; } /* Since we haven't specified SQL_ALL_CATALOGS in the call to SQLTables we shouldn't get tables from special catalogs and only from the regular catalog of the database - (named as the database or blank, depending on the driver) - but to be sure we'll reject tables from catalogs with - other names: + (the catalog name is usually the name of the database or blank, + but depends on the driver and may vary, and can be obtained with: + SQLCHAR *table_catalog = (SQLCHAR *) palloc(sizeof(SQLCHAR) * MAXIMUM_CATALOG_NAME_LEN); + SQLGetData(tables_stmt, 1, SQL_C_CHAR, table_catalog, MAXIMUM_CATALOG_NAME_LEN, &indicator); */ - ret = SQLGetData(tables_stmt, 1, SQL_C_CHAR, table_catalog, MAXIMUM_CATALOG_NAME_LEN, &indicator); - if (!is_blank_string(table_catalog) && strcmp(table_catalog, schema_name)) - { - if (is_blank_string(options.database) || strcmp(table_catalog, options.database)) - { - excluded = TRUE; - } - } /* And now we'll handle tables excluded by an EXCEPT clause */ if (!excluded && stmt->list_type == FDW_IMPORT_SCHEMA_EXCEPT) @@ -1748,7 +1663,7 @@ odbcImportForeignSchema(ImportForeignSchemaStmt *stmt, Oid serverOid) foreach(tables_cell, stmt->table_list) { table_rangevar = (RangeVar*)lfirst(tables_cell); - if (strcmp(TableName, table_rangevar->relname) == 0) + if (strcmp((char*)TableName, table_rangevar->relname) == 0) { excluded = TRUE; } @@ -1761,7 +1676,9 @@ odbcImportForeignSchema(ImportForeignSchemaStmt *stmt, Oid serverOid) } } } + SQLCloseCursor(tables_stmt); + SQLFreeHandle(SQL_HANDLE_STMT, tables_stmt); } else if (stmt->list_type == FDW_IMPORT_SCHEMA_LIMIT_TO) @@ -1833,14 +1750,15 @@ odbcImportForeignSchema(ImportForeignSchemaStmt *stmt, Oid serverOid) // temporarily define vars here... char *table_name = (char*)lfirst(tables_cell); char *columns = (char*)lfirst(table_columns_cell); - - table_columns_cell = lnext(table_columns_cell); StringInfoData create_statement; ListCell *option; int option_count = 0; - char *prefix = empty_string_if_null(options.prefix); + const char *prefix = empty_string_if_null(options.prefix); + + table_columns_cell = lnext(table_columns_cell); + initStringInfo(&create_statement); - appendStringInfo(&create_statement, "CREATE FOREIGN TABLE \"%s%s\" (", prefix, (char *) table_name); + appendStringInfo(&create_statement, "CREATE FOREIGN TABLE \"%s\".\"%s%s\" (", stmt->local_schema, prefix, (char *) table_name); appendStringInfo(&create_statement, "%s", columns); appendStringInfo(&create_statement, ") SERVER %s\n", stmt->server_name); appendStringInfo(&create_statement, "OPTIONS (\n");