diff options
| -rw-r--r-- | scripts/reference_exported_symbols.txt | 1 | ||||
| -rw-r--r-- | src/iso19111/c_api.cpp | 73 | ||||
| -rw-r--r-- | src/iso19111/factory.cpp | 100 | ||||
| -rw-r--r-- | src/proj.h | 3 | ||||
| -rw-r--r-- | test/unit/test_c_api.cpp | 80 | ||||
| -rw-r--r-- | test/unit/test_factory.cpp | 9 |
6 files changed, 192 insertions, 74 deletions
diff --git a/scripts/reference_exported_symbols.txt b/scripts/reference_exported_symbols.txt index aa148762..a4716e96 100644 --- a/scripts/reference_exported_symbols.txt +++ b/scripts/reference_exported_symbols.txt @@ -792,6 +792,7 @@ proj_context_errno proj_context_errno_string proj_context_get_database_metadata proj_context_get_database_path +proj_context_get_database_structure proj_context_get_url_endpoint proj_context_get_use_proj4_init_rules proj_context_get_user_writable_directory diff --git a/src/iso19111/c_api.cpp b/src/iso19111/c_api.cpp index a79b387e..2a655e51 100644 --- a/src/iso19111/c_api.cpp +++ b/src/iso19111/c_api.cpp @@ -105,6 +105,28 @@ static void PROJ_NO_INLINE proj_log_debug(PJ_CONTEXT *ctx, const char *function, // --------------------------------------------------------------------------- +template <class T> static PROJ_STRING_LIST to_string_list(T &&set) { + auto ret = new char *[set.size() + 1]; + size_t i = 0; + for (const auto &str : set) { + try { + ret[i] = new char[str.size() + 1]; + } catch (const std::exception &) { + while (--i > 0) { + delete[] ret[i]; + } + delete[] ret; + throw; + } + std::memcpy(ret[i], str.c_str(), str.size() + 1); + i++; + } + ret[i] = nullptr; + return ret; +} + +// --------------------------------------------------------------------------- + void proj_context_delete_cpp_context(struct projCppContext *cppContext) { delete cppContext; } @@ -390,6 +412,35 @@ const char *proj_context_get_database_metadata(PJ_CONTEXT *ctx, // --------------------------------------------------------------------------- +/** \brief Return the database structure + * + * Return SQL statements to run to initiate a new valid auxiliary empty + * database. It contains definitions of tables, views and triggers, as well + * as metadata for the version of the layout of the database. + * + * @param ctx PROJ context, or NULL for default context + * @param options null-terminated list of options, or NULL. None currently. + * @return list of SQL statements (to be freed with proj_string_list_destroy()), + * or NULL in case of error. + * @since 8.1 + */ +PROJ_STRING_LIST +proj_context_get_database_structure(PJ_CONTEXT *ctx, + const char *const *options) { + SANITIZE_CTX(ctx); + (void)options; + try { + auto ret = to_string_list(getDBcontext(ctx)->getDatabaseStructure()); + ctx->safeAutoCloseDbIfNeeded(); + return ret; + } catch (const std::exception &e) { + proj_log_error(ctx, __FUNCTION__, e.what()); + return nullptr; + } +} + +// --------------------------------------------------------------------------- + /** \brief Guess the "dialect" of the WKT string. * * @param ctx PROJ context, or NULL for default context @@ -508,28 +559,6 @@ PJ *proj_create(PJ_CONTEXT *ctx, const char *text) { // --------------------------------------------------------------------------- -template <class T> static PROJ_STRING_LIST to_string_list(T &&set) { - auto ret = new char *[set.size() + 1]; - size_t i = 0; - for (const auto &str : set) { - try { - ret[i] = new char[str.size() + 1]; - } catch (const std::exception &) { - while (--i > 0) { - delete[] ret[i]; - } - delete[] ret; - throw; - } - std::memcpy(ret[i], str.c_str(), str.size() + 1); - i++; - } - ret[i] = nullptr; - return ret; -} - -// --------------------------------------------------------------------------- - /** \brief Instantiate an object from a WKT string. * * This function calls osgeo::proj::io::WKTParser::createFromWKT() diff --git a/src/iso19111/factory.cpp b/src/iso19111/factory.cpp index 750e9054..af9736e6 100644 --- a/src/iso19111/factory.cpp +++ b/src/iso19111/factory.cpp @@ -274,6 +274,8 @@ struct DatabaseContext::Private { std::vector<std::string> auxiliaryDatabasePaths_{}; bool close_handle_ = true; sqlite3 *sqlite_handle_{}; + int nLayoutVersionMajor_ = 0; + int nLayoutVersionMinor_ = 0; std::map<std::string, sqlite3_stmt *> mapSqlToStatement_{}; PJ_CONTEXT *pjCtxt_ = nullptr; int recLevel_ = 0; @@ -305,7 +307,8 @@ struct DatabaseContext::Private { lru11::Cache<std::string, std::list<std::string>> cacheAliasNames_{ CACHE_SIZE}; - void checkDatabaseLayout(); + void checkDatabaseLayout(const std::string &path, + const std::string &dbNamePrefix); static void insertIntoCache(LRUCacheOfObjects &cache, const std::string &code, @@ -709,10 +712,22 @@ void DatabaseContext::Private::open(const std::string &databasePath, // --------------------------------------------------------------------------- -void DatabaseContext::Private::checkDatabaseLayout() { - auto res = run("SELECT key, value FROM metadata WHERE key IN " +void DatabaseContext::Private::checkDatabaseLayout( + const std::string &path, const std::string &dbNamePrefix) { + if (!dbNamePrefix.empty() && run("SELECT 1 FROM " + dbNamePrefix + + "sqlite_master WHERE name = 'metadata'") + .empty()) { + // Accept auxiliary databases without metadata table (sparse DBs) + return; + } + auto res = run("SELECT key, value FROM " + dbNamePrefix + + "metadata WHERE key IN " "('DATABASE.LAYOUT.VERSION.MAJOR', " "'DATABASE.LAYOUT.VERSION.MINOR')"); + if (res.empty() && !dbNamePrefix.empty()) { + // Accept auxiliary databases without layout metadata. + return; + } if (res.size() != 2) { // The database layout of PROJ 7.2 that shipped with EPSG v10.003 is // at the time of writing still compatible of the one we support. @@ -730,36 +745,47 @@ void DatabaseContext::Private::checkDatabaseLayout() { } throw FactoryException( - databasePath_ + - " lacks DATABASE.LAYOUT.VERSION.MAJOR / " - "DATABASE.LAYOUT.VERSION.MINOR " - "metadata. It comes from another PROJ installation."); + path + " lacks DATABASE.LAYOUT.VERSION.MAJOR / " + "DATABASE.LAYOUT.VERSION.MINOR " + "metadata. It comes from another PROJ installation."); } - int nMajor = 0; - int nMinor = 0; + int major = 0; + int minor = 0; for (const auto &row : res) { if (row[0] == "DATABASE.LAYOUT.VERSION.MAJOR") { - nMajor = atoi(row[1].c_str()); + major = atoi(row[1].c_str()); } else if (row[0] == "DATABASE.LAYOUT.VERSION.MINOR") { - nMinor = atoi(row[1].c_str()); + minor = atoi(row[1].c_str()); } } - if (nMajor != DATABASE_LAYOUT_VERSION_MAJOR) { + if (major != DATABASE_LAYOUT_VERSION_MAJOR) { throw FactoryException( - databasePath_ + - " contains DATABASE.LAYOUT.VERSION.MAJOR = " + toString(nMajor) + + path + + " contains DATABASE.LAYOUT.VERSION.MAJOR = " + toString(major) + " whereas " + toString(DATABASE_LAYOUT_VERSION_MAJOR) + " is expected. " "It comes from another PROJ installation."); } - if (nMinor < DATABASE_LAYOUT_VERSION_MINOR) { + if (minor < DATABASE_LAYOUT_VERSION_MINOR) { throw FactoryException( - databasePath_ + - " contains DATABASE.LAYOUT.VERSION.MINOR = " + toString(nMinor) + + path + + " contains DATABASE.LAYOUT.VERSION.MINOR = " + toString(minor) + " whereas a number >= " + toString(DATABASE_LAYOUT_VERSION_MINOR) + " is expected. " "It comes from another PROJ installation."); } + if (dbNamePrefix.empty()) { + nLayoutVersionMajor_ = major; + nLayoutVersionMinor_ = minor; + } else if (nLayoutVersionMajor_ != major || nLayoutVersionMinor_ != minor) { + throw FactoryException( + "Auxiliary database " + path + + " contains a DATABASE.LAYOUT.VERSION = " + toString(major) + '.' + + toString(minor) + + " which is different from the one from the main database " + + databasePath_ + " which is " + toString(nLayoutVersionMajor_) + + '.' + toString(nLayoutVersionMinor_)); + } } // --------------------------------------------------------------------------- @@ -777,17 +803,28 @@ void DatabaseContext::Private::setHandle(sqlite3 *sqlite_handle) { // --------------------------------------------------------------------------- std::vector<std::string> DatabaseContext::Private::getDatabaseStructure() { - const char *sqls[] = { - "SELECT sql FROM sqlite_master WHERE type = 'table'", - "SELECT sql FROM sqlite_master WHERE type = 'view'", - "SELECT sql FROM sqlite_master WHERE type = 'trigger'"}; + const std::string dbNamePrefix(auxiliaryDatabasePaths_.empty() && + memoryDbForInsertPath_.empty() + ? "" + : "db_0."); + const auto sqlBegin("SELECT sql||';' FROM " + dbNamePrefix + + "sqlite_master WHERE type = "); + const char *const objectTypes[] = {"'table'", "'view'", "'trigger'"}; std::vector<std::string> res; - for (const auto &sql : sqls) { - auto sqlRes = run(sql); + for (const auto &objectType : objectTypes) { + const auto sqlRes = run(sqlBegin + objectType); for (const auto &row : sqlRes) { res.emplace_back(row[0]); } } + if (nLayoutVersionMajor_ > 0) { + res.emplace_back( + "INSERT INTO metadata VALUES('DATABASE.LAYOUT.VERSION.MAJOR'," + + toString(nLayoutVersionMajor_) + ");"); + res.emplace_back( + "INSERT INTO metadata VALUES('DATABASE.LAYOUT.VERSION.MINOR'," + + toString(nLayoutVersionMinor_) + ");"); + } return res; } @@ -828,13 +865,16 @@ void DatabaseContext::Private::attachExtraDatabases( "' AS db_0"); detach_ = true; int count = 1; - for (const auto &otherDb : auxiliaryDatabasePaths) { + for (const auto &otherDbPath : auxiliaryDatabasePaths) { + const auto attachedDbName("db_" + toString(static_cast<int>(count))); std::string sql = "ATTACH DATABASE '"; - sql += replaceAll(otherDb, "'", "''"); - sql += "' AS db_"; - sql += toString(static_cast<int>(count)); + sql += replaceAll(otherDbPath, "'", "''"); + sql += "' AS "; + sql += attachedDbName; count++; run(sql); + + checkDatabaseLayout(otherDbPath, attachedDbName + '.'); } for (const auto &pair : tableStructure) { @@ -2452,11 +2492,11 @@ DatabaseContext::create(const std::string &databasePath, auto dbCtx = DatabaseContext::nn_make_shared<DatabaseContext>(); auto dbCtxPrivate = dbCtx->getPrivate(); dbCtxPrivate->open(databasePath, ctx); + dbCtxPrivate->checkDatabaseLayout(databasePath, std::string()); if (!auxiliaryDatabasePaths.empty()) { dbCtxPrivate->attachExtraDatabases(auxiliaryDatabasePaths); dbCtxPrivate->auxiliaryDatabasePaths_ = auxiliaryDatabasePaths; } - dbCtxPrivate->checkDatabaseLayout(); dbCtxPrivate->self_ = dbCtx.as_nullable(); return dbCtx; } @@ -2527,6 +2567,9 @@ void DatabaseContext::startInsertStatementsSession() { "stopInsertStatementsSession() is."); } + d->memoryDbForInsertPath_.clear(); + const auto sqlStatements = getDatabaseStructure(); + // Create a in-memory temporary sqlite3 database std::ostringstream buffer; buffer << "file:temp_db_for_insert_statements_"; @@ -2541,7 +2584,6 @@ void DatabaseContext::startInsertStatementsSession() { } // Fill the structure of this database - const auto sqlStatements = getDatabaseStructure(); for (const auto &sql : sqlStatements) { if (sqlite3_exec(d->memoryDbHandle_, sql.c_str(), nullptr, nullptr, nullptr) != SQLITE_OK) { @@ -1044,6 +1044,9 @@ const char PROJ_DLL *proj_context_get_database_path(PJ_CONTEXT *ctx); const char PROJ_DLL *proj_context_get_database_metadata(PJ_CONTEXT* ctx, const char* key); +PROJ_STRING_LIST PROJ_DLL proj_context_get_database_structure( + PJ_CONTEXT* ctx, + const char* const *options); PJ_GUESSED_WKT_DIALECT PROJ_DLL proj_context_guess_wkt_dialect(PJ_CONTEXT *ctx, const char *wkt); diff --git a/test/unit/test_c_api.cpp b/test/unit/test_c_api.cpp index 696e3615..0bdb9287 100644 --- a/test/unit/test_c_api.cpp +++ b/test/unit/test_c_api.cpp @@ -1772,29 +1772,58 @@ TEST_F(CApi, proj_context_set_database_path_null) { // --------------------------------------------------------------------------- -TEST_F(CApi, proj_context_set_database_path_main_memory_one_aux) { +TEST_F(CApi, proj_context_set_database_path_aux) { + + const std::string auxDbName( + "file:proj_test_aux.db?mode=memory&cache=shared"); + + sqlite3 *dbAux = nullptr; + sqlite3_open_v2( + auxDbName.c_str(), &dbAux, + SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_URI, nullptr); + ASSERT_TRUE(dbAux != nullptr); + ASSERT_TRUE(sqlite3_exec(dbAux, "BEGIN", nullptr, nullptr, nullptr) == + SQLITE_OK); + { + auto ctxt = DatabaseContext::create(); + const auto dbStructure = ctxt->getDatabaseStructure(); + for (const auto &sql : dbStructure) { + ASSERT_TRUE(sqlite3_exec(dbAux, sql.c_str(), nullptr, nullptr, + nullptr) == SQLITE_OK); + } + } - auto c_path = proj_context_get_database_path(m_ctxt); - ASSERT_TRUE(c_path != nullptr); - std::string path(c_path); - const char *aux_db_list[] = {path.c_str(), nullptr}; - - // This is super exotic and a miracle that it works. :memory: as the - // main DB is empty. The real stuff is in the aux_db_list. No view - // is created in the ':memory:' internal DB, but as there's only one - // aux DB its tables and views can be directly queried... - // If that breaks at some point, that wouldn't be a big issue. - // Keeping that one as I had a hard time figuring out why it worked ! - // The real thing is tested by the C++ - // factory::attachExtraDatabases_auxiliary - EXPECT_TRUE(proj_context_set_database_path(m_ctxt, ":memory:", aux_db_list, - nullptr)); + ASSERT_TRUE(sqlite3_exec( + dbAux, + "INSERT INTO geodetic_crs VALUES('OTHER','OTHER_4326','WGS " + "84',NULL,'geographic 2D','EPSG','6422','EPSG','6326'," + "NULL,0);", + nullptr, nullptr, nullptr) == SQLITE_OK); + ASSERT_TRUE(sqlite3_exec(dbAux, "COMMIT", nullptr, nullptr, nullptr) == + SQLITE_OK); - auto source_crs = proj_create_from_database(m_ctxt, "EPSG", "4326", - PJ_CATEGORY_CRS, false, - nullptr); // WGS84 - ASSERT_NE(source_crs, nullptr); - ObjectKeeper keeper_source_crs(source_crs); + const char *const aux_db_list[] = {auxDbName.c_str(), nullptr}; + + EXPECT_TRUE( + proj_context_set_database_path(m_ctxt, nullptr, aux_db_list, nullptr)); + + sqlite3_close(dbAux); + + { + auto crs = proj_create_from_database(m_ctxt, "EPSG", "4326", + PJ_CATEGORY_CRS, false, + nullptr); // WGS84 + ASSERT_NE(crs, nullptr); + ObjectKeeper keeper_source_crs(crs); + } + + { + auto crs = proj_create_from_database(m_ctxt, "OTHER", "OTHER_4326", + PJ_CATEGORY_CRS, false, + nullptr); // WGS84 + ASSERT_NE(crs, nullptr); + ObjectKeeper keeper_source_crs(crs); + } } // --------------------------------------------------------------------------- @@ -2724,6 +2753,15 @@ TEST_F(CApi, proj_context_get_database_metadata) { // --------------------------------------------------------------------------- +TEST_F(CApi, proj_context_get_database_structure) { + auto list = proj_context_get_database_structure(m_ctxt, nullptr); + ASSERT_NE(list, nullptr); + ASSERT_NE(list[0], nullptr); + proj_string_list_destroy(list); +} + +// --------------------------------------------------------------------------- + TEST_F(CApi, proj_clone) { auto obj = proj_create(m_ctxt, "+proj=longlat"); ObjectKeeper keeper(obj); diff --git a/test/unit/test_factory.cpp b/test/unit/test_factory.cpp index 5d336d0c..e0616baa 100644 --- a/test/unit/test_factory.cpp +++ b/test/unit/test_factory.cpp @@ -2828,10 +2828,12 @@ TEST(factory, attachExtraDatabases_auxiliary) { ASSERT_TRUE(dbAux != nullptr); ASSERT_TRUE(sqlite3_exec(dbAux, "BEGIN", nullptr, nullptr, nullptr) == SQLITE_OK); + + std::vector<std::string> tableStructureBefore; { auto ctxt = DatabaseContext::create(); - const auto dbStructure = ctxt->getDatabaseStructure(); - for (const auto &sql : dbStructure) { + tableStructureBefore = ctxt->getDatabaseStructure(); + for (const auto &sql : tableStructureBefore) { if (sql.find("CREATE TRIGGER") == std::string::npos) { ASSERT_TRUE(sqlite3_exec(dbAux, sql.c_str(), nullptr, nullptr, nullptr) == SQLITE_OK); @@ -2864,6 +2866,9 @@ TEST(factory, attachExtraDatabases_auxiliary) { auto gcrs = nn_dynamic_pointer_cast<GeographicCRS>(crs); EXPECT_TRUE(gcrs != nullptr); } + + const auto dbStructure = ctxt->getDatabaseStructure(); + EXPECT_EQ(dbStructure, tableStructureBefore); } { |
