Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
5b08ea0
Initial plan
Copilot Mar 13, 2026
d61ffca
Add list_columns, list_relationships, list_table_relationships APIs w…
Copilot Mar 13, 2026
7f06533
End-to-end SQL support: schema discovery, SELECT * expansion, DataFra…
Mar 21, 2026
12d3e86
Adjust guardrails: remove TOP injection (server enforces 5000 cap), a…
Mar 21, 2026
89adad3
Add SQL helper functions: sql_columns, sql_select, sql_joins, sql_join
Mar 21, 2026
86f9c37
Add OData helpers: odata_select, odata_expands, odata_expand, odata_bind
Mar 21, 2026
76525ff
Fix: add ManyToOneRelationships to list_table_relationships
Mar 22, 2026
769616b
Fix: exclude computed display-name columns from sql_columns/odata_select
Mar 22, 2026
e251f42
Fix: move write guardrail before table extraction + test 8-table JOIN…
Mar 22, 2026
3945b8b
Update sql_examples.py: add AND/OR, NOT IN, deep JOINs, helpers, SQL …
Mar 22, 2026
50cf7d7
Proven: 15-table SQL JOINs work (no depth limit, unlike OData's 10-le…
Mar 22, 2026
f8f5151
Add anti-pattern warnings + best practices section to sql_examples.py
Mar 22, 2026
7096624
Upgrade: cross join guardrail from warning to ValidationError
Mar 22, 2026
68d32ea
Revert: cross join guardrail back to UserWarning (not ValidationError)
Mar 22, 2026
a09e266
Block all server-rejected SQL patterns in SDK (save round-trip)
Mar 22, 2026
b18a219
Improve anti-pattern #4: mention sql_select() helper, clarify SELECT …
Mar 22, 2026
a595a72
Address all 11 PR review comments
Mar 22, 2026
a0ae21d
Fix README: list_table_relationships includes ManyToOne (confirmed Da…
Mar 22, 2026
b5eb12b
Full audit: fix Learn docstrings, add __all__, fix param doc, add Att…
Mar 22, 2026
56f19b8
Fix CodeQL ReDoS: replace vulnerable write regex with comment-strippi…
Mar 22, 2026
e129eed
fix: address 5 Copilot review comments (round 2)
Mar 22, 2026
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
87 changes: 83 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,14 @@ client.dataframe.update("account", df, id_column="accountid", clear_nulls=True)

# Delete records by passing a Series of GUIDs
client.dataframe.delete("account", new_accounts["accountid"])

# SQL query directly to DataFrame (supports JOINs, aggregates, GROUP BY)
df = client.dataframe.sql(
"SELECT a.name, COUNT(c.contactid) as contacts "
"FROM account a "
"JOIN contact c ON a.accountid = c.parentcustomerid "
"GROUP BY a.name"
)
```

### Query data
Expand Down Expand Up @@ -372,19 +380,68 @@ results = (client.query.builder("account")
.execute())
```

**SQL queries** provide an alternative read-only query syntax:
**SQL queries** provide an alternative read-only query syntax with support for
JOINs, aggregates, GROUP BY, DISTINCT, and OFFSET FETCH pagination:

```python
# Basic query
results = client.query.sql(
"SELECT TOP 10 accountid, name FROM account WHERE statecode = 0"
)
for record in results:
print(record["name"])

# JOINs and aggregates work
results = client.query.sql(
"SELECT a.name, COUNT(c.contactid) as cnt "
"FROM account a "
"JOIN contact c ON a.accountid = c.parentcustomerid "
"GROUP BY a.name"
)

# SELECT * is auto-expanded by the SDK
results = client.query.sql("SELECT * FROM account")

# SQL results directly as a DataFrame
df = client.dataframe.sql(
"SELECT name, revenue FROM account ORDER BY revenue DESC"
)

# SQL helpers: discover columns and JOINs from metadata
cols = client.query.sql_select("account") # "accountid, name, revenue, ..."
join = client.query.sql_join("contact", "account", from_alias="c", to_alias="a")
# Returns: "JOIN account a ON c.parentcustomerid = a.accountid"

# Build queries using helpers -- no OData knowledge needed
sql = f"SELECT TOP 10 c.fullname, a.name FROM contact c {join}"
df = client.dataframe.sql(sql)

# Discover all possible JOINs from a table (including polymorphic)
joins = client.query.sql_joins("opportunity")
for j in joins:
print(f"{j['column']:30s} -> {j['target']}.{j['target_pk']}")
```

**Raw OData queries** are available via `records.get()` for cases where you need direct control over the OData filter string:
**Raw OData queries** are available via `records.get()` for cases where you need direct control over the OData filter string. The SDK provides helpers to eliminate the most error-prone parts:

```python
# Discover columns for $select (returns list ready for select= parameter)
cols = client.query.odata_select("account")
for page in client.records.get("account", select=cols, top=10):
...

# Discover $expand navigation properties (auto-resolves PascalCase names)
nav = client.query.odata_expand("contact", "account")
# Returns: "parentcustomerid_account"
for page in client.records.get("contact", select=["fullname"], expand=[nav], top=5):
for r in page:
acct = r.get(nav) or {}
print(f"{r['fullname']} -> {acct.get('name')}")

# Build @odata.bind for lookup fields (no manual name construction)
bind = client.query.odata_bind("contact", "account", account_id)
# Returns: {"parentcustomerid_account@odata.bind": "/accounts(guid)"}
client.records.create("contact", {"firstname": "Jane", **bind})

# Raw OData query with manual parameters
for page in client.records.get(
"account",
select=["name"],
Expand Down Expand Up @@ -433,6 +490,18 @@ client.tables.add_columns("new_Product", {"new_Category": "string"})
# Remove columns
client.tables.remove_columns("new_Product", ["new_Category"])

# List all columns (attributes) for a table to discover schema
columns = client.tables.list_columns("account")
for col in columns:
print(f"{col['LogicalName']} ({col.get('AttributeType')})")

# List only specific properties
columns = client.tables.list_columns(
"account",
select=["LogicalName", "SchemaName", "AttributeType"],
filter="AttributeType eq 'String'",
)

# Clean up
client.tables.delete("new_Product")
```
Expand Down Expand Up @@ -485,6 +554,16 @@ rel = client.tables.get_relationship("new_Department_Employee")
if rel:
print(f"Found: {rel['SchemaName']}")

# List all relationships
rels = client.tables.list_relationships()
for rel in rels:
print(f"{rel['SchemaName']} ({rel.get('@odata.type')})")

# List relationships for a specific table (one-to-many + many-to-one + many-to-many)
account_rels = client.tables.list_table_relationships("account")
for rel in account_rels:
print(f"{rel['SchemaName']} -> {rel.get('@odata.type')}")

# Delete a relationship
client.tables.delete_relationship(result['relationship_id'])
```
Expand Down
17 changes: 17 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,19 @@ Deep-dive into production-ready patterns and specialized functionality:
- Column metadata management and multi-language support
- Interactive cleanup and best practices

- **`sql_examples.py`** - **SQL QUERY END-TO-END** 🔍
- Schema discovery before writing SQL (list_columns, list_relationships)
- Full SQL capabilities: SELECT, WHERE, TOP, ORDER BY, LIKE, IN, BETWEEN
- JOINs (INNER, LEFT, multi-table), GROUP BY, DISTINCT, aggregates
- OFFSET FETCH for server-side pagination
- SELECT * auto-expansion (SDK rewrites for server compatibility)
- Polymorphic lookups via SQL (ownerid, customerid, createdby)
- SQL read -> DataFrame transform -> SDK write-back (full round-trip)
- SQL-driven bulk create, update, and delete patterns
- SQL to DataFrame via `client.dataframe.sql()`
- Limitations with SDK fallbacks (writes, subqueries, functions)
- Complete reference table: SQL vs SDK method mapping

- **`file_upload.py`** - **FILE OPERATIONS** 📎
- File upload to Dataverse file columns with chunking
- Advanced file handling patterns
Expand Down Expand Up @@ -68,13 +81,17 @@ python examples/basic/functional_testing.py
```bash
# Comprehensive walkthrough with production patterns
python examples/advanced/walkthrough.py

# SQL queries end-to-end with SDK fallbacks for unsupported operations
python examples/advanced/sql_examples.py
```

## 🎯 Quick Start Recommendations

- **New to the SDK?** → Start with `examples/basic/installation_example.py`
- **Need to test/validate?** → Use `examples/basic/functional_testing.py`
- **Want to see all features?** → Run `examples/advanced/walkthrough.py`
- **Using SQL queries?** → Run `examples/advanced/sql_examples.py`
- **Building production apps?** → Study patterns in `examples/advanced/walkthrough.py`

## 📋 Prerequisites
Expand Down
Loading
Loading