iOS and Android (Java) sdk framework (#643)

Documents the FFI layer for Mentat, and provides transaction functionality via an EDN string. Creates two native libraries for iOS (Swift) and Android (Java) and fully tests the FFI for both platforms.

Closes #619 #614 #611
This commit is contained in:
Emily Toop 2018-05-14 16:20:36 +01:00 committed by GitHub
parent 60cb5d2432
commit 013629dec6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
76 changed files with 6545 additions and 288 deletions

25
.gitignore vendored
View file

@ -54,3 +54,28 @@ pom.xml.asc
/fixtures/*.db-shm
/fixtures/*.db-wal
/query-parser/out/
## Build generated
/sdks/swift/Mentat/build/
DerivedData
build.xcarchive
## Various settings
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
/sdks/swift/Mentat/*.xcodeproj/project.xcworkspace/xcuserdata
## Other
*.xccheckout
*.moved-aside
*.xcuserstate
*.xcscmblueprint
## Obj-C/Swift specific
*.hmap
*.ipa

4
android_build_all.sh Executable file
View file

@ -0,0 +1,4 @@
# This will eventually become a complete build script, not just for Android
cargo build -p mentat_ffi --target i686-linux-android --release
cargo build -p mentat_ffi --target armv7-linux-androideabi --release
cargo build -p mentat_ffi --target aarch64-linux-android --release

View file

@ -19,7 +19,7 @@ use rustc_version::{
/// MIN_VERSION should be changed when there's a new minimum version of rustc required
/// to build the project.
static MIN_VERSION: &'static str = "1.24.0";
static MIN_VERSION: &'static str = "1.25.0";
fn main() {
let ver = version().unwrap();

View file

@ -755,6 +755,27 @@ impl Binding {
_ => None,
}
}
pub fn into_c_string(self) -> Option<*mut c_char> {
match self {
Binding::Scalar(v) => v.into_c_string(),
_ => None,
}
}
pub fn into_kw_c_string(self) -> Option<*mut c_char> {
match self {
Binding::Scalar(v) => v.into_kw_c_string(),
_ => None,
}
}
pub fn into_uuid_c_string(self) -> Option<*mut c_char> {
match self {
Binding::Scalar(v) => v.into_uuid_c_string(),
_ => None,
}
}
}
#[test]

View file

@ -1,10 +1,14 @@
[package]
name = "mentat_ffi"
version = "0.1.0"
version = "0.0.1"
authors = ["Emily Toop <etoop@mozilla.com>"]
[lib]
name = "mentat_ffi"
crate-type = ["lib", "staticlib", "cdylib"]
[dependencies]
libc = "0.2"
[dependencies.mentat]
path = ".."
path = "../"

File diff suppressed because it is too large Load diff

View file

@ -19,19 +19,18 @@ pub mod strings {
Keyword,
};
pub fn c_char_to_string(cchar: *const c_char) -> String {
pub fn c_char_to_string(cchar: *const c_char) -> &'static str {
let c_str = unsafe { CStr::from_ptr(cchar) };
let r_str = c_str.to_str().unwrap_or("");
r_str.to_string()
c_str.to_str().unwrap_or("")
}
pub fn string_to_c_char<T>(r_string: T) -> *mut c_char where T: Into<String> {
CString::new(r_string.into()).unwrap().into_raw()
}
pub fn kw_from_string(keyword_string: &'static str) -> Keyword {
// TODO: validate. The input might not be a keyword!
pub fn kw_from_string(mut keyword_string: String) -> Keyword {
let attr_name = keyword_string.split_off(1);
let attr_name = keyword_string.trim_left_matches(":");
let parts: Vec<&str> = attr_name.split("/").collect();
Keyword::namespaced(parts[0], parts[1])
}

9
sdks/android/Mentat/.gitignore vendored Normal file
View file

@ -0,0 +1,9 @@
*.iml
.gradle
/local.properties
/.idea/workspace.xml
/.idea/libraries
.DS_Store
/build
/captures
.externalNativeBuild

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>Mentat</name>
<comment>Project Mentat created by Buildship.</comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.buildship.core.gradleprojectbuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.buildship.core.gradleprojectnature</nature>
</natures>
</projectDescription>

View file

@ -0,0 +1,27 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = '1.1.60'
repositories {
jcenter()
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.1.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
jcenter()
google()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}

View file

@ -0,0 +1,17 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx1536m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true

View file

@ -0,0 +1,6 @@
#Thu Mar 29 09:07:24 BST 2018
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip

160
sdks/android/Mentat/gradlew vendored Executable file
View file

@ -0,0 +1,160 @@
#!/usr/bin/env bash
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn ( ) {
echo "$*"
}
die ( ) {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
esac
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
function splitJvmOpts() {
JVM_OPTS=("$@")
}
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"

90
sdks/android/Mentat/gradlew.bat vendored Normal file
View file

@ -0,0 +1,90 @@
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windowz variants
if not "%OS%" == "Windows_NT" goto win9xME_args
if "%@eval[2+2]" == "4" goto 4NT_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
goto execute
:4NT_args
@rem Get arguments from the 4NT Shell from JP Software
set CMD_LINE_ARGS=%$
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8/"/>
<classpathentry kind="con" path="org.eclipse.buildship.core.gradleclasspathcontainer"/>
<classpathentry kind="output" path="bin/default"/>
</classpath>

View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>app</name>
<comment>Project app created by Buildship.</comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.buildship.core.gradleprojectbuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.buildship.core.gradleprojectnature</nature>
</natures>
</projectDescription>

View file

@ -0,0 +1,42 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
android {
compileSdkVersion 26
defaultConfig {
minSdkVersion 26
targetSdkVersion 26
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
sourceSets {
androidTest.assets.srcDirs += '../../../../fixtures'
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
androidTestImplementation('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
androidTestImplementation 'com.android.support:support-annotations:24.0.0'
androidTestImplementation 'com.android.support.test:runner:0.5'
androidTestImplementation 'com.android.support.test:rules:0.5'
implementation 'com.android.support:appcompat-v7:26.1.0'
implementation 'com.android.support.constraint:constraint-layout:1.0.2'
implementation 'com.android.support:design:26.1.0'
testImplementation 'junit:junit:4.12'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
implementation 'net.java.dev.jna:jna:4.5.1'
}
repositories {
mavenCentral()
}

View file

@ -0,0 +1,25 @@
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in ~/.android-sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View file

@ -0,0 +1,27 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* Copyright 2018 Mozilla
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of the
* License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License. */
package com.mozilla.mentat;
import java.util.EventListener;
interface ExpectationEventListener extends EventListener {
public void fulfill();
}
public class Expectation implements ExpectationEventListener {
public boolean isFulfilled = false;
public void fulfill() {
this.isFulfilled = true;
synchronized (this) {
notifyAll( );
}
}
}

View file

@ -0,0 +1,725 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* Copyright 2018 Mozilla
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of the
* License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License. */
package com.mozilla.mentat;
import android.content.Context;
import android.content.res.AssetManager;
import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.ByteBuffer;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.UUID;
import static org.junit.Assert.*;
/**
* Instrumentation test, which will execute on an Android device.
*/
@RunWith(AndroidJUnit4.class)
public class FFIIntegrationTest {
Mentat mentat = null;
@Test
public void openInMemoryStoreSucceeds() throws Exception {
Mentat mentat = new Mentat();
assertNotNull(mentat);
}
@Test
public void openStoreInLocationSucceeds() throws Exception {
Context context = InstrumentationRegistry.getTargetContext();
String path = context.getDatabasePath("test.db").getAbsolutePath();
Mentat mentat = new Mentat(path);
assertNotNull(mentat);
}
public String readFile(String fileName) {
Context testContext = InstrumentationRegistry.getInstrumentation().getContext();
AssetManager assetManager = testContext.getAssets();
try {
InputStream inputStream = assetManager.open(fileName);
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
StringBuilder out = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
out.append(line + "\n");
}
return out.toString();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
public TxReport transactCitiesSchema(Mentat mentat) {
String citiesSchema = this.readFile("cities.schema");
return mentat.transact(citiesSchema);
}
public TxReport transactSeattleData(Mentat mentat) {
String seattleData = this.readFile("all_seattle.edn");
return mentat.transact(seattleData);
}
public Mentat openAndInitializeCitiesStore() {
if (this.mentat == null) {
this.mentat = new Mentat();
this.transactCitiesSchema(mentat);
this.transactSeattleData(mentat);
}
return this.mentat;
}
public TxReport populateWithTypesSchema(Mentat mentat) {
String schema = "[\n" +
" [:db/add \"b\" :db/ident :foo/boolean]\n" +
" [:db/add \"b\" :db/valueType :db.type/boolean]\n" +
" [:db/add \"b\" :db/cardinality :db.cardinality/one]\n" +
" [:db/add \"l\" :db/ident :foo/long]\n" +
" [:db/add \"l\" :db/valueType :db.type/long]\n" +
" [:db/add \"l\" :db/cardinality :db.cardinality/one]\n" +
" [:db/add \"r\" :db/ident :foo/ref]\n" +
" [:db/add \"r\" :db/valueType :db.type/ref]\n" +
" [:db/add \"r\" :db/cardinality :db.cardinality/one]\n" +
" [:db/add \"i\" :db/ident :foo/instant]\n" +
" [:db/add \"i\" :db/valueType :db.type/instant]\n" +
" [:db/add \"i\" :db/cardinality :db.cardinality/one]\n" +
" [:db/add \"d\" :db/ident :foo/double]\n" +
" [:db/add \"d\" :db/valueType :db.type/double]\n" +
" [:db/add \"d\" :db/cardinality :db.cardinality/one]\n" +
" [:db/add \"s\" :db/ident :foo/string]\n" +
" [:db/add \"s\" :db/valueType :db.type/string]\n" +
" [:db/add \"s\" :db/cardinality :db.cardinality/one]\n" +
" [:db/add \"k\" :db/ident :foo/keyword]\n" +
" [:db/add \"k\" :db/valueType :db.type/keyword]\n" +
" [:db/add \"k\" :db/cardinality :db.cardinality/one]\n" +
" [:db/add \"u\" :db/ident :foo/uuid]\n" +
" [:db/add \"u\" :db/valueType :db.type/uuid]\n" +
" [:db/add \"u\" :db/cardinality :db.cardinality/one]\n" +
" ]";
TxReport report = mentat.transact(schema);
Long stringEntid = report.getEntidForTempId("s");
String data = "[\n" +
" [:db/add \"a\" :foo/boolean true]\n" +
" [:db/add \"a\" :foo/long 25]\n" +
" [:db/add \"a\" :foo/instant #inst \"2017-01-01T11:00:00.000Z\"]\n" +
" [:db/add \"a\" :foo/double 11.23]\n" +
" [:db/add \"a\" :foo/string \"The higher we soar the smaller we appear to those who cannot fly.\"]\n" +
" [:db/add \"a\" :foo/keyword :foo/string]\n" +
" [:db/add \"a\" :foo/uuid #uuid \"550e8400-e29b-41d4-a716-446655440000\"]\n" +
" [:db/add \"b\" :foo/boolean false]\n" +
" [:db/add \"b\" :foo/ref "+ stringEntid +"]\n" +
" [:db/add \"b\" :foo/long 50]\n" +
" [:db/add \"b\" :foo/instant #inst \"2018-01-01T11:00:00.000Z\"]\n" +
" [:db/add \"b\" :foo/double 22.46]\n" +
" [:db/add \"b\" :foo/string \"Silence is worse; all truths that are kept silent become poisonous.\"]\n" +
" [:db/add \"b\" :foo/uuid #uuid \"4cb3f828-752d-497a-90c9-b1fd516d5644\"]\n" +
" ]";
return mentat.transact(data);
}
@Test
public void transactingVocabularySucceeds() {
Mentat mentat = new Mentat();
TxReport schemaReport = this.transactCitiesSchema(mentat);
assertNotNull(schemaReport);
assertTrue(schemaReport.getTxId() > 0);
}
@Test
public void transactingEntitiesSucceeds() {
Mentat mentat = new Mentat();
this.transactCitiesSchema(mentat);
TxReport dataReport = this.transactSeattleData(mentat);
assertNotNull(dataReport);
assertTrue(dataReport.getTxId() > 0);
Long entid = dataReport.getEntidForTempId("a17592186045605");
assertEquals(65733, entid.longValue());
}
@Test
public void runScalarSucceeds() throws InterruptedException {
Mentat mentat = openAndInitializeCitiesStore();
String query = "[:find ?n . :in ?name :where [(fulltext $ :community/name ?name) [[?e ?n]]]]";
final Expectation expectation = new Expectation();
mentat.query(query).bindString("?name", "Wallingford").runScalar(new ScalarResultHandler() {
@Override
public void handleValue(TypedValue value) {
assertNotNull(value);
assertEquals("KOMO Communities - Wallingford", value.asString());
expectation.fulfill();
}
});
synchronized (expectation) {
expectation.wait(1000);
}
assertTrue(expectation.isFulfilled);
}
@Test
public void runCollSucceeds() throws InterruptedException {
Mentat mentat = openAndInitializeCitiesStore();
String query = "[:find [?when ...] :where [_ :db/txInstant ?when] :order (asc ?when)]";
final Expectation expectation = new Expectation();
mentat.query(query).runColl(new CollResultHandler() {
@Override
public void handleList(CollResult list) {
assertNotNull(list);
for (int i = 0; i < 3; ++i) {
assertNotNull(list.asDate(i));
}
expectation.fulfill();
}
});
synchronized (expectation) {
expectation.wait(1000);
}
assertTrue(expectation.isFulfilled);
}
@Test
public void runCollResultIteratorSucceeds() throws InterruptedException {
Mentat mentat = openAndInitializeCitiesStore();
String query = "[:find [?when ...] :where [_ :db/txInstant ?when] :order (asc ?when)]";
final Expectation expectation = new Expectation();
mentat.query(query).runColl(new CollResultHandler() {
@Override
public void handleList(CollResult list) {
assertNotNull(list);
for(TypedValue value: list) {
assertNotNull(value.asDate());
}
expectation.fulfill();
}
});
synchronized (expectation) {
expectation.wait(1000);
}
assertTrue(expectation.isFulfilled);
}
@Test
public void runTupleSucceeds() throws InterruptedException {
Mentat mentat = openAndInitializeCitiesStore();
String query = "[:find [?name ?cat]\n" +
" :where\n" +
" [?c :community/name ?name]\n" +
" [?c :community/type :community.type/website]\n" +
" [(fulltext $ :community/category \"food\") [[?c ?cat]]]]";
final Expectation expectation = new Expectation();
mentat.query(query).runTuple(new TupleResultHandler() {
@Override
public void handleRow(TupleResult row) {
assertNotNull(row);
String name = row.asString(0);
String category = row.asString(1);
assert(name == "Community Harvest of Southwest Seattle");
assert(category == "sustainable food");
expectation.fulfill();
}
});
synchronized (expectation) {
expectation.wait(1000);
}
assertTrue(expectation.isFulfilled);
}
@Test
public void runRelIteratorSucceeds() throws InterruptedException {
Mentat mentat = openAndInitializeCitiesStore();
String query = "[:find ?name ?cat\n" +
" :where\n" +
" [?c :community/name ?name]\n" +
" [?c :community/type :community.type/website]\n" +
" [(fulltext $ :community/category \"food\") [[?c ?cat]]]]";
final LinkedHashMap expectedResults = new LinkedHashMap<String, String>();
expectedResults.put("InBallard", "food");
expectedResults.put("Seattle Chinatown Guide", "food");
expectedResults.put("Community Harvest of Southwest Seattle", "sustainable food");
expectedResults.put("University District Food Bank", "food bank");
final Expectation expectation = new Expectation();
mentat.query(query).run(new RelResultHandler() {
@Override
public void handleRows(RelResult rows) {
assertNotNull(rows);
int index = 0;
for (TupleResult row: rows) {
String name = row.asString(0);
assertNotNull(name);
String category = row.asString(1);
assertNotNull(category);
String expectedCategory = expectedResults.get(name).toString();
assertNotNull(expectedCategory);
assertEquals(expectedCategory, category);
++index;
}
assertEquals(expectedResults.size(), index);
expectation.fulfill();
}
});
synchronized (expectation) {
expectation.wait(1000);
}
assertTrue(expectation.isFulfilled);
}
@Test
public void runRelSucceeds() throws InterruptedException {
Mentat mentat = openAndInitializeCitiesStore();
String query = "[:find ?name ?cat\n" +
" :where\n" +
" [?c :community/name ?name]\n" +
" [?c :community/type :community.type/website]\n" +
" [(fulltext $ :community/category \"food\") [[?c ?cat]]]]";
final LinkedHashMap expectedResults = new LinkedHashMap<String, String>();
expectedResults.put("InBallard", "food");
expectedResults.put("Seattle Chinatown Guide", "food");
expectedResults.put("Community Harvest of Southwest Seattle", "sustainable food");
expectedResults.put("University District Food Bank", "food bank");
final Expectation expectation = new Expectation();
mentat.query(query).run(new RelResultHandler() {
@Override
public void handleRows(RelResult rows) {
assertNotNull(rows);
for (int i = 0; i < expectedResults.size(); ++i) {
TupleResult row = rows.rowAtIndex(i);
assertNotNull(row);
String name = row.asString(0);
assertNotNull(name);
String category = row.asString(1);
assertNotNull(category);
String expectedCategory = expectedResults.get(name).toString();
assertNotNull(expectedCategory);
assertEquals(expectedCategory, category);
}
expectation.fulfill();
}
});
synchronized (expectation) {
expectation.wait(1000);
}
assertTrue(expectation.isFulfilled);
}
@Test
public void bindingLongValueSucceeds() throws InterruptedException {
Mentat mentat = new Mentat();
TxReport report = this.populateWithTypesSchema(mentat);
final Long aEntid = report.getEntidForTempId("a");
String query = "[:find ?e . :in ?long :where [?e :foo/long ?long]]";
final Expectation expectation = new Expectation();
mentat.query(query).bindLong("?long", 25).runScalar(new ScalarResultHandler() {
@Override
public void handleValue(TypedValue value) {
assertNotNull(value);
assertEquals(aEntid, value.asEntid());
expectation.fulfill();
}
});
synchronized (expectation) {
expectation.wait(1000);
}
assertTrue(expectation.isFulfilled);
}
@Test
public void bindingRefValueSucceeds() throws InterruptedException {
Mentat mentat = new Mentat();
TxReport report = this.populateWithTypesSchema(mentat);
long stringEntid = mentat.entIdForAttribute(":foo/string");
final Long bEntid = report.getEntidForTempId("b");
String query = "[:find ?e . :in ?ref :where [?e :foo/ref ?ref]]";
final Expectation expectation = new Expectation();
mentat.query(query).bindEntidReference("?ref", stringEntid).runScalar(new ScalarResultHandler() {
@Override
public void handleValue(TypedValue value) {
assertNotNull(value);
assertEquals(bEntid, value.asEntid());
expectation.fulfill();
}
});
synchronized (expectation) {
expectation.wait(1000);
}
assertTrue(expectation.isFulfilled);
}
@Test
public void bindingRefKwValueSucceeds() throws InterruptedException {
Mentat mentat = new Mentat();
TxReport report = this.populateWithTypesSchema(mentat);
String refKeyword = ":foo/string";
final Long bEntid = report.getEntidForTempId("b");
String query = "[:find ?e . :in ?ref :where [?e :foo/ref ?ref]]";
final Expectation expectation = new Expectation();
mentat.query(query).bindKeywordReference("?ref", refKeyword).runScalar(new ScalarResultHandler() {
@Override
public void handleValue(TypedValue value) {
assertNotNull(value);
assertEquals(bEntid, value.asEntid());
expectation.fulfill();
}
});
synchronized (expectation) {
expectation.wait(1000);
}
assertTrue(expectation.isFulfilled);
}
@Test
public void bindingKwValueSucceeds() throws InterruptedException {
Mentat mentat = new Mentat();
TxReport report = this.populateWithTypesSchema(mentat);
final Long aEntid = report.getEntidForTempId("a");
String query = "[:find ?e . :in ?kw :where [?e :foo/keyword ?kw]]";
final Expectation expectation = new Expectation();
mentat.query(query).bindKeyword("?kw", ":foo/string").runScalar(new ScalarResultHandler() {
@Override
public void handleValue(TypedValue value) {
assertNotNull(value);
assertEquals(aEntid, value.asEntid());
expectation.fulfill();
}
});
synchronized (expectation) {
expectation.wait(1000);
}
assertTrue(expectation.isFulfilled);
}
@Test
public void bindingDateValueSucceeds() throws InterruptedException, ParseException {
Mentat mentat = new Mentat();
TxReport report = this.populateWithTypesSchema(mentat);
final Long aEntid = report.getEntidForTempId("a");
Date date = new Date(1523896758000L);
String query = "[:find [?e ?d] :in ?now :where [?e :foo/instant ?d] [(< ?d ?now)]]";
final Expectation expectation = new Expectation();
mentat.query(query).bindDate("?now", date).runTuple(new TupleResultHandler() {
@Override
public void handleRow(TupleResult row) {
assertNotNull(row);
TypedValue value = row.get(0);
assertNotNull(value);
assertEquals(aEntid, value.asEntid());
expectation.fulfill();
}
});
synchronized (expectation) {
expectation.wait(1000);
}
assertTrue(expectation.isFulfilled);
}
@Test
public void bindingStringValueSucceeds() throws InterruptedException {
Mentat mentat = this.openAndInitializeCitiesStore();
String query = "[:find ?n . :in ?name :where [(fulltext $ :community/name ?name) [[?e ?n]]]]";
final Expectation expectation = new Expectation();
mentat.query(query).bindString("?name", "Wallingford").runScalar(new ScalarResultHandler() {
@Override
public void handleValue(TypedValue value) {
assertNotNull(value);
assertEquals("KOMO Communities - Wallingford", value.asString());
expectation.fulfill();
}
});
synchronized (expectation) {
expectation.wait(1000);
}
assertTrue(expectation.isFulfilled);
}
@Test
public void bindingUuidValueSucceeds() throws InterruptedException {
Mentat mentat = new Mentat();
TxReport report = this.populateWithTypesSchema(mentat);
final Long aEntid = report.getEntidForTempId("a");
String query = "[:find ?e . :in ?uuid :where [?e :foo/uuid ?uuid]]";
UUID uuid = UUID.fromString("550e8400-e29b-41d4-a716-446655440000");
final Expectation expectation = new Expectation();
mentat.query(query).bindUUID("?uuid", uuid).runScalar(new ScalarResultHandler() {
@Override
public void handleValue(TypedValue value) {
assertNotNull(value);
assertEquals(aEntid, value.asEntid());
expectation.fulfill();
}
});
synchronized (expectation) {
expectation.wait(1000);
}
assertTrue(expectation.isFulfilled);
}
@Test
public void bindingBooleanValueSucceeds() throws InterruptedException {
Mentat mentat = new Mentat();
TxReport report = this.populateWithTypesSchema(mentat);
final Long aEntid = report.getEntidForTempId("a");
String query = "[:find ?e . :in ?bool :where [?e :foo/boolean ?bool]]";
final Expectation expectation = new Expectation();
mentat.query(query).bindBoolean("?bool", true).runScalar(new ScalarResultHandler() {
@Override
public void handleValue(TypedValue value) {
assertNotNull(value);
assertEquals(aEntid, value.asEntid());
expectation.fulfill();
}
});
synchronized (expectation) {
expectation.wait(1000);
}
assertTrue(expectation.isFulfilled);
}
@Test
public void bindingDoubleValueSucceeds() throws InterruptedException {
Mentat mentat = new Mentat();
TxReport report = this.populateWithTypesSchema(mentat);
final Long aEntid = report.getEntidForTempId("a");
String query = "[:find ?e . :in ?double :where [?e :foo/double ?double]]";
final Expectation expectation = new Expectation();
mentat.query(query).bindDouble("?double", 11.23).runScalar(new ScalarResultHandler() {
@Override
public void handleValue(TypedValue value) {
assertNotNull(value);
assertEquals(aEntid, value.asEntid());
expectation.fulfill();
}
});
synchronized (expectation) {
expectation.wait(1000);
}
assertTrue(expectation.isFulfilled);
}
@Test
public void typedValueConvertsToLong() throws InterruptedException {
Mentat mentat = new Mentat();
TxReport report = this.populateWithTypesSchema(mentat);
final Long aEntid = report.getEntidForTempId("a");
String query = "[:find ?v . :in ?e :where [?e :foo/long ?v]]";
final Expectation expectation = new Expectation();
mentat.query(query).bindEntidReference("?e", aEntid).runScalar(new ScalarResultHandler() {
@Override
public void handleValue(TypedValue value) {
assertNotNull(value);
assertEquals(25, value.asLong().longValue());
assertEquals(25, value.asLong().longValue());
expectation.fulfill();
}
});
synchronized (expectation) {
expectation.wait(1000);
}
assertTrue(expectation.isFulfilled);
}
@Test
public void typedValueConvertsToRef() throws InterruptedException {
Mentat mentat = new Mentat();
TxReport report = this.populateWithTypesSchema(mentat);
final Long aEntid = report.getEntidForTempId("a");
String query = "[:find ?e . :where [?e :foo/long 25]]";
final Expectation expectation = new Expectation();
mentat.query(query).runScalar(new ScalarResultHandler() {
@Override
public void handleValue(TypedValue value) {
assertNotNull(value);
assertEquals(aEntid, value.asEntid());
assertEquals(aEntid, value.asEntid());
expectation.fulfill();
}
});
synchronized (expectation) {
expectation.wait(1000);
}
assertTrue(expectation.isFulfilled);
}
@Test
public void typedValueConvertsToKeyword() throws InterruptedException {
Mentat mentat = new Mentat();
TxReport report = this.populateWithTypesSchema(mentat);
final Long aEntid = report.getEntidForTempId("a");
String query = "[:find ?v . :in ?e :where [?e :foo/keyword ?v]]";
final Expectation expectation = new Expectation();
mentat.query(query).bindEntidReference("?e", aEntid).runScalar(new ScalarResultHandler() {
@Override
public void handleValue(TypedValue value) {
assertNotNull(value);
assertEquals(":foo/string", value.asKeyword());
assertEquals(":foo/string", value.asKeyword());
expectation.fulfill();
}
});
synchronized (expectation) {
expectation.wait(1000);
}
assertTrue(expectation.isFulfilled);
}
@Test
public void typedValueConvertsToBoolean() throws InterruptedException {
Mentat mentat = new Mentat();
TxReport report = this.populateWithTypesSchema(mentat);
final Long aEntid = report.getEntidForTempId("a");
String query = "[:find ?v . :in ?e :where [?e :foo/boolean ?v]]";
final Expectation expectation = new Expectation();
mentat.query(query).bindEntidReference("?e", aEntid).runScalar(new ScalarResultHandler() {
@Override
public void handleValue(TypedValue value) {
assertNotNull(value);
assertEquals(true, value.asBoolean());
assertEquals(true, value.asBoolean());
expectation.fulfill();
}
});
synchronized (expectation) {
expectation.wait(1000);
}
assertTrue(expectation.isFulfilled);
}
@Test
public void typedValueConvertsToDouble() throws InterruptedException {
Mentat mentat = new Mentat();
TxReport report = this.populateWithTypesSchema(mentat);
final Long aEntid = report.getEntidForTempId("a");
String query = "[:find ?v . :in ?e :where [?e :foo/double ?v]]";
final Expectation expectation = new Expectation();
mentat.query(query).bindEntidReference("?e", aEntid).runScalar(new ScalarResultHandler() {
@Override
public void handleValue(TypedValue value) {
assertNotNull(value);
assertEquals(new Double(11.23), value.asDouble());
assertEquals(new Double(11.23), value.asDouble());
expectation.fulfill();
}
});
synchronized (expectation) {
expectation.wait(1000);
}
assertTrue(expectation.isFulfilled);
}
@Test
public void typedValueConvertsToDate() throws InterruptedException, ParseException {
Mentat mentat = new Mentat();
TxReport report = this.populateWithTypesSchema(mentat);
final Long aEntid = report.getEntidForTempId("a");
String query = "[:find ?v . :in ?e :where [?e :foo/instant ?v]]";
final Expectation expectation = new Expectation();
DateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZZZZZ", Locale.ENGLISH);
format.parse("2017-01-01T11:00:00+00:00");
final Calendar expectedDate = format.getCalendar();
mentat.query(query).bindEntidReference("?e", aEntid).runScalar(new ScalarResultHandler() {
@Override
public void handleValue(TypedValue value) {
assertNotNull(value);
assertEquals(expectedDate.getTime(), value.asDate());
assertEquals(expectedDate.getTime(), value.asDate());
expectation.fulfill();
}
});
synchronized (expectation) {
expectation.wait(1000);
}
assertTrue(expectation.isFulfilled);
}
@Test
public void typedValueConvertsToString() throws InterruptedException {
Mentat mentat = new Mentat();
TxReport report = this.populateWithTypesSchema(mentat);
final Long aEntid = report.getEntidForTempId("a");
String query = "[:find ?v . :in ?e :where [?e :foo/string ?v]]";
final Expectation expectation = new Expectation();
mentat.query(query).bindEntidReference("?e", aEntid).runScalar(new ScalarResultHandler() {
@Override
public void handleValue(TypedValue value) {
assertNotNull(value);
assertEquals("The higher we soar the smaller we appear to those who cannot fly.", value.asString());
assertEquals("The higher we soar the smaller we appear to those who cannot fly.", value.asString());
expectation.fulfill();
}
});
synchronized (expectation) {
expectation.wait(1000);
}
assertTrue(expectation.isFulfilled);
}
@Test
public void typedValueConvertsToUUID() throws InterruptedException {
Mentat mentat = new Mentat();
TxReport report = this.populateWithTypesSchema(mentat);
final Long aEntid = report.getEntidForTempId("a");
String query = "[:find ?v . :in ?e :where [?e :foo/uuid ?v]]";
final UUID expectedUUID = UUID.fromString("550e8400-e29b-41d4-a716-446655440000");
final Expectation expectation = new Expectation();
mentat.query(query).bindEntidReference("?e", aEntid).runScalar(new ScalarResultHandler() {
@Override
public void handleValue(TypedValue value) {
assertNotNull(value);
assertEquals(expectedUUID, value.asUUID());
assertEquals(expectedUUID, value.asUUID());
expectation.fulfill();
}
});
synchronized (expectation) {
expectation.wait(1000);
}
assertTrue(expectation.isFulfilled);
}
@Test
public void valueForAttributeOfEntitySucceeds() throws InterruptedException {
Mentat mentat = new Mentat();
TxReport report = this.populateWithTypesSchema(mentat);
final Long aEntid = report.getEntidForTempId("a");
TypedValue value = mentat.valueForAttributeOfEntity(":foo/long", aEntid);
assertNotNull(value);
assertEquals(25, value.asLong().longValue());
}
@Test
public void entidForAttributeSucceeds() {
Mentat mentat = new Mentat();
this.populateWithTypesSchema(mentat);
long entid = mentat.entIdForAttribute(":foo/long");
assertEquals(65540, entid);
}
}

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.mozilla.mentat">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
</manifest>

View file

@ -0,0 +1,48 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* Copyright 2018 Mozilla
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of the
* License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License. */
package com.mozilla.mentat;
import com.sun.jna.Structure;
import com.sun.jna.ptr.IntByReference;
import java.io.Closeable;
import java.util.Arrays;
import java.util.List;
/**
* Represents a C struct of a list of Strings containing attributes in the format
* `:namespace/name`.
*/
public class AttributeList extends Structure implements Closeable {
public static class ByReference extends AttributeList implements Structure.ByReference {
}
public static class ByValue extends AttributeList implements Structure.ByValue {
}
public IntByReference attributes;
public int numberOfItems;
// Used by the Swift counterpart, JNA does this for us automagically.
// But we still need it here so that the number of fields and their order is correct
public int len;
@Override
protected List<String> getFieldOrder() {
return Arrays.asList("attributes", "numberOfItems", "len");
}
@Override
public void close() {
if (this.getPointer() != null) {
JNA.INSTANCE.destroy(this.getPointer());
}
}
}

View file

@ -0,0 +1,54 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* Copyright 2018 Mozilla
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of the
* License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License. */
package com.mozilla.mentat;
import com.sun.jna.Pointer;
import java.util.Iterator;
/**
* Iterator for a {@link CollResult}
*/
public class ColResultIterator extends RustObject implements Iterator {
Pointer nextPointer;
ColResultIterator(Pointer iterator) {
this.rawPointer = iterator;
}
private Pointer getNextPointer() {
return JNA.INSTANCE.typed_value_list_iter_next(this.rawPointer);
}
@Override
public boolean hasNext() {
this.nextPointer = getNextPointer();
return this.nextPointer != null;
}
@Override
public TypedValue next() {
Pointer next = this.nextPointer == null ? getNextPointer() : this.nextPointer;
if (next == null) {
return null;
}
return new TypedValue(next);
}
@Override
public void close() {
if (this.rawPointer != null) {
JNA.INSTANCE.typed_value_list_iter_destroy(this.rawPointer);
}
}
}

View file

@ -0,0 +1,60 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* Copyright 2018 Mozilla
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of the
* License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License. */
package com.mozilla.mentat;
import com.sun.jna.Pointer;
import java.util.Date;
import java.util.UUID;
/**
* Wraps a `Coll` result from a Mentat query.
* A `Coll` result is a list of rows of single values of type {@link TypedValue}.
* Values for individual rows can be fetched as {@link TypedValue} or converted into a requested type.
* <p>
* Row values can be fetched as one of the following types:
* <ul>
* <li>{@link TypedValue}</li>
* <li>long</li>
* <li>Entid (as long)</li>
* <li>Keyword (as String)</li>
* <li>boolean</li>
* <li>double</li>
* <li>{@link Date}</li>
* <li>{@link String}</li>
* <li>{@link UUID}</li>
* </ul>
* <p>
* To iterate over the result set use standard iteration flows.
*/
public class CollResult extends TupleResult implements Iterable<TypedValue> {
public CollResult(Pointer pointer) {
super(pointer);
}
@Override
public void close() {
if (this.rawPointer != null) {
JNA.INSTANCE.destroy(this.rawPointer);
}
}
@Override
public ColResultIterator iterator() {
Pointer iterPointer = JNA.INSTANCE.typed_value_list_into_iter(this.rawPointer);
this.rawPointer = null;
if (iterPointer == null) {
return null;
}
return new ColResultIterator(iterPointer);
}
}

View file

@ -0,0 +1,18 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* Copyright 2018 Mozilla
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of the
* License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License. */
package com.mozilla.mentat;
/**
* Interface defining the structure of a callback from a query returning a {@link CollResult}.
*/
public interface CollResultHandler {
void handleList(CollResult list);
}

View file

@ -0,0 +1,101 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* Copyright 2018 Mozilla
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of the
* License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License. */
package com.mozilla.mentat;
import com.sun.jna.Library;
import com.sun.jna.Native;
import com.sun.jna.NativeLibrary;
import com.sun.jna.Pointer;
/**
* JNA interface for FFI to Mentat's Rust library
* Each function definition here link directly to a function in Mentat's FFI crate.
* Signatures must match for the linking to work correctly.
*/
public interface JNA extends Library {
String JNA_LIBRARY_NAME = "mentat_ffi";
NativeLibrary JNA_NATIVE_LIB = NativeLibrary.getInstance(JNA_LIBRARY_NAME);
JNA INSTANCE = (JNA) Native.loadLibrary(JNA_LIBRARY_NAME, JNA.class);
Pointer store_open(String dbPath);
void destroy(Pointer obj);
void query_builder_destroy(Pointer obj);
void store_destroy(Pointer obj);
void typed_value_destroy(Pointer obj);
void typed_value_list_destroy(Pointer obj);
void typed_value_list_iter_destroy(Pointer obj);
void typed_value_result_set_destroy(Pointer obj);
void typed_value_result_set_iter_destroy(Pointer obj);
void tx_report_destroy(Pointer obj);
// transact
RustResult store_transact(Pointer store, String transaction);
Pointer tx_report_entity_for_temp_id(Pointer report, String tempid);
long tx_report_get_entid(Pointer report);
long tx_report_get_tx_instant(Pointer report);
// sync
RustResult store_sync(Pointer store, String userUuid, String serverUri);
// observers
void store_register_observer(Pointer store, String key, Pointer attributes, int len, TxObserverCallback callback);
void store_unregister_observer(Pointer store, String key);
long store_entid_for_attribute(Pointer store, String attr);
// Query Building
Pointer store_query(Pointer store, String query);
RustResult store_value_for_attribute(Pointer store, long entid, String attribute);
void query_builder_bind_long(Pointer query, String var, long value);
void query_builder_bind_ref(Pointer query, String var, long value);
void query_builder_bind_ref_kw(Pointer query, String var, String value);
void query_builder_bind_kw(Pointer query, String var, String value);
void query_builder_bind_boolean(Pointer query, String var, int value);
void query_builder_bind_double(Pointer query, String var, double value);
void query_builder_bind_timestamp(Pointer query, String var, long value);
void query_builder_bind_string(Pointer query, String var, String value);
void query_builder_bind_uuid(Pointer query, String var, Pointer value);
// Query Execution
RustResult query_builder_execute(Pointer query);
RustResult query_builder_execute_scalar(Pointer query);
RustResult query_builder_execute_coll(Pointer query);
RustResult query_builder_execute_tuple(Pointer query);
// Query Result Processing
long typed_value_into_long(Pointer value);
long typed_value_into_entid(Pointer value);
String typed_value_into_kw(Pointer value);
String typed_value_into_string(Pointer value);
Pointer typed_value_into_uuid(Pointer value);
int typed_value_into_boolean(Pointer value);
double typed_value_into_double(Pointer value);
long typed_value_into_timestamp(Pointer value);
Pointer typed_value_value_type(Pointer value);
Pointer row_at_index(Pointer rows, int index);
Pointer typed_value_result_set_into_iter(Pointer rows);
Pointer typed_value_result_set_iter_next(Pointer iter);
Pointer typed_value_list_into_iter(Pointer rows);
Pointer typed_value_list_iter_next(Pointer iter);
Pointer value_at_index(Pointer rows, int index);
long value_at_index_into_long(Pointer rows, int index);
long value_at_index_into_entid(Pointer rows, int index);
String value_at_index_into_kw(Pointer rows, int index);
String value_at_index_into_string(Pointer rows, int index);
Pointer value_at_index_into_uuid(Pointer rows, int index);
long value_at_index_into_boolean(Pointer rows, int index);
double value_at_index_into_double(Pointer rows, int index);
long value_at_index_into_timestamp(Pointer rows, int index);
}

View file

@ -0,0 +1,147 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* Copyright 2018 Mozilla
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of the
* License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License. */
package com.mozilla.mentat;
import android.util.Log;
import com.sun.jna.Memory;
import com.sun.jna.Pointer;
/**
* The primary class for accessing Mentat's API.<br/>
* This class provides all of the basic API that can be found in Mentat's Store struct.<br/>
* The raw pointer it holds is a pointer to a Store.
*/
public class Mentat extends RustObject {
static {
System.loadLibrary("mentat_ffi");
}
/**
* Open a connection to a Store in a given location.<br/>
* If the store does not already exist, one will be created.
* @param dbPath The URI as a String of the store to open.
*/
public Mentat(String dbPath) {
this.rawPointer = JNA.INSTANCE.store_open(dbPath);
}
/**
* Open a connection to an in-memory Store.
*/
public Mentat() {
this.rawPointer = JNA.INSTANCE.store_open("");
}
/**
* Create a new Mentat with the provided pointer to a Mentat Store
* @param rawPointer A pointer to a Mentat Store.
*/
public Mentat(Pointer rawPointer) { this.rawPointer = rawPointer; }
/**
* Simple transact of an EDN string.
* TODO: Throw an exception if the transact fails
* @param transaction The string, as EDN, to be transacted.
* @return The {@link TxReport} of the completed transaction
*/
public TxReport transact(String transaction) {
RustResult result = JNA.INSTANCE.store_transact(this.rawPointer, transaction);
if (result.isFailure()) {
Log.e("Mentat", result.err);
return null;
}
if (result.isSuccess()) {
return new TxReport(result.ok);
} else {
return null;
}
}
/**
* Get the the `Entid` of the attribute
* @param attribute The string represeting the attribute whose `Entid` we are after. The string is represented as `:namespace/name`.
* @return The `Entid` associated with the attribute.
*/
public long entIdForAttribute(String attribute) {
return JNA.INSTANCE.store_entid_for_attribute(this.rawPointer, attribute);
}
/**
* Start a query.
* @param query The string represeting the the query to be executed.
* @return The {@link Query} representing the query that can be executed.
*/
public Query query(String query) {
return new Query(JNA.INSTANCE.store_query(this.rawPointer, query));
}
/**
* Retrieve a single value of an attribute for an Entity
* TODO: Throw an exception if there is no the result contains an error.
* @param attribute The string the attribute whose value is to be returned. The string is represented as `:namespace/name`.
* @param entid The `Entid` of the entity we want the value from.
* @return The {@link TypedValue} containing the value of the attribute for the entity.
*/
public TypedValue valueForAttributeOfEntity(String attribute, long entid) {
RustResult result = JNA.INSTANCE.store_value_for_attribute(this.rawPointer, entid, attribute);
if (result.isSuccess()) {
return new TypedValue(result.ok);
}
if (result.isFailure()) {
Log.e("Mentat", result.err);
}
return null;
}
/**
* Register an callback and a set of attributes to observer for transaction observation.
* The callback function is called when a transaction occurs in the `Store` that this `Mentat`
* is connected to that affects the attributes that an observer has registered for.
* @param key `String` representing an identifier for the observer.
* @param attributes An array of Strings representing the attributes that the observer wishes
* to be notified about if they are referenced in a transaction.
* @param callback the function to call when an observer notice is fired.
*/
public void registerObserver(String key, String[] attributes, TxObserverCallback callback) {
// turn string array into int array
long[] attrEntids = new long[attributes.length];
for(int i = 0; i < attributes.length; i++) {
attrEntids[i] = JNA.INSTANCE.store_entid_for_attribute(this.rawPointer, attributes[i]);
}
final Pointer entidsNativeArray = new Memory(8 * attrEntids.length);
entidsNativeArray.write(0, attrEntids, 0, attrEntids.length);
JNA.INSTANCE.store_register_observer(rawPointer, key, entidsNativeArray, attrEntids.length, callback);
}
/**
* Unregister the observer that was registered with the provided key such that it will no longer be called
* if a transaction occurs that affects the attributes that the observer was registered to observe.
* <p/>
* The observer will need to re-register if it wants to start observing again.
* @param key String representing an identifier for the observer.
*/
public void unregisterObserver(String key) {
JNA.INSTANCE.store_unregister_observer(rawPointer, key);
}
@Override
public void close() {
if (this.rawPointer != null) {
JNA.INSTANCE.store_destroy(this.rawPointer);
}
}
}

View file

@ -0,0 +1,330 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* Copyright 2018 Mozilla
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of the
* License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License. */
package com.mozilla.mentat;
import android.util.Log;
import com.sun.jna.Memory;
import com.sun.jna.Pointer;
import java.nio.ByteBuffer;
import java.util.Date;
import java.util.UUID;
/**
* This class allows you to construct a query, bind values to variables and run those queries against a mentat DB.
* <p/>
* This class cannot be created directly, but must be created through `Mentat.query(String:)`.
* <p/>
* The types of values you can bind are:
* <ul>
* <li>{@link TypedValue}</li>
* <li>long</li>
* <li>Entid (as long)</li>
* <li>Keyword (as String)</li>
* <li>boolean</li>
* <li>double</li>
* <li>{@link Date}</li>
* <li>{@link String}</li>
* <li>{@link UUID}</li>
* </ul>
* <p>
* <p/>
* Each bound variable must have a corresponding value in the query string used to create this query.
* <p/>
* <pre>{@code
* String query = "[:find ?name ?cat\n" +
* " :in ?type\n" +
* " :where\n" +
* " [?c :community/name ?name]\n" +
* " [?c :community/type ?type]\n" +
* " [?c :community/category ?cat]]";
* mentat.query(query).bindKeywordReference("?type", ":community.type/website").run(new RelResultHandler() {
* @Override
* public void handleRows(RelResult rows) {
* ...
* }
* });
*}</pre>
* <p/>
* Queries can be run and the results returned in a number of different formats. Individual result values are returned as `TypedValues` and
* the format differences relate to the number and structure of those values. The result format is related to the format provided in the query string.
* <p/>
* - `Rel` - This is the default `run` function and returns a list of rows of values. Queries that wish to have `Rel` results should format their query strings:
*
* <pre>{@code
* String query = "[: find ?a ?b ?c\n" +
* " : where ... ]";
* mentat.query(query).run(new RelResultHandler() {
* @Override
* public void handleRows(RelResult rows) {
* ...
* }
* });
*}</pre>
* <p/>
* - `Scalar` - This returns a single value as a result. This can be optional, as the value may not be present. Queries that wish to have `Scalar` results should format their query strings:
*
* <pre>{@code
* String query = "[: find ?a .\n" +
* " : where ... ]";
* mentat.query(query).runScalar(new ScalarResultHandler() {
* @Override
* public void handleValue(TypedValue value) {
* ...
* }
* });
*}</pre>
* <p/>
* - `Coll` - This returns a list of single values as a result. Queries that wish to have `Coll` results should format their query strings:
* <pre>{@code
* String query = "[: find [?a ...]\n" +
* " : where ... ]";
* mentat.query(query).runColl(new ScalarResultHandler() {
* @Override
* public void handleList(CollResult list) {
* ...
* }
* });
*}</pre>
* <p/>
* - `Tuple` - This returns a single row of values. Queries that wish to have `Tuple` results should format their query strings:
* <pre>{@code
* String query = "[: find [?a ?b ?c]\n" +
* " : where ... ]";
* mentat.query(query).runTuple(new TupleResultHandler() {
* @Override
* public void handleRow(TupleResult row) {
* ...
* }
* });
*}</pre>
*/
public class Query extends RustObject {
public Query(Pointer pointer) {
this.rawPointer = pointer;
}
/**
* Binds a long value to the provided variable name.
* TODO: Throw an exception if the query raw pointer has been consumed.
* @param varName The name of the variable in the format `?name`.
* @param value The value to be bound
* @return This {@link Query} such that further function can be called.
*/
Query bindLong(String varName, long value) {
this.validate();
JNA.INSTANCE.query_builder_bind_long(this.rawPointer, varName, value);
return this;
}
/**
* Binds a Entid value to the provided variable name.
* TODO: Throw an exception if the query raw pointer has been consumed.
* @param varName The name of the variable in the format `?name`.
* @param value The value to be bound
* @return This {@link Query} such that further function can be called.
*/
Query bindEntidReference(String varName, long value) {
this.validate();
JNA.INSTANCE.query_builder_bind_ref(this.rawPointer, varName, value);
return this;
}
/**
* Binds a String keyword value to the provided variable name.
* TODO: Throw an exception if the query raw pointer has been consumed.
* @param varName The name of the variable in the format `?name`.
* @param value The value to be bound
* @return This {@link Query} such that further function can be called.
*/
Query bindKeywordReference(String varName, String value) {
this.validate();
JNA.INSTANCE.query_builder_bind_ref_kw(this.rawPointer, varName, value);
return this;
}
/**
* Binds a keyword value to the provided variable name.
* TODO: Throw an exception if the query raw pointer has been consumed.
* @param varName The name of the variable in the format `?name`.
* @param value The value to be bound
* @return This {@link Query} such that further function can be called.
*/
Query bindKeyword(String varName, String value) {
this.validate();
JNA.INSTANCE.query_builder_bind_kw(this.rawPointer, varName, value);
return this;
}
/**
* Binds a boolean value to the provided variable name.
* TODO: Throw an exception if the query raw pointer has been consumed.
* @param varName The name of the variable in the format `?name`.
* @param value The value to be bound
* @return This {@link Query} such that further function can be called.
*/
Query bindBoolean(String varName, boolean value) {
this.validate();
JNA.INSTANCE.query_builder_bind_boolean(this.rawPointer, varName, value ? 1 : 0);
return this;
}
/**
* Binds a double value to the provided variable name.
* TODO: Throw an exception if the query raw pointer has been consumed.
* @param varName The name of the variable in the format `?name`.
* @param value The value to be bound
* @return This {@link Query} such that further function can be called.
*/
Query bindDouble(String varName, double value) {
this.validate();
JNA.INSTANCE.query_builder_bind_double(this.rawPointer, varName, value);
return this;
}
/**
* Binds a {@link Date} value to the provided variable name.
* TODO: Throw an exception if the query raw pointer has been consumed.
* @param varName The name of the variable in the format `?name`.
* @param value The value to be bound
* @return This {@link Query} such that further function can be called.
*/
Query bindDate(String varName, Date value) {
this.validate();
long timestamp = value.getTime() * 1000;
JNA.INSTANCE.query_builder_bind_timestamp(this.rawPointer, varName, timestamp);
return this;
}
/**
* Binds a {@link String} value to the provided variable name.
* TODO: Throw an exception if the query raw pointer has been consumed.
* @param varName The name of the variable in the format `?name`.
* @param value The value to be bound
* @return This {@link Query} such that further function can be called.
*/
Query bindString(String varName, String value) {
this.validate();
JNA.INSTANCE.query_builder_bind_string(this.rawPointer, varName, value);
return this;
}
/**
* Binds a {@link UUID} value to the provided variable name.
* TODO: Throw an exception if the query raw pointer has been consumed.
* @param varName The name of the variable in the format `?name`.
* @param value The value to be bound
* @return This {@link Query} such that further function can be called.
*/
Query bindUUID(String varName, UUID value) {
this.validate();
ByteBuffer bb = ByteBuffer.wrap(new byte[16]);
bb.putLong(value.getMostSignificantBits());
bb.putLong(value.getLeastSignificantBits());
byte[] bytes = bb.array();
final Pointer bytesNativeArray = new Memory(bytes.length);
bytesNativeArray.write(0, bytes, 0, bytes.length);
JNA.INSTANCE.query_builder_bind_uuid(this.rawPointer, varName, bytesNativeArray);
return this;
}
/**
* Execute the query with the values bound associated with this {@link Query} and call the provided
* callback function with the results as a list of rows of {@link TypedValue}s.
* TODO: Throw an exception if the query raw pointer has been consumed or the query fails to execute
* @param handler the handler to call with the results of this query
*/
void run(final RelResultHandler handler) {
this.validate();
RustResult result = JNA.INSTANCE.query_builder_execute(rawPointer);
rawPointer = null;
if (result.isFailure()) {
Log.e("Query", result.err);
return;
}
handler.handleRows(new RelResult(result.ok));
}
/**
* Execute the query with the values bound associated with this {@link Query} and call the provided
* callback function with the results with the result as a single {@link TypedValue}.
* TODO: Throw an exception if the query raw pointer has been consumed or the query fails to execute
* @param handler the handler to call with the results of this query
*/
void runScalar(final ScalarResultHandler handler) {
this.validate();
RustResult result = JNA.INSTANCE.query_builder_execute_scalar(rawPointer);
rawPointer = null;
if (result.isFailure()) {
Log.e("Query", result.err);
return;
}
if (result.isSuccess()) {
handler.handleValue(new TypedValue(result.ok));
} else {
handler.handleValue(null);
}
}
/**
* Execute the query with the values bound associated with this {@link Query} and call the provided
* callback function with the results with the result as a list of single {@link TypedValue}s.
* TODO: Throw an exception if the query raw pointer has been consumed or the query fails to execute
* @param handler the handler to call with the results of this query
*/
void runColl(final CollResultHandler handler) {
this.validate();
RustResult result = JNA.INSTANCE.query_builder_execute_coll(rawPointer);
rawPointer = null;
if (result.isFailure()) {
Log.e("Query", result.err);
return;
}
handler.handleList(new CollResult(result.ok));
}
/**
* Execute the query with the values bound associated with this {@link Query} and call the provided
* callback function with the results with the result as a list of single {@link TypedValue}s.
* TODO: Throw an exception if the query raw pointer has been consumed or the query fails to execute
* @param handler the handler to call with the results of this query
*/
void runTuple(final TupleResultHandler handler) {
this.validate();
RustResult result = JNA.INSTANCE.query_builder_execute_tuple(rawPointer);
rawPointer = null;
if (result.isFailure()) {
Log.e("Query", result.err);
return;
}
if (result.isSuccess()) {
handler.handleRow(new TupleResult(result.ok));
} else {
handler.handleRow(null);
}
}
@Override
public void close() {
if (this.rawPointer == null) {
return;
}
JNA.INSTANCE.query_builder_destroy(this.rawPointer);
}
}

View file

@ -0,0 +1,86 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* Copyright 2018 Mozilla
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of the
* License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License. */
package com.mozilla.mentat;
import com.sun.jna.Pointer;
/**
* Wraps a `Rel` result from a Mentat query.
* A `Rel` result is a list of rows of `TypedValues`.
* Individual rows can be fetched or the set can be iterated.
* </p>
* To fetch individual rows from a `RelResult` use `row(Int32)`.
* </p>
* <pre>{@code
* mentat.query(query).run(new RelResultHandler() {
* @Override
* public void handleRows(RelResult rows) {
* TupleResult row1 = rows.rowAtIndex(0);
* TupleResult row2 = rows.rowAtIndex(1);
* ...
* }
* });
*}</pre>
* </p>
* To iterate over the result set use standard iteration flows.
* <pre>{@code
* mentat.query(query).run(new RelResultHandler() {
* @Override
* public void handleRows(RelResult rows) {
* for (TupleResult row: rows) {
* ...
* }
* }
* });
*}</pre>
* </p>
* Note that iteration is consuming and can only be done once.
*/
public class RelResult extends RustObject implements Iterable<TupleResult> {
public RelResult(Pointer pointer) {
this.rawPointer = pointer;
}
/**
* Fetch the row at the requested index.
* TODO: Throw an exception if the result set has already been iterated.
* @param index the index of the row to be fetched
* @return The row at the requested index as a `TupleResult`, if present, or nil if there is no row at that index.
*/
public TupleResult rowAtIndex(int index) {
this.validate();
Pointer pointer = JNA.INSTANCE.row_at_index(this.rawPointer, index);
if (pointer == null) {
return null;
}
return new TupleResult(pointer);
}
@Override
public RelResultIterator iterator() {
this.validate();
Pointer iterPointer = JNA.INSTANCE.typed_value_result_set_into_iter(this.rawPointer);
this.rawPointer = null;
if (iterPointer == null) {
return null;
}
return new RelResultIterator(iterPointer);
}
@Override
public void close() {
if (this.rawPointer != null) {
JNA.INSTANCE.typed_value_result_set_destroy(this.rawPointer);
}
}
}

View file

@ -0,0 +1,18 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* Copyright 2018 Mozilla
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of the
* License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License. */
package com.mozilla.mentat;
/**
* Interface defining the structure of a callback from a query returning a {@link RelResult}.
*/
public interface RelResultHandler {
void handleRows(RelResult rows);
}

View file

@ -0,0 +1,54 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* Copyright 2018 Mozilla
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of the
* License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License. */
package com.mozilla.mentat;
import com.sun.jna.Pointer;
import java.util.Iterator;
/**
* Iterator for a {@link RelResult}
*/
public class RelResultIterator extends RustObject implements Iterator {
Pointer nextPointer;
RelResultIterator(Pointer iterator) {
this.rawPointer = iterator;
}
private Pointer getNextPointer() {
return JNA.INSTANCE.typed_value_result_set_iter_next(this.rawPointer);
}
@Override
public boolean hasNext() {
this.nextPointer = getNextPointer();
return this.nextPointer != null;
}
@Override
public TupleResult next() {
Pointer next = this.nextPointer == null ? getNextPointer() : this.nextPointer;
if (next == null) {
return null;
}
return new TupleResult(next);
}
@Override
public void close() {
if (this.rawPointer != null) {
JNA.INSTANCE.typed_value_result_set_iter_destroy(this.rawPointer);
}
}
}

View file

@ -0,0 +1,34 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* Copyright 2018 Mozilla
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of the
* License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License. */
package com.mozilla.mentat;
import com.sun.jna.Pointer;
import java.io.Closeable;
/**
* Base class that wraps an non-optional {@link Pointer} representing a pointer to a Rust object.
* This class implements {@link Closeable} but does not provide an implementation, forcing all
* subclasses to implement it. This ensures that all classes that inherit from RustObject
* will have their {@link Pointer} destroyed when the Java wrapper is destroyed.
*/
abstract class RustObject implements Closeable {
Pointer rawPointer;
/**
* Throws a {@link NullPointerException} if the underlying {@link Pointer} is null.
*/
void validate() {
if (this.rawPointer == null) {
throw new NullPointerException(this.getClass() + " consumed");
}
}
}

View file

@ -0,0 +1,62 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* Copyright 2018 Mozilla
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of the
* License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License. */
package com.mozilla.mentat;
import com.sun.jna.Pointer;
import com.sun.jna.Structure;
import java.io.Closeable;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
/**
* Represents a C struct containing a {@link Pointer}s and String that map to a Rust Result.
* A RustResult will contain either an ok value, OR an err value, or neither - never both.
*/
public class RustResult extends Structure implements Closeable {
public static class ByReference extends RustResult implements Structure.ByReference {
}
public static class ByValue extends RustResult implements Structure.ByValue {
}
public Pointer ok;
public String err;
/**
* Is there an value attached to this result
* @return true if a value is present, false otherwise
*/
public boolean isSuccess() {
return this.ok != null;
}
/**
* Is there an error attached to this result?
* @return true is an error is present, false otherwise
*/
public boolean isFailure() {
return this.err != null;
}
@Override
protected List<String> getFieldOrder() {
return Arrays.asList("ok", "err");
}
@Override
public void close() throws IOException {
if (this.getPointer() != null) {
JNA.INSTANCE.destroy(this.getPointer());
}
}
}

View file

@ -0,0 +1,18 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* Copyright 2018 Mozilla
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of the
* License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License. */
package com.mozilla.mentat;
/**
* Interface defining the structure of a callback from a query returning a single {@link TypedValue}.
*/
public interface ScalarResultHandler {
void handleValue(TypedValue value);
}

View file

@ -0,0 +1,168 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* Copyright 2018 Mozilla
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of the
* License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License. */
package com.mozilla.mentat;
import com.sun.jna.Pointer;
import java.nio.ByteBuffer;
import java.util.Date;
import java.util.UUID;
/**
* Wraps a `Tuple` result from a Mentat query.
* A `Tuple` result is a single row {@link TypedValue}s.
* Values for individual fields can be fetched as {@link TypedValue} or converted into a requested type.
* <p>
* Field values can be fetched as one of the following types:
* <ul>
* <li>{@link TypedValue}</li>
* <li>long</li>
* <li>Entid (as long)</li>
* <li>Keyword (as String)</li>
* <li>boolean</li>
* <li>double</li>
* <li>{@link Date}</li>
* <li>{@link String}</li>
* <li>{@link UUID}</li>
* </ul>
* <p>
* To iterate over the result set use standard iteration flows.
*/
public class TupleResult extends RustObject {
public TupleResult(Pointer pointer) {
this.rawPointer = pointer;
}
/**
* Return the {@link TypedValue} at the specified index.
* If the index is greater than the number of values then this function will crash.
* @param index The index of the value to fetch.
* @return The {@link TypedValue} at that index.
*/
public TypedValue get(Integer index) {
this.validate();
Pointer pointer = JNA.INSTANCE.value_at_index(this.rawPointer, index);
if (pointer == null) {
return null;
}
return new TypedValue(pointer);
}
/**
* Return the {@link Long} at the specified index.
* If the index is greater than the number of values then this function will crash.
* If the value type if the {@link TypedValue} at this index is not `Long` then this function will crash.
* @param index The index of the value to fetch.
* @return The {@link Long} at that index.
*/
public Long asLong(Integer index) {
this.validate();
return JNA.INSTANCE.value_at_index_into_long(this.rawPointer, index);
}
/**
* Return the Entid at the specified index.
* If the index is greater than the number of values then this function will crash.
* If the value type if the {@link TypedValue} at this index is not `Ref` then this function will crash.
* @param index The index of the value to fetch.
* @return The Entid at that index.
*/
public Long asEntid(Integer index) {
this.validate();
return JNA.INSTANCE.value_at_index_into_entid(this.rawPointer, index);
}
/**
* Return the keyword {@link String} at the specified index.
* If the index is greater than the number of values then this function will crash.
* If the value type if the {@link TypedValue} at this index is not `Keyword` then this function will crash.
* @param index The index of the value to fetch.
* @return The keyword at that index.
*/
public String asKeyword(Integer index) {
this.validate();
return JNA.INSTANCE.value_at_index_into_kw(this.rawPointer, index);
}
/**
* Return the {@link Boolean} at the specified index.
* If the index is greater than the number of values then this function will crash.
* If the value type if the {@link TypedValue} at this index is not `Boolean` then this function will crash.
* @param index The index of the value to fetch.
* @return The {@link Boolean} at that index.
*/
public Boolean asBool(Integer index) {
this.validate();
return JNA.INSTANCE.value_at_index_into_boolean(this.rawPointer, index) == 0 ? false : true;
}
/**
* Return the {@link Double} at the specified index.
* If the index is greater than the number of values then this function will crash.
* If the value type if the {@link TypedValue} at this index is not `Double` then this function will crash.
* @param index The index of the value to fetch.
* @return The {@link Double} at that index.
*/
public Double asDouble(Integer index) {
this.validate();
return JNA.INSTANCE.value_at_index_into_double(this.rawPointer, index);
}
/**
* Return the {@link Date} at the specified index.
* If the index is greater than the number of values then this function will crash.
* If the value type if the {@link TypedValue} at this index is not `Instant` then this function will crash.
* @param index The index of the value to fetch.
* @return The {@link Date} at that index.
*/
public Date asDate(Integer index) {
this.validate();
return new Date(JNA.INSTANCE.value_at_index_into_timestamp(this.rawPointer, index));
}
/**
* Return the {@link String} at the specified index.
* If the index is greater than the number of values then this function will crash.
* If the value type if the {@link TypedValue} at this index is not `String` then this function will crash.
* @param index The index of the value to fetch.
* @return The {@link String} at that index.
*/
public String asString(Integer index) {
this.validate();
return JNA.INSTANCE.value_at_index_into_string(this.rawPointer, index);
}
/**
* Return the {@link UUID} at the specified index.
* If the index is greater than the number of values then this function will crash.
* If the value type if the {@link TypedValue} at this index is not `Uuid` then this function will crash.
* @param index The index of the value to fetch.
* @return The {@link UUID} at that index.
*/
public UUID asUUID(Integer index) {
this.validate();
Pointer uuidPtr = JNA.INSTANCE.value_at_index_into_uuid(this.rawPointer, index);
byte[] bytes = uuidPtr.getByteArray(0, 16);
ByteBuffer bb = ByteBuffer.wrap(bytes);
long high = bb.getLong();
long low = bb.getLong();
return new UUID(high, low);
}
@Override
public void close() {
if (this.rawPointer != null) {
JNA.INSTANCE.typed_value_list_destroy(this.rawPointer);
}
}
}

View file

@ -0,0 +1,18 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* Copyright 2018 Mozilla
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of the
* License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License. */
package com.mozilla.mentat;
/**
* Interface defining the structure of a callback from a query returning a {@link TupleResult}.
*/
public interface TupleResultHandler {
void handleRow(TupleResult row);
}

View file

@ -0,0 +1,64 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* Copyright 2018 Mozilla
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of the
* License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License. */
package com.mozilla.mentat;
import com.sun.jna.Pointer;
import com.sun.jna.Structure;
import java.io.Closeable;
import java.util.Arrays;
import java.util.List;
/**
* Represents a C struct representing changes that occured during a transaction.
* These changes contain the transaction identifier, a {@link Pointer} to a list of affected attribute
* Entids and the number of items that the list contains.
*/
public class TxChange extends Structure implements Closeable {
public static class ByReference extends TxChange implements Structure.ByReference {
}
public static class ByValue extends TxChange implements Structure.ByValue {
}
public int txid;
public Pointer changes;
public int numberOfItems;
// Used by the Swift counterpart, JNA does this for us automagically.
// But we still need it here so that the number of fields and their order is correct
public int changes_len;
/**
* Get the affected attributes for this transaction
* @return The changes as a list of Entids of affected attributes
*/
public List<Long> getChanges() {
final long[] array = (long[]) changes.getLongArray(0, numberOfItems);
Long[] longArray = new Long[numberOfItems];
int idx = 0;
for (long change: array) {
longArray[idx++] = change;
}
return Arrays.asList(longArray);
}
@Override
protected List<String> getFieldOrder() {
return Arrays.asList("txid", "changes", "changes_len", "numberOfItems");
}
@Override
public void close() {
if (this.getPointer() != null) {
JNA.INSTANCE.destroy(this.getPointer());
}
}
}

View file

@ -0,0 +1,56 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* Copyright 2018 Mozilla
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of the
* License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License. */
package com.mozilla.mentat;
import com.sun.jna.Structure;
import java.io.Closeable;
import java.util.Arrays;
import java.util.List;
/**
* Represents a C struct containing a list of {@link TxChange}s that occured.
*/
public class TxChangeList extends Structure implements Closeable {
public static class ByReference extends TxChangeList implements Structure.ByReference {
}
public static class ByValue extends TxChangeList implements Structure.ByValue {
}
public TxChange.ByReference reports;
public int numberOfItems;
// Used by the Swift counterpart, JNA does this for us automagically.
// // But we still need it here so that the number of fields and their order is correct
public int len;
/**
* Get the changes that occured
* @return a list of {@link TxChange}s for the notification
*/
public List<TxChange> getReports() {
final TxChange[] array = (TxChange[]) reports.toArray(numberOfItems);
return Arrays.asList(array);
}
@Override
protected List<String> getFieldOrder() {
return Arrays.asList("reports", "numberOfItems", "len");
}
@Override
public void close() {
final TxChange[] nativeReports = (TxChange[]) reports.toArray(numberOfItems);
for (TxChange nativeReport : nativeReports) {
nativeReport.close();
}
}
}

View file

@ -0,0 +1,20 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* Copyright 2018 Mozilla
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of the
* License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License. */
package com.mozilla.mentat;
import com.sun.jna.Callback;
/**
* Protocol to be implemented by any object that wishes to register for transaction observation
*/
public interface TxObserverCallback extends Callback {
void transactionObserverCalled(String key, TxChangeList.ByReference reports);
}

View file

@ -0,0 +1,89 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* Copyright 2018 Mozilla
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of the
* License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License. */
package com.mozilla.mentat;
import com.sun.jna.Pointer;
import java.util.Date;
/**
* This class wraps a raw pointer than points to a Rust `TxReport` object.
* </p>
* The `TxReport` contains information about a successful Mentat transaction.
* </p>
* This information includes:
* <ul>
* <li>`txId` - the identifier for the transaction.</li>
* <li>`txInstant` - the time that the transaction occured.</li>
* <li>a map of temporary identifiers provided in the transaction and the `Entid`s that they were mapped to.</li>
* </ul>
* </p>
* Access an `Entid` for a temporary identifier that was provided in the transaction can be done through `entid(String:)`.
* </p>
* <pre>{@code
* TxReport report = mentat.transact("[[:db/add "a" :foo/boolean true]]");
* long aEntid = report.getEntidForTempId("a");
*}</pre>
*/
public class TxReport extends RustObject {
private Long txId;
private Date txInstant;
public TxReport(Pointer pointer) {
this.rawPointer = pointer;
}
/**
* Get the identifier for the transaction.
* @return The identifier for the transaction.
*/
public Long getTxId() {
if (this.txId == null) {
this.txId = JNA.INSTANCE.tx_report_get_entid(this.rawPointer);
}
return this.txId;
}
/**
* Get the time that the transaction occured.
* @return The time that the transaction occured.
*/
public Date getTxInstant() {
if (this.txInstant == null) {
this.txInstant = new Date(JNA.INSTANCE.tx_report_get_tx_instant(this.rawPointer));
}
return this.txInstant;
}
/**
* Access an `Entid` for a temporary identifier that was provided in the transaction.
* @param tempId A {@link String} representing the temporary identifier to fetch the `Entid` for.
* @return The `Entid` for the temporary identifier, if present, otherwise `null`.
*/
public Long getEntidForTempId(String tempId) {
Pointer longPointer = JNA.INSTANCE.tx_report_entity_for_temp_id(this.rawPointer, tempId);
if (longPointer == null) {
return null;
}
return longPointer.getLong(0);
}
@Override
public void close() {
if (this.rawPointer != null) {
JNA.INSTANCE.tx_report_destroy(this.rawPointer);
}
}
}

View file

@ -0,0 +1,157 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* Copyright 2018 Mozilla
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of the
* License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License. */
package com.mozilla.mentat;
import com.sun.jna.Pointer;
import java.nio.ByteBuffer;
import java.util.Date;
import java.util.UUID;
/**
* A wrapper around Mentat's `TypedValue` Rust object. This class wraps a raw pointer to a Rust `TypedValue`
* struct and provides accessors to the values according to expected result type.
* </p>
* As the FFI functions for fetching values are consuming, this class keeps a copy of the result internally after
* fetching so that the value can be referenced several times.
* </p>
* Also, due to the consuming nature of the FFI layer, this class also manages it's raw pointer, nilling it after calling the
* FFI conversion function so that the underlying base class can manage cleanup.
*/
public class TypedValue extends RustObject {
private Object value;
private boolean isConsumed() {
return this.rawPointer == null;
}
public TypedValue(Pointer pointer) {
this.rawPointer = pointer;
}
/**
* This value as a {@link Long}. This function will panic if the `ValueType` of this
* {@link TypedValue} is not a `Long`
* @return the value of this {@link TypedValue} as a {@link Long}
*/
public Long asLong() {
if (!this.isConsumed()) {
this.value = JNA.INSTANCE.typed_value_into_long(this.rawPointer);
this.rawPointer = null;
}
return (Long)value;
}
/**
* This value as a Entid. This function will panic if the `ValueType` of this
* {@link TypedValue} is not a `Ref`
* @return the value of this {@link TypedValue} as a Entid
*/
public Long asEntid() {
if (!this.isConsumed()) {
this.value = JNA.INSTANCE.typed_value_into_entid(this.rawPointer);
this.rawPointer = null;
}
return (Long)value;
}
/**
* This value as a keyword {@link String}. This function will panic if the `ValueType` of this
* {@link TypedValue} is not a `Keyword`
* @return the value of this {@link TypedValue} as a Keyword
*/
public String asKeyword() {
if (!this.isConsumed()) {
this.value = JNA.INSTANCE.typed_value_into_kw(this.rawPointer);
this.rawPointer = null;
}
return (String)value;
}
/**
* This value as a {@link Boolean}. This function will panic if the `ValueType` of this
* {@link TypedValue} is not a `Boolean`
* @return the value of this {@link TypedValue} as a {@link Boolean}
*/
public Boolean asBoolean() {
if (!this.isConsumed()) {
long value = JNA.INSTANCE.typed_value_into_boolean(this.rawPointer);
this.value = value == 0 ? false : true;
this.rawPointer = null;
}
return (Boolean) this.value;
}
/**
* This value as a {@link Double}. This function will panic if the `ValueType` of this
* {@link TypedValue} is not a `Double`
* @return the value of this {@link TypedValue} as a {@link Double}
*/
public Double asDouble() {
if (!this.isConsumed()) {
this.value = JNA.INSTANCE.typed_value_into_double(this.rawPointer);
this.rawPointer = null;
}
return (Double)value;
}
/**
* This value as a {@link Date}. This function will panic if the `ValueType` of this
* {@link TypedValue} is not a `Instant`
* @return the value of this {@link TypedValue} as a {@link Date}
*/
public Date asDate() {
if (!this.isConsumed()) {
this.value = new Date(JNA.INSTANCE.typed_value_into_timestamp(this.rawPointer) * 1_000);
this.rawPointer = null;
}
return (Date)this.value;
}
/**
* This value as a {@link String}. This function will panic if the `ValueType` of this
* {@link TypedValue} is not a `String`
* @return the value of this {@link TypedValue} as a {@link String}
*/
public String asString() {
if (!this.isConsumed()) {
this.value = JNA.INSTANCE.typed_value_into_string(this.rawPointer);
this.rawPointer = null;
}
return (String)value;
}
/**
* This value as a {@link UUID}. This function will panic if the `ValueType` of this
* {@link TypedValue} is not a `Uuid`
* @return the value of this {@link TypedValue} as a {@link UUID}
*/
public UUID asUUID() {
if (!this.isConsumed()) {
Pointer uuidPtr = JNA.INSTANCE.typed_value_into_uuid(this.rawPointer);
byte[] bytes = uuidPtr.getByteArray(0, 16);
ByteBuffer bb = ByteBuffer.wrap(bytes);
long high = bb.getLong();
long low = bb.getLong();
this.value = new UUID(high, low);
this.rawPointer = null;
}
return (UUID)this.value;
}
@Override
public void close() {
if (this.rawPointer != null) {
JNA.INSTANCE.typed_value_destroy(this.rawPointer);
}
}
}

View file

@ -0,0 +1,3 @@
<resources>
<string name="app_name">Mentat</string>
</resources>

View file

@ -0,0 +1 @@
include ':library'

View file

@ -0,0 +1,615 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 50;
objects = {
/* Begin PBXBuildFile section */
7B74483D208DF667006CFFB0 /* Result+Unwrap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B74483C208DF667006CFFB0 /* Result+Unwrap.swift */; };
7BAE75A22089020E00895D37 /* libmentat_ffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7BEB7D23207BE2AF000369AD /* libmentat_ffi.a */; };
7BAE75A42089022B00895D37 /* libsqlite3.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 7BAE75A32089022B00895D37 /* libsqlite3.tbd */; };
7BDB96942077C299009D0651 /* Mentat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7BDB968A2077C299009D0651 /* Mentat.framework */; };
7BDB96992077C299009D0651 /* MentatTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BDB96982077C299009D0651 /* MentatTests.swift */; };
7BDB969B2077C299009D0651 /* Mentat.h in Headers */ = {isa = PBXBuildFile; fileRef = 7BDB968D2077C299009D0651 /* Mentat.h */; settings = {ATTRIBUTES = (Public, ); }; };
7BDB96AF2077C38E009D0651 /* Query.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BDB96A62077C38D009D0651 /* Query.swift */; };
7BDB96B02077C38E009D0651 /* Mentat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BDB96A72077C38D009D0651 /* Mentat.swift */; };
7BDB96B12077C38E009D0651 /* store.h in Headers */ = {isa = PBXBuildFile; fileRef = 7BDB96A82077C38E009D0651 /* store.h */; };
7BDB96B22077C38E009D0651 /* RelResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BDB96A92077C38E009D0651 /* RelResult.swift */; };
7BDB96B32077C38E009D0651 /* RustObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BDB96AA2077C38E009D0651 /* RustObject.swift */; };
7BDB96B42077C38E009D0651 /* OptionalRustObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BDB96AB2077C38E009D0651 /* OptionalRustObject.swift */; };
7BDB96B52077C38E009D0651 /* TupleResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BDB96AC2077C38E009D0651 /* TupleResult.swift */; };
7BDB96B72077C38E009D0651 /* TypedValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BDB96AE2077C38E009D0651 /* TypedValue.swift */; };
7BDB96C22077CD98009D0651 /* libresolv.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 7BDB96C12077CD98009D0651 /* libresolv.tbd */; };
7BDB96C62077D347009D0651 /* Date+Int64.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BDB96C52077D346009D0651 /* Date+Int64.swift */; };
7BDB96C9207B735A009D0651 /* fixtures in Resources */ = {isa = PBXBuildFile; fileRef = 7BDB96C8207B735A009D0651 /* fixtures */; };
7BDB96CC207B7684009D0651 /* Errors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BDB96CB207B7684009D0651 /* Errors.swift */; };
7BEB7D2C207D03DA000369AD /* TxReport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BEB7D2B207D03DA000369AD /* TxReport.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
7BDB96952077C299009D0651 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 7BDB96812077C299009D0651 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 7BDB96892077C299009D0651;
remoteInfo = Mentat;
};
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
7B74483C208DF667006CFFB0 /* Result+Unwrap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "Result+Unwrap.swift"; path = "Mentat/Extensions/Result+Unwrap.swift"; sourceTree = SOURCE_ROOT; };
7B911E1A2085081D000998CB /* libtoodle.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libtoodle.a; path = "../../../../sync-storage-prototype/rust/target/universal/release/libtoodle.a"; sourceTree = "<group>"; };
7BAE75A32089022B00895D37 /* libsqlite3.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libsqlite3.tbd; path = usr/lib/libsqlite3.tbd; sourceTree = SDKROOT; };
7BDB968A2077C299009D0651 /* Mentat.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Mentat.framework; sourceTree = BUILT_PRODUCTS_DIR; };
7BDB968D2077C299009D0651 /* Mentat.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Mentat.h; sourceTree = "<group>"; };
7BDB968E2077C299009D0651 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
7BDB96932077C299009D0651 /* MentatTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MentatTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
7BDB96982077C299009D0651 /* MentatTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentatTests.swift; sourceTree = "<group>"; };
7BDB969A2077C299009D0651 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
7BDB96A62077C38D009D0651 /* Query.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Query.swift; sourceTree = "<group>"; };
7BDB96A72077C38D009D0651 /* Mentat.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Mentat.swift; sourceTree = "<group>"; };
7BDB96A82077C38E009D0651 /* store.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = store.h; sourceTree = "<group>"; };
7BDB96A92077C38E009D0651 /* RelResult.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RelResult.swift; sourceTree = "<group>"; };
7BDB96AA2077C38E009D0651 /* RustObject.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RustObject.swift; sourceTree = "<group>"; };
7BDB96AB2077C38E009D0651 /* OptionalRustObject.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OptionalRustObject.swift; sourceTree = "<group>"; };
7BDB96AC2077C38E009D0651 /* TupleResult.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TupleResult.swift; sourceTree = "<group>"; };
7BDB96AE2077C38E009D0651 /* TypedValue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TypedValue.swift; sourceTree = "<group>"; };
7BDB96BF2077CD7A009D0651 /* libmentat.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libmentat.a; path = ../../../target/universal/release/libmentat.a; sourceTree = "<group>"; };
7BDB96C12077CD98009D0651 /* libresolv.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libresolv.tbd; path = usr/lib/libresolv.tbd; sourceTree = SDKROOT; };
7BDB96C32077D090009D0651 /* module.map */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = module.map; sourceTree = "<group>"; };
7BDB96C52077D346009D0651 /* Date+Int64.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Date+Int64.swift"; sourceTree = "<group>"; };
7BDB96C8207B735A009D0651 /* fixtures */ = {isa = PBXFileReference; lastKnownFileType = folder; name = fixtures; path = ../../../../fixtures; sourceTree = "<group>"; };
7BDB96CB207B7684009D0651 /* Errors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Errors.swift; sourceTree = "<group>"; };
7BEB7D21207BDDEF000369AD /* libtoodle.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libtoodle.a; path = ../../../target/universal/release/libtoodle.a; sourceTree = "<group>"; };
7BEB7D23207BE2AF000369AD /* libmentat_ffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libmentat_ffi.a; path = ../../../target/universal/release/libmentat_ffi.a; sourceTree = "<group>"; };
7BEB7D2B207D03DA000369AD /* TxReport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TxReport.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
7BDB96862077C299009D0651 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
7BAE75A42089022B00895D37 /* libsqlite3.tbd in Frameworks */,
7BDB96C22077CD98009D0651 /* libresolv.tbd in Frameworks */,
7BAE75A22089020E00895D37 /* libmentat_ffi.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
7BDB96902077C299009D0651 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
7BDB96942077C299009D0651 /* Mentat.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
7BDB96802077C299009D0651 = {
isa = PBXGroup;
children = (
7BDB968C2077C299009D0651 /* Mentat */,
7BDB96972077C299009D0651 /* MentatTests */,
7BDB968B2077C299009D0651 /* Products */,
7BDB96BE2077CD7A009D0651 /* Frameworks */,
);
sourceTree = "<group>";
};
7BDB968B2077C299009D0651 /* Products */ = {
isa = PBXGroup;
children = (
7BDB968A2077C299009D0651 /* Mentat.framework */,
7BDB96932077C299009D0651 /* MentatTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
7BDB968C2077C299009D0651 /* Mentat */ = {
isa = PBXGroup;
children = (
7BDB96CA207B7672009D0651 /* Errors */,
7BDB96C42077D346009D0651 /* Extensions */,
7BDB96BA2077C42B009D0651 /* Core */,
7BDB96A42077C301009D0651 /* Query */,
7BDB96B92077C403009D0651 /* Rust */,
7BDB96A82077C38E009D0651 /* store.h */,
7BDB96A72077C38D009D0651 /* Mentat.swift */,
7BEB7D26207BE5BB000369AD /* Transact */,
7BDB968D2077C299009D0651 /* Mentat.h */,
7BDB968E2077C299009D0651 /* Info.plist */,
7BDB96C32077D090009D0651 /* module.map */,
);
path = Mentat;
sourceTree = "<group>";
};
7BDB96972077C299009D0651 /* MentatTests */ = {
isa = PBXGroup;
children = (
7BDB96C8207B735A009D0651 /* fixtures */,
7BDB96982077C299009D0651 /* MentatTests.swift */,
7BDB969A2077C299009D0651 /* Info.plist */,
);
path = MentatTests;
sourceTree = "<group>";
};
7BDB96A42077C301009D0651 /* Query */ = {
isa = PBXGroup;
children = (
7BDB96A62077C38D009D0651 /* Query.swift */,
7BDB96A92077C38E009D0651 /* RelResult.swift */,
7BDB96AC2077C38E009D0651 /* TupleResult.swift */,
);
path = Query;
sourceTree = "<group>";
};
7BDB96B92077C403009D0651 /* Rust */ = {
isa = PBXGroup;
children = (
7BDB96AB2077C38E009D0651 /* OptionalRustObject.swift */,
7BDB96AA2077C38E009D0651 /* RustObject.swift */,
);
path = Rust;
sourceTree = "<group>";
};
7BDB96BA2077C42B009D0651 /* Core */ = {
isa = PBXGroup;
children = (
7BDB96AE2077C38E009D0651 /* TypedValue.swift */,
);
path = Core;
sourceTree = "<group>";
};
7BDB96BE2077CD7A009D0651 /* Frameworks */ = {
isa = PBXGroup;
children = (
7BAE75A32089022B00895D37 /* libsqlite3.tbd */,
7B911E1A2085081D000998CB /* libtoodle.a */,
7BEB7D23207BE2AF000369AD /* libmentat_ffi.a */,
7BEB7D21207BDDEF000369AD /* libtoodle.a */,
7BDB96C12077CD98009D0651 /* libresolv.tbd */,
7BDB96BF2077CD7A009D0651 /* libmentat.a */,
);
name = Frameworks;
sourceTree = "<group>";
};
7BDB96C42077D346009D0651 /* Extensions */ = {
isa = PBXGroup;
children = (
7B74483C208DF667006CFFB0 /* Result+Unwrap.swift */,
7BDB96C52077D346009D0651 /* Date+Int64.swift */,
);
path = Extensions;
sourceTree = "<group>";
};
7BDB96CA207B7672009D0651 /* Errors */ = {
isa = PBXGroup;
children = (
7BDB96CB207B7684009D0651 /* Errors.swift */,
);
path = Errors;
sourceTree = "<group>";
};
7BEB7D26207BE5BB000369AD /* Transact */ = {
isa = PBXGroup;
children = (
7BEB7D2B207D03DA000369AD /* TxReport.swift */,
);
path = Transact;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXHeadersBuildPhase section */
7BDB96872077C299009D0651 /* Headers */ = {
isa = PBXHeadersBuildPhase;
buildActionMask = 2147483647;
files = (
7BDB96B12077C38E009D0651 /* store.h in Headers */,
7BDB969B2077C299009D0651 /* Mentat.h in Headers */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXHeadersBuildPhase section */
/* Begin PBXNativeTarget section */
7BDB96892077C299009D0651 /* Mentat */ = {
isa = PBXNativeTarget;
buildConfigurationList = 7BDB969E2077C299009D0651 /* Build configuration list for PBXNativeTarget "Mentat" */;
buildPhases = (
7BDB96852077C299009D0651 /* Sources */,
7BDB96862077C299009D0651 /* Frameworks */,
7BDB96872077C299009D0651 /* Headers */,
7BDB96882077C299009D0651 /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = Mentat;
productName = Mentat;
productReference = 7BDB968A2077C299009D0651 /* Mentat.framework */;
productType = "com.apple.product-type.framework";
};
7BDB96922077C299009D0651 /* MentatTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 7BDB96A12077C299009D0651 /* Build configuration list for PBXNativeTarget "MentatTests" */;
buildPhases = (
7BDB968F2077C299009D0651 /* Sources */,
7BDB96902077C299009D0651 /* Frameworks */,
7BDB96912077C299009D0651 /* Resources */,
);
buildRules = (
);
dependencies = (
7BDB96962077C299009D0651 /* PBXTargetDependency */,
);
name = MentatTests;
productName = MentatTests;
productReference = 7BDB96932077C299009D0651 /* MentatTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
7BDB96812077C299009D0651 /* Project object */ = {
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 0930;
LastUpgradeCheck = 0930;
ORGANIZATIONNAME = Mozilla;
TargetAttributes = {
7BDB96892077C299009D0651 = {
CreatedOnToolsVersion = 9.3;
LastSwiftMigration = 0930;
};
7BDB96922077C299009D0651 = {
CreatedOnToolsVersion = 9.3;
};
};
};
buildConfigurationList = 7BDB96842077C299009D0651 /* Build configuration list for PBXProject "Mentat" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
);
mainGroup = 7BDB96802077C299009D0651;
productRefGroup = 7BDB968B2077C299009D0651 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
7BDB96892077C299009D0651 /* Mentat */,
7BDB96922077C299009D0651 /* MentatTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
7BDB96882077C299009D0651 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
7BDB96912077C299009D0651 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
7BDB96C9207B735A009D0651 /* fixtures in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
7BDB96852077C299009D0651 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
7BDB96B32077C38E009D0651 /* RustObject.swift in Sources */,
7BDB96C62077D347009D0651 /* Date+Int64.swift in Sources */,
7BEB7D2C207D03DA000369AD /* TxReport.swift in Sources */,
7BDB96B42077C38E009D0651 /* OptionalRustObject.swift in Sources */,
7BDB96B22077C38E009D0651 /* RelResult.swift in Sources */,
7BDB96AF2077C38E009D0651 /* Query.swift in Sources */,
7BDB96CC207B7684009D0651 /* Errors.swift in Sources */,
7BDB96B02077C38E009D0651 /* Mentat.swift in Sources */,
7BDB96B72077C38E009D0651 /* TypedValue.swift in Sources */,
7BDB96B52077C38E009D0651 /* TupleResult.swift in Sources */,
7B74483D208DF667006CFFB0 /* Result+Unwrap.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
7BDB968F2077C299009D0651 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
7BDB96992077C299009D0651 /* MentatTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
7BDB96962077C299009D0651 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 7BDB96892077C299009D0651 /* Mentat */;
targetProxy = 7BDB96952077C299009D0651 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin XCBuildConfiguration section */
7BDB969C2077C299009D0651 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_IDENTITY = "iPhone Developer";
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 11.3;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
OTHER_CFLAGS = "-fembed-bitcode";
OTHER_LDFLAGS = "";
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_INCLUDE_PATHS = "$(SRCROOT)/Mentat";
SWIFT_OBJC_BRIDGING_HEADER = "";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
VERSIONING_SYSTEM = "apple-generic";
VERSION_INFO_PREFIX = "";
};
name = Debug;
};
7BDB969D2077C299009D0651 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_IDENTITY = "iPhone Developer";
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 11.3;
MTL_ENABLE_DEBUG_INFO = NO;
OTHER_CFLAGS = "-fembed-bitcode";
OTHER_LDFLAGS = "";
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_INCLUDE_PATHS = "$(SRCROOT)/Mentat";
SWIFT_OBJC_BRIDGING_HEADER = "";
SWIFT_OPTIMIZATION_LEVEL = "-O";
VALIDATE_PRODUCT = YES;
VERSIONING_SYSTEM = "apple-generic";
VERSION_INFO_PREFIX = "";
};
name = Release;
};
7BDB969F2077C299009D0651 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "";
CODE_SIGN_STYLE = Automatic;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 8BHJ767F4Y;
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
DYLIB_INSTALL_NAME_BASE = "@rpath";
INFOPLIST_FILE = Mentat/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
LIBRARY_SEARCH_PATHS = "$(PROJECT_DIR)/../../../target/universal/release";
OTHER_CFLAGS = "-fembed-bitcode";
OTHER_LDFLAGS = "";
PRODUCT_BUNDLE_IDENTIFIER = com.mozilla.Mentat;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SKIP_INSTALL = YES;
SWIFT_INCLUDE_PATHS = "$(SRCROOT)/Mentat";
SWIFT_OBJC_BRIDGING_HEADER = "";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 4.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
7BDB96A02077C299009D0651 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "";
CODE_SIGN_STYLE = Automatic;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 8BHJ767F4Y;
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
DYLIB_INSTALL_NAME_BASE = "@rpath";
ENABLE_TESTABILITY = NO;
INFOPLIST_FILE = Mentat/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
LIBRARY_SEARCH_PATHS = "$(PROJECT_DIR)/../../../target/universal/release";
OTHER_CFLAGS = "-fembed-bitcode";
OTHER_LDFLAGS = "";
PRODUCT_BUNDLE_IDENTIFIER = com.mozilla.Mentat;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SKIP_INSTALL = YES;
SWIFT_INCLUDE_PATHS = "$(SRCROOT)/Mentat";
SWIFT_OBJC_BRIDGING_HEADER = "";
SWIFT_VERSION = 4.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
7BDB96A22077C299009D0651 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = 8BHJ767F4Y;
INFOPLIST_FILE = MentatTests/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
OTHER_CFLAGS = "";
OTHER_LDFLAGS = "";
PRODUCT_BUNDLE_IDENTIFIER = com.mozilla.MentatTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 4.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
7BDB96A32077C299009D0651 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = 8BHJ767F4Y;
INFOPLIST_FILE = MentatTests/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
OTHER_CFLAGS = "";
OTHER_LDFLAGS = "";
PRODUCT_BUNDLE_IDENTIFIER = com.mozilla.MentatTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 4.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
7BDB96842077C299009D0651 /* Build configuration list for PBXProject "Mentat" */ = {
isa = XCConfigurationList;
buildConfigurations = (
7BDB969C2077C299009D0651 /* Debug */,
7BDB969D2077C299009D0651 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
7BDB969E2077C299009D0651 /* Build configuration list for PBXNativeTarget "Mentat" */ = {
isa = XCConfigurationList;
buildConfigurations = (
7BDB969F2077C299009D0651 /* Debug */,
7BDB96A02077C299009D0651 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
7BDB96A12077C299009D0651 /* Build configuration list for PBXNativeTarget "MentatTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
7BDB96A22077C299009D0651 /* Debug */,
7BDB96A32077C299009D0651 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 7BDB96812077C299009D0651 /* Project object */;
}

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:Mentat.xcodeproj">
</FileRef>
</Workspace>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
</plist>

View file

@ -0,0 +1,107 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0930"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "7BDB96892077C299009D0651"
BuildableName = "Mentat.framework"
BlueprintName = "Mentat"
ReferencedContainer = "container:Mentat.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "7BDB96922077C299009D0651"
BuildableName = "MentatTests.xctest"
BlueprintName = "MentatTests"
ReferencedContainer = "container:Mentat.xcodeproj">
</BuildableReference>
<SkippedTests>
<Test
Identifier = "MentatTests/testBindBoolean()">
</Test>
<Test
Identifier = "MentatTests/testBindDate()">
</Test>
</SkippedTests>
</TestableReference>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "7BDB96892077C299009D0651"
BuildableName = "Mentat.framework"
BlueprintName = "Mentat"
ReferencedContainer = "container:Mentat.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "7BDB96892077C299009D0651"
BuildableName = "Mentat.framework"
BlueprintName = "Mentat"
ReferencedContainer = "container:Mentat.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "7BDB96892077C299009D0651"
BuildableName = "Mentat.framework"
BlueprintName = "Mentat"
ReferencedContainer = "container:Mentat.xcodeproj">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View file

@ -0,0 +1,99 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0930"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "7BDB96892077C299009D0651"
BuildableName = "Mentat.framework"
BlueprintName = "Mentat"
ReferencedContainer = "container:Mentat.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "7BDB96922077C299009D0651"
BuildableName = "MentatTests.xctest"
BlueprintName = "MentatTests"
ReferencedContainer = "container:Mentat.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "7BDB96892077C299009D0651"
BuildableName = "Mentat.framework"
BlueprintName = "Mentat"
ReferencedContainer = "container:Mentat.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Release"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "7BDB96892077C299009D0651"
BuildableName = "Mentat.framework"
BlueprintName = "Mentat"
ReferencedContainer = "container:Mentat.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "7BDB96892077C299009D0651"
BuildableName = "Mentat.framework"
BlueprintName = "Mentat"
ReferencedContainer = "container:Mentat.xcodeproj">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View file

@ -0,0 +1,181 @@
/* Copyright 2018 Mozilla
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of the
* License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License. */
import Foundation
import MentatStore
/**
A wrapper around Mentat's `TypedValue` Rust object. This class wraps a raw pointer to a Rust `TypedValue`
struct and provides accessors to the values according to expected result type.
As the FFI functions for fetching values are consuming, this class keeps a copy of the result internally after
fetching so that the value can be referenced several times.
Also, due to the consuming nature of the FFI layer, this class also manages it's raw pointer, nilling it after calling the
FFI conversion function so that the underlying base class can manage cleanup.
*/
class TypedValue: OptionalRustObject {
private var value: Any?
/**
The `ValueType` for this `TypedValue`.
- Returns: The `ValueType` for this `TypedValue`.
*/
var valueType: ValueType {
return typed_value_value_type(self.raw!)
}
private func isConsumed() -> Bool {
return self.raw == nil
}
/**
This value as a `Int64`. This function will panic if the `ValueType` of this `TypedValue`
is not a `Long`
- Returns: the value of this `TypedValue` as a `Int64`
*/
func asLong() -> Int64 {
defer {
self.raw = nil
}
if !self.isConsumed() {
self.value = typed_value_into_long(self.raw!)
}
return self.value as! Int64
}
/**
This value as an `Entid`. This function will panic if the `ValueType` of this `TypedValue`
is not a `Ref`
- Returns: the value of this `TypedValue` as an `Entid`
*/
func asEntid() -> Entid {
defer {
self.raw = nil
}
if !self.isConsumed() {
self.value = typed_value_into_entid(self.raw!)
}
return self.value as! Entid
}
/**
This value as a keyword `String`. This function will panic if the `ValueType` of this `TypedValue`
is not a `Keyword`
- Returns: the value of this `TypedValue` as a keyword `String`
*/
func asKeyword() -> String {
defer {
self.raw = nil
}
if !self.isConsumed() {
self.value = String(cString: typed_value_into_kw(self.raw!))
}
return self.value as! String
}
/**
This value as a `Bool`. This function will panic if the `ValueType` of this `TypedValue`
is not a `Boolean`
- Returns: the value of this `TypedValue` as a `Bool`
*/
func asBool() -> Bool {
defer {
self.raw = nil
}
if !self.isConsumed() {
let v = typed_value_into_boolean(self.raw!)
self.value = v > 0
}
return self.value as! Bool
}
/**
This value as a `Double`. This function will panic if the `ValueType` of this `TypedValue`
is not a `Double`
- Returns: the value of this `TypedValue` as a `Double`
*/
func asDouble() -> Double {
defer {
self.raw = nil
}
if !self.isConsumed() {
self.value = typed_value_into_double(self.raw!)
}
return self.value as! Double
}
/**
This value as a `Date`. This function will panic if the `ValueType` of this `TypedValue`
is not a `Instant`
- Returns: the value of this `TypedValue` as a `Date`
*/
func asDate() -> Date {
defer {
self.raw = nil
}
if !self.isConsumed() {
let timestamp = typed_value_into_timestamp(self.raw!)
self.value = Date(timeIntervalSince1970: TimeInterval(timestamp))
}
return self.value as! Date
}
/**
This value as a `String`. This function will panic if the `ValueType` of this `TypedValue`
is not a `String`
- Returns: the value of this `TypedValue` as a `String`
*/
func asString() -> String {
defer {
self.raw = nil
}
if !self.isConsumed() {
self.value = String(cString: typed_value_into_string(self.raw!))
}
return self.value as! String
}
/**
This value as a `UUID`. This function will panic if the `ValueType` of this `TypedValue`
is not a `Uuid`
- Returns: the value of this `TypedValue` as a `UUID?`. If the `UUID` is not valid then this function returns nil.
*/
func asUUID() -> UUID? {
defer {
self.raw = nil
}
if !self.isConsumed() {
let bytes = typed_value_into_uuid(self.raw!).pointee
self.value = UUID(uuid: bytes)
}
return self.value as! UUID?
}
override func cleanup(pointer: OpaquePointer) {
typed_value_destroy(pointer)
}
}

View file

@ -0,0 +1,30 @@
//
/* Copyright 2018 Mozilla
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of the
* License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License. */
import Foundation
public enum QueryError: Error {
case invalidKeyword(message: String)
case executionFailed(message: String)
}
public struct MentatError: Error {
let message: String
}
public enum PointerError: Error {
case pointerConsumed
}
public enum ResultError: Error {
case error(message: String)
case empty
}

View file

@ -0,0 +1,22 @@
/* Copyright 2018 Mozilla
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of the
* License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License. */
import Foundation
extension Date {
/**
This `Date` as microseconds.
- Returns: The `timeIntervalSince1970` in microseconds
*/
func toMicroseconds() -> Int64 {
return Int64(self.timeIntervalSince1970 * 1_000_000)
}
}

View file

@ -0,0 +1,50 @@
/* Copyright 2018 Mozilla
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of the
* License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License. */
import Foundation
import MentatStore
extension Result {
/**
Force unwraps a result.
Expects there to be a value attached and throws an error is there is not.
- Throws: `ResultError.error` if the result contains an error
- Throws: `ResultError.empty` if the result contains no error but also no result.
- Returns: The pointer to the successful result value.
*/
@discardableResult public func unwrap() throws -> UnsafeMutableRawPointer {
guard let success = self.ok else {
if let error = self.err {
throw ResultError.error(message: String(cString: error))
}
throw ResultError.empty
}
return success
}
/**
Unwraps an optional result, yielding either a successful value or a nil.
- Throws: `ResultError.error` if the result contains an error
- Returns: The pointer to the successful result value, or nil if no value is present.
*/
@discardableResult public func tryUnwrap() throws -> UnsafeMutableRawPointer? {
guard let success = self.ok else {
if let error = self.err {
throw ResultError.error(message: String(cString: error))
}
return nil
}
return success
}
}

View file

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSPrincipalClass</key>
<string></string>
</dict>
</plist>

View file

@ -0,0 +1,18 @@
/* Copyright 2018 Mozilla
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of the
* License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License. */
#import <UIKit/UIKit.h>
//! Project version number for Mentat.
FOUNDATION_EXPORT double MentatVersionNumber;
//! Project version string for Mentat.
FOUNDATION_EXPORT const unsigned char MentatVersionString[];

View file

@ -0,0 +1,170 @@
/* Copyright 2018 Mozilla
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of the
* License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License. */
import Foundation
import MentatStore
typealias Entid = Int64
/**
Protocol to be implemented by any object that wishes to register for transaction observation
*/
protocol Observing {
func transactionDidOccur(key: String, reports: [TxChange])
}
/**
Protocol to be implemented by any object that provides an interface to Mentat's transaction observers.
*/
protocol Observable {
func register(key: String, observer: Observing, attributes: [String])
func unregister(key: String)
}
/**
The primary class for accessing Mentat's API.
This class provides all of the basic API that can be found in Mentat's Store struct.
The raw pointer it holds is a pointer to a Store.
*/
class Mentat: RustObject {
fileprivate static var observers = [String: Observing]()
/**
Create a new Mentat with the provided pointer to a Mentat Store
- Parameter raw: A pointer to a Mentat Store.
*/
required override init(raw: OpaquePointer) {
super.init(raw: raw)
}
/**
Open a connection to a Store in a given location.
If the store does not already exist, one will be created.
- Parameter storeURI: The URI as a String of the store to open.
If no store URI is provided, an in-memory store will be opened.
*/
convenience init(storeURI: String = "") {
self.init(raw: store_open(storeURI))
}
/**
Simple transact of an EDN string.
- Parameter transaction: The string, as EDN, to be transacted
- Throws: `MentatError` if the an error occured during the transaction, or the TxReport is nil.
- Returns: The `TxReport` of the completed transaction
*/
func transact(transaction: String) throws -> TxReport {
let result = store_transact(self.raw, transaction).pointee
return TxReport(raw: try result.unwrap())
}
/**
Get the the `Entid` of the attribute.
- Parameter attribute: The string represeting the attribute whose `Entid` we are after.
The string is represented as `:namespace/name`.
- Returns: The `Entid` associated with the attribute.
*/
func entidForAttribute(attribute: String) -> Entid {
return Entid(store_entid_for_attribute(self.raw, attribute))
}
/**
Start a query.
- Parameter query: The string represeting the the query to be executed.
- Returns: The `Query` representing the query that can be executed.
*/
func query(query: String) -> Query {
return Query(raw: store_query(self.raw, query))
}
/**
Retrieve a single value of an attribute for an Entity
- Parameter attribute: The string the attribute whose value is to be returned.
The string is represented as `:namespace/name`.
- Parameter entid: The `Entid` of the entity we want the value from.
- Returns: The `TypedValue` containing the value of the attribute for the entity.
*/
func value(forAttribute attribute: String, ofEntity entid: Entid) throws -> TypedValue? {
let result = store_value_for_attribute(self.raw, entid, attribute).pointee
return TypedValue(raw: try result.unwrap())
}
// Destroys the pointer by passing it back into Rust to be cleaned up
override func cleanup(pointer: OpaquePointer) {
store_destroy(pointer)
}
}
/**
Set up `Mentat` to provide an interface to Mentat's transaction observation
*/
extension Mentat: Observable {
/**
Register an `Observing` and a set of attributes to observer for transaction observation.
The `transactionDidOccur(String: [TxChange]:)` function is called when a transaction
occurs in the `Store` that this `Mentat` is connected to that affects the attributes that an
`Observing` has registered for.
- Parameter key: `String` representing an identifier for the `Observing`.
- Parameter observer: The `Observing` to be notified when a transaction occurs.
- Parameter attributes: An `Array` of `Strings` representing the attributes that the `Observing`
wishes to be notified about if they are referenced in a transaction.
*/
func register(key: String, observer: Observing, attributes: [String]) {
let attrEntIds = attributes.map({ (kw) -> Entid in
let entid = Entid(self.entidForAttribute(attribute: kw));
return entid
})
let ptr = UnsafeMutablePointer<Entid>.allocate(capacity: attrEntIds.count)
let entidPointer = UnsafeMutableBufferPointer(start: ptr, count: attrEntIds.count)
var _ = entidPointer.initialize(from: attrEntIds)
guard let firstElement = entidPointer.baseAddress else {
return
}
Mentat.observers[key] = observer
store_register_observer(self.raw, key, firstElement, Entid(attributes.count), transactionObserverCallback)
}
/**
Unregister the `Observing` that was registered with the provided key such that it will no longer be called
if a transaction occurs that affects the attributes that `Observing` was registered to observe.
The `Observing` will need to re-register if it wants to start observing again.
- Parameter key: `String` representing an identifier for the `Observing`.
*/
func unregister(key: String) {
Mentat.observers.removeValue(forKey: key)
store_unregister_observer(self.raw, key)
}
}
/**
This function needs to be static as callbacks passed into Rust from Swift cannot contain state. Therefore the observers are static, as is
the function that we pass into Rust to receive the callback.
*/
private func transactionObserverCallback(key: UnsafePointer<CChar>, reports: UnsafePointer<TxChangeList>) {
let key = String(cString: key)
guard let observer = Mentat.observers[key] else { return }
DispatchQueue.global(qos: .background).async {
observer.transactionDidOccur(key: key, reports: [TxChange]())
}
}

View file

@ -0,0 +1,337 @@
/* Copyright 2018 Mozilla
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of the
* License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License. */
import Foundation
import MentatStore
/**
This class allows you to construct a query, bind values to variables and run those queries against a mentat DB.
This class cannot be created directly, but must be created through `Mentat.query(String:)`.
The types of values you can bind are
- `Int64`
- `Entid`
- `Keyword`
- `Bool`
- `Double`
- `Date`
- `String`
- `UUID`.
Each bound variable must have a corresponding value in the query string used to create this query.
```
let query = """
[:find ?name ?cat
:in ?type
:where
[?c :community/name ?name]
[?c :community/type ?type]
[?c :community/category ?cat]]
"""
mentat.query(query: query)
.bind(varName: "?type", toKeyword: ":community.type/website")
.run { result in
...
}
```
Queries can be run and the results returned in a number of different formats. Individual result values are returned as `TypedValues` and
the format differences relate to the number and structure of those values. The result format is related to the format provided in the query string.
- `Rel` - This is the default `run` function and returns a list of rows of values. Queries that wish to have `Rel` results should format their query strings:
```
let query = """
[: find ?a ?b ?c
: where ... ]
"""
mentat.query(query: query)
.run { result in
...
}
```
- `Scalar` - This returns a single value as a result. This can be optional, as the value may not be present. Queries that wish to have `Scalar` results should format their query strings:
```
let query = """
[: find ?a .
: where ... ]
"""
mentat.query(query: query)
.runScalar { result in
...
}
```
- `Coll` - This returns a list of single values as a result. Queries that wish to have `Coll` results should format their query strings:
```
let query = """
[: find [?a ...]
: where ... ]
"""
mentat.query(query: query)
.runColl { result in
...
}
```
- `Tuple` - This returns a single row of values. Queries that wish to have `Tuple` results should format their query strings:
```
let query = """
[: find [?a ?b ?c]
: where ... ]
"""
mentat.query(query: query)
.runTuple { result in
...
}
```
*/
class Query: OptionalRustObject {
/**
Binds a `Int64` value to the provided variable name.
- Parameter varName: The name of the variable in the format `?name`.
- Parameter value: The value to be bound
- Throws: `PointerError.pointerConsumed` if the underlying raw pointer has already consumed, which will occur if the query has already been executed.
- Returns: This `Query` such that further function can be called.
*/
func bind(varName: String, toLong value: Int64) throws -> Query {
query_builder_bind_long(try! self.validPointer(), varName, value)
return self
}
/**
Binds a `Entid` value to the provided variable name.
- Parameter varName: The name of the variable in the format `?name`.
- Parameter value: The value to be bound
- Throws: `PointerError.pointerConsumed` if the underlying raw pointer has already consumed, which will occur if the query has already been executed.
- Returns: This `Query` such that further function can be called.
*/
func bind(varName: String, toReference value: Entid) throws -> Query {
query_builder_bind_ref(try! self.validPointer(), varName, value)
return self
}
/**
Binds a `String` value representing a keyword for an attribute to the provided variable name.
Keywords take the format `:namespace/name`.
- Parameter varName: The name of the variable in the format `?name`.
- Parameter value: The value to be bound
- Throws: `PointerError.pointerConsumed` if the underlying raw pointer has already consumed, which will occur if the query has already been executed.
- Returns: This `Query` such that further function can be called.
*/
func bind(varName: String, toReference value: String) throws -> Query {
query_builder_bind_ref_kw(try! self.validPointer(), varName, value)
return self
}
/**
Binds a keyword `String` value to the provided variable name.
Keywords take the format `:namespace/name`.
- Parameter varName: The name of the variable in the format `?name`.
- Parameter value: The value to be bound
- Throws: `PointerError.pointerConsumed` if the underlying raw pointer has already consumed, which will occur if the query has already been executed.
- Returns: This `Query` such that further function can be called.
*/
func bind(varName: String, toKeyword value: String) throws -> Query {
query_builder_bind_kw(try! self.validPointer(), varName, value)
return self
}
/**
Binds a `Bool` value to the provided variable name.
- Parameter varName: The name of the variable in the format `?name`.
- Parameter value: The value to be bound
- Throws: `PointerError.pointerConsumed` if the underlying raw pointer has already consumed, which will occur if the query has already been executed.
- Returns: This `Query` such that further function can be called.
*/
func bind(varName: String, toBoolean value: Bool) throws -> Query {
query_builder_bind_boolean(try! self.validPointer(), varName, value ? 1 : 0)
return self
}
/**
Binds a `Double` value to the provided variable name.
- Parameter varName: The name of the variable in the format `?name`.
- Parameter value: The value to be bound
- Throws: `PointerError.pointerConsumed` if the underlying raw pointer has already consumed, which will occur if the query has already been executed.
- Returns: This `Query` such that further function can be called.
*/
func bind(varName: String, toDouble value: Double) throws -> Query {
query_builder_bind_double(try! self.validPointer(), varName, value)
return self
}
/**
Binds a `Date` value to the provided variable name.
- Parameter varName: The name of the variable in the format `?name`.
- Parameter value: The value to be bound
- Throws: `PointerError.pointerConsumed` if the underlying raw pointer has already consumed, which will occur if the query has already been executed.
- Returns: This `Query` such that further function can be called.
*/
func bind(varName: String, toDate value: Date) throws -> Query {
query_builder_bind_timestamp(try! self.validPointer(), varName, value.toMicroseconds())
return self
}
/**
Binds a `String` value to the provided variable name.
- Parameter varName: The name of the variable in the format `?name`.
- Parameter value: The value to be bound
- Throws: `PointerError.pointerConsumed` if the underlying raw pointer has already consumed, which will occur if the query has already been executed.
- Returns: This `Query` such that further function can be called.
*/
func bind(varName: String, toString value: String) throws -> Query {
query_builder_bind_string(try! self.validPointer(), varName, value)
return self
}
/**
Binds a `UUID` value to the provided variable name.
- Parameter varName: The name of the variable in the format `?name`.
- Parameter value: The value to be bound
- Throws: `PointerError.pointerConsumed` if the underlying raw pointer has already consumed, which will occur if the query has already been executed.
- Returns: This `Query` such that further function can be called.
*/
func bind(varName: String, toUuid value: UUID) throws -> Query {
var rawUuid = value.uuid
withUnsafePointer(to: &rawUuid) { uuidPtr in
query_builder_bind_uuid(try! self.validPointer(), varName, uuidPtr)
}
return self
}
/**
Execute the query with the values bound associated with this `Query` and call the provided callback function with the results as a list of rows of `TypedValues`.
- Parameter callback: the function to call with the results of this query
- Throws: `QueryError.executionFailed` if the query fails to execute. This could be because the provided query did not parse, or that
variable we incorrectly bound, or that the query provided was not `Rel`.
- Throws: `PointerError.pointerConsumed` if the underlying raw pointer has already consumed, which will occur if the query has previously been executed.
*/
func run(callback: @escaping (RelResult?) -> Void) throws {
let result = query_builder_execute(try! self.validPointer())
self.raw = nil
if let err = result.pointee.err {
let message = String(cString: err)
throw QueryError.executionFailed(message: message)
}
guard let results = result.pointee.ok else {
callback(nil)
return
}
callback(RelResult(raw: results))
}
/**
Execute the query with the values bound associated with this `Query` and call the provided callback function with the result as a single `TypedValue`.
- Parameter callback: the function to call with the results of this query
- Throws: `QueryError.executionFailed` if the query fails to execute. This could be because the provided query did not parse, that
variable we incorrectly bound, or that the query provided was not `Scalar`.
- Throws: `PointerError.pointerConsumed` if the underlying raw pointer has already consumed, which will occur if the query has previously been executed.
*/
func runScalar(callback: @escaping (TypedValue?) -> Void) throws {
let result = query_builder_execute_scalar(try! self.validPointer())
self.raw = nil
if let err = result.pointee.err {
let message = String(cString: err)
throw QueryError.executionFailed(message: message)
}
guard let results = result.pointee.ok else {
callback(nil)
return
}
callback(TypedValue(raw: OpaquePointer(results)))
}
/**
Execute the query with the values bound associated with this `Query` and call the provided callback function with the result as a list of single `TypedValues`.
- Parameter callback: the function to call with the results of this query
- Throws: `QueryError.executionFailed` if the query fails to execute. This could be because the provided query did not parse, that
variable we incorrectly bound, or that the query provided was not `Coll`.
- Throws: `PointerError.pointerConsumed` if the underlying raw pointer has already consumed, which will occur if the query has previously been executed.
*/
func runColl(callback: @escaping (ColResult?) -> Void) throws {
let result = query_builder_execute_coll(try! self.validPointer())
self.raw = nil
if let err = result.pointee.err {
let message = String(cString: err)
throw QueryError.executionFailed(message: message)
}
guard let results = result.pointee.ok else {
callback(nil)
return
}
callback(ColResult(raw: results))
}
/**
Execute the query with the values bound associated with this `Query` and call the provided callback function with the result as a list of single `TypedValues`.
- Parameter callback: the function to call with the results of this query
- Throws: `QueryError.executionFailed` if the query fails to execute. This could be because the provided query did not parse, that
variable we incorrectly bound, or that the query provided was not `Tuple`.
- Throws: `PointerError.pointerConsumed` if the underlying raw pointer has already consumed, which will occur if the query has previously been executed.
*/
func runTuple(callback: @escaping (TupleResult?) -> Void) throws {
let result = query_builder_execute_tuple(try! self.validPointer())
self.raw = nil
if let err = result.pointee.err {
let message = String(cString: err)
throw QueryError.executionFailed(message: message)
}
guard let results = result.pointee.ok else {
callback(nil)
return
}
callback(TupleResult(raw: OpaquePointer(results)))
}
override func cleanup(pointer: OpaquePointer) {
query_builder_destroy(pointer)
}
}

View file

@ -0,0 +1,106 @@
/* Copyright 2018 Mozilla
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of the
* License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License. */
import Foundation
import MentatStore
/**
Wraps a `Rel` result from a Mentat query.
A `Rel` result is a list of rows of `TypedValues`.
Individual rows can be fetched or the set can be iterated.
To fetch individual rows from a `RelResult` use `row(Int32)`.
```
query.run { rows in
let row1 = rows.row(0)
let row2 = rows.row(1)
}
```
To iterate over the result set use standard iteration flows.
```
query.run { rows in
rows.forEach { row in
...
}
}
```
Note that iteration is consuming and can only be done once.
*/
class RelResult: OptionalRustObject {
/**
Fetch the row at the requested index.
- Parameter index: the index of the row to be fetched
- Throws: `PointerError.pointerConsumed` if the result set has already been iterated.
- Returns: The row at the requested index as a `TupleResult`, if present, or nil if there is no row at that index.
*/
func row(index: Int32) throws -> TupleResult? {
guard let row = row_at_index(try self.validPointer(), index) else {
return nil
}
return TupleResult(raw: row)
}
override func cleanup(pointer: OpaquePointer) {
destroy(UnsafeMutableRawPointer(pointer))
}
}
/**
Iterator for `RelResult`.
To iterate over the result set use standard iteration flows.
```
query.run { result in
rows.forEach { row in
...
}
}
```
Note that iteration is consuming and can only be done once.
*/
class RelResultIterator: OptionalRustObject, IteratorProtocol {
typealias Element = TupleResult
init(iter: OpaquePointer?) {
super.init(raw: iter)
}
func next() -> Element? {
guard let iter = self.raw,
let rowPtr = typed_value_result_set_iter_next(iter) else {
return nil
}
return TupleResult(raw: rowPtr)
}
override func cleanup(pointer: OpaquePointer) {
typed_value_result_set_iter_destroy(pointer)
}
}
extension RelResult: Sequence {
func makeIterator() -> RelResultIterator {
do {
let rowIter = typed_value_result_set_into_iter(try self.validPointer())
self.raw = nil
return RelResultIterator(iter: rowIter)
} catch {
return RelResultIterator(iter: nil)
}
}
}

View file

@ -0,0 +1,217 @@
/* Copyright 2018 Mozilla
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of the
* License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License. */
import Foundation
import MentatStore
/**
Wraps a `Tuple` result from a Mentat query.
A `Tuple` result is a list of `TypedValues`.
Individual values can be fetched as `TypedValues` or converted into a requested type.
Values can be fetched as one of the following types:
- `TypedValue`
- `Int64`
- `Entid`
- `Keyword`
- `Bool`
- `Double`
- `Date`
- `String`
- `UUID`.
*/
class TupleResult: OptionalRustObject {
/**
Return the `TypedValue` at the specified index.
If the index is greater than the number of values then this function will crash.
- Parameter index: The index of the value to fetch.
- Returns: The `TypedValue` at that index.
*/
func get(index: Int) -> TypedValue {
return TypedValue(raw: value_at_index(self.raw!, Int32(index)))
}
/**
Return the `Int64` at the specified index.
If the index is greater than the number of values then this function will crash.
If the value type if the `TypedValue` at this index is not `Long` then this function will crash.
- Parameter index: The index of the value to fetch.
- Returns: The `Int64` at that index.
*/
func asLong(index: Int) -> Int64 {
return value_at_index_into_long(self.raw!, Int32(index))
}
/**
Return the `Entid` at the specified index.
If the index is greater than the number of values then this function will crash.
If the value type if the `TypedValue` at this index is not `Ref` then this function will crash.
- Parameter index: The index of the value to fetch.
- Returns: The `Entid` at that index.
*/
func asEntid(index: Int) -> Entid {
return value_at_index_into_entid(self.raw!, Int32(index))
}
/**
Return the keyword `String` at the specified index.
If the index is greater than the number of values then this function will crash.
If the value type if the `TypedValue` at this index is not `Keyword` then this function will crash.
- Parameter index: The index of the value to fetch.
- Returns: The keyword `String` at that index.
*/
func asKeyword(index: Int) -> String {
return String(cString: value_at_index_into_kw(self.raw!, Int32(index)))
}
/**
Return the `Bool` at the specified index.
If the index is greater than the number of values then this function will crash.
If the value type if the `TypedValue` at this index is not `Boolean` then this function will crash.
- Parameter index: The index of the value to fetch.
- Returns: The `Bool` at that index.
*/
func asBool(index: Int) -> Bool {
return value_at_index_into_boolean(self.raw!, Int32(index)) == 0 ? false : true
}
/**
Return the `Double` at the specified index.
If the index is greater than the number of values then this function will crash.
If the value type if the `TypedValue` at this index is not `Double` then this function will crash.
- Parameter index: The index of the value to fetch.
- Returns: The `Double` at that index.
*/
func asDouble(index: Int) -> Double {
return value_at_index_into_double(self.raw!, Int32(index))
}
/**
Return the `Date` at the specified index.
If the index is greater than the number of values then this function will crash.
If the value type if the `TypedValue` at this index is not `Instant` then this function will crash.
- Parameter index: The index of the value to fetch.
- Returns: The `Date` at that index.
*/
func asDate(index: Int) -> Date {
return Date(timeIntervalSince1970: TimeInterval(value_at_index_into_timestamp(self.raw!, Int32(index))))
}
/**
Return the `String` at the specified index.
If the index is greater than the number of values then this function will crash.
If the value type if the `TypedValue` at this index is not `String` then this function will crash.
- Parameter index: The index of the value to fetch.
- Returns: The `String` at that index.
*/
func asString(index: Int) -> String {
return String(cString: value_at_index_into_string(self.raw!, Int32(index)))
}
/**
Return the `UUID` at the specified index.
If the index is greater than the number of values then this function will crash.
If the value type if the `TypedValue` at this index is not `Uuid` then this function will crash.
- Parameter index: The index of the value to fetch.
- Returns: The `UUID` at that index.
*/
func asUUID(index: Int) -> UUID? {
return UUID(uuid: value_at_index_into_uuid(self.raw!, Int32(index)).pointee)
}
override func cleanup(pointer: OpaquePointer) {
typed_value_list_destroy(pointer)
}
}
/**
Wraps a `Coll` result from a Mentat query.
A `Coll` result is a list of rows of single values of type `TypedValue`.
Values for individual rows can be fetched as `TypedValue` or converted into a requested type.
Row values can be fetched as one of the following types:
- `TypedValue`
- `Int64`
- `Entid`
- `Keyword`
- `Bool`
- `Double`
- `Date`
- `String`
- `UUID`.
*/
class ColResult: TupleResult {
}
/**
Iterator for `ColResult`.
To iterate over the result set use standard iteration flows.
```
query.runColl { rows in
rows.forEach { value in
...
}
}
```
Note that iteration is consuming and can only be done once.
*/
class ColResultIterator: OptionalRustObject, IteratorProtocol {
typealias Element = TypedValue
init(iter: OpaquePointer?) {
super.init(raw: iter)
}
func next() -> Element? {
guard let iter = self.raw,
let rowPtr = typed_value_list_iter_next(iter) else {
return nil
}
return TypedValue(raw: rowPtr)
}
override func cleanup(pointer: OpaquePointer) {
typed_value_list_iter_destroy(pointer)
}
}
extension ColResult: Sequence {
func makeIterator() -> ColResultIterator {
defer {
self.raw = nil
}
guard let raw = self.raw else {
return ColResultIterator(iter: nil)
}
let rowIter = typed_value_list_into_iter(raw)
return ColResultIterator(iter: rowIter)
}
}

View file

@ -0,0 +1,68 @@
/* Copyright 2018 Mozilla
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of the
* License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License. */
import Foundation
import MentatStore
/**
Base class that wraps an optional `OpaquePointer` representing a pointer to a Rust object.
This class should be used to wrap Rust pointer that point to consuming structs, that is, calling a function
for that Rust pointer, will cause Rust to destroy the pointer, leaving the Swift pointer dangling.
These classes are responsible for ensuring that their raw `OpaquePointer` are `nil`led after calling a consuming
FFI function.
This class provides cleanup functions on deinit, ensuring that all classes
that inherit from it will have their `OpaquePointer` destroyed when the Swift wrapper is destroyed.
If a class does not override `cleanup` then a `fatalError` is thrown.
The optional pointer is managed here such that is the pointer is nil, then the cleanup function is not called
ensuring that we do not double free the pointer on exit.
*/
class OptionalRustObject: Destroyable {
var raw: OpaquePointer?
lazy var uniqueId: ObjectIdentifier = {
ObjectIdentifier(self)
}()
init(raw: UnsafeMutableRawPointer) {
self.raw = OpaquePointer(raw)
}
init(raw: OpaquePointer?) {
self.raw = raw
}
func intoRaw() -> OpaquePointer? {
return self.raw
}
deinit {
guard let raw = self.raw else { return }
self.cleanup(pointer: raw)
}
/**
Provides a non-optional `OpaquePointer` if one exists for this class.
- Throws: `Pointer.pointerConsumed` if the raw pointer wrapped by this class is nil
- Returns: the raw `OpaquePointer` wrapped by this class.
*/
func validPointer() throws -> OpaquePointer {
guard let r = self.raw else {
throw PointerError.pointerConsumed
}
return r
}
func cleanup(pointer: OpaquePointer) {
fatalError("\(cleanup) is not implemented.")
}
}

View file

@ -0,0 +1,49 @@
/* Copyright 2018 Mozilla
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of the
* License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License. */
import Foundation
import MentatStore
protocol Destroyable {
func cleanup(pointer: OpaquePointer)
}
/**
Base class that wraps an non-optional `OpaquePointer` representing a pointer to a Rust object.
This class provides cleanup functions on deinit, ensuring that all classes
that inherit from it will have their `OpaquePointer` destroyed when the Swift wrapper is destroyed.
If a class does not override `cleanup` then a `fatalError` is thrown.
*/
public class RustObject: Destroyable {
var raw: OpaquePointer
init(raw: OpaquePointer) {
self.raw = raw
}
init(raw: UnsafeMutableRawPointer) {
self.raw = OpaquePointer(raw)
}
init?(raw: OpaquePointer?) {
guard let r = raw else {
return nil
}
self.raw = r
}
deinit {
self.cleanup(pointer: self.raw)
}
func cleanup(pointer: OpaquePointer) {
fatalError("\(cleanup) is not implemented.")
}
}

View file

@ -0,0 +1,61 @@
/* Copyright 2018 Mozilla
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of the
* License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License. */
import Foundation
import MentatStore
/**
This class wraps a raw pointer than points to a Rust `TxReport` object.
The `TxReport` contains information about a successful Mentat transaction.
This information includes:
- `txId` - the identifier for the transaction.
- `txInstant` - the time that the transaction occured.
- a map of temporary identifiers provided in the transaction and the `Entid`s that they were mapped to,
Access an `Entid` for a temporary identifier that was provided in the transaction can be done through `entid(String:)`.
```
let report = mentat.transact("[[:db/add "a" :foo/boolean true]]")
let aEntid = report.entid(forTempId: "a")
```
*/
class TxReport: RustObject {
// The identifier for the transaction.
public var txId: Entid {
return tx_report_get_entid(self.raw)
}
// The time that the transaction occured.
public var txInstant: Date {
return Date(timeIntervalSince1970: TimeInterval(tx_report_get_tx_instant(self.raw)))
}
/**
Access an `Entid` for a temporary identifier that was provided in the transaction.
- Parameter tempId: A `String` representing the temporary identifier to fetch the `Entid` for.
- Returns: The `Entid` for the temporary identifier, if present, otherwise `nil`.
*/
public func entid(forTempId tempId: String) -> Entid? {
guard let entidPtr = tx_report_entity_for_temp_id(self.raw, tempId) else {
return nil
}
return entidPtr.pointee
}
override func cleanup(pointer: OpaquePointer) {
tx_report_destroy(pointer)
}
}

View file

@ -0,0 +1,4 @@
module MentatStore [system][extern_c] {
header "store.h"
export *
}

View file

@ -0,0 +1,171 @@
/* Copyright 2018 Mozilla
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of the
* License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License. */
#ifndef store_h
#define store_h
#include <stdint.h>
#include <Foundation/NSObjCRuntime.h>
/*
* This file contains headers for all of the structs and functions that map directly to the functions
* defined in mentat/ffi/src/lib.rs.
*
* The C in this file is specifically formatted to be used with Objective C and Swift and contains
* macros and flags that will not be recognised by other C based languages.
*/
/*
A mapping of the TxChange repr(C) Rust object.
The memory for this is managed by Swift.
*/
struct TxChange {
int64_t txid;
int64_t*_Nonnull* _Nonnull changes;
uint64_t len;
};
/*
A mapping of the TxChangeList repr(C) Rust object.
The memory for this is managed by Swift.
*/
struct TxChangeList {
struct TxChange*_Nonnull* _Nonnull reports;
uint64_t len;
};
typedef struct TxChangeList TxChangeList;
/*
A mapping of the ExternResult repr(C) Rust object.
The memory for this is managed by Swift.
*/
struct Result {
void* _Nullable ok;
char* _Nullable err;
};
typedef struct Result Result;
/*
A mapping of the ExternOption repr(C) Rust object.
The memory for this is managed by Swift.
*/
struct Option {
void* _Nullable value;
};
typedef struct Option Option;
/*
A Mapping for the ValueType Rust object.
*/
typedef NS_ENUM(NSInteger, ValueType) {
ValueTypeRef = 1,
ValueTypeBoolean,
ValueTypeInstant,
ValueTypeLong,
ValueTypeDouble,
ValueTypeString,
ValueTypeKeyword,
ValueTypeUuid
};
// Opaque Structs mapping to Rust types that are passed over the FFI boundary
struct EntityBuilder;
struct InProgress;
struct InProgressBuilder;
struct Query;
struct QueryResultRow;
struct QueryResultRows;
struct QueryRowsIterator;
struct QueryRowIterator;
struct Store;
struct TxReport;
struct TypedValue;
// Store
struct Store*_Nonnull store_open(const char*_Nonnull uri);
// Destructors.
void destroy(void* _Nullable obj);
void query_builder_destroy(struct Query* _Nullable obj);
void store_destroy(struct Store* _Nonnull obj);
void tx_report_destroy(struct TxReport* _Nonnull obj);
void typed_value_destroy(struct TypedValue* _Nullable obj);
void typed_value_list_destroy(struct QueryResultRow* _Nullable obj);
void typed_value_list_iter_destroy(struct QueryRowIterator* _Nullable obj);
void typed_value_result_set_destroy(struct QueryResultRows* _Nullable obj);
void typed_value_result_set_iter_destroy(struct QueryRowsIterator* _Nullable obj);
// transact
struct Result*_Nonnull store_transact(struct Store*_Nonnull store, const char* _Nonnull transaction);
const int64_t* _Nullable tx_report_entity_for_temp_id(const struct TxReport* _Nonnull report, const char* _Nonnull tempid);
int64_t tx_report_get_entid(const struct TxReport* _Nonnull report);
int64_t tx_report_get_tx_instant(const struct TxReport* _Nonnull report);
// Sync
struct Result*_Nonnull store_sync(struct Store*_Nonnull store, const char* _Nonnull user_uuid, const char* _Nonnull server_uri);
// Observers
void store_register_observer(struct Store*_Nonnull store, const char* _Nonnull key, const int64_t* _Nonnull attributes, const int64_t len, void (*_Nonnull callback_fn)(const char* _Nonnull key, const struct TxChangeList* _Nonnull reports));
void store_unregister_observer(struct Store*_Nonnull store, const char* _Nonnull key);
int64_t store_entid_for_attribute(struct Store*_Nonnull store, const char*_Nonnull attr);
int64_t changelist_entry_at(const struct TxChange* _Nonnull report, size_t index);
// Query
struct Query*_Nonnull store_query(struct Store*_Nonnull store, const char* _Nonnull query);
struct Result*_Nonnull store_value_for_attribute(struct Store*_Nonnull store, const int64_t entid, const char* _Nonnull attribute);
// Query Variable Binding
void query_builder_bind_long(struct Query*_Nonnull query, const char* _Nonnull var, const int64_t value);
void query_builder_bind_ref(struct Query*_Nonnull query, const char* _Nonnull var, const int64_t value);
void query_builder_bind_ref_kw(struct Query*_Nonnull query, const char* _Nonnull var, const char* _Nonnull value);
void query_builder_bind_kw(struct Query*_Nonnull query, const char* _Nonnull var, const char* _Nonnull value);
void query_builder_bind_boolean(struct Query*_Nonnull query, const char* _Nonnull var, const int32_t value);
void query_builder_bind_double(struct Query*_Nonnull query, const char* _Nonnull var, const double value);
void query_builder_bind_timestamp(struct Query*_Nonnull query, const char* _Nonnull var, const int64_t value);
void query_builder_bind_string(struct Query*_Nonnull query, const char* _Nonnull var, const char* _Nonnull value);
void query_builder_bind_uuid(struct Query*_Nonnull query, const char* _Nonnull var, const uuid_t* _Nonnull value);
// Query execution
struct Result*_Nonnull query_builder_execute(struct Query*_Nonnull query);
struct Result*_Nonnull query_builder_execute_scalar(struct Query*_Nonnull query);
struct Result*_Nonnull query_builder_execute_coll(struct Query*_Nonnull query);
struct Result*_Nonnull query_builder_execute_tuple(struct Query*_Nonnull query);
// Query Result Processing
int64_t typed_value_into_long(struct TypedValue*_Nonnull value);
int64_t typed_value_into_entid(struct TypedValue*_Nonnull value);
const char* _Nonnull typed_value_into_kw(struct TypedValue*_Nonnull value);
int32_t typed_value_into_boolean(struct TypedValue*_Nonnull value);
double typed_value_into_double(struct TypedValue*_Nonnull value);
int64_t typed_value_into_timestamp(struct TypedValue*_Nonnull value);
const char* _Nonnull typed_value_into_string(struct TypedValue*_Nonnull value);
const uuid_t* _Nonnull typed_value_into_uuid(struct TypedValue*_Nonnull value);
enum ValueType typed_value_value_type(struct TypedValue*_Nonnull value);
struct QueryResultRow* _Nullable row_at_index(struct QueryResultRows* _Nonnull rows, const int32_t index);
struct QueryRowsIterator* _Nonnull typed_value_result_set_into_iter(struct QueryResultRows* _Nonnull rows);
struct QueryResultRow* _Nullable typed_value_result_set_iter_next(struct QueryRowsIterator* _Nonnull iter);
struct QueryRowIterator* _Nonnull typed_value_list_into_iter(struct QueryResultRow* _Nonnull row);
struct TypedValue* _Nullable typed_value_list_iter_next(struct QueryRowIterator* _Nonnull iter);
struct TypedValue* _Nonnull value_at_index(struct QueryResultRow* _Nonnull row, const int32_t index);
int64_t value_at_index_into_long(struct QueryResultRow* _Nonnull row, const int32_t index);
int64_t value_at_index_into_entid(struct QueryResultRow* _Nonnull row, const int32_t index);
const char* _Nonnull value_at_index_into_kw(struct QueryResultRow* _Nonnull row, const int32_t index);
int32_t value_at_index_into_boolean(struct QueryResultRow* _Nonnull row, const int32_t index);
double value_at_index_into_double(struct QueryResultRow* _Nonnull row, const int32_t index);
int64_t value_at_index_into_timestamp(struct QueryResultRow* _Nonnull row, const int32_t index);
const char* _Nonnull value_at_index_into_string(struct QueryResultRow* _Nonnull row, const int32_t index);
const uuid_t* _Nonnull value_at_index_into_uuid(struct QueryResultRow* _Nonnull row, const int32_t index);
// Transaction change lists
const struct TxChange* _Nullable tx_change_list_entry_at(const struct TxChangeList* _Nonnull list, size_t index);
#endif /* store_h */

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>BNDL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
</dict>
</plist>

View file

@ -0,0 +1,765 @@
/* Copyright 2018 Mozilla
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of the
* License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License. */
import XCTest
@testable import Mentat
class MentatTests: XCTestCase {
var citiesSchema: String?
var seattleData: String?
var store: Mentat?
override func setUp() {
super.setUp()
// Put setup code here. This method is called before the invocation of each test method in the class.
}
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
super.tearDown()
}
// test that a store can be opened in memory
func testOpenInMemoryStore() {
XCTAssertNotNil(Mentat().raw)
}
// test that a store can be opened in a specific location
func testOpenStoreInLocation() {
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
let documentsURL = paths[0]
let storeURI = documentsURL.appendingPathComponent("test.db", isDirectory: false).absoluteString
XCTAssertNotNil(Mentat(storeURI: storeURI).raw)
}
func readFile(forResource resource: String, withExtension ext: String, subdirectory: String ) throws -> String {
let bundle = Bundle(for: type(of: self))
let schemaUrl = bundle.url(forResource: resource, withExtension: ext, subdirectory: subdirectory)!
let contents = try String(contentsOf: schemaUrl)
return contents
}
func readCitiesSchema() throws -> String {
guard let schema = self.citiesSchema else {
self.citiesSchema = try self.readFile(forResource: "cities", withExtension: "schema", subdirectory: "fixtures")
return self.citiesSchema!
}
return schema
}
func readSeattleData() throws -> String {
guard let data = self.seattleData else {
self.seattleData = try self.readFile(forResource: "all_seattle", withExtension: "edn", subdirectory: "fixtures")
return self.seattleData!
}
return data
}
func transactCitiesSchema(mentat: Mentat) throws -> TxReport {
let vocab = try readCitiesSchema()
let report = try mentat.transact(transaction: vocab)
return report
}
func transactSeattleData(mentat: Mentat) throws -> TxReport {
let data = try readSeattleData()
let report = try mentat.transact(transaction: data)
return report
}
func openAndInitializeCitiesStore() -> Mentat {
guard let mentat = self.store else {
let mentat = Mentat()
let _ = try! self.transactCitiesSchema(mentat: mentat)
let _ = try! self.transactSeattleData(mentat: mentat)
self.store = mentat
return mentat
}
return mentat
}
func populateWithTypesSchema(mentat: Mentat) -> (TxReport?, TxReport?) {
do {
let schema = """
[
[:db/add "b" :db/ident :foo/boolean]
[:db/add "b" :db/valueType :db.type/boolean]
[:db/add "b" :db/cardinality :db.cardinality/one]
[:db/add "l" :db/ident :foo/long]
[:db/add "l" :db/valueType :db.type/long]
[:db/add "l" :db/cardinality :db.cardinality/one]
[:db/add "r" :db/ident :foo/ref]
[:db/add "r" :db/valueType :db.type/ref]
[:db/add "r" :db/cardinality :db.cardinality/one]
[:db/add "i" :db/ident :foo/instant]
[:db/add "i" :db/valueType :db.type/instant]
[:db/add "i" :db/cardinality :db.cardinality/one]
[:db/add "d" :db/ident :foo/double]
[:db/add "d" :db/valueType :db.type/double]
[:db/add "d" :db/cardinality :db.cardinality/one]
[:db/add "s" :db/ident :foo/string]
[:db/add "s" :db/valueType :db.type/string]
[:db/add "s" :db/cardinality :db.cardinality/one]
[:db/add "k" :db/ident :foo/keyword]
[:db/add "k" :db/valueType :db.type/keyword]
[:db/add "k" :db/cardinality :db.cardinality/one]
[:db/add "u" :db/ident :foo/uuid]
[:db/add "u" :db/valueType :db.type/uuid]
[:db/add "u" :db/cardinality :db.cardinality/one]
]
"""
let report = try mentat.transact(transaction: schema)
let stringEntid = report.entid(forTempId: "s")!
let data = """
[
[:db/add "a" :foo/boolean true]
[:db/add "a" :foo/long 25]
[:db/add "a" :foo/instant #inst "2017-01-01T11:00:00.000Z"]
[:db/add "a" :foo/double 11.23]
[:db/add "a" :foo/string "The higher we soar the smaller we appear to those who cannot fly."]
[:db/add "a" :foo/keyword :foo/string]
[:db/add "a" :foo/uuid #uuid "550e8400-e29b-41d4-a716-446655440000"]
[:db/add "b" :foo/boolean false]
[:db/add "b" :foo/ref \(stringEntid)]
[:db/add "b" :foo/keyword :foo/string]
[:db/add "b" :foo/long 50]
[:db/add "b" :foo/instant #inst "2018-01-01T11:00:00.000Z"]
[:db/add "b" :foo/double 22.46]
[:db/add "b" :foo/string "Silence is worse; all truths that are kept silent become poisonous."]
[:db/add "b" :foo/uuid #uuid "4cb3f828-752d-497a-90c9-b1fd516d5644"]
]
"""
let dataReport = try mentat.transact(transaction: data)
return (report, dataReport)
} catch {
assertionFailure(error.localizedDescription)
}
return (nil, nil)
}
func test1TransactVocabulary() {
do {
let mentat = Mentat()
let vocab = try readCitiesSchema()
let report = try mentat.transact(transaction: vocab)
XCTAssertNotNil(report)
assert(report.txId > 0)
} catch {
assertionFailure(error.localizedDescription)
}
}
func test2TransactEntities() {
do {
let mentat = Mentat()
let vocab = try readCitiesSchema()
let _ = try mentat.transact(transaction: vocab)
let data = try readSeattleData()
let report = try mentat.transact(transaction: data)
XCTAssertNotNil(report)
assert(report.txId > 0)
let entid = report.entid(forTempId: "a17592186045438")
assert(entid == 65566)
} catch {
assertionFailure(error.localizedDescription)
}
}
func testQueryScalar() {
let mentat = openAndInitializeCitiesStore()
let query = "[:find ?n . :in ?name :where [(fulltext $ :community/name ?name) [[?e ?n]]]]"
let expect = expectation(description: "Query is executed")
XCTAssertNoThrow(try mentat.query(query: query).bind(varName: "?name", toString: "Wallingford").runScalar(callback: { scalarResult in
guard let result = scalarResult?.asString() else {
return assertionFailure("No String value received")
}
assert(result == "KOMO Communities - Wallingford")
expect.fulfill()
}))
waitForExpectations(timeout: 1) { error in
if let error = error {
assertionFailure("waitForExpectationsWithTimeout errored: \(error)")
}
}
}
func testQueryColl() {
let mentat = openAndInitializeCitiesStore()
let query = "[:find [?when ...] :where [_ :db/txInstant ?when] :order (asc ?when)]"
let expect = expectation(description: "Query is executed")
XCTAssertNoThrow(try mentat.query(query: query).runColl(callback: { collResult in
guard let rows = collResult else {
return assertionFailure("No results received")
}
// we are expecting 3 results
for i in 0..<3 {
let _ = rows.asDate(index: i)
assert(true)
}
expect.fulfill()
}))
waitForExpectations(timeout: 1) { error in
if let error = error {
assertionFailure("waitForExpectationsWithTimeout errored: \(error)")
}
}
}
func testQueryCollResultIterator() {
let mentat = openAndInitializeCitiesStore()
let query = "[:find [?when ...] :where [_ :db/txInstant ?when] :order (asc ?when)]"
let expect = expectation(description: "Query is executed")
XCTAssertNoThrow(try mentat.query(query: query).runColl(callback: { collResult in
guard let rows = collResult else {
return assertionFailure("No results received")
}
rows.forEach({ (value) in
assert(value.valueType.rawValue == 2)
})
expect.fulfill()
}))
waitForExpectations(timeout: 1) { error in
if let error = error {
assertionFailure("waitForExpectationsWithTimeout errored: \(error)")
}
}
}
func testQueryTuple() {
let mentat = openAndInitializeCitiesStore()
let query = """
[:find [?name ?cat]
:where
[?c :community/name ?name]
[?c :community/type :community.type/website]
[(fulltext $ :community/category "food") [[?c ?cat]]]]
"""
let expect = expectation(description: "Query is executed")
XCTAssertNoThrow(try mentat.query(query: query).runTuple(callback: { tupleResult in
guard let tuple = tupleResult else {
return assertionFailure("expecting a result")
}
let name = tuple.asString(index: 0)
let category = tuple.asString(index: 1)
assert(name == "Community Harvest of Southwest Seattle")
assert(category == "sustainable food")
expect.fulfill()
}))
waitForExpectations(timeout: 1) { error in
if let error = error {
assertionFailure("waitForExpectationsWithTimeout errored: \(error)")
}
}
}
func testQueryRel() {
let mentat = openAndInitializeCitiesStore()
let query = """
[:find ?name ?cat
:where
[?c :community/name ?name]
[?c :community/type :community.type/website]
[(fulltext $ :community/category "food") [[?c ?cat]]]]
"""
let expect = expectation(description: "Query is executed")
let expectedResults = [("InBallard", "food"),
("Seattle Chinatown Guide", "food"),
("Community Harvest of Southwest Seattle", "sustainable food"),
("University District Food Bank", "food bank")]
XCTAssertNoThrow(try mentat.query(query: query).run(callback: { relResult in
guard let rows = relResult else {
return assertionFailure("No results received")
}
for (i, row) in rows.enumerated() {
let (name, category) = expectedResults[i]
assert( row.asString(index: 0) == name)
assert(row.asString(index: 1) == category)
}
expect.fulfill()
}))
waitForExpectations(timeout: 1) { error in
if let error = error {
assertionFailure("waitForExpectationsWithTimeout errored: \(error)")
}
}
}
func testQueryRelResultIterator() {
let mentat = openAndInitializeCitiesStore()
let query = """
[:find ?name ?cat
:where
[?c :community/name ?name]
[?c :community/type :community.type/website]
[(fulltext $ :community/category "food") [[?c ?cat]]]]
"""
let expect = expectation(description: "Query is executed")
let expectedResults = [("InBallard", "food"),
("Seattle Chinatown Guide", "food"),
("Community Harvest of Southwest Seattle", "sustainable food"),
("University District Food Bank", "food bank")]
XCTAssertNoThrow(try mentat.query(query: query).run(callback: { relResult in
guard let rows = relResult else {
return assertionFailure("No results received")
}
var i = 0
rows.forEach({ (row) in
let (name, category) = expectedResults[i]
i += 1
assert(row.asString(index: 0) == name)
assert(row.asString(index: 1) == category)
})
assert(i == 4)
expect.fulfill()
}))
waitForExpectations(timeout: 1) { error in
if let error = error {
assertionFailure("waitForExpectationsWithTimeout errored: \(error)")
}
}
}
func testBindLong() {
let mentat = Mentat()
let (_, report) = self.populateWithTypesSchema(mentat: mentat)
let aEntid = report!.entid(forTempId: "a")
let query = "[:find ?e . :in ?long :where [?e :foo/long ?long]]"
let expect = expectation(description: "Query is executed")
XCTAssertNoThrow(try mentat.query(query: query)
.bind(varName: "?long", toLong: 25)
.runScalar { value in
XCTAssertNotNil(value)
assert(value?.asEntid() == aEntid)
expect.fulfill()
})
waitForExpectations(timeout: 1) { error in
if let error = error {
assertionFailure("waitForExpectationsWithTimeout errored: \(error)")
}
}
}
func testBindRef() {
let mentat = Mentat()
let (_, report) = self.populateWithTypesSchema(mentat: mentat)
let stringEntid = mentat.entidForAttribute(attribute: ":foo/string")
let bEntid = report!.entid(forTempId: "b")
let query = "[:find ?e . :in ?ref :where [?e :foo/ref ?ref]]"
let expect = expectation(description: "Query is executed")
XCTAssertNoThrow(try mentat.query(query: query)
.bind(varName: "?ref", toReference: stringEntid)
.runScalar { value in
XCTAssertNotNil(value)
assert(value?.asEntid() == bEntid)
expect.fulfill()
})
waitForExpectations(timeout: 1) { error in
if let error = error {
assertionFailure("waitForExpectationsWithTimeout errored: \(error)")
}
}
}
func testBindKwRef() {
let mentat = Mentat()
let (_, report) = self.populateWithTypesSchema(mentat: mentat)
let bEntid = report!.entid(forTempId: "b")
let query = "[:find ?e . :in ?ref :where [?e :foo/ref ?ref]]"
let expect = expectation(description: "Query is executed")
XCTAssertNoThrow(try mentat.query(query: query)
.bind(varName: "?ref", toReference: ":foo/string")
.runScalar { value in
XCTAssertNotNil(value)
assert(value?.asEntid() == bEntid)
expect.fulfill()
})
waitForExpectations(timeout: 1) { error in
if let error = error {
assertionFailure("waitForExpectationsWithTimeout errored: \(error)")
}
}
}
func testBindKw() {
let mentat = Mentat()
let (_, report) = self.populateWithTypesSchema(mentat: mentat)
let aEntid = report!.entid(forTempId: "a")
let query = "[:find ?e . :in ?kw :where [?e :foo/keyword ?kw]]"
let expect = expectation(description: "Query is executed")
XCTAssertNoThrow(try mentat.query(query: query)
.bind(varName: "?kw", toKeyword: ":foo/string")
.runScalar { value in
XCTAssertNotNil(value)
assert(value?.asEntid() == aEntid)
expect.fulfill()
})
waitForExpectations(timeout: 1) { error in
if let error = error {
assertionFailure("waitForExpectationsWithTimeout errored: \(error)")
}
}
}
func testBindDate() {
let mentat = Mentat()
let (_, report) = self.populateWithTypesSchema(mentat: mentat)
let aEntid = report!.entid(forTempId: "a")
let query = "[:find [?e ?d] :in ?now :where [?e :foo/instant ?d] [(< ?d ?now)]]"
let expect = expectation(description: "Query is executed")
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ"
let boundDate = formatter.date(from: "2018-04-16T16:39:18+00:00")!
XCTAssertNoThrow(try mentat.query(query: query)
.bind(varName: "?now", toDate: boundDate)
.runTuple { row in
XCTAssertNotNil(row)
assert(row?.asEntid(index: 0) == aEntid)
expect.fulfill()
})
waitForExpectations(timeout: 1) { error in
if let error = error {
assertionFailure("waitForExpectationsWithTimeout errored: \(error)")
}
}
}
func testBindString() {
let mentat = openAndInitializeCitiesStore()
let query = "[:find ?n . :in ?name :where [(fulltext $ :community/name ?name) [[?e ?n]]]]"
let expect = expectation(description: "Query is executed")
XCTAssertNoThrow(try mentat.query(query: query)
.bind(varName: "?name", toString: "Wallingford")
.runScalar(callback: { scalarResult in
guard let result = scalarResult?.asString() else {
return assertionFailure("No String value received")
}
assert(result == "KOMO Communities - Wallingford")
expect.fulfill()
}))
waitForExpectations(timeout: 1) { error in
if let error = error {
assertionFailure("waitForExpectationsWithTimeout errored: \(error)")
}
}
}
func testBindUuid() {
let mentat = Mentat()
let (_, report) = self.populateWithTypesSchema(mentat: mentat)
let aEntid = report!.entid(forTempId: "a")
let query = "[:find ?e . :in ?uuid :where [?e :foo/uuid ?uuid]]"
let uuid = UUID(uuidString: "550e8400-e29b-41d4-a716-446655440000")!
let expect = expectation(description: "Query is rund")
XCTAssertNoThrow(try mentat.query(query: query)
.bind(varName: "?uuid", toUuid: uuid)
.runScalar { value in
XCTAssertNotNil(value)
assert(value?.asEntid() == aEntid)
expect.fulfill()
})
waitForExpectations(timeout: 1) { error in
if let error = error {
assertionFailure("waitForExpectationsWithTimeout errored: \(error)")
}
}
}
func testBindBoolean() {
let mentat = Mentat()
let (_, report) = self.populateWithTypesSchema(mentat: mentat)
let aEntid = report!.entid(forTempId: "a")
let query = "[:find ?e . :in ?bool :where [?e :foo/boolean ?bool]]"
let expect = expectation(description: "Query is executed")
XCTAssertNoThrow(try mentat.query(query: query)
.bind(varName: "?bool", toBoolean: true)
.runScalar { value in
XCTAssertNotNil(value)
assert(value?.asEntid() == aEntid)
expect.fulfill()
})
waitForExpectations(timeout: 1) { error in
if let error = error {
assertionFailure("waitForExpectationsWithTimeout errored: \(error)")
}
}
}
func testBindDouble() {
let mentat = Mentat()
let (_, report) = self.populateWithTypesSchema(mentat: mentat)
let aEntid = report!.entid(forTempId: "a")
let query = "[:find ?e . :in ?double :where [?e :foo/double ?double]]"
let expect = expectation(description: "Query is executed")
XCTAssertNoThrow(try mentat.query(query: query)
.bind(varName: "?double", toDouble: 11.23)
.runScalar { value in
XCTAssertNotNil(value)
assert(value?.asEntid() == aEntid)
expect.fulfill()
})
waitForExpectations(timeout: 1) { error in
if let error = error {
assertionFailure("waitForExpectationsWithTimeout errored: \(error)")
}
}
}
func testTypedValueAsLong() {
let mentat = Mentat()
let (_, report) = self.populateWithTypesSchema(mentat: mentat)
let aEntid = report!.entid(forTempId: "a")!
let query = "[:find ?v . :in ?e :where [?e :foo/long ?v]]"
let expect = expectation(description: "Query is executed")
XCTAssertNoThrow(try mentat.query(query: query)
.bind(varName: "?e", toReference: aEntid)
.runScalar { value in
XCTAssertNotNil(value)
assert(value?.asLong() == 25)
assert(value?.asLong() == 25)
expect.fulfill()
})
waitForExpectations(timeout: 1) { error in
if let error = error {
assertionFailure("waitForExpectationsWithTimeout errored: \(error)")
}
}
}
func testTypedValueAsRef() {
let mentat = Mentat()
let (_, report) = self.populateWithTypesSchema(mentat: mentat)
let aEntid = report!.entid(forTempId: "a")!
let query = "[:find ?e . :where [?e :foo/long 25]]"
let expect = expectation(description: "Query is executed")
XCTAssertNoThrow(try mentat.query(query: query)
.runScalar { value in
XCTAssertNotNil(value)
assert(value?.asEntid() == aEntid)
assert(value?.asEntid() == aEntid)
expect.fulfill()
})
waitForExpectations(timeout: 1) { error in
if let error = error {
assertionFailure("waitForExpectationsWithTimeout errored: \(error)")
}
}
}
func testTypedValueAsKw() {
let mentat = Mentat()
let (_, report) = self.populateWithTypesSchema(mentat: mentat)
let aEntid = report!.entid(forTempId: "a")!
let query = "[:find ?v . :in ?e :where [?e :foo/keyword ?v]]"
let expect = expectation(description: "Query is executed")
XCTAssertNoThrow(try mentat.query(query: query)
.bind(varName: "?e", toReference: aEntid)
.runScalar { value in
XCTAssertNotNil(value)
assert(value?.asKeyword() == ":foo/string")
assert(value?.asKeyword() == ":foo/string")
expect.fulfill()
})
waitForExpectations(timeout: 1) { error in
if let error = error {
assertionFailure("waitForExpectationsWithTimeout errored: \(error)")
}
}
}
func testTypedValueAsBoolean() {
let mentat = Mentat()
let (_, report) = self.populateWithTypesSchema(mentat: mentat)
let aEntid = report!.entid(forTempId: "a")!
let query = "[:find ?v . :in ?e :where [?e :foo/boolean ?v]]"
let expect = expectation(description: "Query is executed")
XCTAssertNoThrow(try mentat.query(query: query)
.bind(varName: "?e", toReference: aEntid)
.runScalar { value in
XCTAssertNotNil(value)
assert(value?.asBool() == true)
assert(value?.asBool() == true)
expect.fulfill()
})
waitForExpectations(timeout: 1) { error in
if let error = error {
assertionFailure("waitForExpectationsWithTimeout errored: \(error)")
}
}
}
func testTypedValueAsDouble() {
let mentat = Mentat()
let (_, report) = self.populateWithTypesSchema(mentat: mentat)
let aEntid = report!.entid(forTempId: "a")!
let query = "[:find ?v . :in ?e :where [?e :foo/double ?v]]"
let expect = expectation(description: "Query is executed")
XCTAssertNoThrow(try mentat.query(query: query)
.bind(varName: "?e", toReference: aEntid)
.runScalar { value in
XCTAssertNotNil(value)
assert(value?.asDouble() == 11.23)
assert(value?.asDouble() == 11.23)
expect.fulfill()
})
waitForExpectations(timeout: 1) { error in
if let error = error {
assertionFailure("waitForExpectationsWithTimeout errored: \(error)")
}
}
}
func testTypedValueAsDate() {
let mentat = Mentat()
let (_, report) = self.populateWithTypesSchema(mentat: mentat)
let aEntid = report!.entid(forTempId: "a")!
let query = "[:find ?v . :in ?e :where [?e :foo/instant ?v]]"
let expect = expectation(description: "Query is executed")
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ"
let expectedDate = formatter.date(from: "2017-01-01T11:00:00+00:00")
XCTAssertNoThrow(try mentat.query(query: query)
.bind(varName: "?e", toReference: aEntid)
.runScalar { value in
XCTAssertNotNil(value)
assert(value?.asDate() == expectedDate)
assert(value?.asDate() == expectedDate)
expect.fulfill()
})
waitForExpectations(timeout: 1) { error in
if let error = error {
assertionFailure("waitForExpectationsWithTimeout errored: \(error)")
}
}
}
func testTypedValueAsString() {
let mentat = Mentat()
let (_, report) = self.populateWithTypesSchema(mentat: mentat)
let aEntid = report!.entid(forTempId: "a")!
let query = "[:find ?v . :in ?e :where [?e :foo/string ?v]]"
let expect = expectation(description: "Query is executed")
XCTAssertNoThrow(try mentat.query(query: query)
.bind(varName: "?e", toReference: aEntid)
.runScalar { value in
XCTAssertNotNil(value)
assert(value?.asString() == "The higher we soar the smaller we appear to those who cannot fly.")
assert(value?.asString() == "The higher we soar the smaller we appear to those who cannot fly.")
expect.fulfill()
})
waitForExpectations(timeout: 1) { error in
if let error = error {
assertionFailure("waitForExpectationsWithTimeout errored: \(error)")
}
}
}
func testTypedValueAsUuid() {
let mentat = Mentat()
let (_, report) = self.populateWithTypesSchema(mentat: mentat)
let aEntid = report!.entid(forTempId: "a")!
let query = "[:find ?v . :in ?e :where [?e :foo/uuid ?v]]"
let expectedUuid = UUID(uuidString: "550e8400-e29b-41d4-a716-446655440000")!
let expect = expectation(description: "Query is executed")
XCTAssertNoThrow(try mentat.query(query: query)
.bind(varName: "?e", toReference: aEntid)
.runScalar { value in
XCTAssertNotNil(value)
assert(value?.asUUID() == expectedUuid)
assert(value?.asUUID() == expectedUuid)
expect.fulfill()
})
waitForExpectations(timeout: 1) { error in
if let error = error {
assertionFailure("waitForExpectationsWithTimeout errored: \(error)")
}
}
}
func testValueForAttributeOfEntity() {
let mentat = Mentat()
let (_, report) = self.populateWithTypesSchema(mentat: mentat)
let aEntid = report!.entid(forTempId: "a")!
var value: TypedValue? = nil;
XCTAssertNoThrow(value = try mentat.value(forAttribute: ":foo/long", ofEntity: aEntid))
XCTAssertNotNil(value)
assert(value?.asLong() == 25)
}
func testEntidForAttribute() {
let mentat = Mentat()
let _ = self.populateWithTypesSchema(mentat: mentat)
let entid = mentat.entidForAttribute(attribute: ":foo/long")
assert(entid == 65540)
}
func testMultipleQueries() {
let mentat = Mentat()
let _ = self.populateWithTypesSchema(mentat: mentat)
let q1 = mentat.query(query: "[:find ?x :where [?x _ _]]")
let q1Expect = expectation(description: "Query 1 is executed")
XCTAssertNoThrow(try q1.run { results in
XCTAssertNotNil(results)
q1Expect.fulfill()
})
let q2 = mentat.query(query: "[:find ?x :where [_ _ ?x]]")
let q2Expect = expectation(description: "Query 2 is executed")
XCTAssertNoThrow(try q2.run { results in
XCTAssertNotNil(results)
q2Expect.fulfill()
})
waitForExpectations(timeout: 1) { error in
if let error = error {
assertionFailure("waitForExpectationsWithTimeout errored: \(error)")
}
}
}
func testNestedQueries() {
let mentat = Mentat()
let _ = self.populateWithTypesSchema(mentat: mentat)
let q1 = mentat.query(query: "[:find ?x :where [?x _ _]]")
let q2 = mentat.query(query: "[:find ?x :where [_ _ ?x]]")
let expect = expectation(description: "Query 1 is executed")
XCTAssertNoThrow(try q1.run { results in
XCTAssertNotNil(results)
try? q2.run { results in
XCTAssertNotNil(results)
expect.fulfill()
}
})
waitForExpectations(timeout: 1) { error in
if let error = error {
assertionFailure("waitForExpectationsWithTimeout errored: \(error)")
}
}
}
// TODO: Add tests for transaction observation
}

View file

@ -94,7 +94,6 @@ use mentat_tolstoy::Syncer;
use uuid::Uuid;
use entity_builder::{
BuildTerms,
InProgressBuilder,
TermBuilder,
};
@ -647,10 +646,6 @@ impl Store {
pub fn unregister_observer(&mut self, key: &String) {
self.conn.unregister_observer(key);
}
pub fn assert_datom<T>(&mut self, entid: T, attribute: Keyword, value: TypedValue) -> Result<()> where T: Into<KnownEntid> {
self.conn.assert_datom(&mut self.sqlite, entid, attribute, value)
}
}
impl Queryable for Store {
@ -973,18 +968,6 @@ impl Conn {
pub fn unregister_observer(&mut self, key: &String) {
self.tx_observer_service.lock().unwrap().deregister(key);
}
// TODO: expose the entity builder over FFI and remove the need for this function entirely
// It's really only here in order to keep the FFI layer as thin as possible.
// Once the entity builder is exposed, we can perform all of these functions over FFI from the client.
pub fn assert_datom<T>(&mut self, sqlite: &mut rusqlite::Connection, entid: T, attribute: Keyword, value: TypedValue) -> Result<()> where T: Into<KnownEntid> {
let in_progress = self.begin_transaction(sqlite)?;
let mut builder = in_progress.builder().describe(entid.into());
builder.add_kw(&attribute, value)?;
builder.commit()
.map_err(|e| e.into())
.and(Ok(()))
}
}
#[cfg(test)]

View file

@ -55,6 +55,7 @@ pub use mentat_query::{
pub use mentat_db::{
CORE_SCHEMA_VERSION,
DB_SCHEMA_CORE,
AttributeSet,
TxObserver,
TxReport,
new_connection,

View file

@ -14,10 +14,12 @@ use std::collections::{
};
use mentat_core::{
DateTime,
Entid,
Keyword,
Binding,
TypedValue,
Utc,
ValueType,
};
@ -37,15 +39,15 @@ use errors::{
};
pub struct QueryBuilder<'a> {
sql: String,
query: String,
values: BTreeMap<Variable, TypedValue>,
types: BTreeMap<Variable, ValueType>,
store: &'a mut Store,
}
impl<'a> QueryBuilder<'a> {
pub fn new<T>(store: &'a mut Store, sql: T) -> QueryBuilder where T: Into<String> {
QueryBuilder { sql: sql.into(), values: BTreeMap::new(), types: BTreeMap::new(), store }
pub fn new<T>(store: &'a mut Store, query: T) -> QueryBuilder where T: Into<String> {
QueryBuilder { query: query.into(), values: BTreeMap::new(), types: BTreeMap::new(), store }
}
pub fn bind_value<T>(&mut self, var: &str, value: T) -> &mut Self where T: Into<TypedValue> {
@ -71,6 +73,12 @@ impl<'a> QueryBuilder<'a> {
pub fn bind_instant(&mut self, var: &str, value: i64) -> &mut Self {
self.values.insert(Variable::from_valid_name(var), TypedValue::instant(value));
self
}
pub fn bind_date_time(&mut self, var: &str, value: DateTime<Utc>) -> &mut Self {
self.values.insert(Variable::from_valid_name(var), TypedValue::Instant(value));
self
}
@ -84,7 +92,7 @@ impl<'a> QueryBuilder<'a> {
let types = ::std::mem::replace(&mut self.types, Default::default());
let query_inputs = QueryInputs::new(types, values)?;
let read = self.store.begin_read()?;
read.q_once(&self.sql, query_inputs)
read.q_once(&self.query, query_inputs)
}
pub fn execute_scalar(&mut self) -> Result<Option<Binding>> {
@ -116,6 +124,11 @@ mod test {
Store,
};
use mentat_core::{
DateTime,
Utc,
};
#[test]
fn test_scalar_query() {
let mut store = Store::open("").expect("store connection");