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
2 changes: 1 addition & 1 deletion api/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ dependencies = [
"drf-writable-nested>=0.6.2,<0.7.0",
"django-filter>=2.4.0,<2.5.0",
"flagsmith-flag-engine>=10.1.0,<11.0.0",
"flagsmith-sql-flag-engine>=0.1.3,<0.2.0",
"flagsmith-sql-flag-engine>=0.2.0,<0.3.0",
"django-clickhouse-backend>=1.4,<2.0",
"clickhouse-driver",
"boto3>=1.35.95,<1.36.0",
Expand Down
19 changes: 16 additions & 3 deletions api/segment_membership/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@
from django.db.backends.utils import CursorWrapper
from django.db.models import Q
from flag_engine.context.types import EvaluationContext
from flagsmith_sql_flag_engine import TranslateContext, translate_segment
from flagsmith_sql_flag_engine import (
Binder,
PyformatParamStyle,
TranslateContext,
translate_segment,
)
from flagsmith_sql_flag_engine.dialects import ClickHouseDialect
from task_processor.models import Task

Expand Down Expand Up @@ -122,13 +127,15 @@ def compute_segment_counts_for_project(
return []

dialect = ClickHouseDialect()
binder = Binder(PyformatParamStyle())
select_clauses: list[str] = []
for seg in segments:
translate_ctx = TranslateContext(
evaluation_context=EvaluationContext(
environment={"key": "_count", "name": project.name}
),
dialect=dialect,
binder=binder,
)
predicate = translate_segment(
map_segment_to_segment_context(map_segment_to_engine(seg)),
Expand All @@ -155,7 +162,7 @@ def compute_segment_counts_for_project(
return []

sql = "\nUNION ALL\n".join(select_clauses)
cursor.execute(sql, {"env_keys": tuple(env_id_by_key)})
cursor.execute(sql, {"env_keys": tuple(env_id_by_key), **binder.params})
rows: list[tuple[Any, ...]] = cursor.fetchall()
membership_counts: list[SegmentMembershipCount] = []
for row in rows:
Expand Down Expand Up @@ -186,11 +193,13 @@ def get_segment_members_page(
Provide identifier as `cursor` to get a page after that identifier.
Provide `q` to filter to identifiers containing it (case-insensitive).
"""
binder = Binder(PyformatParamStyle())
translate_ctx = TranslateContext(
evaluation_context=EvaluationContext(
environment={"key": "_members", "name": segment.project.name}
),
dialect=ClickHouseDialect(),
binder=binder,
)
predicate = translate_segment(
map_segment_to_segment_context(map_segment_to_engine(segment)),
Expand All @@ -208,7 +217,11 @@ def get_segment_members_page(
"i.environment_id = %(env_key)s",
"i.is_deleted = false",
]
params: dict[str, Any] = {"env_key": environment.api_key, "limit": limit}
params: dict[str, Any] = {
"env_key": environment.api_key,
"limit": limit,
**binder.params,
}
if cursor:
conditions.append("i.identifier > %(cursor)s")
params["cursor"] = cursor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from common.test_tools import RunTasksFixture
from django.db import connections
from django.utils import timezone
from flag_engine.segments.constants import EQUAL
from flag_engine.segments.constants import EQUAL, REGEX
from pytest_django.fixtures import SettingsWrapper
from pytest_mock import MockerFixture
from task_processor.models import Task
Expand Down Expand Up @@ -191,6 +191,13 @@ def matching_segment(segment: Segment) -> Segment:
return segment


@pytest.fixture
def percent_regex_segment(segment: Segment) -> Segment:
rule = SegmentRule.objects.create(segment=segment, type=SegmentRule.ALL_RULE)
Condition.objects.create(rule=rule, property="foo", operator=REGEX, value="[b%]ar")
return segment


@pytest.mark.clickhouse
def test_get_segment_members_page__deleted_identity__excluded(
segment_membership_identities: None,
Expand Down Expand Up @@ -281,6 +288,37 @@ def test_compute_segment_counts_for_project__deleted_identity__excluded_from_cou
assert counts[0].count == 2


@pytest.mark.clickhouse
def test_get_segment_members_page__regex_with_percent__returns_matches(
segment_membership_identities: None,
percent_regex_segment: Segment,
environment: Environment,
) -> None:
# Given / When
members = get_segment_members_page(
percent_regex_segment, environment, cursor=None, limit=100
)

# Then
assert [member["identifier"] for member in members] == ["alice", "bob"]


@pytest.mark.clickhouse
def test_compute_segment_counts_for_project__regex_with_percent__counts_matches(
segment_membership_identities: None,
percent_regex_segment: Segment,
project: Project,
) -> None:
# Given / When
with connections["clickhouse"].cursor() as cursor:
counts = compute_segment_counts_for_project(project, cursor)

# Then
assert len(counts) == 1
assert counts[0].segment_id == percent_regex_segment.id
assert counts[0].count == 2


@pytest.mark.clickhouse
def test_get_segment_members_page__matching_identities__returns_members_ordered_by_identifier(
segment_membership_identities: None,
Expand Down
8 changes: 4 additions & 4 deletions api/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,7 @@ Attributes:
### `segment_membership.compute.segment.skipped`

Logged at `error` from:
- `api/segment_membership/services.py:138`
- `api/segment_membership/services.py:145`

Attributes:
- `project.id`
Expand All @@ -379,7 +379,7 @@ Attributes:
### `segment_membership.members.segment.skipped`

Logged at `error` from:
- `api/segment_membership/services.py:200`
- `api/segment_membership/services.py:209`

Attributes:
- `reason`
Expand Down
Loading