- Breaking change: when a query is used in a scalar context, such as
an
INexpression, make it return the first column only (see #75). Previously, such query wouldSELECT NULLunless the query ends with an explicitSelect().
- Fix MySQL reflection. Thanks to Alexander Plavin.
- Add DuckDB support. Thanks to Alexander Plavin.
-
Fix
Joinincorrectly collapsing an outer branch when it may transform a NULL to a non-NULL value. -
Make
@funsqlmacro support operators≥,≤,≢,≡,≠,∉,∈as aliases for>=,<=,IS DISTINCT FROM,IS NOT DISTINCT FROM,<>,IN,NOT IN, thanks to Ashlin Harris.
-
Define: add parametersbeforeandafterfor specifying position of the defined columns. -
Introduce the
SQLColumntype to represent table columns. The type ofSQLTable.columnsis changed fromVector{Symbol}toOrderedDict{Symbol, SQLColumn}. -
Make
SQLTableanAbstractDict{Symbol, SQLColumn}. -
Add DataAPI-compatible metadata to catalog objects
SQLCatalog,SQLTable, andSQLColumn. -
Add a field
SQLString.columnswith an optionalVector{SQLColumn}representing output columns of the SQL query. -
Support docstrings in
@funsqlnotation. -
Remove support for
constand variable assignment syntax from@funsqlnotation. -
Use a simpler and more consistent rule for collapsing JOIN branches (fixes #60).
- Wrap a branch of
UNION ALLin a subquery if it containsORDER BYorLIMITclause.
-
Add support for grouping sets, which are used in SQL to calculate totals and subtotals. The
Group()node accepts an optional parametersets, which is either a grouping mode indicator:cubeor:rollup, or a collection of grouping key setsVector{Vector{Symbol}}. Examples:From(:person) |> Group(:year_of_birth, sets = :cube) From(:person) |> Group(:year_of_birth, :month_of_birth, sets = :rollup) From(:person) |> Group(:year_of_birth, :gender_concept_id, sets = [[:year_of_birth], [:gender_concept_id]])
This release introduces some backward-incompatible changes. Before upgrading, please review these notes.
-
Type resolution has been refactored to allow assembling query fragments based on the type information.
-
Type checking is now more strict. A
Definefield or an optionalJoinwill be validated even when they are to be elided. -
Resolution of ambiguous column names in
Joinhas been changed in favor of the right branch. Previously, an ambiguous name would cause an error. -
Node-bound references are no longer supported. The following query will fail:
qₚ = From(:person) qₗ = From(:location) q = qₚ |> Join(qₗ, on = qₚ.location_id .== qₗ.location_id) |> Select(qₚ.person_id, qₗ.state)
Use nested references instead:
q = @funsql begin from(person) join( location => from(location), location_id == location.location_id) select(person_id, location.state) end
This release introduces some backward-incompatible changes. Before upgrading, please review these notes.
- The
SQLTableconstructor drops theschemaparameter. Instead, it now accepts an optional vector ofqualifiers. - Bare
Append(p, q)now producesp UNION ALL q. Previously, bareAppend(p, q)would be interpreted asSelect() |> Append(p, q). - Add Spark dialect.
- Update nodes
Group()andPartition()to accept an optional parametername, which speficies the field that holds the group data. - Add
Over()node such thatp |> Over(q)is equivalent toq |> With(p). - Add the
@funsqlmacro, described below.
This release introduces @funsql macro, which provides a new, concise notation
for building FunSQL queries. Example:
using FunSQL
q = @funsql from(person).filter(year_of_birth > 1950).select(person_id)This is equivalent to the following query:
using FunSQL: From, Get, Select, Where
q = From(:person) |> Where(Get.year_of_birth .> 1950) |> Select(Get.person_id)The @funsql notation reduces syntactic noise, making queries prettier and
faster to write. Semantically, any query that can be constructed with @funsql
could also be constructed without the macro, and vice versa. Moreover, these
two notations could be freely mixed:
@funsql from(person).$(Where(Get.year_of_birth .> 1950)).select(person_id)
From(:person) |>
@funsql(filter(year_of_birth > 1950)) |>
Select(Get.person_id)In @funsql notation, the chain operator (|>) is replaced either with
period (.) or with block syntax:
@funsql begin
from(person)
filter(year_of_birth > 1950)
select(person_id)
endNames that are not valid identifiers should be wrapped in backticks:
@funsql begin
from(person)
define(`Patient's age` => 2024 - year_of_birth)
filter(`Patient's age` >= 16)
endMany SQL functions and operators are available out of the box:
@funsql from(location).define(city_state => concat(city, ", ", state))
@funsql from(person).filter(year_of_birth < 1900 || year_of_birth > 2024)Comparison chaining is supported:
@funsql from(person).filter(1950 < year_of_birth < 2000)The if statement and the ternary ? : operator are converted to a CASE
expression:
@funsql begin
from(person)
define(
generation =>
year_of_birth <= 1964 ? "Baby Boomer" :
year_of_birth <= 1980 ? "Generation X" :
"Millenial")
endA @funsql query can invoke any SQL function or even an arbitrary scalar
SQL expression:
@funsql from(location).select(fun(`SUBSTRING(? FROM ? FOR ?)`, zip, 1, 3))Aggregate and window functions are supported:
@funsql begin
from(person)
group()
select(
count(),
min(year_of_birth),
max(year_of_birth, filter = gender_concept_id == 8507),
median =>
agg(`(percentile_cont(0.5) WITHIN GROUP (ORDER BY ?))`, year_of_birth))
end
@funsql begin
from(visit_occurrence)
partition(person_id, order_by = [visit_start_date])
filter(row_number() <= 1)
endCustom scalar and aggregate functions can be integrated to @funsql notation:
const funsql_substring = FunSQL.FunClosure("SUBSTRING(? FROM ? FOR ?)")
@funsql from(location).select(substring(zip, 1, 3))
const funsql_median = FunSQL.AggClosure("(percentile_cont(0.5) WITHIN GROUP (ORDER BY ?))")
@funsql from(person).group().select(median(year_of_birth))In general, any Julia function with a name funsql_f() can be invoked as f()
within @funsql macro. For example:
funsql_concept(v, cs...) =
@funsql from(concept).filter(vocabulary_id == $v && in(concept_id, $cs...))
funsql_ICD10CM(cs...) =
@funsql concept("ICD10CM", $cs...)For convenience, @funsql macro can wrap such function definitions:
@funsql concept(v, cs...) = begin
from(concept)
filter(vocabulary_id == $v && in(concept_id, $cs...))
end
@funsql ICD10CM(cs...) =
concept("ICD10CM", $cs...)Or even:
@funsql begin
concept(v, cs...) = begin
from(concept)
filter(vocabulary_id == $v && in(concept_id, $cs...))
end
ICD10CM(cs...) =
concept("ICD10CM", $cs...)
endHere are some other SQL features expressed in @funsql notation. JOIN and
GROUP BY:
@funsql begin
from(person)
filter(between(year_of_birth, 1930, 1940))
join(
location => from(location).filter(state == "IL"),
on = location_id == location.location_id)
left_join(
visit_group => begin
from(visit_occurrence)
group(person_id)
end,
on = person_id == visit_group.person_id)
select(
person_id,
latest_visit_date => visit_group.max(visit_start_date))
endORDER BY and LIMIT:
@funsql from(person).order(year_of_birth.desc()).limit(10)UNION ALL:
@funsql begin
append(
from(measurement).define(date => measurement_date),
from(observation).define(date => observation_date))
endRecursive queries:
@funsql begin
define(n => 1, f => 1)
iterate(define(n => n + 1, f => f * (n + 1)).filter(n <= 10))
endQuery parameters:
@funsql from(person).filter(year_of_birth >= :THRESHOLD)- Fix a number of problems with serializing
Order()andLimit()for MS SQL Server. - Add a column alias to the dummy
NULLwhen generating zero-column output.
No changes since the last release, enable Zenodo integration.
This release introduces some backward-incompatible changes. Before upgrading, please review these notes.
-
The
Iterate()node, which is used for assembling recursive queries, has been simplified. In the previous version, the argument ofIterate()must explicitly refer to the output of the previous iteration:Base() |> Iterate(From(:iteration) |> IterationStep() |> As(:iteration))
In v0.11.0, this query is written as:
Base() |> Iterate(IterationStep())
Alternatively, the output of the previous iteration can be fetched with the
From(^)node:Base() |> Iterate(From(^) |> IterationStep())
-
Function nodes with names
Fun."&&",Fun."||", andFun."!"are now serialized as logical operatorsAND,OR, andNOT. Broadcasting notationp .&& q,p .|| q,.!pis also supported.FunSQL interpretation of
||conflicts with SQL dialects that use||to represent string concatenation. Other dialects use functionconcat. In FunSQL, always useFun.concat, which will pick correct serialization depending on the target dialect. -
Rendering of
Funnodes can now be customized by overriding the methodserialize!(), which is dispatched on the node name. The following names have customized serialization:and,between,case,cast,concat,current_date,current_timestamp,exists,extract,in,is_not_null,is_null,like,not,not_between,not_exists,not_in,not_like,or. -
Introduce template notation for representing irregular SQL syntax. For example,
Fun."SUBSTRING(? FROM ? FOR ?)"(Get.zip, 1, 3)generatesSUBSTRING("location_1"."zip" FROM 1 to 3).In general, the name of a
Funnode is interpreted as a function name, an operator name, or a template string:-
If the name has a custom
serialize!()method, it is used for rendering the node. Example:Fun.in(Get.zip, "60614", "60615"). -
If the name contains a placeholder character
?, it is interpreted as a template. Placeholders are replaced with the arguments of theFunnode. Use??to represent a literal?character. Wrap the template in parentheses if this is necessary to make the syntax unambiguous. -
If the name contains only symbol characters, or if the name starts or ends with a space, it is interpreted as an operator name. Examples:
Fun."-"(2020, Get.year_of_birth),Fun." ILIKE "(Get.city, "Ch%"),Fun." COLLATE \"C\""(Get.state),Fun."CURRENT_TIME "(). -
Otherwise, the name is interpreted as a function name. The function arguments are separated by a comma and wrapped in parentheses. Examples:
Fun.now(),Fun.coalesce(Get.city, "N/A").
-
-
Aggnodes also supportserialize!()and template notation. Custom serialization is provided forAgg.count()andAgg.count_distinct(arg). -
Remove
distinctflag fromAggnodes. To representCOUNT(DISTINCT …), useAgg.count_distinct(…). Otherwise, use template notation, e.g.,Agg."string_agg(DISTINCT ?, ? ORDER BY ?)"(Get.state, ",", Get.state). -
Function names are no longer automatically converted to upper case.
-
Remove ability to customize translation of
Funnodes to clause tree by overriding thetranslate()method. -
Remove clauses
OP,KW,CASE, which were previously used for expressing operators and irregular syntax. -
Add support for table-valued functions. Such functions return tabular data and must appear in a
FROMclause. Example:From(Fun.regexp_matches("2,3,5,7,11", "(\\d+)", "g"), columns = [:captures]) |> Select(Fun."CAST(?[1] AS INTEGER)"(Get.captures))
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")
-
To prevent duplicating SQL expressions,
Define()andGroup()may create an additional subquery (#11). Take, for example:From(:person) |> Define(:age => 2020 .- Get.year_of_birth) |> Where(Get.age .>= 16) |> Select(Get.person_id, Get.age)
In the previous version, the definition of
ageis replicated in bothSELECTandWHEREclauses:SELECT "person_1"."person_id", (2020 - "person_1"."year_of_birth") AS "age" FROM "person" AS "person_1" WHERE ((2020 - "person_1"."year_of_birth") >= 16)
In v0.11.0,
ageis evaluated once in a nested subquery:SELECT "person_2"."person_id", "person_2"."age" FROM ( SELECT "person_1"."person_id", (2020 - "person_1"."year_of_birth") AS "age" FROM "person" AS "person_1" ) AS "person_2" WHERE ("person_2"."age" >= 16)
-
Similarly, aggregate expressions used in
Bind()are evaluated in a separate subquery (#12). -
Fix a bug where FunSQL fails to generate a correct
SELECT DISTINCTquery when key columns ofGroup()are not used by the following nodes. For example, the following query pipeline would fail to render SQL:From(:person) |> Group(Get.year_of_birth) |> Group() |> Select(Agg.count())
Now it correctly renders:
SELECT count(*) AS "count" FROM ( SELECT DISTINCT "person_1"."year_of_birth" FROM "person" AS "person_1" ) AS "person_2"
-
Article Two Kinds of SQL Query Builders is added to documentation.
-
Where possible, pretty-printing replaces
Litnodes with their values.
- Generate aliases for CTE tables (fixes #33).
Fromcan take aDataFrameor anyTables.jl-compatible source.- More examples.
- Add
SQLCatalogtype that encapsulates information about database tables, the target SQL dialect, and a cache of rendered queries. - Add function
reflectto retrieve information about available database tables. From(::Symbol)can refer to a table in the database catalog.- Add a dependency on
DBInterfacepackage. - Add type
SQLConnection <: DBInterface.Connectionthat combines a raw database connection and the database catalog. Also addSQLStatement <: DBInterface.Statement. - Implement
DBInterface.connectto create a database connection and callreflectto retrieve the database catalog. - Implement
DBInterface.prepareandDBInterface.executeto render a query node to SQL and immediately compile or execute it. - Allow
renderto take aSQLConnectionor aSQLCatalogargument. - Rename
SQLStatementtoSQLString, drop thedialectfield. - Update the guide and the examples to use the DBInterface API and improve many docstrings.
- Compatibility with PrettyPrinting 0.4.
Join: addoptionalflag to omit theJOINclause in the case when the data from the right branch is not used by the query.With: addmaterializedflag to make CTEs withMATERIALIZEDandNOT MATERIALIZEDannotations.- Add
WithExternalnode that can be used to prepare the definition for aCREATE TABLE ASor aSELECT INTOstatement. - Rearranging clause types: drop
CTEclause; addcolumnstoASclause; addNOTEclause.
- Add
Iteratenode for making recursive queries. - Add
WithandFrom(::Symbol)nodes for assigning a name to an intermediate dataset. From(nothing)for making a unit dataset.- Rename
Select.list,Append.list, etc toargs. - More documentation updates.
- Require Julia ≥ 1.6.
- Render each argument on a separate line for
SELECT,GROUP BY,ORDER BY, as well as for a top-levelANDinWHEREandHAVING. - Improve
SQLDialectinterface. - Add Jacob's
CASEexample.
- Update documentation and examples.
- Fix quoting of MySQL identifiers.
- Refactor the SQL translator to make it faster and easier to maintain.
- Improve error messages.
- Include columns added by
Defineto the output. - Report an error when
Aggis used withoutGroup. - Deduplicate identical aggregates in a
Groupsubquery. - Support for
WITHclause. - Update the Tutorial/Usage Guide.
- Add
Order,Asc,Desc. - Add
Limit. Fun.inwith a list,Fun.current_timestamp.
- Add
Appendfor creatingUNION ALLqueries.
- Support for specifying the window frame.
- Add
Definefor calculated columns. - Correlated subqueries and
JOIN LATERALwithBind. - Support for query parameters with
Var. - Support for subqueries.
- Add
Partitionand window functions.
- Add
Group. - Add aggregate expressions.
- Collapse
WHEREthat followsGROUPtoHAVING. - Add
LeftJoinalias forJoin(..., left = true).
- Add
Join.
- Flatten nested SQL subqueries.
- Broadcasting syntax for SQL functions.
- Special
Fun.<name>notation for SQL functions. - Support for irregular syntax for functions and operators.
- Outline of the documentation.
- Use
letnotation for displayingSQLNodeobjects. - Add
Highlightnode. - Highlighting of problematic nodes in error messages.
- Initial release.
From,Select, andWhereare implemented.