Querying
Querying with TaxiQL
Introduction
TaxiQL is Taxi’s query language for fetching and transforming data across your systems. Rather than writing integration code against specific APIs or databases, TaxiQL lets you declare what data you want using semantic types.
When you write a TaxiQL query, you’re describing the meaning of the data you need, not where to find it. Query engines like Orbital use these semantic types to automatically discover and orchestrate the necessary service calls - whether that’s a simple database query, or a complex flow across REST APIs, message queues, and serverless functions.
This semantic approach means your queries remain stable even as your architecture evolves. As services change their APIs or data moves between systems, TaxiQL adapts automatically, eliminating the traditional maintenance burden of integration code.
Basic syntax
The basic syntax of a TaxiQL query looks like this:
// Find all the people
find { Person[] }
// Find a person named Jim
find { Person( FirstName == 'Jim' ) }
// Find all the people named Jim
find { Person[]( FirstName == 'Jim' ) }
// Find a stream of person events from somewhere
stream { PersonEvents }
Query types
find
for one-time queries that return a response, and stream
for continuous data streams like event feeds or real-time updates.Constraints
Constraints restrict the operations that are called - operations will only be called if they can satisfy the specified constraints:
// Only calls services that can filter by FirstName
find { Customer(FirstName == 'Jimmy') }
// Multiple constraints
find { Customer[](
FirstName == 'Jimmy' &&
LastName == 'Smith'
) }
Services declare their constraint capabilities:
service CustomerService {
// Declares support for FirstName and LastName filtering
operation findCustomersWithName(@PathVariable first : FirstName, @PathVariable last : LastName):Customer(FirstName == first && LastName == last)
}
service CustomerDatabase {
// Tables implicitly support filtering on all fields
table customers : Customer[]
}
Type-based constraints
Combining multiple criteria
Criteria can be combined using either AND (&&
) or OR (||
):
// Find all people born in the year 2010.
find { Person[]( DateOfBirth > '2010-01-01' && DateOfBirth <= '2010-12-31' ) }
The following operators are supported in a query - although support is determined by the actual services running.
Symbol | Meaning |
---|---|
== | Equal to |
!= | Not equal to |
> | Greater than |
>= | Greater than or equal to |
< | Less than |
< | Less than or equal to |
Client-side filtering
For additional filtering after data retrieval, use filter functions:
// Filter applied in TaxiQL engine after data is fetched
find { Customer[].filter(FirstName -> FirstName == 'Jimmy') }
// Multiple conditions
find {
Customer[].filter(customer ->
customer::FirstName == 'Jimmy' &&
customer::Age > 21
)
}
Service filtering vs TaxiQL expressions
- Server-side constraints are more efficient as they reduce data transfer, but you're limited to the APIs your data sources expose.
- Filtering using TaxiQL expressions is flexible and powerful, but may result in more data being transferred over the wire, and more load on your services
Given statements vs find constraints
Understanding how given
statements differ from constraints in find
statements is crucial for writing effective queries.
Given statements
A given
statement makes data available to your query. This can include ids or values to provide into services.
given { optionalVariableName : VariableType = 'value' }
...rest of the query...
eg:
// Uses the email address to search for a Customer
given { emailAddress : EmailAddress = 'jimmy@demo.com' }
find { Customer }
// The variable name is optional
given { EmailAddress = 'jimmy@demo.com' }
However, unlike constraints, given
statements don’t constrain which operations can be called.
// Makes OrderStatus available but doesn't constrain the query
given { status : OrderStatus = 'PENDING' }
find { Order[] }
service OrdersService {
// This operation will be called because the Order[]
// query isn't constrained, even though it returns
// orders of any status
operation getAllOrders():Order[]
}
Given and Operations
The presence or absence of constraints affects which operations are eligible to be called:
// Example 1: No constraints
given { customerId : CustomerId = '123' }
find { Customer }
service CustomerApi {
// Both operations are eligible because either could
// satisfy the unconstrained Customer request
operation getRandomCustomer():Customer
operation getCustomer(CustomerId):Customer
}
// Example 2: With constraints
given { customerId : CustomerId = '123' }
find { Customer(CustomerId == customerId) }
service CustomerApi {
// Won't be called - can't satisfy the CustomerId constraint
operation getRandomCustomer():Customer
// Will be called - matches the constraint
operation getCustomer(CustomerId):Customer
}
Common pitfall
Projections
Projections are a way of taking data from one place, then transforming & combining it with other data sources.
TaxiQL engines use the information present on the object being projected in order to call services and find other information.
You can project from a pre-defined type to another predefined type - such as:
model Purchase {
transactionId : TransactionId
customerId : CustomerId
}
model CustomerTransaction {
transactionId : TransactionId
name : CustomerName
price : Price
}
find { Purchases[] } as CustomerTransaction[]
It’s also very common to project from a predefined type to a type defined inline within your query (called ‘anonymous types’). e.g.:
model Purchase {
transactionId : TransactionId
customerId : CustomerId
}
find { Purchases[] }
as {
// Projections let you change field names, and reshape objects as required
txn: TransactionId
// Not present on the original Purchase object, so try to
// find it using something we already know (in this case, the CustomerId)
customerName: CustomerName
}[]
Data discovery
Field shorthand
If you want to select fields present on the source type as-is, you can reference the field by it’s field name, leaving out the type:
model Order {
id: OrderId
status: OrderStatus
items: OrderItem[]
customer: CustomerId
total: OrderTotal
}
// Select specific fields
find { Order[] } as {
id // Selects OrderId
status // Selects OrderStatus
total // Selects OrderTotal
}[]
Using a spread operator
You can use a spread operator (...
) to include all fields (or exclude certain fields) from a model:
find { Order[] } as {
... // Include all Order fields
tracking: { // Add extra fields
location: Location
status: ShipmentStatus
}
}[]
// Exclude specific fields
find { Order[] } as {
... except { total, customer } // All fields except total and customer
}[]
The spread operator must be the last entry in the list of fields in your model definition.
Projection scopes
Creating a projection requires the syntax something as { ... }
, which defines a projection scope:
find { Customer } as { // start a projection scope, containing a customer
// ... omitted ...
}
If you’re projecting an array, then each item within the array is projected separately:
find { Customer[] } as { // start a projection scope.
// Within the scope, each item is an individual Customer instance
firstName : FirstName
}[] // Be sure to include the array marker at the end, as the object is an array.
If you need to, you can assign a name to the item within the scope. This can be useful for nested scopes, or controlling inputs into a function. e.g.:
find { Customer[] } as (customer:Customer) -> {
// referencing a field by it's name on Customer
firstName : upperCase(customer.firstName)
// referencing as field by it's type on Customer
lastName : upperCase(customer::LastName)
// using a variable within a constraint:
purchases: Purchase[](CustomerId == customer::CustomerId)
// without referring to a variable, Age is resolved against all variables in scope
age : Age
}[]
Or, when using nested scopes:
find { Customer[] } as (customer:Customer) -> {
name : CustomerName
orders : Order[] as (order:Order) -> {
ageAppropriate : Boolean = customer.age >= order.recommendedAge
}
}
Or, to provide inputs into functions:
model Film {
title : FilmTitle inherits String
headliner : ActorId
cast: Actor[]
}
find { Film[] } as (film:Film) -> {
title : FilmTitle
// singleBy selects a single item from
// an array (film.cast, an array of Actor), that matches a predicate
// (in this case, there the actor ID is the same as the headliner id)
star : singleBy(film.cast, (Actor) -> Actor::ActorId, film.headliner) as (actor:Actor) -> {
name : actor.name
title : film.title
}
}[]
Using variables to modify what's in scope
By default, the scope contains the entire source object. e.g.:
model Film {
title : Title
cast : Actor[]
}
find { Film ) as {
// this scope contains an entire film record
}
You can modify this by specifying the type of the variable in scope:
find { Film } as (Actor[]) -> { // note that film has been removed from the scope...
title : Title //... therfore title isn't knowable -- this field will return null
actorName : ActorName
}[]
You can also use functions to further reduce the scope:
find { Film } as (first(Actor[])) -> {
// Now, the scope only contains a single actor
headliner : ActorName
} // We're not projecting an array anymore, so no aray marker here
Finally, if the data defined in the scope isn’t available on the source, TaxiQL triggers a query to find it. e.g.:
// Define a few models
model Film {
id : FilmId inherits Int
title : Title inherits String
}
model Actor {
name : ActorName inherits String
}
model Cast {
actors : Actor[]
}
// And some services that return them
service Films {
operation getFilm():Film
operation getCast(FilmId):Cast
}
// Here's a query:
find { Film } as (Actor[]) -> {
actorName : Name
filmTitle : Title // should be null, as it's out-of-scope on Actor
}[]
In the above query:
getFilm()
is called, to fetch theFilm
- The projection requests an
Actor[]
in the scope, which isn’t available, so… - A call to
getCast()
is made, passing theFilmId
to fetch theActor[]
- Because it’s an array, each
Actor
within the array ofActor[]
is projected individually actorName
is read from thename
field onActor
, because the requested field asks for the typeName
filmTitle
is out-of-scope, so returned as null
Declaring multiple variables in scope
In the previous example, we saw that filmTitle
was returned as null, because
the Film
was removed from scope.
To run the same query with Film
in scope, simply add it to the projection:
find { Film } as (Film, Actor[]) -> {
actorName : Name
filmTitle : Title // Title is now discoverable, as Film is in scope
}[]
Streaming queries
Streaming queries are executed in the same way as request/response
queries, but use the stream
keyword instead of find
.
For example - assuming a Kafka topic emitting stock price updates:
model StockPrice {
symbol : StockSymbol
price : StockPrice
}
service KafkaService {
stream stockPrices : Stream<StockPrice>
}
This can be queried using the following:
stream { StockPrice }
This can be combined with other standard querying tools, such as projections and mutations:
stream { StockPrice } as {
symbol : StockSymbol
updateReceived : Instant = now()
currentPrice : StockPrice
totalTradedQuantity : TotalTradedQuantity
}
call TradeBookService::saveTradeSnapshot
Filtering streams
To filter a stream, you can use the filterEach()
function:
stream { StockQuotes.filterEach( StockSymbol -> StockSymbol == 'AAPL' ) }
Expressions in queries
Taxi allows the definition of expressions on both types and fields.
Often, expressions are used in a projection within a query.
You can also use them on a model to expose derived information when a model is parsed (eg., when return from a service). So, while documentation here focuses on query projections, you can do everything here on a model too.
Writing an expression in a projection
Expressions can be defined in the fields of a projected result from a query:
find { Flights[] }
as {
flightNumber : FlightNumber
totalSeatsAvailable : TotalSeats
soldSeats : SoldSeats
remainingSeats : Int = (this.totalSeatsAvailable - this.soldSeats)
}
Expressions can be defined in two ways - on a field, or on a type.
Expressions on a field
// Expression types on a field:
find { Flights[] }
as {
flightNumber : FlightNumber
totalSeatsAvailable : TotalSeats
soldSeats : SoldSeats
// field expressions can be defined EITHER using field references...
remainingSeats : Int = (this.totalSeatsAvailable - this.soldSeats)
// ...or type references...
remainingSeats : Int = (TotalSeats - SoldSeats)
}
Expressions on a type
To encapsulate common expressions, you can define a type with the expression:
// Expression type:
type RemainingSeats = TotalSeats - SoldSeats
// Which is then used on a projection:
find { Flights[] }
as {
flightNumber : FlightNumber
totalSeatsAvailable : TotalSeats
soldSeats : SoldSeats
remainingSeats : RemainingSeats
}
Unlike field expressions, type expression cannot use field names, and can only reference other types.
Data discovery within expressions
When evaluating an expression, first on the source object being projected is checked for the input values into the expression.
If any inputs are not available, then a search is performed using the current data available on the source object in an attempt to look up the value.
Finally, if no data is found in scope, then services are called to fetch the required data. The variables available in the current projection scope are available as inputs into services.
Data discovery rules
When projecting, TaxiQL engines will use information present on the source object to discover data on the target object.
Data can be fetched from a single operation that returns the value, or by invoking a chain of operations to return the value.
Operations with @Id fields on return types
If the result of an operation is an object that exposes an @Id
field, then only operations which accept that @Id
field as
an input will be called. e.g.:
model Customer {
@Id customerId : CustomerId
name : CustomerName
}
service CustomerService {
// Can be called when projecting, because
// Person has an @Id of type PersonId
findCustomer(CustomerId):Customer
// Cannot be called when projecting, because
// Person has an @Id, and it isn't PersonName
findCustomerByName(CustomerName):Customer
}
Operations without @Id fields on return types
If the result of an operation is an object that does not expose an @Id
field, then it can be called with
any information available.
Filling in nulls
By default, if a service returns a null value, the data is accepted it as-is.
However, if query annotates a field on a projection type with @FirstNotEmpty
, TaxiQL engines will
attempt to populate values by invoking operations to populate the missing values.
A search is performed using the other values present on the entity being projected as potential inputs to operations, and build a path to populate the missing values.
Operations are invoked following the standard Data Discovery Rules
Named queries
Queries can be defined within your Taxi project, and reused:
// Define a query
query FindPendingOrders {
find { Order[](Status == 'PENDING') }
}
// With parameters
query FindOrdersByStatus(status: OrderStatus) {
find { Order[](Status == status) }
}
Including / excluding services from a query
TaxiQL allows you to control which services are called during query execution using the using
and excluding
keywords.
This helps you manage query execution paths while maintaining the declarative nature of TaxiQL.
Including specific services
Use the using
keyword to specify which services or operations should be called:
find { Film[] } as {
title: Title
reviews: ReviewScore
}
using {
FilmService::getFilms, // Specific operation
ReviewService // Entire service
}
You can include:
- Specific operations using
ServiceName::operationName
- Entire services using just the service name
- A mixture of both
For example:
model Film {
title: FilmTitle inherits String
}
service FilmService {
operation getFilms():Film[]
operation getBlockbusters():Film[]
}
service NetflixService {
operation getFilms():Film[]
}
// Only call specific operations
find { Film[] }
using { FilmService::getFilms, FilmService::getBlockbusters }
// Mix services and operations
find { Film[] }
using { FilmService::getBlockbusters, NetflixService }
Inclusion behavior
Excluding services
Alternatively, use the excluding
keyword to specify which services or operations should NOT be called:
find { Film[] }
excluding {
ImdbApi, // Exclude entire service
RottenTomatoes::getReviews // Exclude specific operation
}
This is useful when you want to allow most services but need to prevent specific ones from being called.
Table operations
You can also include or exclude table operations:
service FilmService {
table films: Film[]
}
// Exclude the films table
find { Film[] }
excluding { FilmService::films }
Service validation