From 4e95381d0cd9d1ba17216387bc50a771e9e39c25 Mon Sep 17 00:00:00 2001 From: "Documenter.jl" Date: Sat, 15 Jun 2024 03:10:25 +0000 Subject: [PATCH] build based on 2459dd0 --- dev/.documenter-siteinfo.json | 2 +- dev/examples/index.html | 2 +- dev/guide/index.html | 2 +- dev/index.html | 2 +- dev/reference/index.html | 130 +++++++++--------- dev/search_index.js | 2 +- dev/test/clauses/index.html | 2 +- dev/test/index.html | 2 +- dev/test/nodes/index.html | 111 ++++++++++++++- dev/test/other/index.html | 2 +- .../index.html | 2 +- 11 files changed, 184 insertions(+), 75 deletions(-) diff --git a/dev/.documenter-siteinfo.json b/dev/.documenter-siteinfo.json index 0c078c64..6bb0b057 100644 --- a/dev/.documenter-siteinfo.json +++ b/dev/.documenter-siteinfo.json @@ -1 +1 @@ -{"documenter":{"julia_version":"1.10.4","generation_timestamp":"2024-06-15T03:00:55","documenter_version":"1.4.1"}} \ No newline at end of file +{"documenter":{"julia_version":"1.10.4","generation_timestamp":"2024-06-15T03:10:21","documenter_version":"1.4.1"}} \ No newline at end of file diff --git a/dev/examples/index.html b/dev/examples/index.html index c323ce77..bb398079 100644 --- a/dev/examples/index.html +++ b/dev/examples/index.html @@ -822,4 +822,4 @@ 10 │ 95538 2009-03-30 2009-09-02 11 │ 107680 2009-06-07 2009-07-30 12 │ 110862 2008-09-07 2010-06-07 -=# +=# diff --git a/dev/guide/index.html b/dev/guide/index.html index e445fd2c..f57ad9ad 100644 --- a/dev/guide/index.html +++ b/dev/guide/index.html @@ -758,4 +758,4 @@ 5 │ 438438 Acute myocardial infarction of a… Condition SNOMED ⋯ 6 │ 444406 Acute subendocardial infarction Condition SNOMED 6 columns omitted -=# +=# diff --git a/dev/index.html b/dev/index.html index d7d6bed5..da111c0f 100644 --- a/dev/index.html +++ b/dev/index.html @@ -1,2 +1,2 @@ -Home · FunSQL.jl
+Home · FunSQL.jl
diff --git a/dev/reference/index.html b/dev/reference/index.html index 345db6a0..e242a9cb 100644 --- a/dev/reference/index.html +++ b/dev/reference/index.html @@ -1,7 +1,7 @@ -API Reference · FunSQL.jl

API Reference

render()

FunSQL.renderMethod
render(node; tables = Dict{Symbol, SQLTable}(),
+API Reference · FunSQL.jl

API Reference

render()

FunSQL.renderMethod
render(node; tables = Dict{Symbol, SQLTable}(),
              dialect = :default,
-             cache = nothing)::SQLString

Create a SQLCatalog object and serialize the query node.

source
FunSQL.renderMethod
render(catalog::Union{SQLConnection, SQLCatalog}, node::SQLNode)::SQLString

Serialize the query node as a SQL statement.

Parameter catalog of SQLCatalog type encapsulates available database tables and the target SQL dialect. A SQLConnection object is also accepted.

Parameter node is a composite SQLNode object.

The function returns a SQLString value. The result is also cached (with the identity of node serving as the key) in the catalog cache.

Examples

julia> catalog = SQLCatalog(
+             cache = nothing)::SQLString

Create a SQLCatalog object and serialize the query node.

source
FunSQL.renderMethod
render(catalog::Union{SQLConnection, SQLCatalog}, node::SQLNode)::SQLString

Serialize the query node as a SQL statement.

Parameter catalog of SQLCatalog type encapsulates available database tables and the target SQL dialect. A SQLConnection object is also accepted.

Parameter node is a composite SQLNode object.

The function returns a SQLString value. The result is also cached (with the identity of node serving as the key) in the catalog cache.

Examples

julia> catalog = SQLCatalog(
            :person => SQLTable(:person, columns = [:person_id, :year_of_birth]),
            dialect = :postgresql);
 
@@ -13,19 +13,19 @@
   "person_1"."person_id",
   "person_1"."year_of_birth"
 FROM "person" AS "person_1"
-WHERE ("person_1"."year_of_birth" >= 1950)
source
FunSQL.renderMethod
render(dialect::Union{SQLConnection, SQLCatalog, SQLDialect},
-       clause::SQLClause)::SQLString

Serialize the syntax tree of a SQL query.

source

reflect()

FunSQL.renderMethod
render(dialect::Union{SQLConnection, SQLCatalog, SQLDialect},
+       clause::SQLClause)::SQLString

Serialize the syntax tree of a SQL query.

source

reflect()

FunSQL.reflectMethod
reflect(conn;
         schema = nothing,
         dialect = nothing,
-        cache = 256)::SQLCatalog

Retrieve the information about available database tables.

The function returns a SQLCatalog object. The catalog will be populated with the tables from the given database schema, or, if parameter schema is not set, from the default database schema (e.g., schema public for PostgreSQL).

Parameter dialect specifies the target SQLDialect. If not set, dialect will be inferred from the type of the connection object.

source

SQLConnection and SQLStatement

FunSQL.SQLConnectionType
SQLConnection(conn; catalog)

Wrap a raw database connection object together with a SQLCatalog object containing information about database tables.

source
DBInterface.connectMethod
DBInterface.connect(DB{RawConnType},
+        cache = 256)::SQLCatalog

Retrieve the information about available database tables.

The function returns a SQLCatalog object. The catalog will be populated with the tables from the given database schema, or, if parameter schema is not set, from the default database schema (e.g., schema public for PostgreSQL).

Parameter dialect specifies the target SQLDialect. If not set, dialect will be inferred from the type of the connection object.

source

SQLConnection and SQLStatement

FunSQL.SQLConnectionType
SQLConnection(conn; catalog)

Wrap a raw database connection object together with a SQLCatalog object containing information about database tables.

source
DBInterface.connectMethod
DBInterface.connect(DB{RawConnType},
                     args...;
                     schema = nothing,
                     dialect = nothing,
                     cache = 256,
-                    kws...)

Connect to the database server, call reflect to retrieve the information about available tables and return a SQLConnection object.

Extra parameters args and kws are passed to the call:

DBInterface.connect(RawConnType, args...; kws...)
source
DBInterface.executeMethod
DBInterface.execute(conn::SQLConnection, sql::SQLNode, params)
-DBInterface.execute(conn::SQLConnection, sql::SQLClause, params)

Serialize and execute the query node.

source
DBInterface.executeMethod
DBInterface.execute(conn::SQLConnection, sql::SQLNode; params...)
-DBInterface.execute(conn::SQLConnection, sql::SQLClause; params...)

Serialize and execute the query node.

source
DBInterface.prepareMethod
DBInterface.prepare(conn::SQLConnection, str::SQLString)::SQLStatement

Generate a prepared SQL statement.

source
DBInterface.prepareMethod
DBInterface.prepare(conn::SQLConnection, sql::SQLNode)::SQLStatement
-DBInterface.prepare(conn::SQLConnection, sql::SQLClause)::SQLStatement

Serialize the query node and return a prepared SQL statement.

source

SQLCatalog, SQLTable, and SQLColumn

FunSQL.SQLCatalogType
SQLCatalog(; tables = Dict{Symbol, SQLTable}(),
+                    kws...)

Connect to the database server, call reflect to retrieve the information about available tables and return a SQLConnection object.

Extra parameters args and kws are passed to the call:

DBInterface.connect(RawConnType, args...; kws...)
source
DBInterface.executeMethod
DBInterface.execute(conn::SQLConnection, sql::SQLNode, params)
+DBInterface.execute(conn::SQLConnection, sql::SQLClause, params)

Serialize and execute the query node.

source
DBInterface.executeMethod
DBInterface.execute(conn::SQLConnection, sql::SQLNode; params...)
+DBInterface.execute(conn::SQLConnection, sql::SQLClause; params...)

Serialize and execute the query node.

source
DBInterface.prepareMethod
DBInterface.prepare(conn::SQLConnection, str::SQLString)::SQLStatement

Generate a prepared SQL statement.

source
DBInterface.prepareMethod
DBInterface.prepare(conn::SQLConnection, sql::SQLNode)::SQLStatement
+DBInterface.prepare(conn::SQLConnection, sql::SQLClause)::SQLStatement

Serialize the query node and return a prepared SQL statement.

source

SQLCatalog, SQLTable, and SQLColumn

FunSQL.SQLCatalogType
SQLCatalog(; tables = Dict{Symbol, SQLTable}(),
              dialect = :default,
              cache = 256,
              metadata = nothing)
@@ -40,8 +40,8 @@
                     SQLColumn(:person_id),
                     SQLColumn(:year_of_birth),
                     SQLColumn(:location_id)),
-           dialect = SQLDialect(:postgresql))
source
FunSQL.SQLColumnType
SQLColumn(; name, metadata = nothing)
-SQLColumn(name; metadata = nothing)

SQLColumn represents a column with the given name and optional metadata.

source
FunSQL.SQLTableType
SQLTable(; qualifiers = [], name, columns, metadata = nothing)
+           dialect = SQLDialect(:postgresql))
source
FunSQL.SQLColumnType
SQLColumn(; name, metadata = nothing)
+SQLColumn(name; metadata = nothing)

SQLColumn represents a column with the given name and optional metadata.

source
FunSQL.SQLTableType
SQLTable(; qualifiers = [], name, columns, metadata = nothing)
 SQLTable(name; qualifiers = [], columns, metadata = nothing)
 SQLTable(name, columns...; qualifiers = [], metadata = nothing)

The structure of a SQL table or a table-like entity (TEMP TABLE, VIEW, etc) for use as a reference in assembling SQL queries.

The SQLTable constructor expects the table name, an optional vector containing the table schema and other qualifiers, an ordered dictionary columns that maps names to columns, and an optional metadata.

Examples

julia> person = SQLTable(qualifiers = ["public"],
                          name = "person",
@@ -51,7 +51,7 @@
          :person,
          SQLColumn(:person_id),
          SQLColumn(:year_of_birth),
-         metadata = [:is_view => false])
source

SQLDialect

SQLDialect

FunSQL.SQLDialectType
SQLDialect(; name = :default, kws...)
 SQLDialect(template::SQLDialect; kws...)
 SQLDialect(name::Symbol, kws...)
 SQLDialect(ConnType::Type)

Properties and capabilities of a particular SQL dialect.

Use SQLDialect(name::Symbol) to create one of the known dialects. The following names are recognized:

  • :mysql
  • :postgresql
  • :redshift
  • :spark
  • :sqlite
  • :sqlserver

Keyword parameters override individual properties of a dialect. For details, check the source code.

Use SQLDialect(ConnType::Type) to detect the dialect based on the type of the database connection object. The following types are recognized:

  • LibPQ.Connection
  • MySQL.Connection
  • SQLite.DB

Examples

julia> postgresql_dialect = SQLDialect(:postgresql)
@@ -60,7 +60,7 @@
 julia> postgresql_odbc_dialect = SQLDialect(:postgresql,
                                             variable_prefix = '?',
                                             variable_style = :positional)
-SQLDialect(:postgresql, variable_prefix = '?', variable_style = :POSITIONAL)
source

SQLString

FunSQL.SQLStringType
SQLString(raw; columns = nothing, vars = Symbol[])

Serialized SQL query.

Parameter columns is a vector describing the output columns.

Parameter vars is a vector of query parameters (created with Var) in the order they are expected by the DBInterface.execute() function.

Examples

julia> person = SQLTable(:person, columns = [:person_id, :year_of_birth]);
+SQLDialect(:postgresql, variable_prefix = '?', variable_style = :POSITIONAL)
source

SQLString

FunSQL.SQLStringType
SQLString(raw; columns = nothing, vars = Symbol[])

Serialized SQL query.

Parameter columns is a vector describing the output columns.

Parameter vars is a vector of query parameters (created with Var) in the order they are expected by the DBInterface.execute() function.

Examples

julia> person = SQLTable(:person, columns = [:person_id, :year_of_birth]);
 
 julia> q = From(person);
 
@@ -97,7 +97,7 @@
             ("person_1"."year_of_birth" >= $1) AND
             ("person_1"."year_of_birth" < ($1 + 10))""",
           columns = [SQLColumn(:person_id), SQLColumn(:year_of_birth)],
-          vars = [:YEAR])
source
FunSQL.packFunction
pack(sql::SQLString, vars::Union{Dict, NamedTuple})::Vector{Any}

Convert a dictionary or a named tuple of query parameters to the positional form expected by DBInterface.execute().

julia> person = SQLTable(:person, columns = [:person_id, :year_of_birth]);
+          vars = [:YEAR])
source
FunSQL.packFunction
pack(sql::SQLString, vars::Union{Dict, NamedTuple})::Vector{Any}

Convert a dictionary or a named tuple of query parameters to the positional form expected by DBInterface.execute().

julia> person = SQLTable(:person, columns = [:person_id, :year_of_birth]);
 
 julia> q = From(person) |> Where(Fun.and(Get.year_of_birth .>= Var.YEAR,
                                          Get.year_of_birth .< Var.YEAR .+ 10));
@@ -113,7 +113,7 @@
 
 julia> pack(sql, (; YEAR = 1950))
 1-element Vector{Any}:
- 1950
source

SQLNode

Agg

FunSQL.AggMethod
Agg(; over = nothing, name, args = [], filter = nothing)
+ 1950
source

SQLNode

Agg

FunSQL.AggMethod
Agg(; over = nothing, name, args = [], filter = nothing)
 Agg(name; over = nothing, args = [], filter = nothing)
 Agg(name, args...; over = nothing, filter = nothing)
 Agg.name(args...; over = nothing, filter = nothing)

An application of an aggregate function.

An Agg node must be applied to the output of a Group or a Partition node. In a Group context, it is translated to a regular aggregate function, and in a Partition context, it is translated to a window function.

Examples

Number of patients per year of birth.

julia> person = SQLTable(:person, columns = [:person_id, :year_of_birth]);
@@ -174,7 +174,7 @@
   "visit_occurrence_1"."person_id",
   "visit_occurrence_1"."visit_start_date",
   ("visit_occurrence_1"."visit_start_date" - (lag("visit_occurrence_1"."visit_start_date") OVER (PARTITION BY "visit_occurrence_1"."person_id" ORDER BY "visit_occurrence_1"."visit_start_date"))) AS "gap"
-FROM "visit_occurrence" AS "visit_occurrence_1"
source

Append

FunSQL.AppendMethod
Append(; over = nothing, args)
+FROM "visit_occurrence" AS "visit_occurrence_1"
source

Append

FunSQL.AppendMethod
Append(; over = nothing, args)
 Append(args...; over = nothing)

Append concatenates input datasets.

Only the columns that are present in every input dataset will be included to the output of Append.

An Append node is translated to a UNION ALL query:

SELECT ...
 FROM $over
 UNION ALL
@@ -199,7 +199,7 @@
 SELECT
   "observation_1"."person_id",
   "observation_1"."observation_date" AS "date"
-FROM "observation" AS "observation_1"
source

As

FunSQL.AsMethod
As(; over = nothing, name)
+FROM "observation" AS "observation_1"
source

As

FunSQL.AsMethod
As(; over = nothing, name)
 As(name; over = nothing)
 name => over

In a scalar context, As specifies the name of the output column. When applied to tabular data, As wraps the data in a nested record.

The arrow operator (=>) is a shorthand notation for As.

Examples

Show all patient IDs.

julia> person = SQLTable(:person, columns = [:person_id, :year_of_birth]);
 
@@ -221,7 +221,7 @@
   "person_1"."person_id",
   "location_1"."state"
 FROM "person" AS "person_1"
-JOIN "location" AS "location_1" ON ("person_1"."location_id" = "location_1"."location_id")
source

Bind

FunSQL.BindMethod
Bind(; over = nothing; args)
+JOIN "location" AS "location_1" ON ("person_1"."location_id" = "location_1"."location_id")
source

Bind

FunSQL.BindMethod
Bind(; over = nothing; args)
 Bind(args...; over = nothing)

The Bind node evaluates a query with parameters. Specifically, Bind provides the values for Var parameters contained in the over node.

In a scalar context, the Bind node is translated to a correlated subquery. When Bind is applied to the joinee branch of a Join node, it is translated to a JOIN LATERAL query.

Examples

Show patients with at least one visit to a heathcare provider.

julia> person = SQLTable(:person, columns = [:person_id]);
 
 julia> visit_occurrence = SQLTable(:visit_occurrence, columns = [:visit_occurrence_id, :person_id]);
@@ -264,23 +264,23 @@
   WHERE ("visit_occurrence_1"."person_id" = "person_1"."person_id")
   ORDER BY "visit_occurrence_1"."visit_start_date" DESC
   FETCH FIRST 1 ROW ONLY
-) AS "visit_1" ON TRUE
source

Define

FunSQL.DefineMethod
Define(; over; args = [])
-Define(args...; over)

The Define node adds or replaces output columns.

Examples

Show patients who are at least 16 years old.

julia> person = SQLTable(:person, columns = [:person_id, :birth_datetime]);
+) AS "visit_1" ON TRUE
source

Define

FunSQL.DefineMethod
Define(; over; args = [], before = nothing, after = nothing)
+Define(args...; over, before = nothing, after = nothing)

The Define node adds or replaces output columns.

By default, new columns are added at the end of the column list while replaced columns retain their position. Set after = true (after = <column>) to add both new and replaced columns at the end (after a specified column). Alternatively, set before = true (before = <column>) to add both new and replaced columns at the front (before the specified column).

Examples

Show patients who are at least 16 years old.

julia> person = SQLTable(:person, columns = [:person_id, :birth_datetime]);
 
 julia> q = From(:person) |>
-           Define(:age => Fun.now() .- Get.birth_datetime) |>
+           Define(:age => Fun.now() .- Get.birth_datetime, before = :birth_datetime) |>
            Where(Get.age .>= "16 years");
 
 julia> print(render(q, tables = [person]))
 SELECT
   "person_2"."person_id",
-  "person_2"."birth_datetime",
-  "person_2"."age"
+  "person_2"."age",
+  "person_2"."birth_datetime"
 FROM (
   SELECT
     "person_1"."person_id",
-    "person_1"."birth_datetime",
-    (now() - "person_1"."birth_datetime") AS "age"
+    (now() - "person_1"."birth_datetime") AS "age",
+    "person_1"."birth_datetime"
   FROM "person" AS "person_1"
 ) AS "person_2"
 WHERE ("person_2"."age" >= '16 years')

Conceal the year of birth of patients born before 1930.

julia> person = SQLTable(:person, columns = [:person_id, :year_of_birth]);
@@ -294,7 +294,7 @@
 SELECT
   "person_1"."person_id",
   (CASE WHEN ("person_1"."year_of_birth" >= 1930) THEN "person_1"."year_of_birth" ELSE NULL END) AS "year_of_birth"
-FROM "person" AS "person_1"
source

From

From

FunSQL.FromMethod
From(; source)
 From(tbl::SQLTable)
 From(name::Symbol)
 From(^)
@@ -374,7 +374,7 @@
 
 julia> print(render(q, dialect = :postgresql))
 SELECT CAST("regexp_matches_1"."captures"[1] AS INTEGER) AS "_"
-FROM regexp_matches('2,3,5,7,11', '(\d+)', 'g') AS "regexp_matches_1" ("captures")
source

Fun

FunSQL.FunMethod
Fun(; name, args = [])
+FROM regexp_matches('2,3,5,7,11', '(\d+)', 'g') AS "regexp_matches_1" ("captures")
source

Fun

FunSQL.FunMethod
Fun(; name, args = [])
 Fun(name; args = [])
 Fun(name, args...)
 Fun.name(args...)

Application of a SQL function or a SQL operator.

A Fun node is also generated by broadcasting on SQLNode objects. Names of Julia operators (==, !=, &&, ||, !) are replaced with their SQL equivalents (=, <>, and, or, not).

If name contains only symbols, or if name starts or ends with a space, the Fun node is translated to a SQL operator.

If name contains one or more ? characters, it serves as a template of a SQL expression where ? symbols are replaced with the given arguments. Use ?? to represent a literal ? mark. Wrap the template in parentheses if this is necessary to make the SQL expression unambiguous.

Certain names have a customized translation in order to generate common SQL functions and operators with irregular syntax:

Fun nodeSQL syntax
Fun.and(p₁, p₂, …)p₁ AND p₂ AND …
Fun.between(x, y, z)x BETWEEN y AND z
Fun.case(p, x, …)CASE WHEN p THEN x … END
Fun.cast(x, "TYPE")CAST(x AS TYPE)
Fun.concat(s₁, s₂, …)dialect-specific, e.g., (s₁ || s₂ || …)
Fun.current_date()CURRENT_DATE
Fun.current_timestamp()CURRENT_TIMESTAMP
Fun.exists(q)EXISTS q
Fun.extract("FIELD", x)EXTRACT(FIELD FROM x)
Fun.in(x, q)x IN q
Fun.in(x, y₁, y₂, …)x IN (y₁, y₂, …)
Fun.is_not_null(x)x IS NOT NULL
Fun.is_null(x)x IS NULL
Fun.like(x, y)x LIKE y
Fun.not(p)NOT p
Fun.not_between(x, y, z)x NOT BETWEEN y AND z
Fun.not_exists(q)NOT EXISTS q
Fun.not_in(x, q)x NOT IN q
Fun.not_in(x, y₁, y₂, …)x NOT IN (y₁, y₂, …)
Fun.not_like(x, y)x NOT LIKE y
Fun.or(p₁, p₂, …)p₁ OR p₂ OR …

Examples

Replace missing values with N/A.

julia> location = SQLTable(:location, columns = [:location_id, :city]);
@@ -415,7 +415,7 @@
 
 julia> print(render(q, tables = [location]))
 SELECT SUBSTRING("location_1"."zip" FROM 1 FOR 3) AS "_"
-FROM "location" AS "location_1"
source

Get

Get

FunSQL.GetMethod
Get(; over, name)
 Get(name; over)
 Get.name        Get."name"      Get[name]       Get["name"]
 over.name       over."name"     over[name]      over["name"]
@@ -440,7 +440,7 @@
   "person_1"."person_id",
   "location_1"."state"
 FROM "person" AS "person_1"
-JOIN "location" AS "location_1" ON ("person_1"."location_id" = "location_1"."location_id")
source

Group

FunSQL.GroupMethod
Group(; over, by = [], sets = sets, name = nothing)
+JOIN "location" AS "location_1" ON ("person_1"."location_id" = "location_1"."location_id")
source

Group

FunSQL.GroupMethod
Group(; over, by = [], sets = sets, name = nothing)
 Group(by...; over, sets = sets, name = nothing)

The Group node summarizes the input dataset.

Specifically, Group outputs all unique values of the given grouping key. This key partitions the input rows into disjoint groups that are summarized by aggregate functions Agg applied to the output of Group. The parameter sets specifies the grouping sets, either with grouping mode indicators :cube or :rollup, or explicitly as Vector{Vector{Symbol}}. An optional parameter name specifies the field to hold the group.

The Group node is translated to a SQL query with a GROUP BY clause:

SELECT ...
 FROM $over
 GROUP BY $by...

Examples

Total number of patients.

julia> person = SQLTable(:person, columns = [:person_id, :year_of_birth]);
@@ -491,13 +491,13 @@
 
 julia> print(render(q, tables = [location]))
 SELECT DISTINCT "location_1"."state"
-FROM "location" AS "location_1"
source

Highlight

Highlight

FunSQL.HighlightMethod
Highlight(; over = nothing; color)
 Highlight(color; over = nothing)

Highlight over with the given color.

The highlighted node is printed with the selected color when the query containing it is displayed.

Available colors can be found in Base.text_colors.

Examples

julia> q = From(:person) |>
            Select(Get.person_id |> Highlight(:bold))
 let q1 = From(:person),
     q2 = q1 |> Select(Get.person_id)
     q2
-end
source

Iterate

Iterate

FunSQL.IterateMethod
Iterate(; over = nothing, iterator)
 Iterate(iterator; over = nothing)

Iterate generates the concatenated output of an iterated query.

The over query is evaluated first. Then the iterator query is repeatedly applied: to the output of over, then to the output of its previous run, and so on, until the iterator produces no data. All these outputs are concatenated to generate the output of Iterate.

The iterator query may explicitly refer to the output of the previous run using From(^) notation.

The Iterate node is translated to a recursive common table expression:

WITH RECURSIVE iterator AS (
   SELECT ...
   FROM $over
@@ -545,7 +545,7 @@
 SELECT
   "__3"."n",
   "__3"."f"
-FROM "__1" AS "__3"
source

Join

FunSQL.JoinMethod
Join(; over = nothing, joinee, on, left = false, right = false, optional = false)
+FROM "__1" AS "__3"
source

Join

FunSQL.JoinMethod
Join(; over = nothing, joinee, on, left = false, right = false, optional = false)
 Join(joinee; over = nothing, on, left = false, right = false, optional = false)
 Join(joinee, on; over = nothing, left = false, right = false, optional = false)

Join correlates two input datasets.

The Join node is translated to a query with a JOIN clause:

SELECT ...
 FROM $over
@@ -563,7 +563,7 @@
   "person_1"."person_id",
   "location_1"."state"
 FROM "person" AS "person_1"
-JOIN "location" AS "location_1" ON ("person_1"."location_id" = "location_1"."location_id")
source

Limit

FunSQL.LimitMethod
Limit(; over = nothing, offset = nothing, limit = nothing)
+JOIN "location" AS "location_1" ON ("person_1"."location_id" = "location_1"."location_id")
source

Limit

FunSQL.LimitMethod
Limit(; over = nothing, offset = nothing, limit = nothing)
 Limit(limit; over = nothing, offset = nothing)
 Limit(offset, limit; over = nothing)
 Limit(start:stop; over = nothing)

The Limit node skips the first offset rows and then emits the next limit rows.

To make the output deterministic, Limit must be applied directly after an Order node.

The Limit node is translated to a query with a LIMIT or a FETCH clause:

SELECT ...
@@ -581,7 +581,7 @@
   "person_1"."year_of_birth"
 FROM "person" AS "person_1"
 ORDER BY "person_1"."year_of_birth"
-FETCH FIRST 1 ROW ONLY
source

Lit

Lit

FunSQL.LitMethod
Lit(; val)
 Lit(val)

A SQL literal.

In a context where a SQL node is expected, missing, numbers, strings, and datetime values are automatically converted to SQL literals.

Examples

julia> q = Select(:null => missing,
                   :boolean => true,
                   :integer => 42,
@@ -594,7 +594,7 @@
   TRUE AS "boolean",
   42 AS "integer",
   'SQL is fun!' AS "text",
-  '2000-01-01' AS "date"
source

Order

Order

FunSQL.OrderMethod
Order(; over = nothing, by)
 Order(by...; over = nothing)

Order sorts the input rows by the given key.

The Ordernode is translated to a query with an ORDER BY clause:

SELECT ...
 FROM $over
 ORDER BY $by...

Specify the sort order with Asc, Desc, or Sort.

Examples

List patients ordered by their age.

julia> person = SQLTable(:person, columns = [:person_id, :year_of_birth]);
@@ -607,7 +607,7 @@
   "person_1"."person_id",
   "person_1"."year_of_birth"
 FROM "person" AS "person_1"
-ORDER BY "person_1"."year_of_birth"
source

Over

FunSQL.OverMethod
Over(; over = nothing, arg, materialized = nothing)
+ORDER BY "person_1"."year_of_birth"
source

Over

FunSQL.OverMethod
Over(; over = nothing, arg, materialized = nothing)
 Over(arg; over = nothing, materialized = nothing)

base |> Over(arg) is an alias for With(base, over = arg).

Examples

julia> person = SQLTable(:person, columns = [:person_id, :year_of_birth]);
 
 julia> condition_occurrence =
@@ -635,7 +635,7 @@
 WHERE ("person_1"."person_id" IN (
   SELECT "essential_hypertension_2"."person_id"
   FROM "essential_hypertension_1" AS "essential_hypertension_2"
-))
source

Partition

FunSQL.PartitionMethod
Partition(; over, by = [], order_by = [], frame = nothing, name = nothing)
+))
source

Partition

FunSQL.PartitionMethod
Partition(; over, by = [], order_by = [], frame = nothing, name = nothing)
 Partition(by...; over, order_by = [], frame = nothing, name = nothing)

The Partition node relates adjacent rows.

Specifically, Partition specifies how to relate each row to the adjacent rows in the same dataset. The rows are partitioned by the given key and ordered within each partition using order_by key. The parameter frame customizes the extent of related rows. These related rows are summarized by aggregate functions Agg applied to the output of Partition. An optional parameter name specifies the field to hold the partition.

The Partition node is translated to a query with a WINDOW clause:

SELECT ...
 FROM $over
 WINDOW w AS (PARTITION BY $by... ORDER BY $order_by...)

Examples

Enumerate patients' visits.

julia> visit_occurrence =
@@ -673,7 +673,7 @@
   "person_1"."year_of_birth",
   (avg(count(*)) OVER (ORDER BY "person_1"."year_of_birth" RANGE BETWEEN 1 PRECEDING AND 1 FOLLOWING)) AS "avg"
 FROM "person" AS "person_1"
-GROUP BY "person_1"."year_of_birth"
source

Select

Select

FunSQL.SelectMethod
Select(; over; args)
 Select(args...; over)

The Select node specifies the output columns.

SELECT $args...
 FROM $over

Set the column labels with As.

Examples

List patient IDs and their age.

julia> person = SQLTable(:person, columns = [:person_id, :birth_datetime]);
 
@@ -685,7 +685,7 @@
 SELECT
   "person_1"."person_id",
   (now() - "person_1"."birth_datetime") AS "age"
-FROM "person" AS "person_1"
source

Sort, Asc, and Desc

FunSQL.AscMethod
Asc(; over = nothing, nulls = nothing)

Ascending order indicator.

source
FunSQL.DescMethod
Desc(; over = nothing, nulls = nothing)

Descending order indicator.

source
FunSQL.SortMethod
Sort(; over = nothing, value, nulls = nothing)
+FROM "person" AS "person_1"
source

Sort, Asc, and Desc

FunSQL.AscMethod
Asc(; over = nothing, nulls = nothing)

Ascending order indicator.

source
FunSQL.DescMethod
Desc(; over = nothing, nulls = nothing)

Descending order indicator.

source
FunSQL.SortMethod
Sort(; over = nothing, value, nulls = nothing)
 Sort(value; over = nothing, nulls = nothing)
 Asc(; over = nothing, nulls = nothing)
 Desc(; over = nothing, nulls = nothing)

Sort order indicator.

Use with Order or Partition nodes.

Examples

List patients ordered by their age.

julia> person = SQLTable(:person, columns = [:person_id, :year_of_birth]);
@@ -698,7 +698,7 @@
   "person_1"."person_id",
   "person_1"."year_of_birth"
 FROM "person" AS "person_1"
-ORDER BY "person_1"."year_of_birth" DESC NULLS FIRST
source

Var

FunSQL.VarMethod
Var(; name)
+ORDER BY "person_1"."year_of_birth" DESC NULLS FIRST
source

Var

FunSQL.VarMethod
Var(; name)
 Var(name)
 Var.name        Var."name"      Var[name]       Var["name"]

A reference to a query parameter.

Specify the value for the parameter with Bind to create a correlated subquery or a lateral join.

Examples

julia> person = SQLTable(:person, columns = [:person_id, :year_of_birth]);
 
@@ -710,7 +710,7 @@
   "person_1"."person_id",
   "person_1"."year_of_birth"
 FROM "person" AS "person_1"
-WHERE ("person_1"."year_of_birth" > :YEAR)
source

Where

FunSQL.WhereMethod
Where(; over = nothing, condition)
+WHERE ("person_1"."year_of_birth" > :YEAR)
source

Where

FunSQL.WhereMethod
Where(; over = nothing, condition)
 Where(condition; over = nothing)

The Where node filters the input rows by the given condition.

Where is translated to a SQL query with a WHERE clause:

SELECT ...
 FROM $over
 WHERE $condition

Examples

julia> person = SQLTable(:person, columns = [:person_id, :year_of_birth]);
@@ -723,7 +723,7 @@
   "person_1"."person_id",
   "person_1"."year_of_birth"
 FROM "person" AS "person_1"
-WHERE ("person_1"."year_of_birth" > 2000)
source

With

FunSQL.WithMethod
With(; over = nothing, args, materialized = nothing)
+WHERE ("person_1"."year_of_birth" > 2000)
source

With

FunSQL.WithMethod
With(; over = nothing, args, materialized = nothing)
 With(args...; over = nothing, materialized = nothing)

With assigns a name to a temporary dataset. The dataset content can be retrieved within the over query using the From node.

With is translated to a common table expression:

WITH $args...
 SELECT ...
 FROM $over

Examples

julia> person = SQLTable(:person, columns = [:person_id, :year_of_birth]);
@@ -753,7 +753,7 @@
 WHERE ("person_1"."person_id" IN (
   SELECT "essential_hypertension_2"."person_id"
   FROM "essential_hypertension_1" AS "essential_hypertension_2"
-))
source

WithExternal

WithExternal

FunSQL.WithExternalMethod
WithExternal(; over = nothing, args, qualifiers = [], handler = nothing)
 WithExternal(args...; over = nothing, qualifiers = [], handler = nothing)

WithExternal assigns a name to a temporary dataset. The dataset content can be retrieved within the over query using the From node.

The definition of the dataset is converted to a Pair{SQLTable, SQLClause} object and sent to handler, which can use it, for instance, to construct a SELECT INTO statement.

Examples

julia> person = SQLTable(:person, columns = [:person_id, :year_of_birth]);
 
 julia> condition_occurrence =
@@ -786,7 +786,7 @@
 WHERE ("person_1"."person_id" IN (
   SELECT "essential_hypertension_1"."person_id"
   FROM "essential_hypertension" AS "essential_hypertension_1"
-))
source

SQLClause

AGG

FunSQL.AGGMethod
AGG(; name, args = [], filter = nothing, over = nothing)
+))
source

SQLClause

AGG

FunSQL.AGGMethod
AGG(; name, args = [], filter = nothing, over = nothing)
 AGG(name; args = [], filter = nothing, over = nothing)
 AGG(name, args...; filter = nothing, over = nothing)

An application of an aggregate function.

Examples

julia> c = AGG(:max, :year_of_birth);
 
@@ -797,19 +797,19 @@
 (count(*) FILTER (WHERE ("year_of_birth" > 1970)))
julia> c = AGG(:row_number, over = PARTITION(:year_of_birth));
 
 julia> print(render(c))
-(row_number() OVER (PARTITION BY "year_of_birth"))
source

AS

FunSQL.ASMethod
AS(; over = nothing, name, columns = nothing)
+(row_number() OVER (PARTITION BY "year_of_birth"))
source

AS

FunSQL.ASMethod
AS(; over = nothing, name, columns = nothing)
 AS(name; over = nothing, columns = nothing)

An AS clause.

Examples

julia> c = ID(:person) |> AS(:p);
 
 julia> print(render(c))
 "person" AS "p"
julia> c = ID(:person) |> AS(:p, columns = [:person_id, :year_of_birth]);
 
 julia> print(render(c))
-"person" AS "p" ("person_id", "year_of_birth")
source

FROM

FunSQL.FROMMethod
FROM(; over = nothing)
+"person" AS "p" ("person_id", "year_of_birth")
source

FROM

FunSQL.FROMMethod
FROM(; over = nothing)
 FROM(over)

A FROM clause.

Examples

julia> c = ID(:person) |> AS(:p) |> FROM() |> SELECT((:p, :person_id));
 
 julia> print(render(c))
 SELECT "p"."person_id"
-FROM "person" AS "p"
source

FUN

FUN

FunSQL.FUNMethod
FUN(; name, args = [])
 FUN(name; args = [])
 FUN(name, args...)

An invocation of a SQL function or a SQL operator.

Examples

julia> c = FUN(:concat, :city, ", ", :state);
 
@@ -820,7 +820,7 @@
 ("city" || ', ' || "state")
julia> c = FUN("SUBSTRING(? FROM ? FOR ?)", :zip, 1, 3);
 
 julia> print(render(c))
-SUBSTRING("zip" FROM 1 FOR 3)
source

GROUP

FunSQL.GROUPMethod
GROUP(; over = nothing, by = [], sets = nothing)
+SUBSTRING("zip" FROM 1 FOR 3)
source

GROUP

FunSQL.GROUPMethod
GROUP(; over = nothing, by = [], sets = nothing)
 GROUP(by...; over = nothing, sets = nothing)

A GROUP BY clause.

Examples

julia> c = FROM(:person) |>
            GROUP(:year_of_birth) |>
            SELECT(:year_of_birth, AGG(:count));
@@ -830,7 +830,7 @@
   "year_of_birth",
   count(*)
 FROM "person"
-GROUP BY "year_of_birth"
source

HAVING

HAVING

FunSQL.HAVINGMethod
HAVING(; over = nothing, condition)
 HAVING(condition; over = nothing)

A HAVING clause.

Examples

julia> c = FROM(:person) |>
            GROUP(:year_of_birth) |>
            HAVING(FUN(">", AGG(:count), 10)) |>
@@ -840,7 +840,7 @@
 SELECT "person_id"
 FROM "person"
 GROUP BY "year_of_birth"
-HAVING (count(*) > 10)
source

ID

FunSQL.IDMethod
ID(; over = nothing, name)
+HAVING (count(*) > 10)
source

ID

FunSQL.IDMethod
ID(; over = nothing, name)
 ID(name; over = nothing)
 ID(qualifiers, name)

A SQL identifier. Specify over or use the |> operator to make a qualified identifier.

Examples

julia> c = ID(:person);
 
@@ -851,7 +851,7 @@
 "p"."birth_datetime"
julia> c = ID([:pg_catalog], :pg_database);
 
 julia> print(render(c))
-"pg_catalog"."pg_database"
source

JOIN

FunSQL.JOINMethod
JOIN(; over = nothing, joinee, on, left = false, right = false, lateral = false)
+"pg_catalog"."pg_database"
source

JOIN

FunSQL.JOINMethod
JOIN(; over = nothing, joinee, on, left = false, right = false, lateral = false)
 JOIN(joinee; over = nothing, on, left = false, right = false, lateral = false)
 JOIN(joinee, on; over = nothing, left = false, right = false, lateral = false)

A JOIN clause.

Examples

julia> c = FROM(:p => :person) |>
            JOIN(:l => :location,
@@ -864,7 +864,7 @@
   "p"."person_id",
   "l"."state"
 FROM "person" AS "p"
-LEFT JOIN "location" AS "l" ON ("p"."location_id" = "l"."location_id")
source

LIMIT

FunSQL.LIMITMethod
LIMIT(; over = nothing, offset = nothing, limit = nothing, with_ties = false)
+LEFT JOIN "location" AS "l" ON ("p"."location_id" = "l"."location_id")
source

LIMIT

FunSQL.LIMITMethod
LIMIT(; over = nothing, offset = nothing, limit = nothing, with_ties = false)
 LIMIT(limit; over = nothing, offset = nothing, with_ties = false)
 LIMIT(offset, limit; over = nothing, with_ties = false)
 LIMIT(start:stop; over = nothing, with_ties = false)

A LIMIT clause.

Examples

julia> c = FROM(:person) |>
@@ -874,21 +874,21 @@
 julia> print(render(c))
 SELECT "person_id"
 FROM "person"
-FETCH FIRST 1 ROW ONLY
source

LIT

LIT

FunSQL.LITMethod
LIT(; val)
 LIT(val)

A SQL literal.

In a context of a SQL clause, missing, numbers, strings and datetime values are automatically converted to SQL literals.

Examples

julia> c = LIT(missing);
 
 julia> print(render(c))
 NULL
julia> c = LIT("SQL is fun!");
 
 julia> print(render(c))
-'SQL is fun!'
source

NOTE

FunSQL.NOTEMethod
NOTE(; over = nothing, text, postfix = false)
+'SQL is fun!'
source

NOTE

FunSQL.NOTEMethod
NOTE(; over = nothing, text, postfix = false)
 NOTE(text; over = nothing, postfix = false)

A free-form prefix of postfix annotation.

Examples

julia> c = FROM(:p => :person) |>
            NOTE("TABLESAMPLE SYSTEM (50)", postfix = true) |>
            SELECT((:p, :person_id));
 
 julia> print(render(c))
 SELECT "p"."person_id"
-FROM "person" AS "p" TABLESAMPLE SYSTEM (50)
source

ORDER

FunSQL.ORDERMethod
ORDER(; over = nothing, by = [])
+FROM "person" AS "p" TABLESAMPLE SYSTEM (50)
source

ORDER

FunSQL.ORDERMethod
ORDER(; over = nothing, by = [])
 ORDER(by...; over = nothing)

An ORDER BY clause.

Examples

julia> c = FROM(:person) |>
            ORDER(:year_of_birth) |>
            SELECT(:person_id);
@@ -896,7 +896,7 @@
 julia> print(render(c))
 SELECT "person_id"
 FROM "person"
-ORDER BY "year_of_birth"
source

PARTITION

FunSQL.PARTITIONMethod
PARTITION(; over = nothing, by = [], order_by = [], frame = nothing)
+ORDER BY "year_of_birth"
source

PARTITION

FunSQL.PARTITIONMethod
PARTITION(; over = nothing, by = [], order_by = [], frame = nothing)
 PARTITION(by...; over = nothing, order_by = [], frame = nothing)

A window definition clause.

Examples

julia> c = FROM(:person) |>
            SELECT(:person_id,
                   AGG(:row_number, over = PARTITION(:year_of_birth)));
@@ -930,7 +930,7 @@
   "year_of_birth",
   (avg(count(*)) OVER (ORDER BY "year_of_birth" RANGE BETWEEN 1 PRECEDING AND 1 FOLLOWING))
 FROM "person"
-GROUP BY "year_of_birth"
source

SELECT

FunSQL.SELECTMethod
SELECT(; over = nothing, top = nothing, distinct = false, args)
+GROUP BY "year_of_birth"
source

SELECT

FunSQL.SELECTMethod
SELECT(; over = nothing, top = nothing, distinct = false, args)
 SELECT(args...; over = nothing, top = nothing, distinct = false)

A SELECT clause. Unlike raw SQL, SELECT() should be placed at the end of a clause chain.

Set distinct to true to add a DISTINCT modifier.

Examples

julia> c = SELECT(true, false);
 
 julia> print(render(c))
@@ -941,7 +941,7 @@
 
 julia> print(render(c))
 SELECT DISTINCT "zip"
-FROM "location"
source

SORT, ASC, and DESC

FunSQL.ASCMethod
ASC(; over = nothing, nulls = nothing)

Ascending order indicator.

source
FunSQL.DESCMethod
DESC(; over = nothing, nulls = nothing)

Descending order indicator.

source
FunSQL.SORTMethod
SORT(; over = nothing, value, nulls = nothing)
+FROM "location"
source

SORT, ASC, and DESC

FunSQL.ASCMethod
ASC(; over = nothing, nulls = nothing)

Ascending order indicator.

source
FunSQL.DESCMethod
DESC(; over = nothing, nulls = nothing)

Descending order indicator.

source
FunSQL.SORTMethod
SORT(; over = nothing, value, nulls = nothing)
 SORT(value; over = nothing, nulls = nothing)
 ASC(; over = nothing, nulls = nothing)
 DESC(; over = nothing, nulls = nothing)

Sort order options.

Examples

julia> c = FROM(:person) |>
@@ -951,7 +951,7 @@
 julia> print(render(c))
 SELECT "person_id"
 FROM "person"
-ORDER BY "year_of_birth" DESC
source

UNION

FunSQL.UNIONMethod
UNION(; over = nothing, all = false, args)
+ORDER BY "year_of_birth" DESC
source

UNION

FunSQL.UNIONMethod
UNION(; over = nothing, all = false, args)
 UNION(args...; over = nothing, all = false)

A UNION clause.

Examples

julia> c = FROM(:measurement) |>
            SELECT(:person_id, :date => :measurement_date) |>
            UNION(all = true,
@@ -967,18 +967,18 @@
 SELECT
   "person_id",
   "observation_date" AS "date"
-FROM "observation"
source

VALUES

VALUES

FunSQL.VALUESMethod
VALUES(; rows)
 VALUES(rows)

A VALUES clause.

Examples

julia> c = VALUES([("SQL", 1974), ("Julia", 2012), ("FunSQL", 2021)]);
 
 julia> print(render(c))
 VALUES
   ('SQL', 1974),
   ('Julia', 2012),
-  ('FunSQL', 2021)
source

VAR

VAR

FunSQL.VARMethod
VAR(; name)
 VAR(name)

A placeholder in a parameterized query.

Examples

julia> c = VAR(:year);
 
 julia> print(render(c))
-:year
source

WHERE

WHERE

FunSQL.WHEREMethod
WHERE(; over = nothing, condition)
 WHERE(condition; over = nothing)

A WHERE clause.

Examples

julia> c = FROM(:location) |>
            WHERE(FUN("=", :zip, "60614")) |>
            SELECT(:location_id);
@@ -986,7 +986,7 @@
 julia> print(render(c))
 SELECT "location_id"
 FROM "location"
-WHERE ("zip" = '60614')
source

WINDOW

WINDOW

FunSQL.WINDOWMethod
WINDOW(; over = nothing, args)
 WINDOW(args...; over = nothing)

A WINDOW clause.

Examples

julia> c = FROM(:person) |>
            WINDOW(:w1 => PARTITION(:year_of_birth),
                   :w2 => :w1 |> PARTITION(order_by = [:month_of_birth, :day_of_birth])) |>
@@ -999,7 +999,7 @@
 FROM "person"
 WINDOW
   "w1" AS (PARTITION BY "year_of_birth"),
-  "w2" AS ("w1" ORDER BY "month_of_birth", "day_of_birth")
source

WITH

FunSQL.WITHMethod
WITH(; over = nothing, recursive = false, args)
+  "w2" AS ("w1" ORDER BY "month_of_birth", "day_of_birth")
source

WITH

FunSQL.WITHMethod
WITH(; over = nothing, recursive = false, args)
 WITH(args...; over = nothing, recursive = false)

A WITH clause.

Examples

julia> c = FROM(:person) |>
            WHERE(FUN(:in, :person_id,
                           FROM(:essential_hypertension) |>
@@ -1056,4 +1056,4 @@
   WHERE ("cr"."relationship_id" = 'Subsumes')
 )
 SELECT *
-FROM "essential_hypertension"
source
+FROM "essential_hypertension"
source
diff --git a/dev/search_index.js b/dev/search_index.js index 92f58190..05758339 100644 --- a/dev/search_index.js +++ b/dev/search_index.js @@ -1,3 +1,3 @@ var documenterSearchIndex = {"docs": -[{"location":"two-kinds-of-sql-query-builders/#Two-Kinds-of-SQL-Query-Builders","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"","category":"section"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"CurrentModule = FunSQL","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"The SQL language has a paradoxical fate. Although it was deliberately designed to appeal to a human user, nowadays most of SQL code is written—or rather generated—by the computer. Many computer programs need to query some database, and, for the vast majority of database servers, the only supported query language is SQL. But generating SQL is difficult because of the complicated and obscure rules of its quasi-English grammar (its original name SEQUEL stands for Structured English Query Language). For this reason, programs that interact with a database often use specialized libraries for generating SQL queries.","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"One of such libraries is FunSQL. FunSQL is designed with two goals in mind: supporting the full range of SQL's querying capabilities and exposing these capabilities in a compositional, data-oriented interface. This combination of goals makes FunSQL a perfect tool for data analysis in SQL and differentiates it from all the other query building libraries. Many query builders offer good coverage of SQL features, fewer provide data-oriented interface, but only FunSQL combines them in a single package.","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"And yet the difference between FunSQL and other query builders is not immediately apparent. In fact, the interfaces of various query building libraries seem almost identical. A query that finds 100 oldest male patients (in the OMOP CDM database) is assembled with FunSQL as follows:","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"From(:person) |>\nWhere(Get.gender_concept_id .== 8507) |>\nOrder(Get.year_of_birth) |>\nLimit(100) |>\nSelect(Get.person_id)","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"The same query can be written in Ruby using Active Record Query Interface:","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"Person\n.where(\"gender_concept_id = ?\", 8507)\n.order(:year_of_birth)\n.limit(100)\n.select(:person_id)","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"Or in PHP with Laravel's Query Builder:","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"DB::table('person')\n->where('gender_concept_id', '=', 8507)\n->orderBy('year_of_birth')\n->limit(100)\n->select('person_id')","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"In C#'s EF/LINQ:","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"Person\n.Where(p => p.gender_concept_id == 8507)\n.OrderBy(p => p.year_of_birth)\n.Take(100)\n.Select(p => new { person_id = p.person_id });","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"Or in R with dbplyr:","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"tbl(conn, \"person\") %>%\nfilter(gender_concept_id == 8507) %>%\narrange(year_of_birth) %>%\nhead(100) %>%\nselect(person_id)","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"In each of these code samples, the query is assembled using essentially the same interface. Stripped of its syntactic shell, the process of assembling the query can be visualized as a diagram of five processing nodes connected in a pipeline:","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"(Image: 100 oldest male patients)","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"It is precisely the fact that the query is progressively assembled using atomic, independent components that lets us call this interface compositional.","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"However we did claim that FunSQL differs from all the other query building libraries, and now apparently proved the opposite? As a matter of fact, there is a difference, even if it is not reflected in notation. To demonstrate this, let us rearrange this pipeline, moving the Order and the Limit nodes in front of Where.","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"(Image: 100 oldest male patients ⟹ Males among 100 oldest patients)","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"How does this rearrangement affect the output of the query? Perhaps unexpectedly, the answer depends on the library. With FunSQL, as well as EF/LINQ and dbplyr, it changes the output from 100 oldest male patients to the males among 100 oldest patients. But not so with the other two libraries, Active Record and Laravel, where rearranging the pipeline has no effect on the output.","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"To summarize, the following query builders are sensitive to the order of the pipeline nodes:","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"FunSQL\nEF/LINQ\ndbplyr","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"And the following are not:","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"Active Record\nLaravel","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"These are the two kinds of query builders from this article's title. But how can these libraries act so differently while sharing the same interface? To answer this question, we need to focus on what is only implicitly present on the pipeline diagram: the information that is processed by the pipeline nodes.","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"(Image: \"Where\" node)","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"A node with one incoming and one outgoing arrow symbolizes a processing unit that takes the input data, transforms it, and emits the output data. While the character of the data is not revealed, it is tempting to assume it to be the tabular data extracted from the database.","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"(Image: \"Where\" node acting on data)","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"But this can't be right, at least not literally, because a SQL query builder cannot read the data in the database. Instead, the query builder generates a SQL query:","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"SELECT \"person_1\".\"person_id\"\nFROM \"person\" AS \"person_1\"\nWHERE (\"person_1\".\"gender_concept_id\" = 8507)\nORDER BY \"person_1\".\"year_of_birth\"\nLIMIT 100","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"But if we assume for a moment that pipeline nodes could process the data directly, we would expect that both the pipeline and the corresponding SQL query produce the same output. In other words, the role of the pipeline is to specify the expected output of the SQL query. This is how pipeline nodes are interpreted by FunSQL and the other two libraries, EF/LINQ and dbplyr. We can call such query builders data-oriented.","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"The conversion of the pipeline to SQL is not always that straightforward. Even though we could freely reorder the nodes in a pipeline, we cannot do the same to the clauses in a SQL query. This is because the SQL grammar arranges the clauses in a rigid order:","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"FROM, followed by zero, one or more\nJOIN, followed by\nWHERE, followed by\nGROUP BY, followed by\nHAVING, followed by\nORDER BY, followed by\nLIMIT, followed by\nSELECT, written at the top of the query, but the last one to perform.","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"This order is compatible with the first pipeline, in which the Where node is followed by Order and Limit, but not the second pipeline, where these nodes change their relative positions. So how could the second pipeline be converted to SQL? We would be out of options if we were still using the original SQL standard, SQL-86, but the next revision of the language, SQL-92, recognized this limitation. Regrettably, it did not relax this rigid clause order. Instead, SQL-92 introduced a workaround: a query can be extended by nesting it into the next query's FROM clause. This gives us a method for converting an arbitrary pipeline into SQL: break the pipeline into smaller chunks that comply with the SQL clause order, convert each chunk into a SQL query, and then nest all these queries together:","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"SELECT \"person_2\".\"person_id\"\nFROM (\n SELECT\n \"person_1\".\"person_id\",\n \"person_1\".\"gender_concept_id\"\n FROM \"person\" AS \"person_1\"\n ORDER BY \"person_1\".\"year_of_birth\"\n LIMIT 100\n) AS \"person_2\"\nWHERE (\"person_2\".\"gender_concept_id\" = 8507)","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"The SQL grammar has a number of deficiencies, including rigid clause order, query nesting, and nonsensical position of the SELECT clause. The position of SELECT violates the execution flow of the query, and this violation is aggravated by query nesting. Complex SQL queries often require multiple levels of nesting, which makes such queries bloated and difficult to interpret. This is where data-oriented query builders, which do not constrain the order of pipeline nodes, offer an improvement over plain SQL.","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"What about the other kind of query builders? Active Record and Laravel employ a pipeline of exactly the same form, but because it is not sensitive to the order of the nodes, it must work on a different principle. Indeed, this pipeline generates a SQL query by incrementally assembling the SQL syntax tree. Because of the rigid clause order, a SQL syntax tree can be faithfully represented as a composite data structure with slots specifying the content of the SELECT, FROM, WHERE, and the other clauses:","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"struct SQLQuery\n select\n from\n joins\n where\n groupby\n having\n orderby\n limit\nend","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"Individual slots of this structure are populated by the corresponding pipeline nodes.","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"(Image: \"Where\" node acting on the syntax tree)","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"This explains why the pipeline is insensitive to the order of the nodes. Indeed, as long as the content of the slots stays the same, it makes no difference in what order the slots are populated.","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"(Image: Pipeline is insensitive to the order of the nodes)","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"This method of incrementally constructing a composite structure is known as the builder pattern. We can call the query builders that employ this pattern syntax-oriented.","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"Both data-oriented and syntax-oriented query builders are compositional: the difference is in the nature of the information processed by the units of composition. Data-oriented query builders incrementally refine the query output; syntax-oriented query builders incrementally assemble the SQL syntax tree. Their interfaces look almost identical, but their methods of operation are fundamentally different.","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"But which one is better? Syntax-oriented query builders have two definite advantages: they are easy to implement and they could support the full range of SQL features. Indeed, the interface of a syntax-oriented query builder is just a collection of builders for the SQL syntax tree. How complete the representation of the syntax tree determines how well various SQL features are supported.","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"On the other hand, syntax-oriented query builders are harder to use. As they directly represent the SQL grammar, they inherit all of its deficiencies. In particular, the rigid clause order makes it difficult to assemble complex data processing pipelines, especially when the arrangement of pipeline nodes is not predetermined.","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"A data-oriented query builder directly represents data processing nodes, which makes assembling data processing pipelines much more straightforward—as long as we can find the necessary nodes among those offered by the builder. But where does the builder get its collection of data processing nodes? And how can we tell if this collection is complete?","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"One way to implement a data-oriented query builder is to adapt a general-purpose query framework. Indeed, this is the origin of EF/LINQ, which is adapted from LINQ, and dbplyr, which is adapted from dplyr. The query framework determines what processing nodes are available and how they operate. In principle, any query framework could be adapted to SQL databases by introducing just one new node, a node that loads the content of a database table. If we place this node at the beginning of a pipeline and make the rest of it out of regular nodes, we obtain a pipeline that processes data from a SQL database. However, this pipeline will be very inefficient compared to a SQL engine, which can use indexes to avoid loading the entire table into memory and thus can process the same data much faster. This is why EF/LINQ and dbplyr generate a SQL query that replaces the pipeline as a whole. The pipeline itself no longer runs directly, but now serves as a specification, with the assumption that if it were to run, it would produce the same output as the SQL query. This method of transforming a general-purpose query framework to a SQL query builder is called SQL pushdown.","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"However, SQL pushdown has a serious limitation. A general-purpose query framework is not designed with SQL compatibility in mind. For this reason, some of the pipelines assembled within this framework cannot be converted to SQL. Even worse, many useful SQL queries have no equivalent pipelines and thus cannot be generated using SQL pushdown. Indeed, SQL accumulated a wide range of features and capabilities since it first appeared in 1974. The first revision of the SQL standard, SQL-86, already supported Cartesian products, filtering, grouping, aggregation, and correlated subqueries. The next revision, SQL-92, added many join types and introduced query nesting. SQL:1999 greatly expanded its analytical capabilities by adding two types of queries: recursive queries, for processing hierarchical data, and data cube queries, which generalize histograms, cross-tabulations, roll-ups, drill-downs, and sub-totals. The follow-up revision, SQL:2003, added support for aggregate functions over a running window. Admittedly, SQL is a quintessential enterprise abomination, a hodgepodge of features added to support every imaginable use case, but with inadequate syntax, weird gaps in functionality, and no regards to internal consistency. Nevertheless, the breadth of SQL's capabilities has not been matched by any other query framework, including LINQ or dplyr. So when we generate SQL queries using EF/LINQ or dbplyr, a large subset of these capabilities remains inaccessible.","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"FunSQL is a data-oriented query builder created specifically to expose full expressive power of SQL. Unlike EF/LINQ and dbplyr, FunSQL was not adapted from an existing query framework, but was carefully designed from scratch to match SQL's capabilities. These capabilities include, for example, support for correlated subqueries and lateral joins (with Bind node), aggregate and window functions (using Group and Partition nodes), as well as recursive queries (with Iterate node). This comprehensive support for SQL capabilities makes FunSQL the only SQL query builder suitable for assembling complex data processing pipelines. Moreover, even though FunSQL pipelines cannot be run directly, every FunSQL node has a well-defined data processing semantics, which means that, in principle, FunSQL could be developed into a full-blown query framework. This potentially opens a path for replacing SQL with an equally powerful, but a more coherent and expressive query language.","category":"page"},{"location":"reference/#API-Reference","page":"API Reference","title":"API Reference","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"CurrentModule = FunSQL","category":"page"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"FunSQL.jl\"]","category":"page"},{"location":"reference/#FunSQL.FunSQLError","page":"API Reference","title":"FunSQL.FunSQLError","text":"Base error class for all errors raised by FunSQL.\n\n\n\n\n\n","category":"type"},{"location":"reference/#render()","page":"API Reference","title":"render()","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"render.jl\"]","category":"page"},{"location":"reference/#FunSQL.render-Tuple{Any}","page":"API Reference","title":"FunSQL.render","text":"render(node; tables = Dict{Symbol, SQLTable}(),\n dialect = :default,\n cache = nothing)::SQLString\n\nCreate a SQLCatalog object and serialize the query node.\n\n\n\n\n\n","category":"method"},{"location":"reference/#FunSQL.render-Tuple{FunSQL.SQLCatalog, FunSQL.SQLNode}","page":"API Reference","title":"FunSQL.render","text":"render(catalog::Union{SQLConnection, SQLCatalog}, node::SQLNode)::SQLString\n\nSerialize the query node as a SQL statement.\n\nParameter catalog of SQLCatalog type encapsulates available database tables and the target SQL dialect. A SQLConnection object is also accepted.\n\nParameter node is a composite SQLNode object.\n\nThe function returns a SQLString value. The result is also cached (with the identity of node serving as the key) in the catalog cache.\n\nExamples\n\njulia> catalog = SQLCatalog(\n :person => SQLTable(:person, columns = [:person_id, :year_of_birth]),\n dialect = :postgresql);\n\njulia> q = From(:person) |>\n Where(Get.year_of_birth .>= 1950);\n\njulia> print(render(catalog, q))\nSELECT\n \"person_1\".\"person_id\",\n \"person_1\".\"year_of_birth\"\nFROM \"person\" AS \"person_1\"\nWHERE (\"person_1\".\"year_of_birth\" >= 1950)\n\n\n\n\n\n","category":"method"},{"location":"reference/#FunSQL.render-Tuple{FunSQL.SQLDialect, FunSQL.SQLClause}","page":"API Reference","title":"FunSQL.render","text":"render(dialect::Union{SQLConnection, SQLCatalog, SQLDialect},\n clause::SQLClause)::SQLString\n\nSerialize the syntax tree of a SQL query.\n\n\n\n\n\n","category":"method"},{"location":"reference/#reflect()","page":"API Reference","title":"reflect()","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"reflect.jl\"]","category":"page"},{"location":"reference/#FunSQL.reflect-Tuple{Any}","page":"API Reference","title":"FunSQL.reflect","text":"reflect(conn;\n schema = nothing,\n dialect = nothing,\n cache = 256)::SQLCatalog\n\nRetrieve the information about available database tables.\n\nThe function returns a SQLCatalog object. The catalog will be populated with the tables from the given database schema, or, if parameter schema is not set, from the default database schema (e.g., schema public for PostgreSQL).\n\nParameter dialect specifies the target SQLDialect. If not set, dialect will be inferred from the type of the connection object.\n\n\n\n\n\n","category":"method"},{"location":"reference/#SQLConnection-and-SQLStatement","page":"API Reference","title":"SQLConnection and SQLStatement","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"connections.jl\"]","category":"page"},{"location":"reference/#FunSQL.DB","page":"API Reference","title":"FunSQL.DB","text":"Shorthand for SQLConnection.\n\n\n\n\n\n","category":"type"},{"location":"reference/#FunSQL.SQLConnection","page":"API Reference","title":"FunSQL.SQLConnection","text":"SQLConnection(conn; catalog)\n\nWrap a raw database connection object together with a SQLCatalog object containing information about database tables.\n\n\n\n\n\n","category":"type"},{"location":"reference/#FunSQL.SQLStatement","page":"API Reference","title":"FunSQL.SQLStatement","text":"SQLStatement(conn, raw; vars = Symbol[])\n\nWrap a prepared SQL statement.\n\n\n\n\n\n","category":"type"},{"location":"reference/#DBInterface.connect-Union{Tuple{RawConnType}, Tuple{Type{FunSQL.SQLConnection{RawConnType}}, Vararg{Any}}} where RawConnType","page":"API Reference","title":"DBInterface.connect","text":"DBInterface.connect(DB{RawConnType},\n args...;\n schema = nothing,\n dialect = nothing,\n cache = 256,\n kws...)\n\nConnect to the database server, call reflect to retrieve the information about available tables and return a SQLConnection object.\n\nExtra parameters args and kws are passed to the call:\n\nDBInterface.connect(RawConnType, args...; kws...)\n\n\n\n\n\n","category":"method"},{"location":"reference/#DBInterface.execute-Tuple{FunSQL.SQLConnection, Union{FunSQL.AbstractSQLClause, FunSQL.AbstractSQLNode}, Any}","page":"API Reference","title":"DBInterface.execute","text":"DBInterface.execute(conn::SQLConnection, sql::SQLNode, params)\nDBInterface.execute(conn::SQLConnection, sql::SQLClause, params)\n\nSerialize and execute the query node.\n\n\n\n\n\n","category":"method"},{"location":"reference/#DBInterface.execute-Tuple{FunSQL.SQLConnection, Union{FunSQL.AbstractSQLClause, FunSQL.AbstractSQLNode}}","page":"API Reference","title":"DBInterface.execute","text":"DBInterface.execute(conn::SQLConnection, sql::SQLNode; params...)\nDBInterface.execute(conn::SQLConnection, sql::SQLClause; params...)\n\nSerialize and execute the query node.\n\n\n\n\n\n","category":"method"},{"location":"reference/#DBInterface.execute-Tuple{FunSQL.SQLStatement, Any}","page":"API Reference","title":"DBInterface.execute","text":"DBInterface.execute(stmt::SQLStatement, params)\n\nExecute the prepared SQL statement.\n\n\n\n\n\n","category":"method"},{"location":"reference/#DBInterface.prepare-Tuple{FunSQL.SQLConnection, FunSQL.SQLString}","page":"API Reference","title":"DBInterface.prepare","text":"DBInterface.prepare(conn::SQLConnection, str::SQLString)::SQLStatement\n\nGenerate a prepared SQL statement.\n\n\n\n\n\n","category":"method"},{"location":"reference/#DBInterface.prepare-Tuple{FunSQL.SQLConnection, Union{FunSQL.AbstractSQLClause, FunSQL.AbstractSQLNode}}","page":"API Reference","title":"DBInterface.prepare","text":"DBInterface.prepare(conn::SQLConnection, sql::SQLNode)::SQLStatement\nDBInterface.prepare(conn::SQLConnection, sql::SQLClause)::SQLStatement\n\nSerialize the query node and return a prepared SQL statement.\n\n\n\n\n\n","category":"method"},{"location":"reference/#SQLCatalog,-SQLTable,-and-SQLColumn","page":"API Reference","title":"SQLCatalog, SQLTable, and SQLColumn","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"catalogs.jl\"]","category":"page"},{"location":"reference/#FunSQL.SQLCatalog","page":"API Reference","title":"FunSQL.SQLCatalog","text":"SQLCatalog(; tables = Dict{Symbol, SQLTable}(),\n dialect = :default,\n cache = 256,\n metadata = nothing)\nSQLCatalog(tables...;\n dialect = :default, cache = 256, metadata = nothing)\n\nSQLCatalog encapsulates available database tables, the target SQL dialect, a cache of serialized queries, and an optional metadata.\n\nParameter tables is either a dictionary or a vector of SQLTable objects, where the vector will be converted to a dictionary with table names as keys. A table in the catalog can be included to a query using the From node.\n\nParameter dialect is a SQLDialect object describing the target SQL dialect.\n\nParameter cache specifies the size of the LRU cache containing results of the render function. Set cache to nothing to disable the cache, or set cache to an arbitrary Dict-like object to provide a custom cache implementation.\n\nExamples\n\njulia> person = SQLTable(:person, columns = [:person_id, :year_of_birth, :location_id]);\n\njulia> location = SQLTable(:location, columns = [:location_id, :state]);\n\njulia> catalog = SQLCatalog(person, location, dialect = :postgresql)\nSQLCatalog(SQLTable(:location, SQLColumn(:location_id), SQLColumn(:state)),\n SQLTable(:person,\n SQLColumn(:person_id),\n SQLColumn(:year_of_birth),\n SQLColumn(:location_id)),\n dialect = SQLDialect(:postgresql))\n\n\n\n\n\n","category":"type"},{"location":"reference/#FunSQL.SQLColumn","page":"API Reference","title":"FunSQL.SQLColumn","text":"SQLColumn(; name, metadata = nothing)\nSQLColumn(name; metadata = nothing)\n\nSQLColumn represents a column with the given name and optional metadata.\n\n\n\n\n\n","category":"type"},{"location":"reference/#FunSQL.SQLTable","page":"API Reference","title":"FunSQL.SQLTable","text":"SQLTable(; qualifiers = [], name, columns, metadata = nothing)\nSQLTable(name; qualifiers = [], columns, metadata = nothing)\nSQLTable(name, columns...; qualifiers = [], metadata = nothing)\n\nThe structure of a SQL table or a table-like entity (TEMP TABLE, VIEW, etc) for use as a reference in assembling SQL queries.\n\nThe SQLTable constructor expects the table name, an optional vector containing the table schema and other qualifiers, an ordered dictionary columns that maps names to columns, and an optional metadata.\n\nExamples\n\njulia> person = SQLTable(qualifiers = [\"public\"],\n name = \"person\",\n columns = [\"person_id\", \"year_of_birth\"],\n metadata = (; is_view = false))\nSQLTable(qualifiers = [:public],\n :person,\n SQLColumn(:person_id),\n SQLColumn(:year_of_birth),\n metadata = [:is_view => false])\n\n\n\n\n\n","category":"type"},{"location":"reference/#SQLDialect","page":"API Reference","title":"SQLDialect","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"dialects.jl\"]","category":"page"},{"location":"reference/#FunSQL.SQLDialect","page":"API Reference","title":"FunSQL.SQLDialect","text":"SQLDialect(; name = :default, kws...)\nSQLDialect(template::SQLDialect; kws...)\nSQLDialect(name::Symbol, kws...)\nSQLDialect(ConnType::Type)\n\nProperties and capabilities of a particular SQL dialect.\n\nUse SQLDialect(name::Symbol) to create one of the known dialects. The following names are recognized:\n\n:mysql\n:postgresql\n:redshift\n:spark\n:sqlite\n:sqlserver\n\nKeyword parameters override individual properties of a dialect. For details, check the source code.\n\nUse SQLDialect(ConnType::Type) to detect the dialect based on the type of the database connection object. The following types are recognized:\n\nLibPQ.Connection\nMySQL.Connection\nSQLite.DB\n\nExamples\n\njulia> postgresql_dialect = SQLDialect(:postgresql)\nSQLDialect(:postgresql)\n\njulia> postgresql_odbc_dialect = SQLDialect(:postgresql,\n variable_prefix = '?',\n variable_style = :positional)\nSQLDialect(:postgresql, variable_prefix = '?', variable_style = :POSITIONAL)\n\n\n\n\n\n","category":"type"},{"location":"reference/#SQLString","page":"API Reference","title":"SQLString","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"strings.jl\"]","category":"page"},{"location":"reference/#FunSQL.SQLString","page":"API Reference","title":"FunSQL.SQLString","text":"SQLString(raw; columns = nothing, vars = Symbol[])\n\nSerialized SQL query.\n\nParameter columns is a vector describing the output columns.\n\nParameter vars is a vector of query parameters (created with Var) in the order they are expected by the DBInterface.execute() function.\n\nExamples\n\njulia> person = SQLTable(:person, columns = [:person_id, :year_of_birth]);\n\njulia> q = From(person);\n\njulia> render(q)\nSQLString(\"\"\"\n SELECT\n \"person_1\".\"person_id\",\n \"person_1\".\"year_of_birth\"\n FROM \"person\" AS \"person_1\\\"\"\"\",\n columns = [SQLColumn(:person_id), SQLColumn(:year_of_birth)])\n\njulia> q = From(person) |> Where(Fun.and(Get.year_of_birth .>= Var.YEAR,\n Get.year_of_birth .< Var.YEAR .+ 10));\n\njulia> render(q, dialect = :mysql)\nSQLString(\"\"\"\n SELECT\n `person_1`.`person_id`,\n `person_1`.`year_of_birth`\n FROM `person` AS `person_1`\n WHERE\n (`person_1`.`year_of_birth` >= ?) AND\n (`person_1`.`year_of_birth` < (? + 10))\"\"\",\n columns = [SQLColumn(:person_id), SQLColumn(:year_of_birth)],\n vars = [:YEAR, :YEAR])\n\njulia> render(q, dialect = :postgresql)\nSQLString(\"\"\"\n SELECT\n \"person_1\".\"person_id\",\n \"person_1\".\"year_of_birth\"\n FROM \"person\" AS \"person_1\"\n WHERE\n (\"person_1\".\"year_of_birth\" >= $1) AND\n (\"person_1\".\"year_of_birth\" < ($1 + 10))\"\"\",\n columns = [SQLColumn(:person_id), SQLColumn(:year_of_birth)],\n vars = [:YEAR])\n\n\n\n\n\n","category":"type"},{"location":"reference/#FunSQL.pack","page":"API Reference","title":"FunSQL.pack","text":"pack(sql::SQLString, vars::Union{Dict, NamedTuple})::Vector{Any}\n\nConvert a dictionary or a named tuple of query parameters to the positional form expected by DBInterface.execute().\n\njulia> person = SQLTable(:person, columns = [:person_id, :year_of_birth]);\n\njulia> q = From(person) |> Where(Fun.and(Get.year_of_birth .>= Var.YEAR,\n Get.year_of_birth .< Var.YEAR .+ 10));\n\njulia> sql = render(q, dialect = :mysql);\n\njulia> pack(sql, (; YEAR = 1950))\n2-element Vector{Any}:\n 1950\n 1950\n\njulia> sql = render(q, dialect = :postgresql);\n\njulia> pack(sql, (; YEAR = 1950))\n1-element Vector{Any}:\n 1950\n\n\n\n\n\n","category":"function"},{"location":"reference/#SQLNode","page":"API Reference","title":"SQLNode","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"nodes.jl\"]","category":"page"},{"location":"reference/#FunSQL.AbstractSQLNode","page":"API Reference","title":"FunSQL.AbstractSQLNode","text":"A tabular or a scalar operation that can be expressed as a SQL query.\n\n\n\n\n\n","category":"type"},{"location":"reference/#FunSQL.DuplicateLabelError","page":"API Reference","title":"FunSQL.DuplicateLabelError","text":"A duplicate label where unique labels are expected.\n\n\n\n\n\n","category":"type"},{"location":"reference/#FunSQL.IllFormedError","page":"API Reference","title":"FunSQL.IllFormedError","text":"A scalar operation where a tabular operation is expected.\n\n\n\n\n\n","category":"type"},{"location":"reference/#FunSQL.InvalidArityError","page":"API Reference","title":"FunSQL.InvalidArityError","text":"Unexpected number of arguments.\n\n\n\n\n\n","category":"type"},{"location":"reference/#FunSQL.InvalidGroupingSetsError","page":"API Reference","title":"FunSQL.InvalidGroupingSetsError","text":"Grouping sets are specified incorrectly.\n\n\n\n\n\n","category":"type"},{"location":"reference/#FunSQL.RebaseError","page":"API Reference","title":"FunSQL.RebaseError","text":"A node that cannot be rebased.\n\n\n\n\n\n","category":"type"},{"location":"reference/#FunSQL.ReferenceError","page":"API Reference","title":"FunSQL.ReferenceError","text":"An undefined or an invalid reference.\n\n\n\n\n\n","category":"type"},{"location":"reference/#FunSQL.SQLNode","page":"API Reference","title":"FunSQL.SQLNode","text":"An opaque wrapper over an arbitrary SQL node.\n\n\n\n\n\n","category":"type"},{"location":"reference/#FunSQL.TabularNode","page":"API Reference","title":"FunSQL.TabularNode","text":"A node that produces tabular output.\n\n\n\n\n\n","category":"type"},{"location":"reference/#FunSQL.TransliterationError","page":"API Reference","title":"FunSQL.TransliterationError","text":"Invalid application of the @funsql macro.\n\n\n\n\n\n","category":"type"},{"location":"reference/#FunSQL.@funsql-Tuple{Any}","page":"API Reference","title":"FunSQL.@funsql","text":"Convenient notation for assembling FunSQL queries.\n\n\n\n\n\n","category":"macro"},{"location":"reference/#Agg","page":"API Reference","title":"Agg","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"nodes/aggregate.jl\"]","category":"page"},{"location":"reference/#FunSQL.Agg-Tuple","page":"API Reference","title":"FunSQL.Agg","text":"Agg(; over = nothing, name, args = [], filter = nothing)\nAgg(name; over = nothing, args = [], filter = nothing)\nAgg(name, args...; over = nothing, filter = nothing)\nAgg.name(args...; over = nothing, filter = nothing)\n\nAn application of an aggregate function.\n\nAn Agg node must be applied to the output of a Group or a Partition node. In a Group context, it is translated to a regular aggregate function, and in a Partition context, it is translated to a window function.\n\nExamples\n\nNumber of patients per year of birth.\n\njulia> person = SQLTable(:person, columns = [:person_id, :year_of_birth]);\n\njulia> q = From(:person) |>\n Group(Get.year_of_birth) |>\n Select(Get.year_of_birth, Agg.count());\n\njulia> print(render(q, tables = [person]))\nSELECT\n \"person_1\".\"year_of_birth\",\n count(*) AS \"count\"\nFROM \"person\" AS \"person_1\"\nGROUP BY \"person_1\".\"year_of_birth\"\n\nNumber of distinct states among all available locations.\n\njulia> location = SQLTable(:location, columns = [:location_id, :state]);\n\njulia> q = From(:location) |>\n Group() |>\n Select(Agg.count_distinct(Get.state));\n\njulia> print(render(q, tables = [location]))\nSELECT count(DISTINCT \"location_1\".\"state\") AS \"count_distinct\"\nFROM \"location\" AS \"location_1\"\n\nFor each patient, show the date of their latest visit to a healthcare provider.\n\njulia> person = SQLTable(:person, columns = [:person_id]);\n\njulia> visit_occurrence =\n SQLTable(:visit_occurrence, columns = [:visit_occurrence_id, :person_id, :visit_start_date]);\n\njulia> q = From(:person) |>\n LeftJoin(:visit_group => From(:visit_occurrence) |>\n Group(Get.person_id),\n on = (Get.person_id .== Get.visit_group.person_id)) |>\n Select(Get.person_id,\n :max_visit_start_date =>\n Get.visit_group |> Agg.max(Get.visit_start_date));\n\njulia> print(render(q, tables = [person, visit_occurrence]))\nSELECT\n \"person_1\".\"person_id\",\n \"visit_group_1\".\"max\" AS \"max_visit_start_date\"\nFROM \"person\" AS \"person_1\"\nLEFT JOIN (\n SELECT\n max(\"visit_occurrence_1\".\"visit_start_date\") AS \"max\",\n \"visit_occurrence_1\".\"person_id\"\n FROM \"visit_occurrence\" AS \"visit_occurrence_1\"\n GROUP BY \"visit_occurrence_1\".\"person_id\"\n) AS \"visit_group_1\" ON (\"person_1\".\"person_id\" = \"visit_group_1\".\"person_id\")\n\nFor each visit, show the number of days passed since the previous visit.\n\njulia> visit_occurrence =\n SQLTable(:visit_occurrence, columns = [:visit_occurrence_id, :person_id, :visit_start_date]);\n\njulia> q = From(:visit_occurrence) |>\n Partition(Get.person_id,\n order_by = [Get.visit_start_date]) |>\n Select(Get.person_id,\n Get.visit_start_date,\n :gap => Get.visit_start_date .- Agg.lag(Get.visit_start_date));\n\njulia> print(render(q, tables = [visit_occurrence]))\nSELECT\n \"visit_occurrence_1\".\"person_id\",\n \"visit_occurrence_1\".\"visit_start_date\",\n (\"visit_occurrence_1\".\"visit_start_date\" - (lag(\"visit_occurrence_1\".\"visit_start_date\") OVER (PARTITION BY \"visit_occurrence_1\".\"person_id\" ORDER BY \"visit_occurrence_1\".\"visit_start_date\"))) AS \"gap\"\nFROM \"visit_occurrence\" AS \"visit_occurrence_1\"\n\n\n\n\n\n","category":"method"},{"location":"reference/#Append","page":"API Reference","title":"Append","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"nodes/append.jl\"]","category":"page"},{"location":"reference/#FunSQL.Append-Tuple","page":"API Reference","title":"FunSQL.Append","text":"Append(; over = nothing, args)\nAppend(args...; over = nothing)\n\nAppend concatenates input datasets.\n\nOnly the columns that are present in every input dataset will be included to the output of Append.\n\nAn Append node is translated to a UNION ALL query:\n\nSELECT ...\nFROM $over\nUNION ALL\nSELECT ...\nFROM $(args[1])\nUNION ALL\n...\n\nExamples\n\nShow the dates of all measuments and observations.\n\njulia> measurement = SQLTable(:measurement, columns = [:measurement_id, :person_id, :measurement_date]);\n\njulia> observation = SQLTable(:observation, columns = [:observation_id, :person_id, :observation_date]);\n\njulia> q = From(:measurement) |>\n Define(:date => Get.measurement_date) |>\n Append(From(:observation) |>\n Define(:date => Get.observation_date));\n\njulia> print(render(q, tables = [measurement, observation]))\nSELECT\n \"measurement_1\".\"person_id\",\n \"measurement_1\".\"measurement_date\" AS \"date\"\nFROM \"measurement\" AS \"measurement_1\"\nUNION ALL\nSELECT\n \"observation_1\".\"person_id\",\n \"observation_1\".\"observation_date\" AS \"date\"\nFROM \"observation\" AS \"observation_1\"\n\n\n\n\n\n","category":"method"},{"location":"reference/#As","page":"API Reference","title":"As","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"nodes/as.jl\"]","category":"page"},{"location":"reference/#FunSQL.As-Tuple","page":"API Reference","title":"FunSQL.As","text":"As(; over = nothing, name)\nAs(name; over = nothing)\nname => over\n\nIn a scalar context, As specifies the name of the output column. When applied to tabular data, As wraps the data in a nested record.\n\nThe arrow operator (=>) is a shorthand notation for As.\n\nExamples\n\nShow all patient IDs.\n\njulia> person = SQLTable(:person, columns = [:person_id, :year_of_birth]);\n\njulia> q = From(:person) |> Select(:id => Get.person_id);\n\njulia> print(render(q, tables = [person]))\nSELECT \"person_1\".\"person_id\" AS \"id\"\nFROM \"person\" AS \"person_1\"\n\nShow all patients together with their state of residence.\n\njulia> person = SQLTable(:person, columns = [:person_id, :year_of_birth, :location_id]);\n\njulia> location = SQLTable(:location, columns = [:location_id, :state]);\n\njulia> q = From(:person) |>\n Join(From(:location) |> As(:location),\n on = Get.location_id .== Get.location.location_id) |>\n Select(Get.person_id, Get.location.state);\n\njulia> print(render(q, tables = [person, location]))\nSELECT\n \"person_1\".\"person_id\",\n \"location_1\".\"state\"\nFROM \"person\" AS \"person_1\"\nJOIN \"location\" AS \"location_1\" ON (\"person_1\".\"location_id\" = \"location_1\".\"location_id\")\n\n\n\n\n\n","category":"method"},{"location":"reference/#Bind","page":"API Reference","title":"Bind","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"nodes/bind.jl\"]","category":"page"},{"location":"reference/#FunSQL.Bind-Tuple","page":"API Reference","title":"FunSQL.Bind","text":"Bind(; over = nothing; args)\nBind(args...; over = nothing)\n\nThe Bind node evaluates a query with parameters. Specifically, Bind provides the values for Var parameters contained in the over node.\n\nIn a scalar context, the Bind node is translated to a correlated subquery. When Bind is applied to the joinee branch of a Join node, it is translated to a JOIN LATERAL query.\n\nExamples\n\nShow patients with at least one visit to a heathcare provider.\n\njulia> person = SQLTable(:person, columns = [:person_id]);\n\njulia> visit_occurrence = SQLTable(:visit_occurrence, columns = [:visit_occurrence_id, :person_id]);\n\njulia> q = From(:person) |>\n Where(Fun.exists(From(:visit_occurrence) |>\n Where(Get.person_id .== Var.PERSON_ID) |>\n Bind(:PERSON_ID => Get.person_id)));\n\njulia> print(render(q, tables = [person, visit_occurrence]))\nSELECT \"person_1\".\"person_id\"\nFROM \"person\" AS \"person_1\"\nWHERE (EXISTS (\n SELECT NULL AS \"_\"\n FROM \"visit_occurrence\" AS \"visit_occurrence_1\"\n WHERE (\"visit_occurrence_1\".\"person_id\" = \"person_1\".\"person_id\")\n))\n\nShow all patients together with the date of their latest visit to a heathcare provider.\n\njulia> person = SQLTable(:person, columns = [:person_id]);\n\njulia> visit_occurrence =\n SQLTable(:visit_occurrence, columns = [:visit_occurrence_id, :person_id, :visit_start_date]);\n\njulia> q = From(:person) |>\n LeftJoin(From(:visit_occurrence) |>\n Where(Get.person_id .== Var.PERSON_ID) |>\n Order(Get.visit_start_date |> Desc()) |>\n Limit(1) |>\n Bind(:PERSON_ID => Get.person_id) |>\n As(:visit),\n on = true) |>\n Select(Get.person_id, Get.visit.visit_start_date);\n\njulia> print(render(q, tables = [person, visit_occurrence]))\nSELECT\n \"person_1\".\"person_id\",\n \"visit_1\".\"visit_start_date\"\nFROM \"person\" AS \"person_1\"\nLEFT JOIN LATERAL (\n SELECT \"visit_occurrence_1\".\"visit_start_date\"\n FROM \"visit_occurrence\" AS \"visit_occurrence_1\"\n WHERE (\"visit_occurrence_1\".\"person_id\" = \"person_1\".\"person_id\")\n ORDER BY \"visit_occurrence_1\".\"visit_start_date\" DESC\n FETCH FIRST 1 ROW ONLY\n) AS \"visit_1\" ON TRUE\n\n\n\n\n\n","category":"method"},{"location":"reference/#Define","page":"API Reference","title":"Define","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"nodes/define.jl\"]","category":"page"},{"location":"reference/#FunSQL.Define-Tuple","page":"API Reference","title":"FunSQL.Define","text":"Define(; over; args = [])\nDefine(args...; over)\n\nThe Define node adds or replaces output columns.\n\nExamples\n\nShow patients who are at least 16 years old.\n\njulia> person = SQLTable(:person, columns = [:person_id, :birth_datetime]);\n\njulia> q = From(:person) |>\n Define(:age => Fun.now() .- Get.birth_datetime) |>\n Where(Get.age .>= \"16 years\");\n\njulia> print(render(q, tables = [person]))\nSELECT\n \"person_2\".\"person_id\",\n \"person_2\".\"birth_datetime\",\n \"person_2\".\"age\"\nFROM (\n SELECT\n \"person_1\".\"person_id\",\n \"person_1\".\"birth_datetime\",\n (now() - \"person_1\".\"birth_datetime\") AS \"age\"\n FROM \"person\" AS \"person_1\"\n) AS \"person_2\"\nWHERE (\"person_2\".\"age\" >= '16 years')\n\nConceal the year of birth of patients born before 1930.\n\njulia> person = SQLTable(:person, columns = [:person_id, :year_of_birth]);\n\njulia> q = From(:person) |>\n Define(:year_of_birth => Fun.case(Get.year_of_birth .>= 1930,\n Get.year_of_birth,\n missing));\n\njulia> print(render(q, tables = [person]))\nSELECT\n \"person_1\".\"person_id\",\n (CASE WHEN (\"person_1\".\"year_of_birth\" >= 1930) THEN \"person_1\".\"year_of_birth\" ELSE NULL END) AS \"year_of_birth\"\nFROM \"person\" AS \"person_1\"\n\n\n\n\n\n","category":"method"},{"location":"reference/#From","page":"API Reference","title":"From","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"nodes/from.jl\"]","category":"page"},{"location":"reference/#FunSQL.From-Tuple","page":"API Reference","title":"FunSQL.From","text":"From(; source)\nFrom(tbl::SQLTable)\nFrom(name::Symbol)\nFrom(^)\nFrom(df)\nFrom(f::SQLNode; columns::Vector{Symbol})\nFrom(::Nothing)\n\nFrom outputs the content of a database table.\n\nThe parameter source could be one of:\n\na SQLTable object;\na Symbol value;\na ^ object;\na DataFrame or any Tables.jl-compatible dataset;\nA SQLNode representing a table-valued function. In this case, From also requires a keyword parameter columns with a list of output columns produced by the function.\nnothing.\n\nWhen source is a symbol, it can refer to either a table in SQLCatalog or an intermediate dataset defined with the With node.\n\nThe From node is translated to a SQL query with a FROM clause:\n\nSELECT ...\nFROM $source\n\nFrom(^) must be a component of Iterate. In the context of Iterate, it refers to the output of the previous iteration.\n\nFrom(::DataFrame) is translated to a VALUES clause.\n\nFrom(nothing) emits a dataset with one row and no columns and can usually be omitted.\n\nExamples\n\nList all patients.\n\njulia> person = SQLTable(:person, columns = [:person_id, :year_of_birth]);\n\njulia> q = From(person);\n\njulia> print(render(q))\nSELECT\n \"person_1\".\"person_id\",\n \"person_1\".\"year_of_birth\"\nFROM \"person\" AS \"person_1\"\n\nList all patients.\n\njulia> person = SQLTable(:person, columns = [:person_id, :year_of_birth]);\n\njulia> q = From(:person);\n\njulia> print(render(q, tables = [person]))\nSELECT\n \"person_1\".\"person_id\",\n \"person_1\".\"year_of_birth\"\nFROM \"person\" AS \"person_1\"\n\nShow all patients diagnosed with essential hypertension.\n\njulia> person = SQLTable(:person, columns = [:person_id, :year_of_birth]);\n\njulia> condition_occurrence =\n SQLTable(:condition_occurrence,\n columns = [:condition_occurrence_id, :person_id, :condition_concept_id]);\n\njulia> q = From(:person) |>\n Where(Fun.in(Get.person_id, From(:essential_hypertension) |>\n Select(Get.person_id))) |>\n With(:essential_hypertension =>\n From(:condition_occurrence) |>\n Where(Get.condition_concept_id .== 320128));\n\njulia> print(render(q, tables = [person, condition_occurrence]))\nWITH \"essential_hypertension_1\" (\"person_id\") AS (\n SELECT \"condition_occurrence_1\".\"person_id\"\n FROM \"condition_occurrence\" AS \"condition_occurrence_1\"\n WHERE (\"condition_occurrence_1\".\"condition_concept_id\" = 320128)\n)\nSELECT\n \"person_1\".\"person_id\",\n \"person_1\".\"year_of_birth\"\nFROM \"person\" AS \"person_1\"\nWHERE (\"person_1\".\"person_id\" IN (\n SELECT \"essential_hypertension_2\".\"person_id\"\n FROM \"essential_hypertension_1\" AS \"essential_hypertension_2\"\n))\n\nShow the current date.\n\njulia> q = From(nothing) |>\n Select(Fun.current_date());\n\njulia> print(render(q))\nSELECT CURRENT_DATE AS \"current_date\"\n\njulia> q = Select(Fun.current_date());\n\njulia> print(render(q))\nSELECT CURRENT_DATE AS \"current_date\"\n\nQuery a DataFrame.\n\njulia> df = DataFrame(name = [\"SQL\", \"Julia\", \"FunSQL\"],\n year = [1974, 2012, 2021]);\n\njulia> q = From(df) |>\n Group() |>\n Select(Agg.min(Get.year), Agg.max(Get.year));\n\njulia> print(render(q))\nSELECT\n min(\"values_1\".\"year\") AS \"min\",\n max(\"values_1\".\"year\") AS \"max\"\nFROM (\n VALUES\n (1974),\n (2012),\n (2021)\n) AS \"values_1\" (\"year\")\n\nParse comma-separated numbers.\n\njulia> q = From(Fun.regexp_matches(\"2,3,5,7,11\", \"(\\\\d+)\", \"g\"),\n columns = [:captures]) |>\n Select(Fun.\"CAST(?[1] AS INTEGER)\"(Get.captures));\n\njulia> print(render(q, dialect = :postgresql))\nSELECT CAST(\"regexp_matches_1\".\"captures\"[1] AS INTEGER) AS \"_\"\nFROM regexp_matches('2,3,5,7,11', '(\\d+)', 'g') AS \"regexp_matches_1\" (\"captures\")\n\n\n\n\n\n","category":"method"},{"location":"reference/#Fun","page":"API Reference","title":"Fun","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"nodes/function.jl\"]","category":"page"},{"location":"reference/#FunSQL.Fun-Tuple","page":"API Reference","title":"FunSQL.Fun","text":"Fun(; name, args = [])\nFun(name; args = [])\nFun(name, args...)\nFun.name(args...)\n\nApplication of a SQL function or a SQL operator.\n\nA Fun node is also generated by broadcasting on SQLNode objects. Names of Julia operators (==, !=, &&, ||, !) are replaced with their SQL equivalents (=, <>, and, or, not).\n\nIf name contains only symbols, or if name starts or ends with a space, the Fun node is translated to a SQL operator.\n\nIf name contains one or more ? characters, it serves as a template of a SQL expression where ? symbols are replaced with the given arguments. Use ?? to represent a literal ? mark. Wrap the template in parentheses if this is necessary to make the SQL expression unambiguous.\n\nCertain names have a customized translation in order to generate common SQL functions and operators with irregular syntax:\n\nFun node SQL syntax\nFun.and(p₁, p₂, …) p₁ AND p₂ AND …\nFun.between(x, y, z) x BETWEEN y AND z\nFun.case(p, x, …) CASE WHEN p THEN x … END\nFun.cast(x, \"TYPE\") CAST(x AS TYPE)\nFun.concat(s₁, s₂, …) dialect-specific, e.g., (s₁ || s₂ || …)\nFun.current_date() CURRENT_DATE\nFun.current_timestamp() CURRENT_TIMESTAMP\nFun.exists(q) EXISTS q\nFun.extract(\"FIELD\", x) EXTRACT(FIELD FROM x)\nFun.in(x, q) x IN q\nFun.in(x, y₁, y₂, …) x IN (y₁, y₂, …)\nFun.is_not_null(x) x IS NOT NULL\nFun.is_null(x) x IS NULL\nFun.like(x, y) x LIKE y\nFun.not(p) NOT p\nFun.not_between(x, y, z) x NOT BETWEEN y AND z\nFun.not_exists(q) NOT EXISTS q\nFun.not_in(x, q) x NOT IN q\nFun.not_in(x, y₁, y₂, …) x NOT IN (y₁, y₂, …)\nFun.not_like(x, y) x NOT LIKE y\nFun.or(p₁, p₂, …) p₁ OR p₂ OR …\n\nExamples\n\nReplace missing values with N/A.\n\njulia> location = SQLTable(:location, columns = [:location_id, :city]);\n\njulia> q = From(:location) |>\n Select(Fun.coalesce(Get.city, \"N/A\"));\n\njulia> print(render(q, tables = [location]))\nSELECT coalesce(\"location_1\".\"city\", 'N/A') AS \"coalesce\"\nFROM \"location\" AS \"location_1\"\n\nFind patients not born in 1980.\n\njulia> person = SQLTable(:person, columns = [:person_id, :year_of_birth]);\n\njulia> q = From(:person) |>\n Where(Get.year_of_birth .!= 1980);\n\njulia> print(render(q, tables = [person]))\nSELECT\n \"person_1\".\"person_id\",\n \"person_1\".\"year_of_birth\"\nFROM \"person\" AS \"person_1\"\nWHERE (\"person_1\".\"year_of_birth\" <> 1980)\n\nFor each patient, show their age in 2000.\n\njulia> person = SQLTable(:person, columns = [:person_id, :year_of_birth]);\n\njulia> q = From(:person) |>\n Select(Fun.\"-\"(2000, Get.year_of_birth));\n\njulia> print(render(q, tables = [person]))\nSELECT (2000 - \"person_1\".\"year_of_birth\") AS \"_\"\nFROM \"person\" AS \"person_1\"\n\nFind invalid zip codes.\n\njulia> location = SQLTable(:location, columns = [:location_id, :zip]);\n\njulia> q = From(:location) |>\n Select(Fun.\" NOT SIMILAR TO '[0-9]{5}'\"(Get.zip));\n\njulia> print(render(q, tables = [location]))\nSELECT (\"location_1\".\"zip\" NOT SIMILAR TO '[0-9]{5}') AS \"_\"\nFROM \"location\" AS \"location_1\"\n\nExtract the first 3 digits of the zip code.\n\njulia> location = SQLTable(:location, columns = [:location_id, :zip]);\n\njulia> q = From(:location) |>\n Select(Fun.\"SUBSTRING(? FROM ? FOR ?)\"(Get.zip, 1, 3));\n\njulia> print(render(q, tables = [location]))\nSELECT SUBSTRING(\"location_1\".\"zip\" FROM 1 FOR 3) AS \"_\"\nFROM \"location\" AS \"location_1\"\n\n\n\n\n\n","category":"method"},{"location":"reference/#Get","page":"API Reference","title":"Get","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"nodes/get.jl\"]","category":"page"},{"location":"reference/#FunSQL.Get-Tuple","page":"API Reference","title":"FunSQL.Get","text":"Get(; over, name)\nGet(name; over)\nGet.name Get.\"name\" Get[name] Get[\"name\"]\nover.name over.\"name\" over[name] over[\"name\"]\nname\n\nA reference to a column of the input dataset.\n\nWhen a column reference is ambiguous (e.g., with Join), use As to disambiguate the columns, and a chained Get node (Get.a.b.….z) to refer to a column wrapped with … |> As(:b) |> As(:a).\n\nExamples\n\nList patient IDs.\n\njulia> person = SQLTable(:person, columns = [:person_id, :year_of_birth]);\n\njulia> q = From(:person) |>\n Select(Get(:person_id));\n\njulia> print(render(q, tables = [person]))\nSELECT \"person_1\".\"person_id\"\nFROM \"person\" AS \"person_1\"\n\nShow patients with their state of residence.\n\njulia> person = SQLTable(:person, columns = [:person_id, :year_of_birth, :location_id]);\n\njulia> location = SQLTable(:location, columns = [:location_id, :state]);\n\njulia> q = From(:person) |>\n Join(From(:location) |> As(:location),\n on = Get.location_id .== Get.location.location_id) |>\n Select(Get.person_id, Get.location.state);\n\njulia> print(render(q, tables = [person, location]))\nSELECT\n \"person_1\".\"person_id\",\n \"location_1\".\"state\"\nFROM \"person\" AS \"person_1\"\nJOIN \"location\" AS \"location_1\" ON (\"person_1\".\"location_id\" = \"location_1\".\"location_id\")\n\n\n\n\n\n","category":"method"},{"location":"reference/#Group","page":"API Reference","title":"Group","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"nodes/group.jl\"]","category":"page"},{"location":"reference/#FunSQL.Group-Tuple","page":"API Reference","title":"FunSQL.Group","text":"Group(; over, by = [], sets = sets, name = nothing)\nGroup(by...; over, sets = sets, name = nothing)\n\nThe Group node summarizes the input dataset.\n\nSpecifically, Group outputs all unique values of the given grouping key. This key partitions the input rows into disjoint groups that are summarized by aggregate functions Agg applied to the output of Group. The parameter sets specifies the grouping sets, either with grouping mode indicators :cube or :rollup, or explicitly as Vector{Vector{Symbol}}. An optional parameter name specifies the field to hold the group.\n\nThe Group node is translated to a SQL query with a GROUP BY clause:\n\nSELECT ...\nFROM $over\nGROUP BY $by...\n\nExamples\n\nTotal number of patients.\n\njulia> person = SQLTable(:person, columns = [:person_id, :year_of_birth]);\n\njulia> q = From(:person) |>\n Group() |>\n Select(Agg.count());\n\njulia> print(render(q, tables = [person]))\nSELECT count(*) AS \"count\"\nFROM \"person\" AS \"person_1\"\n\nNumber of patients per year of birth.\n\njulia> person = SQLTable(:person, columns = [:person_id, :year_of_birth]);\n\njulia> q = From(:person) |>\n Group(Get.year_of_birth) |>\n Select(Get.year_of_birth, Agg.count());\n\njulia> print(render(q, tables = [person]))\nSELECT\n \"person_1\".\"year_of_birth\",\n count(*) AS \"count\"\nFROM \"person\" AS \"person_1\"\nGROUP BY \"person_1\".\"year_of_birth\"\n\nThe same example using an explicit group name.\n\njulia> person = SQLTable(:person, columns = [:person_id, :year_of_birth]);\n\njulia> q = From(:person) |>\n Group(Get.year_of_birth, name = :person) |>\n Select(Get.year_of_birth, Get.person |> Agg.count());\n\njulia> print(render(q, tables = [person]))\nSELECT\n \"person_1\".\"year_of_birth\",\n count(*) AS \"count\"\nFROM \"person\" AS \"person_1\"\nGROUP BY \"person_1\".\"year_of_birth\"\n\nNumber of patients per year of birth and the total number of patients.\n\njulia> person = SQLTable(:person, columns = [:person_id, :year_of_birth]);\n\njulia> q = From(:person) |>\n Group(Get.year_of_birth, sets = :cube) |>\n Select(Get.year_of_birth, Agg.count());\n\njulia> print(render(q, tables = [person]))\nSELECT\n \"person_1\".\"year_of_birth\",\n count(*) AS \"count\"\nFROM \"person\" AS \"person_1\"\nGROUP BY CUBE(\"person_1\".\"year_of_birth\")\n\nDistinct states across all available locations.\n\njulia> location = SQLTable(:location, columns = [:location_id, :state]);\n\njulia> q = From(:location) |>\n Group(Get.state);\n\njulia> print(render(q, tables = [location]))\nSELECT DISTINCT \"location_1\".\"state\"\nFROM \"location\" AS \"location_1\"\n\n\n\n\n\n","category":"method"},{"location":"reference/#Highlight","page":"API Reference","title":"Highlight","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"nodes/highlight.jl\"]","category":"page"},{"location":"reference/#FunSQL.Highlight-Tuple","page":"API Reference","title":"FunSQL.Highlight","text":"Highlight(; over = nothing; color)\nHighlight(color; over = nothing)\n\nHighlight over with the given color.\n\nThe highlighted node is printed with the selected color when the query containing it is displayed.\n\nAvailable colors can be found in Base.text_colors.\n\nExamples\n\njulia> q = From(:person) |>\n Select(Get.person_id |> Highlight(:bold))\nlet q1 = From(:person),\n q2 = q1 |> Select(Get.person_id)\n q2\nend\n\n\n\n\n\n","category":"method"},{"location":"reference/#Iterate","page":"API Reference","title":"Iterate","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"nodes/iterate.jl\"]","category":"page"},{"location":"reference/#FunSQL.Iterate-Tuple","page":"API Reference","title":"FunSQL.Iterate","text":"Iterate(; over = nothing, iterator)\nIterate(iterator; over = nothing)\n\nIterate generates the concatenated output of an iterated query.\n\nThe over query is evaluated first. Then the iterator query is repeatedly applied: to the output of over, then to the output of its previous run, and so on, until the iterator produces no data. All these outputs are concatenated to generate the output of Iterate.\n\nThe iterator query may explicitly refer to the output of the previous run using From(^) notation.\n\nThe Iterate node is translated to a recursive common table expression:\n\nWITH RECURSIVE iterator AS (\n SELECT ...\n FROM $over\n UNION ALL\n SELECT ...\n FROM $iterator\n)\nSELECT ...\nFROM iterator\n\nExamples\n\nCalculate the factorial.\n\njulia> q = Define(:n => 1, :f => 1) |>\n Iterate(From(^) |>\n Where(Get.n .< 10) |>\n Define(:n => Get.n .+ 1, :f => Get.f .* (Get.n .+ 1)));\n\njulia> print(render(q))\nWITH RECURSIVE \"__1\" (\"n\", \"f\") AS (\n SELECT\n 1 AS \"n\",\n 1 AS \"f\"\n UNION ALL\n SELECT\n (\"__2\".\"n\" + 1) AS \"n\",\n (\"__2\".\"f\" * (\"__2\".\"n\" + 1)) AS \"f\"\n FROM \"__1\" AS \"__2\"\n WHERE (\"__2\".\"n\" < 10)\n)\nSELECT\n \"__3\".\"n\",\n \"__3\".\"f\"\nFROM \"__1\" AS \"__3\"\n\n*Calculate the factorial, with implicit From(^).\n\njulia> q = Define(:n => 1, :f => 1) |>\n Iterate(Where(Get.n .< 10) |>\n Define(:n => Get.n .+ 1, :f => Get.f .* (Get.n .+ 1)));\n\njulia> print(render(q))\nWITH RECURSIVE \"__1\" (\"n\", \"f\") AS (\n SELECT\n 1 AS \"n\",\n 1 AS \"f\"\n UNION ALL\n SELECT\n (\"__2\".\"n\" + 1) AS \"n\",\n (\"__2\".\"f\" * (\"__2\".\"n\" + 1)) AS \"f\"\n FROM \"__1\" AS \"__2\"\n WHERE (\"__2\".\"n\" < 10)\n)\nSELECT\n \"__3\".\"n\",\n \"__3\".\"f\"\nFROM \"__1\" AS \"__3\"\n\n\n\n\n\n","category":"method"},{"location":"reference/#Join","page":"API Reference","title":"Join","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"nodes/join.jl\"]","category":"page"},{"location":"reference/#FunSQL.CrossJoin-Tuple","page":"API Reference","title":"FunSQL.CrossJoin","text":"An alias for Join(...; ..., on = true).\n\n\n\n\n\n","category":"method"},{"location":"reference/#FunSQL.Join-Tuple","page":"API Reference","title":"FunSQL.Join","text":"Join(; over = nothing, joinee, on, left = false, right = false, optional = false)\nJoin(joinee; over = nothing, on, left = false, right = false, optional = false)\nJoin(joinee, on; over = nothing, left = false, right = false, optional = false)\n\nJoin correlates two input datasets.\n\nThe Join node is translated to a query with a JOIN clause:\n\nSELECT ...\nFROM $over\nJOIN $joinee ON $on\n\nYou can specify the join type:\n\nINNER JOIN (the default);\nLEFT JOIN (left = true or LeftJoin);\nRIGHT JOIN (right = true);\nFULL JOIN (both left = true and right = true);\nCROSS JOIN (on = true).\n\nWhen optional is set, the JOIN clause is omitted if the query does not depend on any columns from the joinee branch.\n\nTo make a lateral join, apply Bind to the joinee branch.\n\nUse As to disambiguate output columns.\n\nExamples\n\nShow patients with their state of residence.\n\njulia> person = SQLTable(:person, columns = [:person_id, :location_id]);\n\njulia> location = SQLTable(:location, columns = [:location_id, :state]);\n\njulia> q = From(:person) |>\n Join(:location => From(:location),\n Get.location_id .== Get.location.location_id) |>\n Select(Get.person_id, Get.location.state);\n\njulia> print(render(q, tables = [person, location]))\nSELECT\n \"person_1\".\"person_id\",\n \"location_1\".\"state\"\nFROM \"person\" AS \"person_1\"\nJOIN \"location\" AS \"location_1\" ON (\"person_1\".\"location_id\" = \"location_1\".\"location_id\")\n\n\n\n\n\n","category":"method"},{"location":"reference/#FunSQL.LeftJoin-Tuple","page":"API Reference","title":"FunSQL.LeftJoin","text":"An alias for Join(...; ..., left = true).\n\n\n\n\n\n","category":"method"},{"location":"reference/#Limit","page":"API Reference","title":"Limit","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"nodes/limit.jl\"]","category":"page"},{"location":"reference/#FunSQL.Limit-Tuple","page":"API Reference","title":"FunSQL.Limit","text":"Limit(; over = nothing, offset = nothing, limit = nothing)\nLimit(limit; over = nothing, offset = nothing)\nLimit(offset, limit; over = nothing)\nLimit(start:stop; over = nothing)\n\nThe Limit node skips the first offset rows and then emits the next limit rows.\n\nTo make the output deterministic, Limit must be applied directly after an Order node.\n\nThe Limit node is translated to a query with a LIMIT or a FETCH clause:\n\nSELECT ...\nFROM $over\nOFFSET $offset ROWS\nFETCH NEXT $limit ROWS ONLY\n\nExamples\n\nShow the oldest patient.\n\njulia> person = SQLTable(:person, columns = [:person_id, :year_of_birth]);\n\njulia> q = From(:person) |>\n Order(Get.year_of_birth) |>\n Limit(1);\n\njulia> print(render(q, tables = [person]))\nSELECT\n \"person_1\".\"person_id\",\n \"person_1\".\"year_of_birth\"\nFROM \"person\" AS \"person_1\"\nORDER BY \"person_1\".\"year_of_birth\"\nFETCH FIRST 1 ROW ONLY\n\n\n\n\n\n","category":"method"},{"location":"reference/#Lit","page":"API Reference","title":"Lit","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"nodes/literal.jl\"]","category":"page"},{"location":"reference/#FunSQL.Lit-Tuple","page":"API Reference","title":"FunSQL.Lit","text":"Lit(; val)\nLit(val)\n\nA SQL literal.\n\nIn a context where a SQL node is expected, missing, numbers, strings, and datetime values are automatically converted to SQL literals.\n\nExamples\n\njulia> q = Select(:null => missing,\n :boolean => true,\n :integer => 42,\n :text => \"SQL is fun!\",\n :date => Date(2000));\n\njulia> print(render(q))\nSELECT\n NULL AS \"null\",\n TRUE AS \"boolean\",\n 42 AS \"integer\",\n 'SQL is fun!' AS \"text\",\n '2000-01-01' AS \"date\"\n\n\n\n\n\n","category":"method"},{"location":"reference/#Order","page":"API Reference","title":"Order","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"nodes/order.jl\"]","category":"page"},{"location":"reference/#FunSQL.Order-Tuple","page":"API Reference","title":"FunSQL.Order","text":"Order(; over = nothing, by)\nOrder(by...; over = nothing)\n\nOrder sorts the input rows by the given key.\n\nThe Ordernode is translated to a query with an ORDER BY clause:\n\nSELECT ...\nFROM $over\nORDER BY $by...\n\nSpecify the sort order with Asc, Desc, or Sort.\n\nExamples\n\nList patients ordered by their age.\n\njulia> person = SQLTable(:person, columns = [:person_id, :year_of_birth]);\n\njulia> q = From(:person) |>\n Order(Get.year_of_birth);\n\njulia> print(render(q, tables = [person]))\nSELECT\n \"person_1\".\"person_id\",\n \"person_1\".\"year_of_birth\"\nFROM \"person\" AS \"person_1\"\nORDER BY \"person_1\".\"year_of_birth\"\n\n\n\n\n\n","category":"method"},{"location":"reference/#Over","page":"API Reference","title":"Over","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"nodes/over.jl\"]","category":"page"},{"location":"reference/#FunSQL.Over-Tuple","page":"API Reference","title":"FunSQL.Over","text":"Over(; over = nothing, arg, materialized = nothing)\nOver(arg; over = nothing, materialized = nothing)\n\nbase |> Over(arg) is an alias for With(base, over = arg).\n\nExamples\n\njulia> person = SQLTable(:person, columns = [:person_id, :year_of_birth]);\n\njulia> condition_occurrence =\n SQLTable(:condition_occurrence, columns = [:condition_occurrence_id,\n :person_id,\n :condition_concept_id]);\n\njulia> q = From(:condition_occurrence) |>\n Where(Get.condition_concept_id .== 320128) |>\n As(:essential_hypertension) |>\n Over(From(:person) |>\n Where(Fun.in(Get.person_id, From(:essential_hypertension) |>\n Select(Get.person_id))));\n\njulia> print(render(q, tables = [person, condition_occurrence]))\nWITH \"essential_hypertension_1\" (\"person_id\") AS (\n SELECT \"condition_occurrence_1\".\"person_id\"\n FROM \"condition_occurrence\" AS \"condition_occurrence_1\"\n WHERE (\"condition_occurrence_1\".\"condition_concept_id\" = 320128)\n)\nSELECT\n \"person_1\".\"person_id\",\n \"person_1\".\"year_of_birth\"\nFROM \"person\" AS \"person_1\"\nWHERE (\"person_1\".\"person_id\" IN (\n SELECT \"essential_hypertension_2\".\"person_id\"\n FROM \"essential_hypertension_1\" AS \"essential_hypertension_2\"\n))\n\n\n\n\n\n","category":"method"},{"location":"reference/#Partition","page":"API Reference","title":"Partition","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"nodes/partition.jl\"]","category":"page"},{"location":"reference/#FunSQL.Partition-Tuple","page":"API Reference","title":"FunSQL.Partition","text":"Partition(; over, by = [], order_by = [], frame = nothing, name = nothing)\nPartition(by...; over, order_by = [], frame = nothing, name = nothing)\n\nThe Partition node relates adjacent rows.\n\nSpecifically, Partition specifies how to relate each row to the adjacent rows in the same dataset. The rows are partitioned by the given key and ordered within each partition using order_by key. The parameter frame customizes the extent of related rows. These related rows are summarized by aggregate functions Agg applied to the output of Partition. An optional parameter name specifies the field to hold the partition.\n\nThe Partition node is translated to a query with a WINDOW clause:\n\nSELECT ...\nFROM $over\nWINDOW w AS (PARTITION BY $by... ORDER BY $order_by...)\n\nExamples\n\nEnumerate patients' visits.\n\njulia> visit_occurrence =\n SQLTable(:visit_occurrence, columns = [:visit_occurrence_id, :person_id, :visit_start_date]);\n\njulia> q = From(:visit_occurrence) |>\n Partition(Get.person_id, order_by = [Get.visit_start_date]) |>\n Select(Agg.row_number(), Get.visit_occurrence_id);\n\njulia> print(render(q, tables = [visit_occurrence]))\nSELECT\n (row_number() OVER (PARTITION BY \"visit_occurrence_1\".\"person_id\" ORDER BY \"visit_occurrence_1\".\"visit_start_date\")) AS \"row_number\",\n \"visit_occurrence_1\".\"visit_occurrence_id\"\nFROM \"visit_occurrence\" AS \"visit_occurrence_1\"\n\nThe same example using an explicit partition name.\n\njulia> visit_occurrence =\n SQLTable(:visit_occurrence, columns = [:visit_occurrence_id, :person_id, :visit_start_date]);\n\njulia> q = From(:visit_occurrence) |>\n Partition(Get.person_id, order_by = [Get.visit_start_date], name = :visit_by_person) |>\n Select(Get.visit_by_person |> Agg.row_number(), Get.visit_occurrence_id);\n\njulia> print(render(q, tables = [visit_occurrence]))\nSELECT\n (row_number() OVER (PARTITION BY \"visit_occurrence_1\".\"person_id\" ORDER BY \"visit_occurrence_1\".\"visit_start_date\")) AS \"row_number\",\n \"visit_occurrence_1\".\"visit_occurrence_id\"\nFROM \"visit_occurrence\" AS \"visit_occurrence_1\"\n\nCalculate the moving average of the number of patients by the year of birth.\n\njulia> person = SQLTable(:person, columns = [:person_id, :year_of_birth]);\n\njulia> q = From(:person) |>\n Group(Get.year_of_birth) |>\n Partition(order_by = [Get.year_of_birth],\n frame = (mode = :range, start = -1, finish = 1)) |>\n Select(Get.year_of_birth, Agg.avg(Agg.count()));\n\njulia> print(render(q, tables = [person]))\nSELECT\n \"person_1\".\"year_of_birth\",\n (avg(count(*)) OVER (ORDER BY \"person_1\".\"year_of_birth\" RANGE BETWEEN 1 PRECEDING AND 1 FOLLOWING)) AS \"avg\"\nFROM \"person\" AS \"person_1\"\nGROUP BY \"person_1\".\"year_of_birth\"\n\n\n\n\n\n","category":"method"},{"location":"reference/#Select","page":"API Reference","title":"Select","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"nodes/select.jl\"]","category":"page"},{"location":"reference/#FunSQL.Select-Tuple","page":"API Reference","title":"FunSQL.Select","text":"Select(; over; args)\nSelect(args...; over)\n\nThe Select node specifies the output columns.\n\nSELECT $args...\nFROM $over\n\nSet the column labels with As.\n\nExamples\n\nList patient IDs and their age.\n\njulia> person = SQLTable(:person, columns = [:person_id, :birth_datetime]);\n\njulia> q = From(:person) |>\n Select(Get.person_id,\n :age => Fun.now() .- Get.birth_datetime);\n\njulia> print(render(q, tables = [person]))\nSELECT\n \"person_1\".\"person_id\",\n (now() - \"person_1\".\"birth_datetime\") AS \"age\"\nFROM \"person\" AS \"person_1\"\n\n\n\n\n\n","category":"method"},{"location":"reference/#Sort,-Asc,-and-Desc","page":"API Reference","title":"Sort, Asc, and Desc","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"nodes/sort.jl\"]","category":"page"},{"location":"reference/#FunSQL.Asc-Tuple{}","page":"API Reference","title":"FunSQL.Asc","text":"Asc(; over = nothing, nulls = nothing)\n\nAscending order indicator.\n\n\n\n\n\n","category":"method"},{"location":"reference/#FunSQL.Desc-Tuple{}","page":"API Reference","title":"FunSQL.Desc","text":"Desc(; over = nothing, nulls = nothing)\n\nDescending order indicator.\n\n\n\n\n\n","category":"method"},{"location":"reference/#FunSQL.Sort-Tuple","page":"API Reference","title":"FunSQL.Sort","text":"Sort(; over = nothing, value, nulls = nothing)\nSort(value; over = nothing, nulls = nothing)\nAsc(; over = nothing, nulls = nothing)\nDesc(; over = nothing, nulls = nothing)\n\nSort order indicator.\n\nUse with Order or Partition nodes.\n\nExamples\n\nList patients ordered by their age.\n\njulia> person = SQLTable(:person, columns = [:person_id, :year_of_birth]);\n\njulia> q = From(:person) |>\n Order(Get.year_of_birth |> Desc(nulls = :first));\n\njulia> print(render(q, tables = [person]))\nSELECT\n \"person_1\".\"person_id\",\n \"person_1\".\"year_of_birth\"\nFROM \"person\" AS \"person_1\"\nORDER BY \"person_1\".\"year_of_birth\" DESC NULLS FIRST\n\n\n\n\n\n","category":"method"},{"location":"reference/#Var","page":"API Reference","title":"Var","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"nodes/variable.jl\"]","category":"page"},{"location":"reference/#FunSQL.Var-Tuple","page":"API Reference","title":"FunSQL.Var","text":"Var(; name)\nVar(name)\nVar.name Var.\"name\" Var[name] Var[\"name\"]\n\nA reference to a query parameter.\n\nSpecify the value for the parameter with Bind to create a correlated subquery or a lateral join.\n\nExamples\n\njulia> person = SQLTable(:person, columns = [:person_id, :year_of_birth]);\n\njulia> q = From(:person) |>\n Where(Get.year_of_birth .> Var.YEAR);\n\njulia> print(render(q, tables = [person]))\nSELECT\n \"person_1\".\"person_id\",\n \"person_1\".\"year_of_birth\"\nFROM \"person\" AS \"person_1\"\nWHERE (\"person_1\".\"year_of_birth\" > :YEAR)\n\n\n\n\n\n","category":"method"},{"location":"reference/#Where","page":"API Reference","title":"Where","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"nodes/where.jl\"]","category":"page"},{"location":"reference/#FunSQL.Where-Tuple","page":"API Reference","title":"FunSQL.Where","text":"Where(; over = nothing, condition)\nWhere(condition; over = nothing)\n\nThe Where node filters the input rows by the given condition.\n\nWhere is translated to a SQL query with a WHERE clause:\n\nSELECT ...\nFROM $over\nWHERE $condition\n\nExamples\n\njulia> person = SQLTable(:person, columns = [:person_id, :year_of_birth]);\n\njulia> q = From(:person) |>\n Where(Fun(\">\", Get.year_of_birth, 2000));\n\njulia> print(render(q, tables = [person]))\nSELECT\n \"person_1\".\"person_id\",\n \"person_1\".\"year_of_birth\"\nFROM \"person\" AS \"person_1\"\nWHERE (\"person_1\".\"year_of_birth\" > 2000)\n\n\n\n\n\n","category":"method"},{"location":"reference/#With","page":"API Reference","title":"With","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"nodes/with.jl\"]","category":"page"},{"location":"reference/#FunSQL.With-Tuple","page":"API Reference","title":"FunSQL.With","text":"With(; over = nothing, args, materialized = nothing)\nWith(args...; over = nothing, materialized = nothing)\n\nWith assigns a name to a temporary dataset. The dataset content can be retrieved within the over query using the From node.\n\nWith is translated to a common table expression:\n\nWITH $args...\nSELECT ...\nFROM $over\n\nExamples\n\njulia> person = SQLTable(:person, columns = [:person_id, :year_of_birth]);\n\njulia> condition_occurrence =\n SQLTable(:condition_occurrence, columns = [:condition_occurrence_id,\n :person_id,\n :condition_concept_id]);\n\njulia> q = From(:person) |>\n Where(Fun.in(Get.person_id, From(:essential_hypertension) |>\n Select(Get.person_id))) |>\n With(:essential_hypertension =>\n From(:condition_occurrence) |>\n Where(Get.condition_concept_id .== 320128));\n\njulia> print(render(q, tables = [person, condition_occurrence]))\nWITH \"essential_hypertension_1\" (\"person_id\") AS (\n SELECT \"condition_occurrence_1\".\"person_id\"\n FROM \"condition_occurrence\" AS \"condition_occurrence_1\"\n WHERE (\"condition_occurrence_1\".\"condition_concept_id\" = 320128)\n)\nSELECT\n \"person_1\".\"person_id\",\n \"person_1\".\"year_of_birth\"\nFROM \"person\" AS \"person_1\"\nWHERE (\"person_1\".\"person_id\" IN (\n SELECT \"essential_hypertension_2\".\"person_id\"\n FROM \"essential_hypertension_1\" AS \"essential_hypertension_2\"\n))\n\n\n\n\n\n","category":"method"},{"location":"reference/#WithExternal","page":"API Reference","title":"WithExternal","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"nodes/with_external.jl\"]","category":"page"},{"location":"reference/#FunSQL.WithExternal-Tuple","page":"API Reference","title":"FunSQL.WithExternal","text":"WithExternal(; over = nothing, args, qualifiers = [], handler = nothing)\nWithExternal(args...; over = nothing, qualifiers = [], handler = nothing)\n\nWithExternal assigns a name to a temporary dataset. The dataset content can be retrieved within the over query using the From node.\n\nThe definition of the dataset is converted to a Pair{SQLTable, SQLClause} object and sent to handler, which can use it, for instance, to construct a SELECT INTO statement.\n\nExamples\n\njulia> person = SQLTable(:person, columns = [:person_id, :year_of_birth]);\n\njulia> condition_occurrence =\n SQLTable(:condition_occurrence, columns = [:condition_occurrence_id,\n :person_id,\n :condition_concept_id]);\n\njulia> handler((tbl, def)) =\n println(\"CREATE TEMP TABLE \", render(ID(tbl.name)), \" AS\\n\",\n render(def), \";\\n\");\n\njulia> q = From(:person) |>\n Where(Fun.in(Get.person_id, From(:essential_hypertension) |>\n Select(Get.person_id))) |>\n WithExternal(:essential_hypertension =>\n From(:condition_occurrence) |>\n Where(Get.condition_concept_id .== 320128),\n handler = handler);\n\njulia> print(render(q, tables = [person, condition_occurrence]))\nCREATE TEMP TABLE \"essential_hypertension\" AS\nSELECT \"condition_occurrence_1\".\"person_id\"\nFROM \"condition_occurrence\" AS \"condition_occurrence_1\"\nWHERE (\"condition_occurrence_1\".\"condition_concept_id\" = 320128);\n\nSELECT\n \"person_1\".\"person_id\",\n \"person_1\".\"year_of_birth\"\nFROM \"person\" AS \"person_1\"\nWHERE (\"person_1\".\"person_id\" IN (\n SELECT \"essential_hypertension_1\".\"person_id\"\n FROM \"essential_hypertension\" AS \"essential_hypertension_1\"\n))\n\n\n\n\n\n","category":"method"},{"location":"reference/#SQLClause","page":"API Reference","title":"SQLClause","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"clauses.jl\"]","category":"page"},{"location":"reference/#FunSQL.AbstractSQLClause","page":"API Reference","title":"FunSQL.AbstractSQLClause","text":"A component of a SQL syntax tree.\n\n\n\n\n\n","category":"type"},{"location":"reference/#FunSQL.SQLClause","page":"API Reference","title":"FunSQL.SQLClause","text":"An opaque wrapper over an arbitrary SQL clause.\n\n\n\n\n\n","category":"type"},{"location":"reference/#AGG","page":"API Reference","title":"AGG","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"clauses/aggregate.jl\"]","category":"page"},{"location":"reference/#FunSQL.AGG-Tuple","page":"API Reference","title":"FunSQL.AGG","text":"AGG(; name, args = [], filter = nothing, over = nothing)\nAGG(name; args = [], filter = nothing, over = nothing)\nAGG(name, args...; filter = nothing, over = nothing)\n\nAn application of an aggregate function.\n\nExamples\n\njulia> c = AGG(:max, :year_of_birth);\n\njulia> print(render(c))\nmax(\"year_of_birth\")\n\njulia> c = AGG(:count, filter = FUN(\">\", :year_of_birth, 1970));\n\njulia> print(render(c))\n(count(*) FILTER (WHERE (\"year_of_birth\" > 1970)))\n\njulia> c = AGG(:row_number, over = PARTITION(:year_of_birth));\n\njulia> print(render(c))\n(row_number() OVER (PARTITION BY \"year_of_birth\"))\n\n\n\n\n\n","category":"method"},{"location":"reference/#AS","page":"API Reference","title":"AS","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"clauses/as.jl\"]","category":"page"},{"location":"reference/#FunSQL.AS-Tuple","page":"API Reference","title":"FunSQL.AS","text":"AS(; over = nothing, name, columns = nothing)\nAS(name; over = nothing, columns = nothing)\n\nAn AS clause.\n\nExamples\n\njulia> c = ID(:person) |> AS(:p);\n\njulia> print(render(c))\n\"person\" AS \"p\"\n\njulia> c = ID(:person) |> AS(:p, columns = [:person_id, :year_of_birth]);\n\njulia> print(render(c))\n\"person\" AS \"p\" (\"person_id\", \"year_of_birth\")\n\n\n\n\n\n","category":"method"},{"location":"reference/#FROM","page":"API Reference","title":"FROM","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"clauses/from.jl\"]","category":"page"},{"location":"reference/#FunSQL.FROM-Tuple","page":"API Reference","title":"FunSQL.FROM","text":"FROM(; over = nothing)\nFROM(over)\n\nA FROM clause.\n\nExamples\n\njulia> c = ID(:person) |> AS(:p) |> FROM() |> SELECT((:p, :person_id));\n\njulia> print(render(c))\nSELECT \"p\".\"person_id\"\nFROM \"person\" AS \"p\"\n\n\n\n\n\n","category":"method"},{"location":"reference/#FUN","page":"API Reference","title":"FUN","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"clauses/function.jl\"]","category":"page"},{"location":"reference/#FunSQL.FUN-Tuple","page":"API Reference","title":"FunSQL.FUN","text":"FUN(; name, args = [])\nFUN(name; args = [])\nFUN(name, args...)\n\nAn invocation of a SQL function or a SQL operator.\n\nExamples\n\njulia> c = FUN(:concat, :city, \", \", :state);\n\njulia> print(render(c))\nconcat(\"city\", ', ', \"state\")\n\njulia> c = FUN(\"||\", :city, \", \", :state);\n\njulia> print(render(c))\n(\"city\" || ', ' || \"state\")\n\njulia> c = FUN(\"SUBSTRING(? FROM ? FOR ?)\", :zip, 1, 3);\n\njulia> print(render(c))\nSUBSTRING(\"zip\" FROM 1 FOR 3)\n\n\n\n\n\n","category":"method"},{"location":"reference/#GROUP","page":"API Reference","title":"GROUP","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"clauses/group.jl\"]","category":"page"},{"location":"reference/#FunSQL.GROUP-Tuple","page":"API Reference","title":"FunSQL.GROUP","text":"GROUP(; over = nothing, by = [], sets = nothing)\nGROUP(by...; over = nothing, sets = nothing)\n\nA GROUP BY clause.\n\nExamples\n\njulia> c = FROM(:person) |>\n GROUP(:year_of_birth) |>\n SELECT(:year_of_birth, AGG(:count));\n\njulia> print(render(c))\nSELECT\n \"year_of_birth\",\n count(*)\nFROM \"person\"\nGROUP BY \"year_of_birth\"\n\n\n\n\n\n","category":"method"},{"location":"reference/#HAVING","page":"API Reference","title":"HAVING","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"clauses/having.jl\"]","category":"page"},{"location":"reference/#FunSQL.HAVING-Tuple","page":"API Reference","title":"FunSQL.HAVING","text":"HAVING(; over = nothing, condition)\nHAVING(condition; over = nothing)\n\nA HAVING clause.\n\nExamples\n\njulia> c = FROM(:person) |>\n GROUP(:year_of_birth) |>\n HAVING(FUN(\">\", AGG(:count), 10)) |>\n SELECT(:person_id);\n\njulia> print(render(c))\nSELECT \"person_id\"\nFROM \"person\"\nGROUP BY \"year_of_birth\"\nHAVING (count(*) > 10)\n\n\n\n\n\n","category":"method"},{"location":"reference/#ID","page":"API Reference","title":"ID","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"clauses/identifier.jl\"]","category":"page"},{"location":"reference/#FunSQL.ID-Tuple","page":"API Reference","title":"FunSQL.ID","text":"ID(; over = nothing, name)\nID(name; over = nothing)\nID(qualifiers, name)\n\nA SQL identifier. Specify over or use the |> operator to make a qualified identifier.\n\nExamples\n\njulia> c = ID(:person);\n\njulia> print(render(c))\n\"person\"\n\njulia> c = ID(:p) |> ID(:birth_datetime);\n\njulia> print(render(c))\n\"p\".\"birth_datetime\"\n\njulia> c = ID([:pg_catalog], :pg_database);\n\njulia> print(render(c))\n\"pg_catalog\".\"pg_database\"\n\n\n\n\n\n","category":"method"},{"location":"reference/#JOIN","page":"API Reference","title":"JOIN","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"clauses/join.jl\"]","category":"page"},{"location":"reference/#FunSQL.JOIN-Tuple","page":"API Reference","title":"FunSQL.JOIN","text":"JOIN(; over = nothing, joinee, on, left = false, right = false, lateral = false)\nJOIN(joinee; over = nothing, on, left = false, right = false, lateral = false)\nJOIN(joinee, on; over = nothing, left = false, right = false, lateral = false)\n\nA JOIN clause.\n\nExamples\n\njulia> c = FROM(:p => :person) |>\n JOIN(:l => :location,\n on = FUN(\"=\", (:p, :location_id), (:l, :location_id)),\n left = true) |>\n SELECT((:p, :person_id), (:l, :state));\n\njulia> print(render(c))\nSELECT\n \"p\".\"person_id\",\n \"l\".\"state\"\nFROM \"person\" AS \"p\"\nLEFT JOIN \"location\" AS \"l\" ON (\"p\".\"location_id\" = \"l\".\"location_id\")\n\n\n\n\n\n","category":"method"},{"location":"reference/#LIMIT","page":"API Reference","title":"LIMIT","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"clauses/limit.jl\"]","category":"page"},{"location":"reference/#FunSQL.LIMIT-Tuple","page":"API Reference","title":"FunSQL.LIMIT","text":"LIMIT(; over = nothing, offset = nothing, limit = nothing, with_ties = false)\nLIMIT(limit; over = nothing, offset = nothing, with_ties = false)\nLIMIT(offset, limit; over = nothing, with_ties = false)\nLIMIT(start:stop; over = nothing, with_ties = false)\n\nA LIMIT clause.\n\nExamples\n\njulia> c = FROM(:person) |>\n LIMIT(1) |>\n SELECT(:person_id);\n\njulia> print(render(c))\nSELECT \"person_id\"\nFROM \"person\"\nFETCH FIRST 1 ROW ONLY\n\n\n\n\n\n","category":"method"},{"location":"reference/#LIT","page":"API Reference","title":"LIT","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"clauses/literal.jl\"]","category":"page"},{"location":"reference/#FunSQL.LIT-Tuple","page":"API Reference","title":"FunSQL.LIT","text":"LIT(; val)\nLIT(val)\n\nA SQL literal.\n\nIn a context of a SQL clause, missing, numbers, strings and datetime values are automatically converted to SQL literals.\n\nExamples\n\njulia> c = LIT(missing);\n\njulia> print(render(c))\nNULL\n\njulia> c = LIT(\"SQL is fun!\");\n\njulia> print(render(c))\n'SQL is fun!'\n\n\n\n\n\n","category":"method"},{"location":"reference/#NOTE","page":"API Reference","title":"NOTE","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"clauses/note.jl\"]","category":"page"},{"location":"reference/#FunSQL.NOTE-Tuple","page":"API Reference","title":"FunSQL.NOTE","text":"NOTE(; over = nothing, text, postfix = false)\nNOTE(text; over = nothing, postfix = false)\n\nA free-form prefix of postfix annotation.\n\nExamples\n\njulia> c = FROM(:p => :person) |>\n NOTE(\"TABLESAMPLE SYSTEM (50)\", postfix = true) |>\n SELECT((:p, :person_id));\n\njulia> print(render(c))\nSELECT \"p\".\"person_id\"\nFROM \"person\" AS \"p\" TABLESAMPLE SYSTEM (50)\n\n\n\n\n\n","category":"method"},{"location":"reference/#ORDER","page":"API Reference","title":"ORDER","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"clauses/order.jl\"]","category":"page"},{"location":"reference/#FunSQL.ORDER-Tuple","page":"API Reference","title":"FunSQL.ORDER","text":"ORDER(; over = nothing, by = [])\nORDER(by...; over = nothing)\n\nAn ORDER BY clause.\n\nExamples\n\njulia> c = FROM(:person) |>\n ORDER(:year_of_birth) |>\n SELECT(:person_id);\n\njulia> print(render(c))\nSELECT \"person_id\"\nFROM \"person\"\nORDER BY \"year_of_birth\"\n\n\n\n\n\n","category":"method"},{"location":"reference/#PARTITION","page":"API Reference","title":"PARTITION","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"clauses/partition.jl\"]","category":"page"},{"location":"reference/#FunSQL.PARTITION-Tuple","page":"API Reference","title":"FunSQL.PARTITION","text":"PARTITION(; over = nothing, by = [], order_by = [], frame = nothing)\nPARTITION(by...; over = nothing, order_by = [], frame = nothing)\n\nA window definition clause.\n\nExamples\n\njulia> c = FROM(:person) |>\n SELECT(:person_id,\n AGG(:row_number, over = PARTITION(:year_of_birth)));\n\njulia> print(render(c))\nSELECT\n \"person_id\",\n (row_number() OVER (PARTITION BY \"year_of_birth\"))\nFROM \"person\"\n\njulia> c = FROM(:person) |>\n WINDOW(:w1 => PARTITION(:year_of_birth),\n :w2 => :w1 |> PARTITION(order_by = [:month_of_birth, :day_of_birth])) |>\n SELECT(:person_id, AGG(:row_number, over = :w2));\n\njulia> print(render(c))\nSELECT\n \"person_id\",\n (row_number() OVER (\"w2\"))\nFROM \"person\"\nWINDOW\n \"w1\" AS (PARTITION BY \"year_of_birth\"),\n \"w2\" AS (\"w1\" ORDER BY \"month_of_birth\", \"day_of_birth\")\n\njulia> c = FROM(:person) |>\n GROUP(:year_of_birth) |>\n SELECT(:year_of_birth,\n AGG(:avg,\n AGG(:count),\n over = PARTITION(order_by = [:year_of_birth],\n frame = (mode = :range, start = -1, finish = 1))));\n\njulia> print(render(c))\nSELECT\n \"year_of_birth\",\n (avg(count(*)) OVER (ORDER BY \"year_of_birth\" RANGE BETWEEN 1 PRECEDING AND 1 FOLLOWING))\nFROM \"person\"\nGROUP BY \"year_of_birth\"\n\n\n\n\n\n","category":"method"},{"location":"reference/#SELECT","page":"API Reference","title":"SELECT","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"clauses/select.jl\"]","category":"page"},{"location":"reference/#FunSQL.SELECT-Tuple","page":"API Reference","title":"FunSQL.SELECT","text":"SELECT(; over = nothing, top = nothing, distinct = false, args)\nSELECT(args...; over = nothing, top = nothing, distinct = false)\n\nA SELECT clause. Unlike raw SQL, SELECT() should be placed at the end of a clause chain.\n\nSet distinct to true to add a DISTINCT modifier.\n\nExamples\n\njulia> c = SELECT(true, false);\n\njulia> print(render(c))\nSELECT\n TRUE,\n FALSE\n\njulia> c = FROM(:location) |>\n SELECT(distinct = true, :zip);\n\njulia> print(render(c))\nSELECT DISTINCT \"zip\"\nFROM \"location\"\n\n\n\n\n\n","category":"method"},{"location":"reference/#SORT,-ASC,-and-DESC","page":"API Reference","title":"SORT, ASC, and DESC","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"clauses/sort.jl\"]","category":"page"},{"location":"reference/#FunSQL.ASC-Tuple{}","page":"API Reference","title":"FunSQL.ASC","text":"ASC(; over = nothing, nulls = nothing)\n\nAscending order indicator.\n\n\n\n\n\n","category":"method"},{"location":"reference/#FunSQL.DESC-Tuple{}","page":"API Reference","title":"FunSQL.DESC","text":"DESC(; over = nothing, nulls = nothing)\n\nDescending order indicator.\n\n\n\n\n\n","category":"method"},{"location":"reference/#FunSQL.SORT-Tuple","page":"API Reference","title":"FunSQL.SORT","text":"SORT(; over = nothing, value, nulls = nothing)\nSORT(value; over = nothing, nulls = nothing)\nASC(; over = nothing, nulls = nothing)\nDESC(; over = nothing, nulls = nothing)\n\nSort order options.\n\nExamples\n\njulia> c = FROM(:person) |>\n ORDER(:year_of_birth |> DESC()) |>\n SELECT(:person_id);\n\njulia> print(render(c))\nSELECT \"person_id\"\nFROM \"person\"\nORDER BY \"year_of_birth\" DESC\n\n\n\n\n\n","category":"method"},{"location":"reference/#UNION","page":"API Reference","title":"UNION","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"clauses/union.jl\"]","category":"page"},{"location":"reference/#FunSQL.UNION-Tuple","page":"API Reference","title":"FunSQL.UNION","text":"UNION(; over = nothing, all = false, args)\nUNION(args...; over = nothing, all = false)\n\nA UNION clause.\n\nExamples\n\njulia> c = FROM(:measurement) |>\n SELECT(:person_id, :date => :measurement_date) |>\n UNION(all = true,\n FROM(:observation) |>\n SELECT(:person_id, :date => :observation_date));\n\njulia> print(render(c))\nSELECT\n \"person_id\",\n \"measurement_date\" AS \"date\"\nFROM \"measurement\"\nUNION ALL\nSELECT\n \"person_id\",\n \"observation_date\" AS \"date\"\nFROM \"observation\"\n\n\n\n\n\n","category":"method"},{"location":"reference/#VALUES","page":"API Reference","title":"VALUES","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"clauses/values.jl\"]","category":"page"},{"location":"reference/#FunSQL.VALUES-Tuple","page":"API Reference","title":"FunSQL.VALUES","text":"VALUES(; rows)\nVALUES(rows)\n\nA VALUES clause.\n\nExamples\n\njulia> c = VALUES([(\"SQL\", 1974), (\"Julia\", 2012), (\"FunSQL\", 2021)]);\n\njulia> print(render(c))\nVALUES\n ('SQL', 1974),\n ('Julia', 2012),\n ('FunSQL', 2021)\n\n\n\n\n\n","category":"method"},{"location":"reference/#VAR","page":"API Reference","title":"VAR","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"clauses/variable.jl\"]","category":"page"},{"location":"reference/#FunSQL.VAR-Tuple","page":"API Reference","title":"FunSQL.VAR","text":"VAR(; name)\nVAR(name)\n\nA placeholder in a parameterized query.\n\nExamples\n\njulia> c = VAR(:year);\n\njulia> print(render(c))\n:year\n\n\n\n\n\n","category":"method"},{"location":"reference/#WHERE","page":"API Reference","title":"WHERE","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"clauses/where.jl\"]","category":"page"},{"location":"reference/#FunSQL.WHERE-Tuple","page":"API Reference","title":"FunSQL.WHERE","text":"WHERE(; over = nothing, condition)\nWHERE(condition; over = nothing)\n\nA WHERE clause.\n\nExamples\n\njulia> c = FROM(:location) |>\n WHERE(FUN(\"=\", :zip, \"60614\")) |>\n SELECT(:location_id);\n\njulia> print(render(c))\nSELECT \"location_id\"\nFROM \"location\"\nWHERE (\"zip\" = '60614')\n\n\n\n\n\n","category":"method"},{"location":"reference/#WINDOW","page":"API Reference","title":"WINDOW","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"clauses/window.jl\"]","category":"page"},{"location":"reference/#FunSQL.WINDOW-Tuple","page":"API Reference","title":"FunSQL.WINDOW","text":"WINDOW(; over = nothing, args)\nWINDOW(args...; over = nothing)\n\nA WINDOW clause.\n\nExamples\n\njulia> c = FROM(:person) |>\n WINDOW(:w1 => PARTITION(:year_of_birth),\n :w2 => :w1 |> PARTITION(order_by = [:month_of_birth, :day_of_birth])) |>\n SELECT(:person_id, AGG(\"row_number\", over = :w2));\n\njulia> print(render(c))\nSELECT\n \"person_id\",\n (row_number() OVER (\"w2\"))\nFROM \"person\"\nWINDOW\n \"w1\" AS (PARTITION BY \"year_of_birth\"),\n \"w2\" AS (\"w1\" ORDER BY \"month_of_birth\", \"day_of_birth\")\n\n\n\n\n\n","category":"method"},{"location":"reference/#WITH","page":"API Reference","title":"WITH","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"clauses/with.jl\"]","category":"page"},{"location":"reference/#FunSQL.WITH-Tuple","page":"API Reference","title":"FunSQL.WITH","text":"WITH(; over = nothing, recursive = false, args)\nWITH(args...; over = nothing, recursive = false)\n\nA WITH clause.\n\nExamples\n\njulia> c = FROM(:person) |>\n WHERE(FUN(:in, :person_id,\n FROM(:essential_hypertension) |>\n SELECT(:person_id))) |>\n SELECT(:person_id, :year_of_birth) |>\n WITH(FROM(:condition_occurrence) |>\n WHERE(FUN(\"=\", :condition_concept_id, 320128)) |>\n SELECT(:person_id) |>\n AS(:essential_hypertension));\n\njulia> print(render(c))\nWITH \"essential_hypertension\" AS (\n SELECT \"person_id\"\n FROM \"condition_occurrence\"\n WHERE (\"condition_concept_id\" = 320128)\n)\nSELECT\n \"person_id\",\n \"year_of_birth\"\nFROM \"person\"\nWHERE (\"person_id\" IN (\n SELECT \"person_id\"\n FROM \"essential_hypertension\"\n))\n\njulia> c = FROM(:essential_hypertension) |>\n SELECT(*) |>\n WITH(recursive = true,\n FROM(:concept) |>\n WHERE(FUN(\"=\", :concept_id, 320128)) |>\n SELECT(:concept_id, :concept_name) |>\n UNION(all = true,\n FROM(:eh => :essential_hypertension) |>\n JOIN(:cr => :concept_relationship,\n FUN(\"=\", (:eh, :concept_id), (:cr, :concept_id_1))) |>\n JOIN(:c => :concept,\n FUN(\"=\", (:cr, :concept_id_2), (:c, :concept_id))) |>\n WHERE(FUN(\"=\", (:cr, :relationship_id), \"Subsumes\")) |>\n SELECT((:c, :concept_id), (:c, :concept_name))) |>\n AS(:essential_hypertension, columns = [:concept_id, :concept_name]));\n\njulia> print(render(c))\nWITH RECURSIVE \"essential_hypertension\" (\"concept_id\", \"concept_name\") AS (\n SELECT\n \"concept_id\",\n \"concept_name\"\n FROM \"concept\"\n WHERE (\"concept_id\" = 320128)\n UNION ALL\n SELECT\n \"c\".\"concept_id\",\n \"c\".\"concept_name\"\n FROM \"essential_hypertension\" AS \"eh\"\n JOIN \"concept_relationship\" AS \"cr\" ON (\"eh\".\"concept_id\" = \"cr\".\"concept_id_1\")\n JOIN \"concept\" AS \"c\" ON (\"cr\".\"concept_id_2\" = \"c\".\"concept_id\")\n WHERE (\"cr\".\"relationship_id\" = 'Subsumes')\n)\nSELECT *\nFROM \"essential_hypertension\"\n\n\n\n\n\n","category":"method"},{"location":"test/nodes/#SQL-Nodes","page":"SQL Nodes","title":"SQL Nodes","text":"","category":"section"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"using FunSQL\n\nusing FunSQL:\n Agg, Append, As, Asc, Bind, CrossJoin, Define, Desc, Fun, From, Get,\n Group, Highlight, Iterate, Join, LeftJoin, Limit, Lit, Order, Over,\n Partition, SQLNode, SQLTable, Select, Sort, Var, Where, With,\n WithExternal, ID, render","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"We start with specifying the database model.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"const concept =\n SQLTable(:concept, columns = [:concept_id, :vocabulary_id, :concept_code, :concept_name])\n\nconst location =\n SQLTable(:location, columns = [:location_id, :city, :state])\n\nconst person =\n SQLTable(:person, columns = [:person_id, :gender_concept_id, :year_of_birth, :month_of_birth, :day_of_birth, :birth_datetime, :location_id])\n\nconst visit_occurrence =\n SQLTable(:visit_occurrence, columns = [:visit_occurrence_id, :person_id, :visit_start_date, :visit_end_date])\n\nconst measurement =\n SQLTable(:measurement, columns = [:measurement_id, :person_id, :measurement_concept_id, :measurement_date])\n\nconst observation =\n SQLTable(:observation, columns = [:observation_id, :person_id, :observation_concept_id, :observation_date])","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"In FunSQL, a SQL query is generated from a tree of SQLNode objects. The nodes are created using constructors with familiar SQL names and connected together using the chain (|>) operator.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Where(Fun.\">\"(Get.year_of_birth, 2000)) |>\n Select(Get.person_id)\n#-> (…) |> Select(…)","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Displaying a SQLNode object shows how it was constructed.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"display(q)\n#=>\nlet person = SQLTable(:person, …),\n q1 = From(person),\n q2 = q1 |> Where(Fun.\">\"(Get.year_of_birth, 2000)),\n q3 = q2 |> Select(Get.person_id)\n q3\nend\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Each node wraps a concrete node object, which can be accessed using the indexing operator.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q[]\n#-> ((…) |> Select(…))[]\n\ndisplay(q[])\n#=>\nlet person = SQLTable(:person, …),\n q1 = From(person),\n q2 = q1 |> Where(Fun.\">\"(Get.year_of_birth, 2000)),\n q3 = q2 |> Select(Get.person_id)\n q3[]\nend\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"The SQL query is generated using the function render().","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"print(render(q))\n#=>\nSELECT \"person_1\".\"person_id\"\nFROM \"person\" AS \"person_1\"\nWHERE (\"person_1\".\"year_of_birth\" > 2000)\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Ill-formed queries are detected.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |> Agg.count() |> Select(Get.person_id)\nrender(q)\n#=>\nERROR: FunSQL.IllFormedError in:\nlet person = SQLTable(:person, …),\n q1 = From(person),\n q2 = q1 |> Agg.count() |> Select(Get.person_id)\n q2\nend\n=#\n\nq = From(person) |> Fun.current_date()\n#=>\nERROR: FunSQL.RebaseError in:\nFun.current_date()\n=#","category":"page"},{"location":"test/nodes/#@funsql","page":"SQL Nodes","title":"@funsql","text":"","category":"section"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"The @funsql macro provides alternative notation for specifying FunSQL queries.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = @funsql begin\n from(person)\n filter(year_of_birth > 2000)\n select(person_id)\nend\n\ndisplay(q)\n#=>\nlet q1 = From(:person),\n q2 = q1 |> Where(Fun.\">\"(Get.year_of_birth, 2000)),\n q3 = q2 |> Select(Get.person_id)\n q3\nend\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"We can combine @funsql notation with regular Julia code.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = @funsql begin\n from(person)\n $(Where(Get.year_of_birth .> 2000))\n select(person_id)\nend\n\ndisplay(q)\n#=>\nlet q1 = From(:person),\n q2 = q1 |> Where(Fun.\">\"(Get.year_of_birth, 2000)),\n q3 = q2 |> Select(Get.person_id)\n q3\nend\n=#\n\nq = From(:person) |>\n @funsql(filter(year_of_birth > 2000)) |>\n Select(Get.person_id)\n\ndisplay(q)\n#=>\nlet q1 = From(:person),\n q2 = q1 |> Where(Fun.\">\"(Get.year_of_birth, 2000)),\n q3 = q2 |> Select(Get.person_id)\n q3\nend\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"The @funsql notation allows us to encapsulate query fragments into query functions.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"@funsql adults() = from(person).filter(2020 - year_of_birth >= 16)\n\ndisplay(@funsql adults())\n#=>\nlet q1 = From(:person),\n q2 = q1 |> Where(Fun.\">=\"(Fun.\"-\"(2020, Get.year_of_birth), 16))\n q2\nend\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Query functions defined with @funsql can accept parameters.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"@funsql concept_by_code(v, c) =\n begin\n from(concept)\n filter(vocabulary_id == $v && concept_code == $c)\n end\n\ndisplay(@funsql concept_by_code(\"SNOMED\", \"22298006\"))\n#=>\nlet q1 = From(:concept),\n q2 = q1 |>\n Where(Fun.and(Fun.\"=\"(Get.vocabulary_id, \"SNOMED\"),\n Fun.\"=\"(Get.concept_code, \"22298006\")))\n q2\nend\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Query functions support ... notation.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"@funsql concept_by_code(v, cs...) =\n begin\n from(concept)\n filter(vocabulary_id == $v && in(concept_code, $(cs...)))\n end\n\ndisplay(@funsql concept_by_code(\"Visit\", \"IP\", \"ER\"))\n#=>\nlet q1 = From(:concept),\n q2 = q1 |>\n Where(Fun.and(Fun.\"=\"(Get.vocabulary_id, \"Visit\"),\n Fun.in(Get.concept_code, \"IP\", \"ER\")))\n q2\nend\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Query functions support keyword arguments and default values.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"@funsql age(yob = year_of_birth; at = fun(`EXTRACT(YEAR FROM CURRENT_DATE) `)) =\n ($at - $yob)\n\nq = @funsql begin\n from(person)\n define(\n age => age(),\n age_in_2000 => age(at = 2000))\nend\n\ndisplay(q)\n#=>\nlet q1 = From(:person),\n q2 = q1 |>\n Define(Fun.\"-\"(Fun.\"EXTRACT(YEAR FROM CURRENT_DATE) \"(),\n Get.year_of_birth) |>\n As(:age),\n Fun.\"-\"(2000, Get.year_of_birth) |> As(:age_in_2000))\n q2\nend\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"A parameter of a query function accepts a type declaration.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"@funsql concept(c::String, v::String = \"SNOMED\") =\n concept_by_code($v, $c)\n\n@funsql concept(id::Int) =\n from(concept).filter(concept_id == $id)\n\ndisplay(@funsql concept(\"22298006\"))\n#=>\nlet q1 = From(:concept),\n q2 = q1 |>\n Where(Fun.and(Fun.\"=\"(Get.vocabulary_id, \"SNOMED\"),\n Fun.\"=\"(Get.concept_code, \"22298006\")))\n q2\nend\n=#\n\ndisplay(@funsql concept(4329847))\n#=>\nlet q1 = From(:concept),\n q2 = q1 |> Where(Fun.\"=\"(Get.concept_id, 4329847))\n q2\nend\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"A single @funsql macro can wrap multiple definitions.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"@funsql begin\n SNOMED(codes...) = concept_by_code(\"SNOMED\", $(codes...))\n\n `MYOCARDIAL INFARCTION`() = SNOMED(\"22298006\")\nend\n\ndisplay(@funsql `MYOCARDIAL INFARCTION`())\n#=>\nlet q1 = From(:concept),\n q2 = q1 |>\n Where(Fun.and(Fun.\"=\"(Get.vocabulary_id, \"SNOMED\"),\n Fun.\"=\"(Get.concept_code, \"22298006\")))\n q2\nend\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"A query function may have a docstring.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"@funsql begin\n \"SNOMED concept set with the given `codes`\"\n SNOMED\n\n \"Visit concept set with the given `codes`\"\n Visit(codes...) = concept_by_code(\"Visit\", $(codes...))\nend\n\n@doc funsql_SNOMED\n#-> SNOMED concept set with the given `codes`\n\n@doc funsql_Visit\n#-> Visit concept set with the given `codes`","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"An ill-formed @funsql query triggers an error.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"@funsql for p in person; end\n#=>\nERROR: LoadError: FunSQL.TransliterationError: ill-formed @funsql notation:\nquote\n for p = person\n end\nend\nin expression starting at …\n=#","category":"page"},{"location":"test/nodes/#Literals","page":"SQL Nodes","title":"Literals","text":"","category":"section"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"A SQL value is created with Lit() constructor.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"e = Lit(\"SQL is fun!\")\n#-> Lit(\"SQL is fun!\")","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"In a SELECT clause, bare literal expressions get an alias \"_\".","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = Select(e)\n\nprint(render(q))\n#=>\nSELECT 'SQL is fun!' AS \"_\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Values of certain Julia data types are automatically converted to SQL literals when they are used in the context of a SQL node.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"using Dates\n\nq = Select(\"null\" => missing,\n \"boolean\" => true,\n \"integer\" => 42,\n \"text\" => \"SQL is fun!\",\n \"date\" => Date(2000))","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Such plain literals could also be used in @funsql notation.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = @funsql select(null => missing,\n boolean => true,\n integer => 42,\n text => \"SQL is fun!\",\n date => $(Date(2000)))\n\ndisplay(q)\n#=>\nSelect(missing |> As(:null),\n true |> As(:boolean),\n 42 |> As(:integer),\n \"SQL is fun!\" |> As(:text),\n Dates.Date(\"2000-01-01\") |> As(:date))\n=#","category":"page"},{"location":"test/nodes/#Attributes","page":"SQL Nodes","title":"Attributes","text":"","category":"section"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"To reference a table attribute, we use the Get constructor.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"e = Get(:person_id)\n#-> Get.person_id","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Alternatively, use shorthand notation.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Get.person_id\n#-> Get.person_id\n\nGet.\"person_id\"\n#-> Get.person_id\n\nGet[:person_id]\n#-> Get.person_id\n\nGet[\"person_id\"]\n#-> Get.person_id","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Hierarchical notation is supported.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"e = Get.p.person_id\n#-> Get.p.person_id\n\nGet.p |> Get.person_id\n#-> Get.p.person_id","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"In the context where a SQL node is expected, a bare symbol is automatically converted to a reference.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = Select(:person_id)\n\ndisplay(q)\n#-> Select(Get.person_id)","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"@funsql macro translates an identifier to a symbol. In suitable context, this symbol will be translated to a column reference.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"@funsql person_id\n#-> :person_id","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"@funsql notation supports hierarchical references.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"@funsql p.person_id\n#-> Get.p.person_id","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Use backticks to represent a name that is not a valid identifier.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"@funsql `person_id`\n#-> :person_id\n\n@funsql `p`.`person_id`\n#-> Get.p.person_id","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Get can also create bound references.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person)\n\ne = Get(over = q, :year_of_birth)\n#-> (…) |> Get.year_of_birth\n\ndisplay(e)\n#=>\nlet person = SQLTable(:person, …),\n q1 = From(person)\n q1.year_of_birth\nend\n=#\n\nq.person_id\n#-> (…) |> Get.person_id\n\nq.\"person_id\"\n#-> (…) |> Get.person_id\n\nq[:person_id]\n#-> (…) |> Get.person_id\n\nq[\"person_id\"]\n#-> (…) |> Get.person_id","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Get is used for dereferencing an alias created with As.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n As(:p) |>\n Select(Get.p.person_id)\n\nprint(render(q))\n#=>\nSELECT \"person_1\".\"person_id\"\nFROM \"person\" AS \"person_1\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"This is particularly useful when you need to disambiguate the output of Join.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n As(:p) |>\n Join(From(location) |> As(:l),\n on = Get.p.location_id .== Get.l.location_id) |>\n Select(Get.p.person_id, Get.l.state)\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"person_id\",\n \"location_1\".\"state\"\nFROM \"person\" AS \"person_1\"\nJOIN \"location\" AS \"location_1\" ON (\"person_1\".\"location_id\" = \"location_1\".\"location_id\")\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"When Get refers to an unknown attribute, an error is reported.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = Select(Get.person_id)\n\nprint(render(q))\n#=>\nERROR: FunSQL.ReferenceError: cannot find `person_id` in:\nSelect(Get.person_id)\n=#\n\nq = From(person) |>\n As(:p) |>\n Select(Get.q.person_id)\n\nprint(render(q))\n#=>\nERROR: FunSQL.ReferenceError: cannot find `q` in:\nlet person = SQLTable(:person, …),\n q1 = From(person),\n q2 = q1 |> As(:p) |> Select(Get.q.person_id)\n q2\nend\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"An attribute defined in a Join shadows any previously defined attributes with the same name.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = person |>\n Join(person, true) |>\n Select(Get.person_id)\n\nprint(render(q))\n#=>\nSELECT \"person_2\".\"person_id\"\nFROM \"person\" AS \"person_1\"\nCROSS JOIN \"person\" AS \"person_2\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"An incomplete hierarchical reference, as well as an unexpected hierarchical reference, will result in an error.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = person |>\n As(:p) |>\n Select(Get.p)\n\nprint(render(q))\n#=>\nERROR: FunSQL.ReferenceError: incomplete reference `p` in:\nlet person = SQLTable(:person, …),\n q1 = From(person),\n q2 = q1 |> As(:p) |> Select(Get.p)\n q2\nend\n=#\n\nq = person |>\n Select(Get.person_id.year_of_birth)\n\nprint(render(q))\n#=>\nERROR: FunSQL.ReferenceError: unexpected reference after `person_id` in:\nlet person = SQLTable(:person, …),\n q1 = From(person),\n q2 = q1 |> Select(Get.person_id.year_of_birth)\n q2\nend\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"A reference bound to any node other than Get will cause an error.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = (qₚ = From(person)) |> Select(qₚ.person_id)\n\nprint(render(q))\n#=>\nERROR: FunSQL.IllFormedError in:\nlet person = SQLTable(:person, …),\n q1 = From(person),\n q2 = q1 |> Select(q1.person_id)\n q2\nend\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Any expression could be given a name and attached to a query using the Define constructor.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Define(:age => Fun.now() .- Get.birth_datetime)\n#-> (…) |> Define(…)\n\ndisplay(q)\n#=>\nlet person = SQLTable(:person, …),\n q1 = From(person),\n q2 = q1 |> Define(Fun.\"-\"(Fun.now(), Get.birth_datetime) |> As(:age))\n q2\nend\n=#\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"person_id\",\n \"person_1\".\"gender_concept_id\",\n \"person_1\".\"year_of_birth\",\n \"person_1\".\"month_of_birth\",\n \"person_1\".\"day_of_birth\",\n \"person_1\".\"birth_datetime\",\n \"person_1\".\"location_id\",\n (now() - \"person_1\".\"birth_datetime\") AS \"age\"\nFROM \"person\" AS \"person_1\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"This expression could be referred to by name as if it were a regular table attribute.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"print(render(q |> Where(Get.age .> \"16 years\")))\n#=>\nSELECT\n \"person_2\".\"person_id\",\n \"person_2\".\"gender_concept_id\",\n \"person_2\".\"year_of_birth\",\n \"person_2\".\"month_of_birth\",\n \"person_2\".\"day_of_birth\",\n \"person_2\".\"birth_datetime\",\n \"person_2\".\"location_id\",\n \"person_2\".\"age\"\nFROM (\n SELECT\n \"person_1\".\"person_id\",\n \"person_1\".\"gender_concept_id\",\n \"person_1\".\"year_of_birth\",\n \"person_1\".\"month_of_birth\",\n \"person_1\".\"day_of_birth\",\n \"person_1\".\"birth_datetime\",\n \"person_1\".\"location_id\",\n (now() - \"person_1\".\"birth_datetime\") AS \"age\"\n FROM \"person\" AS \"person_1\"\n) AS \"person_2\"\nWHERE (\"person_2\".\"age\" > '16 years')\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"A Define node can be created using @funsql notation.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = @funsql from(person).define(age => 2000 - year_of_birth)\n\ndisplay(q)\n#=>\nlet q1 = From(:person),\n q2 = q1 |> Define(Fun.\"-\"(2000, Get.year_of_birth) |> As(:age))\n q2\nend\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Define does not create a nested query if the definition is a literal or a simple reference.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Define(:year => Get.year_of_birth,\n :threshold => 2000) |>\n Where(Get.year .>= Get.threshold)\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"person_id\",\n \"person_1\".\"gender_concept_id\",\n \"person_1\".\"year_of_birth\",\n \"person_1\".\"month_of_birth\",\n \"person_1\".\"day_of_birth\",\n \"person_1\".\"birth_datetime\",\n \"person_1\".\"location_id\",\n \"person_1\".\"year_of_birth\" AS \"year\",\n 2000 AS \"threshold\"\nFROM \"person\" AS \"person_1\"\nWHERE (\"person_1\".\"year_of_birth\" >= 2000)\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Define can be used to override an existing field.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Define(:person_id => Get.year_of_birth, :year_of_birth => Get.person_id)\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"year_of_birth\" AS \"person_id\",\n \"person_1\".\"gender_concept_id\",\n \"person_1\".\"person_id\" AS \"year_of_birth\",\n \"person_1\".\"month_of_birth\",\n \"person_1\".\"day_of_birth\",\n \"person_1\".\"birth_datetime\",\n \"person_1\".\"location_id\"\nFROM \"person\" AS \"person_1\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Define has no effect if none of the defined fields are used in the query.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Define(:age => 2020 .- Get.year_of_birth) |>\n Select(Get.person_id, Get.year_of_birth)\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"person_id\",\n \"person_1\".\"year_of_birth\"\nFROM \"person\" AS \"person_1\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Define can be used after Select.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Select(Get.person_id, Get.year_of_birth) |>\n Define(:age => 2020 .- Get.year_of_birth)\n\nprint(render(q))\n#=>\nSELECT\n \"person_2\".\"person_id\",\n \"person_2\".\"year_of_birth\",\n (2020 - \"person_2\".\"year_of_birth\") AS \"age\"\nFROM (\n SELECT\n \"person_1\".\"person_id\",\n \"person_1\".\"year_of_birth\"\n FROM \"person\" AS \"person_1\"\n) AS \"person_2\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Define requires that all definitions have a unique alias.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"From(person) |>\nDefine(:age => Fun.now() .- Get.birth_datetime,\n :age => Fun.current_timestamp() .- Get.birth_datetime)\n#=>\nERROR: FunSQL.DuplicateLabelError: `age` is used more than once in:\nDefine(Fun.\"-\"(Fun.now(), Get.birth_datetime) |> As(:age),\n Fun.\"-\"(Fun.current_timestamp(), Get.birth_datetime) |> As(:age))\n=#","category":"page"},{"location":"test/nodes/#Variables","page":"SQL Nodes","title":"Variables","text":"","category":"section"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"A query variable is created with the Var constructor.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"e = Var(:YEAR)\n#-> Var.YEAR","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Alternatively, use shorthand notation.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Var.YEAR\n#-> Var.YEAR\n\nVar.\"YEAR\"\n#-> Var.YEAR\n\nVar[:YEAR]\n#-> Var.YEAR\n\nVar[\"YEAR\"]\n#-> Var.YEAR","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"A variable could be created with @funsql notation.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"@funsql :YEAR\n#-> Var.YEAR","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Unbound query variables are serialized as query parameters.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Where(Get.year_of_birth .> Var.YEAR)\n\nsql = render(q)\n\nprint(sql)\n#=>\nSELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"location_id\"\nFROM \"person\" AS \"person_1\"\nWHERE (\"person_1\".\"year_of_birth\" > :YEAR)\n=#\n\nsql.vars\n#-> [:YEAR]","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Query variables could be bound using the Bind constructor.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q0(person_id) =\n From(visit_occurrence) |>\n Where(Get.person_id .== Var.PERSON_ID) |>\n Bind(:PERSON_ID => person_id)\n\nq0(1)\n#-> (…) |> Bind(…)\n\ndisplay(q0(1))\n#=>\nlet visit_occurrence = SQLTable(:visit_occurrence, …),\n q1 = From(visit_occurrence),\n q2 = q1 |> Where(Fun.\"=\"(Get.person_id, Var.PERSON_ID))\n q2 |> Bind(1 |> As(:PERSON_ID))\nend\n=#\n\nprint(render(q0(1)))\n#=>\nSELECT\n \"visit_occurrence_1\".\"visit_occurrence_id\",\n \"visit_occurrence_1\".\"person_id\",\n \"visit_occurrence_1\".\"visit_start_date\",\n \"visit_occurrence_1\".\"visit_end_date\"\nFROM \"visit_occurrence\" AS \"visit_occurrence_1\"\nWHERE (\"visit_occurrence_1\".\"person_id\" = 1)\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"A Bind node can be created with @funsql notation.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = @funsql begin\n from(visit_occurrence)\n filter(person_id == :PERSON_ID)\n bind(:PERSON_ID => person_id)\nend\n\ndisplay(q)\n#=>\nlet q1 = From(:visit_occurrence),\n q2 = q1 |> Where(Fun.\"=\"(Get.person_id, Var.PERSON_ID))\n q2 |> Bind(Get.person_id |> As(:PERSON_ID))\nend\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Bind lets us create correlated subqueries.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Where(Fun.exists(q0(Get.person_id)))\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"location_id\"\nFROM \"person\" AS \"person_1\"\nWHERE (EXISTS (\n SELECT NULL AS \"_\"\n FROM \"visit_occurrence\" AS \"visit_occurrence_1\"\n WHERE (\"visit_occurrence_1\".\"person_id\" = \"person_1\".\"person_id\")\n))\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"When an argument to Bind is an aggregate, it must be evaluated in a nested subquery.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q0(person_id, date) =\n From(observation) |>\n Where(Fun.and(Get.person_id .== Var.PERSON_ID,\n Get.observation_date .>= Var.DATE)) |>\n Bind(:PERSON_ID => person_id, :DATE => date)\n\nq = From(visit_occurrence) |>\n Group(Get.person_id) |>\n Where(Fun.exists(q0(Get.person_id, Agg.max(Get.visit_start_date))))\n\nprint(render(q))\n#=>\nSELECT \"visit_occurrence_2\".\"person_id\"\nFROM (\n SELECT\n \"visit_occurrence_1\".\"person_id\",\n max(\"visit_occurrence_1\".\"visit_start_date\") AS \"max\"\n FROM \"visit_occurrence\" AS \"visit_occurrence_1\"\n GROUP BY \"visit_occurrence_1\".\"person_id\"\n) AS \"visit_occurrence_2\"\nWHERE (EXISTS (\n SELECT NULL AS \"_\"\n FROM \"observation\" AS \"observation_1\"\n WHERE\n (\"observation_1\".\"person_id\" = \"visit_occurrence_2\".\"person_id\") AND\n (\"observation_1\".\"observation_date\" >= \"visit_occurrence_2\".\"max\")\n))\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"An empty Bind can be created.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Bind(args = [])\n#-> Bind(args = [])","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Bind requires that all variables have a unique name.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Bind(:PERSON_ID => 1, :PERSON_ID => 2)\n#=>\nERROR: FunSQL.DuplicateLabelError: `PERSON_ID` is used more than once in:\nBind(1 |> As(:PERSON_ID), 2 |> As(:PERSON_ID))\n=#","category":"page"},{"location":"test/nodes/#Functions-and-Operators","page":"SQL Nodes","title":"Functions and Operators","text":"","category":"section"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"A function or an operator invocation is created with the Fun constructor.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Fun.\">\"\n#-> Fun.:(\">\")\n\ne = Fun.\">\"(Get.year_of_birth, 2000)\n#-> Fun.:(\">\")(…)\n\ndisplay(e)\n#-> Fun.\">\"(Get.year_of_birth, 2000)","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Alternatively, Fun nodes are created by broadcasting. Common Julia operators are replaced with their SQL equivalents.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"#? VERSION >= v\"1.7\"\ne = Get.location.state .== \"IL\" .|| Get.location.zip .!= \"60615\"\n#-> Fun.or(…)\n\n#? VERSION >= v\"1.7\"\ndisplay(e)\n#-> Fun.or(Fun.\"=\"(Get.location.state, \"IL\"), Fun.\"<>\"(Get.location.zip, \"60615\"))\n\n#? VERSION >= v\"1.7\"\ne = .!(e .&& Get.year_of_birth .> 1950 .&& Get.year_of_birth .< 1990)\n#-> Fun.not(…)\n\n#? VERSION >= v\"1.7\"\ndisplay(e)\n#=>\nFun.not(Fun.and(Fun.or(Fun.\"=\"(Get.location.state, \"IL\"),\n Fun.\"<>\"(Get.location.zip, \"60615\")),\n Fun.and(Fun.\">\"(Get.year_of_birth, 1950),\n Fun.\"<\"(Get.year_of_birth, 1990))))\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"A vector of arguments could be passed directly.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Fun.\">\"(args = SQLNode[Get.year_of_birth, 2000])\n#-> Fun.:(\">\")(…)","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Fun nodes can be generated in @funsql notation.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"e = @funsql fun(>, year_of_birth, 2000)\n\ndisplay(e)\n#-> Fun.\">\"(Get.year_of_birth, 2000)","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"In order to generate Fun nodes using regular function and operator calls, we need to declare these functions and operators in advance.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"e = @funsql concat(location.city, \", \", location.state)\n\ndisplay(e)\n#-> Fun.concat(Get.location.city, \", \", Get.location.state)\n\ne = @funsql 1950 < year_of_birth < 1990\n\ndisplay(e)\n#-> Fun.and(Fun.\"<\"(1950, Get.year_of_birth), Fun.\"<\"(Get.year_of_birth, 1990))\n\ne = @funsql location.state != \"IL\" || location.zip != 60615\n\ndisplay(e)\n#-> Fun.or(Fun.\"<>\"(Get.location.state, \"IL\"), Fun.\"<>\"(Get.location.zip, 60615))\n\ne = @funsql location.state == \"IL\" && location.zip == 60615\n\ndisplay(e)\n#-> Fun.and(Fun.\"=\"(Get.location.state, \"IL\"), Fun.\"=\"(Get.location.zip, 60615))","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"In @funsql notation, use backticks to represent a name that is not a valid identifier.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"e = @funsql fun(`SUBSTRING(? FROM ? FOR ?)`, city, 1, 1)\n\ndisplay(e)\n#-> Fun.\"SUBSTRING(? FROM ? FOR ?)\"(Get.city, 1, 1)\n\nq = @funsql `from`(person).`filter`(year_of_birth <= 1964)\n\ndisplay(q)\n#=>\nlet q1 = From(:person),\n q2 = q1 |> Where(Fun.\"<=\"(Get.year_of_birth, 1964))\n q2\nend\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"In @funsql notation, an if statement is converted to a CASE expression.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"e = @funsql year_of_birth <= 1964 ? \"Boomers\" : \"Millenials\"\n\ndisplay(e)\n#-> Fun.case(Fun.\"<=\"(Get.year_of_birth, 1964), \"Boomers\", \"Millenials\")\n\ne = @funsql year_of_birth <= 1964 ? \"Boomers\" :\n year_of_birth <= 1980 ? \"Generation X\" : \"Millenials\"\n\ndisplay(e)\n#=>\nFun.case(Fun.\"<=\"(Get.year_of_birth, 1964),\n \"Boomers\",\n Fun.\"<=\"(Get.year_of_birth, 1980),\n \"Generation X\",\n \"Millenials\")\n=#\n\ne = @funsql if year_of_birth <= 1964; \"Boomers\"; end\n\ndisplay(e)\n#-> Fun.case(Fun.\"<=\"(Get.year_of_birth, 1964), \"Boomers\")\n\ne = @funsql begin\n if year_of_birth <= 1964\n \"Boomers\"\n elseif year_of_birth <= 1980\n \"Generation X\"\n end\nend\n\ndisplay(e)\n#=>\nFun.case(Fun.\"<=\"(Get.year_of_birth, 1964),\n \"Boomers\",\n Fun.\"<=\"(Get.year_of_birth, 1980),\n \"Generation X\")\n=#\n\ne = @funsql begin\n if year_of_birth <= 1964\n \"Boomers\"\n elseif year_of_birth <= 1980\n \"Generation X\"\n elseif year_of_birth <= 1996\n \"Millenials\"\n else\n \"Generation Z\"\n end\nend\n\ndisplay(e)\n#=>\nFun.case(Fun.\"<=\"(Get.year_of_birth, 1964),\n \"Boomers\",\n Fun.\"<=\"(Get.year_of_birth, 1980),\n \"Generation X\",\n Fun.\"<=\"(Get.year_of_birth, 1996),\n \"Millenials\",\n \"Generation Z\")\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"In a SELECT clause, the function name becomes the column alias.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(location) |>\n Select(Fun.coalesce(Get.city, \"N/A\"))\n\nprint(render(q))\n#=>\nSELECT coalesce(\"location_1\".\"city\", 'N/A') AS \"coalesce\"\nFROM \"location\" AS \"location_1\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"When the name contains only symbol characters, or when it starts or ends with a space character, it is interpreted as an operator.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(location) |>\n Select(Fun.\" || \"(Get.city, \", \", Get.state))\n\nprint(render(q))\n#=>\nSELECT (\"location_1\".\"city\" || ', ' || \"location_1\".\"state\") AS \"_\"\nFROM \"location\" AS \"location_1\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"The function name containing ? serves as a template.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(location) |>\n Select(Fun.\"SUBSTRING(? FROM ? FOR ?)\"(Get.city, 1, 1))\n\nprint(render(q))\n#=>\nSELECT SUBSTRING(\"location_1\".\"city\" FROM 1 FOR 1) AS \"_\"\nFROM \"location\" AS \"location_1\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"The number of arguments to a function must coincide with the number of placeholders in the template.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Fun.\"SUBSTRING(? FROM ? FOR ?)\"(Get.city)\n#=>\nERROR: FunSQL.InvalidArityError: `SUBSTRING(? FROM ? FOR ?)` expects 3 arguments, got 1 in:\nFun.\"SUBSTRING(? FROM ? FOR ?)\"(Get.city)\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Some common functions also validate the number of arguments.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Fun.case()\n#=>\nERROR: FunSQL.InvalidArityError: `case` expects at least 2 arguments, got 0 in:\nFun.case()\n=#\n\nFun.is_null(Get.city, Get.state)\n#=>\nERROR: FunSQL.InvalidArityError: `is_null` expects 1 argument, got 2 in:\nFun.is_null(Get.city, Get.state)\n=#\n\nFun.count(Get.city, Get.state)\n#=>\nERROR: FunSQL.InvalidArityError: `count` expects from 0 to 1 argument, got 2 in:\nFun.count(Get.city, Get.state)\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"A function invocation may include a nested query.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"p = From(person) |>\n Where(Get.year_of_birth .> 1950)\n\nq = Select(Fun.exists(p))\n\nprint(render(q))\n#=>\nSELECT (EXISTS (\n SELECT NULL AS \"_\"\n FROM \"person\" AS \"person_1\"\n WHERE (\"person_1\".\"year_of_birth\" > 1950)\n)) AS \"exists\"\n=#\n\np = From(concept) |>\n Where(Fun.and(Get.vocabulary_id .== \"Gender\",\n Get.concept_code .== \"F\")) |>\n Select(Get.concept_id)\n\nq = From(person) |>\n Where(Fun.in(Get.gender_concept_id, p))\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"location_id\"\nFROM \"person\" AS \"person_1\"\nWHERE (\"person_1\".\"gender_concept_id\" IN (\n SELECT \"concept_1\".\"concept_id\"\n FROM \"concept\" AS \"concept_1\"\n WHERE\n (\"concept_1\".\"vocabulary_id\" = 'Gender') AND\n (\"concept_1\".\"concept_code\" = 'F')\n))\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"FunSQL can simplify logical expressions.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Where(Fun.and())\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"location_id\"\nFROM \"person\" AS \"person_1\"\n=#\n\nq = From(person) |>\n Select(Get.person_id) |>\n Where(Fun.and())\n\nprint(render(q))\n#=>\nSELECT \"person_1\".\"person_id\"\nFROM \"person\" AS \"person_1\"\n=#\n\nq = From(person) |>\n Where(Fun.and(Get.year_of_birth .> 1950))\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"location_id\"\nFROM \"person\" AS \"person_1\"\nWHERE (\"person_1\".\"year_of_birth\" > 1950)\n=#\n\nq = From(person) |>\n Where(foldl(Fun.and, [Get.year_of_birth .> 1950, Get.year_of_birth .< 1960, Get.year_of_birth .!= 1955], init = Fun.and()))\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"location_id\"\nFROM \"person\" AS \"person_1\"\nWHERE\n (\"person_1\".\"year_of_birth\" > 1950) AND\n (\"person_1\".\"year_of_birth\" < 1960) AND\n (\"person_1\".\"year_of_birth\" <> 1955)\n=#\n\nq = From(person) |>\n Where(Fun.or())\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"location_id\"\nFROM \"person\" AS \"person_1\"\nWHERE FALSE\n=#\n\nq = From(person) |>\n Where(Fun.or(Get.year_of_birth .> 1950))\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"location_id\"\nFROM \"person\" AS \"person_1\"\nWHERE (\"person_1\".\"year_of_birth\" > 1950)\n=#\n\nq = From(person) |>\n Where(Fun.or(Fun.or(Fun.or(), Get.year_of_birth .> 1950), Get.year_of_birth .< 1960))\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"location_id\"\nFROM \"person\" AS \"person_1\"\nWHERE\n (\"person_1\".\"year_of_birth\" > 1950) OR\n (\"person_1\".\"year_of_birth\" < 1960)\n=#\n\n#? VERSION >= v\"1.7\"\nq = From(person) |>\n Where(Get.year_of_birth .> 1950 .|| Get.year_of_birth .< 1960 .|| Get.year_of_birth .!= 1955)\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"location_id\"\nFROM \"person\" AS \"person_1\"\nWHERE\n (\"person_1\".\"year_of_birth\" > 1950) OR\n (\"person_1\".\"year_of_birth\" < 1960) OR\n (\"person_1\".\"year_of_birth\" <> 1955)\n=#\n\nq = From(person) |>\n Where(Fun.not(false))\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"location_id\"\nFROM \"person\" AS \"person_1\"\n=#","category":"page"},{"location":"test/nodes/#Append","page":"SQL Nodes","title":"Append","text":"","category":"section"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"The Append constructor creates a subquery that concatenates the output of multiple queries.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(measurement) |>\n Define(:date => Get.measurement_date) |>\n Append(From(observation) |>\n Define(:date => Get.observation_date))\n#-> (…) |> Append(…)\n\ndisplay(q)\n#=>\nlet measurement = SQLTable(:measurement, …),\n observation = SQLTable(:observation, …),\n q1 = From(measurement),\n q2 = q1 |> Define(Get.measurement_date |> As(:date)),\n q3 = From(observation),\n q4 = q3 |> Define(Get.observation_date |> As(:date)),\n q5 = q2 |> Append(q4)\n q5\nend\n=#\n\nprint(render(q |> Select(Get.person_id, Get.date)))\n#=>\nSELECT\n \"union_1\".\"person_id\",\n \"union_1\".\"date\"\nFROM (\n SELECT\n \"measurement_1\".\"person_id\",\n \"measurement_1\".\"measurement_date\" AS \"date\"\n FROM \"measurement\" AS \"measurement_1\"\n UNION ALL\n SELECT\n \"observation_1\".\"person_id\",\n \"observation_1\".\"observation_date\" AS \"date\"\n FROM \"observation\" AS \"observation_1\"\n) AS \"union_1\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Append can also be specified without the over node.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = Append(From(measurement) |>\n Define(:date => Get.measurement_date),\n From(observation) |>\n Define(:date => Get.observation_date)) |>\n Select(Get.person_id, Get.date)\n\nprint(render(q))\n#=>\nSELECT\n \"union_1\".\"person_id\",\n \"union_1\".\"date\"\nFROM (\n SELECT\n \"measurement_1\".\"person_id\",\n \"measurement_1\".\"measurement_date\" AS \"date\"\n FROM \"measurement\" AS \"measurement_1\"\n UNION ALL\n SELECT\n \"observation_1\".\"person_id\",\n \"observation_1\".\"observation_date\" AS \"date\"\n FROM \"observation\" AS \"observation_1\"\n) AS \"union_1\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"An Append node can be created using @funsql notation.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = @funsql begin\n from(measurement).define(date => measurement_date)\n append(from(observation).define(date => observation_date))\nend\n\ndisplay(q)\n#=>\nlet q1 = From(:measurement),\n q2 = q1 |> Define(Get.measurement_date |> As(:date)),\n q3 = From(:observation),\n q4 = q3 |> Define(Get.observation_date |> As(:date)),\n q5 = q2 |> Append(q4)\n q5\nend\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Append will automatically assign unique aliases to the exported columns.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(measurement) |>\n Define(:concept_id => Get.measurement_concept_id) |>\n Group(Get.person_id) |>\n Define(:count => 1, :count_2 => 2) |>\n Append(From(observation) |>\n Define(:concept_id => Get.observation_concept_id) |>\n Group(Get.person_id) |>\n Define(:count => 10, :count_2 => 20)) |>\n Select(Get.person_id, :agg_count => Agg.count(), Get.count_2, Get.count)\n\nprint(render(q))\n#=>\nSELECT\n \"union_1\".\"person_id\",\n \"union_1\".\"count\" AS \"agg_count\",\n \"union_1\".\"count_2\",\n \"union_1\".\"count_3\" AS \"count\"\nFROM (\n SELECT\n \"measurement_1\".\"person_id\",\n count(*) AS \"count\",\n 2 AS \"count_2\",\n 1 AS \"count_3\"\n FROM \"measurement\" AS \"measurement_1\"\n GROUP BY \"measurement_1\".\"person_id\"\n UNION ALL\n SELECT\n \"observation_1\".\"person_id\",\n count(*) AS \"count\",\n 20 AS \"count_2\",\n 10 AS \"count_3\"\n FROM \"observation\" AS \"observation_1\"\n GROUP BY \"observation_1\".\"person_id\"\n) AS \"union_1\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Append will not put duplicate expressions into the SELECT clauses of the nested subqueries.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Join(From(measurement) |>\n Define(:date => Get.measurement_date) |>\n Append(From(observation) |>\n Define(:date => Get.observation_date)) |>\n As(:assessment),\n on = Get.person_id .== Get.assessment.person_id) |>\n Where(Get.assessment.date .> Fun.current_timestamp()) |>\n Select(Get.person_id, Get.assessment.date)\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"person_id\",\n \"assessment_1\".\"date\"\nFROM \"person\" AS \"person_1\"\nJOIN (\n SELECT\n \"measurement_1\".\"measurement_date\" AS \"date\",\n \"measurement_1\".\"person_id\"\n FROM \"measurement\" AS \"measurement_1\"\n UNION ALL\n SELECT\n \"observation_1\".\"observation_date\" AS \"date\",\n \"observation_1\".\"person_id\"\n FROM \"observation\" AS \"observation_1\"\n) AS \"assessment_1\" ON (\"person_1\".\"person_id\" = \"assessment_1\".\"person_id\")\nWHERE (\"assessment_1\".\"date\" > CURRENT_TIMESTAMP)\n=#\n\nq = From(measurement) |>\n Define(:date => Get.measurement_date) |>\n Append(From(observation) |>\n Define(:date => Get.observation_date)) |>\n Group(Get.date) |>\n Define(Agg.count())\n\nprint(render(q))\n#=>\nSELECT\n \"union_1\".\"date\",\n count(*) AS \"count\"\nFROM (\n SELECT \"measurement_1\".\"measurement_date\" AS \"date\"\n FROM \"measurement\" AS \"measurement_1\"\n UNION ALL\n SELECT \"observation_1\".\"observation_date\" AS \"date\"\n FROM \"observation\" AS \"observation_1\"\n) AS \"union_1\"\nGROUP BY \"union_1\".\"date\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Append aligns the columns of its subqueries.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(measurement) |>\n Select(Get.person_id, :date => Get.measurement_date) |>\n Append(From(observation) |>\n Select(:date => Get.observation_date, Get.person_id))\n\nprint(render(q))\n#=>\nSELECT\n \"measurement_1\".\"person_id\",\n \"measurement_1\".\"measurement_date\" AS \"date\"\nFROM \"measurement\" AS \"measurement_1\"\nUNION ALL\nSELECT\n \"observation_2\".\"person_id\",\n \"observation_2\".\"date\"\nFROM (\n SELECT\n \"observation_1\".\"observation_date\" AS \"date\",\n \"observation_1\".\"person_id\"\n FROM \"observation\" AS \"observation_1\"\n) AS \"observation_2\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Arguments of Append may contain ORDER BY or LIMIT clauses, which must be wrapped in a nested subquery.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(measurement) |>\n Order(Get.measurement_date) |>\n Select(Get.person_id, :date => Get.measurement_date) |>\n Append(From(observation) |>\n Define(:date => Get.observation_date) |>\n Limit(1))\n\nprint(render(q))\n#=>\nSELECT\n \"measurement_2\".\"person_id\",\n \"measurement_2\".\"date\"\nFROM (\n SELECT\n \"measurement_1\".\"person_id\",\n \"measurement_1\".\"measurement_date\" AS \"date\"\n FROM \"measurement\" AS \"measurement_1\"\n ORDER BY \"measurement_1\".\"measurement_date\"\n) AS \"measurement_2\"\nUNION ALL\nSELECT\n \"observation_2\".\"person_id\",\n \"observation_2\".\"date\"\nFROM (\n SELECT\n \"observation_1\".\"person_id\",\n \"observation_1\".\"observation_date\" AS \"date\"\n FROM \"observation\" AS \"observation_1\"\n FETCH FIRST 1 ROW ONLY\n) AS \"observation_2\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"An Append without any queries can be created explicitly.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = Append(args = [])\n#-> Append(args = [])\n\nprint(render(q))\n#=>\nSELECT NULL AS \"_\"\nWHERE FALSE\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Without an explicit Select, the output of Append includes the common columns of the nested queries.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = Append(measurement, observation)\n\nprint(render(q))\n#=>\nSELECT \"measurement_1\".\"person_id\"\nFROM \"measurement\" AS \"measurement_1\"\nUNION ALL\nSELECT \"observation_1\".\"person_id\"\nFROM \"observation\" AS \"observation_1\"\n=#","category":"page"},{"location":"test/nodes/#Iterate","page":"SQL Nodes","title":"Iterate","text":"","category":"section"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"The Iterate constructor creates an iteration query. In the argument of Iterate, the From(^) node refers to the output of the previous iteration. We could use Iterate and From(^) to create a factorial table.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = Define(:n => 1, :f => 1) |>\n Iterate(From(^) |>\n Define(:n => Get.n .+ 1, :f => Get.f .* (Get.n .+ 1)) |>\n Where(Get.n .<= 10))\n#-> (…) |> Iterate(…)\n\ndisplay(q)\n#=>\nlet q1 = Define(1 |> As(:n), 1 |> As(:f)),\n q2 = From(^),\n q3 = q2 |>\n Define(Fun.\"+\"(Get.n, 1) |> As(:n),\n Fun.\"*\"(Get.f, Fun.\"+\"(Get.n, 1)) |> As(:f)),\n q4 = q3 |> Where(Fun.\"<=\"(Get.n, 10)),\n q5 = q1 |> Iterate(q4)\n q5\nend\n=#\n\nprint(render(q))\n#=>\nWITH RECURSIVE \"__1\" (\"n\", \"f\") AS (\n SELECT\n 1 AS \"n\",\n 1 AS \"f\"\n UNION ALL\n SELECT\n \"__3\".\"n\",\n \"__3\".\"f\"\n FROM (\n SELECT\n (\"__2\".\"n\" + 1) AS \"n\",\n (\"__2\".\"f\" * (\"__2\".\"n\" + 1)) AS \"f\"\n FROM \"__1\" AS \"__2\"\n ) AS \"__3\"\n WHERE (\"__3\".\"n\" <= 10)\n)\nSELECT\n \"__4\".\"n\",\n \"__4\".\"f\"\nFROM \"__1\" AS \"__4\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"An Iterate node can be created using @funsql notation.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = @funsql begin\n define(n => 1, f => 1)\n iterate(define(n => n + 1, f => f * (n + 1)).filter(n <= 10))\nend\n\ndisplay(q)\n#=>\nlet q1 = Define(1 |> As(:n), 1 |> As(:f)),\n q2 = Define(Fun.\"+\"(Get.n, 1) |> As(:n),\n Fun.\"*\"(Get.f, Fun.\"+\"(Get.n, 1)) |> As(:f)),\n q3 = q2 |> Where(Fun.\"<=\"(Get.n, 10)),\n q4 = q1 |> Iterate(q3)\n q4\nend\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"The From(^) node in front of the iterator query can be omitted.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = Define(:n => 1, :f => 1) |>\n Iterate(Define(:n => Get.n .+ 1, :f => Get.f .* (Get.n .+ 1)) |>\n Where(Get.n .<= 10))\n\nprint(render(q))\n#=>\nWITH RECURSIVE \"__1\" (\"n\", \"f\") AS (\n SELECT\n 1 AS \"n\",\n 1 AS \"f\"\n UNION ALL\n SELECT\n \"__3\".\"n\",\n \"__3\".\"f\"\n FROM (\n SELECT\n (\"__2\".\"n\" + 1) AS \"n\",\n (\"__2\".\"f\" * (\"__2\".\"n\" + 1)) AS \"f\"\n FROM \"__1\" AS \"__2\"\n ) AS \"__3\"\n WHERE (\"__3\".\"n\" <= 10)\n)\nSELECT\n \"__4\".\"n\",\n \"__4\".\"f\"\nFROM \"__1\" AS \"__4\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"An Iterate node may use a CTE.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = Define(:n => 1, :f => 1) |>\n Iterate(Define(:n => Get.n .+ 1, :f => Get.f .* (Get.n .+ 1)) |>\n CrossJoin(From(:threshold)) |>\n Where(Get.n .<= Get.threshold)) |>\n With(:threshold => Define(:threshold => 10))\n\nprint(render(q))\n#=>\nWITH RECURSIVE \"threshold_1\" (\"threshold\") AS (\n SELECT 10 AS \"threshold\"\n),\n\"__1\" (\"n\", \"f\") AS (\n SELECT\n 1 AS \"n\",\n 1 AS \"f\"\n UNION ALL\n SELECT\n \"__3\".\"n\",\n \"__3\".\"f\"\n FROM (\n SELECT\n (\"__2\".\"n\" + 1) AS \"n\",\n (\"__2\".\"f\" * (\"__2\".\"n\" + 1)) AS \"f\",\n \"threshold_2\".\"threshold\"\n FROM \"__1\" AS \"__2\"\n CROSS JOIN \"threshold_1\" AS \"threshold_2\"\n ) AS \"__3\"\n WHERE (\"__3\".\"n\" <= \"__3\".\"threshold\")\n)\nSELECT\n \"__4\".\"n\",\n \"__4\".\"f\"\nFROM \"__1\" AS \"__4\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"It is an error to use From(^) outside of Iterate.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(^)\n\nprint(render(q))\n#=>\nERROR: FunSQL.ReferenceError: self-reference outside of Iterate in:\nlet q1 = From(^)\n q1\nend\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"The set of columns produced by Iterate is the intersection of the columns produced by the base query and the iterator query.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = Define(:k => 0, :m => 0) |>\n Iterate(As(:previous) |>\n Where(Get.previous.m .< 10) |>\n Define(:m => Get.previous.m .+ 1, :n => 0))\n\nprint(render(q))\n#=>\nWITH RECURSIVE \"previous_1\" (\"m\") AS (\n SELECT 0 AS \"m\"\n UNION ALL\n SELECT (\"previous_2\".\"m\" + 1) AS \"m\"\n FROM \"previous_1\" AS \"previous_2\"\n WHERE (\"previous_2\".\"m\" < 10)\n)\nSELECT \"previous_3\".\"m\"\nFROM \"previous_1\" AS \"previous_3\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Iterate aligns the columns of its subqueries.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = Select(:n => 1, :f => 1) |>\n Iterate(Where(Get.n .< 10) |>\n Select(:f => (Get.n .+ 1) .* Get.f,\n :n => Get.n .+ 1))\n\nprint(render(q))\n#=>\nWITH RECURSIVE \"__1\" (\"n\", \"f\") AS (\n SELECT\n 1 AS \"n\",\n 1 AS \"f\"\n UNION ALL\n SELECT\n \"__3\".\"n\",\n \"__3\".\"f\"\n FROM (\n SELECT\n ((\"__2\".\"n\" + 1) * \"__2\".\"f\") AS \"f\",\n (\"__2\".\"n\" + 1) AS \"n\"\n FROM \"__1\" AS \"__2\"\n WHERE (\"__2\".\"n\" < 10)\n ) AS \"__3\"\n)\nSELECT\n \"__4\".\"n\",\n \"__4\".\"f\"\nFROM \"__1\" AS \"__4\"\n=#","category":"page"},{"location":"test/nodes/#As","page":"SQL Nodes","title":"As","text":"","category":"section"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"An alias to an expression can be added with the As constructor.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"e = 42 |> As(:integer)\n#-> (…) |> As(:integer)\n\ndisplay(e)\n#-> 42 |> As(:integer)\n\nprint(render(Select(e)))\n#=>\nSELECT 42 AS \"integer\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"As node can be created with @funsql.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"e = @funsql (42).as(integer)\n\ndisplay(e)\n#-> 42 |> As(:integer)","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"The => shorthand is supported by @funsql.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"e = @funsql integer => 42\n\ndisplay(e)\n#-> :integer => 42","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"As is also used to create an alias for a subquery.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n As(:p) |>\n Select(Get.p.person_id)\n\nprint(render(q))\n#=>\nSELECT \"person_1\".\"person_id\"\nFROM \"person\" AS \"person_1\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"As blocks the default output columns.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |> As(:p)\n\nprint(render(q))\n#=>\nSELECT NULL AS \"_\"\nFROM \"person\" AS \"person_1\"\n=#","category":"page"},{"location":"test/nodes/#From","page":"SQL Nodes","title":"From","text":"","category":"section"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"The From constructor creates a subquery that selects columns from the given table.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person)\n#-> From(…)\n\ndisplay(q)\n#-> From(SQLTable(:person, …))","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"By default, From selects all columns from the table.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"print(render(q))\n#=>\nSELECT\n \"person_1\".\"person_id\",\n \"person_1\".\"gender_concept_id\",\n \"person_1\".\"year_of_birth\",\n \"person_1\".\"month_of_birth\",\n \"person_1\".\"day_of_birth\",\n \"person_1\".\"birth_datetime\",\n \"person_1\".\"location_id\"\nFROM \"person\" AS \"person_1\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"From adds the schema qualifier when the table has the schema.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"const pg_database =\n SQLTable(qualifiers = [:pg_catalog], :pg_database, columns = [:oid, :datname])\n\nq = From(pg_database)\n\nprint(render(q))\n#=>\nSELECT\n \"pg_database_1\".\"oid\",\n \"pg_database_1\".\"datname\"\nFROM \"pg_catalog\".\"pg_database\" AS \"pg_database_1\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"In a suitable context, a SQLTable object is automatically converted to a From subquery.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"print(render(person))\n#=>\nSELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"location_id\"\nFROM \"person\" AS \"person_1\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"From and other subqueries generate a correct SELECT clause when the table has no columns.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"empty = SQLTable(:empty, columns = Symbol[])\n\nq = From(empty) |>\n Where(false) |>\n Select(args = [])\n\ndisplay(q)\n#=>\nlet empty = SQLTable(:empty, …),\n q1 = From(empty),\n q2 = q1 |> Where(false),\n q3 = q2 |> Select(args = [])\n q3\nend\n=#\n\nprint(render(q))\n#=>\nSELECT NULL AS \"_\"\nFROM \"empty\" AS \"empty_1\"\nWHERE FALSE\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"When From takes a Tables-compatible argument, it generates a VALUES query.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"using DataFrames\n\ndf = DataFrame(name = [\"SQL\", \"Julia\", \"FunSQL\"],\n year = [1974, 2012, 2021])\n\nq = From(df)\n#-> From(…)\n\ndisplay(q)\n#-> From((name = [\"SQL\", …], year = [1974, …]))\n\nprint(render(q))\n#=>\nSELECT\n \"values_1\".\"name\",\n \"values_1\".\"year\"\nFROM (\n VALUES\n ('SQL', 1974),\n ('Julia', 2012),\n ('FunSQL', 2021)\n) AS \"values_1\" (\"name\", \"year\")\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"SQLite does not support column aliases with AS clause.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"print(render(q, dialect = :sqlite))\n#=>\nSELECT\n \"values_1\".\"column1\" AS \"name\",\n \"values_1\".\"column2\" AS \"year\"\nFROM (\n VALUES\n ('SQL', 1974),\n ('Julia', 2012),\n ('FunSQL', 2021)\n) AS \"values_1\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Only columns that are used in the query will be serialized.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(df) |>\n Select(Get.name)\n\nprint(render(q))\n#=>\nSELECT \"values_1\".\"name\"\nFROM (\n VALUES\n ('SQL'),\n ('Julia'),\n ('FunSQL')\n) AS \"values_1\" (\"name\")\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"A column of NULLs will be added if no actual columns are used.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(df) |>\n Group() |>\n Select(Agg.count())\n\nprint(render(q))\n#=>\nSELECT count(*) AS \"count\"\nFROM (\n VALUES\n (NULL),\n (NULL),\n (NULL)\n) AS \"values_1\" (\"_\")\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Since VALUES clause requires at least one row of data, a different representation is used when the source table is empty.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(df[1:0, :])\n\nprint(render(q))\n#=>\nSELECT\n NULL AS \"name\",\n NULL AS \"year\"\nWHERE FALSE\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"The source table must have at least one column.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(df[1:0, 1:0])\n#=>\nERROR: DomainError with 0×0 DataFrame:\na table with at least one column is expected\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"From can accept a table-valued function. Since the output type of the function is not known to FunSQL, you must manually specify the names of the output columns.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(Fun.generate_series(0, 100, 10), columns = [:value])\n#-> From(…, columns = [:value])\n\ndisplay(q)\n#-> From(Fun.generate_series(0, 100, 10), columns = [:value])\n\nprint(render(q))\n#=>\nSELECT \"generate_series_1\".\"value\"\nFROM generate_series(0, 100, 10) AS \"generate_series_1\" (\"value\")\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"WITH ORDINALITY annotation adds an extra column that enumerates the output rows.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(Fun.\"? WITH ORDINALITY\"(Fun.generate_series(0, 100, 10)),\n columns = [:value, :index])\n\nprint(render(q))\n#=>\nSELECT\n \"__1\".\"value\",\n \"__1\".\"index\"\nFROM generate_series(0, 100, 10) WITH ORDINALITY AS \"__1\" (\"value\", \"index\")\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"A From node can be created with @funsql notation.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = @funsql from(person)\n\ndisplay(q)\n#-> From(:person)\n\nq = @funsql from(nothing)\n\ndisplay(q)\n#-> From(nothing)\n\nq = @funsql from(^)\n\ndisplay(q)\n#-> From(^)\n\nq = @funsql from($person)\n\ndisplay(q)\n#-> From(SQLTable(:person, …))\n\nq = @funsql from($df)\n\ndisplay(q)\n#-> From((name = [\"SQL\", …], year = [1974, …]))\n\nfunsql_generate_series = FunSQL.FunClosure(:generate_series)\n\nq = @funsql from(generate_series(0, 100, 10), columns = [value])\n\ndisplay(q)\n#-> From(Fun.generate_series(0, 100, 10), columns = [:value])","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"When From with a tabular function is attached to the right branch of a Join node, the function may use data from the left branch of Join, even without being wrapped in a Bind node.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(Fun.regexp_split_to_table(\"(10,20)-(30,40)-(50,60)\", \"-\"),\n columns = [:point]) |>\n CrossJoin(From(Fun.regexp_matches(Get.point, \"(\\\\d+),(\\\\d+)\"),\n columns = [:captures])) |>\n Select(:x => Fun.\"CAST(?[1] AS INTEGER)\"(Get.captures),\n :y => Fun.\"CAST(?[2] AS INTEGER)\"(Get.captures))\n\nprint(render(q))\n#=>\nSELECT\n CAST(\"regexp_matches_1\".\"captures\"[1] AS INTEGER) AS \"x\",\n CAST(\"regexp_matches_1\".\"captures\"[2] AS INTEGER) AS \"y\"\nFROM regexp_split_to_table('(10,20)-(30,40)-(50,60)', '-') AS \"regexp_split_to_table_1\" (\"point\")\nCROSS JOIN regexp_matches(\"regexp_split_to_table_1\".\"point\", '(\\d+),(\\d+)') AS \"regexp_matches_1\" (\"captures\")\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"All the columns of a tabular function must have distinct names.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"From(Fun.\"? WITH ORDINALITY\"(Fun.generate_series(0, 100, 10)),\n columns = [:index, :index])\n#=>\nERROR: FunSQL.DuplicateLabelError: `index` is used more than once in:\nlet q1 = From(Fun.\"? WITH ORDINALITY\"(Fun.generate_series(0, 100, 10)),\n columns = [:index, :index])\n q1\nend\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"From(nothing) will generate a unit dataset with one row.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(nothing)\n\ndisplay(q)\n#-> From(nothing)\n\nprint(render(q))\n#=>\nSELECT NULL AS \"_\"\n=#","category":"page"},{"location":"test/nodes/#With,-Over,-and-WithExternal","page":"SQL Nodes","title":"With, Over, and WithExternal","text":"","category":"section"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"We can create a temporary dataset using With and refer to it with From.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(:male) |>\n With(From(person) |>\n Where(Get.gender_concept_id .== 8507) |>\n As(:male))\n\ndisplay(q)\n#=>\nlet person = SQLTable(:person, …),\n q1 = From(:male),\n q2 = From(person),\n q3 = q2 |> Where(Fun.\"=\"(Get.gender_concept_id, 8507)),\n q4 = q1 |> With(q3 |> As(:male))\n q4\nend\n=#\n\nprint(render(q))\n#=>\nWITH \"male_1\" (\"person_id\", …, \"location_id\") AS (\n SELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"location_id\"\n FROM \"person\" AS \"person_1\"\n WHERE (\"person_1\".\"gender_concept_id\" = 8507)\n)\nSELECT\n \"male_2\".\"person_id\",\n ⋮\n \"male_2\".\"location_id\"\nFROM \"male_1\" AS \"male_2\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"With definitions can be annotated as materialized or not materialized:","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(:male) |>\n With(From(person) |>\n Where(Get.gender_concept_id .== 8507) |>\n As(:male),\n materialized = true)\n#-> (…) |> With(…, materialized = true)\n\nprint(render(q))\n#=>\nWITH \"male_1\" ( … ) AS MATERIALIZED (\n ⋮\n)\nSELECT\n ⋮\nFROM \"male_1\" AS \"male_2\"\n=#\n\nq = From(:male) |>\n With(From(person) |>\n Where(Get.gender_concept_id .== 8507) |>\n As(:male),\n materialized = false)\n\nprint(render(q))\n#=>\nWITH \"male_1\" ( … ) AS NOT MATERIALIZED (\n ⋮\n)\nSELECT\n ⋮\nFROM \"male_1\" AS \"male_2\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"With can take more than one definition.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = Select(:male_count => From(:male) |> Group() |> Select(Agg.count()),\n :female_count => From(:female) |> Group() |> Select(Agg.count())) |>\n With(:male => From(person) |> Where(Get.gender_concept_id .== 8507),\n :female => From(person) |> Where(Get.gender_concept_id .== 8532))\n\nprint(render(q))\n#=>\nWITH \"male_1\" (\"_\") AS (\n SELECT NULL AS \"_\"\n FROM \"person\" AS \"person_1\"\n WHERE (\"person_1\".\"gender_concept_id\" = 8507)\n),\n\"female_1\" (\"_\") AS (\n SELECT NULL AS \"_\"\n FROM \"person\" AS \"person_2\"\n WHERE (\"person_2\".\"gender_concept_id\" = 8532)\n)\nSELECT\n (\n SELECT count(*) AS \"count\"\n FROM \"male_1\" AS \"male_2\"\n ) AS \"male_count\",\n (\n SELECT count(*) AS \"count\"\n FROM \"female_1\" AS \"female_2\"\n ) AS \"female_count\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"With can shadow the previous With definition.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(:cohort) |>\n With(:cohort => From(:cohort) |> Where(Get.gender_concept_id .== 8507)) |>\n With(:cohort => From(:cohort) |> Where(Get.year_of_birth .>= 1950)) |>\n With(:cohort => From(person)) |>\n Select(Get.person_id)\n\nprint(render(q))\n#=>\nWITH \"cohort_1\" (\"person_id\", \"gender_concept_id\", \"year_of_birth\") AS (\n SELECT\n \"person_1\".\"person_id\",\n \"person_1\".\"gender_concept_id\",\n \"person_1\".\"year_of_birth\"\n FROM \"person\" AS \"person_1\"\n),\n\"cohort_3\" (\"person_id\", \"gender_concept_id\") AS (\n SELECT\n \"cohort_2\".\"person_id\",\n \"cohort_2\".\"gender_concept_id\"\n FROM \"cohort_1\" AS \"cohort_2\"\n WHERE (\"cohort_2\".\"year_of_birth\" >= 1950)\n),\n\"cohort_5\" (\"person_id\") AS (\n SELECT \"cohort_4\".\"person_id\"\n FROM \"cohort_3\" AS \"cohort_4\"\n WHERE (\"cohort_4\".\"gender_concept_id\" = 8507)\n)\nSELECT \"cohort_6\".\"person_id\"\nFROM \"cohort_5\" AS \"cohort_6\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"A With node can be created using @funsql.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = @funsql begin\n from(male)\n with(male => from(person).filter(gender_concept_id == 8507),\n materialized = false)\nend\n\ndisplay(q)\n#=>\nlet q1 = From(:male),\n q2 = From(:person),\n q3 = q2 |> Where(Fun.\"=\"(Get.gender_concept_id, 8507)),\n q4 = q1 |> With(q3 |> As(:male), materialized = false)\n q4\nend\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"A dataset defined by With must have an explicit label assigned to it.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(:person) |>\n With(From(person))\n\nprint(render(q))\n#=>\nERROR: FunSQL.ReferenceError: table reference `person` requires As in:\nlet person = SQLTable(:person, …),\n q1 = From(:person),\n q2 = From(person),\n q3 = q1 |> With(q2)\n q3\nend\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Datasets defined by With must have a unique label.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"From(:p) |>\nWith(:p => From(person),\n :p => From(person))\n#=>\nERROR: FunSQL.DuplicateLabelError: `p` is used more than once in:\nlet person = SQLTable(:person, …),\n q1 = From(person),\n q2 = From(person),\n q3 = With(q1 |> As(:p), q2 |> As(:p))\n q3\nend\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"It is an error for From to refer to an undefined dataset.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(:p)\n\nprint(render(q))\n#=>\nERROR: FunSQL.ReferenceError: cannot find `p` in:\nlet q1 = From(:p)\n q1\nend\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"A variant of With called Over exchanges the positions of the definition and the query that uses it.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Where(Get.gender_concept_id .== 8507) |>\n As(:male) |>\n Over(From(:male))\n#-> (…) |> Over(…)\n\ndisplay(q)\n#=>\nlet person = SQLTable(:person, …),\n q1 = From(person),\n q2 = q1 |> Where(Fun.\"=\"(Get.gender_concept_id, 8507)),\n q3 = From(:male),\n q4 = q2 |> As(:male) |> Over(q3)\n q4\nend\n=#\n\nprint(render(q))\n#=>\nWITH \"male_1\" (\"person_id\", …, \"location_id\") AS (\n SELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"location_id\"\n FROM \"person\" AS \"person_1\"\n WHERE (\"person_1\".\"gender_concept_id\" = 8507)\n)\nSELECT\n \"male_2\".\"person_id\",\n ⋮\n \"male_2\".\"location_id\"\nFROM \"male_1\" AS \"male_2\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"An Over node can be created using @funsql.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = @funsql begin\n male => from(person).filter(gender_concept_id == 8507)\n over(from(male), materialized = true)\nend\n\ndisplay(q)\n#=>\nlet q1 = From(:person),\n q2 = q1 |> Where(Fun.\"=\"(Get.gender_concept_id, 8507)),\n q3 = From(:male),\n q4 = q2 |> As(:male) |> Over(q3, materialized = true)\n q4\nend\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"A variant of With called WithExternal can be used to prepare a definition for a CREATE TABLE AS or SELECT INTO statement.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"with_external_handler((tbl, def)) =\n println(\"CREATE TEMP TABLE \",\n render(ID(tbl.qualifiers, tbl.name)),\n \" (\", join([render(ID(c.name)) for (n, c) in tbl.columns], \", \"), \") AS\\n\",\n render(def), \";\\n\")\n\nq = From(:male) |>\n WithExternal(From(person) |>\n Where(Get.gender_concept_id .== 8507) |>\n As(:male),\n qualifiers = [:tmp],\n handler = with_external_handler)\n#-> (…) |> WithExternal(…, qualifiers = [:tmp], handler = with_external_handler)\n\nprint(render(q))\n#=>\nCREATE TEMP TABLE \"tmp\".\"male\" (\"person_id\", …, \"location_id\") AS\nSELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"location_id\"\nFROM \"person\" AS \"person_1\"\nWHERE (\"person_1\".\"gender_concept_id\" = 8507);\n\nSELECT\n \"male_1\".\"person_id\",\n ⋮\n \"male_1\".\"location_id\"\nFROM \"tmp\".\"male\" AS \"male_1\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Datasets defined by WithExternal must have a unique label.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"From(:p) |>\nWithExternal(:p => From(person),\n :p => From(person))\n#=>\nERROR: FunSQL.DuplicateLabelError: `p` is used more than once in:\nlet person = SQLTable(:person, …),\n q1 = From(person),\n q2 = From(person),\n q3 = WithExternal(q1 |> As(:p), q2 |> As(:p))\n q3\nend\n=#","category":"page"},{"location":"test/nodes/#Group","page":"SQL Nodes","title":"Group","text":"","category":"section"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"The Group constructor creates a subquery that summarizes the rows partitioned by the given keys.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Group(Get.year_of_birth)\n#-> (…) |> Group(…)\n\ndisplay(q)\n#=>\nlet person = SQLTable(:person, …),\n q1 = From(person),\n q2 = q1 |> Group(Get.year_of_birth)\n q2\nend\n=#\n\nprint(render(q))\n#=>\nSELECT DISTINCT \"person_1\".\"year_of_birth\"\nFROM \"person\" AS \"person_1\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"A Group node can be created using @funsql notation.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = @funsql from(person).group(year_of_birth)\n\ndisplay(q)\n#=>\nlet q1 = From(:person),\n q2 = q1 |> Group(Get.year_of_birth)\n q2\nend\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Partitions created by Group are summarized using aggregate expressions.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Agg.count\n#-> Agg.count\n\nq = From(person) |>\n Group(Get.year_of_birth) |>\n Select(Get.year_of_birth, Agg.count())\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"year_of_birth\",\n count(*) AS \"count\"\nFROM \"person\" AS \"person_1\"\nGROUP BY \"person_1\".\"year_of_birth\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Aggregate functions can be created with @funsql.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"e = @funsql agg(min, year_of_birth)\n\ndisplay(e)\n#-> Agg.min(Get.year_of_birth)\n\ne = @funsql min(year_of_birth)\n\ndisplay(e)\n#-> Agg.min(Get.year_of_birth)\n\ne = @funsql count(filter = year_of_birth > 1950)\n\ndisplay(e)\n#-> Agg.count(filter = Fun.\">\"(Get.year_of_birth, 1950))\n\ne = @funsql visit_group.count()\n\ndisplay(e)\n#-> Get.visit_group |> Agg.count()\n\ne = @funsql `count`()\n\ndisplay(e)\n#-> Agg.count()\n\ne = @funsql visit_group.`count`()\n\ndisplay(e)\n#-> Get.visit_group |> Agg.count()\n\ne = @funsql `visit_group`.`count`()\n\ndisplay(e)\n#-> Get.visit_group |> Agg.count()","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Group will create a single instance of an aggregate function even if it is used more than once.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Join(:visit_group => From(visit_occurrence) |>\n Group(Get.person_id),\n on = Get.person_id .== Get.visit_group.person_id) |>\n Where(Agg.count(over = Get.visit_group) .>= 2) |>\n Select(Get.person_id, Agg.count(over = Get.visit_group))\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"person_id\",\n \"visit_group_1\".\"count\"\nFROM \"person\" AS \"person_1\"\nJOIN (\n SELECT\n count(*) AS \"count\",\n \"visit_occurrence_1\".\"person_id\"\n FROM \"visit_occurrence\" AS \"visit_occurrence_1\"\n GROUP BY \"visit_occurrence_1\".\"person_id\"\n) AS \"visit_group_1\" ON (\"person_1\".\"person_id\" = \"visit_group_1\".\"person_id\")\nWHERE (\"visit_group_1\".\"count\" >= 2)\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Group creates a nested subquery when this is necessary to avoid duplicating the group key expression.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Group(:age => 2000 .- Get.year_of_birth)\n\nprint(render(q))\n#=>\nSELECT DISTINCT (2000 - \"person_1\".\"year_of_birth\") AS \"age\"\nFROM \"person\" AS \"person_1\"\n=#\n\nq = From(person) |>\n Group(:age => 2000 .- Get.year_of_birth) |>\n Select(Agg.count())\n\nprint(render(q))\n#=>\nSELECT count(*) AS \"count\"\nFROM \"person\" AS \"person_1\"\nGROUP BY (2000 - \"person_1\".\"year_of_birth\")\n=#\n\nq = From(person) |>\n Group(:age => 2000 .- Get.year_of_birth) |>\n Define(Agg.count())\n\nprint(render(q))\n#=>\nSELECT\n \"person_2\".\"age\",\n count(*) AS \"count\"\nFROM (\n SELECT (2000 - \"person_1\".\"year_of_birth\") AS \"age\"\n FROM \"person\" AS \"person_1\"\n) AS \"person_2\"\nGROUP BY \"person_2\".\"age\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Group could be used consequently.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(measurement) |>\n Group(Get.measurement_concept_id) |>\n Group(Agg.count()) |>\n Select(Get.count, :size => Agg.count())\n\nprint(render(q))\n#=>\nSELECT\n \"measurement_2\".\"count\",\n count(*) AS \"size\"\nFROM (\n SELECT count(*) AS \"count\"\n FROM \"measurement\" AS \"measurement_1\"\n GROUP BY \"measurement_1\".\"measurement_concept_id\"\n) AS \"measurement_2\"\nGROUP BY \"measurement_2\".\"count\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Group accepts an empty list of keys.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Group() |>\n Select(Agg.count(), Agg.min(Get.year_of_birth), Agg.max(Get.year_of_birth))\n\nprint(render(q))\n#=>\nSELECT\n count(*) AS \"count\",\n min(\"person_1\".\"year_of_birth\") AS \"min\",\n max(\"person_1\".\"year_of_birth\") AS \"max\"\nFROM \"person\" AS \"person_1\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Group with no keys and no aggregates creates a trivial subquery.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Group()\n\nprint(render(q))\n#-> SELECT NULL AS \"_\"","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"A SELECT DISTINCT query must include all the keys even when they are not used downstream.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Group(Get.year_of_birth) |>\n Group() |>\n Select(Agg.count())\n\nprint(render(q))\n#=>\nSELECT count(*) AS \"count\"\nFROM (\n SELECT DISTINCT \"person_1\".\"year_of_birth\"\n FROM \"person\" AS \"person_1\"\n) AS \"person_2\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Group allows specifying the grouping sets, either with grouping mode indicators :cube or :rollup, or by explicit enumeration.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Group(Get.year_of_birth, sets = :cube)\n Define(Agg.count())\n\ndisplay(q)\n#=>\nlet person = SQLTable(:person, …),\n q1 = From(person),\n q2 = q1 |> Group(Get.year_of_birth, sets = :CUBE)\n q2\nend\n=#\n\nprint(render(q))\n#=>\nSELECT \"person_1\".\"year_of_birth\"\nFROM \"person\" AS \"person_1\"\nGROUP BY CUBE(\"person_1\".\"year_of_birth\")\n=#\n\nq = From(person) |>\n Group(Get.year_of_birth, sets = [[1], Int[]])\n Define(Agg.count())\n\ndisplay(q)\n#=>\nlet person = SQLTable(:person, …),\n q1 = From(person),\n q2 = q1 |> Group(Get.year_of_birth, sets = [[1], []])\n q2\nend\n=#\n\nprint(render(q))\n#=>\nSELECT \"person_1\".\"year_of_birth\"\nFROM \"person\" AS \"person_1\"\nGROUP BY GROUPING SETS((\"person_1\".\"year_of_birth\"), ())\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Group allows specifying grouping sets using names of the grouping keys.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Group(Get.year_of_birth, Get.gender_concept_id,\n sets = ([:year_of_birth], [\"gender_concept_id\"]))\n Define(Agg.count())\n\ndisplay(q)\n#=>\nlet person = SQLTable(:person, …),\n q1 = From(person),\n q2 = q1 |>\n Group(Get.year_of_birth, Get.gender_concept_id, sets = [[1], [2]])\n q2\nend\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Group will report when a grouping set refers to an unknown key.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"From(person) |>\nGroup(Get.year_of_birth, sets = [[:gender_concept_id], []])\n#=>\nERROR: FunSQL.InvalidGroupingSetsError: `gender_concept_id` is not a valid key\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Group complains about out-of-bound or incomplete grouping sets.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"From(person) |>\nGroup(Get.year_of_birth, sets = [[1, 2], [1], []])\n#=>\nERROR: FunSQL.InvalidGroupingSetsError: `2` is out of bounds in:\nlet q1 = Group(Get.year_of_birth, sets = [[1, 2], [1], []])\n q1\nend\n=#\n\nFrom(person) |>\nGroup(Get.year_of_birth, Get.gender_concept_id,\n sets = [[1], []])\n#=>\nERROR: FunSQL.InvalidGroupingSetsError: missing keys `[:year_of_birth]` in:\nlet q1 = Group(Get.year_of_birth, Get.gender_concept_id, sets = [[1], []])\n q1\nend\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Group allows specifying the name of a group field.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Group(Get.year_of_birth, name = :person) |>\n Define(Get.person |> Agg.count())\n\ndisplay(q)\n#=>\nlet person = SQLTable(:person, …),\n q1 = From(person),\n q2 = q1 |> Group(Get.year_of_birth, name = :person),\n q3 = q2 |> Define(Get.person |> Agg.count())\n q3\nend\n=#\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"year_of_birth\",\n count(*) AS \"count\"\nFROM \"person\" AS \"person_1\"\nGROUP BY \"person_1\".\"year_of_birth\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Group requires all keys to have unique aliases.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Group(Get.person_id, Get.person_id)\n#=>\nERROR: FunSQL.DuplicateLabelError: `person_id` is used more than once in:\nGroup(Get.person_id, Get.person_id)\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"The name of group field must also be unique.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Group(:group => Get.year_of_birth, name = :group)\n#=>\nERROR: FunSQL.DuplicateLabelError: `group` is used more than once in:\nGroup(Get.year_of_birth |> As(:group), name = :group)\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Group ensures that each aggregate expression gets a unique alias.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Join(:visit_group => From(visit_occurrence) |>\n Group(Get.person_id),\n on = Get.person_id .== Get.visit_group.person_id) |>\n Select(Get.person_id,\n :max_visit_start_date =>\n Get.visit_group |> Agg.max(Get.visit_start_date),\n :max_visit_end_date =>\n Get.visit_group |> Agg.max(Get.visit_end_date))\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"person_id\",\n \"visit_group_1\".\"max\" AS \"max_visit_start_date\",\n \"visit_group_1\".\"max_2\" AS \"max_visit_end_date\"\nFROM \"person\" AS \"person_1\"\nJOIN (\n SELECT\n max(\"visit_occurrence_1\".\"visit_start_date\") AS \"max\",\n max(\"visit_occurrence_1\".\"visit_end_date\") AS \"max_2\",\n \"visit_occurrence_1\".\"person_id\"\n FROM \"visit_occurrence\" AS \"visit_occurrence_1\"\n GROUP BY \"visit_occurrence_1\".\"person_id\"\n) AS \"visit_group_1\" ON (\"person_1\".\"person_id\" = \"visit_group_1\".\"person_id\")\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Aggregate expressions can be applied to a filtered portion of a partition.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"e = Agg.count(filter = Get.year_of_birth .> 1950)\n#-> Agg.count(filter = (…))\n\ndisplay(e)\n#-> Agg.count(filter = Fun.\">\"(Get.year_of_birth, 1950))\n\nq = From(person) |> Group() |> Select(e)\n\nprint(render(q))\n#=>\nSELECT (count(*) FILTER (WHERE (\"person_1\".\"year_of_birth\" > 1950))) AS \"count\"\nFROM \"person\" AS \"person_1\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"It is an error for an aggregate expression to be used without Group.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |> Select(Agg.count())\n\nprint(render(q))\n#=>\nERROR: FunSQL.ReferenceError: aggregate expression requires Group or Partition in:\nlet person = SQLTable(:person, …),\n q1 = From(person),\n q2 = q1 |> Select(Agg.count())\n q2\nend\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Group in a Join expression shadows any previous applications of Group.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"qₚ = From(person)\nqᵥ = From(visit_occurrence) |> Group(:visit_person_id => Get.person_id)\nqₘ = From(measurement) |> Group(:measurement_person_id => Get.person_id)\n\nq = qₚ |>\n Join(qᵥ, on = Get.person_id .== Get.visit_person_id, left = true) |>\n Join(qₘ, on = Get.person_id .== Get.measurement_person_id, left = true) |>\n Select(Get.person_id, :count => Fun.coalesce(Agg.count(), 0))\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"person_id\",\n coalesce(\"measurement_2\".\"count\", 0) AS \"count\"\nFROM \"person\" AS \"person_1\"\nLEFT JOIN (\n SELECT DISTINCT \"visit_occurrence_1\".\"person_id\" AS \"visit_person_id\"\n FROM \"visit_occurrence\" AS \"visit_occurrence_1\"\n) AS \"visit_occurrence_2\" ON (\"person_1\".\"person_id\" = \"visit_occurrence_2\".\"visit_person_id\")\nLEFT JOIN (\n SELECT\n count(*) AS \"count\",\n \"measurement_1\".\"person_id\" AS \"measurement_person_id\"\n FROM \"measurement\" AS \"measurement_1\"\n GROUP BY \"measurement_1\".\"person_id\"\n) AS \"measurement_2\" ON (\"person_1\".\"person_id\" = \"measurement_2\".\"measurement_person_id\")\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"It is still possible to use an aggregate in the context of a Join when the corresponding Group could be determined unambiguously.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"qₚ = From(person)\nqᵥ = From(visit_occurrence) |> Group(:visit_person_id => Get.person_id)\n\nq = qₚ |>\n Join(qᵥ, on = Get.person_id .== Get.visit_person_id, left = true) |>\n Select(Get.person_id, :count => Fun.coalesce(Agg.count(), 0))\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"person_id\",\n coalesce(\"visit_occurrence_2\".\"count\", 0) AS \"count\"\nFROM \"person\" AS \"person_1\"\nLEFT JOIN (\n SELECT\n count(*) AS \"count\",\n \"visit_occurrence_1\".\"person_id\" AS \"visit_person_id\"\n FROM \"visit_occurrence\" AS \"visit_occurrence_1\"\n GROUP BY \"visit_occurrence_1\".\"person_id\"\n) AS \"visit_occurrence_2\" ON (\"person_1\".\"person_id\" = \"visit_occurrence_2\".\"visit_person_id\")\n=#","category":"page"},{"location":"test/nodes/#Partition","page":"SQL Nodes","title":"Partition","text":"","category":"section"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"The Partition constructor creates a subquery that partitions the rows by the given keys.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Partition(Get.year_of_birth, order_by = [Get.month_of_birth, Get.day_of_birth])\n#-> (…) |> Partition(…, order_by = […])\n\ndisplay(q)\n#=>\nlet person = SQLTable(:person, …),\n q1 = From(person),\n q2 = q1 |>\n Partition(Get.year_of_birth,\n order_by = [Get.month_of_birth, Get.day_of_birth])\n q2\nend\n=#\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"location_id\"\nFROM \"person\" AS \"person_1\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"A Partition node can be created with @funsql notation.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = @funsql begin\n from(person)\n partition(year_of_birth, order_by = [month_of_birth, day_of_birth])\nend\n\ndisplay(q)\n#=>\nlet q1 = From(:person),\n q2 = q1 |>\n Partition(Get.year_of_birth,\n order_by = [Get.month_of_birth, Get.day_of_birth])\n q2\nend\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Calculations across the rows of the partitions are performed by window functions.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Partition(Get.gender_concept_id) |>\n Select(Get.person_id, Agg.row_number())\n\ndisplay(q)\n#=>\nlet person = SQLTable(:person, …),\n q1 = From(person),\n q2 = q1 |> Partition(Get.gender_concept_id),\n q3 = q2 |> Select(Get.person_id, Agg.row_number())\n q3\nend\n=#\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"person_id\",\n (row_number() OVER (PARTITION BY \"person_1\".\"gender_concept_id\")) AS \"row_number\"\nFROM \"person\" AS \"person_1\"\n=#\n\nq = From(visit_occurrence) |>\n Partition(Get.person_id) |>\n Where(Get.visit_start_date .- Agg.min(Get.visit_start_date, filter = Get.visit_start_date .< Get.visit_end_date) .> 30) |>\n Select(Get.person_id, Get.visit_start_date)\n\nprint(render(q))\n#=>\nSELECT\n \"visit_occurrence_2\".\"person_id\",\n \"visit_occurrence_2\".\"visit_start_date\"\nFROM (\n SELECT\n \"visit_occurrence_1\".\"person_id\",\n \"visit_occurrence_1\".\"visit_start_date\",\n (min(\"visit_occurrence_1\".\"visit_start_date\") FILTER (WHERE (\"visit_occurrence_1\".\"visit_start_date\" < \"visit_occurrence_1\".\"visit_end_date\")) OVER (PARTITION BY \"visit_occurrence_1\".\"person_id\")) AS \"min\"\n FROM \"visit_occurrence\" AS \"visit_occurrence_1\"\n) AS \"visit_occurrence_2\"\nWHERE ((\"visit_occurrence_2\".\"visit_start_date\" - \"visit_occurrence_2\".\"min\") > 30)\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"A partition may specify the window frame.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Group(Get.year_of_birth) |>\n Partition(order_by = [Get.year_of_birth],\n frame = (mode = :range, start = -1, finish = 1)) |>\n Select(Get.year_of_birth, Agg.avg(Agg.count()))\n\ndisplay(q)\n#=>\nlet person = SQLTable(:person, …),\n q1 = From(person),\n q2 = q1 |> Group(Get.year_of_birth),\n q3 = q2 |>\n Partition(order_by = [Get.year_of_birth],\n frame = (mode = :RANGE, start = -1, finish = 1)),\n q4 = q3 |> Select(Get.year_of_birth, Agg.avg(Agg.count()))\n q4\nend\n=#\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"year_of_birth\",\n (avg(count(*)) OVER (ORDER BY \"person_1\".\"year_of_birth\" RANGE BETWEEN 1 PRECEDING AND 1 FOLLOWING)) AS \"avg\"\nFROM \"person\" AS \"person_1\"\nGROUP BY \"person_1\".\"year_of_birth\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"A window frame can be specified in @funsql notation.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = @funsql partition(order_by = [year_of_birth], frame = groups)\n\ndisplay(q)\n#-> Partition(order_by = [Get.year_of_birth], frame = :GROUPS)\n\nq = @funsql partition(order_by = [year_of_birth], frame = (mode = range, start = -1, finish = 1))\n\ndisplay(q)\n#=>\nPartition(order_by = [Get.year_of_birth],\n frame = (mode = :RANGE, start = -1, finish = 1))\n=#\n\nq = @funsql partition(; order_by = [year_of_birth], frame = (mode = range, start = -Inf, finish = Inf, exclude = current_row))\n\ndisplay(q)\n#=>\nPartition(\n order_by = [Get.year_of_birth],\n frame =\n (mode = :RANGE, start = -Inf, finish = Inf, exclude = :CURRENT_ROW))\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Partition may assign an explicit name to the partition.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Group(Get.gender_concept_id) |>\n Partition(name = :all) |>\n Define(:pct => 100 .* Agg.count() ./ (Get.all |> Agg.sum(Agg.count())))\n\ndisplay(q)\n#=>\nlet person = SQLTable(:person, …),\n q1 = From(person),\n q2 = q1 |> Group(Get.gender_concept_id),\n q3 = q2 |> Partition(name = :all),\n q4 = q3 |>\n Define(Fun.\"/\"(Fun.\"*\"(100, Agg.count()),\n Get.all |> Agg.sum(Agg.count())) |>\n As(:pct))\n q4\nend\n=#\n\nprint(render(q))\n#=>\nSELECT\n \"person_2\".\"gender_concept_id\",\n ((100 * \"person_2\".\"count\") / (sum(\"person_2\".\"count\") OVER ())) AS \"pct\"\nFROM (\n SELECT\n \"person_1\".\"gender_concept_id\",\n count(*) AS \"count\"\n FROM \"person\" AS \"person_1\"\n GROUP BY \"person_1\".\"gender_concept_id\"\n) AS \"person_2\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"This name may shadow an existing column.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(location) |>\n Partition(Get.location_id, name = :location_id)\n\nprint(render(q))\n#=>\nSELECT\n \"location_1\".\"city\",\n \"location_1\".\"state\"\nFROM \"location\" AS \"location_1\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"It is common to use several Partition nodes in a row like in the following example which calculates non-overlapping visits.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(visit_occurrence) |>\n Partition(Get.person_id,\n order_by = [Get.visit_start_date],\n frame = (mode = :rows, start = -Inf, finish = -1)) |>\n Define(:boundary => Agg.max(Get.visit_end_date)) |>\n Define(:gap => Get.visit_start_date .- Get.boundary) |>\n Define(:new => Fun.case(Get.gap .<= 0, 0, 1)) |>\n Partition(Get.person_id,\n order_by = [Get.visit_start_date, .- Get.new],\n frame = :rows) |>\n Define(:group => Agg.sum(Get.new)) |>\n Group(Get.person_id, Get.group) |>\n Define(:start_date => Agg.min(Get.visit_start_date),\n :end_date => Agg.max(Get.visit_end_date)) |>\n Select(Get.person_id, Get.start_date, Get.end_date)\n\nprint(render(q))\n#=>\nSELECT\n \"visit_occurrence_3\".\"person_id\",\n min(\"visit_occurrence_3\".\"visit_start_date\") AS \"start_date\",\n max(\"visit_occurrence_3\".\"visit_end_date\") AS \"end_date\"\nFROM (\n SELECT\n \"visit_occurrence_2\".\"person_id\",\n (sum(\"visit_occurrence_2\".\"new\") OVER (PARTITION BY \"visit_occurrence_2\".\"person_id\" ORDER BY \"visit_occurrence_2\".\"visit_start_date\", (- \"visit_occurrence_2\".\"new\") ROWS UNBOUNDED PRECEDING)) AS \"group\",\n \"visit_occurrence_2\".\"visit_start_date\",\n \"visit_occurrence_2\".\"visit_end_date\"\n FROM (\n SELECT\n \"visit_occurrence_1\".\"person_id\",\n \"visit_occurrence_1\".\"visit_start_date\",\n \"visit_occurrence_1\".\"visit_end_date\",\n (CASE WHEN ((\"visit_occurrence_1\".\"visit_start_date\" - (max(\"visit_occurrence_1\".\"visit_end_date\") OVER (PARTITION BY \"visit_occurrence_1\".\"person_id\" ORDER BY \"visit_occurrence_1\".\"visit_start_date\" ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING))) <= 0) THEN 0 ELSE 1 END) AS \"new\"\n FROM \"visit_occurrence\" AS \"visit_occurrence_1\"\n ) AS \"visit_occurrence_2\"\n) AS \"visit_occurrence_3\"\nGROUP BY\n \"visit_occurrence_3\".\"person_id\",\n \"visit_occurrence_3\".\"group\"\n=#","category":"page"},{"location":"test/nodes/#Join","page":"SQL Nodes","title":"Join","text":"","category":"section"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"The Join constructor creates a subquery that correlates two nested subqueries.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Join(:location => From(location),\n on = Get.location_id .== Get.location.location_id,\n left = true)\n#-> (…) |> Join(…)\n\ndisplay(q)\n#=>\nlet person = SQLTable(:person, …),\n location = SQLTable(:location, …),\n q1 = From(person),\n q2 = From(location),\n q3 = q1 |>\n Join(q2 |> As(:location),\n Fun.\"=\"(Get.location_id, Get.location.location_id),\n left = true)\n q3\nend\n=#\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"location_id\"\nFROM \"person\" AS \"person_1\"\nLEFT JOIN \"location\" AS \"location_1\" ON (\"person_1\".\"location_id\" = \"location_1\".\"location_id\")\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"LEFT JOIN is commonly used and has its own constructor.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n LeftJoin(:location => From(location),\n on = Get.location_id .== Get.location.location_id)\n\ndisplay(q)\n#=>\nlet person = SQLTable(:person, …),\n location = SQLTable(:location, …),\n q1 = From(person),\n q2 = From(location),\n q3 = q1 |>\n Join(q2 |> As(:location),\n Fun.\"=\"(Get.location_id, Get.location.location_id),\n left = true)\n q3\nend\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Various Join nodes can be created with @funsql notation.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = @funsql begin\n from(person)\n join(location => from(location),\n on = location_id == location.location_id,\n left = true)\nend\n\ndisplay(q)\n#=>\nlet q1 = From(:person),\n q2 = From(:location),\n q3 = q1 |>\n Join(q2 |> As(:location),\n Fun.\"=\"(Get.location_id, Get.location.location_id),\n left = true)\n q3\nend\n=#\n\nq = @funsql begin\n from(person)\n left_join(location => from(location),\n location_id == location.location_id)\nend\n\ndisplay(q)\n#=>\nlet q1 = From(:person),\n q2 = From(:location),\n q3 = q1 |>\n Join(q2 |> As(:location),\n Fun.\"=\"(Get.location_id, Get.location.location_id),\n left = true)\n q3\nend\n=#\n\nq = @funsql begin\n from(person)\n cross_join(other => from(person))\nend\n\ndisplay(q)\n#=>\nlet q1 = From(:person),\n q2 = From(:person),\n q3 = q1 |> Join(q2 |> As(:other), true)\n q3\nend\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Nested subqueries that are combined with Join may fail to collapse.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Where(Get.year_of_birth .> 1970) |>\n Join(:location => From(location) |>\n Where(Get.state .== \"IL\"),\n on = (Get.location_id .== Get.location.location_id)) |>\n Select(Get.person_id, Get.location.city)\n\nprint(render(q))\n#=>\nSELECT\n \"person_2\".\"person_id\",\n \"location_2\".\"city\"\nFROM (\n SELECT\n \"person_1\".\"person_id\",\n \"person_1\".\"location_id\"\n FROM \"person\" AS \"person_1\"\n WHERE (\"person_1\".\"year_of_birth\" > 1970)\n) AS \"person_2\"\nJOIN (\n SELECT\n \"location_1\".\"city\",\n \"location_1\".\"location_id\"\n FROM \"location\" AS \"location_1\"\n WHERE (\"location_1\".\"state\" = 'IL')\n) AS \"location_2\" ON (\"person_2\".\"location_id\" = \"location_2\".\"location_id\")\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Join can be applied to correlated subqueries.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"ql(person_id) =\n From(visit_occurrence) |>\n Where(Get.person_id .== Var.PERSON_ID) |>\n Partition(order_by = [Get.visit_start_date]) |>\n Where(Agg.row_number() .== 1) |>\n Bind(:PERSON_ID => person_id)\n\nprint(render(ql(1)))\n#=>\nSELECT\n \"visit_occurrence_2\".\"visit_occurrence_id\",\n \"visit_occurrence_2\".\"person_id\",\n \"visit_occurrence_2\".\"visit_start_date\",\n \"visit_occurrence_2\".\"visit_end_date\"\nFROM (\n SELECT\n \"visit_occurrence_1\".\"visit_occurrence_id\",\n \"visit_occurrence_1\".\"person_id\",\n \"visit_occurrence_1\".\"visit_start_date\",\n \"visit_occurrence_1\".\"visit_end_date\",\n (row_number() OVER (ORDER BY \"visit_occurrence_1\".\"visit_start_date\")) AS \"row_number\"\n FROM \"visit_occurrence\" AS \"visit_occurrence_1\"\n WHERE (\"visit_occurrence_1\".\"person_id\" = 1)\n) AS \"visit_occurrence_2\"\nWHERE (\"visit_occurrence_2\".\"row_number\" = 1)\n=#\n\nq = From(person) |>\n Join(:visit => ql(Get.person_id), on = true) |>\n Select(Get.person_id,\n Get.visit.visit_occurrence_id,\n Get.visit.visit_start_date)\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"person_id\",\n \"visit_1\".\"visit_occurrence_id\",\n \"visit_1\".\"visit_start_date\"\nFROM \"person\" AS \"person_1\"\nCROSS JOIN LATERAL (\n SELECT\n \"visit_occurrence_2\".\"visit_occurrence_id\",\n \"visit_occurrence_2\".\"visit_start_date\"\n FROM (\n SELECT\n \"visit_occurrence_1\".\"visit_occurrence_id\",\n \"visit_occurrence_1\".\"visit_start_date\",\n (row_number() OVER (ORDER BY \"visit_occurrence_1\".\"visit_start_date\")) AS \"row_number\"\n FROM \"visit_occurrence\" AS \"visit_occurrence_1\"\n WHERE (\"visit_occurrence_1\".\"person_id\" = \"person_1\".\"person_id\")\n ) AS \"visit_occurrence_2\"\n WHERE (\"visit_occurrence_2\".\"row_number\" = 1)\n) AS \"visit_1\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"The LATERAL keyword is omitted when the join branch is reduced to a function call.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(concept) |>\nJoin(\n From(Fun.string_to_table(Get.concept_name, \" \"), columns = [:word]),\n on = true) |>\nGroup(Get.word)\n\nprint(render(q))\n#=>\nSELECT DISTINCT \"string_to_table_1\".\"word\"\nFROM \"concept\" AS \"concept_1\"\nCROSS JOIN string_to_table(\"concept_1\".\"concept_name\", ' ') AS \"string_to_table_1\" (\"word\")\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Some database backends require LATERAL even in this case.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"print(render(q, dialect = :spark))\n#=>\nSELECT DISTINCT `string_to_table_1`.`word`\nFROM `concept` AS `concept_1`\nCROSS JOIN LATERAL string_to_table(`concept_1`.`concept_name`, ' ') AS `string_to_table_1` (`word`)\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"An optional Join is omitted when the output contains no data from its right branch.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n LeftJoin(:location => From(location),\n on = Get.location_id .== Get.location.location_id,\n optional = true)\n\ndisplay(q)\n#=>\nlet person = SQLTable(:person, …),\n location = SQLTable(:location, …),\n q1 = From(person),\n q2 = From(location),\n q3 = q1 |>\n Join(q2 |> As(:location),\n Fun.\"=\"(Get.location_id, Get.location.location_id),\n left = true,\n optional = true)\n q3\nend\n=#\n\nprint(render(q |> Select(Get.year_of_birth)))\n#=>\nSELECT \"person_1\".\"year_of_birth\"\nFROM \"person\" AS \"person_1\"\n=#\n\nprint(render(q |> Select(Get.year_of_birth, Get.location.state)))\n#=>\nSELECT\n \"person_1\".\"year_of_birth\",\n \"location_1\".\"state\"\nFROM \"person\" AS \"person_1\"\nLEFT JOIN \"location\" AS \"location_1\" ON (\"person_1\".\"location_id\" = \"location_1\".\"location_id\")\n=#","category":"page"},{"location":"test/nodes/#Order","page":"SQL Nodes","title":"Order","text":"","category":"section"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"The Order constructor creates a subquery for sorting the data.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Order(Get.year_of_birth)\n#-> (…) |> Order(…)\n\ndisplay(q)\n#=>\nlet person = SQLTable(:person, …),\n q1 = From(person),\n q2 = q1 |> Order(Get.year_of_birth)\n q2\nend\n=#\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"location_id\"\nFROM \"person\" AS \"person_1\"\nORDER BY \"person_1\".\"year_of_birth\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"An Order node can be created with @funsql notation.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = @funsql begin\n from(person)\n order(year_of_birth)\nend\n\ndisplay(q)\n#=>\nlet q1 = From(:person),\n q2 = q1 |> Order(Get.year_of_birth)\n q2\nend\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Order is often used together with Limit.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Order(Get.year_of_birth) |>\n Limit(10) |>\n Order(Get.person_id)\n\nprint(render(q))\n#=>\nSELECT\n \"person_2\".\"person_id\",\n ⋮\n \"person_2\".\"location_id\"\nFROM (\n SELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"location_id\"\n FROM \"person\" AS \"person_1\"\n ORDER BY \"person_1\".\"year_of_birth\"\n FETCH FIRST 10 ROWS ONLY\n) AS \"person_2\"\nORDER BY \"person_2\".\"person_id\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"An Order without columns to sort by is a no-op.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Order(by = [])\n#-> (…) |> Order(by = [])\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"location_id\"\nFROM \"person\" AS \"person_1\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"It is possible to specify ascending or descending order of the sort column.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Order(Get.year_of_birth |> Desc(nulls = :first),\n Get.person_id |> Asc())\n\ndisplay(q)\n#=>\nlet person = SQLTable(:person, …),\n q1 = From(person),\n q2 = q1 |>\n Order(Get.year_of_birth |> Desc(nulls = :NULLS_FIRST),\n Get.person_id |> Asc())\n q2\nend\n=#\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"location_id\"\nFROM \"person\" AS \"person_1\"\nORDER BY\n \"person_1\".\"year_of_birth\" DESC NULLS FIRST,\n \"person_1\".\"person_id\" ASC\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"A generic Sort constructor could also be used for this purpose.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Order(Get.year_of_birth |> Sort(:desc, nulls = :first),\n Get.person_id |> Sort(:asc))\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"location_id\"\nFROM \"person\" AS \"person_1\"\nORDER BY\n \"person_1\".\"year_of_birth\" DESC NULLS FIRST,\n \"person_1\".\"person_id\" ASC\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Sort decorations can be created with @funsql.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = @funsql begin\n from(person)\n order(year_of_birth.desc(nulls = first), person_id.asc())\nend\n\ndisplay(q)\n#=>\nlet q1 = From(:person),\n q2 = q1 |>\n Order(Get.year_of_birth |> Desc(nulls = :NULLS_FIRST),\n Get.person_id |> Asc())\n q2\nend\n=#\n\nq = @funsql begin\n from(person)\n order(year_of_birth.sort(desc, nulls = first), person_id.sort(asc))\nend\n\ndisplay(q)\n#=>\nlet q1 = From(:person),\n q2 = q1 |>\n Order(Get.year_of_birth |> Desc(nulls = :NULLS_FIRST),\n Get.person_id |> Asc())\n q2\nend\n=#","category":"page"},{"location":"test/nodes/#Limit","page":"SQL Nodes","title":"Limit","text":"","category":"section"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"The Limit constructor creates a subquery that takes a fixed-size slice of the dataset.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Order(Get.person_id) |>\n Limit(10)\n#-> (…) |> Limit(10)\n\ndisplay(q)\n#=>\nlet person = SQLTable(:person, …),\n q1 = From(person),\n q2 = q1 |> Order(Get.person_id),\n q3 = q2 |> Limit(10)\n q3\nend\n=#\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"location_id\"\nFROM \"person\" AS \"person_1\"\nORDER BY \"person_1\".\"person_id\"\nFETCH FIRST 10 ROWS ONLY\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Both the offset and the limit can be specified.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Order(Get.person_id) |>\n Limit(100, 10)\n\ndisplay(q)\n#=>\nlet person = SQLTable(:person, …),\n q1 = From(person),\n q2 = q1 |> Order(Get.person_id),\n q3 = q2 |> Limit(100, 10)\n q3\nend\n=#\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"location_id\"\nFROM \"person\" AS \"person_1\"\nORDER BY \"person_1\".\"person_id\"\nOFFSET 100 ROWS\nFETCH NEXT 10 ROWS ONLY\n=#\n\nq = From(person) |>\n Order(Get.person_id) |>\n Limit(101:110)\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"location_id\"\nFROM \"person\" AS \"person_1\"\nORDER BY \"person_1\".\"person_id\"\nOFFSET 100 ROWS\nFETCH NEXT 10 ROWS ONLY\n=#\n\nq = From(person) |>\n Limit(offset = 100) |>\n Limit(limit = 10)\n\nprint(render(q))\n#=>\nSELECT\n \"person_2\".\"person_id\",\n ⋮\n \"person_2\".\"location_id\"\nFROM (\n SELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"location_id\"\n FROM \"person\" AS \"person_1\"\n OFFSET 100 ROWS\n) AS \"person_2\"\nFETCH FIRST 10 ROWS ONLY\n=#\n\nq = From(person) |>\n Limit()\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"location_id\"\nFROM \"person\" AS \"person_1\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"A Limit node can be created with @funsql notation.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = @funsql from(person).order(person_id).limit(10)\n\ndisplay(q)\n#=>\nlet q1 = From(:person),\n q2 = q1 |> Order(Get.person_id),\n q3 = q2 |> Limit(10)\n q3\nend\n=#\n\nq = @funsql from(person).order(person_id).limit(100, 10)\n\ndisplay(q)\n#=>\nlet q1 = From(:person),\n q2 = q1 |> Order(Get.person_id),\n q3 = q2 |> Limit(100, 10)\n q3\nend\n=#\n\nq = @funsql from(person).order(person_id).limit(101:110)\n\ndisplay(q)\n#=>\nlet q1 = From(:person),\n q2 = q1 |> Order(Get.person_id),\n q3 = q2 |> Limit(100, 10)\n q3\nend\n=#","category":"page"},{"location":"test/nodes/#Select","page":"SQL Nodes","title":"Select","text":"","category":"section"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"The Select constructor creates a subquery that fixes the output columns.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Select(Get.person_id)\n#-> (…) |> Select(…)\n\ndisplay(q)\n#=>\nlet person = SQLTable(:person, …),\n q1 = From(person),\n q2 = q1 |> Select(Get.person_id)\n q2\nend\n=#\n\nprint(render(q))\n#=>\nSELECT \"person_1\".\"person_id\"\nFROM \"person\" AS \"person_1\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"A Select node can be created with @funsql notation.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = @funsql from(person).select(person_id)\n\ndisplay(q)\n#=>\nlet q1 = From(:person),\n q2 = q1 |> Select(Get.person_id)\n q2\nend\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Select does not have to be the last subquery in a chain, but it always creates a complete subquery.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Select(Get.year_of_birth) |>\n Where(Fun.\">\"(Get.year_of_birth, 2000))\n\nprint(render(q))\n#=>\nSELECT \"person_2\".\"year_of_birth\"\nFROM (\n SELECT \"person_1\".\"year_of_birth\"\n FROM \"person\" AS \"person_1\"\n) AS \"person_2\"\nWHERE (\"person_2\".\"year_of_birth\" > 2000)\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Select requires all columns in the list to have unique aliases.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Select(Get.person_id, Get.person_id)\n#=>\nERROR: FunSQL.DuplicateLabelError: `person_id` is used more than once in:\nSelect(Get.person_id, Get.person_id)\n=#","category":"page"},{"location":"test/nodes/#Where","page":"SQL Nodes","title":"Where","text":"","category":"section"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"The Where constructor creates a subquery that filters by the given condition.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Where(Fun.\">\"(Get.year_of_birth, 2000))\n#-> (…) |> Where(…)\n\ndisplay(q)\n#=>\nlet person = SQLTable(:person, …),\n q1 = From(person),\n q2 = q1 |> Where(Fun.\">\"(Get.year_of_birth, 2000))\n q2\nend\n=#\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"location_id\"\nFROM \"person\" AS \"person_1\"\nWHERE (\"person_1\".\"year_of_birth\" > 2000)\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"A Where node can be created with @funsql notation.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = @funsql from(person).filter(year_of_birth > 2000)\n\ndisplay(q)\n#=>\nlet q1 = From(:person),\n q2 = q1 |> Where(Fun.\">\"(Get.year_of_birth, 2000))\n q2\nend\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Several Where operations in a row are collapsed to a single WHERE clause.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Where(Fun.\">\"(Get.year_of_birth, 2000)) |>\n Where(Fun.\"<\"(Get.year_of_birth, 2020)) |>\n Where(Fun.\"<>\"(Get.year_of_birth, 2010))\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"location_id\"\nFROM \"person\" AS \"person_1\"\nWHERE\n (\"person_1\".\"year_of_birth\" > 2000) AND\n (\"person_1\".\"year_of_birth\" < 2020) AND\n (\"person_1\".\"year_of_birth\" <> 2010)\n=#\n\nq = From(person) |>\n Where(Get.year_of_birth .!= 2010) |>\n Where(Fun.and(Get.year_of_birth .> 2000, Get.year_of_birth .< 2020))\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"location_id\"\nFROM \"person\" AS \"person_1\"\nWHERE\n (\"person_1\".\"year_of_birth\" <> 2010) AND\n (\"person_1\".\"year_of_birth\" > 2000) AND\n (\"person_1\".\"year_of_birth\" < 2020)\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Where that follows Group subquery is transformed to a HAVING clause.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Group(Get.year_of_birth) |>\n Where(Agg.count() .> 10)\n\nprint(render(q))\n#=>\nSELECT \"person_1\".\"year_of_birth\"\nFROM \"person\" AS \"person_1\"\nGROUP BY \"person_1\".\"year_of_birth\"\nHAVING (count(*) > 10)\n=#\n\nq = From(person) |>\n Group(Get.gender_concept_id) |>\n Where(Agg.count(filter = Get.year_of_birth .== 2010) .> 10) |>\n Where(Agg.count(filter = Get.year_of_birth .== 2000) .< 100) |>\n Where(Fun.and(Agg.count(filter = Get.year_of_birth .== 1933) .!= 33,\n Agg.count(filter = Get.year_of_birth .== 1966) .!= 66))\n\nprint(render(q))\n#=>\nSELECT \"person_1\".\"gender_concept_id\"\nFROM \"person\" AS \"person_1\"\nGROUP BY \"person_1\".\"gender_concept_id\"\nHAVING\n ((count(*) FILTER (WHERE (\"person_1\".\"year_of_birth\" = 2010))) > 10) AND\n ((count(*) FILTER (WHERE (\"person_1\".\"year_of_birth\" = 2000))) < 100) AND\n ((count(*) FILTER (WHERE (\"person_1\".\"year_of_birth\" = 1933))) <> 33) AND\n ((count(*) FILTER (WHERE (\"person_1\".\"year_of_birth\" = 1966))) <> 66)\n=#\n\nq = From(person) |>\n Group(Get.gender_concept_id) |>\n Where(Fun.or(Agg.count(filter = Get.year_of_birth .== 2010) .> 10,\n Agg.count(filter = Get.year_of_birth .== 2000) .< 100))\n\nprint(render(q))\n#=>\nSELECT \"person_1\".\"gender_concept_id\"\nFROM \"person\" AS \"person_1\"\nGROUP BY \"person_1\".\"gender_concept_id\"\nHAVING\n ((count(*) FILTER (WHERE (\"person_1\".\"year_of_birth\" = 2010))) > 10) OR\n ((count(*) FILTER (WHERE (\"person_1\".\"year_of_birth\" = 2000))) < 100)\n=#","category":"page"},{"location":"test/nodes/#Highlighting","page":"SQL Nodes","title":"Highlighting","text":"","category":"section"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"To highlight a node on the output, wrap it with Highlight.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Highlight(:underline) |>\n Where(Fun.\">\"(Get.year_of_birth |> Highlight(:bold), 2000) |>\n Highlight(:white)) |>\n Select(Get.person_id) |>\n Highlight(:green)\n#-> (…) |> Highlight(:green)","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"When the query is displayed on a color terminal, the affected node is highlighted.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"display(q)\n#=>\nlet person = SQLTable(:person, …),\n q1 = From(person),\n q2 = q1 |> Where(Fun.\">\"(Get.year_of_birth, 2000)),\n q3 = q2 |> Select(Get.person_id)\n q3\nend\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"The Highlight node does not otherwise affect processing of the query.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"print(render(q))\n#=>\nSELECT \"person_1\".\"person_id\"\nFROM \"person\" AS \"person_1\"\nWHERE (\"person_1\".\"year_of_birth\" > 2000)\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"A Highlight node can be created with @funsql notation.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = @funsql from(person).highlight(red)\n\ndisplay(q)\n#=>\nlet q1 = From(:person)\n q1\nend\n=#","category":"page"},{"location":"test/nodes/#Debugging","page":"SQL Nodes","title":"Debugging","text":"","category":"section"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Enable debug logging to get some insight on how FunSQL translates a query object into SQL. Set the JULIA_DEBUG environment variable to the name of a translation stage and render() will print the result of this stage.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Consider the following query.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Where(Get.year_of_birth .<= 2000) |>\n Join(:location => From(location) |>\n Where(Get.state .== \"IL\"),\n on = (Get.location_id .== Get.location.location_id)) |>\n Join(:visit_group => From(visit_occurrence) |>\n Group(Get.person_id),\n on = (Get.person_id .== Get.visit_group.person_id),\n left = true) |>\n Select(Get.person_id,\n :max_visit_start_date =>\n Get.visit_group |> Agg.max(Get.visit_start_date))","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"At the first stage of the translation, render() resolves table references and determines node types.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"#? VERSION >= v\"1.7\" # https://github.com/JuliaLang/julia/issues/26798\nwithenv(\"JULIA_DEBUG\" => \"FunSQL.resolve\") do\n render(q)\nend;\n#=>\n┌ Debug: FunSQL.resolve\n│ let person = SQLTable(:person, …),\n│ location = SQLTable(:location, …),\n│ visit_occurrence = SQLTable(:visit_occurrence, …),\n│ q1 = FromTable(table = person),\n│ q2 = Resolved(RowType(:person_id => ScalarType(),\n│ :gender_concept_id => ScalarType(),\n│ :year_of_birth => ScalarType(),\n│ :month_of_birth => ScalarType(),\n│ :day_of_birth => ScalarType(),\n│ :birth_datetime => ScalarType(),\n│ :location_id => ScalarType()),\n│ over = q1) |>\n│ Where(Resolved(ScalarType(),\n│ over = Fun.\"<=\"(Resolved(ScalarType(),\n│ over = Get.year_of_birth),\n│ Resolved(ScalarType(), over = 2000)))),\n⋮\n│ WithContext(over = Resolved(RowType(:person_id => ScalarType(),\n│ :max_visit_start_date => ScalarType()),\n│ over = q9),\n│ catalog = SQLCatalog(dialect = SQLDialect(), cache = nothing))\n│ end\n└ @ FunSQL …\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Next, render() determines, for each tabular node, the data that it must produce.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"#? VERSION >= v\"1.7\"\nwithenv(\"JULIA_DEBUG\" => \"FunSQL.link\") do\n render(q)\nend;\n#=>\n┌ Debug: FunSQL.link\n│ let person = SQLTable(:person, …),\n│ location = SQLTable(:location, …),\n│ visit_occurrence = SQLTable(:visit_occurrence, …),\n│ q1 = FromTable(table = person),\n│ q2 = Get.person_id,\n│ q3 = Get.person_id,\n│ q4 = Get.location_id,\n│ q5 = Get.year_of_birth,\n│ q6 = Linked([q2, q3, q4, q5], 3, over = q1),\n⋮\n│ WithContext(over = q33,\n│ catalog = SQLCatalog(dialect = SQLDialect(), cache = nothing))\n│ end\n└ @ FunSQL …\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"On the next stage, the query object is converted to a SQL syntax tree.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"#? VERSION >= v\"1.7\"\nwithenv(\"JULIA_DEBUG\" => \"FunSQL.translate\") do\n render(q)\nend;\n#=>\n┌ Debug: FunSQL.translate\n│ WITH_CONTEXT(\n│ over = ID(:person) |>\n│ AS(:person_1) |>\n│ FROM() |>\n│ WHERE(FUN(\"<=\", ID(:person_1) |> ID(:year_of_birth), LIT(2000))) |>\n│ SELECT(ID(:person_1) |> ID(:person_id),\n│ ID(:person_1) |> ID(:location_id)) |>\n│ AS(:person_2) |>\n│ FROM() |>\n│ JOIN(ID(:location) |>\n│ AS(:location_1) |>\n│ FROM() |>\n│ WHERE(FUN(\"=\", ID(:location_1) |> ID(:state), LIT(\"IL\"))) |>\n│ SELECT(ID(:location_1) |> ID(:location_id)) |>\n│ AS(:location_2),\n│ FUN(\"=\",\n│ ID(:person_2) |> ID(:location_id),\n│ ID(:location_2) |> ID(:location_id))) |>\n│ JOIN(ID(:visit_occurrence) |>\n│ AS(:visit_occurrence_1) |>\n│ FROM() |>\n│ GROUP(ID(:visit_occurrence_1) |> ID(:person_id)) |>\n│ SELECT(AGG(\"max\",\n│ ID(:visit_occurrence_1) |> ID(:visit_start_date)) |>\n│ AS(:max),\n│ ID(:visit_occurrence_1) |> ID(:person_id)) |>\n│ AS(:visit_group_1),\n│ FUN(\"=\",\n│ ID(:person_2) |> ID(:person_id),\n│ ID(:visit_group_1) |> ID(:person_id)),\n│ left = true) |>\n│ SELECT(ID(:person_2) |> ID(:person_id),\n│ ID(:visit_group_1) |> ID(:max) |> AS(:max_visit_start_date)),\n│ columns = [SQLColumn(:person_id), SQLColumn(:max_visit_start_date)])\n└ @ FunSQL …\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Finally, the SQL tree is serialized into SQL.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"#? VERSION >= v\"1.7\"\nwithenv(\"JULIA_DEBUG\" => \"FunSQL.serialize\") do\n render(q)\nend;\n#=>\n┌ Debug: FunSQL.serialize\n│ SQLString(\n│ \"\"\"\n│ SELECT\n│ \"person_2\".\"person_id\",\n│ \"visit_group_1\".\"max\" AS \"max_visit_start_date\"\n│ FROM (\n│ SELECT\n│ \"person_1\".\"person_id\",\n│ \"person_1\".\"location_id\"\n│ FROM \"person\" AS \"person_1\"\n│ WHERE (\"person_1\".\"year_of_birth\" <= 2000)\n│ ) AS \"person_2\"\n│ JOIN (\n│ SELECT \"location_1\".\"location_id\"\n│ FROM \"location\" AS \"location_1\"\n│ WHERE (\"location_1\".\"state\" = 'IL')\n│ ) AS \"location_2\" ON (\"person_2\".\"location_id\" = \"location_2\".\"location_id\")\n│ LEFT JOIN (\n│ SELECT\n│ max(\"visit_occurrence_1\".\"visit_start_date\") AS \"max\",\n│ \"visit_occurrence_1\".\"person_id\"\n│ FROM \"visit_occurrence\" AS \"visit_occurrence_1\"\n│ GROUP BY \"visit_occurrence_1\".\"person_id\"\n│ ) AS \"visit_group_1\" ON (\"person_2\".\"person_id\" = \"visit_group_1\".\"person_id\")\"\"\",\n│ columns = [SQLColumn(:person_id), SQLColumn(:max_visit_start_date)])\n└ @ FunSQL …\n=#","category":"page"},{"location":"test/other/#Other-Tests","page":"Other Tests","title":"Other Tests","text":"","category":"section"},{"location":"test/other/#SQLConnection-and-SQLStatement","page":"Other Tests","title":"SQLConnection and SQLStatement","text":"","category":"section"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"A SQLConnection object encapsulates a raw database connection together with the database catalog.","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"using FunSQL: SQLConnection, SQLCatalog, SQLTable\nusing Pkg.Artifacts, LazyArtifacts\nusing SQLite\n\nconst DATABASE = joinpath(artifact\"synpuf-10p\", \"synpuf-10p.sqlite\")\n\nraw_conn = DBInterface.connect(SQLite.DB, DATABASE)\n\nperson = SQLTable(:person, columns = [:person_id, :year_of_birth])\n\ncatalog = SQLCatalog(person, dialect = :sqlite)\n\nconn = SQLConnection(raw_conn, catalog = catalog)\n#-> SQLConnection(SQLite.DB( … ), catalog = SQLCatalog(…1 table…, dialect = SQLDialect(:sqlite)))","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"SQLConnection delegates DBInterface calls to the raw connection object.","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"DBInterface.prepare(conn, \"SELECT * FROM person\")\n#-> SQLite.Stmt( … )\n\nDBInterface.execute(conn, \"SELECT * FROM person\")\n#-> SQLite.Query{false}( … )","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"When DBInterface.prepare is applied to a query node, it returns a FunSQL-specific SQLStatement object.","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"using FunSQL: From\n\nq = From(:person)\n\nstmt = DBInterface.prepare(conn, q)\n#-> SQLStatement(SQLConnection( … ), SQLite.Stmt( … ))\n\nDBInterface.getconnection(stmt)\n#-> SQLConnection( … )\n\nDBInterface.execute(stmt)\n#-> SQLite.Query{false}( … )\n\nDBInterface.close!(stmt)","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"For a query with parameters, this allows us to specify the parameter values by name.","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"using FunSQL: Get, Var, Where\n\nq = From(:person) |>\n Where(Get.year_of_birth .>= Var.YEAR)\n\nstmt = DBInterface.prepare(conn, q)\n#-> SQLStatement(SQLConnection( … ), SQLite.Stmt( … ), vars = [:YEAR])\n\nDBInterface.execute(stmt, YEAR = 1950)\n#-> SQLite.Query{false}( … )\n\nDBInterface.close!(stmt)\n\nDBInterface.close!(conn)","category":"page"},{"location":"test/other/#SQLCatalog,-SQLTable,-and-SQLColumn","page":"Other Tests","title":"SQLCatalog, SQLTable, and SQLColumn","text":"","category":"section"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"In FunSQL, tables and table-like entities are represented using SQLTable objects. Their columns are represented using SQLColumn objects. A collection of SQLTable objects is represented as a SQLCatalog object.","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"using FunSQL: SQLCatalog, SQLColumn, SQLTable","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"A SQLTable constructor takes the table name, a vector of columns, and, optionally, the name of the table schema and other qualifiers. A name could be provided either as a Symbol or as a String value. A column can be specified just by its name.","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"location = SQLTable(qualifiers = [:public],\n name = :location,\n columns = [:location_id, :address_1, :address_2,\n :city, :state, :zip])\n#-> SQLTable(qualifiers = [:public], :location, …)\n\nperson = SQLTable(name = \"person\",\n columns = [\"person_id\", \"year_of_birth\", \"location_id\"])\n#-> SQLTable(:person, …)","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"The table and the column names could be provided as positional arguments.","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"concept = SQLTable(\"concept\", \"concept_id\", \"concept_name\", \"vocabulary_id\")\n#-> SQLTable(:concept, …)","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"A column may have a custom name for use with FunSQL and the original name for generating SQL queries.","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"vocabulary = SQLTable(:vocabulary,\n :id => SQLColumn(:vocabulary_id),\n :name => SQLColumn(:vocabulary_name))\n#-> SQLTable(:vocabulary, …)","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"A SQLTable object is displayed as a Julia expression that created the object.","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"display(location)\n#=>\nSQLTable(qualifiers = [:public],\n :location,\n SQLColumn(:location_id),\n SQLColumn(:address_1),\n SQLColumn(:address_2),\n SQLColumn(:city),\n SQLColumn(:state),\n SQLColumn(:zip))\n=#\n\ndisplay(vocabulary)\n#=>\nSQLTable(:vocabulary,\n :id => SQLColumn(:vocabulary_id),\n :name => SQLColumn(:vocabulary_name))\n=#","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"A SQLTable object behaves like a read-only dictionary.","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"person[:person_id]\n#-> SQLColumn(:person_id)\n\nperson[\"person_id\"]\n#-> SQLColumn(:person_id)\n\nperson[1]\n#-> SQLColumn(:person_id)\n\nperson[:visit_occurrence]\n#-> ERROR: KeyError: key :visit_occurrence not found\n\nget(person, :person_id, nothing)\n#-> SQLColumn(:person_id)\n\nget(person, \"person_id\", nothing)\n#-> SQLColumn(:person_id)\n\nget(person, :visit_occurrence, missing)\n#-> missing\n\nget(() -> missing, person, :visit_occurrence)\n#-> missing\n\nlength(person)\n#-> 3\n\ncollect(keys(person))\n#-> [:person_id, :year_of_birth, :location_id]","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"A SQLCatalog constructor takes a collection of SQLTable objects, the target dialect, and the size of the query cache. Just as columns, a table may have a custom name for use with FunSQL and the original name for generating SQL.","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"catalog = SQLCatalog(tables = [person, location, concept, :concept_vocabulary => vocabulary],\n dialect = :sqlite,\n cache = 128)\n#-> SQLCatalog(…4 tables…, dialect = SQLDialect(:sqlite), cache = 128)\n\ndisplay(catalog)\n#=>\nSQLCatalog(SQLTable(:concept,\n SQLColumn(:concept_id),\n SQLColumn(:concept_name),\n SQLColumn(:vocabulary_id)),\n :concept_vocabulary => SQLTable(:vocabulary,\n :id => SQLColumn(:vocabulary_id),\n :name => SQLColumn(\n :vocabulary_name)),\n SQLTable(qualifiers = [:public],\n :location,\n SQLColumn(:location_id),\n SQLColumn(:address_1),\n SQLColumn(:address_2),\n SQLColumn(:city),\n SQLColumn(:state),\n SQLColumn(:zip)),\n SQLTable(:person,\n SQLColumn(:person_id),\n SQLColumn(:year_of_birth),\n SQLColumn(:location_id)),\n dialect = SQLDialect(:sqlite),\n cache = 128)\n=#","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"Number of tables in the catalog affects its representation.","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"SQLCatalog(tables = [:person => person])\n#-> SQLCatalog(…1 table…, dialect = SQLDialect())\n\nSQLCatalog()\n#-> SQLCatalog(dialect = SQLDialect())","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"The query cache can be completely disabled.","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"cacheless_catalog = SQLCatalog(cache = nothing)\n#-> SQLCatalog(dialect = SQLDialect(), cache = nothing)\n\ndisplay(cacheless_catalog)\n#-> SQLCatalog(dialect = SQLDialect(), cache = nothing)","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"Any Dict-like object can serve as a query cache.","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"customcache_catalog = SQLCatalog(cache = Dict())\n#-> SQLCatalog(dialect = SQLDialect(), cache = Dict{Any, Any}())\n\ndisplay(customcache_catalog)\n#-> SQLCatalog(dialect = SQLDialect(), cache = (Dict{Any, Any})())","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"The catalog behaves as a read-only Dict object.","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"catalog[:person]\n#-> SQLTable(:person, …)\n\ncatalog[\"person\"]\n#-> SQLTable(:person, …)\n\ncatalog[:visit_occurrence]\n#-> ERROR: KeyError: key :visit_occurrence not found\n\nget(catalog, :person, nothing)\n#-> SQLTable(:person, …)\n\nget(catalog, \"person\", nothing)\n#-> SQLTable(:person, …)\n\nget(catalog, :visit_occurrence, missing)\n#-> missing\n\nget(() -> missing, catalog, :visit_occurrence)\n#-> missing\n\nlength(catalog)\n#-> 4\n\nsort(collect(keys(catalog)))\n#-> [:concept, :concept_vocabulary, :location, :person]","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"Catalog objects can be assigned arbitrary metadata.","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"metadata_catalog =\n SQLCatalog(SQLTable(:person,\n SQLColumn(:person_id, metadata = (; label = \"Person ID\")),\n SQLColumn(:year_of_birth, metadata = (;)),\n metadata = (; caption = \"Person\", is_view = false)),\n metadata = (; model = \"OMOP\"))\n#-> SQLCatalog(…1 table…, dialect = SQLDialect(), metadata = …)\n\ndisplay(metadata_catalog)\n#=>\nSQLCatalog(SQLTable(:person,\n SQLColumn(:person_id, metadata = [:label => \"Person ID\"]),\n SQLColumn(:year_of_birth),\n metadata = [:caption => \"Person\", :is_view => false]),\n dialect = SQLDialect(),\n metadata = [:model => \"OMOP\"])\n=#","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"FunSQL metadata supports DataAPI metadata interface.","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"using DataAPI\n\nDataAPI.metadata(metadata_catalog)\n#-> Dict(\"model\" => \"OMOP\")\n\nDataAPI.metadata(metadata_catalog, style = true)\n#-> Dict(\"model\" => (\"OMOP\", :default))\n\nDataAPI.metadata(metadata_catalog, :name, :default)\n#-> :default\n\nDataAPI.metadata(metadata_catalog[:person])[\"caption\"]\n#-> \"Person\"\n\nDataAPI.metadata(metadata_catalog[:person], :is_view, true)\n#-> false\n\nDataAPI.colmetadata(metadata_catalog[:person])[:person_id][\"label\"]\n#-> \"Person ID\"\n\nDataAPI.colmetadata(metadata_catalog[:person], 1, :label)\n#-> \"Person ID\"\n\nDataAPI.colmetadata(metadata_catalog[:person], :year_of_birth, :label, \"\")\n#-> \"\"\n\nDataAPI.metadata(metadata_catalog[:person][:person_id])\n#-> Dict(\"label\" => \"Person ID\")\n\nDataAPI.metadata(metadata_catalog[:person][:person_id], :label, \"\")\n#-> \"Person ID\"","category":"page"},{"location":"test/other/#SQLDialect","page":"Other Tests","title":"SQLDialect","text":"","category":"section"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"In FunSQL, properties and capabilities of a particular SQL dialect are encapsulated in a SQLDialect object.","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"using FunSQL: SQLDialect","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"The desired dialect can be specified by name.","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"postgresql_dialect = SQLDialect(:postgresql)\n#-> SQLDialect(:postgresql)\n\ndisplay(postgresql_dialect)\n#-> SQLDialect(:postgresql)","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"If necessary, the dialect can be customized.","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"postgresql_odbc_dialect = SQLDialect(:postgresql,\n variable_prefix = '?',\n variable_style = :positional)\n#-> SQLDialect(:postgresql, …)\n\ndisplay(postgresql_odbc_dialect)\n#-> SQLDialect(:postgresql, variable_prefix = '?', variable_style = :POSITIONAL)","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"The default dialect does not correspond to any particular database server.","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"default_dialect = SQLDialect()\n#-> SQLDialect()\n\ndisplay(default_dialect)\n#-> SQLDialect()","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"A completely custom dialect can be specified.","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"my_dialect = SQLDialect(:my, identifier_quotes = ('<', '>'))\n#-> SQLDialect(name = :my, …)\n\ndisplay(my_dialect)\n#-> SQLDialect(name = :my, identifier_quotes = ('<', '>'))","category":"page"},{"location":"test/other/#SQLString","page":"Other Tests","title":"SQLString","text":"","category":"section"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"SQLString represents a serialized SQL query.","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"using FunSQL: SQLString, pack\n\nsql = SQLString(\"SELECT * FROM person\")\n#-> SQLString(\"SELECT * FROM person\")\n\ndisplay(sql)\n#-> SQLString(\"SELECT * FROM person\")","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"SQLString implements the AbstractString interface.","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"ncodeunits(sql)\n#-> 20\n\ncodeunit(sql)\n#-> UInt8\n\ncodeunit(sql, 1)\n#-> 0x53\n\nisvalid(sql, 1)\n#-> true\n\njoin(collect(sql))\n#-> \"SELECT * FROM person\"\n\nprint(sql)\n#-> SELECT * FROM person\n\nwrite(IOBuffer(), sql)\n#-> 20\n\nString(sql)\n#-> \"SELECT * FROM person\"","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"SQLString may carry a vector columns describing the output columns of the query.","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"sql = SQLString(\"SELECT person_id FROM person\", columns = [SQLColumn(:person_id)])\n#-> SQLString(\"SELECT person_id FROM person\", columns = […1 column…])\n\ndisplay(sql)\n#-> SQLString(\"SELECT person_id FROM person\", columns = [SQLColumn(:person_id)])","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"When the query has parameters, SQLString should include a vector of parameter names in the order they should appear in DBInterface.execute call.","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"sql = SQLString(\"SELECT * FROM person WHERE year_of_birth >= ?\", vars = [:YEAR])\n#-> SQLString(\"SELECT * FROM person WHERE year_of_birth >= ?\", vars = [:YEAR])\n\ndisplay(sql)\n#-> SQLString(\"SELECT * FROM person WHERE year_of_birth >= ?\", vars = [:YEAR])","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"Function pack converts named parameters to the positional form suitable for use with DBInterface.execute.","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"pack(sql, (; YEAR = 1950))\n#-> Any[1950]\n\npack(sql, Dict(:YEAR => 1950))\n#-> Any[1950]\n\npack(sql, Dict(\"YEAR\" => 1950))\n#-> Any[1950]","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"pack can also be applied to a regular string, in which case it returns the parameters unchanged.","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"pack(\"SELECT * FROM person WHERE year_of_birth >= ?\", (1950,))\n#-> (1950,)","category":"page"},{"location":"examples/#Examples","page":"Examples","title":"Examples","text":"","category":"section"},{"location":"examples/","page":"Examples","title":"Examples","text":"CurrentModule = FunSQL","category":"page"},{"location":"examples/#Importing-FunSQL","page":"Examples","title":"Importing FunSQL","text":"","category":"section"},{"location":"examples/","page":"Examples","title":"Examples","text":"FunSQL does not export any symbols by default. The following statement imports all available query constructors and the function render.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"using FunSQL:\n FunSQL, Agg, Append, As, Asc, Bind, CrossJoin, Define, Desc, Fun, From,\n Get, Group, Highlight, Iterate, Join, LeftJoin, Limit, Lit, Order,\n Partition, Select, Sort, Var, Where, With, WithExternal, render","category":"page"},{"location":"examples/#Establishing-a-database-connection","page":"Examples","title":"Establishing a database connection","text":"","category":"section"},{"location":"examples/","page":"Examples","title":"Examples","text":"We use FunSQL to assemble SQL queries. To actually run these queries, we need a regular database library such as SQLite.jl, LibPQ.jl, MySQL.jl, or ODBC.jl.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"In the following examples, we use a SQLite database containing a tiny sample of the CMS DE-SynPuf dataset. See the Usage Guide for the description of the database schema.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"Download the database file.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"const URL = \"https://github.com/MechanicalRabbit/ohdsi-synpuf-demo/releases/download/20210412/synpuf-10p.sqlite\"\nconst DATABASE = download(URL)","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"Download the database file as an artifact.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"using Pkg.Artifacts, LazyArtifacts\n\nconst DATABASE = joinpath(artifact\"synpuf-10p\", \"synpuf-10p.sqlite\")\n#-> ⋮","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"Create a connection object.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"using SQLite\n\nconst conn = DBInterface.connect(FunSQL.DB{SQLite.DB}, DATABASE)","category":"page"},{"location":"examples/#Database-connection-with-LibPQ.jl","page":"Examples","title":"Database connection with LibPQ.jl","text":"","category":"section"},{"location":"examples/","page":"Examples","title":"Examples","text":"To create a connection object, FunSQL relies on the DBInterface.jl package. Unfortunately LibPQ.jl, the PostgreSQL client library, does not support DBInterface. To make DBInterface.connect work, we need to manually bridge LibPQ and DBInterface.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"using LibPQ\nusing DBInterface\n\nDBInterface.connect(::Type{LibPQ.Connection}, args...; kws...) =\n LibPQ.Connection(args...; kws...)\n\nDBInterface.prepare(conn::LibPQ.Connection, args...; kws...) =\n LibPQ.prepare(conn, args...; kws...)\n\nDBInterface.execute(conn::Union{LibPQ.Connection, LibPQ.Statement}, args...; kws...) =\n LibPQ.execute(conn, args...; kws...)","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"Now we can create a FunSQL connection using DBInterface.connect.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"const conn = DBInterface.connect(FunSQL.DB{LibPQ.Connection}, …)","category":"page"},{"location":"examples/#SELECT-*-FROM-table","page":"Examples","title":"SELECT * FROM table","text":"","category":"section"},{"location":"examples/","page":"Examples","title":"Examples","text":"FunSQL does not require that a query object contains Select, so a minimal FunSQL query consists of a single From node.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"Show all patient records.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"q = From(:person)","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"We use the function render to serialize the query node as a SQL statement.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"sql = render(conn, q)\n\nprint(sql)\n#=>\nSELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"ethnicity_source_concept_id\"\nFROM \"person\" AS \"person_1\"\n=#","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"This query could be executed with DBInterface.execute.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"res = DBInterface.execute(conn, sql)","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"To display the output of a query, it is convenient to use the DataFrame interface.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"using DataFrames\n\nDataFrame(res)\n#=>\n10×18 DataFrame\n Row │ person_id gender_concept_id year_of_birth month_of_birth day_of_bir ⋯\n │ Int64 Int64 Int64 Int64 Int64 ⋯\n─────┼──────────────────────────────────────────────────────────────────────────\n 1 │ 1780 8532 1940 2 ⋯\n 2 │ 30091 8532 1932 8\n 3 │ 37455 8532 1913 7\n 4 │ 42383 8507 1922 2\n 5 │ 69985 8532 1956 7 ⋯\n 6 │ 72120 8507 1937 10\n 7 │ 82328 8532 1957 9\n 8 │ 95538 8507 1923 11\n 9 │ 107680 8532 1963 12 ⋯\n 10 │ 110862 8507 1911 4\n 14 columns omitted\n=#","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"We could also directly apply DBInterface.execute to the query node in order to render and immediately execute it.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"DBInterface.execute(conn, q) |> DataFrame\n#=>\n10×18 DataFrame\n⋮\n=#","category":"page"},{"location":"examples/#WHERE,-ORDER,-LIMIT","page":"Examples","title":"WHERE, ORDER, LIMIT","text":"","category":"section"},{"location":"examples/","page":"Examples","title":"Examples","text":"Tabular operations such as Where, Order, and Limit are available in FunSQL. Unlike SQL, FunSQL lets you apply them in any order.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"Show the top 3 oldest male patients.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"q = From(:person) |>\n Where(Get.gender_concept_id .== 8507) |>\n Order(Get.year_of_birth) |>\n Limit(3)\n\nrender(conn, q) |> print\n#=>\nSELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"ethnicity_source_concept_id\"\nFROM \"person\" AS \"person_1\"\nWHERE (\"person_1\".\"gender_concept_id\" = 8507)\nORDER BY \"person_1\".\"year_of_birth\"\nLIMIT 3\n=#\n\nDBInterface.execute(conn, q) |> DataFrame\n#=>\n3×18 DataFrame\n Row │ person_id gender_concept_id year_of_birth month_of_birth day_of_bir ⋯\n │ Int64 Int64 Int64 Int64 Int64 ⋯\n─────┼──────────────────────────────────────────────────────────────────────────\n 1 │ 110862 8507 1911 4 ⋯\n 2 │ 42383 8507 1922 2\n 3 │ 95538 8507 1923 11\n 14 columns omitted\n=#","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"Show all males among the top 3 oldest patients.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"q = From(:person) |>\n Order(Get.year_of_birth) |>\n Limit(3) |>\n Where(Get.gender_concept_id .== 8507)\n\nrender(conn, q) |> print\n#=>\nSELECT\n \"person_2\".\"person_id\",\n ⋮\n \"person_2\".\"ethnicity_source_concept_id\"\nFROM (\n SELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"ethnicity_source_concept_id\"\n FROM \"person\" AS \"person_1\"\n ORDER BY \"person_1\".\"year_of_birth\"\n LIMIT 3\n) AS \"person_2\"\nWHERE (\"person_2\".\"gender_concept_id\" = 8507)\n=#\n\nDBInterface.execute(conn, q) |> DataFrame\n#=>\n2×18 DataFrame\n Row │ person_id gender_concept_id year_of_birth month_of_birth day_of_bir ⋯\n │ Int64 Int64 Int64 Int64 Int64 ⋯\n─────┼──────────────────────────────────────────────────────────────────────────\n 1 │ 110862 8507 1911 4 ⋯\n 2 │ 42383 8507 1922 2\n 14 columns omitted\n=#","category":"page"},{"location":"examples/#SELECT-COUNT(*)-FROM-table","page":"Examples","title":"SELECT COUNT(*) FROM table","text":"","category":"section"},{"location":"examples/","page":"Examples","title":"Examples","text":"To calculate an aggregate value for the whole dataset, we apply a Group node without arguments.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"Show the number of patient records.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"q = From(:person) |>\n Group() |>\n Select(Agg.count())\n\nrender(conn, q) |> print\n#=>\nSELECT count(*) AS \"count\"\nFROM \"person\" AS \"person_1\"\n=#\n\nDBInterface.execute(conn, q) |> DataFrame\n#=>\n1×1 DataFrame\n Row │ count\n │ Int64\n─────┼───────\n 1 │ 10\n=#","category":"page"},{"location":"examples/#SELECT-DISTINCT","page":"Examples","title":"SELECT DISTINCT","text":"","category":"section"},{"location":"examples/","page":"Examples","title":"Examples","text":"If we use a Group node, but do not apply any aggregate functions, FunSQL will render it as a SELECT DISTINCT clause.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"Show all US states present in the location records.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"q = From(:location) |>\n Group(Get.state)\n\nrender(conn, q) |> print\n#=>\nSELECT DISTINCT \"location_1\".\"state\"\nFROM \"location\" AS \"location_1\"\n=#\n\nDBInterface.execute(conn, q) |> DataFrame\n#=>\n10×1 DataFrame\n Row │ state\n │ String\n─────┼────────\n 1 │ MI\n 2 │ WA\n 3 │ FL\n 4 │ MD\n 5 │ NY\n 6 │ MS\n 7 │ CO\n 8 │ GA\n 9 │ MA\n 10 │ IL\n=#","category":"page"},{"location":"examples/#Generating-a-complex-CASE-clause","page":"Examples","title":"Generating a complex CASE clause","text":"","category":"section"},{"location":"examples/","page":"Examples","title":"Examples","text":"Show the number of patients stratified by the age group.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"In this query, we need to place a person's age into one of the age buckets: 0 – 4, 5 – 9, 10 – 14, …, 95 – 99, 100 +. This is a tedious expression to write in raw SQL, but it could be written very compactly in FunSQL by using array comprehension to build the CASE expression.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"using Dates\n\nPersonAgeAt(date) =\n Fun.strftime(\"%Y\", date) .- Get.year_of_birth\n\nAgeGroup(age) =\n Fun.case(Iterators.flatten([(age .< y, \"$(y-5) - $(y-1)\")\n for y = 5:5:100])...,\n \"≥ 100\")\n\nq = From(:person) |>\n Group(:age_group => AgeGroup(PersonAgeAt(Date(\"2020-01-01\")))) |>\n Order(Get.age_group) |>\n Select(Get.age_group, Agg.count())\n\nrender(conn, q) |> print\n#=>\nSELECT\n \"person_2\".\"age_group\",\n count(*) AS \"count\"\nFROM (\n SELECT (CASE WHEN ((strftime('%Y', '2020-01-01') - \"person_1\".\"year_of_birth\") < 5) THEN '0 - 4' … ELSE '≥ 100' END) AS \"age_group\"\n FROM \"person\" AS \"person_1\"\n) AS \"person_2\"\nGROUP BY \"person_2\".\"age_group\"\nORDER BY \"person_2\".\"age_group\"\n=#\n\nDBInterface.execute(conn, q) |> DataFrame\n#=>\n6×2 DataFrame\n Row │ age_group count\n │ String Int64\n─────┼──────────────────\n 1 │ 55 - 59 1\n 2 │ 60 - 64 2\n 3 │ 80 - 84 2\n 4 │ 85 - 89 1\n 5 │ 95 - 99 2\n 6 │ ≥ 100 2\n=#","category":"page"},{"location":"examples/#Filtering-output-columns","page":"Examples","title":"Filtering output columns","text":"","category":"section"},{"location":"examples/","page":"Examples","title":"Examples","text":"By default, the From node outputs all columns of a table, but we could restrict or change the list of output columns using Select. Typically, we would directly pass the definitions of output columns as individual arguments of Select, but occasionally it is convenient to generate the definitions programmatically.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"Filter out all \"source\" columns from patient records.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"const person_table = conn.catalog[:person]\n\nis_not_source_column(c::Symbol) =\n !contains(String(c), \"source\")\n\nq = From(:person) |>\n Select(args = [Get(c) for c in keys(person_table.columns) if is_not_source_column(c)])\n\ndisplay(q)\n#=>\nlet q1 = From(:person),\n q2 = q1 |>\n Select(Get.person_id,\n Get.gender_concept_id,\n Get.year_of_birth,\n Get.month_of_birth,\n Get.day_of_birth,\n Get.time_of_birth,\n Get.race_concept_id,\n Get.ethnicity_concept_id,\n Get.location_id,\n Get.provider_id,\n Get.care_site_id)\n q2\nend\n=#\n\nrender(conn, q) |> print\n#=>\nSELECT\n \"person_1\".\"person_id\",\n \"person_1\".\"gender_concept_id\",\n \"person_1\".\"year_of_birth\",\n \"person_1\".\"month_of_birth\",\n \"person_1\".\"day_of_birth\",\n \"person_1\".\"time_of_birth\",\n \"person_1\".\"race_concept_id\",\n \"person_1\".\"ethnicity_concept_id\",\n \"person_1\".\"location_id\",\n \"person_1\".\"provider_id\",\n \"person_1\".\"care_site_id\"\nFROM \"person\" AS \"person_1\"\n=#\n\nDBInterface.execute(conn, q) |> DataFrame\n#=>\n10×11 DataFrame\n Row │ person_id gender_concept_id year_of_birth month_of_birth day_of_bir ⋯\n │ Int64 Int64 Int64 Int64 Int64 ⋯\n─────┼──────────────────────────────────────────────────────────────────────────\n 1 │ 1780 8532 1940 2 ⋯\n 2 │ 30091 8532 1932 8\n 3 │ 37455 8532 1913 7\n 4 │ 42383 8507 1922 2\n 5 │ 69985 8532 1956 7 ⋯\n 6 │ 72120 8507 1937 10\n 7 │ 82328 8532 1957 9\n 8 │ 95538 8507 1923 11\n 9 │ 107680 8532 1963 12 ⋯\n 10 │ 110862 8507 1911 4\n 7 columns omitted\n=#","category":"page"},{"location":"examples/#Output-columns-of-a-Join","page":"Examples","title":"Output columns of a Join","text":"","category":"section"},{"location":"examples/","page":"Examples","title":"Examples","text":"As is often used to disambiguate the columns of the two input branches of the Join node. By default, columns fenced by As are not present in the output.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"q = From(:person) |>\n Join(From(:visit_occurrence) |> As(:visit),\n on = Get.person_id .== Get.visit.person_id)\n\nrender(conn, q) |> print\n#=>\nSELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"ethnicity_source_concept_id\"\nFROM \"person\" AS \"person_1\"\nJOIN \"visit_occurrence\" AS \"visit_occurrence_1\" ON (\"person_1\".\"person_id\" = \"visit_occurrence_1\".\"person_id\")\n=#\n\nq′ = From(:person) |> As(:person) |>\n Join(From(:visit_occurrence),\n on = Get.person.person_id .== Get.person_id)\n\nrender(conn, q′) |> print\n#=>\nSELECT\n \"visit_occurrence_1\".\"visit_occurrence_id\",\n ⋮\n \"visit_occurrence_1\".\"visit_source_concept_id\"\nFROM \"person\" AS \"person_1\"\nJOIN \"visit_occurrence\" AS \"visit_occurrence_1\" ON (\"person_1\".\"person_id\" = \"visit_occurrence_1\".\"person_id\")\n=#","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"We could use a Select node to output the columns of both branches, however we must ensure that all column names are unique.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"const visit_occurrence_table = conn.catalog[:visit_occurrence]\n\nq = q |>\n Select(Get.(keys(person_table.columns))...,\n Get.(keys(visit_occurrence_table.columns), over = Get.visit)...)\n#=>\nERROR: FunSQL.DuplicateLabelError: `person_id` is used more than once in:\n⋮\n=#\n\nq = q |>\n Select(Get.(keys(person_table.columns))...,\n Get.(filter(!in(keys(person_table.columns)), collect(keys(visit_occurrence_table.columns))),\n over = Get.visit)...)\n\nrender(conn, q) |> print\n#=>\nSELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"ethnicity_source_concept_id\",\n \"visit_occurrence_1\".\"visit_occurrence_id\",\n ⋮\n \"visit_occurrence_1\".\"visit_source_concept_id\"\nFROM \"person\" AS \"person_1\"\nJOIN \"visit_occurrence\" AS \"visit_occurrence_1\" ON (\"person_1\".\"person_id\" = \"visit_occurrence_1\".\"person_id\")\n=#","category":"page"},{"location":"examples/#Querying-concepts","page":"Examples","title":"Querying concepts","text":"","category":"section"},{"location":"examples/","page":"Examples","title":"Examples","text":"Medical terms, such as Inpatient (visit) or Myocardial infarction (condition), are stored in the table concept. Concepts are typically identified by the vocabulary and the code within the vocabulary. For example, Myocardial infarction has a code 22298006 in the SNOMED CT vocabulary.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"Concept may be related to each other. For example, Acute myocardial infarction is a subtype of Myocardial infarction. Relationships between concepts are stored in the table concept_relationship with the column relationship_id specifying the type of the relationship.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"Querying healthcare information often starts with identifying the set of relevant concepts. For example, a researcher may want to specify a concept set containing","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"Myocardial infarction (SNOMED 22298006);\nAnd all the subtypes;\nBut excluding Acute subendocardial infarction (SNOMED 70422006) and its subtypes.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"This suggests us to make a FunSQL-based mini-language for querying concept sets. This language will include primitives for fetching concepts by name, or by vocabulary and code, operations for adding related concepts, and combining and excluding concept sets. These operations could be expressed directly in terms of FunSQL queries.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"We start with a primitive for finding a concept by its code in the vocabulary.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"ConceptByCode(vocabulary, code) =\n From(:concept) |>\n Where(Fun.and(Get.vocabulary_id .== vocabulary,\n Get.concept_code .== code))\n\nConceptByCode(vocabulary, codes...) =\n From(:concept) |>\n Where(Fun.and(Get.vocabulary_id .== vocabulary,\n Fun.in(Get.concept_code, codes...)))","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"It is convenient to add a shortcut for common vocabularies.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"SNOMED(codes...) =\n ConceptByCode(\"SNOMED\", codes...)\n\nVISIT(codes...) =\n ConceptByCode(\"Visit\", codes...)","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"Now we can define","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"q = SNOMED(\"22298006\") # Myocardial infarction\n\nDBInterface.execute(conn, q) |> DataFrame\n#=>\n1×10 DataFrame\n Row │ concept_id concept_name domain_id vocabulary_id concept_cl ⋯\n │ Int64 String String String String ⋯\n─────┼──────────────────────────────────────────────────────────────────────────\n 1 │ 4329847 Myocardial infarction Condition SNOMED Clinical F ⋯\n 6 columns omitted\n=#","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"The following composite query pipeline can be applied to a set of concepts to determine their immediate subtypes.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"ImmediateSubtypes() =\n As(:base) |>\n Join(From(:concept_relationship) |>\n Where(Get.relationship_id .== \"Is a\") |>\n As(:concept_relationship),\n on = Get.base.concept_id .== Get.concept_relationship.concept_id_2) |>\n Join(From(:concept),\n on = Get.concept_relationship.concept_id_1 .== Get.concept_id)\n\nq = SNOMED(\"22298006\") |> # Myocardial infarction\n ImmediateSubtypes()\n\nDBInterface.execute(conn, q) |> DataFrame\n#=>\n1×10 DataFrame\n Row │ concept_id concept_name domain_id vocabulary_id conc ⋯\n │ Int64 String String String Stri ⋯\n─────┼──────────────────────────────────────────────────────────────────────────\n 1 │ 312327 Acute myocardial infarction Condition SNOMED Clin ⋯\n 6 columns omitted\n=#","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"Recursively applying ImmediateSubtypes with Iterate gives us the concept set together will all subtypes.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"WithSubtypes() =\n Iterate(ImmediateSubtypes())\n\nq = SNOMED(\"22298006\") |> # Myocardial infarction\n WithSubtypes()\n\nDBInterface.execute(conn, q) |> DataFrame\n#=>\n6×10 DataFrame\n Row │ concept_id concept_name domain_id vocabulary_id ⋯\n │ Int64 String String String ⋯\n─────┼──────────────────────────────────────────────────────────────────────────\n 1 │ 4329847 Myocardial infarction Condition SNOMED ⋯\n 2 │ 312327 Acute myocardial infarction Condition SNOMED\n 3 │ 434376 Acute myocardial infarction of a… Condition SNOMED\n 4 │ 438170 Acute myocardial infarction of i… Condition SNOMED\n 5 │ 438438 Acute myocardial infarction of a… Condition SNOMED ⋯\n 6 │ 444406 Acute subendocardial infarction Condition SNOMED\n 6 columns omitted\n=#","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"Finally, we add operations on a concept set for adding or removing concepts.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"IncludingConcepts(include) =\n Append(include)\n\nExcludingConcepts(exclude) =\n LeftJoin(:exclude => exclude,\n Get.concept_id .== Get.exclude.concept_id) |>\n Where(Fun.is_null(Get.exclude.concept_id))\n\nq = SNOMED(\"22298006\") |> # Myocardial infarction\n WithSubtypes() |>\n ExcludingConcepts(\n SNOMED(\"70422006\") |> # Acute subendocardial infarction\n WithSubtypes())\n\nDBInterface.execute(conn, q) |> DataFrame\n#=>\n5×10 DataFrame\n Row │ concept_id concept_name domain_id vocabulary_id ⋯\n │ Int64 String String String ⋯\n─────┼──────────────────────────────────────────────────────────────────────────\n 1 │ 4329847 Myocardial infarction Condition SNOMED ⋯\n 2 │ 312327 Acute myocardial infarction Condition SNOMED\n 3 │ 434376 Acute myocardial infarction of a… Condition SNOMED\n 4 │ 438170 Acute myocardial infarction of i… Condition SNOMED\n 5 │ 438438 Acute myocardial infarction of a… Condition SNOMED ⋯\n 6 columns omitted\n=#","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"Given a concept set, it is now easy to find the matching clinical conditions.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"MyocardialInfarctionConcepts() =\n SNOMED(\"22298006\") |> # Myocardial infarction\n WithSubtypes() |>\n ExcludingConcepts(\n SNOMED(\"70422006\") |> # Acute subendocardial infarction\n WithSubtypes())\n\nq = From(:condition_occurrence) |>\n Join(MyocardialInfarctionConcepts(),\n Get.condition_concept_id .== Get.concept_id) |>\n Order(Get.condition_occurrence_id) |>\n Select(Get.person_id, Get.condition_start_date)\n\nDBInterface.execute(conn, q) |> DataFrame\n#=>\n6×2 DataFrame\n Row │ person_id condition_start_date\n │ Int64 String\n─────┼─────────────────────────────────\n 1 │ 1780 2008-04-10\n 2 │ 37455 2010-08-12\n 3 │ 69985 2010-05-06\n 4 │ 110862 2008-09-07\n 5 │ 110862 2008-09-07\n 6 │ 110862 2010-06-07\n=#","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"This notation is much more compact and readable than the corresponding SQL query.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"render(conn, q) |> print\n#=>\nWITH RECURSIVE \"base_1\" (\"concept_id\") AS (\n SELECT \"concept_1\".\"concept_id\"\n FROM \"concept\" AS \"concept_1\"\n WHERE\n (\"concept_1\".\"vocabulary_id\" = 'SNOMED') AND\n (\"concept_1\".\"concept_code\" = '22298006')\n UNION ALL\n SELECT \"concept_2\".\"concept_id\"\n FROM \"base_1\" AS \"base_2\"\n JOIN (\n SELECT\n \"concept_relationship_1\".\"concept_id_1\",\n \"concept_relationship_1\".\"concept_id_2\"\n FROM \"concept_relationship\" AS \"concept_relationship_1\"\n WHERE (\"concept_relationship_1\".\"relationship_id\" = 'Is a')\n ) AS \"concept_relationship_2\" ON (\"base_2\".\"concept_id\" = \"concept_relationship_2\".\"concept_id_2\")\n JOIN \"concept\" AS \"concept_2\" ON (\"concept_relationship_2\".\"concept_id_1\" = \"concept_2\".\"concept_id\")\n),\n\"base_4\" (\"concept_id\") AS (\n SELECT \"concept_3\".\"concept_id\"\n FROM \"concept\" AS \"concept_3\"\n WHERE\n (\"concept_3\".\"vocabulary_id\" = 'SNOMED') AND\n (\"concept_3\".\"concept_code\" = '70422006')\n UNION ALL\n SELECT \"concept_4\".\"concept_id\"\n FROM \"base_4\" AS \"base_5\"\n JOIN (\n SELECT\n \"concept_relationship_3\".\"concept_id_1\",\n \"concept_relationship_3\".\"concept_id_2\"\n FROM \"concept_relationship\" AS \"concept_relationship_3\"\n WHERE (\"concept_relationship_3\".\"relationship_id\" = 'Is a')\n ) AS \"concept_relationship_4\" ON (\"base_5\".\"concept_id\" = \"concept_relationship_4\".\"concept_id_2\")\n JOIN \"concept\" AS \"concept_4\" ON (\"concept_relationship_4\".\"concept_id_1\" = \"concept_4\".\"concept_id\")\n)\nSELECT\n \"condition_occurrence_1\".\"person_id\",\n \"condition_occurrence_1\".\"condition_start_date\"\nFROM \"condition_occurrence\" AS \"condition_occurrence_1\"\nJOIN (\n SELECT \"base_3\".\"concept_id\"\n FROM \"base_1\" AS \"base_3\"\n LEFT JOIN \"base_4\" AS \"base_6\" ON (\"base_3\".\"concept_id\" = \"base_6\".\"concept_id\")\n WHERE (\"base_6\".\"concept_id\" IS NULL)\n) AS \"base_7\" ON (\"condition_occurrence_1\".\"condition_concept_id\" = \"base_7\".\"concept_id\")\nORDER BY \"condition_occurrence_1\".\"condition_occurrence_id\"\n=#","category":"page"},{"location":"examples/#Assembling-queries-incrementally","page":"Examples","title":"Assembling queries incrementally","text":"","category":"section"},{"location":"examples/","page":"Examples","title":"Examples","text":"It is often convenient to build a query incrementally, one component at a time. This allows us to validate individual components, inspect their output, and possibly reuse them in other queries.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"Find all occurrences of myocardial infarction that was diagnosed during an inpatient visit. Filter out repeating occurrences by requiring a 180-day gap between consecutive events.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"We start with generating two datasets: inpatient visits and myocardial infarction conditions. For constructing the concepts Inpatient Visit and Myocardial Infarction, we use the definitions from the section Querying concepts:","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"MyocardialInfarctionConcept() =\n SNOMED(\"22298006\") |>\n WithSubtypes()\n\nDBInterface.execute(conn, MyocardialInfarctionConcept()) |> DataFrame\n#=>\n6×10 DataFrame\n Row │ concept_id concept_name domain_id vocabulary_id ⋯\n │ Int64 String String String ⋯\n─────┼──────────────────────────────────────────────────────────────────────────\n 1 │ 4329847 Myocardial infarction Condition SNOMED ⋯\n 2 │ 312327 Acute myocardial infarction Condition SNOMED\n 3 │ 434376 Acute myocardial infarction of a… Condition SNOMED\n 4 │ 438170 Acute myocardial infarction of i… Condition SNOMED\n 5 │ 438438 Acute myocardial infarction of a… Condition SNOMED ⋯\n 6 │ 444406 Acute subendocardial infarction Condition SNOMED\n 6 columns omitted\n=#\n\nMyocardialInfarctionOccurrence() =\n From(:condition_occurrence) |>\n Join(:concept => MyocardialInfarctionConcept(),\n on = Get.condition_concept_id .== Get.concept.concept_id) |>\n Order(Get.condition_occurrence_id)\n\nDBInterface.execute(conn, MyocardialInfarctionOccurrence()) |> DataFrame\n#=>\n11×11 DataFrame\n Row │ condition_occurrence_id person_id condition_concept_id condition_sta ⋯\n │ Int64 Int64 Int64 String ⋯\n─────┼──────────────────────────────────────────────────────────────────────────\n 1 │ 228161 1780 312327 2008-04-10 ⋯\n 2 │ 3767773 30091 444406 2009-08-02\n 3 │ 4696273 37455 438438 2010-08-12\n 4 │ 8701359 69985 444406 2010-07-22\n 5 │ 8701405 69985 312327 2010-05-06 ⋯\n 6 │ 11881327 95538 444406 2009-03-30\n 7 │ 13374905 107680 444406 2009-07-20\n 8 │ 13769162 110862 444406 2009-09-30\n 9 │ 13769189 110862 438170 2008-09-07 ⋯\n 10 │ 13769190 110862 434376 2008-09-07\n 11 │ 13769260 110862 312327 2010-06-07\n 8 columns omitted\n=#\n\nInpatientVisitConcept() =\n VISIT(\"IP\") |>\n WithSubtypes()\n\nDBInterface.execute(conn, InpatientVisitConcept()) |> DataFrame\n#=>\n2×10 DataFrame\n Row │ concept_id concept_name domain_id vocabulary_id concep ⋯\n │ Int64 String String String String ⋯\n─────┼──────────────────────────────────────────────────────────────────────────\n 1 │ 9201 Inpatient Visit Visit Visit Visit ⋯\n 2 │ 8717 Inpatient Hospital Visit CMS Place of Service Visit\n 6 columns omitted\n=#\n\nInpatientVisitOccurrence() =\n From(:visit_occurrence) |>\n Join(:concept => InpatientVisitConcept(),\n on = Get.visit_concept_id .== Get.concept.concept_id)\n\nDBInterface.execute(conn, InpatientVisitOccurrence()) |> DataFrame\n#=>\n6×12 DataFrame\n Row │ visit_occurrence_id person_id visit_concept_id visit_start_date vis ⋯\n │ Int64 Int64 Int64 String Mis ⋯\n─────┼──────────────────────────────────────────────────────────────────────────\n 1 │ 88179 1780 9201 2008-04-09 ⋯\n 2 │ 1454883 30091 9201 2009-07-30\n 3 │ 3359790 69985 9201 2010-07-22\n 4 │ 4586628 95538 9201 2009-03-30\n 5 │ 5162803 107680 9201 2009-07-20 ⋯\n 6 │ 5314664 110862 9201 2009-09-30\n 8 columns omitted\n=#","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"Using these two datasets, we need to find those conditions that occurred during one of the visits. We start with building a parameterized query that finds visits overlapping with a specified timestamp.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"using Dates\n\nCorrelatedInpatientVisit(person_id, date) =\n InpatientVisitOccurrence() |>\n Where(Fun.and(Get.person_id .== Var.PERSON_ID,\n Fun.between(Var.DATE, Get.visit_start_date, Get.visit_end_date))) |>\n Bind(:PERSON_ID => person_id,\n :DATE => date)\n\nq = CorrelatedInpatientVisit(1780, Date(\"2008-04-10\"))\n\nDBInterface.execute(conn, q) |> DataFrame\n#=>\n1×12 DataFrame\n Row │ visit_occurrence_id person_id visit_concept_id visit_start_date vis ⋯\n │ Int64 Int64 Int64 String Mis ⋯\n─────┼──────────────────────────────────────────────────────────────────────────\n 1 │ 88179 1780 9201 2008-04-09 ⋯\n 8 columns omitted\n=#","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"We will use this query to correlate inpatient visits with the date of the diagnosed condition.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"MyocardialInfarctionDuringInpatientVisit() =\n MyocardialInfarctionOccurrence() |>\n Where(Fun.exists(CorrelatedInpatientVisit(Get.person_id, Get.condition_start_date)))\n\nDBInterface.execute(conn, MyocardialInfarctionDuringInpatientVisit()) |> DataFrame\n#=>\n6×11 DataFrame\n Row │ condition_occurrence_id person_id condition_concept_id condition_sta ⋯\n │ Int64 Int64 Int64 String ⋯\n─────┼──────────────────────────────────────────────────────────────────────────\n 1 │ 228161 1780 312327 2008-04-10 ⋯\n 2 │ 3767773 30091 444406 2009-08-02\n 3 │ 8701359 69985 444406 2010-07-22\n 4 │ 11881327 95538 444406 2009-03-30\n 5 │ 13374905 107680 444406 2009-07-20 ⋯\n 6 │ 13769162 110862 444406 2009-09-30\n 8 columns omitted\n=#","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"Finally, we must exclude any events that occurred within 180 days from the previous event. For this purpose, we build a filtering pipeline:","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"using Dates\n\nFilterByGap(date, gap) =\n Partition(Get.person_id, order_by = [date]) |>\n Define(:boundary => Agg.lag(Fun.date(date, gap))) |>\n Where(Fun.or(Fun.is_null(Get.boundary),\n Get.boundary .< date))","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"To verify that this pipeline operates correctly, we could apply it to a synthetic dataset.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"events = DataFrame([(person_id = 1, date = Date(\"2020-01-01\")), # ✓\n (person_id = 1, date = Date(\"2020-02-01\")), # ✗\n (person_id = 1, date = Date(\"2021-01-01\")), # ✓\n (person_id = 1, date = Date(\"2021-05-01\")), # ✗\n (person_id = 1, date = Date(\"2021-10-01\")), # ✗\n (person_id = 2, date = Date(\"2020-01-01\")), # ✓\n])\n#=>\n6×2 DataFrame\n Row │ person_id date\n │ Int64 Date\n─────┼───────────────────────\n 1 │ 1 2020-01-01\n 2 │ 1 2020-02-01\n 3 │ 1 2021-01-01\n 4 │ 1 2021-05-01\n 5 │ 1 2021-10-01\n 6 │ 2 2020-01-01\n=#\n\nq = From(events) |>\n FilterByGap(Get.date, Day(180))\n\nDBInterface.execute(conn, q) |> DataFrame\n#=>\n3×3 DataFrame\n Row │ person_id date boundary\n │ Int64 String String?\n─────┼───────────────────────────────────\n 1 │ 1 2020-01-01 missing\n 2 │ 1 2021-01-01 2020-07-30\n 3 │ 2 2020-01-01 missing\n=#","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"Now we have all the components to construct the final query:","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"FilteredMyocardialInfarctionDuringInpatientVisit() =\n MyocardialInfarctionDuringInpatientVisit() |>\n FilterByGap(Get.condition_start_date, Day(180))\n\nq = FilteredMyocardialInfarctionDuringInpatientVisit() |>\n Select(Get.person_id, Get.condition_start_date)\n\nDBInterface.execute(conn, q) |> DataFrame\n#=>\n6×2 DataFrame\n Row │ person_id condition_start_date\n │ Int64 String\n─────┼─────────────────────────────────\n 1 │ 1780 2008-04-10\n 2 │ 30091 2009-08-02\n 3 │ 69985 2010-07-22\n 4 │ 95538 2009-03-30\n 5 │ 107680 2009-07-20\n 6 │ 110862 2009-09-30\n=#\n\nrender(conn, q) |> print\n#=>\nWITH RECURSIVE \"base_1\" (\"concept_id\") AS (\n SELECT \"concept_1\".\"concept_id\"\n FROM \"concept\" AS \"concept_1\"\n WHERE\n (\"concept_1\".\"vocabulary_id\" = 'SNOMED') AND\n (\"concept_1\".\"concept_code\" = '22298006')\n UNION ALL\n SELECT \"concept_2\".\"concept_id\"\n FROM \"base_1\" AS \"base_2\"\n JOIN (\n SELECT\n \"concept_relationship_1\".\"concept_id_1\",\n \"concept_relationship_1\".\"concept_id_2\"\n FROM \"concept_relationship\" AS \"concept_relationship_1\"\n WHERE (\"concept_relationship_1\".\"relationship_id\" = 'Is a')\n ) AS \"concept_relationship_2\" ON (\"base_2\".\"concept_id\" = \"concept_relationship_2\".\"concept_id_2\")\n JOIN \"concept\" AS \"concept_2\" ON (\"concept_relationship_2\".\"concept_id_1\" = \"concept_2\".\"concept_id\")\n),\n\"base_4\" (\"concept_id\") AS (\n SELECT \"concept_3\".\"concept_id\"\n FROM \"concept\" AS \"concept_3\"\n WHERE\n (\"concept_3\".\"vocabulary_id\" = 'Visit') AND\n (\"concept_3\".\"concept_code\" = 'IP')\n UNION ALL\n SELECT \"concept_4\".\"concept_id\"\n FROM \"base_4\" AS \"base_5\"\n JOIN (\n SELECT\n \"concept_relationship_3\".\"concept_id_1\",\n \"concept_relationship_3\".\"concept_id_2\"\n FROM \"concept_relationship\" AS \"concept_relationship_3\"\n WHERE (\"concept_relationship_3\".\"relationship_id\" = 'Is a')\n ) AS \"concept_relationship_4\" ON (\"base_5\".\"concept_id\" = \"concept_relationship_4\".\"concept_id_2\")\n JOIN \"concept\" AS \"concept_4\" ON (\"concept_relationship_4\".\"concept_id_1\" = \"concept_4\".\"concept_id\")\n)\nSELECT\n \"condition_occurrence_3\".\"person_id\",\n \"condition_occurrence_3\".\"condition_start_date\"\nFROM (\n SELECT\n \"condition_occurrence_2\".\"person_id\",\n \"condition_occurrence_2\".\"condition_start_date\",\n (lag(date(\"condition_occurrence_2\".\"condition_start_date\", '180 days')) OVER (PARTITION BY \"condition_occurrence_2\".\"person_id\" ORDER BY \"condition_occurrence_2\".\"condition_start_date\")) AS \"boundary\"\n FROM (\n SELECT\n \"condition_occurrence_1\".\"person_id\",\n \"condition_occurrence_1\".\"condition_start_date\"\n FROM \"condition_occurrence\" AS \"condition_occurrence_1\"\n JOIN \"base_1\" AS \"base_3\" ON (\"condition_occurrence_1\".\"condition_concept_id\" = \"base_3\".\"concept_id\")\n ORDER BY \"condition_occurrence_1\".\"condition_occurrence_id\"\n ) AS \"condition_occurrence_2\"\n WHERE (EXISTS (\n SELECT NULL AS \"_\"\n FROM \"visit_occurrence\" AS \"visit_occurrence_1\"\n JOIN \"base_4\" AS \"base_6\" ON (\"visit_occurrence_1\".\"visit_concept_id\" = \"base_6\".\"concept_id\")\n WHERE\n (\"visit_occurrence_1\".\"person_id\" = \"condition_occurrence_2\".\"person_id\") AND\n (\"condition_occurrence_2\".\"condition_start_date\" BETWEEN \"visit_occurrence_1\".\"visit_start_date\" AND \"visit_occurrence_1\".\"visit_end_date\")\n ))\n) AS \"condition_occurrence_3\"\nWHERE\n (\"condition_occurrence_3\".\"boundary\" IS NULL) OR\n (\"condition_occurrence_3\".\"boundary\" < \"condition_occurrence_3\".\"condition_start_date\")\n=#","category":"page"},{"location":"examples/#Merging-overlapping-intervals","page":"Examples","title":"Merging overlapping intervals","text":"","category":"section"},{"location":"examples/","page":"Examples","title":"Examples","text":"Merging overlapping intervals into a single encompassing period could be done in three steps:","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"Tag the intervals that start a new period.\nEnumerate the periods.\nGroup the intervals by the period number.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"FunSQL lets us encapsulate and reuse this rather complex sequence of transformations.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"Merge overlapping visits.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"MergeOverlappingIntervals(start_date, end_date) =\n Partition(Get.person_id,\n order_by = [start_date],\n frame = (mode = :rows, start = -Inf, finish = -1)) |>\n Define(:new => Fun.case(start_date .<= Agg.max(end_date), 0, 1)) |>\n Partition(Get.person_id,\n order_by = [start_date, .- Get.new],\n frame = :rows) |>\n Define(:period => Agg.sum(Get.new)) |>\n Group(Get.person_id, Get.period) |>\n Define(:start_date => Agg.min(start_date),\n :end_date => Agg.max(end_date))\n\nq = From(:visit_occurrence) |>\n MergeOverlappingIntervals(Get.visit_start_date, Get.visit_end_date) |>\n Select(Get.person_id, Get.start_date, Get.end_date)\n\nrender(conn, q) |> print\n#=>\nSELECT\n \"visit_occurrence_3\".\"person_id\",\n min(\"visit_occurrence_3\".\"visit_start_date\") AS \"start_date\",\n max(\"visit_occurrence_3\".\"visit_end_date\") AS \"end_date\"\nFROM (\n SELECT\n \"visit_occurrence_2\".\"person_id\",\n (sum(\"visit_occurrence_2\".\"new\") OVER (PARTITION BY \"visit_occurrence_2\".\"person_id\" ORDER BY \"visit_occurrence_2\".\"visit_start_date\", (- \"visit_occurrence_2\".\"new\") ROWS UNBOUNDED PRECEDING)) AS \"period\",\n \"visit_occurrence_2\".\"visit_start_date\",\n \"visit_occurrence_2\".\"visit_end_date\"\n FROM (\n SELECT\n \"visit_occurrence_1\".\"person_id\",\n \"visit_occurrence_1\".\"visit_start_date\",\n \"visit_occurrence_1\".\"visit_end_date\",\n (CASE WHEN (\"visit_occurrence_1\".\"visit_start_date\" <= (max(\"visit_occurrence_1\".\"visit_end_date\") OVER (PARTITION BY \"visit_occurrence_1\".\"person_id\" ORDER BY \"visit_occurrence_1\".\"visit_start_date\" ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING))) THEN 0 ELSE 1 END) AS \"new\"\n FROM \"visit_occurrence\" AS \"visit_occurrence_1\"\n ) AS \"visit_occurrence_2\"\n) AS \"visit_occurrence_3\"\nGROUP BY\n \"visit_occurrence_3\".\"person_id\",\n \"visit_occurrence_3\".\"period\"\n=#\n\nDBInterface.execute(conn, q) |> DataFrame\n#=>\n25×3 DataFrame\n Row │ person_id start_date end_date\n │ Int64 String String\n─────┼───────────────────────────────────\n 1 │ 1780 2008-04-09 2008-04-13\n 2 │ 1780 2008-11-22 2008-11-22\n 3 │ 1780 2009-05-22 2009-05-22\n 4 │ 30091 2008-11-12 2008-11-12\n 5 │ 30091 2009-07-30 2009-08-07\n 6 │ 37455 2008-03-18 2008-03-18\n 7 │ 37455 2008-10-30 2008-10-30\n 8 │ 37455 2010-08-12 2010-08-12\n ⋮ │ ⋮ ⋮ ⋮\n 19 │ 95538 2009-09-02 2009-09-02\n 20 │ 107680 2009-06-07 2009-06-07\n 21 │ 107680 2009-07-20 2009-07-30\n 22 │ 110862 2008-09-07 2008-09-16\n 23 │ 110862 2009-06-30 2009-06-30\n 24 │ 110862 2009-09-30 2009-10-01\n 25 │ 110862 2010-06-07 2010-06-07\n 10 rows omitted\n=#","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"Derive a patient's observation periods by merging visits with less than one year gap between them.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"MergeIntervalsByGap(start_date, end_date, gap) =\n MergeOverlappingIntervals(start_date, Fun.date(end_date, gap)) |>\n Define(:end_date => Fun.date(Get.end_date, -gap))\n\nq = From(:visit_occurrence) |>\n MergeIntervalsByGap(Get.visit_start_date, Get.visit_end_date, Day(365)) |>\n Select(Get.person_id, Get.start_date, Get.end_date)\n\nrender(conn, q) |> print\n#=>\nSELECT\n \"visit_occurrence_3\".\"person_id\",\n min(\"visit_occurrence_3\".\"visit_start_date\") AS \"start_date\",\n date(max(date(\"visit_occurrence_3\".\"visit_end_date\", '365 days')), '-365 days') AS \"end_date\"\nFROM (\n SELECT\n \"visit_occurrence_2\".\"person_id\",\n (sum(\"visit_occurrence_2\".\"new\") OVER (PARTITION BY \"visit_occurrence_2\".\"person_id\" ORDER BY \"visit_occurrence_2\".\"visit_start_date\", (- \"visit_occurrence_2\".\"new\") ROWS UNBOUNDED PRECEDING)) AS \"period\",\n \"visit_occurrence_2\".\"visit_start_date\",\n \"visit_occurrence_2\".\"visit_end_date\"\n FROM (\n SELECT\n \"visit_occurrence_1\".\"person_id\",\n \"visit_occurrence_1\".\"visit_start_date\",\n \"visit_occurrence_1\".\"visit_end_date\",\n (CASE WHEN (\"visit_occurrence_1\".\"visit_start_date\" <= (max(date(\"visit_occurrence_1\".\"visit_end_date\", '365 days')) OVER (PARTITION BY \"visit_occurrence_1\".\"person_id\" ORDER BY \"visit_occurrence_1\".\"visit_start_date\" ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING))) THEN 0 ELSE 1 END) AS \"new\"\n FROM \"visit_occurrence\" AS \"visit_occurrence_1\"\n ) AS \"visit_occurrence_2\"\n) AS \"visit_occurrence_3\"\nGROUP BY\n \"visit_occurrence_3\".\"person_id\",\n \"visit_occurrence_3\".\"period\"\n=#\n\nDBInterface.execute(conn, q) |> DataFrame\n#=>\n12×3 DataFrame\n Row │ person_id start_date end_date\n │ Int64 String String\n─────┼───────────────────────────────────\n 1 │ 1780 2008-04-09 2009-05-22\n 2 │ 30091 2008-11-12 2009-08-07\n 3 │ 37455 2008-03-18 2008-10-30\n 4 │ 37455 2010-08-12 2010-08-12\n 5 │ 42383 2009-06-29 2010-04-15\n 6 │ 69985 2009-01-09 2009-01-09\n 7 │ 69985 2010-04-17 2010-07-30\n 8 │ 72120 2008-12-15 2008-12-15\n 9 │ 82328 2008-10-20 2009-01-25\n 10 │ 95538 2009-03-30 2009-09-02\n 11 │ 107680 2009-06-07 2009-07-30\n 12 │ 110862 2008-09-07 2010-06-07\n=#","category":"page"},{"location":"guide/#Usage-Guide","page":"Usage Guide","title":"Usage Guide","text":"","category":"section"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"CurrentModule = FunSQL","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"This guide will teach you how to assemble SQL queries using FunSQL.","category":"page"},{"location":"guide/#Test-Database","page":"Usage Guide","title":"Test Database","text":"","category":"section"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"To demonstrate database queries, we need a test database. The database we use in this guide is a tiny 10 person sample of simulated patient data extracted from a much larger CMS DE-SynPuf dataset. For a database engine, we picked SQLite. Using SQLite in a guide is convenient because it does not require a database server to run and allows us to distribute the whole database as a single file. FunSQL supports SQLite and many other database engines. The techniques discussed here are not specific to SQLite and once you learn them, you will be able to apply them to any SQL database.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"The data in the test database is stored in the format of the OMOP Common Data Model, an open source database schema for observational healthcare data. In this guide, we will only use a small fragment of the Common Data Model.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"(Image: Fragment of the OMOP Common Data Model)","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"The patient data, including basic demographic information, is stored in the table person. Patient addresses are stored in a separate table location, linked to person by the key column location_id.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"The bulk of patient data consists of clinical events: visits to healthcare providers, recorded observations, diagnosed conditions, prescribed medications, etc. In this guide we only use two types of events, visits and conditions.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"The specific type of the event (e.g., Inpatient visit or Essential hypertension condition) is indicated using a concept id column, which refers to the concept table. Different concepts may be related to each other. For instance, Essential hypertension is a Hypertensive disorder, which itself is a Disorder of cardiovascular system. Concept relationships are recorded in the corresponding table.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"If you wish to follow along with the guide and run the examples, download the database file:","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"DATABASE = download(\"https://github.com/MechanicalRabbit/ohdsi-synpuf-demo/releases/download/20210412/synpuf-10p.sqlite\")","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"All examples in this guide are tested on each update using the NarrativeTest package. To avoid downloading the database file all the time, we registered the download URL as an artifact and use Pkg.Artifacts API to fetch it:","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"using Pkg.Artifacts, LazyArtifacts\n\nDATABASE = joinpath(artifact\"synpuf-10p\", \"synpuf-10p.sqlite\")\n#-> ⋮","category":"page"},{"location":"guide/#Using-FunSQL","page":"Usage Guide","title":"Using FunSQL","text":"","category":"section"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"To interact with an SQLite database from Julia code, we need to install the SQLite package:","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"using Pkg\n\nPkg.add(\"SQLite\")","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"With the package installed, we can open a database connection:","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"using FunSQL\nusing SQLite\n\nconn = DBInterface.connect(FunSQL.DB{SQLite.DB}, DATABASE)","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"This call to DBInterface.connect creates a connection to the SQLite database, retrieves the catalog of available database tables, and returns a FunSQL connection object.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Some applications open many connections to the same database. For instance, a web application may open a new database connection on every incoming HTTP request. In this case, it may be worth to have all these connections to share the same database catalog. The application can start with loading the catalog using using reflect:","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"using FunSQL: reflect\n\ncatalog = reflect(DBInterface.connect(SQLite.DB, DATABASE))","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Then whenever a new connection is created, this catalog object could be reused:","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"conn = FunSQL.DB(DBInterface.connect(SQLite.DB, DATABASE),\n catalog = catalog)","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"warning: Warning\nSome database drivers, including the PostgreSQL client library LibPQ.jl, do not support DBInterface. For instructions on how to enable DBInterface for LibPQ, see this example.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Using the connection object, we can execute FunSQL queries. For example, the following query outputs the content of the table person:","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"using FunSQL: From\n\nq = From(:person)","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"This query could be executed with DBInterface.execute:","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"res = DBInterface.execute(conn, q)","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"To display the result of a query, it is convenient to convert it to a DataFrame object:","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"using DataFrames\n\nDataFrame(res)\n#=>\n10×18 DataFrame\n Row │ person_id gender_concept_id year_of_birth month_of_birth day_of_bir ⋯\n │ Int64 Int64 Int64 Int64 Int64 ⋯\n─────┼──────────────────────────────────────────────────────────────────────────\n 1 │ 1780 8532 1940 2 ⋯\n 2 │ 30091 8532 1932 8\n 3 │ 37455 8532 1913 7\n 4 │ 42383 8507 1922 2\n 5 │ 69985 8532 1956 7 ⋯\n 6 │ 72120 8507 1937 10\n 7 │ 82328 8532 1957 9\n 8 │ 95538 8507 1923 11\n 9 │ 107680 8532 1963 12 ⋯\n 10 │ 110862 8507 1911 4\n 14 columns omitted\n=#","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Instead of executing the query directly, we can render it to generate the corresponding SQL statement:","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"using FunSQL: render\n\nsql = render(conn, q)\n\nprint(sql)\n#=>\nSELECT\n \"person_1\".\"person_id\",\n \"person_1\".\"gender_concept_id\",\n \"person_1\".\"year_of_birth\",\n \"person_1\".\"month_of_birth\",\n \"person_1\".\"day_of_birth\",\n \"person_1\".\"time_of_birth\",\n \"person_1\".\"race_concept_id\",\n \"person_1\".\"ethnicity_concept_id\",\n \"person_1\".\"location_id\",\n \"person_1\".\"provider_id\",\n \"person_1\".\"care_site_id\",\n \"person_1\".\"person_source_value\",\n \"person_1\".\"gender_source_value\",\n \"person_1\".\"gender_source_concept_id\",\n \"person_1\".\"race_source_value\",\n \"person_1\".\"race_source_concept_id\",\n \"person_1\".\"ethnicity_source_value\",\n \"person_1\".\"ethnicity_source_concept_id\"\nFROM \"person\" AS \"person_1\"\n=#","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"In fact, we do not need a database connection if all we want is to generate a SQL query. For this purpose, we only need a SQLCatalog object that describes the structure of the database tables and the target SQL dialect:","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"using FunSQL: SQLCatalog, SQLTable\n\ncatalog = SQLCatalog(SQLTable(:person, columns = [:person_id, :year_of_birth]),\n dialect = :sqlite)\n\nsql = render(catalog, q)\n\nprint(sql)\n#=>\nSELECT\n \"person_1\".\"person_id\",\n \"person_1\".\"year_of_birth\"\nFROM \"person\" AS \"person_1\"\n=#","category":"page"},{"location":"guide/#Why-FunSQL?","page":"Usage Guide","title":"Why FunSQL?","text":"","category":"section"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Let us clarify the purpose of FunSQL. Consider a problem:","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Find all patients born between 1930 and 1940 and living in Illinois, and for each patient show their current age (by the end of 2020).","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"The answer can be obtained with the following SQL query:","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"SELECT p.person_id, 2020 - p.year_of_birth AS age\nFROM person p\nJOIN location l ON (p.location_id = l.location_id)\nWHERE (p.year_of_birth BETWEEN 1930 AND 1940) AND (l.state = 'IL')","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"The simplest way to incorporate this query into Julia code is to embed it as a string literal:","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"sql = \"\"\"\nSELECT p.person_id, 2020 - p.year_of_birth AS age\nFROM person p\nJOIN location l ON (p.location_id = l.location_id)\nWHERE (p.year_of_birth BETWEEN 1930 AND 1940) AND (l.state = 'IL')\n\"\"\"\n\nDBInterface.execute(conn, sql) |> DataFrame\n#=>\n1×2 DataFrame\n Row │ person_id age\n │ Int64 Int64\n─────┼──────────────────\n 1 │ 72120 83\n=#","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"With FunSQL, instead of embedding the SQL query directly into Julia code, we construct a query object:","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"using FunSQL: As, From, Fun, Get, Join, Select, Where\n\nq = From(:person) |>\n Where(Fun.between(Get.year_of_birth, 1930, 1940)) |>\n Join(From(:location) |> Where(Get.state .== \"IL\") |> As(:location),\n on = Get.location_id .== Get.location.location_id) |>\n Select(Get.person_id, :age => 2020 .- Get.year_of_birth)","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"The value of q is a composite object of type SQLNode. \"Composite\" means that q is assembled from components (also of type SQLNode), which themselves are either atomic or assembled from smaller components. Different kinds of components are created by SQLNode constructors such as From, Where, Fun, Get, etc.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"We use the same DBInterface.execute method to serialize the query object as a SQL statement and immediately execute it:","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"DBInterface.execute(conn, q) |> DataFrame\n#=>\n1×2 DataFrame\n Row │ person_id age\n │ Int64 Int64\n─────┼──────────────────\n 1 │ 72120 83\n=#","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Why, instead of embedding a complete SQL query, we prefer to generate it through a query object? To justify this extra step, consider that in a real Julia program, any query is likely going to be parameterized:","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Find all patients born between $start_year and $end_year and living in $states, and for each patient show the $output_columns.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"If this is the case, the SQL query cannot be prepared in advance and must be assembled on the fly. While it is possible to assemble a SQL query from string fragments, it is tedious, error-prone and definitely not fun. FunSQL provides a more robust and effective approach: build the query as a composite data structure.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Here is how this parameterized query may be constructed with FunSQL:","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"function FindPatients(; start_year = nothing,\n end_year = nothing,\n states = String[])\n q = From(:person) |>\n Where(BirthRange(start_year, end_year))\n if !isempty(states)\n q = q |>\n Join(:location => From(:location) |>\n Where(Fun.in(Get.state, states...)),\n on = Get.location_id .== Get.location.location_id)\n end\n q\nend\n\nfunction BirthRange(start_year, end_year)\n p = true\n if start_year !== nothing\n p = Fun.and(p, Get.year_of_birth .>= start_year)\n end\n if end_year !== nothing\n p = Fun.and(p, Get.year_of_birth .<= end_year)\n end\n p\nend","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"The function FindPatients effectively becomes a new SQLNode constructor, which can be used directly or as a component of a larger query.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Show all patient data.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"q = FindPatients()\n\nDBInterface.execute(conn, q) |> DataFrame\n#=>\n10×18 DataFrame\n Row │ person_id gender_concept_id year_of_birth month_of_birth day_of_bir ⋯\n │ Int64 Int64 Int64 Int64 Int64 ⋯\n─────┼──────────────────────────────────────────────────────────────────────────\n 1 │ 1780 8532 1940 2 ⋯\n 2 │ 30091 8532 1932 8\n 3 │ 37455 8532 1913 7\n 4 │ 42383 8507 1922 2\n 5 │ 69985 8532 1956 7 ⋯\n 6 │ 72120 8507 1937 10\n 7 │ 82328 8532 1957 9\n 8 │ 95538 8507 1923 11\n 9 │ 107680 8532 1963 12 ⋯\n 10 │ 110862 8507 1911 4\n 14 columns omitted\n=#","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Show all patients born in or after 1930.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"q = FindPatients(start_year = 1930) |>\n Select(Get.person_id)\n\nDBInterface.execute(conn, q) |> DataFrame\n#=>\n6×1 DataFrame\n Row │ person_id\n │ Int64\n─────┼───────────\n 1 │ 1780\n 2 │ 30091\n 3 │ 69985\n 4 │ 72120\n 5 │ 82328\n 6 │ 107680\n=#","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Find all patients born between 1930 and 1940 and living in Illinois, and for each patient show their current age.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"q = FindPatients(start_year = 1930, end_year = 1940, states = [\"IL\"]) |>\n Select(Get.person_id, :age => 2020 .- Get.year_of_birth)\n\nDBInterface.execute(conn, q) |> DataFrame\n#=>\n1×2 DataFrame\n Row │ person_id age\n │ Int64 Int64\n─────┼──────────────────\n 1 │ 72120 83\n=#","category":"page"},{"location":"guide/#Tabular-Operations","page":"Usage Guide","title":"Tabular Operations","text":"","category":"section"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Recall the query from the previous section:","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Find all patients born between 1930 and 1940 and living in Illinois, and for each patient show their current age.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"From(:person) |>\nWhere(Fun.between(Get.year_of_birth, 1930, 1940)) |>\nJoin(From(:location) |> Where(Get.state .== \"IL\") |> As(:location),\n on = Get.location_id .== Get.location.location_id) |>\nSelect(Get.person_id, :age => 2020 .- Get.year_of_birth)","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"At the outer level, this query is constructed from tabular operations From, Where, Join, and Select arranged in a pipeline by the pipe (|>) operator. In SQL, a tabular operation takes a certain number of input datasets and produces an output dataset. It is helpful to visualize a tabular operation as a node with a certain number of input arrows and one output arrow.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"(Image: From, Where, Select, and Join nodes)","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Then the whole query can be visualized as a pipeline diagram. Each arrow in this diagram represents a dataset, and each node represents an elementary data processing operation.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"(Image: Query pipeline)","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"The following tabular operations are available in FunSQL.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Constructor Function\nAppend concatenate datasets\nAs wrap all columns in a nested record\nDefine add an output column\nFrom produce the content of a database table\nGroup partition the dataset into disjoint groups\nIterate iterate a query\nJoin correlate two datasets\nLimit truncate the dataset\nOrder sort the dataset\nPartition relate dataset rows to each other\nSelect specify output columns\nWhere filter the dataset by the given condition\nWith assign a name to a temporary dataset","category":"page"},{"location":"guide/#From,-Select,-and-Define","page":"Usage Guide","title":"From, Select, and Define","text":"","category":"section"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"The From node outputs the content of a database table. The constructor takes one argument, the name of the table.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"As opposed to SQL, FunSQL does not demand that all queries have an explicit Select. The following query will produce all columns of the table:","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Show all patients.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"using FunSQL: From\n\nq = From(:person)\n\nrender(conn, q) |> print\n#=>\nSELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"ethnicity_source_concept_id\"\nFROM \"person\" AS \"person_1\"\n=#","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"The From node also accepts a DataFrame or any argument supporting the Tables.jl interface, which is very convenient when you need to correlate database content with external data. Keep in mind that From serializes a DataFrame argument as a part of the query, so for a large DataFrame it is better to load it into the database and query it as a regular table.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"df = DataFrame(person_id = [\"SQL\", \"Julia\", \"FunSQL\"],\n year_of_birth = [1974, 2012, 2021])\n\nq = From(df)\n\nrender(conn, q) |> print\n#=>\nSELECT\n \"values_1\".\"column1\" AS \"person_id\",\n \"values_1\".\"column2\" AS \"year_of_birth\"\nFROM (\n VALUES\n ('SQL', 1974),\n ('Julia', 2012),\n ('FunSQL', 2021)\n) AS \"values_1\"\n=#","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"It is possible for a query not to have a From node:","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Show the current date and time.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"using FunSQL: Select\n\nq = Select(Fun.current_timestamp())\n\nsql = render(q)\n\nprint(sql)\n#-> SELECT CURRENT_TIMESTAMP AS \"current_timestamp\"","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"In this query, the Select node is not connected to any source of data. In such a case, it is supplied with a unit dataset containing one row and no columns. Hence this query will generate one row of output.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"The same effect could be achieved with From(nothing):","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"q = From(nothing) |>\n Select(Fun.current_timestamp())\n\nsql = render(q)\n\nprint(sql)\n#-> SELECT CURRENT_TIMESTAMP AS \"current_timestamp\"","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"The Select node is used to specify the output columns. The name of the column is either derived from the expression or set explicitly with As (or the shorthand =>).","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"For each patient, show their ID and the current age.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"q = From(:person) |>\n Select(Get.person_id,\n :age => 2020 .- Get.year_of_birth)\n\nrender(conn, q) |> print\n#=>\nSELECT\n \"person_1\".\"person_id\",\n (2020 - \"person_1\".\"year_of_birth\") AS \"age\"\nFROM \"person\" AS \"person_1\"\n=#","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"To add a new column while preserving existing output columns, we use the Define node.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Show the patient data together with their current age.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"using FunSQL: Define\n\nq = From(:person) |>\n Define(:age => 2020 .- Get.year_of_birth)\n\nrender(conn, q) |> print\n#=>\nSELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"ethnicity_source_concept_id\",\n (2020 - \"person_1\".\"year_of_birth\") AS \"age\"\nFROM \"person\" AS \"person_1\"\n=#","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Define could also be used to replace an existing column.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Hide the day of birth of patients born before 1930.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"q = From(:person) |>\n Define(:day_of_birth => Fun.case(Get.year_of_birth .>= 1930,\n Get.day_of_birth,\n missing))\n\nrender(conn, q) |> print\n#=>\nSELECT\n \"person_1\".\"person_id\",\n \"person_1\".\"gender_concept_id\",\n \"person_1\".\"year_of_birth\",\n \"person_1\".\"month_of_birth\",\n (CASE WHEN (\"person_1\".\"year_of_birth\" >= 1930) THEN \"person_1\".\"day_of_birth\" ELSE NULL END) AS \"day_of_birth\",\n ⋮\n \"person_1\".\"ethnicity_source_concept_id\"\nFROM \"person\" AS \"person_1\"\n=#","category":"page"},{"location":"guide/#Join","page":"Usage Guide","title":"Join","text":"","category":"section"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"The Join node correlates the rows of two input datasets. Predominantly, Join is used for looking up table records by key. In the following example, Join associates each person record with their location using the key column location_id that uniquely identifies a location record.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Show all patients together with their state of residence.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"From(:person) |>\nJoin(:location => From(:location),\n Get.location_id .== Get.location.location_id,\n left = true) |>\nSelect(Get.person_id, Get.location.state)","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"The modifier left = true tells Join that it must output all person records including those without the corresponding location. Since this is a very common requirement, FunSQL provides an alias:","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"using FunSQL: LeftJoin\n\nFrom(:person) |>\nLeftJoin(:location => From(:location),\n Get.location_id .== Get.location.location_id) |>\nSelect(Get.person_id, Get.location.state)","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Since Join needs two input datasets, it must be attached to two input pipelines. The first pipeline is attached using the |> operator and the second one is provided as an argument to the Join constructor. Alternatively, both input pipelines can be specified as keyword arguments:","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Join(over = From(:person),\n joinee = :location => From(:location),\n on = Get.location_id .== Get.location.location_id,\n left = true) |>\nSelect(Get.person_id, Get.location.state)","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"The output of Join combines columns of both input datasets, which will cause ambiguity if both datasets have a column with the same name. Such is the case in the previous example since both tables, person and location, have a column called location_id. To disambiguate them, we can place all columns of one of the datasets into a nested record. This is the action of the arrow (=>) operator or its full form, the As node:","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"using FunSQL: As\n\nFrom(:person) |>\nLeftJoin(From(:location) |> As(:location),\n on = Get.location_id .== Get.location.location_id) |>\nSelect(Get.person_id, Get.location.state)","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Alternatively, we could use bound column references, which are described in a later section.","category":"page"},{"location":"guide/#Scalar-Operations","page":"Usage Guide","title":"Scalar Operations","text":"","category":"section"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Many tabular operations including Join, Select and Where are parameterized with scalar operations. A scalar operation acts on an individual row of a dataset and produces a scalar value. Scalar operations are assembled from literal values, column references, and applications of SQL functions and operators. Below is a list of scalar operations available in FunSQL.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Constructor Function\nAgg apply an aggregate function\nAs assign a column alias\nBind create a correlated subquery\nFun apply a scalar function or a scalar operator\nGet produce the value of a column\nLit produce a constant value\nSort indicate the sort order\nVar produce the value of a query parameter","category":"page"},{"location":"guide/#Lit:-SQL-Literals","page":"Usage Guide","title":"Lit: SQL Literals","text":"","category":"section"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"The Lit node creates a literal value, although we could usually omit the constructor:","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"using FunSQL: Lit\n\nSelect(Lit(42))\nSelect(42)","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"The SQL value NULL is represented by the Julia constant missing:","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"q = Select(missing)\n\nrender(conn, q) |> print\n#-> SELECT NULL AS \"_\"","category":"page"},{"location":"guide/#Get:-Column-References","page":"Usage Guide","title":"Get: Column References","text":"","category":"section"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"The Get node creates a column reference. The Get constructor admits several equivalent forms:","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Get.year_of_birth\nGet(:year_of_birth)\nGet.\"year_of_birth\"\nGet(\"year_of_birth\")","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Such column references are resolved at the place of use against the input dataset. As we mentioned earlier, sometimes column references cannot be resolved unambiguously. To alleviate this problem, we can bind the column reference to the node that produces it:","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Show all patients with their state of residence.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"qₚ = From(:person)\nqₗ = From(:location)\nq = qₚ |>\n LeftJoin(qₗ, on = qₚ.location_id .== qₗ.location_id) |>\n Select(qₚ.person_id, qₗ.state)","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"The notation qₚ.location_id and qₗ.location_id is a syntax sugar for","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Get(:location_id, over = qₚ)\nGet(:location_id, over = qₗ)","category":"page"},{"location":"guide/#Fun:-SQL-Functions-and-Operators","page":"Usage Guide","title":"Fun: SQL Functions and Operators","text":"","category":"section"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"SQL functions and operators are represented using the Fun node, which also has several equivalent forms:","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Fun.between(Get.year_of_birth, 1930, 1940)\nFun(:between, Get.year_of_birth, 1930, 1940)\nFun.\"between\"(Get.year_of_birth, 1930, 1940)\nFun(\"between\", Get.year_of_birth, 1930, 1940)","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Certain SQL operators, notably logical and comparison operators, can be represented using Julia broadcasting notation:","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Fun.\">=\"(Get.year_of_birth, 1930)\nGet.year_of_birth .>= 1930\n\nFun.and(Fun.\"=\"(Get.city, \"CHICAGO\"), Fun.\"=\"(Get.state, \"IL\"))\n#? VERSION >= v\"1.7\"\nGet.city .== \"CHICAGO\" .&& Get.state .== \"IL\"","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"We should note that FunSQL does not verify if a SQL function or an operator is used correctly or even whether it exists or not. In such a case, FunSQL will generate a SQL query that fails to execute:","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"q = From(:person) |>\n Select(Fun.frobnicate(Get.year_of_birth))\n\nrender(conn, q) |> print\n#=>\nSELECT frobnicate(\"person_1\".\"year_of_birth\") AS \"frobnicate\"\nFROM \"person\" AS \"person_1\"\n=#\n\nDBInterface.execute(conn, q)\n#-> ERROR: SQLite.SQLiteException(\"no such function: frobnicate\")","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"On the other hand, FunSQL will correctly serialize many SQL functions and operators that have irregular syntax including AND, OR, NOT, IN, EXISTS, CASE, and others.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Show the demographic cohort of each patient.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"q = From(:person) |>\n Select(Fun.case(Get.year_of_birth .<= 1960, \"boomer\", \"millenial\"))\n\nrender(conn, q) |> print\n#=>\nSELECT (CASE WHEN (\"person_1\".\"year_of_birth\" <= 1960) THEN 'boomer' ELSE 'millenial' END) AS \"case\"\nFROM \"person\" AS \"person_1\"\n=#","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Since FunSQL recognizes || as a logical or operator, it conflicts with those SQL dialects that use || for string concatenation. Other SQL dialects use function concat for this purpose. In FunSQL, always use Fun.concat, which will pick correct serialization depending on the target SQL dialect.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Show city, state.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"q = From(:location) |>\n Select(Fun.concat(Get.city, \", \", Get.state))\n\nrender(conn, q) |> print\n#=>\nSELECT (\"location_1\".\"city\" || ', ' || \"location_1\".\"state\") AS \"concat\"\nFROM \"location\" AS \"location_1\"\n=#","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"When the name of the Fun node contains one or more ? symbols, this name serves as a template of a SQL expression. When the node is rendered, the ? symbols are substituted with the node arguments.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"q = From(:person) |>\n Select(Fun.\"CAST(? AS TEXT)\"(Get.year_of_birth))\n\nrender(conn, q) |> print\n#=>\nSELECT CAST(\"person_1\".\"year_of_birth\" AS TEXT) AS \"_\"\nFROM \"person\" AS \"person_1\"\n=#","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"To decide how to render a Fun node, FunSQL checks the node name:","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"If the name has a specialized implementation of FunSQL.serialize!(), this implementation is used for rendering the node.\nIf the name contains one or more placeholders (?), the node is rendered as a template.\nIf the name contains only symbol characters, or if the name starts or ends with a space, the node is rendered as an operator.\nOtherwise, the node is rendered as a function.","category":"page"},{"location":"guide/#Group-and-Aggregate-Functions","page":"Usage Guide","title":"Group and Aggregate Functions","text":"","category":"section"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Group and aggregate functions are used for summarizing data to report totals, averages and so on. We start by applying the Group node to partition the input rows into disjoint groups. Then, for each group, we can calculate summary values using aggregate functions. In FunSQL, aggregate functions are created using the Agg node. In the following example, we use the aggregate function Agg.count, which simply counts the number of rows in each group.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Show the number of patients by the year of birth.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"using FunSQL: Agg, Group\n\nq = From(:person) |>\n Group(Get.year_of_birth) |>\n Select(Get.year_of_birth, Agg.count())\n\nrender(conn, q) |> print\n#=>\nSELECT\n \"person_1\".\"year_of_birth\",\n count(*) AS \"count\"\nFROM \"person\" AS \"person_1\"\nGROUP BY \"person_1\".\"year_of_birth\"\n=#\n\nDBInterface.execute(conn, q) |> DataFrame\n#=>\n10×2 DataFrame\n Row │ year_of_birth count\n │ Int64 Int64\n─────┼──────────────────────\n 1 │ 1911 1\n 2 │ 1913 1\n 3 │ 1922 1\n⋮\n=#","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"To indicate that aggregate functions must be applied to the dataset as a whole, we create a Group node without arguments. This is the case where FunSQL notation deviates from SQL, where we would omit the GROUP BY clause to achieve the same effect.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Show the average year of birth.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"q = From(:person) |>\n Group() |>\n Select(Agg.avg(Get.year_of_birth))\n\nrender(conn, q) |> print\n#=>\nSELECT avg(\"person_1\".\"year_of_birth\") AS \"avg\"\nFROM \"person\" AS \"person_1\"\n=#\n\nDBInterface.execute(conn, q) |> DataFrame\n#=>\n1×1 DataFrame\n Row │ avg\n │ Float64\n─────┼─────────\n 1 │ 1935.4\n=#","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"In general, the arguments of the Group node form the grouping key so that two rows of the input dataset belongs to the same group when they have the same value of the grouping key. The output of Group contains all distinct values of the grouping key.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Show the US states that are present in the location records.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"q = From(:location) |>\n Group(Get.state)\n\nrender(conn, q) |> print\n#=>\nSELECT DISTINCT \"location_1\".\"state\"\nFROM \"location\" AS \"location_1\"\n=#\n\nDBInterface.execute(conn, q) |> DataFrame\n#=>\n10×1 DataFrame\n Row │ state\n │ String\n─────┼────────\n 1 │ MI\n 2 │ WA\n 3 │ FL\n⋮\n=#","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"FunSQL has no lexical limitations on the use of aggregate functions. While in SQL aggregate functions can only be used in the SELECT or HAVING clauses, there is no such restriction in FunSQL: they could be used in any context where an ordinary expression is permitted. The only requirement is that for each aggregate function, FunSQL can determine the corresponding Group node. It is convenient to imagine that the output of Group contains the grouped rows, which cannot be observed directly, but whose presence in the output allows us to apply aggregate functions.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"In particular, we use a regular Where node where SQL would require a HAVING clause.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Show patients who saw a doctor within the last year.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"q = From(:visit_occurrence) |>\n Group(Get.person_id) |>\n Where(Agg.max(Get.visit_end_date) .>= Fun.date(\"now\", \"-1 year\"))\n\nrender(conn, q) |> print\n#=>\nSELECT \"visit_occurrence_1\".\"person_id\"\nFROM \"visit_occurrence\" AS \"visit_occurrence_1\"\nGROUP BY \"visit_occurrence_1\".\"person_id\"\nHAVING (max(\"visit_occurrence_1\".\"visit_end_date\") >= date('now', '-1 year'))\n=#","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"When the output of Group is blocked by an As node, we need to traverse it with Get in order to use an aggregate function.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"For each patient, show the date of their latest visit to a doctor.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"q = From(:person) |>\n LeftJoin(:visit_group => From(:visit_occurrence) |> Group(Get.person_id),\n on = Get.person_id .== Get.visit_group.person_id) |>\n Select(Get.person_id,\n Get.visit_group |> Agg.max(Get.visit_start_date))\n\nrender(conn, q) |> print\n#=>\nSELECT\n \"person_1\".\"person_id\",\n \"visit_group_1\".\"max\"\nFROM \"person\" AS \"person_1\"\nLEFT JOIN (\n SELECT\n max(\"visit_occurrence_1\".\"visit_start_date\") AS \"max\",\n \"visit_occurrence_1\".\"person_id\"\n FROM \"visit_occurrence\" AS \"visit_occurrence_1\"\n GROUP BY \"visit_occurrence_1\".\"person_id\"\n) AS \"visit_group_1\" ON (\"person_1\".\"person_id\" = \"visit_group_1\".\"person_id\")\n=#","category":"page"},{"location":"guide/#Partition-and-Window-Functions","page":"Usage Guide","title":"Partition and Window Functions","text":"","category":"section"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"We can relate each row to other rows in the same dataset using the Partition node and window functions. We start by applying the Partition node to partition the input rows into disjoint groups. The rows in each group are reordered according to the given sort order. Unlike Group, which collapses each row group into a single row, the Partition node preserves the original rows, but allows us to relate each row to adjacent rows in the same partition. In particular, we can apply regular aggregate functions, which calculate the summary value of a subset of rows related to the current row.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"In the following example, the rows visit_occurrence are partitioned per patient and ordered by the starting date of the visit. The frame clause specifies the subset of rows relative to the current row (the window frame) to be used by aggregate functions. In this example, the frame contains all rows prior to the current row.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"For each visit, show the time passed since the previous visit.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"using FunSQL: Partition\n\nq = From(:visit_occurrence) |>\n Partition(Get.person_id,\n order_by = [Get.visit_start_date],\n frame = (mode = :rows, start = -Inf, finish = -1)) |>\n Select(Get.person_id,\n Get.visit_start_date,\n Get.visit_end_date,\n :gap => Fun.julianday(Get.visit_start_date) .- Fun.julianday(Agg.max(Get.visit_end_date)))\n\nrender(conn, q) |> print\n#=>\nSELECT\n \"visit_occurrence_1\".\"person_id\",\n \"visit_occurrence_1\".\"visit_start_date\",\n \"visit_occurrence_1\".\"visit_end_date\",\n (julianday(\"visit_occurrence_1\".\"visit_start_date\") - julianday((max(\"visit_occurrence_1\".\"visit_end_date\") OVER (PARTITION BY \"visit_occurrence_1\".\"person_id\" ORDER BY \"visit_occurrence_1\".\"visit_start_date\" ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING)))) AS \"gap\"\nFROM \"visit_occurrence\" AS \"visit_occurrence_1\"\n=#\n\nDBInterface.execute(conn, q) |> DataFrame\n#=>\n27×4 DataFrame\n Row │ person_id visit_start_date visit_end_date gap\n │ Int64 String String Float64?\n─────┼────────────────────────────────────────────────────────\n 1 │ 1780 2008-04-09 2008-04-13 missing\n 2 │ 1780 2008-04-10 2008-04-10 -3.0\n 3 │ 1780 2008-11-22 2008-11-22 223.0\n 4 │ 1780 2009-05-22 2009-05-22 181.0\n⋮\n=#","category":"page"},{"location":"guide/#Query-Parameters","page":"Usage Guide","title":"Query Parameters","text":"","category":"section"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"A SQL query may include a reference to a query parameter. When we execute such a query, we must supply the actual values for all parameters used in the query. This is a restricted form of dynamic query construction directly supported by SQL syntax.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Show all patients born between $start_year and $end_year.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"sql = \"\"\"\nSELECT p.person_id\nFROM person p\nWHERE p.year_of_birth BETWEEN ? AND ?\n\"\"\"\n\nDBInterface.execute(conn, sql, (1930, 1940)) |> DataFrame\n#=>\n3×1 DataFrame\n Row │ person_id\n │ Int64\n─────┼───────────\n 1 │ 1780\n 2 │ 30091\n 3 │ 72120\n=#","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"FunSQL can be used to construct a query with parameters. Similar to Get, parameter references are created using the Var node:","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"using FunSQL: Var\n\nq = From(:person) |>\n Where(Fun.between(Get.year_of_birth, Var.START_YEAR, Var.END_YEAR)) |>\n Select(Get.person_id)\n\nrender(conn, q) |> print\n#=>\nSELECT \"person_1\".\"person_id\"\nFROM \"person\" AS \"person_1\"\nWHERE (\"person_1\".\"year_of_birth\" BETWEEN ?1 AND ?2)\n=#","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"While we specified parameters by name, in the generated SQL query the same parameters are numbered. FunSQL will automatically pack named parameters in the order in which they appear in the SQL query.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"DBInterface.execute(conn, q, START_YEAR = 1930, END_YEAR = 1940) |> DataFrame\n#=>\n3×1 DataFrame\n Row │ person_id\n │ Int64\n─────┼───────────\n 1 │ 1780\n 2 │ 30091\n 3 │ 72120\n=#","category":"page"},{"location":"guide/#Correlated-Queries","page":"Usage Guide","title":"Correlated Queries","text":"","category":"section"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"An inner query is a SQL query that is included into the outer query as a part of a scalar expression. An inner query must either produce a single value or be used as an argument of a query operator, such as IN or EXISTS, which transforms the query output to a scalar value.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"It is easy to assemble an inner query with FunSQL:","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Find the oldest patients.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"qᵢ = From(:person) |>\n Group() |>\n Select(Agg.min(Get.year_of_birth))\n\nqₒ = From(:person) |>\n Where(Get.year_of_birth .== qᵢ) |>\n Select(Get.person_id, Get.year_of_birth)\n\nrender(conn, qₒ) |> print\n#=>\nSELECT\n \"person_1\".\"person_id\",\n \"person_1\".\"year_of_birth\"\nFROM \"person\" AS \"person_1\"\nWHERE (\"person_1\".\"year_of_birth\" = (\n SELECT min(\"person_2\".\"year_of_birth\") AS \"min\"\n FROM \"person\" AS \"person_2\"\n))\n=#\n\nDBInterface.execute(conn, qₒ) |> DataFrame\n#=>\n1×2 DataFrame\n Row │ person_id year_of_birth\n │ Int64 Int64\n─────┼──────────────────────────\n 1 │ 110862 1911\n=#","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Find patients with no visits to a healthcare provider.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"qᵢ = From(:visit_occurrence) |>\n Select(Get.person_id)\n\nqₒ = From(:person) |>\n Where(Fun.not_in(Get.person_id, qᵢ))\n\nrender(conn, qₒ) |> print\n#=>\nSELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"ethnicity_source_concept_id\"\nFROM \"person\" AS \"person_1\"\nWHERE (\"person_1\".\"person_id\" NOT IN (\n SELECT \"visit_occurrence_1\".\"person_id\"\n FROM \"visit_occurrence\" AS \"visit_occurrence_1\"\n))\n=#\n\nDBInterface.execute(conn, qₒ) |> DataFrame\n#=>\n0×18 DataFrame\n Row │ person_id gender_concept_id year_of_birth month_of_birth day_of_bir ⋯\n │ Int64? Int64? Int64? Int64? Int64? ⋯\n─────┴──────────────────────────────────────────────────────────────────────────\n 14 columns omitted\n=#","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"The inner query may depend on the data from the outer query. Such inner queries are called correlated. In FunSQL, correlated queries are created using the Bind node. Specifically, in the body of a correlated query we use query parameters to refer to the external data. The Bind node, which wrap the correlated query, binds each parameter to an expression evaluated in the context of the outer query.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Find all visits where at least one condition was diagnosed.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"using FunSQL: Bind\n\nCorrelatedCondition(person_id, start_date, end_date) =\n From(:condition_occurrence) |>\n Where(Fun.and(Get.person_id .== Var.PERSON_ID,\n Fun.between(Get.condition_start_date, Var.START_DATE, Var.END_DATE))) |>\n Bind(:PERSON_ID => person_id,\n :START_DATE => start_date,\n :END_DATE => end_date)\n\nq = From(:visit_occurrence) |>\n Where(Fun.exists(CorrelatedCondition(Get.person_id, Get.visit_start_date, Get.visit_end_date)))\n\nrender(conn, q) |> print\n#=>\nSELECT\n \"visit_occurrence_1\".\"visit_occurrence_id\",\n ⋮\n \"visit_occurrence_1\".\"visit_source_concept_id\"\nFROM \"visit_occurrence\" AS \"visit_occurrence_1\"\nWHERE (EXISTS (\n SELECT NULL AS \"_\"\n FROM \"condition_occurrence\" AS \"condition_occurrence_1\"\n WHERE\n (\"condition_occurrence_1\".\"person_id\" = \"visit_occurrence_1\".\"person_id\") AND\n (\"condition_occurrence_1\".\"condition_start_date\" BETWEEN \"visit_occurrence_1\".\"visit_start_date\" AND \"visit_occurrence_1\".\"visit_end_date\")\n))\n=#","category":"page"},{"location":"guide/#Order-and-Limit","page":"Usage Guide","title":"Order and Limit","text":"","category":"section"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"The database server emits the output rows in an arbitrary order. In fact, different runs of the same query may produce rows in a different order. To specify a particular order of output rows, we use the Order node.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Show patients ordered by the year of birth.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"using FunSQL: Order\n\nq = From(:person) |>\n Order(Get.year_of_birth)\n\nrender(conn, q) |> print\n#=>\nSELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"ethnicity_source_concept_id\"\nFROM \"person\" AS \"person_1\"\nORDER BY \"person_1\".\"year_of_birth\"\n=#","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"The Asc and the Desc modifiers specify whether to sort the rows in an ascending or in a descending order.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Show patients ordered by the year of birth in the descending order.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"using FunSQL: Desc\n\nq = From(:person) |>\n Order(Get.year_of_birth |> Desc())\n\nrender(conn, q) |> print\n#=>\nSELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"ethnicity_source_concept_id\"\nFROM \"person\" AS \"person_1\"\nORDER BY \"person_1\".\"year_of_birth\" DESC\n=#","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"The Limit node lets us take a slice of the input dataset. To make the output deterministic, Limit must be applied right after Order.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Show the top three oldest patients.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"using FunSQL: Limit\n\nq = From(:person) |>\n Order(Get.year_of_birth) |>\n Limit(1:3)\n\nrender(conn, q) |> print\n#=>\nSELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"ethnicity_source_concept_id\"\nFROM \"person\" AS \"person_1\"\nORDER BY \"person_1\".\"year_of_birth\"\nLIMIT 3\nOFFSET 0\n=#","category":"page"},{"location":"guide/#Append-and-Iterate","page":"Usage Guide","title":"Append and Iterate","text":"","category":"section"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"The Append node concatenates two or more input datasets. Only the columns that are present in every input dataset will be included to the output.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Show all clinical events (visits and conditions) associated with each patient.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"using FunSQL: Append\n\nq = From(:visit_occurrence) |>\n Define(:type => \"visit\", :date => Get.visit_start_date) |>\n Append(From(:condition_occurrence) |>\n Define(:type => \"condition\", :date => Get.condition_start_date)) |>\n Order(Get.person_id, Get.date)\n\nrender(conn, q) |> print\n#=>\nSELECT\n \"union_1\".\"visit_occurrence_id\",\n \"union_1\".\"person_id\",\n \"union_1\".\"provider_id\",\n \"union_1\".\"type\",\n \"union_1\".\"date\"\nFROM (\n SELECT\n \"visit_occurrence_1\".\"visit_occurrence_id\",\n \"visit_occurrence_1\".\"person_id\",\n \"visit_occurrence_1\".\"provider_id\",\n 'visit' AS \"type\",\n \"visit_occurrence_1\".\"visit_start_date\" AS \"date\"\n FROM \"visit_occurrence\" AS \"visit_occurrence_1\"\n UNION ALL\n SELECT\n \"condition_occurrence_1\".\"visit_occurrence_id\",\n \"condition_occurrence_1\".\"person_id\",\n \"condition_occurrence_1\".\"provider_id\",\n 'condition' AS \"type\",\n \"condition_occurrence_1\".\"condition_start_date\" AS \"date\"\n FROM \"condition_occurrence\" AS \"condition_occurrence_1\"\n) AS \"union_1\"\nORDER BY\n \"union_1\".\"person_id\",\n \"union_1\".\"date\"\n=#\n\nDBInterface.execute(conn, q) |> DataFrame\n#=>\n53×5 DataFrame\n Row │ visit_occurrence_id person_id provider_id type date\n │ Int64 Int64 Int64 String String\n─────┼────────────────────────────────────────────────────────────────────\n 1 │ 88179 1780 5247 visit 2008-04-09\n 2 │ 88246 1780 61112 visit 2008-04-10\n 3 │ 88246 1780 61112 condition 2008-04-10\n 4 │ 88214 1780 12674 visit 2008-11-22\n 5 │ 88214 1780 12674 condition 2008-11-22\n 6 │ 88263 1780 61118 visit 2009-05-22\n 7 │ 88263 1780 61118 condition 2009-05-22\n 8 │ 1454922 30091 36303 visit 2008-11-12\n ⋮ │ ⋮ ⋮ ⋮ ⋮ ⋮\n 47 │ 5314671 110862 5159 condition 2008-09-07\n 48 │ 5314690 110862 31906 visit 2009-06-30\n 49 │ 5314690 110862 31906 condition 2009-06-30\n 50 │ 5314664 110862 31857 visit 2009-09-30\n 51 │ 5314664 110862 31857 condition 2009-09-30\n 52 │ 5314696 110862 192777 visit 2010-06-07\n 53 │ 5314696 110862 192777 condition 2010-06-07\n 38 rows omitted\n=#","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"For a second example, consider the table concept, which contains the vocabulary of medical concepts (such as Myocardial Infarction). These concepts may be related to each other (Myocardial Infarction has a subtype Acute Myocardial Infarction), and their relationships are stored in the table concept_relationship. We can encapsulate construction of a query that finds immediate subtypes as the function:","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"SubtypesOf(base) =\n From(:concept) |>\n Join(From(:concept_relationship) |>\n Where(Get.relationship_id .== \"Is a\"),\n on = Get.concept_id .== Get.concept_id_1) |>\n Join(:base => base,\n on = Get.concept_id_2 .== Get.base.concept_id)","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Show the concept \"Myocardial Infarction\" and its immediate subtypes.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"base = From(:concept) |>\n Where(Get.concept_name .== \"Myocardial infarction\")\n\nq = base |> Append(SubtypesOf(base))\n\nDBInterface.execute(conn, q) |> DataFrame\n#=>\n2×10 DataFrame\n Row │ concept_id concept_name domain_id vocabulary_id conc ⋯\n │ Int64 String String String Stri ⋯\n─────┼──────────────────────────────────────────────────────────────────────────\n 1 │ 4329847 Myocardial infarction Condition SNOMED Clin ⋯\n 2 │ 312327 Acute myocardial infarction Condition SNOMED Clin\n 6 columns omitted\n=#","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"But how can we fetch not just immediate, but all of the subtypes of a concept?","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Show the concept \"Myocardial Infarction\" and all of its subtypes.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"A good start is to repeatedly apply SubtypesOf and concatenate all the outputs:","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"base |>\nAppend(SubtypesOf(base),\n SubtypesOf(SubtypesOf(base)),\n SubtypesOf(SubtypesOf(SubtypesOf(base))),\n SubtypesOf(SubtypesOf(SubtypesOf(SubtypesOf(base)))))","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"However we do not know if 4 iterations of SubtypesOf is enough to fully traverse the concept hierarchy. Ideally, we should continue applying SubtypesOf until the last iteration produces an empty output. This is exactly the action of the Iterate node.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"using FunSQL: Iterate\n\nq = base |>\n Iterate(SubtypesOf(From(^)))\n\nrender(conn, q) |> print\n#=>\nWITH RECURSIVE \"__1\" (\"concept_id\", \"concept_name\", \"domain_id\", \"vocabulary_id\", \"concept_class_id\", \"standard_concept\", \"concept_code\", \"valid_start_date\", \"valid_end_date\", \"invalid_reason\") AS (\n SELECT\n \"concept_1\".\"concept_id\",\n \"concept_1\".\"concept_name\",\n \"concept_1\".\"domain_id\",\n \"concept_1\".\"vocabulary_id\",\n \"concept_1\".\"concept_class_id\",\n \"concept_1\".\"standard_concept\",\n \"concept_1\".\"concept_code\",\n \"concept_1\".\"valid_start_date\",\n \"concept_1\".\"valid_end_date\",\n \"concept_1\".\"invalid_reason\"\n FROM \"concept\" AS \"concept_1\"\n WHERE (\"concept_1\".\"concept_name\" = 'Myocardial infarction')\n UNION ALL\n SELECT\n \"concept_2\".\"concept_id\",\n \"concept_2\".\"concept_name\",\n \"concept_2\".\"domain_id\",\n \"concept_2\".\"vocabulary_id\",\n \"concept_2\".\"concept_class_id\",\n \"concept_2\".\"standard_concept\",\n \"concept_2\".\"concept_code\",\n \"concept_relationship_2\".\"valid_start_date\",\n \"concept_relationship_2\".\"valid_end_date\",\n \"concept_relationship_2\".\"invalid_reason\"\n FROM \"concept\" AS \"concept_2\"\n JOIN (\n SELECT\n \"concept_relationship_1\".\"valid_start_date\",\n \"concept_relationship_1\".\"valid_end_date\",\n \"concept_relationship_1\".\"invalid_reason\",\n \"concept_relationship_1\".\"concept_id_2\",\n \"concept_relationship_1\".\"concept_id_1\"\n FROM \"concept_relationship\" AS \"concept_relationship_1\"\n WHERE (\"concept_relationship_1\".\"relationship_id\" = 'Is a')\n ) AS \"concept_relationship_2\" ON (\"concept_2\".\"concept_id\" = \"concept_relationship_2\".\"concept_id_1\")\n JOIN \"__1\" AS \"__2\" ON (\"concept_relationship_2\".\"concept_id_2\" = \"__2\".\"concept_id\")\n)\nSELECT\n \"concept_3\".\"concept_id\",\n \"concept_3\".\"concept_name\",\n \"concept_3\".\"domain_id\",\n \"concept_3\".\"vocabulary_id\",\n \"concept_3\".\"concept_class_id\",\n \"concept_3\".\"standard_concept\",\n \"concept_3\".\"concept_code\",\n \"concept_3\".\"valid_start_date\",\n \"concept_3\".\"valid_end_date\",\n \"concept_3\".\"invalid_reason\"\nFROM \"__1\" AS \"concept_3\"\n=#\n\nDBInterface.execute(conn, q) |> DataFrame\n#=>\n6×10 DataFrame\n Row │ concept_id concept_name domain_id vocabulary_id ⋯\n │ Int64 String String String ⋯\n─────┼──────────────────────────────────────────────────────────────────────────\n 1 │ 4329847 Myocardial infarction Condition SNOMED ⋯\n 2 │ 312327 Acute myocardial infarction Condition SNOMED\n 3 │ 434376 Acute myocardial infarction of a… Condition SNOMED\n 4 │ 438170 Acute myocardial infarction of i… Condition SNOMED\n 5 │ 438438 Acute myocardial infarction of a… Condition SNOMED ⋯\n 6 │ 444406 Acute subendocardial infarction Condition SNOMED\n 6 columns omitted\n=#","category":"page"},{"location":"#FunSQL.jl","page":"Home","title":"FunSQL.jl","text":"","category":"section"},{"location":"","page":"Home","title":"Home","text":"FunSQL is a Julia library for compositional construction of SQL queries.","category":"page"},{"location":"#Table-of-Contents","page":"Home","title":"Table of Contents","text":"","category":"section"},{"location":"","page":"Home","title":"Home","text":"Pages = [\n \"guide/index.md\",\n \"reference/index.md\",\n \"examples/index.md\",\n \"test/index.md\",\n \"two-kinds-of-sql-query-builders/index.md\",\n]","category":"page"},{"location":"test/clauses/#SQL-Clauses","page":"SQL Clauses","title":"SQL Clauses","text":"","category":"section"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"using FunSQL:\n AGG, AS, ASC, DESC, FROM, FUN, GROUP, HAVING, ID, JOIN, LIMIT, LIT,\n NOTE, ORDER, PARTITION, SELECT, SORT, UNION, VALUES, VAR, WHERE,\n WINDOW, WITH, pack, render","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"The syntactic structure of a SQL query is represented as a tree of SQLClause objects. Different types of clauses are created by specialized constructors and connected using the chain (|>) operator.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FROM(:person) |>\n SELECT(:person_id, :year_of_birth)\n#-> (…) |> SELECT(…)","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"Displaying a SQLClause object shows how it was constructed.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"display(c)\n#-> ID(:person) |> FROM() |> SELECT(ID(:person_id), ID(:year_of_birth))","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"A SQLClause object wraps a concrete clause object, which can be accessed using the indexing operator.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c[]\n#-> ((…) |> SELECT(…))[]\n\ndisplay(c[])\n#-> (ID(:person) |> FROM() |> SELECT(ID(:person_id), ID(:year_of_birth)))[]","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"To generate SQL, we use function render().","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"print(render(c))\n#=>\nSELECT\n \"person_id\",\n \"year_of_birth\"\nFROM \"person\"\n=#","category":"page"},{"location":"test/clauses/#SQL-Literals","page":"SQL Clauses","title":"SQL Literals","text":"","category":"section"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"A SQL literal is created using a LIT() constructor.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = LIT(\"SQL is fun!\")\n#-> LIT(\"SQL is fun!\")","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"Values of certain Julia data types are automatically converted to SQL literals when they are used in the context of a SQL clause.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"using Dates\n\nc = SELECT(missing, true, 42, \"SQL is fun!\", Date(2000))\n\ndisplay(c)\n#=>\nSELECT(LIT(missing),\n LIT(true),\n LIT(42),\n LIT(\"SQL is fun!\"),\n LIT(Dates.Date(\"2000-01-01\")))\n=#\n\nprint(render(c))\n#=>\nSELECT\n NULL,\n TRUE,\n 42,\n 'SQL is fun!',\n '2000-01-01'\n=#","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"Some values may render differently depending on the dialect.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = LIT(false)\n\nprint(render(c, dialect = :sqlserver))\n#-> (1 = 0)","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"A quote character in a string literal is represented by a pair of quotes.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = LIT(\"O'Hare\")\n\nprint(render(c))\n#-> 'O''Hare'","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"Some dialects use backslash to escape quote characters.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"print(render(c, dialect = :spark))\n#-> 'O\\'Hare'","category":"page"},{"location":"test/clauses/#SQL-Identifiers","page":"SQL Clauses","title":"SQL Identifiers","text":"","category":"section"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"A SQL identifier is created with ID() constructor.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = ID(:person)\n#-> ID(:person)\n\ndisplay(c)\n#-> ID(:person)\n\nprint(render(c))\n#-> \"person\"","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"Serialization of an identifier depends on the SQL dialect.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"print(render(c, dialect = :sqlserver))\n#-> [person]","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"A quote character in an identifier is properly escaped.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = ID(\"year of \\\"birth\\\"\")\n\nprint(render(c))\n#-> \"year of \"\"birth\"\"\"","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"A qualified identifier is created using the chain operator.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = ID(:person) |> ID(:year_of_birth)\n#-> (…) |> ID(:year_of_birth)\n\ndisplay(c)\n#-> ID(:person) |> ID(:year_of_birth)\n\nprint(render(c))\n#-> \"person\".\"year_of_birth\"","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"Symbols and pairs of symbols are automatically converted to SQL identifiers when they are used in the context of a SQL clause.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FROM(:p => :person) |> SELECT((:p, :person_id))\ndisplay(c)\n#-> ID(:person) |> AS(:p) |> FROM() |> SELECT(ID(:p) |> ID(:person_id))\n\nprint(render(c))\n#=>\nSELECT \"p\".\"person_id\"\nFROM \"person\" AS \"p\"\n=#","category":"page"},{"location":"test/clauses/#SQL-Variables","page":"SQL Clauses","title":"SQL Variables","text":"","category":"section"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"Placeholder parameters to a SQL query are created with VAR() constructor.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = VAR(:YEAR)\n#-> VAR(:YEAR)\n\ndisplay(c)\n#-> VAR(:YEAR)\n\nprint(render(c))\n#-> :YEAR","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"Rendering of a SQL parameter depends on the chosen dialect.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"print(render(c, dialect = :sqlite))\n#-> ?1\n\nprint(render(c, dialect = :postgresql))\n#-> $1\n\nprint(render(c, dialect = :mysql))\n#-> ?","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"Function pack() converts named parameters to a positional form.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FROM(:person) |>\n WHERE(FUN(:or, FUN(\"=\", :gender_concept_id, VAR(:GENDER)),\n FUN(\"=\", :gender_source_concept_id, VAR(:GENDER)))) |>\n SELECT(:person_id)\n\nsql = render(c, dialect = :sqlite)\n\nprint(sql)\n#=>\nSELECT \"person_id\"\nFROM \"person\"\nWHERE\n (\"gender_concept_id\" = ?1) OR\n (\"gender_source_concept_id\" = ?1)\n=#\n\npack(sql, (GENDER = 8532,))\n#-> Any[8532]\n\npack(sql, Dict(:GENDER => 8532))\n#-> Any[8532]\n\npack(sql, Dict(\"GENDER\" => 8532))\n#-> Any[8532]","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"If the dialect does not support numbered parameters, pack() may need to duplicate parameter values.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"sql = render(c, dialect = :mysql)\n\nprint(sql)\n#=>\nSELECT `person_id`\nFROM `person`\nWHERE\n (`gender_concept_id` = ?) OR\n (`gender_source_concept_id` = ?)\n=#\n\npack(sql, (GENDER = 8532,))\n#-> Any[8532, 8532]","category":"page"},{"location":"test/clauses/#SQL-Functions-and-Operators","page":"SQL Clauses","title":"SQL Functions and Operators","text":"","category":"section"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"An application of a SQL function is created with FUN() constructor.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FUN(:concat, :city, \", \", :state)\n#-> FUN(\"concat\", …)\n\ndisplay(c)\n#-> FUN(\"concat\", ID(:city), LIT(\", \"), ID(:state))\n\nprint(render(c))\n#-> concat(\"city\", ', ', \"state\")\n\nc = FUN(:now)\n#-> FUN(\"now\")\n\nprint(render(c))\n#-> now()","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"FUN() with an empty name generates a comma-separated list of values.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FUN(\"\", \"60614\", \"60615\")\n\nprint(render(c))\n#-> ('60614', '60615')","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"A name that contains only symbol characters is considered an operator.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FUN(\"||\", :city, \", \", :state)\n\nprint(render(c))\n#-> (\"city\" || ', ' || \"state\")","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"To create an operator containing alphabetical characters, add a leading or a trailing space to its name.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FUN(\" IS DISTINCT FROM \", :zip, missing)\n\nprint(render(c))\n#-> (\"zip\" IS DISTINCT FROM NULL)\n\nc = FUN(\" IS DISTINCT FROM\", :zip, missing)\n\nprint(render(c))\n#-> (\"zip\" IS DISTINCT FROM NULL)\n\nc = FUN(\" COLLATE \\\"C\\\"\", :zip)\n\nprint(render(c))\n#-> (\"zip\" COLLATE \"C\")\n\nc = FUN(\"DATE \", \"2000-01-01\")\n\nprint(render(c))\n#-> (DATE '2000-01-01')\n\nc = FUN(\"CURRENT_TIME \")\n\nprint(render(c))\n#-> CURRENT_TIME\n\nc = FUN(\" CURRENT_TIME\")\n\nprint(render(c))\n#-> CURRENT_TIME","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"To create a SQL expression with irregular syntax, supply FUN() with a template string.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FUN(\"SUBSTRING(? FROM ? FOR ?)\", :zip, 1, 3)\n\nprint(render(c))\n#-> SUBSTRING(\"zip\" FROM 1 FOR 3)\n\nc = FUN(\"?::date\", \"2000-01-01\")\n\nprint(render(c))\n#-> '2000-01-01'::date","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"Write ?? to use ? in an operator name or a template.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FUN(\"??-\", \"(1,0)\", \"(0,0)\")\n\nprint(render(c))\n#-> ('(1,0)' ?- '(0,0)')\n\nc = FUN(\"('(?,?)'::point ??| '(?,?)'::point)\", 0, 1, 0, 0)\n\nprint(render(c))\n#-> ('(0,1)'::point ?| '(0,0)'::point)","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"Some functions and operators have specialized serializers.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FUN(:and)\n\nprint(render(c))\n#-> TRUE\n\nc = FUN(:and, true)\n\nprint(render(c))\n#-> TRUE\n\nc = FUN(:and, true, false)\n\nprint(render(c))\n#-> (TRUE AND FALSE)\n\nc = FUN(:or)\n\nprint(render(c))\n#-> FALSE\n\nc = FUN(:or, true)\n\nprint(render(c))\n#-> TRUE\n\nc = FUN(:or, true, false)\n\nprint(render(c))\n#-> (TRUE OR FALSE)\n\nc = FUN(:not, true)\n\nprint(render(c))\n#-> (NOT TRUE)\n\nc = FUN(:concat, :city, \", \", :state)\n\nprint(render(c))\n#-> concat(\"city\", ', ', \"state\")\n\nprint(render(c, dialect = :sqlite))\n#-> (\"city\" || ', ' || \"state\")\n\nc = FUN(:in, :zip)\n\nprint(render(c))\n#-> FALSE\n\nc = FUN(:in, :zip, \"60614\", \"60615\")\n\nprint(render(c))\n#-> (\"zip\" IN ('60614', '60615'))\n\nc = SELECT(FUN(:in, \"60615\", FROM(:location) |> SELECT(:zip)))\n\nprint(render(c))\n#=>\nSELECT ('60615' IN (\n SELECT \"zip\"\n FROM \"location\"\n))\n=#\n\nc = FUN(:not_in, :zip)\n\nprint(render(c))\n#-> TRUE\n\nc = FUN(:not_in, :zip, \"60614\", \"60615\")\n\nprint(render(c))\n#-> (\"zip\" NOT IN ('60614', '60615'))\n\nc = SELECT(FUN(:not_in, \"60615\", FROM(:location) |> SELECT(:zip)))\n\nprint(render(c))\n#=>\nSELECT ('60615' NOT IN (\n SELECT \"zip\"\n FROM \"location\"\n))\n=#\n\nc = SELECT(FUN(:exists, FROM(:location) |>\n WHERE(FUN(\"=\", :zip, \"60615\")) |>\n SELECT(missing)))\n\nprint(render(c))\n#=>\nSELECT (EXISTS (\n SELECT NULL\n FROM \"location\"\n WHERE (\"zip\" = '60615')\n))\n=#\n\nc = SELECT(FUN(:not_exists, FROM(:location) |>\n WHERE(FUN(\"=\", :zip, \"60615\")) |>\n SELECT(missing)))\n\nprint(render(c))\n#=>\nSELECT (NOT EXISTS (\n SELECT NULL\n FROM \"location\"\n WHERE (\"zip\" = '60615')\n))\n=#\n\nc = FUN(:is_null, :zip)\n\nprint(render(c))\n#-> (\"zip\" IS NULL)\n\nc = FUN(:is_not_null, :zip)\n\nprint(render(c))\n#-> (\"zip\" IS NOT NULL)\n\nc = FUN(:like, :zip, \"606%\")\n\nprint(render(c))\n#-> (\"zip\" LIKE '606%')\n\nc = FUN(:not_like, :zip, \"606%\")\n\nprint(render(c))\n#-> (\"zip\" NOT LIKE '606%')\n\nc = FUN(:case, FUN(\"<\", :year_of_birth, 1970), \"boomer\")\n\nprint(render(c))\n#-> (CASE WHEN (\"year_of_birth\" < 1970) THEN 'boomer' END)\n\nc = FUN(:case, FUN(\"<\", :year_of_birth, 1970), \"boomer\", \"millenial\")\n\nprint(render(c))\n#-> (CASE WHEN (\"year_of_birth\" < 1970) THEN 'boomer' ELSE 'millenial' END)\n\nc = FUN(:cast, \"2020-01-01\", \"DATE\")\n\nprint(render(c))\n#-> CAST('2020-01-01' AS DATE)\n\nc = FUN(:extract, \"YEAR\", c)\n\nprint(render(c))\n#-> EXTRACT(YEAR FROM CAST('2020-01-01' AS DATE))\n\nc = FUN(:between, :year_of_birth, 1950, 2000)\n\nprint(render(c))\n#-> (\"year_of_birth\" BETWEEN 1950 AND 2000)\n\nc = FUN(:not_between, :year_of_birth, 1950, 2000)\n\nprint(render(c))\n#-> (\"year_of_birth\" NOT BETWEEN 1950 AND 2000)\n\nc = FUN(:current_date)\n\nprint(render(c))\n#-> CURRENT_DATE\n\nc = FUN(:current_date, 1)\n\nprint(render(c))\n#-> CURRENT_DATE(1)\n\nc = FUN(:current_timestamp)\n\nprint(render(c))\n#-> CURRENT_TIMESTAMP","category":"page"},{"location":"test/clauses/#Aggregate-Functions","page":"SQL Clauses","title":"Aggregate Functions","text":"","category":"section"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"Aggregate SQL functions have a specialized AGG() constructor.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = AGG(:max, :year_of_birth)\n#-> AGG(\"max\", …)\n\ndisplay(c)\n#-> AGG(\"max\", ID(:year_of_birth))\n\nprint(render(c))\n#-> max(\"year_of_birth\")","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"Some well-known aggregate functions with irregular syntax are supported.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = AGG(:count)\n#-> AGG(\"count\")\n\ndisplay(c)\n#-> AGG(\"count\")\n\nprint(render(c))\n#-> count(*)\n\nc = AGG(:count_distinct, :zip)\n\nprint(render(c))\n#-> count(DISTINCT \"zip\")","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"Otherwise, a template name can be used.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = AGG(\"string_agg(DISTINCT ?, ',' ORDER BY ?)\", :zip, :zip)\n\nprint(render(c))\n#-> string_agg(DISTINCT \"zip\", ',' ORDER BY \"zip\")","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"An aggregate function may have a FILTER modifier.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = AGG(:count, filter = FUN(\">\", :year_of_birth, 1970))\n\ndisplay(c)\n#-> AGG(\"count\", filter = FUN(\">\", ID(:year_of_birth), LIT(1970)))\n\nprint(render(c))\n#-> (count(*) FILTER (WHERE (\"year_of_birth\" > 1970)))","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"A window function can be created by adding an OVER modifier.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = PARTITION(:year_of_birth, order_by = [:month_of_birth, :day_of_birth]) |>\n AGG(\"row_number\")\n\ndisplay(c)\n#=>\nAGG(\"row_number\",\n over = PARTITION(ID(:year_of_birth),\n order_by = [ID(:month_of_birth), ID(:day_of_birth)]))\n=#\n\nprint(render(c))\n#-> (row_number() OVER (PARTITION BY \"year_of_birth\" ORDER BY \"month_of_birth\", \"day_of_birth\"))\n\nc = AGG(\"row_number\", over = :w)\n\nprint(render(c))\n#-> (row_number() OVER (\"w\"))","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"The PARTITION clause may contain a frame specification including the frame mode, frame endpoints, and frame exclusion.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = PARTITION(order_by = [:year_of_birth], frame = :groups)\n#-> PARTITION(order_by = […], frame = :GROUPS)\n\nprint(render(c))\n#-> ORDER BY \"year_of_birth\" GROUPS UNBOUNDED PRECEDING\n\nc = PARTITION(order_by = [:year_of_birth], frame = (mode = :rows,))\n#-> PARTITION(order_by = […], frame = :ROWS)\n\nprint(render(c))\n#-> ORDER BY \"year_of_birth\" ROWS UNBOUNDED PRECEDING\n\nc = PARTITION(order_by = [:year_of_birth], frame = (mode = :range, start = -1, finish = 1, exclude = :current_row))\n#-> PARTITION(order_by = […], frame = (mode = :RANGE, start = -1, finish = 1, exclude = :CURRENT_ROW))\n\nprint(render(c))\n#-> ORDER BY \"year_of_birth\" RANGE BETWEEN 1 PRECEDING AND 1 FOLLOWING EXCLUDE CURRENT ROW\n\nc = PARTITION(order_by = [:year_of_birth], frame = (mode = :range, start = -Inf, finish = 0))\n\nprint(render(c))\n#-> ORDER BY \"year_of_birth\" RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW\n\nc = PARTITION(order_by = [:year_of_birth], frame = (mode = :range, start = 0, finish = Inf))\n\nprint(render(c))\n#-> ORDER BY \"year_of_birth\" RANGE BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING\n\nc = PARTITION(order_by = [:year_of_birth], frame = (mode = :range, exclude = :no_others))\n\nprint(render(c))\n#-> ORDER BY \"year_of_birth\" RANGE UNBOUNDED PRECEDING EXCLUDE NO OTHERS\n\nc = PARTITION(order_by = [:year_of_birth], frame = (mode = :range, exclude = :group))\n\nprint(render(c))\n#-> ORDER BY \"year_of_birth\" RANGE UNBOUNDED PRECEDING EXCLUDE GROUP\n\nc = PARTITION(order_by = [:year_of_birth], frame = (mode = :range, exclude = :ties))\n\nprint(render(c))\n#-> ORDER BY \"year_of_birth\" RANGE UNBOUNDED PRECEDING EXCLUDE TIES","category":"page"},{"location":"test/clauses/#AS-Clause","page":"SQL Clauses","title":"AS Clause","text":"","category":"section"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"An AS clause is created with AS() constructor.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = ID(:person) |> AS(:p)\n#-> (…) |> AS(:p)\n\ndisplay(c)\n#-> ID(:person) |> AS(:p)\n\nprint(render(c))\n#-> \"person\" AS \"p\"","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"A pair expression is automatically converted to an AS clause.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FROM(:p => :person)\ndisplay(c)\n#-> ID(:person) |> AS(:p) |> FROM()\n\nprint(render(c |> SELECT((:p, :person_id))))\n#=>\nSELECT \"p\".\"person_id\"\nFROM \"person\" AS \"p\"\n=#","category":"page"},{"location":"test/clauses/#FROM-Clause","page":"SQL Clauses","title":"FROM Clause","text":"","category":"section"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"A FROM clause is created with FROM() constructor.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FROM(:person)\n#-> (…) |> FROM()\n\ndisplay(c)\n#-> ID(:person) |> FROM()\n\nprint(render(c |> SELECT(:person_id)))\n#=>\nSELECT \"person_id\"\nFROM \"person\"\n=#","category":"page"},{"location":"test/clauses/#SELECT-Clause","page":"SQL Clauses","title":"SELECT Clause","text":"","category":"section"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"A SELECT clause is created with SELECT() constructor. While in SQL, SELECT typically opens a query, in FunSQL, SELECT() should be placed at the end of a clause chain.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = :person |> FROM() |> SELECT(:person_id, :year_of_birth)\n#-> (…) |> SELECT(…)\n\ndisplay(c)\n#-> ID(:person) |> FROM() |> SELECT(ID(:person_id), ID(:year_of_birth))\n\nprint(render(c))\n#=>\nSELECT\n \"person_id\",\n \"year_of_birth\"\nFROM \"person\"\n=#","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"The DISTINCT modifier can be added from the constructor.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FROM(:location) |> SELECT(distinct = true, :zip)\n#-> (…) |> SELECT(…)\n\ndisplay(c)\n#-> ID(:location) |> FROM() |> SELECT(distinct = true, ID(:zip))\n\nprint(render(c))\n#=>\nSELECT DISTINCT \"zip\"\nFROM \"location\"\n=#","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"A TOP modifier could be specified.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FROM(:person) |> SELECT(top = 1, :person_id)\n\ndisplay(c)\n#-> ID(:person) |> FROM() |> SELECT(top = 1, ID(:person_id))\n\nprint(render(c))\n#=>\nSELECT TOP 1 \"person_id\"\nFROM \"person\"\n=#\n\nc = FROM(:person) |>\n ORDER(:year_of_birth) |>\n SELECT(top = (limit = 1, with_ties = true), :person_id)\n\ndisplay(c)\n#=>\nID(:person) |>\nFROM() |>\nORDER(ID(:year_of_birth)) |>\nSELECT(top = (limit = 1, with_ties = true), ID(:person_id))\n=#\n\nprint(render(c))\n#=>\nSELECT TOP 1 WITH TIES \"person_id\"\nFROM \"person\"\nORDER BY \"year_of_birth\"\n=#","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"A SELECT clause with an empty list of arguments can be created explicitly.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = SELECT(args = [])\n#-> SELECT(…)","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"Rendering a nested SELECT clause adds parentheses around it.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = :location |> FROM() |> SELECT(:state, :zip) |> FROM() |> SELECT(:zip)\n\nprint(render(c))\n#=>\nSELECT \"zip\"\nFROM (\n SELECT\n \"state\",\n \"zip\"\n FROM \"location\"\n)\n=#","category":"page"},{"location":"test/clauses/#WHERE-Clause","page":"SQL Clauses","title":"WHERE Clause","text":"","category":"section"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"A WHERE clause is created with WHERE() constructor.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FROM(:person) |> WHERE(FUN(\">\", :year_of_birth, 2000))\n#-> (…) |> WHERE(…)\n\ndisplay(c)\n#-> ID(:person) |> FROM() |> WHERE(FUN(\">\", ID(:year_of_birth), LIT(2000)))\n\nprint(render(c |> SELECT(:person_id)))\n#=>\nSELECT \"person_id\"\nFROM \"person\"\nWHERE (\"year_of_birth\" > 2000)\n=#","category":"page"},{"location":"test/clauses/#LIMIT-Clause","page":"SQL Clauses","title":"LIMIT Clause","text":"","category":"section"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"A LIMIT/OFFSET (or OFFSET/FETCH) clause is created with LIMIT() constructor.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FROM(:person) |> LIMIT(10)\n#-> (…) |> LIMIT(10)\n\ndisplay(c)\n#-> ID(:person) |> FROM() |> LIMIT(10)\n\nprint(render(c |> SELECT(:person_id)))\n#=>\nSELECT \"person_id\"\nFROM \"person\"\nFETCH FIRST 10 ROWS ONLY\n=#","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"Many SQL dialects represent LIMIT clause with a non-standard syntax.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"print(render(c |> SELECT(:person_id), dialect = :mysql))\n#=>\nSELECT `person_id`\nFROM `person`\nLIMIT 10\n=#\n\nprint(render(c |> SELECT(:person_id), dialect = :postgresql))\n#=>\nSELECT \"person_id\"\nFROM \"person\"\nLIMIT 10\n=#\n\nprint(render(c |> SELECT(:person_id), dialect = :sqlite))\n#=>\nSELECT \"person_id\"\nFROM \"person\"\nLIMIT 10\n=#\n\nprint(render(c |> SELECT(:person_id), dialect = :sqlserver))\n#=>\nSELECT TOP 10 [person_id]\nFROM [person]\n=#","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"Both limit (the number of rows) and offset (number of rows to skip) can be specified.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FROM(:person) |> LIMIT(100, 10) |> SELECT(:person_id)\n\ndisplay(c)\n#-> ID(:person) |> FROM() |> LIMIT(100, 10) |> SELECT(ID(:person_id))\n\nprint(render(c))\n#=>\nSELECT \"person_id\"\nFROM \"person\"\nOFFSET 100 ROWS\nFETCH NEXT 10 ROWS ONLY\n=#\n\nprint(render(c, dialect = :mysql))\n#=>\nSELECT `person_id`\nFROM `person`\nLIMIT 100, 10\n=#\n\nprint(render(c, dialect = :postgresql))\n#=>\nSELECT \"person_id\"\nFROM \"person\"\nLIMIT 10\nOFFSET 100\n=#\n\nprint(render(c, dialect = :sqlite))\n#=>\nSELECT \"person_id\"\nFROM \"person\"\nLIMIT 10\nOFFSET 100\n=#\n\nprint(render(c, dialect = :sqlserver))\n#=>\nSELECT [person_id]\nFROM [person]\nOFFSET 100 ROWS\nFETCH NEXT 10 ROWS ONLY\n=#","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"Alternatively, both limit and offset can be specified as a unit range.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FROM(:person) |> LIMIT(101:110)\n\nprint(render(c |> SELECT(:person_id)))\n#=>\nSELECT \"person_id\"\nFROM \"person\"\nOFFSET 100 ROWS\nFETCH NEXT 10 ROWS ONLY\n=#","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"It is possible to specify the offset without the limit.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FROM(:person) |> LIMIT(offset = 100) |> SELECT(:person_id)\n\ndisplay(c)\n#-> ID(:person) |> FROM() |> LIMIT(100, nothing) |> SELECT(ID(:person_id))\n\nprint(render(c))\n#=>\nSELECT \"person_id\"\nFROM \"person\"\nOFFSET 100 ROWS\n=#\n\nprint(render(c, dialect = :mysql))\n#=>\nSELECT `person_id`\nFROM `person`\nLIMIT 100, 18446744073709551615\n=#\n\nprint(render(c, dialect = :postgresql))\n#=>\nSELECT \"person_id\"\nFROM \"person\"\nOFFSET 100\n=#\n\nprint(render(c, dialect = :sqlite))\n#=>\nSELECT \"person_id\"\nFROM \"person\"\nLIMIT -1\nOFFSET 100\n=#\n\nprint(render(c, dialect = :sqlserver))\n#=>\nSELECT [person_id]\nFROM [person]\nOFFSET 100 ROWS\n=#","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"It is possible to specify the limit with ties.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FROM(:person) |>\n ORDER(:year_of_birth) |>\n LIMIT(10, with_ties = true) |>\n SELECT(:person_id)\n\ndisplay(c)\n#=>\nID(:person) |>\nFROM() |>\nORDER(ID(:year_of_birth)) |>\nLIMIT(10, with_ties = true) |>\nSELECT(ID(:person_id))\n=#\n\nprint(render(c))\n#=>\nSELECT \"person_id\"\nFROM \"person\"\nORDER BY \"year_of_birth\"\nFETCH FIRST 10 ROWS WITH TIES\n=#","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"SQL Server prohibits ORDER BY without limiting in a nested query, so FunSQL automatically adds OFFSET 0 clause to the query.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FROM(:person) |>\n ORDER(:year_of_birth) |>\n SELECT(:person_id, :gender_concept_id) |>\n AS(:person) |>\n FROM() |>\n WHERE(FUN(\"=\", :gender_concept_id, 8507)) |>\n SELECT(:person_id)\n\nprint(render(c, dialect = :sqlserver))\n#=>\nSELECT [person_id]\nFROM (\n SELECT\n [person_id],\n [gender_concept_id]\n FROM [person]\n ORDER BY [year_of_birth]\n OFFSET 0 ROWS\n) AS [person]\nWHERE ([gender_concept_id] = 8507)\n=#","category":"page"},{"location":"test/clauses/#JOIN-Clause","page":"SQL Clauses","title":"JOIN Clause","text":"","category":"section"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"A JOIN clause is created with JOIN() constructor.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FROM(:p => :person) |>\n JOIN(:l => :location, FUN(\"=\", (:p, :location_id), (:l, :location_id)), left = true)\n#-> (…) |> JOIN(…)\n\ndisplay(c)\n#=>\nID(:person) |>\nAS(:p) |>\nFROM() |>\nJOIN(ID(:location) |> AS(:l),\n FUN(\"=\", ID(:p) |> ID(:location_id), ID(:l) |> ID(:location_id)),\n left = true)\n=#\n\nprint(render(c |> SELECT((:p, :person_id), (:l, :state))))\n#=>\nSELECT\n \"p\".\"person_id\",\n \"l\".\"state\"\nFROM \"person\" AS \"p\"\nLEFT JOIN \"location\" AS \"l\" ON (\"p\".\"location_id\" = \"l\".\"location_id\")\n=#","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"Different types of JOIN are supported.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FROM(:p => :person) |>\n JOIN(:op => :observation_period,\n on = FUN(\"=\", (:p, :person_id), (:op, :person_id)))\n\ndisplay(c)\n#=>\nID(:person) |>\nAS(:p) |>\nFROM() |>\nJOIN(ID(:observation_period) |> AS(:op),\n FUN(\"=\", ID(:p) |> ID(:person_id), ID(:op) |> ID(:person_id)))\n=#\n\nprint(render(c |> SELECT((:p, :person_id), (:op, :observation_period_start_date))))\n#=>\nSELECT\n \"p\".\"person_id\",\n \"op\".\"observation_period_start_date\"\nFROM \"person\" AS \"p\"\nJOIN \"observation_period\" AS \"op\" ON (\"p\".\"person_id\" = \"op\".\"person_id\")\n=#\n\nc = FROM(:l => :location) |>\n JOIN(:cs => :care_site,\n on = FUN(\"=\", (:l, :location_id), (:cs, :location_id)),\n right = true)\n\ndisplay(c)\n#=>\nID(:location) |>\nAS(:l) |>\nFROM() |>\nJOIN(ID(:care_site) |> AS(:cs),\n FUN(\"=\", ID(:l) |> ID(:location_id), ID(:cs) |> ID(:location_id)),\n right = true)\n=#\n\nprint(render(c |> SELECT((:cs, :care_site_name), (:l, :state))))\n#=>\nSELECT\n \"cs\".\"care_site_name\",\n \"l\".\"state\"\nFROM \"location\" AS \"l\"\nRIGHT JOIN \"care_site\" AS \"cs\" ON (\"l\".\"location_id\" = \"cs\".\"location_id\")\n=#\n\nc = FROM(:p => :person) |>\n JOIN(:pr => :provider,\n on = FUN(\"=\", (:p, :provider_id), (:pr, :provider_id)),\n left = true,\n right = true)\n\ndisplay(c)\n#=>\nID(:person) |>\nAS(:p) |>\nFROM() |>\nJOIN(ID(:provider) |> AS(:pr),\n FUN(\"=\", ID(:p) |> ID(:provider_id), ID(:pr) |> ID(:provider_id)),\n left = true,\n right = true)\n=#\n\nprint(render(c |> SELECT((:p, :person_id), (:pr, :npi))))\n#=>\nSELECT\n \"p\".\"person_id\",\n \"pr\".\"npi\"\nFROM \"person\" AS \"p\"\nFULL JOIN \"provider\" AS \"pr\" ON (\"p\".\"provider_id\" = \"pr\".\"provider_id\")\n=#","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"To render a CROSS JOIN, set the join condition to true.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FROM(:p1 => :person) |>\n JOIN(:p2 => :person,\n on = true)\n\nprint(render(c |> SELECT((:p1, :person_id), (:p2, :person_id))))\n#=>\nSELECT\n \"p1\".\"person_id\",\n \"p2\".\"person_id\"\nFROM \"person\" AS \"p1\"\nCROSS JOIN \"person\" AS \"p2\"\n=#","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"A JOIN LATERAL clause can be created.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FROM(:p => :person) |>\n JOIN(:vo => FROM(:vo => :visit_occurrence) |>\n WHERE(FUN(\"=\", (:p, :person_id), (:vo, :person_id))) |>\n ORDER((:vo, :visit_start_date) |> DESC()) |>\n LIMIT(1) |>\n SELECT((:vo, :visit_start_date)),\n on = true,\n left = true,\n lateral = true)\n\ndisplay(c)\n#=>\nID(:person) |>\nAS(:p) |>\nFROM() |>\nJOIN(ID(:visit_occurrence) |>\n AS(:vo) |>\n FROM() |>\n WHERE(FUN(\"=\", ID(:p) |> ID(:person_id), ID(:vo) |> ID(:person_id))) |>\n ORDER(ID(:vo) |> ID(:visit_start_date) |> DESC()) |>\n LIMIT(1) |>\n SELECT(ID(:vo) |> ID(:visit_start_date)) |>\n AS(:vo),\n LIT(true),\n left = true,\n lateral = true)\n=#\n\nprint(render(c |> SELECT((:p, :person_id), (:vo, :visit_start_date))))\n#=>\nSELECT\n \"p\".\"person_id\",\n \"vo\".\"visit_start_date\"\nFROM \"person\" AS \"p\"\nLEFT JOIN LATERAL (\n SELECT \"vo\".\"visit_start_date\"\n FROM \"visit_occurrence\" AS \"vo\"\n WHERE (\"p\".\"person_id\" = \"vo\".\"person_id\")\n ORDER BY \"vo\".\"visit_start_date\" DESC\n FETCH FIRST 1 ROW ONLY\n) AS \"vo\" ON TRUE\n=#","category":"page"},{"location":"test/clauses/#GROUP-Clause","page":"SQL Clauses","title":"GROUP Clause","text":"","category":"section"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"A GROUP BY clause is created with GROUP constructor.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FROM(:person) |> GROUP(:year_of_birth)\n#-> (…) |> GROUP(…)\n\ndisplay(c)\n#-> ID(:person) |> FROM() |> GROUP(ID(:year_of_birth))\n\nprint(render(c |> SELECT(:year_of_birth, AGG(:count))))\n#=>\nSELECT\n \"year_of_birth\",\n count(*)\nFROM \"person\"\nGROUP BY \"year_of_birth\"\n=#","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"A GROUP constructor accepts an empty partition list, in which case, it is not rendered.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FROM(:person) |> GROUP()\n#-> (…) |> GROUP()\n\nprint(render(c |> SELECT(AGG(:count))))\n#=>\nSELECT count(*)\nFROM \"person\"\n=#","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"GROUP can accept the grouping mode or a vector of grouping sets.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FROM(:person) |> GROUP(:year_of_birth, sets = :ROLLUP)\n#-> (…) |> GROUP(…, sets = :ROLLUP)\n\nprint(render(c |> SELECT(:year_of_birth, AGG(:count))))\n#=>\nSELECT\n \"year_of_birth\",\n count(*)\nFROM \"person\"\nGROUP BY ROLLUP(\"year_of_birth\")\n=#\n\nc = FROM(:person) |> GROUP(:year_of_birth, sets = :CUBE)\n#-> (…) |> GROUP(…, sets = :CUBE)\n\nprint(render(c |> SELECT(:year_of_birth, AGG(:count))))\n#=>\nSELECT\n \"year_of_birth\",\n count(*)\nFROM \"person\"\nGROUP BY CUBE(\"year_of_birth\")\n=#\n\nc = FROM(:person) |> GROUP(:year_of_birth, sets = [[1], Int[]])\n#-> (…) |> GROUP(…, sets = [[1], Int64[]])\n\nprint(render(c |> SELECT(:year_of_birth, AGG(:count))))\n#=>\nSELECT\n \"year_of_birth\",\n count(*)\nFROM \"person\"\nGROUP BY GROUPING SETS((\"year_of_birth\"), ())\n=#","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"GROUP raises an error when the vector of grouping sets is out of bounds.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"FROM(:person) |> GROUP(:year_of_birth, sets = [[1, 2], [1], Int[]])\n#=>\nERROR: DomainError with [[1, 2], [1], Int64[]]:\nsets are out of bounds\n=#","category":"page"},{"location":"test/clauses/#HAVING-Clause","page":"SQL Clauses","title":"HAVING Clause","text":"","category":"section"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"A HAVING clause is created with HAVING() constructor.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FROM(:person) |>\n GROUP(:year_of_birth) |>\n HAVING(FUN(\">\", AGG(:count), 10))\n#-> (…) |> HAVING(…)\n\ndisplay(c)\n#=>\nID(:person) |>\nFROM() |>\nGROUP(ID(:year_of_birth)) |>\nHAVING(FUN(\">\", AGG(\"count\"), LIT(10)))\n=#\n\nprint(render(c |> SELECT(:person_id)))\n#=>\nSELECT \"person_id\"\nFROM \"person\"\nGROUP BY \"year_of_birth\"\nHAVING (count(*) > 10)\n=#","category":"page"},{"location":"test/clauses/#ORDER-Clause","page":"SQL Clauses","title":"ORDER Clause","text":"","category":"section"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"An ORDER BY clause is created with ORDER constructor.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FROM(:person) |> ORDER(:year_of_birth)\n#-> (…) |> ORDER(…)\n\ndisplay(c)\n#-> ID(:person) |> FROM() |> ORDER(ID(:year_of_birth))\n\nprint(render(c |> SELECT(:person_id)))\n#=>\nSELECT \"person_id\"\nFROM \"person\"\nORDER BY \"year_of_birth\"\n=#","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"An ORDER constructor accepts an empty list, in which case, it is not rendered.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FROM(:person) |> ORDER()\n#-> (…) |> ORDER()\n\nprint(render(c |> SELECT(:person_id)))\n#=>\nSELECT \"person_id\"\nFROM \"person\"\n=#","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"It is possible to specify ascending or descending order of the sort column.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FROM(:person) |>\n ORDER(:year_of_birth |> DESC(nulls = :first),\n :person_id |> ASC()) |>\n SELECT(:person_id)\n\ndisplay(c)\n#=>\nID(:person) |>\nFROM() |>\nORDER(ID(:year_of_birth) |> DESC(nulls = :NULLS_FIRST),\n ID(:person_id) |> ASC()) |>\nSELECT(ID(:person_id))\n=#\n\nprint(render(c))\n#=>\nSELECT \"person_id\"\nFROM \"person\"\nORDER BY\n \"year_of_birth\" DESC NULLS FIRST,\n \"person_id\" ASC\n=#","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"Instead of ASC and DESC, a generic SORT constructor can be used.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FROM(:person) |>\n ORDER(:year_of_birth |> SORT(:desc, nulls = :last),\n :person_id |> SORT(:asc)) |>\n SELECT(:person_id)\n\nprint(render(c))\n#=>\nSELECT \"person_id\"\nFROM \"person\"\nORDER BY\n \"year_of_birth\" DESC NULLS LAST,\n \"person_id\" ASC\n=#","category":"page"},{"location":"test/clauses/#UNION-Clause.","page":"SQL Clauses","title":"UNION Clause.","text":"","category":"section"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"UNION and UNION ALL clauses are created with UNION() constructor.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FROM(:measurement) |>\n SELECT(:person_id, :date => :measurement_date) |>\n UNION(all = true,\n FROM(:observation) |>\n SELECT(:person_id, :date => :observation_date))\n#-> (…) |> UNION(all = true, …)\n\ndisplay(c)\n#=>\nID(:measurement) |>\nFROM() |>\nSELECT(ID(:person_id), ID(:measurement_date) |> AS(:date)) |>\nUNION(all = true,\n ID(:observation) |>\n FROM() |>\n SELECT(ID(:person_id), ID(:observation_date) |> AS(:date)))\n=#\n\nprint(render(c))\n#=>\nSELECT\n \"person_id\",\n \"measurement_date\" AS \"date\"\nFROM \"measurement\"\nUNION ALL\nSELECT\n \"person_id\",\n \"observation_date\" AS \"date\"\nFROM \"observation\"\n=#","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"A UNION clause with no subqueries can be created explicitly.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"UNION(args = [])\n#-> UNION(args = [])","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"Rendering a nested UNION clause adds parentheses around it.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FROM(:measurement) |>\n SELECT(:person_id, :date => :measurement_date) |>\n UNION(all = true,\n FROM(:observation) |>\n SELECT(:person_id, :date => :observation_date)) |>\n FROM() |>\n AS(:union) |>\n WHERE(FUN(\">\", ID(:date), Date(2000))) |>\n SELECT(ID(:person_id))\n\nprint(render(c))\n#=>\nSELECT \"person_id\"\nFROM (\n SELECT\n \"person_id\",\n \"measurement_date\" AS \"date\"\n FROM \"measurement\"\n UNION ALL\n SELECT\n \"person_id\",\n \"observation_date\" AS \"date\"\n FROM \"observation\"\n) AS \"union\"\nWHERE (\"date\" > '2000-01-01')\n=#","category":"page"},{"location":"test/clauses/#VALUES-Clause","page":"SQL Clauses","title":"VALUES Clause","text":"","category":"section"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"A VALUES clause is created with VALUES() constructor.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = VALUES([(\"SQL\", 1974), (\"Julia\", 2012), (\"FunSQL\", 2021)])\n#-> VALUES([(\"SQL\", 1974), (\"Julia\", 2012), (\"FunSQL\", 2021)])\n\ndisplay(c)\n#-> VALUES([(\"SQL\", 1974), (\"Julia\", 2012), (\"FunSQL\", 2021)])\n\nprint(render(c))\n#=>\nVALUES\n ('SQL', 1974),\n ('Julia', 2012),\n ('FunSQL', 2021)\n=#","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"MySQL has special syntax for rows.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"print(render(c, dialect = :mysql))\n#=>\nVALUES\n ROW('SQL', 1974),\n ROW('Julia', 2012),\n ROW('FunSQL', 2021)\n=#","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"When VALUES clause contains a single row, it is emitted on the same line.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = VALUES([(\"SQL\", 1974)])\n\nprint(render(c))\n#-> VALUES ('SQL', 1974)","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"VALUES accepts a vector of scalar values.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = VALUES([\"SQL\", \"Julia\", \"FunSQL\"])\n\nprint(render(c))\n#=>\nVALUES\n 'SQL',\n 'Julia',\n 'FunSQL'\n=#","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"When VALUES is nested in a FROM clause, it is wrapped in parentheses.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = VALUES([(\"SQL\", 1974), (\"Julia\", 2012), (\"FunSQL\", 2021)]) |>\n AS(:values, columns = [:name, :year]) |>\n FROM() |>\n SELECT(FUN(\"*\"))\n\nprint(render(c))\n#=>\nSELECT *\nFROM (\n VALUES\n ('SQL', 1974),\n ('Julia', 2012),\n ('FunSQL', 2021)\n) AS \"values\" (\"name\", \"year\")\n=#","category":"page"},{"location":"test/clauses/#WINDOW-Clause","page":"SQL Clauses","title":"WINDOW Clause","text":"","category":"section"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"A WINDOW clause is created with WINDOW() constructor.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FROM(:person) |>\n WINDOW(:w1 => PARTITION(:gender_concept_id),\n :w2 => :w1 |> PARTITION(:year_of_birth, order_by = [:month_of_birth, :day_of_birth]))\n#-> (…) |> WINDOW(…)\n\ndisplay(c)\n#=>\nID(:person) |>\nFROM() |>\nWINDOW(PARTITION(ID(:gender_concept_id)) |> AS(:w1),\n ID(:w1) |>\n PARTITION(ID(:year_of_birth),\n order_by = [ID(:month_of_birth), ID(:day_of_birth)]) |>\n AS(:w2))\n=#\n\nprint(render(c |> SELECT(:w1 |> AGG(\"row_number\"), :w2 |> AGG(\"row_number\"))))\n#=>\nSELECT\n (row_number() OVER (\"w1\")),\n (row_number() OVER (\"w2\"))\nFROM \"person\"\nWINDOW\n \"w1\" AS (PARTITION BY \"gender_concept_id\"),\n \"w2\" AS (\"w1\" PARTITION BY \"year_of_birth\" ORDER BY \"month_of_birth\", \"day_of_birth\")\n=#","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"The WINDOW() constructor accepts an empty list of partitions, in which case, it is not rendered.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FROM(:person) |>\n WINDOW(args = [])\n\ndisplay(c)\n#-> ID(:person) |> FROM() |> WINDOW(args = [])\n\nprint(render(c |> SELECT(AGG(\"row_number\", over = PARTITION()))))\n#=>\nSELECT (row_number() OVER ())\nFROM \"person\"\n=#","category":"page"},{"location":"test/clauses/#WITH-Clause-and-Common-Table-Expressions","page":"SQL Clauses","title":"WITH Clause and Common Table Expressions","text":"","category":"section"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"The AS clause that defines a common table expression is created using the AS constructor.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"cte1 =\n FROM(:concept) |>\n WHERE(FUN(\"=\", :concept_id, 320128)) |>\n SELECT(:concept_id, :concept_name) |>\n AS(:essential_hypertension)\n#-> (…) |> AS(:essential_hypertension)\n\ncte2 =\n FROM(:essential_hypertension) |>\n SELECT(:concept_id, :concept_name) |>\n UNION(all = true,\n FROM(:eh => :essential_hypertension_with_descendants) |>\n JOIN(:cr => :concept_relationship,\n FUN(\"=\", (:eh, :concept_id), (:cr, :concept_id_1))) |>\n JOIN(:c => :concept,\n FUN(\"=\", (:cr, :concept_id_2), (:c, :concept_id))) |>\n WHERE(FUN(\"=\", (:cr, :relationship_id), \"Subsumes\")) |>\n SELECT((:c, :concept_id), (:c, :concept_name))) |>\n AS(:essential_hypertension_with_descendants,\n columns = [:concept_id, :concept_name])\n#-> (…) |> AS(:essential_hypertension_with_descendants, columns = […])","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"The WITH clause is created using the WITH() constructor.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FROM(:essential_hypertension_with_descendants) |>\n SELECT(*) |>\n WITH(recursive = true, cte1, cte2)\n#-> (…) |> WITH(recursive = true, …)\n\ndisplay(c)\n#=>\nID(:essential_hypertension_with_descendants) |>\nFROM() |>\nSELECT(FUN(\"*\")) |>\nWITH(recursive = true,\n ID(:concept) |>\n FROM() |>\n WHERE(FUN(\"=\", ID(:concept_id), LIT(320128))) |>\n SELECT(ID(:concept_id), ID(:concept_name)) |>\n AS(:essential_hypertension),\n ID(:essential_hypertension) |>\n FROM() |>\n SELECT(ID(:concept_id), ID(:concept_name)) |>\n UNION(all = true,\n ID(:essential_hypertension_with_descendants) |>\n AS(:eh) |>\n FROM() |>\n JOIN(ID(:concept_relationship) |> AS(:cr),\n FUN(\"=\",\n ID(:eh) |> ID(:concept_id),\n ID(:cr) |> ID(:concept_id_1))) |>\n JOIN(ID(:concept) |> AS(:c),\n FUN(\"=\",\n ID(:cr) |> ID(:concept_id_2),\n ID(:c) |> ID(:concept_id))) |>\n WHERE(FUN(\"=\", ID(:cr) |> ID(:relationship_id), LIT(\"Subsumes\"))) |>\n SELECT(ID(:c) |> ID(:concept_id), ID(:c) |> ID(:concept_name))) |>\n AS(:essential_hypertension_with_descendants,\n columns = [:concept_id, :concept_name]))\n=#\n\nprint(render(c))\n#=>\nWITH RECURSIVE \"essential_hypertension\" AS (\n SELECT\n \"concept_id\",\n \"concept_name\"\n FROM \"concept\"\n WHERE (\"concept_id\" = 320128)\n),\n\"essential_hypertension_with_descendants\" (\"concept_id\", \"concept_name\") AS (\n SELECT\n \"concept_id\",\n \"concept_name\"\n FROM \"essential_hypertension\"\n UNION ALL\n SELECT\n \"c\".\"concept_id\",\n \"c\".\"concept_name\"\n FROM \"essential_hypertension_with_descendants\" AS \"eh\"\n JOIN \"concept_relationship\" AS \"cr\" ON (\"eh\".\"concept_id\" = \"cr\".\"concept_id_1\")\n JOIN \"concept\" AS \"c\" ON (\"cr\".\"concept_id_2\" = \"c\".\"concept_id\")\n WHERE (\"cr\".\"relationship_id\" = 'Subsumes')\n)\nSELECT *\nFROM \"essential_hypertension_with_descendants\"\n=#","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"The MATERIALIZED annotation can be added using NOTE.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"cte =\n FROM(:condition_occurrence) |>\n WHERE(FUN(\"=\", :condition_concept_id, 320128)) |>\n SELECT(:person_id) |>\n NOTE(\"MATERIALIZED\") |>\n AS(:essential_hypertension_occurrence)\n#-> (…) |> AS(:essential_hypertension_occurrence)\n\ndisplay(cte)\n#=>\nID(:condition_occurrence) |>\nFROM() |>\nWHERE(FUN(\"=\", ID(:condition_concept_id), LIT(320128))) |>\nSELECT(ID(:person_id)) |>\nNOTE(\"MATERIALIZED\") |>\nAS(:essential_hypertension_occurrence)\n=#\n\nprint(render(FROM(:essential_hypertension_occurrence) |> SELECT(*) |> WITH(cte)))\n#=>\nWITH \"essential_hypertension_occurrence\" AS MATERIALIZED (\n SELECT \"person_id\"\n FROM \"condition_occurrence\"\n WHERE (\"condition_concept_id\" = 320128)\n)\nSELECT *\nFROM \"essential_hypertension_occurrence\"\n=#","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"A WITH clause without any common table expressions will be omitted.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FROM(:condition_occurrence) |>\n SELECT(*) |>\n WITH(args = [])\n#-> (…) |> WITH(args = [])\n\nprint(render(c))\n#=>\nSELECT *\nFROM \"condition_occurrence\"\n=#","category":"page"},{"location":"test/#Test-Suite","page":"Test Suite","title":"Test Suite","text":"","category":"section"},{"location":"test/","page":"Test Suite","title":"Test Suite","text":"Pages = [\n \"clauses.md\",\n \"nodes.md\",\n \"other.md\",\n]","category":"page"}] +[{"location":"two-kinds-of-sql-query-builders/#Two-Kinds-of-SQL-Query-Builders","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"","category":"section"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"CurrentModule = FunSQL","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"The SQL language has a paradoxical fate. Although it was deliberately designed to appeal to a human user, nowadays most of SQL code is written—or rather generated—by the computer. Many computer programs need to query some database, and, for the vast majority of database servers, the only supported query language is SQL. But generating SQL is difficult because of the complicated and obscure rules of its quasi-English grammar (its original name SEQUEL stands for Structured English Query Language). For this reason, programs that interact with a database often use specialized libraries for generating SQL queries.","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"One of such libraries is FunSQL. FunSQL is designed with two goals in mind: supporting the full range of SQL's querying capabilities and exposing these capabilities in a compositional, data-oriented interface. This combination of goals makes FunSQL a perfect tool for data analysis in SQL and differentiates it from all the other query building libraries. Many query builders offer good coverage of SQL features, fewer provide data-oriented interface, but only FunSQL combines them in a single package.","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"And yet the difference between FunSQL and other query builders is not immediately apparent. In fact, the interfaces of various query building libraries seem almost identical. A query that finds 100 oldest male patients (in the OMOP CDM database) is assembled with FunSQL as follows:","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"From(:person) |>\nWhere(Get.gender_concept_id .== 8507) |>\nOrder(Get.year_of_birth) |>\nLimit(100) |>\nSelect(Get.person_id)","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"The same query can be written in Ruby using Active Record Query Interface:","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"Person\n.where(\"gender_concept_id = ?\", 8507)\n.order(:year_of_birth)\n.limit(100)\n.select(:person_id)","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"Or in PHP with Laravel's Query Builder:","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"DB::table('person')\n->where('gender_concept_id', '=', 8507)\n->orderBy('year_of_birth')\n->limit(100)\n->select('person_id')","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"In C#'s EF/LINQ:","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"Person\n.Where(p => p.gender_concept_id == 8507)\n.OrderBy(p => p.year_of_birth)\n.Take(100)\n.Select(p => new { person_id = p.person_id });","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"Or in R with dbplyr:","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"tbl(conn, \"person\") %>%\nfilter(gender_concept_id == 8507) %>%\narrange(year_of_birth) %>%\nhead(100) %>%\nselect(person_id)","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"In each of these code samples, the query is assembled using essentially the same interface. Stripped of its syntactic shell, the process of assembling the query can be visualized as a diagram of five processing nodes connected in a pipeline:","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"(Image: 100 oldest male patients)","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"It is precisely the fact that the query is progressively assembled using atomic, independent components that lets us call this interface compositional.","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"However we did claim that FunSQL differs from all the other query building libraries, and now apparently proved the opposite? As a matter of fact, there is a difference, even if it is not reflected in notation. To demonstrate this, let us rearrange this pipeline, moving the Order and the Limit nodes in front of Where.","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"(Image: 100 oldest male patients ⟹ Males among 100 oldest patients)","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"How does this rearrangement affect the output of the query? Perhaps unexpectedly, the answer depends on the library. With FunSQL, as well as EF/LINQ and dbplyr, it changes the output from 100 oldest male patients to the males among 100 oldest patients. But not so with the other two libraries, Active Record and Laravel, where rearranging the pipeline has no effect on the output.","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"To summarize, the following query builders are sensitive to the order of the pipeline nodes:","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"FunSQL\nEF/LINQ\ndbplyr","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"And the following are not:","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"Active Record\nLaravel","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"These are the two kinds of query builders from this article's title. But how can these libraries act so differently while sharing the same interface? To answer this question, we need to focus on what is only implicitly present on the pipeline diagram: the information that is processed by the pipeline nodes.","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"(Image: \"Where\" node)","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"A node with one incoming and one outgoing arrow symbolizes a processing unit that takes the input data, transforms it, and emits the output data. While the character of the data is not revealed, it is tempting to assume it to be the tabular data extracted from the database.","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"(Image: \"Where\" node acting on data)","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"But this can't be right, at least not literally, because a SQL query builder cannot read the data in the database. Instead, the query builder generates a SQL query:","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"SELECT \"person_1\".\"person_id\"\nFROM \"person\" AS \"person_1\"\nWHERE (\"person_1\".\"gender_concept_id\" = 8507)\nORDER BY \"person_1\".\"year_of_birth\"\nLIMIT 100","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"But if we assume for a moment that pipeline nodes could process the data directly, we would expect that both the pipeline and the corresponding SQL query produce the same output. In other words, the role of the pipeline is to specify the expected output of the SQL query. This is how pipeline nodes are interpreted by FunSQL and the other two libraries, EF/LINQ and dbplyr. We can call such query builders data-oriented.","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"The conversion of the pipeline to SQL is not always that straightforward. Even though we could freely reorder the nodes in a pipeline, we cannot do the same to the clauses in a SQL query. This is because the SQL grammar arranges the clauses in a rigid order:","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"FROM, followed by zero, one or more\nJOIN, followed by\nWHERE, followed by\nGROUP BY, followed by\nHAVING, followed by\nORDER BY, followed by\nLIMIT, followed by\nSELECT, written at the top of the query, but the last one to perform.","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"This order is compatible with the first pipeline, in which the Where node is followed by Order and Limit, but not the second pipeline, where these nodes change their relative positions. So how could the second pipeline be converted to SQL? We would be out of options if we were still using the original SQL standard, SQL-86, but the next revision of the language, SQL-92, recognized this limitation. Regrettably, it did not relax this rigid clause order. Instead, SQL-92 introduced a workaround: a query can be extended by nesting it into the next query's FROM clause. This gives us a method for converting an arbitrary pipeline into SQL: break the pipeline into smaller chunks that comply with the SQL clause order, convert each chunk into a SQL query, and then nest all these queries together:","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"SELECT \"person_2\".\"person_id\"\nFROM (\n SELECT\n \"person_1\".\"person_id\",\n \"person_1\".\"gender_concept_id\"\n FROM \"person\" AS \"person_1\"\n ORDER BY \"person_1\".\"year_of_birth\"\n LIMIT 100\n) AS \"person_2\"\nWHERE (\"person_2\".\"gender_concept_id\" = 8507)","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"The SQL grammar has a number of deficiencies, including rigid clause order, query nesting, and nonsensical position of the SELECT clause. The position of SELECT violates the execution flow of the query, and this violation is aggravated by query nesting. Complex SQL queries often require multiple levels of nesting, which makes such queries bloated and difficult to interpret. This is where data-oriented query builders, which do not constrain the order of pipeline nodes, offer an improvement over plain SQL.","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"What about the other kind of query builders? Active Record and Laravel employ a pipeline of exactly the same form, but because it is not sensitive to the order of the nodes, it must work on a different principle. Indeed, this pipeline generates a SQL query by incrementally assembling the SQL syntax tree. Because of the rigid clause order, a SQL syntax tree can be faithfully represented as a composite data structure with slots specifying the content of the SELECT, FROM, WHERE, and the other clauses:","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"struct SQLQuery\n select\n from\n joins\n where\n groupby\n having\n orderby\n limit\nend","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"Individual slots of this structure are populated by the corresponding pipeline nodes.","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"(Image: \"Where\" node acting on the syntax tree)","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"This explains why the pipeline is insensitive to the order of the nodes. Indeed, as long as the content of the slots stays the same, it makes no difference in what order the slots are populated.","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"(Image: Pipeline is insensitive to the order of the nodes)","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"This method of incrementally constructing a composite structure is known as the builder pattern. We can call the query builders that employ this pattern syntax-oriented.","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"Both data-oriented and syntax-oriented query builders are compositional: the difference is in the nature of the information processed by the units of composition. Data-oriented query builders incrementally refine the query output; syntax-oriented query builders incrementally assemble the SQL syntax tree. Their interfaces look almost identical, but their methods of operation are fundamentally different.","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"But which one is better? Syntax-oriented query builders have two definite advantages: they are easy to implement and they could support the full range of SQL features. Indeed, the interface of a syntax-oriented query builder is just a collection of builders for the SQL syntax tree. How complete the representation of the syntax tree determines how well various SQL features are supported.","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"On the other hand, syntax-oriented query builders are harder to use. As they directly represent the SQL grammar, they inherit all of its deficiencies. In particular, the rigid clause order makes it difficult to assemble complex data processing pipelines, especially when the arrangement of pipeline nodes is not predetermined.","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"A data-oriented query builder directly represents data processing nodes, which makes assembling data processing pipelines much more straightforward—as long as we can find the necessary nodes among those offered by the builder. But where does the builder get its collection of data processing nodes? And how can we tell if this collection is complete?","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"One way to implement a data-oriented query builder is to adapt a general-purpose query framework. Indeed, this is the origin of EF/LINQ, which is adapted from LINQ, and dbplyr, which is adapted from dplyr. The query framework determines what processing nodes are available and how they operate. In principle, any query framework could be adapted to SQL databases by introducing just one new node, a node that loads the content of a database table. If we place this node at the beginning of a pipeline and make the rest of it out of regular nodes, we obtain a pipeline that processes data from a SQL database. However, this pipeline will be very inefficient compared to a SQL engine, which can use indexes to avoid loading the entire table into memory and thus can process the same data much faster. This is why EF/LINQ and dbplyr generate a SQL query that replaces the pipeline as a whole. The pipeline itself no longer runs directly, but now serves as a specification, with the assumption that if it were to run, it would produce the same output as the SQL query. This method of transforming a general-purpose query framework to a SQL query builder is called SQL pushdown.","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"However, SQL pushdown has a serious limitation. A general-purpose query framework is not designed with SQL compatibility in mind. For this reason, some of the pipelines assembled within this framework cannot be converted to SQL. Even worse, many useful SQL queries have no equivalent pipelines and thus cannot be generated using SQL pushdown. Indeed, SQL accumulated a wide range of features and capabilities since it first appeared in 1974. The first revision of the SQL standard, SQL-86, already supported Cartesian products, filtering, grouping, aggregation, and correlated subqueries. The next revision, SQL-92, added many join types and introduced query nesting. SQL:1999 greatly expanded its analytical capabilities by adding two types of queries: recursive queries, for processing hierarchical data, and data cube queries, which generalize histograms, cross-tabulations, roll-ups, drill-downs, and sub-totals. The follow-up revision, SQL:2003, added support for aggregate functions over a running window. Admittedly, SQL is a quintessential enterprise abomination, a hodgepodge of features added to support every imaginable use case, but with inadequate syntax, weird gaps in functionality, and no regards to internal consistency. Nevertheless, the breadth of SQL's capabilities has not been matched by any other query framework, including LINQ or dplyr. So when we generate SQL queries using EF/LINQ or dbplyr, a large subset of these capabilities remains inaccessible.","category":"page"},{"location":"two-kinds-of-sql-query-builders/","page":"Two Kinds of SQL Query Builders","title":"Two Kinds of SQL Query Builders","text":"FunSQL is a data-oriented query builder created specifically to expose full expressive power of SQL. Unlike EF/LINQ and dbplyr, FunSQL was not adapted from an existing query framework, but was carefully designed from scratch to match SQL's capabilities. These capabilities include, for example, support for correlated subqueries and lateral joins (with Bind node), aggregate and window functions (using Group and Partition nodes), as well as recursive queries (with Iterate node). This comprehensive support for SQL capabilities makes FunSQL the only SQL query builder suitable for assembling complex data processing pipelines. Moreover, even though FunSQL pipelines cannot be run directly, every FunSQL node has a well-defined data processing semantics, which means that, in principle, FunSQL could be developed into a full-blown query framework. This potentially opens a path for replacing SQL with an equally powerful, but a more coherent and expressive query language.","category":"page"},{"location":"reference/#API-Reference","page":"API Reference","title":"API Reference","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"CurrentModule = FunSQL","category":"page"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"FunSQL.jl\"]","category":"page"},{"location":"reference/#FunSQL.FunSQLError","page":"API Reference","title":"FunSQL.FunSQLError","text":"Base error class for all errors raised by FunSQL.\n\n\n\n\n\n","category":"type"},{"location":"reference/#render()","page":"API Reference","title":"render()","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"render.jl\"]","category":"page"},{"location":"reference/#FunSQL.render-Tuple{Any}","page":"API Reference","title":"FunSQL.render","text":"render(node; tables = Dict{Symbol, SQLTable}(),\n dialect = :default,\n cache = nothing)::SQLString\n\nCreate a SQLCatalog object and serialize the query node.\n\n\n\n\n\n","category":"method"},{"location":"reference/#FunSQL.render-Tuple{FunSQL.SQLCatalog, FunSQL.SQLNode}","page":"API Reference","title":"FunSQL.render","text":"render(catalog::Union{SQLConnection, SQLCatalog}, node::SQLNode)::SQLString\n\nSerialize the query node as a SQL statement.\n\nParameter catalog of SQLCatalog type encapsulates available database tables and the target SQL dialect. A SQLConnection object is also accepted.\n\nParameter node is a composite SQLNode object.\n\nThe function returns a SQLString value. The result is also cached (with the identity of node serving as the key) in the catalog cache.\n\nExamples\n\njulia> catalog = SQLCatalog(\n :person => SQLTable(:person, columns = [:person_id, :year_of_birth]),\n dialect = :postgresql);\n\njulia> q = From(:person) |>\n Where(Get.year_of_birth .>= 1950);\n\njulia> print(render(catalog, q))\nSELECT\n \"person_1\".\"person_id\",\n \"person_1\".\"year_of_birth\"\nFROM \"person\" AS \"person_1\"\nWHERE (\"person_1\".\"year_of_birth\" >= 1950)\n\n\n\n\n\n","category":"method"},{"location":"reference/#FunSQL.render-Tuple{FunSQL.SQLDialect, FunSQL.SQLClause}","page":"API Reference","title":"FunSQL.render","text":"render(dialect::Union{SQLConnection, SQLCatalog, SQLDialect},\n clause::SQLClause)::SQLString\n\nSerialize the syntax tree of a SQL query.\n\n\n\n\n\n","category":"method"},{"location":"reference/#reflect()","page":"API Reference","title":"reflect()","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"reflect.jl\"]","category":"page"},{"location":"reference/#FunSQL.reflect-Tuple{Any}","page":"API Reference","title":"FunSQL.reflect","text":"reflect(conn;\n schema = nothing,\n dialect = nothing,\n cache = 256)::SQLCatalog\n\nRetrieve the information about available database tables.\n\nThe function returns a SQLCatalog object. The catalog will be populated with the tables from the given database schema, or, if parameter schema is not set, from the default database schema (e.g., schema public for PostgreSQL).\n\nParameter dialect specifies the target SQLDialect. If not set, dialect will be inferred from the type of the connection object.\n\n\n\n\n\n","category":"method"},{"location":"reference/#SQLConnection-and-SQLStatement","page":"API Reference","title":"SQLConnection and SQLStatement","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"connections.jl\"]","category":"page"},{"location":"reference/#FunSQL.DB","page":"API Reference","title":"FunSQL.DB","text":"Shorthand for SQLConnection.\n\n\n\n\n\n","category":"type"},{"location":"reference/#FunSQL.SQLConnection","page":"API Reference","title":"FunSQL.SQLConnection","text":"SQLConnection(conn; catalog)\n\nWrap a raw database connection object together with a SQLCatalog object containing information about database tables.\n\n\n\n\n\n","category":"type"},{"location":"reference/#FunSQL.SQLStatement","page":"API Reference","title":"FunSQL.SQLStatement","text":"SQLStatement(conn, raw; vars = Symbol[])\n\nWrap a prepared SQL statement.\n\n\n\n\n\n","category":"type"},{"location":"reference/#DBInterface.connect-Union{Tuple{RawConnType}, Tuple{Type{FunSQL.SQLConnection{RawConnType}}, Vararg{Any}}} where RawConnType","page":"API Reference","title":"DBInterface.connect","text":"DBInterface.connect(DB{RawConnType},\n args...;\n schema = nothing,\n dialect = nothing,\n cache = 256,\n kws...)\n\nConnect to the database server, call reflect to retrieve the information about available tables and return a SQLConnection object.\n\nExtra parameters args and kws are passed to the call:\n\nDBInterface.connect(RawConnType, args...; kws...)\n\n\n\n\n\n","category":"method"},{"location":"reference/#DBInterface.execute-Tuple{FunSQL.SQLConnection, Union{FunSQL.AbstractSQLClause, FunSQL.AbstractSQLNode}, Any}","page":"API Reference","title":"DBInterface.execute","text":"DBInterface.execute(conn::SQLConnection, sql::SQLNode, params)\nDBInterface.execute(conn::SQLConnection, sql::SQLClause, params)\n\nSerialize and execute the query node.\n\n\n\n\n\n","category":"method"},{"location":"reference/#DBInterface.execute-Tuple{FunSQL.SQLConnection, Union{FunSQL.AbstractSQLClause, FunSQL.AbstractSQLNode}}","page":"API Reference","title":"DBInterface.execute","text":"DBInterface.execute(conn::SQLConnection, sql::SQLNode; params...)\nDBInterface.execute(conn::SQLConnection, sql::SQLClause; params...)\n\nSerialize and execute the query node.\n\n\n\n\n\n","category":"method"},{"location":"reference/#DBInterface.execute-Tuple{FunSQL.SQLStatement, Any}","page":"API Reference","title":"DBInterface.execute","text":"DBInterface.execute(stmt::SQLStatement, params)\n\nExecute the prepared SQL statement.\n\n\n\n\n\n","category":"method"},{"location":"reference/#DBInterface.prepare-Tuple{FunSQL.SQLConnection, FunSQL.SQLString}","page":"API Reference","title":"DBInterface.prepare","text":"DBInterface.prepare(conn::SQLConnection, str::SQLString)::SQLStatement\n\nGenerate a prepared SQL statement.\n\n\n\n\n\n","category":"method"},{"location":"reference/#DBInterface.prepare-Tuple{FunSQL.SQLConnection, Union{FunSQL.AbstractSQLClause, FunSQL.AbstractSQLNode}}","page":"API Reference","title":"DBInterface.prepare","text":"DBInterface.prepare(conn::SQLConnection, sql::SQLNode)::SQLStatement\nDBInterface.prepare(conn::SQLConnection, sql::SQLClause)::SQLStatement\n\nSerialize the query node and return a prepared SQL statement.\n\n\n\n\n\n","category":"method"},{"location":"reference/#SQLCatalog,-SQLTable,-and-SQLColumn","page":"API Reference","title":"SQLCatalog, SQLTable, and SQLColumn","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"catalogs.jl\"]","category":"page"},{"location":"reference/#FunSQL.SQLCatalog","page":"API Reference","title":"FunSQL.SQLCatalog","text":"SQLCatalog(; tables = Dict{Symbol, SQLTable}(),\n dialect = :default,\n cache = 256,\n metadata = nothing)\nSQLCatalog(tables...;\n dialect = :default, cache = 256, metadata = nothing)\n\nSQLCatalog encapsulates available database tables, the target SQL dialect, a cache of serialized queries, and an optional metadata.\n\nParameter tables is either a dictionary or a vector of SQLTable objects, where the vector will be converted to a dictionary with table names as keys. A table in the catalog can be included to a query using the From node.\n\nParameter dialect is a SQLDialect object describing the target SQL dialect.\n\nParameter cache specifies the size of the LRU cache containing results of the render function. Set cache to nothing to disable the cache, or set cache to an arbitrary Dict-like object to provide a custom cache implementation.\n\nExamples\n\njulia> person = SQLTable(:person, columns = [:person_id, :year_of_birth, :location_id]);\n\njulia> location = SQLTable(:location, columns = [:location_id, :state]);\n\njulia> catalog = SQLCatalog(person, location, dialect = :postgresql)\nSQLCatalog(SQLTable(:location, SQLColumn(:location_id), SQLColumn(:state)),\n SQLTable(:person,\n SQLColumn(:person_id),\n SQLColumn(:year_of_birth),\n SQLColumn(:location_id)),\n dialect = SQLDialect(:postgresql))\n\n\n\n\n\n","category":"type"},{"location":"reference/#FunSQL.SQLColumn","page":"API Reference","title":"FunSQL.SQLColumn","text":"SQLColumn(; name, metadata = nothing)\nSQLColumn(name; metadata = nothing)\n\nSQLColumn represents a column with the given name and optional metadata.\n\n\n\n\n\n","category":"type"},{"location":"reference/#FunSQL.SQLTable","page":"API Reference","title":"FunSQL.SQLTable","text":"SQLTable(; qualifiers = [], name, columns, metadata = nothing)\nSQLTable(name; qualifiers = [], columns, metadata = nothing)\nSQLTable(name, columns...; qualifiers = [], metadata = nothing)\n\nThe structure of a SQL table or a table-like entity (TEMP TABLE, VIEW, etc) for use as a reference in assembling SQL queries.\n\nThe SQLTable constructor expects the table name, an optional vector containing the table schema and other qualifiers, an ordered dictionary columns that maps names to columns, and an optional metadata.\n\nExamples\n\njulia> person = SQLTable(qualifiers = [\"public\"],\n name = \"person\",\n columns = [\"person_id\", \"year_of_birth\"],\n metadata = (; is_view = false))\nSQLTable(qualifiers = [:public],\n :person,\n SQLColumn(:person_id),\n SQLColumn(:year_of_birth),\n metadata = [:is_view => false])\n\n\n\n\n\n","category":"type"},{"location":"reference/#SQLDialect","page":"API Reference","title":"SQLDialect","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"dialects.jl\"]","category":"page"},{"location":"reference/#FunSQL.SQLDialect","page":"API Reference","title":"FunSQL.SQLDialect","text":"SQLDialect(; name = :default, kws...)\nSQLDialect(template::SQLDialect; kws...)\nSQLDialect(name::Symbol, kws...)\nSQLDialect(ConnType::Type)\n\nProperties and capabilities of a particular SQL dialect.\n\nUse SQLDialect(name::Symbol) to create one of the known dialects. The following names are recognized:\n\n:mysql\n:postgresql\n:redshift\n:spark\n:sqlite\n:sqlserver\n\nKeyword parameters override individual properties of a dialect. For details, check the source code.\n\nUse SQLDialect(ConnType::Type) to detect the dialect based on the type of the database connection object. The following types are recognized:\n\nLibPQ.Connection\nMySQL.Connection\nSQLite.DB\n\nExamples\n\njulia> postgresql_dialect = SQLDialect(:postgresql)\nSQLDialect(:postgresql)\n\njulia> postgresql_odbc_dialect = SQLDialect(:postgresql,\n variable_prefix = '?',\n variable_style = :positional)\nSQLDialect(:postgresql, variable_prefix = '?', variable_style = :POSITIONAL)\n\n\n\n\n\n","category":"type"},{"location":"reference/#SQLString","page":"API Reference","title":"SQLString","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"strings.jl\"]","category":"page"},{"location":"reference/#FunSQL.SQLString","page":"API Reference","title":"FunSQL.SQLString","text":"SQLString(raw; columns = nothing, vars = Symbol[])\n\nSerialized SQL query.\n\nParameter columns is a vector describing the output columns.\n\nParameter vars is a vector of query parameters (created with Var) in the order they are expected by the DBInterface.execute() function.\n\nExamples\n\njulia> person = SQLTable(:person, columns = [:person_id, :year_of_birth]);\n\njulia> q = From(person);\n\njulia> render(q)\nSQLString(\"\"\"\n SELECT\n \"person_1\".\"person_id\",\n \"person_1\".\"year_of_birth\"\n FROM \"person\" AS \"person_1\\\"\"\"\",\n columns = [SQLColumn(:person_id), SQLColumn(:year_of_birth)])\n\njulia> q = From(person) |> Where(Fun.and(Get.year_of_birth .>= Var.YEAR,\n Get.year_of_birth .< Var.YEAR .+ 10));\n\njulia> render(q, dialect = :mysql)\nSQLString(\"\"\"\n SELECT\n `person_1`.`person_id`,\n `person_1`.`year_of_birth`\n FROM `person` AS `person_1`\n WHERE\n (`person_1`.`year_of_birth` >= ?) AND\n (`person_1`.`year_of_birth` < (? + 10))\"\"\",\n columns = [SQLColumn(:person_id), SQLColumn(:year_of_birth)],\n vars = [:YEAR, :YEAR])\n\njulia> render(q, dialect = :postgresql)\nSQLString(\"\"\"\n SELECT\n \"person_1\".\"person_id\",\n \"person_1\".\"year_of_birth\"\n FROM \"person\" AS \"person_1\"\n WHERE\n (\"person_1\".\"year_of_birth\" >= $1) AND\n (\"person_1\".\"year_of_birth\" < ($1 + 10))\"\"\",\n columns = [SQLColumn(:person_id), SQLColumn(:year_of_birth)],\n vars = [:YEAR])\n\n\n\n\n\n","category":"type"},{"location":"reference/#FunSQL.pack","page":"API Reference","title":"FunSQL.pack","text":"pack(sql::SQLString, vars::Union{Dict, NamedTuple})::Vector{Any}\n\nConvert a dictionary or a named tuple of query parameters to the positional form expected by DBInterface.execute().\n\njulia> person = SQLTable(:person, columns = [:person_id, :year_of_birth]);\n\njulia> q = From(person) |> Where(Fun.and(Get.year_of_birth .>= Var.YEAR,\n Get.year_of_birth .< Var.YEAR .+ 10));\n\njulia> sql = render(q, dialect = :mysql);\n\njulia> pack(sql, (; YEAR = 1950))\n2-element Vector{Any}:\n 1950\n 1950\n\njulia> sql = render(q, dialect = :postgresql);\n\njulia> pack(sql, (; YEAR = 1950))\n1-element Vector{Any}:\n 1950\n\n\n\n\n\n","category":"function"},{"location":"reference/#SQLNode","page":"API Reference","title":"SQLNode","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"nodes.jl\"]","category":"page"},{"location":"reference/#FunSQL.AbstractSQLNode","page":"API Reference","title":"FunSQL.AbstractSQLNode","text":"A tabular or a scalar operation that can be expressed as a SQL query.\n\n\n\n\n\n","category":"type"},{"location":"reference/#FunSQL.DuplicateLabelError","page":"API Reference","title":"FunSQL.DuplicateLabelError","text":"A duplicate label where unique labels are expected.\n\n\n\n\n\n","category":"type"},{"location":"reference/#FunSQL.IllFormedError","page":"API Reference","title":"FunSQL.IllFormedError","text":"A scalar operation where a tabular operation is expected.\n\n\n\n\n\n","category":"type"},{"location":"reference/#FunSQL.InvalidArityError","page":"API Reference","title":"FunSQL.InvalidArityError","text":"Unexpected number of arguments.\n\n\n\n\n\n","category":"type"},{"location":"reference/#FunSQL.InvalidGroupingSetsError","page":"API Reference","title":"FunSQL.InvalidGroupingSetsError","text":"Grouping sets are specified incorrectly.\n\n\n\n\n\n","category":"type"},{"location":"reference/#FunSQL.RebaseError","page":"API Reference","title":"FunSQL.RebaseError","text":"A node that cannot be rebased.\n\n\n\n\n\n","category":"type"},{"location":"reference/#FunSQL.ReferenceError","page":"API Reference","title":"FunSQL.ReferenceError","text":"An undefined or an invalid reference.\n\n\n\n\n\n","category":"type"},{"location":"reference/#FunSQL.SQLNode","page":"API Reference","title":"FunSQL.SQLNode","text":"An opaque wrapper over an arbitrary SQL node.\n\n\n\n\n\n","category":"type"},{"location":"reference/#FunSQL.TabularNode","page":"API Reference","title":"FunSQL.TabularNode","text":"A node that produces tabular output.\n\n\n\n\n\n","category":"type"},{"location":"reference/#FunSQL.TransliterationError","page":"API Reference","title":"FunSQL.TransliterationError","text":"Invalid application of the @funsql macro.\n\n\n\n\n\n","category":"type"},{"location":"reference/#FunSQL.@funsql-Tuple{Any}","page":"API Reference","title":"FunSQL.@funsql","text":"Convenient notation for assembling FunSQL queries.\n\n\n\n\n\n","category":"macro"},{"location":"reference/#Agg","page":"API Reference","title":"Agg","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"nodes/aggregate.jl\"]","category":"page"},{"location":"reference/#FunSQL.Agg-Tuple","page":"API Reference","title":"FunSQL.Agg","text":"Agg(; over = nothing, name, args = [], filter = nothing)\nAgg(name; over = nothing, args = [], filter = nothing)\nAgg(name, args...; over = nothing, filter = nothing)\nAgg.name(args...; over = nothing, filter = nothing)\n\nAn application of an aggregate function.\n\nAn Agg node must be applied to the output of a Group or a Partition node. In a Group context, it is translated to a regular aggregate function, and in a Partition context, it is translated to a window function.\n\nExamples\n\nNumber of patients per year of birth.\n\njulia> person = SQLTable(:person, columns = [:person_id, :year_of_birth]);\n\njulia> q = From(:person) |>\n Group(Get.year_of_birth) |>\n Select(Get.year_of_birth, Agg.count());\n\njulia> print(render(q, tables = [person]))\nSELECT\n \"person_1\".\"year_of_birth\",\n count(*) AS \"count\"\nFROM \"person\" AS \"person_1\"\nGROUP BY \"person_1\".\"year_of_birth\"\n\nNumber of distinct states among all available locations.\n\njulia> location = SQLTable(:location, columns = [:location_id, :state]);\n\njulia> q = From(:location) |>\n Group() |>\n Select(Agg.count_distinct(Get.state));\n\njulia> print(render(q, tables = [location]))\nSELECT count(DISTINCT \"location_1\".\"state\") AS \"count_distinct\"\nFROM \"location\" AS \"location_1\"\n\nFor each patient, show the date of their latest visit to a healthcare provider.\n\njulia> person = SQLTable(:person, columns = [:person_id]);\n\njulia> visit_occurrence =\n SQLTable(:visit_occurrence, columns = [:visit_occurrence_id, :person_id, :visit_start_date]);\n\njulia> q = From(:person) |>\n LeftJoin(:visit_group => From(:visit_occurrence) |>\n Group(Get.person_id),\n on = (Get.person_id .== Get.visit_group.person_id)) |>\n Select(Get.person_id,\n :max_visit_start_date =>\n Get.visit_group |> Agg.max(Get.visit_start_date));\n\njulia> print(render(q, tables = [person, visit_occurrence]))\nSELECT\n \"person_1\".\"person_id\",\n \"visit_group_1\".\"max\" AS \"max_visit_start_date\"\nFROM \"person\" AS \"person_1\"\nLEFT JOIN (\n SELECT\n max(\"visit_occurrence_1\".\"visit_start_date\") AS \"max\",\n \"visit_occurrence_1\".\"person_id\"\n FROM \"visit_occurrence\" AS \"visit_occurrence_1\"\n GROUP BY \"visit_occurrence_1\".\"person_id\"\n) AS \"visit_group_1\" ON (\"person_1\".\"person_id\" = \"visit_group_1\".\"person_id\")\n\nFor each visit, show the number of days passed since the previous visit.\n\njulia> visit_occurrence =\n SQLTable(:visit_occurrence, columns = [:visit_occurrence_id, :person_id, :visit_start_date]);\n\njulia> q = From(:visit_occurrence) |>\n Partition(Get.person_id,\n order_by = [Get.visit_start_date]) |>\n Select(Get.person_id,\n Get.visit_start_date,\n :gap => Get.visit_start_date .- Agg.lag(Get.visit_start_date));\n\njulia> print(render(q, tables = [visit_occurrence]))\nSELECT\n \"visit_occurrence_1\".\"person_id\",\n \"visit_occurrence_1\".\"visit_start_date\",\n (\"visit_occurrence_1\".\"visit_start_date\" - (lag(\"visit_occurrence_1\".\"visit_start_date\") OVER (PARTITION BY \"visit_occurrence_1\".\"person_id\" ORDER BY \"visit_occurrence_1\".\"visit_start_date\"))) AS \"gap\"\nFROM \"visit_occurrence\" AS \"visit_occurrence_1\"\n\n\n\n\n\n","category":"method"},{"location":"reference/#Append","page":"API Reference","title":"Append","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"nodes/append.jl\"]","category":"page"},{"location":"reference/#FunSQL.Append-Tuple","page":"API Reference","title":"FunSQL.Append","text":"Append(; over = nothing, args)\nAppend(args...; over = nothing)\n\nAppend concatenates input datasets.\n\nOnly the columns that are present in every input dataset will be included to the output of Append.\n\nAn Append node is translated to a UNION ALL query:\n\nSELECT ...\nFROM $over\nUNION ALL\nSELECT ...\nFROM $(args[1])\nUNION ALL\n...\n\nExamples\n\nShow the dates of all measuments and observations.\n\njulia> measurement = SQLTable(:measurement, columns = [:measurement_id, :person_id, :measurement_date]);\n\njulia> observation = SQLTable(:observation, columns = [:observation_id, :person_id, :observation_date]);\n\njulia> q = From(:measurement) |>\n Define(:date => Get.measurement_date) |>\n Append(From(:observation) |>\n Define(:date => Get.observation_date));\n\njulia> print(render(q, tables = [measurement, observation]))\nSELECT\n \"measurement_1\".\"person_id\",\n \"measurement_1\".\"measurement_date\" AS \"date\"\nFROM \"measurement\" AS \"measurement_1\"\nUNION ALL\nSELECT\n \"observation_1\".\"person_id\",\n \"observation_1\".\"observation_date\" AS \"date\"\nFROM \"observation\" AS \"observation_1\"\n\n\n\n\n\n","category":"method"},{"location":"reference/#As","page":"API Reference","title":"As","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"nodes/as.jl\"]","category":"page"},{"location":"reference/#FunSQL.As-Tuple","page":"API Reference","title":"FunSQL.As","text":"As(; over = nothing, name)\nAs(name; over = nothing)\nname => over\n\nIn a scalar context, As specifies the name of the output column. When applied to tabular data, As wraps the data in a nested record.\n\nThe arrow operator (=>) is a shorthand notation for As.\n\nExamples\n\nShow all patient IDs.\n\njulia> person = SQLTable(:person, columns = [:person_id, :year_of_birth]);\n\njulia> q = From(:person) |> Select(:id => Get.person_id);\n\njulia> print(render(q, tables = [person]))\nSELECT \"person_1\".\"person_id\" AS \"id\"\nFROM \"person\" AS \"person_1\"\n\nShow all patients together with their state of residence.\n\njulia> person = SQLTable(:person, columns = [:person_id, :year_of_birth, :location_id]);\n\njulia> location = SQLTable(:location, columns = [:location_id, :state]);\n\njulia> q = From(:person) |>\n Join(From(:location) |> As(:location),\n on = Get.location_id .== Get.location.location_id) |>\n Select(Get.person_id, Get.location.state);\n\njulia> print(render(q, tables = [person, location]))\nSELECT\n \"person_1\".\"person_id\",\n \"location_1\".\"state\"\nFROM \"person\" AS \"person_1\"\nJOIN \"location\" AS \"location_1\" ON (\"person_1\".\"location_id\" = \"location_1\".\"location_id\")\n\n\n\n\n\n","category":"method"},{"location":"reference/#Bind","page":"API Reference","title":"Bind","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"nodes/bind.jl\"]","category":"page"},{"location":"reference/#FunSQL.Bind-Tuple","page":"API Reference","title":"FunSQL.Bind","text":"Bind(; over = nothing; args)\nBind(args...; over = nothing)\n\nThe Bind node evaluates a query with parameters. Specifically, Bind provides the values for Var parameters contained in the over node.\n\nIn a scalar context, the Bind node is translated to a correlated subquery. When Bind is applied to the joinee branch of a Join node, it is translated to a JOIN LATERAL query.\n\nExamples\n\nShow patients with at least one visit to a heathcare provider.\n\njulia> person = SQLTable(:person, columns = [:person_id]);\n\njulia> visit_occurrence = SQLTable(:visit_occurrence, columns = [:visit_occurrence_id, :person_id]);\n\njulia> q = From(:person) |>\n Where(Fun.exists(From(:visit_occurrence) |>\n Where(Get.person_id .== Var.PERSON_ID) |>\n Bind(:PERSON_ID => Get.person_id)));\n\njulia> print(render(q, tables = [person, visit_occurrence]))\nSELECT \"person_1\".\"person_id\"\nFROM \"person\" AS \"person_1\"\nWHERE (EXISTS (\n SELECT NULL AS \"_\"\n FROM \"visit_occurrence\" AS \"visit_occurrence_1\"\n WHERE (\"visit_occurrence_1\".\"person_id\" = \"person_1\".\"person_id\")\n))\n\nShow all patients together with the date of their latest visit to a heathcare provider.\n\njulia> person = SQLTable(:person, columns = [:person_id]);\n\njulia> visit_occurrence =\n SQLTable(:visit_occurrence, columns = [:visit_occurrence_id, :person_id, :visit_start_date]);\n\njulia> q = From(:person) |>\n LeftJoin(From(:visit_occurrence) |>\n Where(Get.person_id .== Var.PERSON_ID) |>\n Order(Get.visit_start_date |> Desc()) |>\n Limit(1) |>\n Bind(:PERSON_ID => Get.person_id) |>\n As(:visit),\n on = true) |>\n Select(Get.person_id, Get.visit.visit_start_date);\n\njulia> print(render(q, tables = [person, visit_occurrence]))\nSELECT\n \"person_1\".\"person_id\",\n \"visit_1\".\"visit_start_date\"\nFROM \"person\" AS \"person_1\"\nLEFT JOIN LATERAL (\n SELECT \"visit_occurrence_1\".\"visit_start_date\"\n FROM \"visit_occurrence\" AS \"visit_occurrence_1\"\n WHERE (\"visit_occurrence_1\".\"person_id\" = \"person_1\".\"person_id\")\n ORDER BY \"visit_occurrence_1\".\"visit_start_date\" DESC\n FETCH FIRST 1 ROW ONLY\n) AS \"visit_1\" ON TRUE\n\n\n\n\n\n","category":"method"},{"location":"reference/#Define","page":"API Reference","title":"Define","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"nodes/define.jl\"]","category":"page"},{"location":"reference/#FunSQL.Define-Tuple","page":"API Reference","title":"FunSQL.Define","text":"Define(; over; args = [], before = nothing, after = nothing)\nDefine(args...; over, before = nothing, after = nothing)\n\nThe Define node adds or replaces output columns.\n\nBy default, new columns are added at the end of the column list while replaced columns retain their position. Set after = true (after = ) to add both new and replaced columns at the end (after a specified column). Alternatively, set before = true (before = ) to add both new and replaced columns at the front (before the specified column).\n\nExamples\n\nShow patients who are at least 16 years old.\n\njulia> person = SQLTable(:person, columns = [:person_id, :birth_datetime]);\n\njulia> q = From(:person) |>\n Define(:age => Fun.now() .- Get.birth_datetime, before = :birth_datetime) |>\n Where(Get.age .>= \"16 years\");\n\njulia> print(render(q, tables = [person]))\nSELECT\n \"person_2\".\"person_id\",\n \"person_2\".\"age\",\n \"person_2\".\"birth_datetime\"\nFROM (\n SELECT\n \"person_1\".\"person_id\",\n (now() - \"person_1\".\"birth_datetime\") AS \"age\",\n \"person_1\".\"birth_datetime\"\n FROM \"person\" AS \"person_1\"\n) AS \"person_2\"\nWHERE (\"person_2\".\"age\" >= '16 years')\n\nConceal the year of birth of patients born before 1930.\n\njulia> person = SQLTable(:person, columns = [:person_id, :year_of_birth]);\n\njulia> q = From(:person) |>\n Define(:year_of_birth => Fun.case(Get.year_of_birth .>= 1930,\n Get.year_of_birth,\n missing));\n\njulia> print(render(q, tables = [person]))\nSELECT\n \"person_1\".\"person_id\",\n (CASE WHEN (\"person_1\".\"year_of_birth\" >= 1930) THEN \"person_1\".\"year_of_birth\" ELSE NULL END) AS \"year_of_birth\"\nFROM \"person\" AS \"person_1\"\n\n\n\n\n\n","category":"method"},{"location":"reference/#From","page":"API Reference","title":"From","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"nodes/from.jl\"]","category":"page"},{"location":"reference/#FunSQL.From-Tuple","page":"API Reference","title":"FunSQL.From","text":"From(; source)\nFrom(tbl::SQLTable)\nFrom(name::Symbol)\nFrom(^)\nFrom(df)\nFrom(f::SQLNode; columns::Vector{Symbol})\nFrom(::Nothing)\n\nFrom outputs the content of a database table.\n\nThe parameter source could be one of:\n\na SQLTable object;\na Symbol value;\na ^ object;\na DataFrame or any Tables.jl-compatible dataset;\nA SQLNode representing a table-valued function. In this case, From also requires a keyword parameter columns with a list of output columns produced by the function.\nnothing.\n\nWhen source is a symbol, it can refer to either a table in SQLCatalog or an intermediate dataset defined with the With node.\n\nThe From node is translated to a SQL query with a FROM clause:\n\nSELECT ...\nFROM $source\n\nFrom(^) must be a component of Iterate. In the context of Iterate, it refers to the output of the previous iteration.\n\nFrom(::DataFrame) is translated to a VALUES clause.\n\nFrom(nothing) emits a dataset with one row and no columns and can usually be omitted.\n\nExamples\n\nList all patients.\n\njulia> person = SQLTable(:person, columns = [:person_id, :year_of_birth]);\n\njulia> q = From(person);\n\njulia> print(render(q))\nSELECT\n \"person_1\".\"person_id\",\n \"person_1\".\"year_of_birth\"\nFROM \"person\" AS \"person_1\"\n\nList all patients.\n\njulia> person = SQLTable(:person, columns = [:person_id, :year_of_birth]);\n\njulia> q = From(:person);\n\njulia> print(render(q, tables = [person]))\nSELECT\n \"person_1\".\"person_id\",\n \"person_1\".\"year_of_birth\"\nFROM \"person\" AS \"person_1\"\n\nShow all patients diagnosed with essential hypertension.\n\njulia> person = SQLTable(:person, columns = [:person_id, :year_of_birth]);\n\njulia> condition_occurrence =\n SQLTable(:condition_occurrence,\n columns = [:condition_occurrence_id, :person_id, :condition_concept_id]);\n\njulia> q = From(:person) |>\n Where(Fun.in(Get.person_id, From(:essential_hypertension) |>\n Select(Get.person_id))) |>\n With(:essential_hypertension =>\n From(:condition_occurrence) |>\n Where(Get.condition_concept_id .== 320128));\n\njulia> print(render(q, tables = [person, condition_occurrence]))\nWITH \"essential_hypertension_1\" (\"person_id\") AS (\n SELECT \"condition_occurrence_1\".\"person_id\"\n FROM \"condition_occurrence\" AS \"condition_occurrence_1\"\n WHERE (\"condition_occurrence_1\".\"condition_concept_id\" = 320128)\n)\nSELECT\n \"person_1\".\"person_id\",\n \"person_1\".\"year_of_birth\"\nFROM \"person\" AS \"person_1\"\nWHERE (\"person_1\".\"person_id\" IN (\n SELECT \"essential_hypertension_2\".\"person_id\"\n FROM \"essential_hypertension_1\" AS \"essential_hypertension_2\"\n))\n\nShow the current date.\n\njulia> q = From(nothing) |>\n Select(Fun.current_date());\n\njulia> print(render(q))\nSELECT CURRENT_DATE AS \"current_date\"\n\njulia> q = Select(Fun.current_date());\n\njulia> print(render(q))\nSELECT CURRENT_DATE AS \"current_date\"\n\nQuery a DataFrame.\n\njulia> df = DataFrame(name = [\"SQL\", \"Julia\", \"FunSQL\"],\n year = [1974, 2012, 2021]);\n\njulia> q = From(df) |>\n Group() |>\n Select(Agg.min(Get.year), Agg.max(Get.year));\n\njulia> print(render(q))\nSELECT\n min(\"values_1\".\"year\") AS \"min\",\n max(\"values_1\".\"year\") AS \"max\"\nFROM (\n VALUES\n (1974),\n (2012),\n (2021)\n) AS \"values_1\" (\"year\")\n\nParse comma-separated numbers.\n\njulia> q = From(Fun.regexp_matches(\"2,3,5,7,11\", \"(\\\\d+)\", \"g\"),\n columns = [:captures]) |>\n Select(Fun.\"CAST(?[1] AS INTEGER)\"(Get.captures));\n\njulia> print(render(q, dialect = :postgresql))\nSELECT CAST(\"regexp_matches_1\".\"captures\"[1] AS INTEGER) AS \"_\"\nFROM regexp_matches('2,3,5,7,11', '(\\d+)', 'g') AS \"regexp_matches_1\" (\"captures\")\n\n\n\n\n\n","category":"method"},{"location":"reference/#Fun","page":"API Reference","title":"Fun","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"nodes/function.jl\"]","category":"page"},{"location":"reference/#FunSQL.Fun-Tuple","page":"API Reference","title":"FunSQL.Fun","text":"Fun(; name, args = [])\nFun(name; args = [])\nFun(name, args...)\nFun.name(args...)\n\nApplication of a SQL function or a SQL operator.\n\nA Fun node is also generated by broadcasting on SQLNode objects. Names of Julia operators (==, !=, &&, ||, !) are replaced with their SQL equivalents (=, <>, and, or, not).\n\nIf name contains only symbols, or if name starts or ends with a space, the Fun node is translated to a SQL operator.\n\nIf name contains one or more ? characters, it serves as a template of a SQL expression where ? symbols are replaced with the given arguments. Use ?? to represent a literal ? mark. Wrap the template in parentheses if this is necessary to make the SQL expression unambiguous.\n\nCertain names have a customized translation in order to generate common SQL functions and operators with irregular syntax:\n\nFun node SQL syntax\nFun.and(p₁, p₂, …) p₁ AND p₂ AND …\nFun.between(x, y, z) x BETWEEN y AND z\nFun.case(p, x, …) CASE WHEN p THEN x … END\nFun.cast(x, \"TYPE\") CAST(x AS TYPE)\nFun.concat(s₁, s₂, …) dialect-specific, e.g., (s₁ || s₂ || …)\nFun.current_date() CURRENT_DATE\nFun.current_timestamp() CURRENT_TIMESTAMP\nFun.exists(q) EXISTS q\nFun.extract(\"FIELD\", x) EXTRACT(FIELD FROM x)\nFun.in(x, q) x IN q\nFun.in(x, y₁, y₂, …) x IN (y₁, y₂, …)\nFun.is_not_null(x) x IS NOT NULL\nFun.is_null(x) x IS NULL\nFun.like(x, y) x LIKE y\nFun.not(p) NOT p\nFun.not_between(x, y, z) x NOT BETWEEN y AND z\nFun.not_exists(q) NOT EXISTS q\nFun.not_in(x, q) x NOT IN q\nFun.not_in(x, y₁, y₂, …) x NOT IN (y₁, y₂, …)\nFun.not_like(x, y) x NOT LIKE y\nFun.or(p₁, p₂, …) p₁ OR p₂ OR …\n\nExamples\n\nReplace missing values with N/A.\n\njulia> location = SQLTable(:location, columns = [:location_id, :city]);\n\njulia> q = From(:location) |>\n Select(Fun.coalesce(Get.city, \"N/A\"));\n\njulia> print(render(q, tables = [location]))\nSELECT coalesce(\"location_1\".\"city\", 'N/A') AS \"coalesce\"\nFROM \"location\" AS \"location_1\"\n\nFind patients not born in 1980.\n\njulia> person = SQLTable(:person, columns = [:person_id, :year_of_birth]);\n\njulia> q = From(:person) |>\n Where(Get.year_of_birth .!= 1980);\n\njulia> print(render(q, tables = [person]))\nSELECT\n \"person_1\".\"person_id\",\n \"person_1\".\"year_of_birth\"\nFROM \"person\" AS \"person_1\"\nWHERE (\"person_1\".\"year_of_birth\" <> 1980)\n\nFor each patient, show their age in 2000.\n\njulia> person = SQLTable(:person, columns = [:person_id, :year_of_birth]);\n\njulia> q = From(:person) |>\n Select(Fun.\"-\"(2000, Get.year_of_birth));\n\njulia> print(render(q, tables = [person]))\nSELECT (2000 - \"person_1\".\"year_of_birth\") AS \"_\"\nFROM \"person\" AS \"person_1\"\n\nFind invalid zip codes.\n\njulia> location = SQLTable(:location, columns = [:location_id, :zip]);\n\njulia> q = From(:location) |>\n Select(Fun.\" NOT SIMILAR TO '[0-9]{5}'\"(Get.zip));\n\njulia> print(render(q, tables = [location]))\nSELECT (\"location_1\".\"zip\" NOT SIMILAR TO '[0-9]{5}') AS \"_\"\nFROM \"location\" AS \"location_1\"\n\nExtract the first 3 digits of the zip code.\n\njulia> location = SQLTable(:location, columns = [:location_id, :zip]);\n\njulia> q = From(:location) |>\n Select(Fun.\"SUBSTRING(? FROM ? FOR ?)\"(Get.zip, 1, 3));\n\njulia> print(render(q, tables = [location]))\nSELECT SUBSTRING(\"location_1\".\"zip\" FROM 1 FOR 3) AS \"_\"\nFROM \"location\" AS \"location_1\"\n\n\n\n\n\n","category":"method"},{"location":"reference/#Get","page":"API Reference","title":"Get","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"nodes/get.jl\"]","category":"page"},{"location":"reference/#FunSQL.Get-Tuple","page":"API Reference","title":"FunSQL.Get","text":"Get(; over, name)\nGet(name; over)\nGet.name Get.\"name\" Get[name] Get[\"name\"]\nover.name over.\"name\" over[name] over[\"name\"]\nname\n\nA reference to a column of the input dataset.\n\nWhen a column reference is ambiguous (e.g., with Join), use As to disambiguate the columns, and a chained Get node (Get.a.b.….z) to refer to a column wrapped with … |> As(:b) |> As(:a).\n\nExamples\n\nList patient IDs.\n\njulia> person = SQLTable(:person, columns = [:person_id, :year_of_birth]);\n\njulia> q = From(:person) |>\n Select(Get(:person_id));\n\njulia> print(render(q, tables = [person]))\nSELECT \"person_1\".\"person_id\"\nFROM \"person\" AS \"person_1\"\n\nShow patients with their state of residence.\n\njulia> person = SQLTable(:person, columns = [:person_id, :year_of_birth, :location_id]);\n\njulia> location = SQLTable(:location, columns = [:location_id, :state]);\n\njulia> q = From(:person) |>\n Join(From(:location) |> As(:location),\n on = Get.location_id .== Get.location.location_id) |>\n Select(Get.person_id, Get.location.state);\n\njulia> print(render(q, tables = [person, location]))\nSELECT\n \"person_1\".\"person_id\",\n \"location_1\".\"state\"\nFROM \"person\" AS \"person_1\"\nJOIN \"location\" AS \"location_1\" ON (\"person_1\".\"location_id\" = \"location_1\".\"location_id\")\n\n\n\n\n\n","category":"method"},{"location":"reference/#Group","page":"API Reference","title":"Group","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"nodes/group.jl\"]","category":"page"},{"location":"reference/#FunSQL.Group-Tuple","page":"API Reference","title":"FunSQL.Group","text":"Group(; over, by = [], sets = sets, name = nothing)\nGroup(by...; over, sets = sets, name = nothing)\n\nThe Group node summarizes the input dataset.\n\nSpecifically, Group outputs all unique values of the given grouping key. This key partitions the input rows into disjoint groups that are summarized by aggregate functions Agg applied to the output of Group. The parameter sets specifies the grouping sets, either with grouping mode indicators :cube or :rollup, or explicitly as Vector{Vector{Symbol}}. An optional parameter name specifies the field to hold the group.\n\nThe Group node is translated to a SQL query with a GROUP BY clause:\n\nSELECT ...\nFROM $over\nGROUP BY $by...\n\nExamples\n\nTotal number of patients.\n\njulia> person = SQLTable(:person, columns = [:person_id, :year_of_birth]);\n\njulia> q = From(:person) |>\n Group() |>\n Select(Agg.count());\n\njulia> print(render(q, tables = [person]))\nSELECT count(*) AS \"count\"\nFROM \"person\" AS \"person_1\"\n\nNumber of patients per year of birth.\n\njulia> person = SQLTable(:person, columns = [:person_id, :year_of_birth]);\n\njulia> q = From(:person) |>\n Group(Get.year_of_birth) |>\n Select(Get.year_of_birth, Agg.count());\n\njulia> print(render(q, tables = [person]))\nSELECT\n \"person_1\".\"year_of_birth\",\n count(*) AS \"count\"\nFROM \"person\" AS \"person_1\"\nGROUP BY \"person_1\".\"year_of_birth\"\n\nThe same example using an explicit group name.\n\njulia> person = SQLTable(:person, columns = [:person_id, :year_of_birth]);\n\njulia> q = From(:person) |>\n Group(Get.year_of_birth, name = :person) |>\n Select(Get.year_of_birth, Get.person |> Agg.count());\n\njulia> print(render(q, tables = [person]))\nSELECT\n \"person_1\".\"year_of_birth\",\n count(*) AS \"count\"\nFROM \"person\" AS \"person_1\"\nGROUP BY \"person_1\".\"year_of_birth\"\n\nNumber of patients per year of birth and the total number of patients.\n\njulia> person = SQLTable(:person, columns = [:person_id, :year_of_birth]);\n\njulia> q = From(:person) |>\n Group(Get.year_of_birth, sets = :cube) |>\n Select(Get.year_of_birth, Agg.count());\n\njulia> print(render(q, tables = [person]))\nSELECT\n \"person_1\".\"year_of_birth\",\n count(*) AS \"count\"\nFROM \"person\" AS \"person_1\"\nGROUP BY CUBE(\"person_1\".\"year_of_birth\")\n\nDistinct states across all available locations.\n\njulia> location = SQLTable(:location, columns = [:location_id, :state]);\n\njulia> q = From(:location) |>\n Group(Get.state);\n\njulia> print(render(q, tables = [location]))\nSELECT DISTINCT \"location_1\".\"state\"\nFROM \"location\" AS \"location_1\"\n\n\n\n\n\n","category":"method"},{"location":"reference/#Highlight","page":"API Reference","title":"Highlight","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"nodes/highlight.jl\"]","category":"page"},{"location":"reference/#FunSQL.Highlight-Tuple","page":"API Reference","title":"FunSQL.Highlight","text":"Highlight(; over = nothing; color)\nHighlight(color; over = nothing)\n\nHighlight over with the given color.\n\nThe highlighted node is printed with the selected color when the query containing it is displayed.\n\nAvailable colors can be found in Base.text_colors.\n\nExamples\n\njulia> q = From(:person) |>\n Select(Get.person_id |> Highlight(:bold))\nlet q1 = From(:person),\n q2 = q1 |> Select(Get.person_id)\n q2\nend\n\n\n\n\n\n","category":"method"},{"location":"reference/#Iterate","page":"API Reference","title":"Iterate","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"nodes/iterate.jl\"]","category":"page"},{"location":"reference/#FunSQL.Iterate-Tuple","page":"API Reference","title":"FunSQL.Iterate","text":"Iterate(; over = nothing, iterator)\nIterate(iterator; over = nothing)\n\nIterate generates the concatenated output of an iterated query.\n\nThe over query is evaluated first. Then the iterator query is repeatedly applied: to the output of over, then to the output of its previous run, and so on, until the iterator produces no data. All these outputs are concatenated to generate the output of Iterate.\n\nThe iterator query may explicitly refer to the output of the previous run using From(^) notation.\n\nThe Iterate node is translated to a recursive common table expression:\n\nWITH RECURSIVE iterator AS (\n SELECT ...\n FROM $over\n UNION ALL\n SELECT ...\n FROM $iterator\n)\nSELECT ...\nFROM iterator\n\nExamples\n\nCalculate the factorial.\n\njulia> q = Define(:n => 1, :f => 1) |>\n Iterate(From(^) |>\n Where(Get.n .< 10) |>\n Define(:n => Get.n .+ 1, :f => Get.f .* (Get.n .+ 1)));\n\njulia> print(render(q))\nWITH RECURSIVE \"__1\" (\"n\", \"f\") AS (\n SELECT\n 1 AS \"n\",\n 1 AS \"f\"\n UNION ALL\n SELECT\n (\"__2\".\"n\" + 1) AS \"n\",\n (\"__2\".\"f\" * (\"__2\".\"n\" + 1)) AS \"f\"\n FROM \"__1\" AS \"__2\"\n WHERE (\"__2\".\"n\" < 10)\n)\nSELECT\n \"__3\".\"n\",\n \"__3\".\"f\"\nFROM \"__1\" AS \"__3\"\n\n*Calculate the factorial, with implicit From(^).\n\njulia> q = Define(:n => 1, :f => 1) |>\n Iterate(Where(Get.n .< 10) |>\n Define(:n => Get.n .+ 1, :f => Get.f .* (Get.n .+ 1)));\n\njulia> print(render(q))\nWITH RECURSIVE \"__1\" (\"n\", \"f\") AS (\n SELECT\n 1 AS \"n\",\n 1 AS \"f\"\n UNION ALL\n SELECT\n (\"__2\".\"n\" + 1) AS \"n\",\n (\"__2\".\"f\" * (\"__2\".\"n\" + 1)) AS \"f\"\n FROM \"__1\" AS \"__2\"\n WHERE (\"__2\".\"n\" < 10)\n)\nSELECT\n \"__3\".\"n\",\n \"__3\".\"f\"\nFROM \"__1\" AS \"__3\"\n\n\n\n\n\n","category":"method"},{"location":"reference/#Join","page":"API Reference","title":"Join","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"nodes/join.jl\"]","category":"page"},{"location":"reference/#FunSQL.CrossJoin-Tuple","page":"API Reference","title":"FunSQL.CrossJoin","text":"An alias for Join(...; ..., on = true).\n\n\n\n\n\n","category":"method"},{"location":"reference/#FunSQL.Join-Tuple","page":"API Reference","title":"FunSQL.Join","text":"Join(; over = nothing, joinee, on, left = false, right = false, optional = false)\nJoin(joinee; over = nothing, on, left = false, right = false, optional = false)\nJoin(joinee, on; over = nothing, left = false, right = false, optional = false)\n\nJoin correlates two input datasets.\n\nThe Join node is translated to a query with a JOIN clause:\n\nSELECT ...\nFROM $over\nJOIN $joinee ON $on\n\nYou can specify the join type:\n\nINNER JOIN (the default);\nLEFT JOIN (left = true or LeftJoin);\nRIGHT JOIN (right = true);\nFULL JOIN (both left = true and right = true);\nCROSS JOIN (on = true).\n\nWhen optional is set, the JOIN clause is omitted if the query does not depend on any columns from the joinee branch.\n\nTo make a lateral join, apply Bind to the joinee branch.\n\nUse As to disambiguate output columns.\n\nExamples\n\nShow patients with their state of residence.\n\njulia> person = SQLTable(:person, columns = [:person_id, :location_id]);\n\njulia> location = SQLTable(:location, columns = [:location_id, :state]);\n\njulia> q = From(:person) |>\n Join(:location => From(:location),\n Get.location_id .== Get.location.location_id) |>\n Select(Get.person_id, Get.location.state);\n\njulia> print(render(q, tables = [person, location]))\nSELECT\n \"person_1\".\"person_id\",\n \"location_1\".\"state\"\nFROM \"person\" AS \"person_1\"\nJOIN \"location\" AS \"location_1\" ON (\"person_1\".\"location_id\" = \"location_1\".\"location_id\")\n\n\n\n\n\n","category":"method"},{"location":"reference/#FunSQL.LeftJoin-Tuple","page":"API Reference","title":"FunSQL.LeftJoin","text":"An alias for Join(...; ..., left = true).\n\n\n\n\n\n","category":"method"},{"location":"reference/#Limit","page":"API Reference","title":"Limit","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"nodes/limit.jl\"]","category":"page"},{"location":"reference/#FunSQL.Limit-Tuple","page":"API Reference","title":"FunSQL.Limit","text":"Limit(; over = nothing, offset = nothing, limit = nothing)\nLimit(limit; over = nothing, offset = nothing)\nLimit(offset, limit; over = nothing)\nLimit(start:stop; over = nothing)\n\nThe Limit node skips the first offset rows and then emits the next limit rows.\n\nTo make the output deterministic, Limit must be applied directly after an Order node.\n\nThe Limit node is translated to a query with a LIMIT or a FETCH clause:\n\nSELECT ...\nFROM $over\nOFFSET $offset ROWS\nFETCH NEXT $limit ROWS ONLY\n\nExamples\n\nShow the oldest patient.\n\njulia> person = SQLTable(:person, columns = [:person_id, :year_of_birth]);\n\njulia> q = From(:person) |>\n Order(Get.year_of_birth) |>\n Limit(1);\n\njulia> print(render(q, tables = [person]))\nSELECT\n \"person_1\".\"person_id\",\n \"person_1\".\"year_of_birth\"\nFROM \"person\" AS \"person_1\"\nORDER BY \"person_1\".\"year_of_birth\"\nFETCH FIRST 1 ROW ONLY\n\n\n\n\n\n","category":"method"},{"location":"reference/#Lit","page":"API Reference","title":"Lit","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"nodes/literal.jl\"]","category":"page"},{"location":"reference/#FunSQL.Lit-Tuple","page":"API Reference","title":"FunSQL.Lit","text":"Lit(; val)\nLit(val)\n\nA SQL literal.\n\nIn a context where a SQL node is expected, missing, numbers, strings, and datetime values are automatically converted to SQL literals.\n\nExamples\n\njulia> q = Select(:null => missing,\n :boolean => true,\n :integer => 42,\n :text => \"SQL is fun!\",\n :date => Date(2000));\n\njulia> print(render(q))\nSELECT\n NULL AS \"null\",\n TRUE AS \"boolean\",\n 42 AS \"integer\",\n 'SQL is fun!' AS \"text\",\n '2000-01-01' AS \"date\"\n\n\n\n\n\n","category":"method"},{"location":"reference/#Order","page":"API Reference","title":"Order","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"nodes/order.jl\"]","category":"page"},{"location":"reference/#FunSQL.Order-Tuple","page":"API Reference","title":"FunSQL.Order","text":"Order(; over = nothing, by)\nOrder(by...; over = nothing)\n\nOrder sorts the input rows by the given key.\n\nThe Ordernode is translated to a query with an ORDER BY clause:\n\nSELECT ...\nFROM $over\nORDER BY $by...\n\nSpecify the sort order with Asc, Desc, or Sort.\n\nExamples\n\nList patients ordered by their age.\n\njulia> person = SQLTable(:person, columns = [:person_id, :year_of_birth]);\n\njulia> q = From(:person) |>\n Order(Get.year_of_birth);\n\njulia> print(render(q, tables = [person]))\nSELECT\n \"person_1\".\"person_id\",\n \"person_1\".\"year_of_birth\"\nFROM \"person\" AS \"person_1\"\nORDER BY \"person_1\".\"year_of_birth\"\n\n\n\n\n\n","category":"method"},{"location":"reference/#Over","page":"API Reference","title":"Over","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"nodes/over.jl\"]","category":"page"},{"location":"reference/#FunSQL.Over-Tuple","page":"API Reference","title":"FunSQL.Over","text":"Over(; over = nothing, arg, materialized = nothing)\nOver(arg; over = nothing, materialized = nothing)\n\nbase |> Over(arg) is an alias for With(base, over = arg).\n\nExamples\n\njulia> person = SQLTable(:person, columns = [:person_id, :year_of_birth]);\n\njulia> condition_occurrence =\n SQLTable(:condition_occurrence, columns = [:condition_occurrence_id,\n :person_id,\n :condition_concept_id]);\n\njulia> q = From(:condition_occurrence) |>\n Where(Get.condition_concept_id .== 320128) |>\n As(:essential_hypertension) |>\n Over(From(:person) |>\n Where(Fun.in(Get.person_id, From(:essential_hypertension) |>\n Select(Get.person_id))));\n\njulia> print(render(q, tables = [person, condition_occurrence]))\nWITH \"essential_hypertension_1\" (\"person_id\") AS (\n SELECT \"condition_occurrence_1\".\"person_id\"\n FROM \"condition_occurrence\" AS \"condition_occurrence_1\"\n WHERE (\"condition_occurrence_1\".\"condition_concept_id\" = 320128)\n)\nSELECT\n \"person_1\".\"person_id\",\n \"person_1\".\"year_of_birth\"\nFROM \"person\" AS \"person_1\"\nWHERE (\"person_1\".\"person_id\" IN (\n SELECT \"essential_hypertension_2\".\"person_id\"\n FROM \"essential_hypertension_1\" AS \"essential_hypertension_2\"\n))\n\n\n\n\n\n","category":"method"},{"location":"reference/#Partition","page":"API Reference","title":"Partition","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"nodes/partition.jl\"]","category":"page"},{"location":"reference/#FunSQL.Partition-Tuple","page":"API Reference","title":"FunSQL.Partition","text":"Partition(; over, by = [], order_by = [], frame = nothing, name = nothing)\nPartition(by...; over, order_by = [], frame = nothing, name = nothing)\n\nThe Partition node relates adjacent rows.\n\nSpecifically, Partition specifies how to relate each row to the adjacent rows in the same dataset. The rows are partitioned by the given key and ordered within each partition using order_by key. The parameter frame customizes the extent of related rows. These related rows are summarized by aggregate functions Agg applied to the output of Partition. An optional parameter name specifies the field to hold the partition.\n\nThe Partition node is translated to a query with a WINDOW clause:\n\nSELECT ...\nFROM $over\nWINDOW w AS (PARTITION BY $by... ORDER BY $order_by...)\n\nExamples\n\nEnumerate patients' visits.\n\njulia> visit_occurrence =\n SQLTable(:visit_occurrence, columns = [:visit_occurrence_id, :person_id, :visit_start_date]);\n\njulia> q = From(:visit_occurrence) |>\n Partition(Get.person_id, order_by = [Get.visit_start_date]) |>\n Select(Agg.row_number(), Get.visit_occurrence_id);\n\njulia> print(render(q, tables = [visit_occurrence]))\nSELECT\n (row_number() OVER (PARTITION BY \"visit_occurrence_1\".\"person_id\" ORDER BY \"visit_occurrence_1\".\"visit_start_date\")) AS \"row_number\",\n \"visit_occurrence_1\".\"visit_occurrence_id\"\nFROM \"visit_occurrence\" AS \"visit_occurrence_1\"\n\nThe same example using an explicit partition name.\n\njulia> visit_occurrence =\n SQLTable(:visit_occurrence, columns = [:visit_occurrence_id, :person_id, :visit_start_date]);\n\njulia> q = From(:visit_occurrence) |>\n Partition(Get.person_id, order_by = [Get.visit_start_date], name = :visit_by_person) |>\n Select(Get.visit_by_person |> Agg.row_number(), Get.visit_occurrence_id);\n\njulia> print(render(q, tables = [visit_occurrence]))\nSELECT\n (row_number() OVER (PARTITION BY \"visit_occurrence_1\".\"person_id\" ORDER BY \"visit_occurrence_1\".\"visit_start_date\")) AS \"row_number\",\n \"visit_occurrence_1\".\"visit_occurrence_id\"\nFROM \"visit_occurrence\" AS \"visit_occurrence_1\"\n\nCalculate the moving average of the number of patients by the year of birth.\n\njulia> person = SQLTable(:person, columns = [:person_id, :year_of_birth]);\n\njulia> q = From(:person) |>\n Group(Get.year_of_birth) |>\n Partition(order_by = [Get.year_of_birth],\n frame = (mode = :range, start = -1, finish = 1)) |>\n Select(Get.year_of_birth, Agg.avg(Agg.count()));\n\njulia> print(render(q, tables = [person]))\nSELECT\n \"person_1\".\"year_of_birth\",\n (avg(count(*)) OVER (ORDER BY \"person_1\".\"year_of_birth\" RANGE BETWEEN 1 PRECEDING AND 1 FOLLOWING)) AS \"avg\"\nFROM \"person\" AS \"person_1\"\nGROUP BY \"person_1\".\"year_of_birth\"\n\n\n\n\n\n","category":"method"},{"location":"reference/#Select","page":"API Reference","title":"Select","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"nodes/select.jl\"]","category":"page"},{"location":"reference/#FunSQL.Select-Tuple","page":"API Reference","title":"FunSQL.Select","text":"Select(; over; args)\nSelect(args...; over)\n\nThe Select node specifies the output columns.\n\nSELECT $args...\nFROM $over\n\nSet the column labels with As.\n\nExamples\n\nList patient IDs and their age.\n\njulia> person = SQLTable(:person, columns = [:person_id, :birth_datetime]);\n\njulia> q = From(:person) |>\n Select(Get.person_id,\n :age => Fun.now() .- Get.birth_datetime);\n\njulia> print(render(q, tables = [person]))\nSELECT\n \"person_1\".\"person_id\",\n (now() - \"person_1\".\"birth_datetime\") AS \"age\"\nFROM \"person\" AS \"person_1\"\n\n\n\n\n\n","category":"method"},{"location":"reference/#Sort,-Asc,-and-Desc","page":"API Reference","title":"Sort, Asc, and Desc","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"nodes/sort.jl\"]","category":"page"},{"location":"reference/#FunSQL.Asc-Tuple{}","page":"API Reference","title":"FunSQL.Asc","text":"Asc(; over = nothing, nulls = nothing)\n\nAscending order indicator.\n\n\n\n\n\n","category":"method"},{"location":"reference/#FunSQL.Desc-Tuple{}","page":"API Reference","title":"FunSQL.Desc","text":"Desc(; over = nothing, nulls = nothing)\n\nDescending order indicator.\n\n\n\n\n\n","category":"method"},{"location":"reference/#FunSQL.Sort-Tuple","page":"API Reference","title":"FunSQL.Sort","text":"Sort(; over = nothing, value, nulls = nothing)\nSort(value; over = nothing, nulls = nothing)\nAsc(; over = nothing, nulls = nothing)\nDesc(; over = nothing, nulls = nothing)\n\nSort order indicator.\n\nUse with Order or Partition nodes.\n\nExamples\n\nList patients ordered by their age.\n\njulia> person = SQLTable(:person, columns = [:person_id, :year_of_birth]);\n\njulia> q = From(:person) |>\n Order(Get.year_of_birth |> Desc(nulls = :first));\n\njulia> print(render(q, tables = [person]))\nSELECT\n \"person_1\".\"person_id\",\n \"person_1\".\"year_of_birth\"\nFROM \"person\" AS \"person_1\"\nORDER BY \"person_1\".\"year_of_birth\" DESC NULLS FIRST\n\n\n\n\n\n","category":"method"},{"location":"reference/#Var","page":"API Reference","title":"Var","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"nodes/variable.jl\"]","category":"page"},{"location":"reference/#FunSQL.Var-Tuple","page":"API Reference","title":"FunSQL.Var","text":"Var(; name)\nVar(name)\nVar.name Var.\"name\" Var[name] Var[\"name\"]\n\nA reference to a query parameter.\n\nSpecify the value for the parameter with Bind to create a correlated subquery or a lateral join.\n\nExamples\n\njulia> person = SQLTable(:person, columns = [:person_id, :year_of_birth]);\n\njulia> q = From(:person) |>\n Where(Get.year_of_birth .> Var.YEAR);\n\njulia> print(render(q, tables = [person]))\nSELECT\n \"person_1\".\"person_id\",\n \"person_1\".\"year_of_birth\"\nFROM \"person\" AS \"person_1\"\nWHERE (\"person_1\".\"year_of_birth\" > :YEAR)\n\n\n\n\n\n","category":"method"},{"location":"reference/#Where","page":"API Reference","title":"Where","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"nodes/where.jl\"]","category":"page"},{"location":"reference/#FunSQL.Where-Tuple","page":"API Reference","title":"FunSQL.Where","text":"Where(; over = nothing, condition)\nWhere(condition; over = nothing)\n\nThe Where node filters the input rows by the given condition.\n\nWhere is translated to a SQL query with a WHERE clause:\n\nSELECT ...\nFROM $over\nWHERE $condition\n\nExamples\n\njulia> person = SQLTable(:person, columns = [:person_id, :year_of_birth]);\n\njulia> q = From(:person) |>\n Where(Fun(\">\", Get.year_of_birth, 2000));\n\njulia> print(render(q, tables = [person]))\nSELECT\n \"person_1\".\"person_id\",\n \"person_1\".\"year_of_birth\"\nFROM \"person\" AS \"person_1\"\nWHERE (\"person_1\".\"year_of_birth\" > 2000)\n\n\n\n\n\n","category":"method"},{"location":"reference/#With","page":"API Reference","title":"With","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"nodes/with.jl\"]","category":"page"},{"location":"reference/#FunSQL.With-Tuple","page":"API Reference","title":"FunSQL.With","text":"With(; over = nothing, args, materialized = nothing)\nWith(args...; over = nothing, materialized = nothing)\n\nWith assigns a name to a temporary dataset. The dataset content can be retrieved within the over query using the From node.\n\nWith is translated to a common table expression:\n\nWITH $args...\nSELECT ...\nFROM $over\n\nExamples\n\njulia> person = SQLTable(:person, columns = [:person_id, :year_of_birth]);\n\njulia> condition_occurrence =\n SQLTable(:condition_occurrence, columns = [:condition_occurrence_id,\n :person_id,\n :condition_concept_id]);\n\njulia> q = From(:person) |>\n Where(Fun.in(Get.person_id, From(:essential_hypertension) |>\n Select(Get.person_id))) |>\n With(:essential_hypertension =>\n From(:condition_occurrence) |>\n Where(Get.condition_concept_id .== 320128));\n\njulia> print(render(q, tables = [person, condition_occurrence]))\nWITH \"essential_hypertension_1\" (\"person_id\") AS (\n SELECT \"condition_occurrence_1\".\"person_id\"\n FROM \"condition_occurrence\" AS \"condition_occurrence_1\"\n WHERE (\"condition_occurrence_1\".\"condition_concept_id\" = 320128)\n)\nSELECT\n \"person_1\".\"person_id\",\n \"person_1\".\"year_of_birth\"\nFROM \"person\" AS \"person_1\"\nWHERE (\"person_1\".\"person_id\" IN (\n SELECT \"essential_hypertension_2\".\"person_id\"\n FROM \"essential_hypertension_1\" AS \"essential_hypertension_2\"\n))\n\n\n\n\n\n","category":"method"},{"location":"reference/#WithExternal","page":"API Reference","title":"WithExternal","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"nodes/with_external.jl\"]","category":"page"},{"location":"reference/#FunSQL.WithExternal-Tuple","page":"API Reference","title":"FunSQL.WithExternal","text":"WithExternal(; over = nothing, args, qualifiers = [], handler = nothing)\nWithExternal(args...; over = nothing, qualifiers = [], handler = nothing)\n\nWithExternal assigns a name to a temporary dataset. The dataset content can be retrieved within the over query using the From node.\n\nThe definition of the dataset is converted to a Pair{SQLTable, SQLClause} object and sent to handler, which can use it, for instance, to construct a SELECT INTO statement.\n\nExamples\n\njulia> person = SQLTable(:person, columns = [:person_id, :year_of_birth]);\n\njulia> condition_occurrence =\n SQLTable(:condition_occurrence, columns = [:condition_occurrence_id,\n :person_id,\n :condition_concept_id]);\n\njulia> handler((tbl, def)) =\n println(\"CREATE TEMP TABLE \", render(ID(tbl.name)), \" AS\\n\",\n render(def), \";\\n\");\n\njulia> q = From(:person) |>\n Where(Fun.in(Get.person_id, From(:essential_hypertension) |>\n Select(Get.person_id))) |>\n WithExternal(:essential_hypertension =>\n From(:condition_occurrence) |>\n Where(Get.condition_concept_id .== 320128),\n handler = handler);\n\njulia> print(render(q, tables = [person, condition_occurrence]))\nCREATE TEMP TABLE \"essential_hypertension\" AS\nSELECT \"condition_occurrence_1\".\"person_id\"\nFROM \"condition_occurrence\" AS \"condition_occurrence_1\"\nWHERE (\"condition_occurrence_1\".\"condition_concept_id\" = 320128);\n\nSELECT\n \"person_1\".\"person_id\",\n \"person_1\".\"year_of_birth\"\nFROM \"person\" AS \"person_1\"\nWHERE (\"person_1\".\"person_id\" IN (\n SELECT \"essential_hypertension_1\".\"person_id\"\n FROM \"essential_hypertension\" AS \"essential_hypertension_1\"\n))\n\n\n\n\n\n","category":"method"},{"location":"reference/#SQLClause","page":"API Reference","title":"SQLClause","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"clauses.jl\"]","category":"page"},{"location":"reference/#FunSQL.AbstractSQLClause","page":"API Reference","title":"FunSQL.AbstractSQLClause","text":"A component of a SQL syntax tree.\n\n\n\n\n\n","category":"type"},{"location":"reference/#FunSQL.SQLClause","page":"API Reference","title":"FunSQL.SQLClause","text":"An opaque wrapper over an arbitrary SQL clause.\n\n\n\n\n\n","category":"type"},{"location":"reference/#AGG","page":"API Reference","title":"AGG","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"clauses/aggregate.jl\"]","category":"page"},{"location":"reference/#FunSQL.AGG-Tuple","page":"API Reference","title":"FunSQL.AGG","text":"AGG(; name, args = [], filter = nothing, over = nothing)\nAGG(name; args = [], filter = nothing, over = nothing)\nAGG(name, args...; filter = nothing, over = nothing)\n\nAn application of an aggregate function.\n\nExamples\n\njulia> c = AGG(:max, :year_of_birth);\n\njulia> print(render(c))\nmax(\"year_of_birth\")\n\njulia> c = AGG(:count, filter = FUN(\">\", :year_of_birth, 1970));\n\njulia> print(render(c))\n(count(*) FILTER (WHERE (\"year_of_birth\" > 1970)))\n\njulia> c = AGG(:row_number, over = PARTITION(:year_of_birth));\n\njulia> print(render(c))\n(row_number() OVER (PARTITION BY \"year_of_birth\"))\n\n\n\n\n\n","category":"method"},{"location":"reference/#AS","page":"API Reference","title":"AS","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"clauses/as.jl\"]","category":"page"},{"location":"reference/#FunSQL.AS-Tuple","page":"API Reference","title":"FunSQL.AS","text":"AS(; over = nothing, name, columns = nothing)\nAS(name; over = nothing, columns = nothing)\n\nAn AS clause.\n\nExamples\n\njulia> c = ID(:person) |> AS(:p);\n\njulia> print(render(c))\n\"person\" AS \"p\"\n\njulia> c = ID(:person) |> AS(:p, columns = [:person_id, :year_of_birth]);\n\njulia> print(render(c))\n\"person\" AS \"p\" (\"person_id\", \"year_of_birth\")\n\n\n\n\n\n","category":"method"},{"location":"reference/#FROM","page":"API Reference","title":"FROM","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"clauses/from.jl\"]","category":"page"},{"location":"reference/#FunSQL.FROM-Tuple","page":"API Reference","title":"FunSQL.FROM","text":"FROM(; over = nothing)\nFROM(over)\n\nA FROM clause.\n\nExamples\n\njulia> c = ID(:person) |> AS(:p) |> FROM() |> SELECT((:p, :person_id));\n\njulia> print(render(c))\nSELECT \"p\".\"person_id\"\nFROM \"person\" AS \"p\"\n\n\n\n\n\n","category":"method"},{"location":"reference/#FUN","page":"API Reference","title":"FUN","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"clauses/function.jl\"]","category":"page"},{"location":"reference/#FunSQL.FUN-Tuple","page":"API Reference","title":"FunSQL.FUN","text":"FUN(; name, args = [])\nFUN(name; args = [])\nFUN(name, args...)\n\nAn invocation of a SQL function or a SQL operator.\n\nExamples\n\njulia> c = FUN(:concat, :city, \", \", :state);\n\njulia> print(render(c))\nconcat(\"city\", ', ', \"state\")\n\njulia> c = FUN(\"||\", :city, \", \", :state);\n\njulia> print(render(c))\n(\"city\" || ', ' || \"state\")\n\njulia> c = FUN(\"SUBSTRING(? FROM ? FOR ?)\", :zip, 1, 3);\n\njulia> print(render(c))\nSUBSTRING(\"zip\" FROM 1 FOR 3)\n\n\n\n\n\n","category":"method"},{"location":"reference/#GROUP","page":"API Reference","title":"GROUP","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"clauses/group.jl\"]","category":"page"},{"location":"reference/#FunSQL.GROUP-Tuple","page":"API Reference","title":"FunSQL.GROUP","text":"GROUP(; over = nothing, by = [], sets = nothing)\nGROUP(by...; over = nothing, sets = nothing)\n\nA GROUP BY clause.\n\nExamples\n\njulia> c = FROM(:person) |>\n GROUP(:year_of_birth) |>\n SELECT(:year_of_birth, AGG(:count));\n\njulia> print(render(c))\nSELECT\n \"year_of_birth\",\n count(*)\nFROM \"person\"\nGROUP BY \"year_of_birth\"\n\n\n\n\n\n","category":"method"},{"location":"reference/#HAVING","page":"API Reference","title":"HAVING","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"clauses/having.jl\"]","category":"page"},{"location":"reference/#FunSQL.HAVING-Tuple","page":"API Reference","title":"FunSQL.HAVING","text":"HAVING(; over = nothing, condition)\nHAVING(condition; over = nothing)\n\nA HAVING clause.\n\nExamples\n\njulia> c = FROM(:person) |>\n GROUP(:year_of_birth) |>\n HAVING(FUN(\">\", AGG(:count), 10)) |>\n SELECT(:person_id);\n\njulia> print(render(c))\nSELECT \"person_id\"\nFROM \"person\"\nGROUP BY \"year_of_birth\"\nHAVING (count(*) > 10)\n\n\n\n\n\n","category":"method"},{"location":"reference/#ID","page":"API Reference","title":"ID","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"clauses/identifier.jl\"]","category":"page"},{"location":"reference/#FunSQL.ID-Tuple","page":"API Reference","title":"FunSQL.ID","text":"ID(; over = nothing, name)\nID(name; over = nothing)\nID(qualifiers, name)\n\nA SQL identifier. Specify over or use the |> operator to make a qualified identifier.\n\nExamples\n\njulia> c = ID(:person);\n\njulia> print(render(c))\n\"person\"\n\njulia> c = ID(:p) |> ID(:birth_datetime);\n\njulia> print(render(c))\n\"p\".\"birth_datetime\"\n\njulia> c = ID([:pg_catalog], :pg_database);\n\njulia> print(render(c))\n\"pg_catalog\".\"pg_database\"\n\n\n\n\n\n","category":"method"},{"location":"reference/#JOIN","page":"API Reference","title":"JOIN","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"clauses/join.jl\"]","category":"page"},{"location":"reference/#FunSQL.JOIN-Tuple","page":"API Reference","title":"FunSQL.JOIN","text":"JOIN(; over = nothing, joinee, on, left = false, right = false, lateral = false)\nJOIN(joinee; over = nothing, on, left = false, right = false, lateral = false)\nJOIN(joinee, on; over = nothing, left = false, right = false, lateral = false)\n\nA JOIN clause.\n\nExamples\n\njulia> c = FROM(:p => :person) |>\n JOIN(:l => :location,\n on = FUN(\"=\", (:p, :location_id), (:l, :location_id)),\n left = true) |>\n SELECT((:p, :person_id), (:l, :state));\n\njulia> print(render(c))\nSELECT\n \"p\".\"person_id\",\n \"l\".\"state\"\nFROM \"person\" AS \"p\"\nLEFT JOIN \"location\" AS \"l\" ON (\"p\".\"location_id\" = \"l\".\"location_id\")\n\n\n\n\n\n","category":"method"},{"location":"reference/#LIMIT","page":"API Reference","title":"LIMIT","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"clauses/limit.jl\"]","category":"page"},{"location":"reference/#FunSQL.LIMIT-Tuple","page":"API Reference","title":"FunSQL.LIMIT","text":"LIMIT(; over = nothing, offset = nothing, limit = nothing, with_ties = false)\nLIMIT(limit; over = nothing, offset = nothing, with_ties = false)\nLIMIT(offset, limit; over = nothing, with_ties = false)\nLIMIT(start:stop; over = nothing, with_ties = false)\n\nA LIMIT clause.\n\nExamples\n\njulia> c = FROM(:person) |>\n LIMIT(1) |>\n SELECT(:person_id);\n\njulia> print(render(c))\nSELECT \"person_id\"\nFROM \"person\"\nFETCH FIRST 1 ROW ONLY\n\n\n\n\n\n","category":"method"},{"location":"reference/#LIT","page":"API Reference","title":"LIT","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"clauses/literal.jl\"]","category":"page"},{"location":"reference/#FunSQL.LIT-Tuple","page":"API Reference","title":"FunSQL.LIT","text":"LIT(; val)\nLIT(val)\n\nA SQL literal.\n\nIn a context of a SQL clause, missing, numbers, strings and datetime values are automatically converted to SQL literals.\n\nExamples\n\njulia> c = LIT(missing);\n\njulia> print(render(c))\nNULL\n\njulia> c = LIT(\"SQL is fun!\");\n\njulia> print(render(c))\n'SQL is fun!'\n\n\n\n\n\n","category":"method"},{"location":"reference/#NOTE","page":"API Reference","title":"NOTE","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"clauses/note.jl\"]","category":"page"},{"location":"reference/#FunSQL.NOTE-Tuple","page":"API Reference","title":"FunSQL.NOTE","text":"NOTE(; over = nothing, text, postfix = false)\nNOTE(text; over = nothing, postfix = false)\n\nA free-form prefix of postfix annotation.\n\nExamples\n\njulia> c = FROM(:p => :person) |>\n NOTE(\"TABLESAMPLE SYSTEM (50)\", postfix = true) |>\n SELECT((:p, :person_id));\n\njulia> print(render(c))\nSELECT \"p\".\"person_id\"\nFROM \"person\" AS \"p\" TABLESAMPLE SYSTEM (50)\n\n\n\n\n\n","category":"method"},{"location":"reference/#ORDER","page":"API Reference","title":"ORDER","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"clauses/order.jl\"]","category":"page"},{"location":"reference/#FunSQL.ORDER-Tuple","page":"API Reference","title":"FunSQL.ORDER","text":"ORDER(; over = nothing, by = [])\nORDER(by...; over = nothing)\n\nAn ORDER BY clause.\n\nExamples\n\njulia> c = FROM(:person) |>\n ORDER(:year_of_birth) |>\n SELECT(:person_id);\n\njulia> print(render(c))\nSELECT \"person_id\"\nFROM \"person\"\nORDER BY \"year_of_birth\"\n\n\n\n\n\n","category":"method"},{"location":"reference/#PARTITION","page":"API Reference","title":"PARTITION","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"clauses/partition.jl\"]","category":"page"},{"location":"reference/#FunSQL.PARTITION-Tuple","page":"API Reference","title":"FunSQL.PARTITION","text":"PARTITION(; over = nothing, by = [], order_by = [], frame = nothing)\nPARTITION(by...; over = nothing, order_by = [], frame = nothing)\n\nA window definition clause.\n\nExamples\n\njulia> c = FROM(:person) |>\n SELECT(:person_id,\n AGG(:row_number, over = PARTITION(:year_of_birth)));\n\njulia> print(render(c))\nSELECT\n \"person_id\",\n (row_number() OVER (PARTITION BY \"year_of_birth\"))\nFROM \"person\"\n\njulia> c = FROM(:person) |>\n WINDOW(:w1 => PARTITION(:year_of_birth),\n :w2 => :w1 |> PARTITION(order_by = [:month_of_birth, :day_of_birth])) |>\n SELECT(:person_id, AGG(:row_number, over = :w2));\n\njulia> print(render(c))\nSELECT\n \"person_id\",\n (row_number() OVER (\"w2\"))\nFROM \"person\"\nWINDOW\n \"w1\" AS (PARTITION BY \"year_of_birth\"),\n \"w2\" AS (\"w1\" ORDER BY \"month_of_birth\", \"day_of_birth\")\n\njulia> c = FROM(:person) |>\n GROUP(:year_of_birth) |>\n SELECT(:year_of_birth,\n AGG(:avg,\n AGG(:count),\n over = PARTITION(order_by = [:year_of_birth],\n frame = (mode = :range, start = -1, finish = 1))));\n\njulia> print(render(c))\nSELECT\n \"year_of_birth\",\n (avg(count(*)) OVER (ORDER BY \"year_of_birth\" RANGE BETWEEN 1 PRECEDING AND 1 FOLLOWING))\nFROM \"person\"\nGROUP BY \"year_of_birth\"\n\n\n\n\n\n","category":"method"},{"location":"reference/#SELECT","page":"API Reference","title":"SELECT","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"clauses/select.jl\"]","category":"page"},{"location":"reference/#FunSQL.SELECT-Tuple","page":"API Reference","title":"FunSQL.SELECT","text":"SELECT(; over = nothing, top = nothing, distinct = false, args)\nSELECT(args...; over = nothing, top = nothing, distinct = false)\n\nA SELECT clause. Unlike raw SQL, SELECT() should be placed at the end of a clause chain.\n\nSet distinct to true to add a DISTINCT modifier.\n\nExamples\n\njulia> c = SELECT(true, false);\n\njulia> print(render(c))\nSELECT\n TRUE,\n FALSE\n\njulia> c = FROM(:location) |>\n SELECT(distinct = true, :zip);\n\njulia> print(render(c))\nSELECT DISTINCT \"zip\"\nFROM \"location\"\n\n\n\n\n\n","category":"method"},{"location":"reference/#SORT,-ASC,-and-DESC","page":"API Reference","title":"SORT, ASC, and DESC","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"clauses/sort.jl\"]","category":"page"},{"location":"reference/#FunSQL.ASC-Tuple{}","page":"API Reference","title":"FunSQL.ASC","text":"ASC(; over = nothing, nulls = nothing)\n\nAscending order indicator.\n\n\n\n\n\n","category":"method"},{"location":"reference/#FunSQL.DESC-Tuple{}","page":"API Reference","title":"FunSQL.DESC","text":"DESC(; over = nothing, nulls = nothing)\n\nDescending order indicator.\n\n\n\n\n\n","category":"method"},{"location":"reference/#FunSQL.SORT-Tuple","page":"API Reference","title":"FunSQL.SORT","text":"SORT(; over = nothing, value, nulls = nothing)\nSORT(value; over = nothing, nulls = nothing)\nASC(; over = nothing, nulls = nothing)\nDESC(; over = nothing, nulls = nothing)\n\nSort order options.\n\nExamples\n\njulia> c = FROM(:person) |>\n ORDER(:year_of_birth |> DESC()) |>\n SELECT(:person_id);\n\njulia> print(render(c))\nSELECT \"person_id\"\nFROM \"person\"\nORDER BY \"year_of_birth\" DESC\n\n\n\n\n\n","category":"method"},{"location":"reference/#UNION","page":"API Reference","title":"UNION","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"clauses/union.jl\"]","category":"page"},{"location":"reference/#FunSQL.UNION-Tuple","page":"API Reference","title":"FunSQL.UNION","text":"UNION(; over = nothing, all = false, args)\nUNION(args...; over = nothing, all = false)\n\nA UNION clause.\n\nExamples\n\njulia> c = FROM(:measurement) |>\n SELECT(:person_id, :date => :measurement_date) |>\n UNION(all = true,\n FROM(:observation) |>\n SELECT(:person_id, :date => :observation_date));\n\njulia> print(render(c))\nSELECT\n \"person_id\",\n \"measurement_date\" AS \"date\"\nFROM \"measurement\"\nUNION ALL\nSELECT\n \"person_id\",\n \"observation_date\" AS \"date\"\nFROM \"observation\"\n\n\n\n\n\n","category":"method"},{"location":"reference/#VALUES","page":"API Reference","title":"VALUES","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"clauses/values.jl\"]","category":"page"},{"location":"reference/#FunSQL.VALUES-Tuple","page":"API Reference","title":"FunSQL.VALUES","text":"VALUES(; rows)\nVALUES(rows)\n\nA VALUES clause.\n\nExamples\n\njulia> c = VALUES([(\"SQL\", 1974), (\"Julia\", 2012), (\"FunSQL\", 2021)]);\n\njulia> print(render(c))\nVALUES\n ('SQL', 1974),\n ('Julia', 2012),\n ('FunSQL', 2021)\n\n\n\n\n\n","category":"method"},{"location":"reference/#VAR","page":"API Reference","title":"VAR","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"clauses/variable.jl\"]","category":"page"},{"location":"reference/#FunSQL.VAR-Tuple","page":"API Reference","title":"FunSQL.VAR","text":"VAR(; name)\nVAR(name)\n\nA placeholder in a parameterized query.\n\nExamples\n\njulia> c = VAR(:year);\n\njulia> print(render(c))\n:year\n\n\n\n\n\n","category":"method"},{"location":"reference/#WHERE","page":"API Reference","title":"WHERE","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"clauses/where.jl\"]","category":"page"},{"location":"reference/#FunSQL.WHERE-Tuple","page":"API Reference","title":"FunSQL.WHERE","text":"WHERE(; over = nothing, condition)\nWHERE(condition; over = nothing)\n\nA WHERE clause.\n\nExamples\n\njulia> c = FROM(:location) |>\n WHERE(FUN(\"=\", :zip, \"60614\")) |>\n SELECT(:location_id);\n\njulia> print(render(c))\nSELECT \"location_id\"\nFROM \"location\"\nWHERE (\"zip\" = '60614')\n\n\n\n\n\n","category":"method"},{"location":"reference/#WINDOW","page":"API Reference","title":"WINDOW","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"clauses/window.jl\"]","category":"page"},{"location":"reference/#FunSQL.WINDOW-Tuple","page":"API Reference","title":"FunSQL.WINDOW","text":"WINDOW(; over = nothing, args)\nWINDOW(args...; over = nothing)\n\nA WINDOW clause.\n\nExamples\n\njulia> c = FROM(:person) |>\n WINDOW(:w1 => PARTITION(:year_of_birth),\n :w2 => :w1 |> PARTITION(order_by = [:month_of_birth, :day_of_birth])) |>\n SELECT(:person_id, AGG(\"row_number\", over = :w2));\n\njulia> print(render(c))\nSELECT\n \"person_id\",\n (row_number() OVER (\"w2\"))\nFROM \"person\"\nWINDOW\n \"w1\" AS (PARTITION BY \"year_of_birth\"),\n \"w2\" AS (\"w1\" ORDER BY \"month_of_birth\", \"day_of_birth\")\n\n\n\n\n\n","category":"method"},{"location":"reference/#WITH","page":"API Reference","title":"WITH","text":"","category":"section"},{"location":"reference/","page":"API Reference","title":"API Reference","text":"Modules = [FunSQL]\nPages = [\"clauses/with.jl\"]","category":"page"},{"location":"reference/#FunSQL.WITH-Tuple","page":"API Reference","title":"FunSQL.WITH","text":"WITH(; over = nothing, recursive = false, args)\nWITH(args...; over = nothing, recursive = false)\n\nA WITH clause.\n\nExamples\n\njulia> c = FROM(:person) |>\n WHERE(FUN(:in, :person_id,\n FROM(:essential_hypertension) |>\n SELECT(:person_id))) |>\n SELECT(:person_id, :year_of_birth) |>\n WITH(FROM(:condition_occurrence) |>\n WHERE(FUN(\"=\", :condition_concept_id, 320128)) |>\n SELECT(:person_id) |>\n AS(:essential_hypertension));\n\njulia> print(render(c))\nWITH \"essential_hypertension\" AS (\n SELECT \"person_id\"\n FROM \"condition_occurrence\"\n WHERE (\"condition_concept_id\" = 320128)\n)\nSELECT\n \"person_id\",\n \"year_of_birth\"\nFROM \"person\"\nWHERE (\"person_id\" IN (\n SELECT \"person_id\"\n FROM \"essential_hypertension\"\n))\n\njulia> c = FROM(:essential_hypertension) |>\n SELECT(*) |>\n WITH(recursive = true,\n FROM(:concept) |>\n WHERE(FUN(\"=\", :concept_id, 320128)) |>\n SELECT(:concept_id, :concept_name) |>\n UNION(all = true,\n FROM(:eh => :essential_hypertension) |>\n JOIN(:cr => :concept_relationship,\n FUN(\"=\", (:eh, :concept_id), (:cr, :concept_id_1))) |>\n JOIN(:c => :concept,\n FUN(\"=\", (:cr, :concept_id_2), (:c, :concept_id))) |>\n WHERE(FUN(\"=\", (:cr, :relationship_id), \"Subsumes\")) |>\n SELECT((:c, :concept_id), (:c, :concept_name))) |>\n AS(:essential_hypertension, columns = [:concept_id, :concept_name]));\n\njulia> print(render(c))\nWITH RECURSIVE \"essential_hypertension\" (\"concept_id\", \"concept_name\") AS (\n SELECT\n \"concept_id\",\n \"concept_name\"\n FROM \"concept\"\n WHERE (\"concept_id\" = 320128)\n UNION ALL\n SELECT\n \"c\".\"concept_id\",\n \"c\".\"concept_name\"\n FROM \"essential_hypertension\" AS \"eh\"\n JOIN \"concept_relationship\" AS \"cr\" ON (\"eh\".\"concept_id\" = \"cr\".\"concept_id_1\")\n JOIN \"concept\" AS \"c\" ON (\"cr\".\"concept_id_2\" = \"c\".\"concept_id\")\n WHERE (\"cr\".\"relationship_id\" = 'Subsumes')\n)\nSELECT *\nFROM \"essential_hypertension\"\n\n\n\n\n\n","category":"method"},{"location":"test/nodes/#SQL-Nodes","page":"SQL Nodes","title":"SQL Nodes","text":"","category":"section"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"using FunSQL\n\nusing FunSQL:\n Agg, Append, As, Asc, Bind, CrossJoin, Define, Desc, Fun, From, Get,\n Group, Highlight, Iterate, Join, LeftJoin, Limit, Lit, Order, Over,\n Partition, SQLNode, SQLTable, Select, Sort, Var, Where, With,\n WithExternal, ID, render","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"We start with specifying the database model.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"const concept =\n SQLTable(:concept, columns = [:concept_id, :vocabulary_id, :concept_code, :concept_name])\n\nconst location =\n SQLTable(:location, columns = [:location_id, :city, :state])\n\nconst person =\n SQLTable(:person, columns = [:person_id, :gender_concept_id, :year_of_birth, :month_of_birth, :day_of_birth, :birth_datetime, :location_id])\n\nconst visit_occurrence =\n SQLTable(:visit_occurrence, columns = [:visit_occurrence_id, :person_id, :visit_start_date, :visit_end_date])\n\nconst measurement =\n SQLTable(:measurement, columns = [:measurement_id, :person_id, :measurement_concept_id, :measurement_date])\n\nconst observation =\n SQLTable(:observation, columns = [:observation_id, :person_id, :observation_concept_id, :observation_date])","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"In FunSQL, a SQL query is generated from a tree of SQLNode objects. The nodes are created using constructors with familiar SQL names and connected together using the chain (|>) operator.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Where(Fun.\">\"(Get.year_of_birth, 2000)) |>\n Select(Get.person_id)\n#-> (…) |> Select(…)","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Displaying a SQLNode object shows how it was constructed.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"display(q)\n#=>\nlet person = SQLTable(:person, …),\n q1 = From(person),\n q2 = q1 |> Where(Fun.\">\"(Get.year_of_birth, 2000)),\n q3 = q2 |> Select(Get.person_id)\n q3\nend\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Each node wraps a concrete node object, which can be accessed using the indexing operator.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q[]\n#-> ((…) |> Select(…))[]\n\ndisplay(q[])\n#=>\nlet person = SQLTable(:person, …),\n q1 = From(person),\n q2 = q1 |> Where(Fun.\">\"(Get.year_of_birth, 2000)),\n q3 = q2 |> Select(Get.person_id)\n q3[]\nend\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"The SQL query is generated using the function render().","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"print(render(q))\n#=>\nSELECT \"person_1\".\"person_id\"\nFROM \"person\" AS \"person_1\"\nWHERE (\"person_1\".\"year_of_birth\" > 2000)\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Ill-formed queries are detected.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |> Agg.count() |> Select(Get.person_id)\nrender(q)\n#=>\nERROR: FunSQL.IllFormedError in:\nlet person = SQLTable(:person, …),\n q1 = From(person),\n q2 = q1 |> Agg.count() |> Select(Get.person_id)\n q2\nend\n=#\n\nq = From(person) |> Fun.current_date()\n#=>\nERROR: FunSQL.RebaseError in:\nFun.current_date()\n=#","category":"page"},{"location":"test/nodes/#@funsql","page":"SQL Nodes","title":"@funsql","text":"","category":"section"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"The @funsql macro provides alternative notation for specifying FunSQL queries.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = @funsql begin\n from(person)\n filter(year_of_birth > 2000)\n select(person_id)\nend\n\ndisplay(q)\n#=>\nlet q1 = From(:person),\n q2 = q1 |> Where(Fun.\">\"(Get.year_of_birth, 2000)),\n q3 = q2 |> Select(Get.person_id)\n q3\nend\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"We can combine @funsql notation with regular Julia code.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = @funsql begin\n from(person)\n $(Where(Get.year_of_birth .> 2000))\n select(person_id)\nend\n\ndisplay(q)\n#=>\nlet q1 = From(:person),\n q2 = q1 |> Where(Fun.\">\"(Get.year_of_birth, 2000)),\n q3 = q2 |> Select(Get.person_id)\n q3\nend\n=#\n\nq = From(:person) |>\n @funsql(filter(year_of_birth > 2000)) |>\n Select(Get.person_id)\n\ndisplay(q)\n#=>\nlet q1 = From(:person),\n q2 = q1 |> Where(Fun.\">\"(Get.year_of_birth, 2000)),\n q3 = q2 |> Select(Get.person_id)\n q3\nend\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"The @funsql notation allows us to encapsulate query fragments into query functions.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"@funsql adults() = from(person).filter(2020 - year_of_birth >= 16)\n\ndisplay(@funsql adults())\n#=>\nlet q1 = From(:person),\n q2 = q1 |> Where(Fun.\">=\"(Fun.\"-\"(2020, Get.year_of_birth), 16))\n q2\nend\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Query functions defined with @funsql can accept parameters.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"@funsql concept_by_code(v, c) =\n begin\n from(concept)\n filter(vocabulary_id == $v && concept_code == $c)\n end\n\ndisplay(@funsql concept_by_code(\"SNOMED\", \"22298006\"))\n#=>\nlet q1 = From(:concept),\n q2 = q1 |>\n Where(Fun.and(Fun.\"=\"(Get.vocabulary_id, \"SNOMED\"),\n Fun.\"=\"(Get.concept_code, \"22298006\")))\n q2\nend\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Query functions support ... notation.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"@funsql concept_by_code(v, cs...) =\n begin\n from(concept)\n filter(vocabulary_id == $v && in(concept_code, $(cs...)))\n end\n\ndisplay(@funsql concept_by_code(\"Visit\", \"IP\", \"ER\"))\n#=>\nlet q1 = From(:concept),\n q2 = q1 |>\n Where(Fun.and(Fun.\"=\"(Get.vocabulary_id, \"Visit\"),\n Fun.in(Get.concept_code, \"IP\", \"ER\")))\n q2\nend\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Query functions support keyword arguments and default values.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"@funsql age(yob = year_of_birth; at = fun(`EXTRACT(YEAR FROM CURRENT_DATE) `)) =\n ($at - $yob)\n\nq = @funsql begin\n from(person)\n define(\n age => age(),\n age_in_2000 => age(at = 2000))\nend\n\ndisplay(q)\n#=>\nlet q1 = From(:person),\n q2 = q1 |>\n Define(Fun.\"-\"(Fun.\"EXTRACT(YEAR FROM CURRENT_DATE) \"(),\n Get.year_of_birth) |>\n As(:age),\n Fun.\"-\"(2000, Get.year_of_birth) |> As(:age_in_2000))\n q2\nend\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"A parameter of a query function accepts a type declaration.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"@funsql concept(c::String, v::String = \"SNOMED\") =\n concept_by_code($v, $c)\n\n@funsql concept(id::Int) =\n from(concept).filter(concept_id == $id)\n\ndisplay(@funsql concept(\"22298006\"))\n#=>\nlet q1 = From(:concept),\n q2 = q1 |>\n Where(Fun.and(Fun.\"=\"(Get.vocabulary_id, \"SNOMED\"),\n Fun.\"=\"(Get.concept_code, \"22298006\")))\n q2\nend\n=#\n\ndisplay(@funsql concept(4329847))\n#=>\nlet q1 = From(:concept),\n q2 = q1 |> Where(Fun.\"=\"(Get.concept_id, 4329847))\n q2\nend\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"A single @funsql macro can wrap multiple definitions.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"@funsql begin\n SNOMED(codes...) = concept_by_code(\"SNOMED\", $(codes...))\n\n `MYOCARDIAL INFARCTION`() = SNOMED(\"22298006\")\nend\n\ndisplay(@funsql `MYOCARDIAL INFARCTION`())\n#=>\nlet q1 = From(:concept),\n q2 = q1 |>\n Where(Fun.and(Fun.\"=\"(Get.vocabulary_id, \"SNOMED\"),\n Fun.\"=\"(Get.concept_code, \"22298006\")))\n q2\nend\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"A query function may have a docstring.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"@funsql begin\n \"SNOMED concept set with the given `codes`\"\n SNOMED\n\n \"Visit concept set with the given `codes`\"\n Visit(codes...) = concept_by_code(\"Visit\", $(codes...))\nend\n\n@doc funsql_SNOMED\n#-> SNOMED concept set with the given `codes`\n\n@doc funsql_Visit\n#-> Visit concept set with the given `codes`","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"An ill-formed @funsql query triggers an error.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"@funsql for p in person; end\n#=>\nERROR: LoadError: FunSQL.TransliterationError: ill-formed @funsql notation:\nquote\n for p = person\n end\nend\nin expression starting at …\n=#","category":"page"},{"location":"test/nodes/#Literals","page":"SQL Nodes","title":"Literals","text":"","category":"section"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"A SQL value is created with Lit() constructor.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"e = Lit(\"SQL is fun!\")\n#-> Lit(\"SQL is fun!\")","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"In a SELECT clause, bare literal expressions get an alias \"_\".","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = Select(e)\n\nprint(render(q))\n#=>\nSELECT 'SQL is fun!' AS \"_\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Values of certain Julia data types are automatically converted to SQL literals when they are used in the context of a SQL node.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"using Dates\n\nq = Select(\"null\" => missing,\n \"boolean\" => true,\n \"integer\" => 42,\n \"text\" => \"SQL is fun!\",\n \"date\" => Date(2000))","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Such plain literals could also be used in @funsql notation.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = @funsql select(null => missing,\n boolean => true,\n integer => 42,\n text => \"SQL is fun!\",\n date => $(Date(2000)))\n\ndisplay(q)\n#=>\nSelect(missing |> As(:null),\n true |> As(:boolean),\n 42 |> As(:integer),\n \"SQL is fun!\" |> As(:text),\n Dates.Date(\"2000-01-01\") |> As(:date))\n=#","category":"page"},{"location":"test/nodes/#Attributes","page":"SQL Nodes","title":"Attributes","text":"","category":"section"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"To reference a table attribute, we use the Get constructor.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"e = Get(:person_id)\n#-> Get.person_id","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Alternatively, use shorthand notation.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Get.person_id\n#-> Get.person_id\n\nGet.\"person_id\"\n#-> Get.person_id\n\nGet[:person_id]\n#-> Get.person_id\n\nGet[\"person_id\"]\n#-> Get.person_id","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Hierarchical notation is supported.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"e = Get.p.person_id\n#-> Get.p.person_id\n\nGet.p |> Get.person_id\n#-> Get.p.person_id","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"In the context where a SQL node is expected, a bare symbol is automatically converted to a reference.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = Select(:person_id)\n\ndisplay(q)\n#-> Select(Get.person_id)","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"@funsql macro translates an identifier to a symbol. In suitable context, this symbol will be translated to a column reference.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"@funsql person_id\n#-> :person_id","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"@funsql notation supports hierarchical references.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"@funsql p.person_id\n#-> Get.p.person_id","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Use backticks to represent a name that is not a valid identifier.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"@funsql `person_id`\n#-> :person_id\n\n@funsql `p`.`person_id`\n#-> Get.p.person_id","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Get can also create bound references.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person)\n\ne = Get(over = q, :year_of_birth)\n#-> (…) |> Get.year_of_birth\n\ndisplay(e)\n#=>\nlet person = SQLTable(:person, …),\n q1 = From(person)\n q1.year_of_birth\nend\n=#\n\nq.person_id\n#-> (…) |> Get.person_id\n\nq.\"person_id\"\n#-> (…) |> Get.person_id\n\nq[:person_id]\n#-> (…) |> Get.person_id\n\nq[\"person_id\"]\n#-> (…) |> Get.person_id","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Get is used for dereferencing an alias created with As.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n As(:p) |>\n Select(Get.p.person_id)\n\nprint(render(q))\n#=>\nSELECT \"person_1\".\"person_id\"\nFROM \"person\" AS \"person_1\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"This is particularly useful when you need to disambiguate the output of Join.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n As(:p) |>\n Join(From(location) |> As(:l),\n on = Get.p.location_id .== Get.l.location_id) |>\n Select(Get.p.person_id, Get.l.state)\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"person_id\",\n \"location_1\".\"state\"\nFROM \"person\" AS \"person_1\"\nJOIN \"location\" AS \"location_1\" ON (\"person_1\".\"location_id\" = \"location_1\".\"location_id\")\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"When Get refers to an unknown attribute, an error is reported.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = Select(Get.person_id)\n\nprint(render(q))\n#=>\nERROR: FunSQL.ReferenceError: cannot find `person_id` in:\nSelect(Get.person_id)\n=#\n\nq = From(person) |>\n As(:p) |>\n Select(Get.q.person_id)\n\nprint(render(q))\n#=>\nERROR: FunSQL.ReferenceError: cannot find `q` in:\nlet person = SQLTable(:person, …),\n q1 = From(person),\n q2 = q1 |> As(:p) |> Select(Get.q.person_id)\n q2\nend\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"An attribute defined in a Join shadows any previously defined attributes with the same name.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = person |>\n Join(person, true) |>\n Select(Get.person_id)\n\nprint(render(q))\n#=>\nSELECT \"person_2\".\"person_id\"\nFROM \"person\" AS \"person_1\"\nCROSS JOIN \"person\" AS \"person_2\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"An incomplete hierarchical reference, as well as an unexpected hierarchical reference, will result in an error.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = person |>\n As(:p) |>\n Select(Get.p)\n\nprint(render(q))\n#=>\nERROR: FunSQL.ReferenceError: incomplete reference `p` in:\nlet person = SQLTable(:person, …),\n q1 = From(person),\n q2 = q1 |> As(:p) |> Select(Get.p)\n q2\nend\n=#\n\nq = person |>\n Select(Get.person_id.year_of_birth)\n\nprint(render(q))\n#=>\nERROR: FunSQL.ReferenceError: unexpected reference after `person_id` in:\nlet person = SQLTable(:person, …),\n q1 = From(person),\n q2 = q1 |> Select(Get.person_id.year_of_birth)\n q2\nend\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"A reference bound to any node other than Get will cause an error.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = (qₚ = From(person)) |> Select(qₚ.person_id)\n\nprint(render(q))\n#=>\nERROR: FunSQL.IllFormedError in:\nlet person = SQLTable(:person, …),\n q1 = From(person),\n q2 = q1 |> Select(q1.person_id)\n q2\nend\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Any expression could be given a name and attached to a query using the Define constructor.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Define(:age => Fun.now() .- Get.birth_datetime)\n#-> (…) |> Define(…)\n\ndisplay(q)\n#=>\nlet person = SQLTable(:person, …),\n q1 = From(person),\n q2 = q1 |> Define(Fun.\"-\"(Fun.now(), Get.birth_datetime) |> As(:age))\n q2\nend\n=#\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"person_id\",\n \"person_1\".\"gender_concept_id\",\n \"person_1\".\"year_of_birth\",\n \"person_1\".\"month_of_birth\",\n \"person_1\".\"day_of_birth\",\n \"person_1\".\"birth_datetime\",\n \"person_1\".\"location_id\",\n (now() - \"person_1\".\"birth_datetime\") AS \"age\"\nFROM \"person\" AS \"person_1\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"This expression could be referred to by name as if it were a regular table attribute.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"print(render(q |> Where(Get.age .> \"16 years\")))\n#=>\nSELECT\n \"person_2\".\"person_id\",\n \"person_2\".\"gender_concept_id\",\n \"person_2\".\"year_of_birth\",\n \"person_2\".\"month_of_birth\",\n \"person_2\".\"day_of_birth\",\n \"person_2\".\"birth_datetime\",\n \"person_2\".\"location_id\",\n \"person_2\".\"age\"\nFROM (\n SELECT\n \"person_1\".\"person_id\",\n \"person_1\".\"gender_concept_id\",\n \"person_1\".\"year_of_birth\",\n \"person_1\".\"month_of_birth\",\n \"person_1\".\"day_of_birth\",\n \"person_1\".\"birth_datetime\",\n \"person_1\".\"location_id\",\n (now() - \"person_1\".\"birth_datetime\") AS \"age\"\n FROM \"person\" AS \"person_1\"\n) AS \"person_2\"\nWHERE (\"person_2\".\"age\" > '16 years')\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"A Define node can be created using @funsql notation.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = @funsql from(person).define(age => 2000 - year_of_birth)\n\ndisplay(q)\n#=>\nlet q1 = From(:person),\n q2 = q1 |> Define(Fun.\"-\"(2000, Get.year_of_birth) |> As(:age))\n q2\nend\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Define does not create a nested query if the definition is a literal or a simple reference.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Define(:year => Get.year_of_birth,\n :threshold => 2000) |>\n Where(Get.year .>= Get.threshold)\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"person_id\",\n \"person_1\".\"gender_concept_id\",\n \"person_1\".\"year_of_birth\",\n \"person_1\".\"month_of_birth\",\n \"person_1\".\"day_of_birth\",\n \"person_1\".\"birth_datetime\",\n \"person_1\".\"location_id\",\n \"person_1\".\"year_of_birth\" AS \"year\",\n 2000 AS \"threshold\"\nFROM \"person\" AS \"person_1\"\nWHERE (\"person_1\".\"year_of_birth\" >= 2000)\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Define can be used to override an existing field.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Define(:person_id => Get.year_of_birth, :year_of_birth => Get.person_id)\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"year_of_birth\" AS \"person_id\",\n \"person_1\".\"gender_concept_id\",\n \"person_1\".\"person_id\" AS \"year_of_birth\",\n \"person_1\".\"month_of_birth\",\n \"person_1\".\"day_of_birth\",\n \"person_1\".\"birth_datetime\",\n \"person_1\".\"location_id\"\nFROM \"person\" AS \"person_1\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Define allows you to insert columns at the beginning or at the end of the column list.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Define(:age => Fun.now() .- Get.birth_datetime, Get.birth_datetime,\n before = true)\n\ndisplay(q)\n#=>\nlet person = SQLTable(:person, …),\n q1 = From(person),\n q2 = q1 |>\n Define(Fun.\"-\"(Fun.now(), Get.birth_datetime) |> As(:age),\n Get.birth_datetime,\n before = true)\n q2\nend\n=#\n\nprint(render(q))\n#=>\nSELECT\n (now() - \"person_1\".\"birth_datetime\") AS \"age\",\n \"person_1\".\"birth_datetime\",\n \"person_1\".\"person_id\",\n \"person_1\".\"gender_concept_id\",\n \"person_1\".\"year_of_birth\",\n \"person_1\".\"month_of_birth\",\n \"person_1\".\"day_of_birth\",\n \"person_1\".\"location_id\"\nFROM \"person\" AS \"person_1\"\n=#\n\nq = From(person) |>\n Define(:age => Fun.now() .- Get.birth_datetime, Get.birth_datetime,\n after = true)\n\ndisplay(q)\n#=>\nlet person = SQLTable(:person, …),\n q1 = From(person),\n q2 = q1 |>\n Define(Fun.\"-\"(Fun.now(), Get.birth_datetime) |> As(:age),\n Get.birth_datetime,\n after = true)\n q2\nend\n=#\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"person_id\",\n \"person_1\".\"gender_concept_id\",\n \"person_1\".\"year_of_birth\",\n \"person_1\".\"month_of_birth\",\n \"person_1\".\"day_of_birth\",\n \"person_1\".\"location_id\",\n (now() - \"person_1\".\"birth_datetime\") AS \"age\",\n \"person_1\".\"birth_datetime\"\nFROM \"person\" AS \"person_1\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"It can also insert columns in front of or right after a specified column.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Define(:age => Fun.now() .- Get.birth_datetime, Get.birth_datetime,\n before = :year_of_birth)\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"person_id\",\n \"person_1\".\"gender_concept_id\",\n (now() - \"person_1\".\"birth_datetime\") AS \"age\",\n \"person_1\".\"birth_datetime\",\n \"person_1\".\"year_of_birth\",\n \"person_1\".\"month_of_birth\",\n \"person_1\".\"day_of_birth\",\n \"person_1\".\"location_id\"\nFROM \"person\" AS \"person_1\"\n=#\n\nq = From(person) |>\n Define(:age => Fun.now() .- Get.birth_datetime, Get.birth_datetime,\n after = :birth_datetime)\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"person_id\",\n \"person_1\".\"gender_concept_id\",\n \"person_1\".\"year_of_birth\",\n \"person_1\".\"month_of_birth\",\n \"person_1\".\"day_of_birth\",\n (now() - \"person_1\".\"birth_datetime\") AS \"age\",\n \"person_1\".\"birth_datetime\",\n \"person_1\".\"location_id\"\nFROM \"person\" AS \"person_1\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"It is an error to set both before and after or to refer to a non-existent column.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Define(before = true, after = true)\n\nprint(render(q))\n#=>\nERROR: DomainError with (before = true, after = true):\nonly one of `before` and `after` could be set\n=#\n\nq = Define(before = :person_id)\n\nprint(render(q))\n#=>\nERROR: FunSQL.ReferenceError: cannot find `person_id` in:\nlet q1 = Define(before = :person_id)\n q1\nend\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Define has no effect if none of the defined fields are used in the query.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Define(:age => 2020 .- Get.year_of_birth) |>\n Select(Get.person_id, Get.year_of_birth)\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"person_id\",\n \"person_1\".\"year_of_birth\"\nFROM \"person\" AS \"person_1\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Define can be used after Select.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Select(Get.person_id, Get.year_of_birth) |>\n Define(:age => 2020 .- Get.year_of_birth)\n\nprint(render(q))\n#=>\nSELECT\n \"person_2\".\"person_id\",\n \"person_2\".\"year_of_birth\",\n (2020 - \"person_2\".\"year_of_birth\") AS \"age\"\nFROM (\n SELECT\n \"person_1\".\"person_id\",\n \"person_1\".\"year_of_birth\"\n FROM \"person\" AS \"person_1\"\n) AS \"person_2\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Define requires that all definitions have a unique alias.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"From(person) |>\nDefine(:age => Fun.now() .- Get.birth_datetime,\n :age => Fun.current_timestamp() .- Get.birth_datetime)\n#=>\nERROR: FunSQL.DuplicateLabelError: `age` is used more than once in:\nDefine(Fun.\"-\"(Fun.now(), Get.birth_datetime) |> As(:age),\n Fun.\"-\"(Fun.current_timestamp(), Get.birth_datetime) |> As(:age))\n=#","category":"page"},{"location":"test/nodes/#Variables","page":"SQL Nodes","title":"Variables","text":"","category":"section"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"A query variable is created with the Var constructor.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"e = Var(:YEAR)\n#-> Var.YEAR","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Alternatively, use shorthand notation.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Var.YEAR\n#-> Var.YEAR\n\nVar.\"YEAR\"\n#-> Var.YEAR\n\nVar[:YEAR]\n#-> Var.YEAR\n\nVar[\"YEAR\"]\n#-> Var.YEAR","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"A variable could be created with @funsql notation.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"@funsql :YEAR\n#-> Var.YEAR","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Unbound query variables are serialized as query parameters.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Where(Get.year_of_birth .> Var.YEAR)\n\nsql = render(q)\n\nprint(sql)\n#=>\nSELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"location_id\"\nFROM \"person\" AS \"person_1\"\nWHERE (\"person_1\".\"year_of_birth\" > :YEAR)\n=#\n\nsql.vars\n#-> [:YEAR]","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Query variables could be bound using the Bind constructor.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q0(person_id) =\n From(visit_occurrence) |>\n Where(Get.person_id .== Var.PERSON_ID) |>\n Bind(:PERSON_ID => person_id)\n\nq0(1)\n#-> (…) |> Bind(…)\n\ndisplay(q0(1))\n#=>\nlet visit_occurrence = SQLTable(:visit_occurrence, …),\n q1 = From(visit_occurrence),\n q2 = q1 |> Where(Fun.\"=\"(Get.person_id, Var.PERSON_ID))\n q2 |> Bind(1 |> As(:PERSON_ID))\nend\n=#\n\nprint(render(q0(1)))\n#=>\nSELECT\n \"visit_occurrence_1\".\"visit_occurrence_id\",\n \"visit_occurrence_1\".\"person_id\",\n \"visit_occurrence_1\".\"visit_start_date\",\n \"visit_occurrence_1\".\"visit_end_date\"\nFROM \"visit_occurrence\" AS \"visit_occurrence_1\"\nWHERE (\"visit_occurrence_1\".\"person_id\" = 1)\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"A Bind node can be created with @funsql notation.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = @funsql begin\n from(visit_occurrence)\n filter(person_id == :PERSON_ID)\n bind(:PERSON_ID => person_id)\nend\n\ndisplay(q)\n#=>\nlet q1 = From(:visit_occurrence),\n q2 = q1 |> Where(Fun.\"=\"(Get.person_id, Var.PERSON_ID))\n q2 |> Bind(Get.person_id |> As(:PERSON_ID))\nend\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Bind lets us create correlated subqueries.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Where(Fun.exists(q0(Get.person_id)))\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"location_id\"\nFROM \"person\" AS \"person_1\"\nWHERE (EXISTS (\n SELECT NULL AS \"_\"\n FROM \"visit_occurrence\" AS \"visit_occurrence_1\"\n WHERE (\"visit_occurrence_1\".\"person_id\" = \"person_1\".\"person_id\")\n))\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"When an argument to Bind is an aggregate, it must be evaluated in a nested subquery.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q0(person_id, date) =\n From(observation) |>\n Where(Fun.and(Get.person_id .== Var.PERSON_ID,\n Get.observation_date .>= Var.DATE)) |>\n Bind(:PERSON_ID => person_id, :DATE => date)\n\nq = From(visit_occurrence) |>\n Group(Get.person_id) |>\n Where(Fun.exists(q0(Get.person_id, Agg.max(Get.visit_start_date))))\n\nprint(render(q))\n#=>\nSELECT \"visit_occurrence_2\".\"person_id\"\nFROM (\n SELECT\n \"visit_occurrence_1\".\"person_id\",\n max(\"visit_occurrence_1\".\"visit_start_date\") AS \"max\"\n FROM \"visit_occurrence\" AS \"visit_occurrence_1\"\n GROUP BY \"visit_occurrence_1\".\"person_id\"\n) AS \"visit_occurrence_2\"\nWHERE (EXISTS (\n SELECT NULL AS \"_\"\n FROM \"observation\" AS \"observation_1\"\n WHERE\n (\"observation_1\".\"person_id\" = \"visit_occurrence_2\".\"person_id\") AND\n (\"observation_1\".\"observation_date\" >= \"visit_occurrence_2\".\"max\")\n))\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"An empty Bind can be created.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Bind(args = [])\n#-> Bind(args = [])","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Bind requires that all variables have a unique name.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Bind(:PERSON_ID => 1, :PERSON_ID => 2)\n#=>\nERROR: FunSQL.DuplicateLabelError: `PERSON_ID` is used more than once in:\nBind(1 |> As(:PERSON_ID), 2 |> As(:PERSON_ID))\n=#","category":"page"},{"location":"test/nodes/#Functions-and-Operators","page":"SQL Nodes","title":"Functions and Operators","text":"","category":"section"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"A function or an operator invocation is created with the Fun constructor.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Fun.\">\"\n#-> Fun.:(\">\")\n\ne = Fun.\">\"(Get.year_of_birth, 2000)\n#-> Fun.:(\">\")(…)\n\ndisplay(e)\n#-> Fun.\">\"(Get.year_of_birth, 2000)","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Alternatively, Fun nodes are created by broadcasting. Common Julia operators are replaced with their SQL equivalents.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"#? VERSION >= v\"1.7\"\ne = Get.location.state .== \"IL\" .|| Get.location.zip .!= \"60615\"\n#-> Fun.or(…)\n\n#? VERSION >= v\"1.7\"\ndisplay(e)\n#-> Fun.or(Fun.\"=\"(Get.location.state, \"IL\"), Fun.\"<>\"(Get.location.zip, \"60615\"))\n\n#? VERSION >= v\"1.7\"\ne = .!(e .&& Get.year_of_birth .> 1950 .&& Get.year_of_birth .< 1990)\n#-> Fun.not(…)\n\n#? VERSION >= v\"1.7\"\ndisplay(e)\n#=>\nFun.not(Fun.and(Fun.or(Fun.\"=\"(Get.location.state, \"IL\"),\n Fun.\"<>\"(Get.location.zip, \"60615\")),\n Fun.and(Fun.\">\"(Get.year_of_birth, 1950),\n Fun.\"<\"(Get.year_of_birth, 1990))))\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"A vector of arguments could be passed directly.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Fun.\">\"(args = SQLNode[Get.year_of_birth, 2000])\n#-> Fun.:(\">\")(…)","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Fun nodes can be generated in @funsql notation.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"e = @funsql fun(>, year_of_birth, 2000)\n\ndisplay(e)\n#-> Fun.\">\"(Get.year_of_birth, 2000)","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"In order to generate Fun nodes using regular function and operator calls, we need to declare these functions and operators in advance.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"e = @funsql concat(location.city, \", \", location.state)\n\ndisplay(e)\n#-> Fun.concat(Get.location.city, \", \", Get.location.state)\n\ne = @funsql 1950 < year_of_birth < 1990\n\ndisplay(e)\n#-> Fun.and(Fun.\"<\"(1950, Get.year_of_birth), Fun.\"<\"(Get.year_of_birth, 1990))\n\ne = @funsql location.state != \"IL\" || location.zip != 60615\n\ndisplay(e)\n#-> Fun.or(Fun.\"<>\"(Get.location.state, \"IL\"), Fun.\"<>\"(Get.location.zip, 60615))\n\ne = @funsql location.state == \"IL\" && location.zip == 60615\n\ndisplay(e)\n#-> Fun.and(Fun.\"=\"(Get.location.state, \"IL\"), Fun.\"=\"(Get.location.zip, 60615))","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"In @funsql notation, use backticks to represent a name that is not a valid identifier.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"e = @funsql fun(`SUBSTRING(? FROM ? FOR ?)`, city, 1, 1)\n\ndisplay(e)\n#-> Fun.\"SUBSTRING(? FROM ? FOR ?)\"(Get.city, 1, 1)\n\nq = @funsql `from`(person).`filter`(year_of_birth <= 1964)\n\ndisplay(q)\n#=>\nlet q1 = From(:person),\n q2 = q1 |> Where(Fun.\"<=\"(Get.year_of_birth, 1964))\n q2\nend\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"In @funsql notation, an if statement is converted to a CASE expression.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"e = @funsql year_of_birth <= 1964 ? \"Boomers\" : \"Millenials\"\n\ndisplay(e)\n#-> Fun.case(Fun.\"<=\"(Get.year_of_birth, 1964), \"Boomers\", \"Millenials\")\n\ne = @funsql year_of_birth <= 1964 ? \"Boomers\" :\n year_of_birth <= 1980 ? \"Generation X\" : \"Millenials\"\n\ndisplay(e)\n#=>\nFun.case(Fun.\"<=\"(Get.year_of_birth, 1964),\n \"Boomers\",\n Fun.\"<=\"(Get.year_of_birth, 1980),\n \"Generation X\",\n \"Millenials\")\n=#\n\ne = @funsql if year_of_birth <= 1964; \"Boomers\"; end\n\ndisplay(e)\n#-> Fun.case(Fun.\"<=\"(Get.year_of_birth, 1964), \"Boomers\")\n\ne = @funsql begin\n if year_of_birth <= 1964\n \"Boomers\"\n elseif year_of_birth <= 1980\n \"Generation X\"\n end\nend\n\ndisplay(e)\n#=>\nFun.case(Fun.\"<=\"(Get.year_of_birth, 1964),\n \"Boomers\",\n Fun.\"<=\"(Get.year_of_birth, 1980),\n \"Generation X\")\n=#\n\ne = @funsql begin\n if year_of_birth <= 1964\n \"Boomers\"\n elseif year_of_birth <= 1980\n \"Generation X\"\n elseif year_of_birth <= 1996\n \"Millenials\"\n else\n \"Generation Z\"\n end\nend\n\ndisplay(e)\n#=>\nFun.case(Fun.\"<=\"(Get.year_of_birth, 1964),\n \"Boomers\",\n Fun.\"<=\"(Get.year_of_birth, 1980),\n \"Generation X\",\n Fun.\"<=\"(Get.year_of_birth, 1996),\n \"Millenials\",\n \"Generation Z\")\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"In a SELECT clause, the function name becomes the column alias.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(location) |>\n Select(Fun.coalesce(Get.city, \"N/A\"))\n\nprint(render(q))\n#=>\nSELECT coalesce(\"location_1\".\"city\", 'N/A') AS \"coalesce\"\nFROM \"location\" AS \"location_1\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"When the name contains only symbol characters, or when it starts or ends with a space character, it is interpreted as an operator.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(location) |>\n Select(Fun.\" || \"(Get.city, \", \", Get.state))\n\nprint(render(q))\n#=>\nSELECT (\"location_1\".\"city\" || ', ' || \"location_1\".\"state\") AS \"_\"\nFROM \"location\" AS \"location_1\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"The function name containing ? serves as a template.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(location) |>\n Select(Fun.\"SUBSTRING(? FROM ? FOR ?)\"(Get.city, 1, 1))\n\nprint(render(q))\n#=>\nSELECT SUBSTRING(\"location_1\".\"city\" FROM 1 FOR 1) AS \"_\"\nFROM \"location\" AS \"location_1\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"The number of arguments to a function must coincide with the number of placeholders in the template.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Fun.\"SUBSTRING(? FROM ? FOR ?)\"(Get.city)\n#=>\nERROR: FunSQL.InvalidArityError: `SUBSTRING(? FROM ? FOR ?)` expects 3 arguments, got 1 in:\nFun.\"SUBSTRING(? FROM ? FOR ?)\"(Get.city)\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Some common functions also validate the number of arguments.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Fun.case()\n#=>\nERROR: FunSQL.InvalidArityError: `case` expects at least 2 arguments, got 0 in:\nFun.case()\n=#\n\nFun.is_null(Get.city, Get.state)\n#=>\nERROR: FunSQL.InvalidArityError: `is_null` expects 1 argument, got 2 in:\nFun.is_null(Get.city, Get.state)\n=#\n\nFun.count(Get.city, Get.state)\n#=>\nERROR: FunSQL.InvalidArityError: `count` expects from 0 to 1 argument, got 2 in:\nFun.count(Get.city, Get.state)\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"A function invocation may include a nested query.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"p = From(person) |>\n Where(Get.year_of_birth .> 1950)\n\nq = Select(Fun.exists(p))\n\nprint(render(q))\n#=>\nSELECT (EXISTS (\n SELECT NULL AS \"_\"\n FROM \"person\" AS \"person_1\"\n WHERE (\"person_1\".\"year_of_birth\" > 1950)\n)) AS \"exists\"\n=#\n\np = From(concept) |>\n Where(Fun.and(Get.vocabulary_id .== \"Gender\",\n Get.concept_code .== \"F\")) |>\n Select(Get.concept_id)\n\nq = From(person) |>\n Where(Fun.in(Get.gender_concept_id, p))\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"location_id\"\nFROM \"person\" AS \"person_1\"\nWHERE (\"person_1\".\"gender_concept_id\" IN (\n SELECT \"concept_1\".\"concept_id\"\n FROM \"concept\" AS \"concept_1\"\n WHERE\n (\"concept_1\".\"vocabulary_id\" = 'Gender') AND\n (\"concept_1\".\"concept_code\" = 'F')\n))\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"FunSQL can simplify logical expressions.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Where(Fun.and())\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"location_id\"\nFROM \"person\" AS \"person_1\"\n=#\n\nq = From(person) |>\n Select(Get.person_id) |>\n Where(Fun.and())\n\nprint(render(q))\n#=>\nSELECT \"person_1\".\"person_id\"\nFROM \"person\" AS \"person_1\"\n=#\n\nq = From(person) |>\n Where(Fun.and(Get.year_of_birth .> 1950))\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"location_id\"\nFROM \"person\" AS \"person_1\"\nWHERE (\"person_1\".\"year_of_birth\" > 1950)\n=#\n\nq = From(person) |>\n Where(foldl(Fun.and, [Get.year_of_birth .> 1950, Get.year_of_birth .< 1960, Get.year_of_birth .!= 1955], init = Fun.and()))\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"location_id\"\nFROM \"person\" AS \"person_1\"\nWHERE\n (\"person_1\".\"year_of_birth\" > 1950) AND\n (\"person_1\".\"year_of_birth\" < 1960) AND\n (\"person_1\".\"year_of_birth\" <> 1955)\n=#\n\nq = From(person) |>\n Where(Fun.or())\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"location_id\"\nFROM \"person\" AS \"person_1\"\nWHERE FALSE\n=#\n\nq = From(person) |>\n Where(Fun.or(Get.year_of_birth .> 1950))\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"location_id\"\nFROM \"person\" AS \"person_1\"\nWHERE (\"person_1\".\"year_of_birth\" > 1950)\n=#\n\nq = From(person) |>\n Where(Fun.or(Fun.or(Fun.or(), Get.year_of_birth .> 1950), Get.year_of_birth .< 1960))\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"location_id\"\nFROM \"person\" AS \"person_1\"\nWHERE\n (\"person_1\".\"year_of_birth\" > 1950) OR\n (\"person_1\".\"year_of_birth\" < 1960)\n=#\n\n#? VERSION >= v\"1.7\"\nq = From(person) |>\n Where(Get.year_of_birth .> 1950 .|| Get.year_of_birth .< 1960 .|| Get.year_of_birth .!= 1955)\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"location_id\"\nFROM \"person\" AS \"person_1\"\nWHERE\n (\"person_1\".\"year_of_birth\" > 1950) OR\n (\"person_1\".\"year_of_birth\" < 1960) OR\n (\"person_1\".\"year_of_birth\" <> 1955)\n=#\n\nq = From(person) |>\n Where(Fun.not(false))\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"location_id\"\nFROM \"person\" AS \"person_1\"\n=#","category":"page"},{"location":"test/nodes/#Append","page":"SQL Nodes","title":"Append","text":"","category":"section"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"The Append constructor creates a subquery that concatenates the output of multiple queries.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(measurement) |>\n Define(:date => Get.measurement_date) |>\n Append(From(observation) |>\n Define(:date => Get.observation_date))\n#-> (…) |> Append(…)\n\ndisplay(q)\n#=>\nlet measurement = SQLTable(:measurement, …),\n observation = SQLTable(:observation, …),\n q1 = From(measurement),\n q2 = q1 |> Define(Get.measurement_date |> As(:date)),\n q3 = From(observation),\n q4 = q3 |> Define(Get.observation_date |> As(:date)),\n q5 = q2 |> Append(q4)\n q5\nend\n=#\n\nprint(render(q |> Select(Get.person_id, Get.date)))\n#=>\nSELECT\n \"union_1\".\"person_id\",\n \"union_1\".\"date\"\nFROM (\n SELECT\n \"measurement_1\".\"person_id\",\n \"measurement_1\".\"measurement_date\" AS \"date\"\n FROM \"measurement\" AS \"measurement_1\"\n UNION ALL\n SELECT\n \"observation_1\".\"person_id\",\n \"observation_1\".\"observation_date\" AS \"date\"\n FROM \"observation\" AS \"observation_1\"\n) AS \"union_1\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Append can also be specified without the over node.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = Append(From(measurement) |>\n Define(:date => Get.measurement_date),\n From(observation) |>\n Define(:date => Get.observation_date)) |>\n Select(Get.person_id, Get.date)\n\nprint(render(q))\n#=>\nSELECT\n \"union_1\".\"person_id\",\n \"union_1\".\"date\"\nFROM (\n SELECT\n \"measurement_1\".\"person_id\",\n \"measurement_1\".\"measurement_date\" AS \"date\"\n FROM \"measurement\" AS \"measurement_1\"\n UNION ALL\n SELECT\n \"observation_1\".\"person_id\",\n \"observation_1\".\"observation_date\" AS \"date\"\n FROM \"observation\" AS \"observation_1\"\n) AS \"union_1\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"An Append node can be created using @funsql notation.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = @funsql begin\n from(measurement).define(date => measurement_date)\n append(from(observation).define(date => observation_date))\nend\n\ndisplay(q)\n#=>\nlet q1 = From(:measurement),\n q2 = q1 |> Define(Get.measurement_date |> As(:date)),\n q3 = From(:observation),\n q4 = q3 |> Define(Get.observation_date |> As(:date)),\n q5 = q2 |> Append(q4)\n q5\nend\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Append will automatically assign unique aliases to the exported columns.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(measurement) |>\n Define(:concept_id => Get.measurement_concept_id) |>\n Group(Get.person_id) |>\n Define(:count => 1, :count_2 => 2) |>\n Append(From(observation) |>\n Define(:concept_id => Get.observation_concept_id) |>\n Group(Get.person_id) |>\n Define(:count => 10, :count_2 => 20)) |>\n Select(Get.person_id, :agg_count => Agg.count(), Get.count_2, Get.count)\n\nprint(render(q))\n#=>\nSELECT\n \"union_1\".\"person_id\",\n \"union_1\".\"count\" AS \"agg_count\",\n \"union_1\".\"count_2\",\n \"union_1\".\"count_3\" AS \"count\"\nFROM (\n SELECT\n \"measurement_1\".\"person_id\",\n count(*) AS \"count\",\n 2 AS \"count_2\",\n 1 AS \"count_3\"\n FROM \"measurement\" AS \"measurement_1\"\n GROUP BY \"measurement_1\".\"person_id\"\n UNION ALL\n SELECT\n \"observation_1\".\"person_id\",\n count(*) AS \"count\",\n 20 AS \"count_2\",\n 10 AS \"count_3\"\n FROM \"observation\" AS \"observation_1\"\n GROUP BY \"observation_1\".\"person_id\"\n) AS \"union_1\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Append will not put duplicate expressions into the SELECT clauses of the nested subqueries.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Join(From(measurement) |>\n Define(:date => Get.measurement_date) |>\n Append(From(observation) |>\n Define(:date => Get.observation_date)) |>\n As(:assessment),\n on = Get.person_id .== Get.assessment.person_id) |>\n Where(Get.assessment.date .> Fun.current_timestamp()) |>\n Select(Get.person_id, Get.assessment.date)\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"person_id\",\n \"assessment_1\".\"date\"\nFROM \"person\" AS \"person_1\"\nJOIN (\n SELECT\n \"measurement_1\".\"measurement_date\" AS \"date\",\n \"measurement_1\".\"person_id\"\n FROM \"measurement\" AS \"measurement_1\"\n UNION ALL\n SELECT\n \"observation_1\".\"observation_date\" AS \"date\",\n \"observation_1\".\"person_id\"\n FROM \"observation\" AS \"observation_1\"\n) AS \"assessment_1\" ON (\"person_1\".\"person_id\" = \"assessment_1\".\"person_id\")\nWHERE (\"assessment_1\".\"date\" > CURRENT_TIMESTAMP)\n=#\n\nq = From(measurement) |>\n Define(:date => Get.measurement_date) |>\n Append(From(observation) |>\n Define(:date => Get.observation_date)) |>\n Group(Get.date) |>\n Define(Agg.count())\n\nprint(render(q))\n#=>\nSELECT\n \"union_1\".\"date\",\n count(*) AS \"count\"\nFROM (\n SELECT \"measurement_1\".\"measurement_date\" AS \"date\"\n FROM \"measurement\" AS \"measurement_1\"\n UNION ALL\n SELECT \"observation_1\".\"observation_date\" AS \"date\"\n FROM \"observation\" AS \"observation_1\"\n) AS \"union_1\"\nGROUP BY \"union_1\".\"date\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Append aligns the columns of its subqueries.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(measurement) |>\n Select(Get.person_id, :date => Get.measurement_date) |>\n Append(From(observation) |>\n Select(:date => Get.observation_date, Get.person_id))\n\nprint(render(q))\n#=>\nSELECT\n \"measurement_1\".\"person_id\",\n \"measurement_1\".\"measurement_date\" AS \"date\"\nFROM \"measurement\" AS \"measurement_1\"\nUNION ALL\nSELECT\n \"observation_2\".\"person_id\",\n \"observation_2\".\"date\"\nFROM (\n SELECT\n \"observation_1\".\"observation_date\" AS \"date\",\n \"observation_1\".\"person_id\"\n FROM \"observation\" AS \"observation_1\"\n) AS \"observation_2\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Arguments of Append may contain ORDER BY or LIMIT clauses, which must be wrapped in a nested subquery.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(measurement) |>\n Order(Get.measurement_date) |>\n Select(Get.person_id, :date => Get.measurement_date) |>\n Append(From(observation) |>\n Define(:date => Get.observation_date) |>\n Limit(1))\n\nprint(render(q))\n#=>\nSELECT\n \"measurement_2\".\"person_id\",\n \"measurement_2\".\"date\"\nFROM (\n SELECT\n \"measurement_1\".\"person_id\",\n \"measurement_1\".\"measurement_date\" AS \"date\"\n FROM \"measurement\" AS \"measurement_1\"\n ORDER BY \"measurement_1\".\"measurement_date\"\n) AS \"measurement_2\"\nUNION ALL\nSELECT\n \"observation_2\".\"person_id\",\n \"observation_2\".\"date\"\nFROM (\n SELECT\n \"observation_1\".\"person_id\",\n \"observation_1\".\"observation_date\" AS \"date\"\n FROM \"observation\" AS \"observation_1\"\n FETCH FIRST 1 ROW ONLY\n) AS \"observation_2\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"An Append without any queries can be created explicitly.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = Append(args = [])\n#-> Append(args = [])\n\nprint(render(q))\n#=>\nSELECT NULL AS \"_\"\nWHERE FALSE\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Without an explicit Select, the output of Append includes the common columns of the nested queries.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = Append(measurement, observation)\n\nprint(render(q))\n#=>\nSELECT \"measurement_1\".\"person_id\"\nFROM \"measurement\" AS \"measurement_1\"\nUNION ALL\nSELECT \"observation_1\".\"person_id\"\nFROM \"observation\" AS \"observation_1\"\n=#","category":"page"},{"location":"test/nodes/#Iterate","page":"SQL Nodes","title":"Iterate","text":"","category":"section"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"The Iterate constructor creates an iteration query. In the argument of Iterate, the From(^) node refers to the output of the previous iteration. We could use Iterate and From(^) to create a factorial table.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = Define(:n => 1, :f => 1) |>\n Iterate(From(^) |>\n Define(:n => Get.n .+ 1, :f => Get.f .* (Get.n .+ 1)) |>\n Where(Get.n .<= 10))\n#-> (…) |> Iterate(…)\n\ndisplay(q)\n#=>\nlet q1 = Define(1 |> As(:n), 1 |> As(:f)),\n q2 = From(^),\n q3 = q2 |>\n Define(Fun.\"+\"(Get.n, 1) |> As(:n),\n Fun.\"*\"(Get.f, Fun.\"+\"(Get.n, 1)) |> As(:f)),\n q4 = q3 |> Where(Fun.\"<=\"(Get.n, 10)),\n q5 = q1 |> Iterate(q4)\n q5\nend\n=#\n\nprint(render(q))\n#=>\nWITH RECURSIVE \"__1\" (\"n\", \"f\") AS (\n SELECT\n 1 AS \"n\",\n 1 AS \"f\"\n UNION ALL\n SELECT\n \"__3\".\"n\",\n \"__3\".\"f\"\n FROM (\n SELECT\n (\"__2\".\"n\" + 1) AS \"n\",\n (\"__2\".\"f\" * (\"__2\".\"n\" + 1)) AS \"f\"\n FROM \"__1\" AS \"__2\"\n ) AS \"__3\"\n WHERE (\"__3\".\"n\" <= 10)\n)\nSELECT\n \"__4\".\"n\",\n \"__4\".\"f\"\nFROM \"__1\" AS \"__4\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"An Iterate node can be created using @funsql notation.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = @funsql begin\n define(n => 1, f => 1)\n iterate(define(n => n + 1, f => f * (n + 1)).filter(n <= 10))\nend\n\ndisplay(q)\n#=>\nlet q1 = Define(1 |> As(:n), 1 |> As(:f)),\n q2 = Define(Fun.\"+\"(Get.n, 1) |> As(:n),\n Fun.\"*\"(Get.f, Fun.\"+\"(Get.n, 1)) |> As(:f)),\n q3 = q2 |> Where(Fun.\"<=\"(Get.n, 10)),\n q4 = q1 |> Iterate(q3)\n q4\nend\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"The From(^) node in front of the iterator query can be omitted.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = Define(:n => 1, :f => 1) |>\n Iterate(Define(:n => Get.n .+ 1, :f => Get.f .* (Get.n .+ 1)) |>\n Where(Get.n .<= 10))\n\nprint(render(q))\n#=>\nWITH RECURSIVE \"__1\" (\"n\", \"f\") AS (\n SELECT\n 1 AS \"n\",\n 1 AS \"f\"\n UNION ALL\n SELECT\n \"__3\".\"n\",\n \"__3\".\"f\"\n FROM (\n SELECT\n (\"__2\".\"n\" + 1) AS \"n\",\n (\"__2\".\"f\" * (\"__2\".\"n\" + 1)) AS \"f\"\n FROM \"__1\" AS \"__2\"\n ) AS \"__3\"\n WHERE (\"__3\".\"n\" <= 10)\n)\nSELECT\n \"__4\".\"n\",\n \"__4\".\"f\"\nFROM \"__1\" AS \"__4\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"An Iterate node may use a CTE.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = Define(:n => 1, :f => 1) |>\n Iterate(Define(:n => Get.n .+ 1, :f => Get.f .* (Get.n .+ 1)) |>\n CrossJoin(From(:threshold)) |>\n Where(Get.n .<= Get.threshold)) |>\n With(:threshold => Define(:threshold => 10))\n\nprint(render(q))\n#=>\nWITH RECURSIVE \"threshold_1\" (\"threshold\") AS (\n SELECT 10 AS \"threshold\"\n),\n\"__1\" (\"n\", \"f\") AS (\n SELECT\n 1 AS \"n\",\n 1 AS \"f\"\n UNION ALL\n SELECT\n \"__3\".\"n\",\n \"__3\".\"f\"\n FROM (\n SELECT\n (\"__2\".\"n\" + 1) AS \"n\",\n (\"__2\".\"f\" * (\"__2\".\"n\" + 1)) AS \"f\",\n \"threshold_2\".\"threshold\"\n FROM \"__1\" AS \"__2\"\n CROSS JOIN \"threshold_1\" AS \"threshold_2\"\n ) AS \"__3\"\n WHERE (\"__3\".\"n\" <= \"__3\".\"threshold\")\n)\nSELECT\n \"__4\".\"n\",\n \"__4\".\"f\"\nFROM \"__1\" AS \"__4\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"It is an error to use From(^) outside of Iterate.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(^)\n\nprint(render(q))\n#=>\nERROR: FunSQL.ReferenceError: self-reference outside of Iterate in:\nlet q1 = From(^)\n q1\nend\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"The set of columns produced by Iterate is the intersection of the columns produced by the base query and the iterator query.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = Define(:k => 0, :m => 0) |>\n Iterate(As(:previous) |>\n Where(Get.previous.m .< 10) |>\n Define(:m => Get.previous.m .+ 1, :n => 0))\n\nprint(render(q))\n#=>\nWITH RECURSIVE \"previous_1\" (\"m\") AS (\n SELECT 0 AS \"m\"\n UNION ALL\n SELECT (\"previous_2\".\"m\" + 1) AS \"m\"\n FROM \"previous_1\" AS \"previous_2\"\n WHERE (\"previous_2\".\"m\" < 10)\n)\nSELECT \"previous_3\".\"m\"\nFROM \"previous_1\" AS \"previous_3\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Iterate aligns the columns of its subqueries.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = Select(:n => 1, :f => 1) |>\n Iterate(Where(Get.n .< 10) |>\n Select(:f => (Get.n .+ 1) .* Get.f,\n :n => Get.n .+ 1))\n\nprint(render(q))\n#=>\nWITH RECURSIVE \"__1\" (\"n\", \"f\") AS (\n SELECT\n 1 AS \"n\",\n 1 AS \"f\"\n UNION ALL\n SELECT\n \"__3\".\"n\",\n \"__3\".\"f\"\n FROM (\n SELECT\n ((\"__2\".\"n\" + 1) * \"__2\".\"f\") AS \"f\",\n (\"__2\".\"n\" + 1) AS \"n\"\n FROM \"__1\" AS \"__2\"\n WHERE (\"__2\".\"n\" < 10)\n ) AS \"__3\"\n)\nSELECT\n \"__4\".\"n\",\n \"__4\".\"f\"\nFROM \"__1\" AS \"__4\"\n=#","category":"page"},{"location":"test/nodes/#As","page":"SQL Nodes","title":"As","text":"","category":"section"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"An alias to an expression can be added with the As constructor.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"e = 42 |> As(:integer)\n#-> (…) |> As(:integer)\n\ndisplay(e)\n#-> 42 |> As(:integer)\n\nprint(render(Select(e)))\n#=>\nSELECT 42 AS \"integer\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"As node can be created with @funsql.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"e = @funsql (42).as(integer)\n\ndisplay(e)\n#-> 42 |> As(:integer)","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"The => shorthand is supported by @funsql.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"e = @funsql integer => 42\n\ndisplay(e)\n#-> :integer => 42","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"As is also used to create an alias for a subquery.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n As(:p) |>\n Select(Get.p.person_id)\n\nprint(render(q))\n#=>\nSELECT \"person_1\".\"person_id\"\nFROM \"person\" AS \"person_1\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"As blocks the default output columns.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |> As(:p)\n\nprint(render(q))\n#=>\nSELECT NULL AS \"_\"\nFROM \"person\" AS \"person_1\"\n=#","category":"page"},{"location":"test/nodes/#From","page":"SQL Nodes","title":"From","text":"","category":"section"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"The From constructor creates a subquery that selects columns from the given table.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person)\n#-> From(…)\n\ndisplay(q)\n#-> From(SQLTable(:person, …))","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"By default, From selects all columns from the table.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"print(render(q))\n#=>\nSELECT\n \"person_1\".\"person_id\",\n \"person_1\".\"gender_concept_id\",\n \"person_1\".\"year_of_birth\",\n \"person_1\".\"month_of_birth\",\n \"person_1\".\"day_of_birth\",\n \"person_1\".\"birth_datetime\",\n \"person_1\".\"location_id\"\nFROM \"person\" AS \"person_1\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"From adds the schema qualifier when the table has the schema.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"const pg_database =\n SQLTable(qualifiers = [:pg_catalog], :pg_database, columns = [:oid, :datname])\n\nq = From(pg_database)\n\nprint(render(q))\n#=>\nSELECT\n \"pg_database_1\".\"oid\",\n \"pg_database_1\".\"datname\"\nFROM \"pg_catalog\".\"pg_database\" AS \"pg_database_1\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"In a suitable context, a SQLTable object is automatically converted to a From subquery.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"print(render(person))\n#=>\nSELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"location_id\"\nFROM \"person\" AS \"person_1\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"From and other subqueries generate a correct SELECT clause when the table has no columns.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"empty = SQLTable(:empty, columns = Symbol[])\n\nq = From(empty) |>\n Where(false) |>\n Select(args = [])\n\ndisplay(q)\n#=>\nlet empty = SQLTable(:empty, …),\n q1 = From(empty),\n q2 = q1 |> Where(false),\n q3 = q2 |> Select(args = [])\n q3\nend\n=#\n\nprint(render(q))\n#=>\nSELECT NULL AS \"_\"\nFROM \"empty\" AS \"empty_1\"\nWHERE FALSE\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"When From takes a Tables-compatible argument, it generates a VALUES query.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"using DataFrames\n\ndf = DataFrame(name = [\"SQL\", \"Julia\", \"FunSQL\"],\n year = [1974, 2012, 2021])\n\nq = From(df)\n#-> From(…)\n\ndisplay(q)\n#-> From((name = [\"SQL\", …], year = [1974, …]))\n\nprint(render(q))\n#=>\nSELECT\n \"values_1\".\"name\",\n \"values_1\".\"year\"\nFROM (\n VALUES\n ('SQL', 1974),\n ('Julia', 2012),\n ('FunSQL', 2021)\n) AS \"values_1\" (\"name\", \"year\")\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"SQLite does not support column aliases with AS clause.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"print(render(q, dialect = :sqlite))\n#=>\nSELECT\n \"values_1\".\"column1\" AS \"name\",\n \"values_1\".\"column2\" AS \"year\"\nFROM (\n VALUES\n ('SQL', 1974),\n ('Julia', 2012),\n ('FunSQL', 2021)\n) AS \"values_1\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Only columns that are used in the query will be serialized.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(df) |>\n Select(Get.name)\n\nprint(render(q))\n#=>\nSELECT \"values_1\".\"name\"\nFROM (\n VALUES\n ('SQL'),\n ('Julia'),\n ('FunSQL')\n) AS \"values_1\" (\"name\")\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"A column of NULLs will be added if no actual columns are used.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(df) |>\n Group() |>\n Select(Agg.count())\n\nprint(render(q))\n#=>\nSELECT count(*) AS \"count\"\nFROM (\n VALUES\n (NULL),\n (NULL),\n (NULL)\n) AS \"values_1\" (\"_\")\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Since VALUES clause requires at least one row of data, a different representation is used when the source table is empty.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(df[1:0, :])\n\nprint(render(q))\n#=>\nSELECT\n NULL AS \"name\",\n NULL AS \"year\"\nWHERE FALSE\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"The source table must have at least one column.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(df[1:0, 1:0])\n#=>\nERROR: DomainError with 0×0 DataFrame:\na table with at least one column is expected\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"From can accept a table-valued function. Since the output type of the function is not known to FunSQL, you must manually specify the names of the output columns.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(Fun.generate_series(0, 100, 10), columns = [:value])\n#-> From(…, columns = [:value])\n\ndisplay(q)\n#-> From(Fun.generate_series(0, 100, 10), columns = [:value])\n\nprint(render(q))\n#=>\nSELECT \"generate_series_1\".\"value\"\nFROM generate_series(0, 100, 10) AS \"generate_series_1\" (\"value\")\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"WITH ORDINALITY annotation adds an extra column that enumerates the output rows.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(Fun.\"? WITH ORDINALITY\"(Fun.generate_series(0, 100, 10)),\n columns = [:value, :index])\n\nprint(render(q))\n#=>\nSELECT\n \"__1\".\"value\",\n \"__1\".\"index\"\nFROM generate_series(0, 100, 10) WITH ORDINALITY AS \"__1\" (\"value\", \"index\")\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"A From node can be created with @funsql notation.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = @funsql from(person)\n\ndisplay(q)\n#-> From(:person)\n\nq = @funsql from(nothing)\n\ndisplay(q)\n#-> From(nothing)\n\nq = @funsql from(^)\n\ndisplay(q)\n#-> From(^)\n\nq = @funsql from($person)\n\ndisplay(q)\n#-> From(SQLTable(:person, …))\n\nq = @funsql from($df)\n\ndisplay(q)\n#-> From((name = [\"SQL\", …], year = [1974, …]))\n\nfunsql_generate_series = FunSQL.FunClosure(:generate_series)\n\nq = @funsql from(generate_series(0, 100, 10), columns = [value])\n\ndisplay(q)\n#-> From(Fun.generate_series(0, 100, 10), columns = [:value])","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"When From with a tabular function is attached to the right branch of a Join node, the function may use data from the left branch of Join, even without being wrapped in a Bind node.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(Fun.regexp_split_to_table(\"(10,20)-(30,40)-(50,60)\", \"-\"),\n columns = [:point]) |>\n CrossJoin(From(Fun.regexp_matches(Get.point, \"(\\\\d+),(\\\\d+)\"),\n columns = [:captures])) |>\n Select(:x => Fun.\"CAST(?[1] AS INTEGER)\"(Get.captures),\n :y => Fun.\"CAST(?[2] AS INTEGER)\"(Get.captures))\n\nprint(render(q))\n#=>\nSELECT\n CAST(\"regexp_matches_1\".\"captures\"[1] AS INTEGER) AS \"x\",\n CAST(\"regexp_matches_1\".\"captures\"[2] AS INTEGER) AS \"y\"\nFROM regexp_split_to_table('(10,20)-(30,40)-(50,60)', '-') AS \"regexp_split_to_table_1\" (\"point\")\nCROSS JOIN regexp_matches(\"regexp_split_to_table_1\".\"point\", '(\\d+),(\\d+)') AS \"regexp_matches_1\" (\"captures\")\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"All the columns of a tabular function must have distinct names.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"From(Fun.\"? WITH ORDINALITY\"(Fun.generate_series(0, 100, 10)),\n columns = [:index, :index])\n#=>\nERROR: FunSQL.DuplicateLabelError: `index` is used more than once in:\nlet q1 = From(Fun.\"? WITH ORDINALITY\"(Fun.generate_series(0, 100, 10)),\n columns = [:index, :index])\n q1\nend\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"From(nothing) will generate a unit dataset with one row.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(nothing)\n\ndisplay(q)\n#-> From(nothing)\n\nprint(render(q))\n#=>\nSELECT NULL AS \"_\"\n=#","category":"page"},{"location":"test/nodes/#With,-Over,-and-WithExternal","page":"SQL Nodes","title":"With, Over, and WithExternal","text":"","category":"section"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"We can create a temporary dataset using With and refer to it with From.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(:male) |>\n With(From(person) |>\n Where(Get.gender_concept_id .== 8507) |>\n As(:male))\n\ndisplay(q)\n#=>\nlet person = SQLTable(:person, …),\n q1 = From(:male),\n q2 = From(person),\n q3 = q2 |> Where(Fun.\"=\"(Get.gender_concept_id, 8507)),\n q4 = q1 |> With(q3 |> As(:male))\n q4\nend\n=#\n\nprint(render(q))\n#=>\nWITH \"male_1\" (\"person_id\", …, \"location_id\") AS (\n SELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"location_id\"\n FROM \"person\" AS \"person_1\"\n WHERE (\"person_1\".\"gender_concept_id\" = 8507)\n)\nSELECT\n \"male_2\".\"person_id\",\n ⋮\n \"male_2\".\"location_id\"\nFROM \"male_1\" AS \"male_2\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"With definitions can be annotated as materialized or not materialized:","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(:male) |>\n With(From(person) |>\n Where(Get.gender_concept_id .== 8507) |>\n As(:male),\n materialized = true)\n#-> (…) |> With(…, materialized = true)\n\nprint(render(q))\n#=>\nWITH \"male_1\" ( … ) AS MATERIALIZED (\n ⋮\n)\nSELECT\n ⋮\nFROM \"male_1\" AS \"male_2\"\n=#\n\nq = From(:male) |>\n With(From(person) |>\n Where(Get.gender_concept_id .== 8507) |>\n As(:male),\n materialized = false)\n\nprint(render(q))\n#=>\nWITH \"male_1\" ( … ) AS NOT MATERIALIZED (\n ⋮\n)\nSELECT\n ⋮\nFROM \"male_1\" AS \"male_2\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"With can take more than one definition.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = Select(:male_count => From(:male) |> Group() |> Select(Agg.count()),\n :female_count => From(:female) |> Group() |> Select(Agg.count())) |>\n With(:male => From(person) |> Where(Get.gender_concept_id .== 8507),\n :female => From(person) |> Where(Get.gender_concept_id .== 8532))\n\nprint(render(q))\n#=>\nWITH \"male_1\" (\"_\") AS (\n SELECT NULL AS \"_\"\n FROM \"person\" AS \"person_1\"\n WHERE (\"person_1\".\"gender_concept_id\" = 8507)\n),\n\"female_1\" (\"_\") AS (\n SELECT NULL AS \"_\"\n FROM \"person\" AS \"person_2\"\n WHERE (\"person_2\".\"gender_concept_id\" = 8532)\n)\nSELECT\n (\n SELECT count(*) AS \"count\"\n FROM \"male_1\" AS \"male_2\"\n ) AS \"male_count\",\n (\n SELECT count(*) AS \"count\"\n FROM \"female_1\" AS \"female_2\"\n ) AS \"female_count\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"With can shadow the previous With definition.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(:cohort) |>\n With(:cohort => From(:cohort) |> Where(Get.gender_concept_id .== 8507)) |>\n With(:cohort => From(:cohort) |> Where(Get.year_of_birth .>= 1950)) |>\n With(:cohort => From(person)) |>\n Select(Get.person_id)\n\nprint(render(q))\n#=>\nWITH \"cohort_1\" (\"person_id\", \"gender_concept_id\", \"year_of_birth\") AS (\n SELECT\n \"person_1\".\"person_id\",\n \"person_1\".\"gender_concept_id\",\n \"person_1\".\"year_of_birth\"\n FROM \"person\" AS \"person_1\"\n),\n\"cohort_3\" (\"person_id\", \"gender_concept_id\") AS (\n SELECT\n \"cohort_2\".\"person_id\",\n \"cohort_2\".\"gender_concept_id\"\n FROM \"cohort_1\" AS \"cohort_2\"\n WHERE (\"cohort_2\".\"year_of_birth\" >= 1950)\n),\n\"cohort_5\" (\"person_id\") AS (\n SELECT \"cohort_4\".\"person_id\"\n FROM \"cohort_3\" AS \"cohort_4\"\n WHERE (\"cohort_4\".\"gender_concept_id\" = 8507)\n)\nSELECT \"cohort_6\".\"person_id\"\nFROM \"cohort_5\" AS \"cohort_6\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"A With node can be created using @funsql.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = @funsql begin\n from(male)\n with(male => from(person).filter(gender_concept_id == 8507),\n materialized = false)\nend\n\ndisplay(q)\n#=>\nlet q1 = From(:male),\n q2 = From(:person),\n q3 = q2 |> Where(Fun.\"=\"(Get.gender_concept_id, 8507)),\n q4 = q1 |> With(q3 |> As(:male), materialized = false)\n q4\nend\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"A dataset defined by With must have an explicit label assigned to it.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(:person) |>\n With(From(person))\n\nprint(render(q))\n#=>\nERROR: FunSQL.ReferenceError: table reference `person` requires As in:\nlet person = SQLTable(:person, …),\n q1 = From(:person),\n q2 = From(person),\n q3 = q1 |> With(q2)\n q3\nend\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Datasets defined by With must have a unique label.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"From(:p) |>\nWith(:p => From(person),\n :p => From(person))\n#=>\nERROR: FunSQL.DuplicateLabelError: `p` is used more than once in:\nlet person = SQLTable(:person, …),\n q1 = From(person),\n q2 = From(person),\n q3 = With(q1 |> As(:p), q2 |> As(:p))\n q3\nend\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"It is an error for From to refer to an undefined dataset.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(:p)\n\nprint(render(q))\n#=>\nERROR: FunSQL.ReferenceError: cannot find `p` in:\nlet q1 = From(:p)\n q1\nend\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"A variant of With called Over exchanges the positions of the definition and the query that uses it.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Where(Get.gender_concept_id .== 8507) |>\n As(:male) |>\n Over(From(:male))\n#-> (…) |> Over(…)\n\ndisplay(q)\n#=>\nlet person = SQLTable(:person, …),\n q1 = From(person),\n q2 = q1 |> Where(Fun.\"=\"(Get.gender_concept_id, 8507)),\n q3 = From(:male),\n q4 = q2 |> As(:male) |> Over(q3)\n q4\nend\n=#\n\nprint(render(q))\n#=>\nWITH \"male_1\" (\"person_id\", …, \"location_id\") AS (\n SELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"location_id\"\n FROM \"person\" AS \"person_1\"\n WHERE (\"person_1\".\"gender_concept_id\" = 8507)\n)\nSELECT\n \"male_2\".\"person_id\",\n ⋮\n \"male_2\".\"location_id\"\nFROM \"male_1\" AS \"male_2\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"An Over node can be created using @funsql.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = @funsql begin\n male => from(person).filter(gender_concept_id == 8507)\n over(from(male), materialized = true)\nend\n\ndisplay(q)\n#=>\nlet q1 = From(:person),\n q2 = q1 |> Where(Fun.\"=\"(Get.gender_concept_id, 8507)),\n q3 = From(:male),\n q4 = q2 |> As(:male) |> Over(q3, materialized = true)\n q4\nend\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"A variant of With called WithExternal can be used to prepare a definition for a CREATE TABLE AS or SELECT INTO statement.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"with_external_handler((tbl, def)) =\n println(\"CREATE TEMP TABLE \",\n render(ID(tbl.qualifiers, tbl.name)),\n \" (\", join([render(ID(c.name)) for (n, c) in tbl.columns], \", \"), \") AS\\n\",\n render(def), \";\\n\")\n\nq = From(:male) |>\n WithExternal(From(person) |>\n Where(Get.gender_concept_id .== 8507) |>\n As(:male),\n qualifiers = [:tmp],\n handler = with_external_handler)\n#-> (…) |> WithExternal(…, qualifiers = [:tmp], handler = with_external_handler)\n\nprint(render(q))\n#=>\nCREATE TEMP TABLE \"tmp\".\"male\" (\"person_id\", …, \"location_id\") AS\nSELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"location_id\"\nFROM \"person\" AS \"person_1\"\nWHERE (\"person_1\".\"gender_concept_id\" = 8507);\n\nSELECT\n \"male_1\".\"person_id\",\n ⋮\n \"male_1\".\"location_id\"\nFROM \"tmp\".\"male\" AS \"male_1\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Datasets defined by WithExternal must have a unique label.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"From(:p) |>\nWithExternal(:p => From(person),\n :p => From(person))\n#=>\nERROR: FunSQL.DuplicateLabelError: `p` is used more than once in:\nlet person = SQLTable(:person, …),\n q1 = From(person),\n q2 = From(person),\n q3 = WithExternal(q1 |> As(:p), q2 |> As(:p))\n q3\nend\n=#","category":"page"},{"location":"test/nodes/#Group","page":"SQL Nodes","title":"Group","text":"","category":"section"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"The Group constructor creates a subquery that summarizes the rows partitioned by the given keys.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Group(Get.year_of_birth)\n#-> (…) |> Group(…)\n\ndisplay(q)\n#=>\nlet person = SQLTable(:person, …),\n q1 = From(person),\n q2 = q1 |> Group(Get.year_of_birth)\n q2\nend\n=#\n\nprint(render(q))\n#=>\nSELECT DISTINCT \"person_1\".\"year_of_birth\"\nFROM \"person\" AS \"person_1\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"A Group node can be created using @funsql notation.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = @funsql from(person).group(year_of_birth)\n\ndisplay(q)\n#=>\nlet q1 = From(:person),\n q2 = q1 |> Group(Get.year_of_birth)\n q2\nend\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Partitions created by Group are summarized using aggregate expressions.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Agg.count\n#-> Agg.count\n\nq = From(person) |>\n Group(Get.year_of_birth) |>\n Select(Get.year_of_birth, Agg.count())\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"year_of_birth\",\n count(*) AS \"count\"\nFROM \"person\" AS \"person_1\"\nGROUP BY \"person_1\".\"year_of_birth\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Aggregate functions can be created with @funsql.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"e = @funsql agg(min, year_of_birth)\n\ndisplay(e)\n#-> Agg.min(Get.year_of_birth)\n\ne = @funsql min(year_of_birth)\n\ndisplay(e)\n#-> Agg.min(Get.year_of_birth)\n\ne = @funsql count(filter = year_of_birth > 1950)\n\ndisplay(e)\n#-> Agg.count(filter = Fun.\">\"(Get.year_of_birth, 1950))\n\ne = @funsql visit_group.count()\n\ndisplay(e)\n#-> Get.visit_group |> Agg.count()\n\ne = @funsql `count`()\n\ndisplay(e)\n#-> Agg.count()\n\ne = @funsql visit_group.`count`()\n\ndisplay(e)\n#-> Get.visit_group |> Agg.count()\n\ne = @funsql `visit_group`.`count`()\n\ndisplay(e)\n#-> Get.visit_group |> Agg.count()","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Group will create a single instance of an aggregate function even if it is used more than once.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Join(:visit_group => From(visit_occurrence) |>\n Group(Get.person_id),\n on = Get.person_id .== Get.visit_group.person_id) |>\n Where(Agg.count(over = Get.visit_group) .>= 2) |>\n Select(Get.person_id, Agg.count(over = Get.visit_group))\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"person_id\",\n \"visit_group_1\".\"count\"\nFROM \"person\" AS \"person_1\"\nJOIN (\n SELECT\n count(*) AS \"count\",\n \"visit_occurrence_1\".\"person_id\"\n FROM \"visit_occurrence\" AS \"visit_occurrence_1\"\n GROUP BY \"visit_occurrence_1\".\"person_id\"\n) AS \"visit_group_1\" ON (\"person_1\".\"person_id\" = \"visit_group_1\".\"person_id\")\nWHERE (\"visit_group_1\".\"count\" >= 2)\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Group creates a nested subquery when this is necessary to avoid duplicating the group key expression.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Group(:age => 2000 .- Get.year_of_birth)\n\nprint(render(q))\n#=>\nSELECT DISTINCT (2000 - \"person_1\".\"year_of_birth\") AS \"age\"\nFROM \"person\" AS \"person_1\"\n=#\n\nq = From(person) |>\n Group(:age => 2000 .- Get.year_of_birth) |>\n Select(Agg.count())\n\nprint(render(q))\n#=>\nSELECT count(*) AS \"count\"\nFROM \"person\" AS \"person_1\"\nGROUP BY (2000 - \"person_1\".\"year_of_birth\")\n=#\n\nq = From(person) |>\n Group(:age => 2000 .- Get.year_of_birth) |>\n Define(Agg.count())\n\nprint(render(q))\n#=>\nSELECT\n \"person_2\".\"age\",\n count(*) AS \"count\"\nFROM (\n SELECT (2000 - \"person_1\".\"year_of_birth\") AS \"age\"\n FROM \"person\" AS \"person_1\"\n) AS \"person_2\"\nGROUP BY \"person_2\".\"age\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Group could be used consequently.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(measurement) |>\n Group(Get.measurement_concept_id) |>\n Group(Agg.count()) |>\n Select(Get.count, :size => Agg.count())\n\nprint(render(q))\n#=>\nSELECT\n \"measurement_2\".\"count\",\n count(*) AS \"size\"\nFROM (\n SELECT count(*) AS \"count\"\n FROM \"measurement\" AS \"measurement_1\"\n GROUP BY \"measurement_1\".\"measurement_concept_id\"\n) AS \"measurement_2\"\nGROUP BY \"measurement_2\".\"count\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Group accepts an empty list of keys.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Group() |>\n Select(Agg.count(), Agg.min(Get.year_of_birth), Agg.max(Get.year_of_birth))\n\nprint(render(q))\n#=>\nSELECT\n count(*) AS \"count\",\n min(\"person_1\".\"year_of_birth\") AS \"min\",\n max(\"person_1\".\"year_of_birth\") AS \"max\"\nFROM \"person\" AS \"person_1\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Group with no keys and no aggregates creates a trivial subquery.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Group()\n\nprint(render(q))\n#-> SELECT NULL AS \"_\"","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"A SELECT DISTINCT query must include all the keys even when they are not used downstream.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Group(Get.year_of_birth) |>\n Group() |>\n Select(Agg.count())\n\nprint(render(q))\n#=>\nSELECT count(*) AS \"count\"\nFROM (\n SELECT DISTINCT \"person_1\".\"year_of_birth\"\n FROM \"person\" AS \"person_1\"\n) AS \"person_2\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Group allows specifying the grouping sets, either with grouping mode indicators :cube or :rollup, or by explicit enumeration.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Group(Get.year_of_birth, sets = :cube)\n Define(Agg.count())\n\ndisplay(q)\n#=>\nlet person = SQLTable(:person, …),\n q1 = From(person),\n q2 = q1 |> Group(Get.year_of_birth, sets = :CUBE)\n q2\nend\n=#\n\nprint(render(q))\n#=>\nSELECT \"person_1\".\"year_of_birth\"\nFROM \"person\" AS \"person_1\"\nGROUP BY CUBE(\"person_1\".\"year_of_birth\")\n=#\n\nq = From(person) |>\n Group(Get.year_of_birth, sets = [[1], Int[]])\n Define(Agg.count())\n\ndisplay(q)\n#=>\nlet person = SQLTable(:person, …),\n q1 = From(person),\n q2 = q1 |> Group(Get.year_of_birth, sets = [[1], []])\n q2\nend\n=#\n\nprint(render(q))\n#=>\nSELECT \"person_1\".\"year_of_birth\"\nFROM \"person\" AS \"person_1\"\nGROUP BY GROUPING SETS((\"person_1\".\"year_of_birth\"), ())\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Group allows specifying grouping sets using names of the grouping keys.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Group(Get.year_of_birth, Get.gender_concept_id,\n sets = ([:year_of_birth], [\"gender_concept_id\"]))\n Define(Agg.count())\n\ndisplay(q)\n#=>\nlet person = SQLTable(:person, …),\n q1 = From(person),\n q2 = q1 |>\n Group(Get.year_of_birth, Get.gender_concept_id, sets = [[1], [2]])\n q2\nend\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Group will report when a grouping set refers to an unknown key.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"From(person) |>\nGroup(Get.year_of_birth, sets = [[:gender_concept_id], []])\n#=>\nERROR: FunSQL.InvalidGroupingSetsError: `gender_concept_id` is not a valid key\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Group complains about out-of-bound or incomplete grouping sets.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"From(person) |>\nGroup(Get.year_of_birth, sets = [[1, 2], [1], []])\n#=>\nERROR: FunSQL.InvalidGroupingSetsError: `2` is out of bounds in:\nlet q1 = Group(Get.year_of_birth, sets = [[1, 2], [1], []])\n q1\nend\n=#\n\nFrom(person) |>\nGroup(Get.year_of_birth, Get.gender_concept_id,\n sets = [[1], []])\n#=>\nERROR: FunSQL.InvalidGroupingSetsError: missing keys `[:year_of_birth]` in:\nlet q1 = Group(Get.year_of_birth, Get.gender_concept_id, sets = [[1], []])\n q1\nend\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Group allows specifying the name of a group field.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Group(Get.year_of_birth, name = :person) |>\n Define(Get.person |> Agg.count())\n\ndisplay(q)\n#=>\nlet person = SQLTable(:person, …),\n q1 = From(person),\n q2 = q1 |> Group(Get.year_of_birth, name = :person),\n q3 = q2 |> Define(Get.person |> Agg.count())\n q3\nend\n=#\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"year_of_birth\",\n count(*) AS \"count\"\nFROM \"person\" AS \"person_1\"\nGROUP BY \"person_1\".\"year_of_birth\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Group requires all keys to have unique aliases.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Group(Get.person_id, Get.person_id)\n#=>\nERROR: FunSQL.DuplicateLabelError: `person_id` is used more than once in:\nGroup(Get.person_id, Get.person_id)\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"The name of group field must also be unique.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Group(:group => Get.year_of_birth, name = :group)\n#=>\nERROR: FunSQL.DuplicateLabelError: `group` is used more than once in:\nGroup(Get.year_of_birth |> As(:group), name = :group)\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Group ensures that each aggregate expression gets a unique alias.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Join(:visit_group => From(visit_occurrence) |>\n Group(Get.person_id),\n on = Get.person_id .== Get.visit_group.person_id) |>\n Select(Get.person_id,\n :max_visit_start_date =>\n Get.visit_group |> Agg.max(Get.visit_start_date),\n :max_visit_end_date =>\n Get.visit_group |> Agg.max(Get.visit_end_date))\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"person_id\",\n \"visit_group_1\".\"max\" AS \"max_visit_start_date\",\n \"visit_group_1\".\"max_2\" AS \"max_visit_end_date\"\nFROM \"person\" AS \"person_1\"\nJOIN (\n SELECT\n max(\"visit_occurrence_1\".\"visit_start_date\") AS \"max\",\n max(\"visit_occurrence_1\".\"visit_end_date\") AS \"max_2\",\n \"visit_occurrence_1\".\"person_id\"\n FROM \"visit_occurrence\" AS \"visit_occurrence_1\"\n GROUP BY \"visit_occurrence_1\".\"person_id\"\n) AS \"visit_group_1\" ON (\"person_1\".\"person_id\" = \"visit_group_1\".\"person_id\")\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Aggregate expressions can be applied to a filtered portion of a partition.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"e = Agg.count(filter = Get.year_of_birth .> 1950)\n#-> Agg.count(filter = (…))\n\ndisplay(e)\n#-> Agg.count(filter = Fun.\">\"(Get.year_of_birth, 1950))\n\nq = From(person) |> Group() |> Select(e)\n\nprint(render(q))\n#=>\nSELECT (count(*) FILTER (WHERE (\"person_1\".\"year_of_birth\" > 1950))) AS \"count\"\nFROM \"person\" AS \"person_1\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"It is an error for an aggregate expression to be used without Group.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |> Select(Agg.count())\n\nprint(render(q))\n#=>\nERROR: FunSQL.ReferenceError: aggregate expression requires Group or Partition in:\nlet person = SQLTable(:person, …),\n q1 = From(person),\n q2 = q1 |> Select(Agg.count())\n q2\nend\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Group in a Join expression shadows any previous applications of Group.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"qₚ = From(person)\nqᵥ = From(visit_occurrence) |> Group(:visit_person_id => Get.person_id)\nqₘ = From(measurement) |> Group(:measurement_person_id => Get.person_id)\n\nq = qₚ |>\n Join(qᵥ, on = Get.person_id .== Get.visit_person_id, left = true) |>\n Join(qₘ, on = Get.person_id .== Get.measurement_person_id, left = true) |>\n Select(Get.person_id, :count => Fun.coalesce(Agg.count(), 0))\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"person_id\",\n coalesce(\"measurement_2\".\"count\", 0) AS \"count\"\nFROM \"person\" AS \"person_1\"\nLEFT JOIN (\n SELECT DISTINCT \"visit_occurrence_1\".\"person_id\" AS \"visit_person_id\"\n FROM \"visit_occurrence\" AS \"visit_occurrence_1\"\n) AS \"visit_occurrence_2\" ON (\"person_1\".\"person_id\" = \"visit_occurrence_2\".\"visit_person_id\")\nLEFT JOIN (\n SELECT\n count(*) AS \"count\",\n \"measurement_1\".\"person_id\" AS \"measurement_person_id\"\n FROM \"measurement\" AS \"measurement_1\"\n GROUP BY \"measurement_1\".\"person_id\"\n) AS \"measurement_2\" ON (\"person_1\".\"person_id\" = \"measurement_2\".\"measurement_person_id\")\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"It is still possible to use an aggregate in the context of a Join when the corresponding Group could be determined unambiguously.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"qₚ = From(person)\nqᵥ = From(visit_occurrence) |> Group(:visit_person_id => Get.person_id)\n\nq = qₚ |>\n Join(qᵥ, on = Get.person_id .== Get.visit_person_id, left = true) |>\n Select(Get.person_id, :count => Fun.coalesce(Agg.count(), 0))\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"person_id\",\n coalesce(\"visit_occurrence_2\".\"count\", 0) AS \"count\"\nFROM \"person\" AS \"person_1\"\nLEFT JOIN (\n SELECT\n count(*) AS \"count\",\n \"visit_occurrence_1\".\"person_id\" AS \"visit_person_id\"\n FROM \"visit_occurrence\" AS \"visit_occurrence_1\"\n GROUP BY \"visit_occurrence_1\".\"person_id\"\n) AS \"visit_occurrence_2\" ON (\"person_1\".\"person_id\" = \"visit_occurrence_2\".\"visit_person_id\")\n=#","category":"page"},{"location":"test/nodes/#Partition","page":"SQL Nodes","title":"Partition","text":"","category":"section"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"The Partition constructor creates a subquery that partitions the rows by the given keys.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Partition(Get.year_of_birth, order_by = [Get.month_of_birth, Get.day_of_birth])\n#-> (…) |> Partition(…, order_by = […])\n\ndisplay(q)\n#=>\nlet person = SQLTable(:person, …),\n q1 = From(person),\n q2 = q1 |>\n Partition(Get.year_of_birth,\n order_by = [Get.month_of_birth, Get.day_of_birth])\n q2\nend\n=#\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"location_id\"\nFROM \"person\" AS \"person_1\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"A Partition node can be created with @funsql notation.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = @funsql begin\n from(person)\n partition(year_of_birth, order_by = [month_of_birth, day_of_birth])\nend\n\ndisplay(q)\n#=>\nlet q1 = From(:person),\n q2 = q1 |>\n Partition(Get.year_of_birth,\n order_by = [Get.month_of_birth, Get.day_of_birth])\n q2\nend\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Calculations across the rows of the partitions are performed by window functions.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Partition(Get.gender_concept_id) |>\n Select(Get.person_id, Agg.row_number())\n\ndisplay(q)\n#=>\nlet person = SQLTable(:person, …),\n q1 = From(person),\n q2 = q1 |> Partition(Get.gender_concept_id),\n q3 = q2 |> Select(Get.person_id, Agg.row_number())\n q3\nend\n=#\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"person_id\",\n (row_number() OVER (PARTITION BY \"person_1\".\"gender_concept_id\")) AS \"row_number\"\nFROM \"person\" AS \"person_1\"\n=#\n\nq = From(visit_occurrence) |>\n Partition(Get.person_id) |>\n Where(Get.visit_start_date .- Agg.min(Get.visit_start_date, filter = Get.visit_start_date .< Get.visit_end_date) .> 30) |>\n Select(Get.person_id, Get.visit_start_date)\n\nprint(render(q))\n#=>\nSELECT\n \"visit_occurrence_2\".\"person_id\",\n \"visit_occurrence_2\".\"visit_start_date\"\nFROM (\n SELECT\n \"visit_occurrence_1\".\"person_id\",\n \"visit_occurrence_1\".\"visit_start_date\",\n (min(\"visit_occurrence_1\".\"visit_start_date\") FILTER (WHERE (\"visit_occurrence_1\".\"visit_start_date\" < \"visit_occurrence_1\".\"visit_end_date\")) OVER (PARTITION BY \"visit_occurrence_1\".\"person_id\")) AS \"min\"\n FROM \"visit_occurrence\" AS \"visit_occurrence_1\"\n) AS \"visit_occurrence_2\"\nWHERE ((\"visit_occurrence_2\".\"visit_start_date\" - \"visit_occurrence_2\".\"min\") > 30)\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"A partition may specify the window frame.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Group(Get.year_of_birth) |>\n Partition(order_by = [Get.year_of_birth],\n frame = (mode = :range, start = -1, finish = 1)) |>\n Select(Get.year_of_birth, Agg.avg(Agg.count()))\n\ndisplay(q)\n#=>\nlet person = SQLTable(:person, …),\n q1 = From(person),\n q2 = q1 |> Group(Get.year_of_birth),\n q3 = q2 |>\n Partition(order_by = [Get.year_of_birth],\n frame = (mode = :RANGE, start = -1, finish = 1)),\n q4 = q3 |> Select(Get.year_of_birth, Agg.avg(Agg.count()))\n q4\nend\n=#\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"year_of_birth\",\n (avg(count(*)) OVER (ORDER BY \"person_1\".\"year_of_birth\" RANGE BETWEEN 1 PRECEDING AND 1 FOLLOWING)) AS \"avg\"\nFROM \"person\" AS \"person_1\"\nGROUP BY \"person_1\".\"year_of_birth\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"A window frame can be specified in @funsql notation.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = @funsql partition(order_by = [year_of_birth], frame = groups)\n\ndisplay(q)\n#-> Partition(order_by = [Get.year_of_birth], frame = :GROUPS)\n\nq = @funsql partition(order_by = [year_of_birth], frame = (mode = range, start = -1, finish = 1))\n\ndisplay(q)\n#=>\nPartition(order_by = [Get.year_of_birth],\n frame = (mode = :RANGE, start = -1, finish = 1))\n=#\n\nq = @funsql partition(; order_by = [year_of_birth], frame = (mode = range, start = -Inf, finish = Inf, exclude = current_row))\n\ndisplay(q)\n#=>\nPartition(\n order_by = [Get.year_of_birth],\n frame =\n (mode = :RANGE, start = -Inf, finish = Inf, exclude = :CURRENT_ROW))\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Partition may assign an explicit name to the partition.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Group(Get.gender_concept_id) |>\n Partition(name = :all) |>\n Define(:pct => 100 .* Agg.count() ./ (Get.all |> Agg.sum(Agg.count())))\n\ndisplay(q)\n#=>\nlet person = SQLTable(:person, …),\n q1 = From(person),\n q2 = q1 |> Group(Get.gender_concept_id),\n q3 = q2 |> Partition(name = :all),\n q4 = q3 |>\n Define(Fun.\"/\"(Fun.\"*\"(100, Agg.count()),\n Get.all |> Agg.sum(Agg.count())) |>\n As(:pct))\n q4\nend\n=#\n\nprint(render(q))\n#=>\nSELECT\n \"person_2\".\"gender_concept_id\",\n ((100 * \"person_2\".\"count\") / (sum(\"person_2\".\"count\") OVER ())) AS \"pct\"\nFROM (\n SELECT\n \"person_1\".\"gender_concept_id\",\n count(*) AS \"count\"\n FROM \"person\" AS \"person_1\"\n GROUP BY \"person_1\".\"gender_concept_id\"\n) AS \"person_2\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"This name may shadow an existing column.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(location) |>\n Partition(Get.location_id, name = :location_id)\n\nprint(render(q))\n#=>\nSELECT\n \"location_1\".\"city\",\n \"location_1\".\"state\"\nFROM \"location\" AS \"location_1\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"It is common to use several Partition nodes in a row like in the following example which calculates non-overlapping visits.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(visit_occurrence) |>\n Partition(Get.person_id,\n order_by = [Get.visit_start_date],\n frame = (mode = :rows, start = -Inf, finish = -1)) |>\n Define(:boundary => Agg.max(Get.visit_end_date)) |>\n Define(:gap => Get.visit_start_date .- Get.boundary) |>\n Define(:new => Fun.case(Get.gap .<= 0, 0, 1)) |>\n Partition(Get.person_id,\n order_by = [Get.visit_start_date, .- Get.new],\n frame = :rows) |>\n Define(:group => Agg.sum(Get.new)) |>\n Group(Get.person_id, Get.group) |>\n Define(:start_date => Agg.min(Get.visit_start_date),\n :end_date => Agg.max(Get.visit_end_date)) |>\n Select(Get.person_id, Get.start_date, Get.end_date)\n\nprint(render(q))\n#=>\nSELECT\n \"visit_occurrence_3\".\"person_id\",\n min(\"visit_occurrence_3\".\"visit_start_date\") AS \"start_date\",\n max(\"visit_occurrence_3\".\"visit_end_date\") AS \"end_date\"\nFROM (\n SELECT\n \"visit_occurrence_2\".\"person_id\",\n (sum(\"visit_occurrence_2\".\"new\") OVER (PARTITION BY \"visit_occurrence_2\".\"person_id\" ORDER BY \"visit_occurrence_2\".\"visit_start_date\", (- \"visit_occurrence_2\".\"new\") ROWS UNBOUNDED PRECEDING)) AS \"group\",\n \"visit_occurrence_2\".\"visit_start_date\",\n \"visit_occurrence_2\".\"visit_end_date\"\n FROM (\n SELECT\n \"visit_occurrence_1\".\"person_id\",\n \"visit_occurrence_1\".\"visit_start_date\",\n \"visit_occurrence_1\".\"visit_end_date\",\n (CASE WHEN ((\"visit_occurrence_1\".\"visit_start_date\" - (max(\"visit_occurrence_1\".\"visit_end_date\") OVER (PARTITION BY \"visit_occurrence_1\".\"person_id\" ORDER BY \"visit_occurrence_1\".\"visit_start_date\" ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING))) <= 0) THEN 0 ELSE 1 END) AS \"new\"\n FROM \"visit_occurrence\" AS \"visit_occurrence_1\"\n ) AS \"visit_occurrence_2\"\n) AS \"visit_occurrence_3\"\nGROUP BY\n \"visit_occurrence_3\".\"person_id\",\n \"visit_occurrence_3\".\"group\"\n=#","category":"page"},{"location":"test/nodes/#Join","page":"SQL Nodes","title":"Join","text":"","category":"section"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"The Join constructor creates a subquery that correlates two nested subqueries.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Join(:location => From(location),\n on = Get.location_id .== Get.location.location_id,\n left = true)\n#-> (…) |> Join(…)\n\ndisplay(q)\n#=>\nlet person = SQLTable(:person, …),\n location = SQLTable(:location, …),\n q1 = From(person),\n q2 = From(location),\n q3 = q1 |>\n Join(q2 |> As(:location),\n Fun.\"=\"(Get.location_id, Get.location.location_id),\n left = true)\n q3\nend\n=#\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"location_id\"\nFROM \"person\" AS \"person_1\"\nLEFT JOIN \"location\" AS \"location_1\" ON (\"person_1\".\"location_id\" = \"location_1\".\"location_id\")\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"LEFT JOIN is commonly used and has its own constructor.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n LeftJoin(:location => From(location),\n on = Get.location_id .== Get.location.location_id)\n\ndisplay(q)\n#=>\nlet person = SQLTable(:person, …),\n location = SQLTable(:location, …),\n q1 = From(person),\n q2 = From(location),\n q3 = q1 |>\n Join(q2 |> As(:location),\n Fun.\"=\"(Get.location_id, Get.location.location_id),\n left = true)\n q3\nend\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Various Join nodes can be created with @funsql notation.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = @funsql begin\n from(person)\n join(location => from(location),\n on = location_id == location.location_id,\n left = true)\nend\n\ndisplay(q)\n#=>\nlet q1 = From(:person),\n q2 = From(:location),\n q3 = q1 |>\n Join(q2 |> As(:location),\n Fun.\"=\"(Get.location_id, Get.location.location_id),\n left = true)\n q3\nend\n=#\n\nq = @funsql begin\n from(person)\n left_join(location => from(location),\n location_id == location.location_id)\nend\n\ndisplay(q)\n#=>\nlet q1 = From(:person),\n q2 = From(:location),\n q3 = q1 |>\n Join(q2 |> As(:location),\n Fun.\"=\"(Get.location_id, Get.location.location_id),\n left = true)\n q3\nend\n=#\n\nq = @funsql begin\n from(person)\n cross_join(other => from(person))\nend\n\ndisplay(q)\n#=>\nlet q1 = From(:person),\n q2 = From(:person),\n q3 = q1 |> Join(q2 |> As(:other), true)\n q3\nend\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Nested subqueries that are combined with Join may fail to collapse.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Where(Get.year_of_birth .> 1970) |>\n Join(:location => From(location) |>\n Where(Get.state .== \"IL\"),\n on = (Get.location_id .== Get.location.location_id)) |>\n Select(Get.person_id, Get.location.city)\n\nprint(render(q))\n#=>\nSELECT\n \"person_2\".\"person_id\",\n \"location_2\".\"city\"\nFROM (\n SELECT\n \"person_1\".\"person_id\",\n \"person_1\".\"location_id\"\n FROM \"person\" AS \"person_1\"\n WHERE (\"person_1\".\"year_of_birth\" > 1970)\n) AS \"person_2\"\nJOIN (\n SELECT\n \"location_1\".\"city\",\n \"location_1\".\"location_id\"\n FROM \"location\" AS \"location_1\"\n WHERE (\"location_1\".\"state\" = 'IL')\n) AS \"location_2\" ON (\"person_2\".\"location_id\" = \"location_2\".\"location_id\")\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Join can be applied to correlated subqueries.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"ql(person_id) =\n From(visit_occurrence) |>\n Where(Get.person_id .== Var.PERSON_ID) |>\n Partition(order_by = [Get.visit_start_date]) |>\n Where(Agg.row_number() .== 1) |>\n Bind(:PERSON_ID => person_id)\n\nprint(render(ql(1)))\n#=>\nSELECT\n \"visit_occurrence_2\".\"visit_occurrence_id\",\n \"visit_occurrence_2\".\"person_id\",\n \"visit_occurrence_2\".\"visit_start_date\",\n \"visit_occurrence_2\".\"visit_end_date\"\nFROM (\n SELECT\n \"visit_occurrence_1\".\"visit_occurrence_id\",\n \"visit_occurrence_1\".\"person_id\",\n \"visit_occurrence_1\".\"visit_start_date\",\n \"visit_occurrence_1\".\"visit_end_date\",\n (row_number() OVER (ORDER BY \"visit_occurrence_1\".\"visit_start_date\")) AS \"row_number\"\n FROM \"visit_occurrence\" AS \"visit_occurrence_1\"\n WHERE (\"visit_occurrence_1\".\"person_id\" = 1)\n) AS \"visit_occurrence_2\"\nWHERE (\"visit_occurrence_2\".\"row_number\" = 1)\n=#\n\nq = From(person) |>\n Join(:visit => ql(Get.person_id), on = true) |>\n Select(Get.person_id,\n Get.visit.visit_occurrence_id,\n Get.visit.visit_start_date)\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"person_id\",\n \"visit_1\".\"visit_occurrence_id\",\n \"visit_1\".\"visit_start_date\"\nFROM \"person\" AS \"person_1\"\nCROSS JOIN LATERAL (\n SELECT\n \"visit_occurrence_2\".\"visit_occurrence_id\",\n \"visit_occurrence_2\".\"visit_start_date\"\n FROM (\n SELECT\n \"visit_occurrence_1\".\"visit_occurrence_id\",\n \"visit_occurrence_1\".\"visit_start_date\",\n (row_number() OVER (ORDER BY \"visit_occurrence_1\".\"visit_start_date\")) AS \"row_number\"\n FROM \"visit_occurrence\" AS \"visit_occurrence_1\"\n WHERE (\"visit_occurrence_1\".\"person_id\" = \"person_1\".\"person_id\")\n ) AS \"visit_occurrence_2\"\n WHERE (\"visit_occurrence_2\".\"row_number\" = 1)\n) AS \"visit_1\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"The LATERAL keyword is omitted when the join branch is reduced to a function call.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(concept) |>\nJoin(\n From(Fun.string_to_table(Get.concept_name, \" \"), columns = [:word]),\n on = true) |>\nGroup(Get.word)\n\nprint(render(q))\n#=>\nSELECT DISTINCT \"string_to_table_1\".\"word\"\nFROM \"concept\" AS \"concept_1\"\nCROSS JOIN string_to_table(\"concept_1\".\"concept_name\", ' ') AS \"string_to_table_1\" (\"word\")\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Some database backends require LATERAL even in this case.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"print(render(q, dialect = :spark))\n#=>\nSELECT DISTINCT `string_to_table_1`.`word`\nFROM `concept` AS `concept_1`\nCROSS JOIN LATERAL string_to_table(`concept_1`.`concept_name`, ' ') AS `string_to_table_1` (`word`)\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"An optional Join is omitted when the output contains no data from its right branch.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n LeftJoin(:location => From(location),\n on = Get.location_id .== Get.location.location_id,\n optional = true)\n\ndisplay(q)\n#=>\nlet person = SQLTable(:person, …),\n location = SQLTable(:location, …),\n q1 = From(person),\n q2 = From(location),\n q3 = q1 |>\n Join(q2 |> As(:location),\n Fun.\"=\"(Get.location_id, Get.location.location_id),\n left = true,\n optional = true)\n q3\nend\n=#\n\nprint(render(q |> Select(Get.year_of_birth)))\n#=>\nSELECT \"person_1\".\"year_of_birth\"\nFROM \"person\" AS \"person_1\"\n=#\n\nprint(render(q |> Select(Get.year_of_birth, Get.location.state)))\n#=>\nSELECT\n \"person_1\".\"year_of_birth\",\n \"location_1\".\"state\"\nFROM \"person\" AS \"person_1\"\nLEFT JOIN \"location\" AS \"location_1\" ON (\"person_1\".\"location_id\" = \"location_1\".\"location_id\")\n=#","category":"page"},{"location":"test/nodes/#Order","page":"SQL Nodes","title":"Order","text":"","category":"section"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"The Order constructor creates a subquery for sorting the data.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Order(Get.year_of_birth)\n#-> (…) |> Order(…)\n\ndisplay(q)\n#=>\nlet person = SQLTable(:person, …),\n q1 = From(person),\n q2 = q1 |> Order(Get.year_of_birth)\n q2\nend\n=#\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"location_id\"\nFROM \"person\" AS \"person_1\"\nORDER BY \"person_1\".\"year_of_birth\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"An Order node can be created with @funsql notation.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = @funsql begin\n from(person)\n order(year_of_birth)\nend\n\ndisplay(q)\n#=>\nlet q1 = From(:person),\n q2 = q1 |> Order(Get.year_of_birth)\n q2\nend\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Order is often used together with Limit.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Order(Get.year_of_birth) |>\n Limit(10) |>\n Order(Get.person_id)\n\nprint(render(q))\n#=>\nSELECT\n \"person_2\".\"person_id\",\n ⋮\n \"person_2\".\"location_id\"\nFROM (\n SELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"location_id\"\n FROM \"person\" AS \"person_1\"\n ORDER BY \"person_1\".\"year_of_birth\"\n FETCH FIRST 10 ROWS ONLY\n) AS \"person_2\"\nORDER BY \"person_2\".\"person_id\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"An Order without columns to sort by is a no-op.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Order(by = [])\n#-> (…) |> Order(by = [])\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"location_id\"\nFROM \"person\" AS \"person_1\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"It is possible to specify ascending or descending order of the sort column.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Order(Get.year_of_birth |> Desc(nulls = :first),\n Get.person_id |> Asc())\n\ndisplay(q)\n#=>\nlet person = SQLTable(:person, …),\n q1 = From(person),\n q2 = q1 |>\n Order(Get.year_of_birth |> Desc(nulls = :NULLS_FIRST),\n Get.person_id |> Asc())\n q2\nend\n=#\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"location_id\"\nFROM \"person\" AS \"person_1\"\nORDER BY\n \"person_1\".\"year_of_birth\" DESC NULLS FIRST,\n \"person_1\".\"person_id\" ASC\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"A generic Sort constructor could also be used for this purpose.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Order(Get.year_of_birth |> Sort(:desc, nulls = :first),\n Get.person_id |> Sort(:asc))\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"location_id\"\nFROM \"person\" AS \"person_1\"\nORDER BY\n \"person_1\".\"year_of_birth\" DESC NULLS FIRST,\n \"person_1\".\"person_id\" ASC\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Sort decorations can be created with @funsql.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = @funsql begin\n from(person)\n order(year_of_birth.desc(nulls = first), person_id.asc())\nend\n\ndisplay(q)\n#=>\nlet q1 = From(:person),\n q2 = q1 |>\n Order(Get.year_of_birth |> Desc(nulls = :NULLS_FIRST),\n Get.person_id |> Asc())\n q2\nend\n=#\n\nq = @funsql begin\n from(person)\n order(year_of_birth.sort(desc, nulls = first), person_id.sort(asc))\nend\n\ndisplay(q)\n#=>\nlet q1 = From(:person),\n q2 = q1 |>\n Order(Get.year_of_birth |> Desc(nulls = :NULLS_FIRST),\n Get.person_id |> Asc())\n q2\nend\n=#","category":"page"},{"location":"test/nodes/#Limit","page":"SQL Nodes","title":"Limit","text":"","category":"section"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"The Limit constructor creates a subquery that takes a fixed-size slice of the dataset.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Order(Get.person_id) |>\n Limit(10)\n#-> (…) |> Limit(10)\n\ndisplay(q)\n#=>\nlet person = SQLTable(:person, …),\n q1 = From(person),\n q2 = q1 |> Order(Get.person_id),\n q3 = q2 |> Limit(10)\n q3\nend\n=#\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"location_id\"\nFROM \"person\" AS \"person_1\"\nORDER BY \"person_1\".\"person_id\"\nFETCH FIRST 10 ROWS ONLY\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Both the offset and the limit can be specified.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Order(Get.person_id) |>\n Limit(100, 10)\n\ndisplay(q)\n#=>\nlet person = SQLTable(:person, …),\n q1 = From(person),\n q2 = q1 |> Order(Get.person_id),\n q3 = q2 |> Limit(100, 10)\n q3\nend\n=#\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"location_id\"\nFROM \"person\" AS \"person_1\"\nORDER BY \"person_1\".\"person_id\"\nOFFSET 100 ROWS\nFETCH NEXT 10 ROWS ONLY\n=#\n\nq = From(person) |>\n Order(Get.person_id) |>\n Limit(101:110)\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"location_id\"\nFROM \"person\" AS \"person_1\"\nORDER BY \"person_1\".\"person_id\"\nOFFSET 100 ROWS\nFETCH NEXT 10 ROWS ONLY\n=#\n\nq = From(person) |>\n Limit(offset = 100) |>\n Limit(limit = 10)\n\nprint(render(q))\n#=>\nSELECT\n \"person_2\".\"person_id\",\n ⋮\n \"person_2\".\"location_id\"\nFROM (\n SELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"location_id\"\n FROM \"person\" AS \"person_1\"\n OFFSET 100 ROWS\n) AS \"person_2\"\nFETCH FIRST 10 ROWS ONLY\n=#\n\nq = From(person) |>\n Limit()\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"location_id\"\nFROM \"person\" AS \"person_1\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"A Limit node can be created with @funsql notation.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = @funsql from(person).order(person_id).limit(10)\n\ndisplay(q)\n#=>\nlet q1 = From(:person),\n q2 = q1 |> Order(Get.person_id),\n q3 = q2 |> Limit(10)\n q3\nend\n=#\n\nq = @funsql from(person).order(person_id).limit(100, 10)\n\ndisplay(q)\n#=>\nlet q1 = From(:person),\n q2 = q1 |> Order(Get.person_id),\n q3 = q2 |> Limit(100, 10)\n q3\nend\n=#\n\nq = @funsql from(person).order(person_id).limit(101:110)\n\ndisplay(q)\n#=>\nlet q1 = From(:person),\n q2 = q1 |> Order(Get.person_id),\n q3 = q2 |> Limit(100, 10)\n q3\nend\n=#","category":"page"},{"location":"test/nodes/#Select","page":"SQL Nodes","title":"Select","text":"","category":"section"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"The Select constructor creates a subquery that fixes the output columns.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Select(Get.person_id)\n#-> (…) |> Select(…)\n\ndisplay(q)\n#=>\nlet person = SQLTable(:person, …),\n q1 = From(person),\n q2 = q1 |> Select(Get.person_id)\n q2\nend\n=#\n\nprint(render(q))\n#=>\nSELECT \"person_1\".\"person_id\"\nFROM \"person\" AS \"person_1\"\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"A Select node can be created with @funsql notation.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = @funsql from(person).select(person_id)\n\ndisplay(q)\n#=>\nlet q1 = From(:person),\n q2 = q1 |> Select(Get.person_id)\n q2\nend\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Select does not have to be the last subquery in a chain, but it always creates a complete subquery.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Select(Get.year_of_birth) |>\n Where(Fun.\">\"(Get.year_of_birth, 2000))\n\nprint(render(q))\n#=>\nSELECT \"person_2\".\"year_of_birth\"\nFROM (\n SELECT \"person_1\".\"year_of_birth\"\n FROM \"person\" AS \"person_1\"\n) AS \"person_2\"\nWHERE (\"person_2\".\"year_of_birth\" > 2000)\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Select requires all columns in the list to have unique aliases.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Select(Get.person_id, Get.person_id)\n#=>\nERROR: FunSQL.DuplicateLabelError: `person_id` is used more than once in:\nSelect(Get.person_id, Get.person_id)\n=#","category":"page"},{"location":"test/nodes/#Where","page":"SQL Nodes","title":"Where","text":"","category":"section"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"The Where constructor creates a subquery that filters by the given condition.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Where(Fun.\">\"(Get.year_of_birth, 2000))\n#-> (…) |> Where(…)\n\ndisplay(q)\n#=>\nlet person = SQLTable(:person, …),\n q1 = From(person),\n q2 = q1 |> Where(Fun.\">\"(Get.year_of_birth, 2000))\n q2\nend\n=#\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"location_id\"\nFROM \"person\" AS \"person_1\"\nWHERE (\"person_1\".\"year_of_birth\" > 2000)\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"A Where node can be created with @funsql notation.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = @funsql from(person).filter(year_of_birth > 2000)\n\ndisplay(q)\n#=>\nlet q1 = From(:person),\n q2 = q1 |> Where(Fun.\">\"(Get.year_of_birth, 2000))\n q2\nend\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Several Where operations in a row are collapsed to a single WHERE clause.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Where(Fun.\">\"(Get.year_of_birth, 2000)) |>\n Where(Fun.\"<\"(Get.year_of_birth, 2020)) |>\n Where(Fun.\"<>\"(Get.year_of_birth, 2010))\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"location_id\"\nFROM \"person\" AS \"person_1\"\nWHERE\n (\"person_1\".\"year_of_birth\" > 2000) AND\n (\"person_1\".\"year_of_birth\" < 2020) AND\n (\"person_1\".\"year_of_birth\" <> 2010)\n=#\n\nq = From(person) |>\n Where(Get.year_of_birth .!= 2010) |>\n Where(Fun.and(Get.year_of_birth .> 2000, Get.year_of_birth .< 2020))\n\nprint(render(q))\n#=>\nSELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"location_id\"\nFROM \"person\" AS \"person_1\"\nWHERE\n (\"person_1\".\"year_of_birth\" <> 2010) AND\n (\"person_1\".\"year_of_birth\" > 2000) AND\n (\"person_1\".\"year_of_birth\" < 2020)\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Where that follows Group subquery is transformed to a HAVING clause.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Group(Get.year_of_birth) |>\n Where(Agg.count() .> 10)\n\nprint(render(q))\n#=>\nSELECT \"person_1\".\"year_of_birth\"\nFROM \"person\" AS \"person_1\"\nGROUP BY \"person_1\".\"year_of_birth\"\nHAVING (count(*) > 10)\n=#\n\nq = From(person) |>\n Group(Get.gender_concept_id) |>\n Where(Agg.count(filter = Get.year_of_birth .== 2010) .> 10) |>\n Where(Agg.count(filter = Get.year_of_birth .== 2000) .< 100) |>\n Where(Fun.and(Agg.count(filter = Get.year_of_birth .== 1933) .!= 33,\n Agg.count(filter = Get.year_of_birth .== 1966) .!= 66))\n\nprint(render(q))\n#=>\nSELECT \"person_1\".\"gender_concept_id\"\nFROM \"person\" AS \"person_1\"\nGROUP BY \"person_1\".\"gender_concept_id\"\nHAVING\n ((count(*) FILTER (WHERE (\"person_1\".\"year_of_birth\" = 2010))) > 10) AND\n ((count(*) FILTER (WHERE (\"person_1\".\"year_of_birth\" = 2000))) < 100) AND\n ((count(*) FILTER (WHERE (\"person_1\".\"year_of_birth\" = 1933))) <> 33) AND\n ((count(*) FILTER (WHERE (\"person_1\".\"year_of_birth\" = 1966))) <> 66)\n=#\n\nq = From(person) |>\n Group(Get.gender_concept_id) |>\n Where(Fun.or(Agg.count(filter = Get.year_of_birth .== 2010) .> 10,\n Agg.count(filter = Get.year_of_birth .== 2000) .< 100))\n\nprint(render(q))\n#=>\nSELECT \"person_1\".\"gender_concept_id\"\nFROM \"person\" AS \"person_1\"\nGROUP BY \"person_1\".\"gender_concept_id\"\nHAVING\n ((count(*) FILTER (WHERE (\"person_1\".\"year_of_birth\" = 2010))) > 10) OR\n ((count(*) FILTER (WHERE (\"person_1\".\"year_of_birth\" = 2000))) < 100)\n=#","category":"page"},{"location":"test/nodes/#Highlighting","page":"SQL Nodes","title":"Highlighting","text":"","category":"section"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"To highlight a node on the output, wrap it with Highlight.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Highlight(:underline) |>\n Where(Fun.\">\"(Get.year_of_birth |> Highlight(:bold), 2000) |>\n Highlight(:white)) |>\n Select(Get.person_id) |>\n Highlight(:green)\n#-> (…) |> Highlight(:green)","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"When the query is displayed on a color terminal, the affected node is highlighted.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"display(q)\n#=>\nlet person = SQLTable(:person, …),\n q1 = From(person),\n q2 = q1 |> Where(Fun.\">\"(Get.year_of_birth, 2000)),\n q3 = q2 |> Select(Get.person_id)\n q3\nend\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"The Highlight node does not otherwise affect processing of the query.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"print(render(q))\n#=>\nSELECT \"person_1\".\"person_id\"\nFROM \"person\" AS \"person_1\"\nWHERE (\"person_1\".\"year_of_birth\" > 2000)\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"A Highlight node can be created with @funsql notation.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = @funsql from(person).highlight(red)\n\ndisplay(q)\n#=>\nlet q1 = From(:person)\n q1\nend\n=#","category":"page"},{"location":"test/nodes/#Debugging","page":"SQL Nodes","title":"Debugging","text":"","category":"section"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Enable debug logging to get some insight on how FunSQL translates a query object into SQL. Set the JULIA_DEBUG environment variable to the name of a translation stage and render() will print the result of this stage.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Consider the following query.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"q = From(person) |>\n Where(Get.year_of_birth .<= 2000) |>\n Join(:location => From(location) |>\n Where(Get.state .== \"IL\"),\n on = (Get.location_id .== Get.location.location_id)) |>\n Join(:visit_group => From(visit_occurrence) |>\n Group(Get.person_id),\n on = (Get.person_id .== Get.visit_group.person_id),\n left = true) |>\n Select(Get.person_id,\n :max_visit_start_date =>\n Get.visit_group |> Agg.max(Get.visit_start_date))","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"At the first stage of the translation, render() resolves table references and determines node types.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"#? VERSION >= v\"1.7\" # https://github.com/JuliaLang/julia/issues/26798\nwithenv(\"JULIA_DEBUG\" => \"FunSQL.resolve\") do\n render(q)\nend;\n#=>\n┌ Debug: FunSQL.resolve\n│ let person = SQLTable(:person, …),\n│ location = SQLTable(:location, …),\n│ visit_occurrence = SQLTable(:visit_occurrence, …),\n│ q1 = FromTable(table = person),\n│ q2 = Resolved(RowType(:person_id => ScalarType(),\n│ :gender_concept_id => ScalarType(),\n│ :year_of_birth => ScalarType(),\n│ :month_of_birth => ScalarType(),\n│ :day_of_birth => ScalarType(),\n│ :birth_datetime => ScalarType(),\n│ :location_id => ScalarType()),\n│ over = q1) |>\n│ Where(Resolved(ScalarType(),\n│ over = Fun.\"<=\"(Resolved(ScalarType(),\n│ over = Get.year_of_birth),\n│ Resolved(ScalarType(), over = 2000)))),\n⋮\n│ WithContext(over = Resolved(RowType(:person_id => ScalarType(),\n│ :max_visit_start_date => ScalarType()),\n│ over = q9),\n│ catalog = SQLCatalog(dialect = SQLDialect(), cache = nothing))\n│ end\n└ @ FunSQL …\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Next, render() determines, for each tabular node, the data that it must produce.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"#? VERSION >= v\"1.7\"\nwithenv(\"JULIA_DEBUG\" => \"FunSQL.link\") do\n render(q)\nend;\n#=>\n┌ Debug: FunSQL.link\n│ let person = SQLTable(:person, …),\n│ location = SQLTable(:location, …),\n│ visit_occurrence = SQLTable(:visit_occurrence, …),\n│ q1 = FromTable(table = person),\n│ q2 = Get.person_id,\n│ q3 = Get.person_id,\n│ q4 = Get.location_id,\n│ q5 = Get.year_of_birth,\n│ q6 = Linked([q2, q3, q4, q5], 3, over = q1),\n⋮\n│ WithContext(over = q33,\n│ catalog = SQLCatalog(dialect = SQLDialect(), cache = nothing))\n│ end\n└ @ FunSQL …\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"On the next stage, the query object is converted to a SQL syntax tree.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"#? VERSION >= v\"1.7\"\nwithenv(\"JULIA_DEBUG\" => \"FunSQL.translate\") do\n render(q)\nend;\n#=>\n┌ Debug: FunSQL.translate\n│ WITH_CONTEXT(\n│ over = ID(:person) |>\n│ AS(:person_1) |>\n│ FROM() |>\n│ WHERE(FUN(\"<=\", ID(:person_1) |> ID(:year_of_birth), LIT(2000))) |>\n│ SELECT(ID(:person_1) |> ID(:person_id),\n│ ID(:person_1) |> ID(:location_id)) |>\n│ AS(:person_2) |>\n│ FROM() |>\n│ JOIN(ID(:location) |>\n│ AS(:location_1) |>\n│ FROM() |>\n│ WHERE(FUN(\"=\", ID(:location_1) |> ID(:state), LIT(\"IL\"))) |>\n│ SELECT(ID(:location_1) |> ID(:location_id)) |>\n│ AS(:location_2),\n│ FUN(\"=\",\n│ ID(:person_2) |> ID(:location_id),\n│ ID(:location_2) |> ID(:location_id))) |>\n│ JOIN(ID(:visit_occurrence) |>\n│ AS(:visit_occurrence_1) |>\n│ FROM() |>\n│ GROUP(ID(:visit_occurrence_1) |> ID(:person_id)) |>\n│ SELECT(AGG(\"max\",\n│ ID(:visit_occurrence_1) |> ID(:visit_start_date)) |>\n│ AS(:max),\n│ ID(:visit_occurrence_1) |> ID(:person_id)) |>\n│ AS(:visit_group_1),\n│ FUN(\"=\",\n│ ID(:person_2) |> ID(:person_id),\n│ ID(:visit_group_1) |> ID(:person_id)),\n│ left = true) |>\n│ SELECT(ID(:person_2) |> ID(:person_id),\n│ ID(:visit_group_1) |> ID(:max) |> AS(:max_visit_start_date)),\n│ columns = [SQLColumn(:person_id), SQLColumn(:max_visit_start_date)])\n└ @ FunSQL …\n=#","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"Finally, the SQL tree is serialized into SQL.","category":"page"},{"location":"test/nodes/","page":"SQL Nodes","title":"SQL Nodes","text":"#? VERSION >= v\"1.7\"\nwithenv(\"JULIA_DEBUG\" => \"FunSQL.serialize\") do\n render(q)\nend;\n#=>\n┌ Debug: FunSQL.serialize\n│ SQLString(\n│ \"\"\"\n│ SELECT\n│ \"person_2\".\"person_id\",\n│ \"visit_group_1\".\"max\" AS \"max_visit_start_date\"\n│ FROM (\n│ SELECT\n│ \"person_1\".\"person_id\",\n│ \"person_1\".\"location_id\"\n│ FROM \"person\" AS \"person_1\"\n│ WHERE (\"person_1\".\"year_of_birth\" <= 2000)\n│ ) AS \"person_2\"\n│ JOIN (\n│ SELECT \"location_1\".\"location_id\"\n│ FROM \"location\" AS \"location_1\"\n│ WHERE (\"location_1\".\"state\" = 'IL')\n│ ) AS \"location_2\" ON (\"person_2\".\"location_id\" = \"location_2\".\"location_id\")\n│ LEFT JOIN (\n│ SELECT\n│ max(\"visit_occurrence_1\".\"visit_start_date\") AS \"max\",\n│ \"visit_occurrence_1\".\"person_id\"\n│ FROM \"visit_occurrence\" AS \"visit_occurrence_1\"\n│ GROUP BY \"visit_occurrence_1\".\"person_id\"\n│ ) AS \"visit_group_1\" ON (\"person_2\".\"person_id\" = \"visit_group_1\".\"person_id\")\"\"\",\n│ columns = [SQLColumn(:person_id), SQLColumn(:max_visit_start_date)])\n└ @ FunSQL …\n=#","category":"page"},{"location":"test/other/#Other-Tests","page":"Other Tests","title":"Other Tests","text":"","category":"section"},{"location":"test/other/#SQLConnection-and-SQLStatement","page":"Other Tests","title":"SQLConnection and SQLStatement","text":"","category":"section"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"A SQLConnection object encapsulates a raw database connection together with the database catalog.","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"using FunSQL: SQLConnection, SQLCatalog, SQLTable\nusing Pkg.Artifacts, LazyArtifacts\nusing SQLite\n\nconst DATABASE = joinpath(artifact\"synpuf-10p\", \"synpuf-10p.sqlite\")\n\nraw_conn = DBInterface.connect(SQLite.DB, DATABASE)\n\nperson = SQLTable(:person, columns = [:person_id, :year_of_birth])\n\ncatalog = SQLCatalog(person, dialect = :sqlite)\n\nconn = SQLConnection(raw_conn, catalog = catalog)\n#-> SQLConnection(SQLite.DB( … ), catalog = SQLCatalog(…1 table…, dialect = SQLDialect(:sqlite)))","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"SQLConnection delegates DBInterface calls to the raw connection object.","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"DBInterface.prepare(conn, \"SELECT * FROM person\")\n#-> SQLite.Stmt( … )\n\nDBInterface.execute(conn, \"SELECT * FROM person\")\n#-> SQLite.Query{false}( … )","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"When DBInterface.prepare is applied to a query node, it returns a FunSQL-specific SQLStatement object.","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"using FunSQL: From\n\nq = From(:person)\n\nstmt = DBInterface.prepare(conn, q)\n#-> SQLStatement(SQLConnection( … ), SQLite.Stmt( … ))\n\nDBInterface.getconnection(stmt)\n#-> SQLConnection( … )\n\nDBInterface.execute(stmt)\n#-> SQLite.Query{false}( … )\n\nDBInterface.close!(stmt)","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"For a query with parameters, this allows us to specify the parameter values by name.","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"using FunSQL: Get, Var, Where\n\nq = From(:person) |>\n Where(Get.year_of_birth .>= Var.YEAR)\n\nstmt = DBInterface.prepare(conn, q)\n#-> SQLStatement(SQLConnection( … ), SQLite.Stmt( … ), vars = [:YEAR])\n\nDBInterface.execute(stmt, YEAR = 1950)\n#-> SQLite.Query{false}( … )\n\nDBInterface.close!(stmt)\n\nDBInterface.close!(conn)","category":"page"},{"location":"test/other/#SQLCatalog,-SQLTable,-and-SQLColumn","page":"Other Tests","title":"SQLCatalog, SQLTable, and SQLColumn","text":"","category":"section"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"In FunSQL, tables and table-like entities are represented using SQLTable objects. Their columns are represented using SQLColumn objects. A collection of SQLTable objects is represented as a SQLCatalog object.","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"using FunSQL: SQLCatalog, SQLColumn, SQLTable","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"A SQLTable constructor takes the table name, a vector of columns, and, optionally, the name of the table schema and other qualifiers. A name could be provided either as a Symbol or as a String value. A column can be specified just by its name.","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"location = SQLTable(qualifiers = [:public],\n name = :location,\n columns = [:location_id, :address_1, :address_2,\n :city, :state, :zip])\n#-> SQLTable(qualifiers = [:public], :location, …)\n\nperson = SQLTable(name = \"person\",\n columns = [\"person_id\", \"year_of_birth\", \"location_id\"])\n#-> SQLTable(:person, …)","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"The table and the column names could be provided as positional arguments.","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"concept = SQLTable(\"concept\", \"concept_id\", \"concept_name\", \"vocabulary_id\")\n#-> SQLTable(:concept, …)","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"A column may have a custom name for use with FunSQL and the original name for generating SQL queries.","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"vocabulary = SQLTable(:vocabulary,\n :id => SQLColumn(:vocabulary_id),\n :name => SQLColumn(:vocabulary_name))\n#-> SQLTable(:vocabulary, …)","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"A SQLTable object is displayed as a Julia expression that created the object.","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"display(location)\n#=>\nSQLTable(qualifiers = [:public],\n :location,\n SQLColumn(:location_id),\n SQLColumn(:address_1),\n SQLColumn(:address_2),\n SQLColumn(:city),\n SQLColumn(:state),\n SQLColumn(:zip))\n=#\n\ndisplay(vocabulary)\n#=>\nSQLTable(:vocabulary,\n :id => SQLColumn(:vocabulary_id),\n :name => SQLColumn(:vocabulary_name))\n=#","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"A SQLTable object behaves like a read-only dictionary.","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"person[:person_id]\n#-> SQLColumn(:person_id)\n\nperson[\"person_id\"]\n#-> SQLColumn(:person_id)\n\nperson[1]\n#-> SQLColumn(:person_id)\n\nperson[:visit_occurrence]\n#-> ERROR: KeyError: key :visit_occurrence not found\n\nget(person, :person_id, nothing)\n#-> SQLColumn(:person_id)\n\nget(person, \"person_id\", nothing)\n#-> SQLColumn(:person_id)\n\nget(person, :visit_occurrence, missing)\n#-> missing\n\nget(() -> missing, person, :visit_occurrence)\n#-> missing\n\nlength(person)\n#-> 3\n\ncollect(keys(person))\n#-> [:person_id, :year_of_birth, :location_id]","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"A SQLCatalog constructor takes a collection of SQLTable objects, the target dialect, and the size of the query cache. Just as columns, a table may have a custom name for use with FunSQL and the original name for generating SQL.","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"catalog = SQLCatalog(tables = [person, location, concept, :concept_vocabulary => vocabulary],\n dialect = :sqlite,\n cache = 128)\n#-> SQLCatalog(…4 tables…, dialect = SQLDialect(:sqlite), cache = 128)\n\ndisplay(catalog)\n#=>\nSQLCatalog(SQLTable(:concept,\n SQLColumn(:concept_id),\n SQLColumn(:concept_name),\n SQLColumn(:vocabulary_id)),\n :concept_vocabulary => SQLTable(:vocabulary,\n :id => SQLColumn(:vocabulary_id),\n :name => SQLColumn(\n :vocabulary_name)),\n SQLTable(qualifiers = [:public],\n :location,\n SQLColumn(:location_id),\n SQLColumn(:address_1),\n SQLColumn(:address_2),\n SQLColumn(:city),\n SQLColumn(:state),\n SQLColumn(:zip)),\n SQLTable(:person,\n SQLColumn(:person_id),\n SQLColumn(:year_of_birth),\n SQLColumn(:location_id)),\n dialect = SQLDialect(:sqlite),\n cache = 128)\n=#","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"Number of tables in the catalog affects its representation.","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"SQLCatalog(tables = [:person => person])\n#-> SQLCatalog(…1 table…, dialect = SQLDialect())\n\nSQLCatalog()\n#-> SQLCatalog(dialect = SQLDialect())","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"The query cache can be completely disabled.","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"cacheless_catalog = SQLCatalog(cache = nothing)\n#-> SQLCatalog(dialect = SQLDialect(), cache = nothing)\n\ndisplay(cacheless_catalog)\n#-> SQLCatalog(dialect = SQLDialect(), cache = nothing)","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"Any Dict-like object can serve as a query cache.","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"customcache_catalog = SQLCatalog(cache = Dict())\n#-> SQLCatalog(dialect = SQLDialect(), cache = Dict{Any, Any}())\n\ndisplay(customcache_catalog)\n#-> SQLCatalog(dialect = SQLDialect(), cache = (Dict{Any, Any})())","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"The catalog behaves as a read-only Dict object.","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"catalog[:person]\n#-> SQLTable(:person, …)\n\ncatalog[\"person\"]\n#-> SQLTable(:person, …)\n\ncatalog[:visit_occurrence]\n#-> ERROR: KeyError: key :visit_occurrence not found\n\nget(catalog, :person, nothing)\n#-> SQLTable(:person, …)\n\nget(catalog, \"person\", nothing)\n#-> SQLTable(:person, …)\n\nget(catalog, :visit_occurrence, missing)\n#-> missing\n\nget(() -> missing, catalog, :visit_occurrence)\n#-> missing\n\nlength(catalog)\n#-> 4\n\nsort(collect(keys(catalog)))\n#-> [:concept, :concept_vocabulary, :location, :person]","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"Catalog objects can be assigned arbitrary metadata.","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"metadata_catalog =\n SQLCatalog(SQLTable(:person,\n SQLColumn(:person_id, metadata = (; label = \"Person ID\")),\n SQLColumn(:year_of_birth, metadata = (;)),\n metadata = (; caption = \"Person\", is_view = false)),\n metadata = (; model = \"OMOP\"))\n#-> SQLCatalog(…1 table…, dialect = SQLDialect(), metadata = …)\n\ndisplay(metadata_catalog)\n#=>\nSQLCatalog(SQLTable(:person,\n SQLColumn(:person_id, metadata = [:label => \"Person ID\"]),\n SQLColumn(:year_of_birth),\n metadata = [:caption => \"Person\", :is_view => false]),\n dialect = SQLDialect(),\n metadata = [:model => \"OMOP\"])\n=#","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"FunSQL metadata supports DataAPI metadata interface.","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"using DataAPI\n\nDataAPI.metadata(metadata_catalog)\n#-> Dict(\"model\" => \"OMOP\")\n\nDataAPI.metadata(metadata_catalog, style = true)\n#-> Dict(\"model\" => (\"OMOP\", :default))\n\nDataAPI.metadata(metadata_catalog, :name, :default)\n#-> :default\n\nDataAPI.metadata(metadata_catalog[:person])[\"caption\"]\n#-> \"Person\"\n\nDataAPI.metadata(metadata_catalog[:person], :is_view, true)\n#-> false\n\nDataAPI.colmetadata(metadata_catalog[:person])[:person_id][\"label\"]\n#-> \"Person ID\"\n\nDataAPI.colmetadata(metadata_catalog[:person], 1, :label)\n#-> \"Person ID\"\n\nDataAPI.colmetadata(metadata_catalog[:person], :year_of_birth, :label, \"\")\n#-> \"\"\n\nDataAPI.metadata(metadata_catalog[:person][:person_id])\n#-> Dict(\"label\" => \"Person ID\")\n\nDataAPI.metadata(metadata_catalog[:person][:person_id], :label, \"\")\n#-> \"Person ID\"","category":"page"},{"location":"test/other/#SQLDialect","page":"Other Tests","title":"SQLDialect","text":"","category":"section"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"In FunSQL, properties and capabilities of a particular SQL dialect are encapsulated in a SQLDialect object.","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"using FunSQL: SQLDialect","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"The desired dialect can be specified by name.","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"postgresql_dialect = SQLDialect(:postgresql)\n#-> SQLDialect(:postgresql)\n\ndisplay(postgresql_dialect)\n#-> SQLDialect(:postgresql)","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"If necessary, the dialect can be customized.","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"postgresql_odbc_dialect = SQLDialect(:postgresql,\n variable_prefix = '?',\n variable_style = :positional)\n#-> SQLDialect(:postgresql, …)\n\ndisplay(postgresql_odbc_dialect)\n#-> SQLDialect(:postgresql, variable_prefix = '?', variable_style = :POSITIONAL)","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"The default dialect does not correspond to any particular database server.","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"default_dialect = SQLDialect()\n#-> SQLDialect()\n\ndisplay(default_dialect)\n#-> SQLDialect()","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"A completely custom dialect can be specified.","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"my_dialect = SQLDialect(:my, identifier_quotes = ('<', '>'))\n#-> SQLDialect(name = :my, …)\n\ndisplay(my_dialect)\n#-> SQLDialect(name = :my, identifier_quotes = ('<', '>'))","category":"page"},{"location":"test/other/#SQLString","page":"Other Tests","title":"SQLString","text":"","category":"section"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"SQLString represents a serialized SQL query.","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"using FunSQL: SQLString, pack\n\nsql = SQLString(\"SELECT * FROM person\")\n#-> SQLString(\"SELECT * FROM person\")\n\ndisplay(sql)\n#-> SQLString(\"SELECT * FROM person\")","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"SQLString implements the AbstractString interface.","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"ncodeunits(sql)\n#-> 20\n\ncodeunit(sql)\n#-> UInt8\n\ncodeunit(sql, 1)\n#-> 0x53\n\nisvalid(sql, 1)\n#-> true\n\njoin(collect(sql))\n#-> \"SELECT * FROM person\"\n\nprint(sql)\n#-> SELECT * FROM person\n\nwrite(IOBuffer(), sql)\n#-> 20\n\nString(sql)\n#-> \"SELECT * FROM person\"","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"SQLString may carry a vector columns describing the output columns of the query.","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"sql = SQLString(\"SELECT person_id FROM person\", columns = [SQLColumn(:person_id)])\n#-> SQLString(\"SELECT person_id FROM person\", columns = […1 column…])\n\ndisplay(sql)\n#-> SQLString(\"SELECT person_id FROM person\", columns = [SQLColumn(:person_id)])","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"When the query has parameters, SQLString should include a vector of parameter names in the order they should appear in DBInterface.execute call.","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"sql = SQLString(\"SELECT * FROM person WHERE year_of_birth >= ?\", vars = [:YEAR])\n#-> SQLString(\"SELECT * FROM person WHERE year_of_birth >= ?\", vars = [:YEAR])\n\ndisplay(sql)\n#-> SQLString(\"SELECT * FROM person WHERE year_of_birth >= ?\", vars = [:YEAR])","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"Function pack converts named parameters to the positional form suitable for use with DBInterface.execute.","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"pack(sql, (; YEAR = 1950))\n#-> Any[1950]\n\npack(sql, Dict(:YEAR => 1950))\n#-> Any[1950]\n\npack(sql, Dict(\"YEAR\" => 1950))\n#-> Any[1950]","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"pack can also be applied to a regular string, in which case it returns the parameters unchanged.","category":"page"},{"location":"test/other/","page":"Other Tests","title":"Other Tests","text":"pack(\"SELECT * FROM person WHERE year_of_birth >= ?\", (1950,))\n#-> (1950,)","category":"page"},{"location":"examples/#Examples","page":"Examples","title":"Examples","text":"","category":"section"},{"location":"examples/","page":"Examples","title":"Examples","text":"CurrentModule = FunSQL","category":"page"},{"location":"examples/#Importing-FunSQL","page":"Examples","title":"Importing FunSQL","text":"","category":"section"},{"location":"examples/","page":"Examples","title":"Examples","text":"FunSQL does not export any symbols by default. The following statement imports all available query constructors and the function render.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"using FunSQL:\n FunSQL, Agg, Append, As, Asc, Bind, CrossJoin, Define, Desc, Fun, From,\n Get, Group, Highlight, Iterate, Join, LeftJoin, Limit, Lit, Order,\n Partition, Select, Sort, Var, Where, With, WithExternal, render","category":"page"},{"location":"examples/#Establishing-a-database-connection","page":"Examples","title":"Establishing a database connection","text":"","category":"section"},{"location":"examples/","page":"Examples","title":"Examples","text":"We use FunSQL to assemble SQL queries. To actually run these queries, we need a regular database library such as SQLite.jl, LibPQ.jl, MySQL.jl, or ODBC.jl.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"In the following examples, we use a SQLite database containing a tiny sample of the CMS DE-SynPuf dataset. See the Usage Guide for the description of the database schema.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"Download the database file.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"const URL = \"https://github.com/MechanicalRabbit/ohdsi-synpuf-demo/releases/download/20210412/synpuf-10p.sqlite\"\nconst DATABASE = download(URL)","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"Download the database file as an artifact.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"using Pkg.Artifacts, LazyArtifacts\n\nconst DATABASE = joinpath(artifact\"synpuf-10p\", \"synpuf-10p.sqlite\")\n#-> ⋮","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"Create a connection object.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"using SQLite\n\nconst conn = DBInterface.connect(FunSQL.DB{SQLite.DB}, DATABASE)","category":"page"},{"location":"examples/#Database-connection-with-LibPQ.jl","page":"Examples","title":"Database connection with LibPQ.jl","text":"","category":"section"},{"location":"examples/","page":"Examples","title":"Examples","text":"To create a connection object, FunSQL relies on the DBInterface.jl package. Unfortunately LibPQ.jl, the PostgreSQL client library, does not support DBInterface. To make DBInterface.connect work, we need to manually bridge LibPQ and DBInterface.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"using LibPQ\nusing DBInterface\n\nDBInterface.connect(::Type{LibPQ.Connection}, args...; kws...) =\n LibPQ.Connection(args...; kws...)\n\nDBInterface.prepare(conn::LibPQ.Connection, args...; kws...) =\n LibPQ.prepare(conn, args...; kws...)\n\nDBInterface.execute(conn::Union{LibPQ.Connection, LibPQ.Statement}, args...; kws...) =\n LibPQ.execute(conn, args...; kws...)","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"Now we can create a FunSQL connection using DBInterface.connect.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"const conn = DBInterface.connect(FunSQL.DB{LibPQ.Connection}, …)","category":"page"},{"location":"examples/#SELECT-*-FROM-table","page":"Examples","title":"SELECT * FROM table","text":"","category":"section"},{"location":"examples/","page":"Examples","title":"Examples","text":"FunSQL does not require that a query object contains Select, so a minimal FunSQL query consists of a single From node.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"Show all patient records.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"q = From(:person)","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"We use the function render to serialize the query node as a SQL statement.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"sql = render(conn, q)\n\nprint(sql)\n#=>\nSELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"ethnicity_source_concept_id\"\nFROM \"person\" AS \"person_1\"\n=#","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"This query could be executed with DBInterface.execute.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"res = DBInterface.execute(conn, sql)","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"To display the output of a query, it is convenient to use the DataFrame interface.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"using DataFrames\n\nDataFrame(res)\n#=>\n10×18 DataFrame\n Row │ person_id gender_concept_id year_of_birth month_of_birth day_of_bir ⋯\n │ Int64 Int64 Int64 Int64 Int64 ⋯\n─────┼──────────────────────────────────────────────────────────────────────────\n 1 │ 1780 8532 1940 2 ⋯\n 2 │ 30091 8532 1932 8\n 3 │ 37455 8532 1913 7\n 4 │ 42383 8507 1922 2\n 5 │ 69985 8532 1956 7 ⋯\n 6 │ 72120 8507 1937 10\n 7 │ 82328 8532 1957 9\n 8 │ 95538 8507 1923 11\n 9 │ 107680 8532 1963 12 ⋯\n 10 │ 110862 8507 1911 4\n 14 columns omitted\n=#","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"We could also directly apply DBInterface.execute to the query node in order to render and immediately execute it.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"DBInterface.execute(conn, q) |> DataFrame\n#=>\n10×18 DataFrame\n⋮\n=#","category":"page"},{"location":"examples/#WHERE,-ORDER,-LIMIT","page":"Examples","title":"WHERE, ORDER, LIMIT","text":"","category":"section"},{"location":"examples/","page":"Examples","title":"Examples","text":"Tabular operations such as Where, Order, and Limit are available in FunSQL. Unlike SQL, FunSQL lets you apply them in any order.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"Show the top 3 oldest male patients.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"q = From(:person) |>\n Where(Get.gender_concept_id .== 8507) |>\n Order(Get.year_of_birth) |>\n Limit(3)\n\nrender(conn, q) |> print\n#=>\nSELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"ethnicity_source_concept_id\"\nFROM \"person\" AS \"person_1\"\nWHERE (\"person_1\".\"gender_concept_id\" = 8507)\nORDER BY \"person_1\".\"year_of_birth\"\nLIMIT 3\n=#\n\nDBInterface.execute(conn, q) |> DataFrame\n#=>\n3×18 DataFrame\n Row │ person_id gender_concept_id year_of_birth month_of_birth day_of_bir ⋯\n │ Int64 Int64 Int64 Int64 Int64 ⋯\n─────┼──────────────────────────────────────────────────────────────────────────\n 1 │ 110862 8507 1911 4 ⋯\n 2 │ 42383 8507 1922 2\n 3 │ 95538 8507 1923 11\n 14 columns omitted\n=#","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"Show all males among the top 3 oldest patients.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"q = From(:person) |>\n Order(Get.year_of_birth) |>\n Limit(3) |>\n Where(Get.gender_concept_id .== 8507)\n\nrender(conn, q) |> print\n#=>\nSELECT\n \"person_2\".\"person_id\",\n ⋮\n \"person_2\".\"ethnicity_source_concept_id\"\nFROM (\n SELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"ethnicity_source_concept_id\"\n FROM \"person\" AS \"person_1\"\n ORDER BY \"person_1\".\"year_of_birth\"\n LIMIT 3\n) AS \"person_2\"\nWHERE (\"person_2\".\"gender_concept_id\" = 8507)\n=#\n\nDBInterface.execute(conn, q) |> DataFrame\n#=>\n2×18 DataFrame\n Row │ person_id gender_concept_id year_of_birth month_of_birth day_of_bir ⋯\n │ Int64 Int64 Int64 Int64 Int64 ⋯\n─────┼──────────────────────────────────────────────────────────────────────────\n 1 │ 110862 8507 1911 4 ⋯\n 2 │ 42383 8507 1922 2\n 14 columns omitted\n=#","category":"page"},{"location":"examples/#SELECT-COUNT(*)-FROM-table","page":"Examples","title":"SELECT COUNT(*) FROM table","text":"","category":"section"},{"location":"examples/","page":"Examples","title":"Examples","text":"To calculate an aggregate value for the whole dataset, we apply a Group node without arguments.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"Show the number of patient records.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"q = From(:person) |>\n Group() |>\n Select(Agg.count())\n\nrender(conn, q) |> print\n#=>\nSELECT count(*) AS \"count\"\nFROM \"person\" AS \"person_1\"\n=#\n\nDBInterface.execute(conn, q) |> DataFrame\n#=>\n1×1 DataFrame\n Row │ count\n │ Int64\n─────┼───────\n 1 │ 10\n=#","category":"page"},{"location":"examples/#SELECT-DISTINCT","page":"Examples","title":"SELECT DISTINCT","text":"","category":"section"},{"location":"examples/","page":"Examples","title":"Examples","text":"If we use a Group node, but do not apply any aggregate functions, FunSQL will render it as a SELECT DISTINCT clause.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"Show all US states present in the location records.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"q = From(:location) |>\n Group(Get.state)\n\nrender(conn, q) |> print\n#=>\nSELECT DISTINCT \"location_1\".\"state\"\nFROM \"location\" AS \"location_1\"\n=#\n\nDBInterface.execute(conn, q) |> DataFrame\n#=>\n10×1 DataFrame\n Row │ state\n │ String\n─────┼────────\n 1 │ MI\n 2 │ WA\n 3 │ FL\n 4 │ MD\n 5 │ NY\n 6 │ MS\n 7 │ CO\n 8 │ GA\n 9 │ MA\n 10 │ IL\n=#","category":"page"},{"location":"examples/#Generating-a-complex-CASE-clause","page":"Examples","title":"Generating a complex CASE clause","text":"","category":"section"},{"location":"examples/","page":"Examples","title":"Examples","text":"Show the number of patients stratified by the age group.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"In this query, we need to place a person's age into one of the age buckets: 0 – 4, 5 – 9, 10 – 14, …, 95 – 99, 100 +. This is a tedious expression to write in raw SQL, but it could be written very compactly in FunSQL by using array comprehension to build the CASE expression.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"using Dates\n\nPersonAgeAt(date) =\n Fun.strftime(\"%Y\", date) .- Get.year_of_birth\n\nAgeGroup(age) =\n Fun.case(Iterators.flatten([(age .< y, \"$(y-5) - $(y-1)\")\n for y = 5:5:100])...,\n \"≥ 100\")\n\nq = From(:person) |>\n Group(:age_group => AgeGroup(PersonAgeAt(Date(\"2020-01-01\")))) |>\n Order(Get.age_group) |>\n Select(Get.age_group, Agg.count())\n\nrender(conn, q) |> print\n#=>\nSELECT\n \"person_2\".\"age_group\",\n count(*) AS \"count\"\nFROM (\n SELECT (CASE WHEN ((strftime('%Y', '2020-01-01') - \"person_1\".\"year_of_birth\") < 5) THEN '0 - 4' … ELSE '≥ 100' END) AS \"age_group\"\n FROM \"person\" AS \"person_1\"\n) AS \"person_2\"\nGROUP BY \"person_2\".\"age_group\"\nORDER BY \"person_2\".\"age_group\"\n=#\n\nDBInterface.execute(conn, q) |> DataFrame\n#=>\n6×2 DataFrame\n Row │ age_group count\n │ String Int64\n─────┼──────────────────\n 1 │ 55 - 59 1\n 2 │ 60 - 64 2\n 3 │ 80 - 84 2\n 4 │ 85 - 89 1\n 5 │ 95 - 99 2\n 6 │ ≥ 100 2\n=#","category":"page"},{"location":"examples/#Filtering-output-columns","page":"Examples","title":"Filtering output columns","text":"","category":"section"},{"location":"examples/","page":"Examples","title":"Examples","text":"By default, the From node outputs all columns of a table, but we could restrict or change the list of output columns using Select. Typically, we would directly pass the definitions of output columns as individual arguments of Select, but occasionally it is convenient to generate the definitions programmatically.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"Filter out all \"source\" columns from patient records.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"const person_table = conn.catalog[:person]\n\nis_not_source_column(c::Symbol) =\n !contains(String(c), \"source\")\n\nq = From(:person) |>\n Select(args = [Get(c) for c in keys(person_table.columns) if is_not_source_column(c)])\n\ndisplay(q)\n#=>\nlet q1 = From(:person),\n q2 = q1 |>\n Select(Get.person_id,\n Get.gender_concept_id,\n Get.year_of_birth,\n Get.month_of_birth,\n Get.day_of_birth,\n Get.time_of_birth,\n Get.race_concept_id,\n Get.ethnicity_concept_id,\n Get.location_id,\n Get.provider_id,\n Get.care_site_id)\n q2\nend\n=#\n\nrender(conn, q) |> print\n#=>\nSELECT\n \"person_1\".\"person_id\",\n \"person_1\".\"gender_concept_id\",\n \"person_1\".\"year_of_birth\",\n \"person_1\".\"month_of_birth\",\n \"person_1\".\"day_of_birth\",\n \"person_1\".\"time_of_birth\",\n \"person_1\".\"race_concept_id\",\n \"person_1\".\"ethnicity_concept_id\",\n \"person_1\".\"location_id\",\n \"person_1\".\"provider_id\",\n \"person_1\".\"care_site_id\"\nFROM \"person\" AS \"person_1\"\n=#\n\nDBInterface.execute(conn, q) |> DataFrame\n#=>\n10×11 DataFrame\n Row │ person_id gender_concept_id year_of_birth month_of_birth day_of_bir ⋯\n │ Int64 Int64 Int64 Int64 Int64 ⋯\n─────┼──────────────────────────────────────────────────────────────────────────\n 1 │ 1780 8532 1940 2 ⋯\n 2 │ 30091 8532 1932 8\n 3 │ 37455 8532 1913 7\n 4 │ 42383 8507 1922 2\n 5 │ 69985 8532 1956 7 ⋯\n 6 │ 72120 8507 1937 10\n 7 │ 82328 8532 1957 9\n 8 │ 95538 8507 1923 11\n 9 │ 107680 8532 1963 12 ⋯\n 10 │ 110862 8507 1911 4\n 7 columns omitted\n=#","category":"page"},{"location":"examples/#Output-columns-of-a-Join","page":"Examples","title":"Output columns of a Join","text":"","category":"section"},{"location":"examples/","page":"Examples","title":"Examples","text":"As is often used to disambiguate the columns of the two input branches of the Join node. By default, columns fenced by As are not present in the output.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"q = From(:person) |>\n Join(From(:visit_occurrence) |> As(:visit),\n on = Get.person_id .== Get.visit.person_id)\n\nrender(conn, q) |> print\n#=>\nSELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"ethnicity_source_concept_id\"\nFROM \"person\" AS \"person_1\"\nJOIN \"visit_occurrence\" AS \"visit_occurrence_1\" ON (\"person_1\".\"person_id\" = \"visit_occurrence_1\".\"person_id\")\n=#\n\nq′ = From(:person) |> As(:person) |>\n Join(From(:visit_occurrence),\n on = Get.person.person_id .== Get.person_id)\n\nrender(conn, q′) |> print\n#=>\nSELECT\n \"visit_occurrence_1\".\"visit_occurrence_id\",\n ⋮\n \"visit_occurrence_1\".\"visit_source_concept_id\"\nFROM \"person\" AS \"person_1\"\nJOIN \"visit_occurrence\" AS \"visit_occurrence_1\" ON (\"person_1\".\"person_id\" = \"visit_occurrence_1\".\"person_id\")\n=#","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"We could use a Select node to output the columns of both branches, however we must ensure that all column names are unique.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"const visit_occurrence_table = conn.catalog[:visit_occurrence]\n\nq = q |>\n Select(Get.(keys(person_table.columns))...,\n Get.(keys(visit_occurrence_table.columns), over = Get.visit)...)\n#=>\nERROR: FunSQL.DuplicateLabelError: `person_id` is used more than once in:\n⋮\n=#\n\nq = q |>\n Select(Get.(keys(person_table.columns))...,\n Get.(filter(!in(keys(person_table.columns)), collect(keys(visit_occurrence_table.columns))),\n over = Get.visit)...)\n\nrender(conn, q) |> print\n#=>\nSELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"ethnicity_source_concept_id\",\n \"visit_occurrence_1\".\"visit_occurrence_id\",\n ⋮\n \"visit_occurrence_1\".\"visit_source_concept_id\"\nFROM \"person\" AS \"person_1\"\nJOIN \"visit_occurrence\" AS \"visit_occurrence_1\" ON (\"person_1\".\"person_id\" = \"visit_occurrence_1\".\"person_id\")\n=#","category":"page"},{"location":"examples/#Querying-concepts","page":"Examples","title":"Querying concepts","text":"","category":"section"},{"location":"examples/","page":"Examples","title":"Examples","text":"Medical terms, such as Inpatient (visit) or Myocardial infarction (condition), are stored in the table concept. Concepts are typically identified by the vocabulary and the code within the vocabulary. For example, Myocardial infarction has a code 22298006 in the SNOMED CT vocabulary.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"Concept may be related to each other. For example, Acute myocardial infarction is a subtype of Myocardial infarction. Relationships between concepts are stored in the table concept_relationship with the column relationship_id specifying the type of the relationship.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"Querying healthcare information often starts with identifying the set of relevant concepts. For example, a researcher may want to specify a concept set containing","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"Myocardial infarction (SNOMED 22298006);\nAnd all the subtypes;\nBut excluding Acute subendocardial infarction (SNOMED 70422006) and its subtypes.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"This suggests us to make a FunSQL-based mini-language for querying concept sets. This language will include primitives for fetching concepts by name, or by vocabulary and code, operations for adding related concepts, and combining and excluding concept sets. These operations could be expressed directly in terms of FunSQL queries.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"We start with a primitive for finding a concept by its code in the vocabulary.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"ConceptByCode(vocabulary, code) =\n From(:concept) |>\n Where(Fun.and(Get.vocabulary_id .== vocabulary,\n Get.concept_code .== code))\n\nConceptByCode(vocabulary, codes...) =\n From(:concept) |>\n Where(Fun.and(Get.vocabulary_id .== vocabulary,\n Fun.in(Get.concept_code, codes...)))","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"It is convenient to add a shortcut for common vocabularies.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"SNOMED(codes...) =\n ConceptByCode(\"SNOMED\", codes...)\n\nVISIT(codes...) =\n ConceptByCode(\"Visit\", codes...)","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"Now we can define","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"q = SNOMED(\"22298006\") # Myocardial infarction\n\nDBInterface.execute(conn, q) |> DataFrame\n#=>\n1×10 DataFrame\n Row │ concept_id concept_name domain_id vocabulary_id concept_cl ⋯\n │ Int64 String String String String ⋯\n─────┼──────────────────────────────────────────────────────────────────────────\n 1 │ 4329847 Myocardial infarction Condition SNOMED Clinical F ⋯\n 6 columns omitted\n=#","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"The following composite query pipeline can be applied to a set of concepts to determine their immediate subtypes.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"ImmediateSubtypes() =\n As(:base) |>\n Join(From(:concept_relationship) |>\n Where(Get.relationship_id .== \"Is a\") |>\n As(:concept_relationship),\n on = Get.base.concept_id .== Get.concept_relationship.concept_id_2) |>\n Join(From(:concept),\n on = Get.concept_relationship.concept_id_1 .== Get.concept_id)\n\nq = SNOMED(\"22298006\") |> # Myocardial infarction\n ImmediateSubtypes()\n\nDBInterface.execute(conn, q) |> DataFrame\n#=>\n1×10 DataFrame\n Row │ concept_id concept_name domain_id vocabulary_id conc ⋯\n │ Int64 String String String Stri ⋯\n─────┼──────────────────────────────────────────────────────────────────────────\n 1 │ 312327 Acute myocardial infarction Condition SNOMED Clin ⋯\n 6 columns omitted\n=#","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"Recursively applying ImmediateSubtypes with Iterate gives us the concept set together will all subtypes.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"WithSubtypes() =\n Iterate(ImmediateSubtypes())\n\nq = SNOMED(\"22298006\") |> # Myocardial infarction\n WithSubtypes()\n\nDBInterface.execute(conn, q) |> DataFrame\n#=>\n6×10 DataFrame\n Row │ concept_id concept_name domain_id vocabulary_id ⋯\n │ Int64 String String String ⋯\n─────┼──────────────────────────────────────────────────────────────────────────\n 1 │ 4329847 Myocardial infarction Condition SNOMED ⋯\n 2 │ 312327 Acute myocardial infarction Condition SNOMED\n 3 │ 434376 Acute myocardial infarction of a… Condition SNOMED\n 4 │ 438170 Acute myocardial infarction of i… Condition SNOMED\n 5 │ 438438 Acute myocardial infarction of a… Condition SNOMED ⋯\n 6 │ 444406 Acute subendocardial infarction Condition SNOMED\n 6 columns omitted\n=#","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"Finally, we add operations on a concept set for adding or removing concepts.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"IncludingConcepts(include) =\n Append(include)\n\nExcludingConcepts(exclude) =\n LeftJoin(:exclude => exclude,\n Get.concept_id .== Get.exclude.concept_id) |>\n Where(Fun.is_null(Get.exclude.concept_id))\n\nq = SNOMED(\"22298006\") |> # Myocardial infarction\n WithSubtypes() |>\n ExcludingConcepts(\n SNOMED(\"70422006\") |> # Acute subendocardial infarction\n WithSubtypes())\n\nDBInterface.execute(conn, q) |> DataFrame\n#=>\n5×10 DataFrame\n Row │ concept_id concept_name domain_id vocabulary_id ⋯\n │ Int64 String String String ⋯\n─────┼──────────────────────────────────────────────────────────────────────────\n 1 │ 4329847 Myocardial infarction Condition SNOMED ⋯\n 2 │ 312327 Acute myocardial infarction Condition SNOMED\n 3 │ 434376 Acute myocardial infarction of a… Condition SNOMED\n 4 │ 438170 Acute myocardial infarction of i… Condition SNOMED\n 5 │ 438438 Acute myocardial infarction of a… Condition SNOMED ⋯\n 6 columns omitted\n=#","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"Given a concept set, it is now easy to find the matching clinical conditions.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"MyocardialInfarctionConcepts() =\n SNOMED(\"22298006\") |> # Myocardial infarction\n WithSubtypes() |>\n ExcludingConcepts(\n SNOMED(\"70422006\") |> # Acute subendocardial infarction\n WithSubtypes())\n\nq = From(:condition_occurrence) |>\n Join(MyocardialInfarctionConcepts(),\n Get.condition_concept_id .== Get.concept_id) |>\n Order(Get.condition_occurrence_id) |>\n Select(Get.person_id, Get.condition_start_date)\n\nDBInterface.execute(conn, q) |> DataFrame\n#=>\n6×2 DataFrame\n Row │ person_id condition_start_date\n │ Int64 String\n─────┼─────────────────────────────────\n 1 │ 1780 2008-04-10\n 2 │ 37455 2010-08-12\n 3 │ 69985 2010-05-06\n 4 │ 110862 2008-09-07\n 5 │ 110862 2008-09-07\n 6 │ 110862 2010-06-07\n=#","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"This notation is much more compact and readable than the corresponding SQL query.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"render(conn, q) |> print\n#=>\nWITH RECURSIVE \"base_1\" (\"concept_id\") AS (\n SELECT \"concept_1\".\"concept_id\"\n FROM \"concept\" AS \"concept_1\"\n WHERE\n (\"concept_1\".\"vocabulary_id\" = 'SNOMED') AND\n (\"concept_1\".\"concept_code\" = '22298006')\n UNION ALL\n SELECT \"concept_2\".\"concept_id\"\n FROM \"base_1\" AS \"base_2\"\n JOIN (\n SELECT\n \"concept_relationship_1\".\"concept_id_1\",\n \"concept_relationship_1\".\"concept_id_2\"\n FROM \"concept_relationship\" AS \"concept_relationship_1\"\n WHERE (\"concept_relationship_1\".\"relationship_id\" = 'Is a')\n ) AS \"concept_relationship_2\" ON (\"base_2\".\"concept_id\" = \"concept_relationship_2\".\"concept_id_2\")\n JOIN \"concept\" AS \"concept_2\" ON (\"concept_relationship_2\".\"concept_id_1\" = \"concept_2\".\"concept_id\")\n),\n\"base_4\" (\"concept_id\") AS (\n SELECT \"concept_3\".\"concept_id\"\n FROM \"concept\" AS \"concept_3\"\n WHERE\n (\"concept_3\".\"vocabulary_id\" = 'SNOMED') AND\n (\"concept_3\".\"concept_code\" = '70422006')\n UNION ALL\n SELECT \"concept_4\".\"concept_id\"\n FROM \"base_4\" AS \"base_5\"\n JOIN (\n SELECT\n \"concept_relationship_3\".\"concept_id_1\",\n \"concept_relationship_3\".\"concept_id_2\"\n FROM \"concept_relationship\" AS \"concept_relationship_3\"\n WHERE (\"concept_relationship_3\".\"relationship_id\" = 'Is a')\n ) AS \"concept_relationship_4\" ON (\"base_5\".\"concept_id\" = \"concept_relationship_4\".\"concept_id_2\")\n JOIN \"concept\" AS \"concept_4\" ON (\"concept_relationship_4\".\"concept_id_1\" = \"concept_4\".\"concept_id\")\n)\nSELECT\n \"condition_occurrence_1\".\"person_id\",\n \"condition_occurrence_1\".\"condition_start_date\"\nFROM \"condition_occurrence\" AS \"condition_occurrence_1\"\nJOIN (\n SELECT \"base_3\".\"concept_id\"\n FROM \"base_1\" AS \"base_3\"\n LEFT JOIN \"base_4\" AS \"base_6\" ON (\"base_3\".\"concept_id\" = \"base_6\".\"concept_id\")\n WHERE (\"base_6\".\"concept_id\" IS NULL)\n) AS \"base_7\" ON (\"condition_occurrence_1\".\"condition_concept_id\" = \"base_7\".\"concept_id\")\nORDER BY \"condition_occurrence_1\".\"condition_occurrence_id\"\n=#","category":"page"},{"location":"examples/#Assembling-queries-incrementally","page":"Examples","title":"Assembling queries incrementally","text":"","category":"section"},{"location":"examples/","page":"Examples","title":"Examples","text":"It is often convenient to build a query incrementally, one component at a time. This allows us to validate individual components, inspect their output, and possibly reuse them in other queries.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"Find all occurrences of myocardial infarction that was diagnosed during an inpatient visit. Filter out repeating occurrences by requiring a 180-day gap between consecutive events.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"We start with generating two datasets: inpatient visits and myocardial infarction conditions. For constructing the concepts Inpatient Visit and Myocardial Infarction, we use the definitions from the section Querying concepts:","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"MyocardialInfarctionConcept() =\n SNOMED(\"22298006\") |>\n WithSubtypes()\n\nDBInterface.execute(conn, MyocardialInfarctionConcept()) |> DataFrame\n#=>\n6×10 DataFrame\n Row │ concept_id concept_name domain_id vocabulary_id ⋯\n │ Int64 String String String ⋯\n─────┼──────────────────────────────────────────────────────────────────────────\n 1 │ 4329847 Myocardial infarction Condition SNOMED ⋯\n 2 │ 312327 Acute myocardial infarction Condition SNOMED\n 3 │ 434376 Acute myocardial infarction of a… Condition SNOMED\n 4 │ 438170 Acute myocardial infarction of i… Condition SNOMED\n 5 │ 438438 Acute myocardial infarction of a… Condition SNOMED ⋯\n 6 │ 444406 Acute subendocardial infarction Condition SNOMED\n 6 columns omitted\n=#\n\nMyocardialInfarctionOccurrence() =\n From(:condition_occurrence) |>\n Join(:concept => MyocardialInfarctionConcept(),\n on = Get.condition_concept_id .== Get.concept.concept_id) |>\n Order(Get.condition_occurrence_id)\n\nDBInterface.execute(conn, MyocardialInfarctionOccurrence()) |> DataFrame\n#=>\n11×11 DataFrame\n Row │ condition_occurrence_id person_id condition_concept_id condition_sta ⋯\n │ Int64 Int64 Int64 String ⋯\n─────┼──────────────────────────────────────────────────────────────────────────\n 1 │ 228161 1780 312327 2008-04-10 ⋯\n 2 │ 3767773 30091 444406 2009-08-02\n 3 │ 4696273 37455 438438 2010-08-12\n 4 │ 8701359 69985 444406 2010-07-22\n 5 │ 8701405 69985 312327 2010-05-06 ⋯\n 6 │ 11881327 95538 444406 2009-03-30\n 7 │ 13374905 107680 444406 2009-07-20\n 8 │ 13769162 110862 444406 2009-09-30\n 9 │ 13769189 110862 438170 2008-09-07 ⋯\n 10 │ 13769190 110862 434376 2008-09-07\n 11 │ 13769260 110862 312327 2010-06-07\n 8 columns omitted\n=#\n\nInpatientVisitConcept() =\n VISIT(\"IP\") |>\n WithSubtypes()\n\nDBInterface.execute(conn, InpatientVisitConcept()) |> DataFrame\n#=>\n2×10 DataFrame\n Row │ concept_id concept_name domain_id vocabulary_id concep ⋯\n │ Int64 String String String String ⋯\n─────┼──────────────────────────────────────────────────────────────────────────\n 1 │ 9201 Inpatient Visit Visit Visit Visit ⋯\n 2 │ 8717 Inpatient Hospital Visit CMS Place of Service Visit\n 6 columns omitted\n=#\n\nInpatientVisitOccurrence() =\n From(:visit_occurrence) |>\n Join(:concept => InpatientVisitConcept(),\n on = Get.visit_concept_id .== Get.concept.concept_id)\n\nDBInterface.execute(conn, InpatientVisitOccurrence()) |> DataFrame\n#=>\n6×12 DataFrame\n Row │ visit_occurrence_id person_id visit_concept_id visit_start_date vis ⋯\n │ Int64 Int64 Int64 String Mis ⋯\n─────┼──────────────────────────────────────────────────────────────────────────\n 1 │ 88179 1780 9201 2008-04-09 ⋯\n 2 │ 1454883 30091 9201 2009-07-30\n 3 │ 3359790 69985 9201 2010-07-22\n 4 │ 4586628 95538 9201 2009-03-30\n 5 │ 5162803 107680 9201 2009-07-20 ⋯\n 6 │ 5314664 110862 9201 2009-09-30\n 8 columns omitted\n=#","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"Using these two datasets, we need to find those conditions that occurred during one of the visits. We start with building a parameterized query that finds visits overlapping with a specified timestamp.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"using Dates\n\nCorrelatedInpatientVisit(person_id, date) =\n InpatientVisitOccurrence() |>\n Where(Fun.and(Get.person_id .== Var.PERSON_ID,\n Fun.between(Var.DATE, Get.visit_start_date, Get.visit_end_date))) |>\n Bind(:PERSON_ID => person_id,\n :DATE => date)\n\nq = CorrelatedInpatientVisit(1780, Date(\"2008-04-10\"))\n\nDBInterface.execute(conn, q) |> DataFrame\n#=>\n1×12 DataFrame\n Row │ visit_occurrence_id person_id visit_concept_id visit_start_date vis ⋯\n │ Int64 Int64 Int64 String Mis ⋯\n─────┼──────────────────────────────────────────────────────────────────────────\n 1 │ 88179 1780 9201 2008-04-09 ⋯\n 8 columns omitted\n=#","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"We will use this query to correlate inpatient visits with the date of the diagnosed condition.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"MyocardialInfarctionDuringInpatientVisit() =\n MyocardialInfarctionOccurrence() |>\n Where(Fun.exists(CorrelatedInpatientVisit(Get.person_id, Get.condition_start_date)))\n\nDBInterface.execute(conn, MyocardialInfarctionDuringInpatientVisit()) |> DataFrame\n#=>\n6×11 DataFrame\n Row │ condition_occurrence_id person_id condition_concept_id condition_sta ⋯\n │ Int64 Int64 Int64 String ⋯\n─────┼──────────────────────────────────────────────────────────────────────────\n 1 │ 228161 1780 312327 2008-04-10 ⋯\n 2 │ 3767773 30091 444406 2009-08-02\n 3 │ 8701359 69985 444406 2010-07-22\n 4 │ 11881327 95538 444406 2009-03-30\n 5 │ 13374905 107680 444406 2009-07-20 ⋯\n 6 │ 13769162 110862 444406 2009-09-30\n 8 columns omitted\n=#","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"Finally, we must exclude any events that occurred within 180 days from the previous event. For this purpose, we build a filtering pipeline:","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"using Dates\n\nFilterByGap(date, gap) =\n Partition(Get.person_id, order_by = [date]) |>\n Define(:boundary => Agg.lag(Fun.date(date, gap))) |>\n Where(Fun.or(Fun.is_null(Get.boundary),\n Get.boundary .< date))","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"To verify that this pipeline operates correctly, we could apply it to a synthetic dataset.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"events = DataFrame([(person_id = 1, date = Date(\"2020-01-01\")), # ✓\n (person_id = 1, date = Date(\"2020-02-01\")), # ✗\n (person_id = 1, date = Date(\"2021-01-01\")), # ✓\n (person_id = 1, date = Date(\"2021-05-01\")), # ✗\n (person_id = 1, date = Date(\"2021-10-01\")), # ✗\n (person_id = 2, date = Date(\"2020-01-01\")), # ✓\n])\n#=>\n6×2 DataFrame\n Row │ person_id date\n │ Int64 Date\n─────┼───────────────────────\n 1 │ 1 2020-01-01\n 2 │ 1 2020-02-01\n 3 │ 1 2021-01-01\n 4 │ 1 2021-05-01\n 5 │ 1 2021-10-01\n 6 │ 2 2020-01-01\n=#\n\nq = From(events) |>\n FilterByGap(Get.date, Day(180))\n\nDBInterface.execute(conn, q) |> DataFrame\n#=>\n3×3 DataFrame\n Row │ person_id date boundary\n │ Int64 String String?\n─────┼───────────────────────────────────\n 1 │ 1 2020-01-01 missing\n 2 │ 1 2021-01-01 2020-07-30\n 3 │ 2 2020-01-01 missing\n=#","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"Now we have all the components to construct the final query:","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"FilteredMyocardialInfarctionDuringInpatientVisit() =\n MyocardialInfarctionDuringInpatientVisit() |>\n FilterByGap(Get.condition_start_date, Day(180))\n\nq = FilteredMyocardialInfarctionDuringInpatientVisit() |>\n Select(Get.person_id, Get.condition_start_date)\n\nDBInterface.execute(conn, q) |> DataFrame\n#=>\n6×2 DataFrame\n Row │ person_id condition_start_date\n │ Int64 String\n─────┼─────────────────────────────────\n 1 │ 1780 2008-04-10\n 2 │ 30091 2009-08-02\n 3 │ 69985 2010-07-22\n 4 │ 95538 2009-03-30\n 5 │ 107680 2009-07-20\n 6 │ 110862 2009-09-30\n=#\n\nrender(conn, q) |> print\n#=>\nWITH RECURSIVE \"base_1\" (\"concept_id\") AS (\n SELECT \"concept_1\".\"concept_id\"\n FROM \"concept\" AS \"concept_1\"\n WHERE\n (\"concept_1\".\"vocabulary_id\" = 'SNOMED') AND\n (\"concept_1\".\"concept_code\" = '22298006')\n UNION ALL\n SELECT \"concept_2\".\"concept_id\"\n FROM \"base_1\" AS \"base_2\"\n JOIN (\n SELECT\n \"concept_relationship_1\".\"concept_id_1\",\n \"concept_relationship_1\".\"concept_id_2\"\n FROM \"concept_relationship\" AS \"concept_relationship_1\"\n WHERE (\"concept_relationship_1\".\"relationship_id\" = 'Is a')\n ) AS \"concept_relationship_2\" ON (\"base_2\".\"concept_id\" = \"concept_relationship_2\".\"concept_id_2\")\n JOIN \"concept\" AS \"concept_2\" ON (\"concept_relationship_2\".\"concept_id_1\" = \"concept_2\".\"concept_id\")\n),\n\"base_4\" (\"concept_id\") AS (\n SELECT \"concept_3\".\"concept_id\"\n FROM \"concept\" AS \"concept_3\"\n WHERE\n (\"concept_3\".\"vocabulary_id\" = 'Visit') AND\n (\"concept_3\".\"concept_code\" = 'IP')\n UNION ALL\n SELECT \"concept_4\".\"concept_id\"\n FROM \"base_4\" AS \"base_5\"\n JOIN (\n SELECT\n \"concept_relationship_3\".\"concept_id_1\",\n \"concept_relationship_3\".\"concept_id_2\"\n FROM \"concept_relationship\" AS \"concept_relationship_3\"\n WHERE (\"concept_relationship_3\".\"relationship_id\" = 'Is a')\n ) AS \"concept_relationship_4\" ON (\"base_5\".\"concept_id\" = \"concept_relationship_4\".\"concept_id_2\")\n JOIN \"concept\" AS \"concept_4\" ON (\"concept_relationship_4\".\"concept_id_1\" = \"concept_4\".\"concept_id\")\n)\nSELECT\n \"condition_occurrence_3\".\"person_id\",\n \"condition_occurrence_3\".\"condition_start_date\"\nFROM (\n SELECT\n \"condition_occurrence_2\".\"person_id\",\n \"condition_occurrence_2\".\"condition_start_date\",\n (lag(date(\"condition_occurrence_2\".\"condition_start_date\", '180 days')) OVER (PARTITION BY \"condition_occurrence_2\".\"person_id\" ORDER BY \"condition_occurrence_2\".\"condition_start_date\")) AS \"boundary\"\n FROM (\n SELECT\n \"condition_occurrence_1\".\"person_id\",\n \"condition_occurrence_1\".\"condition_start_date\"\n FROM \"condition_occurrence\" AS \"condition_occurrence_1\"\n JOIN \"base_1\" AS \"base_3\" ON (\"condition_occurrence_1\".\"condition_concept_id\" = \"base_3\".\"concept_id\")\n ORDER BY \"condition_occurrence_1\".\"condition_occurrence_id\"\n ) AS \"condition_occurrence_2\"\n WHERE (EXISTS (\n SELECT NULL AS \"_\"\n FROM \"visit_occurrence\" AS \"visit_occurrence_1\"\n JOIN \"base_4\" AS \"base_6\" ON (\"visit_occurrence_1\".\"visit_concept_id\" = \"base_6\".\"concept_id\")\n WHERE\n (\"visit_occurrence_1\".\"person_id\" = \"condition_occurrence_2\".\"person_id\") AND\n (\"condition_occurrence_2\".\"condition_start_date\" BETWEEN \"visit_occurrence_1\".\"visit_start_date\" AND \"visit_occurrence_1\".\"visit_end_date\")\n ))\n) AS \"condition_occurrence_3\"\nWHERE\n (\"condition_occurrence_3\".\"boundary\" IS NULL) OR\n (\"condition_occurrence_3\".\"boundary\" < \"condition_occurrence_3\".\"condition_start_date\")\n=#","category":"page"},{"location":"examples/#Merging-overlapping-intervals","page":"Examples","title":"Merging overlapping intervals","text":"","category":"section"},{"location":"examples/","page":"Examples","title":"Examples","text":"Merging overlapping intervals into a single encompassing period could be done in three steps:","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"Tag the intervals that start a new period.\nEnumerate the periods.\nGroup the intervals by the period number.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"FunSQL lets us encapsulate and reuse this rather complex sequence of transformations.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"Merge overlapping visits.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"MergeOverlappingIntervals(start_date, end_date) =\n Partition(Get.person_id,\n order_by = [start_date],\n frame = (mode = :rows, start = -Inf, finish = -1)) |>\n Define(:new => Fun.case(start_date .<= Agg.max(end_date), 0, 1)) |>\n Partition(Get.person_id,\n order_by = [start_date, .- Get.new],\n frame = :rows) |>\n Define(:period => Agg.sum(Get.new)) |>\n Group(Get.person_id, Get.period) |>\n Define(:start_date => Agg.min(start_date),\n :end_date => Agg.max(end_date))\n\nq = From(:visit_occurrence) |>\n MergeOverlappingIntervals(Get.visit_start_date, Get.visit_end_date) |>\n Select(Get.person_id, Get.start_date, Get.end_date)\n\nrender(conn, q) |> print\n#=>\nSELECT\n \"visit_occurrence_3\".\"person_id\",\n min(\"visit_occurrence_3\".\"visit_start_date\") AS \"start_date\",\n max(\"visit_occurrence_3\".\"visit_end_date\") AS \"end_date\"\nFROM (\n SELECT\n \"visit_occurrence_2\".\"person_id\",\n (sum(\"visit_occurrence_2\".\"new\") OVER (PARTITION BY \"visit_occurrence_2\".\"person_id\" ORDER BY \"visit_occurrence_2\".\"visit_start_date\", (- \"visit_occurrence_2\".\"new\") ROWS UNBOUNDED PRECEDING)) AS \"period\",\n \"visit_occurrence_2\".\"visit_start_date\",\n \"visit_occurrence_2\".\"visit_end_date\"\n FROM (\n SELECT\n \"visit_occurrence_1\".\"person_id\",\n \"visit_occurrence_1\".\"visit_start_date\",\n \"visit_occurrence_1\".\"visit_end_date\",\n (CASE WHEN (\"visit_occurrence_1\".\"visit_start_date\" <= (max(\"visit_occurrence_1\".\"visit_end_date\") OVER (PARTITION BY \"visit_occurrence_1\".\"person_id\" ORDER BY \"visit_occurrence_1\".\"visit_start_date\" ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING))) THEN 0 ELSE 1 END) AS \"new\"\n FROM \"visit_occurrence\" AS \"visit_occurrence_1\"\n ) AS \"visit_occurrence_2\"\n) AS \"visit_occurrence_3\"\nGROUP BY\n \"visit_occurrence_3\".\"person_id\",\n \"visit_occurrence_3\".\"period\"\n=#\n\nDBInterface.execute(conn, q) |> DataFrame\n#=>\n25×3 DataFrame\n Row │ person_id start_date end_date\n │ Int64 String String\n─────┼───────────────────────────────────\n 1 │ 1780 2008-04-09 2008-04-13\n 2 │ 1780 2008-11-22 2008-11-22\n 3 │ 1780 2009-05-22 2009-05-22\n 4 │ 30091 2008-11-12 2008-11-12\n 5 │ 30091 2009-07-30 2009-08-07\n 6 │ 37455 2008-03-18 2008-03-18\n 7 │ 37455 2008-10-30 2008-10-30\n 8 │ 37455 2010-08-12 2010-08-12\n ⋮ │ ⋮ ⋮ ⋮\n 19 │ 95538 2009-09-02 2009-09-02\n 20 │ 107680 2009-06-07 2009-06-07\n 21 │ 107680 2009-07-20 2009-07-30\n 22 │ 110862 2008-09-07 2008-09-16\n 23 │ 110862 2009-06-30 2009-06-30\n 24 │ 110862 2009-09-30 2009-10-01\n 25 │ 110862 2010-06-07 2010-06-07\n 10 rows omitted\n=#","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"Derive a patient's observation periods by merging visits with less than one year gap between them.","category":"page"},{"location":"examples/","page":"Examples","title":"Examples","text":"MergeIntervalsByGap(start_date, end_date, gap) =\n MergeOverlappingIntervals(start_date, Fun.date(end_date, gap)) |>\n Define(:end_date => Fun.date(Get.end_date, -gap))\n\nq = From(:visit_occurrence) |>\n MergeIntervalsByGap(Get.visit_start_date, Get.visit_end_date, Day(365)) |>\n Select(Get.person_id, Get.start_date, Get.end_date)\n\nrender(conn, q) |> print\n#=>\nSELECT\n \"visit_occurrence_3\".\"person_id\",\n min(\"visit_occurrence_3\".\"visit_start_date\") AS \"start_date\",\n date(max(date(\"visit_occurrence_3\".\"visit_end_date\", '365 days')), '-365 days') AS \"end_date\"\nFROM (\n SELECT\n \"visit_occurrence_2\".\"person_id\",\n (sum(\"visit_occurrence_2\".\"new\") OVER (PARTITION BY \"visit_occurrence_2\".\"person_id\" ORDER BY \"visit_occurrence_2\".\"visit_start_date\", (- \"visit_occurrence_2\".\"new\") ROWS UNBOUNDED PRECEDING)) AS \"period\",\n \"visit_occurrence_2\".\"visit_start_date\",\n \"visit_occurrence_2\".\"visit_end_date\"\n FROM (\n SELECT\n \"visit_occurrence_1\".\"person_id\",\n \"visit_occurrence_1\".\"visit_start_date\",\n \"visit_occurrence_1\".\"visit_end_date\",\n (CASE WHEN (\"visit_occurrence_1\".\"visit_start_date\" <= (max(date(\"visit_occurrence_1\".\"visit_end_date\", '365 days')) OVER (PARTITION BY \"visit_occurrence_1\".\"person_id\" ORDER BY \"visit_occurrence_1\".\"visit_start_date\" ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING))) THEN 0 ELSE 1 END) AS \"new\"\n FROM \"visit_occurrence\" AS \"visit_occurrence_1\"\n ) AS \"visit_occurrence_2\"\n) AS \"visit_occurrence_3\"\nGROUP BY\n \"visit_occurrence_3\".\"person_id\",\n \"visit_occurrence_3\".\"period\"\n=#\n\nDBInterface.execute(conn, q) |> DataFrame\n#=>\n12×3 DataFrame\n Row │ person_id start_date end_date\n │ Int64 String String\n─────┼───────────────────────────────────\n 1 │ 1780 2008-04-09 2009-05-22\n 2 │ 30091 2008-11-12 2009-08-07\n 3 │ 37455 2008-03-18 2008-10-30\n 4 │ 37455 2010-08-12 2010-08-12\n 5 │ 42383 2009-06-29 2010-04-15\n 6 │ 69985 2009-01-09 2009-01-09\n 7 │ 69985 2010-04-17 2010-07-30\n 8 │ 72120 2008-12-15 2008-12-15\n 9 │ 82328 2008-10-20 2009-01-25\n 10 │ 95538 2009-03-30 2009-09-02\n 11 │ 107680 2009-06-07 2009-07-30\n 12 │ 110862 2008-09-07 2010-06-07\n=#","category":"page"},{"location":"guide/#Usage-Guide","page":"Usage Guide","title":"Usage Guide","text":"","category":"section"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"CurrentModule = FunSQL","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"This guide will teach you how to assemble SQL queries using FunSQL.","category":"page"},{"location":"guide/#Test-Database","page":"Usage Guide","title":"Test Database","text":"","category":"section"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"To demonstrate database queries, we need a test database. The database we use in this guide is a tiny 10 person sample of simulated patient data extracted from a much larger CMS DE-SynPuf dataset. For a database engine, we picked SQLite. Using SQLite in a guide is convenient because it does not require a database server to run and allows us to distribute the whole database as a single file. FunSQL supports SQLite and many other database engines. The techniques discussed here are not specific to SQLite and once you learn them, you will be able to apply them to any SQL database.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"The data in the test database is stored in the format of the OMOP Common Data Model, an open source database schema for observational healthcare data. In this guide, we will only use a small fragment of the Common Data Model.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"(Image: Fragment of the OMOP Common Data Model)","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"The patient data, including basic demographic information, is stored in the table person. Patient addresses are stored in a separate table location, linked to person by the key column location_id.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"The bulk of patient data consists of clinical events: visits to healthcare providers, recorded observations, diagnosed conditions, prescribed medications, etc. In this guide we only use two types of events, visits and conditions.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"The specific type of the event (e.g., Inpatient visit or Essential hypertension condition) is indicated using a concept id column, which refers to the concept table. Different concepts may be related to each other. For instance, Essential hypertension is a Hypertensive disorder, which itself is a Disorder of cardiovascular system. Concept relationships are recorded in the corresponding table.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"If you wish to follow along with the guide and run the examples, download the database file:","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"DATABASE = download(\"https://github.com/MechanicalRabbit/ohdsi-synpuf-demo/releases/download/20210412/synpuf-10p.sqlite\")","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"All examples in this guide are tested on each update using the NarrativeTest package. To avoid downloading the database file all the time, we registered the download URL as an artifact and use Pkg.Artifacts API to fetch it:","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"using Pkg.Artifacts, LazyArtifacts\n\nDATABASE = joinpath(artifact\"synpuf-10p\", \"synpuf-10p.sqlite\")\n#-> ⋮","category":"page"},{"location":"guide/#Using-FunSQL","page":"Usage Guide","title":"Using FunSQL","text":"","category":"section"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"To interact with an SQLite database from Julia code, we need to install the SQLite package:","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"using Pkg\n\nPkg.add(\"SQLite\")","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"With the package installed, we can open a database connection:","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"using FunSQL\nusing SQLite\n\nconn = DBInterface.connect(FunSQL.DB{SQLite.DB}, DATABASE)","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"This call to DBInterface.connect creates a connection to the SQLite database, retrieves the catalog of available database tables, and returns a FunSQL connection object.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Some applications open many connections to the same database. For instance, a web application may open a new database connection on every incoming HTTP request. In this case, it may be worth to have all these connections to share the same database catalog. The application can start with loading the catalog using using reflect:","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"using FunSQL: reflect\n\ncatalog = reflect(DBInterface.connect(SQLite.DB, DATABASE))","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Then whenever a new connection is created, this catalog object could be reused:","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"conn = FunSQL.DB(DBInterface.connect(SQLite.DB, DATABASE),\n catalog = catalog)","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"warning: Warning\nSome database drivers, including the PostgreSQL client library LibPQ.jl, do not support DBInterface. For instructions on how to enable DBInterface for LibPQ, see this example.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Using the connection object, we can execute FunSQL queries. For example, the following query outputs the content of the table person:","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"using FunSQL: From\n\nq = From(:person)","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"This query could be executed with DBInterface.execute:","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"res = DBInterface.execute(conn, q)","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"To display the result of a query, it is convenient to convert it to a DataFrame object:","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"using DataFrames\n\nDataFrame(res)\n#=>\n10×18 DataFrame\n Row │ person_id gender_concept_id year_of_birth month_of_birth day_of_bir ⋯\n │ Int64 Int64 Int64 Int64 Int64 ⋯\n─────┼──────────────────────────────────────────────────────────────────────────\n 1 │ 1780 8532 1940 2 ⋯\n 2 │ 30091 8532 1932 8\n 3 │ 37455 8532 1913 7\n 4 │ 42383 8507 1922 2\n 5 │ 69985 8532 1956 7 ⋯\n 6 │ 72120 8507 1937 10\n 7 │ 82328 8532 1957 9\n 8 │ 95538 8507 1923 11\n 9 │ 107680 8532 1963 12 ⋯\n 10 │ 110862 8507 1911 4\n 14 columns omitted\n=#","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Instead of executing the query directly, we can render it to generate the corresponding SQL statement:","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"using FunSQL: render\n\nsql = render(conn, q)\n\nprint(sql)\n#=>\nSELECT\n \"person_1\".\"person_id\",\n \"person_1\".\"gender_concept_id\",\n \"person_1\".\"year_of_birth\",\n \"person_1\".\"month_of_birth\",\n \"person_1\".\"day_of_birth\",\n \"person_1\".\"time_of_birth\",\n \"person_1\".\"race_concept_id\",\n \"person_1\".\"ethnicity_concept_id\",\n \"person_1\".\"location_id\",\n \"person_1\".\"provider_id\",\n \"person_1\".\"care_site_id\",\n \"person_1\".\"person_source_value\",\n \"person_1\".\"gender_source_value\",\n \"person_1\".\"gender_source_concept_id\",\n \"person_1\".\"race_source_value\",\n \"person_1\".\"race_source_concept_id\",\n \"person_1\".\"ethnicity_source_value\",\n \"person_1\".\"ethnicity_source_concept_id\"\nFROM \"person\" AS \"person_1\"\n=#","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"In fact, we do not need a database connection if all we want is to generate a SQL query. For this purpose, we only need a SQLCatalog object that describes the structure of the database tables and the target SQL dialect:","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"using FunSQL: SQLCatalog, SQLTable\n\ncatalog = SQLCatalog(SQLTable(:person, columns = [:person_id, :year_of_birth]),\n dialect = :sqlite)\n\nsql = render(catalog, q)\n\nprint(sql)\n#=>\nSELECT\n \"person_1\".\"person_id\",\n \"person_1\".\"year_of_birth\"\nFROM \"person\" AS \"person_1\"\n=#","category":"page"},{"location":"guide/#Why-FunSQL?","page":"Usage Guide","title":"Why FunSQL?","text":"","category":"section"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Let us clarify the purpose of FunSQL. Consider a problem:","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Find all patients born between 1930 and 1940 and living in Illinois, and for each patient show their current age (by the end of 2020).","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"The answer can be obtained with the following SQL query:","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"SELECT p.person_id, 2020 - p.year_of_birth AS age\nFROM person p\nJOIN location l ON (p.location_id = l.location_id)\nWHERE (p.year_of_birth BETWEEN 1930 AND 1940) AND (l.state = 'IL')","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"The simplest way to incorporate this query into Julia code is to embed it as a string literal:","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"sql = \"\"\"\nSELECT p.person_id, 2020 - p.year_of_birth AS age\nFROM person p\nJOIN location l ON (p.location_id = l.location_id)\nWHERE (p.year_of_birth BETWEEN 1930 AND 1940) AND (l.state = 'IL')\n\"\"\"\n\nDBInterface.execute(conn, sql) |> DataFrame\n#=>\n1×2 DataFrame\n Row │ person_id age\n │ Int64 Int64\n─────┼──────────────────\n 1 │ 72120 83\n=#","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"With FunSQL, instead of embedding the SQL query directly into Julia code, we construct a query object:","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"using FunSQL: As, From, Fun, Get, Join, Select, Where\n\nq = From(:person) |>\n Where(Fun.between(Get.year_of_birth, 1930, 1940)) |>\n Join(From(:location) |> Where(Get.state .== \"IL\") |> As(:location),\n on = Get.location_id .== Get.location.location_id) |>\n Select(Get.person_id, :age => 2020 .- Get.year_of_birth)","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"The value of q is a composite object of type SQLNode. \"Composite\" means that q is assembled from components (also of type SQLNode), which themselves are either atomic or assembled from smaller components. Different kinds of components are created by SQLNode constructors such as From, Where, Fun, Get, etc.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"We use the same DBInterface.execute method to serialize the query object as a SQL statement and immediately execute it:","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"DBInterface.execute(conn, q) |> DataFrame\n#=>\n1×2 DataFrame\n Row │ person_id age\n │ Int64 Int64\n─────┼──────────────────\n 1 │ 72120 83\n=#","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Why, instead of embedding a complete SQL query, we prefer to generate it through a query object? To justify this extra step, consider that in a real Julia program, any query is likely going to be parameterized:","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Find all patients born between $start_year and $end_year and living in $states, and for each patient show the $output_columns.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"If this is the case, the SQL query cannot be prepared in advance and must be assembled on the fly. While it is possible to assemble a SQL query from string fragments, it is tedious, error-prone and definitely not fun. FunSQL provides a more robust and effective approach: build the query as a composite data structure.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Here is how this parameterized query may be constructed with FunSQL:","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"function FindPatients(; start_year = nothing,\n end_year = nothing,\n states = String[])\n q = From(:person) |>\n Where(BirthRange(start_year, end_year))\n if !isempty(states)\n q = q |>\n Join(:location => From(:location) |>\n Where(Fun.in(Get.state, states...)),\n on = Get.location_id .== Get.location.location_id)\n end\n q\nend\n\nfunction BirthRange(start_year, end_year)\n p = true\n if start_year !== nothing\n p = Fun.and(p, Get.year_of_birth .>= start_year)\n end\n if end_year !== nothing\n p = Fun.and(p, Get.year_of_birth .<= end_year)\n end\n p\nend","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"The function FindPatients effectively becomes a new SQLNode constructor, which can be used directly or as a component of a larger query.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Show all patient data.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"q = FindPatients()\n\nDBInterface.execute(conn, q) |> DataFrame\n#=>\n10×18 DataFrame\n Row │ person_id gender_concept_id year_of_birth month_of_birth day_of_bir ⋯\n │ Int64 Int64 Int64 Int64 Int64 ⋯\n─────┼──────────────────────────────────────────────────────────────────────────\n 1 │ 1780 8532 1940 2 ⋯\n 2 │ 30091 8532 1932 8\n 3 │ 37455 8532 1913 7\n 4 │ 42383 8507 1922 2\n 5 │ 69985 8532 1956 7 ⋯\n 6 │ 72120 8507 1937 10\n 7 │ 82328 8532 1957 9\n 8 │ 95538 8507 1923 11\n 9 │ 107680 8532 1963 12 ⋯\n 10 │ 110862 8507 1911 4\n 14 columns omitted\n=#","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Show all patients born in or after 1930.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"q = FindPatients(start_year = 1930) |>\n Select(Get.person_id)\n\nDBInterface.execute(conn, q) |> DataFrame\n#=>\n6×1 DataFrame\n Row │ person_id\n │ Int64\n─────┼───────────\n 1 │ 1780\n 2 │ 30091\n 3 │ 69985\n 4 │ 72120\n 5 │ 82328\n 6 │ 107680\n=#","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Find all patients born between 1930 and 1940 and living in Illinois, and for each patient show their current age.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"q = FindPatients(start_year = 1930, end_year = 1940, states = [\"IL\"]) |>\n Select(Get.person_id, :age => 2020 .- Get.year_of_birth)\n\nDBInterface.execute(conn, q) |> DataFrame\n#=>\n1×2 DataFrame\n Row │ person_id age\n │ Int64 Int64\n─────┼──────────────────\n 1 │ 72120 83\n=#","category":"page"},{"location":"guide/#Tabular-Operations","page":"Usage Guide","title":"Tabular Operations","text":"","category":"section"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Recall the query from the previous section:","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Find all patients born between 1930 and 1940 and living in Illinois, and for each patient show their current age.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"From(:person) |>\nWhere(Fun.between(Get.year_of_birth, 1930, 1940)) |>\nJoin(From(:location) |> Where(Get.state .== \"IL\") |> As(:location),\n on = Get.location_id .== Get.location.location_id) |>\nSelect(Get.person_id, :age => 2020 .- Get.year_of_birth)","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"At the outer level, this query is constructed from tabular operations From, Where, Join, and Select arranged in a pipeline by the pipe (|>) operator. In SQL, a tabular operation takes a certain number of input datasets and produces an output dataset. It is helpful to visualize a tabular operation as a node with a certain number of input arrows and one output arrow.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"(Image: From, Where, Select, and Join nodes)","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Then the whole query can be visualized as a pipeline diagram. Each arrow in this diagram represents a dataset, and each node represents an elementary data processing operation.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"(Image: Query pipeline)","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"The following tabular operations are available in FunSQL.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Constructor Function\nAppend concatenate datasets\nAs wrap all columns in a nested record\nDefine add an output column\nFrom produce the content of a database table\nGroup partition the dataset into disjoint groups\nIterate iterate a query\nJoin correlate two datasets\nLimit truncate the dataset\nOrder sort the dataset\nPartition relate dataset rows to each other\nSelect specify output columns\nWhere filter the dataset by the given condition\nWith assign a name to a temporary dataset","category":"page"},{"location":"guide/#From,-Select,-and-Define","page":"Usage Guide","title":"From, Select, and Define","text":"","category":"section"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"The From node outputs the content of a database table. The constructor takes one argument, the name of the table.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"As opposed to SQL, FunSQL does not demand that all queries have an explicit Select. The following query will produce all columns of the table:","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Show all patients.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"using FunSQL: From\n\nq = From(:person)\n\nrender(conn, q) |> print\n#=>\nSELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"ethnicity_source_concept_id\"\nFROM \"person\" AS \"person_1\"\n=#","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"The From node also accepts a DataFrame or any argument supporting the Tables.jl interface, which is very convenient when you need to correlate database content with external data. Keep in mind that From serializes a DataFrame argument as a part of the query, so for a large DataFrame it is better to load it into the database and query it as a regular table.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"df = DataFrame(person_id = [\"SQL\", \"Julia\", \"FunSQL\"],\n year_of_birth = [1974, 2012, 2021])\n\nq = From(df)\n\nrender(conn, q) |> print\n#=>\nSELECT\n \"values_1\".\"column1\" AS \"person_id\",\n \"values_1\".\"column2\" AS \"year_of_birth\"\nFROM (\n VALUES\n ('SQL', 1974),\n ('Julia', 2012),\n ('FunSQL', 2021)\n) AS \"values_1\"\n=#","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"It is possible for a query not to have a From node:","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Show the current date and time.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"using FunSQL: Select\n\nq = Select(Fun.current_timestamp())\n\nsql = render(q)\n\nprint(sql)\n#-> SELECT CURRENT_TIMESTAMP AS \"current_timestamp\"","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"In this query, the Select node is not connected to any source of data. In such a case, it is supplied with a unit dataset containing one row and no columns. Hence this query will generate one row of output.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"The same effect could be achieved with From(nothing):","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"q = From(nothing) |>\n Select(Fun.current_timestamp())\n\nsql = render(q)\n\nprint(sql)\n#-> SELECT CURRENT_TIMESTAMP AS \"current_timestamp\"","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"The Select node is used to specify the output columns. The name of the column is either derived from the expression or set explicitly with As (or the shorthand =>).","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"For each patient, show their ID and the current age.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"q = From(:person) |>\n Select(Get.person_id,\n :age => 2020 .- Get.year_of_birth)\n\nrender(conn, q) |> print\n#=>\nSELECT\n \"person_1\".\"person_id\",\n (2020 - \"person_1\".\"year_of_birth\") AS \"age\"\nFROM \"person\" AS \"person_1\"\n=#","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"To add a new column while preserving existing output columns, we use the Define node.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Show the patient data together with their current age.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"using FunSQL: Define\n\nq = From(:person) |>\n Define(:age => 2020 .- Get.year_of_birth)\n\nrender(conn, q) |> print\n#=>\nSELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"ethnicity_source_concept_id\",\n (2020 - \"person_1\".\"year_of_birth\") AS \"age\"\nFROM \"person\" AS \"person_1\"\n=#","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Define could also be used to replace an existing column.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Hide the day of birth of patients born before 1930.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"q = From(:person) |>\n Define(:day_of_birth => Fun.case(Get.year_of_birth .>= 1930,\n Get.day_of_birth,\n missing))\n\nrender(conn, q) |> print\n#=>\nSELECT\n \"person_1\".\"person_id\",\n \"person_1\".\"gender_concept_id\",\n \"person_1\".\"year_of_birth\",\n \"person_1\".\"month_of_birth\",\n (CASE WHEN (\"person_1\".\"year_of_birth\" >= 1930) THEN \"person_1\".\"day_of_birth\" ELSE NULL END) AS \"day_of_birth\",\n ⋮\n \"person_1\".\"ethnicity_source_concept_id\"\nFROM \"person\" AS \"person_1\"\n=#","category":"page"},{"location":"guide/#Join","page":"Usage Guide","title":"Join","text":"","category":"section"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"The Join node correlates the rows of two input datasets. Predominantly, Join is used for looking up table records by key. In the following example, Join associates each person record with their location using the key column location_id that uniquely identifies a location record.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Show all patients together with their state of residence.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"From(:person) |>\nJoin(:location => From(:location),\n Get.location_id .== Get.location.location_id,\n left = true) |>\nSelect(Get.person_id, Get.location.state)","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"The modifier left = true tells Join that it must output all person records including those without the corresponding location. Since this is a very common requirement, FunSQL provides an alias:","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"using FunSQL: LeftJoin\n\nFrom(:person) |>\nLeftJoin(:location => From(:location),\n Get.location_id .== Get.location.location_id) |>\nSelect(Get.person_id, Get.location.state)","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Since Join needs two input datasets, it must be attached to two input pipelines. The first pipeline is attached using the |> operator and the second one is provided as an argument to the Join constructor. Alternatively, both input pipelines can be specified as keyword arguments:","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Join(over = From(:person),\n joinee = :location => From(:location),\n on = Get.location_id .== Get.location.location_id,\n left = true) |>\nSelect(Get.person_id, Get.location.state)","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"The output of Join combines columns of both input datasets, which will cause ambiguity if both datasets have a column with the same name. Such is the case in the previous example since both tables, person and location, have a column called location_id. To disambiguate them, we can place all columns of one of the datasets into a nested record. This is the action of the arrow (=>) operator or its full form, the As node:","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"using FunSQL: As\n\nFrom(:person) |>\nLeftJoin(From(:location) |> As(:location),\n on = Get.location_id .== Get.location.location_id) |>\nSelect(Get.person_id, Get.location.state)","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Alternatively, we could use bound column references, which are described in a later section.","category":"page"},{"location":"guide/#Scalar-Operations","page":"Usage Guide","title":"Scalar Operations","text":"","category":"section"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Many tabular operations including Join, Select and Where are parameterized with scalar operations. A scalar operation acts on an individual row of a dataset and produces a scalar value. Scalar operations are assembled from literal values, column references, and applications of SQL functions and operators. Below is a list of scalar operations available in FunSQL.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Constructor Function\nAgg apply an aggregate function\nAs assign a column alias\nBind create a correlated subquery\nFun apply a scalar function or a scalar operator\nGet produce the value of a column\nLit produce a constant value\nSort indicate the sort order\nVar produce the value of a query parameter","category":"page"},{"location":"guide/#Lit:-SQL-Literals","page":"Usage Guide","title":"Lit: SQL Literals","text":"","category":"section"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"The Lit node creates a literal value, although we could usually omit the constructor:","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"using FunSQL: Lit\n\nSelect(Lit(42))\nSelect(42)","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"The SQL value NULL is represented by the Julia constant missing:","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"q = Select(missing)\n\nrender(conn, q) |> print\n#-> SELECT NULL AS \"_\"","category":"page"},{"location":"guide/#Get:-Column-References","page":"Usage Guide","title":"Get: Column References","text":"","category":"section"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"The Get node creates a column reference. The Get constructor admits several equivalent forms:","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Get.year_of_birth\nGet(:year_of_birth)\nGet.\"year_of_birth\"\nGet(\"year_of_birth\")","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Such column references are resolved at the place of use against the input dataset. As we mentioned earlier, sometimes column references cannot be resolved unambiguously. To alleviate this problem, we can bind the column reference to the node that produces it:","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Show all patients with their state of residence.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"qₚ = From(:person)\nqₗ = From(:location)\nq = qₚ |>\n LeftJoin(qₗ, on = qₚ.location_id .== qₗ.location_id) |>\n Select(qₚ.person_id, qₗ.state)","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"The notation qₚ.location_id and qₗ.location_id is a syntax sugar for","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Get(:location_id, over = qₚ)\nGet(:location_id, over = qₗ)","category":"page"},{"location":"guide/#Fun:-SQL-Functions-and-Operators","page":"Usage Guide","title":"Fun: SQL Functions and Operators","text":"","category":"section"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"SQL functions and operators are represented using the Fun node, which also has several equivalent forms:","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Fun.between(Get.year_of_birth, 1930, 1940)\nFun(:between, Get.year_of_birth, 1930, 1940)\nFun.\"between\"(Get.year_of_birth, 1930, 1940)\nFun(\"between\", Get.year_of_birth, 1930, 1940)","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Certain SQL operators, notably logical and comparison operators, can be represented using Julia broadcasting notation:","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Fun.\">=\"(Get.year_of_birth, 1930)\nGet.year_of_birth .>= 1930\n\nFun.and(Fun.\"=\"(Get.city, \"CHICAGO\"), Fun.\"=\"(Get.state, \"IL\"))\n#? VERSION >= v\"1.7\"\nGet.city .== \"CHICAGO\" .&& Get.state .== \"IL\"","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"We should note that FunSQL does not verify if a SQL function or an operator is used correctly or even whether it exists or not. In such a case, FunSQL will generate a SQL query that fails to execute:","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"q = From(:person) |>\n Select(Fun.frobnicate(Get.year_of_birth))\n\nrender(conn, q) |> print\n#=>\nSELECT frobnicate(\"person_1\".\"year_of_birth\") AS \"frobnicate\"\nFROM \"person\" AS \"person_1\"\n=#\n\nDBInterface.execute(conn, q)\n#-> ERROR: SQLite.SQLiteException(\"no such function: frobnicate\")","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"On the other hand, FunSQL will correctly serialize many SQL functions and operators that have irregular syntax including AND, OR, NOT, IN, EXISTS, CASE, and others.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Show the demographic cohort of each patient.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"q = From(:person) |>\n Select(Fun.case(Get.year_of_birth .<= 1960, \"boomer\", \"millenial\"))\n\nrender(conn, q) |> print\n#=>\nSELECT (CASE WHEN (\"person_1\".\"year_of_birth\" <= 1960) THEN 'boomer' ELSE 'millenial' END) AS \"case\"\nFROM \"person\" AS \"person_1\"\n=#","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Since FunSQL recognizes || as a logical or operator, it conflicts with those SQL dialects that use || for string concatenation. Other SQL dialects use function concat for this purpose. In FunSQL, always use Fun.concat, which will pick correct serialization depending on the target SQL dialect.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Show city, state.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"q = From(:location) |>\n Select(Fun.concat(Get.city, \", \", Get.state))\n\nrender(conn, q) |> print\n#=>\nSELECT (\"location_1\".\"city\" || ', ' || \"location_1\".\"state\") AS \"concat\"\nFROM \"location\" AS \"location_1\"\n=#","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"When the name of the Fun node contains one or more ? symbols, this name serves as a template of a SQL expression. When the node is rendered, the ? symbols are substituted with the node arguments.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"q = From(:person) |>\n Select(Fun.\"CAST(? AS TEXT)\"(Get.year_of_birth))\n\nrender(conn, q) |> print\n#=>\nSELECT CAST(\"person_1\".\"year_of_birth\" AS TEXT) AS \"_\"\nFROM \"person\" AS \"person_1\"\n=#","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"To decide how to render a Fun node, FunSQL checks the node name:","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"If the name has a specialized implementation of FunSQL.serialize!(), this implementation is used for rendering the node.\nIf the name contains one or more placeholders (?), the node is rendered as a template.\nIf the name contains only symbol characters, or if the name starts or ends with a space, the node is rendered as an operator.\nOtherwise, the node is rendered as a function.","category":"page"},{"location":"guide/#Group-and-Aggregate-Functions","page":"Usage Guide","title":"Group and Aggregate Functions","text":"","category":"section"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Group and aggregate functions are used for summarizing data to report totals, averages and so on. We start by applying the Group node to partition the input rows into disjoint groups. Then, for each group, we can calculate summary values using aggregate functions. In FunSQL, aggregate functions are created using the Agg node. In the following example, we use the aggregate function Agg.count, which simply counts the number of rows in each group.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Show the number of patients by the year of birth.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"using FunSQL: Agg, Group\n\nq = From(:person) |>\n Group(Get.year_of_birth) |>\n Select(Get.year_of_birth, Agg.count())\n\nrender(conn, q) |> print\n#=>\nSELECT\n \"person_1\".\"year_of_birth\",\n count(*) AS \"count\"\nFROM \"person\" AS \"person_1\"\nGROUP BY \"person_1\".\"year_of_birth\"\n=#\n\nDBInterface.execute(conn, q) |> DataFrame\n#=>\n10×2 DataFrame\n Row │ year_of_birth count\n │ Int64 Int64\n─────┼──────────────────────\n 1 │ 1911 1\n 2 │ 1913 1\n 3 │ 1922 1\n⋮\n=#","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"To indicate that aggregate functions must be applied to the dataset as a whole, we create a Group node without arguments. This is the case where FunSQL notation deviates from SQL, where we would omit the GROUP BY clause to achieve the same effect.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Show the average year of birth.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"q = From(:person) |>\n Group() |>\n Select(Agg.avg(Get.year_of_birth))\n\nrender(conn, q) |> print\n#=>\nSELECT avg(\"person_1\".\"year_of_birth\") AS \"avg\"\nFROM \"person\" AS \"person_1\"\n=#\n\nDBInterface.execute(conn, q) |> DataFrame\n#=>\n1×1 DataFrame\n Row │ avg\n │ Float64\n─────┼─────────\n 1 │ 1935.4\n=#","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"In general, the arguments of the Group node form the grouping key so that two rows of the input dataset belongs to the same group when they have the same value of the grouping key. The output of Group contains all distinct values of the grouping key.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Show the US states that are present in the location records.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"q = From(:location) |>\n Group(Get.state)\n\nrender(conn, q) |> print\n#=>\nSELECT DISTINCT \"location_1\".\"state\"\nFROM \"location\" AS \"location_1\"\n=#\n\nDBInterface.execute(conn, q) |> DataFrame\n#=>\n10×1 DataFrame\n Row │ state\n │ String\n─────┼────────\n 1 │ MI\n 2 │ WA\n 3 │ FL\n⋮\n=#","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"FunSQL has no lexical limitations on the use of aggregate functions. While in SQL aggregate functions can only be used in the SELECT or HAVING clauses, there is no such restriction in FunSQL: they could be used in any context where an ordinary expression is permitted. The only requirement is that for each aggregate function, FunSQL can determine the corresponding Group node. It is convenient to imagine that the output of Group contains the grouped rows, which cannot be observed directly, but whose presence in the output allows us to apply aggregate functions.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"In particular, we use a regular Where node where SQL would require a HAVING clause.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Show patients who saw a doctor within the last year.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"q = From(:visit_occurrence) |>\n Group(Get.person_id) |>\n Where(Agg.max(Get.visit_end_date) .>= Fun.date(\"now\", \"-1 year\"))\n\nrender(conn, q) |> print\n#=>\nSELECT \"visit_occurrence_1\".\"person_id\"\nFROM \"visit_occurrence\" AS \"visit_occurrence_1\"\nGROUP BY \"visit_occurrence_1\".\"person_id\"\nHAVING (max(\"visit_occurrence_1\".\"visit_end_date\") >= date('now', '-1 year'))\n=#","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"When the output of Group is blocked by an As node, we need to traverse it with Get in order to use an aggregate function.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"For each patient, show the date of their latest visit to a doctor.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"q = From(:person) |>\n LeftJoin(:visit_group => From(:visit_occurrence) |> Group(Get.person_id),\n on = Get.person_id .== Get.visit_group.person_id) |>\n Select(Get.person_id,\n Get.visit_group |> Agg.max(Get.visit_start_date))\n\nrender(conn, q) |> print\n#=>\nSELECT\n \"person_1\".\"person_id\",\n \"visit_group_1\".\"max\"\nFROM \"person\" AS \"person_1\"\nLEFT JOIN (\n SELECT\n max(\"visit_occurrence_1\".\"visit_start_date\") AS \"max\",\n \"visit_occurrence_1\".\"person_id\"\n FROM \"visit_occurrence\" AS \"visit_occurrence_1\"\n GROUP BY \"visit_occurrence_1\".\"person_id\"\n) AS \"visit_group_1\" ON (\"person_1\".\"person_id\" = \"visit_group_1\".\"person_id\")\n=#","category":"page"},{"location":"guide/#Partition-and-Window-Functions","page":"Usage Guide","title":"Partition and Window Functions","text":"","category":"section"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"We can relate each row to other rows in the same dataset using the Partition node and window functions. We start by applying the Partition node to partition the input rows into disjoint groups. The rows in each group are reordered according to the given sort order. Unlike Group, which collapses each row group into a single row, the Partition node preserves the original rows, but allows us to relate each row to adjacent rows in the same partition. In particular, we can apply regular aggregate functions, which calculate the summary value of a subset of rows related to the current row.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"In the following example, the rows visit_occurrence are partitioned per patient and ordered by the starting date of the visit. The frame clause specifies the subset of rows relative to the current row (the window frame) to be used by aggregate functions. In this example, the frame contains all rows prior to the current row.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"For each visit, show the time passed since the previous visit.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"using FunSQL: Partition\n\nq = From(:visit_occurrence) |>\n Partition(Get.person_id,\n order_by = [Get.visit_start_date],\n frame = (mode = :rows, start = -Inf, finish = -1)) |>\n Select(Get.person_id,\n Get.visit_start_date,\n Get.visit_end_date,\n :gap => Fun.julianday(Get.visit_start_date) .- Fun.julianday(Agg.max(Get.visit_end_date)))\n\nrender(conn, q) |> print\n#=>\nSELECT\n \"visit_occurrence_1\".\"person_id\",\n \"visit_occurrence_1\".\"visit_start_date\",\n \"visit_occurrence_1\".\"visit_end_date\",\n (julianday(\"visit_occurrence_1\".\"visit_start_date\") - julianday((max(\"visit_occurrence_1\".\"visit_end_date\") OVER (PARTITION BY \"visit_occurrence_1\".\"person_id\" ORDER BY \"visit_occurrence_1\".\"visit_start_date\" ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING)))) AS \"gap\"\nFROM \"visit_occurrence\" AS \"visit_occurrence_1\"\n=#\n\nDBInterface.execute(conn, q) |> DataFrame\n#=>\n27×4 DataFrame\n Row │ person_id visit_start_date visit_end_date gap\n │ Int64 String String Float64?\n─────┼────────────────────────────────────────────────────────\n 1 │ 1780 2008-04-09 2008-04-13 missing\n 2 │ 1780 2008-04-10 2008-04-10 -3.0\n 3 │ 1780 2008-11-22 2008-11-22 223.0\n 4 │ 1780 2009-05-22 2009-05-22 181.0\n⋮\n=#","category":"page"},{"location":"guide/#Query-Parameters","page":"Usage Guide","title":"Query Parameters","text":"","category":"section"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"A SQL query may include a reference to a query parameter. When we execute such a query, we must supply the actual values for all parameters used in the query. This is a restricted form of dynamic query construction directly supported by SQL syntax.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Show all patients born between $start_year and $end_year.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"sql = \"\"\"\nSELECT p.person_id\nFROM person p\nWHERE p.year_of_birth BETWEEN ? AND ?\n\"\"\"\n\nDBInterface.execute(conn, sql, (1930, 1940)) |> DataFrame\n#=>\n3×1 DataFrame\n Row │ person_id\n │ Int64\n─────┼───────────\n 1 │ 1780\n 2 │ 30091\n 3 │ 72120\n=#","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"FunSQL can be used to construct a query with parameters. Similar to Get, parameter references are created using the Var node:","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"using FunSQL: Var\n\nq = From(:person) |>\n Where(Fun.between(Get.year_of_birth, Var.START_YEAR, Var.END_YEAR)) |>\n Select(Get.person_id)\n\nrender(conn, q) |> print\n#=>\nSELECT \"person_1\".\"person_id\"\nFROM \"person\" AS \"person_1\"\nWHERE (\"person_1\".\"year_of_birth\" BETWEEN ?1 AND ?2)\n=#","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"While we specified parameters by name, in the generated SQL query the same parameters are numbered. FunSQL will automatically pack named parameters in the order in which they appear in the SQL query.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"DBInterface.execute(conn, q, START_YEAR = 1930, END_YEAR = 1940) |> DataFrame\n#=>\n3×1 DataFrame\n Row │ person_id\n │ Int64\n─────┼───────────\n 1 │ 1780\n 2 │ 30091\n 3 │ 72120\n=#","category":"page"},{"location":"guide/#Correlated-Queries","page":"Usage Guide","title":"Correlated Queries","text":"","category":"section"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"An inner query is a SQL query that is included into the outer query as a part of a scalar expression. An inner query must either produce a single value or be used as an argument of a query operator, such as IN or EXISTS, which transforms the query output to a scalar value.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"It is easy to assemble an inner query with FunSQL:","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Find the oldest patients.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"qᵢ = From(:person) |>\n Group() |>\n Select(Agg.min(Get.year_of_birth))\n\nqₒ = From(:person) |>\n Where(Get.year_of_birth .== qᵢ) |>\n Select(Get.person_id, Get.year_of_birth)\n\nrender(conn, qₒ) |> print\n#=>\nSELECT\n \"person_1\".\"person_id\",\n \"person_1\".\"year_of_birth\"\nFROM \"person\" AS \"person_1\"\nWHERE (\"person_1\".\"year_of_birth\" = (\n SELECT min(\"person_2\".\"year_of_birth\") AS \"min\"\n FROM \"person\" AS \"person_2\"\n))\n=#\n\nDBInterface.execute(conn, qₒ) |> DataFrame\n#=>\n1×2 DataFrame\n Row │ person_id year_of_birth\n │ Int64 Int64\n─────┼──────────────────────────\n 1 │ 110862 1911\n=#","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Find patients with no visits to a healthcare provider.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"qᵢ = From(:visit_occurrence) |>\n Select(Get.person_id)\n\nqₒ = From(:person) |>\n Where(Fun.not_in(Get.person_id, qᵢ))\n\nrender(conn, qₒ) |> print\n#=>\nSELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"ethnicity_source_concept_id\"\nFROM \"person\" AS \"person_1\"\nWHERE (\"person_1\".\"person_id\" NOT IN (\n SELECT \"visit_occurrence_1\".\"person_id\"\n FROM \"visit_occurrence\" AS \"visit_occurrence_1\"\n))\n=#\n\nDBInterface.execute(conn, qₒ) |> DataFrame\n#=>\n0×18 DataFrame\n Row │ person_id gender_concept_id year_of_birth month_of_birth day_of_bir ⋯\n │ Int64? Int64? Int64? Int64? Int64? ⋯\n─────┴──────────────────────────────────────────────────────────────────────────\n 14 columns omitted\n=#","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"The inner query may depend on the data from the outer query. Such inner queries are called correlated. In FunSQL, correlated queries are created using the Bind node. Specifically, in the body of a correlated query we use query parameters to refer to the external data. The Bind node, which wrap the correlated query, binds each parameter to an expression evaluated in the context of the outer query.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Find all visits where at least one condition was diagnosed.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"using FunSQL: Bind\n\nCorrelatedCondition(person_id, start_date, end_date) =\n From(:condition_occurrence) |>\n Where(Fun.and(Get.person_id .== Var.PERSON_ID,\n Fun.between(Get.condition_start_date, Var.START_DATE, Var.END_DATE))) |>\n Bind(:PERSON_ID => person_id,\n :START_DATE => start_date,\n :END_DATE => end_date)\n\nq = From(:visit_occurrence) |>\n Where(Fun.exists(CorrelatedCondition(Get.person_id, Get.visit_start_date, Get.visit_end_date)))\n\nrender(conn, q) |> print\n#=>\nSELECT\n \"visit_occurrence_1\".\"visit_occurrence_id\",\n ⋮\n \"visit_occurrence_1\".\"visit_source_concept_id\"\nFROM \"visit_occurrence\" AS \"visit_occurrence_1\"\nWHERE (EXISTS (\n SELECT NULL AS \"_\"\n FROM \"condition_occurrence\" AS \"condition_occurrence_1\"\n WHERE\n (\"condition_occurrence_1\".\"person_id\" = \"visit_occurrence_1\".\"person_id\") AND\n (\"condition_occurrence_1\".\"condition_start_date\" BETWEEN \"visit_occurrence_1\".\"visit_start_date\" AND \"visit_occurrence_1\".\"visit_end_date\")\n))\n=#","category":"page"},{"location":"guide/#Order-and-Limit","page":"Usage Guide","title":"Order and Limit","text":"","category":"section"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"The database server emits the output rows in an arbitrary order. In fact, different runs of the same query may produce rows in a different order. To specify a particular order of output rows, we use the Order node.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Show patients ordered by the year of birth.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"using FunSQL: Order\n\nq = From(:person) |>\n Order(Get.year_of_birth)\n\nrender(conn, q) |> print\n#=>\nSELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"ethnicity_source_concept_id\"\nFROM \"person\" AS \"person_1\"\nORDER BY \"person_1\".\"year_of_birth\"\n=#","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"The Asc and the Desc modifiers specify whether to sort the rows in an ascending or in a descending order.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Show patients ordered by the year of birth in the descending order.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"using FunSQL: Desc\n\nq = From(:person) |>\n Order(Get.year_of_birth |> Desc())\n\nrender(conn, q) |> print\n#=>\nSELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"ethnicity_source_concept_id\"\nFROM \"person\" AS \"person_1\"\nORDER BY \"person_1\".\"year_of_birth\" DESC\n=#","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"The Limit node lets us take a slice of the input dataset. To make the output deterministic, Limit must be applied right after Order.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Show the top three oldest patients.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"using FunSQL: Limit\n\nq = From(:person) |>\n Order(Get.year_of_birth) |>\n Limit(1:3)\n\nrender(conn, q) |> print\n#=>\nSELECT\n \"person_1\".\"person_id\",\n ⋮\n \"person_1\".\"ethnicity_source_concept_id\"\nFROM \"person\" AS \"person_1\"\nORDER BY \"person_1\".\"year_of_birth\"\nLIMIT 3\nOFFSET 0\n=#","category":"page"},{"location":"guide/#Append-and-Iterate","page":"Usage Guide","title":"Append and Iterate","text":"","category":"section"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"The Append node concatenates two or more input datasets. Only the columns that are present in every input dataset will be included to the output.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Show all clinical events (visits and conditions) associated with each patient.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"using FunSQL: Append\n\nq = From(:visit_occurrence) |>\n Define(:type => \"visit\", :date => Get.visit_start_date) |>\n Append(From(:condition_occurrence) |>\n Define(:type => \"condition\", :date => Get.condition_start_date)) |>\n Order(Get.person_id, Get.date)\n\nrender(conn, q) |> print\n#=>\nSELECT\n \"union_1\".\"visit_occurrence_id\",\n \"union_1\".\"person_id\",\n \"union_1\".\"provider_id\",\n \"union_1\".\"type\",\n \"union_1\".\"date\"\nFROM (\n SELECT\n \"visit_occurrence_1\".\"visit_occurrence_id\",\n \"visit_occurrence_1\".\"person_id\",\n \"visit_occurrence_1\".\"provider_id\",\n 'visit' AS \"type\",\n \"visit_occurrence_1\".\"visit_start_date\" AS \"date\"\n FROM \"visit_occurrence\" AS \"visit_occurrence_1\"\n UNION ALL\n SELECT\n \"condition_occurrence_1\".\"visit_occurrence_id\",\n \"condition_occurrence_1\".\"person_id\",\n \"condition_occurrence_1\".\"provider_id\",\n 'condition' AS \"type\",\n \"condition_occurrence_1\".\"condition_start_date\" AS \"date\"\n FROM \"condition_occurrence\" AS \"condition_occurrence_1\"\n) AS \"union_1\"\nORDER BY\n \"union_1\".\"person_id\",\n \"union_1\".\"date\"\n=#\n\nDBInterface.execute(conn, q) |> DataFrame\n#=>\n53×5 DataFrame\n Row │ visit_occurrence_id person_id provider_id type date\n │ Int64 Int64 Int64 String String\n─────┼────────────────────────────────────────────────────────────────────\n 1 │ 88179 1780 5247 visit 2008-04-09\n 2 │ 88246 1780 61112 visit 2008-04-10\n 3 │ 88246 1780 61112 condition 2008-04-10\n 4 │ 88214 1780 12674 visit 2008-11-22\n 5 │ 88214 1780 12674 condition 2008-11-22\n 6 │ 88263 1780 61118 visit 2009-05-22\n 7 │ 88263 1780 61118 condition 2009-05-22\n 8 │ 1454922 30091 36303 visit 2008-11-12\n ⋮ │ ⋮ ⋮ ⋮ ⋮ ⋮\n 47 │ 5314671 110862 5159 condition 2008-09-07\n 48 │ 5314690 110862 31906 visit 2009-06-30\n 49 │ 5314690 110862 31906 condition 2009-06-30\n 50 │ 5314664 110862 31857 visit 2009-09-30\n 51 │ 5314664 110862 31857 condition 2009-09-30\n 52 │ 5314696 110862 192777 visit 2010-06-07\n 53 │ 5314696 110862 192777 condition 2010-06-07\n 38 rows omitted\n=#","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"For a second example, consider the table concept, which contains the vocabulary of medical concepts (such as Myocardial Infarction). These concepts may be related to each other (Myocardial Infarction has a subtype Acute Myocardial Infarction), and their relationships are stored in the table concept_relationship. We can encapsulate construction of a query that finds immediate subtypes as the function:","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"SubtypesOf(base) =\n From(:concept) |>\n Join(From(:concept_relationship) |>\n Where(Get.relationship_id .== \"Is a\"),\n on = Get.concept_id .== Get.concept_id_1) |>\n Join(:base => base,\n on = Get.concept_id_2 .== Get.base.concept_id)","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Show the concept \"Myocardial Infarction\" and its immediate subtypes.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"base = From(:concept) |>\n Where(Get.concept_name .== \"Myocardial infarction\")\n\nq = base |> Append(SubtypesOf(base))\n\nDBInterface.execute(conn, q) |> DataFrame\n#=>\n2×10 DataFrame\n Row │ concept_id concept_name domain_id vocabulary_id conc ⋯\n │ Int64 String String String Stri ⋯\n─────┼──────────────────────────────────────────────────────────────────────────\n 1 │ 4329847 Myocardial infarction Condition SNOMED Clin ⋯\n 2 │ 312327 Acute myocardial infarction Condition SNOMED Clin\n 6 columns omitted\n=#","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"But how can we fetch not just immediate, but all of the subtypes of a concept?","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"Show the concept \"Myocardial Infarction\" and all of its subtypes.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"A good start is to repeatedly apply SubtypesOf and concatenate all the outputs:","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"base |>\nAppend(SubtypesOf(base),\n SubtypesOf(SubtypesOf(base)),\n SubtypesOf(SubtypesOf(SubtypesOf(base))),\n SubtypesOf(SubtypesOf(SubtypesOf(SubtypesOf(base)))))","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"However we do not know if 4 iterations of SubtypesOf is enough to fully traverse the concept hierarchy. Ideally, we should continue applying SubtypesOf until the last iteration produces an empty output. This is exactly the action of the Iterate node.","category":"page"},{"location":"guide/","page":"Usage Guide","title":"Usage Guide","text":"using FunSQL: Iterate\n\nq = base |>\n Iterate(SubtypesOf(From(^)))\n\nrender(conn, q) |> print\n#=>\nWITH RECURSIVE \"__1\" (\"concept_id\", \"concept_name\", \"domain_id\", \"vocabulary_id\", \"concept_class_id\", \"standard_concept\", \"concept_code\", \"valid_start_date\", \"valid_end_date\", \"invalid_reason\") AS (\n SELECT\n \"concept_1\".\"concept_id\",\n \"concept_1\".\"concept_name\",\n \"concept_1\".\"domain_id\",\n \"concept_1\".\"vocabulary_id\",\n \"concept_1\".\"concept_class_id\",\n \"concept_1\".\"standard_concept\",\n \"concept_1\".\"concept_code\",\n \"concept_1\".\"valid_start_date\",\n \"concept_1\".\"valid_end_date\",\n \"concept_1\".\"invalid_reason\"\n FROM \"concept\" AS \"concept_1\"\n WHERE (\"concept_1\".\"concept_name\" = 'Myocardial infarction')\n UNION ALL\n SELECT\n \"concept_2\".\"concept_id\",\n \"concept_2\".\"concept_name\",\n \"concept_2\".\"domain_id\",\n \"concept_2\".\"vocabulary_id\",\n \"concept_2\".\"concept_class_id\",\n \"concept_2\".\"standard_concept\",\n \"concept_2\".\"concept_code\",\n \"concept_relationship_2\".\"valid_start_date\",\n \"concept_relationship_2\".\"valid_end_date\",\n \"concept_relationship_2\".\"invalid_reason\"\n FROM \"concept\" AS \"concept_2\"\n JOIN (\n SELECT\n \"concept_relationship_1\".\"valid_start_date\",\n \"concept_relationship_1\".\"valid_end_date\",\n \"concept_relationship_1\".\"invalid_reason\",\n \"concept_relationship_1\".\"concept_id_2\",\n \"concept_relationship_1\".\"concept_id_1\"\n FROM \"concept_relationship\" AS \"concept_relationship_1\"\n WHERE (\"concept_relationship_1\".\"relationship_id\" = 'Is a')\n ) AS \"concept_relationship_2\" ON (\"concept_2\".\"concept_id\" = \"concept_relationship_2\".\"concept_id_1\")\n JOIN \"__1\" AS \"__2\" ON (\"concept_relationship_2\".\"concept_id_2\" = \"__2\".\"concept_id\")\n)\nSELECT\n \"concept_3\".\"concept_id\",\n \"concept_3\".\"concept_name\",\n \"concept_3\".\"domain_id\",\n \"concept_3\".\"vocabulary_id\",\n \"concept_3\".\"concept_class_id\",\n \"concept_3\".\"standard_concept\",\n \"concept_3\".\"concept_code\",\n \"concept_3\".\"valid_start_date\",\n \"concept_3\".\"valid_end_date\",\n \"concept_3\".\"invalid_reason\"\nFROM \"__1\" AS \"concept_3\"\n=#\n\nDBInterface.execute(conn, q) |> DataFrame\n#=>\n6×10 DataFrame\n Row │ concept_id concept_name domain_id vocabulary_id ⋯\n │ Int64 String String String ⋯\n─────┼──────────────────────────────────────────────────────────────────────────\n 1 │ 4329847 Myocardial infarction Condition SNOMED ⋯\n 2 │ 312327 Acute myocardial infarction Condition SNOMED\n 3 │ 434376 Acute myocardial infarction of a… Condition SNOMED\n 4 │ 438170 Acute myocardial infarction of i… Condition SNOMED\n 5 │ 438438 Acute myocardial infarction of a… Condition SNOMED ⋯\n 6 │ 444406 Acute subendocardial infarction Condition SNOMED\n 6 columns omitted\n=#","category":"page"},{"location":"#FunSQL.jl","page":"Home","title":"FunSQL.jl","text":"","category":"section"},{"location":"","page":"Home","title":"Home","text":"FunSQL is a Julia library for compositional construction of SQL queries.","category":"page"},{"location":"#Table-of-Contents","page":"Home","title":"Table of Contents","text":"","category":"section"},{"location":"","page":"Home","title":"Home","text":"Pages = [\n \"guide/index.md\",\n \"reference/index.md\",\n \"examples/index.md\",\n \"test/index.md\",\n \"two-kinds-of-sql-query-builders/index.md\",\n]","category":"page"},{"location":"test/clauses/#SQL-Clauses","page":"SQL Clauses","title":"SQL Clauses","text":"","category":"section"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"using FunSQL:\n AGG, AS, ASC, DESC, FROM, FUN, GROUP, HAVING, ID, JOIN, LIMIT, LIT,\n NOTE, ORDER, PARTITION, SELECT, SORT, UNION, VALUES, VAR, WHERE,\n WINDOW, WITH, pack, render","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"The syntactic structure of a SQL query is represented as a tree of SQLClause objects. Different types of clauses are created by specialized constructors and connected using the chain (|>) operator.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FROM(:person) |>\n SELECT(:person_id, :year_of_birth)\n#-> (…) |> SELECT(…)","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"Displaying a SQLClause object shows how it was constructed.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"display(c)\n#-> ID(:person) |> FROM() |> SELECT(ID(:person_id), ID(:year_of_birth))","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"A SQLClause object wraps a concrete clause object, which can be accessed using the indexing operator.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c[]\n#-> ((…) |> SELECT(…))[]\n\ndisplay(c[])\n#-> (ID(:person) |> FROM() |> SELECT(ID(:person_id), ID(:year_of_birth)))[]","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"To generate SQL, we use function render().","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"print(render(c))\n#=>\nSELECT\n \"person_id\",\n \"year_of_birth\"\nFROM \"person\"\n=#","category":"page"},{"location":"test/clauses/#SQL-Literals","page":"SQL Clauses","title":"SQL Literals","text":"","category":"section"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"A SQL literal is created using a LIT() constructor.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = LIT(\"SQL is fun!\")\n#-> LIT(\"SQL is fun!\")","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"Values of certain Julia data types are automatically converted to SQL literals when they are used in the context of a SQL clause.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"using Dates\n\nc = SELECT(missing, true, 42, \"SQL is fun!\", Date(2000))\n\ndisplay(c)\n#=>\nSELECT(LIT(missing),\n LIT(true),\n LIT(42),\n LIT(\"SQL is fun!\"),\n LIT(Dates.Date(\"2000-01-01\")))\n=#\n\nprint(render(c))\n#=>\nSELECT\n NULL,\n TRUE,\n 42,\n 'SQL is fun!',\n '2000-01-01'\n=#","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"Some values may render differently depending on the dialect.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = LIT(false)\n\nprint(render(c, dialect = :sqlserver))\n#-> (1 = 0)","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"A quote character in a string literal is represented by a pair of quotes.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = LIT(\"O'Hare\")\n\nprint(render(c))\n#-> 'O''Hare'","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"Some dialects use backslash to escape quote characters.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"print(render(c, dialect = :spark))\n#-> 'O\\'Hare'","category":"page"},{"location":"test/clauses/#SQL-Identifiers","page":"SQL Clauses","title":"SQL Identifiers","text":"","category":"section"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"A SQL identifier is created with ID() constructor.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = ID(:person)\n#-> ID(:person)\n\ndisplay(c)\n#-> ID(:person)\n\nprint(render(c))\n#-> \"person\"","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"Serialization of an identifier depends on the SQL dialect.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"print(render(c, dialect = :sqlserver))\n#-> [person]","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"A quote character in an identifier is properly escaped.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = ID(\"year of \\\"birth\\\"\")\n\nprint(render(c))\n#-> \"year of \"\"birth\"\"\"","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"A qualified identifier is created using the chain operator.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = ID(:person) |> ID(:year_of_birth)\n#-> (…) |> ID(:year_of_birth)\n\ndisplay(c)\n#-> ID(:person) |> ID(:year_of_birth)\n\nprint(render(c))\n#-> \"person\".\"year_of_birth\"","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"Symbols and pairs of symbols are automatically converted to SQL identifiers when they are used in the context of a SQL clause.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FROM(:p => :person) |> SELECT((:p, :person_id))\ndisplay(c)\n#-> ID(:person) |> AS(:p) |> FROM() |> SELECT(ID(:p) |> ID(:person_id))\n\nprint(render(c))\n#=>\nSELECT \"p\".\"person_id\"\nFROM \"person\" AS \"p\"\n=#","category":"page"},{"location":"test/clauses/#SQL-Variables","page":"SQL Clauses","title":"SQL Variables","text":"","category":"section"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"Placeholder parameters to a SQL query are created with VAR() constructor.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = VAR(:YEAR)\n#-> VAR(:YEAR)\n\ndisplay(c)\n#-> VAR(:YEAR)\n\nprint(render(c))\n#-> :YEAR","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"Rendering of a SQL parameter depends on the chosen dialect.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"print(render(c, dialect = :sqlite))\n#-> ?1\n\nprint(render(c, dialect = :postgresql))\n#-> $1\n\nprint(render(c, dialect = :mysql))\n#-> ?","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"Function pack() converts named parameters to a positional form.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FROM(:person) |>\n WHERE(FUN(:or, FUN(\"=\", :gender_concept_id, VAR(:GENDER)),\n FUN(\"=\", :gender_source_concept_id, VAR(:GENDER)))) |>\n SELECT(:person_id)\n\nsql = render(c, dialect = :sqlite)\n\nprint(sql)\n#=>\nSELECT \"person_id\"\nFROM \"person\"\nWHERE\n (\"gender_concept_id\" = ?1) OR\n (\"gender_source_concept_id\" = ?1)\n=#\n\npack(sql, (GENDER = 8532,))\n#-> Any[8532]\n\npack(sql, Dict(:GENDER => 8532))\n#-> Any[8532]\n\npack(sql, Dict(\"GENDER\" => 8532))\n#-> Any[8532]","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"If the dialect does not support numbered parameters, pack() may need to duplicate parameter values.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"sql = render(c, dialect = :mysql)\n\nprint(sql)\n#=>\nSELECT `person_id`\nFROM `person`\nWHERE\n (`gender_concept_id` = ?) OR\n (`gender_source_concept_id` = ?)\n=#\n\npack(sql, (GENDER = 8532,))\n#-> Any[8532, 8532]","category":"page"},{"location":"test/clauses/#SQL-Functions-and-Operators","page":"SQL Clauses","title":"SQL Functions and Operators","text":"","category":"section"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"An application of a SQL function is created with FUN() constructor.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FUN(:concat, :city, \", \", :state)\n#-> FUN(\"concat\", …)\n\ndisplay(c)\n#-> FUN(\"concat\", ID(:city), LIT(\", \"), ID(:state))\n\nprint(render(c))\n#-> concat(\"city\", ', ', \"state\")\n\nc = FUN(:now)\n#-> FUN(\"now\")\n\nprint(render(c))\n#-> now()","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"FUN() with an empty name generates a comma-separated list of values.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FUN(\"\", \"60614\", \"60615\")\n\nprint(render(c))\n#-> ('60614', '60615')","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"A name that contains only symbol characters is considered an operator.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FUN(\"||\", :city, \", \", :state)\n\nprint(render(c))\n#-> (\"city\" || ', ' || \"state\")","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"To create an operator containing alphabetical characters, add a leading or a trailing space to its name.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FUN(\" IS DISTINCT FROM \", :zip, missing)\n\nprint(render(c))\n#-> (\"zip\" IS DISTINCT FROM NULL)\n\nc = FUN(\" IS DISTINCT FROM\", :zip, missing)\n\nprint(render(c))\n#-> (\"zip\" IS DISTINCT FROM NULL)\n\nc = FUN(\" COLLATE \\\"C\\\"\", :zip)\n\nprint(render(c))\n#-> (\"zip\" COLLATE \"C\")\n\nc = FUN(\"DATE \", \"2000-01-01\")\n\nprint(render(c))\n#-> (DATE '2000-01-01')\n\nc = FUN(\"CURRENT_TIME \")\n\nprint(render(c))\n#-> CURRENT_TIME\n\nc = FUN(\" CURRENT_TIME\")\n\nprint(render(c))\n#-> CURRENT_TIME","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"To create a SQL expression with irregular syntax, supply FUN() with a template string.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FUN(\"SUBSTRING(? FROM ? FOR ?)\", :zip, 1, 3)\n\nprint(render(c))\n#-> SUBSTRING(\"zip\" FROM 1 FOR 3)\n\nc = FUN(\"?::date\", \"2000-01-01\")\n\nprint(render(c))\n#-> '2000-01-01'::date","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"Write ?? to use ? in an operator name or a template.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FUN(\"??-\", \"(1,0)\", \"(0,0)\")\n\nprint(render(c))\n#-> ('(1,0)' ?- '(0,0)')\n\nc = FUN(\"('(?,?)'::point ??| '(?,?)'::point)\", 0, 1, 0, 0)\n\nprint(render(c))\n#-> ('(0,1)'::point ?| '(0,0)'::point)","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"Some functions and operators have specialized serializers.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FUN(:and)\n\nprint(render(c))\n#-> TRUE\n\nc = FUN(:and, true)\n\nprint(render(c))\n#-> TRUE\n\nc = FUN(:and, true, false)\n\nprint(render(c))\n#-> (TRUE AND FALSE)\n\nc = FUN(:or)\n\nprint(render(c))\n#-> FALSE\n\nc = FUN(:or, true)\n\nprint(render(c))\n#-> TRUE\n\nc = FUN(:or, true, false)\n\nprint(render(c))\n#-> (TRUE OR FALSE)\n\nc = FUN(:not, true)\n\nprint(render(c))\n#-> (NOT TRUE)\n\nc = FUN(:concat, :city, \", \", :state)\n\nprint(render(c))\n#-> concat(\"city\", ', ', \"state\")\n\nprint(render(c, dialect = :sqlite))\n#-> (\"city\" || ', ' || \"state\")\n\nc = FUN(:in, :zip)\n\nprint(render(c))\n#-> FALSE\n\nc = FUN(:in, :zip, \"60614\", \"60615\")\n\nprint(render(c))\n#-> (\"zip\" IN ('60614', '60615'))\n\nc = SELECT(FUN(:in, \"60615\", FROM(:location) |> SELECT(:zip)))\n\nprint(render(c))\n#=>\nSELECT ('60615' IN (\n SELECT \"zip\"\n FROM \"location\"\n))\n=#\n\nc = FUN(:not_in, :zip)\n\nprint(render(c))\n#-> TRUE\n\nc = FUN(:not_in, :zip, \"60614\", \"60615\")\n\nprint(render(c))\n#-> (\"zip\" NOT IN ('60614', '60615'))\n\nc = SELECT(FUN(:not_in, \"60615\", FROM(:location) |> SELECT(:zip)))\n\nprint(render(c))\n#=>\nSELECT ('60615' NOT IN (\n SELECT \"zip\"\n FROM \"location\"\n))\n=#\n\nc = SELECT(FUN(:exists, FROM(:location) |>\n WHERE(FUN(\"=\", :zip, \"60615\")) |>\n SELECT(missing)))\n\nprint(render(c))\n#=>\nSELECT (EXISTS (\n SELECT NULL\n FROM \"location\"\n WHERE (\"zip\" = '60615')\n))\n=#\n\nc = SELECT(FUN(:not_exists, FROM(:location) |>\n WHERE(FUN(\"=\", :zip, \"60615\")) |>\n SELECT(missing)))\n\nprint(render(c))\n#=>\nSELECT (NOT EXISTS (\n SELECT NULL\n FROM \"location\"\n WHERE (\"zip\" = '60615')\n))\n=#\n\nc = FUN(:is_null, :zip)\n\nprint(render(c))\n#-> (\"zip\" IS NULL)\n\nc = FUN(:is_not_null, :zip)\n\nprint(render(c))\n#-> (\"zip\" IS NOT NULL)\n\nc = FUN(:like, :zip, \"606%\")\n\nprint(render(c))\n#-> (\"zip\" LIKE '606%')\n\nc = FUN(:not_like, :zip, \"606%\")\n\nprint(render(c))\n#-> (\"zip\" NOT LIKE '606%')\n\nc = FUN(:case, FUN(\"<\", :year_of_birth, 1970), \"boomer\")\n\nprint(render(c))\n#-> (CASE WHEN (\"year_of_birth\" < 1970) THEN 'boomer' END)\n\nc = FUN(:case, FUN(\"<\", :year_of_birth, 1970), \"boomer\", \"millenial\")\n\nprint(render(c))\n#-> (CASE WHEN (\"year_of_birth\" < 1970) THEN 'boomer' ELSE 'millenial' END)\n\nc = FUN(:cast, \"2020-01-01\", \"DATE\")\n\nprint(render(c))\n#-> CAST('2020-01-01' AS DATE)\n\nc = FUN(:extract, \"YEAR\", c)\n\nprint(render(c))\n#-> EXTRACT(YEAR FROM CAST('2020-01-01' AS DATE))\n\nc = FUN(:between, :year_of_birth, 1950, 2000)\n\nprint(render(c))\n#-> (\"year_of_birth\" BETWEEN 1950 AND 2000)\n\nc = FUN(:not_between, :year_of_birth, 1950, 2000)\n\nprint(render(c))\n#-> (\"year_of_birth\" NOT BETWEEN 1950 AND 2000)\n\nc = FUN(:current_date)\n\nprint(render(c))\n#-> CURRENT_DATE\n\nc = FUN(:current_date, 1)\n\nprint(render(c))\n#-> CURRENT_DATE(1)\n\nc = FUN(:current_timestamp)\n\nprint(render(c))\n#-> CURRENT_TIMESTAMP","category":"page"},{"location":"test/clauses/#Aggregate-Functions","page":"SQL Clauses","title":"Aggregate Functions","text":"","category":"section"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"Aggregate SQL functions have a specialized AGG() constructor.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = AGG(:max, :year_of_birth)\n#-> AGG(\"max\", …)\n\ndisplay(c)\n#-> AGG(\"max\", ID(:year_of_birth))\n\nprint(render(c))\n#-> max(\"year_of_birth\")","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"Some well-known aggregate functions with irregular syntax are supported.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = AGG(:count)\n#-> AGG(\"count\")\n\ndisplay(c)\n#-> AGG(\"count\")\n\nprint(render(c))\n#-> count(*)\n\nc = AGG(:count_distinct, :zip)\n\nprint(render(c))\n#-> count(DISTINCT \"zip\")","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"Otherwise, a template name can be used.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = AGG(\"string_agg(DISTINCT ?, ',' ORDER BY ?)\", :zip, :zip)\n\nprint(render(c))\n#-> string_agg(DISTINCT \"zip\", ',' ORDER BY \"zip\")","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"An aggregate function may have a FILTER modifier.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = AGG(:count, filter = FUN(\">\", :year_of_birth, 1970))\n\ndisplay(c)\n#-> AGG(\"count\", filter = FUN(\">\", ID(:year_of_birth), LIT(1970)))\n\nprint(render(c))\n#-> (count(*) FILTER (WHERE (\"year_of_birth\" > 1970)))","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"A window function can be created by adding an OVER modifier.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = PARTITION(:year_of_birth, order_by = [:month_of_birth, :day_of_birth]) |>\n AGG(\"row_number\")\n\ndisplay(c)\n#=>\nAGG(\"row_number\",\n over = PARTITION(ID(:year_of_birth),\n order_by = [ID(:month_of_birth), ID(:day_of_birth)]))\n=#\n\nprint(render(c))\n#-> (row_number() OVER (PARTITION BY \"year_of_birth\" ORDER BY \"month_of_birth\", \"day_of_birth\"))\n\nc = AGG(\"row_number\", over = :w)\n\nprint(render(c))\n#-> (row_number() OVER (\"w\"))","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"The PARTITION clause may contain a frame specification including the frame mode, frame endpoints, and frame exclusion.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = PARTITION(order_by = [:year_of_birth], frame = :groups)\n#-> PARTITION(order_by = […], frame = :GROUPS)\n\nprint(render(c))\n#-> ORDER BY \"year_of_birth\" GROUPS UNBOUNDED PRECEDING\n\nc = PARTITION(order_by = [:year_of_birth], frame = (mode = :rows,))\n#-> PARTITION(order_by = […], frame = :ROWS)\n\nprint(render(c))\n#-> ORDER BY \"year_of_birth\" ROWS UNBOUNDED PRECEDING\n\nc = PARTITION(order_by = [:year_of_birth], frame = (mode = :range, start = -1, finish = 1, exclude = :current_row))\n#-> PARTITION(order_by = […], frame = (mode = :RANGE, start = -1, finish = 1, exclude = :CURRENT_ROW))\n\nprint(render(c))\n#-> ORDER BY \"year_of_birth\" RANGE BETWEEN 1 PRECEDING AND 1 FOLLOWING EXCLUDE CURRENT ROW\n\nc = PARTITION(order_by = [:year_of_birth], frame = (mode = :range, start = -Inf, finish = 0))\n\nprint(render(c))\n#-> ORDER BY \"year_of_birth\" RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW\n\nc = PARTITION(order_by = [:year_of_birth], frame = (mode = :range, start = 0, finish = Inf))\n\nprint(render(c))\n#-> ORDER BY \"year_of_birth\" RANGE BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING\n\nc = PARTITION(order_by = [:year_of_birth], frame = (mode = :range, exclude = :no_others))\n\nprint(render(c))\n#-> ORDER BY \"year_of_birth\" RANGE UNBOUNDED PRECEDING EXCLUDE NO OTHERS\n\nc = PARTITION(order_by = [:year_of_birth], frame = (mode = :range, exclude = :group))\n\nprint(render(c))\n#-> ORDER BY \"year_of_birth\" RANGE UNBOUNDED PRECEDING EXCLUDE GROUP\n\nc = PARTITION(order_by = [:year_of_birth], frame = (mode = :range, exclude = :ties))\n\nprint(render(c))\n#-> ORDER BY \"year_of_birth\" RANGE UNBOUNDED PRECEDING EXCLUDE TIES","category":"page"},{"location":"test/clauses/#AS-Clause","page":"SQL Clauses","title":"AS Clause","text":"","category":"section"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"An AS clause is created with AS() constructor.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = ID(:person) |> AS(:p)\n#-> (…) |> AS(:p)\n\ndisplay(c)\n#-> ID(:person) |> AS(:p)\n\nprint(render(c))\n#-> \"person\" AS \"p\"","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"A pair expression is automatically converted to an AS clause.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FROM(:p => :person)\ndisplay(c)\n#-> ID(:person) |> AS(:p) |> FROM()\n\nprint(render(c |> SELECT((:p, :person_id))))\n#=>\nSELECT \"p\".\"person_id\"\nFROM \"person\" AS \"p\"\n=#","category":"page"},{"location":"test/clauses/#FROM-Clause","page":"SQL Clauses","title":"FROM Clause","text":"","category":"section"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"A FROM clause is created with FROM() constructor.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FROM(:person)\n#-> (…) |> FROM()\n\ndisplay(c)\n#-> ID(:person) |> FROM()\n\nprint(render(c |> SELECT(:person_id)))\n#=>\nSELECT \"person_id\"\nFROM \"person\"\n=#","category":"page"},{"location":"test/clauses/#SELECT-Clause","page":"SQL Clauses","title":"SELECT Clause","text":"","category":"section"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"A SELECT clause is created with SELECT() constructor. While in SQL, SELECT typically opens a query, in FunSQL, SELECT() should be placed at the end of a clause chain.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = :person |> FROM() |> SELECT(:person_id, :year_of_birth)\n#-> (…) |> SELECT(…)\n\ndisplay(c)\n#-> ID(:person) |> FROM() |> SELECT(ID(:person_id), ID(:year_of_birth))\n\nprint(render(c))\n#=>\nSELECT\n \"person_id\",\n \"year_of_birth\"\nFROM \"person\"\n=#","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"The DISTINCT modifier can be added from the constructor.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FROM(:location) |> SELECT(distinct = true, :zip)\n#-> (…) |> SELECT(…)\n\ndisplay(c)\n#-> ID(:location) |> FROM() |> SELECT(distinct = true, ID(:zip))\n\nprint(render(c))\n#=>\nSELECT DISTINCT \"zip\"\nFROM \"location\"\n=#","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"A TOP modifier could be specified.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FROM(:person) |> SELECT(top = 1, :person_id)\n\ndisplay(c)\n#-> ID(:person) |> FROM() |> SELECT(top = 1, ID(:person_id))\n\nprint(render(c))\n#=>\nSELECT TOP 1 \"person_id\"\nFROM \"person\"\n=#\n\nc = FROM(:person) |>\n ORDER(:year_of_birth) |>\n SELECT(top = (limit = 1, with_ties = true), :person_id)\n\ndisplay(c)\n#=>\nID(:person) |>\nFROM() |>\nORDER(ID(:year_of_birth)) |>\nSELECT(top = (limit = 1, with_ties = true), ID(:person_id))\n=#\n\nprint(render(c))\n#=>\nSELECT TOP 1 WITH TIES \"person_id\"\nFROM \"person\"\nORDER BY \"year_of_birth\"\n=#","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"A SELECT clause with an empty list of arguments can be created explicitly.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = SELECT(args = [])\n#-> SELECT(…)","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"Rendering a nested SELECT clause adds parentheses around it.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = :location |> FROM() |> SELECT(:state, :zip) |> FROM() |> SELECT(:zip)\n\nprint(render(c))\n#=>\nSELECT \"zip\"\nFROM (\n SELECT\n \"state\",\n \"zip\"\n FROM \"location\"\n)\n=#","category":"page"},{"location":"test/clauses/#WHERE-Clause","page":"SQL Clauses","title":"WHERE Clause","text":"","category":"section"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"A WHERE clause is created with WHERE() constructor.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FROM(:person) |> WHERE(FUN(\">\", :year_of_birth, 2000))\n#-> (…) |> WHERE(…)\n\ndisplay(c)\n#-> ID(:person) |> FROM() |> WHERE(FUN(\">\", ID(:year_of_birth), LIT(2000)))\n\nprint(render(c |> SELECT(:person_id)))\n#=>\nSELECT \"person_id\"\nFROM \"person\"\nWHERE (\"year_of_birth\" > 2000)\n=#","category":"page"},{"location":"test/clauses/#LIMIT-Clause","page":"SQL Clauses","title":"LIMIT Clause","text":"","category":"section"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"A LIMIT/OFFSET (or OFFSET/FETCH) clause is created with LIMIT() constructor.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FROM(:person) |> LIMIT(10)\n#-> (…) |> LIMIT(10)\n\ndisplay(c)\n#-> ID(:person) |> FROM() |> LIMIT(10)\n\nprint(render(c |> SELECT(:person_id)))\n#=>\nSELECT \"person_id\"\nFROM \"person\"\nFETCH FIRST 10 ROWS ONLY\n=#","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"Many SQL dialects represent LIMIT clause with a non-standard syntax.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"print(render(c |> SELECT(:person_id), dialect = :mysql))\n#=>\nSELECT `person_id`\nFROM `person`\nLIMIT 10\n=#\n\nprint(render(c |> SELECT(:person_id), dialect = :postgresql))\n#=>\nSELECT \"person_id\"\nFROM \"person\"\nLIMIT 10\n=#\n\nprint(render(c |> SELECT(:person_id), dialect = :sqlite))\n#=>\nSELECT \"person_id\"\nFROM \"person\"\nLIMIT 10\n=#\n\nprint(render(c |> SELECT(:person_id), dialect = :sqlserver))\n#=>\nSELECT TOP 10 [person_id]\nFROM [person]\n=#","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"Both limit (the number of rows) and offset (number of rows to skip) can be specified.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FROM(:person) |> LIMIT(100, 10) |> SELECT(:person_id)\n\ndisplay(c)\n#-> ID(:person) |> FROM() |> LIMIT(100, 10) |> SELECT(ID(:person_id))\n\nprint(render(c))\n#=>\nSELECT \"person_id\"\nFROM \"person\"\nOFFSET 100 ROWS\nFETCH NEXT 10 ROWS ONLY\n=#\n\nprint(render(c, dialect = :mysql))\n#=>\nSELECT `person_id`\nFROM `person`\nLIMIT 100, 10\n=#\n\nprint(render(c, dialect = :postgresql))\n#=>\nSELECT \"person_id\"\nFROM \"person\"\nLIMIT 10\nOFFSET 100\n=#\n\nprint(render(c, dialect = :sqlite))\n#=>\nSELECT \"person_id\"\nFROM \"person\"\nLIMIT 10\nOFFSET 100\n=#\n\nprint(render(c, dialect = :sqlserver))\n#=>\nSELECT [person_id]\nFROM [person]\nOFFSET 100 ROWS\nFETCH NEXT 10 ROWS ONLY\n=#","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"Alternatively, both limit and offset can be specified as a unit range.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FROM(:person) |> LIMIT(101:110)\n\nprint(render(c |> SELECT(:person_id)))\n#=>\nSELECT \"person_id\"\nFROM \"person\"\nOFFSET 100 ROWS\nFETCH NEXT 10 ROWS ONLY\n=#","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"It is possible to specify the offset without the limit.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FROM(:person) |> LIMIT(offset = 100) |> SELECT(:person_id)\n\ndisplay(c)\n#-> ID(:person) |> FROM() |> LIMIT(100, nothing) |> SELECT(ID(:person_id))\n\nprint(render(c))\n#=>\nSELECT \"person_id\"\nFROM \"person\"\nOFFSET 100 ROWS\n=#\n\nprint(render(c, dialect = :mysql))\n#=>\nSELECT `person_id`\nFROM `person`\nLIMIT 100, 18446744073709551615\n=#\n\nprint(render(c, dialect = :postgresql))\n#=>\nSELECT \"person_id\"\nFROM \"person\"\nOFFSET 100\n=#\n\nprint(render(c, dialect = :sqlite))\n#=>\nSELECT \"person_id\"\nFROM \"person\"\nLIMIT -1\nOFFSET 100\n=#\n\nprint(render(c, dialect = :sqlserver))\n#=>\nSELECT [person_id]\nFROM [person]\nOFFSET 100 ROWS\n=#","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"It is possible to specify the limit with ties.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FROM(:person) |>\n ORDER(:year_of_birth) |>\n LIMIT(10, with_ties = true) |>\n SELECT(:person_id)\n\ndisplay(c)\n#=>\nID(:person) |>\nFROM() |>\nORDER(ID(:year_of_birth)) |>\nLIMIT(10, with_ties = true) |>\nSELECT(ID(:person_id))\n=#\n\nprint(render(c))\n#=>\nSELECT \"person_id\"\nFROM \"person\"\nORDER BY \"year_of_birth\"\nFETCH FIRST 10 ROWS WITH TIES\n=#","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"SQL Server prohibits ORDER BY without limiting in a nested query, so FunSQL automatically adds OFFSET 0 clause to the query.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FROM(:person) |>\n ORDER(:year_of_birth) |>\n SELECT(:person_id, :gender_concept_id) |>\n AS(:person) |>\n FROM() |>\n WHERE(FUN(\"=\", :gender_concept_id, 8507)) |>\n SELECT(:person_id)\n\nprint(render(c, dialect = :sqlserver))\n#=>\nSELECT [person_id]\nFROM (\n SELECT\n [person_id],\n [gender_concept_id]\n FROM [person]\n ORDER BY [year_of_birth]\n OFFSET 0 ROWS\n) AS [person]\nWHERE ([gender_concept_id] = 8507)\n=#","category":"page"},{"location":"test/clauses/#JOIN-Clause","page":"SQL Clauses","title":"JOIN Clause","text":"","category":"section"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"A JOIN clause is created with JOIN() constructor.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FROM(:p => :person) |>\n JOIN(:l => :location, FUN(\"=\", (:p, :location_id), (:l, :location_id)), left = true)\n#-> (…) |> JOIN(…)\n\ndisplay(c)\n#=>\nID(:person) |>\nAS(:p) |>\nFROM() |>\nJOIN(ID(:location) |> AS(:l),\n FUN(\"=\", ID(:p) |> ID(:location_id), ID(:l) |> ID(:location_id)),\n left = true)\n=#\n\nprint(render(c |> SELECT((:p, :person_id), (:l, :state))))\n#=>\nSELECT\n \"p\".\"person_id\",\n \"l\".\"state\"\nFROM \"person\" AS \"p\"\nLEFT JOIN \"location\" AS \"l\" ON (\"p\".\"location_id\" = \"l\".\"location_id\")\n=#","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"Different types of JOIN are supported.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FROM(:p => :person) |>\n JOIN(:op => :observation_period,\n on = FUN(\"=\", (:p, :person_id), (:op, :person_id)))\n\ndisplay(c)\n#=>\nID(:person) |>\nAS(:p) |>\nFROM() |>\nJOIN(ID(:observation_period) |> AS(:op),\n FUN(\"=\", ID(:p) |> ID(:person_id), ID(:op) |> ID(:person_id)))\n=#\n\nprint(render(c |> SELECT((:p, :person_id), (:op, :observation_period_start_date))))\n#=>\nSELECT\n \"p\".\"person_id\",\n \"op\".\"observation_period_start_date\"\nFROM \"person\" AS \"p\"\nJOIN \"observation_period\" AS \"op\" ON (\"p\".\"person_id\" = \"op\".\"person_id\")\n=#\n\nc = FROM(:l => :location) |>\n JOIN(:cs => :care_site,\n on = FUN(\"=\", (:l, :location_id), (:cs, :location_id)),\n right = true)\n\ndisplay(c)\n#=>\nID(:location) |>\nAS(:l) |>\nFROM() |>\nJOIN(ID(:care_site) |> AS(:cs),\n FUN(\"=\", ID(:l) |> ID(:location_id), ID(:cs) |> ID(:location_id)),\n right = true)\n=#\n\nprint(render(c |> SELECT((:cs, :care_site_name), (:l, :state))))\n#=>\nSELECT\n \"cs\".\"care_site_name\",\n \"l\".\"state\"\nFROM \"location\" AS \"l\"\nRIGHT JOIN \"care_site\" AS \"cs\" ON (\"l\".\"location_id\" = \"cs\".\"location_id\")\n=#\n\nc = FROM(:p => :person) |>\n JOIN(:pr => :provider,\n on = FUN(\"=\", (:p, :provider_id), (:pr, :provider_id)),\n left = true,\n right = true)\n\ndisplay(c)\n#=>\nID(:person) |>\nAS(:p) |>\nFROM() |>\nJOIN(ID(:provider) |> AS(:pr),\n FUN(\"=\", ID(:p) |> ID(:provider_id), ID(:pr) |> ID(:provider_id)),\n left = true,\n right = true)\n=#\n\nprint(render(c |> SELECT((:p, :person_id), (:pr, :npi))))\n#=>\nSELECT\n \"p\".\"person_id\",\n \"pr\".\"npi\"\nFROM \"person\" AS \"p\"\nFULL JOIN \"provider\" AS \"pr\" ON (\"p\".\"provider_id\" = \"pr\".\"provider_id\")\n=#","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"To render a CROSS JOIN, set the join condition to true.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FROM(:p1 => :person) |>\n JOIN(:p2 => :person,\n on = true)\n\nprint(render(c |> SELECT((:p1, :person_id), (:p2, :person_id))))\n#=>\nSELECT\n \"p1\".\"person_id\",\n \"p2\".\"person_id\"\nFROM \"person\" AS \"p1\"\nCROSS JOIN \"person\" AS \"p2\"\n=#","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"A JOIN LATERAL clause can be created.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FROM(:p => :person) |>\n JOIN(:vo => FROM(:vo => :visit_occurrence) |>\n WHERE(FUN(\"=\", (:p, :person_id), (:vo, :person_id))) |>\n ORDER((:vo, :visit_start_date) |> DESC()) |>\n LIMIT(1) |>\n SELECT((:vo, :visit_start_date)),\n on = true,\n left = true,\n lateral = true)\n\ndisplay(c)\n#=>\nID(:person) |>\nAS(:p) |>\nFROM() |>\nJOIN(ID(:visit_occurrence) |>\n AS(:vo) |>\n FROM() |>\n WHERE(FUN(\"=\", ID(:p) |> ID(:person_id), ID(:vo) |> ID(:person_id))) |>\n ORDER(ID(:vo) |> ID(:visit_start_date) |> DESC()) |>\n LIMIT(1) |>\n SELECT(ID(:vo) |> ID(:visit_start_date)) |>\n AS(:vo),\n LIT(true),\n left = true,\n lateral = true)\n=#\n\nprint(render(c |> SELECT((:p, :person_id), (:vo, :visit_start_date))))\n#=>\nSELECT\n \"p\".\"person_id\",\n \"vo\".\"visit_start_date\"\nFROM \"person\" AS \"p\"\nLEFT JOIN LATERAL (\n SELECT \"vo\".\"visit_start_date\"\n FROM \"visit_occurrence\" AS \"vo\"\n WHERE (\"p\".\"person_id\" = \"vo\".\"person_id\")\n ORDER BY \"vo\".\"visit_start_date\" DESC\n FETCH FIRST 1 ROW ONLY\n) AS \"vo\" ON TRUE\n=#","category":"page"},{"location":"test/clauses/#GROUP-Clause","page":"SQL Clauses","title":"GROUP Clause","text":"","category":"section"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"A GROUP BY clause is created with GROUP constructor.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FROM(:person) |> GROUP(:year_of_birth)\n#-> (…) |> GROUP(…)\n\ndisplay(c)\n#-> ID(:person) |> FROM() |> GROUP(ID(:year_of_birth))\n\nprint(render(c |> SELECT(:year_of_birth, AGG(:count))))\n#=>\nSELECT\n \"year_of_birth\",\n count(*)\nFROM \"person\"\nGROUP BY \"year_of_birth\"\n=#","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"A GROUP constructor accepts an empty partition list, in which case, it is not rendered.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FROM(:person) |> GROUP()\n#-> (…) |> GROUP()\n\nprint(render(c |> SELECT(AGG(:count))))\n#=>\nSELECT count(*)\nFROM \"person\"\n=#","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"GROUP can accept the grouping mode or a vector of grouping sets.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FROM(:person) |> GROUP(:year_of_birth, sets = :ROLLUP)\n#-> (…) |> GROUP(…, sets = :ROLLUP)\n\nprint(render(c |> SELECT(:year_of_birth, AGG(:count))))\n#=>\nSELECT\n \"year_of_birth\",\n count(*)\nFROM \"person\"\nGROUP BY ROLLUP(\"year_of_birth\")\n=#\n\nc = FROM(:person) |> GROUP(:year_of_birth, sets = :CUBE)\n#-> (…) |> GROUP(…, sets = :CUBE)\n\nprint(render(c |> SELECT(:year_of_birth, AGG(:count))))\n#=>\nSELECT\n \"year_of_birth\",\n count(*)\nFROM \"person\"\nGROUP BY CUBE(\"year_of_birth\")\n=#\n\nc = FROM(:person) |> GROUP(:year_of_birth, sets = [[1], Int[]])\n#-> (…) |> GROUP(…, sets = [[1], Int64[]])\n\nprint(render(c |> SELECT(:year_of_birth, AGG(:count))))\n#=>\nSELECT\n \"year_of_birth\",\n count(*)\nFROM \"person\"\nGROUP BY GROUPING SETS((\"year_of_birth\"), ())\n=#","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"GROUP raises an error when the vector of grouping sets is out of bounds.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"FROM(:person) |> GROUP(:year_of_birth, sets = [[1, 2], [1], Int[]])\n#=>\nERROR: DomainError with [[1, 2], [1], Int64[]]:\nsets are out of bounds\n=#","category":"page"},{"location":"test/clauses/#HAVING-Clause","page":"SQL Clauses","title":"HAVING Clause","text":"","category":"section"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"A HAVING clause is created with HAVING() constructor.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FROM(:person) |>\n GROUP(:year_of_birth) |>\n HAVING(FUN(\">\", AGG(:count), 10))\n#-> (…) |> HAVING(…)\n\ndisplay(c)\n#=>\nID(:person) |>\nFROM() |>\nGROUP(ID(:year_of_birth)) |>\nHAVING(FUN(\">\", AGG(\"count\"), LIT(10)))\n=#\n\nprint(render(c |> SELECT(:person_id)))\n#=>\nSELECT \"person_id\"\nFROM \"person\"\nGROUP BY \"year_of_birth\"\nHAVING (count(*) > 10)\n=#","category":"page"},{"location":"test/clauses/#ORDER-Clause","page":"SQL Clauses","title":"ORDER Clause","text":"","category":"section"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"An ORDER BY clause is created with ORDER constructor.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FROM(:person) |> ORDER(:year_of_birth)\n#-> (…) |> ORDER(…)\n\ndisplay(c)\n#-> ID(:person) |> FROM() |> ORDER(ID(:year_of_birth))\n\nprint(render(c |> SELECT(:person_id)))\n#=>\nSELECT \"person_id\"\nFROM \"person\"\nORDER BY \"year_of_birth\"\n=#","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"An ORDER constructor accepts an empty list, in which case, it is not rendered.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FROM(:person) |> ORDER()\n#-> (…) |> ORDER()\n\nprint(render(c |> SELECT(:person_id)))\n#=>\nSELECT \"person_id\"\nFROM \"person\"\n=#","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"It is possible to specify ascending or descending order of the sort column.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FROM(:person) |>\n ORDER(:year_of_birth |> DESC(nulls = :first),\n :person_id |> ASC()) |>\n SELECT(:person_id)\n\ndisplay(c)\n#=>\nID(:person) |>\nFROM() |>\nORDER(ID(:year_of_birth) |> DESC(nulls = :NULLS_FIRST),\n ID(:person_id) |> ASC()) |>\nSELECT(ID(:person_id))\n=#\n\nprint(render(c))\n#=>\nSELECT \"person_id\"\nFROM \"person\"\nORDER BY\n \"year_of_birth\" DESC NULLS FIRST,\n \"person_id\" ASC\n=#","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"Instead of ASC and DESC, a generic SORT constructor can be used.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FROM(:person) |>\n ORDER(:year_of_birth |> SORT(:desc, nulls = :last),\n :person_id |> SORT(:asc)) |>\n SELECT(:person_id)\n\nprint(render(c))\n#=>\nSELECT \"person_id\"\nFROM \"person\"\nORDER BY\n \"year_of_birth\" DESC NULLS LAST,\n \"person_id\" ASC\n=#","category":"page"},{"location":"test/clauses/#UNION-Clause.","page":"SQL Clauses","title":"UNION Clause.","text":"","category":"section"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"UNION and UNION ALL clauses are created with UNION() constructor.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FROM(:measurement) |>\n SELECT(:person_id, :date => :measurement_date) |>\n UNION(all = true,\n FROM(:observation) |>\n SELECT(:person_id, :date => :observation_date))\n#-> (…) |> UNION(all = true, …)\n\ndisplay(c)\n#=>\nID(:measurement) |>\nFROM() |>\nSELECT(ID(:person_id), ID(:measurement_date) |> AS(:date)) |>\nUNION(all = true,\n ID(:observation) |>\n FROM() |>\n SELECT(ID(:person_id), ID(:observation_date) |> AS(:date)))\n=#\n\nprint(render(c))\n#=>\nSELECT\n \"person_id\",\n \"measurement_date\" AS \"date\"\nFROM \"measurement\"\nUNION ALL\nSELECT\n \"person_id\",\n \"observation_date\" AS \"date\"\nFROM \"observation\"\n=#","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"A UNION clause with no subqueries can be created explicitly.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"UNION(args = [])\n#-> UNION(args = [])","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"Rendering a nested UNION clause adds parentheses around it.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FROM(:measurement) |>\n SELECT(:person_id, :date => :measurement_date) |>\n UNION(all = true,\n FROM(:observation) |>\n SELECT(:person_id, :date => :observation_date)) |>\n FROM() |>\n AS(:union) |>\n WHERE(FUN(\">\", ID(:date), Date(2000))) |>\n SELECT(ID(:person_id))\n\nprint(render(c))\n#=>\nSELECT \"person_id\"\nFROM (\n SELECT\n \"person_id\",\n \"measurement_date\" AS \"date\"\n FROM \"measurement\"\n UNION ALL\n SELECT\n \"person_id\",\n \"observation_date\" AS \"date\"\n FROM \"observation\"\n) AS \"union\"\nWHERE (\"date\" > '2000-01-01')\n=#","category":"page"},{"location":"test/clauses/#VALUES-Clause","page":"SQL Clauses","title":"VALUES Clause","text":"","category":"section"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"A VALUES clause is created with VALUES() constructor.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = VALUES([(\"SQL\", 1974), (\"Julia\", 2012), (\"FunSQL\", 2021)])\n#-> VALUES([(\"SQL\", 1974), (\"Julia\", 2012), (\"FunSQL\", 2021)])\n\ndisplay(c)\n#-> VALUES([(\"SQL\", 1974), (\"Julia\", 2012), (\"FunSQL\", 2021)])\n\nprint(render(c))\n#=>\nVALUES\n ('SQL', 1974),\n ('Julia', 2012),\n ('FunSQL', 2021)\n=#","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"MySQL has special syntax for rows.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"print(render(c, dialect = :mysql))\n#=>\nVALUES\n ROW('SQL', 1974),\n ROW('Julia', 2012),\n ROW('FunSQL', 2021)\n=#","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"When VALUES clause contains a single row, it is emitted on the same line.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = VALUES([(\"SQL\", 1974)])\n\nprint(render(c))\n#-> VALUES ('SQL', 1974)","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"VALUES accepts a vector of scalar values.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = VALUES([\"SQL\", \"Julia\", \"FunSQL\"])\n\nprint(render(c))\n#=>\nVALUES\n 'SQL',\n 'Julia',\n 'FunSQL'\n=#","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"When VALUES is nested in a FROM clause, it is wrapped in parentheses.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = VALUES([(\"SQL\", 1974), (\"Julia\", 2012), (\"FunSQL\", 2021)]) |>\n AS(:values, columns = [:name, :year]) |>\n FROM() |>\n SELECT(FUN(\"*\"))\n\nprint(render(c))\n#=>\nSELECT *\nFROM (\n VALUES\n ('SQL', 1974),\n ('Julia', 2012),\n ('FunSQL', 2021)\n) AS \"values\" (\"name\", \"year\")\n=#","category":"page"},{"location":"test/clauses/#WINDOW-Clause","page":"SQL Clauses","title":"WINDOW Clause","text":"","category":"section"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"A WINDOW clause is created with WINDOW() constructor.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FROM(:person) |>\n WINDOW(:w1 => PARTITION(:gender_concept_id),\n :w2 => :w1 |> PARTITION(:year_of_birth, order_by = [:month_of_birth, :day_of_birth]))\n#-> (…) |> WINDOW(…)\n\ndisplay(c)\n#=>\nID(:person) |>\nFROM() |>\nWINDOW(PARTITION(ID(:gender_concept_id)) |> AS(:w1),\n ID(:w1) |>\n PARTITION(ID(:year_of_birth),\n order_by = [ID(:month_of_birth), ID(:day_of_birth)]) |>\n AS(:w2))\n=#\n\nprint(render(c |> SELECT(:w1 |> AGG(\"row_number\"), :w2 |> AGG(\"row_number\"))))\n#=>\nSELECT\n (row_number() OVER (\"w1\")),\n (row_number() OVER (\"w2\"))\nFROM \"person\"\nWINDOW\n \"w1\" AS (PARTITION BY \"gender_concept_id\"),\n \"w2\" AS (\"w1\" PARTITION BY \"year_of_birth\" ORDER BY \"month_of_birth\", \"day_of_birth\")\n=#","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"The WINDOW() constructor accepts an empty list of partitions, in which case, it is not rendered.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FROM(:person) |>\n WINDOW(args = [])\n\ndisplay(c)\n#-> ID(:person) |> FROM() |> WINDOW(args = [])\n\nprint(render(c |> SELECT(AGG(\"row_number\", over = PARTITION()))))\n#=>\nSELECT (row_number() OVER ())\nFROM \"person\"\n=#","category":"page"},{"location":"test/clauses/#WITH-Clause-and-Common-Table-Expressions","page":"SQL Clauses","title":"WITH Clause and Common Table Expressions","text":"","category":"section"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"The AS clause that defines a common table expression is created using the AS constructor.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"cte1 =\n FROM(:concept) |>\n WHERE(FUN(\"=\", :concept_id, 320128)) |>\n SELECT(:concept_id, :concept_name) |>\n AS(:essential_hypertension)\n#-> (…) |> AS(:essential_hypertension)\n\ncte2 =\n FROM(:essential_hypertension) |>\n SELECT(:concept_id, :concept_name) |>\n UNION(all = true,\n FROM(:eh => :essential_hypertension_with_descendants) |>\n JOIN(:cr => :concept_relationship,\n FUN(\"=\", (:eh, :concept_id), (:cr, :concept_id_1))) |>\n JOIN(:c => :concept,\n FUN(\"=\", (:cr, :concept_id_2), (:c, :concept_id))) |>\n WHERE(FUN(\"=\", (:cr, :relationship_id), \"Subsumes\")) |>\n SELECT((:c, :concept_id), (:c, :concept_name))) |>\n AS(:essential_hypertension_with_descendants,\n columns = [:concept_id, :concept_name])\n#-> (…) |> AS(:essential_hypertension_with_descendants, columns = […])","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"The WITH clause is created using the WITH() constructor.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FROM(:essential_hypertension_with_descendants) |>\n SELECT(*) |>\n WITH(recursive = true, cte1, cte2)\n#-> (…) |> WITH(recursive = true, …)\n\ndisplay(c)\n#=>\nID(:essential_hypertension_with_descendants) |>\nFROM() |>\nSELECT(FUN(\"*\")) |>\nWITH(recursive = true,\n ID(:concept) |>\n FROM() |>\n WHERE(FUN(\"=\", ID(:concept_id), LIT(320128))) |>\n SELECT(ID(:concept_id), ID(:concept_name)) |>\n AS(:essential_hypertension),\n ID(:essential_hypertension) |>\n FROM() |>\n SELECT(ID(:concept_id), ID(:concept_name)) |>\n UNION(all = true,\n ID(:essential_hypertension_with_descendants) |>\n AS(:eh) |>\n FROM() |>\n JOIN(ID(:concept_relationship) |> AS(:cr),\n FUN(\"=\",\n ID(:eh) |> ID(:concept_id),\n ID(:cr) |> ID(:concept_id_1))) |>\n JOIN(ID(:concept) |> AS(:c),\n FUN(\"=\",\n ID(:cr) |> ID(:concept_id_2),\n ID(:c) |> ID(:concept_id))) |>\n WHERE(FUN(\"=\", ID(:cr) |> ID(:relationship_id), LIT(\"Subsumes\"))) |>\n SELECT(ID(:c) |> ID(:concept_id), ID(:c) |> ID(:concept_name))) |>\n AS(:essential_hypertension_with_descendants,\n columns = [:concept_id, :concept_name]))\n=#\n\nprint(render(c))\n#=>\nWITH RECURSIVE \"essential_hypertension\" AS (\n SELECT\n \"concept_id\",\n \"concept_name\"\n FROM \"concept\"\n WHERE (\"concept_id\" = 320128)\n),\n\"essential_hypertension_with_descendants\" (\"concept_id\", \"concept_name\") AS (\n SELECT\n \"concept_id\",\n \"concept_name\"\n FROM \"essential_hypertension\"\n UNION ALL\n SELECT\n \"c\".\"concept_id\",\n \"c\".\"concept_name\"\n FROM \"essential_hypertension_with_descendants\" AS \"eh\"\n JOIN \"concept_relationship\" AS \"cr\" ON (\"eh\".\"concept_id\" = \"cr\".\"concept_id_1\")\n JOIN \"concept\" AS \"c\" ON (\"cr\".\"concept_id_2\" = \"c\".\"concept_id\")\n WHERE (\"cr\".\"relationship_id\" = 'Subsumes')\n)\nSELECT *\nFROM \"essential_hypertension_with_descendants\"\n=#","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"The MATERIALIZED annotation can be added using NOTE.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"cte =\n FROM(:condition_occurrence) |>\n WHERE(FUN(\"=\", :condition_concept_id, 320128)) |>\n SELECT(:person_id) |>\n NOTE(\"MATERIALIZED\") |>\n AS(:essential_hypertension_occurrence)\n#-> (…) |> AS(:essential_hypertension_occurrence)\n\ndisplay(cte)\n#=>\nID(:condition_occurrence) |>\nFROM() |>\nWHERE(FUN(\"=\", ID(:condition_concept_id), LIT(320128))) |>\nSELECT(ID(:person_id)) |>\nNOTE(\"MATERIALIZED\") |>\nAS(:essential_hypertension_occurrence)\n=#\n\nprint(render(FROM(:essential_hypertension_occurrence) |> SELECT(*) |> WITH(cte)))\n#=>\nWITH \"essential_hypertension_occurrence\" AS MATERIALIZED (\n SELECT \"person_id\"\n FROM \"condition_occurrence\"\n WHERE (\"condition_concept_id\" = 320128)\n)\nSELECT *\nFROM \"essential_hypertension_occurrence\"\n=#","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"A WITH clause without any common table expressions will be omitted.","category":"page"},{"location":"test/clauses/","page":"SQL Clauses","title":"SQL Clauses","text":"c = FROM(:condition_occurrence) |>\n SELECT(*) |>\n WITH(args = [])\n#-> (…) |> WITH(args = [])\n\nprint(render(c))\n#=>\nSELECT *\nFROM \"condition_occurrence\"\n=#","category":"page"},{"location":"test/#Test-Suite","page":"Test Suite","title":"Test Suite","text":"","category":"section"},{"location":"test/","page":"Test Suite","title":"Test Suite","text":"Pages = [\n \"clauses.md\",\n \"nodes.md\",\n \"other.md\",\n]","category":"page"}] } diff --git a/dev/test/clauses/index.html b/dev/test/clauses/index.html index a1384454..735701bf 100644 --- a/dev/test/clauses/index.html +++ b/dev/test/clauses/index.html @@ -1224,4 +1224,4 @@ #=> SELECT * FROM "condition_occurrence" -=# +=# diff --git a/dev/test/index.html b/dev/test/index.html index 8306832a..0434ad95 100644 --- a/dev/test/index.html +++ b/dev/test/index.html @@ -1,2 +1,2 @@ -Test Suite · FunSQL.jl +Test Suite · FunSQL.jl diff --git a/dev/test/nodes/index.html b/dev/test/nodes/index.html index e47dc29c..5d416ffc 100644 --- a/dev/test/nodes/index.html +++ b/dev/test/nodes/index.html @@ -461,6 +461,115 @@ "person_1"."birth_datetime", "person_1"."location_id" FROM "person" AS "person_1" +=#

Define allows you to insert columns at the beginning or at the end of the column list.

q = From(person) |>
+    Define(:age => Fun.now() .- Get.birth_datetime, Get.birth_datetime,
+           before = true)
+
+display(q)
+#=>
+let person = SQLTable(:person, …),
+    q1 = From(person),
+    q2 = q1 |>
+         Define(Fun."-"(Fun.now(), Get.birth_datetime) |> As(:age),
+                Get.birth_datetime,
+                before = true)
+    q2
+end
+=#
+
+print(render(q))
+#=>
+SELECT
+  (now() - "person_1"."birth_datetime") AS "age",
+  "person_1"."birth_datetime",
+  "person_1"."person_id",
+  "person_1"."gender_concept_id",
+  "person_1"."year_of_birth",
+  "person_1"."month_of_birth",
+  "person_1"."day_of_birth",
+  "person_1"."location_id"
+FROM "person" AS "person_1"
+=#
+
+q = From(person) |>
+    Define(:age => Fun.now() .- Get.birth_datetime, Get.birth_datetime,
+           after = true)
+
+display(q)
+#=>
+let person = SQLTable(:person, …),
+    q1 = From(person),
+    q2 = q1 |>
+         Define(Fun."-"(Fun.now(), Get.birth_datetime) |> As(:age),
+                Get.birth_datetime,
+                after = true)
+    q2
+end
+=#
+
+print(render(q))
+#=>
+SELECT
+  "person_1"."person_id",
+  "person_1"."gender_concept_id",
+  "person_1"."year_of_birth",
+  "person_1"."month_of_birth",
+  "person_1"."day_of_birth",
+  "person_1"."location_id",
+  (now() - "person_1"."birth_datetime") AS "age",
+  "person_1"."birth_datetime"
+FROM "person" AS "person_1"
+=#

It can also insert columns in front of or right after a specified column.

q = From(person) |>
+    Define(:age => Fun.now() .- Get.birth_datetime, Get.birth_datetime,
+           before = :year_of_birth)
+
+print(render(q))
+#=>
+SELECT
+  "person_1"."person_id",
+  "person_1"."gender_concept_id",
+  (now() - "person_1"."birth_datetime") AS "age",
+  "person_1"."birth_datetime",
+  "person_1"."year_of_birth",
+  "person_1"."month_of_birth",
+  "person_1"."day_of_birth",
+  "person_1"."location_id"
+FROM "person" AS "person_1"
+=#
+
+q = From(person) |>
+    Define(:age => Fun.now() .- Get.birth_datetime, Get.birth_datetime,
+           after = :birth_datetime)
+
+print(render(q))
+#=>
+SELECT
+  "person_1"."person_id",
+  "person_1"."gender_concept_id",
+  "person_1"."year_of_birth",
+  "person_1"."month_of_birth",
+  "person_1"."day_of_birth",
+  (now() - "person_1"."birth_datetime") AS "age",
+  "person_1"."birth_datetime",
+  "person_1"."location_id"
+FROM "person" AS "person_1"
+=#

It is an error to set both before and after or to refer to a non-existent column.

q = From(person) |>
+    Define(before = true, after = true)
+
+print(render(q))
+#=>
+ERROR: DomainError with (before = true, after = true):
+only one of `before` and `after` could be set
+=#
+
+q = Define(before = :person_id)
+
+print(render(q))
+#=>
+ERROR: FunSQL.ReferenceError: cannot find `person_id` in:
+let q1 = Define(before = :person_id)
+    q1
+end
 =#

Define has no effect if none of the defined fields are used in the query.

q = From(person) |>
     Define(:age => 2020 .- Get.year_of_birth) |>
     Select(Get.person_id, Get.year_of_birth)
@@ -3145,4 +3254,4 @@
 │     ) AS "visit_group_1" ON ("person_2"."person_id" = "visit_group_1"."person_id")""",
 │     columns = [SQLColumn(:person_id), SQLColumn(:max_visit_start_date)])
 └ @ FunSQL …
-=#
+=# diff --git a/dev/test/other/index.html b/dev/test/other/index.html index c821c224..7d8ecc56 100644 --- a/dev/test/other/index.html +++ b/dev/test/other/index.html @@ -273,4 +273,4 @@ pack(sql, Dict("YEAR" => 1950)) #-> Any[1950]

pack can also be applied to a regular string, in which case it returns the parameters unchanged.

pack("SELECT * FROM person WHERE year_of_birth >= ?", (1950,))
-#-> (1950,)
+#-> (1950,) diff --git a/dev/two-kinds-of-sql-query-builders/index.html b/dev/two-kinds-of-sql-query-builders/index.html index 49176b9d..a2d8227a 100644 --- a/dev/two-kinds-of-sql-query-builders/index.html +++ b/dev/two-kinds-of-sql-query-builders/index.html @@ -41,4 +41,4 @@ having orderby limit -end

Individual slots of this structure are populated by the corresponding pipeline nodes.

"Where" node acting on the syntax tree

This explains why the pipeline is insensitive to the order of the nodes. Indeed, as long as the content of the slots stays the same, it makes no difference in what order the slots are populated.

Pipeline is insensitive to the order of the nodes

This method of incrementally constructing a composite structure is known as the builder pattern. We can call the query builders that employ this pattern syntax-oriented.

Both data-oriented and syntax-oriented query builders are compositional: the difference is in the nature of the information processed by the units of composition. Data-oriented query builders incrementally refine the query output; syntax-oriented query builders incrementally assemble the SQL syntax tree. Their interfaces look almost identical, but their methods of operation are fundamentally different.

But which one is better? Syntax-oriented query builders have two definite advantages: they are easy to implement and they could support the full range of SQL features. Indeed, the interface of a syntax-oriented query builder is just a collection of builders for the SQL syntax tree. How complete the representation of the syntax tree determines how well various SQL features are supported.

On the other hand, syntax-oriented query builders are harder to use. As they directly represent the SQL grammar, they inherit all of its deficiencies. In particular, the rigid clause order makes it difficult to assemble complex data processing pipelines, especially when the arrangement of pipeline nodes is not predetermined.

A data-oriented query builder directly represents data processing nodes, which makes assembling data processing pipelines much more straightforward—as long as we can find the necessary nodes among those offered by the builder. But where does the builder get its collection of data processing nodes? And how can we tell if this collection is complete?

One way to implement a data-oriented query builder is to adapt a general-purpose query framework. Indeed, this is the origin of EF/LINQ, which is adapted from LINQ, and dbplyr, which is adapted from dplyr. The query framework determines what processing nodes are available and how they operate. In principle, any query framework could be adapted to SQL databases by introducing just one new node, a node that loads the content of a database table. If we place this node at the beginning of a pipeline and make the rest of it out of regular nodes, we obtain a pipeline that processes data from a SQL database. However, this pipeline will be very inefficient compared to a SQL engine, which can use indexes to avoid loading the entire table into memory and thus can process the same data much faster. This is why EF/LINQ and dbplyr generate a SQL query that replaces the pipeline as a whole. The pipeline itself no longer runs directly, but now serves as a specification, with the assumption that if it were to run, it would produce the same output as the SQL query. This method of transforming a general-purpose query framework to a SQL query builder is called SQL pushdown.

However, SQL pushdown has a serious limitation. A general-purpose query framework is not designed with SQL compatibility in mind. For this reason, some of the pipelines assembled within this framework cannot be converted to SQL. Even worse, many useful SQL queries have no equivalent pipelines and thus cannot be generated using SQL pushdown. Indeed, SQL accumulated a wide range of features and capabilities since it first appeared in 1974. The first revision of the SQL standard, SQL-86, already supported Cartesian products, filtering, grouping, aggregation, and correlated subqueries. The next revision, SQL-92, added many join types and introduced query nesting. SQL:1999 greatly expanded its analytical capabilities by adding two types of queries: recursive queries, for processing hierarchical data, and data cube queries, which generalize histograms, cross-tabulations, roll-ups, drill-downs, and sub-totals. The follow-up revision, SQL:2003, added support for aggregate functions over a running window. Admittedly, SQL is a quintessential enterprise abomination, a hodgepodge of features added to support every imaginable use case, but with inadequate syntax, weird gaps in functionality, and no regards to internal consistency. Nevertheless, the breadth of SQL's capabilities has not been matched by any other query framework, including LINQ or dplyr. So when we generate SQL queries using EF/LINQ or dbplyr, a large subset of these capabilities remains inaccessible.

FunSQL is a data-oriented query builder created specifically to expose full expressive power of SQL. Unlike EF/LINQ and dbplyr, FunSQL was not adapted from an existing query framework, but was carefully designed from scratch to match SQL's capabilities. These capabilities include, for example, support for correlated subqueries and lateral joins (with Bind node), aggregate and window functions (using Group and Partition nodes), as well as recursive queries (with Iterate node). This comprehensive support for SQL capabilities makes FunSQL the only SQL query builder suitable for assembling complex data processing pipelines. Moreover, even though FunSQL pipelines cannot be run directly, every FunSQL node has a well-defined data processing semantics, which means that, in principle, FunSQL could be developed into a full-blown query framework. This potentially opens a path for replacing SQL with an equally powerful, but a more coherent and expressive query language.

+end

Individual slots of this structure are populated by the corresponding pipeline nodes.

"Where" node acting on the syntax tree

This explains why the pipeline is insensitive to the order of the nodes. Indeed, as long as the content of the slots stays the same, it makes no difference in what order the slots are populated.

Pipeline is insensitive to the order of the nodes

This method of incrementally constructing a composite structure is known as the builder pattern. We can call the query builders that employ this pattern syntax-oriented.

Both data-oriented and syntax-oriented query builders are compositional: the difference is in the nature of the information processed by the units of composition. Data-oriented query builders incrementally refine the query output; syntax-oriented query builders incrementally assemble the SQL syntax tree. Their interfaces look almost identical, but their methods of operation are fundamentally different.

But which one is better? Syntax-oriented query builders have two definite advantages: they are easy to implement and they could support the full range of SQL features. Indeed, the interface of a syntax-oriented query builder is just a collection of builders for the SQL syntax tree. How complete the representation of the syntax tree determines how well various SQL features are supported.

On the other hand, syntax-oriented query builders are harder to use. As they directly represent the SQL grammar, they inherit all of its deficiencies. In particular, the rigid clause order makes it difficult to assemble complex data processing pipelines, especially when the arrangement of pipeline nodes is not predetermined.

A data-oriented query builder directly represents data processing nodes, which makes assembling data processing pipelines much more straightforward—as long as we can find the necessary nodes among those offered by the builder. But where does the builder get its collection of data processing nodes? And how can we tell if this collection is complete?

One way to implement a data-oriented query builder is to adapt a general-purpose query framework. Indeed, this is the origin of EF/LINQ, which is adapted from LINQ, and dbplyr, which is adapted from dplyr. The query framework determines what processing nodes are available and how they operate. In principle, any query framework could be adapted to SQL databases by introducing just one new node, a node that loads the content of a database table. If we place this node at the beginning of a pipeline and make the rest of it out of regular nodes, we obtain a pipeline that processes data from a SQL database. However, this pipeline will be very inefficient compared to a SQL engine, which can use indexes to avoid loading the entire table into memory and thus can process the same data much faster. This is why EF/LINQ and dbplyr generate a SQL query that replaces the pipeline as a whole. The pipeline itself no longer runs directly, but now serves as a specification, with the assumption that if it were to run, it would produce the same output as the SQL query. This method of transforming a general-purpose query framework to a SQL query builder is called SQL pushdown.

However, SQL pushdown has a serious limitation. A general-purpose query framework is not designed with SQL compatibility in mind. For this reason, some of the pipelines assembled within this framework cannot be converted to SQL. Even worse, many useful SQL queries have no equivalent pipelines and thus cannot be generated using SQL pushdown. Indeed, SQL accumulated a wide range of features and capabilities since it first appeared in 1974. The first revision of the SQL standard, SQL-86, already supported Cartesian products, filtering, grouping, aggregation, and correlated subqueries. The next revision, SQL-92, added many join types and introduced query nesting. SQL:1999 greatly expanded its analytical capabilities by adding two types of queries: recursive queries, for processing hierarchical data, and data cube queries, which generalize histograms, cross-tabulations, roll-ups, drill-downs, and sub-totals. The follow-up revision, SQL:2003, added support for aggregate functions over a running window. Admittedly, SQL is a quintessential enterprise abomination, a hodgepodge of features added to support every imaginable use case, but with inadequate syntax, weird gaps in functionality, and no regards to internal consistency. Nevertheless, the breadth of SQL's capabilities has not been matched by any other query framework, including LINQ or dplyr. So when we generate SQL queries using EF/LINQ or dbplyr, a large subset of these capabilities remains inaccessible.

FunSQL is a data-oriented query builder created specifically to expose full expressive power of SQL. Unlike EF/LINQ and dbplyr, FunSQL was not adapted from an existing query framework, but was carefully designed from scratch to match SQL's capabilities. These capabilities include, for example, support for correlated subqueries and lateral joins (with Bind node), aggregate and window functions (using Group and Partition nodes), as well as recursive queries (with Iterate node). This comprehensive support for SQL capabilities makes FunSQL the only SQL query builder suitable for assembling complex data processing pipelines. Moreover, even though FunSQL pipelines cannot be run directly, every FunSQL node has a well-defined data processing semantics, which means that, in principle, FunSQL could be developed into a full-blown query framework. This potentially opens a path for replacing SQL with an equally powerful, but a more coherent and expressive query language.