Introduction
Open PQL is a high-performance Rust implementation of the Poker Query Language (PQL), enabling SQL-like queries for poker probability analysis. It is a spiritual successor to the original Java implementation that powers Odds Oracle and propokertools.com.
⚠️ Work in Progress: this project is under active development. Public APIs, syntax, and supported features may change.
What is PQL?
PQL lets you ask poker questions in a declarative, SQL-like syntax:
select avg(equity(hero))
from game='holdem', hero='AwAx', villain='*'
Read it as: “compute hero’s average all-in equity holding any pair of aces against a random hand in Hold’em.”
PQL is intended for people with a decent level of poker and technical sophistication who want to explore probability questions without writing custom programs.
What kinds of questions can PQL answer?
A few examples to give you the flavour:
Hero’s equity on a known flop against a tight range:
select equity(hero)
from game='holdem', hero='AhKh', villain='QQ+', board='Ah9s2c'
How often hero flops a set with a small pair, given villain has any two cards:
select count(flopHandCategory(hero) = flopset) as pct_set
from game='holdem', hero='TT', villain='*'
How often the river completes a monotone (single-suit) board:
select count(monotoneBoard(river)) as pct_monotone
from game='holdem', hero='*', villain='*'
Why “PQL”?
The syntax is loosely based on SQL: it borrows SELECT, FROM, WHERE, AS, AND, OR, NOT, the comparison operators (=, <, <=, >, >=), arithmetic (+, -, *, /), and parentheses for grouping. If you’ve written SQL before, the shape will look familiar.
Workspace Crates
| Crate | Purpose |
|---|---|
openpql-prelude | Core poker types: cards, hands, evaluators, game variants |
openpql-core | Game abstraction and shared compute kernels |
openpql-range-parser | Parser for the (generic) range notation, e.g. AwKw, 77-55 |
openpql-pql-parser | Parser for PQL syntax (LALRPOP grammar) |
openpql-runner | Query executor and the opql CLI |
openpql-macro | Internal procedural macros for function registration |
How to Read This Book
- Start with Installation, then Your First Query.
- The PQL Language section covers selectors, the
from/whereclauses, ranges, boards, games, and types. - Built-in Functions lists every function available inside
selectwith its argument and return types. - The Tutorials walk through realistic analysis workflows.
- The Reference section covers embedding the runner from a Rust program.
Try It Online
An interactive demo is available at https://pql-playground.solve.poker.
Installation
Open PQL ships as a Cargo workspace. You can use it as a command-line tool (opql) or embed the runner crate in your own Rust program.
Requirements
- Rust 1.85 or newer (edition 2024)
- A recent
cargotoolchain
Install the CLI
Clone the repository and install the runner crate’s binary:
git clone https://github.com/solve-poker/Poker-Query-Language.git
cd Poker-Query-Language
cargo install --path openpql-runner --features cli
This installs the opql binary into ~/.cargo/bin/. Verify that it’s on your PATH:
opql --help
Use as a Library
Add the runner crate to your Cargo.toml:
[dependencies]
openpql-runner = "0.1"
The library entry point is opql::PQLRunner. See Library Usage for integration details.
Build From Source
If you plan to contribute or run tests, clone the workspace and use the provided justfile:
just build # cargo build
just test # cargo nextest run
just lint # cargo clippy
just doc # cargo doc --no-deps
Next Step
Continue to Your First Query.
Your First Query
Let’s compute hero’s equity against a range of villain hands on a known flop.
The Query
select equity(hero)
from game='holdem', hero='AhKh', villain='QQ+', board='Ah9s2c'
Reading left to right:
select equity(hero)— ask for hero’s equity (sampled per trial).game='holdem'— play the hand as Texas Hold’em.hero='AhKh'— hero holds the Ace and King of hearts (specific cards).villain='QQ+'— villain has any pocket pair QQ or better.board='Ah9s2c'— three community cards are already on the table.
Run It
opql --run "select equity(hero) from game='holdem', hero='AhKh', villain='QQ+', board='Ah9s2c'"
The runner samples random runouts (turn + river) and prints the resulting equity. Re-run the query for a fresh Monte Carlo estimate.
A Second Example
Average number of suits on the river when hero holds A♠9♠ on a two-tone flop:
opql --run "select avg(boardSuitCount(river)) from game='holdem', hero='As9s', villain='*', board='2s3sJh'"
Here villain='*' means “any two cards”, and avg(...) averages a per-trial value across samples.
A Counting Example
Frequency hero flops top pair against any two cards:
opql --run "select count(hasTopBoardRank(hero, flop)) as pct_toppair from game='holdem', hero='AxKy', villain='*', board=''"
count(predicate) divides the number of trials where the predicate held by the total number of trials, giving you a probability.
Next Step
Learn more about CLI usage in CLI Basics, or skip ahead to Query Structure.
CLI Basics
The opql binary is a thin wrapper around the runner crate. It currently exposes a single entry point, --run, that executes a PQL string and writes the result to stdout.
Usage
opql --run "<PQL query>"
Example:
opql --run "select equity(hero) from game='holdem', hero='AA', villain='KK', board='AhKh2c'"
Notes
- Quote the full query with double quotes so the shell passes it as one argument.
- Use single quotes inside the query for hand, range, board, and game literals.
- Multiple statements may be separated by
;and will be reported one after another. - Errors in parsing or evaluation are written to stderr; successful results go to stdout.
- The default trial count is set in
VmStaticData::DEFAULT_N_TRIALS(60,000 in release builds, 100 in debug builds). It is currently not configurable from the CLI.
Getting Help
opql --help
For richer workflows — programmatic access to results, custom trial counts, or building queries from data — use the library API directly.
Query Structure
Every PQL query follows a select … from … [where …] shape, modeled after SQL.
select <selectors>
from <bindings>
where <predicate> -- optional
A query produces one report row per selector, computed over a fixed number of Monte Carlo trials.
Selectors
Selectors describe what you want to measure. Each selector is an aggregate over an inner expression:
select avg(equity(hero)) as heroEv,
count(wins(hero)) as heroWinPct,
max(handType(hero, river))
from ...
PQL supports four aggregate selectors: avg, count, max, min. See Selectors for the full semantics. Each selector can be aliased with as to give the report column a readable name.
Bindings (the from clause)
Bindings declare the poker situation to simulate. They are written as key='value' pairs, separated by commas:
from game='holdem', hero='AhKh', villain='QQ+', board='Ah9s2c'
Reserved keys are game, board, dead. Any other key is treated as a player name. See From Clause for the full list.
Filtering with where
A where clause filters trials before they reach the selectors. The expression must be a boolean and may reference any function, player, or street:
select avg(equity(hero))
from game='holdem', hero='AhKh', villain='**'
where hasTopBoardRank(hero, flop)
See Where Clause.
Identifiers and Case
Keywords (select, from, where, as, and, or, not) and function names are case-insensitive. The book uses lowercase for keywords and camelCase for function names by convention.
Card characters follow standard notation: ranks 2-9 T J Q K A and suits s h d c.
Operators
PQL inherits a subset of SQL operators:
| Class | Operators |
|---|---|
| Comparison | =, <, <=, >, >= |
| Arithmetic | +, -, *, / |
| Boolean | and, or, not |
| Grouping | (, ) |
Not currently implemented: <> / !=, ||, IN, CASE … WHEN, line comments (-- …), block comments (/* … */).
Whitespace and Commas
Whitespace is free-form. Selectors and bindings are comma-separated. Trailing commas are accepted.
Multiple Statements
A ; separates independent statements. Each statement is parsed and run in turn, with its own report:
select equity(hero) from game='holdem', hero='AA', villain='KK';
select equity(hero) from game='holdem', hero='AA', villain='QQ';
Selectors
A selector is an aggregate that reduces an inner expression evaluated over each trial into a single report value. Every PQL select list is one or more selectors separated by commas.
PQL supports four selectors:
| Selector | Description | Example |
|---|---|---|
avg(expr) | Mean of a numeric expression across all trials | avg(equity(hero)) |
count(pred) | Fraction of trials for which a boolean expression is true (a probability) | count(wins(hero)) |
max(expr) | Largest value of an expression seen across trials | max(handType(hero, river)) |
min(expr) | Smallest value of an expression seen across trials | min(fractionalRiverEquity(villain)) |
Note: a
histogramselector exists in the original PQL spec but is not yet implemented in Open PQL.
Combining Selectors
A query can ask for any number of selectors in a single shot:
select count(wins(hero)) as heroWon,
avg(equity(hero)) as heroEv,
count(handType(hero, river) = flush) as pctFlush
from game='holdem', hero='AwKw', villain='**'
Each selector is reported on its own line.
Aliases (as)
Use as to give a selector a readable name:
select avg(boardSuitCount(river)) as river_suits
from game='holdem', hero='As9s', villain='*', board='2s3sJh'
If as is omitted, a default name is generated from the expression.
Inner Expressions
The expression inside a selector can be any combination of:
- Function calls —
equity(hero),handType(hero, flop),boardSuitCount(river), … - Constants — numbers, single-quoted strings, hand-type and category keywords (
pair,flopset, …) - Comparisons and boolean operators —
handType(hero, river) = flush,equity(hero) > 0.5 and hasTopBoardRank(hero, flop) - Arithmetic —
equity(hero) - equity(villain)
See Built-in Functions for the available primitives.
Common Recipes
Probability of an event:
select count(<predicate>) from ...
Average value of a metric:
select avg(<numeric expression>) from ...
Worst-case value over the simulation:
select min(<numeric expression>) from ...
From Clause
The from clause defines the scenario PQL will simulate. Each binding is key='value', with bindings separated by commas. Keys are case-insensitive; values are always single-quoted strings.
from game='holdem', hero='AhKh', villain='QQ+', board='Ah9s2c', dead='2c'
A binding key may appear at most once. Duplicate keys produce a parse error.
Reserved Keys
| Key | Value type | Meaning |
|---|---|---|
game | game name | Which poker variant to play (default holdem) |
board | board range | Community cards or a board pattern |
dead | card list | Cards removed from the deck before dealing |
Anything that is not one of those three is interpreted as a player name and its value parsed as a range.
game
Selects the poker variant. Open PQL currently supports:
holdem— Texas Hold’em (default)omaha— Pot-Limit Omaha (4 hole cards)shortdeck— 6+ Hold’em (36-card deck)
See Supported Games for the full description.
Players
Players are declared by giving each one a name and a range:
hero='AhKh' -- exact two cards
villain='QQ+' -- pocket pair QQ or better
villain1='AwKw, AxKy' -- a small named range
fish='*' -- any two cards
Any identifier (other than game, board, dead) is accepted as a player name; the convention is hero, villain, villain1, …, villainN. The full set of players in the from clause defines the seat lineup for that query.
See Range Notation for the value syntax. Classic notation (AKs, AKo) is not yet implemented; only the generic variable-suit syntax is supported.
board
The community-card situation. The board can be a fully-known set of cards or a board range pattern:
board='Ah9s2c' -- flop
board='Ah9s2c7d' -- turn
board='Ah9s2c7dTs' -- river
board='' -- preflop (no community cards)
board='Aw9x2y' -- generic flop pattern (any rainbow A-9-2)
When a partial board is given, remaining streets are sampled per trial. See Boards and Streets.
dead
Cards that should be removed from the deck before dealing. Useful for “given the burn cards…” scenarios:
dead='2c, 7h'
dead cards never appear in any player’s holding nor on the board.
Defaults
| Field | Default if omitted |
|---|---|
game | holdem |
board | * (preflop, all five board cards sampled) |
dead | empty |
| players | (no players declared — usually you want at least one) |
Ordering
Bindings may appear in any order. By convention, declare game first, then players, then board, then dead.
Where Clause
The where clause is an optional boolean predicate that filters trials before they reach the selectors. It is evaluated after sampling and before the selector expression. Trials that fail the predicate are discarded and not counted in the aggregate.
select avg(equity(hero))
from game='holdem', hero='AhKh', villain='**'
where hasTopBoardRank(hero, flop)
This computes hero’s average equity conditional on flopping top pair.
What it can contain
The predicate is an arbitrary boolean expression:
- Function calls returning
TBoolean— e.g.nutHi(hero, flop),pocketPair(hero). - Comparisons — e.g.
equity(hero) > 0.5,handType(hero, flop) = flush. - Boolean composition —
and,or,not, plus()for grouping.
where equity(villain) > equity(hero)
and minHandType(hero, turn, pair)
When the where clause helps
A where clause is useful when you want a conditional probability or expected value. For example:
-- How often does hero win when villain hits the flop?
select count(wins(hero)) as heroWinsGivenVillainHits
from game='holdem', hero='AwAx', villain='**'
where not (handType(villain, flop) = highcard)
If you need the unconditional probability or expectation, omit where.
Filtering Cost
A trial that fails the where predicate is wasted work — the runner counts it as a failed sample but does not feed it into selectors. If your where clause matches very rarely, the simulation may take a long time to gather enough successful trials. Tighten the from clause (e.g. fix more cards) when possible.
Range Notation
Ranges describe sets of starting hands. They appear on the right-hand side of any player binding, on the board= binding, and inside functions like inRange and boardInRange.
Heads-up: Open PQL implements only the generic range syntax (variable-suit notation). The classic shorthand (
AKs,AKo,99+) used by some other tools is not yet implemented.
The grammar lives in the openpql-range-parser crate.
Concrete Cards
Use a rank (2-9 T J Q K A) followed by a concrete suit (s h d c) to fix a single card:
As -- Ace of spades
AsKh -- Ace of spades + King of hearts
A two-card hand for Hold’em or Short Deck is two such cards juxtaposed; an Omaha hand is four cards.
Suit Variables
Suits can be left abstract using suit variables w x y z. The same letter means the same (still-unspecified) suit, different letters mean different suits.
| Notation | Meaning |
|---|---|
AwKw | A and K of the same suit (suited AK) |
AxKy | A and K of different suits (offsuit AK) |
AK | Any AK (suited or offsuit) |
TT | Any pocket tens |
** | Any two cards (Hold’em) |
* | Wildcard rank (matches any rank) |
* may also stand in for an entire wildcard card — e.g. A* means “an Ace plus any card”.
Spans
A + extends the leading shape upward; a - between two shapes denotes an inclusive interval going downward from the larger to the smaller.
| Notation | Meaning |
|---|---|
QQ+ | Pocket pairs QQ or better |
88-55 | Pocket pairs 88 down to 55 |
AwJw+ | Suited aces from AJ up |
KwQw-KwTw | Suited kings from KQ down to KT |
Lists
[a, b, c] is an alternation that fills one slot. Combine with another card for products:
[2,4,6,8,T]A -- A2, A4, A6, A8, AT
A[2,3,4] -- A2, A3, A4
You may also write [span] to embed a span literal, e.g. [QQ+].
Combining Many Atoms
Comma-separated terms in the same string union into a single range:
AA, KK, AwKw, 77-55
Boards
The board= binding accepts the same range syntax. A wholly concrete value ('Ah9s2c') pins the flop; a partial pattern leaves some cards generic:
board='Aw9x2y' -- any rainbow flop with ranks A, 9, 2
Conflicts and Blockers
Combos that collide with already-known cards (other players’ holdings, the board, or dead cards) are excluded automatically during sampling. You don’t need to subtract blockers by hand.
Errors
A malformed range surfaces as a parse error at query evaluation time, with the offending span in the error report.
Boards and Streets
PQL simulates complete runouts, so it always has a notion of the current street based on how many cards board= provides.
Streets
| Street | Board size | Sampled per trial |
|---|---|---|
preflop | 0 | flop + turn + river |
flop | 3 | turn + river |
turn | 4 | river |
river | 5 | nothing — fully deterministic |
Street identifiers (preflop, flop, turn, river) are case-insensitive bare keywords — not strings. They appear as arguments to many functions.
Referencing Streets in Functions
Most board-aware functions take a street so you can ask about the board as it will look on a future card:
select avg(boardSuitCount(river))
from game='holdem', hero='As9s', villain='*', board='2s3sJh'
river here means the completed five-card board, even though the simulation starts on the flop.
Fixed Boards
If board is a full five-card string the runner samples no community cards — the query becomes a deterministic evaluation, useful for checking concrete spots:
select equity(hero)
from game='holdem', hero='AhKh', villain='QcQd', board='Qs9h2c7d3d'
Board Patterns
A partial board pattern (with suit variables or *) randomises the unspecified parts:
board='Aw9x2y' -- any rainbow A-9-2 flop
See Range Notation for the full pattern syntax.
Dead Cards
Any card mentioned in a player range, in the board, or in dead='…' is removed from the deck for the rest of the deal. This prevents impossible combinations from being generated.
Game-Specific Notes
- Hold’em / Short Deck: 5-card community board, all from the same deck the players draw from.
- Omaha: same 5-card community board; players must use exactly two of their four hole cards.
- Short Deck: 36-card deck, so fewer possible boards and faster enumeration on river-locked queries.
Supported Games
The game='…' binding selects the poker variant. Each variant changes the deck, the number of hole cards, and the hand evaluator.
| Value | Variant | Hole cards | Deck |
|---|---|---|---|
holdem | Texas Hold’em | 2 | Full 52 |
omaha | Pot-Limit Omaha | 4 | Full 52 |
shortdeck | Short-Deck Hold’em | 2 | 36 (6s–As) |
holdem is the default if game is omitted. Open PQL is currently a Hi-only implementation — Hi/Lo splits (Omaha 8, Stud 8) and stud variants (Stud Hi, Razz) are not supported.
Hold’em
Players are dealt two hole cards, share a five-card board, and use any combination of seven cards to make the best five-card hand.
select equity(hero)
from game='holdem', hero='AhKh', villain='QQ+', board='Ah9s2c'
Omaha
Four hole cards per player. Each player must use exactly two of their hole cards and three of the board cards. Range strings still use the same notation; concrete hands require four cards (e.g. AhAsKhKs):
select equity(hero)
from game='omaha', hero='AhAsKhKs', villain='**'
Short Deck
A 36-card deck (deuces through fives removed). Common Short-Deck rule choices apply: A-6-7-8-9 is the wheel straight, and flushes beat full houses. The prelude crate’s evaluator implements the standard ranking.
select equity(hero)
from game='shortdeck', hero='AwAx', villain='**'
One Game per Query
Each query targets a single game. You cannot mix variants inside one query.
Types
PQL is dynamically typed at the surface, but every expression has a known PQL type that determines what other expressions it can combine with. You usually don’t need to think about types explicitly — function signatures will guide you — but the table below is useful when chasing a type-mismatch error.
Scalar Types
| Type | Meaning |
|---|---|
TBoolean | true or false |
TInteger / TLong | A whole number |
TDouble | A double-precision floating point number |
TFraction | An exact fraction such as 1/2, 2/5, 13/914 |
TEquity | A TDouble between 0.0 and 1.0 |
TNumeric | Any of the numeric types above |
TString | A single-quoted string literal |
Card / Hand Types
| Type | Meaning |
|---|---|
TCard | A single card (e.g. the Jack of Diamonds) |
TCardCount | An integer between 0 and 52 |
TRank | A rank (an Ace, a Ten, a Deuce, …) |
TRankSet | A set of unique ranks |
TStreet | One of preflop, flop, turn, river |
THandType | A 5-card hand category (see below) |
TFlopHandCategory | A flop-specific hand category (see below) |
THiRating | A hi-hand rating, used for comparing hand strength |
Players and Ranges
| Type | Meaning |
|---|---|
TPlayer | A player declared in the from clause (e.g. hero, villain) |
TPlayerCount | An integer between 0 and the number of players |
TRange | A range expression such as 'AwKw, 77-55' |
TBoardRange | A range expression for the board, e.g. 'AwKx2y' |
THandType
The 5-card hand category for a player on a given street.
highcardpairtwopairtripsstraightflushfullhousequadsstraightflush
Hand-type values are bare keywords inside expressions: handType(hero, river) = flush.
TFlopHandCategory
A finer-grained classification of what a flop hand looks like. Useful for “did I flop bottom two?” style filters.
| Value | Meaning |
|---|---|
flopnothing | No pair, no draws of consequence (made nothing) |
flopunderpair | Pocket pair lower than every flop card |
flopthirdpair | Hits the third (lowest) flop rank |
floppocket23 | Pocket pair between the second and third flop ranks |
flopsecondpair | Hits the second flop rank |
floppocket12 | Pocket pair between the first and second flop ranks |
floptoppair | Hits the top flop rank |
flopoverpair | Pocket pair larger than every flop card |
flopbottomtwo | Hits the two lowest of three distinct flop ranks |
floptopandbottom | Hits the top and bottom of three distinct flop ranks |
floptoptwo | Hits the top two of three distinct flop ranks |
floptrips | Three of a kind on a paired flop |
flopset | Three of a kind on an unpaired flop |
flopstraight | Made a straight |
flopflush | Made a flush |
flopfullhouse | Made a full house (takes precedence over pocket-pair categories) |
flopquads | Four of a kind |
flopstraightflush | Straight flush |
Notes on Lo Types
The original PQL spec defines TLoRating and a number of Lo-hand functions. Open PQL has the type symbol reserved but no Lo functions are implemented yet, so TLoRating should be treated as a placeholder.
Built-in Functions Overview
Open PQL ships with a library of poker-specific functions you can use inside select and where. They group loosely into the categories below; each has its own page with descriptions and examples.
| Category | Page |
|---|---|
| Equity | Equity |
| Hand categories, types, and ratings | Hand Categories |
| Board texture | Board Texture |
| Rank utilities | Rank Utilities |
| Outs | Outs |
| Outcomes & helpers | Outcomes |
Common shapes
Most functions take some combination of:
TPlayer— an identifier declared in thefromclause (hero,villain, …)TStreet— one ofpreflop,flop,turn,riverTHandTypeorTFlopHandCategory— a hand-class keywordTRange/TBoardRange— a single-quoted range stringTRankSet— typically the result ofboardRanks(...)orhandRanks(...)
Function names are case-insensitive. The book uses camelCase for readability, but boardSuitCount, boardsuitcount, and BOARDSUITCOUNT all parse the same.
Full function index
The 49 unique functions currently implemented (with two extra aliases) are:
| Function | Argument types | Return type |
|---|---|---|
bestHiRating | TPlayer, TStreet | TBoolean |
boardInRange | TBoardRange | TBoolean |
boardRanks | TStreet | TRankSet |
boardSuitCount | TStreet | TCardCount |
duplicatedBoardRanks | TStreet | TRankSet |
duplicatedHandRanks | TPlayer, TStreet | TRankSet |
equity (alias of hvhEquity) | TPlayer, TStreet | TEquity |
exactFlopHandCategory | TPlayer, TFlopHandCategory | TBoolean |
exactHandType | TPlayer, TStreet, THandType | TBoolean |
flopHandCategory | TPlayer | TFlopHandCategory |
flushingBoard | TStreet | TBoolean |
fractionalRiverEquity | TPlayer | TFraction |
handBoardIntersections | TPlayer, TStreet | TCardCount |
handRanks | TPlayer, TStreet | TRankSet |
handType | TPlayer, TStreet | THandType |
hasSecondBoardRank | TPlayer, TStreet | TBoolean |
hasTopBoardRank | TPlayer, TStreet | TBoolean |
hiRating | TPlayer, TStreet | THiRating |
hvhEquity | TPlayer, TStreet | TEquity |
inRange | TPlayer, TRange | TBoolean |
intersectingHandRanks | TPlayer, TStreet | TRankSet |
maxHiRating | TPlayer, TStreet | TBoolean |
maxRank | TRankSet | TRank |
minEquity (alias of minHvHEquity) | TPlayer, TStreet, TDouble | TBoolean |
minFlopHandCategory | TPlayer, TFlopHandCategory | TBoolean |
minHandType | TPlayer, TStreet, THandType | TBoolean |
minHiRating | TPlayer, TStreet, THiRating | TBoolean |
minHvHEquity | TPlayer, TStreet, TDouble | TBoolean |
minRank | TRankSet | TRank |
monotoneBoard | TStreet | TBoolean |
nonIntersectingHandRanks | TPlayer, TStreet | TRankSet |
nthRank | TInteger, TRankSet | TRank |
nutHi | TPlayer, TStreet | TBoolean |
nutHiForHandType | TPlayer, TStreet | TBoolean |
overpair | TPlayer, TStreet | TBoolean |
pairedBoard | TStreet | TBoolean |
pocketPair | TPlayer | TBoolean |
rainbowBoard | TStreet | TBoolean |
rankCount | TRankSet | TCardCount |
rateHiHand | TString | THiRating |
riverCard | TCard | |
riverEquity | TPlayer | TEquity |
scoops | TPlayer | TBoolean |
straightBoard | TStreet | TBoolean |
tiesHi | TPlayer | TBoolean |
toCard | TString | TCard |
toRank | TString | TRank |
turnCard | TCard | |
twoToneBoard | TStreet | TBoolean |
winningHandType | THandType | |
winsHi | TPlayer | TBoolean |
Functions in the original PQL spec but not yet implemented
For users coming from the original Java PQL, the following functions are documented in the upstream spec but not yet available in Open PQL:
- All Lo-hand functions:
bestLoRating,boardAllowsMadeLo,boardHasOneDistinctLoCard,boardHasTwoDistinctLoCards,boardLoCardCount,loRating,madeLo,minLoRating,nutLo,nutLoOuts,rateLoHand,tiesLo,winsLo - Multi-opponent random-range equities:
HvREquity,HvPerceivedRangeEquity,minHvREquity,minHvPerceivedRangeEquity - Hand-strength helpers:
fiveCardHiHandNumber,handRanking,handRankingFor,cardsPlay,upCard,outsToHandType,minOutsToHandType,nutHiOuts,fourFlush,threeFlush,toString - The higher-order
handsHavingselector
Where the implementations live
The function implementations live under openpql-runner/src/functions/. If the book lags behind, the source is the source of truth.
Equity
Equity functions estimate each player’s share of the pot at showdown, averaged over sampled runouts.
equity(player, street)
TPlayer × TStreet → TEquity
Alias for hvhEquity. Returns the player’s hand-vs-hand equity on the given street, against the union of every other declared player’s hand on that same street.
select avg(equity(hero))
from game='holdem', hero='AhKh', villain='QQ+', board='Ah9s2c'
hvhEquity(player, street)
TPlayer × TStreet → TEquity
Hand-vs-hand equity for a specific player on a given street. The board is fixed at the requested street and the remaining cards are sampled.
select avg(hvhEquity(hero, flop)),
avg(hvhEquity(hero, turn))
from game='holdem', hero='AhKh', villain='QcQd', board='Ah9s2c'
minHvHEquity(player, street, threshold) (alias minEquity)
TPlayer × TStreet × TDouble → TBoolean
Returns true when the player’s hand-vs-hand equity on the given street is at least threshold. Convenient inside where clauses or count(...) selectors:
select count(minHvHEquity(hero, flop, 0.5)) as pct_favoured
from game='holdem', hero='AhKh', villain='**'
riverEquity(player)
TPlayer → TEquity
Equity computed strictly on the river — the board is fully known, so this is exact rather than sampled. Equivalent to equity(player, river) when the board is five cards.
select avg(riverEquity(hero))
from game='holdem', hero='AwKw', villain='QQ-TT', board='2s3s7d'
fractionalRiverEquity(player)
TPlayer → TFraction
Like riverEquity, but reports the player’s exact pot share as a fraction (e.g. 1/2 for a chop, 1/3 for a three-way tie) instead of a real number.
select avg(fractionalRiverEquity(villain))
from game='holdem', hero='AhKh', villain='QQ+', board='Ah9s2c'
Tips
- Equity over a range is the combo-weighted average of per-combo equities.
- For preflop all-in spots, leave
board=''. - The runner uses Monte Carlo sampling; re-run the query to get a fresh estimate.
- For deterministic spots (5-card board), use
riverEquityto skip sampling overhead.
Hand Categories
These functions classify a player’s made hand: the broad five-card category, the more detailed flop category, and ratings for comparing hand strengths.
See Types for the full list of THandType and TFlopHandCategory values.
Flop hand categories
flopHandCategory(player)
TPlayer → TFlopHandCategory
The general flop hand category for player. Gives more specific information than handType — for instance, distinguishing top pair from middle pair, or flopping a set vs. trips.
select count(flopHandCategory(hero) = floptoppair) as pct_toppair
from game='holdem', hero='AwKw', villain='*', board='As7d2c'
exactFlopHandCategory(player, category)
TPlayer × TFlopHandCategory → TBoolean
Returns true if the player has exactly the given category on the flop. Equivalent to flopHandCategory(player) = category, expressed as a single function call.
minFlopHandCategory(player, category)
TPlayer × TFlopHandCategory → TBoolean
Returns true if the player’s flop category is at least as strong as the given one (categories are ordered).
Five-card hand types
handType(player, street)
TPlayer × TStreet → THandType
Returns the player’s best 5-card hand category on the given street. Output is one of highcard, pair, twopair, trips, straight, flush, fullhouse, quads, straightflush.
select count(handType(hero, river) = flush) as pct_flush_river
from game='holdem', hero='AwKw', villain='*', board=''
exactHandType(player, street, type)
TPlayer × TStreet × THandType → TBoolean
Returns true if the player has exactly the given hand type on that street.
minHandType(player, street, type)
TPlayer × TStreet × THandType → TBoolean
Returns true if the player has at least the given hand type. Useful for “top pair or better” filters:
select count(minHandType(hero, flop, pair)) as pct_pair_or_better
from game='holdem', hero='AxKy', villain='*', board=''
winningHandType()
→ THandType
The hand type of the winning hand on the river. If the pot is split between hands of equal type, the shared type is returned.
select count(winningHandType() = flush) as pct_winner_was_flush
from game='holdem', hero='*', villain='*'
Pocket-pair predicates
pocketPair(player)
TPlayer → TBoolean
true if the player’s hole cards contain at least two cards of the same rank.
overpair(player, street)
TPlayer × TStreet → TBoolean
true if the player has a pocket pair strictly higher than every rank on the board at the given street.
select count(overpair(hero, flop)) as pct_overpair
from game='holdem', hero='QQ+', villain='*', board='Jc7d2s'
Ratings
Ratings are opaque numeric scores you use only to compare hands within the same game. The absolute values have no external meaning.
hiRating(player, street)
TPlayer × TStreet → THiRating
Returns a rating representing the strength of the player’s best hi hand on the given street. Useful when comparing hand strengths over a sequence of streets.
bestHiRating(player, street)
TPlayer × TStreet → TBoolean
true if no other declared player has a strictly better hi hand on the given street.
maxHiRating(player, street)
TPlayer × TStreet → TBoolean
true if the player has the maximum hi rating across declared players (ties allowed).
minHiRating(player, street, rating)
TPlayer × TStreet × THiRating → TBoolean
true if the player’s hi rating equals or exceeds the given rating value.
rateHiHand(cards)
TString → THiRating
Given a 5-card string (e.g. 'AsKsQsJsTs'), returns its hi rating. Useful for building thresholds to feed into minHiRating.
select count(minHiRating(hero, river, rateHiHand('AsAdKsKdQc'))) as pct_aa_kk_or_better
from game='holdem', hero='*', villain='*'
Board Texture
Board-texture functions describe the community cards independent of any player’s hand. They are typically used inside count(...) or as where filters.
Suit profile
boardSuitCount(street)
TStreet → TCardCount
Number of distinct suits present on the board at the given street.
select avg(boardSuitCount(river))
from game='holdem', hero='As9s', villain='*', board='2s3sJh'
rainbowBoard(street)
TStreet → TBoolean
true if every board card is a different suit (boardSuitCount(street) = 3 on the flop).
twoToneBoard(street)
TStreet → TBoolean
true if exactly two suits appear on the board.
monotoneBoard(street)
TStreet → TBoolean
true if every board card is the same suit.
flushingBoard(street)
TStreet → TBoolean
true if a flush is possible using only the board — i.e. three or more cards share a suit.
Pairing and straights
pairedBoard(street)
TStreet → TBoolean
true if at least two board cards share a rank.
straightBoard(street)
TStreet → TBoolean
true if the board itself contains a 5-card straight.
Single-card accessors
turnCard()
→ TCard
The card dealt on the turn (the fourth board card).
riverCard()
→ TCard
The card dealt on the river (the fifth board card).
select count(turnCard() = toCard('As'))
from game='holdem', hero='*', villain='*', board='Kh9d2c'
Range membership
boardInRange(boardRange)
TBoardRange → TBoolean
true if the board (at the river — full 5-card form) lies in the given board-range pattern. Useful for filtering by texture without writing a chain of predicates:
select avg(equity(hero))
from game='holdem', hero='AwKw', villain='**'
where boardInRange('AwKxQyJzTw') -- straight flush boards in spades, say
Combining
Board functions compose naturally with aggregates and equity. For example:
-- Hero's equity on flushing rivers, holding suited AK
select avg(equity(hero))
from game='holdem', hero='AwKw', villain='**'
where flushingBoard(river)
Rank Utilities
Rank utilities reason about which ranks (2 through A) appear in a hand or on the board. They return either a TRankSet, a single TRank, or a count.
Set-returning
boardRanks(street)
TStreet → TRankSet
The set of distinct ranks present on the board at the given street.
handRanks(player, street)
TPlayer × TStreet → TRankSet
The set of distinct ranks in the player’s hole cards. (For Hold’em the answer doesn’t depend on the street; for stud variants — when added — it would.)
duplicatedBoardRanks(street)
TStreet → TRankSet
The set of ranks that appear more than once on the board (e.g. on a paired board).
duplicatedHandRanks(player, street)
TPlayer × TStreet → TRankSet
Ranks that appear more than once in the player’s hand. For Omaha this lets you spot pocket pairs and trips inside the four hole cards.
intersectingHandRanks(player, street)
TPlayer × TStreet → TRankSet
Hand ranks that also appear on the board.
nonIntersectingHandRanks(player, street)
TPlayer × TStreet → TRankSet
Hand ranks that do not appear on the board.
Scalar
maxRank(ranks)
TRankSet → TRank
Highest rank in the set. Errors on an empty set.
minRank(ranks)
TRankSet → TRank
Lowest rank in the set. Errors on an empty set.
nthRank(n, ranks)
TInteger × TRankSet → TRank
The n-th highest rank in the set, 1-indexed. For instance, on boardRanks(flop) = {A, K, J}:
nthRank(1, boardRanks(flop)) = A
nthRank(2, boardRanks(flop)) = K
nthRank(3, boardRanks(flop)) = J
rankCount(ranks)
TRankSet → TCardCount
Cardinality of the set (number of distinct ranks).
handBoardIntersections(player, street)
TPlayer × TStreet → TCardCount
How many of the player’s hole cards share a rank with at least one board card.
Predicates
hasTopBoardRank(player, street)
TPlayer × TStreet → TBoolean
true if the player has at least one card matching the highest rank on the board at the given street.
hasSecondBoardRank(player, street)
TPlayer × TStreet → TBoolean
true if the player has at least one card matching the second-highest board rank. Returns false on a board with only one distinct rank.
Example
Frequency hero flops top pair:
select count(hasTopBoardRank(hero, flop)) as pct_toppair
from game='holdem', hero='AxKy', villain='*', board=''
Average highest rank on the river:
select avg(maxRank(boardRanks(river)))
from game='holdem', hero='*', villain='*'
Outs
Outs functions check whether a player has live cards to a particular target on the next street.
Open PQL currently exposes the predicate-style outs functions only. The original PQL also defined
nutHiOuts,outsToHandType, andminOutsToHandType(returning counts); those are not yet implemented.
nutHi(player, street)
TPlayer × TStreet → TBoolean
true if the player currently holds the nut hi hand on the given street. Considers known dead cards when judging which hands are still possible.
select count(nutHi(hero, flop)) as pct_flopped_nuts
from game='holdem', hero='AwKw', villain='*', board=''
nutHiForHandType(player, street)
TPlayer × TStreet → TBoolean
true if the player has the best possible hand for their hand type on the given street. For instance, with hero='AsKh' and a flop of AdTd2d, the player has pair and is the best possible pair (top pair, top kicker), so nutHiForHandType(hero, flop) is true.
This is useful for distinguishing “made the absolute nuts” from “made the best version of a weaker class”.
Notes
- “Unseen” deck for these functions excludes every declared player’s holding, the board, and any
dead='…'cards. - For multi-street planning (e.g. equity on the turn given hero held a draw on the flop), combine
nutHiwith awhereclause and an aggregate selector.
Outcomes
Functions describing what happened at showdown, plus a few small helpers used to build expressions.
Showdown outcomes
winsHi(player)
TPlayer → TBoolean
true if player wins the entire hi pot outright (no other player has an equal or better hi hand).
select count(winsHi(hero)) as heroWins
from game='holdem', hero='AhKh', villain='QQ+', board='Ah9s2c'
tiesHi(player)
TPlayer → TBoolean
true if player ties for the hi half of the pot with at least one other player.
scoops(player)
TPlayer → TBoolean
true if player wins the entire pot. In a Hi-only game (which is what Open PQL currently supports), scoops and winsHi mean the same thing; the function is included for compatibility and forward-compatibility with split-pot games.
The Lo counterparts (
winsLo,tiesLo) are not yet implemented.
Range membership
inRange(player, range)
TPlayer × TRange → TBoolean
true if the cards dealt to player fall inside the given range. Useful when conditioning on a sub-range without changing the from clause:
select avg(equity(hero))
from game='holdem', hero='**', villain='**'
where inRange(hero, 'AwKw, AxKy, QQ+')
Range weights (e.g. AK@10) are ignored if present.
String-to-value helpers
toCard(s)
TString → TCard
Parses a single card from its string form, e.g. toCard('As'). Useful for comparing the result of turnCard() or riverCard() against a specific card.
toRank(s)
TString → TRank
Parses a rank string (e.g. toRank('A'), toRank('T')). Useful for comparing the output of maxRank, minRank, etc., against a literal.
select count(maxRank(boardRanks(river)) = toRank('A')) as pct_ace_high_river
from game='holdem', hero='*', villain='*'
Preflop Equity vs a Range
A common analysis: “how does my hand fare against a given range of opponent hands, before any community cards are dealt?”
Suited AK vs a Tight Open Range
A 5%-ish opening range might be QQ+, AwQw+, AxKy. Hero’s equity holding suited AK against that range:
select avg(equity(hero))
from game='holdem',
hero='AhKh',
villain='QQ+, AwQw+, AxKy',
board=''
Run it:
opql --run "select avg(equity(hero)) from game='holdem', hero='AhKh', villain='QQ+, AwQw+, AxKy', board=''"
Expect equity in the mid-to-high 40s — AK flips with a tight opener.
Hero on a Range
You can also give hero a range to estimate range-vs-range equity:
select avg(equity(hero))
from game='holdem',
hero='99+, AwJw+',
villain='QQ+, AxKy',
board=''
The result is the combo-weighted average of hero’s range equity against villain’s range.
Conditional Equity
Add a where clause to ask a conditional question — e.g. “given hero is dealt a specific sub-range, what’s the equity?”:
select avg(equity(hero))
from game='holdem', hero='**', villain='**', board=''
where inRange(hero, 'AwKw, AwQw, AwJw')
Adding a Board
To see how equity shifts postflop, add a board='…'. See Postflop Analysis.
Postflop Analysis
Once you’ve fixed a flop (or turn or river) you can ask more specific questions — frequencies of made hands, equity by street, conditional probabilities, and so on.
Flopping a Set with a Pocket Pair
How often does hero flop a set with pocket tens?
select count(flopHandCategory(hero) = flopset) as pct_set
from game='holdem', hero='TT', villain='*', board=''
board='' means we sample every flop. count(predicate) divides the trials where the predicate held by the total trial count, giving a probability.
Equity Holding a Flush Draw
Hero has a nut flush draw on the flop; what’s the showdown equity against an over-pair range?
select avg(equity(hero))
from game='holdem', hero='AhKh', villain='AA-JJ', board='Qh7h2c'
Conditional: Pair on the Turn Given Two Overcards
Given hero holds two unpaired overcards on a low flop, how often does the turn pair one of them?
select count(hasTopBoardRank(hero, turn)) as pct_pair_on_turn
from game='holdem', hero='AxKy', villain='*', board='8s5d2c'
Equity on a Specific River Texture
Average river equity for an overpair against a random hand, restricted to non-paired rivers:
select avg(riverEquity(hero))
from game='holdem', hero='QQ', villain='**', board='Jc7d2s'
where not pairedBoard(river)
Multiple Selectors at Once
Mix probabilities, equities, and counts in a single query:
select count(overpair(hero, river)) as pct_overpair_held,
count(handType(hero, river) = twopair) as pct_twopair,
avg(riverEquity(hero)) as avg_eq
from game='holdem', hero='QQ', villain='**', board='Jc7d2s'
Board Texture Studies
This tutorial shows how to measure the distribution of board textures a hand encounters.
Suit Profile on the River
Starting from a two-tone flop, how many suits appear on the river on average?
select avg(boardSuitCount(river))
from game='holdem', hero='As9s', villain='*', board='2s3sJh'
With two spades on the flop, the river often stays three-suited and sometimes narrows to two.
Frequency of Monotone Rivers
How often does the river complete a single-suit board?
select count(monotoneBoard(river)) as pct_monotone_river
from game='holdem', hero='*', villain='*', board=''
Pair Frequency by Street
select count(pairedBoard(flop)) as flop_paired,
count(pairedBoard(turn)) as turn_paired,
count(pairedBoard(river)) as river_paired
from game='holdem', hero='*', villain='*', board=''
Each selector produces an independent probability, and together they show how pairing accumulates across streets.
Straight-Possible Boards
select count(straightBoard(river)) as pct_straight_possible
from game='holdem', hero='*', villain='*', board=''
Composing Board Texture with Equity
Hero’s equity on flushing rivers, holding suited AK:
select avg(equity(hero))
from game='holdem', hero='AwKw', villain='**'
where flushingBoard(river)
A Texture-Range Filter
Use boardInRange(...) to limit the simulation to a specific class of boards:
select avg(equity(hero))
from game='holdem', hero='AwKw', villain='**', board='*'
where boardInRange('AwKxQy****') -- AKQ-high boards, any rest
Library Usage
Open PQL’s runner can be embedded in a Rust program to evaluate PQL strings without going through the CLI.
Add the Dependency
[dependencies]
openpql-runner = "0.1"
The library is exposed under the crate name opql (the Cargo package is openpql-runner, but the library target’s name is opql).
Run a Query — Stream Output
PQLRunner::run parses, compiles, and evaluates a query, streaming a human-readable report to the writers you provide:
use std::io;
use opql::PQLRunner;
fn main() -> io::Result<()> {
let query = "select avg(equity(hero)) \
from game='holdem', hero='AhKh', villain='QQ+', board='Ah9s2c'";
PQLRunner::run(query, &mut io::stdout(), &mut io::stderr())
}
The first writer receives result rows (one per selector), the second receives parse and runtime errors.
Run a Query — Structured Output
If you need the per-selector values for further processing, parse the query first and then call try_run_stmt:
use opql::PQLRunner;
use openpql_pql_parser::parse_pql;
let stmts = parse_pql(
"select avg(equity(hero)) from game='holdem', hero='AhKh', villain='QQ+', board='Ah9s2c'",
)?;
for stmt in &stmts {
let output = PQLRunner::try_run_stmt(stmt)?;
// output.values, output.n_succ, etc.
}
try_run_stmt is currently marked as a temporary API in the runner — see the source for the latest shape.
Parsing Only
The parser crates can be used independently if you want to lint PQL strings, rewrite them, or generate queries programmatically:
use openpql_pql_parser::parse_pql;
let stmts = parse_pql(
"select equity(hero) from game='holdem', hero='AA', villain='KK', board='AhKh2c'",
)?;
Range Parsing
openpql-range-parser exposes a parser for range strings like QQ+, AwKw, 77-55. Useful for validating user input before passing it into a PQL query.
API Docs
Auto-generated reference documentation lives at API Docs.
API Docs
Rustdoc-generated reference documentation for the workspace crates is built separately and published alongside this book.
openpql-runner— main library and CLI entry pointsopenpql-prelude— core card and hand typesopenpql-pql-parser— PQL grammar and ASTopenpql-range-parser— range-notation parser
Building Locally
cargo doc --no-deps --workspace --open
This generates HTML under target/doc/. Open target/doc/openpql_runner/index.html to explore the runner’s public API.
Why Separate?
This book is written for humans reading top-to-bottom, while rustdoc is a reference derived from the source. Both are useful; when they disagree, rustdoc is authoritative because it’s regenerated from the code on every commit.