├── .classpath ├── .gitignore ├── .project ├── .settings └── ch.epfl.lamp.sdt.core.prefs ├── Jetty.launch ├── README.markdown ├── buildfile ├── fakesdb.rb ├── ivy.xml ├── ivysettings.xml └── src ├── main ├── resources │ └── manifest.mf ├── scala │ └── fakesdb │ │ ├── Data.scala │ │ ├── FakeSdbServlet.scala │ │ ├── Jetty.scala │ │ ├── Params.scala │ │ ├── SelectParser.scala │ │ ├── actions │ │ ├── Action.scala │ │ ├── BatchDeleteAttributes.scala │ │ ├── BatchPutAttributes.scala │ │ ├── ConditionalChecking.scala │ │ ├── CreateDomain.scala │ │ ├── DeleteAttributes.scala │ │ ├── DeleteDomain.scala │ │ ├── DomainMetadata.scala │ │ ├── Errors.scala │ │ ├── GetAttributes.scala │ │ ├── ItemUpdates.scala │ │ ├── ListDomains.scala │ │ ├── PutAttributes.scala │ │ └── Select.scala │ │ └── stub │ │ └── AmazonSimpleDbStub.scala └── webapp │ └── WEB-INF │ └── web.xml └── test └── scala └── fakesdb ├── AbstractFakeSdbTest.scala ├── BatchDeleteAttributesTest.scala ├── BatchPutAttributesTest.scala ├── CreateDomainTest.scala ├── DeleteAttributesTest.scala ├── DomainMetadataTest.scala ├── ErrorTest.scala ├── FlushDomainsTest.scala ├── GetAttributesTest.scala ├── ListDomainsTest.scala ├── PutAttributesTest.scala ├── SelectParserTest.scala └── SelectTest.scala /.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | bin 3 | .manager 4 | target 5 | boot 6 | lib 7 | lib_managed 8 | .scala_dependencies 9 | reports 10 | -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | fakesdb 4 | 5 | 6 | 7 | 8 | 9 | org.scala-ide.sdt.core.scalabuilder 10 | 11 | 12 | 13 | 14 | 15 | org.scala-ide.sdt.core.scalanature 16 | org.eclipse.jdt.core.javanature 17 | org.apache.ivyde.eclipse.ivynature 18 | 19 | 20 | -------------------------------------------------------------------------------- /.settings/ch.epfl.lamp.sdt.core.prefs: -------------------------------------------------------------------------------- 1 | #Mon Jan 18 23:12:20 CST 2010 2 | =false 3 | Xcheck-null=false 4 | Xcheckinit=false 5 | Xdisable-assertions=false 6 | Xexperimental=false 7 | Xfuture=false 8 | Xlog-implicits=false 9 | Xno-uescape=false 10 | Xno-varargs-conversion=false 11 | Xstrict-warnings=false 12 | Xwarninit=false 13 | Yclosure-elim=false 14 | Ydead-code=false 15 | Ydetach=false 16 | Yinline=false 17 | Ylinearizer=rpo 18 | Yno-generic-signatures=false 19 | Yno-imports=false 20 | Yno-predefs=false 21 | Yself-in-annots=false 22 | Yspecialize=false 23 | Ysqueeze=on 24 | Ytailrecommend=false 25 | Ywarn-catches=false 26 | Ywarn-dead-code=false 27 | Ywarn-shadowing=false 28 | deprecation=true 29 | eclipse.preferences.version=1 30 | g=vars 31 | optimise=false 32 | scala.compiler.useProjectSettings=true 33 | target=jvm-1.5 34 | unchecked=false 35 | -------------------------------------------------------------------------------- /Jetty.launch: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | Overview 2 | ======== 3 | 4 | A fake version of Amazon's SimpleDB. Local, in-memory, consistent, deployed as a war. 5 | 6 | The entire REST API (Query, Select, etc.) is implemented in ~750 lines of Scala. 7 | 8 | Install 9 | ======= 10 | 11 | You can get fakesdb from: 12 | 13 | 1. The [downloads](http://github.com/stephenh/fakesdb/downloads) page 14 | 15 | 2. The [http://repo.joist.ws](joist.ws) Maven repository, e.g. `com.bizo` + `fakesdb-testing_2.9.1` + `2.6.1` [here](http://repo.joist.ws/com/bizo/fakesdb-testing_2.9.1/) 16 | 17 | There are a few different modules: 18 | 19 | * `fakesdb-standalone` has all of the dependencies (including Scala) as an uber-jar for running from the CLI. 20 | 21 | You can run this as: 22 | 23 | `java -jar fakesdb-standalone-1.5.jar` 24 | 25 | And it will start up an embedded instance of Jetty on port 8080. You can pass `-Dport=X` to specify a different port. 26 | 27 | * `fakesdb-testing` is the dependencies (without Scala) as an uber-jar for embedding. 28 | 29 | From within a unit test, you can call: 30 | 31 | fakesdb.Jetty.apply(8080) 32 | 33 | To start up fakesdb on port 8080. 34 | 35 | * `fakesdb-servlet` is just the fakesdb classes (no dependencies) for running within your own webapp. E.g. add the `fakesdb.FakeSdbServlet` to your own `web.xml` file. 36 | 37 | You could also use this version for `AmazonSimpleDbStub` if you want to do in-memory only testing. 38 | 39 | * `fakesdb.war` is (was--it's not being published right now) to drop into your Tomcat/etc. 40 | 41 | Notes 42 | ===== 43 | 44 | * To facilitate testing, issuing a `CreateDomain` command with `DomainName=_flush` will reset all of the data 45 | 46 | * If you're using the `typica` Java SimpleDB client, versions through 1.6 only use port 80, even when given a non-80 setting. So you'll either have to run `fakesdb` on port 80 or else redirect port 80 traffic to 8080 with a firewall rule. 47 | 48 | Changelog 49 | ========= 50 | 51 | * 2.6.1 - 15 November 2012 52 | * Add `` to error responses (Alex Boisvert) 53 | * Build improvements (Alex Boisvert) 54 | * 2.5 - 6 Oct 2012 55 | * Add AmazonSimpleDbStub for in-memory testing of clients using the AWS Java SDK 56 | * Switch from sbt to buildr 57 | * 2.4 - 23 Aug 2011 58 | * Upgrade to Scala 2.9.0-1 and Jetty 8.0.0.RC0 59 | * Make new `fakesdb-testing.jar` which has jetty but not scala-library 60 | * 2.3 - 23 Aug 2011 61 | * Remove `Jetty` bootstrap classes from `fakesdb-servlet.jar` 62 | * 2.2 - 25 Apr 2011 63 | * BatchDeleteAttributes support (Alexander Gorkunov) 64 | * [Partial Select](http://aws.amazon.com/about-aws/whats-new/2009/02/19/new-features-for-amazon-simpledb/) support (Alexander Gorkunov) 65 | * Pre-2.2 various releases 66 | 67 | Todo 68 | ==== 69 | 70 | * Loading the SDB URL in a browser (e.g. without a REST action) should display all of the current data 71 | * [Release It](http://www.pragprog.com/titles/mnee/release-it) talks about having "fake" (better term?) versions of systems like `fakesdb` purposefully lock up, fail, etc., to test how your real application responds--it would be cool to flip `fakesdb` into several error modes either via a web UI or meta-domains (like the current `_flush` domain) 72 | 73 | ## License 74 | 75 | Licensed under the terms of the Apache Software License v2.0. 76 | 77 | http://www.apache.org/licenses/LICENSE-2.0.html 78 | -------------------------------------------------------------------------------- /buildfile: -------------------------------------------------------------------------------- 1 | Buildr.settings.build['scala.version'] = '2.10.4' 2 | 3 | require 'buildr/ivy_extension' 4 | require 'buildr/scala' 5 | require './fakesdb.rb' 6 | 7 | VERSION_NUMBER = ENV['version'] || 'SNAPSHOT' 8 | 9 | # to resolve the ${version} in the ivy.xml 10 | Java.java.lang.System.setProperty("version", VERSION_NUMBER) 11 | 12 | repositories.remote << "http://mirrors.ibiblio.org/maven2" 13 | repositories.release_to = 'sftp://joist.ws/var/www/joist.repo' 14 | repositories.release_to[:permissions] = 0644 15 | 16 | define FakeSDB::fakesdb do 17 | extend FakeSDB # project-specific extensions 18 | 19 | project.version = VERSION_NUMBER 20 | project.group = 'com.bizo' 21 | ivy.compile_conf(['servlet', 'war', 'buildtime']).test_conf('test') 22 | 23 | test.using :junit 24 | 25 | task "retrieve" do 26 | ivy.ivy4r.retrieve 27 | end 28 | 29 | package_with_sources 30 | 31 | file 'target/pom.xml' => task('ivy:makepom') 32 | package(:jar).pom.tap do |pom| 33 | pom.from 'target/pom.xml' 34 | end 35 | 36 | all_in_one_jar :id => "standalone", 37 | :libs => ["aws-java-sdk", "jetty", "servlet-api", "scala-library", "scala-reflect"] 38 | 39 | # without scala-library 40 | all_in_one_jar :id => "testing", 41 | :libs => ["aws-java-sdk", "jetty", "servlet-api"] 42 | 43 | # fakesdb-servlet is just fakesdb so you can set it up with your own web.xml 44 | package(:jar, :id => fakesdb("servlet")).tap do |pkg| 45 | pkg.exclude Dir[_(:target, :classes, "fakesdb") + "/Jetty*"] 46 | end 47 | end 48 | 49 | -------------------------------------------------------------------------------- /fakesdb.rb: -------------------------------------------------------------------------------- 1 | 2 | # Returns scala version (from settings or else from $SCALA_HOME) 3 | def scala_version 4 | Buildr.settings.build['scala.version'] || begin 5 | fail "Unable to infer scala version" unless ENV["SCALA_HOME"] =~ /scala-([^\/]*)/ 6 | version = Buildr.settings.build['scala.version'] = $1 7 | puts "Inferred Scala version #{version}" 8 | return version 9 | end 10 | end 11 | 12 | # Project-specific extensions 13 | module FakeSDB 14 | extend self 15 | 16 | # Return fakesdb artifact name embedding scala version, with optional suffix 17 | # e.g. fakesdb => "fakesdb_2.9.1" 18 | # fakesdb("standalone") => "fakesdb-standalone_2.9.1" 19 | def fakesdb(suffix = nil) 20 | short_scala_version = (scala_version =~ /^(\d+)\.(\d+).*/) ? "#{$1}.#{$2}" : fail # strip .RC suffix (if any) 21 | "fakesdb#{suffix ? "-" + suffix : ""}_#{short_scala_version}" 22 | end 23 | 24 | # Create all-in-one jar by merging dependencies from `lib` using prefixes 25 | # 26 | # e.g. all_in_one_jar :id => "standlone", :libs => ["aws-java-sdk", "jetty", ...] 27 | # 28 | def all_in_one_jar(options) 29 | name = options[:id] || fail("Missing :id") 30 | libs = options[:libs] || fail("Missing :id") 31 | package(:jar, :id => fakesdb(name)).tap do |pkg| 32 | pkg.enhance [task(:retrieve)] 33 | # double-enhance so merge happens after base jar is created 34 | pkg.enhance do 35 | pkg.enhance do 36 | libs.each do |prefix| 37 | Dir[_("lib") + "/#{prefix}-*.jar"].each do |dep| 38 | fast_merge_jar pkg, dep if dep !~ /-sources/ 39 | end 40 | end 41 | end 42 | end 43 | end 44 | end 45 | 46 | # Merge jars using `jar` tool since rubyzip is kinda slow 47 | # 48 | # e.g. fast_merge_jar("my-super.jar, "some-dependency.jar") 49 | # 50 | def fast_merge_jar(target_jar, merge_jar) 51 | info "merging #{merge_jar}" 52 | tmp = _(:target, "tmp") 53 | sh "rm -rf #{tmp}" 54 | sh "mkdir -p #{tmp}; cd #{tmp}; jar -xf #{merge_jar}; jar -uf #{target_jar} *" 55 | sh "rm -rf #{tmp}" 56 | end 57 | end 58 | 59 | 60 | -------------------------------------------------------------------------------- /ivy.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /ivysettings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/main/resources/manifest.mf: -------------------------------------------------------------------------------- 1 | Main-Class: fakesdb.Jetty 2 | -------------------------------------------------------------------------------- /src/main/scala/fakesdb/Data.scala: -------------------------------------------------------------------------------- 1 | package fakesdb 2 | 3 | import fakesdb.actions.NumberItemAttributesExceededException 4 | import fakesdb.actions.EmptyAttributeNameException 5 | import fakesdb.actions.InvalidParameterValue 6 | import scala.collection.mutable.LinkedHashSet 7 | import scala.collection.mutable.LinkedHashMap 8 | import com.amazonaws.services.simpledb.model.AttributeDoesNotExistException 9 | import fakesdb.actions.ConditionalCheckFailedException 10 | 11 | class Data { 12 | private val domains = new LinkedHashMap[String, Domain]() 13 | 14 | def getDomains(): Iterator[Domain] = domains.valuesIterator 15 | 16 | def getDomain(name: String): Option[Domain] = domains.get(name) 17 | 18 | def getOrCreateDomain(name: String): Domain = { 19 | if (!name.matches("[a-zA-Z0-9_\\-\\.]{3,255}")) { 20 | throw new InvalidParameterValue("Value (\"%s\") for parameter DomainName is invalid.".format(name)) 21 | } 22 | domains.getOrElseUpdate(name, new Domain(name)) 23 | } 24 | 25 | def deleteDomain(domain: Domain): Unit = domains.remove(domain.name) 26 | 27 | def flush(): Unit = domains.clear 28 | } 29 | 30 | class Domain(val name: String) { 31 | private val items = new LinkedHashMap[String, Item]() 32 | 33 | def getItems(): Iterator[Item] = items.valuesIterator 34 | 35 | def getItem(name: String): Option[Item] = items.get(name) 36 | 37 | def getOrCreateItem(name: String): Item = { 38 | InvalidParameterValue.failIfOver1024("Name", name); 39 | items.getOrElseUpdate(name, new Item(name)) 40 | } 41 | 42 | def deleteIfEmpty(item: Item) = if (!item.getAttributes.hasNext) items.remove(item.name) 43 | 44 | def deleteItem(item: Item) = items.remove(item.name) 45 | } 46 | 47 | class Item(val name: String) { 48 | private val attributes = new LinkedHashMap[String, Attribute]() 49 | 50 | def getAttributes(): Iterator[Attribute] = attributes.valuesIterator 51 | 52 | def getAttribute(name: String): Option[Attribute] = attributes.get(name) 53 | 54 | def getOrCreateAttribute(name: String): Attribute = attributes.getOrElseUpdate(name, new Attribute(name)) 55 | 56 | // this put overload is used in a lot of tests 57 | def put(name: String, value: String, replace: Boolean): Unit = { 58 | put(name, List(value), replace) 59 | } 60 | 61 | def put(name: String, values: Seq[String], replace: Boolean) { 62 | // the limit is 256 (name,value) unique pairs, so make (name,value) pairs and then combine them 63 | val existingPairs = attributes.toList.flatMap((e) => { e._2.values.map((v) => (e._1, v)) }) 64 | val newPairs = values.map((v) => (name, v)) 65 | if ((existingPairs ++ newPairs).toSet.size > 256) { 66 | throw new NumberItemAttributesExceededException 67 | } 68 | if (name == "") { 69 | throw new EmptyAttributeNameException 70 | } 71 | InvalidParameterValue.failIfOver1024("Name", name); 72 | this.getOrCreateAttribute(name).put(values, replace) 73 | } 74 | 75 | def delete(name: String): Unit = attributes.remove(name) 76 | 77 | def delete(name: String, value: String): Unit = { 78 | getAttribute(name) match { 79 | case Some(a) => a.deleteValues(value) ; removeIfNoValues(a) 80 | case None => 81 | } 82 | } 83 | 84 | def assertCondition(condition: Tuple2[String, Option[String]]) = { 85 | condition match { 86 | case (name, None) => for (f <- getAttributes.find(_.name == name)) throw new ConditionalCheckFailedException(condition) 87 | case (name, Some(value)) => getAttributes find (_.name == name) match { 88 | case None => throw new AttributeDoesNotExistException(name) 89 | case Some(attr) => if (attr.getValues.toList != List(value)) throw new ConditionalCheckFailedException(condition, attr.getValues.toList) 90 | } 91 | } 92 | } 93 | 94 | private def removeIfNoValues(attribute: Attribute) = { 95 | if (attribute.empty) attributes.remove(attribute.name) 96 | } 97 | } 98 | 99 | class Attribute(val name: String) { 100 | private val _values = new LinkedHashSet[String]() 101 | 102 | def values(): Traversable[String] = _values 103 | 104 | def getValues(): Iterator[String] = _values.iterator 105 | 106 | def empty(): Boolean = values.size == 0 107 | 108 | def deleteValues(value: String) = { 109 | _values.remove(value) 110 | } 111 | 112 | def put(__values: Seq[String], replace: Boolean) = { 113 | if (replace) _values.clear 114 | __values.foreach((v) => { 115 | InvalidParameterValue.failIfOver1024("Value", v); 116 | _values += v 117 | }) 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/main/scala/fakesdb/FakeSdbServlet.scala: -------------------------------------------------------------------------------- 1 | package fakesdb 2 | 3 | import java.io.{PrintWriter, StringWriter} 4 | import javax.servlet.http._ 5 | import fakesdb.actions._ 6 | 7 | class FakeSdbServlet extends HttpServlet { 8 | 9 | val data = new Data 10 | 11 | override def doGet(request: HttpServletRequest, response: HttpServletResponse): Unit = synchronized { 12 | val params = Params(request) 13 | if (!params.contains("Action")) { 14 | response.setStatus(404) 15 | return 16 | } 17 | 18 | var xml = "" 19 | try { 20 | val action = params("Action") match { 21 | case "CreateDomain" => new CreateDomain(data) 22 | case "DeleteDomain" => new DeleteDomain(data) 23 | case "DomainMetadata" => new DomainMetadata(data) 24 | case "ListDomains" => new ListDomains(data) 25 | case "GetAttributes" => new GetAttributes(data) 26 | case "PutAttributes" => new PutAttributes(data) 27 | case "BatchPutAttributes" => new BatchPutAttributes(data) 28 | case "BatchDeleteAttributes" => new BatchDeleteAttributes(data) 29 | case "DeleteAttributes" => new DeleteAttributes(data) 30 | case "Select" => new Select(data) 31 | case other => throw new InvalidActionException(other) 32 | } 33 | xml = action.handle(params).toString 34 | } catch { 35 | case e: Exception => { 36 | xml = toXML(e).toString 37 | response.setStatus(e match { 38 | case se: SDBException => se.httpStatus 39 | case _ => 400 40 | }) 41 | } 42 | } 43 | 44 | response.setContentType("text/xml") 45 | response.getWriter.write(xml) 46 | } 47 | 48 | private def toXML(t: Throwable) = { 49 | val xmlCode = t match { 50 | case se: SDBException => se.xmlCode 51 | case _ => "InternalError" 52 | } 53 | 54 | val stacktrace = new StringWriter() 55 | t.printStackTrace(new PrintWriter(stacktrace)) 56 | 57 | 58 | {xmlCode}{t.getClass.getSimpleName}: {t.getMessage}0 59 | 0 60 | {stacktrace.toString} 61 | 62 | } 63 | 64 | override def doPost(request: HttpServletRequest, response: HttpServletResponse): Unit = doGet(request, response) 65 | 66 | class InvalidActionException(action: String) 67 | extends SDBException(400, "InvalidAction", "The action %s is not valid for this web service.".format(action)) 68 | } 69 | -------------------------------------------------------------------------------- /src/main/scala/fakesdb/Jetty.scala: -------------------------------------------------------------------------------- 1 | package fakesdb 2 | 3 | import org.eclipse.jetty.server.Server 4 | import org.eclipse.jetty.server.bio.SocketConnector 5 | import org.eclipse.jetty.server.handler.ContextHandlerCollection 6 | import org.eclipse.jetty.servlet.ServletHandler 7 | import org.eclipse.jetty.servlet.ServletContextHandler 8 | 9 | class Jetty(val server: Server) 10 | 11 | object Jetty { 12 | def apply(port: Int): Jetty = { 13 | val server = new Server 14 | // Use SocketConnector because SelectChannelConnector locks files 15 | val connector = new SocketConnector 16 | connector.setPort(port) 17 | connector.setMaxIdleTime(60000) 18 | connector.setRequestBufferSize(24 * 1024) 19 | 20 | val handler = new ServletContextHandler(ServletContextHandler.NO_SESSIONS) 21 | handler.addServlet(classOf[FakeSdbServlet].getName(), "/*") 22 | 23 | server.setConnectors(Array(connector)) 24 | server.setHandler(handler) 25 | server.setAttribute("org.mortbay.jetty.Request.maxFormContentSize", 0); 26 | server.setStopAtShutdown(true); 27 | 28 | new Jetty(server) 29 | } 30 | 31 | def main(args: Array[String]): Unit = { 32 | val port = System.getProperty("port", "8080").toInt 33 | Jetty(port).server.start() 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/scala/fakesdb/Params.scala: -------------------------------------------------------------------------------- 1 | package fakesdb 2 | 3 | import javax.servlet.http.HttpServletRequest 4 | import scala.collection.mutable.HashMap 5 | 6 | class Params extends HashMap[String, String] { 7 | } 8 | 9 | object Params { 10 | def apply(request: HttpServletRequest): Params = { 11 | val p = new Params 12 | val i = request.getParameterMap.asInstanceOf[java.util.Map[String, Array[String]]].entrySet.iterator 13 | while (i.hasNext) { 14 | val e = i.next 15 | p.update(e.getKey(), e.getValue()(0)) 16 | } 17 | return p 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/scala/fakesdb/SelectParser.scala: -------------------------------------------------------------------------------- 1 | package fakesdb 2 | 3 | import scala.util.parsing.combinator.syntactical._ 4 | import scala.util.parsing.combinator.syntactical._ 5 | import scala.util.parsing.combinator.lexical._ 6 | import scala.util.parsing.input.CharArrayReader.EofCh 7 | 8 | case class SelectEval(output: OutputEval, from: String, where: WhereEval, order: OrderEval, limit: LimitEval) { 9 | def select(data: Data, nextToken: Option[Int] = None): (List[(String, List[(String,String)])], Int, Boolean) = { 10 | val domain = data.getDomain(from).getOrElse(sys.error("Invalid from "+from)) 11 | val drop = new SomeDrop(nextToken getOrElse 0) 12 | val (items, hasMore) = limit.limit(drop.drop(order.sort(where.filter(domain, domain.getItems.toList)))) 13 | (output.what(domain, items), items.length, hasMore) 14 | } 15 | } 16 | 17 | abstract class OutputEval { 18 | type OutputList = List[(String, List[(String, String)])] 19 | def what(domain: Domain, items: List[Item]): OutputList 20 | 21 | protected def flatAttrs(attrs: Iterator[Attribute]): List[(String, String)] = { 22 | attrs.flatMap((a: Attribute) => a.getValues.map((v: String) => (a.name, v))).toList 23 | } 24 | } 25 | case class CompoundOutput(attrNames: List[String]) extends OutputEval { 26 | def what(domain: Domain, items: List[Item]): OutputList = { 27 | items.map((item: Item) => { 28 | var i = (item.name, flatAttrs(item.getAttributes.filter((a: Attribute) => attrNames.contains(a.name)))) 29 | if (attrNames.contains("itemName()")) { // ugly 30 | i = (i._1, ("itemName()", item.name) :: i._2) 31 | } 32 | i 33 | }).filter(_._2.size > 0) 34 | } 35 | } 36 | class AllOutput extends OutputEval { 37 | def what(domain: Domain, items: List[Item]): OutputList = { 38 | items.map((item: Item) => { 39 | (item.name, flatAttrs(item.getAttributes)) 40 | }).filter(_._2.size > 0) 41 | } 42 | } 43 | class CountOutput extends OutputEval { 44 | def what(domain: Domain, items: List[Item]): OutputList = { 45 | List(("Domain", List(("Count", items.size.toString)))) 46 | } 47 | } 48 | 49 | abstract class WhereEval { 50 | def filter(domain: Domain, items: List[Item]): List[Item] 51 | 52 | protected def getFunc(op: String): Function2[String, String, Boolean] = op match { 53 | case "=" => _ == _ 54 | case "!=" => _ != _ 55 | case ">" => _ > _ 56 | case "<" => _ < _ 57 | case ">=" => _ >= _ 58 | case "<=" => _ <= _ 59 | case "like" => (v1, v2) => v1.matches(v2.replaceAll("%", ".*")) 60 | case "not-like" => (v1, v2) => !v1.matches(v2.replaceAll("%", ".*")) 61 | } 62 | } 63 | case class NoopWhere() extends WhereEval { 64 | def filter(domain: Domain, items: List[Item]): List[Item] = items 65 | } 66 | case class SimpleWhereEval(name: String, op: String, value: String) extends WhereEval { 67 | def filter(domain: Domain, items: List[Item]): List[Item] = { 68 | val func = getFunc(op) 69 | items.filter((i: Item) => i.getAttribute(name) match { 70 | case Some(a) => a.getValues.find(func(_, value)).isDefined 71 | case None => false 72 | }).toList 73 | } 74 | } 75 | 76 | abstract class LimitEval { 77 | def limit(items: List[Item]): (List[Item], Boolean) 78 | } 79 | case class NoopLimit() extends LimitEval { 80 | def limit(items: List[Item]) = (items, false) 81 | } 82 | case class SomeLimit(limit: Int) extends LimitEval { 83 | def limit(items: List[Item]) = (items take limit, items.size > limit) 84 | } 85 | 86 | case class SomeDrop(count: Int) { 87 | def drop(items: List[Item]) = items drop count 88 | } 89 | 90 | case class EveryEval(name: String, op: String, value: String) extends WhereEval { 91 | override def filter(domain: Domain, items: List[Item]): List[Item] = { 92 | val func = getFunc(op) 93 | items.filter((i: Item) => i.getAttribute(name) match { 94 | case Some(a) => a.getValues.forall(func(_, value)) 95 | case None => false 96 | }).toList 97 | } 98 | } 99 | case class CompoundWhereEval(sp: WhereEval, op: String, rest: WhereEval) extends WhereEval { 100 | def filter(domain: Domain, items: List[Item]): List[Item] = { 101 | op match { 102 | case "intersection" => sp.filter(domain, items).toList intersect rest.filter(domain, items).toList 103 | case "and" => sp.filter(domain, items).toList intersect rest.filter(domain, items).toList 104 | case "or" => sp.filter(domain, items).toList union rest.filter(domain, items).toList 105 | case _ => sys.error("Invalid operator "+op) 106 | } 107 | } 108 | } 109 | case class IsNullEval(name: String, isNull: Boolean) extends WhereEval { 110 | def filter(domain: Domain, items: List[Item]): List[Item] = { 111 | items.filter((i: Item) => if (isNull) { 112 | i.getAttribute(name).isEmpty 113 | } else { 114 | i.getAttribute(name).isDefined 115 | }).toList 116 | } 117 | } 118 | case class IsBetweenEval(name: String, lower: String, upper: String) extends WhereEval { 119 | def filter(domain: Domain, items: List[Item]): List[Item] = { 120 | items.filter((i: Item) => i.getAttribute(name) match { 121 | case Some(a) => a.getValues.exists(_ >= lower) && a.getValues.exists(_ <= upper) 122 | case None => false 123 | }).toList 124 | } 125 | } 126 | case class InEval(name: String, values: List[String]) extends WhereEval { 127 | def filter(domain: Domain, items: List[Item]): List[Item] = { 128 | items.filter((i: Item) => i.getAttribute(name) match { 129 | case Some(a) => a.getValues.exists(values.contains(_)) 130 | case None => false 131 | }).toList 132 | } 133 | } 134 | 135 | abstract class OrderEval { 136 | def sort(items: List[Item]): List[Item] 137 | } 138 | case class NoopOrder() extends OrderEval { 139 | def sort(items: List[Item]) = items 140 | } 141 | case class SimpleOrderEval(name: String, way: String) extends OrderEval { 142 | def sort(items: List[Item]): List[Item] = { 143 | val comp = (lv: String, rv: String) => way match { 144 | case "desc" => lv > rv 145 | case _ => lv < rv 146 | } 147 | items.sortWith((l, r) => { 148 | comp(resolveValue(l), resolveValue(r)) 149 | }) 150 | } 151 | def resolveValue(item: Item) = { 152 | if (name == "itemName()") { 153 | item.name 154 | } else { 155 | item.getAttribute(name) match { 156 | case Some(a) => a.getValues.next 157 | case None => "" // default value 158 | } 159 | } 160 | } 161 | } 162 | 163 | class SelectLexical extends StdLexical { 164 | override def token: Parser[Token] = 165 | ( accept("itemName()".toList) ^^^ { Identifier("itemName()") } 166 | | acceptInsensitiveSeq("count(*)".toList) ^^^ { Keyword("count(*)") } 167 | | '\'' ~> rep(chrWithDoubleTicks) <~ '\'' ^^ { chars => StringLit(chars mkString "") } 168 | | '"' ~> rep(chrWithDoubleQuotes) <~ '"' ^^ { chars => StringLit(chars mkString "") } 169 | | '`' ~> rep(chrWithDoubleBackTicks) <~ '`' ^^ { chars => Identifier(chars mkString "") } 170 | | super.token 171 | ) 172 | 173 | // Add $ to letters and _ as acceptable first characters of unquoted identifiers 174 | override def identChar = letter | elem('_') | elem('$') 175 | 176 | // Allow case insensitive keywords by lower casing everything 177 | override protected def processIdent(name: String) = 178 | if (reserved contains name.toLowerCase) Keyword(name.toLowerCase) else Identifier(name) 179 | 180 | // Wow this works--inline acceptSeq and acceptIf, but adds _.toLowerCase 181 | def acceptInsensitiveSeq[ES <% Iterable[Elem]](es: ES): Parser[List[Elem]] = 182 | es.foldRight[Parser[List[Elem]]](success(Nil)){(x, pxs) => acceptIf(_.toLower == x)("`"+x+"' expected but " + _ + " found") ~ pxs ^^ mkList} 183 | 184 | def chrWithDoubleTicks = ('\'' ~ '\'') ^^^ '\'' | chrExcept('\'', EofCh) 185 | 186 | def chrWithDoubleQuotes = ('"' ~ '"') ^^^ '"' | chrExcept('"', EofCh) 187 | 188 | def chrWithDoubleBackTicks = ('`' ~ '`') ^^^ '`' | chrExcept('`', EofCh) 189 | } 190 | 191 | object SelectParser extends StandardTokenParsers { 192 | override val lexical = new SelectLexical 193 | lexical.delimiters ++= List("*", ",", "=", "!=", ">", "<", ">=", "<=", "(", ")") 194 | lexical.reserved ++= List( 195 | "select", "from", "where", "and", "or", "like", "not", "is", "null", "between", 196 | "every", "in", "order", "by", "asc", "desc", "intersection", "limit", "count(*)" 197 | ) 198 | 199 | def expr = ("select" ~> outputList) ~ ("from" ~> ident) ~ whereClause ~ order ~ limit ^^ { case ol ~ i ~ w ~ o ~ l => SelectEval(ol, i, w, o, l) } 200 | 201 | def order: Parser[OrderEval] = 202 | ( "order" ~> "by" ~> ident ~ ("asc" | "desc") ^^ { case i ~ way => SimpleOrderEval(i, way) } 203 | | "order" ~> "by" ~> ident ^^ { i => SimpleOrderEval(i, "asc") } 204 | | success(NoopOrder()) 205 | ) 206 | 207 | def limit: Parser[LimitEval] = 208 | ( "limit" ~> numericLit ^^ { num => SomeLimit(num.toInt) } 209 | | success(NoopLimit()) 210 | ) 211 | 212 | def whereClause: Parser[WhereEval] = 213 | ( "where" ~> where 214 | | success(new NoopWhere) 215 | ) 216 | def where: Parser[WhereEval] = 217 | ( simplePredicate ~ setOp ~ where ^^ { case sp ~ op ~ rp => CompoundWhereEval(sp, op, rp) } 218 | | simplePredicate 219 | ) 220 | 221 | def simplePredicate: Parser[WhereEval] = 222 | ( ident <~ "is" <~ "null" ^^ { i => IsNullEval(i, true) } 223 | | ident <~ "is" <~ "not" <~ "null" ^^ { i => IsNullEval(i, false) } 224 | | ident ~ ("between" ~> stringLit) ~ ("and" ~> stringLit) ^^ { case i ~ a ~ b => IsBetweenEval(i, a, b) } 225 | | ident ~ ("in" ~> "(" ~> repsep(stringLit, ",") <~ ")") ^^ { case i ~ strs => InEval(i, strs) } 226 | | ("every" ~> "(" ~> ident <~ ")") ~ op ~ stringLit ^^ { case i ~ o ~ v => EveryEval(i, o, v)} 227 | | ident ~ op ~ stringLit ^^ { case i ~ o ~ v => SimpleWhereEval(i, o, v) } 228 | | "(" ~> where <~ ")" 229 | ) 230 | def setOp = "and" | "or" | "intersection" 231 | def op = "=" | "!=" | ">" | "<" | ">=" | "<=" | "like" | "not" ~ "like" ^^^ { "not-like" } 232 | 233 | def outputList: Parser[OutputEval] = 234 | ( "*" ^^^ { new AllOutput } 235 | | "count(*)" ^^^ { new CountOutput } 236 | | repsep(ident, ",") ^^ { attrNames => CompoundOutput(attrNames) } 237 | ) 238 | 239 | def makeSelectEval(input: String): SelectEval = { 240 | val tokens = new lexical.Scanner(input) 241 | phrase(expr)(tokens) match { 242 | case Success(selectEval, _) => selectEval 243 | case Failure(msg, _) => sys.error(msg) 244 | case Error(msg, _) => sys.error(msg) 245 | } 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /src/main/scala/fakesdb/actions/Action.scala: -------------------------------------------------------------------------------- 1 | package fakesdb.actions 2 | 3 | import scala.xml.NodeSeq 4 | import fakesdb._ 5 | 6 | abstract class Action(data: Data) { 7 | 8 | def handle(params: Params): NodeSeq 9 | 10 | protected def responseMetaData() = { 11 | {requestId}0 12 | } 13 | 14 | protected def parseDomain(params: Params): Domain = { 15 | val domainName = params.getOrElse("DomainName", sys.error("No domain name")) 16 | return data.getDomain(domainName).getOrElse(sys.error("Invalid domain name "+domainName)) 17 | } 18 | 19 | val namespace = "http://sdb.amazonaws.com/doc/2009-04-15/" 20 | 21 | val requestId = Action.requestCounter.incrementAndGet() 22 | 23 | } 24 | 25 | object Action { 26 | private val requestCounter = new java.util.concurrent.atomic.AtomicInteger() 27 | } 28 | -------------------------------------------------------------------------------- /src/main/scala/fakesdb/actions/BatchDeleteAttributes.scala: -------------------------------------------------------------------------------- 1 | package fakesdb.actions 2 | 3 | import scala.collection.mutable.ListBuffer 4 | import scala.collection.mutable.LinkedHashMap 5 | import scala.xml.NodeSeq 6 | import fakesdb._ 7 | 8 | class BatchDeleteAttributes(data: Data) extends Action(data) { 9 | 10 | def handle(params: Params): NodeSeq = { 11 | val domain = parseDomain(params) 12 | discoverAttributes(params).delete(domain) 13 | 14 | {responseMetaData} 15 | 16 | } 17 | 18 | private def discoverAttributes(params: Params): ItemUpdates = { 19 | val updates = new ItemUpdates() 20 | var i = 0 21 | var stop = false 22 | while (!stop) { 23 | val itemName = params.get("Item."+i+".ItemName") 24 | if (itemName.isEmpty) { 25 | if (i > 1) stop = true 26 | } else { 27 | var j = 0 28 | var stop2 = false 29 | while (!stop2) { 30 | val attrName = params.get("Item."+i+".Attribute."+j+".Name") 31 | //values currently not supported. 32 | //val attrValue = params.get("Item."+i+".Attribute."+j+".Value") 33 | if (attrName.isEmpty) { 34 | if (j > 1) stop2 = true 35 | updates.add(itemName.get) 36 | } else { 37 | updates.add(itemName.get, attrName.get) 38 | } 39 | j += 1 40 | } 41 | } 42 | i += 1 43 | } 44 | updates 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/main/scala/fakesdb/actions/BatchPutAttributes.scala: -------------------------------------------------------------------------------- 1 | package fakesdb.actions 2 | 3 | import scala.collection.mutable.ListBuffer 4 | import scala.collection.mutable.LinkedHashMap 5 | import scala.xml.NodeSeq 6 | import fakesdb._ 7 | 8 | class BatchPutAttributes(data: Data) extends Action(data) { 9 | 10 | def handle(params: Params): NodeSeq = { 11 | val domain = parseDomain(params) 12 | discoverAttributes(params).update(domain) 13 | 14 | {responseMetaData} 15 | 16 | } 17 | 18 | private def discoverAttributes(params: Params): ItemUpdates = { 19 | val updates = new ItemUpdates 20 | var i = 0 21 | var stop = false 22 | while (!stop) { 23 | val itemName = params.get("Item."+i+".ItemName") 24 | if (itemName.isEmpty) { 25 | if (i > 1) stop = true 26 | } else { 27 | var j = 0 28 | var stop2 = false 29 | while (!stop2) { 30 | val attrName = params.get("Item."+i+".Attribute."+j+".Name") 31 | val attrValue = params.get("Item."+i+".Attribute."+j+".Value") 32 | val attrReplace = params.get("Item."+i+".Attribute."+j+".Replace") 33 | if (attrName.isEmpty || attrValue.isEmpty) { 34 | if (j > 1) stop2 = true 35 | } else { 36 | val replace = attrReplace.getOrElse("false").toBoolean 37 | updates.add(itemName.get, attrName.get, attrValue.get, replace) 38 | } 39 | j += 1 40 | } 41 | } 42 | i += 1 43 | } 44 | updates 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/main/scala/fakesdb/actions/ConditionalChecking.scala: -------------------------------------------------------------------------------- 1 | package fakesdb.actions 2 | 3 | import fakesdb._ 4 | 5 | trait ConditionalChecking { 6 | 7 | private val expectedNamePattern = """Expected\.(\d+)\.Name""".r 8 | private val expectedNamePattern2 = """Expected\.Name""".r 9 | 10 | def checkConditionals(item: Item, params: Params) { 11 | for (condition <- discoverConditional(params)) { 12 | condition match { 13 | case (name, None) => for (f <- item.getAttributes.find(_.name == name)) throw new ConditionalCheckFailedException(condition) 14 | case (name, Some(value)) => item.getAttributes find (_.name == name) match { 15 | case None => throw new AttributeDoesNotExistException(name) 16 | case Some(attr) => if (attr.getValues.toList != List(value)) throw new ConditionalCheckFailedException(condition, attr.getValues.toList) 17 | } 18 | } 19 | } 20 | } 21 | 22 | private def discoverConditional(params: Params): Option[Tuple2[String, Option[String]]] = { 23 | val keys = params.keys find (k => k.startsWith("Expected") && k.endsWith("Name")) 24 | if (keys.isEmpty) { 25 | return None 26 | } 27 | if (keys.size > 1) { 28 | sys.error("Only one condition may be specified") 29 | } 30 | val name = params.get(keys.head).get 31 | keys.head match { 32 | case expectedNamePattern2() => { 33 | // the aws jdk sends "Expected.Name" with no digit 34 | for (v <- params.get("Expected.Exists")) { 35 | if (v == "false") { 36 | return Some((name, None)) 37 | } 38 | } 39 | for (v <- params.get("Expected.Value")) { 40 | return Some((name, Some(v))) 41 | } 42 | } 43 | case expectedNamePattern(digit) => { 44 | // typica sent "Expected.x.Name" with a digit 45 | for (v <- params.get("Expected.%s.Exists".format(digit))) { 46 | if (v == "false") { 47 | return Some((name, None)) 48 | } 49 | } 50 | for (v <- params.get("Expected.%s.Value".format(digit))) { 51 | return Some((name, Some(v))) 52 | } 53 | } 54 | } 55 | None 56 | } 57 | } 58 | 59 | class ConditionalCheckFailedException(message: String) extends SDBException(409, "ConditionalCheckFailed", message) { 60 | def this(condition: Tuple2[String, Option[String]]) = { 61 | this("Attribute (%s) value exists".format(condition._1)) 62 | } 63 | 64 | def this(condition: Tuple2[String, Option[String]], actual: List[String]) = { 65 | this("Attribute (%s) value is (%s) but was expected (%s)".format(condition._1, actual, condition._2.get)) 66 | } 67 | } 68 | 69 | class AttributeDoesNotExistException(name: String) 70 | extends SDBException(404, "AttributeDoesNotExist", "Attribute (%s) does not exist".format(name)) 71 | -------------------------------------------------------------------------------- /src/main/scala/fakesdb/actions/CreateDomain.scala: -------------------------------------------------------------------------------- 1 | package fakesdb.actions 2 | 3 | import scala.xml.NodeSeq 4 | import fakesdb._ 5 | 6 | class CreateDomain(data: Data) extends Action(data) { 7 | 8 | def handle(params: Params): NodeSeq = { 9 | val domainName = params.getOrElse("DomainName", sys.error("No domain name")) 10 | if (domainName == "_flush") { 11 | data.flush() // The special one 12 | } else if (domainName == "_dump") { 13 | dump(domainName) 14 | } else { 15 | data.getOrCreateDomain(domainName) 16 | } 17 | 18 | {responseMetaData} 19 | 20 | } 21 | 22 | def dump(domainName: String) { 23 | for (d <- data.getDomains) { 24 | println("Domain "+d.name) 25 | for (i <- d.getItems) { 26 | println("\tItem "+i.name) 27 | for (a <- i.getAttributes) { 28 | println("\t\t"+a.name+" = "+a.getValues.mkString(", ")) 29 | } 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/scala/fakesdb/actions/DeleteAttributes.scala: -------------------------------------------------------------------------------- 1 | package fakesdb.actions 2 | 3 | import scala.collection.mutable.ListBuffer 4 | import scala.xml.NodeSeq 5 | import fakesdb._ 6 | 7 | class DeleteAttributes(data: Data) extends Action(data) with ConditionalChecking { 8 | 9 | def handle(params: Params): NodeSeq = { 10 | val domain = parseDomain(params) 11 | val itemName = params.getOrElse("ItemName", sys.error("No item name")) 12 | val item = domain.getItem(itemName) match { 13 | case Some(item) => { 14 | checkConditionals(item, params) 15 | doDelete(params, domain, item) 16 | } 17 | case _ => 18 | } 19 | 20 | {responseMetaData} 21 | 22 | } 23 | 24 | private def doDelete(params: Params, domain: Domain, item: Item) = { 25 | val destroy = discoverAttributes(params) 26 | if (destroy.isEmpty) { 27 | domain.deleteItem(item) 28 | } else { 29 | for (attr <- destroy) attr._2 match { 30 | case Some(value) => item.delete(attr._1, value) 31 | case None => item.delete(attr._1) 32 | } 33 | domain.deleteIfEmpty(item) 34 | } 35 | } 36 | 37 | private def discoverAttributes(params: Params): List[(String, Option[String])] = { 38 | val attrs = new ListBuffer[(String, Option[String])]() 39 | var i = 0 40 | var stop = false 41 | while (!stop) { 42 | val attrName = params.get("Attribute."+i+".Name") 43 | val attrValue = params.get("Attribute."+i+".Value") 44 | if (attrName.isEmpty && attrValue.isEmpty) { 45 | if (i > 1) stop = true 46 | } else { 47 | attrs += Tuple2(attrName.get, attrValue) 48 | } 49 | i += 1 50 | } 51 | attrs.toList 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/main/scala/fakesdb/actions/DeleteDomain.scala: -------------------------------------------------------------------------------- 1 | package fakesdb.actions 2 | 3 | import scala.xml.NodeSeq 4 | import fakesdb._ 5 | 6 | class DeleteDomain(data: Data) extends Action(data) { 7 | 8 | def handle(params: Params): NodeSeq = { 9 | data.deleteDomain(parseDomain(params)) 10 | 11 | {responseMetaData} 12 | 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/main/scala/fakesdb/actions/DomainMetadata.scala: -------------------------------------------------------------------------------- 1 | package fakesdb.actions 2 | 3 | import scala.xml.NodeSeq 4 | import fakesdb._ 5 | 6 | class DomainMetadata(data: Data) extends Action(data) { 7 | 8 | def handle(params: Params): NodeSeq = { 9 | def sum(list: List[Int]) = list.foldLeft(0)(_ + _) 10 | val allItems = data.getDomains.flatMap(_.getItems).toList 11 | val allAttrs = allItems.flatMap(_.getAttributes.toList) 12 | val allValues = allAttrs.flatMap(_.getValues.toList) 13 | 14 | 15 | {allItems.size} 16 | {sum(allItems.map(_.name.size))} 17 | {allAttrs.toList.size} 18 | {sum(allAttrs.map(_.name.size))} 19 | {allValues.size} 20 | {sum(allValues.map(_.size))} 21 | 0 22 | 23 | {responseMetaData} 24 | 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/scala/fakesdb/actions/Errors.scala: -------------------------------------------------------------------------------- 1 | package fakesdb.actions 2 | 3 | // http://docs.amazonwebservices.com/AmazonSimpleDB/latest/DeveloperGuide/index.html?APIError.html 4 | class SDBException(val httpStatus: Int, val xmlCode: String, val message: String) extends RuntimeException(message) 5 | 6 | class NumberItemAttributesExceededException 7 | extends SDBException(409, "NumberItemAttributesExceeded", "Too many attributes in this item") 8 | 9 | class NumberSubmittedItemsExceeded 10 | extends SDBException(409, "NumberSubmittedItemsExceeded", "Too many items in a single call. Up to 25 items per call allowed.") 11 | 12 | class EmptyAttributeNameException 13 | extends SDBException(400, "InvalidParameterValue", "Value () for parameter Name is invalid. The empty string is an illegal attribute name") 14 | 15 | class MissingItemNameException 16 | extends SDBException(400, "MissingParameter", "The request must contain the parameter ItemName.") 17 | 18 | class InvalidParameterValue(message: String) 19 | extends SDBException(400, "InvalidParameterValue", message) 20 | 21 | object InvalidParameterValue { 22 | def failIfOver1024(name: String, value: String): Unit = { 23 | if (value.getBytes.size > 1024) { 24 | throw new InvalidParameterValue("Value (\"%s\") for parameter %s is invalid. Value exceeds maximum length of 1024.".format(value, name)); 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /src/main/scala/fakesdb/actions/GetAttributes.scala: -------------------------------------------------------------------------------- 1 | package fakesdb.actions 2 | 3 | import scala.collection.mutable.ListBuffer 4 | import scala.xml.NodeSeq 5 | import fakesdb._ 6 | 7 | class GetAttributes(data: Data) extends Action(data) { 8 | 9 | def handle(params: Params): NodeSeq = { 10 | val domain = parseDomain(params) 11 | val itemName = params.getOrElse("ItemName", sys.error("No item name")) 12 | val items = domain.getItem(itemName) match { 13 | case Some(item) => List(item) 14 | case None => List() 15 | } 16 | val requested = discoverAttributes(params) 17 | 18 | 19 | {for (item <- items) yield 20 | {for (nv <- filter(item, requested)) yield 21 | {nv._1.name}{nv._2} 22 | } 23 | } 24 | 25 | {responseMetaData} 26 | 27 | } 28 | 29 | protected def filter(item: Item, requested: List[String]): Iterator[(Attribute, String)] = { 30 | val attrs = item.getAttributes.filter((a: Attribute) => requested.isEmpty || requested.contains(a.name)) 31 | attrs.flatMap((a: Attribute) => a.getValues.map((v: String) => (a, v))) 32 | } 33 | 34 | protected def discoverAttributes(params: Params): List[String] = { 35 | val requested = new ListBuffer[String]() 36 | var i = 0 37 | var stop = false 38 | while (!stop) { 39 | val attrName = params.get("AttributeName."+i) 40 | if (attrName.isEmpty) { 41 | if (i > 1) stop = true 42 | } else { 43 | requested += attrName.get 44 | } 45 | i += 1 46 | } 47 | requested.toList 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/main/scala/fakesdb/actions/ItemUpdates.scala: -------------------------------------------------------------------------------- 1 | package fakesdb.actions 2 | 3 | import scala.collection.mutable.ListBuffer 4 | import scala.collection.mutable.LinkedHashMap 5 | import fakesdb._ 6 | 7 | /** itemName -> [attrName -> AttributeUpdate] */ 8 | class ItemUpdates extends LinkedHashMap[String, LinkedHashMap[String, AttributeUpdate]] { 9 | def add(itemName: String, attrName: String, attrValue: String, replace: Boolean): Unit = { 10 | val attrs = getOrElseUpdate(itemName, new LinkedHashMap[String, AttributeUpdate]) 11 | val attr = attrs.getOrElseUpdate(attrName, new AttributeUpdate(replace)) 12 | attr.values += attrValue 13 | } 14 | 15 | def add(itemName: String, attrName: String): Unit = { 16 | val attrs = getOrElseUpdate(itemName, new LinkedHashMap[String, AttributeUpdate]) 17 | val attr = attrs.getOrElseUpdate(attrName, new AttributeUpdate(false)) 18 | } 19 | 20 | def add(itemName: String): Unit = { 21 | val attrs = getOrElseUpdate(itemName, new LinkedHashMap[String, AttributeUpdate]) 22 | } 23 | 24 | def update(domain: Domain): Unit = { 25 | checkSize() 26 | foreach { case (itemName, attrs) => { 27 | attrs.foreach { case (attrName, attrUpdate) => { 28 | domain.getOrCreateItem(itemName).put(attrName, attrUpdate.values, attrUpdate.replace) 29 | }} 30 | }} 31 | } 32 | 33 | def delete(domain: Domain): Unit = { 34 | checkSize() 35 | foreach { case (itemName, attrs) => { 36 | val item = domain.getItem(itemName) match { 37 | case Some(item) => { 38 | if (attrs.isEmpty) { 39 | domain.deleteItem(item) 40 | } else { 41 | attrs.foreach { case (attrName, replace) => { 42 | item.delete(attrName) 43 | }} 44 | domain.deleteIfEmpty(item) 45 | } 46 | } 47 | case _ => 48 | } 49 | }} 50 | } 51 | 52 | private def checkSize() = { 53 | if (size > 25) { 54 | throw new NumberSubmittedItemsExceeded 55 | } 56 | } 57 | } 58 | 59 | class AttributeUpdate(val replace: Boolean) { 60 | val values = new ListBuffer[String]() 61 | } 62 | -------------------------------------------------------------------------------- /src/main/scala/fakesdb/actions/ListDomains.scala: -------------------------------------------------------------------------------- 1 | package fakesdb.actions 2 | 3 | import scala.xml.NodeSeq 4 | import fakesdb._ 5 | 6 | class ListDomains(data: Data) extends Action(data) { 7 | 8 | def handle(params: Params): NodeSeq = { 9 | 10 | 11 | {for (domain <- data.getDomains) yield 12 | {domain.name} 13 | } 14 | 15 | {responseMetaData} 16 | 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/scala/fakesdb/actions/PutAttributes.scala: -------------------------------------------------------------------------------- 1 | package fakesdb.actions 2 | 3 | import scala.xml.NodeSeq 4 | import fakesdb._ 5 | 6 | class PutAttributes(data: Data) extends Action(data) with ConditionalChecking { 7 | 8 | def handle(params: Params): NodeSeq = { 9 | val domain = parseDomain(params) 10 | val itemName = params.getOrElse("ItemName", throw new MissingItemNameException) 11 | val item = domain.getOrCreateItem(itemName) 12 | 13 | checkConditionals(item, params) 14 | 15 | discoverAttributes(itemName, params).update(domain) 16 | 17 | 18 | {responseMetaData} 19 | 20 | } 21 | 22 | private def discoverAttributes(itemName: String, params: Params): ItemUpdates = { 23 | val updates = new ItemUpdates() 24 | var i = 0 25 | var stop = false 26 | while (!stop) { 27 | val attrName = params.get("Attribute."+i+".Name") 28 | val attrValue = params.get("Attribute."+i+".Value") 29 | val attrReplace = params.get("Attribute."+i+".Replace") 30 | if (attrName.isEmpty || attrValue.isEmpty) { 31 | if (i > 1) stop = true 32 | } else { 33 | val replace = attrReplace.getOrElse("false").toBoolean 34 | updates.add(itemName, attrName.get, attrValue.get, replace) 35 | } 36 | i += 1 37 | } 38 | updates 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/scala/fakesdb/actions/Select.scala: -------------------------------------------------------------------------------- 1 | package fakesdb.actions 2 | 3 | import scala.xml.NodeSeq 4 | import fakesdb._ 5 | 6 | class Select(data: Data) extends Action(data) { 7 | 8 | override def handle(params: Params): NodeSeq = { 9 | val nextToken = params.get("NextToken") map { _.toInt } 10 | val itemsData = params.get("SelectExpression") match { 11 | case Some(s) => val se = SelectParser.makeSelectEval(s) ; se.select(data, nextToken) 12 | case None => sys.error("No select expression") 13 | } 14 | val items = itemsData._1 15 | val itemsLength = itemsData._2 16 | val newNextToken = if (itemsData._3) List(itemsLength) else List() 17 | 18 | 19 | {for (item <- items) yield 20 | 21 | {item._1} 22 | {for (nv <- item._2) yield 23 | {nv._1}{nv._2} 24 | } 25 | 26 | } 27 | {for (token <- newNextToken) yield 28 | {token} 29 | } 30 | 31 | {responseMetaData} 32 | 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/main/scala/fakesdb/stub/AmazonSimpleDbStub.scala: -------------------------------------------------------------------------------- 1 | package fakesdb.stub 2 | 3 | import fakesdb.actions.ItemUpdates 4 | import fakesdb.SelectParser 5 | import com.amazonaws.regions.Region 6 | import com.amazonaws.services.simpledb.AmazonSimpleDB 7 | import com.amazonaws.services.simpledb.model.UpdateCondition 8 | import com.amazonaws.services.simpledb.model.Attribute 9 | import com.amazonaws.services.simpledb.model.Item 10 | import com.amazonaws.services.simpledb.model.SelectRequest 11 | import com.amazonaws.services.simpledb.model.ListDomainsRequest 12 | import com.amazonaws.services.simpledb.model.GetAttributesRequest 13 | import com.amazonaws.services.simpledb.model.PutAttributesRequest 14 | import com.amazonaws.services.simpledb.model.CreateDomainRequest 15 | import com.amazonaws.services.simpledb.model.SelectResult 16 | import com.amazonaws.services.simpledb.model.BatchDeleteAttributesRequest 17 | import com.amazonaws.services.simpledb.model.DeleteDomainRequest 18 | import com.amazonaws.services.simpledb.model.DeleteAttributesRequest 19 | import com.amazonaws.services.simpledb.model.GetAttributesResult 20 | import com.amazonaws.services.simpledb.model.DomainMetadataResult 21 | import com.amazonaws.services.simpledb.model.DomainMetadataRequest 22 | import com.amazonaws.services.simpledb.model.ListDomainsResult 23 | import com.amazonaws.services.simpledb.model.BatchPutAttributesRequest 24 | import scala.collection.JavaConversions._ 25 | 26 | /** Stubs out the {@link AmazonSimpleDB} interface in memory. */ 27 | class AmazonSimpleDbStub extends AmazonSimpleDB { 28 | 29 | private val data = new fakesdb.Data() 30 | 31 | def flush() { 32 | data.flush 33 | } 34 | 35 | def dump() { 36 | data.getDomains.foreach { domain => 37 | domain.getItems.foreach { item => 38 | item.getAttributes.foreach { attribute => 39 | println(domain.name + " " + item.name + " " + attribute.name + " " + attribute.values.mkString(", ")) 40 | } 41 | } 42 | } 43 | } 44 | 45 | override def setEndpoint(endpoint: String): Unit = { 46 | // noop 47 | } 48 | 49 | override def setRegion(region: Region): Unit = { 50 | // noop 51 | } 52 | 53 | override def select(req: SelectRequest): SelectResult = { 54 | val se = SelectParser.makeSelectEval(req.getSelectExpression) 55 | val result = se.select(data, Option(req.getNextToken).map(_.toInt)) 56 | val nextToken = if (result._3) result._2.toString else null 57 | return new SelectResult() 58 | .withNextToken(nextToken) 59 | .withItems(result._1.map { i => new Item(i._1, i._2.map(a => new Attribute(a._1, a._2))) } ) 60 | } 61 | 62 | override def deleteAttributes(req: DeleteAttributesRequest): Unit = { 63 | for (c <- toConditional(req.getExpected)) { 64 | data.getDomain(req.getDomainName).get.getItem(req.getItemName).get.assertCondition(c) 65 | } 66 | val updates = new ItemUpdates() 67 | for (attribute <- req.getAttributes) { 68 | updates.add(req.getItemName, attribute.getName) 69 | } 70 | updates.delete(data.getDomain(req.getDomainName).get) 71 | } 72 | 73 | override def putAttributes(req: PutAttributesRequest): Unit = { 74 | for (c <- toConditional(req.getExpected)) { 75 | data.getDomain(req.getDomainName).get.getItem(req.getItemName).get.assertCondition(c) 76 | } 77 | val updates = new ItemUpdates() 78 | for (attribute <- req.getAttributes) { 79 | updates.add(req.getItemName, attribute.getName, attribute.getValue, attribute.getReplace) 80 | } 81 | updates.update(data.getDomain(req.getDomainName).get) 82 | } 83 | 84 | override def batchDeleteAttributes(req: BatchDeleteAttributesRequest): Unit = { 85 | val updates = new ItemUpdates() 86 | for (item <- req.getItems) { 87 | for (attribute <- item.getAttributes) { 88 | updates.add(item.getName, attribute.getName) 89 | } 90 | } 91 | updates.delete(data.getDomain(req.getDomainName).get) 92 | } 93 | 94 | override def batchPutAttributes(req: BatchPutAttributesRequest): Unit = { 95 | val updates = new ItemUpdates() 96 | for (item <- req.getItems) { 97 | for (attribute <- item.getAttributes) { 98 | updates.add(item.getName, attribute.getName, attribute.getValue, attribute.getReplace) 99 | } 100 | } 101 | updates.update(data.getDomain(req.getDomainName).get) 102 | } 103 | 104 | override def deleteDomain(req: DeleteDomainRequest): Unit = { 105 | data.deleteDomain(data.getDomain(req.getDomainName).get) 106 | } 107 | 108 | override def createDomain(req: CreateDomainRequest): Unit = { 109 | data.getOrCreateDomain(req.getDomainName) 110 | } 111 | 112 | override def listDomains(): ListDomainsResult = { 113 | new ListDomainsResult().withDomainNames(data.getDomains.map { _.name }.toList) 114 | } 115 | 116 | override def listDomains(req: ListDomainsRequest): ListDomainsResult = { 117 | new ListDomainsResult().withDomainNames(data.getDomains.map { _.name }.toList) 118 | } 119 | 120 | override def getAttributes(req: GetAttributesRequest): GetAttributesResult = { 121 | data.getDomain(req.getDomainName).get.getItem(req.getItemName) match { 122 | case Some(item) => new GetAttributesResult().withAttributes(item.getAttributes 123 | .filter(a => req.getAttributeNames.size == 0 || req.getAttributeNames.contains(a.name)) 124 | .flatMap(a => a.getValues.map(v => new Attribute(a.name, v))).toList) 125 | case None => new GetAttributesResult() 126 | } 127 | } 128 | 129 | override def domainMetadata(req: DomainMetadataRequest): DomainMetadataResult = { 130 | val domain = data.getDomain(req.getDomainName).get 131 | return new DomainMetadataResult() 132 | .withItemCount(domain.getItems.size) 133 | .withAttributeNameCount(domain.getItems.foldLeft(0)(_ + _.getAttributes.size)) 134 | .withAttributeValueCount(domain.getItems.foldLeft(0)(_ + _.getAttributes.foldLeft(0)(_ + _.getValues.size))) 135 | } 136 | 137 | private def toConditional(condition: UpdateCondition): Option[Tuple2[String, Option[String]]] = { 138 | if (condition == null) { 139 | None 140 | } else if (condition.getExists != null && !condition.getExists) { 141 | Some((condition.getName, None)) 142 | } else if (condition.getValue != null) { 143 | Some((condition.getName, Some(condition.getValue))) 144 | } else if (condition.getName != null) { 145 | sys.error("Expected value or false exists") 146 | } else { 147 | None 148 | } 149 | } 150 | 151 | } 152 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/web.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | fakesdb 4 | 5 | FakeSdbServlet 6 | fakesdb.FakeSdbServlet 7 | 1 8 | 9 | 10 | FakeSdbServlet 11 | /* 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/test/scala/fakesdb/AbstractFakeSdbTest.scala: -------------------------------------------------------------------------------- 1 | package fakesdb 2 | 3 | import org.junit._ 4 | import org.junit.Assert._ 5 | import org.hamcrest.Matchers._ 6 | import com.amazonaws.AmazonServiceException 7 | import com.amazonaws.auth.BasicAWSCredentials 8 | import com.amazonaws.services.simpledb.AmazonSimpleDBClient 9 | import com.amazonaws.services.simpledb.model._ 10 | import scala.collection.JavaConversions._ 11 | 12 | object AbstractFakeSdbTest { 13 | val jetty = Jetty(8080) 14 | jetty.server.start() 15 | } 16 | 17 | abstract class AbstractFakeSdbTest { 18 | 19 | // start jetty 20 | AbstractFakeSdbTest.jetty 21 | 22 | val sdb = new AmazonSimpleDBClient(new BasicAWSCredentials("ignored", "ignored")) 23 | sdb.setEndpoint("http://127.0.0.1:8080") 24 | 25 | val domaina = "domaina" 26 | 27 | @Before 28 | def setUp(): Unit = { 29 | flush() 30 | } 31 | 32 | def flush(): Unit = { 33 | createDomain("_flush") 34 | } 35 | 36 | type KV = Tuple2[String, String] 37 | 38 | def add(domain: String, itemName: String, attrs: KV*): Unit = { 39 | _add(domain, itemName, attrs, None) 40 | } 41 | 42 | def add(domain: String, itemName: String, cond: UpdateCondition, attrs: KV*): Unit = { 43 | _add(domain, itemName, attrs, Some(cond)) 44 | } 45 | 46 | def doesNotExist(attrName: String) = new UpdateCondition(attrName, null, false) 47 | 48 | def hasValue(attrName: String, attrValue: String) = new UpdateCondition(attrName, attrValue, true) 49 | 50 | def assertFails(code: String, message: String, block: => Unit) { 51 | try { 52 | block 53 | fail("Should have failed with " + message) 54 | } catch { 55 | case e: AmazonServiceException => { 56 | assertThat(e.getErrorCode, is(code)) 57 | assertThat(e.getMessage, startsWith(message)) 58 | } 59 | case e: Exception => { 60 | assertThat(e.getMessage, startsWith(message)) 61 | } 62 | } 63 | } 64 | 65 | def assertItems(domain: String, item: String, expectedItems: String*) { 66 | val result = sdb.getAttributes(new GetAttributesRequest(domain, item)) 67 | val actualItems = result.getAttributes.map { a => a.getName + " = " + a.getValue } 68 | assertEquals(expectedItems.mkString("\n"), actualItems.mkString("\n")) 69 | } 70 | 71 | private def _add(domain: String, itemName: String, attrs: Seq[KV], cond: Option[UpdateCondition]): Unit = { 72 | val req = new PutAttributesRequest() 73 | .withDomainName(domain) 74 | .withItemName(itemName) 75 | for (a <- attrs) { 76 | val replace = asScalaBuffer(req.getAttributes).exists { _.getName == a._1 } 77 | req.withAttributes(new ReplaceableAttribute(a._1, a._2, replace)) 78 | } 79 | cond match { 80 | case Some(c) => req.withExpected(c) 81 | case None => 82 | } 83 | sdb.putAttributes(req) 84 | } 85 | 86 | protected def select(query: String): SelectResult = { 87 | sdb.select(new SelectRequest(query, true)) 88 | } 89 | 90 | protected def select(query: String, nextToken: String): SelectResult = { 91 | sdb.select(new SelectRequest(query, true).withNextToken(nextToken)) 92 | } 93 | 94 | protected def createDomain(name: String) { 95 | sdb.createDomain(new CreateDomainRequest(name)) 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /src/test/scala/fakesdb/BatchDeleteAttributesTest.scala: -------------------------------------------------------------------------------- 1 | package fakesdb 2 | 3 | import org.junit._ 4 | import org.junit.Assert._ 5 | import com.amazonaws.services.simpledb.model._ 6 | 7 | class BatchDeleteAttributesTest extends AbstractFakeSdbTest { 8 | 9 | @Before 10 | def createDomain(): Unit = { 11 | createDomain(domaina) 12 | } 13 | 14 | @Test 15 | def testDelete(): Unit = { 16 | add(domaina, "itema", "a" -> "1") 17 | add(domaina, "itemb", "a" -> "1", "b" -> "1") 18 | 19 | sdb.batchDeleteAttributes(new BatchDeleteAttributesRequest().withDomainName(domaina).withItems( 20 | new DeletableItem().withName("itema"), 21 | new DeletableItem().withName("itemb") 22 | )) 23 | 24 | assertEquals(0, select("SELECT * FROM domaina").getItems.size) 25 | } 26 | 27 | @Test 28 | def testDeleteTooMany(): Unit = { 29 | val req = new BatchDeleteAttributesRequest().withDomainName(domaina) 30 | for (i <- 1.to(26)) { 31 | req.withItems(new DeletableItem().withName("item" + i)) 32 | } 33 | assertFails("NumberSubmittedItemsExceeded", "NumberSubmittedItemsExceeded: Too many items in a single call. Up to 25 items per call allowed.", { 34 | sdb.batchDeleteAttributes(req) 35 | }) 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/test/scala/fakesdb/BatchPutAttributesTest.scala: -------------------------------------------------------------------------------- 1 | package fakesdb 2 | 3 | import org.junit._ 4 | import com.amazonaws.services.simpledb.model._ 5 | 6 | class BatchPutAttributesTest extends AbstractFakeSdbTest { 7 | 8 | @Before 9 | def createDomain(): Unit = { 10 | createDomain("domaina") 11 | } 12 | 13 | @Test 14 | def testPut(): Unit = { 15 | val req = new BatchPutAttributesRequest().withDomainName("domaina").withItems( 16 | new ReplaceableItem("itema").withAttributes( 17 | new ReplaceableAttribute("a", "1", true), 18 | new ReplaceableAttribute("b", "2", true) 19 | ), 20 | new ReplaceableItem("itemb").withAttributes( 21 | new ReplaceableAttribute("c", "3", true) 22 | ) 23 | ) 24 | sdb.batchPutAttributes(req) 25 | 26 | // Check results 27 | assertItems("domaina", "itema", "a = 1", "b = 2") 28 | assertItems("domaina", "itemb", "c = 3") 29 | } 30 | 31 | @Test 32 | def testPutTooMany(): Unit = { 33 | val req = new BatchPutAttributesRequest().withDomainName("domaina") 34 | for (i <- 1.to(26)) { 35 | req.withItems(new ReplaceableItem("item"+i).withAttributes( 36 | new ReplaceableAttribute("a", "1", true) 37 | )) 38 | } 39 | assertFails("NumberSubmittedItemsExceeded", "NumberSubmittedItemsExceeded: Too many items in a single call. Up to 25 items per call allowed.", { 40 | sdb.batchPutAttributes(req) 41 | }) 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/test/scala/fakesdb/CreateDomainTest.scala: -------------------------------------------------------------------------------- 1 | package fakesdb 2 | 3 | import org.junit._ 4 | 5 | class CreateDomainTest extends AbstractFakeSdbTest { 6 | 7 | @Test 8 | def invalidDomainName(): Unit = { 9 | assertFails("InvalidParameterValue", "InvalidParameterValue: Value (\"foo!\") for parameter DomainName is invalid.", { 10 | createDomain("foo!") 11 | }) 12 | } 13 | 14 | } -------------------------------------------------------------------------------- /src/test/scala/fakesdb/DeleteAttributesTest.scala: -------------------------------------------------------------------------------- 1 | package fakesdb 2 | 3 | import org.junit._ 4 | import org.junit.Assert._ 5 | import com.amazonaws.services.simpledb.model._ 6 | import com.amazonaws.services.simpledb.model.{Attribute => AwsAttribute} 7 | import scala.collection.JavaConversions._ 8 | 9 | class DeleteAttributesTest extends AbstractFakeSdbTest { 10 | 11 | @Before 12 | def createDomain(): Unit = { 13 | createDomain("domaina") 14 | } 15 | 16 | @Test 17 | def testDeleteOneEntireAttribute(): Unit = { 18 | add(domaina, "itema", "a" -> "1", "b" -> "2") 19 | assertItems(domaina, "itema", "a = 1", "b = 2") 20 | 21 | delete("a") 22 | assertItems(domaina, "itema", "b = 2") 23 | } 24 | 25 | @Test 26 | def testDeleteBothAttributesDeletesTheItem(): Unit = { 27 | add(domaina, "itema", "a" -> "1", "b" -> "2") 28 | delete("a", "b") 29 | assertEquals(0, select("SELECT * FROM domaina").getItems.size) 30 | } 31 | 32 | @Test 33 | def testDeleteNoAttributesDeletesTheItem(): Unit = { 34 | add(domaina, "itema", "a" -> "1", "b" -> "2") 35 | delete() 36 | assertEquals(0, select("SELECT * FROM domaina").getItems.size) 37 | } 38 | 39 | @Test 40 | def testDeleteConditionalValueSuccess(): Unit = { 41 | add(domaina, "itema", "a" -> "1", "b" -> "2") 42 | delete(hasValue("a", "1"), "a") 43 | assertHas("b = 2") 44 | } 45 | 46 | @Test 47 | def testDeleteConditionalDoesNotExistSuccess(): Unit = { 48 | add(domaina, "itema", "a" -> "1", "b" -> "2") 49 | delete(doesNotExist("c"), "a") 50 | assertHas("b = 2") 51 | } 52 | 53 | @Test 54 | def testDeleteConditionalValueFailure(): Unit = { 55 | add(domaina, "itema", "a" -> "1", "b" -> "2") 56 | assertFails("ConditionalCheckFailed", "ConditionalCheckFailedException: Attribute (a) value is (List(1)) but was expected (2)", { 57 | delete(hasValue("a", "2"), "a") 58 | }) 59 | assertHas("a = 1", "b = 2") 60 | } 61 | 62 | @Test 63 | def testDeleteConditionalDoesNotExistFailure(): Unit = { 64 | add(domaina, "itema", "a" -> "1", "b" -> "2") 65 | assertFails("ConditionalCheckFailed", "ConditionalCheckFailedException: Attribute (b) value exists", { 66 | delete(doesNotExist("b"), "a") 67 | }) 68 | assertHas("a = 1", "b = 2") 69 | } 70 | 71 | private def assertHas(attrs: String*): Unit = { 72 | assertItems(domaina, "itema", attrs: _*) 73 | } 74 | 75 | private def delete(attrNames: String*): Unit = { 76 | sdb.deleteAttributes(new DeleteAttributesRequest() 77 | .withDomainName(domaina) 78 | .withItemName("itema") 79 | .withAttributes(attrNames.map { new AwsAttribute(_, null) })) 80 | } 81 | 82 | private def delete(cond: UpdateCondition, attrNames: String*): Unit = { 83 | sdb.deleteAttributes(new DeleteAttributesRequest() 84 | .withDomainName(domaina) 85 | .withItemName("itema") 86 | .withAttributes(attrNames.map { new AwsAttribute(_, null) }) 87 | .withExpected(cond)) 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /src/test/scala/fakesdb/DomainMetadataTest.scala: -------------------------------------------------------------------------------- 1 | package fakesdb 2 | 3 | import org.junit._ 4 | import org.junit.Assert._ 5 | import com.amazonaws.services.simpledb.model._ 6 | 7 | class DomainMetadataTest extends AbstractFakeSdbTest { 8 | 9 | @Before 10 | def createDomain(): Unit = { 11 | createDomain(domaina) 12 | } 13 | 14 | @Test 15 | def testFoo(): Unit = { 16 | add(domaina, "itema", "aa" -> "111", "bb" -> "222", "bb" -> "333") 17 | 18 | val result = sdb.domainMetadata(new DomainMetadataRequest(domaina)) 19 | assertEquals(1, result.getItemCount) 20 | assertEquals(5l, result.getItemNamesSizeBytes) 21 | assertEquals(2, result.getAttributeNameCount) 22 | assertEquals(4l, result.getAttributeNamesSizeBytes) 23 | assertEquals(3, result.getAttributeValueCount) 24 | assertEquals(9l, result.getAttributeValuesSizeBytes) 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/test/scala/fakesdb/ErrorTest.scala: -------------------------------------------------------------------------------- 1 | package fakesdb 2 | 3 | import org.junit._ 4 | import org.junit.Assert._ 5 | 6 | class ErrorTest extends AbstractFakeSdbTest { 7 | @Test 8 | def testEmptyQuery(): Unit = { 9 | assertFails("InternalError", "RuntimeException: Invalid from domaina", { 10 | select("SELECT * FROM domaina") 11 | }) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/test/scala/fakesdb/FlushDomainsTest.scala: -------------------------------------------------------------------------------- 1 | package fakesdb 2 | 3 | import org.junit._ 4 | import org.junit.Assert._ 5 | 6 | class FlushDomainsTest extends AbstractFakeSdbTest { 7 | 8 | @Test 9 | def testFoo(): Unit = { 10 | // Start with a flush 11 | createDomain("_flush") 12 | 13 | // Now two real ones 14 | createDomain("one") 15 | createDomain("two") 16 | 17 | // See that we've got 2 18 | val domains = sdb.listDomains.getDomainNames 19 | assertEquals(2, domains.size) 20 | assertEquals("one", domains.get(0)) 21 | assertEquals("two", domains.get(1)) 22 | 23 | // Re-flush 24 | createDomain("_flush") 25 | 26 | // Now 0 27 | assertEquals(0, sdb.listDomains.getDomainNames.size) 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/test/scala/fakesdb/GetAttributesTest.scala: -------------------------------------------------------------------------------- 1 | package fakesdb 2 | 3 | import org.junit._ 4 | import org.junit.Assert._ 5 | import com.amazonaws.services.simpledb.model._ 6 | 7 | class GetAttributesTest extends AbstractFakeSdbTest { 8 | 9 | @Before 10 | def createDomain(): Unit = { 11 | createDomain(domaina) 12 | } 13 | 14 | @Test 15 | def testGetMultipleValues(): Unit = { 16 | add(domaina, "itema", "a" -> "1", "a" -> "2", "b" -> "3") 17 | val attrs = sdb.getAttributes(new GetAttributesRequest(domaina, "itema")).getAttributes 18 | assertEquals(3, attrs.size) 19 | assertEquals("a", attrs.get(0).getName) 20 | assertEquals("1", attrs.get(0).getValue) 21 | assertEquals("a", attrs.get(1).getName) 22 | assertEquals("2", attrs.get(1).getValue) 23 | assertEquals("b", attrs.get(2).getName) 24 | assertEquals("3", attrs.get(2).getValue) 25 | } 26 | 27 | @Test 28 | def testGetOneAttribute(): Unit = { 29 | add(domaina, "itema", 30 | "a" -> "1", 31 | "b" -> "2") 32 | 33 | val attrs = sdb.getAttributes(new GetAttributesRequest(domaina, "itema").withAttributeNames("a")).getAttributes 34 | assertEquals(1, attrs.size) 35 | assertEquals("a", attrs.get(0).getName) 36 | assertEquals("1", attrs.get(0).getValue) 37 | } 38 | 39 | @Test 40 | def testGetAttributesDoesNotCreateAnItem(): Unit = { 41 | val attrs = sdb.getAttributes(new GetAttributesRequest(domaina, "itema")).getAttributes 42 | assertEquals(0, attrs.size) 43 | assertEquals(0, select("SELECT * FROM domaina").getItems.size) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/test/scala/fakesdb/ListDomainsTest.scala: -------------------------------------------------------------------------------- 1 | package fakesdb 2 | 3 | import org.junit._ 4 | import org.junit.Assert._ 5 | 6 | class ListDomainsTest extends AbstractFakeSdbTest { 7 | 8 | @Test 9 | def testFoo(): Unit = { 10 | createDomain("domain1") 11 | createDomain("domain2") 12 | 13 | val domains = sdb.listDomains.getDomainNames 14 | assertEquals(2, domains.size) 15 | assertEquals("domain1", domains.get(0)) 16 | assertEquals("domain2", domains.get(1)) 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/test/scala/fakesdb/PutAttributesTest.scala: -------------------------------------------------------------------------------- 1 | package fakesdb 2 | 3 | import org.junit._ 4 | import org.junit.Assert._ 5 | import scala.collection.mutable._ 6 | import scala.collection.JavaConversions._ 7 | import com.amazonaws.services.simpledb.model._ 8 | 9 | class PutAttributesTest extends AbstractFakeSdbTest { 10 | 11 | private val nameOf1025 = "a" * 1025 12 | val itema = "itema" 13 | 14 | @Before 15 | def createDomain(): Unit = { 16 | createDomain(domaina) 17 | } 18 | 19 | @Test 20 | def testPutOne(): Unit = { 21 | add(domaina, itema, "a" -> "1") 22 | assertItems(domaina, itema, "a = 1") 23 | } 24 | 25 | @Test 26 | def testPutMultipleValues(): Unit = { 27 | add(domaina, itema, "a" -> "1", "a" -> "2") 28 | assertItems(domaina, itema, "a = 1", "a = 2") 29 | } 30 | 31 | @Test 32 | def testPutMultipleValuesWithSameValue(): Unit = { 33 | add(domaina, itema, "a" -> "1", "a" -> "1") 34 | assertItems(domaina, itema, "a = 1") 35 | } 36 | 37 | @Test 38 | def testLimitInTwoRequests(): Unit = { 39 | add(domaina, "itema", "a" -> "1", "b" -> "1") 40 | assertFails("NumberItemAttributesExceeded", "NumberItemAttributesExceededException: Too many attributes in this item", { 41 | addLots("itema", 255) 42 | }) 43 | val attrs = sdb.getAttributes(new GetAttributesRequest(domaina, itema)).getAttributes 44 | assertEquals(true, attrs.find { _.getName == "attr1" }.isDefined) 45 | assertEquals(true, attrs.find { _.getName == "attr254" }.isDefined) 46 | assertEquals(false, attrs.find { _.getName == "attr255" }.isDefined) 47 | } 48 | 49 | @Test 50 | def testLimitInTwoRequestsWithOverlappingAttributesIsOkay(): Unit = { 51 | add(domaina, "itema", "attr256" -> "value256") // value255 matches our new value 52 | addLots("itema", 256) 53 | val attrs = sdb.getAttributes(new GetAttributesRequest(domaina, itema)).getAttributes 54 | assertEquals(256, attrs.size) 55 | assertEquals(true, attrs.find { _.getName == "attr1" }.isDefined) 56 | assertEquals(true, attrs.find { _.getName == "attr256" }.isDefined) 57 | } 58 | 59 | @Test 60 | def testLimitInTwoRequestsWithNonOverlappingAttributeValuesFails(): Unit = { 61 | add(domaina, "itema", "attr256" -> "valueFoo") // valueFoo does not match our new value 62 | assertFails("NumberItemAttributesExceeded", "NumberItemAttributesExceededException: Too many attributes in this item", { 63 | addLots("itema", 256) 64 | }) 65 | val attrs = sdb.getAttributes(new GetAttributesRequest(domaina, itema)).getAttributes 66 | assertEquals(256, attrs.size) 67 | assertEquals(true, attrs.find { (a) => a.getName == "attr1" }.isDefined) 68 | assertEquals(true, attrs.find { (a) => a.getName == "attr256" && a.getValue == "value256" }.isEmpty) 69 | } 70 | 71 | @Test 72 | def testLimitInOneRequest(): Unit = { 73 | assertFails("NumberItemAttributesExceeded", "NumberItemAttributesExceededException: Too many attributes in this item", { 74 | addLots("itema", 257) 75 | }) 76 | val attrs = sdb.getAttributes(new GetAttributesRequest(domaina, itema)).getAttributes 77 | assertEquals(true, attrs.find { _.getName == "attr257" }.isEmpty) 78 | } 79 | 80 | @Test 81 | def testFailEmptyAttributeName(): Unit = { 82 | assertFails("InvalidParameterValue", "EmptyAttributeNameException: Value () for parameter Name is invalid. The empty string is an illegal attribute name", { 83 | add(domaina, "itema", "" -> "1") 84 | }) 85 | } 86 | 87 | @Test 88 | def testConditionalPutSucceeds(): Unit = { 89 | add(domaina, "itema", "a" -> "1") 90 | add(domaina, "itema", hasValue("a", "1"), "b" -> "1") 91 | val attrs = sdb.getAttributes(new GetAttributesRequest(domaina, itema)).getAttributes 92 | assertEquals(2, attrs.size) 93 | assertEquals("a", attrs.get(0).getName) 94 | assertEquals("1", attrs.get(0).getValue) 95 | assertEquals("b", attrs.get(1).getName) 96 | assertEquals("1", attrs.get(1).getValue) 97 | } 98 | 99 | @Test 100 | def testConditionalPutFailsWithWrongValue(): Unit = { 101 | add(domaina, "itema", "a" -> "1") 102 | assertFails("ConditionalCheckFailed", "ConditionalCheckFailedException: Attribute (a) value is (List(1)) but was expected (2)", { 103 | add(domaina, "itema", hasValue("a", "2"), "b" -> "1") 104 | }) 105 | } 106 | 107 | @Test 108 | def testConditionalPutDoesNotExist(): Unit = { 109 | add(domaina, "itema", "a" -> "1") 110 | assertFails("ConditionalCheckFailed", "ConditionalCheckFailedException: Attribute (a) value exists", { 111 | add(domaina, "itema", doesNotExist("a"), "b" -> "1") 112 | }) 113 | } 114 | 115 | @Test 116 | def testConditionalPutFailsAgainstMutipleValues(): Unit = { 117 | add(domaina, "itema", "a" -> "1", "a" -> "2") 118 | assertFails("ConditionalCheckFailed", "ConditionalCheckFailedException: Attribute (a) value is (List(1, 2)) but was expected (1)", { 119 | add(domaina, "itema", hasValue("a", "1"), "b" -> "1") 120 | }) 121 | } 122 | 123 | @Test 124 | def testConditionalPutFailsAgainstInvalidAttribute(): Unit = { 125 | add(domaina, "itema", "a" -> "1") 126 | assertFails("AttributeDoesNotExist", "AttributeDoesNotExistException: Attribute (c) does not exist", { 127 | add(domaina, "itema", hasValue("c", "1"), "b" -> "1") 128 | }) 129 | } 130 | 131 | @Test 132 | def testTooLongItemName(): Unit = { 133 | assertFails("InvalidParameterValue", "InvalidParameterValue: Value (\"%s\") for parameter Name is invalid. Value exceeds maximum length of 1024.".format(nameOf1025), { 134 | add(domaina, nameOf1025, "a" -> "1") 135 | }) 136 | } 137 | 138 | @Test 139 | def testTooLongAttributeName(): Unit = { 140 | assertFails("InvalidParameterValue", "InvalidParameterValue: Value (\"%s\") for parameter Name is invalid. Value exceeds maximum length of 1024.".format(nameOf1025), { 141 | add(domaina, "i", nameOf1025 -> "1") 142 | }) 143 | } 144 | 145 | @Test 146 | def testTooLongValue(): Unit = { 147 | assertFails("InvalidParameterValue", "InvalidParameterValue: Value (\"%s\") for parameter Value is invalid. Value exceeds maximum length of 1024.".format(nameOf1025), { 148 | add(domaina, "i", "a" -> nameOf1025) 149 | }) 150 | } 151 | 152 | private def addLots(itemName: String, number: Int): Unit = { 153 | val req = new PutAttributesRequest().withDomainName(domaina).withItemName(itemName) 154 | for (i <- 1.to(number)) { 155 | req.withAttributes(new ReplaceableAttribute("attr" + i, "value" + i, false)) 156 | } 157 | sdb.putAttributes(req) 158 | } 159 | 160 | } 161 | -------------------------------------------------------------------------------- /src/test/scala/fakesdb/SelectParserTest.scala: -------------------------------------------------------------------------------- 1 | package fakesdb 2 | 3 | import org.junit._ 4 | import org.junit.Assert._ 5 | 6 | class SelectParserTest { 7 | 8 | val data = new Data 9 | var domaina: Domain = null 10 | 11 | @Before 12 | def setUp(): Unit = { 13 | data.flush 14 | domaina = data.getOrCreateDomain("domaina") 15 | } 16 | 17 | @Test 18 | def testEmptyFrom(): Unit = { 19 | val se = SelectParser.makeSelectEval("select * from domaina") 20 | assertEquals(0, se.select(data)._1.size) 21 | } 22 | 23 | @Test 24 | def testFromWithData(): Unit = { 25 | domaina.getOrCreateItem("itema").put("a", "1", true) 26 | domaina.getOrCreateItem("itema").put("b", "2", true) 27 | domaina.getOrCreateItem("itemb").put("c", "3", true) 28 | val results = SelectParser.makeSelectEval("select * from domaina").select(data)._1 29 | assertEquals(2, results.size) 30 | assertEquals(("itema", List(("a", "1"), ("b", "2"))), results(0)) 31 | assertEquals(("itemb", List(("c", "3"))), results(1)) 32 | } 33 | 34 | @Test 35 | def testFromWithOneAttribute(): Unit = { 36 | domaina.getOrCreateItem("itema").put("a", "1", true) 37 | domaina.getOrCreateItem("itema").put("b", "2", true) 38 | domaina.getOrCreateItem("itemb").put("a", "3", true) 39 | // a is in both 40 | val results = SelectParser.makeSelectEval("select a from domaina").select(data)._1 41 | assertEquals(2, results.size) 42 | assertEquals(("itema", List(("a", "1"))), results(0)) 43 | assertEquals(("itemb", List(("a", "3"))), results(1)) 44 | // b in in only one 45 | val results2 = SelectParser.makeSelectEval("select b from domaina").select(data)._1 46 | assertEquals(1, results2.size) 47 | assertEquals(("itema", List(("b", "2"))), results2(0)) 48 | } 49 | 50 | @Test 51 | def testFromWithOneAttributeWithMultipleValues(): Unit = { 52 | domaina.getOrCreateItem("itema").put("a", "1", true) 53 | domaina.getOrCreateItem("itema").put("a", "1", false) 54 | val results = SelectParser.makeSelectEval("select a from domaina").select(data)._1 55 | assertEquals(1, results.size) 56 | assertEquals(("itema", List(("a", "1"))), results(0)) 57 | } 58 | 59 | @Test 60 | def testFromWithTwoAttributes(): Unit = { 61 | domaina.getOrCreateItem("itema").put("a", "1", true) 62 | domaina.getOrCreateItem("itema").put("b", "2", true) 63 | domaina.getOrCreateItem("itemb").put("a", "3", true) 64 | val results = SelectParser.makeSelectEval("select a, b from domaina").select(data)._1 65 | assertEquals(2, results.size) 66 | assertEquals(("itema", List(("a", "1"), ("b", "2"))), results(0)) 67 | assertEquals(("itemb", List(("a", "3"))), results(1)) 68 | } 69 | 70 | @Test 71 | def testFromCount(): Unit = { 72 | domaina.getOrCreateItem("itema").put("a", "1", true) 73 | val results = SelectParser.makeSelectEval("select count(*) from domaina").select(data)._1 74 | assertEquals(1, results.size) 75 | assertEquals(("Domain", List(("Count", "1"))), results(0)) 76 | } 77 | 78 | @Test 79 | def testFromCountUpperCase(): Unit = { 80 | domaina.getOrCreateItem("itema").put("a", "1", true) 81 | val results = SelectParser.makeSelectEval("SELECT COUNT(*) FROM domaina").select(data)._1 82 | assertEquals(1, results.size) 83 | assertEquals(("Domain", List(("Count", "1"))), results(0)) 84 | } 85 | 86 | @Test 87 | def testFromItemName(): Unit = { 88 | domaina.getOrCreateItem("itema").put("a", "1", true) 89 | val results = SelectParser.makeSelectEval("select itemName(), a from domaina").select(data)._1 90 | assertEquals(1, results.size) 91 | assertEquals(("itema", List(("itemName()", "itema"), ("a", "1"))), results(0)) 92 | } 93 | 94 | @Test 95 | def testWhereEquals(): Unit = { 96 | domaina.getOrCreateItem("itema").put("a", "1", true) 97 | domaina.getOrCreateItem("itemb").put("a", "2", true) 98 | val results = SelectParser.makeSelectEval("select * from domaina where a = '1'").select(data)._1 99 | assertEquals(1, results.size) 100 | assertEquals(("itema", List(("a", "1"))), results(0)) 101 | } 102 | 103 | @Test 104 | def testWhereDoubleTicks(): Unit = { 105 | domaina.getOrCreateItem("itema").put("a", "1'1", true) 106 | domaina.getOrCreateItem("itemb").put("a", "2'2", true) 107 | val results = SelectParser.makeSelectEval("select * from domaina where a = '1''1'").select(data)._1 108 | assertEquals(1, results.size) 109 | assertEquals(("itema", List(("a", "1'1"))), results(0)) 110 | } 111 | 112 | @Test 113 | def testWhereDoubleQuotes(): Unit = { 114 | domaina.getOrCreateItem("itema").put("a", "1\"1", true) 115 | domaina.getOrCreateItem("itemb").put("a", "2\"2", true) 116 | val results = SelectParser.makeSelectEval("select * from domaina where a = \"1\"\"1\"").select(data)._1 117 | assertEquals(1, results.size) 118 | assertEquals(("itema", List(("a", "1\"1"))), results(0)) 119 | } 120 | 121 | @Test 122 | def testWhereNotEquals(): Unit = { 123 | domaina.getOrCreateItem("itema").put("a", "1", true) 124 | domaina.getOrCreateItem("itemb").put("a", "2", true) 125 | val results = SelectParser.makeSelectEval("select * from domaina where a != '1'").select(data)._1 126 | assertEquals(1, results.size) 127 | assertEquals(("itemb", List(("a", "2"))), results(0)) 128 | } 129 | 130 | @Test 131 | def testWhereGreaterThan(): Unit = { 132 | domaina.getOrCreateItem("itema").put("a", "1", true) 133 | domaina.getOrCreateItem("itemb").put("a", "2", true) 134 | val results = SelectParser.makeSelectEval("select * from domaina where a > '1'").select(data)._1 135 | assertEquals(1, results.size) 136 | assertEquals(("itemb", List(("a", "2"))), results(0)) 137 | } 138 | 139 | @Test 140 | def testWhereLessThan(): Unit = { 141 | domaina.getOrCreateItem("itema").put("a", "1", true) 142 | domaina.getOrCreateItem("itemb").put("a", "2", true) 143 | val results = SelectParser.makeSelectEval("select * from domaina where a < '2'").select(data)._1 144 | assertEquals(1, results.size) 145 | assertEquals(("itema", List(("a", "1"))), results(0)) 146 | } 147 | 148 | @Test 149 | def testWhereGreaterThanOrEqual(): Unit = { 150 | domaina.getOrCreateItem("itema").put("a", "1", true) 151 | domaina.getOrCreateItem("itemb").put("a", "2", true) 152 | domaina.getOrCreateItem("itemc").put("a", "3", true) 153 | val results = SelectParser.makeSelectEval("select * from domaina where a >= '2'").select(data)._1 154 | assertEquals(2, results.size) 155 | assertEquals(("itemb", List(("a", "2"))), results(0)) 156 | assertEquals(("itemc", List(("a", "3"))), results(1)) 157 | } 158 | 159 | @Test 160 | def testWhereLessThanOrEqual(): Unit = { 161 | domaina.getOrCreateItem("itema").put("a", "1", true) 162 | domaina.getOrCreateItem("itemb").put("a", "2", true) 163 | domaina.getOrCreateItem("itemc").put("a", "3", true) 164 | val results = SelectParser.makeSelectEval("select * from domaina where a <= '2'").select(data)._1 165 | assertEquals(2, results.size) 166 | assertEquals(("itema", List(("a", "1"))), results(0)) 167 | assertEquals(("itemb", List(("a", "2"))), results(1)) 168 | } 169 | 170 | @Test 171 | def testWhereLike(): Unit = { 172 | domaina.getOrCreateItem("itema").put("a", "1", true) 173 | domaina.getOrCreateItem("itemb").put("a", "2", true) 174 | val results = SelectParser.makeSelectEval("select * from domaina where a like '1%'").select(data)._1 175 | assertEquals(1, results.size) 176 | assertEquals(("itema", List(("a", "1"))), results(0)) 177 | } 178 | 179 | @Test 180 | def testWhereNotLike(): Unit = { 181 | domaina.getOrCreateItem("itema").put("a", "1", true) 182 | domaina.getOrCreateItem("itemb").put("a", "2", true) 183 | val results = SelectParser.makeSelectEval("select * from domaina where a not like '1%'").select(data)._1 184 | assertEquals(1, results.size) 185 | assertEquals(("itemb", List(("a", "2"))), results(0)) 186 | } 187 | 188 | @Test 189 | def testWhereIsNull(): Unit = { 190 | domaina.getOrCreateItem("itema").put("a", "1", true) 191 | domaina.getOrCreateItem("itemb").put("b", "2", true) 192 | val results = SelectParser.makeSelectEval("select * from domaina where a is null").select(data)._1 193 | assertEquals(1, results.size) 194 | assertEquals(("itemb", List(("b", "2"))), results(0)) 195 | } 196 | 197 | @Test 198 | def testWhereIsNotNull(): Unit = { 199 | domaina.getOrCreateItem("itema").put("a", "1", true) 200 | domaina.getOrCreateItem("itemb").put("b", "2", true) 201 | val results = SelectParser.makeSelectEval("select * from domaina where a is not null").select(data)._1 202 | assertEquals(1, results.size) 203 | assertEquals(("itema", List(("a", "1"))), results(0)) 204 | } 205 | 206 | @Test 207 | def testWhereBetween(): Unit = { 208 | domaina.getOrCreateItem("itema").put("a", "1", true) 209 | domaina.getOrCreateItem("itemb").put("a", "2", true) 210 | val results = SelectParser.makeSelectEval("select * from domaina where a between '2' and '3'").select(data)._1 211 | assertEquals(1, results.size) 212 | assertEquals(("itemb", List(("a", "2"))), results(0)) 213 | } 214 | 215 | @Test 216 | def testWhereEvery(): Unit = { 217 | domaina.getOrCreateItem("itema").put("a", "1", true) 218 | domaina.getOrCreateItem("itemb").put("a", "1", true) 219 | domaina.getOrCreateItem("itemb").put("a", "2", true) 220 | val results = SelectParser.makeSelectEval("select * from domaina where every(a) = '1'").select(data)._1 221 | assertEquals(1, results.size) 222 | assertEquals(("itema", List(("a", "1"))), results(0)) 223 | } 224 | 225 | @Test 226 | def testWhereIn(): Unit = { 227 | domaina.getOrCreateItem("itema").put("a", "1", true) 228 | domaina.getOrCreateItem("itemb").put("a", "2", true) 229 | domaina.getOrCreateItem("itemc").put("a", "3", true) 230 | val results = SelectParser.makeSelectEval("select * from domaina where a in ('1', '2')").select(data)._1 231 | assertEquals(2, results.size) 232 | assertEquals(("itema", List(("a", "1"))), results(0)) 233 | assertEquals(("itemb", List(("a", "2"))), results(1)) 234 | } 235 | 236 | @Test 237 | def testWhereEqualsAnd(): Unit = { 238 | domaina.getOrCreateItem("itema").put("a", "1", true) 239 | domaina.getOrCreateItem("itema").put("b", "2", true) 240 | domaina.getOrCreateItem("itemb").put("a", "1", true) 241 | val results = SelectParser.makeSelectEval("select * from domaina where a = '1' and b = '2'").select(data)._1 242 | assertEquals(1, results.size) 243 | assertEquals(("itema", List(("a", "1"), ("b", "2"))), results(0)) 244 | } 245 | 246 | @Test 247 | def testWhereEqualsOr(): Unit = { 248 | domaina.getOrCreateItem("itema").put("a", "1", true) 249 | domaina.getOrCreateItem("itemb").put("a", "2", true) 250 | val results = SelectParser.makeSelectEval("select * from domaina where a = '1' or a = '2'").select(data)._1 251 | assertEquals(2, results.size) 252 | assertEquals(("itema", List(("a", "1"))), results(0)) 253 | assertEquals(("itemb", List(("a", "2"))), results(1)) 254 | } 255 | 256 | @Test 257 | def testWhereParens(): Unit = { 258 | domaina.getOrCreateItem("itema").put("a", "1", true) 259 | domaina.getOrCreateItem("itema").put("b", "1", true) 260 | domaina.getOrCreateItem("itemb").put("a", "2", true) 261 | val results = SelectParser.makeSelectEval("select * from domaina where (a = '1' and b = '1') or a = '2'").select(data)._1 262 | assertEquals(2, results.size) 263 | assertEquals(("itema", List(("a", "1"), ("b", "1"))), results(0)) 264 | assertEquals(("itemb", List(("a", "2"))), results(1)) 265 | } 266 | 267 | @Test 268 | def testOrderBy(): Unit = { 269 | domaina.getOrCreateItem("itema").put("a", "2", true) 270 | domaina.getOrCreateItem("itemb").put("a", "1", true) 271 | val results = SelectParser.makeSelectEval("select * from domaina where a >= '1' order by a").select(data)._1 272 | assertEquals(2, results.size) 273 | assertEquals(("itemb", List(("a", "1"))), results(0)) 274 | assertEquals(("itema", List(("a", "2"))), results(1)) 275 | } 276 | 277 | @Test 278 | def testOrderByDesc(): Unit = { 279 | domaina.getOrCreateItem("itema").put("a", "1", true) 280 | domaina.getOrCreateItem("itemb").put("a", "2", true) 281 | val results = SelectParser.makeSelectEval("select * from domaina where a >= '1' order by a desc").select(data)._1 282 | assertEquals(2, results.size) 283 | assertEquals(("itemb", List(("a", "2"))), results(0)) 284 | assertEquals(("itema", List(("a", "1"))), results(1)) 285 | } 286 | 287 | @Test 288 | def testOrderByItemNameDesc(): Unit = { 289 | domaina.getOrCreateItem("itema").put("a", "1", true) 290 | domaina.getOrCreateItem("itemb").put("a", "2", true) 291 | domaina.getOrCreateItem("itemc").put("a", "3", true) 292 | val results = SelectParser.makeSelectEval("select * from domaina where a >= '1' order by itemName() desc").select(data)._1 293 | assertEquals(3, results.size) 294 | assertEquals(("itemc", List(("a", "3"))), results(0)) 295 | assertEquals(("itemb", List(("a", "2"))), results(1)) 296 | assertEquals(("itema", List(("a", "1"))), results(2)) 297 | } 298 | 299 | @Test 300 | def testWhereCrazyAnd(): Unit = { 301 | domaina.getOrCreateItem("itema").put("a", "1", true) 302 | domaina.getOrCreateItem("itema").put("a", "2", false) 303 | val results = SelectParser.makeSelectEval("select * from domaina where a = '1' and a = '2'").select(data)._1 304 | assertEquals(1, results.size) // FAIL 305 | } 306 | 307 | @Test 308 | def testWhereIntersection(): Unit = { 309 | domaina.getOrCreateItem("itema").put("a", "1", true) 310 | domaina.getOrCreateItem("itema").put("a", "2", false) 311 | domaina.getOrCreateItem("itemb").put("a", "1", true) 312 | val results = SelectParser.makeSelectEval("select * from domaina where a = '1' intersection a = '2'").select(data)._1 313 | assertEquals(1, results.size) 314 | assertEquals(("itema", List(("a", "1"), ("a", "2"))), results(0)) 315 | } 316 | 317 | @Test 318 | def testKeyWithUnderscores(): Unit = { 319 | domaina.getOrCreateItem("itema").put("foo_bar", "1", true) 320 | val results = SelectParser.makeSelectEval("select * from domaina where foo_bar = '1'").select(data)._1 321 | assertEquals(1, results.size) 322 | assertEquals(("itema", List(("foo_bar", "1"))), results(0)) 323 | } 324 | 325 | @Test 326 | def testCaseInsensitiveKeywords(): Unit = { 327 | domaina.getOrCreateItem("itema").put("foo_bar", "1", true) 328 | val results = SelectParser.makeSelectEval("SELECT * FROM domaina WHERE foo_bar = '1'").select(data)._1 329 | assertEquals(1, results.size) 330 | assertEquals(("itema", List(("foo_bar", "1"))), results(0)) 331 | } 332 | 333 | @Test 334 | def testLarrysQuery(): Unit = { 335 | val usage = data.getOrCreateDomain("dev.api-web-usage") 336 | val a = usage.getOrCreateItem("itema") 337 | a.put("api_key", "1", true) 338 | a.put("dt", "2010010001", true) 339 | val results = SelectParser.makeSelectEval("select * from `dev.api-web-usage` where api_key = '1' and dt > '2010010000' and dt < '2010013224'").select(data)._1 340 | assertEquals(1, results.size) 341 | } 342 | 343 | @Test 344 | def testLimit(): Unit = { 345 | domaina.getOrCreateItem("itema").put("foo", "1", true) 346 | domaina.getOrCreateItem("itemb").put("foo", "2", true) 347 | domaina.getOrCreateItem("itemc").put("foo", "3", true) 348 | val results = SelectParser.makeSelectEval("SELECT * FROM domaina WHERE foo > '0' limit 2").select(data)._1 349 | assertEquals(2, results.size) 350 | assertEquals(("itema", List(("foo", "1"))), results(0)) 351 | assertEquals(("itemb", List(("foo", "2"))), results(1)) 352 | // Now with order by 353 | val results2 = SelectParser.makeSelectEval("SELECT * FROM domaina WHERE foo > '0' order by foo desc limit 2").select(data)._1 354 | assertEquals(2, results2.size) 355 | assertEquals(("itemc", List(("foo", "3"))), results2(0)) 356 | assertEquals(("itemb", List(("foo", "2"))), results2(1)) 357 | } 358 | 359 | @Test 360 | def testAttributeDoubleBacktick(): Unit = { 361 | domaina.getOrCreateItem("itema").put("a`a", "1", true) 362 | val results = SelectParser.makeSelectEval("select `a``a` from domaina where `a``a` = '1'").select(data)._1 363 | assertEquals(1, results.size) 364 | assertEquals(("itema", List(("a`a", "1"))), results(0)) 365 | } 366 | 367 | @Test 368 | def testAttributeLegalChars(): Unit = { 369 | domaina.getOrCreateItem("itema").put("a1$_", "1", true) 370 | val results = SelectParser.makeSelectEval("select a1$_ from domaina where a1$_ = '1'").select(data)._1 371 | assertEquals(1, results.size) 372 | assertEquals(("itema", List(("a1$_", "1"))), results(0)) 373 | } 374 | 375 | @Test 376 | def testAttributeLegalCharsInTheFirstPosition(): Unit = { 377 | domaina.getOrCreateItem("itema").put("$_", "1", true) 378 | val results = SelectParser.makeSelectEval("select $_ from domaina where $_ = '1'").select(data)._1 379 | assertEquals(1, results.size) 380 | assertEquals(("itema", List(("$_", "1"))), results(0)) 381 | } 382 | } 383 | -------------------------------------------------------------------------------- /src/test/scala/fakesdb/SelectTest.scala: -------------------------------------------------------------------------------- 1 | package fakesdb 2 | 3 | import org.junit._ 4 | import org.junit.Assert._ 5 | import scala.collection.JavaConversions._ 6 | import com.amazonaws.services.simpledb.model._ 7 | 8 | class SelectTest extends AbstractFakeSdbTest { 9 | 10 | @Before 11 | def createDomain() { 12 | createDomain(domaina) 13 | } 14 | 15 | @Test 16 | def testCount() { 17 | val results = select("select count(*) from domaina") 18 | assertEquals(1, results.getItems.size) 19 | val item = results.getItems.get(0) 20 | assertEquals("Domain", item.getName) 21 | assertEquals("Count", item.getAttributes.get(0).getName) 22 | assertEquals("0", item.getAttributes.get(0).getValue) 23 | assertEquals(null, results.getNextToken) 24 | } 25 | 26 | @Test 27 | def testNextTokenIsNotReturnedIfLimitIsMet() { 28 | for (i <- 1.to(10)) { 29 | add(domaina, i.toString(), "a" -> i.toString()) 30 | } 31 | val results = select("select count(*) from domaina limit 10") 32 | assertEquals(null, results.getNextToken) 33 | } 34 | 35 | @Test 36 | def testPartialSelect() { 37 | for (i <- 1.to(10)) { 38 | add(domaina, i.toString(), "a" -> i.toString()) 39 | } 40 | // http://stackoverflow.com/questions/1795245/how-to-do-paging-with-simpledb/1832779#1832779 41 | // first query to get a dummy next token 42 | var results = select("select count(*) from domaina limit 5") 43 | // second query to start after that 44 | results = select("select a from domaina limit 5", results.getNextToken) 45 | assertEquals(5, results.getItems.size) 46 | var i = 6 47 | results.getItems foreach { item => 48 | item.getAttributes foreach { attr => 49 | assertEquals(i.toString(), attr.getValue); 50 | i += 1 51 | } 52 | } 53 | } 54 | } 55 | --------------------------------------------------------------------------------