Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

CratePurpose
openpql-preludeCore poker types: cards, hands, evaluators, game variants
openpql-coreGame abstraction and shared compute kernels
openpql-range-parserParser for the (generic) range notation, e.g. AwKw, 77-55
openpql-pql-parserParser for PQL syntax (LALRPOP grammar)
openpql-runnerQuery executor and the opql CLI
openpql-macroInternal procedural macros for function registration

How to Read This Book

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 cargo toolchain

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:

ClassOperators
Comparison=, <, <=, >, >=
Arithmetic+, -, *, /
Booleanand, 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:

SelectorDescriptionExample
avg(expr)Mean of a numeric expression across all trialsavg(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 trialsmax(handType(hero, river))
min(expr)Smallest value of an expression seen across trialsmin(fractionalRiverEquity(villain))

Note: a histogram selector 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 callsequity(hero), handType(hero, flop), boardSuitCount(river), …
  • Constants — numbers, single-quoted strings, hand-type and category keywords (pair, flopset, …)
  • Comparisons and boolean operatorshandType(hero, river) = flush, equity(hero) > 0.5 and hasTopBoardRank(hero, flop)
  • Arithmeticequity(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

KeyValue typeMeaning
gamegame nameWhich poker variant to play (default holdem)
boardboard rangeCommunity cards or a board pattern
deadcard listCards 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

FieldDefault if omitted
gameholdem
board* (preflop, all five board cards sampled)
deadempty
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.

NotationMeaning
AwKwA and K of the same suit (suited AK)
AxKyA and K of different suits (offsuit AK)
AKAny AK (suited or offsuit)
TTAny 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.

NotationMeaning
QQ+Pocket pairs QQ or better
88-55Pocket pairs 88 down to 55
AwJw+Suited aces from AJ up
KwQw-KwTwSuited 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

StreetBoard sizeSampled per trial
preflop0flop + turn + river
flop3turn + river
turn4river
river5nothing — 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.

ValueVariantHole cardsDeck
holdemTexas Hold’em2Full 52
omahaPot-Limit Omaha4Full 52
shortdeckShort-Deck Hold’em236 (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

TypeMeaning
TBooleantrue or false
TInteger / TLongA whole number
TDoubleA double-precision floating point number
TFractionAn exact fraction such as 1/2, 2/5, 13/914
TEquityA TDouble between 0.0 and 1.0
TNumericAny of the numeric types above
TStringA single-quoted string literal

Card / Hand Types

TypeMeaning
TCardA single card (e.g. the Jack of Diamonds)
TCardCountAn integer between 0 and 52
TRankA rank (an Ace, a Ten, a Deuce, …)
TRankSetA set of unique ranks
TStreetOne of preflop, flop, turn, river
THandTypeA 5-card hand category (see below)
TFlopHandCategoryA flop-specific hand category (see below)
THiRatingA hi-hand rating, used for comparing hand strength

Players and Ranges

TypeMeaning
TPlayerA player declared in the from clause (e.g. hero, villain)
TPlayerCountAn integer between 0 and the number of players
TRangeA range expression such as 'AwKw, 77-55'
TBoardRangeA range expression for the board, e.g. 'AwKx2y'

THandType

The 5-card hand category for a player on a given street.

  • highcard
  • pair
  • twopair
  • trips
  • straight
  • flush
  • fullhouse
  • quads
  • straightflush

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.

ValueMeaning
flopnothingNo pair, no draws of consequence (made nothing)
flopunderpairPocket pair lower than every flop card
flopthirdpairHits the third (lowest) flop rank
floppocket23Pocket pair between the second and third flop ranks
flopsecondpairHits the second flop rank
floppocket12Pocket pair between the first and second flop ranks
floptoppairHits the top flop rank
flopoverpairPocket pair larger than every flop card
flopbottomtwoHits the two lowest of three distinct flop ranks
floptopandbottomHits the top and bottom of three distinct flop ranks
floptoptwoHits the top two of three distinct flop ranks
floptripsThree of a kind on a paired flop
flopsetThree of a kind on an unpaired flop
flopstraightMade a straight
flopflushMade a flush
flopfullhouseMade a full house (takes precedence over pocket-pair categories)
flopquadsFour of a kind
flopstraightflushStraight 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.

CategoryPage
EquityEquity
Hand categories, types, and ratingsHand Categories
Board textureBoard Texture
Rank utilitiesRank Utilities
OutsOuts
Outcomes & helpersOutcomes

Common shapes

Most functions take some combination of:

  • TPlayer — an identifier declared in the from clause (hero, villain, …)
  • TStreet — one of preflop, flop, turn, river
  • THandType or TFlopHandCategory — a hand-class keyword
  • TRange / TBoardRange — a single-quoted range string
  • TRankSet — typically the result of boardRanks(...) or handRanks(...)

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:

FunctionArgument typesReturn type
bestHiRatingTPlayer, TStreetTBoolean
boardInRangeTBoardRangeTBoolean
boardRanksTStreetTRankSet
boardSuitCountTStreetTCardCount
duplicatedBoardRanksTStreetTRankSet
duplicatedHandRanksTPlayer, TStreetTRankSet
equity (alias of hvhEquity)TPlayer, TStreetTEquity
exactFlopHandCategoryTPlayer, TFlopHandCategoryTBoolean
exactHandTypeTPlayer, TStreet, THandTypeTBoolean
flopHandCategoryTPlayerTFlopHandCategory
flushingBoardTStreetTBoolean
fractionalRiverEquityTPlayerTFraction
handBoardIntersectionsTPlayer, TStreetTCardCount
handRanksTPlayer, TStreetTRankSet
handTypeTPlayer, TStreetTHandType
hasSecondBoardRankTPlayer, TStreetTBoolean
hasTopBoardRankTPlayer, TStreetTBoolean
hiRatingTPlayer, TStreetTHiRating
hvhEquityTPlayer, TStreetTEquity
inRangeTPlayer, TRangeTBoolean
intersectingHandRanksTPlayer, TStreetTRankSet
maxHiRatingTPlayer, TStreetTBoolean
maxRankTRankSetTRank
minEquity (alias of minHvHEquity)TPlayer, TStreet, TDoubleTBoolean
minFlopHandCategoryTPlayer, TFlopHandCategoryTBoolean
minHandTypeTPlayer, TStreet, THandTypeTBoolean
minHiRatingTPlayer, TStreet, THiRatingTBoolean
minHvHEquityTPlayer, TStreet, TDoubleTBoolean
minRankTRankSetTRank
monotoneBoardTStreetTBoolean
nonIntersectingHandRanksTPlayer, TStreetTRankSet
nthRankTInteger, TRankSetTRank
nutHiTPlayer, TStreetTBoolean
nutHiForHandTypeTPlayer, TStreetTBoolean
overpairTPlayer, TStreetTBoolean
pairedBoardTStreetTBoolean
pocketPairTPlayerTBoolean
rainbowBoardTStreetTBoolean
rankCountTRankSetTCardCount
rateHiHandTStringTHiRating
riverCardTCard
riverEquityTPlayerTEquity
scoopsTPlayerTBoolean
straightBoardTStreetTBoolean
tiesHiTPlayerTBoolean
toCardTStringTCard
toRankTStringTRank
turnCardTCard
twoToneBoardTStreetTBoolean
winningHandTypeTHandType
winsHiTPlayerTBoolean

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 handsHaving selector

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 riverEquity to 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, and minOutsToHandType (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 nutHi with a where clause 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 points
  • openpql-prelude — core card and hand types
  • openpql-pql-parser — PQL grammar and AST
  • openpql-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.