├── .gitignore ├── src ├── main │ └── scala │ │ └── com │ │ └── log4p │ │ └── sqldsl │ │ ├── SQLParser.scala │ │ ├── QueryBuilder.scala │ │ └── AnsiSqlRenderer.scala └── test │ └── scala │ └── com │ └── log4p │ └── sqldsl │ ├── QueryBuilderSpec.scala │ └── SQLParserSpec.scala ├── README.textile └── pom.xml /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.iws 3 | *.ipr 4 | *.iml 5 | 6 | out 7 | target 8 | 9 | -------------------------------------------------------------------------------- /src/main/scala/com/log4p/sqldsl/SQLParser.scala: -------------------------------------------------------------------------------- 1 | package com.log4p.sqldsl 2 | 3 | import scala.util.parsing.combinator._ 4 | import scala.util.parsing.combinator.syntactical._ 5 | 6 | class SQLParser extends JavaTokenParsers { 7 | 8 | def query:Parser[Query] = operation ~ from ~ opt(where) ~ opt(order) ^^ { 9 | case operation ~ from ~ where ~ order => Query(operation, from, where, order) 10 | } 11 | 12 | def operation:Parser[Operation] = { 13 | ("select" | "update" | "delete") ~ repsep(ident, ",") ^^ { 14 | case "select" ~ f => Select(f:_*) 15 | case _ => throw new IllegalArgumentException("Operation not implemented") 16 | } 17 | } 18 | 19 | def from:Parser[From] = "from" ~> ident ^^ (From(_)) 20 | 21 | def where:Parser[Where] = "where" ~> rep(clause) ^^ (Where(_:_*)) 22 | 23 | def clause:Parser[Clause] = (predicate|parens) * ( 24 | "and" ^^^ { (a:Clause, b:Clause) => And(a,b) } | 25 | "or" ^^^ { (a:Clause, b:Clause) => Or(a,b) } 26 | ) 27 | 28 | def parens:Parser[Clause] = "(" ~> clause <~ ")" 29 | 30 | def predicate = ( 31 | ident ~ "=" ~ boolean ^^ { case f ~ "=" ~ b => BooleanEquals(f,b)} 32 | | ident ~ "=" ~ stringLiteral ^^ { case f ~ "=" ~ v => StringEquals(f,stripQuotes(v))} 33 | | ident ~ "=" ~ wholeNumber ^^ { case f ~ "=" ~ i => NumberEquals(f,i.toInt)} 34 | 35 | ) 36 | 37 | def boolean = ("true" ^^^ (true) | "false" ^^^ (false)) 38 | 39 | def order:Parser[Direction] = { 40 | "order" ~> "by" ~> ident ~ ("asc" | "desc") ^^ { 41 | case f ~ "asc" => Asc(f) 42 | case f ~ "desc" => Desc(f) 43 | } 44 | } 45 | 46 | def stripQuotes(s:String) = s.substring(1, s.length-1) 47 | 48 | def parse(sql:String):Option[Query] = { 49 | parseAll(query, sql) match { 50 | case Success(r, q) => Option(r) 51 | case x => println(x); None 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /src/main/scala/com/log4p/sqldsl/QueryBuilder.scala: -------------------------------------------------------------------------------- 1 | package com.log4p.sqldsl 2 | 3 | case class Query(val operation:Operation, val from: From, val where: Option[Where], val order: Option[Direction] = None) { 4 | def order(dir: Direction): Query = this.copy(order = Option(dir)) 5 | } 6 | 7 | abstract class Operation { 8 | def from(table: String) = From(table, Option(this)) 9 | } 10 | case class Select(val fields:String*) extends Operation 11 | case class From(val table: String, val operation:Option[Operation] = None) { 12 | def where(clauses: Clause*): Query = Query(operation.get, this, Option(Where(clauses: _*))) 13 | } 14 | 15 | case class Where(val clauses: Clause*) 16 | 17 | abstract class Clause { 18 | def and(otherField: Clause): Clause = And(this, otherField) 19 | def or(otherField: Clause): Clause = Or(this, otherField) 20 | } 21 | 22 | case class StringEquals(val f: String, val value: String) extends Clause 23 | case class NumberEquals(val f: String, val value: Number) extends Clause 24 | case class BooleanEquals(val f: String, val value: Boolean) extends Clause 25 | case class In(val field: String, val values: String*) extends Clause 26 | case class And(val lClause:Clause, val rClause:Clause) extends Clause 27 | case class Or(val lClause:Clause, val rClause:Clause) extends Clause 28 | 29 | abstract class Direction 30 | case class Asc(field: String) extends Direction 31 | case class Desc(field: String) extends Direction 32 | 33 | object QueryBuilder { 34 | implicit def tuple2field(t: (String, String)): StringEquals = StringEquals(t._1, t._2) 35 | implicit def tuple2field(t: (String, Int)): NumberEquals = NumberEquals(t._1, t._2) 36 | implicit def tuple2field(t: (String, Boolean)): BooleanEquals = BooleanEquals(t._1, t._2) 37 | implicit def from2query(f: From): Query = Query(f.operation.get, f, Option(Where())) 38 | 39 | /** entrypoint for starting a select query */ 40 | def select(fields:String*) = Select(fields:_*) 41 | def select(symbol: Symbol): Select = symbol match { 42 | case 'all => select("*") 43 | case _ => throw new RuntimeException("Only 'all allowed as symbol") 44 | } 45 | 46 | def in(field: String, values: String*) = In(field, values: _*) 47 | } -------------------------------------------------------------------------------- /README.textile: -------------------------------------------------------------------------------- 1 | h1. Scala SQL DSL 2 | 3 | h2. Scala implementation of a really small subset of the SQL language 4 | 5 | To understand how to write DSLs (internal/external) in Scala I started implementing a small subset of the SQL language. 6 | 7 | Implemented: 8 | 9 | * select operation with fields 10 | * where clause with (typed) equals, in, and, or 11 | * order 12 | * A 'renderer' to create a SQL String from the given Query object 13 | 14 | Scala SQL DSL lets you write stuff like: 15 | 16 |
17 | scala> val q = select ("*") from ("user") where (("name","peter") and (("active", true) or ("role", "admin")))
18 | scala> q.sql
19 | res0: java.lang.String = select * from user where (name = 'peter' and (active = true or role = 'admin'))
20 |
21 |
22 | Or using the parser:
23 |
24 | 25 | scala> val sql = """select name,age from users where name = "peter" and (active = true or age = 30)""" 26 | scala> val query = p.parse(sql).get 27 | scala> AnsiSqlRenderer.sql(query) 28 | res0: java.lang.String = select name,age from users where (name = 'peter' and (active = true or age = 30)) 29 |30 | 31 | h2. Prerequisites for building / running tests 32 | 33 | * maven2 34 | * maven uses the maven-scala-test plugin of which I have a for here: [http://github.com/p3t0r/maven-scalatest-plugin/] for running the specs 35 | 36 | h2. Usage 37 | 38 | The spec contains various examples on how to write queries. But basically you start by: 39 | 40 | * Import all functions/implicits in the QueryBuilder object: import QueryBuilder._ 41 | * Import the AnsiSqlRenderer object: AnsiSqlRenderer._ 42 | 43 | h2. Author 44 | 45 | Written by Peter Maas 46 | 47 | * http://www.twitter.com/p3t0r 48 | * http://log4p.com 49 | 50 | h1. License 51 | 52 |
53 | This software is licensed under the Apache 2 license, quoted below.
54 |
55 | Licensed under the Apache License, Version 2.0 (the "License"); you may not
56 | use this file except in compliance with the License. You may obtain a copy of
57 | the License at
58 |
59 | http://www.apache.org/licenses/LICENSE-2.0
60 |
61 | Unless required by applicable law or agreed to in writing, software
62 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
63 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
64 | License for the specific language governing permissions and limitations under
65 | the License.
66 |
--------------------------------------------------------------------------------
/src/main/scala/com/log4p/sqldsl/AnsiSqlRenderer.scala:
--------------------------------------------------------------------------------
1 | package com.log4p.sqldsl
2 |
3 | case class SQL(val sql:String)
4 |
5 | object AnsiSqlRenderer {
6 | implicit def query2sql(q:Query):SQL = SQL(sql(q))
7 | implicit def from2sql(f: From): SQL = SQL(sql(Query(f.operation.get, f, None)))
8 |
9 | def sql(query: Query): String = {
10 | List(
11 | expandOperation(query),
12 | expandFrom(query),
13 | expandWhere(query),
14 | expandOrder(query)
15 | )
16 | .filter(_ != None)
17 | .map(_ match {
18 | case Some(s) => s
19 | case s:String => s
20 | })
21 | .mkString(" ")
22 | }
23 |
24 | def expandOperation(query:Query):String = query.operation match {
25 | case Select(field:String) => "select %s".format(field)
26 | case s:Select => "select %s".format(s.fields.mkString(","))
27 | case _ => throw new IllegalArgumentException("Operation %s not implemented".format(query.operation))
28 | }
29 |
30 | def expandFrom(query: Query) = "from %s".format(query.from.table)
31 | def expandWhere(query: Query):Option[String] = {
32 | if (query.where.isEmpty || query.where.get.clauses.isEmpty)
33 | None
34 | else
35 | Option("where %s".format(query.where.get.clauses.map(expandClause(_)).mkString(" ")))
36 | }
37 |
38 | def expandClause(clause: Clause): String = clause match {
39 | case StringEquals(field, value) => "%s = %s".format(field, quote(value))
40 | case BooleanEquals(field, value) => "%s = %s".format(field, value)
41 | case NumberEquals(field, value) => "%s = %s".format(field, value)
42 | case in:In => "%s in (%s)".format(in.field, in.values.map(quote(_)).mkString(","))
43 | case and:And => "(%s and %s)".format(expandClause(and.lClause), expandClause(and.rClause))
44 | case or:Or => "(%s or %s)".format(expandClause(or.lClause), expandClause(or.rClause))
45 | case _ => throw new IllegalArgumentException("Clause %s not implemented".format(clause))
46 | }
47 |
48 | def expandOrder(query: Query):Option[String] = query.order match {
49 | case Some(direction) => direction match {
50 | case Asc(field) => Option("order by %s asc".format(field))
51 | case Desc(field) => Option("order by %s desc".format(field))
52 | }
53 | case None => None
54 | }
55 |
56 | def quote(value: String) = "'%s'".format(escape(value))
57 | def escape(value: String) = value.replaceAll("'", "''")
58 | }
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 4.0.0
4 | com.log4p
5 | scala-sql-dsl
6 | jar
7 | 1.0-SNAPSHOT
8 | scala-sql-dsl
9 |
10 |
11 | scala-tools.org
12 | Scala-tools Maven2 Repository
13 | http://scala-tools.org/repo-releases
14 |
15 |
16 |
17 |
18 | scala-tools.org
19 | Scala-tools Maven2 Repository
20 | http://scala-tools.org/repo-releases
21 |
22 |
23 |
24 |
25 | org.scala-lang
26 | scala-library
27 | 2.8.0
28 |
29 |
30 | org.scala-lang
31 | scala-compiler
32 | 2.8.0
33 |
34 |
35 | org.scalatest
36 | scalatest
37 | 1.2
38 |
39 |
40 |
41 |
42 |
43 | org.scala-tools
44 | maven-scala-plugin
45 |
46 |
47 | compile
48 |
49 | compile
50 |
51 | compile
52 |
53 |
54 | test-compile
55 |
56 | testCompile
57 |
58 | test-compile
59 |
60 |
61 | process-resources
62 |
63 | compile
64 |
65 |
66 |
67 |
68 |
69 | com.jteigen
70 | maven-scalatest-plugin
71 | 1.1-SNAPSHOT
72 |
73 |
74 |
75 |
76 |
--------------------------------------------------------------------------------
/src/test/scala/com/log4p/sqldsl/QueryBuilderSpec.scala:
--------------------------------------------------------------------------------
1 | package com.log4p.sqldsl
2 |
3 | import org.scalatest.matchers.ShouldMatchers
4 | import org.scalatest.Spec
5 |
6 | import QueryBuilder._
7 | import AnsiSqlRenderer._
8 |
9 | class QueryBuilderSpec extends Spec with ShouldMatchers {
10 |
11 | val SQL = QueryBuilder
12 |
13 | describe("A Query") {
14 | describe("(when containing nested 'and' or 'or' clauses)") {
15 | val q = SQL select 'all from ("user") where (("name","peter") and (("active", true) or ("role", "admin")))
16 |
17 | it("should contain parentheses at the correct locations in the resulting SQL") {
18 | q.sql should be ("select * from user where (name = 'peter' and (active = true or role = 'admin'))")
19 | }
20 | }
21 | describe("(when containing quotes in values)") {
22 | val q = SQL select "*" from ("user") where (("name","p'eter"))
23 |
24 | it("should escape those quotes in the resulting SQL") {
25 | q.sql should be ("select * from user where name = 'p''eter'")
26 | }
27 | }
28 | describe("(when testing against numeric value)") {
29 | val q = SQL select "*" from("user") where (("id", 100))
30 |
31 | it("shouldn't quote numbers in resulting SQL") {
32 | q.sql should be ("select * from user where id = 100")
33 | }
34 | }
35 | describe("(when containing 'in' clause)") {
36 | val q = SQL select "*" from ("user") where (in("name","pe'ter","petrus"))
37 |
38 | it("should generate in-clause with escaped values in SQL") {
39 | q.sql should be ("select * from user where name in ('pe''ter','petrus')")
40 | }
41 | }
42 | describe("(when ordered)") {
43 | val q = SQL select "*" from ("user") where (("name","peter")) order Desc("name")
44 |
45 | it("should generate order SQL") {
46 | q.sql should be ("select * from user where name = 'peter' order by name desc")
47 | }
48 | }
49 | describe("(when asked to select 'all)") {
50 | val q = SQL select 'all from ("user") where (("id", 100))
51 |
52 | it("should select *") {
53 | q.sql should be ("select * from user where id = 100")
54 | }
55 | }
56 | describe("(when asked to select using another symbol)") {
57 | it("should throw an exception, for now") {
58 | evaluating { val q = select ('bla) from ("user") where (("id", 100)) } should produce[Throwable]
59 | }
60 | }
61 | describe("(when no where clause is specified)") {
62 | val q = SQL select 'all from ("user")
63 |
64 | it("should select all rows without filter") {
65 | q.sql should be ("select * from user")
66 | }
67 | }
68 | describe("(when a where clause is not, but an order clause is specified)") {
69 | val q = SQL select 'all from ("user") order Asc("name")
70 |
71 | it("should select and order all rows without filter") {
72 | q.sql should be ("select * from user order by name asc")
73 | }
74 | }
75 | describe("(when specified with lots less parentheses)") {
76 | val q = SQL select 'all from "user" where (("id", 100))
77 |
78 | it("should still work") {
79 | q.sql should be ("select * from user where id = 100")
80 | }
81 | }
82 | }
83 | }
--------------------------------------------------------------------------------
/src/test/scala/com/log4p/sqldsl/SQLParserSpec.scala:
--------------------------------------------------------------------------------
1 | package com.log4p.sqldsl
2 |
3 | import org.scalatest.matchers.ShouldMatchers
4 | import org.scalatest.Spec
5 |
6 | class SQLParserSpec extends Spec with ShouldMatchers {
7 | val p = new SQLParser
8 | describe("given a sql string with an order clause") {
9 | describe("(when direction is asc)") {
10 | val sql = "select name from users order by name asc"
11 | it("should be parsed into an Asc object containing the given field") {
12 | val query = p.parse(sql).get
13 |
14 | query.operation should be (Select("name"))
15 | query.from should be (From("users"))
16 | query.order should be (Option(Asc("name")))
17 | }
18 | }
19 | }
20 | describe("given a sql string with a single where clause") {
21 | describe("(when equals predicate with string literal)") {
22 | val sql = "select name from users where name = \"peter\""
23 | it("should be parsed into an StringEquals object containing the given field and value") {
24 | println(sql)
25 | val query = p.parse(sql).get
26 | query.operation should be (Select("name"))
27 | query.from should be (From("users"))
28 | query.where.get.clauses.head should be (StringEquals("name","peter"))
29 | }
30 | }
31 | describe("(when equals predicate with numeric literal)") {
32 | val sql = """select age from users where age = 30"""
33 | it("should be parsed into an NumberEquals object containing the given field and value") {
34 | println(sql)
35 | val query = p.parse(sql).get
36 | query.operation should be (Select("age"))
37 | query.from should be (From("users"))
38 | query.where.get.clauses.head should be (NumberEquals("age",30))
39 | }
40 | }
41 | }
42 | describe("given a sql string with a combined where clause") {
43 | describe("(when equals predicate contains and)") {
44 | val sql = """select name from users where name = "peter" and age = 30"""
45 | it("should be parsed into an And object containing to correct subclauses") {
46 | println(sql)
47 | val query = p.parse(sql).get
48 | query.operation should be (Select("name"))
49 | query.from should be (From("users"))
50 | query.where.get.clauses.head should be (And(StringEquals("name","peter"), NumberEquals("age",30)))
51 | }
52 | }
53 | describe("(when equals predicate contains or)") {
54 | val sql = """select name from users where age = 20 or age = 30"""
55 | it("should be parsed into an Or object containing to correct subclauses") {
56 | println(sql)
57 | val query = p.parse(sql).get
58 | query.operation should be (Select("name"))
59 | query.from should be (From("users"))
60 | query.where.get.clauses.head should be (Or(NumberEquals("age",20), NumberEquals("age",30)))
61 | }
62 | }
63 | describe("(when equals predicate contains multiple combined clauses)") {
64 | val sql = """select name from users where name = "peter" and age = 20 or age = 30"""
65 | it("should be parsed into an Or object containing and And object and and Equals predicate") {
66 | println(sql)
67 | val query = p.parse(sql).get
68 | query.operation should be (Select("name"))
69 | query.from should be (From("users"))
70 | query.where.get.clauses.head should be (Or(And(StringEquals("name","peter"), NumberEquals("age",20)), NumberEquals("age",30)))
71 | println(AnsiSqlRenderer.sql(query))
72 | }
73 | }
74 | describe("(when equals predicate contains multiple combined clauses where the presedence is dictated by parens)") {
75 | val sql = """select name,age from users where name = "peter" and (active = true or age = 30)"""
76 | it("should be parsed into an Or object containing and And object and and Equals predicate") {
77 | println(sql)
78 | val query = p.parse(sql).get
79 | query.operation should be (Select(List("name","age"):_*))
80 | query.from should be (From("users"))
81 | query.where.get.clauses.head should be (And(StringEquals("name","peter"), Or(BooleanEquals("active",true), NumberEquals("age",30))))
82 | println(AnsiSqlRenderer.sql(query))
83 | }
84 | }
85 |
86 | }
87 | }
--------------------------------------------------------------------------------