feat: connect to a data table with every role of the job's viewer

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Ruben Fiszel
2026-10-12 20:39:46 +02:00
co-authored by Claude Opus 5.5
parent 9fa9b77926
commit 87c87facd2
30 changed files with 462 additions and 34 deletions
+21 -2
View File
@@ -1220,6 +1220,8 @@ parseS3Object(s3Object: S3Object): S3ObjectRecord
* @param name - Database/datatable name (default: "main")
* @param opts.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.
* @param opts.as - \`"viewer"\` connects with every role of the job's viewer at once, so row-level
* security policies see the user of the app rather than its publisher.
* @returns SQL template function for building parameterized queries
* @example
* let sql = wmill.datatable()
@@ -1232,6 +1234,9 @@ parseS3Object(s3Object: S3Object): S3ObjectRecord
* @example
* // Read through a restricted role
* let sql = wmill.datatable("main", { role: "analytics" })
* @example
* // In an app's backend runnable: read what the app's viewer may read
* let sql = wmill.datatable("main", { as: "viewer" })
*/
datatable(name: string = "main", opts?: DatatableOptions): DatatableSqlTemplateFunction
@@ -2063,6 +2068,8 @@ parseS3Object(s3Object: S3Object): S3ObjectRecord
* @param name - Database/datatable name (default: "main")
* @param opts.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.
* @param opts.as - \`"viewer"\` connects with every role of the job's viewer at once, so row-level
* security policies see the user of the app rather than its publisher.
* @returns SQL template function for building parameterized queries
* @example
* let sql = wmill.datatable()
@@ -2075,6 +2082,9 @@ parseS3Object(s3Object: S3Object): S3ObjectRecord
* @example
* // Read through a restricted role
* let sql = wmill.datatable("main", { role: "analytics" })
* @example
* // In an app's backend runnable: read what the app's viewer may read
* let sql = wmill.datatable("main", { as: "viewer" })
*/
datatable(name: string = "main", opts?: DatatableOptions): DatatableSqlTemplateFunction
@@ -3012,6 +3022,8 @@ parseS3Object(s3Object: S3Object): S3ObjectRecord
* @param name - Database/datatable name (default: "main")
* @param opts.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.
* @param opts.as - \`"viewer"\` connects with every role of the job's viewer at once, so row-level
* security policies see the user of the app rather than its publisher.
* @returns SQL template function for building parameterized queries
* @example
* let sql = wmill.datatable()
@@ -3024,6 +3036,9 @@ parseS3Object(s3Object: S3Object): S3ObjectRecord
* @example
* // Read through a restricted role
* let sql = wmill.datatable("main", { role: "analytics" })
* @example
* // In an app's backend runnable: read what the app's viewer may read
* let sql = wmill.datatable("main", { as: "viewer" })
*/
datatable(name: string = "main", opts?: DatatableOptions): DatatableSqlTemplateFunction
@@ -4892,10 +4907,13 @@ def send_teams_message(conversation_id: str, text: str, success: bool = True, ca
# 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
def datatable(name: str = 'main', *, role: Optional[str] = None)
def datatable(name: str = 'main', *, role: Optional[str] = None, as_viewer: bool = False)
# Get a DuckLake client for DuckDB queries.
#
@@ -6522,7 +6540,7 @@ data:
- \`<datatable>/<table>\` — Specific table in public schema
- \`<datatable>/<schema>:<table>\` — Table in specific schema
**Roles:** when a datatable is under roles, its queries run as a role, which only reaches what it was granted. \`roles\` records the role the app uses each datatable through; the app's code must pass the same role: \`wmill.datatable('main', { role: 'analyst' })\` in TypeScript, \`wmill.datatable('main', role='analyst')\` in Python. A datatable without an entry is used as its default role.
**Roles:** when a datatable is under roles, its queries run as a role, which only reaches what it was granted. \`roles\` records the role the app uses each datatable through; the app's code must pass the same role: \`wmill.datatable('main', { role: 'analyst' })\` in TypeScript, \`wmill.datatable('main', role='analyst')\` in Python. A datatable without an entry is used as its default role. To use a datatable with the roles of whoever is viewing the app rather than one fixed role, pass \`{ as: 'viewer' }\` (\`as_viewer=True\` in Python) instead of a role.
## SQL Migrations (sql_to_apply/)
@@ -6816,6 +6834,7 @@ Data tables are PostgreSQL databases managed by Windmill. Backend runnables quer
3. **No DDL inside runnables**: runnables only read and write rows (SELECT, INSERT, UPDATE, DELETE) on existing tables. Never CREATE, ALTER or DROP a table from a runnable.
4. **Qualify table names**: an unqualified name means the \`public\` schema, so write every other table as \`schema.table\`, in table creation and queries alike. The app's \`data\` config sets the default datatable and schema its tables go in; use them consistently across runnables.
5. **Pass the role**: when the app's \`data.roles\` gives a data table a role, every \`wmill.datatable\` call on it passes that role (\`wmill.datatable('main', { role: 'analyst' })\` in TypeScript, \`wmill.datatable('main', role='analyst')\` in Python). The role only reaches what it was granted, so a query outside it fails with \`permission denied\`.
6. **Per-viewer rows**: a runnable runs as the app's publisher, whoever is using the app. To have a data table under roles answer for the viewer instead, connect as the viewer: \`wmill.datatable('main', { as: 'viewer' })\` in TypeScript, \`wmill.datatable('main', as_viewer=True)\` in Python, \`datatable://main?as=viewer\` as a SQL runnable's database or in a DuckDB \`ATTACH\`. The connection then holds every role the viewer is a tenant of, so row-level security policies written as \`pg_has_role(current_user, '<role>', 'member')\` return that viewer's rows. The server works out who the viewer is; a viewer with no role on the data table, or an anonymous one, is refused. Never combine it with \`role\`.
\`wmill datatable list\` lists the workspace's data tables. Create or change tables with a migration in \`sql_to_apply/\` (see "SQL Migrations" above), then add them to \`data.tables\` in \`raw_app.yaml\`.