Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
2fb5666
📝 Register Queries
Kezzsim Jul 21, 2025
a4193d2
🤔 Make it work... (with top level keys only)
Kezzsim Jul 25, 2025
8467a67
🧠 Key search using `tsvector`
Kezzsim Jul 25, 2025
f50ba82
😉 FTS5 includes `key` as a column
Kezzsim Jul 25, 2025
0c2562d
✅ Finish MVP
Kezzsim Jul 28, 2025
76232c3
Merge branch 'bluesky:main' into to_be_or_not_to_be
Kezzsim Jul 29, 2025
2a22e66
↪️ Change the strategy to align with requirements
Kezzsim Jul 31, 2025
6209423
➰ Reformat API to `keyExists()`
Kezzsim Jul 31, 2025
0b96bb8
➿ Remove references to `KeyNotIn`
Kezzsim Jul 31, 2025
1949628
🏔️ Define tests and fix logic
Kezzsim Aug 4, 2025
353588e
🧹 Black / Flake8
Kezzsim Aug 4, 2025
6fd9ede
📚️ Finish docs
Kezzsim Aug 4, 2025
05a7706
1️⃣ EZMerge step 1 : parity check
Kezzsim Aug 12, 2025
2d100d5
Merge branch 'bluesky:main' into to_be_or_not_to_be
Kezzsim Aug 12, 2025
bc57e94
2️⃣ Complete EZMerge from upstream
Kezzsim Aug 12, 2025
e467e1b
Merge branch 'bluesky:main' into to_be_or_not_to_be
Kezzsim Aug 12, 2025
a0e9eac
FIX: imported name
genematx Aug 12, 2025
03c2dc1
🏷️ Rename `KeyExists` to `KeyPresent`
Kezzsim Aug 13, 2025
c7650d7
*️⃣ Renaming of interior functions
Kezzsim Aug 13, 2025
6af4cb1
Merge branch 'bluesky:main' into to_be_or_not_to_be
Kezzsim Aug 19, 2025
e5253ec
💲Add missing sqlite dollarsign string
Kezzsim Aug 20, 2025
c862590
🚥 Experiment with dynamic test data
Kezzsim Aug 20, 2025
32291fa
Revert to existing test paradigm for now.
danielballan Aug 25, 2025
b048aa7
Add test cases
danielballan Aug 25, 2025
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
1 change: 1 addition & 0 deletions docs/source/reference/queries.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Follow the links in the table below for examples specific to each query.
tiled.queries.Eq
tiled.queries.NotEq
tiled.queries.FullText
tiled.queries.KeyPresent
tiled.queries.In
tiled.queries.Like
tiled.queries.NotIn
Expand Down
20 changes: 20 additions & 0 deletions tiled/_tests/test_queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
FullText,
In,
Key,
KeyPresent,
KeysFilter,
Like,
NotEq,
Expand Down Expand Up @@ -326,6 +327,25 @@ def test_structure_families(client):
assert set(client.search(StructureFamilyQuery("array"))) == set(mapping)


def test_key_present(client):
if client.metadata["backend"] == "map":
pytest.skip("No 'KeyPresent' support on MapAdapter")
# These containers have a "color" key.
assert list(client.search(KeyPresent("color"))) == [
"full_text_test_case",
"full_text_test_case_urple",
]
# These are all the containers that do not have a "number" key.
assert list(client.search(KeyPresent("number", False))) == [
"does_contain_z",
"does_not_contain_z",
"full_text_test_case",
"full_text_test_case_urple",
"specs_foo_bar",
"specs_foo_bar_baz",
]


def test_keys_filter(client):
expected = ["a", "b", "c"]
results = client.search(KeysFilter(keys=expected))
Expand Down
23 changes: 23 additions & 0 deletions tiled/adapters/mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
Eq,
FullText,
In,
KeyPresent,
KeysFilter,
NotEq,
NotIn,
Expand Down Expand Up @@ -753,6 +754,28 @@ def notin(query: Any, tree: MapAdapter) -> MapAdapter:
MapAdapter.register_query(NotIn, notin)


def keypresent(query: Any, tree: MapAdapter) -> MapAdapter:
"""

Parameters
----------
query :
tree :

Returns
-------

"""
matches = {}
for key, value, term in iter_child_metadata(query.key, tree):
if term in query.key:
matches[key] = value
return tree.new_variation(mapping=matches)


MapAdapter.register_query(KeyPresent, keypresent)


def specs(query: Any, tree: MapAdapter) -> MapAdapter:
"""

Expand Down
16 changes: 16 additions & 0 deletions tiled/catalog/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
Eq,
FullText,
In,
KeyPresent,
KeysFilter,
Like,
NotEq,
Expand Down Expand Up @@ -1431,6 +1432,20 @@ def in_or_not_in(query, tree, method):
return _IN_OR_NOT_IN_DIALECT_DISPATCH[dialect_name](query, tree, method)


def has_key(query, tree):
# Functionally in SQLAlchemy 'is not None' does not work as expected
if tree.context.engine.url.get_dialect().name == "sqlite":
condition = orm.Node.metadata_.op("->")(query.key) != None # noqa: E711
else:
keys = query.key.split(".")
condition = (
orm.Node.metadata_.op("#>")(sql_cast(keys, ARRAY(TEXT)))
!= None # noqa: E711
)
condition = condition if getattr(query, "exists", True) else not_(condition)
return tree.new_variation(conditions=tree.conditions + [condition])


def keys_filter(query, tree):
condition = orm.Node.key.in_(query.keys)
return tree.new_variation(conditions=tree.conditions + [condition])
Expand All @@ -1447,6 +1462,7 @@ def structure_family(query, tree):
CatalogNodeAdapter.register_query(Contains, contains)
CatalogNodeAdapter.register_query(In, partial(in_or_not_in, method="in_"))
CatalogNodeAdapter.register_query(NotIn, partial(in_or_not_in, method="not_in"))
CatalogNodeAdapter.register_query(KeyPresent, has_key)
CatalogNodeAdapter.register_query(KeysFilter, keys_filter)
CatalogNodeAdapter.register_query(StructureFamilyQuery, structure_family)
CatalogNodeAdapter.register_query(SpecsQuery, specs)
Expand Down
35 changes: 35 additions & 0 deletions tiled/queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,41 @@ def decode(cls, *, key, value):
return cls(key=key, value=json.loads(value))


@register(name="keypresent")
@dataclass
class KeyPresent:
"""
Query to retrieve containers that have a specific key at any level.

Parameters
----------
key : str
e.g. "color", "sample.name"
exists : bool
Set to True by default, but can be set to False to find the inverse
Examples
--------

Search for containers that have the key "color"

>>> c.search(KeyPresent("color"))

Search for containers that do not have the key "sample.name"

>>> c.search(KeyPresent("sample.name", exists=False))
"""

key: str
exists: bool = True

def encode(self):
return {"key": self.key, "exists": self.exists}

@classmethod
def decode(cls, *, key, exists):
return cls(key=key, exists=exists)


@register(name="like")
@dataclass
class Like(NoBool):
Expand Down
Loading