Skip to content

Commit

Permalink
Merge pull request #1 from davenverse/enablePureCross
Browse files Browse the repository at this point in the history
Enable Pure Cross Sqlite
  • Loading branch information
ChristopherDavenport authored Sep 27, 2022
2 parents 507c66c + 45ff49e commit 52e595b
Show file tree
Hide file tree
Showing 10 changed files with 293 additions and 6 deletions.
26 changes: 23 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
os: [ubuntu-latest]
scala: [2.13.7, 3.1.1]
java: [temurin@8]
project: [rootJS]
project: [rootJS, rootJVM]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout current branch (full)
Expand Down Expand Up @@ -86,11 +86,11 @@ jobs:

- name: Make target directories
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main')
run: mkdir -p examples/target target .js/target site/target .jvm/target .native/target core/target project/target
run: mkdir -p cross/js/target examples/target target examples-cross/.js/target .js/target site/target cross/jvm/target .jvm/target .native/target core/target examples-cross/.jvm/target project/target

- name: Compress target directories
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main')
run: tar cf targets.tar examples/target target .js/target site/target .jvm/target .native/target core/target project/target
run: tar cf targets.tar cross/js/target examples/target target examples-cross/.js/target .js/target site/target cross/jvm/target .jvm/target .native/target core/target examples-cross/.jvm/target project/target

- name: Upload target directories
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main')
Expand Down Expand Up @@ -153,6 +153,16 @@ jobs:
tar xf targets.tar
rm targets.tar
- name: Download target directories (2.13.7, rootJVM)
uses: actions/download-artifact@v2
with:
name: target-${{ matrix.os }}-${{ matrix.java }}-2.13.7-rootJVM

- name: Inflate target directories (2.13.7, rootJVM)
run: |
tar xf targets.tar
rm targets.tar
- name: Download target directories (3.1.1, rootJS)
uses: actions/download-artifact@v2
with:
Expand All @@ -163,6 +173,16 @@ jobs:
tar xf targets.tar
rm targets.tar
- name: Download target directories (3.1.1, rootJVM)
uses: actions/download-artifact@v2
with:
name: target-${{ matrix.os }}-${{ matrix.java }}-3.1.1-rootJVM

- name: Inflate target directories (3.1.1, rootJVM)
run: |
tar xf targets.tar
rm targets.tar
- name: Import signing key
if: env.PGP_SECRET != '' && env.PGP_PASSPHRASE == ''
run: echo $PGP_SECRET | base64 -di | gpg --import
Expand Down
44 changes: 41 additions & 3 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ ThisBuild / scalaVersion := Scala213
ThisBuild / testFrameworks += new TestFramework("munit.Framework")

val catsV = "2.7.0"
val catsEffectV = "3.3.12"
val fs2V = "3.2.7"
val catsEffectV = "3.3.14"
val fs2V = "3.2.9"
val http4sV = "0.23.11"
val circeV = "0.14.2"
val doobieV = "1.0.0-RC2"
Expand All @@ -28,7 +28,7 @@ val munitCatsEffectV = "1.0.7"

// Projects
lazy val `sqlite-sjs` = tlCrossRootProject
.aggregate(core, examples)
.aggregate(core, examples, cross, `examples-cross`)

lazy val core = project.in(file("core"))
.enablePlugins(ScalaJSPlugin)
Expand All @@ -40,6 +40,7 @@ lazy val core = project.in(file("core"))
"sqlite" -> "4.1.1",
"sqlite3" -> "5.0.9"
),
mimaPreviousArtifacts := Set(),
scalaJSLinkerConfig ~= { _.withModuleKind(ModuleKind.CommonJSModule)},

libraryDependencies ++= Seq(
Expand Down Expand Up @@ -67,6 +68,43 @@ lazy val examples = project
scalaJSUseMainModuleInitializer := true,
)

lazy val cross = crossProject(JVMPlatform, JSPlatform)
.crossType(CrossType.Full)
.in(file("cross"))
.settings(
name := "sqlite-sjs-cross",
mimaPreviousArtifacts := Set(),
libraryDependencies ++= Seq(
"io.circe" %%% "circe-core" % circeV,
)
).jsSettings(
scalaJSLinkerConfig ~= { _.withModuleKind(ModuleKind.CommonJSModule)},
).jsConfigure { project => project.dependsOn(core) }
.jvmSettings(
libraryDependencies ++= Seq(
"org.tpolecat" %% "doobie-core" % doobieV,
"org.xerial" % "sqlite-jdbc" % "3.36.0.3",
)
)

lazy val `examples-cross` = crossProject(JVMPlatform, JSPlatform)
.crossType(CrossType.Pure)
.in(file("examples-cross"))
.enablePlugins(NoPublishPlugin)
.settings(
libraryDependencies ++= Seq (
"io.circe" %%% "circe-generic" % circeV,
"co.fs2" %%% "fs2-io" % fs2V,
)
)
.dependsOn(cross)
.jsConfigure(project => project.enablePlugins(ScalaJSBundlerPlugin))
.jsSettings(
scalaJSLinkerConfig ~= { _.withModuleKind(ModuleKind.CommonJSModule)},
scalaJSUseMainModuleInitializer := true,
)


// lazy val shims = project
// .in(file("shims"))
// .enablePlugins(NoPublishPlugin)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package io.chrisdavenport.sqlitesjs.cross

// import io.chrisdavenport.sqlitesjs.Sqlite

trait PackagePlatform {
// Warning: SJS encodes Booleans as Ints. If using boolean it will fail, use IntBoolean directly
// for automatic or contramap from Decoder[IntBoolean] to build your decoder for use with booleans.
type Write[A] = io.circe.Encoder[A]
type Read[A] = io.circe.Decoder[A]

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package io.chrisdavenport.sqlitesjs.cross

import cats.effect.kernel._
import cats.syntax.all._
import io.chrisdavenport.sqlitesjs.Sqlite
import io.circe.{Json, JsonNumber, JsonObject}

trait SqliteCrossCompanionPlatform {
def impl[F[_]: Async](path: String): Resource[F, SqliteCross[F]] = {
Sqlite.fromFile(path).map(new SqliteCrossJSImpl[F](_))
}

private class SqliteCrossJSImpl[F[_]: Async](sqlite: Sqlite[F]) extends SqliteCross[F]{

private def encode[A: Write](a: A): List[Json] = {
val json = io.circe.Encoder[A].apply(a)
def toList(json: Json): List[Json] = {
json.fold(
jsonNull = List(Json.Null),
jsonBoolean = {(bool: Boolean) => List({if (bool) Json.fromInt(1) else Json.fromInt(0)})}, // Throw maybe
jsonNumber = {(number: JsonNumber) => List(Json.fromJsonNumber(number))},
jsonString = {(s: String) => List(Json.fromString(s))},
jsonArray = {(v: Vector[Json]) => v.toList}, // Throw Maybe
jsonObject = {(obj: JsonObject) => obj.toList.flatMap(a => toList(a._2))} // Throw maybe?
)
}
toList(json)
}

def exec(sql: String): F[Unit] = sqlite.exec(sql)
def get[B: Read](sql: String): F[Option[B]] = sqlite.get(sql).flatMap(_.traverse(_.as[B]).liftTo[F])
def get[A: Write, B: Read](sql: String, write: A): F[Option[B]] = {
sqlite.get(sql, encode(write)).flatMap(_.traverse(_.as[B]).liftTo[F])
}

def all[B: Read](sql: String): F[List[B]] =
sqlite.all(sql).flatMap(_.traverse(_.as[B]).liftTo[F])
def all[A: Write, B: Read](sql: String, write: A): F[List[B]] =
sqlite.all(sql, encode(write)).flatMap(_.traverse(_.as[B]).liftTo[F])

def run(sql: String): F[Int] = sqlite.run(sql)
def run[A: Write](sql: String, write: A): F[Int] = sqlite.run(sql, encode(write))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package io.chrisdavenport.sqlitesjs.cross



trait PackagePlatform {
// type SqliteConnection[F[_]] = doobie.Transactor[F]
type Write[A] = doobie.Write[A]
type Read[A] = doobie.Read[A]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package io.chrisdavenport.sqlitesjs.cross

import doobie.{Write => _, Read => _, _}
import doobie.syntax.all._
import cats.effect.kernel._
import cats.syntax.all._

trait SqliteCrossCompanionPlatform {
def impl[F[_]: Async](path: String): Resource[F, SqliteCross[F]] = Resource.pure[F, SqliteCross[F]]{
val ts = Transactor.fromDriverManager[F]("org.sqlite.JDBC", s"jdbc:sqlite:${path.toString}", "", "")
new SqliteCrossJVMImpl[F](ts)
}

private class SqliteCrossJVMImpl[F[_]: MonadCancelThrow](ts: Transactor[F]) extends SqliteCross[F]{
def exec(sql: String): F[Unit] = Update(sql).run(()).transact(ts).void
def get[B: Read](sql: String): F[Option[B]] = Query0(sql).option.transact(ts)
def get[A: Write, B: Read](sql: String, write: A): F[Option[B]] =
Query[A, B](sql).option(write).transact(ts)

def all[B: Read](sql: String): F[List[B]] =
Query0(sql).to[List].transact(ts)
def all[A: Write, B: Read](sql: String, write: A): F[List[B]] =
Query[A, B](sql).to[List](write).transact(ts)

def run(sql: String): F[Int] = Update(sql).run(()).transact(ts)
def run[A: Write](sql: String, write: A): F[Int] = Update[A](sql).run(write).transact(ts)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package io.chrisdavenport.sqlitesjs.cross

import io.circe._
import cats.syntax.all._

// Use this rather than boolean in models, otherwise javascript platform will
// die.
case class IntBoolean(boolean: Boolean)
object IntBoolean {
implicit val decoder: Decoder[IntBoolean] = new Decoder[IntBoolean]{
def apply(c: HCursor): Decoder.Result[IntBoolean] =
Decoder[Int].emap{
case 0 => IntBoolean(false).asRight
case 1 => IntBoolean(true).asRight
case other => s"Invalid IntBoolean: $other".asLeft
}.or(Decoder.decodeBoolean.map(IntBoolean(_)))(c)
}

// implicit val decoder: Decoder[IntBoolean] = Decoder[Int].emap{
// case 0 => IntBoolean(false).asRight
// case 1 => IntBoolean(true).asRight
// case other => s"Invalid IntBoolean: $other".asLeft
// }

implicit val encoder: Encoder[IntBoolean] = Encoder[Int].contramap[IntBoolean]{
case IntBoolean(true) => 1
case IntBoolean(false) => 0
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package io.chrisdavenport.sqlitesjs.cross


trait SqliteCross[F[_]]{
def exec(sql: String): F[Unit]
// Uses Positional Encoding
def get[B: Read](sql: String): F[Option[B]]
def get[A: Write, B: Read](sql: String, write: A): F[Option[B]]


def all[B: Read](sql: String): F[List[B]]
def all[A: Write, B: Read](sql: String, write: A): F[List[B]]

def run(sql: String): F[Int]
def run[A: Write](sql: String, write: A): F[Int]
}

object SqliteCross extends SqliteCrossCompanionPlatform {}


Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package io.chrisdavenport.sqlitesjs

package object cross extends PackagePlatform {

}
83 changes: 83 additions & 0 deletions examples-cross/src/main/scala/Example.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import io.chrisdavenport.sqlitesjs.cross._

import cats.syntax.all._
import cats.effect._

object Example extends IOApp.Simple {

def run: IO[Unit] = SqliteCross.impl[IO]("testing/testCrossExample.sqlite").use{ cross =>

for {
_ <- cross.exec(createTableStatement)
init <- cross.all[Hello](select)
_ <- if (init.isEmpty) IO.unit else IO.println("Initial State:")
_ <- init.traverse(h => IO.println(h))
_ <- IO.println("Please Say Hello To Someone or say CLEAR:")
// Hangs on js
// who <- fs2.io.stdin[IO](4096).through(fs2.text.utf8.decode).takeThrough(_.contains("\n")).compile.string.map(_.trim())
who <- std.Console[IO].readLine // BROKEN
_ <- if (who === "CLEAR") clear(cross) else sayHello(cross, who)
} yield ()
}

def sayHello(cross: SqliteCross[IO], name: String): IO[Unit] = for {
now <- Clock[IO].realTime.map(_.toMillis)
record = Hello(name, now, IntBoolean(name.toUpperCase() === name))
_ <- cross.run(upsert, record)
_ <- IO.println("") >> IO.println("Current State:")
after <- cross.all[Hello](select)
_ <- after.traverse(h => IO.println(h))
} yield ()

def clear(cross: SqliteCross[IO]): IO[Unit] = {
cross.run(del).flatMap(i => IO.println(s"Cleared $i rows"))
}

case class Hello(name: String, lastAccessed: Long, allCaps: IntBoolean)
// IntBoolean is used to get booleans in a compatible fashion without custom encoding/generic logic.
// You can use a custom decoder which maps to Boolean from IntBoolean if you do want to prevent
// intermediates appearing in your code.
object Hello {
// Can use anything to get the decoder for this
// Generic for 2.12
// derives Codec.AsObject for 3
// implicit val codec: io.circe.Codec[Hello] = ???
// In this example I use semi-auto derivation
implicit val codec: io.circe.Codec[Hello] = io.circe.generic.semiauto.deriveCodec[Hello]

// An example of how to do this without use of custom type, but explicitly encoding/decoding
// implicit val decoder: Decoder[Hello] = new Decoder[Hello]{
// def apply(h: HCursor) = for {
// name <- h.downField("name").as[String]
// accessed <- h.downField("lastAccessed").as[Long]
// allCaps <- h.downField("allCaps").as[IntBoolean].map(_.boolean)
// } yield Hello(name, accessed, allCaps)
// }

// implicit val encoder: Encoder[Hello] = new Encoder[Hello]{
// def apply(a: Hello): Json = Json.obj(
// "name" -> a.name.asJson,
// "lastAccessed" -> a.lastAccessed.asJson,
// "allCaps" -> IntBoolean(a.allCaps).asJson
// )
// }
}

val createTableStatement = {
"""CREATE TABLE IF NOT EXISTS hello (
|name TEXT NOT NULL PRIMARY KEY,
|lastAccessed INTEGER NOT NULL,
|allCaps INTEGER NOT NULL)""".stripMargin
}

val select = {
"SELECT name,lastAccessed,allCaps FROM hello"
}

val upsert = {
"""INSERT OR REPLACE INTO hello (name, lastAccessed, allCaps) VALUES (?, ?, ?)"""
}

val del = """DELETE FROM hello"""

}

0 comments on commit 52e595b

Please sign in to comment.