-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Bring Pinning to PowerShell Cmdlets #6190
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
32b17d4
697ca1d
b6502ef
ae483b0
fac4ec4
7ce3b27
35322e7
236e667
1c89957
fa617a6
7f33f56
3c7d4a6
99a8760
e336194
0cb653b
29d6203
184cc7a
952e56e
e823b75
195846f
0468b34
4bfde99
243798b
4b7b8ef
e93e3d1
ae87ff1
917db77
34a727c
01aad8e
ad9932f
a96e43e
fde7382
d5f682c
8f819c1
91f532a
595abca
7e83a83
615ddca
1f8932d
df55096
6d085dd
ea9f0d8
d207c77
349a0a1
840e5ae
65f8373
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -133,6 +133,105 @@ void WorkflowTask(Execution::Context& context) | |||||
| - Parsing in `AppInstallerCommonCore/Manifest/` | ||||||
| - Multi-file manifests: installer, locale, version, defaultLocale | ||||||
|
|
||||||
| ## SQLite Statement Builder | ||||||
|
|
||||||
| **Always use `AppInstaller::SQLite::Builder::StatementBuilder` when writing SQLite database code. Never write raw SQL strings.** | ||||||
|
|
||||||
| The builder is in `<winget/SQLiteStatementBuilder.h>` (namespace `AppInstaller::SQLite::Builder`). It generates type-safe, parameterized SQL and ensures symbolic names are used for tables and columns throughout. | ||||||
|
|
||||||
| ### Key conventions | ||||||
|
|
||||||
| - Define table and column names as `constexpr std::string_view` constants, then pass them to the builder. | ||||||
| - Use `ColumnBuilder` with chained modifiers (`.NotNull()`, `.Unique()`, `.Default(value)`) when creating tables. | ||||||
| - Use `IntegerPrimaryKey()` for auto-increment rowid primary keys. | ||||||
| - Use `Unbound` as a placeholder and bind values later via `Statement::Bind()`, or pass values directly to have them bound automatically. | ||||||
|
|
||||||
| ### Common operations | ||||||
|
|
||||||
| ```cpp | ||||||
| using namespace AppInstaller::SQLite::Builder; | ||||||
|
|
||||||
| // Symbolic names | ||||||
| static constexpr std::string_view s_MyTable = "my_table"sv; | ||||||
| static constexpr std::string_view s_Col_Id = "id"sv; | ||||||
| static constexpr std::string_view s_Col_Name = "name"sv; | ||||||
| static constexpr std::string_view s_Col_Value = "value"sv; | ||||||
|
|
||||||
| // CREATE TABLE | ||||||
| StatementBuilder builder; | ||||||
| builder.CreateTable(s_MyTable).Columns({ | ||||||
| IntegerPrimaryKey(), | ||||||
| ColumnBuilder(s_Col_Name, Type::Text).NotNull().Unique(), | ||||||
| ColumnBuilder(s_Col_Value, Type::Int64).NotNull().Default(0), | ||||||
| }); | ||||||
| builder.Execute(connection); | ||||||
|
|
||||||
| // SELECT | ||||||
| StatementBuilder builder; | ||||||
| builder.Select({ s_Col_Id, s_Col_Name }) | ||||||
| .From(s_MyTable) | ||||||
| .Where(s_Col_Name).Equals(nameValue); | ||||||
| auto stmt = builder.Prepare(connection); | ||||||
| while (stmt.Step()) { /* stmt.GetColumn<T>(index) */ } | ||||||
|
|
||||||
| // INSERT | ||||||
| StatementBuilder builder; | ||||||
| builder.InsertInto(s_MyTable) | ||||||
| .Columns({ s_Col_Name, s_Col_Value }) | ||||||
| .Values(nameValue, intValue); | ||||||
| builder.Execute(connection); | ||||||
|
|
||||||
| // UPDATE | ||||||
| StatementBuilder builder; | ||||||
| builder.Update(s_MyTable).Set() | ||||||
| .Column(s_Col_Value).AssignValue(newValue) | ||||||
| .Where(s_Col_Id).Equals(rowId); | ||||||
| builder.Execute(connection); | ||||||
|
|
||||||
| // DELETE | ||||||
| StatementBuilder builder; | ||||||
| builder.DeleteFrom(s_MyTable) | ||||||
| .Where(s_Col_Id).Equals(rowId); | ||||||
| builder.Execute(connection); | ||||||
|
|
||||||
| // ALTER TABLE – add a column | ||||||
| StatementBuilder builder; | ||||||
| builder.AlterTable(s_MyTable).Add(s_Col_NewCol, Type::Text).NotNull().Default(0); | ||||||
| builder.Execute(connection); | ||||||
| ``` | ||||||
|
|
||||||
| ### Nullable values: `Equals()` vs `AssignValue()` | ||||||
|
|
||||||
| Both accept `std::optional<T>`, but they behave differently when the optional is empty and must be used in the right context: | ||||||
|
|
||||||
| | Method | Empty optional emits | Use in | | ||||||
| |---|---|---| | ||||||
| | `Equals(optional<T>)` | `IS NULL` | **WHERE** / filter clauses | | ||||||
| | `AssignValue(optional<T>)` | `= ?` (binds NULL) | **UPDATE SET** assignments | | ||||||
|
|
||||||
| Using `Equals(optional)` in an UPDATE SET clause is a bug — SQLite does not accept `col = IS NULL`. | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
|
||||||
| ```cpp | ||||||
| // ✅ Correct: Equals for WHERE filter, AssignValue for SET assignment | ||||||
| std::optional<int64_t> maybeEpoch = ...; | ||||||
| std::optional<std::string> maybeNote = ...; | ||||||
|
|
||||||
| builder.Update(s_MyTable).Set() | ||||||
| .Column(s_Col_Name).AssignValue(requiredName) // non-optional: AssignValue in SET | ||||||
| .Column(s_Col_Epoch).AssignValue(maybeEpoch) // nullable: must use AssignValue in SET | ||||||
| .Column(s_Col_Note).AssignValue(maybeNote) // nullable: must use AssignValue in SET | ||||||
| .Where(s_Col_Id).Equals(rowId); // filter: Equals is correct here | ||||||
|
|
||||||
| // ✅ Correct: Equals(optional) in WHERE — emits "IS NULL" when empty | ||||||
| builder.Select(s_Col_Id).From(s_MyTable) | ||||||
| .Where(s_Col_Note).Equals(maybeNote); // → "WHERE note IS NULL" when empty | ||||||
| ``` | ||||||
|
|
||||||
| ### Execution | ||||||
|
|
||||||
| - `builder.Execute(connection)` — prepares, binds, and runs a statement that returns no rows. | ||||||
| - `builder.Prepare(connection)` — returns a `SQLite::Statement`; call `.Step()` to iterate, `.GetColumn<T>(index)` to read values. | ||||||
|
|
||||||
| ## Naming Conventions | ||||||
|
|
||||||
| - **Namespace structure**: `AppInstaller::<Area>[::<Subarea>]` | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,6 +23,7 @@ namespace AppInstaller::CLI | |
| std::make_unique<PinRemoveCommand>(FullName()), | ||
| std::make_unique<PinListCommand>(FullName()), | ||
| std::make_unique<PinResetCommand>(FullName()), | ||
| std::make_unique<PinShowCommand>(FullName()), | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would prefer that we follow the model of package list and have Followup: Well it looks like it would need a whole new search mechanism to actually implement the arguments that it claims to support...
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, we would need a whole new search mechanism anyways. I do think that there is some overlap with I will think on this and probably end up refactoring to extend the behavior of list instead of adding show |
||
| }); | ||
| } | ||
|
|
||
|
|
@@ -65,6 +66,7 @@ namespace AppInstaller::CLI | |
| Argument::ForType(Args::Type::Force), | ||
| Argument{ Args::Type::BlockingPin, Resource::String::PinAddBlockingArgumentDescription, ArgumentType::Flag }, | ||
| Argument{ Args::Type::PinInstalled, Resource::String::PinInstalledArgumentDescription, ArgumentType::Flag }, | ||
| Argument{ Args::Type::PinNote, Resource::String::PinNoteArgumentDescription, ArgumentType::Standard }, | ||
| }; | ||
| } | ||
|
|
||
|
|
@@ -343,4 +345,36 @@ namespace AppInstaller::CLI | |
| Workflow::ReportPins; | ||
| } | ||
| } | ||
|
|
||
| std::vector<Argument> PinShowCommand::GetArguments() const | ||
| { | ||
| return { | ||
| Argument::ForType(Args::Type::Query), | ||
| Argument::ForType(Args::Type::Id), | ||
| Argument::ForType(Args::Type::Name), | ||
| Argument::ForType(Args::Type::Exact), | ||
| }; | ||
| } | ||
|
|
||
| Resource::LocString PinShowCommand::ShortDescription() const | ||
| { | ||
| return { Resource::String::PinShowCommandShortDescription }; | ||
| } | ||
|
|
||
| Resource::LocString PinShowCommand::LongDescription() const | ||
| { | ||
| return { Resource::String::PinShowCommandLongDescription }; | ||
| } | ||
|
|
||
| Utility::LocIndView PinShowCommand::HelpLink() const | ||
| { | ||
| return s_PinCommand_HelpLink; | ||
| } | ||
|
|
||
| void PinShowCommand::ExecuteInternal(Execution::Context& context) const | ||
| { | ||
| context << | ||
| Workflow::OpenPinningIndex(/* readOnly */ true) << | ||
| Workflow::ShowPinDetails; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -3,7 +3,9 @@ | |||||||||||||||
| #include "pch.h" | ||||||||||||||||
| #include "Resources.h" | ||||||||||||||||
| #include "PinFlow.h" | ||||||||||||||||
| #include "ShowFlow.h" | ||||||||||||||||
| #include "TableOutput.h" | ||||||||||||||||
| #include <AppInstallerDateTime.h> | ||||||||||||||||
| #include <winget/PinningData.h> | ||||||||||||||||
| #include <winget/RepositorySearch.h> | ||||||||||||||||
| #include <winget/PackageVersionSelection.h> | ||||||||||||||||
|
|
@@ -197,9 +199,18 @@ namespace AppInstaller::CLI::Workflow | |||||||||||||||
|
|
||||||||||||||||
| if (!pinsToAddOrUpdate.empty()) | ||||||||||||||||
| { | ||||||||||||||||
| for (const auto& pin : pinsToAddOrUpdate) | ||||||||||||||||
| auto pinTime = std::chrono::system_clock::now(); | ||||||||||||||||
|
|
||||||||||||||||
| std::optional<std::string> note; | ||||||||||||||||
| if (context.Args.Contains(Execution::Args::Type::PinNote)) | ||||||||||||||||
| { | ||||||||||||||||
| note = std::string{ context.Args.GetArg(Execution::Args::Type::PinNote) }; | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| for (auto& pin : pinsToAddOrUpdate) | ||||||||||||||||
| { | ||||||||||||||||
| pin.SetDateAdded(pinTime); | ||||||||||||||||
| pin.SetNote(note); | ||||||||||||||||
| pinningData.AddOrUpdatePin(pin); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
|
|
@@ -335,4 +346,106 @@ namespace AppInstaller::CLI::Workflow | |||||||||||||||
| context.Reporter.Info() << Resource::String::PinNoPinsExist << std::endl; | ||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| void ShowPinDetails(Execution::Context& context) | ||||||||||||||||
| { | ||||||||||||||||
| auto& pinningData = context.Get<Execution::Data::PinningData>(); | ||||||||||||||||
| auto allPins = pinningData.GetAllPins(); | ||||||||||||||||
|
|
||||||||||||||||
| // Apply filtering based on provided arguments | ||||||||||||||||
| bool hasId = context.Args.Contains(Execution::Args::Type::Id); | ||||||||||||||||
| bool hasName = context.Args.Contains(Execution::Args::Type::Name); | ||||||||||||||||
| bool hasQuery = context.Args.Contains(Execution::Args::Type::Query); | ||||||||||||||||
| bool exactMatch = context.Args.Contains(Execution::Args::Type::Exact); | ||||||||||||||||
|
|
||||||||||||||||
|
||||||||||||||||
| if (!hasId && !hasName && !hasQuery) | |
| { | |
| context.Reporter.Error() << "The 'pin show' command requires at least one of --id, --name, or --query." << std::endl; | |
| AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_INVALID_CL_ARGUMENTS); | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should go into a path specific instructions file (with the appropriate target paths). It is a lot of lines that are only needed in a few cases.