Repository: dbt-labs/dbt-core

Issue:
[CT-2924] [Bug] Retry does not construct flags properly for 0 valued parameter
### Is this a new bug in dbt-core?

- [X] I believe this is a new bug in dbt-core
- [X] I have searched the existing issues, and I could not find an existing issue for this bug

### Current Behavior

Retry would fail if previous command run with `--log-file-max-bytes 0` specified.

### Expected Behavior

Retry properly get the behavior of `--log-file-max-bytes 0`

### Steps To Reproduce

1. In a dbt project, run `dbt --log-file-max-bytes 0 run`.
2. run `dbt retry`

### Relevant log output

```shell
20:12:53  Running with dbt=1.6.0
20:12:53  Registered adapter: postgres=1.6.0
20:12:53  Encountered an error:
No such option: --no-log-file-max-bytes
20:12:53  Traceback (most recent call last):
  File "/Users/chenyuli/git/dbt-core/core/dbt/cli/requires.py", line 87, in wrapper
    result, success = func(*args, **kwargs)
                      ^^^^^^^^^^^^^^^^^^^^^
  File "/Users/chenyuli/git/dbt-core/core/dbt/cli/requires.py", line 72, in wrapper
    return func(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^
  File "/Users/chenyuli/git/dbt-core/core/dbt/cli/requires.py", line 143, in wrapper
    return func(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^
  File "/Users/chenyuli/git/dbt-core/core/dbt/cli/requires.py", line 172, in wrapper
    return func(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^
  File "/Users/chenyuli/git/dbt-core/core/dbt/cli/requires.py", line 219, in wrapper
    return func(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^
  File "/Users/chenyuli/git/dbt-core/core/dbt/cli/requires.py", line 259, in wrapper
    return func(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^
  File "/Users/chenyuli/git/dbt-core/core/dbt/cli/main.py", line 608, in retry
    results = task.run()
              ^^^^^^^^^^
  File "/Users/chenyuli/git/dbt-core/core/dbt/task/retry.py", line 94, in run
    retry_flags = Flags.from_dict(cli_command, self.previous_args)
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/chenyuli/git/dbt-core/core/dbt/cli/flags.py", line 294, in from_dict
    ctx = args_to_context(command_arg_list)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/chenyuli/git/dbt-core/core/dbt/cli/flags.py", line 59, in args_to_context
    cli_ctx = cli.make_context(cli.name, args)
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/chenyuli/git/ide-server/ide-server/lib/python3.11/site-packages/click/core.py", line 920, in make_context
    self.parse_args(ctx, args)
  File "/Users/chenyuli/git/ide-server/ide-server/lib/python3.11/site-packages/click/core.py", line 1613, in parse_args
    rest = super().parse_args(ctx, args)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/chenyuli/git/ide-server/ide-server/lib/python3.11/site-packages/click/core.py", line 1375, in parse_args
    opts, args, param_order = parser.parse_args(args=args)
                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/chenyuli/git/ide-server/ide-server/lib/python3.11/site-packages/click/parser.py", line 337, in parse_args
    self._process_args_for_options(state)
  File "/Users/chenyuli/git/ide-server/ide-server/lib/python3.11/site-packages/click/parser.py", line 364, in _process_args_for_options
    self._process_opts(arg, state)
  File "/Users/chenyuli/git/ide-server/ide-server/lib/python3.11/site-packages/click/parser.py", line 514, in _process_opts
    self._match_long_opt(norm_long_opt, explicit_value, state)
  File "/Users/chenyuli/git/ide-server/ide-server/lib/python3.11/site-packages/click/parser.py", line 398, in _match_long_opt
    raise NoSuchOption(opt, possibilities=possibilities, ctx=self.ctx)
click.exceptions.NoSuchOption: No such option: --no-log-file-max-bytes
```



Discussion:

Resolving patch:
diff --git a/core/dbt/cli/flags.py b/core/dbt/cli/flags.py
--- a/core/dbt/cli/flags.py
+++ b/core/dbt/cli/flags.py
@@ -61,7 +61,6 @@ def args_to_context(args: List[str]) -> Context:
     if len(args) == 1 and "," in args[0]:
         args = args[0].split(",")
     sub_command_name, sub_command, args = cli.resolve_command(cli_ctx, args)
-
     # Handle source and docs group.
     if isinstance(sub_command, Group):
         sub_command_name, sub_command, args = sub_command.resolve_command(cli_ctx, args)
@@ -319,7 +318,6 @@ def command_params(command: CliCommand, args_dict: Dict[str, Any]) -> CommandPar
 
     for k, v in args_dict.items():
         k = k.lower()
-
         # if a "which" value exists in the args dict, it should match the command provided
         if k == WHICH_KEY:
             if v != command.value:
@@ -344,7 +342,8 @@ def add_fn(x):
 
         if k == "macro" and command == CliCommand.RUN_OPERATION:
             add_fn(v)
-        elif v in (None, False):
+        # None is a Singleton, False is a Flyweight, only one instance of each.
+        elif v is None or v is False:
             add_fn(f"--no-{spinal_cased}")
         elif v is True:
             add_fn(f"--{spinal_cased}")


Test patch:
diff --git a/tests/unit/test_cli_flags.py b/tests/unit/test_cli_flags.py
--- a/tests/unit/test_cli_flags.py
+++ b/tests/unit/test_cli_flags.py
@@ -391,3 +391,8 @@ def test_from_dict__which_fails(self):
         args_dict = {"which": "some bad command"}
         with pytest.raises(DbtInternalError, match=r"does not match value of which"):
             self._create_flags_from_dict(Command.RUN, args_dict)
+
+    def test_from_dict_0_value(self):
+        args_dict = {"log_file_max_bytes": 0}
+        flags = Flags.from_dict(Command.RUN, args_dict)
+        assert flags.LOG_FILE_MAX_BYTES == 0