Skip to content

Releases: scalamolecule/molecule

v0.15.0 Unenforced FK constraints

20 Dec 16:11
Compare
Choose a tag to compare

Using Molecule with sbt-molecule 1.11.0 no longer enforces foreign key constraints for SQL databases.

Power and orphans

This is a double-edged sword since we can then delete any entity we want now.

But if another entity references it, that reference id becomes an orphan reference pointing nowhere (to the deleted entity). So we get freedom to delete what we want at the cost of risking having orphaned reference ids hanging around.

Avoiding orphan refs

To avoid orphan ref ids, we can either

  • delete orphan ref ids manually,
  • add a foreign key constraint to our database schema or
  • not care

Adding a foreign key constraint

In the generated SQL schemas, you can copy any foreign key constraints you'd like to enforce and copy them to your live schema. Here's an example from an H2 schema:

// ALTER TABLE A ADD CONSTRAINT `_a` FOREIGN KEY (a) REFERENCES A (id);

v0.14.1 Transaction handling

19 Dec 17:58
Compare
Choose a tag to compare

3 new transaction handling methods are introduced for SQL databases on the jvm side.

Examples shown with the sync api - could as well be async, zio or io apis

Transaction bundle

Transact multiple actions in one commit with transact(<actions...>)

transact(
  Ns.int(1).save,         
  Ns.int.insert(2, 3),    
  Ns(1).delete,           
  Ns(3).int.*(10).update, 
) // result: List(2, 30)

It's easier to write than calling .transact on all 4 actions

Ns.int(1).save.transact         
Ns.int.insert(2, 3).transact   
Ns(1).delete.transact          
Ns(3).int.*(10).update.transact

Rollback all on any error

transact(<actions..>) rolls back all actions if any fails. If for instance Type.int has a validation defined in the Data Model that requires integers to be larger than 1, then both of the following actions will be rolled back:

try {
  transact(
    Type.int.insert(4, 5),
    Type.int(1).save, // not valid, triggers rollback of both actions
  )
} catch {
  case ValidationErrors(errorMap) =>
    errorMap.head._2.head ==>
      s"""Type.int with value `1` doesn't satisfy validation:
         |_ > 2
         |""".stripMargin
}

Unit of work

When you need to transact multiple actions and also at the same time query things, you can use unitOfWork:

// Initial balance in two bank accounts
Ns.s("fromAccount").int(100).save.transact
Ns.s("toAccount").int(50).save.transact

try {
  unitOfWork {
    Ns.s_("fromAccount").int.-(200).update.transact
    Ns.s_("toAccount").int.+(200).update.transact

    // Check that fromAccount had sufficient funds
    if (Ns.s_("fromAccount").int.query.get.head < 0) {
      // Abort all transactions in this unit of work
      throw new Exception(
        "Insufficient funds in fromAccount..."
      )
    }
  }
} catch {
  case e: Exception =>
    // Do something with failure...
    e.getMessage ==> "Insufficient funds in fromAccount..."
}

// No data transacted
Ns.s.int.query.get ==> List(
  ("fromAccount", 100),
  ("toAccount", 50),
)

Savepoints

Rollback scoped actions within a unitOfWork with savepoint:

Ns.int.insert(1 to 4).transact
Ns.int(count).query.get.head ==> 4

unitOfWork {
  savepoint { sp =>
    // Delete all
    Ns.int_.delete.transact
    Ns.int(count).query.get.head ==> 0

    // Roll back delete action within savepoint scope
    sp.rollback()
    Ns.int(count).query.get.head ==> 4
  }
}

// Nothing deleted
Ns.int(count).query.get.head ==> 4

Throwing an exception instead of calling sp.rollback() has the same effect.

Using savepoints can be a convenient way of rolling back only parts of a larger body of transactions/unitOfWork. Savepoints can even be nested (see tests).

v0.13.0 Computed Attribute values

08 Dec 23:01
Compare
Choose a tag to compare

Molecule can now update and query computed attribute values.

Computed update operations

A classical example is increasing a counter where the current value is increased by 1:

// Current counter == c
val (id, c) = Game.id.counter.query.get.head

// Increase counter value with 1
Game(id).counter.+(1).update.transact

// Counter has increased
Game.counter.query.get.head == c + 1

Number updates

If a number attribute has value 4, we can see what update operations we can perform on it:

// Arithmetic
Something(id).number.+(1).update.transact // 4 + 1 => 5 
Something(id).number.-(1).update.transact // 4 - 1 => 3
Something(id).number.*(2).update.transact // 4 * 2 => 8
Something(id).number./(2).update.transact // 4 / 2 => 2

//  4 => -4
// -4 =>  4
Something(id).number.negate.update.transact

//  4 => 4
// -4 => 4
Something(id).number.abs.update.transact

//  4 => -4
// -4 => -4
Something(id).number.absNeg.update.transact

//  4.4 =>  5
// -4.4 => -4
Something(id).number.ceil.update.transact

//  4.4 =>  4
// -4.4 => -5
Something(id).number.floor.update.transact

String updates

With a string attribute having value Hello we can do the following update operations:

Something(id).string.+(" World").update.transact // => "Hello World" 

Something(id).string.prepend("Say ").update.transact // => "Say Hello" 

// Substring from start to end indexes
Something(id).string.substring(2, 4).update.transact // => "ll" 

Something(id).string.replaceAll("l", "_").update.transact // => "He__o"
// regex
Something(id).string.replaceAll("[h-z]", "-").update.transact // => "He---" 

Something(id).string.toLower.update.transact // => "hello" 
Something(id).string.toUpper.update.transact // => "HELLO" 

Boolean updates

With a bool attribute having value true we can do the following update operations:

// AND
Something(id).bool.&&(true).update.transact // =>  true 
Something(id).bool.&&(false).update.transact // => false

// OR
Something(id).bool.||(true).update.transact // =>  true 
Something(id).bool.||(false).update.transact // => true 

// NOT
Something(id).bool.!.update.transact // => false 

Computed query filters

Some operations on numbers were added to generate computed query filters based on the attribute value:

Integers

Int, Long, BigInt, Short, Byte

Something.int.even.query.get // List(0, 2, 4) 
Something.int.odd.query.get //  List(1, 3, 5) 

// Modulo
Something.int.%(3, 0).query.get //  List(0, 3) 
Something.int.%(3, 1).query.get //  List(1, 4) 
Something.int.%(3, 2).query.get //  List(2, 5)

Decimals

Double, BigDecimal, Float

Something.dec.query.get // List(1.1, 2.2, 3.3) 

Something.dec.floor.query.get // List(1, 2, 3) 
Something.dec.ceil.query.get //  List(2, 3, 4) 

v0.12.1 Map attr ops aligned

01 Oct 12:06
Compare
Choose a tag to compare

Map attributes can now be filtered by key or value in a consistent way that corresponds to operations on a Scala Map. This should make working with Map attributes in Molecule feel natural for a Scala programmer.

Examples

We'll use the following example of a Map attribute capturing Shostakovich's name in different languages and see how we can use the operations available.

val namesMap = Map(
  "en" -> "Shostakovich",
  "de" -> "Schostakowitsch",
  "fr" -> "Chostakovitch",
)
Person.name("Shostakovich").langNames(namesMap).save.transact

Filter by key

attr(key)

Retrieve a value by applying a key to a mandatory Map attribute:

// Get German spelling of Shostakovich 
Person.langNames("de").query.get ==> List("Schostakowitsch")

Equivalent to calling apply on a Scala Map

namesMap("de") ==> "Schostakowitsch"

Looking up a non-existing key simply returns an empty result

Person.langNames("xx").query.get ==> Nil

attr_(key)

Ensure a certain key is present by applying the key to a tacit Map attribute:

// Get (English) name having a German spelling 
Person.name.langNames_("de").query.get ==> List("Shostakovich") 
Person.name.langNames_("xx").query.get ==> Nil

attr_?(key)

Retrieve an optional value by applying a key to an optional Map attribute:

Person.langNames_?("de").query.get.head ==> Some("Schostakowitsch")
Person.langNames_?("xx").query.get.head ==> None

Equivalent to calling get on a Scala Map

namesMap.get("de") ==> Some("Schostakowitsch")
namesMap.get("xx") ==> None

attr.not(keys)

Get Maps not having a certain key by applying the key to not of a mandatory Map attribute:

// Get langNames maps without a Spanish spelling
Person.langNames.not("es").query.get ==> List(namesMap)

// Get langNames maps without an English spelling
Person.langNames.not("en").query.get ==> Nil

Multiple keys kan be applied as varargs or a Seq

// Get langNames maps without Spanish or Chinese spelling
Person.langNames.not("es", "cn").query.get ==> List(namesMap)
Person.langNames.not(Seq("es", "cn")).query.get ==> List(namesMap)

// One of the keys exists, so no match 
Person.langNames.not(List("es", "en")).query.get ==> Nil

attr_.not(keys)

Match Maps not having a certain key by applying the key to not of a tacit Map attribute:

// Match langNames maps without a Spanish spelling
Person.name.langNames_.not("es").query.get ==> List("Shostakovich")

// Match langNames maps without an English spelling
Person.name.langNames_.not("en").query.get ==> Nil

Multiple keys kan be applied as varargs or a Seq

// Match langNames maps without Spanish or Chinese spelling
Person.name.langNames_.not("es", "cn").query.get ==> List("Shostakovich")
Person.name.langNames_.not(Seq("es", "cn")).query.get ==> List("Shostakovich")

// One of the keys exists, so no match 
Person.name.langNames_.not(List("es", "en")).query.get ==> Nil

Filter by value

attr.has(values)

Return Maps that have certain values with has on a mandatory Map attribute:

// Get map if it has a spelling value of "Chostakovitch"
Person.langNames.has("Chostakovitch").query.get ==> List(namesMap)

// Get map if it has a spelling 
// value of "Chostakovitch" or "Sjostakovitj"
Person.langNames.has("Chostakovitch", "Sjostakovitj").query.get ==> List(namesMap)

attr_.has(values)

Match Maps that have certain values with has on a tacit Map attribute:

// Match map if it has a spelling value of "Chostakovitch"
Person.name.langNames_.has("Chostakovitch").query.get ==> List("Shostakovich")

Person.langNames_.has("Chostakovitch", "Sjostakovitj").query.get ==> List("Shostakovich")

Likewise we can ask for Map attributes without certain values:

attr.hasNo(values)

Return Maps without certain values using hasNo on a mandatory Map attribute:

// Get map if it doesn't have a spelling of "Sjostakovitj"
Person.langNames.hasNo("Sjostakovitj").query.get ==> List(namesMap)

// Get map if it doesn't have a spelling 
// value of "Chostakovitch" or "Sjostakovitj"
Person.langNames.hasNo("Chostakovitch", "Sjostakovitj").query.get ==> Nil

attr_.hasNo(values)

Match Maps without certain values using hasNo on a tacit Map attribute:

// Match map if it doesn't have a spelling value of "Sjostakovitj"
Person.name.langNames_.hasNo("Sjostakovitj").query.get ==> List("Shostakovich")

// Match map if it doesn't have a spelling 
// value of "Chostakovitch" or "Sjostakovitj"
Person.langNames_.hasNo("Chostakovitch", "Sjostakovitj").query.get ==> Nil

v0.11.0 Cats effect IO API

27 Sep 14:00
Compare
Choose a tag to compare

Molecule now supports returning data wrapped in a cats.effect.IO by importing the relevant database and API. So now there are 4 APIs to choose from:

Synchronous

import molecule.sql.postgres.sync._
       
val persons: List[(String, Int, String)] =
  Person.name.age.Address.street.query.get

Asynchronous (Future)

import molecule.sql.postgres.async._

val persons: Future[List[(String, Int, String)]] =
  Person.name.age.Address.street.query.get

ZIO

import molecule.sql.postgres.zio._

val persons: ZIO[Conn, MoleculeError, List[(String, Int, String)]] =
  Person.name.age.Address.street.query.get

IO

import molecule.sql.postgres.io._ 

val persons: cats.effect.IO[List[(String, Int, String)]] =
  Person.name.age.Address.street.query.get

Same molecules

As you can see the molecules are identical for each API. The import determines the return type. Likewise, transaction reports are wrapped as the queries for each API.

v0.10.1 RPC bug fixes

15 Aug 14:27
Compare
Choose a tag to compare

Fixing correct resolution in RPC calls when an attribute name collides with a reserved keyword of a database.

v0.10.0 Optional ref (left join)

11 Aug 14:49
Compare
Choose a tag to compare

Molecule now lets you make an optional cardinality-one reference to another namespace with ?. Data is returned as an Option with a value or tuple of ref attribute value(s):

A.i(1).save.transact

// Optional card-one ref (SQL left join) - no ref values as None
A.i.B.?(B.i).query.get ==> List(
  (1, None),
)

A.i(2).B.i(3).s("b").save.transact

// Optional card-one ref (SQL left join) - values in optional tuple
A.i.B.?(B.i.s).query.get ==> List(
  (1, None),
  (2, Some((3, "b"))),
)

The query translates to the following SQL:

SELECT DISTINCT
  A.i, B.i, B.s
FROM A
  LEFT JOIN B ON A.b = B.id
WHERE
  A.i IS NOT NULL;

Nested optional refs

We can "nest" optional references, also in inserts:

A.i.B.?(B.s.i.C.?(C.s.i)).insert(List(
  (1, None),
  (2, Some(("b", 2, None))),
  (3, Some(("b", 3, Some(("c", 3))))),
)).transact

A.i.B.?(B.s.i.C.?(C.s.i)).query.get ==> List(
  (1, None),
  (2, Some(("b", 2, None))),
  (3, Some(("b", 3, Some(("c", 3)))))
)

Which translates to 2 left joins, A -> B and B -> C

SELECT DISTINCT
  A.i, B.s, B.i, C.s, C.i
FROM A
  LEFT JOIN B ON A.b = B.id
  LEFT JOIN C ON B.c = C.id
WHERE
  A.i IS NOT NULL;

Adjacent optional refs

Use "adjacent" optional ref to make multiple optional refs/left joins from the initial namespace (A). Notice how a new definition starts outside the previous optional ref parenthesis.

A.i
  .B.?(B.i.s)
  .C.?(C.s.i).insert(List(
    (1, None, None),
    (2, Some((20, "b")), None),
    (3, None, Some(("c", 300))),
    (4, Some((40, "b")), Some(("c", 400))),
  )).transact

A.i
  .B.?(B.i.s)
  .C.?(C.s.i).query.get ==> List(
    (1, None, None),
    (2, Some((20, "b")), None),
    (3, None, Some(("c", 300))),
    (4, Some((40, "b")), Some(("c", 400))),
  )

Which translates to 2 left joins, A -> B and A -> C

SELECT DISTINCT
  A.i, B.i, B.s, C.s, C.i
FROM A
  LEFT JOIN B ON A.b = B.id
  LEFT JOIN C ON A.c = C.id
WHERE
    A.i IS NOT NULL;

In Datomic optional ref data is pulled (dummy default "__none__" value used for consistent output arities)

Nested:

[:find  ?b 
        (
          pull ?id1 [
            {(:A/b :default "__none__") [
              (:B/s :default "__none__")
              (:B/i :default "__none__")
              {(:B/c :default "__none__") [
                (:C/s :default "__none__")
                (:C/i :default "__none__")
              ]}
              ]
            }
          ]
        )  
 :where [?a :A/i ?b]
        [(identity ?a) ?id1]]

Adjacent:

[:find  ?b 
        (
          pull ?id1 [
            {(:A/b :default "__none__") [
              (:B/i :default "__none__")
              (:B/s :default "__none__")
            ]}
          ]
        ) 
        (
          pull ?id2 [
            {(:A/c :default "__none__") [
              (:C/s :default "__none__")
              (:C/i :default "__none__")
            ]}
          ]
        )  
 :where [?a :A/i ?b]
        [(identity ?a) ?id1]
        [(identity ?a) ?id2]]

v0.9.1 Scala 3 supported

09 Jul 08:12
Compare
Choose a tag to compare

Molecule now fully supports Scala 3.

A bug in the sbt-molecule plugin missed to pack new Scala 3 .tasty files into the generated jar file with Molecule boilerplate code. Now that it does, everything works like a charm.

Make sure to add the latest version of sbt-molecule in your project/plugins.sbt :

addSbtPlugin("org.scalamolecule" % "sbt-molecule" % "1.8.1")

v0.9.0 SQlite

05 Jul 11:57
Compare
Choose a tag to compare

This is a big release with a lot of fundamental changes.

Letting go of MongDB

Realising that Molecule likely won't appeal to the schema-less use cases of Mongo. Besides, Mongo with it's two data models (embedded/referenced) is a nightmare to implement. Possible, but likely not worth it.

Some background examples of thoughts against Mongo:
https://www.reddit.com/r/PostgreSQL/comments/19bkn8b/doubt_regarding_postgresql_vs_mongodb/
https://www.reddit.com/r/programming/comments/15qtfvf/goodbye_mongodb/

And an older one:
https://news.ycombinator.com/item?id=6712703

Welcoming SQlite

SQlite is now implemented in Molecule.

General improvements

  • Improved Map api and key/value handling.
  • More powerful and clear update/upsert semantics allowing updating across relationships!
  • Distinction between Seqs of all primitive types except Bytes that is saved in an Array for serialization etc.
  • General focusing of expression api.
  • Aggregation of collection types dropped to keep things simple and focused.
  • RawQuery now return List[List[Any]] - dropping futile attempt at returning a typed result. Raw is raw.

v0.8.0 MongoDB

25 Feb 20:28
Compare
Choose a tag to compare

MongoDB added with full implementation of the SPI.