Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
@staticmethod
def generate_qualified_name(
*, dataset_qualified_name: str, question_text: str
) -> str:
"""
Derive the deterministic qualifiedName for a SqlInsightBusinessQuestion,
identical to the SQL-Intelligence miner's formula — so a human-confirmed
question and a later mined observation of the same question converge on
one entity instead of duplicating:

``dataset_qn || '/question/' || md5(question_text)``

The hash is over the question TEXT alone, so rewording a question makes a
new entity rather than updating the old one.

:param dataset_qualified_name: unique name of the dataset the question is about
:param question_text: the business question, verbatim
:returns: the deterministic qualifiedName for the business-question entity
"""
digest = hashlib.md5( # noqa: S324 (miner-compatible identity, not crypto)
question_text.encode()
).hexdigest()
return f"{dataset_qualified_name}/question/{digest}"

@classmethod
@init_guid
def creator(
cls,
*,
dataset: SQL,
question_text: str,
canonical_sql: Optional[str] = None,
name: Optional[str] = None,
) -> SqlInsightBusinessQuestion:
"""
Create a SqlInsightBusinessQuestion against a SQL dataset, carrying both
the string qualified-name attribute and the dataset relationship edge —
plus a deterministic, miner-identical qualifiedName so repeated
confirmation or a later mined observation converges on the same entity.

:param dataset: the dataset the question is about, e.g.
``Table.ref_by_qualified_name(...)`` or a search result — must carry
its real type (Table / View / MaterialisedView) and qualifiedName
:param question_text: the business question, verbatim. The qualifiedName is
derived from this, so rewording it creates a NEW entity
:param canonical_sql: optional SQL that answers the question
:param name: optional display name (defaults to the question text)
:returns: the minimal request to create the SqlInsightBusinessQuestion
"""
attributes = SqlInsightBusinessQuestion.Attributes.creator(
dataset=dataset,
question_text=question_text,
canonical_sql=canonical_sql,
name=name,
)
return cls(attributes=attributes)
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
@staticmethod
def generate_qualified_name(*, column_qualified_name: str, operator: str) -> str:
"""
Derive the deterministic qualifiedName for a SqlInsightFilter, identical to
the SQL-Intelligence miner's formula — so a human-confirmed filter and a
later mined observation of the same filter converge on one entity instead
of duplicating:

``column_qn || '/filter/' || md5(operator)``

Note this hangs off the COLUMN, not the dataset: filters sit one level
deeper than joins and business questions.

:param column_qualified_name: unique name of the column being filtered
:param operator: the filter operator, e.g. ``IN``, ``=``, ``BETWEEN``
:returns: the deterministic qualifiedName for the filter entity
"""
digest = hashlib.md5( # noqa: S324 (miner-compatible identity, not crypto)
operator.encode()
).hexdigest()
return f"{column_qualified_name}/filter/{digest}"

@classmethod
@init_guid
def creator(
cls,
*,
column: Column,
operator: str,
predicate_sql: Optional[str] = None,
when_to_use: Optional[str] = None,
common_values: Optional[Set[str]] = None,
name: Optional[str] = None,
) -> SqlInsightFilter:
"""
Create a SqlInsightFilter on a column, carrying both the string
qualified-name attributes (the dataset attribute is how the asset page's
Usage & Intelligence tab finds filters) and the column relationship edge —
plus a deterministic, miner-identical qualifiedName so repeated
confirmation or a later mined observation converges on the same entity.

:param column: the column being filtered, e.g.
``Column.ref_by_qualified_name(...)`` or a search result — must carry
its qualifiedName
:param operator: the filter operator, e.g. ``IN``, ``=``, ``BETWEEN``.
The qualifiedName is derived from this, so two filters on the same
column with the same operator are ONE entity
:param predicate_sql: optional SQL predicate the filter applies
:param when_to_use: optional guidance on when this filter should be used
:param common_values: optional set of values commonly filtered on
:param name: optional display name (defaults to "<COLUMN> <OPERATOR>")
:returns: the minimal request to create the SqlInsightFilter
"""
attributes = SqlInsightFilter.Attributes.creator(
column=column,
operator=operator,
predicate_sql=predicate_sql,
when_to_use=when_to_use,
common_values=common_values,
name=name,
)
return cls(attributes=attributes)
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
@classmethod
@init_guid
def creator(
cls,
*,
dataset: SQL,
question_text: str,
canonical_sql: Optional[str] = None,
name: Optional[str] = None,
) -> SqlInsightBusinessQuestion.Attributes:
validate_required_fields(
["dataset", "question_text"], [dataset, question_text]
)
dataset_qualified_name = dataset.qualified_name
validate_required_fields(
["dataset.qualified_name"], [dataset_qualified_name]
)
return SqlInsightBusinessQuestion.Attributes(
name=name or question_text,
qualified_name=SqlInsightBusinessQuestion.generate_qualified_name(
dataset_qualified_name=dataset_qualified_name, # type: ignore[arg-type]
question_text=question_text,
),
sql_insight_business_question_dataset_qualified_name=dataset_qualified_name,
sql_insight_business_question_text=question_text,
sql_insight_business_question_canonical_s_q_l=canonical_sql,
# A human-declared question has no observed usage: never claim any.
sql_insight_business_question_query_count=0,
sql_insight_business_question_unique_users=0,
sql_insight_dataset=dataset,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
@classmethod
@init_guid
def creator(
cls,
*,
column: Column,
operator: str,
predicate_sql: Optional[str] = None,
when_to_use: Optional[str] = None,
common_values: Optional[Set[str]] = None,
name: Optional[str] = None,
) -> SqlInsightFilter.Attributes:
validate_required_fields(["column", "operator"], [column, operator])
column_qualified_name = column.qualified_name
validate_required_fields(
["column.qualified_name"], [column_qualified_name]
)
# A filter's qualifiedName is <columnQN>/filter/md5(operator), and a
# column's own qualifiedName is <datasetQN>/<column>. So the dataset is
# the column's parent, never a separate input that could disagree with it.
dataset_qualified_name = column_qualified_name.rsplit("/", 1)[0] # type: ignore[union-attr]
return SqlInsightFilter.Attributes(
name=name or f"{column_qualified_name.rsplit('/', 1)[-1]} {operator}", # type: ignore[union-attr]
qualified_name=SqlInsightFilter.generate_qualified_name(
column_qualified_name=column_qualified_name, # type: ignore[arg-type]
operator=operator,
),
# Both are load-bearing: the Usage & Intelligence tab finds filters by
# the dataset ATTRIBUTE, while the column RELATIONSHIP is what renders
# the row on the column itself. A filter missing either is half-visible.
sql_insight_filter_dataset_qualified_name=dataset_qualified_name,
sql_insight_filter_column_qualified_name=column_qualified_name,
sql_insight_filter_operator=operator,
sql_insight_filter_predicate_s_q_l=predicate_sql,
sql_insight_filter_when_to_use=when_to_use,
sql_insight_filter_common_values=common_values,
# A human-declared filter has no observed usage: never claim any.
sql_insight_filter_query_count=0,
sql_insight_filter_unique_users=0,
sql_insight_column=column,
)
91 changes: 91 additions & 0 deletions pyatlan/model/assets/core/sql_insight_business_question.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,79 @@

from __future__ import annotations

import hashlib
from datetime import datetime
from typing import ClassVar, List, Optional

from pydantic.v1 import Field, validator

from pyatlan.model.fields.atlan_fields import KeywordField, NumericField, RelationField
from pyatlan.model.structs import PopularityInsights
from pyatlan.utils import init_guid, validate_required_fields

from .sql_insight import SqlInsight


class SqlInsightBusinessQuestion(SqlInsight):
"""Description"""

@staticmethod
def generate_qualified_name(
*, dataset_qualified_name: str, question_text: str
) -> str:
"""
Derive the deterministic qualifiedName for a SqlInsightBusinessQuestion,
identical to the SQL-Intelligence miner's formula — so a human-confirmed
question and a later mined observation of the same question converge on
one entity instead of duplicating:

``dataset_qn || '/question/' || md5(question_text)``

The hash is over the question TEXT alone, so rewording a question makes a
new entity rather than updating the old one.

:param dataset_qualified_name: unique name of the dataset the question is about
:param question_text: the business question, verbatim
:returns: the deterministic qualifiedName for the business-question entity
"""
digest = hashlib.md5( # noqa: S324 (miner-compatible identity, not crypto)
question_text.encode()
).hexdigest()
return f"{dataset_qualified_name}/question/{digest}"

@classmethod
@init_guid
def creator(
cls,
*,
dataset: SQL,
question_text: str,
canonical_sql: Optional[str] = None,
name: Optional[str] = None,
) -> SqlInsightBusinessQuestion:
"""
Create a SqlInsightBusinessQuestion against a SQL dataset, carrying both
the string qualified-name attribute and the dataset relationship edge —
plus a deterministic, miner-identical qualifiedName so repeated
confirmation or a later mined observation converges on the same entity.

:param dataset: the dataset the question is about, e.g.
``Table.ref_by_qualified_name(...)`` or a search result — must carry
its real type (Table / View / MaterialisedView) and qualifiedName
:param question_text: the business question, verbatim. The qualifiedName is
derived from this, so rewording it creates a NEW entity
:param canonical_sql: optional SQL that answers the question
:param name: optional display name (defaults to the question text)
:returns: the minimal request to create the SqlInsightBusinessQuestion
"""
attributes = SqlInsightBusinessQuestion.Attributes.creator(
dataset=dataset,
question_text=question_text,
canonical_sql=canonical_sql,
name=name,
)
return cls(attributes=attributes)

type_name: str = Field(default="SqlInsightBusinessQuestion", allow_mutation=False)

@validator("type_name")
Expand Down Expand Up @@ -264,6 +323,38 @@ class Attributes(SqlInsight.Attributes):
default=None, description=""
) # relationship

@classmethod
@init_guid
def creator(
cls,
*,
dataset: SQL,
question_text: str,
canonical_sql: Optional[str] = None,
name: Optional[str] = None,
) -> SqlInsightBusinessQuestion.Attributes:
validate_required_fields(
["dataset", "question_text"], [dataset, question_text]
)
dataset_qualified_name = dataset.qualified_name
validate_required_fields(
["dataset.qualified_name"], [dataset_qualified_name]
)
return SqlInsightBusinessQuestion.Attributes(
name=name or question_text,
qualified_name=SqlInsightBusinessQuestion.generate_qualified_name(
dataset_qualified_name=dataset_qualified_name, # type: ignore[arg-type]
question_text=question_text,
),
sql_insight_business_question_dataset_qualified_name=dataset_qualified_name,
sql_insight_business_question_text=question_text,
sql_insight_business_question_canonical_s_q_l=canonical_sql,
# A human-declared question has no observed usage: never claim any.
sql_insight_business_question_query_count=0,
sql_insight_business_question_unique_users=0,
sql_insight_dataset=dataset,
)

attributes: SqlInsightBusinessQuestion.Attributes = Field(
default_factory=lambda: SqlInsightBusinessQuestion.Attributes(),
description=(
Expand Down
Loading
Loading