Hello!
I’m happy to announce that after far too long of a wait, we’ve finally released Rel8 1.5!
Rel8 is a Haskell library for interacting with PostgreSQL databases, built on top of the fantastic Opaleye library.
The main objectives of Rel8 are:
-
Conciseness: Users using Rel8 should not need to write boiler-plate code. By using expressive types, we can provide sufficient information for the compiler to infer code whenever possible.
-
Inferrable: Despite using a lot of type level magic, Rel8 aims to have excellent and predictable type inference.
-
Familiar: writing Rel8 queries should feel like normal Haskell programming.
There are a lot of changes in this release. Before we get to that, I’m aware that this release will be a bit of a flag day event for a lot of people, and I somewhat regret that. Hopefully all of these goodies will make up for it!
Here’s the full changelog for this release:
1.5.0.0 — 2024-03-19
Removed
- Removed
nullaryFunction. Insteadfunctioncan be called with(). (#258)
Added
-
Support PostgreSQL’s
inettype (which maps to the HaskellNetAddr IPtype). (#227) -
Rel8.materializeandRel8.Tabulate.materialize, which add a materialization/optimisation fence toSELECTstatements by binding a query to aWITHsubquery. Note that explicitly materialized common table expressions are only supported in PostgreSQL 12 an higher. (#180) (#284) -
Rel8.head,Rel8.headExpr,Rel8.last,Rel8.lastExprfor accessing the first/last elements ofListTables and arrays. We have also added variants forNonEmptyTables/non-empty arrays with the1suffix (e.g.,head1). (#245) -
Rel8 now has extensive support for
WITHstatements and data-modifying statements (PostgreSQL: Documentation: 16: 7.8. WITH Queries (Common Table Expressions)).This work offers a lot of new power to Rel8. One new possibility is “moving” rows between tables, for example to archive rows in one table into a log table:
import Rel8 archive :: Statement () archive = do deleted <- delete Delete { from = mainTable , using = pure () , deleteWhere = \foo -> fooId foo ==. lit 123 , returning = Returning id } insert Insert { into = archiveTable , rows = deleted , onConflict = DoNothing , returning = NoReturninvg }This
Statementwill compile to a single SQL statement - essentially:WITH deleted_rows (DELETE FROM main_table WHERE id = 123 RETURNING *) INSERT INTO archive_table SELECT * FROM deleted_rowsThis feature is a significant performant improvement, as it avoids an entire roundtrip.
This change has necessitated a change to how a
SELECTstatement is ran:selectnow will now produce aRel8.Statement, which you have torunto turn it into a HasqlStatement. Rel8 offers a variety ofrunfunctions depending on how many rows need to be returned - see the various family ofrunfunctions in Rel8’s documentation for more. -
Rel8.loopandRel8.loopDistinct, which allow writingWITH .. RECURSIVEqueries. (#180) -
Added the
QualifiedNametype for named PostgreSQL objects (tables, views, functions, operators, sequences, etc.) that can optionally be qualified by a schema, including anIsStringinstance. (#257) (#263) -
Added
queryFunctionforSELECTing from table-returning functions such asjsonb_to_recordset. (#241) -
TypeNamerecord, which gives a richer representation of the components of a PostgreSQL type name (name, schema, modifiers, scalar/array). (#263) -
Rel8.lengthandRel8.lengthExprfor getting the lengthListTables and arrays. We have also added variants forNonEmptyTables/non-empty arrays with the1suffix (e.g.,length1). (#268) -
Added aggregators
listCatandnonEmptyCatfor folding a collection of lists into a single list by concatenation. (#270) -
DBTypeinstance forFixedthat would map (e.g.)Microtonumeric(1000, 6)andPicotonumeric(1000, 12). (#280) -
aggregationFunction, which allows custom aggregation functions to be used. (#283) -
Add support for ordered-set aggregation functions, including
mode,percentile,percentileContinuous,hypotheticalRank,hypotheticalDenseRank,hypotheticalPercentRankandhypotheticalCumeDist. (#282) -
Added
index,index1,indexExpr, andindex1Exprfunctions for extracting individual elements fromListTables andNonEmptyTables. (#285) -
Rel8 now supports GHC 9.8. (#299)
Changed
-
Rel8’s API regarding aggregation has changed significantly, and is now a closer match to Opaleye.
The previous aggregation API had
aggregatetransform aTablefrom theAggregatecontext back into theExprcontext:myQuery = aggregate do a <- each tableA return $ liftF2 (,) (sum (foo a)) (countDistinct (bar a))This API seemed convenient, but has some significant shortcomings. The new API requires an explicit
Aggregatorbe passed toaggregate:myQuery = aggregate (liftA2 (,) (sumOn foo) (countDistinctOn bar)) do each tableAFor more details, see #235
-
TypeInformation'sdecoderfield has changed. Instead of taking aHasql.Decoder, it now takes aRel8.Decoder, which itself is comprised of aHasql.Decoderand anattoparsecParser. This is necessitated by the fix for #168; we generally decode things in PostgreSQL’s binary format (using aHasql.Decoder), but for nested arrays we now get things in PostgreSQL’s text format (for which we need anattoparsecParser), so must have both. MostDBTypeinstances that usemapTypeInformationorParseTypeInformation, orDerivingViahelpers likeReadShow,JSONBEncoded,EnumandCompositeare unaffected by this change. (#243) -
The
schemafield fromTableSchemahas been removed and the name field changed fromStringtoQualifiedName. (#257) -
nextval,functionandbinaryOperatornow take aQualifiedNameinstead of aString. (#262) -
functionhas been changed to accept a single argument (as opposed to variadic arguments). (#258) -
TypeInformation'stypeNameparameter fromStringtoTypeName. (#263) -
DBEnum'senumTypeNamemethod fromStringtoQualifiedName. (#263) -
DBComposite'scompositeTypeNamemethod fromStringtoQualifiedName. (#263) -
Changed
Upsertby adding apredicatefield, which allows partial indexes to be specified as conflict targets. (#264) -
The window functions
lag,lead,firstValue,lastValueandnthValuecan now operate on entire rows at once as opposed to just single columns. (#281)
Happy querying!