Coder Social home page Coder Social logo

logograph's Introduction

Logograph

Summary

This library provides a typesafe Scala DSL for generating SQL queries and statements
These statements can then be executed against a Database using the Logograph Context,
which uses JDBC for connecting to the Database, or any other Database connection middleware
provided in the client application

By using Scala macros in order to generate the SQL statetements at compile-time,
SQL generation provides a useful abstraction while having no performance
overhead at runtime compared to running a raw SQL statement.

These SQL generation macros also perform validation on the SQL statements,
so the library user can detect and fix several kinds of errors without the need to execute
the statement against the Database. This also means that most SQL syntax errors are guaranteed
to never happen in production code.

The generated SQL statements can embed runtime values and are fully parameterised,
so there is no risk of SQL Injection attacks.

The DSL works in a similar fashion to some other SQL compile-time DSLs available for Scala,
for instance Quill, but it aims to provide a streamlined API, with a focus on simplicity
from the user standpoint, leaving some advanced functionality aside, in order to adapt better
to the most common business use cases, following a convention over configuration approac.

There is some in-project customization around both global naming conventions as well as explicit table and column names via the use of an optional logograph.conf file in the project. You can see an example here

For some examples of statements serialisation and execution (such as complex nested queries or query unions & intersections), you can check the unit or integration test folders.

Usage

In order to use this library, you would need to add it to the dependencies in build.sbt:
All the releases are cross-compiled and therefore available for both scala 3 & scala 2.13.
All the tests also run for both Scala versions, since the macro implementations differ between the language versions.

libraryDependencies += "io.github.albertprz" % "logograph" %% "0.1.0")

Logograph DSL aim is to reflect as closely as possible the underlying SQL representation,
so the API is very SQL like:

import com.albertprz.logograph._

val qry = from[(Person, Address, Telephone)].select {
  case (p, a, t) โ‡’ Query(
    Select          (Result (p.name, p.age, a.street, t.number))
    Where           (a.street like "%Baker St%",
                      p.name in names,
                      coalesce (p.isEmployer, false)),
    OrderBy         (desc (p.age)),
    LeftJoin (a)    (a.id === p.addressId),
    LeftJoin (t)    (t.id === p.telephoneId))
  }

The DSL is very concise and uses the same operator and functions that the SQL equivalent.
The SQL output for this query would be the following:

SELECT      p.[name], p.[age], a.[street], t.[number]
FROM        [Person] AS p
LEFT JOIN   [Address] AS a ON a.[id] = p.[addressId]
LEFT JOIN   [Telephone] AS t ON t.[id] = p.[telephoneId]
WHERE       (a.[street] like '%Baker St%') AND
            (p.[name] in (?, ?, ?)) AND
            (coalesce (p.[isEmployer], 0))
ORDER BY    p.[age] desc

And the parameters that were used in this query, for the runtime values are:

{@Application.names -> [John, Mark, Thomas]}

The query is generated in a fully typesafe manner. The query input tables must be specified by
case classes that extend the DbTable trait and the query result type must be a case class
that extends either the DbTable or the DbResult traits.
The qry value will be an object of type SelectStatement[Result], in this case.

// Database Table Models
case class Person (name: String, age: Int, isEmployer: Boolean, addressId: Int, telephoneId: Int)
                    extends DbTable
case class Address (id: Int, street: String) extends DbTable
case class Telephone (id: Int, number: String) extends DbTable


// Query Result Model
case class Result (name: String, age: Int, street: String, telephoneNumber: String) extends DbResult

Additionally the SQL Statement API methods ending in Debug, can be used in order to generate
a compile time error that will expose the SQL statement at compile time as well as the internal AST,
that was used to generate the SQL:

QueryClause (
  SelectClause ([Field (p, name), Field (p, age), Field (a, street), Field (t, number)]),
  FromClause ({p -> Person}), [
  LeftJoinClause (Address, a, [
    Operation (===, [Field (a, id), Field (p, addressId)])]),
  LeftJoinClause (Telephone, t, [
    Operation (===, [Field (t, id), Field (p, telephoneId)])])],
  WhereClause ([
    Operation (like, [Field (a, street),
      LiteralVal ("%Baker St%")]),
    Operation (in, [Field (p, name), Identity (?)]),
    Operation (coalesce, [Field (p, isEmployer), LiteralVal (0)])]),
  OrderByClause ([
    Operation (desc, [Field (p, age)])]))

The API also exposes Insert, Update & Delete statements, which have a common trait (SQLStatefulStament):

val stmts = Seq(insert(john),

                insert(johnAddress),

                update[Person] (p => (Map(p.name -> "Mark",
                                          p.age  -> 50),
                                      Where(p.age >= 10))),

                delete[Address] (a => Where(a.street <> "Baker Street"))

These statements will generate the following SQL output:

INSERT INTO [Person] ([name], [age], [isEmployer], [addressId], [telephoneId])
VALUES      (?, ?, ?, ?, ?)

INSERT INTO [Address] ([id], [street])
VALUES      (?, ?)

UPDATE      [Person]
SET         [name] = 'Mark',
            [age] = 50
WHERE       [age] >= 10

DELETE FROM [Address]
WHERE       [street] <> 'Baker Street'

The raw SQL and runtime params from any statement can be obtained at runtime
by accessing the sql and params fields:

(qry.sql, qry.params)

At last, the statements can be run against a Database by using implicitly / explicitly
a in-scope LogographContext instance, using the appropiate JDBC connection object

val conn: Connection
val qry: SelectStatement[Person]
val stmt: UpdateStatement[Address]

implicit val context = new LogographContext[IO](conn)
val resultsIO: IO[Seq[Person]] = qry.run()
val operationIO: IO[Unit] = stmt.run()

or

val conn: Connection
val qry: SelectStatement[Person]
val stmts: List[SQLStatefulStatements]

val context = new LogographContext[IO](conn)
val resultsIO: IO[Seq[Person]] = context.run(qry)
val operationsIO: IO[Unit] = context.run(stmts:_*)

logograph's People

Contributors

albertprz avatar

Stargazers

 avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.