feat: add the viewer's roles to a data table connection that names none

The default role of a data table under roles is now a floor: a connection
naming no role also holds the roles its viewer is a tenant of, unless the
default is admin. This replaces the explicit as=viewer option, which is
removed from the reference parser, the executors and both clients. The roles
drawer warns while the default is admin and confirms before removing the
default role.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Ruben Fiszel
2026-10-13 09:27:24 +02:00
co-authored by Claude Opus 5.5
parent 949ce49365
commit 85e73ae4f1
24 changed files with 175 additions and 289 deletions
+6 -31
View File
@@ -1456,9 +1456,7 @@ class Windmill:
},
)
def datatable(
self, name: str = "main", *, role: Optional[str] = None, as_viewer: bool = False
):
def datatable(self, name: str = "main", *, role: Optional[str] = None):
"""Get a DataTable client for SQL queries.
Args:
@@ -1466,14 +1464,11 @@ class Windmill:
role: Connect as this data table role instead of the data table's default one.
Only meaningful on a data table under roles, and only for a role you are a
tenant of.
as_viewer: Connect with every role of the job's viewer at once: the user of the
app the job runs for, or whoever ran the script. The server resolves who that
is. Cannot be combined with role.
Returns:
DataTableClient instance
"""
return DataTableClient(self, name, role=role, as_viewer=as_viewer)
return DataTableClient(self, name, role=role)
def ducklake(self, name: str = "main"):
"""Get a DuckLake client for DuckDB queries.
@@ -2330,21 +2325,17 @@ def username_to_email(username: str) -> str:
@init_global_client
def datatable(
name: str = "main", *, role: Optional[str] = None, as_viewer: bool = False
) -> DataTableClient:
def datatable(name: str = "main", *, role: Optional[str] = None) -> DataTableClient:
"""Get a DataTable client for SQL queries.
Args:
name: Database name (default: "main")
role: Connect as this data table role instead of the data table's default one.
as_viewer: Connect with every role of the job's viewer at once: the user of the app
the job runs for, or whoever ran the script. Cannot be combined with role.
Returns:
DataTableClient instance
"""
return _client.datatable(name, role=role, as_viewer=as_viewer)
return _client.datatable(name, role=role)
@init_global_client
def ducklake(name: str = "main") -> DucklakeClient:
@@ -2427,30 +2418,20 @@ _ROLE_NAME_RE = re.compile(r"^[A-Za-z0-9_-]{1,63}$")
class DataTableClient:
"""Client for executing SQL queries against Windmill DataTables."""
def __init__(
self,
client: Windmill,
name: str,
role: Optional[str] = None,
as_viewer: bool = False,
):
def __init__(self, client: Windmill, name: str, role: Optional[str] = None):
"""Initialize DataTableClient.
Args:
client: Windmill client instance
name: DataTable name
role: Data table role to connect as, or None for the data table's default
as_viewer: Connect with every role of the job's viewer instead of one role
"""
if role is not None and not _ROLE_NAME_RE.match(role):
raise ValueError(
f"Invalid data table role '{role}': only letters, digits, '_' and '-' are allowed"
)
if as_viewer and role is not None:
raise ValueError("A data table is used as a role or as the viewer, not both")
self.client = client
self.role = role
self.as_viewer = as_viewer
self.name, self.schema = parse_sql_client_name(name)
def query(self, sql: str, *args) -> SqlQuery:
"""Execute a SQL query against the DataTable.
@@ -2479,13 +2460,7 @@ class DataTableClient:
lambda sql: self.client.run_inline_script_preview(
content=sql,
language="postgresql",
# In the reference rather than an annotation: a worker that does not know `?as=`
# refuses the reference, where it would skip an annotation and connect as the
# default role.
args={
"database": f"datatable://{self.name}{'?as=viewer' if self.as_viewer else ''}",
**args_dict,
},
args={"database": f"datatable://{self.name}", **args_dict},
)
)