From 1d54f243840a716f309f3ac69dfefe3433b5dcbd Mon Sep 17 00:00:00 2001 From: Abel Milash Date: Wed, 1 Apr 2026 09:07:17 -0700 Subject: [PATCH 1/4] Add memo/multiline column type support with docs and tests --- .claude/skills/dataverse-sdk-use/SKILL.md | 1 + README.md | 3 +- examples/advanced/walkthrough.py | 25 ++++++++++---- .../claude_skill/dataverse-sdk-use/SKILL.md | 1 + src/PowerPlatform/Dataverse/data/_odata.py | 10 ++++++ .../Dataverse/operations/tables.py | 3 +- tests/unit/data/test_odata_internal.py | 34 +++++++++++++++++++ tests/unit/test_tables_operations.py | 10 ++++++ 8 files changed, 79 insertions(+), 8 deletions(-) diff --git a/.claude/skills/dataverse-sdk-use/SKILL.md b/.claude/skills/dataverse-sdk-use/SKILL.md index 9edb733f..9288a535 100644 --- a/.claude/skills/dataverse-sdk-use/SKILL.md +++ b/.claude/skills/dataverse-sdk-use/SKILL.md @@ -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 diff --git a/README.md b/README.md index 3b892644..28f86b59 100644 --- a/README.md +++ b/README.md @@ -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" }) @@ -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 diff --git a/examples/advanced/walkthrough.py b/examples/advanced/walkthrough.py index ef633d00..a67b5e68 100644 --- a/examples/advanced/walkthrough.py +++ b/examples/advanced/walkthrough.py @@ -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)) @@ -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)) @@ -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"), }, @@ -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], {{...}})") @@ -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 diff --git a/src/PowerPlatform/Dataverse/claude_skill/dataverse-sdk-use/SKILL.md b/src/PowerPlatform/Dataverse/claude_skill/dataverse-sdk-use/SKILL.md index 9edb733f..9288a535 100644 --- a/src/PowerPlatform/Dataverse/claude_skill/dataverse-sdk-use/SKILL.md +++ b/src/PowerPlatform/Dataverse/claude_skill/dataverse-sdk-use/SKILL.md @@ -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 diff --git a/src/PowerPlatform/Dataverse/data/_odata.py b/src/PowerPlatform/Dataverse/data/_odata.py index 78ce4091..36dee096 100644 --- a/src/PowerPlatform/Dataverse/data/_odata.py +++ b/src/PowerPlatform/Dataverse/data/_odata.py @@ -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, + "FormatName": {"Value": "Text"}, + "ImeMode": "Auto", + } if dtype_l in ("int", "integer"): return { "@odata.type": "Microsoft.Dynamics.CRM.IntegerAttributeMetadata", diff --git a/src/PowerPlatform/Dataverse/operations/tables.py b/src/PowerPlatform/Dataverse/operations/tables.py index 729e0eba..5df5bd64 100644 --- a/src/PowerPlatform/Dataverse/operations/tables.py +++ b/src/PowerPlatform/Dataverse/operations/tables.py @@ -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 diff --git a/tests/unit/data/test_odata_internal.py b/tests/unit/data/test_odata_internal.py index bea5a3d6..91b37718 100644 --- a/tests/unit/data/test_odata_internal.py +++ b/tests/unit/data/test_odata_internal.py @@ -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"}) + 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() diff --git a/tests/unit/test_tables_operations.py b/tests/unit/test_tables_operations.py index 69f57a58..d04cef24 100644 --- a/tests/unit/test_tables_operations.py +++ b/tests/unit/test_tables_operations.py @@ -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): From a10738fd38cf2d0c10ccec41d34bdf576e32e646 Mon Sep 17 00:00:00 2001 From: Abel Milash Date: Fri, 3 Apr 2026 13:30:19 -0700 Subject: [PATCH 2/4] Update memo MaxLength to platform maximum (1048576) --- .claude/skills/dataverse-sdk-use/SKILL.md | 2 +- .../Dataverse/claude_skill/dataverse-sdk-use/SKILL.md | 2 +- src/PowerPlatform/Dataverse/data/_odata.py | 2 +- tests/unit/data/test_odata_internal.py | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.claude/skills/dataverse-sdk-use/SKILL.md b/.claude/skills/dataverse-sdk-use/SKILL.md index 9288a535..ae1ef761 100644 --- a/.claude/skills/dataverse-sdk-use/SKILL.md +++ b/.claude/skills/dataverse-sdk-use/SKILL.md @@ -249,7 +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) +- `"memo"` or `"multiline"` - Multiple lines of text (up to 1,048,576 characters) - `"int"` or `"integer"` - Whole number - `"decimal"` or `"money"` - Decimal number - `"float"` or `"double"` - Floating point number diff --git a/src/PowerPlatform/Dataverse/claude_skill/dataverse-sdk-use/SKILL.md b/src/PowerPlatform/Dataverse/claude_skill/dataverse-sdk-use/SKILL.md index 9288a535..ae1ef761 100644 --- a/src/PowerPlatform/Dataverse/claude_skill/dataverse-sdk-use/SKILL.md +++ b/src/PowerPlatform/Dataverse/claude_skill/dataverse-sdk-use/SKILL.md @@ -249,7 +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) +- `"memo"` or `"multiline"` - Multiple lines of text (up to 1,048,576 characters) - `"int"` or `"integer"` - Whole number - `"decimal"` or `"money"` - Decimal number - `"float"` or `"double"` - Floating point number diff --git a/src/PowerPlatform/Dataverse/data/_odata.py b/src/PowerPlatform/Dataverse/data/_odata.py index 36dee096..04d278d4 100644 --- a/src/PowerPlatform/Dataverse/data/_odata.py +++ b/src/PowerPlatform/Dataverse/data/_odata.py @@ -1380,7 +1380,7 @@ def _attribute_payload( "SchemaName": column_schema_name, "DisplayName": self._label(label), "RequiredLevel": {"Value": "None"}, - "MaxLength": 4000, + "MaxLength": 1048576, "FormatName": {"Value": "Text"}, "ImeMode": "Auto", } diff --git a/tests/unit/data/test_odata_internal.py b/tests/unit/data/test_odata_internal.py index 91b37718..2174b5d8 100644 --- a/tests/unit/data/test_odata_internal.py +++ b/tests/unit/data/test_odata_internal.py @@ -537,11 +537,11 @@ def setUp(self): self.od = _make_odata_client() def test_memo_type(self): - """'memo' should produce MemoAttributeMetadata with MaxLength 4000.""" + """'memo' should produce MemoAttributeMetadata with MaxLength 1048576.""" 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["MaxLength"], 1048576) self.assertEqual(result["FormatName"], {"Value": "Text"}) self.assertNotIn("IsPrimaryName", result) From da6ffda3185d0d8e6a23660a930cba26b738097d Mon Sep 17 00:00:00 2001 From: Abel Milash Date: Mon, 6 Apr 2026 10:54:27 -0700 Subject: [PATCH 3/4] Set memo MaxLength to 4000 --- .claude/skills/dataverse-sdk-use/SKILL.md | 2 +- .../Dataverse/claude_skill/dataverse-sdk-use/SKILL.md | 2 +- src/PowerPlatform/Dataverse/data/_odata.py | 2 +- tests/unit/data/test_odata_internal.py | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.claude/skills/dataverse-sdk-use/SKILL.md b/.claude/skills/dataverse-sdk-use/SKILL.md index ae1ef761..0fd69b70 100644 --- a/.claude/skills/dataverse-sdk-use/SKILL.md +++ b/.claude/skills/dataverse-sdk-use/SKILL.md @@ -249,7 +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 (up to 1,048,576 characters) +- `"memo"` or `"multiline"` - Multiple lines of text (4000 character default, up to 1,048,576) - `"int"` or `"integer"` - Whole number - `"decimal"` or `"money"` - Decimal number - `"float"` or `"double"` - Floating point number diff --git a/src/PowerPlatform/Dataverse/claude_skill/dataverse-sdk-use/SKILL.md b/src/PowerPlatform/Dataverse/claude_skill/dataverse-sdk-use/SKILL.md index ae1ef761..0fd69b70 100644 --- a/src/PowerPlatform/Dataverse/claude_skill/dataverse-sdk-use/SKILL.md +++ b/src/PowerPlatform/Dataverse/claude_skill/dataverse-sdk-use/SKILL.md @@ -249,7 +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 (up to 1,048,576 characters) +- `"memo"` or `"multiline"` - Multiple lines of text (4000 character default, up to 1,048,576) - `"int"` or `"integer"` - Whole number - `"decimal"` or `"money"` - Decimal number - `"float"` or `"double"` - Floating point number diff --git a/src/PowerPlatform/Dataverse/data/_odata.py b/src/PowerPlatform/Dataverse/data/_odata.py index 04d278d4..36dee096 100644 --- a/src/PowerPlatform/Dataverse/data/_odata.py +++ b/src/PowerPlatform/Dataverse/data/_odata.py @@ -1380,7 +1380,7 @@ def _attribute_payload( "SchemaName": column_schema_name, "DisplayName": self._label(label), "RequiredLevel": {"Value": "None"}, - "MaxLength": 1048576, + "MaxLength": 4000, "FormatName": {"Value": "Text"}, "ImeMode": "Auto", } diff --git a/tests/unit/data/test_odata_internal.py b/tests/unit/data/test_odata_internal.py index 2174b5d8..91b37718 100644 --- a/tests/unit/data/test_odata_internal.py +++ b/tests/unit/data/test_odata_internal.py @@ -537,11 +537,11 @@ def setUp(self): self.od = _make_odata_client() def test_memo_type(self): - """'memo' should produce MemoAttributeMetadata with MaxLength 1048576.""" + """'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"], 1048576) + self.assertEqual(result["MaxLength"], 4000) self.assertEqual(result["FormatName"], {"Value": "Text"}) self.assertNotIn("IsPrimaryName", result) From 6c92164693d93e08f980f5eac94e8fefd863228c Mon Sep 17 00:00:00 2001 From: Abel Milash Date: Mon, 6 Apr 2026 10:57:23 -0700 Subject: [PATCH 4/4] Simplify memo docs wording --- .claude/skills/dataverse-sdk-use/SKILL.md | 2 +- .../Dataverse/claude_skill/dataverse-sdk-use/SKILL.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.claude/skills/dataverse-sdk-use/SKILL.md b/.claude/skills/dataverse-sdk-use/SKILL.md index 0fd69b70..9288a535 100644 --- a/.claude/skills/dataverse-sdk-use/SKILL.md +++ b/.claude/skills/dataverse-sdk-use/SKILL.md @@ -249,7 +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, up to 1,048,576) +- `"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 diff --git a/src/PowerPlatform/Dataverse/claude_skill/dataverse-sdk-use/SKILL.md b/src/PowerPlatform/Dataverse/claude_skill/dataverse-sdk-use/SKILL.md index 0fd69b70..9288a535 100644 --- a/src/PowerPlatform/Dataverse/claude_skill/dataverse-sdk-use/SKILL.md +++ b/src/PowerPlatform/Dataverse/claude_skill/dataverse-sdk-use/SKILL.md @@ -249,7 +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, up to 1,048,576) +- `"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