Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .claude/skills/dataverse-sdk-use/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@ table_info = client.tables.create(
#### Supported Column Types
Types on the same line map to the same exact format under the hood
- `"string"` or `"text"` - Single line of text
- `"memo"` or `"multiline"` - Multiple lines of text (4000 character default)
- `"int"` or `"integer"` - Whole number
- `"decimal"` or `"money"` - Decimal number
- `"float"` or `"double"` - Floating point number
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,7 @@ for page in client.records.get(
# Create a custom table, including the customization prefix value in the schema names for the table and columns.
table_info = client.tables.create("new_Product", {
"new_Code": "string",
"new_Description": "memo",
"new_Price": "decimal",
"new_Active": "bool"
})
Expand Down Expand Up @@ -587,7 +588,7 @@ For optimal performance in production environments:
### Limitations

- SQL queries are **read-only** and support a limited subset of SQL syntax
- Create Table supports a limited number of column types (string, int, decimal, bool, datetime, picklist)
- Create Table supports the following column types: string, memo, int, decimal, float, bool, datetime, file, and picklist (Enum subclass)
- File uploads are limited by Dataverse file size restrictions (default 128MB per file)

## Contributing
Expand Down
25 changes: 19 additions & 6 deletions examples/advanced/walkthrough.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ def _run_walkthrough(client):
"new_Quantity": "int",
"new_Amount": "decimal",
"new_Completed": "bool",
"new_Notes": "memo",
"new_Priority": Priority,
}
table_info = backoff(lambda: client.tables.create(table_name, columns))
Expand All @@ -139,6 +140,7 @@ def _run_walkthrough(client):
"new_Quantity": 5,
"new_Amount": 1250.50,
"new_Completed": False,
"new_Notes": "This is a multiline memo field.\nIt supports longer text content.",
"new_Priority": Priority.MEDIUM,
}
id1 = backoff(lambda: client.records.create(table_name, single_record))
Expand Down Expand Up @@ -191,6 +193,7 @@ def _run_walkthrough(client):
"new_quantity": record.get("new_quantity"),
"new_amount": record.get("new_amount"),
"new_completed": record.get("new_completed"),
"new_notes": record.get("new_notes"),
"new_priority": record.get("new_priority"),
"new_priority@FormattedValue": record.get("new_priority@OData.Community.Display.V1.FormattedValue"),
},
Expand All @@ -217,9 +220,19 @@ def _run_walkthrough(client):

# Single update
log_call(f"client.records.update('{table_name}', '{id1}', {{...}})")
backoff(lambda: client.records.update(table_name, id1, {"new_Quantity": 100}))
backoff(
lambda: client.records.update(
table_name,
id1,
{
"new_Quantity": 100,
"new_Notes": "Updated memo field.\nNow with revised content across multiple lines.",
},
)
)
updated = backoff(lambda: client.records.get(table_name, id1))
print(f"[OK] Updated single record new_Quantity: {updated.get('new_quantity')}")
print(f" new_Notes: {repr(updated.get('new_notes'))}")

# Multiple update (broadcast same change)
log_call(f"client.records.update('{table_name}', [{len(ids)} IDs], {{...}})")
Expand Down Expand Up @@ -451,14 +464,14 @@ def _run_walkthrough(client):
print("11. Column Management")
print("=" * 80)

log_call(f"client.tables.add_columns('{table_name}', {{'new_Notes': 'string'}})")
created_cols = backoff(lambda: client.tables.add_columns(table_name, {"new_Notes": "string"}))
log_call(f"client.tables.add_columns('{table_name}', {{'new_Tags': 'string'}})")
created_cols = backoff(lambda: client.tables.add_columns(table_name, {"new_Tags": "string"}))
print(f"[OK] Added column: {created_cols[0]}")

# Delete the column we just added
log_call(f"client.tables.remove_columns('{table_name}', ['new_Notes'])")
backoff(lambda: client.tables.remove_columns(table_name, ["new_Notes"]))
print(f"[OK] Deleted column: new_Notes")
log_call(f"client.tables.remove_columns('{table_name}', ['new_Tags'])")
backoff(lambda: client.tables.remove_columns(table_name, ["new_Tags"]))
print(f"[OK] Deleted column: new_Tags")

# ============================================================================
# 12. DELETE OPERATIONS
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@ table_info = client.tables.create(
#### Supported Column Types
Types on the same line map to the same exact format under the hood
- `"string"` or `"text"` - Single line of text
- `"memo"` or `"multiline"` - Multiple lines of text (4000 character default)
- `"int"` or `"integer"` - Whole number
- `"decimal"` or `"money"` - Decimal number
- `"float"` or `"double"` - Floating point number
Expand Down
10 changes: 10 additions & 0 deletions src/PowerPlatform/Dataverse/data/_odata.py
Original file line number Diff line number Diff line change
Expand Up @@ -1374,6 +1374,16 @@ def _attribute_payload(
"FormatName": {"Value": "Text"},
"IsPrimaryName": bool(is_primary_name),
}
if dtype_l in ("memo", "multiline"):
return {
"@odata.type": "Microsoft.Dynamics.CRM.MemoAttributeMetadata",
"SchemaName": column_schema_name,
"DisplayName": self._label(label),
"RequiredLevel": {"Value": "None"},
"MaxLength": 4000,
Comment thread
abelmilash-msft marked this conversation as resolved.
"FormatName": {"Value": "Text"},
"ImeMode": "Auto",
}
Comment thread
abelmilash-msft marked this conversation as resolved.
if dtype_l in ("int", "integer"):
return {
"@odata.type": "Microsoft.Dynamics.CRM.IntegerAttributeMetadata",
Expand Down
3 changes: 2 additions & 1 deletion src/PowerPlatform/Dataverse/operations/tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,8 @@ def create(
:type table: :class:`str`
:param columns: Mapping of column schema names (with customization
prefix) to their types. Supported types include ``"string"``
(or ``"text"``), ``"int"`` (or ``"integer"``), ``"decimal"``
(or ``"text"``), ``"memo"`` (or ``"multiline"``),
``"int"`` (or ``"integer"``), ``"decimal"``
(or ``"money"``), ``"float"`` (or ``"double"``), ``"datetime"``
(or ``"date"``), ``"bool"`` (or ``"boolean"``), ``"file"``, and
``Enum`` subclasses
Expand Down
34 changes: 34 additions & 0 deletions tests/unit/data/test_odata_internal.py
Original file line number Diff line number Diff line change
Expand Up @@ -530,5 +530,39 @@ def test_returns_none(self):
self.assertIsNone(result)


class TestAttributePayload(unittest.TestCase):
"""Unit tests for _ODataClient._attribute_payload."""

def setUp(self):
self.od = _make_odata_client()

def test_memo_type(self):
"""'memo' should produce MemoAttributeMetadata with MaxLength 4000."""
result = self.od._attribute_payload("new_Notes", "memo")
self.assertEqual(result["@odata.type"], "Microsoft.Dynamics.CRM.MemoAttributeMetadata")
self.assertEqual(result["SchemaName"], "new_Notes")
self.assertEqual(result["MaxLength"], 4000)
self.assertEqual(result["FormatName"], {"Value": "Text"})
Comment thread
abelmilash-msft marked this conversation as resolved.
self.assertNotIn("IsPrimaryName", result)

def test_multiline_alias(self):
"""'multiline' should produce identical payload to 'memo'."""
memo_result = self.od._attribute_payload("new_Description", "memo")
multiline_result = self.od._attribute_payload("new_Description", "multiline")
self.assertEqual(multiline_result, memo_result)

def test_string_type(self):
"""'string' should produce StringAttributeMetadata with MaxLength 200."""
result = self.od._attribute_payload("new_Title", "string")
self.assertEqual(result["@odata.type"], "Microsoft.Dynamics.CRM.StringAttributeMetadata")
self.assertEqual(result["MaxLength"], 200)
self.assertEqual(result["FormatName"], {"Value": "Text"})

def test_unsupported_type_returns_none(self):
"""An unknown type string should return None."""
result = self.od._attribute_payload("new_Col", "unknown_type")
self.assertIsNone(result)


if __name__ == "__main__":
unittest.main()
10 changes: 10 additions & 0 deletions tests/unit/test_tables_operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,16 @@ def test_add_columns(self):
self.client._odata._create_columns.assert_called_once_with("new_Product", columns)
self.assertEqual(result, ["new_Notes", "new_Active"])

def test_add_columns_memo(self):
"""add_columns() with memo type should pass through correctly."""
self.client._odata._create_columns.return_value = ["new_Description"]

columns = {"new_Description": "memo"}
result = self.client.tables.add_columns("new_Product", columns)

self.client._odata._create_columns.assert_called_once_with("new_Product", columns)
self.assertEqual(result, ["new_Description"])

# --------------------------------------------------------- remove_columns

def test_remove_columns_single(self):
Expand Down
Loading