Implement the Suggestions Database (#846)

This commit is contained in:
Dmitry Bushev 2020-06-23 11:26:05 +03:00 committed by GitHub
parent 07265e6164
commit 9ba1aa6d34
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 1249 additions and 0 deletions

View File

@ -570,6 +570,17 @@ lazy val `core-definition` = (project in file("lib/core-definition"))
.dependsOn(graph) .dependsOn(graph)
.dependsOn(syntax.jvm) .dependsOn(syntax.jvm)
lazy val searcher = project
.in(file("lib/searcher"))
.configs(Test)
.settings(
libraryDependencies ++= Seq(
"com.typesafe.slick" %% "slick" % "3.3.2",
"org.xerial" % "sqlite-jdbc" % "3.31.1",
"org.scalatest" %% "scalatest" % scalatestVersion % Test,
)
)
// ============================================================================ // ============================================================================
// === Sub-Projects =========================================================== // === Sub-Projects ===========================================================
// ============================================================================ // ============================================================================
@ -746,6 +757,7 @@ lazy val runtime = (project in file("engine/runtime"))
.dependsOn(graph) .dependsOn(graph)
.dependsOn(`polyglot-api`) .dependsOn(`polyglot-api`)
.dependsOn(`text-buffer`) .dependsOn(`text-buffer`)
.dependsOn(`searcher`)
/* Note [Unmanaged Classpath] /* Note [Unmanaged Classpath]
* ~~~~~~~~~~~~~~~~~~~~~~~~~~ * ~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -0,0 +1,330 @@
package org.enso.compiler.context
import org.enso.compiler.core.IR
import org.enso.compiler.pass.resolve.{DocumentationComments, TypeSignatures}
import org.enso.searcher.Suggestion
import org.enso.syntax.text.Location
import scala.collection.immutable.VectorBuilder
import scala.collection.mutable
/** Module that extracts [[Suggestion]] entries from the [[IR]]. */
final class SuggestionBuilder {
import SuggestionBuilder._
/** Build suggestions from the given `ir`.
*
* @param ir the input `IR`
* @return the list of suggestion entries extracted from the given `IR`
*/
def build(ir: IR.Module): Vector[Suggestion] = {
@scala.annotation.tailrec
def go(
scope: Scope,
scopes: mutable.Queue[Scope],
acc: mutable.Builder[Suggestion, Vector[Suggestion]]
): Vector[Suggestion] =
if (scope.queue.isEmpty) {
if (scopes.isEmpty) {
acc.result()
} else {
val scope = scopes.dequeue()
go(scope, scopes, acc)
}
} else {
val ir = scope.queue.dequeue()
val doc = ir.getMetadata(DocumentationComments).map(_.documentation)
ir match {
case IR.Module.Scope.Definition.Method
.Explicit(
IR.Name.MethodReference(typePtr, methodName, _, _, _),
IR.Function.Lambda(args, body, _, _, _, _),
_,
_,
_
) =>
val typeSignature = ir.getMetadata(TypeSignatures)
acc += buildMethod(methodName, typePtr, args, doc, typeSignature)
scopes += Scope(body.children, body.location.map(_.location))
go(scope, scopes, acc)
case IR.Expression.Binding(
name,
IR.Function.Lambda(args, body, _, _, _, _),
_,
_,
_
) if name.location.isDefined =>
val typeSignature = ir.getMetadata(TypeSignatures)
acc += buildFunction(name, args, scope.location.get, typeSignature)
scopes += Scope(body.children, body.location.map(_.location))
go(scope, scopes, acc)
case IR.Expression.Binding(name, expr, _, _, _)
if name.location.isDefined =>
val typeSignature = ir.getMetadata(TypeSignatures)
acc += buildLocal(name.name, scope.location.get, typeSignature)
scopes += Scope(expr.children, expr.location.map(_.location))
go(scope, scopes, acc)
case IR.Module.Scope.Definition.Atom(name, arguments, _, _, _) =>
acc += buildAtom(name.name, arguments, doc)
go(scope, scopes, acc)
case _ =>
go(scope, scopes, acc)
}
}
go(
Scope(ir.children, ir.location.map(_.location)),
mutable.Queue(),
new VectorBuilder()
)
}
private def buildMethod(
name: IR.Name,
typeRef: Seq[IR.Name],
args: Seq[IR.DefinitionArgument],
doc: Option[String],
typeSignature: Option[TypeSignatures.Metadata]
): Suggestion.Method = {
typeSignature match {
case Some(TypeSignatures.Signature(typeExpr)) =>
val selfType = buildSelfType(typeRef)
val typeSig = buildTypeSignature(typeExpr)
val (methodArgs, returnTypeDef) =
buildMethodArguments(args, typeSig, selfType)
Suggestion.Method(
name = name.name,
arguments = methodArgs,
selfType = selfType,
returnType = buildReturnType(returnTypeDef),
documentation = doc
)
case _ =>
Suggestion.Method(
name = name.name,
arguments = args.map(buildArgument),
selfType = buildSelfType(typeRef),
returnType = Any,
documentation = doc
)
}
}
private def buildFunction(
name: IR.Name,
args: Seq[IR.DefinitionArgument],
location: Location,
typeSignature: Option[TypeSignatures.Metadata]
): Suggestion.Function = {
typeSignature match {
case Some(TypeSignatures.Signature(typeExpr)) =>
val typeSig = buildTypeSignature(typeExpr)
val (methodArgs, returnTypeDef) =
buildFunctionArguments(args, typeSig)
Suggestion.Function(
name = name.name,
arguments = methodArgs,
returnType = buildReturnType(returnTypeDef),
scope = buildScope(location)
)
case _ =>
Suggestion.Function(
name = name.name,
arguments = args.map(buildArgument),
returnType = Any,
scope = buildScope(location)
)
}
}
private def buildLocal(
name: String,
location: Location,
typeSignature: Option[TypeSignatures.Metadata]
): Suggestion.Local =
typeSignature match {
case Some(TypeSignatures.Signature(tname: IR.Name)) =>
Suggestion.Local(name, tname.name, buildScope(location))
case _ =>
Suggestion.Local(name, Any, buildScope(location))
}
private def buildAtom(
name: String,
arguments: Seq[IR.DefinitionArgument],
doc: Option[String]
): Suggestion.Atom =
Suggestion.Atom(
name = name,
arguments = arguments.map(buildArgument),
returnType = name,
documentation = doc
)
private def buildTypeSignature(typeExpr: IR.Expression): Vector[TypeArg] = {
@scala.annotation.tailrec
def go(typeExpr: IR.Expression, args: Vector[TypeArg]): Vector[TypeArg] =
typeExpr match {
case IR.Function.Lambda(List(targ), body, _, _, _, _) =>
val tdef = TypeArg(targ.name.name, targ.suspended)
go(body, args :+ tdef)
case tname: IR.Name =>
args :+ TypeArg(tname.name, isSuspended = false)
case _ =>
args
}
go(typeExpr, Vector())
}
private def buildMethodArguments(
vargs: Seq[IR.DefinitionArgument],
targs: Seq[TypeArg],
selfType: String
): (Seq[Suggestion.Argument], Option[TypeArg]) = {
@scala.annotation.tailrec
def go(
vargs: Seq[IR.DefinitionArgument],
targs: Seq[TypeArg],
acc: Vector[Suggestion.Argument]
): (Vector[Suggestion.Argument], Option[TypeArg]) =
if (vargs.isEmpty) {
(acc, targs.lastOption)
} else {
vargs match {
case IR.DefinitionArgument.Specified(
name: IR.Name.This,
defaultValue,
suspended,
_,
_,
_
) +: vtail =>
val thisArg = Suggestion.Argument(
name = name.name,
reprType = selfType,
isSuspended = suspended,
hasDefault = defaultValue.isDefined,
defaultValue = defaultValue.flatMap(buildDefaultValue)
)
go(vtail, targs, acc :+ thisArg)
case varg +: vtail =>
targs match {
case targ +: ttail =>
go(vtail, ttail, acc :+ buildTypedArgument(varg, targ))
case _ =>
go(vtail, targs, acc :+ buildArgument(varg))
}
}
}
go(vargs, targs, Vector())
}
private def buildFunctionArguments(
vargs: Seq[IR.DefinitionArgument],
targs: Seq[TypeArg]
): (Seq[Suggestion.Argument], Option[TypeArg]) = {
@scala.annotation.tailrec
def go(
vargs: Seq[IR.DefinitionArgument],
targs: Seq[TypeArg],
acc: Vector[Suggestion.Argument]
): (Seq[Suggestion.Argument], Option[TypeArg]) =
if (vargs.isEmpty) {
(acc, targs.lastOption)
} else {
vargs match {
case varg +: vtail =>
targs match {
case targ +: ttail =>
go(vtail, ttail, acc :+ buildTypedArgument(varg, targ))
case _ =>
go(vtail, targs, acc :+ buildArgument(varg))
}
}
}
go(vargs, targs, Vector())
}
private def buildTypedArgument(
varg: IR.DefinitionArgument,
targ: TypeArg
): Suggestion.Argument =
Suggestion.Argument(
name = varg.name.name,
reprType = targ.name,
isSuspended = targ.isSuspended,
hasDefault = varg.defaultValue.isDefined,
defaultValue = varg.defaultValue.flatMap(buildDefaultValue)
)
private def buildArgument(arg: IR.DefinitionArgument): Suggestion.Argument =
Suggestion.Argument(
name = arg.name.name,
reprType = Any,
isSuspended = arg.suspended,
hasDefault = arg.defaultValue.isDefined,
defaultValue = arg.defaultValue.flatMap(buildDefaultValue)
)
def buildArgument(
varg: IR.DefinitionArgument,
targ: Option[TypeArg]
): Suggestion.Argument =
Suggestion.Argument(
name = varg.name.name,
reprType = targ.fold(Any)(_.name),
isSuspended = targ.fold(varg.suspended)(_.isSuspended),
hasDefault = varg.defaultValue.isDefined,
defaultValue = varg.defaultValue.flatMap(buildDefaultValue)
)
private def buildReturnType(typeDef: Option[TypeArg]): String =
typeDef match {
case Some(TypeArg(name, _)) => name
case None => Any
}
private def buildSelfType(ref: Seq[IR.Name]): String =
ref.map(_.name).mkString(".")
private def buildDefaultValue(expr: IR): Option[String] =
expr match {
case IR.Literal.Number(value, _, _, _) => Some(value)
case IR.Literal.Text(text, _, _, _) => Some(text)
case _ => None
}
private def buildScope(location: Location): Suggestion.Scope =
Suggestion.Scope(location.start, location.end)
}
object SuggestionBuilder {
/** A single level of an `IR`.
*
* @param queue the nodes in the scope
* @param location the scope location
*/
private case class Scope(queue: mutable.Queue[IR], location: Option[Location])
private object Scope {
/** Create new scope from the list of items. */
def apply(items: Seq[IR], location: Option[Location]): Scope =
new Scope(mutable.Queue(items: _*), location)
}
/** Type of the argument.
*
* @param name the name of the type
* @param isSuspended is the argument lazy
*/
private case class TypeArg(name: String, isSuspended: Boolean)
private val Any: String = "Any"
}

View File

@ -0,0 +1,399 @@
package org.enso.compiler.test.context
import org.enso.compiler.Passes
import org.enso.compiler.context.{
FreshNameSupply,
ModuleContext,
SuggestionBuilder
}
import org.enso.compiler.core.IR
import org.enso.compiler.pass.PassManager
import org.enso.compiler.test.CompilerTest
import org.enso.searcher.Suggestion
class SuggestionBuilderTest extends CompilerTest {
implicit val passManager: PassManager = new Passes().passManager
"SuggestionBuilder" should {
"build method without explicit arguments" in {
implicit val moduleContext: ModuleContext = freshModuleContext
val code = """foo = 42""".stripMargin
val module = code.preprocessModule
build(module) should contain theSameElementsAs Seq(
Suggestion.Method(
name = "foo",
arguments = Seq(
Suggestion.Argument("this", "Any", false, false, None)
),
selfType = "here",
returnType = "Any",
documentation = None
)
)
}
"build method with documentation" in {
pending // fix documentation
implicit val moduleContext: ModuleContext = freshModuleContext
val code =
"""## The foo
|foo = 42""".stripMargin
val module = code.preprocessModule
build(module) should contain theSameElementsAs Seq(
Suggestion.Method(
name = "foo",
arguments = Seq(
Suggestion.Argument("this", "Any", false, false, None)
),
selfType = "here",
returnType = "Any",
documentation = Some(" The foo")
)
)
}
"build method with arguments" in {
implicit val moduleContext: ModuleContext = freshModuleContext
val code =
"""foo a b =
| x : Number
| x = a + 1
| y = b - 2
| x * y""".stripMargin
val module = code.preprocessModule
build(module) should contain theSameElementsAs Seq(
Suggestion.Method(
name = "foo",
arguments = Seq(
Suggestion.Argument("this", "Any", false, false, None),
Suggestion.Argument("a", "Any", false, false, None),
Suggestion.Argument("b", "Any", false, false, None)
),
selfType = "here",
returnType = "Any",
documentation = None
),
Suggestion.Local("x", "Number", Suggestion.Scope(9, 62)),
Suggestion.Local("y", "Any", Suggestion.Scope(9, 62))
)
}
"build method with default arguments" in {
implicit val moduleContext: ModuleContext = freshModuleContext
val code =
"""foo (a = 0) = a + 1""".stripMargin
val module = code.preprocessModule
build(module) should contain theSameElementsAs Seq(
Suggestion.Method(
name = "foo",
arguments = Seq(
Suggestion.Argument("this", "Any", false, false, None),
Suggestion.Argument("a", "Any", false, true, Some("0"))
),
selfType = "here",
returnType = "Any",
documentation = None
)
)
}
"build method with associated type signature" in {
implicit val moduleContext: ModuleContext = freshModuleContext
val code =
"""
|MyAtom.bar : Number -> Number -> Number
|MyAtom.bar a b = a + b
|""".stripMargin
val module = code.preprocessModule
build(module) should contain theSameElementsAs Seq(
Suggestion.Method(
name = "bar",
arguments = Seq(
Suggestion.Argument("this", "MyAtom", false, false, None),
Suggestion.Argument("a", "Number", false, false, None),
Suggestion.Argument("b", "Number", false, false, None)
),
selfType = "MyAtom",
returnType = "Number",
documentation = None
)
)
}
"build method with lazy arguments" in {
implicit val moduleContext: ModuleContext = freshModuleContext
val code =
"""foo ~a = a + 1""".stripMargin
val module = code.preprocessModule
build(module) should contain theSameElementsAs Seq(
Suggestion.Method(
name = "foo",
arguments = Seq(
Suggestion.Argument("this", "Any", false, false, None),
Suggestion.Argument("a", "Any", true, false, None)
),
selfType = "here",
returnType = "Any",
documentation = None
)
)
}
"build function" in {
implicit val moduleContext: ModuleContext = freshModuleContext
val code =
"""main =
| foo a = a + 1
| foo 42""".stripMargin
val module = code.preprocessModule
build(module) should contain theSameElementsAs Seq(
Suggestion.Method(
name = "main",
arguments = Seq(
Suggestion.Argument("this", "Any", false, false, None)
),
selfType = "here",
returnType = "Any",
documentation = None
),
Suggestion.Function(
name = "foo",
arguments = Seq(
Suggestion.Argument("a", "Any", false, false, None)
),
returnType = "Any",
scope = Suggestion.Scope(6, 35)
)
)
}
"build function with associated type signature" in {
implicit val moduleContext: ModuleContext = freshModuleContext
val code =
"""main =
| foo : Number -> Number
| foo a = a + 1
| foo 42""".stripMargin
val module = code.preprocessModule
build(module) should contain theSameElementsAs Seq(
Suggestion.Method(
name = "main",
arguments = Seq(
Suggestion.Argument("this", "Any", false, false, None)
),
selfType = "here",
returnType = "Any",
documentation = None
),
Suggestion.Function(
name = "foo",
arguments = Seq(
Suggestion.Argument("a", "Number", false, false, None)
),
returnType = "Number",
scope = Suggestion.Scope(6, 62)
)
)
}
"build atom simple" in {
implicit val moduleContext: ModuleContext = freshModuleContext
val code = """type MyType a b"""
val module = code.preprocessModule
build(module) should contain theSameElementsAs Seq(
Suggestion.Atom(
name = "MyType",
arguments = Seq(
Suggestion.Argument("a", "Any", false, false, None),
Suggestion.Argument("b", "Any", false, false, None)
),
returnType = "MyType",
documentation = None
)
)
}
"build atom with documentation" in {
implicit val moduleContext: ModuleContext = freshModuleContext
val code =
"""## My sweet type
|type MyType a b""".stripMargin
val module = code.preprocessModule
build(module) should contain theSameElementsAs Seq(
Suggestion.Atom(
name = "MyType",
arguments = Seq(
Suggestion.Argument("a", "Any", false, false, None),
Suggestion.Argument("b", "Any", false, false, None)
),
returnType = "MyType",
documentation = Some(" My sweet type")
)
)
}
"build type simple" in {
implicit val moduleContext: ModuleContext = freshModuleContext
val code =
"""type Maybe
| type Nothing
| type Just a""".stripMargin
val module = code.preprocessModule
build(module) should contain theSameElementsAs Seq(
Suggestion.Atom(
name = "Nothing",
arguments = Seq(),
returnType = "Nothing",
documentation = None
),
Suggestion.Atom(
name = "Just",
arguments = Seq(
Suggestion.Argument("a", "Any", false, false, None)
),
returnType = "Just",
documentation = None
)
)
}
"build type with documentation" in {
implicit val moduleContext: ModuleContext = freshModuleContext
val code =
"""## When in doubt
|type Maybe
| ## Nothing here
| type Nothing
| ## Something there
| type Just a""".stripMargin
val module = code.preprocessModule
build(module) should contain theSameElementsAs Seq(
Suggestion.Atom(
name = "Nothing",
arguments = Seq(),
returnType = "Nothing",
documentation = Some(" Nothing here")
),
Suggestion.Atom(
name = "Just",
arguments = Seq(
Suggestion.Argument("a", "Any", false, false, None)
),
returnType = "Just",
documentation = Some(" Something there")
)
)
}
"build type with methods" in {
implicit val moduleContext: ModuleContext = freshModuleContext
val code =
"""type Maybe
| type Nothing
| type Just a
|
| map f = case this of
| Just a -> Just (f a)
| Nothing -> Nothing""".stripMargin
val module = code.preprocessModule
build(module) should contain theSameElementsAs Seq(
Suggestion.Atom(
name = "Nothing",
arguments = Seq(),
returnType = "Nothing",
documentation = None
),
Suggestion.Atom(
name = "Just",
arguments = Seq(
Suggestion.Argument("a", "Any", false, false, None)
),
returnType = "Just",
documentation = None
),
Suggestion.Method(
name = "map",
arguments = Seq(
Suggestion.Argument("this", "Any", false, false, None),
Suggestion.Argument("f", "Any", false, false, None)
),
selfType = "Just",
returnType = "Any",
documentation = None
),
Suggestion.Method(
name = "map",
arguments = Seq(
Suggestion.Argument("this", "Any", false, false, None),
Suggestion.Argument("f", "Any", false, false, None)
),
selfType = "Nothing",
returnType = "Any",
documentation = None
)
)
}
"build type with methods with type signature" in {
implicit val moduleContext: ModuleContext = freshModuleContext
val code =
"""type MyType
| type MyAtom
|
| is_atom : this -> Boolean
| is_atom = true""".stripMargin
val module = code.preprocessModule
build(module) should contain theSameElementsAs Seq(
Suggestion.Atom(
name = "MyAtom",
arguments = Seq(),
returnType = "MyAtom",
documentation = None
),
Suggestion.Method(
name = "is_atom",
arguments = Seq(
Suggestion.Argument("this", "MyAtom", false, false, None)
),
selfType = "MyAtom",
returnType = "Boolean",
documentation = None
)
)
}
}
private def build(ir: IR.Module): Vector[Suggestion] =
new SuggestionBuilder().build(ir)
private def freshModuleContext: ModuleContext =
ModuleContext(freshNameSupply = Some(new FreshNameSupply))
}

View File

@ -0,0 +1,8 @@
searcher {
db {
url = "jdbc:sqlite:searcher.db"
driver = org.sqlite.JDBC
connectionPool = disabled
keepAliveConnection = true
}
}

View File

@ -0,0 +1,81 @@
package org.enso.searcher
/** A search suggestion. */
sealed trait Suggestion
object Suggestion {
/** An argument of an atom or a function.
*
* @param name the argument name
* @param reprType the type of the argument
* @param isSuspended is the argument lazy
* @param hasDefault does the argument have a default
* @param defaultValue optional default value
*/
case class Argument(
name: String,
reprType: String,
isSuspended: Boolean,
hasDefault: Boolean,
defaultValue: Option[String]
)
/** The definition scope.
* @param start the start of the definition scope
* @param end the end of the definition scope
*/
case class Scope(start: Int, end: Int)
/** A value constructor.
*
* @param name the atom name
* @param arguments the list of arguments
* @param returnType the type of an atom
* @param documentation the documentation string
*/
case class Atom(
name: String,
arguments: Seq[Argument],
returnType: String,
documentation: Option[String]
) extends Suggestion
/** A function defined on a type or a module.
*
* @param name the method name
* @param arguments the function arguments
* @param selfType the self type of a method
* @param returnType the return type of a method
* @param documentation the documentation string
*/
case class Method(
name: String,
arguments: Seq[Argument],
selfType: String,
returnType: String,
documentation: Option[String]
) extends Suggestion
/** A local function definition.
*
* @param name the function name
* @param arguments the function arguments
* @param returnType the return type of a function
* @param scope the scope where the function is defined
*/
case class Function(
name: String,
arguments: Seq[Argument],
returnType: String,
scope: Scope
) extends Suggestion
/** A local value.
*
* @param name the name of a value
* @param returnType the type of a local value
* @param scope the scope where the value is defined
*/
case class Local(name: String, returnType: String, scope: Scope)
extends Suggestion
}

View File

@ -0,0 +1,28 @@
package org.enso.searcher.sql
import org.enso.searcher.Suggestion
/** The object for accessing the suggestions database. */
trait SuggestionsRepo[F[_]] {
/** Find suggestions by the return type.
*
* @param returnType the return type of a suggestion
* @return the list of suggestions
*/
def findBy(returnType: String): F[Seq[Suggestion]]
/** Select the suggestion by id.
*
* @param id the id of a suggestion
* @return return the suggestion
*/
def select(id: Long): F[Option[Suggestion]]
/** Insert the suggestion
*
* @param suggestion the suggestion to insert
* @return the id of an inserted suggestion
*/
def insert(suggestion: Suggestion): F[Long]
}

View File

@ -0,0 +1,177 @@
package org.enso.searcher.sql
import org.enso.searcher.Suggestion
import slick.jdbc.SQLiteProfile.api._
import scala.concurrent.ExecutionContext
/** The object for accessing the suggestions database. */
final class SqlSuggestionsRepo(implicit ec: ExecutionContext)
extends SuggestionsRepo[DBIO] {
/** The query returning the arguments joined with the corresponding
* suggestions. */
private val joined: Query[
(Rep[Option[ArgumentsTable]], SuggestionsTable),
(Option[ArgumentRow], SuggestionRow),
Seq
] =
arguments
.joinRight(suggestions)
.on(_.suggestionId === _.id)
/** @inheritdoc **/
override def findBy(returnType: String): DBIO[Seq[Suggestion]] = {
val query = for {
(argument, suggestion) <- joined
if suggestion.returnType === returnType
} yield (argument, suggestion)
query.result.map(joinedToSuggestion)
}
/** @inheritdoc **/
override def select(id: Long): DBIO[Option[Suggestion]] = {
val query = for {
(argument, suggestion) <- joined
if suggestion.id === id
} yield (argument, suggestion)
query.result.map(coll => joinedToSuggestion(coll).headOption)
}
/** @inheritdoc **/
override def insert(suggestion: Suggestion): DBIO[Long] = {
val (suggestionRow, args) = toSuggestionRow(suggestion)
for {
id <- suggestions.returning(suggestions.map(_.id)) += suggestionRow
_ <- arguments ++= args.map(toArgumentRow(id, _))
} yield id
}
private def joinedToSuggestion(
coll: Seq[(Option[ArgumentRow], SuggestionRow)]
): Seq[Suggestion] = {
coll
.groupBy(_._2)
.view
.mapValues(_.flatMap(_._1))
.map(Function.tupled(toSuggestion))
.toSeq
}
private def toSuggestionRow(
suggestion: Suggestion
): (SuggestionRow, Seq[Suggestion.Argument]) =
suggestion match {
case Suggestion.Atom(name, args, returnType, doc) =>
val row = SuggestionRow(
id = None,
kind = SuggestionKind.ATOM,
name = name,
selfType = None,
returnType = returnType,
documentation = doc,
scopeStart = None,
scopeEnd = None
)
row -> args
case Suggestion.Method(name, args, selfType, returnType, doc) =>
val row = SuggestionRow(
id = None,
kind = SuggestionKind.METHOD,
name = name,
selfType = Some(selfType),
returnType = returnType,
documentation = doc,
scopeStart = None,
scopeEnd = None
)
row -> args
case Suggestion.Function(name, args, returnType, scope) =>
val row = SuggestionRow(
id = None,
kind = SuggestionKind.FUNCTION,
name = name,
selfType = None,
returnType = returnType,
documentation = None,
scopeStart = Some(scope.start),
scopeEnd = Some(scope.end)
)
row -> args
case Suggestion.Local(name, returnType, scope) =>
val row = SuggestionRow(
id = None,
kind = SuggestionKind.LOCAL,
name = name,
selfType = None,
returnType = returnType,
documentation = None,
scopeStart = Some(scope.start),
scopeEnd = Some(scope.end)
)
row -> Seq()
}
private def toArgumentRow(
suggestionId: Long,
argument: Suggestion.Argument
): ArgumentRow =
ArgumentRow(
id = None,
suggestionId = suggestionId,
name = argument.name,
tpe = argument.reprType,
isSuspended = argument.isSuspended,
hasDefault = argument.hasDefault,
defaultValue = argument.defaultValue
)
private def toSuggestion(
suggestion: SuggestionRow,
arguments: Seq[ArgumentRow]
): Suggestion =
suggestion.kind match {
case SuggestionKind.ATOM =>
Suggestion.Atom(
name = suggestion.name,
arguments = arguments.map(toArgument),
returnType = suggestion.returnType,
documentation = suggestion.documentation
)
case SuggestionKind.METHOD =>
Suggestion.Method(
name = suggestion.name,
arguments = arguments.map(toArgument),
selfType = suggestion.selfType.get,
returnType = suggestion.returnType,
documentation = suggestion.documentation
)
case SuggestionKind.FUNCTION =>
Suggestion.Function(
name = suggestion.name,
arguments = arguments.map(toArgument),
returnType = suggestion.returnType,
scope =
Suggestion.Scope(suggestion.scopeStart.get, suggestion.scopeEnd.get)
)
case SuggestionKind.LOCAL =>
Suggestion.Local(
name = suggestion.name,
returnType = suggestion.returnType,
scope =
Suggestion.Scope(suggestion.scopeStart.get, suggestion.scopeEnd.get)
)
case k =>
throw new NoSuchElementException(s"Unknown suggestion kind: $k")
}
private def toArgument(row: ArgumentRow): Suggestion.Argument =
Suggestion.Argument(
name = row.name,
reprType = row.tpe,
isSuspended = row.isSuspended,
hasDefault = row.hasDefault,
defaultValue = row.defaultValue
)
}

View File

@ -0,0 +1,110 @@
package org.enso.searcher.sql
import slick.jdbc.SQLiteProfile.api._
/** A row in the arguments table.
*
* @param id the id of an argument
* @param suggestionId the id of the suggestion
* @param name the argument name
* @param tpe the argument type
* @param isSuspended is the argument lazy
* @param hasDefault does the argument have the default value
* @param defaultValue optional default value
*/
case class ArgumentRow(
id: Option[Long],
suggestionId: Long,
name: String,
tpe: String,
isSuspended: Boolean,
hasDefault: Boolean,
defaultValue: Option[String]
)
/** A row in the suggestions table.
*
* @param id the id of a suggestion
* @param kind the type of a suggestion
* @param name the suggestion name
* @param selfType the self type of a suggestion
* @param returnType the return type of a suggestion
* @param documentation the documentation string
* @param scopeStart the start of the scope
* @param scopeEnd the end of the scope
*/
case class SuggestionRow(
id: Option[Long],
kind: Byte,
name: String,
selfType: Option[String],
returnType: String,
documentation: Option[String],
scopeStart: Option[Int],
scopeEnd: Option[Int]
)
/** The type of a suggestion. */
object SuggestionKind {
val ATOM: Byte = 0
val METHOD: Byte = 1
val FUNCTION: Byte = 2
val LOCAL: Byte = 3
}
/** The schema of the arguments table. */
final class ArgumentsTable(tag: Tag)
extends Table[ArgumentRow](tag, "arguments") {
def id = column[Long]("id", O.PrimaryKey, O.AutoInc)
def suggestionId = column[Long]("suggestion_id")
def name = column[String]("name")
def tpe = column[String]("type")
def isSuspended = column[Boolean]("is_suspended", O.Default(false))
def hasDefault = column[Boolean]("has_default", O.Default(false))
def defaultValue = column[Option[String]]("default_value")
def * =
(id.?, suggestionId, name, tpe, isSuspended, hasDefault, defaultValue) <>
(ArgumentRow.tupled, ArgumentRow.unapply)
def suggestion =
foreignKey("suggestion_fk", suggestionId, suggestions)(
_.id,
onUpdate = ForeignKeyAction.Restrict,
onDelete = ForeignKeyAction.Cascade
)
}
/** The schema of the suggestions table. */
final class SuggestionsTable(tag: Tag)
extends Table[SuggestionRow](tag, "suggestions") {
def id = column[Long]("id", O.PrimaryKey, O.AutoInc)
def kind = column[Byte]("kind")
def name = column[String]("name")
def selfType = column[Option[String]]("self_type")
def returnType = column[String]("return_type")
def documentation = column[Option[String]]("documentation")
def scopeStart = column[Option[Int]]("scope_start")
def scopeEnd = column[Option[Int]]("scope_end")
def * =
(
id.?,
kind,
name,
selfType,
returnType,
documentation,
scopeStart,
scopeEnd
) <>
(SuggestionRow.tupled, SuggestionRow.unapply)
def selfTypeIdx = index("self_type_idx", selfType)
def returnTypeIdx = index("return_type_idx", name)
}
object arguments extends TableQuery(new ArgumentsTable(_))
object suggestions extends TableQuery(new SuggestionsTable(_))

View File

@ -0,0 +1,3 @@
searcher.db {
url = "jdbc:sqlite:file::memory:?cache=shared"
}

View File

@ -0,0 +1,101 @@
package org.enso.searcher.sql
import org.enso.searcher.Suggestion
import org.scalatest.BeforeAndAfterAll
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec
import slick.jdbc.SQLiteProfile.api._
import scala.concurrent.Await
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._
class SuggestionsRepoTest
extends AnyWordSpec
with Matchers
with BeforeAndAfterAll {
val Timeout: FiniteDuration = 3.seconds
val db = Database.forConfig("searcher.db")
val repo = new SqlSuggestionsRepo()
override def beforeAll(): Unit = {
Await.ready(
db.run((suggestions.schema ++ arguments.schema).createIfNotExists),
Timeout
)
}
override def afterAll(): Unit = {
db.close()
}
"SuggestionsDBIO" should {
"select suggestion by id" in {
val action =
for {
id <- db.run(repo.insert(suggestion.atom))
res <- db.run(repo.select(id))
} yield res
Await.result(action, Timeout) shouldEqual Some(suggestion.atom)
}
"find suggestion by returnType" in {
val action =
for {
_ <- db.run(repo.insert(suggestion.local))
_ <- db.run(repo.insert(suggestion.method))
_ <- db.run(repo.insert(suggestion.function))
res <- db.run(repo.findBy("MyType"))
} yield res
Await.result(action, Timeout) should contain theSameElementsAs Seq(
suggestion.local,
suggestion.function
)
}
}
object suggestion {
val atom: Suggestion.Atom =
Suggestion.Atom(
name = "Pair",
arguments = Seq(
Suggestion.Argument("a", "Any", false, false, None),
Suggestion.Argument("b", "Any", false, false, None)
),
returnType = "Pair",
documentation = Some("Awesome")
)
val method: Suggestion.Method =
Suggestion.Method(
name = "main",
arguments = Seq(),
selfType = "Main",
returnType = "IO",
documentation = None
)
val function: Suggestion.Function =
Suggestion.Function(
name = "bar",
arguments = Seq(
Suggestion.Argument("x", "Number", false, true, Some("0"))
),
returnType = "MyType",
scope = Suggestion.Scope(5, 9)
)
val local: Suggestion.Local =
Suggestion.Local(
name = "bazz",
returnType = "MyType",
scope = Suggestion.Scope(37, 84)
)
}
}