Skip to content

Commit 33dd068

Browse files
committed
enables backup retention and collation setting for SQL DB
1 parent a14137c commit 33dd068

File tree

5 files changed

+355
-0
lines changed

5 files changed

+355
-0
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
kind: added
2+
body: Add SQL Database creationPayload support for mkdir command with mode, backupRetentionDays, and collation parameters
3+
time: 2026-03-13T00:00:00.000000000Z
4+
custom:
5+
Author: dzsquared
6+
AuthorLink: https://github.com/dzsquared

src/fabric_cli/utils/fab_cmd_mkdir_utils.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,27 @@ def add_type_specific_payload(item: Item, args, payload):
188188
]
189189
}
190190

191+
case ItemType.SQL_DATABASE:
192+
_mode = params.get("mode")
193+
_backup_retention_days = params.get("backupretentiondays")
194+
_collation = params.get("collation")
195+
196+
if _mode or _backup_retention_days or _collation:
197+
creation_payload: dict = {"creationMode": _mode if _mode else "new"}
198+
if _backup_retention_days:
199+
try:
200+
creation_payload["backupRetentionDays"] = int(
201+
_backup_retention_days
202+
)
203+
except ValueError:
204+
raise FabricCLIError(
205+
f"'backupRetentionDays' must be a valid integer, got '{_backup_retention_days}'",
206+
fab_constant.ERROR_INVALID_INPUT,
207+
)
208+
if _collation:
209+
creation_payload["collation"] = _collation
210+
payload_dict["creationPayload"] = creation_payload
211+
191212
return payload_dict
192213

193214

@@ -309,6 +330,8 @@ def get_params_per_item_type(item: Item):
309330
optional_params = ["semanticModelId"]
310331
case ItemType.MOUNTED_DATA_FACTORY:
311332
required_params = ["subscriptionId", "resourceGroup", "factoryName"]
333+
case ItemType.SQL_DATABASE:
334+
optional_params = ["mode", "backupRetentionDays", "collation"]
312335

313336
return required_params, optional_params
314337

tests/test_commands/conftest.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@
8383
["Latin1_General_100_CI_AS_KS_WS_SC_UTF8"]),
8484
(ItemType.WAREHOUSE, "", ["Latin1_General_100_BIN2_UTF8"]),
8585
(ItemType.REPORT, "", ["_auto"]),
86+
(ItemType.SQL_DATABASE, "backupRetentionDays=21,collation=SQL_Latin1_General_CP1_CS_AS",
87+
["properties"]),
8688
])
8789

8890
mv_item_to_item_success_params = pytest.mark.parametrize("item_type", [

tests/test_commands/test_mkdir.py

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,184 @@ def test_mkdir_item_with_creation_payload_success(
286286
# Cleanup
287287
rm(item_full_path)
288288

289+
def test_mkdir_sqldatabase_without_params_success(
290+
self,
291+
workspace,
292+
cli_executor,
293+
mock_print_done,
294+
mock_questionary_print,
295+
vcr_instance,
296+
cassette_name,
297+
upsert_item_to_cache,
298+
):
299+
# Setup
300+
sqldb_display_name = generate_random_string(vcr_instance, cassette_name)
301+
sqldb_full_path = cli_path_join(
302+
workspace.full_path, f"{sqldb_display_name}.{ItemType.SQL_DATABASE}"
303+
)
304+
305+
# Execute command without -P flag
306+
cli_executor.exec_command(f"mkdir {sqldb_full_path}")
307+
308+
# Assert creation success
309+
upsert_item_to_cache.assert_called_once()
310+
mock_print_done.assert_called_once()
311+
assert sqldb_display_name in mock_print_done.call_args[0][0]
312+
313+
# Verify item was created without creationPayload
314+
mock_questionary_print.reset_mock()
315+
get(sqldb_full_path, query=".")
316+
mock_questionary_print.assert_called_once()
317+
result_output = mock_questionary_print.call_args[0][0]
318+
assert sqldb_display_name in result_output
319+
assert "creationPayload" not in result_output
320+
321+
# Cleanup
322+
rm(sqldb_full_path)
323+
324+
def test_mkdir_sqldatabase_partial_params_success(
325+
self,
326+
workspace,
327+
cli_executor,
328+
mock_print_done,
329+
mock_questionary_print,
330+
vcr_instance,
331+
cassette_name,
332+
upsert_item_to_cache,
333+
):
334+
# Setup
335+
sqldb_display_name = generate_random_string(vcr_instance, cassette_name)
336+
sqldb_full_path = cli_path_join(
337+
workspace.full_path, f"{sqldb_display_name}.{ItemType.SQL_DATABASE}"
338+
)
339+
340+
# Execute command with only backupRetentionDays (no collation)
341+
cli_executor.exec_command(f"mkdir {sqldb_full_path} -P backupRetentionDays=21")
342+
343+
# Assert creation success
344+
upsert_item_to_cache.assert_called_once()
345+
mock_print_done.assert_called_once()
346+
assert sqldb_display_name in mock_print_done.call_args[0][0]
347+
348+
# Verify item was created with properties
349+
mock_questionary_print.reset_mock()
350+
get(sqldb_full_path, query=".")
351+
mock_questionary_print.assert_called_once()
352+
result_output = mock_questionary_print.call_args[0][0]
353+
assert sqldb_display_name in result_output
354+
355+
# Cleanup
356+
rm(sqldb_full_path)
357+
358+
def test_mkdir_sqldatabase_invalid_retention_days_failure(
359+
self,
360+
workspace,
361+
cli_executor,
362+
assert_fabric_cli_error,
363+
vcr_instance,
364+
cassette_name,
365+
):
366+
# Setup
367+
sqldb_display_name = generate_random_string(vcr_instance, cassette_name)
368+
sqldb_full_path = cli_path_join(
369+
workspace.full_path, f"{sqldb_display_name}.{ItemType.SQL_DATABASE}"
370+
)
371+
372+
# Execute command with invalid backupRetentionDays
373+
cli_executor.exec_command(f"mkdir {sqldb_full_path} -P backupRetentionDays=abc")
374+
375+
# Assert failure due to invalid integer
376+
assert_fabric_cli_error(constant.ERROR_INVALID_INPUT)
377+
378+
def test_mkdir_sqldatabase_explicit_mode_success(
379+
self,
380+
workspace,
381+
cli_executor,
382+
mock_print_done,
383+
mock_questionary_print,
384+
vcr_instance,
385+
cassette_name,
386+
upsert_item_to_cache,
387+
):
388+
# Setup
389+
sqldb_display_name = generate_random_string(vcr_instance, cassette_name)
390+
sqldb_full_path = cli_path_join(
391+
workspace.full_path, f"{sqldb_display_name}.{ItemType.SQL_DATABASE}"
392+
)
393+
394+
# Execute command with explicit mode
395+
cli_executor.exec_command(f"mkdir {sqldb_full_path} -P mode=new")
396+
397+
# Assert creation success
398+
upsert_item_to_cache.assert_called_once()
399+
mock_print_done.assert_called_once()
400+
assert sqldb_display_name in mock_print_done.call_args[0][0]
401+
402+
# Verify item was created with properties
403+
mock_questionary_print.reset_mock()
404+
get(sqldb_full_path, query=".")
405+
mock_questionary_print.assert_called_once()
406+
result_output = mock_questionary_print.call_args[0][0]
407+
assert sqldb_display_name in result_output
408+
409+
# Cleanup
410+
rm(sqldb_full_path)
411+
412+
def test_mkdir_sqldatabase_mode_only_success(
413+
self,
414+
workspace,
415+
cli_executor,
416+
mock_print_done,
417+
mock_questionary_print,
418+
vcr_instance,
419+
cassette_name,
420+
upsert_item_to_cache,
421+
):
422+
# Setup
423+
sqldb_display_name = generate_random_string(vcr_instance, cassette_name)
424+
sqldb_full_path = cli_path_join(
425+
workspace.full_path, f"{sqldb_display_name}.{ItemType.SQL_DATABASE}"
426+
)
427+
428+
# Execute command with only mode param
429+
cli_executor.exec_command(f"mkdir {sqldb_full_path} -P mode=new")
430+
431+
# Assert creation success
432+
upsert_item_to_cache.assert_called_once()
433+
mock_print_done.assert_called_once()
434+
assert sqldb_display_name in mock_print_done.call_args[0][0]
435+
436+
# Verify item was created
437+
mock_questionary_print.reset_mock()
438+
get(sqldb_full_path, query=".")
439+
mock_questionary_print.assert_called_once()
440+
result_output = mock_questionary_print.call_args[0][0]
441+
assert sqldb_display_name in result_output
442+
443+
# Cleanup
444+
rm(sqldb_full_path)
445+
446+
def test_mkdir_sqldatabase_param_discovery(
447+
self, workspace, cli_executor, mock_questionary_print
448+
):
449+
# Setup
450+
sqldb_full_path = cli_path_join(
451+
workspace.full_path, f"paramDiscovery.{ItemType.SQL_DATABASE}"
452+
)
453+
454+
# Execute command with -P but no value
455+
cli_executor.exec_command(f"mkdir {sqldb_full_path} -P")
456+
457+
# Assert param discovery message is printed
458+
no_params_message = (
459+
"Params for '.SQLDatabase'. Use key=value separated by commas."
460+
)
461+
output = mock_questionary_print.call_args[0][0]
462+
assert no_params_message in output
463+
assert "backupRetentionDays" in output
464+
assert "collation" in output
465+
assert "mode" in output
466+
289467
def test_mkdir_mounted_data_factory_with_required_params_success(
290468
self,
291469
workspace,

tests/test_utils/test_fab_cmd_mkdir_utils.py

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,12 @@
1010
from fabric_cli.core.fab_exceptions import FabricCLIError
1111
from fabric_cli.errors import ErrorMessages
1212
from fabric_cli.utils.fab_cmd_mkdir_utils import (
13+
add_type_specific_payload,
1314
find_mpe_connection,
1415
get_connection_config_from_params,
16+
get_params_per_item_type,
1517
)
18+
from fabric_cli.core.fab_types import ItemType
1619

1720

1821
def test_fabric_data_pipelines_workspace_identity_no_params_success():
@@ -209,4 +212,147 @@ def test_find_mpe_connection_return_403_success(self):
209212
called_url = call_args.args[1] if len(call_args.args) > 1 else call_args.kwargs['url']
210213
assert "privateEndpointConnections" in called_url
211214
assert "api-version=2023-11-01" in called_url
215+
216+
217+
class TestGetParamsPerItemTypeSqlDatabase:
218+
"""Test cases for get_params_per_item_type with SQL_DATABASE."""
219+
220+
def test_sql_database_returns_optional_params(self):
221+
"""Test that SQL_DATABASE returns the correct optional params."""
222+
mock_item = Mock()
223+
mock_item.item_type = ItemType.SQL_DATABASE
224+
225+
required, optional = get_params_per_item_type(mock_item)
226+
227+
assert required == []
228+
assert optional == ["mode", "backupRetentionDays", "collation"]
229+
230+
def test_sql_database_has_no_required_params(self):
231+
"""Test that SQL_DATABASE has no required params."""
232+
mock_item = Mock()
233+
mock_item.item_type = ItemType.SQL_DATABASE
234+
235+
required, _ = get_params_per_item_type(mock_item)
236+
237+
assert len(required) == 0
238+
239+
240+
class TestAddTypeSpecificPayloadSqlDatabase:
241+
"""Test cases for add_type_specific_payload with SQL_DATABASE."""
242+
243+
def _make_item_and_args(self, params):
244+
"""Helper to create mock item and args for SQL_DATABASE."""
245+
mock_item = Mock()
246+
mock_item.item_type = ItemType.SQL_DATABASE
247+
mock_args = Namespace(params=params)
248+
return mock_item, mock_args
249+
250+
def test_all_params_success(self):
251+
"""Test SQL_DATABASE with all params provided."""
252+
item, args = self._make_item_and_args(
253+
{
254+
"mode": "new",
255+
"backupretentiondays": "21",
256+
"collation": "SQL_Latin1_General_CP1_CI_AS",
257+
}
258+
)
259+
payload = {"displayName": "testdb"}
260+
261+
result = add_type_specific_payload(item, args, payload)
262+
263+
assert "creationPayload" in result
264+
cp = result["creationPayload"]
265+
assert cp["creationMode"] == "new"
266+
assert cp["backupRetentionDays"] == 21
267+
assert cp["collation"] == "SQL_Latin1_General_CP1_CI_AS"
268+
269+
def test_partial_params_backup_only_success(self):
270+
"""Test SQL_DATABASE with only backupRetentionDays provided."""
271+
item, args = self._make_item_and_args({"backupretentiondays": "7"})
272+
payload = {"displayName": "testdb"}
273+
274+
result = add_type_specific_payload(item, args, payload)
275+
276+
assert "creationPayload" in result
277+
cp = result["creationPayload"]
278+
assert cp["creationMode"] == "new" # Default
279+
assert cp["backupRetentionDays"] == 7
280+
assert "collation" not in cp
281+
282+
def test_partial_params_collation_only_success(self):
283+
"""Test SQL_DATABASE with only collation provided."""
284+
item, args = self._make_item_and_args(
285+
{"collation": "SQL_Latin1_General_CP1_CI_AS"}
286+
)
287+
payload = {"displayName": "testdb"}
288+
289+
result = add_type_specific_payload(item, args, payload)
290+
291+
assert "creationPayload" in result
292+
cp = result["creationPayload"]
293+
assert cp["creationMode"] == "new" # Default
294+
assert "backupRetentionDays" not in cp
295+
assert cp["collation"] == "SQL_Latin1_General_CP1_CI_AS"
296+
297+
def test_no_recognized_params_no_creation_payload(self):
298+
"""Test SQL_DATABASE with no recognized params produces no creationPayload."""
299+
item, args = self._make_item_and_args({"unrelated": "value"})
300+
payload = {"displayName": "testdb"}
301+
302+
result = add_type_specific_payload(item, args, payload)
303+
304+
assert "creationPayload" not in result
305+
306+
def test_empty_params_no_creation_payload(self):
307+
"""Test SQL_DATABASE with empty params dict produces no creationPayload."""
308+
item, args = self._make_item_and_args({})
309+
payload = {"displayName": "testdb"}
310+
311+
result = add_type_specific_payload(item, args, payload)
312+
313+
assert "creationPayload" not in result
314+
315+
def test_invalid_backup_retention_days_failure(self):
316+
"""Test SQL_DATABASE with non-integer backupRetentionDays raises FabricCLIError."""
317+
item, args = self._make_item_and_args({"backupretentiondays": "abc"})
318+
payload = {"displayName": "testdb"}
319+
320+
with pytest.raises(FabricCLIError) as exc_info:
321+
add_type_specific_payload(item, args, payload)
322+
323+
assert "backupRetentionDays" in str(exc_info.value.message)
324+
assert "abc" in str(exc_info.value.message)
325+
assert exc_info.value.status_code == fab_constant.ERROR_INVALID_INPUT
326+
327+
def test_explicit_mode_success(self):
328+
"""Test SQL_DATABASE with explicit mode value."""
329+
item, args = self._make_item_and_args({"mode": "copy"})
330+
payload = {"displayName": "testdb"}
331+
332+
result = add_type_specific_payload(item, args, payload)
333+
334+
assert result["creationPayload"]["creationMode"] == "copy"
335+
336+
def test_mode_only_success(self):
337+
"""Test SQL_DATABASE with only mode provided."""
338+
item, args = self._make_item_and_args({"mode": "new"})
339+
payload = {"displayName": "testdb"}
340+
341+
result = add_type_specific_payload(item, args, payload)
342+
343+
cp = result["creationPayload"]
344+
assert cp["creationMode"] == "new"
345+
assert "backupRetentionDays" not in cp
346+
assert "collation" not in cp
347+
348+
def test_preserves_existing_payload_fields(self):
349+
"""Test that SQL_DATABASE creation doesn't remove existing payload fields."""
350+
item, args = self._make_item_and_args({"mode": "new"})
351+
payload = {"displayName": "testdb", "description": "A test database"}
352+
353+
result = add_type_specific_payload(item, args, payload)
354+
355+
assert result["displayName"] == "testdb"
356+
assert result["description"] == "A test database"
357+
assert "creationPayload" in result
212358

0 commit comments

Comments
 (0)