Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -250,6 +250,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 @@ -404,6 +404,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 @@ -674,7 +675,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 @@ -120,6 +120,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 @@ -140,6 +141,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 @@ -192,6 +194,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 @@ -218,9 +221,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 @@ -462,14 +475,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 @@ -250,6 +250,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 @@ -1268,6 +1268,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,
"FormatName": {"Value": "Text"},
"ImeMode": "Auto",
}
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 @@ -519,6 +519,40 @@ 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"})
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)


class TestPicklistLabelResolution(unittest.TestCase):
"""Tests for picklist label-to-integer resolution.

Expand Down
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