Skip to content
Open
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,8 @@ If this returns `{"job_id": "...", "state": "COMPLETED", "rowCount": 1, "rows":
| Group | Commands | What it does |
|-------|----------|--------------|
| `dremio query` | `run`, `status`, `cancel` | Execute SQL, check job status, cancel running jobs |
| `dremio folder` | `list`, `get`, `create`, `delete`, `grants` | Browse and manage spaces/folders, view ACLs |
| `dremio space` | `list`, `get`, `create`, `delete` | Manage top-level spaces in the catalog |
| `dremio folder` | `list`, `get`, `create`, `delete`, `grants` | Browse top-level catalog entities and manage nested folders, view ACLs |
| `dremio schema` | `describe`, `lineage`, `sample` | Column types, dependency graph, preview rows |
| `dremio wiki` | `get`, `update` | Read and update wiki documentation on entities |
| `dremio tag` | `get`, `update` | Read and update tags on entities |
Expand Down Expand Up @@ -214,7 +215,8 @@ Every Dremio object has consistent CLI commands using standard CRUD verbs (`list

| Object | List | Get | Create | Update | Delete |
|--------|------|-----|--------|--------|--------|
| **Spaces/Folders** | `folder list` | `folder get` | `folder create` | — | `folder delete` |
| **Spaces** | `space list` | `space get` | `space create` | — | `space delete` |
| **Folders** | — | `folder get` | `folder create` | — | `folder delete` |
| **Tables/Views** | `folder get` | `schema describe/sample` | `query run` (DDL) | `query run` (DDL) | `folder delete` |
| **Wiki** | — | `wiki get` | `wiki update` | `wiki update` | — |
| **Tags** | — | `tag get` | `tag update` | `tag update` | — |
Expand All @@ -226,7 +228,7 @@ Every Dremio object has consistent CLI commands using standard CRUD verbs (`list
| **Projects** | `project list` | `project get` | `project create` | `project update` | `project delete` |
| **Jobs** | `job list` | `job get/profile` | `query run` | — | `query cancel` |

`folder create` uses SQL under the hood (`CREATE SPACE` for top-level, `CREATE FOLDER` for nested paths). All other mutations use the REST API.
`space create` uses SQL (`CREATE SPACE`) for top-level space creation. `folder create` uses `CREATE FOLDER` for all paths; single-component paths are deprecated and may fail on Space-Plugin-enabled clusters — use `dremio space create` instead. All other mutations use the REST API.

## How it works

Expand Down Expand Up @@ -360,6 +362,7 @@ src/drs/
introspect.py # Command schema registry for dremio describe
commands/
query.py # run, status, cancel
space.py # list, get, create, delete
folder.py # list, get, create, delete, grants
schema.py # describe, lineage, sample
wiki.py # get, update
Expand Down
2 changes: 2 additions & 0 deletions src/drs/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
role,
schema,
setup,
space,
tag,
user,
wiki,
Expand Down Expand Up @@ -69,6 +70,7 @@
app.add_typer(grant.app, name="grant")
app.add_typer(project.app, name="project")
app.add_typer(chat.app, name="chat")
app.add_typer(space.app, name="space")
app.command("setup")(setup.setup_command)

# Global state for config
Expand Down
64 changes: 47 additions & 17 deletions src/drs/commands/folder.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""dremio folder — manage spaces and folders in the Dremio catalog."""
"""dremio folder — manage nested folders and list top-level catalog entities."""

from __future__ import annotations

Expand All @@ -24,11 +24,12 @@

from drs.client import DremioClient
from drs.commands.query import run_query
from drs.output import OutputFormat, error, output
from drs.output import OutputFormat, error, output, warn
from drs.utils import handle_api_error, parse_path, quote_path_sql

app = typer.Typer(
help="Manage spaces and folders in the Dremio catalog.", context_settings={"help_option_names": ["-h", "--help"]}
help="Manage nested folders and list top-level catalog entities. Use `dremio space` for top-level spaces.",
context_settings={"help_option_names": ["-h", "--help"]},
)


Expand All @@ -52,14 +53,21 @@ async def get_entity(client: DremioClient, path: str) -> dict:


async def create_folder(client: DremioClient, path: str) -> dict:
"""Create a space (single component) or folder (nested path) using SQL."""
"""Create a folder at the given path using SQL."""
from drs.utils import DremioAPIError

parts = parse_path(path)
if len(parts) == 1:
sql = f'CREATE SPACE "{parts[0]}"'
else:
quoted = quote_path_sql(path)
sql = f"CREATE FOLDER {quoted}"
return await run_query(client, sql)
warn(
f"Top-level folder creation is deprecated. "
f"Use `dremio space create {parts[0]}` instead. "
"On Space-Plugin-enabled clusters this will fail."
)
quoted = quote_path_sql(path)
result = await run_query(client, f"CREATE FOLDER {quoted}")
if result.get("state") == "FAILED":
raise DremioAPIError(0, result.get("error", ""))
return result


async def delete_entity(client: DremioClient, path: str) -> dict:
Expand All @@ -77,6 +85,22 @@ async def delete_entity(client: DremioClient, path: str) -> dict:
raise handle_api_error(exc) from exc


async def get_folder(client: DremioClient, path: str) -> dict:
"""Get a folder by path; rejects top-level (single-component) paths."""
parts = parse_path(path)
if len(parts) == 1:
raise ValueError(f"'{parts[0]}' is a top-level space. Use `dremio space get {parts[0]}` instead.")
return await get_entity(client, path)


async def delete_folder(client: DremioClient, path: str) -> dict:
"""Delete a folder by path; rejects top-level (single-component) paths."""
parts = parse_path(path)
if len(parts) == 1:
raise ValueError(f"'{parts[0]}' is a top-level space. Use `dremio space delete {parts[0]}` instead.")
return await delete_entity(client, path)


async def grants(client: DremioClient, path: str) -> dict:
"""Get ACL grants on a catalog entity."""
parts = parse_path(path)
Expand Down Expand Up @@ -141,20 +165,23 @@ def cli_get(
) -> None:
"""Get full metadata for a catalog entity by path."""
client = _get_client()
_run_command(get_entity(client, path), client, fmt, fields=fields)
_run_command(get_folder(client, path), client, fmt, fields=fields)


@app.command("create")
def cli_create(
path: str = typer.Argument(
help="Space name (single component) or dot-separated folder path (e.g., myspace.newfolder)"
help="Dot-separated folder path (e.g., myspace.newfolder). For top-level spaces use `dremio space create`."
),
fmt: OutputFormat = typer.Option(OutputFormat.json, "--output", "-o", help="Output format"),
) -> None:
"""Create a space or folder.
"""Create a folder at the given path.

Single path component (e.g., 'Analytics') creates a space.
Nested path (e.g., 'Analytics.reports') creates a folder.
Nested paths (e.g. 'Analytics.reports') create a folder inside a space.
Single-component paths attempt top-level creation for compatibility with
pre-Space-Plugin clusters; this is deprecated — use `dremio space create`
instead. On Space-Plugin-enabled clusters, single-component paths will
fail server-side.
"""
client = _get_client()
_run_command(create_folder(client, path), client, fmt)
Expand All @@ -166,12 +193,15 @@ def cli_delete(
dry_run: bool = typer.Option(False, "--dry-run", help="Show what would be deleted without deleting"),
fmt: OutputFormat = typer.Option(OutputFormat.json, "--output", "-o", help="Output format"),
) -> None:
"""Delete a catalog entity (space, folder, view, etc.). Cannot be undone."""
"""Delete a nested catalog entity by path. Cannot be undone.

Single-component paths (top-level spaces) are rejected — use `dremio space delete` instead.
"""
client = _get_client()
if dry_run:
_run_command(get_entity(client, path), client, fmt)
_run_command(get_folder(client, path), client, fmt)
return
_run_command(delete_entity(client, path), client, fmt)
_run_command(delete_folder(client, path), client, fmt)


@app.command("grants")
Expand Down
156 changes: 156 additions & 0 deletions src/drs/commands/space.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
#
# Copyright (C) 2017-2026 Dremio Corporation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""dremio space — manage spaces in the Dremio catalog."""

from __future__ import annotations

import asyncio

import typer

from drs.client import DremioClient
from drs.commands.folder import delete_entity, get_entity, list_catalog
from drs.commands.query import run_query
from drs.output import OutputFormat, error, output
from drs.utils import DremioAPIError, parse_path

app = typer.Typer(help="Manage spaces in the Dremio catalog.", context_settings={"help_option_names": ["-h", "--help"]})


async def list_spaces(client: DremioClient) -> dict:
"""List all spaces in the catalog."""
result = await list_catalog(client)
spaces = [e for e in result.get("entities", []) if e.get("containerType") == "SPACE"]
return {"entities": spaces}


_LEGACY_SPACE_NOT_SUPPORTED = "Legacy spaces are not supported"


async def create_space(client: DremioClient, name: str) -> dict:
"""Create a space.

Uses CREATE SPACE SQL. On pre-Space-Plugin clusters that reject it with
"Legacy spaces are not supported", falls back to CREATE FOLDER with a
single-component path. All other failures are propagated as DremioAPIError.
"""
result = await run_query(client, f'CREATE SPACE "{name}"')
if result.get("state") == "FAILED":
err = result.get("error", "")
if _LEGACY_SPACE_NOT_SUPPORTED in err:
fallback = await run_query(client, f'CREATE FOLDER "{name}"')
if fallback.get("state") == "FAILED":
fallback_err = fallback.get("error", "")
if "already exists" in fallback_err:
raise DremioAPIError(0, f"Space [{name}] already exists.")
raise DremioAPIError(0, fallback_err)
return fallback
raise DremioAPIError(0, err)
return result


async def get_space(client: DremioClient, name: str) -> dict:
"""Get space metadata by name."""
if len(parse_path(name)) > 1:
raise ValueError(f"'{name}' is a nested path. Use `dremio folder get {name}` for folders.")
return await get_entity(client, name)


async def delete_space(client: DremioClient, name: str) -> dict:
"""Delete a space by name."""
if len(parse_path(name)) > 1:
raise ValueError(f"'{name}' is a nested path. Use `dremio folder delete {name}` for folders.")
entity = await get_entity(client, name)
if entity.get("containerType") != "SPACE":
kind = entity.get("containerType", "entity").lower()
raise ValueError(f"'{name}' is a {kind}, not a space.")
return await delete_entity(client, name)
Comment thread
sandhyasun marked this conversation as resolved.


# -- CLI wrappers --


def _get_client() -> DremioClient:
from drs.cli import get_client

return get_client()


def _run_command(coro, client, fmt: OutputFormat = OutputFormat.json, fields: str | None = None) -> None:
async def _execute():
try:
return await coro
finally:
await client.close()

try:
result = asyncio.run(_execute())
except Exception as exc:
from drs.utils import DremioAPIError

if isinstance(exc, DremioAPIError):
error(str(exc))
raise typer.Exit(1)
if isinstance(exc, ValueError):
error(str(exc))
raise typer.Exit(1)
raise
output(result, fmt, fields=fields)


@app.command("list")
def cli_list(
fmt: OutputFormat = typer.Option(OutputFormat.json, "--output", "-o", help="Output format"),
fields: str = typer.Option(None, "--fields", "-f", help="Comma-separated fields to include"),
) -> None:
"""List all spaces in the catalog."""
client = _get_client()
_run_command(list_spaces(client), client, fmt, fields=fields)


@app.command("get")
def cli_get(
name: str = typer.Argument(help="Space name"),
fmt: OutputFormat = typer.Option(OutputFormat.json, "--output", "-o", help="Output format"),
fields: str = typer.Option(None, "--fields", "-f", help="Comma-separated fields to include"),
) -> None:
"""Get metadata for a space by name."""
client = _get_client()
_run_command(get_space(client, name), client, fmt, fields=fields)


@app.command("create")
def cli_create(
name: str = typer.Argument(help="Space name to create"),
fmt: OutputFormat = typer.Option(OutputFormat.json, "--output", "-o", help="Output format"),
) -> None:
"""Create a new space."""
client = _get_client()
_run_command(create_space(client, name), client, fmt)


@app.command("delete")
def cli_delete(
name: str = typer.Argument(help="Space name to delete"),
dry_run: bool = typer.Option(False, "--dry-run", help="Show what would be deleted without deleting"),
fmt: OutputFormat = typer.Option(OutputFormat.json, "--output", "-o", help="Output format"),
) -> None:
"""Delete a space. Cannot be undone."""
client = _get_client()
if dry_run:
_run_command(get_space(client, name), client, fmt)
return
_run_command(delete_space(client, name), client, fmt)
Loading