From 5487967d8774383f11dfef8807cc58c5d935b9ea Mon Sep 17 00:00:00 2001 From: Alex Ameen Date: Wed, 23 Aug 2023 10:50:43 -0500 Subject: [PATCH] feat: query DB packages with filters (#40) * more query builder * query builder test case * fix verbosity in 'tests/read.cc' * validate params * carry error code in PkgQueryArgException * omit packages which do not record a license when filtering by licenses * test licenses, allowBroken, and allowUnfree * fmt * Update src/pkgdb/query-builder.cc Co-authored-by: Matthew Kenigsberg * Update src/pkgdb/query-builder.cc Co-authored-by: Matthew Kenigsberg * Update query-builder.cc * Update query-builder.cc * review changes * use EXPECT_EQ in buildPkgQuery tests * fmt * fmt * don't use '.value()' on 'std::optional's * Update src/pkgdb/query-builder.cc Co-authored-by: Matthew Kenigsberg * fix lints * fix lints --------- Co-authored-by: Matthew Kenigsberg --- .clang-tidy | 57 +++++ .gitignore | 1 + Makefile | 9 +- flake.nix | 5 + include/flox/core/command.hh | 4 +- include/flox/flox-flake.hh | 2 +- include/flox/package.hh | 14 +- include/flox/pkgdb/query-builder.hh | 104 +++++++-- include/flox/pkgdb/read.hh | 5 + src/command.cc | 16 +- src/flake-package.cc | 45 ++-- src/flox-flake.cc | 8 +- src/get.cc | 38 ++-- src/main.cc | 4 +- src/pkgdb/command.cc | 12 +- src/pkgdb/query-builder.cc | 215 +++++++++++++++++-- src/pkgdb/read.cc | 2 +- src/pkgdb/write.cc | 26 +-- src/raw-package.cc | 8 +- src/scrape.cc | 8 +- src/semver.cc | 25 ++- tests/.gitignore | 1 + tests/pkgdb.cc | 317 +++++++++++++++++++++++++++- tests/read.cc | 44 ++-- 24 files changed, 784 insertions(+), 186 deletions(-) create mode 100644 .clang-tidy diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 00000000..0288b499 --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,57 @@ +--- +Checks: > + clang-diagnostic-* + ,clang-analyzer-* + ,bugprone-* + ,cert-* + ,readability-* + ,performance-* + ,portability-* + ,hicpp-* + ,cppcoreguidelines-* + ,modernize-* + ,-modernize-use-trailing-return-type + ,-cppcoreguidelines-pro-type-union-access + +WarningsAsErrors: '' +HeaderFilterRegex: '' +AnalyzeTemporaryDtors: false +FormatStyle: none +CheckOptions: + - key: llvm-else-after-return.WarnOnConditionVariables + value: 'false' + - key: modernize-loop-convert.MinConfidence + value: reasonable + - key: modernize-replace-auto-ptr.IncludeStyle + value: llvm + - key: cert-str34-c.DiagnoseSignedUnsignedCharComparisons + value: 'false' + - key: google-readability-namespace-comments.ShortNamespaceLines + value: '10' + - key: cert-err33-c.CheckedFunctions + value: '::aligned_alloc;::asctime_s;::at_quick_exit;::atexit;::bsearch;::bsearch_s;::btowc;::c16rtomb;::c32rtomb;::calloc;::clock;::cnd_broadcast;::cnd_init;::cnd_signal;::cnd_timedwait;::cnd_wait;::ctime_s;::fclose;::fflush;::fgetc;::fgetpos;::fgets;::fgetwc;::fopen;::fopen_s;::fprintf;::fprintf_s;::fputc;::fputs;::fputwc;::fputws;::fread;::freopen;::freopen_s;::fscanf;::fscanf_s;::fseek;::fsetpos;::ftell;::fwprintf;::fwprintf_s;::fwrite;::fwscanf;::fwscanf_s;::getc;::getchar;::getenv;::getenv_s;::gets_s;::getwc;::getwchar;::gmtime;::gmtime_s;::localtime;::localtime_s;::malloc;::mbrtoc16;::mbrtoc32;::mbsrtowcs;::mbsrtowcs_s;::mbstowcs;::mbstowcs_s;::memchr;::mktime;::mtx_init;::mtx_lock;::mtx_timedlock;::mtx_trylock;::mtx_unlock;::printf_s;::putc;::putwc;::raise;::realloc;::remove;::rename;::scanf;::scanf_s;::setlocale;::setvbuf;::signal;::snprintf;::snprintf_s;::sprintf;::sprintf_s;::sscanf;::sscanf_s;::strchr;::strerror_s;::strftime;::strpbrk;::strrchr;::strstr;::strtod;::strtof;::strtoimax;::strtok;::strtok_s;::strtol;::strtold;::strtoll;::strtoul;::strtoull;::strtoumax;::strxfrm;::swprintf;::swprintf_s;::swscanf;::swscanf_s;::thrd_create;::thrd_detach;::thrd_join;::thrd_sleep;::time;::timespec_get;::tmpfile;::tmpfile_s;::tmpnam;::tmpnam_s;::tss_create;::tss_get;::tss_set;::ungetc;::ungetwc;::vfprintf;::vfprintf_s;::vfscanf;::vfscanf_s;::vfwprintf;::vfwprintf_s;::vfwscanf;::vfwscanf_s;::vprintf_s;::vscanf;::vscanf_s;::vsnprintf;::vsnprintf_s;::vsprintf;::vsprintf_s;::vsscanf;::vsscanf_s;::vswprintf;::vswprintf_s;::vswscanf;::vswscanf_s;::vwprintf_s;::vwscanf;::vwscanf_s;::wcrtomb;::wcschr;::wcsftime;::wcspbrk;::wcsrchr;::wcsrtombs;::wcsrtombs_s;::wcsstr;::wcstod;::wcstof;::wcstoimax;::wcstok;::wcstok_s;::wcstol;::wcstold;::wcstoll;::wcstombs;::wcstombs_s;::wcstoul;::wcstoull;::wcstoumax;::wcsxfrm;::wctob;::wctrans;::wctype;::wmemchr;::wprintf_s;::wscanf;::wscanf_s;' + - key: cert-oop54-cpp.WarnOnlyIfThisHasSuspiciousField + value: 'false' + - key: cert-dcl16-c.NewSuffixes + value: 'L;LL;LU;LLU' + - key: google-readability-braces-around-statements.ShortStatementLines + value: '1' + - key: cppcoreguidelines-non-private-member-variables-in-classes.IgnoreClassesWithAllMemberVariablesBeingPublic + value: 'true' + - key: google-readability-namespace-comments.SpacesBeforeComments + value: '2' + - key: modernize-loop-convert.MaxCopySize + value: '16' + - key: modernize-pass-by-value.IncludeStyle + value: llvm + - key: modernize-use-nullptr.NullMacros + value: 'NULL' + - key: llvm-qualified-auto.AddConstToQualified + value: 'false' + - key: modernize-loop-convert.NamingStyle + value: CamelCase + - key: llvm-else-after-return.WarnOnUnfixable + value: 'false' + - key: google-readability-function-size.StatementThreshold + value: '800' +... diff --git a/.gitignore b/.gitignore index 25ca8757..e1bb75a3 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,4 @@ *.db result result-* +compile_commands.json diff --git a/Makefile b/Makefile index 5da0c2c2..e32aa1fe 100644 --- a/Makefile +++ b/Makefile @@ -27,6 +27,7 @@ TR ?= tr SED ?= sed TEST ?= test DOXYGEN ?= doxygen +BEAR ?= bear # ---------------------------------------------------------------------------- # @@ -279,8 +280,9 @@ most: bin lib ignores # ---------------------------------------------------------------------------- # -.PHONY: ccls +.PHONY: ccls cdb ccls: .ccls +cdb: compile_commands.json .ccls: FORCE echo 'clang' > "$@"; @@ -295,6 +297,11 @@ ccls: .ccls }|$(TR) ' ' '\n'|$(SED) 's/-std=/%cpp -std=/' >> "$@"; +compile_commands.json: FORCE + -$(MAKE) -C $(MAKEFILE_DIR) clean; + $(BEAR) --output "$@" -- $(MAKE) -C $(MAKEFILE_DIR) -j all; + + # ---------------------------------------------------------------------------- # .PHONY: docs diff --git a/flake.nix b/flake.nix index ccef9ef4..fc09bc07 100644 --- a/flake.nix +++ b/flake.nix @@ -95,6 +95,11 @@ ( if pkgsFor.stdenv.cc.isGNU then pkgsFor.gdb else pkgsFor.lldb ) # For doc pkgsFor.doxygen + # For IDEs + pkgsFor.ccls + pkgsFor.bear + # For lints/fmt + pkgsFor.clang-tools ] ++ nixpkgs.lib.optionals pkgsFor.stdenv.isLinux [ # For debugging pkgsFor.valgrind diff --git a/include/flox/core/command.hh b/include/flox/core/command.hh index 577d87f2..545af160 100644 --- a/include/flox/core/command.hh +++ b/include/flox/core/command.hh @@ -44,7 +44,9 @@ namespace flox { * } Verbosity; */ struct VerboseParser : public argparse::ArgumentParser { - explicit VerboseParser( std::string name, std::string version = "0.1.0" ); + explicit VerboseParser( const std::string & name + , const std::string & version = "0.1.0" + ); }; /* End struct `VerboseParser' */ diff --git a/include/flox/flox-flake.hh b/include/flox/flox-flake.hh index 23456aac..b43e143c 100644 --- a/include/flox/flox-flake.hh +++ b/include/flox/flox-flake.hh @@ -70,7 +70,7 @@ class FloxFlake : public std::enable_shared_from_this { nix::ref state; const nix::flake::LockedFlake lockedFlake; - FloxFlake( nix::ref state + FloxFlake( const nix::ref & state , const nix::FlakeRef & ref ); diff --git a/include/flox/package.hh b/include/flox/package.hh index 7123cfa0..3dff16e5 100644 --- a/include/flox/package.hh +++ b/include/flox/package.hh @@ -153,7 +153,7 @@ class Package { { std::optional version = this->getVersion(); if ( ! version.has_value() ) { return std::nullopt; } - return versions::coerceSemver( version.value() ); + return versions::coerceSemver( * version ); } /** @@ -194,26 +194,26 @@ class Package { } } }; std::optional os = this->getVersion(); - if ( os.has_value() ) { j[system].emplace( "version", os.value() ); } + if ( os.has_value() ) { j[system].emplace( "version", * os ); } else { j[system].emplace( "version", nlohmann::json() ); } os = this->getSemver(); - if ( os.has_value() ) { j[system].emplace( "semver", os.value() ); } + if ( os.has_value() ) { j[system].emplace( "semver", * os ); } else { j[system].emplace( "semver", nlohmann::json() ); } j[system].emplace( "outputs", this->getOutputs() ); j[system].emplace( "outputsToInstall", this->getOutputsToInstall() ); os = this->getLicense(); - if ( os.has_value() ) { j[system].emplace( "license", os.value() ); } + if ( os.has_value() ) { j[system].emplace( "license", * os ); } else { j[system].emplace( "license", nlohmann::json() ); } std::optional ob = this->isBroken(); - if ( ob.has_value() ) { j[system].emplace( "broken", ob.value() ); } + if ( ob.has_value() ) { j[system].emplace( "broken", * ob ); } else { j[system].emplace( "broken", nlohmann::json() ); } ob = this->isUnfree(); - if ( ob.has_value() ) { j[system].emplace( "unfree", ob.value() ); } + if ( ob.has_value() ) { j[system].emplace( "unfree", * ob ); } else { j[system].emplace( "unfree", nlohmann::json() ); } if ( withDescription ) @@ -221,7 +221,7 @@ class Package { std::optional od = this->getDescription(); if ( od.has_value() ) { - j[system].emplace( "description", od.value() ); + j[system].emplace( "description", * od ); } else { diff --git a/include/flox/pkgdb/query-builder.hh b/include/flox/pkgdb/query-builder.hh index 85cb4490..2eb2c139 100644 --- a/include/flox/pkgdb/query-builder.hh +++ b/include/flox/pkgdb/query-builder.hh @@ -12,9 +12,12 @@ #include #include #include +#include +#include #include +#include "flox/core/exceptions.hh" #include "flox/core/types.hh" #include "flox/core/util.hh" @@ -27,44 +30,103 @@ namespace flox { /* -------------------------------------------------------------------------- */ +/** + * Collection of query parameters used to lookup packages in a database. + * These use a combination of SQL statements and post processing with + * `node-semver` to produce a list of satisfactory packages. + */ struct PkgQueryArgs { - /** Match partial name/description */ - std::optional match; - std::optional pathPrefix; - std::optional subtree; - std::optional stability; - std::optional system; - - std::optional name; - std::optional pname; - std::optional version; - std::optional semver; + + /** Filter results by partial name/description match. */ + std::optional match; + std::optional name; /**< Filter results by exact `name`. */ + std::optional pname; /**< Filter results by exact `pname`. */ + std::optional version; /**< Filter results by exact version. */ + std::optional semver; /**< Filter results by version range. */ + + /** Filter results to those explicitly marked with the given licenses. */ std::optional> licenses; - bool allowBroken = false; - bool allowUnfree = true; + /** Whether to include packages which are explicitly marked `broken`. */ + bool allowBroken = false; + /** Whether to include packages which are explicitly marked `unfree`. */ + bool allowUnfree = true; + /** Whether pre-release versions should be ordered before releases. */ bool preferPreReleases = false; + /** + * Subtrees to search. + * TODO: Default to first of `catalog`, `packages`, or `legacyPackages`. + * Requires `db` to be read. + */ std::optional> subtrees; - std::vector systems = { nix::settings.thisSystem.get() }; - std::vector stabilities = flox::defaultCatalogStabilities; + /** Systems to search */ + std::vector systems = { nix::settings.thisSystem.get() }; + + /** Stabilities to search ( if any ) */ + std::optional> stabilities; + + + /** Errors concerning validity of package query parameters. */ + struct PkgQueryInvalidArgException : public flox::FloxException { + public: + enum error_code { + PDQEC_ERROR = 1 /**< Generic Exception */ + /** Name/{pname,version,semver} are mutually exclusive */ + , PDQEC_MIX_NAME = 2 + /** Version/semver are mutually exclusive */ + , PDQEC_MIX_VERSION_SEMVER = 3 + , PDQEC_INVALID_SEMVER = 4 /**< Semver Parse Error */ + , PDQEC_INVALID_LICENSE = 5 /**< License has invalid character */ + , PDQEC_INVALID_SUBTREE = 6 /**< Unrecognized subtree */ + , PDQEC_CONFLICTING_SUBTREE = 7 /**< Conflicting subtree/stability */ + , PDQEC_INVALID_SYSTEM = 8 /**< Unrecognized/unsupported system */ + , PDQEC_INVALID_STABILITY = 9 /**< Unrecognized stability */ + } errorCode; + protected: + static std::string errorMessage( const error_code & ec ); + public: + PkgQueryInvalidArgException( const error_code & ec = PDQEC_ERROR ) + : flox::FloxException( + PkgQueryInvalidArgException::errorMessage( ec ) + ) + , errorCode( ec ) + {} + }; /* End struct `PkgDbQueryInvalidArgException' */ /** * Sanity check parameters. * Make sure `systems` are valid systems. - * Make sure `stability` is a valid stability. - * Make sure `pathPrefix` is consistent with `subtree`, `systems`, - * and `stability`. + * Make sure `stabilities` are valid stabilities. * Make sure `name` is not set when `pname`, `version`, or `semver` are set. * Make sure `version` is not set when `semver` is set. - * @return `true` iff the above conditions are met. + * @return `std::nullopt` iff the above conditions are met, an error + * code otherwise. */ - bool validate(); + std::optional validate() const; + }; /* End struct `PkgQueryArgs' */ -std::string buildPkgQuery( PkgQueryArgs && params ); +/* -------------------------------------------------------------------------- */ + + /* A SQL statement string with a mapping of host parameters to their + * respective values. */ +using SQLBinds = std::unordered_map; + +/** + * Construct a SQL query string with a set of parameters to be bound. + * Binding is left to the caller to allow a single result to be reused across + * multiple databases. + * + * This routine does NOT perform filtering by `semver`. + * TODO: filter by `match`. + * + * @return A SQL statement string with a mapping of host parameters to their + * respective values. + */ +std::pair buildPkgQuery( const PkgQueryArgs & params ); /* -------------------------------------------------------------------------- */ diff --git a/include/flox/pkgdb/read.hh b/include/flox/pkgdb/read.hh index 64e7ead6..1c813d4e 100644 --- a/include/flox/pkgdb/read.hh +++ b/include/flox/pkgdb/read.hh @@ -261,6 +261,11 @@ class PkgDbReadOnly { */ std::optional distanceFromMatch( Package & pkg, std::string match ); +/* -------------------------------------------------------------------------- */ + +bool isSQLError( int rc ); + + /* -------------------------------------------------------------------------- */ } /* End Namespace `flox::pkgdb' */ diff --git a/src/command.cc b/src/command.cc index 55dc2923..b0d696e8 100644 --- a/src/command.cc +++ b/src/command.cc @@ -21,12 +21,13 @@ /* -------------------------------------------------------------------------- */ -namespace flox { - namespace command { +namespace flox::command { /* -------------------------------------------------------------------------- */ -VerboseParser::VerboseParser( std::string name, std::string version ) +VerboseParser::VerboseParser( const std::string & name + , const std::string & version + ) : argparse::ArgumentParser( name, version, argparse::default_arguments::help ) { this->add_argument( "-q", "--quiet" ) @@ -115,9 +116,9 @@ AttrPathMixin::addAttrPathArgs( argparse::ArgumentParser & parser ) .help( "Attribute path to scrape" ) .metavar( "ATTRS..." ) .remaining() - .action( [&]( const std::string & p ) + .action( [&]( const std::string & path ) { - this->attrPath.emplace_back( p ); + this->attrPath.emplace_back( path ); } ); } @@ -126,7 +127,7 @@ AttrPathMixin::addAttrPathArgs( argparse::ArgumentParser & parser ) void AttrPathMixin::postProcessArgs() { - if ( this->attrPath.size() < 1 ) { this->attrPath.push_back( "packages" ); } + if ( this->attrPath.empty() ) { this->attrPath.push_back( "packages" ); } if ( this->attrPath.size() < 2 ) { this->attrPath.push_back( @@ -144,8 +145,7 @@ AttrPathMixin::postProcessArgs() /* -------------------------------------------------------------------------- */ - } /* End namespaces `flox::command' */ -} /* End namespaces `flox' */ +} /* End namespaces `flox::command' */ /* -------------------------------------------------------------------------- * diff --git a/src/flake-package.cc b/src/flake-package.cc index b07aaf0f..bf6d2fad 100644 --- a/src/flake-package.cc +++ b/src/flake-package.cc @@ -59,36 +59,35 @@ FlakePackage::init( bool checkDrv ) this->_system = this->_pathS[1]; - MaybeCursor c = this->_cursor->maybeGetAttr( "meta" ); - this->_hasMetaAttr = c != nullptr; - if ( c != nullptr ) + MaybeCursor cursor = this->_cursor->maybeGetAttr( "meta" ); + this->_hasMetaAttr = cursor != nullptr; + if ( cursor != nullptr ) { - if ( c = c->maybeGetAttr( "license" ); c != nullptr ) + if ( cursor = cursor->maybeGetAttr( "license" ); cursor != nullptr ) { - try { this->_license = c->getAttr( "spdxId" )->getString(); } + try { this->_license = cursor->getAttr( "spdxId" )->getString(); } catch( ... ) {} } } - c = this->_cursor->maybeGetAttr( "pname" ); - if ( c != nullptr ) + cursor = this->_cursor->maybeGetAttr( "pname" ); + if ( cursor != nullptr ) { try { - this->_pname = c->getString(); + this->_pname = cursor->getString(); this->_hasPnameAttr = true; } catch( ... ) {} } /* Version and Semver */ - c = this->_cursor->maybeGetAttr( "version" ); - if ( c != nullptr ) + cursor = this->_cursor->maybeGetAttr( "version" ); + if ( cursor != nullptr ) { - std::string v; try { - this->_version = c->getString(); + this->_version = cursor->getString(); this->_hasVersionAttr = true; } catch( ... ) {} @@ -108,15 +107,15 @@ FlakePackage::getOutputsToInstall() const { if ( this->_hasMetaAttr ) { - MaybeCursor m = + MaybeCursor cursor = this->_cursor->getAttr( "meta" )->maybeGetAttr( "outputsToInstall" ); - if ( m != nullptr ) { return m->getListOfStrings(); } + if ( cursor != nullptr ) { return cursor->getListOfStrings(); } } std::vector rsl; - for ( std::string o : this->getOutputs() ) + for ( const std::string & output : this->getOutputs() ) { - rsl.push_back( o ); - if ( o == "out" ) { break; } + rsl.push_back( output ); + if ( output == "out" ) { break; } } return rsl; } @@ -130,10 +129,10 @@ FlakePackage::isBroken() const if ( ! this->_hasMetaAttr ) { return std::nullopt; } try { - MaybeCursor b = + MaybeCursor cursor = this->_cursor->getAttr( "meta" )->maybeGetAttr( "broken" ); - if ( b == nullptr ) { return std::nullopt; } - return b->getBool(); + if ( cursor == nullptr ) { return std::nullopt; } + return cursor->getBool(); } catch( ... ) { @@ -147,10 +146,10 @@ FlakePackage::isUnfree() const if ( ! this->_hasMetaAttr ) { return std::nullopt; } try { - MaybeCursor u = + MaybeCursor cursor = this->_cursor->getAttr( "meta" )->maybeGetAttr( "unfree" ); - if ( u == nullptr ) { return std::nullopt; } - return u->getBool(); + if ( cursor == nullptr ) { return std::nullopt; } + return cursor->getBool(); } catch( ... ) { diff --git a/src/flox-flake.cc b/src/flox-flake.cc index 1f027b5a..9b35b2aa 100644 --- a/src/flox-flake.cc +++ b/src/flox-flake.cc @@ -19,7 +19,7 @@ namespace flox { /* -------------------------------------------------------------------------- */ -FloxFlake::FloxFlake( nix::ref state +FloxFlake::FloxFlake( const nix::ref & state , const nix::FlakeRef & ref ) : state( state ) @@ -73,9 +73,9 @@ FloxFlake::openEvalCache() FloxFlake::maybeOpenCursor( const AttrPath & path ) { MaybeCursor cur = this->openEvalCache()->getRoot(); - for ( const auto & p : path ) + for ( const auto & part : path ) { - cur = cur->maybeGetAttr( p ); + cur = cur->maybeGetAttr( part ); if ( cur == nullptr ) { break; } } return cur; @@ -88,7 +88,7 @@ FloxFlake::maybeOpenCursor( const AttrPath & path ) FloxFlake::openCursor( const AttrPath & path ) { Cursor cur = this->openEvalCache()->getRoot(); - for ( const auto & p : path ) { cur = cur->getAttr( p ); } + for ( const auto & part : path ) { cur = cur->getAttr( part ); } return cur; } diff --git a/src/get.cc b/src/get.cc index 7040e28e..24c4d409 100644 --- a/src/get.cc +++ b/src/get.cc @@ -15,14 +15,12 @@ /* -------------------------------------------------------------------------- */ -namespace flox { - namespace pkgdb { +namespace flox::pkgdb { /* -------------------------------------------------------------------------- */ GetCommand::GetCommand() - : flox::NixState() - , parser( "get" ) + : parser( "get" ) , pId( "id" ) , pPath( "path" ) , pFlake( "flake" ) @@ -50,9 +48,9 @@ GetCommand::GetCommand() this->pPath.add_argument( "id" ) .help( "Row `id' to lookup" ) .nargs( 1 ) - .action( [&]( const std::string & i ) + .action( [&]( const std::string & rowId ) { - this->id = std::stoull( i ); + this->id = std::stoull( rowId ); } ); this->parser.add_subparser( this->pPath ); @@ -108,12 +106,12 @@ GetCommand::runPath() int GetCommand::runFlake() { - nlohmann::json j = { + nlohmann::json flakeInfo = { { "string", this->db->lockedRef.string } - , { "attrs", this->db->lockedRef.attrs } + , { "attrs", this->db->lockedRef.attrs } , { "fingerprint", this->db->fingerprint.to_string( nix::Base16, false ) } }; - std::cout << j.dump() << std::endl; + std::cout << flakeInfo.dump() << std::endl; return EXIT_SUCCESS; } @@ -125,7 +123,7 @@ GetCommand::runDb() { if ( this->dbPath.has_value() ) { - std::cout << ( (std::string) this->dbPath.value() ) << std::endl; + std::cout << ( (std::string) * this->dbPath ) << std::endl; } else { @@ -147,33 +145,27 @@ GetCommand::run() { return this->runId(); } - else if ( this->parser.is_subcommand_used( "path" ) ) + if ( this->parser.is_subcommand_used( "path" ) ) { return this->runPath(); } - else if ( this->parser.is_subcommand_used( "flake" ) ) + if ( this->parser.is_subcommand_used( "flake" ) ) { return this->runFlake(); } - else if ( this->parser.is_subcommand_used( "db" ) ) + if ( this->parser.is_subcommand_used( "db" ) ) { return this->runDb(); } - else - { - std::cerr << this->parser << std::endl; - throw flox::FloxException( - "You must provide a valid 'get' subcommand" - ); - return EXIT_FAILURE; - } + std::cerr << this->parser << std::endl; + throw flox::FloxException( "You must provide a valid 'get' subcommand" ); + return EXIT_FAILURE; } /* -------------------------------------------------------------------------- */ - } /* End namespaces `flox::command' */ -} /* End namespaces `flox' */ +} /* End namespaces `flox::command' */ /* -------------------------------------------------------------------------- * diff --git a/src/main.cc b/src/main.cc index d95d5b09..ce4c1a41 100644 --- a/src/main.cc +++ b/src/main.cc @@ -7,11 +7,11 @@ * * -------------------------------------------------------------------------- */ +#include #include -#include -#include "flox/pkgdb.hh" #include "flox/core/command.hh" +#include "flox/pkgdb.hh" #include "flox/pkgdb/command.hh" diff --git a/src/pkgdb/command.cc b/src/pkgdb/command.cc index 7124a7f0..839d9ddc 100644 --- a/src/pkgdb/command.cc +++ b/src/pkgdb/command.cc @@ -37,7 +37,7 @@ DbPathMixin::addDatabasePathOption( argparse::ArgumentParser & parser ) { this->dbPath = nix::absPath( dbPath ); std::filesystem::create_directories( - this->dbPath.value().parent_path() + this->dbPath->parent_path() ); } ); @@ -54,7 +54,7 @@ PkgDbMixin::openPkgDb() { this->db = std::make_unique( this->flake->lockedFlake - , (std::string) this->dbPath.value() + , (std::string) * this->dbPath ); } else if ( this->flake != nullptr ) @@ -62,17 +62,17 @@ PkgDbMixin::openPkgDb() this->dbPath = flox::pkgdb::genPkgDbName( this->flake->lockedFlake.getFingerprint() ); - std::filesystem::create_directories( this->dbPath.value().parent_path() ); + std::filesystem::create_directories( this->dbPath->parent_path() ); this->db = std::make_unique( this->flake->lockedFlake - , (std::string) this->dbPath.value() + , (std::string) * this->dbPath ); } else if ( this->dbPath.has_value() ) { - std::filesystem::create_directories( this->dbPath.value().parent_path() ); + std::filesystem::create_directories( this->dbPath->parent_path() ); this->db = std::make_unique( - (std::string) this->dbPath.value() + (std::string) * this->dbPath ); } else diff --git a/src/pkgdb/query-builder.cc b/src/pkgdb/query-builder.cc index ddfe68d9..d6db65ff 100644 --- a/src/pkgdb/query-builder.cc +++ b/src/pkgdb/query-builder.cc @@ -9,6 +9,7 @@ #include +#include "flox/pkgdb.hh" #include "flox/pkgdb/query-builder.hh" @@ -21,21 +22,157 @@ namespace flox { /* -------------------------------------------------------------------------- */ std::string -buildPkgQuery( PkgQueryArgs && params ) +PkgQueryArgs::PkgQueryInvalidArgException::errorMessage( + const PkgQueryArgs::PkgQueryInvalidArgException::error_code & ec +) { + switch ( ec ) + { + case PDQEC_MIX_NAME: + return "Queries may not mix `name' parameter with any of `pname', " + "`version', or `semver' parameters"; + break; + case PDQEC_MIX_VERSION_SEMVER: + return "Queries may not mix `version' and `semver' parameters"; + break; + case PDQEC_INVALID_SEMVER: + return "Semver Parse Error"; + break; + case PDQEC_INVALID_LICENSE: + return "Query `license' parameter contains invalid character \"'\""; + break; + case PDQEC_INVALID_SUBTREE: + return "Unrecognized subtree in query arguments"; + break; + case PDQEC_CONFLICTING_SUBTREE: + return "Query `stability' parameter may only be used in " + "\"catalog\" `subtree'"; + break; + case PDQEC_INVALID_SYSTEM: + return "Unrecognized or unsupported `system' in query arguments"; + break; + case PDQEC_INVALID_STABILITY: + return "Unrecognized `stability' in query arguments"; + break; + default: + case PDQEC_ERROR: + return "Encountered and error processing query arguments"; + break; + } +} + + +/* -------------------------------------------------------------------------- */ + + std::optional +PkgQueryArgs::validate() const +{ + using error_code = PkgQueryArgs::PkgQueryInvalidArgException::error_code; + + // TODO: semver + + if ( this->name.has_value() && + ( this->pname.has_value() || + this->version.has_value() || + this->semver.has_value() + ) + ) + { + return error_code::PDQEC_MIX_NAME; + } + + if ( this->version.has_value() && this->semver.has_value() ) + { + return error_code::PDQEC_MIX_VERSION_SEMVER; + } + + /* Check licenses don't contain the ' character */ + if ( this->licenses.has_value() ) + { + for ( const auto & l : * this->licenses ) + { + if ( l.find( '\'' ) != l.npos ) + { + return error_code::PDQEC_INVALID_LICENSE; + } + } + } + + /* Systems */ + for ( const auto & s : this->systems ) + { + if ( std::find( flox::defaultSystems.begin() + , flox::defaultSystems.end() + , s + ) + == flox::defaultSystems.end() + ) + { + return error_code::PDQEC_INVALID_SYSTEM; + } + } + + /* Stabilities */ + if ( this->stabilities.has_value() ) + { + for ( const auto & s : * this->stabilities ) + { + if ( std::find( flox::defaultCatalogStabilities.begin() + , flox::defaultCatalogStabilities.end() + , s + ) + == flox::defaultCatalogStabilities.end() + ) + { + return error_code::PDQEC_INVALID_STABILITY; + } + } + if ( ( this->subtrees.has_value() ) && + ( ( this->subtrees->size() != 1 ) || + ( this->subtrees->front() != ST_CATALOG ) + ) + ) + { + return error_code::PDQEC_CONFLICTING_SUBTREE; + } + } + + return std::nullopt; +} + + +/* -------------------------------------------------------------------------- */ + + std::pair +buildPkgQuery( const PkgQueryArgs & params ) +{ + /* It's a pain to use `bind' with `in` lists because each element would need + * its own variable, so instead we scan for unsafe characters and quote + * /the good ol' fashioned way/ in those cases. + * Other than that, we use `bind`. */ + using namespace sql; - // TODO: validate params + + /* Validate parameters */ + if ( auto maybe_ec = params.validate(); maybe_ec != std::nullopt ) + { + throw PkgQueryArgs::PkgQueryInvalidArgException( * maybe_ec ); + } + SelectModel q; - q.select( "id" ).from( "Packages" ); + q.select( "id" ).from( "v_PackagesSearch" ); + std::unordered_map binds; if ( params.name.has_value() ) { - q.where( column( "name" ) == ":name" ); + q.where( column( "name" ) == Param( ":name" ) ); + binds.emplace( ":name", * params.name ); } if ( params.pname.has_value() ) { - q.where( column( "pname" ) == ":pname" ); + q.where( column( "pname" ) == Param( ":pname" ) ); + binds.emplace( ":pname", * params.pname ); } if ( params.match.has_value() && !params.match->empty() ) @@ -45,30 +182,78 @@ buildPkgQuery( PkgQueryArgs && params ) if ( params.version.has_value() ) { - q.where( column( "version" ) == ":version" ); + q.where( column( "version" ) == Param( ":version" ) ); + binds.emplace( ":version", * params.version ); } - // TODO: validate license names, this is a PITA to handle with bind. - if ( params.licenses.has_value() ) + if ( params.licenses.has_value() && ( ! params.licenses->empty() ) ) { - q.where( column( "license" ).in( params.licenses.value() ) ); + q.where( "(" + ( + column( "license" ).is_not_null() and + column( "license" ).in( * params.licenses ) + ).str() + ")" ); } if ( ! params.allowBroken ) { - q.where( column( "broken" ).is_null() or ( column( "broken" ) == 0 ) ); + q.where( "(" + + ( column( "broken" ).is_null() or ( column( "broken" ) == 0 ) ).str() + + ")" ); } if ( ! params.allowUnfree ) { - q.where( column( "unfree" ).is_null() or ( column( "unfree" ) == 0 ) ); + q.where( "(" + + ( column( "unfree" ).is_null() or ( column( "unfree" ) == 0 ) ).str() + + ")" ); + } + + /* Subtrees */ + if ( params.stabilities.has_value() ) + { + q.where( column( "subtree" ) == "catalog" ); + } + else if ( params.subtrees.has_value() ) + { + std::vector lst; + for ( const auto s : * params.subtrees ) + { + switch ( s ) + { + case ST_LEGACY: lst.emplace_back( "legacyPackages" ); break; + case ST_PACKAGES: lst.emplace_back( "packages" ); break; + case ST_CATALOG: lst.emplace_back( "catalog" ); break; + default: + throw PkgQueryArgs::PkgQueryInvalidArgException(); + break; + } + } + q.where( column( "subtree" ).in( lst ) ); + } + + /* Systems */ + q.where( column( "system" ).in( params.systems ) ); + + /* Stabilities */ + if ( params.stabilities.has_value() ) + { + if ( params.stabilities->size() == 1 ) + { + q.where( + column( "stability" ) == params.stabilities->front() + ); + } + else + { + q.where( column( "stability" ).in( * params.stabilities ) ); + } } - // TODO: stabilities - // TODO: semver and pre-releases - // TODO: bind + // TODO: Match + // TODO: Semver and pre-releases + // TODO: Sort/"order by" results - return q.str(); + return std::make_pair( q.str(), binds ); } diff --git a/src/pkgdb/read.cc b/src/pkgdb/read.cc index 75c4afda..e38ea495 100644 --- a/src/pkgdb/read.cc +++ b/src/pkgdb/read.cc @@ -21,7 +21,7 @@ namespace flox { /* -------------------------------------------------------------------------- */ - static inline bool + bool isSQLError( int rc ) { switch ( rc ) diff --git a/src/pkgdb/write.cc b/src/pkgdb/write.cc index 511d53eb..ad0a8234 100644 --- a/src/pkgdb/write.cc +++ b/src/pkgdb/write.cc @@ -24,22 +24,6 @@ namespace flox { /* -------------------------------------------------------------------------- */ - static inline bool -isSQLError( int rc ) -{ - switch ( rc ) - { - case SQLITE_OK: return false; break; - case SQLITE_ROW: return false; break; - case SQLITE_DONE: return false; break; - default: return true; break; - } -} - - -/* -------------------------------------------------------------------------- */ - - void PkgDb::writeInput() { @@ -287,7 +271,7 @@ PkgDb::addPackage( row_id parentId if ( pkg._semver.has_value() ) { - cmd.bind( ":semver", pkg._semver.value(), sqlite3pp::nocopy ); + cmd.bind( ":semver", * pkg._semver, sqlite3pp::nocopy ); } else { @@ -308,7 +292,7 @@ PkgDb::addPackage( row_id parentId { if ( auto m = pkg.getLicense(); m.has_value() ) { - cmd.bind( ":license", m.value(), sqlite3pp::copy ); + cmd.bind( ":license", * m, sqlite3pp::copy ); } else { @@ -317,7 +301,7 @@ PkgDb::addPackage( row_id parentId if ( auto m = pkg.isBroken(); m.has_value() ) { - cmd.bind( ":broken", (int) m.value() ); + cmd.bind( ":broken", (int) * m ); } else { @@ -326,7 +310,7 @@ PkgDb::addPackage( row_id parentId if ( auto m = pkg.isUnfree(); m.has_value() ) { - cmd.bind( ":unfree", (int) m.value() ); + cmd.bind( ":unfree", (int) * m ); } else /* TODO: Derive value from `license'? */ { @@ -335,7 +319,7 @@ PkgDb::addPackage( row_id parentId if ( auto m = pkg.getDescription(); m.has_value() ) { - row_id descriptionId = this->addOrGetDescriptionId( m.value() ); + row_id descriptionId = this->addOrGetDescriptionId( * m ); cmd.bind( ":descriptionId", (long long) descriptionId ); } else diff --git a/src/raw-package.cc b/src/raw-package.cc index 300a7123..be644a10 100644 --- a/src/raw-package.cc +++ b/src/raw-package.cc @@ -85,15 +85,11 @@ RawPackage::RawPackage( nlohmann::json && drvInfo ) : std::vector{} ) , _broken( drvInfo.contains( "broken" ) - ? std::make_optional( - std::move( drvInfo.at( "broken" ).get() ) - ) + ? std::make_optional( drvInfo.at( "broken" ).get() ) : std::nullopt ) , _unfree( drvInfo.contains( "unfree" ) - ? std::make_optional( - std::move( drvInfo.at( "unfree" ).get() ) - ) + ? std::make_optional( drvInfo.at( "unfree" ).get() ) : std::nullopt ) , _description( drvInfo.contains( "description" ) diff --git a/src/scrape.cc b/src/scrape.cc index bd6f9a9c..83dd91be 100644 --- a/src/scrape.cc +++ b/src/scrape.cc @@ -21,7 +21,7 @@ namespace flox { /* Scrape Subcommand */ -ScrapeCommand::ScrapeCommand() : flox::NixState(), parser( "scrape" ) +ScrapeCommand::ScrapeCommand() : parser( "scrape" ) { this->parser.add_description( "Scrape a flake and emit a SQLite3 DB" ); this->parser.add_argument( "-f", "--force" ) @@ -86,17 +86,17 @@ ScrapeCommand::run() todo.pop(); } } - catch( const nix::EvalError & e ) + catch( const nix::EvalError & ) { txn.rollback(); - throw e; + throw; } txn.commit(); } /* Print path to database. */ - std::cout << ( (std::string) this->dbPath.value() ) << std::endl; + std::cout << ( (std::string) * this->dbPath ) << std::endl; return EXIT_SUCCESS; /* GG, GG */ } diff --git a/src/semver.cc b/src/semver.cc index de773504..202e2901 100644 --- a/src/semver.cc +++ b/src/semver.cc @@ -19,9 +19,8 @@ namespace versions { /* -------------------------------------------------------------------------- */ /* Matches Semantic Version strings, e.g. `4.2.0-pre' */ -#define _re_vp "(0|[1-9][0-9]*)" static const std::regex semverRE( - _re_vp "\\." _re_vp "\\." _re_vp "(-[-[:alnum:]_+.]+)?" + "(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(-[-[:alnum:]_+.]+)?" , std::regex::ECMAScript ); @@ -102,16 +101,16 @@ coerceSemver( std::string_view version ) } /* Try try matching the coercive pattern. */ - std::smatch sm; - if ( isDate( v ) || ( ! std::regex_match( v, sm, semverCoerceRE ) ) ) + std::smatch match; + if ( isDate( v ) || ( ! std::regex_match( v, match, semverCoerceRE ) ) ) { return std::nullopt; } #if defined( DEBUG ) && ( DEBUG != 0 ) - for ( unsigned int i = 0; i < sm.size(); ++i ) + for ( unsigned int i = 0; i < match.size(); ++i ) { - std::cerr << "[" << i << "]: " << sm[i] << std::endl; + std::cerr << "[" << i << "]: " << match[i] << std::endl; } #endif @@ -134,11 +133,11 @@ coerceSemver( std::string_view version ) * some characters with null terminators. * To avoid this we convert each submatch to a string from right to left. */ - std::string tag( sm[8].str() ); - std::string patch( sm[7].str() ); - std::string minor( sm[5].str() ); + std::string tag( match[8].str() ); + std::string patch( match[7].str() ); + std::string minor( match[5].str() ); - std::string rsl( sm[3].str() + "." ); + std::string rsl( match[3].str() + "." ); if ( minor.empty() ) { rsl += "0."; } else { rsl += minor + "."; } @@ -191,13 +190,13 @@ semverSat( const std::string & range, const std::list & versions ) std::list args = { "--include-prerelease", "--loose", "--range", range }; - for ( auto & v : versions ) { args.push_back( v ); } + for ( const auto & version : versions ) { args.push_back( version ); } auto [ec, lines] = runSemver( args ); if ( ! nix::statusOk( ec ) ) { return {}; } std::list rsl; - std::stringstream ss( lines ); + std::stringstream oss( lines ); std::string l; - while ( std::getline( ss, l, '\n' ) ) + while ( std::getline( oss, l, '\n' ) ) { if ( ! l.empty() ) { rsl.push_back( std::move( l ) ); } } diff --git a/tests/.gitignore b/tests/.gitignore index e3a41f6b..84abb3df 100644 --- a/tests/.gitignore +++ b/tests/.gitignore @@ -1,3 +1,4 @@ is_sqlite3 pkgdb +read semver diff --git a/tests/pkgdb.cc b/tests/pkgdb.cc index 7e0e0691..ab09c211 100644 --- a/tests/pkgdb.cc +++ b/tests/pkgdb.cc @@ -10,12 +10,17 @@ * test state by doing things like clearing tables in test cases where * it may be relevant to an action we're about to test. * + * In general tests should clear the database's tables at the top of + * their function. + * This allows `throw` and early terminations to exit at arbitrary points + * without polluting later test cases. + * * * -------------------------------------------------------------------------- */ #include #include -#include +#include #include #include #include @@ -31,6 +36,7 @@ #include "flox/core/util.hh" #include "flox/core/types.hh" #include "flox/pkgdb.hh" +#include "flox/pkgdb/query-builder.hh" #include "test.hh" @@ -51,6 +57,18 @@ getRowCount( flox::pkgdb::PkgDb & db, const std::string table ) } +/* -------------------------------------------------------------------------- */ + + static inline void +clearTables( flox::pkgdb::PkgDb & db ) +{ + /* Clear DB */ + db.execute_all( + "DELETE FROM Packages; DELETE FROM AttrSets; DELETE FROM Descriptions" + ); +} + + /* -------------------------------------------------------------------------- */ /** @@ -61,6 +79,8 @@ getRowCount( flox::pkgdb::PkgDb & db, const std::string table ) bool test_addOrGetAttrSetId0( flox::pkgdb::PkgDb & db ) { + clearTables( db ); + /* Make sure `AttrSets` is empty. */ row_id startId = getRowCount( db, "AttrSets" ); EXPECT_EQ( startId, (row_id) 0 ); @@ -82,11 +102,12 @@ test_addOrGetAttrSetId0( flox::pkgdb::PkgDb & db ) bool test_addOrGetAttrSetId1( flox::pkgdb::PkgDb & db ) { - row_id startId = getRowCount( db, "AttrSets" ); + clearTables( db ); + try { /* Ensure we throw an error for undefined `AttrSet.id' parents. */ - db.addOrGetAttrSetId( "phony", startId + 99999999 ); + db.addOrGetAttrSetId( "phony", 1 ); return false; } catch( const flox::pkgdb::PkgDbException & e ) { /* Expected */ } @@ -119,6 +140,8 @@ test_getDbVersion0( flox::pkgdb::PkgDb & db ) bool test_hasAttrSet0( flox::pkgdb::PkgDb & db ) { + clearTables( db ); + /* Make sure the attr-set exists, and clear it. */ row_id id = db.addOrGetAttrSetId( "x86_64-linux" , db.addOrGetAttrSetId( "legacyPackages" ) @@ -146,6 +169,8 @@ test_hasAttrSet0( flox::pkgdb::PkgDb & db ) bool test_hasAttrSet1( flox::pkgdb::PkgDb & db ) { + clearTables( db ); + /* Make sure the attr-set exists. */ row_id id = db.addOrGetAttrSetId( "x86_64-linux" , db.addOrGetAttrSetId( "legacyPackages" ) @@ -178,6 +203,8 @@ test_hasAttrSet1( flox::pkgdb::PkgDb & db ) bool test_getAttrSetId0( flox::pkgdb::PkgDb & db ) { + clearTables( db ); + /* Make sure the attr-set exists. */ row_id id = db.addOrGetAttrSetId( "x86_64-linux" , db.addOrGetAttrSetId( "legacyPackages" ) @@ -199,6 +226,8 @@ test_getAttrSetId0( flox::pkgdb::PkgDb & db ) bool test_getAttrSetPath0( flox::pkgdb::PkgDb & db ) { + clearTables( db ); + /* Make sure the attr-set exists. */ row_id id = db.addOrGetAttrSetId( "x86_64-linux" , db.addOrGetAttrSetId( "legacyPackages" ) @@ -214,6 +243,8 @@ test_getAttrSetPath0( flox::pkgdb::PkgDb & db ) bool test_hasPackage0( flox::pkgdb::PkgDb & db ) { + clearTables( db ); + /* Make sure the attr-set exists. */ row_id id = db.addOrGetAttrSetId( "x86_64-linux" , db.addOrGetAttrSetId( "legacyPackages" ) @@ -260,8 +291,8 @@ test_descriptions0( flox::pkgdb::PkgDb & db ) bool test_descendants0( flox::pkgdb::PkgDb & db ) { - /* Clear `AttrSets' and Make a tree. */ - db.execute_all( "DELETE FROM Packages; DELETE FROM AttrSets" ); + clearTables( db ); + row_id linux = db.addOrGetAttrSetId( flox::AttrPath { "legacyPackages", "x86_64-linux" } ); row_id python = db.addOrGetAttrSetId( "python3Packages", linux ); @@ -278,8 +309,6 @@ test_descendants0( flox::pkgdb::PkgDb & db ) db.addOrGetAttrSetId( flox::AttrPath { "legacyPackages", "x86_64-darwin" } ); std::vector descendants = db.getDescendantAttrSets( linux ); - /* Clear the DB */ - db.execute( "DELETE FROM AttrSets" ); EXPECT( descendants == ( std::vector { python, node, foo, quux, bar, baz, karl } ) @@ -289,6 +318,277 @@ test_descendants0( flox::pkgdb::PkgDb & db ) } +/* -------------------------------------------------------------------------- */ + +/* Tests `systems', `name', `pname', `version', and `subtree' filtering. */ + bool +test_buildPkgQuery0( flox::pkgdb::PkgDb & db ) +{ + clearTables( db ); + + /* Make a package */ + row_id linux = + db.addOrGetAttrSetId( flox::AttrPath { "legacyPackages", "x86_64-linux" } ); + row_id desc = + db.addOrGetDescriptionId( "A program with a friendly greeting" ); + sqlite3pp::command cmd( db.db, R"SQL( + INSERT INTO Packages ( + parentId, attrName, name, pname, version, semver, outputs, descriptionId + ) VALUES ( :parentId, 'hello', 'hello-2.12.1', 'hello', '2.12.1', '2.12.1' + , '["out"]', :descriptionId + ) + )SQL" ); + cmd.bind( ":parentId", (long long) linux ); + cmd.bind( ":descriptionId", (long long) desc ); + if ( flox::pkgdb::sql_rc rc = cmd.execute(); flox::pkgdb::isSQLError( rc ) ) + { + throw flox::pkgdb::PkgDbException( + db.dbPath + , nix::fmt( "Failed to write Package 'hello':(%d) %s" + , rc + , db.db.error_msg() + ) + ); + } + flox::pkgdb::PkgQueryArgs qargs = { + .match = std::nullopt + , .name = std::nullopt + , .pname = std::nullopt + , .version = std::nullopt + , .semver = std::nullopt + , .licenses = std::nullopt + , .allowBroken = false + , .allowUnfree = true + , .preferPreReleases = false + , .subtrees = std::nullopt + , .systems = std::vector { "x86_64-linux" } + , .stabilities = std::nullopt + }; + + /* Run empty query */ + { + auto [query, binds] = flox::pkgdb::buildPkgQuery( qargs ); + sqlite3pp::query qry( db.db, query.c_str() ); + for ( const auto & [var, val] : binds ) + { + qry.bind( var.c_str(), val, sqlite3pp::copy ); + } + auto i = qry.begin(); + EXPECT( ( i != qry.end() ) && ( 0 < ( * i ).get( 0 ) ) ); + } + + /* Run `pname' query */ + { + qargs.pname = "hello"; + auto [query, binds] = flox::pkgdb::buildPkgQuery( qargs ); + qargs.pname = std::nullopt; + sqlite3pp::query qry( db.db, query.c_str() ); + for ( const auto & [var, val] : binds ) + { + qry.bind( var.c_str(), val, sqlite3pp::copy ); + } + auto i = qry.begin(); + EXPECT( ( i != qry.end() ) && ( 0 < ( * i ).get( 0 ) ) ); + } + + /* Run `version' query */ + { + qargs.version = "2.12.1"; + auto [query, binds] = flox::pkgdb::buildPkgQuery( qargs ); + qargs.version = std::nullopt; + sqlite3pp::query qry( db.db, query.c_str() ); + for ( const auto & [var, val] : binds ) + { + qry.bind( var.c_str(), val, sqlite3pp::copy ); + } + auto i = qry.begin(); + EXPECT( ( i != qry.end() ) && ( 0 < ( * i ).get( 0 ) ) ); + } + + /* Run `name' query */ + { + qargs.name = "hello-2.12.1"; + auto [query, binds] = flox::pkgdb::buildPkgQuery( qargs ); + qargs.name = std::nullopt; + sqlite3pp::query qry( db.db, query.c_str() ); + for ( const auto & [var, val] : binds ) + { + qry.bind( var.c_str(), val, sqlite3pp::copy ); + } + auto i = qry.begin(); + EXPECT( ( i != qry.end() ) && ( 0 < ( * i ).get( 0 ) ) ); + } + + /* Run `subtrees' query */ + { + qargs.subtrees = std::vector { flox::ST_LEGACY }; + auto [query, binds] = flox::pkgdb::buildPkgQuery( qargs ); + qargs.subtrees = std::nullopt; + sqlite3pp::query qry( db.db, query.c_str() ); + for ( const auto & [var, val] : binds ) + { + qry.bind( var.c_str(), val, sqlite3pp::copy ); + } + auto i = qry.begin(); + EXPECT( ( i != qry.end() ) && ( 0 < ( * i ).get( 0 ) ) ); + } + + return true; +} + + +/* -------------------------------------------------------------------------- */ + +/* Tests `license', `allowBroken', and `allowUnfree' filtering. */ + bool +test_buildPkgQuery1( flox::pkgdb::PkgDb & db ) +{ + clearTables( db ); + + /* Make a package */ + row_id linux = + db.addOrGetAttrSetId( flox::AttrPath { "legacyPackages", "x86_64-linux" } ); + row_id desc = + db.addOrGetDescriptionId( "A program with a friendly greeting/farewell" ); + sqlite3pp::command cmd( db.db, R"SQL( + INSERT INTO Packages ( + parentId, attrName, name, pname, version, semver, outputs, license + , broken, unfree, descriptionId + ) VALUES + ( :parentId, 'hello', 'hello-2.12.1', 'hello', '2.12.1', '2.12.1' + , '["out"]', "GPL-3.0-or-later", FALSE, FALSE, :descriptionId + ) + , ( :parentId, 'goodbye', 'goodbye-2.12.1', 'goodbye', '2.12.1', '2.12.1' + , '["out"]', NULL, FALSE, TRUE, :descriptionId + ) + , ( :parentId, 'hola', 'hola-2.12.1', 'hola', '2.12.1', '2.12.1' + , '["out"]', "BUSL-1.1", FALSE, FALSE, :descriptionId + ) + , ( :parentId, 'ciao', 'ciao-2.12.1', 'ciao', '2.12.1', '2.12.1' + , '["out"]', NULL, TRUE, FALSE, :descriptionId + ) + )SQL" ); + cmd.bind( ":parentId", (long long) linux ); + cmd.bind( ":descriptionId", (long long) desc ); + if ( flox::pkgdb::sql_rc rc = cmd.execute_all(); + flox::pkgdb::isSQLError( rc ) + ) + { + throw flox::pkgdb::PkgDbException( + db.dbPath + , nix::fmt( "Failed to write Package 'hello':(%d) %s" + , rc + , db.db.error_msg() + ) + ); + } + flox::pkgdb::PkgQueryArgs qargs = { + .match = std::nullopt + , .name = std::nullopt + , .pname = std::nullopt + , .version = std::nullopt + , .semver = std::nullopt + , .licenses = std::nullopt + , .allowBroken = false + , .allowUnfree = true + , .preferPreReleases = false + , .subtrees = std::nullopt + , .systems = std::vector { "x86_64-linux" } + , .stabilities = std::nullopt + }; + + /* Run `allowBroken = false' query */ + { + auto [query, binds] = flox::pkgdb::buildPkgQuery( qargs ); + sqlite3pp::query qry( db.db, query.c_str() ); + for ( const auto & [var, val] : binds ) + { + qry.bind( var.c_str(), val, sqlite3pp::copy ); + } + size_t count = 0; + for ( const auto r : qry ) { (void) r; ++count; } + EXPECT_EQ( count, (size_t) 3 ); + } + + /* Run `allowBroken = true' query */ + { + qargs.allowBroken = true; + auto [query, binds] = flox::pkgdb::buildPkgQuery( qargs ); + qargs.allowBroken = false; + sqlite3pp::query qry( db.db, query.c_str() ); + for ( const auto & [var, val] : binds ) + { + qry.bind( var.c_str(), val, sqlite3pp::copy ); + } + size_t count = 0; + for ( const auto r : qry ) { (void) r; ++count; } + EXPECT_EQ( count, (size_t) 4 ); + } + + /* Run `allowUnfree = true' query */ + { + auto [query, binds] = flox::pkgdb::buildPkgQuery( qargs ); + sqlite3pp::query qry( db.db, query.c_str() ); + for ( const auto & [var, val] : binds ) + { + qry.bind( var.c_str(), val, sqlite3pp::copy ); + } + size_t count = 0; + for ( const auto r : qry ) { (void) r; ++count; } + EXPECT_EQ( count, (size_t) 3 ); /* still omits broken */ + } + + /* Run `allowUnfree = false' query */ + { + qargs.allowUnfree = false; + auto [query, binds] = flox::pkgdb::buildPkgQuery( qargs ); + qargs.allowUnfree = true; + sqlite3pp::query qry( db.db, query.c_str() ); + for ( const auto & [var, val] : binds ) + { + qry.bind( var.c_str(), val, sqlite3pp::copy ); + } + size_t count = 0; + for ( const auto r : qry ) { (void) r; ++count; } + EXPECT_EQ( count, (size_t) 2 ); /* still omits broken as well */ + } + + /* Run `licenses = ["GPL-3.0-or-later", "BUSL-1.1", "MIT"]' query */ + { + qargs.licenses = std::vector { + "GPL-3.0-or-later", "BUSL-1.1", "MIT" + }; + auto [query, binds] = flox::pkgdb::buildPkgQuery( qargs ); + qargs.licenses = std::nullopt; + sqlite3pp::query qry( db.db, query.c_str() ); + for ( const auto & [var, val] : binds ) + { + qry.bind( var.c_str(), val, sqlite3pp::copy ); + } + size_t count = 0; + for ( const auto r : qry ) { (void) r; ++count; } + EXPECT_EQ( count, (size_t) 2 ); /* omits NULL licenses */ + } + + /* Run `licenses = ["BUSL-1.1", "MIT"]' query */ + { + qargs.licenses = std::vector { "BUSL-1.1", "MIT" }; + auto [query, binds] = flox::pkgdb::buildPkgQuery( qargs ); + qargs.licenses = std::nullopt; + sqlite3pp::query qry( db.db, query.c_str() ); + for ( const auto & [var, val] : binds ) + { + qry.bind( var.c_str(), val, sqlite3pp::copy ); + } + size_t count = 0; + for ( const auto r : qry ) { (void) r; ++count; } + EXPECT_EQ( count, (size_t) 1 ); /* omits NULL licenses */ + } + + return true; +} + + /* ========================================================================== */ int @@ -354,6 +654,9 @@ main( int argc, char * argv[] ) RUN_TEST( descendants0, db ); + RUN_TEST( buildPkgQuery0, db ); + RUN_TEST( buildPkgQuery1, db ); + } diff --git a/tests/read.cc b/tests/read.cc index 1ac17f20..de33f816 100644 --- a/tests/read.cc +++ b/tests/read.cc @@ -29,29 +29,30 @@ using namespace flox; test_distanceFromMatch() { std::tuple cases[] = { - { "match", "match", 0 }, - { "match", "partial match", 0 }, - { "match", "miss", 0 }, - { "partial match", "match", 1 }, - { "partial match", "partial match", 1 }, - { "partial match", "miss", 2 }, - { "miss", "match", 3 }, - { "miss", "partial match", 3 }, - { "miss", "miss", 4 }, + { "match", "match", 0 } + , { "match", "partial match", 0 } + , { "match", "miss", 0 } + , { "partial match", "match", 1 } + , { "partial match", "partial match", 1 } + , { "partial match", "miss", 2 } + , { "miss", "match", 3 } + , { "miss", "partial match", 3 } + , { "miss", "miss", 4 } }; RawPackage pkg; - for (auto [pname, description, distance] : cases) { - pkg = RawPackage(nlohmann::json { - { "name", "name" }, - { "pname", pname }, - { "description", description }, - }); - EXPECT_EQ(*pkgdb::distanceFromMatch(pkg, "match"), distance); - } + for ( auto [pname, description, distance] : cases ) + { + pkg = RawPackage( nlohmann::json { + { "name", "name" } + , { "pname", pname } + , { "description", description } + } ); + EXPECT_EQ( * pkgdb::distanceFromMatch( pkg, "match"), distance ); + } - // Should return std::nullopt for empty match string. - EXPECT(pkgdb::distanceFromMatch(pkg, "") == std::nullopt); + /* Should return std::nullopt for empty match string. */ + EXPECT( pkgdb::distanceFromMatch( pkg, "" ) == std::nullopt ); return true; } @@ -65,14 +66,13 @@ main( int argc, char * argv[] ) /* -------------------------------------------------------------------------- */ - nix::Verbosity verbosity; if ( ( 1 < argc ) && ( std::string_view( argv[1] ) == "-v" ) ) { - verbosity = nix::lvlDebug; + nix::verbosity = nix::lvlDebug; } else { - verbosity = nix::lvlWarn; + nix::verbosity = nix::lvlWarn; } /* -------------------------------------------------------------------------- */