Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
56 changes: 56 additions & 0 deletions posthog/hogql/database/schema/system.py
Original file line number Diff line number Diff line change
Expand Up @@ -2188,6 +2188,61 @@ def ticket_assignment_join(join_to_add: LazyJoinToAdd, context: HogQLContext, no
)


canvases: PostgresTable = PostgresTable(
name="canvases",
postgres_table_name="posthog_canvas",
access_scope="canvas",
# Mirror the REST API's default filter: soft-deleted canvases are not exposed.
predicates=[parse_expr("deleted != true")],
Comment thread
k11kirky marked this conversation as resolved.
Comment thread
k11kirky marked this conversation as resolved.
description="Canvases (agent-built sandboxed browser apps, filed into channels); one row per canvas (soft-deleted canvases are excluded).",
fields={
"id": StringDatabaseField(name="id", description="Canvas UUID."),
"team_id": IntegerDatabaseField(name="team_id"),
"channel_id": StringDatabaseField(
name="channel_id", description="Channel the canvas is filed into (tasks Channel UUID)."
),
"name": StringDatabaseField(name="name", description="Canvas name."),
"template_id": StringDatabaseField(
name="template_id", description="Template the canvas was created from, e.g. 'freeform'."
),
"context": StringDatabaseField(
name="context", description="Author-written context (markdown) passed to generation tasks."
),
"generation_task_id": StringDatabaseField(
name="generation_task_id",
nullable=True,
description="Task currently generating this canvas; joins to tasks.id.",
),
"pinned_at": DateTimeDatabaseField(
name="pinned_at",
nullable=True,
description="When the canvas was pinned to its channel; NULL if not pinned.",
),
"current_source_version_id": StringDatabaseField(
name="current_source_version_id",
nullable=True,
description="The canvas's head source version; NULL until the first publish.",
),
"published_build_id": StringDatabaseField(
name="published_build_id",
nullable=True,
description="Build whose artifact the canvas app currently renders; NULL until the first successful build.",
),
"created_by_id": IntegerDatabaseField(
name="created_by_id", nullable=True, description="User who created the canvas."
),
"_deleted": BooleanDatabaseField(name="deleted", hidden=True),
"deleted": ExpressionField(
name="deleted",
expr=ast.Call(name="toInt", args=[ast.Field(chain=["_deleted"])]),
description="1 if the canvas has been deleted, 0 otherwise (always 0 here due to the table filter).",
),
"created_at": DateTimeDatabaseField(name="created_at", description="When the canvas was created."),
"updated_at": DateTimeDatabaseField(name="updated_at", description="When the canvas was last updated."),
},
)


tags: PostgresTable = PostgresTable(
name="tags",
postgres_table_name="posthog_tag",
Expand Down Expand Up @@ -2229,6 +2284,7 @@ class SystemTables(TableNode):
name="business_knowledge_documents", table=business_knowledge_documents
),
"business_knowledge_sources": TableNode(name="business_knowledge_sources", table=business_knowledge_sources),
"canvases": TableNode(name="canvases", table=canvases),
"cohort_calculation_history": TableNode(name="cohort_calculation_history", table=cohort_calculation_history),
"cohorts": TableNode(name="cohorts", table=cohorts),
"custom_property_definitions": TableNode(name="custom_property_definitions", table=custom_property_definitions),
Expand Down
43 changes: 43 additions & 0 deletions posthog/hogql/database/schema/test/test_system_tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,15 @@ def _create_task(team: Team, label: str):
)


def _create_canvas(team: Team, label: str):
Channel = apps.get_model("tasks", "Channel")
Canvas = apps.get_model("canvas", "Canvas")

with team_scope(team.pk):
channel = Channel.objects.create(team=team, name=f"channel_for_canvas_{label}")
return Canvas.objects.create(team=team, channel=channel, name=f"canvas_{label}")


def _create_task_run(team: Team, label: str):
Task = apps.get_model("tasks", "Task")
TaskRun = apps.get_model("tasks", "TaskRun")
Expand Down Expand Up @@ -681,6 +690,7 @@ def _create_business_knowledge_chunk(team: Team, label: str):
("business_knowledge_chunks", _create_business_knowledge_chunk),
("business_knowledge_documents", _create_business_knowledge_document),
("business_knowledge_sources", _create_business_knowledge_source),
("canvases", _create_canvas),
("cohorts", _create_cohort),
("cohort_calculation_history", _create_cohort_calculation_history),
("custom_property_definitions", _create_custom_property_definition),
Expand Down Expand Up @@ -820,6 +830,39 @@ def test_internal_environments_excluded(self):
assert str(internal_env.pk) not in ids


class TestSystemTablesCanvasDeletedExclusion(BaseTest):
"""Verify the canvases system table excludes soft-deleted canvases,
mirroring the REST API's default filter."""

def test_generated_sql_includes_deleted_predicate(self):
db = Database.create_for(team=self.team, user=self.user)
context = HogQLContext(team_id=self.team.pk, enable_select_queries=True, database=db)
query, _ = prepare_and_print_ast(parse_select("SELECT id FROM system.canvases"), context, dialect="clickhouse")
assert "system__canvases.deleted" in query
assert f"equals(system__canvases.team_id, {self.team.pk})" in query


class TestSystemTablesCanvasDeletedExclusionIsolation(NonAtomicBaseTest):
"""End-to-end check that soft-deleted canvases are never returned via HogQL."""

CLASS_DATA_LEVEL_SETUP = False

def test_deleted_canvases_excluded(self):
Channel = apps.get_model("tasks", "Channel")
Canvas = apps.get_model("canvas", "Canvas")

with team_scope(self.team.pk):
channel = Channel.objects.create(team=self.team, name="canvas-exclusion-channel")
live_canvas = Canvas.objects.create(team=self.team, channel=channel, name="live")
deleted_canvas = Canvas.objects.create(team=self.team, channel=channel, name="deleted", deleted=True)

response = execute_hogql_query("SELECT id FROM system.canvases", team=self.team, user=self.user)
ids = {str(row[0]) for row in response.results}

assert str(live_canvas.pk) in ids
assert str(deleted_canvas.pk) not in ids


class TestSystemTablesTaskInternalExclusion(BaseTest):
"""Verify the tasks system table excludes internal tasks (signals pipeline, etc.)
mirroring the REST API's default filter."""
Expand Down
Loading
Loading