What if SELECT, FROM, WHERE were functions?

remywang1 pts0 comments

prela

What if<br>SELECT, FROM, WHERE were<br>functions?

Check out this code:

movie WHERE ((company.country == "[us]") AND<br>(keyword == "character-name-in-title"))<br>SELECT (title AND cast.person.alias.text)

From a mile a way this just looks like a regular SQL query. If you<br>stare at it though, a few things are off: where's the FROM<br>clause? Why does the SELECT clause come last (like a<br>sane language)? And what's going on with<br>cast.person.alias.text? Is that JSON? Let me explain.<br>First, I'll strip away some syntax magic. The same code in Rust looks<br>like this:

movie.with(company.s(country).eq("[us]")<br>.and(keyword.eq("character-name-in-title"))<br>.select(title.and(cast.s(person).s(alias).s(text)))

Both snippets are queries in a new embedded query language/library<br>we're building called Prela. The<br>original snippet was actually in Scala, and each of WHERE,<br>AND, and SELECT is an infix function,<br>corresponding to Rust's .with, .and, and<br>.select. The == operator is overloaded and<br>corresponds to .eq. Finally, the . operator is<br>also overloaded; it corresponds to Rust's .s which is just<br>an alias for .select. In other words, each of the<br>keywords/operators is a function. But what do the functions<br>take in and produce?

The source code of Prela can be found here.

SQL is based on Codd's Relational<br>Algebra (RA) which is an algebra of relations, a.k.a. tables. Each<br>relational algebra operator takes in tables and produces a table. In<br>contrast, Prela is based on Tarski's Algebra<br>of Relations1 (TAR), which is much less known and<br>much older than Codd's Relational Algebra — although TAR is named after<br>Tarski, its main ideas date all the way back to De Morgan in the 1860s.<br>We've said RA is an algebra of tables — we can think of TAR as an<br>algebra of columns . In both code snippets, each of the<br>variables including movie, company,<br>country conceptually corresponds to a table column. More<br>concretely, they are represented as binary relations under the<br>hood. To understand what's going on, let's consider the following<br>table:

ID<br>title<br>year<br>keyword

646<br>The Godfather<br>1972<br>Crime

478<br>Seven Samurai<br>1954<br>War

583<br>Casablanca<br>1942<br>Romance

This table decomposes into 4 binary relations: the first maps each ID<br>to its row, and the other 3 map each row to its title, year, and<br>keyword, respectively. The special treatment of the ID column will make<br>sense later.

movie

ID

646

478

583

title

title

The Godfather

Seven Samurai

Casablanca

year

year

1972

1954

1942

keyword

keyword

Crime

War

Romance

We'll call the tables movie, title,<br>year, and keyword from left to right. Now<br>let's consider the simplest query we can write:<br>movie.select(title) in Rust, or<br>movie SELECT title in Scala. The .select<br>operator implements relation composition , which first<br>joins its arguments using the second column of the LHS and the first<br>column of the RHS, then throws away the join column. Formally,<br>r.select(s) is equivalent to the RA expression \pi_{r.1, s.2}(r \Join_{r.2 = s.1} s) where<br>r.i is the i-th column of r. Another way to understand relation<br>composition is to view it as a generalization of function composition,<br>in the same way (binary) relations generalize functions. A binary<br>relation is just a function that can map the same "input" to multiple<br>different "outputs". Let's write r[x]<br>to denote the values x maps to under<br>r as if "applying" r to x,<br>i.e., r[x] = \{y \mid (x, y) \in r\}.<br>Further generalize the notation to allow a set as an argument, i.e.,<br>r[\mathbf{x}] = \bigcup_{x\in \mathbf{x}}<br>r[x]. Then, the composition r.select(s) is exactly<br>\lambda \mathbf{x} . r[s[\mathbf{x}]]<br>(with further abuse of notation identifying a relation with its<br>"function"). That's all very abstract, so let's go back to our example.<br>The composition movie.select(title) joins the<br>movie relation (the ID relation) with the<br>title relation on row number. The result is a binary<br>relation mapping every movie ID to its title. This also explains why we<br>needed the row number on the second column of the ID table, because we<br>need to join on it. Similarly, movie.select(year) maps IDs<br>to years, and movie.select(keyword) maps IDs to<br>keywords.

Composition also plays the roles of joins. Say we add a<br>company column to the movie table, mapping<br>each movie's row number to the ID of its production company, and<br>decompose a separate company table with columns<br>id, name, and country.

company

company

657

188

353

id2row

ID

657

188

353

name

name

Paramount

Toho

Warner Bros.

country

country

USA

Japan

USA

The query movie.s(company).s(id2row).s(country) then<br>finds the country of a movie's production company, where<br>id2row maps company IDs to row numbers. Because joining on<br>a foreign key almost always means "resolving" the key to a row number<br>first, Prela automatically inserts that .s(id2row) step for<br>us, the same way Rust automatically dereferences pointers on field<br>access. This lets us simply write<br>movie.s(company).s(country), which reads as "movie's<br>company's country". This is also what...

movie select title company country keyword

Related Articles