Query Building
Xivapi has a query syntax that you can absolutely pass to the client's search
method as a built string, but xivapy provides a way of programatically build queries. Let's look at some query types you can do, and how to build them with QueryBuilder
Simple searches¶
Let's say you want to search the Item sheet where ItemUICategory is 44.
In xivapi terms, this would be:
ItemUICategory=44
To search with this string, you can absolutely do that:
async for item in client.search(Item, query='ItemUICategory=44')
print(item)
But you can also use the QueryBuilder
class to do the same thing:
query = QueryBuilder().where(ItemUICategory=44)
async for item in client.search(Item, query=query)
where()
is the simplest way of specifying items. You can also provide strings and booleans. Let's look at a few examples:
xivapi query | QueryBuilder syntax |
---|---|
Name="Spinning Edge" |
QueryBuilder().where(Name='Spinning Edge') |
IsPvP=false |
QueryBuilder().where(IsPvP=False) |
Name="Spinning Edge" IsPvP=false |
QueryBuilder().where(Name='Spinning Edge', IsPvP=False) |
ItemResult.Name="Bronze Ingot" |
QueryBuilder().where(**{'ItemResult.Name': 'Bronze Ingot'}) |
There are also other methods that mimic where
, but have different functionality. Let's find all ContentFinderCondition entries where the name contains 'coil':
# Equivalent to Name~"coil"
query = QueryBuilder().contains(Name='coil')
async for content in client.search(ContentFinderCondition, query=query):
print(content.data)
As you can guess by now, it's all key=value entries. Here's the complete list:
QueryBuilder method | Built output | Description |
---|---|---|
.where(Name='Coils') |
Name="Coils" |
Searches for strings that exactly match (but case-insensitive) the word "Coils" |
.contains(Name='Coils') |
Name~"Coils" |
Searches for strings that contain the (case-insensitive) name "Coils" |
.gt(ClassJobLevelRequired=50) |
ClassJobLevelRequired>50 |
Searches for items where the value is greater than the listed number |
.gte(ClassJobLevelRequired=50) |
ClassJobLevelRequired>=50 |
Searches for items where the value is greater than or equal to the listed number |
.lt(ClassJobLevelRequired=50) |
ClassJobLevelRequired<50 |
Searches for items where the value is less than the listed number |
.lte(ClassJobLevelRequired=50) |
ClassJobLevelRequired<=50 |
Searches for items where the value is less than or equal to the listed number |
Marking queries as required or excluded¶
Combining some of the examples above, let's say want to search for actions that aren't pvp actions, so you might assume the following is correct:
query = QueryBuilder().where(IsPvP=False).contains(Name=search_param) # => 'IsPvP=false Name~"Broil"'
Well, let's give that a test:
>>> actions = []
>>> async for result in client.search(Action, query=xivapy.QueryBuilder().contains(Name='Broil').where(IsPvP=False)):
... actions.append(result)
...
>>> len(actions)
49201
That's.. a lot of actions. And if you look through them, you might notice actions where IsPvP=true
- but what gives? This is because individual items are "OR" or "optional". Let's visualize your request as an if statement:
if name in action.name or action.is_pvp == False:
That's not what we intended to ask for at all. In xivapy, you can mark a query as "required" with +
:
+Name~"Broil" IsPvP=false
Which will mark that the name must contain "Broil". In xivapy, you can do this by using .required()
after a query:
QueryBuilder().contains(Name='Broil').required().where(IsPvP=False)
So let's try this:
>>> async for result in client.search(Action, query=xivapy.QueryBuilder().contains(Name='Broil').required().where(IsPvP=False)):
... actions.append(result.data)
...
>>> len(actions)
11
This is certainly better, but I get the feeling that this isn't quite what we want either. Let's investigate:
>>> from pprint import pprint
>>> pprint(actions)
[Action(name='Broil', is_pvp=False),
Action(name='Broil', is_pvp=False),
Action(name='Broil II', is_pvp=False),
Action(name='Broil II', is_pvp=False),
Action(name='Broil II', is_pvp=False),
Action(name='Broil IV', is_pvp=False),
Action(name='Broil III', is_pvp=False),
Action(name='Broil III', is_pvp=False),
Action(name='Embroiling Flame', is_pvp=False),
Action(name='Embroiling Flame', is_pvp=False),
Action(name='Broil IV', is_pvp=True)]
Oh no, IsPvP
is optional too. All we did was end up telling xivapi "The name must contain 'Broil' or pvp is false". Well, let's mark them both as required:
>>> actions = []
>>> async for result in client.search(Action, query=xivapy.QueryBuilder().contains(Name='Broil').required().where(IsPvP=False).required()):
... actions.append(result.data)
...
>>> pprint(actions)
[Action(name='Broil', is_pvp=False),
Action(name='Broil', is_pvp=False),
Action(name='Broil II', is_pvp=False),
Action(name='Broil II', is_pvp=False),
Action(name='Broil II', is_pvp=False),
Action(name='Broil IV', is_pvp=False),
Action(name='Broil III', is_pvp=False),
Action(name='Broil III', is_pvp=False),
Action(name='Embroiling Flame', is_pvp=False),
Action(name='Embroiling Flame', is_pvp=False)]
Yes! This is what we want - well, except for the last two, but that's a technicality. There's another option like .required()
called excluded()
, it does what you might expect:
>>> async for result in client.search(Action, query=xivapy.QueryBuilder().contains(Name='Broil').required().where(IsPvP=True).excluded()):
... actions.append(result.data)
...
>>> pprint(actions)
[Action(name='Broil', is_pvp=False),
Action(name='Broil', is_pvp=False),
Action(name='Broil II', is_pvp=False),
Action(name='Broil II', is_pvp=False),
Action(name='Broil II', is_pvp=False),
Action(name='Broil IV', is_pvp=False),
Action(name='Broil III', is_pvp=False),
Action(name='Broil III', is_pvp=False),
Action(name='Embroiling Flame', is_pvp=False),
Action(name='Embroiling Flame', is_pvp=False)]
Note that we changed IsPvP
to True, but then excluded()
it, which ends up with the same result that you're looking for here.
The lesson to learn here is that query items are "or" by default, instead of an "and" (unless you mark both terms as required) - if you're treating it like an if statement with an and
, you must use required()
/excluded()
on both items to get what you want.
Model-based Queries¶
If you decide to annotate your models a bit differently, you get all the flexibility of models and the convienience of queries. Let's look at a previous example:
>>> async for result in client.search(Action, query=xivapy.QueryBuilder().contains(Name='Broil').required().where(IsPvP=False).required()):
Let's be honest, that doesn't read very cleanly. You already wrote an Action
model, specifically named name
to Name
, etc. Why not reuse that? Well, if you mark your fields with QueryField
, you can:
class Action(xivapy.Model):
row_id: xivapy.QueryField[int]
name: xivapy.QueryField[str] = xivapy.QueryField(xivapy.FieldMapping('Name'))
is_pvp: xivapy.QueryField[bool] = xivapy.QueryField(xivapy.FieldMapping('IsPvP'))
query = xivapy.QueryBuilder().where(Action.name.contains('Broil')).required().where(Action.is_pvp == True).required()
async for result in client.search(Action, query=query):
print(result.data)
That's pretty much it - write a model once, and use it for client searches, sheet fetches, querying - anything. There's, of course, caveats:
- You can only use these queries with
QueryBuilder().custom()
orQueryBuilder().where()
- These only work with
QueryField
annotations and instantiations
However, the chart from before? Let's look at QueryBuilder plain methods vs the new QueryField examples:
QueryBuilder method | QueryField | Built output |
---|---|---|
.where(Name='Coils') |
Foo.name == Coils | Name="Coils" |
.contains(Name='Coils') |
Foo.name.contains('Coils') | Name~"Coils" |
.gt(ClassJobLevelRequired=50) |
Foo.cjl_req > 50 | ClassJobLevelRequired>50 |
.gte(ClassJobLevelRequired=50) |
Foo.cjl_req >= 50 | ClassJobLevelRequired>=50 |
.lt(ClassJobLevelRequired=50) |
Foo.cjl_req < 50 | ClassJobLevelRequired<50 |
.lte(ClassJobLevelRequired=50) |
Foo.cjl_req <= 50 | ClassJobLevelRequired<=50 |
As you notice, I took some liberties and shortened the names to show how much easier it can make your life.
Note
If you make a FieldMapping
that has a nested field (e.g., Content.BGM.File
), it will use that for the field name. The following are equivalent:
QueryBuilder().contains(**{'Content.BGM.File': 'Foo'})
QueryBuilder().custom(Query('Content.BGM.File', '~', 'Foo'))
QueryBuilder().custom(SomeModel.bgm_file.contains('Foo'))
Query API¶
QueryBuilder¶
Builder for constructing xivapi search queries.
Provides an abstracted interface for dealing with xivapi-style search queries, including grouping, operators like >=, ~, =, etc. Queries can be chained and grouped to create more complex queries.
Example
query = (QueryBuilder() ... .contains(Name='Alexander') ... .gte(ItemLevel=50) ... .required()) print(query.build()) Name="Alexander" +ItemLevel>=50
where ¶
where(*queries: Query, **kwargs) -> Self
Add an equality condition to the query.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
*queries
|
Query
|
A list of plain queries to add to the list. |
()
|
**kwargs
|
Field-value pairs for exact matches. |
{}
|
Returns:
Type | Description |
---|---|
Self
|
Self for method chaining. |
contains ¶
contains(**kwargs) -> Self
Add a partial string match to the query.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
**kwargs
|
Field-value pairs for partial matching. |
{}
|
Returns:
Type | Description |
---|---|
Self
|
Self for method chaining. |
gt ¶
gt(**kwargs) -> Self
Add a greater than (>) numeric comparison to the query.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
**kwargs
|
Field-value pairs for comparison. |
{}
|
Returns:
Type | Description |
---|---|
Self
|
Self for method chaining. |
gte ¶
gte(**kwargs) -> Self
Add a greater than or equal (>=) numeric comparison to the query.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
**kwargs
|
Field-value pairs for comparison. |
{}
|
Returns:
Type | Description |
---|---|
Self
|
Self for method chaining. |
lt ¶
lt(**kwargs) -> Self
Add a less than (<) numeric comparison to the query.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
**kwargs
|
Field-value pairs for comparison. |
{}
|
Returns:
Type | Description |
---|---|
Self
|
Self for method chaining. |
lte ¶
lte(**kwargs) -> Self
Add a less than or equal (<=) numeric comparison to the query.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
**kwargs
|
Field-value pairs for comparison. |
{}
|
Returns:
Type | Description |
---|---|
Self
|
Self for method chaining. |
required ¶
required() -> Self
Marks the previous query item as required.
Returns:
Type | Description |
---|---|
Self
|
Self for method chaining. |
excluded ¶
excluded() -> Self
Marks the previous query item as excluded.
Returns:
Type | Description |
---|---|
Self
|
Self for method chaining. |
custom ¶
custom(*items: Query) -> Self
Allows injecting regular Queries into QueryBuilder.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
items
|
Query
|
one or more Query items to be added to the list of clauses to evaluate. |
()
|
Returns:
Type | Description |
---|---|
Self
|
Self for method chaining. |
or_any ¶
or_any(*items: Query | QueryBuilder) -> Self
Creates a group to match agains.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
items
|
Query | QueryBuilder
|
one or more Query or QueryBuilder instances to add to the group. |
()
|
Returns:
Type | Description |
---|---|
Self
|
Self for method chaining. |
build ¶
build() -> str
Builds the current query as a string.
Returns:
Type | Description |
---|---|
str
|
A string representation of the query. |
Query¶
Represents a composable query unit in xivapi's query interface.
Provides an abstracted interface for dealing with xivapi-style query elements, which are created automatically as part of QueryBuilder's methods.
Example
query = (QueryBuilder() ... .custom('Name', '=', 'Alexander', required=True) ... .custom('ItemLevel', '>=', 50) ... .required()) print(query.build()) +Name="Alexander" +ItemLevel>=50
Group¶
Constructs groups to be used with QueryBuilder.
Provides an abstracted interfaces around xivapi-style nested grouped query items.
Example
query = (QueryBuilder() ... .or_any( # This creates a Group instance ... QueryBuilder().where(Name="The Binding Coils of Bahamut"), ... Query('ClassJob.Abbreviation', '=', 'PCT')) ... .required()) print(query.build()) +(Name="The Binding Coils of Bahamud" ClassJob.Abbreviation="PCT")