PQL — Pipeline Query Language
A dplyr/pandas-style verb pipeline that compiles to safe, read-only ClickHouse SQL over the
Q-omics warehouse. Write methods don't exist in the grammar, so unsafe queries are grammatically
inexpressible. This page is the full surface behind the four pql_* tools.
01What PQL is
PQL describes a query as a chain of verbs: pick a source, then transform it with
.where(), .join(), .group_by(), .agg() and friends. The
server folds the chain into a QueryIR and emits ClickHouse SQL directly — guarded by a locked
engine policy (timeout, row cap, no writes/DDL). You don't write SQL; you compose a pipeline and call
pql_run.
# mental model — a chain of verbs, left to right Q('rna').where(gene='MKI67', lineage='BRCA').group_by('Stage').agg(mu=mean('Value'))
Q('rna') source .where(…) filter .group_by(…) group .agg(…) aggregate → compiles to read-only SQL
02The 4 tools
Same rhythm as the rest of the connector: discover, then execute.
| Tool | Layer | Purpose |
|---|---|---|
| pql_grammar | Discover | Verb signatures, aggregates, sources + cheatsheet. Learn the language. No arguments. |
| pql_schema | Discover | Conventions + source list, or one source's grain/columns/aliases. source optional. |
| pql_examples | Discover | Browse the worked corpus, or pass a NL question → one entity-filled, compilable example. complexity optional. |
| pql_run | Execute | Compile + guarded execute. Returns {columns, rows, row_count, sql, query_id, elapsed_sec} or {error}. pql required. |
pql_schema (and optionally
pql_examples) to ground itself in real sources/columns before composing a query — exactly like
resolve grounds the Guided Analysis tools.03Sources
Every pipeline starts at a source.
| Form | What it is |
|---|---|
| Q('rna') | Sugar sources over omics_data with Data_Type pinned: rna, protein_ms, protein_rppa, drug, sgrna, immune_cell, methylation, go_rna… Column aliases: gene→E_name, value→Value, lineage→Lineage_Name, sample→Sample_ID. |
| Q('q_nt') | Raw tables — curated result tables and metadata: q_nt, q_survival_single, q_cross_asso, q_cross_response, q_sl_cross, q_neoantigen, info_sample, info_mutation… |
| Input(keys=[…], vals=[…]) | A literal input vector (e.g. a gene signature), used as a join target to correlate samples against. |
Call pql_schema('<source>') for a source's grain, exposed columns (name/type/role/domain), and query aliases.
04Verbs
The full chain-method set. The parser and the internal builder both allow only these.
| Verb | Does |
|---|---|
| .where(…) | Filter. col=val, a raw 'sql expr', or a semijoin col__in=Q(…).select('c'). |
| .select(cols) | Project columns. |
| .mutate(x=…) | Add a column — bin(), case(), or a raw expr like r='a/b'. |
| .join(Q(…), on=[(L,R)], how=…) | Join another pipeline. how='any_inner'|'any_left'|'inner'. A join whose right ends in .select() = an enrich (LEFT JOIN attaching those cols). |
| .group_by(cols) | Grouping keys for the following .agg(). |
| .agg(name=fn(…)) | Aggregate. Multiple arms coexist in one call. |
| .having(expr) | Filter groups after aggregation (e.g. coverage 'n >= 100'). |
| .window(partition_by=[…], order_by=…) | Open a window; follow with .mutate(rk='row_number()'). |
| .qualify(expr) | Filter on a window result (e.g. 'rk <= 3'). |
| .arrange('-col') | Order rows (- = descending). |
| .top(n, per=[…]) | Top-N rows per group. |
| .head(n) | Limit rows. |
05Aggregates & expression functions
Statistical tests (return stat + p-value)
Tests take by='<group expr>', e.g. welch_t('Value', by='Stage >= 3').
Scalar aggregates
arg_min(arg, val) / arg_max(arg, val) return the value of arg at the row where val is extreme.
Synthesis & window
06Special forms
Conditional aggregation (ClickHouse -If)
Any scalar agg takes where='<predicate>' to aggregate only matching rows. Multiple arms
coexist; for a ratio, agg the parts then post-agg with .mutate().
.agg(mut=uniq('Sample_ID', where="E_name='KRAS' AND Variant NOT IN ('Silent','Intron','RNA')"), n=uniq('Sample_ID')).mutate(freq='mut / n')
case() → multiIf
.mutate(lbl=case(('Value > 5', "'hi'"), default="'lo'")) # conditions & values are raw SQL; self-quote labels
bin() → quantile cut
.mutate(tert=bin('Value', method='tertile'))
Semijoin & enrich
Restrict to a cohort with col__in=Q(…).select('c') (semijoin); attach columns by ending a
.join()'s right side in .select(cols) (enrich, put after agg/head).
07Examples · Distribution & ranking
All examples below are DB-verified golden queries from the pql_examples corpus.
Q('rna').where(gene='KRAS', Sample_Type='Tissue').group_by('Lineage_Name') .agg(mu=mean('Value'), med=quantile('Value'), sd=stddev('Value'), n=count()).arrange('-mu').head(30)
Q('rna').where(lineage='GBM', Sample_Type='Tissue').group_by('E_name') .agg(v=var('Value'), n=count()).having('n >= 100').arrange('-v').head(50)
Q('rna').where(Sample_Type='Tissue', Dataset='TCGA').group_by('E_name') .agg(mu=mean('Value'), sd=stddev('Value'), n=count()).having('mu >= 5', 'n >= 5000') .mutate(cv='sd / mu').arrange('cv').head(30)
08Examples · Mutations
Q('info_mutation').where(Dataset='TCGA', Lineage_Name='PAAD').group_by('Lineage_Name') .agg(mut=uniq('Sample_ID', where="E_name='KRAS' AND Variant NOT IN ('Silent','Intron','RNA')"), seq=uniq('Sample_ID')).mutate(freq='mut / seq')
Q('info_mutation').where(Dataset='TCGA').group_by('Lineage_Name') .agg(mut=uniq('Sample_ID', where="E_name='BRAF' AND Variant NOT IN ('Silent','Intron','RNA')"), n=uniq('Sample_ID')).mutate(freq='mut / n').arrange('-freq').head(20)
Q('info_mutation').where(Dataset='TCGA', Lineage_Name='SKCM').group_by('Sample_ID') .agg(burden=count(where="Variant NOT IN ('Silent','Intron','RNA')")).arrange('-burden').head(20)
09Examples · Survival & Normal-vs-Tumor
Q('q_survival_single').where(Data_Type='rna', Lineage_Name='BRCA', OS_DFS=1) .where('Sur_p < 0.05').arrange('Sur_p').head(100)
Q('q_survival_single').where(Data_Type='rna', Lineage_Name='GBM', Period=5, OS_DFS=1) .where('Sur_p < 0.05').group_by('Entity_Name') .agg(best_p=min('Sur_p'), dir=arg_min('Sur_auc1', 'Sur_p')).arrange('best_p').head(20)
Q('q_nt').where(Data_Type='rna', Lineage_Name='BRCA').where('Pvalue < 0.05') .join(Q('q_survival_single').where(Data_Type='rna', Lineage_Name='BRCA') .where('Sur_p < 0.05').select('Sur_p'), on=[('Entity_id','Entity_id'),('Lineage_Name','Lineage_Name')], how='any_inner') .arrange('-S_Cscore').head(50)
Q('rna').where(gene='TP53', lineage='BRCA') .join(Q('info_sample').where('Stage IS NOT NULL'), on=[('Sample_ID','Sample_ID'),('Lineage_Name','Lineage_Name'),('Dataset','Dataset')], how='any_inner') .agg(w=welch_t('Value', by='Stage >= 3'))
10Examples · Drug & dependency
Q('drug').where(E_name='Olaparib').arrange('Value').head(20)
Q('drug').where(E_name='Selumetinib') .where(Sample_ID__in=Q('info_mutation').where(E_name='KRAS', Sample_Type='Cell_line').select('Sample_ID')) .agg(mean_resp=mean('Value'), n=count())
Q('sgrna').where(E_name='PARP1', lineage='OV') .where(Sample_ID__in=Q('info_mutation').where(E_name='BRCA1', Sample_Type='Cell_line').select('Sample_ID')) .agg(mean_dep=mean('Value'), n=count())
11Examples · Curated multi-omics lookups
The q_cross_asso / q_cross_response tables hold pre-computed associations —
cheap point lookups, no on-the-fly stats.
Q('q_cross_asso').where(Data_Type1='protein_ms', Data_Type2='rna', X_name='EGFR', Y_name='EGFR', Lineage_Name='LUAD')
Q('q_cross_asso').where(Data_Type1='rna', Data_Type2='immune_cell', Y_name='CD8+ T-cells', Lineage_Name='SKCM').where('X_Y_p < 0.05').arrange('-X_Y_fc').head(50)
12Examples · Advanced pipelines
Q('rna').where(gene='EGFR', Sample_Type='Tissue') .window(partition_by=['Lineage_Name'], order_by='-Value') .mutate(rk='row_number()').qualify('rk <= 3')
Q('info_mutation').where(Dataset='TCGA').where("Variant NOT IN ('Silent','Intron','RNA')") .group_by('Lineage_Name', 'E_name').agg(n=uniq('Sample_ID')).top(3, per=['Lineage_Name'], by='-n')
Q('rna').join(Input(keys=['TP53','MYC','EGFR','PTEN','RB1'], vals=[1.2,-0.4,0.8,-1.1,0.3]), on=[('E_name','key')], how='inner') .group_by('Sample_Type','Lineage_Name','Sample_ID') .agg(r=corr('Value','input.val'), n=count()).having('n >= 3').arrange('-r').top(10, per=['Sample_Type'])
Q('rna').where(gene='ERBB2') .join(Q('protein_ms').where(gene='ERBB2'), on=[('Sample_ID','Sample_ID'),('Lineage_Name','Lineage_Name'),('Dataset','Dataset')], how='any_inner') .group_by('Lineage_Name').agg(r=corr('rna.Value','protein_ms.Value'))
pql_examples with a natural-language
question — it returns the closest of these templates with your entities filled in, ready to
pql_run.