updates
This commit is contained in:
parent
6f17db399b
commit
aad6b904ea
2 changed files with 149 additions and 7 deletions
|
@ -1,7 +1,7 @@
|
|||
:PROPERTIES:
|
||||
:ID: 7b0f97f3-9037-4d05-9170-a478e97c8d1f
|
||||
:END:
|
||||
#+title: Translating the search DSL
|
||||
#+title: Modeling the new search DSL
|
||||
|
||||
Defining and translating the Search DSL for the [[id:11edd6c9-b976-403b-a419-b5542ddedaae][Subscriber Search Service]].
|
||||
|
||||
|
@ -10,7 +10,8 @@ Defining and translating the Search DSL for the [[id:11edd6c9-b976-403b-a419-b55
|
|||
#+begin_src python :noweb-ref search
|
||||
@dataclasses.dataclass
|
||||
class Search:
|
||||
conditions: typing.List[Group]
|
||||
group: Group
|
||||
# TODO: sorting : Sorting
|
||||
#+end_src
|
||||
|
||||
** A grouping is a collection of conditions
|
||||
|
@ -26,13 +27,31 @@ Defining and translating the Search DSL for the [[id:11edd6c9-b976-403b-a419-b55
|
|||
conditions: typing.List[Condition]
|
||||
#+end_src
|
||||
|
||||
** A condition is a boolean expression applied to a field
|
||||
** A condition is a filter applied to a field
|
||||
#+begin_src python :noweb-ref condition
|
||||
@dataclasses.dataclass
|
||||
class Condition:
|
||||
field: Field
|
||||
filter: Filter
|
||||
match : str
|
||||
#+end_src
|
||||
|
||||
** A filter is a boolean expression applied to a field with an optional argument
|
||||
|
||||
#+begin_src python :noweb-ref filter
|
||||
class InputType(enum.Enum):
|
||||
Nothing = 1
|
||||
String = 2
|
||||
Date = 3
|
||||
Tag = 4
|
||||
TagSet = 5
|
||||
Message = 6
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class Filter:
|
||||
operator: str
|
||||
value: typing.Optional[str]
|
||||
field: Field
|
||||
input_type: InputType
|
||||
#+end_src
|
||||
|
||||
** A field refers to a specific database field somewhere in our system
|
||||
|
@ -55,16 +74,86 @@ Defining and translating the Search DSL for the [[id:11edd6c9-b976-403b-a419-b55
|
|||
database: Database
|
||||
#+end_src
|
||||
|
||||
** Allowable conditions
|
||||
** Available filters
|
||||
*** Subscriber email is x
|
||||
#+begin_src python :noweb-ref fields
|
||||
email = Field(
|
||||
name="email",
|
||||
column="email",
|
||||
table="subscribers",
|
||||
database=Database.AppDB,
|
||||
)
|
||||
#+end_src
|
||||
|
||||
#+begin_src python :noweb-ref filters
|
||||
email = Filter(field=fields.email, operator="is", input_type=InputType.String)
|
||||
#+end_src
|
||||
** Sample searches
|
||||
|
||||
*** Match subscriber email
|
||||
#+begin_src python :noweb-ref searches
|
||||
Search(
|
||||
group=Group(
|
||||
group_type=GroupType.AND,
|
||||
conditions=[Condition(filter=filters.email, match="test@example.org")],
|
||||
)
|
||||
)
|
||||
#+end_src
|
||||
|
||||
* SQL Generation
|
||||
|
||||
#+begin_src python :noweb-ref builder
|
||||
def to_sql(search: Search) -> str:
|
||||
tables: typing.Set[str] = {"subscribers"}
|
||||
tables = tables | {
|
||||
condition.filter.field.table for condition in search.group.conditions
|
||||
}
|
||||
|
||||
def condition_to_sql(condition: Condition):
|
||||
field = ".".join([condition.filter.field.table, condition.filter.field.column])
|
||||
return f"{field} {condition.filter.operator} {condition.match}"
|
||||
|
||||
def group_to_sql(group: Group) -> str:
|
||||
operator = "AND" if search.group.group_type == GroupType.AND else "OR"
|
||||
clauses = f" {operator} ".join(
|
||||
[condition_to_sql(condition) for condition in group.conditions]
|
||||
)
|
||||
return f"({clauses})"
|
||||
|
||||
where = group_to_sql(search.group)
|
||||
return f"""SELECT * FROM {', '.join(tables)} WHERE {where}"""
|
||||
#+end_src
|
||||
* Decisions
|
||||
|
||||
** Should the input type presented to the end-user be tied to the database field or the conditional operator?
|
||||
** DONE Should the input type presented to the end-user be tied to the database field or the conditional operator?
|
||||
Seems it should be the operator, as an "equals" operator would match a single
|
||||
value, whereas an "in" operator would match against multiple. That said, it
|
||||
could be /parameterized/ by the field's type (e.g. a tag has type =str=, its
|
||||
"equals" operator has type =str=, its "in" operator has type =List[str]=).
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
The input type will be defined as a property of the filter being applied.
|
||||
|
||||
** TODO Should the search service maintain a set of filters, or field types and operators?
|
||||
- A filter is a combination of a field, an operator, and a type
|
||||
- A field has a type, and operators could be defined that work with a type or set of types
|
||||
|
||||
For the former, the service would have total control over the search filters
|
||||
available to the UI, and the UI would be coupled to the filter collection. With
|
||||
the latter, the UI would have total control over which fields it's able to
|
||||
search on and how, provided the fields are available.
|
||||
** TODO How should the values of each filter be represented in the request schema?
|
||||
Should they be normalized to strings, or should we allow any type and validate
|
||||
it when we attempt to build the search data model? If the latter, could the
|
||||
available filters be baked into the OpenAPI schema?
|
||||
** TODO How should the SQL be generated for each filter?
|
||||
Should a SQL template or generation function be attached to each filter?
|
||||
** TODO How do we want to define the joins for the various tables that may come into play?
|
||||
We'll have to know, one way or another, how to narrow the records from the
|
||||
joined table. Will they all be joined by the subscriber id, or will we need to
|
||||
maintain a map?
|
||||
|
||||
* Code
|
||||
#+begin_src python :noweb yes :noweb-ref final :exports code :results silent
|
||||
import dataclasses
|
||||
|
@ -74,6 +163,9 @@ could be /parameterized/ by the field's type (e.g. a tag has type =str=, its
|
|||
<<field>>
|
||||
|
||||
|
||||
<<filter>>
|
||||
|
||||
|
||||
<<condition>>
|
||||
|
||||
|
||||
|
@ -81,4 +173,42 @@ could be /parameterized/ by the field's type (e.g. a tag has type =str=, its
|
|||
|
||||
|
||||
<<search>>
|
||||
|
||||
|
||||
<<builder>>
|
||||
|
||||
|
||||
class fields:
|
||||
<<fields>>
|
||||
|
||||
|
||||
class filters:
|
||||
<<filters>>
|
||||
|
||||
|
||||
searches = [
|
||||
<<searches>>,
|
||||
]
|
||||
#+end_src
|
||||
|
||||
#+RESULTS:
|
||||
|
||||
#+caption: Mypy analysis
|
||||
#+begin_src bash :noweb yes :results output :exports results
|
||||
mypy <(cat <<'EOF'
|
||||
<<final>>
|
||||
EOF) 2>&1 || true
|
||||
#+end_src
|
||||
|
||||
#+RESULTS:
|
||||
: Success: no issues found in 1 source file
|
||||
* Output
|
||||
#+caption: Generated queries
|
||||
#+begin_src python :noweb yes :exports results
|
||||
<<final>>
|
||||
|
||||
return [[to_sql(search)] for search in searches]
|
||||
#+end_src
|
||||
|
||||
#+RESULTS:
|
||||
| SELECT * FROM subscribers WHERE (subscribers.email is test@example.org) |
|
||||
|
|
12
daily/2021-09-29.org
Normal file
12
daily/2021-09-29.org
Normal file
|
@ -0,0 +1,12 @@
|
|||
:PROPERTIES:
|
||||
:ID: c7df7606-fd59-44e1-9704-449b70b3539d
|
||||
:END:
|
||||
#+title: 2021-09-29
|
||||
* Adjusting broadcast report endpoints
|
||||
Continuing from [[file:2021-09-28.org::*Understanding issues with dashboard broadcast reporting][yesterday]].
|
||||
|
||||
- Reports data should replicate what's being done in the [[https://gitlab.aweber.io/CP/applications/sites/-/blob/master/aweber_app/controllers/analytics_charts_controller.php#L2262-2402][old report's graph]]
|
||||
- Add subject line
|
||||
- Maintain dashboard behavior, possibly increasing the limit (currently limited
|
||||
to 2 broadcasts)
|
||||
- Report should support date range selection
|
Loading…
Reference in a new issue