diff --git a/include/mapnik/sql_utils.hpp b/include/mapnik/sql_utils.hpp index d54cdbc04..96ec79342 100644 --- a/include/mapnik/sql_utils.hpp +++ b/include/mapnik/sql_utils.hpp @@ -33,12 +33,77 @@ #pragma GCC diagnostic pop // stl -#include -#include +#include +#include #include namespace mapnik { namespace sql_utils { + struct quoted_string + { + std::string const* operator-> () const { return &str; } + std::string const& str; + char const quot; + }; + + inline quoted_string identifier(std::string const& str) + { + return { str, '"' }; + } + + inline quoted_string literal(std::string const& str) + { + return { str, '\'' }; + } + + inline std::ostream& operator << (std::ostream& os, quoted_string qs) + { + std::size_t pos = 0, next; + + os.put(qs.quot); + while ((next = qs->find(qs.quot, pos)) != std::string::npos) + { + os.write(qs->data() + pos, next - pos + 1); + os.put(qs.quot); + pos = next + 1; + } + if ((next = qs->size()) > pos) + { + os.write(qs->data() + pos, next - pos); + } + return os.put(qs.quot); + } + + // Does nothing if `str` doesn't start with `quot`. + // Otherwise erases the opening quote, collapses inner quote pairs, + // and erases everything from the closing quote to the end of the + // string. The closing quote is the first non-paired quote after the + // opening one. For a well-formed quoted string, it is also the last + // character, so nothing gets lost. + inline void unquote(char quot, std::string & str) + { + if (!str.empty() && str.front() == quot) + { + std::size_t di = 0; + for (std::size_t si = 1; si < str.size(); ++si) + { + char c = str[si]; + if (c == quot && (++si >= str.size() || str[si] != quot)) + break; + str[di++] = c; + } + str.erase(di); + } + } + + inline std::string unquote_copy(char quot, std::string const& str) + { + std::string tmp(str); + sql_utils::unquote(quot, tmp); + return tmp; + } + + [[deprecated("flawed")]] inline std::string unquote_double(std::string const& sql) { std::string table_name = sql; @@ -46,6 +111,7 @@ namespace mapnik { namespace sql_utils { return table_name; } + [[deprecated("flawed")]] inline std::string unquote(std::string const& sql) { std::string table_name = sql; @@ -53,11 +119,75 @@ namespace mapnik { namespace sql_utils { return table_name; } + [[deprecated("flawed")]] inline void quote_attr(std::ostringstream & s, std::string const& field) { s << ",\"" << field << "\""; } + const std::regex re_from{ + "\\bFROM\\b" + , std::regex::icase + }; + + const std::regex re_table_name{ + "\\s*(\\w+|(\"[^\"]*\")+)" // $1 = schema + "(\\.(\\w+|(\"[^\"]*\")+))?" // $4 = table + "\\s*" + }; + + inline bool table_from_sql(std::string const& sql, + std::string & schema, + std::string & table) + { + std::smatch m; + auto start = sql.begin(); + auto end = sql.end(); + auto flags = std::regex_constants::match_default; + auto found = std::regex_match(start, end, m, re_table_name); + auto extract_matched_parts = [&]() + { + if (m[4].matched) + { + table.assign(m[4].first, m[4].second); + schema.assign(m[1].first, m[1].second); + } + else + { + table.assign(m[1].first, m[1].second); + schema.clear(); + } + }; + + if (found) + { + // input is not subquery, just "[schema.]table" + extract_matched_parts(); + } + else + { + // search "FROM [schema.]table" in subquery + while (std::regex_search(start, end, m, re_from, flags)) + { + start = m[0].second; + if (std::regex_search(start, end, m, re_table_name, + std::regex_constants::match_continuous)) + { + extract_matched_parts(); + found = true; + start = m[0].second; + } + flags = std::regex_constants::match_prev_avail; + } + } + if (found) + { + sql_utils::unquote('"', schema); + sql_utils::unquote('"', table); + } + return found; + } + inline std::string table_from_sql(std::string const& sql) { std::string table_name = sql; diff --git a/plugins/input/pgraster/pgraster_datasource.cpp b/plugins/input/pgraster/pgraster_datasource.cpp index 093c8bd00..11ade8da2 100644 --- a/plugins/input/pgraster/pgraster_datasource.cpp +++ b/plugins/input/pgraster/pgraster_datasource.cpp @@ -41,10 +41,10 @@ #pragma GCC diagnostic push #include #include -#include #pragma GCC diagnostic pop // stl +#include // FLT_MAX #include #include #include @@ -53,28 +53,19 @@ DATASOURCE_PLUGIN(pgraster_datasource) -const double pgraster_datasource::FMAX = std::numeric_limits::max(); const std::string pgraster_datasource::RASTER_COLUMNS = "raster_columns"; const std::string pgraster_datasource::RASTER_OVERVIEWS = "raster_overviews"; const std::string pgraster_datasource::SPATIAL_REF_SYS = "spatial_ref_system"; using std::shared_ptr; using mapnik::attribute_descriptor; +using mapnik::sql_utils::identifier; +using mapnik::sql_utils::literal; using mapnik::value_integer; -namespace { - - // TODO: move to sql_utils - std::string quote_ident(std::string& s) { - return "\"" + s + "\""; // TODO: escape internal quotes - } - -}; - pgraster_datasource::pgraster_datasource(parameters const& params) : datasource(params), table_(*params.get("table", "")), - schema_(""), raster_table_(*params.get("raster_table", "")), raster_field_(*params.get("raster_field", "")), key_field_(*params.get("key_field", "")), @@ -94,10 +85,7 @@ pgraster_datasource::pgraster_datasource(parameters const& params) params.get("user"), params.get("password"), params.get("connect_timeout", "4")), - bbox_token_("!bbox!"), - scale_denom_token_("!scale_denominator!"), - pixel_width_token_("!pixel_width!"), - pixel_height_token_("!pixel_height!"), + re_tokens_("!(@?\\w+)!"), // matches !mapnik_var! or !@user_var! pool_max_size_(*params_.get("max_size", 10)), persist_connection_(*params.get("persist_connection", true)), extent_from_subquery_(*params.get("extent_from_subquery", false)), @@ -141,160 +129,112 @@ pgraster_datasource::pgraster_datasource(parameters const& params) ConnectionManager::instance().registerPool(creator_, *initial_size, pool_max_size_); CnxPool_ptr pool = ConnectionManager::instance().getPool(creator_.id()); - if (pool) + if (!pool) return; + + shared_ptr conn = pool->borrowObject(); + if (!conn) return; + + if (conn->isOK()) { - shared_ptr conn = pool->borrowObject(); - if (!conn) return; + desc_.set_encoding(conn->client_encoding()); - if (conn->isOK()) + if (raster_table_.empty()) { + mapnik::sql_utils::table_from_sql + (table_, parsed_schema_, parsed_table_); - desc_.set_encoding(conn->client_encoding()); - - if (raster_table_.empty()) + // non-trivial subqueries (having no FROM) make it + // impossible to use overviews + // TODO: improve "table_from_sql" ? + auto nsp = table_.find_first_not_of(" \t\r\n"); + if (nsp != std::string::npos && table_[nsp] == '(') { - raster_table_ = mapnik::sql_utils::table_from_sql(table_); - // non-trivial subqueries (having no FROM) make it - // impossible to use overviews - // TODO: improve "table_from_sql" ? - if ( raster_table_[raster_table_.find_first_not_of(" \t\r\n")] == '(' ) - { - raster_table_.clear(); if ( use_overviews_ ) { - std::ostringstream err; - err << "Pgraster Plugin: overviews cannot be used " - "with non-trivial subqueries"; - MAPNIK_LOG_WARN(pgraster) << err.str(); - use_overviews_ = false; + std::ostringstream err; + err << "Pgraster Plugin: overviews cannot be used " + "with non-trivial subqueries"; + MAPNIK_LOG_WARN(pgraster) << err.str(); + use_overviews_ = false; } if ( ! extent_from_subquery_ ) { - std::ostringstream err; - err << "Pgraster Plugin: extent can only be computed " - "from subquery as we could not found table source"; - MAPNIK_LOG_WARN(pgraster) << err.str(); - extent_from_subquery_ = true; + std::ostringstream err; + err << "Pgraster Plugin: extent can only be computed " + "from subquery as we could not found table source"; + MAPNIK_LOG_WARN(pgraster) << err.str(); + extent_from_subquery_ = true; } - - } } + } + else + { + mapnik::sql_utils::table_from_sql + (raster_table_, parsed_schema_, parsed_table_); + } - std::string::size_type idx = raster_table_.find_last_of('.'); - if (idx != std::string::npos) - { - schema_ = raster_table_.substr(0, idx); - raster_table_ = raster_table_.substr(idx + 1); - } - - // If we do not know either the geometry_field or the srid or we - // want to use overviews but do not know about schema, or - // no extent was specified, then attempt to fetch the missing - // information from a raster_columns entry. - // - // This will return no records if we are querying a bogus table returned - // from the simplistic table parsing in table_from_sql() or if - // the table parameter references a table, view, or subselect not - // registered in the geometry columns. - // - geometryColumn_ = mapnik::sql_utils::unquote_double(raster_field_); - if (!raster_table_.empty() && ( - geometryColumn_.empty() || srid_ == 0 || - (schema_.empty() && use_overviews_) || - ! extent_initialized_ - )) - { + // If we do not know either the geometry_field or the srid or we + // want to use overviews but do not know about schema, or + // no extent was specified, then attempt to fetch the missing + // information from a raster_columns entry. + // + // This will return no records if we are querying a bogus table returned + // from the simplistic table parsing in table_from_sql() or if + // the table parameter references a table, view, or subselect not + // registered in the geometry columns. + // + geometryColumn_ = mapnik::sql_utils::unquote_copy('"', raster_field_); + if (!parsed_table_.empty() && ( + geometryColumn_.empty() || srid_ == 0 || + (parsed_schema_.empty() && use_overviews_) || + ! extent_initialized_ + )) + { #ifdef MAPNIK_STATS - mapnik::progress_timer __stats2__(std::clog, "pgraster_datasource::init(get_srid_and_geometry_column)"); + mapnik::progress_timer __stats2__(std::clog, "pgraster_datasource::init(get_srid_and_geometry_column)"); #endif - std::ostringstream s; + std::ostringstream s; - try + try + { + s << "SELECT r_raster_column col, srid, r_table_schema"; + if ( ! extent_initialized_ ) { + s << ", st_xmin(extent) xmin, st_ymin(extent) ymin" + << ", st_xmax(extent) xmax, st_ymax(extent) ymax"; + } + s << " FROM " << RASTER_COLUMNS + << " WHERE r_table_name=" << literal(parsed_table_); + if (!parsed_schema_.empty()) { - s << "SELECT r_raster_column col, srid, r_table_schema"; - if ( ! extent_initialized_ ) { - s << ", st_xmin(extent) xmin, st_ymin(extent) ymin" - << ", st_xmax(extent) xmax, st_ymax(extent) ymax"; - } - s << " FROM " - << RASTER_COLUMNS << " WHERE r_table_name='" - << mapnik::sql_utils::unquote_double(raster_table_) - << "'"; - if (! schema_.empty()) + s << " AND r_table_schema=" << literal(parsed_schema_); + } + if (!geometryColumn_.empty()) + { + s << " AND r_raster_column=" << literal(geometryColumn_); + } + MAPNIK_LOG_DEBUG(pgraster) << + "pgraster_datasource: running query " << s.str(); + shared_ptr rs = conn->executeQuery(s.str()); + if (rs->next()) + { + geometryColumn_ = rs->getValue("col"); + if ( ! extent_initialized_ ) { - s << " AND r_table_schema='" - << mapnik::sql_utils::unquote_double(schema_) - << "'"; - } - if (! raster_field_.empty()) - { - s << " AND r_raster_column='" - << mapnik::sql_utils::unquote_double(raster_field_) - << "'"; - } - MAPNIK_LOG_DEBUG(pgraster) << - "pgraster_datasource: running query " << s.str(); - shared_ptr rs = conn->executeQuery(s.str()); - if (rs->next()) - { - geometryColumn_ = rs->getValue("col"); - if ( ! extent_initialized_ ) + double lox, loy, hix, hiy; + if (mapnik::util::string2double(rs->getValue("xmin"), lox) && + mapnik::util::string2double(rs->getValue("ymin"), loy) && + mapnik::util::string2double(rs->getValue("xmax"), hix) && + mapnik::util::string2double(rs->getValue("ymax"), hiy)) { - double lox, loy, hix, hiy; - if (mapnik::util::string2double(rs->getValue("xmin"), lox) && - mapnik::util::string2double(rs->getValue("ymin"), loy) && - mapnik::util::string2double(rs->getValue("xmax"), hix) && - mapnik::util::string2double(rs->getValue("ymax"), hiy)) - { extent_.init(lox, loy, hix, hiy); extent_initialized_ = true; MAPNIK_LOG_DEBUG(pgraster) << "pgraster_datasource: Layer extent=" << extent_; - } - else - { + } + else + { MAPNIK_LOG_DEBUG(pgraster) << "pgraster_datasource: Could not determine extent from query: " << s.str(); - } - } - if (srid_ == 0) - { - const char* srid_c = rs->getValue("srid"); - if (srid_c != nullptr) - { - int result = 0; - const char * end = srid_c + std::strlen(srid_c); - if (mapnik::util::string2int(srid_c, end, result)) - { - srid_ = result; - } - } - } - if ( schema_.empty() ) - { - schema_ = rs->getValue("r_table_schema"); } } - else - { - MAPNIK_LOG_DEBUG(pgraster) << "pgraster_datasource: no response from metadata query " << s.str(); - } - rs->close(); - } - catch (mapnik::datasource_exception const& ex) { - // let this pass on query error and use the fallback below - MAPNIK_LOG_WARN(pgraster) << "pgraster_datasource: metadata query failed: " << ex.what(); - } - - // If we still do not know the srid then we can try to fetch - // it from the 'table_' parameter, which should work even if it is - // a subselect as long as we know the geometry_field to query - if (! geometryColumn_.empty() && srid_ <= 0) - { - s.str(""); - - s << "SELECT ST_SRID(\"" << geometryColumn_ << "\") AS srid FROM " - << populate_tokens(table_) << " WHERE \"" << geometryColumn_ << "\" IS NOT NULL LIMIT 1;"; - - shared_ptr rs = conn->executeQuery(s.str()); - if (rs->next()) + if (srid_ == 0) { const char* srid_c = rs->getValue("srid"); if (srid_c != nullptr) @@ -307,275 +247,312 @@ pgraster_datasource::pgraster_datasource(parameters const& params) } } } - rs->close(); - } - } - - // If overviews were requested, take note of the max scale - // of each available overview, sorted by scale descending - if ( use_overviews_ ) - { - std::ostringstream err; - if ( schema_.empty() ) - { - err << "Pgraster Plugin: unable to lookup available table" - << " overviews due to unknown schema"; - throw mapnik::datasource_exception(err.str()); - } - if ( geometryColumn_.empty() ) - { - err << "Pgraster Plugin: unable to lookup available table" - << " overviews due to unknown column name"; - throw mapnik::datasource_exception(err.str()); - } - - std::ostringstream s; - s << "select " - "r.r_table_schema sch, " - "r.r_table_name tab, " - "r.r_raster_column col, " - "greatest(abs(r.scale_x), abs(r.scale_y)) scl " - "from" - " raster_overviews o," - " raster_columns r " - "where" - " o.r_table_schema = '" - << mapnik::sql_utils::unquote_double(schema_) - << "' and o.r_table_name = '" - << mapnik::sql_utils::unquote_double(raster_table_) - << "' and o.r_raster_column = '" - << mapnik::sql_utils::unquote_double(geometryColumn_) - << "' and r.r_table_schema = o.o_table_schema" - " and r.r_table_name = o.o_table_name" - " and r.r_raster_column = o.o_raster_column" - " ORDER BY scl ASC"; - MAPNIK_LOG_DEBUG(pgraster) << "pgraster_datasource: running query " << s.str(); - shared_ptr rs = conn->executeQuery(s.str()); - while (rs->next()) - { - pgraster_overview ov = pgraster_overview(); - - ov.schema = rs->getValue("sch"); - ov.table = rs->getValue("tab"); - ov.column = rs->getValue("col"); - ov.scale = atof(rs->getValue("scl")); - - if(ov.scale == 0.0f) - { - MAPNIK_LOG_WARN(pgraster) << "pgraster_datasource: found invalid overview " - << ov.schema << "." << ov.table << "." << ov.column << " with scale " << ov.scale; - continue; - } - - overviews_.push_back(ov); - - MAPNIK_LOG_DEBUG(pgraster) << "pgraster_datasource: found overview " << ov.schema << "." << ov.table << "." << ov.column << " with scale " << ov.scale; - } - rs->close(); - if ( overviews_.empty() ) { - MAPNIK_LOG_DEBUG(pgraster) << "pgraster_datasource: no overview found for " << schema_ << "." << raster_table_ << "." << geometryColumn_; - } - } - - // detect primary key - if (*autodetect_key_field && key_field_.empty()) - { -#ifdef MAPNIK_STATS - mapnik::progress_timer __stats2__(std::clog, "pgraster_datasource::bind(get_primary_key)"); -#endif - - std::ostringstream s; - s << "SELECT a.attname, a.attnum, t.typname, t.typname in ('int2','int4','int8') " - "AS is_int FROM pg_class c, pg_attribute a, pg_type t, pg_namespace n, pg_index i " - "WHERE a.attnum > 0 AND a.attrelid = c.oid " - "AND a.atttypid = t.oid AND c.relnamespace = n.oid " - "AND c.oid = i.indrelid AND i.indisprimary = 't' " - "AND t.typname !~ '^geom' AND c.relname =" - << " '" << mapnik::sql_utils::unquote_double(raster_table_) << "' " - //"AND a.attnum = ANY (i.indkey) " // postgres >= 8.1 - << "AND (i.indkey[0]=a.attnum OR i.indkey[1]=a.attnum OR i.indkey[2]=a.attnum " - "OR i.indkey[3]=a.attnum OR i.indkey[4]=a.attnum OR i.indkey[5]=a.attnum " - "OR i.indkey[6]=a.attnum OR i.indkey[7]=a.attnum OR i.indkey[8]=a.attnum " - "OR i.indkey[9]=a.attnum) "; - if (! schema_.empty()) - { - s << "AND n.nspname='" - << mapnik::sql_utils::unquote_double(schema_) - << "' "; - } - s << "ORDER BY a.attnum"; - - shared_ptr rs_key = conn->executeQuery(s.str()); - if (rs_key->next()) - { - unsigned int result_rows = rs_key->size(); - if (result_rows == 1) + if (parsed_schema_.empty()) { - bool is_int = (std::string(rs_key->getValue(3)) == "t"); - if (is_int) - { - const char* key_field_string = rs_key->getValue(0); - if (key_field_string) - { - key_field_ = std::string(key_field_string); - - MAPNIK_LOG_DEBUG(pgraster) << "pgraster_datasource: auto-detected key field of '" - << key_field_ << "' on table '" << raster_table_ << "'"; - } - } - else - { - // throw for cases like a numeric primary key, which is invalid - // as it should be floating point (int numerics are useless) - std::ostringstream err; - err << "PostGIS Plugin: Error: '" - << rs_key->getValue(0) - << "' on table '" - << raster_table_ - << "' is not a valid integer primary key field\n"; - throw mapnik::datasource_exception(err.str()); - } - } - else if (result_rows > 1) - { - std::ostringstream err; - err << "PostGIS Plugin: Error: '" - << "multi column primary key detected but is not supported"; - throw mapnik::datasource_exception(err.str()); - } - } - rs_key->close(); - } - - // if a globally unique key field/primary key is required - // but still not known at this point, then throw - if (*autodetect_key_field && key_field_.empty()) - { - throw mapnik::datasource_exception(std::string("PostGIS Plugin: Error: primary key required") - + " but could not be detected for table '" + - raster_table_ + "', please supply 'key_field' option to specify field to use for primary key"); - } - - if (srid_ == 0) - { - srid_ = -1; - - MAPNIK_LOG_DEBUG(pgraster) << "pgraster_datasource: Table " << table_ << " is using SRID=" << srid_; - } - - // At this point the geometry_field may still not be known - // but we'll catch that where more useful... - MAPNIK_LOG_DEBUG(pgraster) << "pgraster_datasource: Using SRID=" << srid_; - MAPNIK_LOG_DEBUG(pgraster) << "pgraster_datasource: Using geometry_column=" << geometryColumn_; - - // collect attribute desc -#ifdef MAPNIK_STATS - mapnik::progress_timer __stats2__(std::clog, "pgraster_datasource::bind(get_column_description)"); -#endif - - std::ostringstream s; - s << "SELECT * FROM " << populate_tokens(table_) << " LIMIT 0"; - - shared_ptr rs = conn->executeQuery(s.str()); - int count = rs->getNumFields(); - bool found_key_field = false; - for (int i = 0; i < count; ++i) - { - std::string fld_name = rs->getFieldName(i); - int type_oid = rs->getTypeOID(i); - - // validate type of key_field - if (! found_key_field && ! key_field_.empty() && fld_name == key_field_) - { - if (type_oid == 20 || type_oid == 21 || type_oid == 23) - { - found_key_field = true; - desc_.add_descriptor(attribute_descriptor(fld_name, mapnik::Integer)); - } - else - { - std::ostringstream error_s; - error_s << "invalid type '"; - - std::ostringstream type_s; - type_s << "SELECT oid, typname FROM pg_type WHERE oid = " << type_oid; - - shared_ptr rs_oid = conn->executeQuery(type_s.str()); - if (rs_oid->next()) - { - error_s << rs_oid->getValue("typname") - << "' (oid:" << rs_oid->getValue("oid") << ")"; - } - else - { - error_s << "oid:" << type_oid << "'"; - } - - rs_oid->close(); - error_s << " for key_field '" << fld_name << "' - " - << "must be an integer primary key"; - - rs->close(); - throw mapnik::datasource_exception(error_s.str()); + parsed_schema_ = rs->getValue("r_table_schema"); } } else { - switch (type_oid) - { - case 16: // bool - desc_.add_descriptor(attribute_descriptor(fld_name, mapnik::Boolean)); - break; - case 20: // int8 - case 21: // int2 - case 23: // int4 - desc_.add_descriptor(attribute_descriptor(fld_name, mapnik::Integer)); - break; - case 700: // float4 - case 701: // float8 - case 1700: // numeric - desc_.add_descriptor(attribute_descriptor(fld_name, mapnik::Double)); - break; - case 1042: // bpchar - case 1043: // varchar - case 25: // text - case 705: // literal - desc_.add_descriptor(attribute_descriptor(fld_name, mapnik::String)); - break; - default: // should not get here -#ifdef MAPNIK_LOG - s.str(""); - s << "SELECT oid, typname FROM pg_type WHERE oid = " << type_oid; - - shared_ptr rs_oid = conn->executeQuery(s.str()); - if (rs_oid->next()) - { - std::string typname(rs_oid->getValue("typname")); - if (typname != "geometry" && typname != "raster") - { - MAPNIK_LOG_WARN(pgraster) << "pgraster_datasource: Unknown type=" << typname - << " (oid:" << rs_oid->getValue("oid") << ")"; - } - } - else - { - MAPNIK_LOG_WARN(pgraster) << "pgraster_datasource: Unknown type_oid=" << type_oid; - } - rs_oid->close(); -#endif - break; - } + MAPNIK_LOG_DEBUG(pgraster) << "pgraster_datasource: no response from metadata query " << s.str(); } + rs->close(); + } + catch (mapnik::datasource_exception const& ex) { + // let this pass on query error and use the fallback below + MAPNIK_LOG_WARN(pgraster) << "pgraster_datasource: metadata query failed: " << ex.what(); } - rs->close(); + // If we still do not know the srid then we can try to fetch + // it from the 'table_' parameter, which should work even if it is + // a subselect as long as we know the geometry_field to query + if (! geometryColumn_.empty() && srid_ <= 0) + { + s.str(""); + s << "SELECT ST_SRID(" << identifier(geometryColumn_) + << ") AS srid FROM " << populate_tokens(table_) + << " WHERE " << identifier(geometryColumn_) + << " IS NOT NULL LIMIT 1"; + + shared_ptr rs = conn->executeQuery(s.str()); + if (rs->next()) + { + const char* srid_c = rs->getValue("srid"); + if (srid_c != nullptr) + { + int result = 0; + const char * end = srid_c + std::strlen(srid_c); + if (mapnik::util::string2int(srid_c, end, result)) + { + srid_ = result; + } + } + } + rs->close(); + } } - // Close explicitly the connection so we can 'fork()' without sharing open connections - conn->close(); + // If overviews were requested, take note of the max scale + // of each available overview, sorted by scale descending + if ( use_overviews_ ) + { + std::ostringstream err; + if (parsed_schema_.empty()) + { + err << "Pgraster Plugin: unable to lookup available table" + << " overviews due to unknown schema"; + throw mapnik::datasource_exception(err.str()); + } + if (geometryColumn_.empty()) + { + err << "Pgraster Plugin: unable to lookup available table" + << " overviews due to unknown column name"; + throw mapnik::datasource_exception(err.str()); + } + std::ostringstream s; + s << "select " + "r.r_table_schema sch, " + "r.r_table_name tab, " + "r.r_raster_column col, " + "greatest(abs(r.scale_x), abs(r.scale_y)) scl " + "from" + " raster_overviews o," + " raster_columns r " + "where" + " o.r_table_schema = " << literal(parsed_schema_) + << " and o.r_table_name = " << literal(parsed_table_) + << " and o.r_raster_column = " << literal(geometryColumn_) + << " and r.r_table_schema = o.o_table_schema" + " and r.r_table_name = o.o_table_name" + " and r.r_raster_column = o.o_raster_column" + " ORDER BY scl ASC"; + MAPNIK_LOG_DEBUG(pgraster) << "pgraster_datasource: running query " << s.str(); + shared_ptr rs = conn->executeQuery(s.str()); + while (rs->next()) + { + pgraster_overview ov = pgraster_overview(); + + ov.schema = rs->getValue("sch"); + ov.table = rs->getValue("tab"); + ov.column = rs->getValue("col"); + ov.scale = atof(rs->getValue("scl")); + + if(ov.scale == 0.0f) + { + MAPNIK_LOG_WARN(pgraster) << "pgraster_datasource: found invalid overview " + << ov.schema << "." << ov.table << "." << ov.column << " with scale " << ov.scale; + continue; + } + + overviews_.push_back(ov); + + MAPNIK_LOG_DEBUG(pgraster) << "pgraster_datasource: found overview " + << ov.schema << "." << ov.table << "." << ov.column << " with scale " << ov.scale; + } + rs->close(); + if ( overviews_.empty() ) { + MAPNIK_LOG_DEBUG(pgraster) << "pgraster_datasource: no overview found for " + << parsed_schema_ << "." << parsed_table_ << "." << geometryColumn_; + } + } + + // detect primary key + if (*autodetect_key_field && key_field_.empty()) + { +#ifdef MAPNIK_STATS + mapnik::progress_timer __stats2__(std::clog, "pgraster_datasource::bind(get_primary_key)"); +#endif + + std::ostringstream s; + s << "SELECT a.attname, a.attnum, t.typname, t.typname in ('int2','int4','int8') " + "AS is_int FROM pg_class c, pg_attribute a, pg_type t, pg_namespace n, pg_index i " + "WHERE a.attnum > 0 AND a.attrelid = c.oid " + "AND a.atttypid = t.oid AND c.relnamespace = n.oid " + "AND c.oid = i.indrelid AND i.indisprimary = 't' " + "AND t.typname !~ '^geom' AND c.relname = " << literal(parsed_table_) << " " + //"AND a.attnum = ANY (i.indkey) " // postgres >= 8.1 + << "AND (i.indkey[0]=a.attnum OR i.indkey[1]=a.attnum OR i.indkey[2]=a.attnum " + "OR i.indkey[3]=a.attnum OR i.indkey[4]=a.attnum OR i.indkey[5]=a.attnum " + "OR i.indkey[6]=a.attnum OR i.indkey[7]=a.attnum OR i.indkey[8]=a.attnum " + "OR i.indkey[9]=a.attnum) "; + if (!parsed_schema_.empty()) + { + s << "AND n.nspname=" << literal(parsed_schema_) << ' '; + } + s << "ORDER BY a.attnum"; + + shared_ptr rs_key = conn->executeQuery(s.str()); + if (rs_key->next()) + { + unsigned int result_rows = rs_key->size(); + if (result_rows == 1) + { + bool is_int = (std::string(rs_key->getValue(3)) == "t"); + if (is_int) + { + const char* key_field_string = rs_key->getValue(0); + if (key_field_string) + { + key_field_ = std::string(key_field_string); + + MAPNIK_LOG_DEBUG(pgraster) << "pgraster_datasource: auto-detected key field of '" + << key_field_ << "' on table '" << parsed_table_ << "'"; + } + } + else + { + // throw for cases like a numeric primary key, which is invalid + // as it should be floating point (int numerics are useless) + std::ostringstream err; + err << "PostGIS Plugin: Error: '" + << rs_key->getValue(0) + << "' on table '" + << parsed_table_ + << "' is not a valid integer primary key field\n"; + throw mapnik::datasource_exception(err.str()); + } + } + else if (result_rows > 1) + { + std::ostringstream err; + err << "PostGIS Plugin: Error: '" + << "multi column primary key detected but is not supported"; + throw mapnik::datasource_exception(err.str()); + } + } + rs_key->close(); + } + + // if a globally unique key field/primary key is required + // but still not known at this point, then throw + if (*autodetect_key_field && key_field_.empty()) + { + throw mapnik::datasource_exception( + "PostGIS Plugin: Error: primary key required" + " but could not be detected for table '" + + parsed_table_ + "', please supply 'key_field'" + " option to specify field to use for primary key"); + } + + if (srid_ == 0) + { + srid_ = -1; + + MAPNIK_LOG_DEBUG(pgraster) << "pgraster_datasource: Table " << table_ << " is using SRID=" << srid_; + } + + // At this point the geometry_field may still not be known + // but we'll catch that where more useful... + MAPNIK_LOG_DEBUG(pgraster) << "pgraster_datasource: Using SRID=" << srid_; + MAPNIK_LOG_DEBUG(pgraster) << "pgraster_datasource: Using geometry_column=" << geometryColumn_; + + // collect attribute desc +#ifdef MAPNIK_STATS + mapnik::progress_timer __stats2__(std::clog, "pgraster_datasource::bind(get_column_description)"); +#endif + + std::ostringstream s; + s << "SELECT * FROM " << populate_tokens(table_) << " LIMIT 0"; + + shared_ptr rs = conn->executeQuery(s.str()); + int count = rs->getNumFields(); + bool found_key_field = false; + for (int i = 0; i < count; ++i) + { + std::string fld_name = rs->getFieldName(i); + int type_oid = rs->getTypeOID(i); + + // validate type of key_field + if (! found_key_field && ! key_field_.empty() && fld_name == key_field_) + { + if (type_oid == 20 || type_oid == 21 || type_oid == 23) + { + found_key_field = true; + desc_.add_descriptor(attribute_descriptor(fld_name, mapnik::Integer)); + } + else + { + std::ostringstream error_s; + error_s << "invalid type '"; + + std::ostringstream type_s; + type_s << "SELECT oid, typname FROM pg_type WHERE oid = " << type_oid; + + shared_ptr rs_oid = conn->executeQuery(type_s.str()); + if (rs_oid->next()) + { + error_s << rs_oid->getValue("typname") + << "' (oid:" << rs_oid->getValue("oid") << ")"; + } + else + { + error_s << "oid:" << type_oid << "'"; + } + + rs_oid->close(); + error_s << " for key_field '" << fld_name << "' - " + << "must be an integer primary key"; + + rs->close(); + throw mapnik::datasource_exception(error_s.str()); + } + } + else + { + switch (type_oid) + { + case 16: // bool + desc_.add_descriptor(attribute_descriptor(fld_name, mapnik::Boolean)); + break; + case 20: // int8 + case 21: // int2 + case 23: // int4 + desc_.add_descriptor(attribute_descriptor(fld_name, mapnik::Integer)); + break; + case 700: // float4 + case 701: // float8 + case 1700: // numeric + desc_.add_descriptor(attribute_descriptor(fld_name, mapnik::Double)); + break; + case 1042: // bpchar + case 1043: // varchar + case 25: // text + case 705: // literal + desc_.add_descriptor(attribute_descriptor(fld_name, mapnik::String)); + break; + default: // should not get here +#ifdef MAPNIK_LOG + s.str(""); + s << "SELECT oid, typname FROM pg_type WHERE oid = " << type_oid; + + shared_ptr rs_oid = conn->executeQuery(s.str()); + if (rs_oid->next()) + { + std::string typname(rs_oid->getValue("typname")); + if (typname != "geometry" && typname != "raster") + { + MAPNIK_LOG_WARN(pgraster) << "pgraster_datasource: Unknown type=" << typname + << " (oid:" << rs_oid->getValue("oid") << ")"; + } + } + else + { + MAPNIK_LOG_WARN(pgraster) << "pgraster_datasource: Unknown type_oid=" << type_oid; + } + rs_oid->close(); +#endif + break; + } + } + } + + rs->close(); } + + // Close explicitly the connection so we can 'fork()' without sharing open connections + conn->close(); } pgraster_datasource::~pgraster_datasource() @@ -642,77 +619,76 @@ std::string pgraster_datasource::sql_bbox(box2d const& env) const std::string pgraster_datasource::populate_tokens(std::string const& sql) const { - std::string populated_sql = sql; - - if (boost::algorithm::icontains(sql, bbox_token_)) - { - box2d max_env(-1.0 * FMAX, -1.0 * FMAX, FMAX, FMAX); - const std::string max_box = sql_bbox(max_env); - boost::algorithm::replace_all(populated_sql, bbox_token_, max_box); - } - - if (boost::algorithm::icontains(sql, scale_denom_token_)) - { - std::ostringstream ss; - ss << FMAX; - boost::algorithm::replace_all(populated_sql, scale_denom_token_, ss.str()); - } - - if (boost::algorithm::icontains(sql, pixel_width_token_)) - { - std::ostringstream ss; - ss << 0; - boost::algorithm::replace_all(populated_sql, pixel_width_token_, ss.str()); - } - - if (boost::algorithm::icontains(sql, pixel_height_token_)) - { - std::ostringstream ss; - ss << 0; - boost::algorithm::replace_all(populated_sql, pixel_height_token_, ss.str()); - } - - return populated_sql; + return populate_tokens(sql, FLT_MAX, + box2d(-FLT_MAX, -FLT_MAX, FLT_MAX, FLT_MAX), + 0, 0, mapnik::attributes{}, false); } -std::string pgraster_datasource::populate_tokens(std::string const& sql, double scale_denom, box2d const& env, double pixel_width, double pixel_height) const +std::string pgraster_datasource::populate_tokens(std::string const& sql, + double scale_denom, + box2d const& env, + double pixel_width, + double pixel_height, + mapnik::attributes const& vars, + bool intersect) const { - std::string populated_sql = sql; - std::string box = sql_bbox(env); + std::ostringstream populated_sql; + std::cmatch m; + char const* start = sql.data(); + char const* end = start + sql.size(); - if (boost::algorithm::icontains(populated_sql, scale_denom_token_)) + while (std::regex_search(start, end, m, re_tokens_)) { - std::ostringstream ss; - ss << scale_denom; - boost::algorithm::replace_all(populated_sql, scale_denom_token_, ss.str()); + populated_sql.write(start, m[0].first - start); + start = m[0].second; + + auto m1 = boost::make_iterator_range(m[1].first, m[1].second); + if (m1.front() == '@') + { + std::string var_name(m1.begin() + 1, m1.end()); + auto itr = vars.find(var_name); + if (itr != vars.end()) + { + auto var_value = itr->second.to_string(); + populated_sql << literal(var_value); + } + else + { + populated_sql << "NULL"; // undefined @variable + } + } + else if (boost::algorithm::equals(m1, "bbox")) + { + populated_sql << sql_bbox(env); + intersect = false; + } + else if (boost::algorithm::equals(m1, "pixel_height")) + { + populated_sql << pixel_height; + } + else if (boost::algorithm::equals(m1, "pixel_width")) + { + populated_sql << pixel_width; + } + else if (boost::algorithm::equals(m1, "scale_denominator")) + { + populated_sql << scale_denom; + } + else + { + populated_sql << "NULL"; // unrecognized !token! + } } - if (boost::algorithm::icontains(sql, pixel_width_token_)) - { - std::ostringstream ss; - ss << pixel_width; - boost::algorithm::replace_all(populated_sql, pixel_width_token_, ss.str()); - } + populated_sql.write(start, end - start); - if (boost::algorithm::icontains(sql, pixel_height_token_)) + if (intersect) { - std::ostringstream ss; - ss << pixel_height; - boost::algorithm::replace_all(populated_sql, pixel_height_token_, ss.str()); - } - - if (boost::algorithm::icontains(populated_sql, bbox_token_)) - { - boost::algorithm::replace_all(populated_sql, bbox_token_, box); - return populated_sql; - } - else - { - std::ostringstream s; - if (intersect_min_scale_ > 0 && (scale_denom <= intersect_min_scale_)) { - s << " WHERE ST_Intersects(\"" << geometryColumn_ << "\"," << box << ")"; + populated_sql << " WHERE ST_Intersects(" + << identifier(geometryColumn_) << ", " + << sql_bbox(env) << ")"; } else if (intersect_max_scale_ > 0 && (scale_denom >= intersect_max_scale_)) { @@ -720,11 +696,13 @@ std::string pgraster_datasource::populate_tokens(std::string const& sql, double } else { - s << " WHERE \"" << geometryColumn_ << "\" && " << box; + populated_sql << " WHERE " + << identifier(geometryColumn_) << " && " + << sql_bbox(env); } - - return populated_sql + s.str(); } + + return populated_sql.str(); } @@ -852,19 +830,19 @@ featureset_ptr pgraster_datasource::features_with_context(query const& q,process std::ostringstream s_error; s_error << "PostGIS: geometry name lookup failed for table '"; - if (! schema_.empty()) + if (!parsed_schema_.empty()) { - s_error << schema_ << "."; + s_error << parsed_schema_ << "."; } - s_error << raster_table_ + s_error << parsed_table_ << "'. Please manually provide the 'geometry_field' parameter or add an entry " << "in the geometry_columns for '"; - if (! schema_.empty()) + if (!parsed_schema_.empty()) { - s_error << schema_ << "."; + s_error << parsed_schema_ << "."; } - s_error << raster_table_ << "'."; + s_error << parsed_table_ << "'."; throw mapnik::datasource_exception(s_error.str()); } @@ -877,6 +855,7 @@ featureset_ptr pgraster_datasource::features_with_context(query const& q,process std::string table_with_bbox; std::string col = geometryColumn_; + table_with_bbox = table_; // possibly a subquery if ( use_overviews_ && !overviews_.empty()) { std::string sch = overviews_[0].schema; @@ -904,18 +883,12 @@ featureset_ptr pgraster_datasource::features_with_context(query const& q,process << " not good for min out scale " << scale; } } - table_with_bbox = table_; // possibly a subquery - boost::algorithm::replace_all(table_with_bbox, - mapnik::sql_utils::unquote_double(raster_table_), tab); - boost::algorithm::replace_all(table_with_bbox, - mapnik::sql_utils::unquote_double(schema_), sch); - boost::algorithm::replace_all(table_with_bbox, - mapnik::sql_utils::unquote_double(geometryColumn_), col); - table_with_bbox = populate_tokens(table_with_bbox, - scale_denom, box, px_gw, px_gh); - } else { - table_with_bbox = populate_tokens(table_, scale_denom, box, px_gw, px_gh); + boost::algorithm::replace_all(table_with_bbox, parsed_table_, tab); + boost::algorithm::replace_all(table_with_bbox, parsed_schema_, sch); + boost::algorithm::replace_all(table_with_bbox, geometryColumn_, col); } + table_with_bbox = populate_tokens(table_with_bbox, scale_denom, box, + px_gw, px_gh, q.variables()); std::ostringstream s; @@ -927,21 +900,21 @@ featureset_ptr pgraster_datasource::features_with_context(query const& q,process if (clip_rasters_) s << "ST_Clip("; - s << "\"" << col << "\""; + s << identifier(col); if (clip_rasters_) { s << ", ST_Expand(" << sql_bbox(box) - << ", greatest(abs(ST_ScaleX(\"" - << col << "\")), abs(ST_ScaleY(\"" - << col << "\")))))"; + << ", greatest(abs(ST_ScaleX(" + << identifier(col) << ")), abs(ST_ScaleY(" + << identifier(col) << ")))))"; } if (prescale_rasters_) { const double scale = std::min(px_gw, px_gh); - s << ", least(abs(ST_ScaleX(\"" << col - << "\"))::float8/" << scale - << ", 1.0), least(abs(ST_ScaleY(\"" << col - << "\"))::float8/" << scale << ", 1.0))"; + s << ", least(1.0, abs(ST_ScaleX(" << identifier(col) + << "))::float8/" << scale + << "), least(1.0, abs(ST_ScaleY(" << identifier(col) + << "))::float8/" << scale << "))"; // TODO: if band_ is given, we'll interpret as indexed so // the rescaling must NOT ruin it (use algorithm mode!) } @@ -957,14 +930,14 @@ featureset_ptr pgraster_datasource::features_with_context(query const& q,process if (! key_field_.empty()) { - mapnik::sql_utils::quote_attr(s, key_field_); + s << ',' << identifier(key_field_); ctx->push(key_field_); for (; pos != end; ++pos) { if (*pos != key_field_) { - mapnik::sql_utils::quote_attr(s, *pos); + s << ',' << identifier(*pos); ctx->push(*pos); } } @@ -973,7 +946,7 @@ featureset_ptr pgraster_datasource::features_with_context(query const& q,process { for (; pos != end; ++pos) { - mapnik::sql_utils::quote_attr(s, *pos); + s << ',' << identifier(*pos); ctx->push(*pos); } } @@ -1020,39 +993,39 @@ featureset_ptr pgraster_datasource::features_at_point(coord2d const& pt, double std::ostringstream s_error; s_error << "PostGIS: geometry name lookup failed for table '"; - if (! schema_.empty()) + if (!parsed_schema_.empty()) { - s_error << schema_ << "."; + s_error << parsed_schema_ << "."; } - s_error << raster_table_ + s_error << parsed_table_ << "'. Please manually provide the 'geometry_field' parameter or add an entry " << "in the geometry_columns for '"; - if (! schema_.empty()) + if (!parsed_schema_.empty()) { - s_error << schema_ << "."; + s_error << parsed_schema_ << "."; } - s_error << raster_table_ << "'."; + s_error << parsed_table_ << "'."; throw mapnik::datasource_exception(s_error.str()); } std::ostringstream s; - s << "SELECT ST_AsBinary(\"" << geometryColumn_ << "\") AS geom"; + s << "SELECT ST_AsBinary(" << identifier(geometryColumn_) << ") AS geom"; mapnik::context_ptr ctx = std::make_shared(); auto const& desc = desc_.get_descriptors(); if (!key_field_.empty()) { - mapnik::sql_utils::quote_attr(s, key_field_); + s << ',' << identifier(key_field_); ctx->push(key_field_); for (auto const& attr_info : desc) { std::string const& name = attr_info.get_name(); if (name != key_field_) { - mapnik::sql_utils::quote_attr(s, name); + s << ',' << identifier(name); ctx->push(name); } } @@ -1062,13 +1035,14 @@ featureset_ptr pgraster_datasource::features_at_point(coord2d const& pt, double for (auto const& attr_info : desc) { std::string const& name = attr_info.get_name(); - mapnik::sql_utils::quote_attr(s, name); + s << ',' << identifier(name); ctx->push(name); } } box2d box(pt.x - tol, pt.y - tol, pt.x + tol, pt.y + tol); - std::string table_with_bbox = populate_tokens(table_, FMAX, box, 0, 0); + std::string table_with_bbox = populate_tokens(table_, FLT_MAX, box, 0, 0, + mapnik::attributes{}); s << " FROM " << table_with_bbox; @@ -1101,9 +1075,9 @@ box2d pgraster_datasource::envelope() const { std::ostringstream s; - std::string col = mapnik::sql_utils::unquote_double(geometryColumn_); - std::string sch = mapnik::sql_utils::unquote_double(schema_); - std::string tab = mapnik::sql_utils::unquote_double(raster_table_); + std::string col = geometryColumn_; + std::string sch = parsed_schema_; + std::string tab = parsed_table_; if ( ! overviews_.empty() ) { @@ -1123,7 +1097,7 @@ box2d pgraster_datasource::envelope() const { s_error << sch << "."; } - s_error << raster_table_ << "' because we cannot determine the raster field name." + s_error << parsed_table_ << "' because we cannot determine the raster field name." << "\nPlease provide either an 'extent' parameter to skip this query, " << "a 'raster_field' and/or 'raster_table' parameter, or add " << "standard constraints to your raster table."; @@ -1147,16 +1121,16 @@ box2d pgraster_datasource::envelope() const if (! sch.empty()) { - s << mapnik::sql_utils::unquote_double(sch) << "','"; + s << literal(sch) << ','; } - s << mapnik::sql_utils::unquote_double(tab) << "','" - << mapnik::sql_utils::unquote_double(col) << "') as ext) as tmp"; + s << literal(tab) << ',' + << literal(col) << ") as ext) as tmp"; } else { s << "SELECT ST_XMin(ext),ST_YMin(ext),ST_XMax(ext),ST_YMax(ext)" - << " FROM (SELECT ST_Extent(" << quote_ident(col) << "::geometry) as ext from "; + << " FROM (SELECT ST_Extent(" << identifier(col) << "::geometry) as ext from "; if (extent_from_subquery_) { @@ -1168,12 +1142,12 @@ box2d pgraster_datasource::envelope() const { if (! sch.empty()) { - s << quote_ident(sch) << "."; + s << identifier(sch) << "."; } // but if the subquery does not limit records then querying the // actual table will be faster as indexes can be used - s << quote_ident(tab) << ") as tmp"; + s << identifier(tab) << ") as tmp"; } } diff --git a/plugins/input/pgraster/pgraster_datasource.hpp b/plugins/input/pgraster/pgraster_datasource.hpp index cc86abf63..0ffe762b5 100644 --- a/plugins/input/pgraster/pgraster_datasource.hpp +++ b/plugins/input/pgraster/pgraster_datasource.hpp @@ -42,6 +42,7 @@ #include // stl +#include #include #include #include @@ -90,24 +91,26 @@ public: private: std::string sql_bbox(box2d const& env) const; - std::string populate_tokens(std::string const& sql, double scale_denom, box2d const& env, double pixel_width, double pixel_height) const; + std::string populate_tokens(std::string const& sql, double scale_denom, + box2d const& env, + double pixel_width, double pixel_height, + mapnik::attributes const& vars, + bool intersect = true) const; std::string populate_tokens(std::string const& sql) const; std::shared_ptr get_resultset(std::shared_ptr &conn, std::string const& sql, CnxPool_ptr const& pool, processor_context_ptr ctx= processor_context_ptr()) const; static const std::string RASTER_COLUMNS; static const std::string RASTER_OVERVIEWS; static const std::string SPATIAL_REF_SYS; - static const double FMAX; const std::string uri_; const std::string username_; const std::string password_; // table name (schema qualified or not) or subquery const std::string table_; - // schema name (possibly extracted from table_) - std::string schema_; - // table name (possibly extracted from table_) - std::string raster_table_; + const std::string raster_table_; // possibly schema-qualified const std::string raster_field_; + std::string parsed_schema_; // extracted from raster_table_ or table_ + std::string parsed_table_; // extracted from raster_table_ or table_ std::string key_field_; mapnik::value_integer cursor_fetch_size_; mapnik::value_integer row_limit_; @@ -129,10 +132,7 @@ private: bool clip_rasters_; layer_descriptor desc_; ConnectionCreator creator_; - const std::string bbox_token_; - const std::string scale_denom_token_; - const std::string pixel_width_token_; - const std::string pixel_height_token_; + std::regex re_tokens_; int pool_max_size_; bool persist_connection_; bool extent_from_subquery_; diff --git a/plugins/input/postgis/build.py b/plugins/input/postgis/build.py index 157952f45..635efb848 100644 --- a/plugins/input/postgis/build.py +++ b/plugins/input/postgis/build.py @@ -52,7 +52,6 @@ libraries = copy(plugin_env['LIBS']) if env['PLUGIN_LINKING'] == 'shared': libraries.append('boost_system%s' % env['BOOST_APPEND']) - libraries.append('boost_regex%s' % env['BOOST_APPEND']) libraries.insert(0,env['MAPNIK_NAME']) libraries.append(env['ICU_LIB_NAME']) diff --git a/plugins/input/postgis/postgis_datasource.cpp b/plugins/input/postgis/postgis_datasource.cpp index c9f20b76d..0e90ac5fb 100644 --- a/plugins/input/postgis/postgis_datasource.cpp +++ b/plugins/input/postgis/postgis_datasource.cpp @@ -38,11 +38,10 @@ #pragma GCC diagnostic push #include #include -#include -#include #pragma GCC diagnostic pop // stl +#include // FLT_MAX #include #include #include @@ -52,17 +51,17 @@ DATASOURCE_PLUGIN(postgis_datasource) -const double postgis_datasource::FMAX = std::numeric_limits::max(); const std::string postgis_datasource::GEOMETRY_COLUMNS = "geometry_columns"; const std::string postgis_datasource::SPATIAL_REF_SYS = "spatial_ref_system"; using std::shared_ptr; using mapnik::attribute_descriptor; +using mapnik::sql_utils::identifier; +using mapnik::sql_utils::literal; postgis_datasource::postgis_datasource(parameters const& params) : datasource(params), table_(*params.get("table", "")), - schema_(""), geometry_table_(*params.get("geometry_table", "")), geometry_field_(*params.get("geometry_field", "")), key_field_(*params.get("key_field", "")), @@ -79,10 +78,6 @@ postgis_datasource::postgis_datasource(parameters const& params) params.get("user"), params.get("password"), params.get("connect_timeout", "4")), - bbox_token_("!bbox!"), - scale_denom_token_("!scale_denominator!"), - pixel_width_token_("!pixel_width!"), - pixel_height_token_("!pixel_height!"), pool_max_size_(*params_.get("max_size", 10)), persist_connection_(*params.get("persist_connection", true)), extent_from_subquery_(*params.get("extent_from_subquery", false)), @@ -99,8 +94,7 @@ postgis_datasource::postgis_datasource(parameters const& params) simplify_prefilter_(*params_.get("simplify_prefilter", 0.0)), simplify_dp_preserve_(false), simplify_clip_resolution_(*params_.get("simplify_clip_resolution", 0.0)), - // TODO - use for known tokens too: "(@\\w+|!\\w+!)" - pattern_(boost::regex("(@\\w+)",boost::regex::normal | boost::regbase::icase)), + re_tokens_("!(@?\\w+)!"), // matches !mapnik_var! or !@user_var! // params below are for testing purposes only and may be removed at any time intersect_min_scale_(*params.get("intersect_min_scale", 0)), intersect_max_scale_(*params.get("intersect_max_scale", 0)), @@ -159,21 +153,13 @@ postgis_datasource::postgis_datasource(parameters const& params) desc_.set_encoding(conn->client_encoding()); - if (geometry_table_.empty()) - { - geometry_table_ = mapnik::sql_utils::table_from_sql(table_); - } + mapnik::sql_utils::table_from_sql( + geometry_table_.empty() ? table_ : geometry_table_, + parsed_schema_, parsed_table_); - std::string::size_type idx = geometry_table_.find_last_of('.'); - if (idx != std::string::npos) - { - schema_ = geometry_table_.substr(0, idx); - geometry_table_ = geometry_table_.substr(idx + 1); - } - - // NOTE: geometry_table_ how should ideally be a table name, but + // NOTE: parsed_table_ now should ideally be a table name, but // there are known edge cases where this will break down and - // geometry_table_ may even be empty: https://github.com/mapnik/mapnik/issues/2718 + // it may even be empty: https://github.com/mapnik/mapnik/issues/2718 // If we do not know both the geometry_field and the srid // then first attempt to fetch the geometry name from a geometry_columns entry. @@ -181,8 +167,8 @@ postgis_datasource::postgis_datasource(parameters const& params) // from the simplistic table parsing in table_from_sql() or if // the table parameter references a table, view, or subselect not // registered in the geometry columns. - geometryColumn_ = geometry_field_; - if (!geometry_table_.empty() && (geometryColumn_.empty() || srid_ == 0)) + geometryColumn_ = mapnik::sql_utils::unquote_copy('"', geometry_field_); + if (!parsed_table_.empty() && (geometryColumn_.empty() || srid_ == 0)) { #ifdef MAPNIK_STATS mapnik::progress_timer __stats2__(std::clog, "postgis_datasource::init(get_srid_and_geometry_column)"); @@ -191,21 +177,15 @@ postgis_datasource::postgis_datasource(parameters const& params) try { - s << "SELECT f_geometry_column, srid FROM " - << GEOMETRY_COLUMNS <<" WHERE f_table_name='" - << mapnik::sql_utils::unquote_double(geometry_table_) - << "'"; - if (! schema_.empty()) + s << "SELECT f_geometry_column, srid FROM " << GEOMETRY_COLUMNS + << " WHERE f_table_name=" << literal(parsed_table_); + if (!parsed_schema_.empty()) { - s << " AND f_table_schema='" - << mapnik::sql_utils::unquote_double(schema_) - << "'"; + s << " AND f_table_schema=" << literal(parsed_schema_); } - if (! geometry_field_.empty()) + if (!geometryColumn_.empty()) { - s << " AND f_geometry_column='" - << mapnik::sql_utils::unquote_double(geometry_field_) - << "'"; + s << " AND f_geometry_column=" << literal(geometryColumn_); } shared_ptr rs = conn->executeQuery(s.str()); if (rs->next()) @@ -237,26 +217,24 @@ postgis_datasource::postgis_datasource(parameters const& params) } // If we still do not know the srid then we can try to fetch - // it from the 'geometry_table_' parameter, which should work even if it is + // it from the 'parsed_table_' parameter, which should work even if it is // a subselect as long as we know the geometry_field to query if (!geometryColumn_.empty() && srid_ <= 0) { std::ostringstream s; - s << "SELECT ST_SRID(\"" << geometryColumn_ << "\") AS srid FROM "; - if (!geometry_table_.empty()) + s << "SELECT ST_SRID(" << identifier(geometryColumn_) + << ") AS srid FROM "; + if (!parsed_table_.empty()) { - if (!schema_.empty()) - { - s << schema_ << '.'; - } - s << geometry_table_; + append_geometry_table(s); } else { s << populate_tokens(table_); } - s << " WHERE \"" << geometryColumn_ << "\" IS NOT NULL LIMIT 1;"; + s << " WHERE " << identifier(geometryColumn_) + << " IS NOT NULL LIMIT 1"; shared_ptr rs = conn->executeQuery(s.str()); if (rs->next()) @@ -288,18 +266,16 @@ postgis_datasource::postgis_datasource(parameters const& params) "WHERE a.attnum > 0 AND a.attrelid = c.oid " "AND a.atttypid = t.oid AND c.relnamespace = n.oid " "AND c.oid = i.indrelid AND i.indisprimary = 't' " - "AND t.typname !~ '^geom' AND c.relname =" - << " '" << mapnik::sql_utils::unquote_double(geometry_table_) << "' " + "AND t.typname !~ '^geom' AND c.relname = " + << literal(parsed_table_) << " " //"AND a.attnum = ANY (i.indkey) " // postgres >= 8.1 << "AND (i.indkey[0]=a.attnum OR i.indkey[1]=a.attnum OR i.indkey[2]=a.attnum " "OR i.indkey[3]=a.attnum OR i.indkey[4]=a.attnum OR i.indkey[5]=a.attnum " "OR i.indkey[6]=a.attnum OR i.indkey[7]=a.attnum OR i.indkey[8]=a.attnum " "OR i.indkey[9]=a.attnum) "; - if (! schema_.empty()) + if (!parsed_schema_.empty()) { - s << "AND n.nspname='" - << mapnik::sql_utils::unquote_double(schema_) - << "' "; + s << "AND n.nspname=" << literal(parsed_schema_) << ' '; } s << "ORDER BY a.attnum"; @@ -318,7 +294,7 @@ postgis_datasource::postgis_datasource(parameters const& params) key_field_ = std::string(key_field_string); MAPNIK_LOG_DEBUG(postgis) << "postgis_datasource: auto-detected key field of '" - << key_field_ << "' on table '" << geometry_table_ << "'"; + << key_field_ << "' on table '" << parsed_table_ << "'"; } } else @@ -329,7 +305,7 @@ postgis_datasource::postgis_datasource(parameters const& params) err << "PostGIS Plugin: Error: '" << rs_key->getValue(0) << "' on table '" - << geometry_table_ + << parsed_table_ << "' is not a valid integer primary key field\n"; throw mapnik::datasource_exception(err.str()); } @@ -349,9 +325,11 @@ postgis_datasource::postgis_datasource(parameters const& params) // but still not known at this point, then throw if (*autodetect_key_field && key_field_.empty()) { - throw mapnik::datasource_exception(std::string("PostGIS Plugin: Error: primary key required") - + " but could not be detected for table '" + - geometry_table_ + "', please supply 'key_field' option to specify field to use for primary key"); + throw mapnik::datasource_exception( + "PostGIS Plugin: Error: primary key required" + " but could not be detected for table '" + + parsed_table_ + "', please supply 'key_field'" + " option to specify field to use for primary key"); } if (srid_ == 0) @@ -551,43 +529,9 @@ std::string postgis_datasource::sql_bbox(box2d const& env) const std::string postgis_datasource::populate_tokens(std::string const& sql) const { - std::string populated_sql = sql; - - if (boost::algorithm::icontains(sql, bbox_token_)) - { - box2d max_env(-1.0 * FMAX, -1.0 * FMAX, FMAX, FMAX); - const std::string max_box = sql_bbox(max_env); - boost::algorithm::replace_all(populated_sql, bbox_token_, max_box); - } - - if (boost::algorithm::icontains(sql, scale_denom_token_)) - { - std::ostringstream ss; - ss << FMAX; - boost::algorithm::replace_all(populated_sql, scale_denom_token_, ss.str()); - } - - if (boost::algorithm::icontains(sql, pixel_width_token_)) - { - boost::algorithm::replace_all(populated_sql, pixel_width_token_, "0"); - } - - if (boost::algorithm::icontains(sql, pixel_height_token_)) - { - boost::algorithm::replace_all(populated_sql, pixel_height_token_, "0"); - } - - std::string copy2 = populated_sql; - std::list l; - boost::regex_split(std::back_inserter(l), copy2, pattern_); - if (!l.empty()) - { - for (auto const & token: l) - { - boost::algorithm::replace_all(populated_sql, token, "null"); - } - } - return populated_sql; + return populate_tokens(sql, FLT_MAX, + box2d(-FLT_MAX, -FLT_MAX, FLT_MAX, FLT_MAX), + 0, 0, mapnik::attributes{}, false); } std::string postgis_datasource::populate_tokens( @@ -596,43 +540,66 @@ std::string postgis_datasource::populate_tokens( box2d const& env, double pixel_width, double pixel_height, - mapnik::attributes const& vars) const + mapnik::attributes const& vars, + bool intersect) const { - std::string populated_sql = sql; - std::string box = sql_bbox(env); + std::ostringstream populated_sql; + std::cmatch m; + char const* start = sql.data(); + char const* end = start + sql.size(); - if (boost::algorithm::icontains(populated_sql, scale_denom_token_)) + while (std::regex_search(start, end, m, re_tokens_)) { - std::ostringstream ss; - ss << scale_denom; - boost::algorithm::replace_all(populated_sql, scale_denom_token_, ss.str()); + populated_sql.write(start, m[0].first - start); + start = m[0].second; + + auto m1 = boost::make_iterator_range(m[1].first, m[1].second); + if (m1.front() == '@') + { + std::string var_name(m1.begin() + 1, m1.end()); + auto itr = vars.find(var_name); + if (itr != vars.end()) + { + auto var_value = itr->second.to_string(); + populated_sql << literal(var_value); + } + else + { + populated_sql << "NULL"; // undefined @variable + } + } + else if (boost::algorithm::equals(m1, "bbox")) + { + populated_sql << sql_bbox(env); + intersect = false; + } + else if (boost::algorithm::equals(m1, "pixel_height")) + { + populated_sql << pixel_height; + } + else if (boost::algorithm::equals(m1, "pixel_width")) + { + populated_sql << pixel_width; + } + else if (boost::algorithm::equals(m1, "scale_denominator")) + { + populated_sql << scale_denom; + } + else + { + populated_sql << "NULL"; // unrecognized !token! + } } - if (boost::algorithm::icontains(sql, pixel_width_token_)) - { - std::ostringstream ss; - ss << pixel_width; - boost::algorithm::replace_all(populated_sql, pixel_width_token_, ss.str()); - } + populated_sql.write(start, end - start); - if (boost::algorithm::icontains(sql, pixel_height_token_)) + if (intersect) { - std::ostringstream ss; - ss << pixel_height; - boost::algorithm::replace_all(populated_sql, pixel_height_token_, ss.str()); - } - - if (boost::algorithm::icontains(populated_sql, bbox_token_)) - { - boost::algorithm::replace_all(populated_sql, bbox_token_, box); - } - else - { - std::ostringstream s; - if (intersect_min_scale_ > 0 && (scale_denom <= intersect_min_scale_)) { - s << " WHERE ST_Intersects(\"" << geometryColumn_ << "\"," << box << ")"; + populated_sql << " WHERE ST_Intersects(" + << identifier(geometryColumn_) << ", " + << sql_bbox(env) << ")"; } else if (intersect_max_scale_ > 0 && (scale_denom >= intersect_max_scale_)) { @@ -640,31 +607,34 @@ std::string postgis_datasource::populate_tokens( } else { - s << " WHERE \"" << geometryColumn_ << "\" && " << box; - } - populated_sql += s.str(); - } - std::string copy2 = populated_sql; - std::list l; - boost::regex_split(std::back_inserter(l), copy2, pattern_); - if (!l.empty()) - { - for (auto const & token: l) - { - auto itr = vars.find(token.substr(1,std::string::npos)); - if (itr != vars.end()) - { - boost::algorithm::replace_all(populated_sql, token, itr->second.to_string()); - } - else - { - boost::algorithm::replace_all(populated_sql, token, "null"); - } + populated_sql << " WHERE " + << identifier(geometryColumn_) << " && " + << sql_bbox(env); } } - return populated_sql; + + return populated_sql.str(); } +void postgis_datasource::append_geometry_table(std::ostream & os) const +{ + if (!geometry_table_.empty()) + { + os << geometry_table_; // assume datasource parameter is valid SQL + } + else if (!parsed_schema_.empty()) + { + os << identifier(parsed_schema_) << '.' << identifier(parsed_table_); + } + else if (!parsed_table_.empty()) + { + os << identifier(parsed_table_); + } + else + { + os << table_; // assume datasource parameter is valid SQL + } +} std::shared_ptr postgis_datasource::get_resultset(std::shared_ptr &conn, std::string const& sql, CnxPool_ptr const& pool, processor_context_ptr ctx) const { @@ -789,20 +759,11 @@ featureset_ptr postgis_datasource::features_with_context(query const& q,processo { std::ostringstream s_error; s_error << "PostGIS: geometry name lookup failed for table '"; - - if (! schema_.empty()) - { - s_error << schema_ << "."; - } - s_error << geometry_table_ - << "'. Please manually provide the 'geometry_field' parameter or add an entry " + append_geometry_table(s_error); + s_error << "'. Please manually provide the 'geometry_field' parameter or add an entry " << "in the geometry_columns for '"; - - if (! schema_.empty()) - { - s_error << schema_ << "."; - } - s_error << geometry_table_ << "'."; + append_geometry_table(s_error); + s_error << "'."; throw mapnik::datasource_exception(s_error.str()); } @@ -833,7 +794,7 @@ featureset_ptr postgis_datasource::features_with_context(query const& q,processo { s << "ST_ClipByBox2D("; } - s << "\"" << geometryColumn_ << "\""; + s << identifier(geometryColumn_); // ! ST_ClipByBox2D() if (simplify_clip_resolution_ > 0.0 && simplify_clip_resolution_ > px_sz) @@ -865,7 +826,7 @@ featureset_ptr postgis_datasource::features_with_context(query const& q,processo } // Geometry column! - s << "\"" << geometryColumn_ << "\""; + s << identifier(geometryColumn_); // ! ST_SnapToGrid() if (simplify_geometries_ && simplify_snap_ratio_ > 0.0) @@ -903,7 +864,7 @@ featureset_ptr postgis_datasource::features_with_context(query const& q,processo if (! key_field_.empty()) { - mapnik::sql_utils::quote_attr(s, key_field_); + s << ',' << identifier(key_field_); if (key_field_as_attribute_) { ctx->push(key_field_); @@ -913,7 +874,7 @@ featureset_ptr postgis_datasource::features_with_context(query const& q,processo { if (*pos != key_field_) { - mapnik::sql_utils::quote_attr(s, *pos); + s << ',' << identifier(*pos); ctx->push(*pos); } } @@ -922,7 +883,7 @@ featureset_ptr postgis_datasource::features_with_context(query const& q,processo { for (; pos != end; ++pos) { - mapnik::sql_utils::quote_attr(s, *pos); + s << ',' << identifier(*pos); ctx->push(*pos); } } @@ -963,33 +924,25 @@ featureset_ptr postgis_datasource::features_at_point(coord2d const& pt, double t { std::ostringstream s_error; s_error << "PostGIS: geometry name lookup failed for table '"; - - if (! schema_.empty()) - { - s_error << schema_ << "."; - } - s_error << geometry_table_ - << "'. Please manually provide the 'geometry_field' parameter or add an entry " + append_geometry_table(s_error); + s_error << "'. Please manually provide the 'geometry_field' parameter or add an entry " << "in the geometry_columns for '"; - - if (! schema_.empty()) - { - s_error << schema_ << "."; - } - s_error << geometry_table_ << "'."; + append_geometry_table(s_error); + s_error << "'."; throw mapnik::datasource_exception(s_error.str()); } std::ostringstream s; - s << "SELECT ST_AsBinary(\"" << geometryColumn_ << "\") AS geom"; + s << "SELECT ST_AsBinary(" << identifier(geometryColumn_) + << ") AS geom"; mapnik::context_ptr ctx = std::make_shared(); auto const& desc = desc_.get_descriptors(); if (!key_field_.empty()) { - mapnik::sql_utils::quote_attr(s, key_field_); + s << ',' << identifier(key_field_); if (key_field_as_attribute_) { ctx->push(key_field_); @@ -999,7 +952,7 @@ featureset_ptr postgis_datasource::features_at_point(coord2d const& pt, double t std::string const& name = attr_info.get_name(); if (name != key_field_) { - mapnik::sql_utils::quote_attr(s, name); + s << ',' << identifier(name); ctx->push(name); } } @@ -1009,13 +962,14 @@ featureset_ptr postgis_datasource::features_at_point(coord2d const& pt, double t for (auto const& attr_info : desc) { std::string const& name = attr_info.get_name(); - mapnik::sql_utils::quote_attr(s, name); + s << ',' << identifier(name); ctx->push(name); } } box2d box(pt.x - tol, pt.y - tol, pt.x + tol, pt.y + tol); - std::string table_with_bbox = populate_tokens(table_, FMAX, box, 0, 0, mapnik::attributes()); + std::string table_with_bbox = populate_tokens(table_, FLT_MAX, box, 0, 0, + mapnik::attributes{}); s << " FROM " << table_with_bbox; @@ -1053,12 +1007,8 @@ box2d postgis_datasource::envelope() const { std::ostringstream s_error; s_error << "PostGIS: unable to query the layer extent of table '"; - - if (! schema_.empty()) - { - s_error << schema_ << "."; - } - s_error << geometry_table_ << "' because we cannot determine the geometry field name." + append_geometry_table(s_error); + s_error << "' because we cannot determine the geometry field name." << "\nPlease provide either an 'extent' parameter to skip this query, " << "a 'geometry_field' and/or 'geometry_table' parameter, or add a " << "record to the 'geometry_columns' for your table."; @@ -1070,19 +1020,19 @@ box2d postgis_datasource::envelope() const { s << "SELECT ST_XMin(ext),ST_YMin(ext),ST_XMax(ext),ST_YMax(ext)" << " FROM (SELECT ST_EstimatedExtent('"; - - if (! schema_.empty()) + if (!parsed_schema_.empty()) { - s << mapnik::sql_utils::unquote_double(schema_) << "','"; + s << literal(parsed_schema_) << ','; } - s << mapnik::sql_utils::unquote_double(geometry_table_) << "','" - << mapnik::sql_utils::unquote_double(geometryColumn_) << "') as ext) as tmp"; + s << literal(parsed_table_) << ',' + << literal(geometryColumn_) << ") as ext) as tmp"; } else { s << "SELECT ST_XMin(ext),ST_YMin(ext),ST_XMax(ext),ST_YMax(ext)" - << " FROM (SELECT ST_Extent(" < postgis_datasource::envelope() const } else { - if (! schema_.empty()) - { - s << schema_ << "."; - } - // but if the subquery does not limit records then querying the // actual table will be faster as indexes can be used - s << geometry_table_ << ") as tmp"; + append_geometry_table(s); + s << ") as tmp"; } } @@ -1142,21 +1088,15 @@ boost::optional postgis_datasource::get_geometry_ std::string g_type; try { - s << "SELECT lower(type) as type FROM " - << GEOMETRY_COLUMNS <<" WHERE f_table_name='" - << mapnik::sql_utils::unquote_double(geometry_table_) - << "'"; - if (! schema_.empty()) + s << "SELECT lower(type) as type FROM " << GEOMETRY_COLUMNS + << " WHERE f_table_name=" << literal(parsed_table_); + if (!parsed_schema_.empty()) { - s << " AND f_table_schema='" - << mapnik::sql_utils::unquote_double(schema_) - << "'"; + s << " AND f_table_schema=" << literal(parsed_schema_); } - if (! geometry_field_.empty()) + if (!geometryColumn_.empty()) { - s << " AND f_geometry_column='" - << mapnik::sql_utils::unquote_double(geometry_field_) - << "'"; + s << " AND f_geometry_column=" << literal(geometryColumn_); } shared_ptr rs = conn->executeQuery(s.str()); if (rs->next()) @@ -1195,8 +1135,8 @@ boost::optional postgis_datasource::get_geometry_ std::string prev_type(""); - s << "SELECT ST_GeometryType(\"" - << geometryColumn_ << "\") AS geom" + s << "SELECT ST_GeometryType(" + << identifier(geometryColumn_) << ") AS geom" << " FROM " << populate_tokens(table_); if (row_limit_ > 0 && row_limit_ < 5) diff --git a/plugins/input/postgis/postgis_datasource.hpp b/plugins/input/postgis/postgis_datasource.hpp index db41e1233..636ccd4e8 100644 --- a/plugins/input/postgis/postgis_datasource.hpp +++ b/plugins/input/postgis/postgis_datasource.hpp @@ -37,10 +37,10 @@ // boost #include -#include // stl #include +#include #include #include @@ -84,20 +84,22 @@ private: box2d const& env, double pixel_width, double pixel_height, - mapnik::attributes const& vars) const; + mapnik::attributes const& vars, + bool intersect = true) const; std::string populate_tokens(std::string const& sql) const; + void append_geometry_table(std::ostream & os) const; std::shared_ptr get_resultset(std::shared_ptr &conn, std::string const& sql, CnxPool_ptr const& pool, processor_context_ptr ctx= processor_context_ptr()) const; static const std::string GEOMETRY_COLUMNS; static const std::string SPATIAL_REF_SYS; - static const double FMAX; const std::string uri_; const std::string username_; const std::string password_; const std::string table_; - std::string schema_; - std::string geometry_table_; + const std::string geometry_table_; const std::string geometry_field_; + std::string parsed_schema_; + std::string parsed_table_; std::string key_field_; mapnik::value_integer cursor_fetch_size_; mapnik::value_integer row_limit_; @@ -109,10 +111,6 @@ private: bool simplify_geometries_; layer_descriptor desc_; ConnectionCreator creator_; - const std::string bbox_token_; - const std::string scale_denom_token_; - const std::string pixel_width_token_; - const std::string pixel_height_token_; int pool_max_size_; bool persist_connection_; bool extent_from_subquery_; @@ -126,7 +124,7 @@ private: mapnik::value_double simplify_prefilter_; bool simplify_dp_preserve_; mapnik::value_double simplify_clip_resolution_; - boost::regex pattern_; + std::regex re_tokens_; int intersect_min_scale_; int intersect_max_scale_; bool key_field_as_attribute_; diff --git a/test/unit/datasource/postgis.cpp b/test/unit/datasource/postgis.cpp index 219dcda8e..f74ee1548 100644 --- a/test/unit/datasource/postgis.cpp +++ b/test/unit/datasource/postgis.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include /* @@ -185,6 +186,24 @@ TEST_CASE("postgis") { CHECK(ds->get_geometry_type() == mapnik::datasource_geometry_t::Point); } + SECTION("Postgis properly escapes names with single quotes") + { + mapnik::parameters params(base_params); + params["table"] = "\"test'single'quotes\""; + auto ds = mapnik::datasource_cache::instance().create(params); + REQUIRE(ds != nullptr); + CHECK(ds->get_geometry_type() == mapnik::datasource_geometry_t::Point); + } + + SECTION("Postgis properly escapes names with double quotes") + { + mapnik::parameters params(base_params); + params["table"] = "\"test\"\"double\"\"quotes\""; + auto ds = mapnik::datasource_cache::instance().create(params); + REQUIRE(ds != nullptr); + CHECK(ds->get_geometry_type() == mapnik::datasource_geometry_t::Point); + } + SECTION("Postgis query field names") { mapnik::parameters params(base_params); @@ -270,6 +289,55 @@ TEST_CASE("postgis") { REQUIRE(ext.maxy() == 4); } + SECTION("Postgis doesn't interpret @domain in email address as @variable") + { + mapnik::parameters params(base_params); + params["table"] = "(SELECT gid, geom, 'fake@mail.ru' as email" + " FROM public.test LIMIT 1) AS data"; + auto ds = mapnik::datasource_cache::instance().create(params); + REQUIRE(ds != nullptr); + auto featureset = all_features(ds); + auto feature = featureset->next(); + CHECKED_IF(feature != nullptr) + { + CHECK(feature->get("email").to_string() == "fake@mail.ru"); + } + } + + SECTION("Postgis interpolates !@uservar! tokens in query") + { + mapnik::parameters params(base_params); + params["table"] = "(SELECT * FROM public.test" + " WHERE GeometryType(geom) = !@wantedGeomType!" + " LIMIT 1) AS data"; + auto ds = mapnik::datasource_cache::instance().create(params); + REQUIRE(ds != nullptr); + + mapnik::transcoder tr("utf8"); + mapnik::query qry(ds->envelope()); + qry.set_variables({{"wantedGeomType", tr.transcode("POINT")}}); + CHECK(qry.variables().count("wantedGeomType") == 1); + + auto featureset = ds->features(qry); + auto feature = featureset->next(); + CHECKED_IF(feature != nullptr) + { + auto const& geom = feature->get_geometry(); + CHECK(mapnik::geometry::geometry_type(geom) == mapnik::geometry::Point); + } + + qry.set_variables({{"wantedGeomType", tr.transcode("POLYGON")}}); + CHECK(qry.variables().count("wantedGeomType") == 1); + + featureset = ds->features(qry); + feature = featureset->next(); + CHECKED_IF(feature != nullptr) + { + auto const& geom = feature->get_geometry(); + CHECK(mapnik::geometry::geometry_type(geom) == mapnik::geometry::Polygon); + } + } + SECTION("Postgis query extent: full dataset") { //include schema to increase coverage