]> git.ipfire.org Git - thirdparty/sqlite.git/commitdiff
Add high-level window function wrapper to the JNI wrapper1 interface.
authorstephan <stephan@noemail.net>
Sun, 22 Oct 2023 23:36:16 +0000 (23:36 +0000)
committerstephan <stephan@noemail.net>
Sun, 22 Oct 2023 23:36:16 +0000 (23:36 +0000)
FossilOrigin-Name: a27e7471231a24864cbd04b77cbc4b336ce180d738a36ce4318543e2666ed708

ext/jni/GNUmakefile
ext/jni/src/org/sqlite/jni/capi/AggregateFunction.java
ext/jni/src/org/sqlite/jni/capi/SQLFunction.java
ext/jni/src/org/sqlite/jni/capi/Tester1.java
ext/jni/src/org/sqlite/jni/wrapper1/AggregateFunction.java
ext/jni/src/org/sqlite/jni/wrapper1/SqlFunction.java
ext/jni/src/org/sqlite/jni/wrapper1/Sqlite.java
ext/jni/src/org/sqlite/jni/wrapper1/Tester2.java
ext/jni/src/org/sqlite/jni/wrapper1/WindowFunction.java [new file with mode: 0644]
manifest
manifest.uuid

index 19a5080471ffb1255c16fb27f94f533b63d2c3d8..155e4e7f632a4e644d77a39bb98b5e3f19085b49 100644 (file)
@@ -120,6 +120,7 @@ JAVA_FILES.main := $(patsubst %,$(dir.src.jni)/annotation/%,\
   Sqlite.java \
   SqliteException.java \
   ValueHolder.java \
+  WindowFunction.java \
 )
 
 JAVA_FILES.unittest := $(patsubst %,$(dir.src.jni)/%,\
index 89c4f274211c71c56cc47a62f7a50b1f5fe3bc96..1fa6c6b8051d15b863db0fb751bb7bed65c856e0 100644 (file)
@@ -42,9 +42,75 @@ public abstract class AggregateFunction<T> implements SQLFunction {
   */
   public void xDestroy() {}
 
+  /**
+     PerContextState assists aggregate and window functions in
+     managing their accumulator state across calls to the UDF's
+     callbacks.
+
+     <p>T must be of a type which can be legally stored as a value in
+     java.util.HashMap<KeyType,T>.
+
+     <p>If a given aggregate or window function is called multiple times
+     in a single SQL statement, e.g. SELECT MYFUNC(A), MYFUNC(B)...,
+     then the clients need some way of knowing which call is which so
+     that they can map their state between their various UDF callbacks
+     and reset it via xFinal(). This class takes care of such
+     mappings.
+
+     <p>This class works by mapping
+     sqlite3_context.getAggregateContext() to a single piece of
+     state, of a client-defined type (the T part of this class), which
+     persists across a "matching set" of the UDF's callbacks.
+
+     <p>This class is a helper providing commonly-needed functionality
+     - it is not required for use with aggregate or window functions.
+     Client UDFs are free to perform such mappings using custom
+     approaches. The provided {@link AggregateFunction} and {@link
+     WindowFunction} classes use this.
+  */
+  public static final class PerContextState<T> {
+    private final java.util.Map<Long,ValueHolder<T>> map
+      = new java.util.HashMap<>();
+
+    /**
+       Should be called from a UDF's xStep(), xValue(), and xInverse()
+       methods, passing it that method's first argument and an initial
+       value for the persistent state. If there is currently no
+       mapping for the given context within the map, one is created
+       using the given initial value, else the existing one is used
+       and the 2nd argument is ignored.  It returns a ValueHolder<T>
+       which can be used to modify that state directly without
+       requiring that the client update the underlying map's entry.
+
+       <p>The caller is obligated to eventually call
+       takeAggregateState() to clear the mapping.
+    */
+    public ValueHolder<T> getAggregateState(sqlite3_context cx, T initialValue){
+      final Long key = cx.getAggregateContext(true);
+      ValueHolder<T> rc = null==key ? null : map.get(key);
+      if( null==rc ){
+        map.put(key, rc = new ValueHolder<>(initialValue));
+      }
+      return rc;
+    }
+
+    /**
+       Should be called from a UDF's xFinal() method and passed that
+       method's first argument. This function removes the value
+       associated with cx.getAggregateContext() from the map and
+       returns it, returning null if no other UDF method has been
+       called to set up such a mapping. The latter condition will be
+       the case if a UDF is used in a statement which has no result
+       rows.
+    */
+    public T takeAggregateState(sqlite3_context cx){
+      final ValueHolder<T> h = map.remove(cx.getAggregateContext(false));
+      return null==h ? null : h.value;
+    }
+  }
+
   /** Per-invocation state for the UDF. */
-  private final SQLFunction.PerContextState<T> map =
-    new SQLFunction.PerContextState<>();
+  private final PerContextState<T> map = new PerContextState<>();
 
   /**
      To be called from the implementation's xStep() method, as well
index 4806e2fc0c691a4f2029624ed41d1e54e7313ad8..7ad1381a7ada40b6d85e7dfa2b695412fdfc6ca8 100644 (file)
@@ -33,71 +33,4 @@ package org.sqlite.jni.capi;
 */
 public interface SQLFunction {
 
-  /**
-     PerContextState assists aggregate and window functions in
-     managing their accumulator state across calls to the UDF's
-     callbacks.
-
-     <p>T must be of a type which can be legally stored as a value in
-     java.util.HashMap<KeyType,T>.
-
-     <p>If a given aggregate or window function is called multiple times
-     in a single SQL statement, e.g. SELECT MYFUNC(A), MYFUNC(B)...,
-     then the clients need some way of knowing which call is which so
-     that they can map their state between their various UDF callbacks
-     and reset it via xFinal(). This class takes care of such
-     mappings.
-
-     <p>This class works by mapping
-     sqlite3_context.getAggregateContext() to a single piece of
-     state, of a client-defined type (the T part of this class), which
-     persists across a "matching set" of the UDF's callbacks.
-
-     <p>This class is a helper providing commonly-needed functionality
-     - it is not required for use with aggregate or window functions.
-     Client UDFs are free to perform such mappings using custom
-     approaches. The provided {@link AggregateFunction} and {@link
-     WindowFunction} classes use this.
-  */
-  public static final class PerContextState<T> {
-    private final java.util.Map<Long,ValueHolder<T>> map
-      = new java.util.HashMap<>();
-
-    /**
-       Should be called from a UDF's xStep(), xValue(), and xInverse()
-       methods, passing it that method's first argument and an initial
-       value for the persistent state. If there is currently no
-       mapping for the given context within the map, one is created
-       using the given initial value, else the existing one is used
-       and the 2nd argument is ignored.  It returns a ValueHolder<T>
-       which can be used to modify that state directly without
-       requiring that the client update the underlying map's entry.
-
-       <p>The caller is obligated to eventually call
-       takeAggregateState() to clear the mapping.
-    */
-    public ValueHolder<T> getAggregateState(sqlite3_context cx, T initialValue){
-      final Long key = cx.getAggregateContext(true);
-      ValueHolder<T> rc = null==key ? null : map.get(key);
-      if( null==rc ){
-        map.put(key, rc = new ValueHolder<>(initialValue));
-      }
-      return rc;
-    }
-
-    /**
-       Should be called from a UDF's xFinal() method and passed that
-       method's first argument. This function removes the value
-       associated with cx.getAggregateContext() from the map and
-       returns it, returning null if no other UDF method has been
-       called to set up such a mapping. The latter condition will be
-       the case if a UDF is used in a statement which has no result
-       rows.
-    */
-    public T takeAggregateState(sqlite3_context cx){
-      final ValueHolder<T> h = map.remove(cx.getAggregateContext(false));
-      return null==h ? null : h.value;
-    }
-  }
-
 }
index b1d8df11cd2c2b8f6cd633d2f6485e1f30a985ef..46571856582c8a266c12885c4a0cd3c590833c60 100644 (file)
@@ -929,7 +929,6 @@ public class Tester1 implements Runnable {
                          "ORDER BY x ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING"+
                          ") AS sum_y "+
                          "FROM twin ORDER BY x;");
-    affirm( 0 == rc );
     int n = 0;
     while( SQLITE_ROW == sqlite3_step(stmt) ){
       final String s = sqlite3_column_text16(stmt, 0);
index 173d775e62d39fe22fceecc9445c1fe13d938cf3..6a38d4b530e02b0ba3979f2aaeeef9d596ac2ace 100644 (file)
@@ -51,9 +51,75 @@ public abstract class AggregateFunction<T> implements SqlFunction  {
   */
   public void xDestroy() {}
 
+  /**
+     PerContextState assists aggregate and window functions in
+     managing their accumulator state across calls to the UDF's
+     callbacks.
+
+     <p>T must be of a type which can be legally stored as a value in
+     java.util.HashMap<KeyType,T>.
+
+     <p>If a given aggregate or window function is called multiple times
+     in a single SQL statement, e.g. SELECT MYFUNC(A), MYFUNC(B)...,
+     then the clients need some way of knowing which call is which so
+     that they can map their state between their various UDF callbacks
+     and reset it via xFinal(). This class takes care of such
+     mappings.
+
+     <p>This class works by mapping
+     sqlite3_context.getAggregateContext() to a single piece of
+     state, of a client-defined type (the T part of this class), which
+     persists across a "matching set" of the UDF's callbacks.
+
+     <p>This class is a helper providing commonly-needed functionality
+     - it is not required for use with aggregate or window functions.
+     Client UDFs are free to perform such mappings using custom
+     approaches. The provided {@link AggregateFunction} and {@link
+     WindowFunction} classes use this.
+  */
+  public static final class PerContextState<T> {
+    private final java.util.Map<Long,ValueHolder<T>> map
+      = new java.util.HashMap<>();
+
+    /**
+       Should be called from a UDF's xStep(), xValue(), and xInverse()
+       methods, passing it that method's first argument and an initial
+       value for the persistent state. If there is currently no
+       mapping for the given context within the map, one is created
+       using the given initial value, else the existing one is used
+       and the 2nd argument is ignored.  It returns a ValueHolder<T>
+       which can be used to modify that state directly without
+       requiring that the client update the underlying map's entry.
+
+       <p>The caller is obligated to eventually call
+       takeAggregateState() to clear the mapping.
+    */
+    public ValueHolder<T> getAggregateState(SqlFunction.Arguments args, T initialValue){
+      final Long key = args.getContext().getAggregateContext(true);
+      ValueHolder<T> rc = null==key ? null : map.get(key);
+      if( null==rc ){
+        map.put(key, rc = new ValueHolder<>(initialValue));
+      }
+      return rc;
+    }
+
+    /**
+       Should be called from a UDF's xFinal() method and passed that
+       method's first argument. This function removes the value
+       associated with with the arguments' aggregate context from the
+       map and returns it, returning null if no other UDF method has
+       been called to set up such a mapping. The latter condition will
+       be the case if a UDF is used in a statement which has no result
+       rows.
+    */
+    public T takeAggregateState(SqlFunction.Arguments args){
+      final ValueHolder<T> h = map.remove(args.getContext().getAggregateContext(false));
+      return null==h ? null : h.value;
+    }
+  }
+
   /** Per-invocation state for the UDF. */
-  private final SqlFunction.PerContextState<T> map =
-    new SqlFunction.PerContextState<>();
+  private final PerContextState<T> map = new PerContextState<>();
 
   /**
      To be called from the implementation's xStep() method, as well
index 2e0be3cd6338a980a828814dbb258f1a2d17de39..311fdcff0e6cb16a14b8ae1edf58d2641312c5d6 100644 (file)
@@ -22,6 +22,17 @@ import org.sqlite.jni.capi.sqlite3_value;
 */
 public interface SqlFunction  {
 
+  public static final int DETERMINISTIC = CApi.SQLITE_DETERMINISTIC;
+  public static final int INNOCUOUS = CApi.SQLITE_INNOCUOUS;
+  public static final int DIRECTONLY = CApi.SQLITE_DIRECTONLY;
+  public static final int UTF8 = CApi.SQLITE_UTF8;
+  public static final int UTF16 = CApi.SQLITE_UTF16;
+  // /**
+  //    For Window functions only and is not currently bound because
+  //    doing so may require exposing sqlite3_value for effective use.
+  // */
+  // public static final int SUBTYPE = CApi.SQLITE_SUBTYPE;
+
   /**
      The Arguments type is an abstraction on top of the lower-level
      UDF function argument types. It provides _most_ of the functionality
@@ -49,44 +60,6 @@ public interface SqlFunction  {
       this.length = this.args.length;
     }
 
-    /**
-       Wrapper for a single SqlFunction argument. Primarily intended
-       for use with the Arguments class's Iterable interface.
-    */
-    public final static class Arg {
-      private final Arguments a;
-      private final int ndx;
-      /* Only for use by the Arguments class. */
-      private Arg(Arguments a, int ndx){
-        this.a = a;
-        this.ndx = ndx;
-      }
-      /** Returns this argument's index in its parent argument list. */
-      public int getIndex(){return ndx;}
-      public int getInt(){return a.getInt(ndx);}
-      public long getInt64(){return a.getInt64(ndx);}
-      public double getDouble(){return a.getDouble(ndx);}
-      public byte[] getBlob(){return a.getBlob(ndx);}
-      public byte[] getText(){return a.getText(ndx);}
-      public String getText16(){return a.getText16(ndx);}
-      public int getBytes(){return a.getBytes(ndx);}
-      public int getBytes16(){return a.getBytes16(ndx);}
-      public Object getObject(){return a.getObject(ndx);}
-      public <T> T getObject(Class<T> type){ return a.getObject(ndx, type); }
-      public int getType(){return a.getType(ndx);}
-      public Object getAuxData(){return a.getAuxData(ndx);}
-      public void setAuxData(Object o){a.setAuxData(ndx, o);}
-    }
-
-    @Override
-    public java.util.Iterator<SqlFunction.Arguments.Arg> iterator(){
-      final Arg[] proxies = new Arg[args.length];
-      for( int i = 0; i < args.length; ++i ){
-        proxies[i] = new Arg(this, i);
-      }
-      return java.util.Arrays.stream(proxies).iterator();
-    }
-
     /**
        Returns the sqlite3_value at the given argument index or throws
        an IllegalArgumentException exception if ndx is out of range.
@@ -100,29 +73,30 @@ public interface SqlFunction  {
       return args[ndx];
     }
 
+    //! Returns the underlying sqlite3_context for these arguments.
     sqlite3_context getContext(){return cx;}
 
     public int getArgCount(){ return args.length; }
 
-    public int getInt(int arg){return CApi.sqlite3_value_int(valueAt(arg));}
-    public long getInt64(int arg){return CApi.sqlite3_value_int64(valueAt(arg));}
-    public double getDouble(int arg){return CApi.sqlite3_value_double(valueAt(arg));}
-    public byte[] getBlob(int arg){return CApi.sqlite3_value_blob(valueAt(arg));}
-    public byte[] getText(int arg){return CApi.sqlite3_value_text(valueAt(arg));}
-    public String getText16(int arg){return CApi.sqlite3_value_text16(valueAt(arg));}
-    public int getBytes(int arg){return CApi.sqlite3_value_bytes(valueAt(arg));}
-    public int getBytes16(int arg){return CApi.sqlite3_value_bytes16(valueAt(arg));}
-    public Object getObject(int arg){return CApi.sqlite3_value_java_object(valueAt(arg));}
-    public <T> T getObject(int arg, Class<T> type){
-      return CApi.sqlite3_value_java_object(valueAt(arg), type);
+    public int getInt(int argNdx){return CApi.sqlite3_value_int(valueAt(argNdx));}
+    public long getInt64(int argNdx){return CApi.sqlite3_value_int64(valueAt(argNdx));}
+    public double getDouble(int argNdx){return CApi.sqlite3_value_double(valueAt(argNdx));}
+    public byte[] getBlob(int argNdx){return CApi.sqlite3_value_blob(valueAt(argNdx));}
+    public byte[] getText(int argNdx){return CApi.sqlite3_value_text(valueAt(argNdx));}
+    public String getText16(int argNdx){return CApi.sqlite3_value_text16(valueAt(argNdx));}
+    public int getBytes(int argNdx){return CApi.sqlite3_value_bytes(valueAt(argNdx));}
+    public int getBytes16(int argNdx){return CApi.sqlite3_value_bytes16(valueAt(argNdx));}
+    public Object getObject(int argNdx){return CApi.sqlite3_value_java_object(valueAt(argNdx));}
+    public <T> T getObject(int argNdx, Class<T> type){
+      return CApi.sqlite3_value_java_object(valueAt(argNdx), type);
     }
 
-    public int getType(int arg){return CApi.sqlite3_value_type(valueAt(arg));}
-    public int getSubtype(int arg){return CApi.sqlite3_value_subtype(valueAt(arg));}
-    public int getNumericType(int arg){return CApi.sqlite3_value_numeric_type(valueAt(arg));}
-    public int getNoChange(int arg){return CApi.sqlite3_value_nochange(valueAt(arg));}
-    public boolean getFromBind(int arg){return CApi.sqlite3_value_frombind(valueAt(arg));}
-    public int getEncoding(int arg){return CApi.sqlite3_value_encoding(valueAt(arg));}
+    public int getType(int argNdx){return CApi.sqlite3_value_type(valueAt(argNdx));}
+    public int getSubtype(int argNdx){return CApi.sqlite3_value_subtype(valueAt(argNdx));}
+    public int getNumericType(int argNdx){return CApi.sqlite3_value_numeric_type(valueAt(argNdx));}
+    public int getNoChange(int argNdx){return CApi.sqlite3_value_nochange(valueAt(argNdx));}
+    public boolean getFromBind(int argNdx){return CApi.sqlite3_value_frombind(valueAt(argNdx));}
+    public int getEncoding(int argNdx){return CApi.sqlite3_value_encoding(valueAt(argNdx));}
 
     public void resultInt(int v){ CApi.sqlite3_result_int(cx, v); }
     public void resultInt64(long v){ CApi.sqlite3_result_int64(cx, v); }
@@ -146,88 +120,60 @@ public interface SqlFunction  {
     public void resultText16(byte[] utf16){CApi.sqlite3_result_text16(cx, utf16);}
     public void resultText16(String txt){CApi.sqlite3_result_text16(cx, txt);}
 
-    public void setAuxData(int arg, Object o){
+    public void setAuxData(int argNdx, Object o){
       /* From the API docs: https://www.sqlite.org/c3ref/get_auxdata.html
 
          The value of the N parameter to these interfaces should be
          non-negative. Future enhancements may make use of negative N
          values to define new kinds of function caching behavior.
       */
-      valueAt(arg);
-      CApi.sqlite3_set_auxdata(cx, arg, o);
+      valueAt(argNdx);
+      CApi.sqlite3_set_auxdata(cx, argNdx, o);
     }
 
-    public Object getAuxData(int arg){
-      valueAt(arg);
-      return CApi.sqlite3_get_auxdata(cx, arg);
+    public Object getAuxData(int argNdx){
+      valueAt(argNdx);
+      return CApi.sqlite3_get_auxdata(cx, argNdx);
     }
-  }
-
-  /**
-     PerContextState assists aggregate and window functions in
-     managing their accumulator state across calls to the UDF's
-     callbacks.
-
-     <p>T must be of a type which can be legally stored as a value in
-     java.util.HashMap<KeyType,T>.
-
-     <p>If a given aggregate or window function is called multiple times
-     in a single SQL statement, e.g. SELECT MYFUNC(A), MYFUNC(B)...,
-     then the clients need some way of knowing which call is which so
-     that they can map their state between their various UDF callbacks
-     and reset it via xFinal(). This class takes care of such
-     mappings.
-
-     <p>This class works by mapping
-     sqlite3_context.getAggregateContext() to a single piece of
-     state, of a client-defined type (the T part of this class), which
-     persists across a "matching set" of the UDF's callbacks.
-
-     <p>This class is a helper providing commonly-needed functionality
-     - it is not required for use with aggregate or window functions.
-     Client UDFs are free to perform such mappings using custom
-     approaches. The provided {@link AggregateFunction} and {@link
-     WindowFunction} classes use this.
-  */
-  public static final class PerContextState<T> {
-    private final java.util.Map<Long,ValueHolder<T>> map
-      = new java.util.HashMap<>();
 
     /**
-       Should be called from a UDF's xStep(), xValue(), and xInverse()
-       methods, passing it that method's first argument and an initial
-       value for the persistent state. If there is currently no
-       mapping for the given context within the map, one is created
-       using the given initial value, else the existing one is used
-       and the 2nd argument is ignored.  It returns a ValueHolder<T>
-       which can be used to modify that state directly without
-       requiring that the client update the underlying map's entry.
-
-       <p>The caller is obligated to eventually call
-       takeAggregateState() to clear the mapping.
+       Wrapper for a single SqlFunction argument. Primarily intended
+       for use with the Arguments class's Iterable interface.
     */
-    public ValueHolder<T> getAggregateState(SqlFunction.Arguments args, T initialValue){
-      final Long key = args.getContext().getAggregateContext(true);
-      ValueHolder<T> rc = null==key ? null : map.get(key);
-      if( null==rc ){
-        map.put(key, rc = new ValueHolder<>(initialValue));
+    public final static class Arg {
+      private final Arguments a;
+      private final int ndx;
+      /* Only for use by the Arguments class. */
+      private Arg(Arguments a, int ndx){
+        this.a = a;
+        this.ndx = ndx;
       }
-      return rc;
+      /** Returns this argument's index in its parent argument list. */
+      public int getIndex(){return ndx;}
+      public int getInt(){return a.getInt(ndx);}
+      public long getInt64(){return a.getInt64(ndx);}
+      public double getDouble(){return a.getDouble(ndx);}
+      public byte[] getBlob(){return a.getBlob(ndx);}
+      public byte[] getText(){return a.getText(ndx);}
+      public String getText16(){return a.getText16(ndx);}
+      public int getBytes(){return a.getBytes(ndx);}
+      public int getBytes16(){return a.getBytes16(ndx);}
+      public Object getObject(){return a.getObject(ndx);}
+      public <T> T getObject(Class<T> type){ return a.getObject(ndx, type); }
+      public int getType(){return a.getType(ndx);}
+      public Object getAuxData(){return a.getAuxData(ndx);}
+      public void setAuxData(Object o){a.setAuxData(ndx, o);}
     }
 
-    /**
-       Should be called from a UDF's xFinal() method and passed that
-       method's first argument. This function removes the value
-       associated with with the arguments' aggregate context from the
-       map and returns it, returning null if no other UDF method has
-       been called to set up such a mapping. The latter condition will
-       be the case if a UDF is used in a statement which has no result
-       rows.
-    */
-    public T takeAggregateState(SqlFunction.Arguments args){
-      final ValueHolder<T> h = map.remove(args.getContext().getAggregateContext(false));
-      return null==h ? null : h.value;
+    @Override
+    public java.util.Iterator<SqlFunction.Arguments.Arg> iterator(){
+      final Arg[] proxies = new Arg[args.length];
+      for( int i = 0; i < args.length; ++i ){
+        proxies[i] = new Arg(this, i);
+      }
+      return java.util.Arrays.stream(proxies).iterator();
     }
+
   }
 
   /**
@@ -235,7 +181,7 @@ public interface SqlFunction  {
      for use with the org.sqlite.jni.capi.ScalarFunction interface.
   */
   static final class ScalarAdapter extends org.sqlite.jni.capi.ScalarFunction {
-    final ScalarFunction impl;
+    private final ScalarFunction impl;
     ScalarAdapter(ScalarFunction impl){
       this.impl = impl;
     }
@@ -261,8 +207,9 @@ public interface SqlFunction  {
      Internal-use adapter for wrapping this package's AggregateFunction
      for use with the org.sqlite.jni.capi.AggregateFunction interface.
   */
-  static final class AggregateAdapter extends org.sqlite.jni.capi.AggregateFunction {
-    final AggregateFunction impl;
+  static /*cannot be final without duplicating the whole body in WindowAdapter*/
+  class AggregateAdapter extends org.sqlite.jni.capi.AggregateFunction {
+    private final AggregateFunction impl;
     AggregateAdapter(AggregateFunction impl){
       this.impl = impl;
     }
@@ -282,8 +229,9 @@ public interface SqlFunction  {
     }
 
     /**
-       As for the xFinal() argument of the C API's sqlite3_create_function().
-       If the proxied function throws, it is translated into a sqlite3_result_error().
+       As for the xFinal() argument of the C API's
+       sqlite3_create_function().  If the proxied function throws, it
+       is translated into a sqlite3_result_error().
     */
     public void xFinal(sqlite3_context cx){
       try{
@@ -298,4 +246,46 @@ public interface SqlFunction  {
     }
   }
 
+  /**
+     Internal-use adapter for wrapping this package's WindowFunction
+     for use with the org.sqlite.jni.capi.WindowFunction interface.
+  */
+  static final class WindowAdapter extends AggregateAdapter {
+    private final WindowFunction impl;
+    WindowAdapter(WindowFunction impl){
+      super(impl);
+      this.impl = impl;
+    }
+
+    /**
+       Proxies this.impl.xInverse(), adapting the call arguments to that
+       function's signature. If the proxied function throws, it is
+       translated to sqlite_result_error() with the exception's
+       message.
+    */
+    public void xInverse(sqlite3_context cx, sqlite3_value[] args){
+      try{
+        impl.xInverse( new SqlFunction.Arguments(cx, args) );
+      }catch(Exception e){
+        CApi.sqlite3_result_error(cx, e);
+      }
+    }
+
+    /**
+       As for the xValue() argument of the C API's sqlite3_create_window_function().
+       If the proxied function throws, it is translated into a sqlite3_result_error().
+    */
+    public void xValue(sqlite3_context cx){
+      try{
+        impl.xValue( new SqlFunction.Arguments(cx, null) );
+      }catch(Exception e){
+        CApi.sqlite3_result_error(cx, e);
+      }
+    }
+
+    public void xDestroy(){
+      impl.xDestroy();
+    }
+  }
+
 }
index 25628e080581d20f9c2b60e3f9831ad0fb60ca49..016eb0b89c45767e6cf4549ee2b4d3a06e8756eb 100644 (file)
@@ -29,6 +29,10 @@ import org.sqlite.jni.capi.OutputPointer;
 public final class Sqlite implements AutoCloseable  {
   private sqlite3 db;
 
+  public static final int OPEN_READWRITE = CApi.SQLITE_OPEN_READWRITE;
+  public static final int OPEN_CREATE = CApi.SQLITE_OPEN_CREATE;
+  public static final int OPEN_EXRESCODE = CApi.SQLITE_OPEN_EXRESCODE;
+
   //! Used only by the open() factory functions.
   private Sqlite(sqlite3 db){
     this.db = db;
@@ -120,7 +124,7 @@ public final class Sqlite implements AutoCloseable  {
     return prepare(sql, 0);
   }
 
-  public void createFunction(String name, int nArg, int eTextRep, ScalarFunction f ){
+  public void createFunction(String name, int nArg, int eTextRep, ScalarFunction f){
     int rc = CApi.sqlite3_create_function(thisDb(), name, nArg, eTextRep,
                                            new SqlFunction.ScalarAdapter(f));
     if( 0!=rc ) throw new SqliteException(db);
@@ -130,7 +134,7 @@ public final class Sqlite implements AutoCloseable  {
     this.createFunction(name, nArg, CApi.SQLITE_UTF8, f);
   }
 
-  public void createFunction(String name, int nArg, int eTextRep, AggregateFunction f ){
+  public void createFunction(String name, int nArg, int eTextRep, AggregateFunction f){
     int rc = CApi.sqlite3_create_function(thisDb(), name, nArg, eTextRep,
                                            new SqlFunction.AggregateAdapter(f));
     if( 0!=rc ) throw new SqliteException(db);
@@ -140,6 +144,16 @@ public final class Sqlite implements AutoCloseable  {
     this.createFunction(name, nArg, CApi.SQLITE_UTF8, f);
   }
 
+  public void createFunction(String name, int nArg, int eTextRep, WindowFunction f){
+    int rc = CApi.sqlite3_create_function(thisDb(), name, nArg, eTextRep,
+                                          new SqlFunction.WindowAdapter(f));
+    if( 0!=rc ) throw new SqliteException(db);
+  }
+
+  public void createFunction(String name, int nArg, WindowFunction f){
+    this.createFunction(name, nArg, CApi.SQLITE_UTF8, f);
+  }
+
   /**
      Corresponds to the sqlite3_stmt class. Use Sqlite.prepare() to
      create new instances.
@@ -223,11 +237,19 @@ public final class Sqlite implements AutoCloseable  {
     }
 
     /**
-       Works like sqlite3_step() but throws SqliteException for any
-       result other than 0, SQLITE_ROW, or SQLITE_DONE.
+       Works like sqlite3_step() but returns true for SQLITE_ROW,
+       false for SQLITE_DONE, and throws SqliteException for any other
+       result.
     */
-    public int step(){
-      return checkRc(sqlite3_step(thisStmt()));
+    public boolean step(){
+      switch(checkRc(sqlite3_step(thisStmt()))){
+        case CApi.SQLITE_ROW: return true;
+        case CApi.SQLITE_DONE: return false;
+        default:
+          throw new IllegalStateException(
+            "This \"cannot happen\": all possible result codes were checked already."
+          );
+      }
       /*
         Potential signature change TODO:
 
index 8fb36a3b54d91cf79ef7094359b240eea7eb2e15..ca4e6d052185a56e0034d1c6a4f0b3ce16300c99 100644 (file)
@@ -129,6 +129,11 @@ public class Tester2 implements Runnable {
     execSql(db, String.join("", sql));
   }
 
+  /**
+     Executes all SQL statements in the given string. If throwOnError
+     is true then it will throw for any prepare/step errors, else it
+     will return the corresponding non-0 result code.
+  */
   public static int execSql(Sqlite dbw, boolean throwOnError, String sql){
     final sqlite3 db = dbw.nativeHandle();
     OutputPointer.Int32 oTail = new OutputPointer.Int32();
@@ -145,7 +150,7 @@ public class Tester2 implements Runnable {
       }
       if( 0==sqlChunk.length ) break;
       rc = CApi.sqlite3_prepare_v2(db, sqlChunk, outStmt, oTail);
-      if(throwOnError) affirm(0 == rc);
+      if( throwOnError ) affirm(0 == rc);
       else if( 0!=rc ) break;
       pos = oTail.value;
       stmt = outStmt.take();
@@ -158,7 +163,7 @@ public class Tester2 implements Runnable {
       }
       CApi.sqlite3_finalize(stmt);
       affirm(0 == stmt.getNativePointer());
-      if(0!=rc && CApi.SQLITE_ROW!=rc && CApi.SQLITE_DONE!=rc){
+      if(CApi.SQLITE_DONE!=rc){
         break;
       }
     }
@@ -194,9 +199,9 @@ public class Tester2 implements Runnable {
   }
 
   Sqlite openDb(String name){
-    final Sqlite db = Sqlite.open(name, CApi.SQLITE_OPEN_READWRITE|
-                                  CApi.SQLITE_OPEN_CREATE|
-                                  CApi.SQLITE_OPEN_EXRESCODE);
+    final Sqlite db = Sqlite.open(name, Sqlite.OPEN_READWRITE|
+                                  Sqlite.OPEN_CREATE|
+                                  Sqlite.OPEN_EXRESCODE);
     ++metrics.dbOpen;
     return db;
   }
@@ -245,7 +250,7 @@ public class Tester2 implements Runnable {
       catch(Exception ex){ e = ex; }
       affirm( null!=e );
       e = null;
-      affirm( CApi.SQLITE_ROW == stmt.step() );
+      affirm( stmt.step() );
       try{ stmt.columnInt(1); }
       catch(Exception ex){ e = ex; }
       affirm( null!=e );
@@ -254,16 +259,16 @@ public class Tester2 implements Runnable {
       affirm( 17L == stmt.columnInt64(0) );
       affirm( 17.0 == stmt.columnDouble(0) );
       affirm( "17".equals(stmt.columnText16(0)) );
-      affirm( CApi.SQLITE_DONE == stmt.step() );
+      affirm( !stmt.step() );
       stmt.reset();
-      affirm( CApi.SQLITE_ROW == stmt.step() );
-      affirm( CApi.SQLITE_DONE == stmt.step() );
+      affirm( stmt.step() );
+      affirm( !stmt.step() );
       affirm( 0 == stmt.finalizeStmt() );
       affirm( null==stmt.nativeHandle() );
 
       stmt = db.prepare("SELECT ?");
       stmt.bindObject(1, db);
-      affirm( CApi.SQLITE_ROW == stmt.step() );
+      affirm( stmt.step() );
       affirm( db==stmt.columnObject(0) );
       affirm( db==stmt.columnObject(0, Sqlite.class ) );
       affirm( null==stmt.columnObject(0, Sqlite.Stmt.class ) );
@@ -328,7 +333,7 @@ public class Tester2 implements Runnable {
         /* ------------------^^^^^^^^^^^ ensures that we're handling
            sqlite3_aggregate_context() properly. */
       );
-      affirm( CApi.SQLITE_ROW==q.step() );
+      affirm( q.step() );
       affirm( 15==q.columnInt(0) );
       q.finalizeStmt();
       q = null;
@@ -336,7 +341,7 @@ public class Tester2 implements Runnable {
       db.createFunction("summerN", -1, f);
 
       q = db.prepare("select summerN(1,8,9), summerN(2,3,4)");
-      affirm( CApi.SQLITE_ROW==q.step() );
+      affirm( q.step() );
       affirm( 18==q.columnInt(0) );
       affirm( 9==q.columnInt(1) );
       q.finalizeStmt();
@@ -350,6 +355,63 @@ public class Tester2 implements Runnable {
             /* because we've bound the same instance twice */ );
   }
 
+  private void testUdfWindow(){
+    final Sqlite db = openDb();
+    /* Example window function, table, and results taken from:
+       https://sqlite.org/windowfunctions.html#udfwinfunc */
+    final WindowFunction func = new WindowFunction<Integer>(){
+        //! Impl of xStep() and xInverse()
+        private void xStepInverse(SqlFunction.Arguments args, int v){
+          this.getAggregateState(args,0).value += v;
+        }
+        @Override public void xStep(SqlFunction.Arguments args){
+          this.xStepInverse(args, args.getInt(0));
+        }
+        @Override public void xInverse(SqlFunction.Arguments args){
+          this.xStepInverse(args, -args.getInt(0));
+        }
+        //! Impl of xFinal() and xValue()
+        private void xFinalValue(SqlFunction.Arguments args, Integer v){
+          if(null == v) args.resultNull();
+          else args.resultInt(v);
+        }
+        @Override public void xFinal(SqlFunction.Arguments args){
+          xFinalValue(args, this.takeAggregateState(args));
+          affirm( null == this.getAggregateState(args,null).value );
+        }
+        @Override public void xValue(SqlFunction.Arguments args){
+          xFinalValue(args, this.getAggregateState(args,null).value);
+        }
+      };
+    db.createFunction("winsumint", 1, func);
+    execSql(db, new String[] {
+        "CREATE TEMP TABLE twin(x, y); INSERT INTO twin VALUES",
+        "('a', 4),('b', 5),('c', 3),('d', 8),('e', 1)"
+      });
+    final Sqlite.Stmt stmt = db.prepare(
+      "SELECT x, winsumint(y) OVER ("+
+      "ORDER BY x ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING"+
+      ") AS sum_y "+
+      "FROM twin ORDER BY x;"
+    );
+    int n = 0;
+    while( stmt.step() ){
+      final String s = stmt.columnText16(0);
+      final int i = stmt.columnInt(1);
+      switch(++n){
+        case 1: affirm( "a".equals(s) && 9==i ); break;
+        case 2: affirm( "b".equals(s) && 12==i ); break;
+        case 3: affirm( "c".equals(s) && 16==i ); break;
+        case 4: affirm( "d".equals(s) && 12==i ); break;
+        case 5: affirm( "e".equals(s) && 9==i ); break;
+        default: affirm( false /* cannot happen */ );
+      }
+    }
+    stmt.close();
+    affirm( 5 == n );
+    db.close();
+  }
+
   private void runTests(boolean fromThread) throws Exception {
     List<java.lang.reflect.Method> mlist = testMethods;
     affirm( null!=mlist );
diff --git a/ext/jni/src/org/sqlite/jni/wrapper1/WindowFunction.java b/ext/jni/src/org/sqlite/jni/wrapper1/WindowFunction.java
new file mode 100644 (file)
index 0000000..479fc74
--- /dev/null
@@ -0,0 +1,46 @@
+/*
+** 2023-10-16
+**
+** The author disclaims copyright to this source code.  In place of
+** a legal notice, here is a blessing:
+**
+**    May you do good and not evil.
+**    May you find forgiveness for yourself and forgive others.
+**    May you share freely, never taking more than you give.
+**
+*************************************************************************
+** This file is part of the wrapper1 interface for sqlite3.
+*/
+package org.sqlite.jni.wrapper1;
+import org.sqlite.jni.capi.CApi;
+import org.sqlite.jni.annotation.*;
+import org.sqlite.jni.capi.sqlite3_context;
+import org.sqlite.jni.capi.sqlite3_value;
+
+/**
+   A SqlFunction implementation for window functions. The T type
+   represents the type of data accumulated by this function while it
+   works. e.g. a SUM()-like UDF might use Integer or Long and a
+   CONCAT()-like UDF might use a StringBuilder or a List<String>.
+*/
+public abstract class WindowFunction<T> extends AggregateFunction<T>  {
+
+  /**
+     As for the xInverse() argument of the C API's
+     sqlite3_create_window_function().  If this function throws, the
+     exception is reported via sqlite3_result_error().
+  */
+  public abstract void xInverse(SqlFunction.Arguments args);
+
+  /**
+     As for the xValue() argument of the C API's
+     sqlite3_create_window_function(). If this function throws, it is
+     translated into sqlite3_result_error().
+
+     Note that the passed-in object will not actually contain any
+     arguments for xValue() but will contain the context object needed
+     for setting the call's result or error state.
+  */
+  public abstract void xValue(SqlFunction.Arguments args);
+
+}
index d20131a353078620bad87bd22fe54594a9702a0e..f6e1c2056ada49b32393ed6a73c5a5037e1c2e84 100644 (file)
--- a/manifest
+++ b/manifest
@@ -1,5 +1,5 @@
-C JNI:\sflesh\sout\sand\ssimplify\sthe\sAPIs\sfor\sbinding\sand\sfetching\sarbitrary\sJava\sobjects.
-D 2023-10-22T14:25:37.634
+C Add\shigh-level\swindow\sfunction\swrapper\sto\sthe\sJNI\swrapper1\sinterface.
+D 2023-10-22T23:36:16.690
 F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1
 F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea
 F LICENSE.md df5091916dbb40e6e9686186587125e1b2ff51f022cc334e886c19a0e9982724
@@ -236,7 +236,7 @@ F ext/fts5/tool/showfts5.tcl d54da0e067306663e2d5d523965ca487698e722c
 F ext/icu/README.txt 7ab7ced8ae78e3a645b57e78570ff589d4c672b71370f5aa9e1cd7024f400fc9
 F ext/icu/icu.c c074519b46baa484bb5396c7e01e051034da8884bad1a1cb7f09bbe6be3f0282
 F ext/icu/sqliteicu.h fa373836ed5a1ee7478bdf8a1650689294e41d0c89c1daab26e9ae78a32075a8
-F ext/jni/GNUmakefile 5c3ac326bf3853486ebe0d70819abc790cc65c412182ce4ebd5012b008d9b059
+F ext/jni/GNUmakefile 36919b7c4fb8447da4330df9996c7b064b766957f8b7be214a30eab55a8b8072
 F ext/jni/README.md ef9ac115e97704ea995d743b4a8334e23c659e5534c3b64065a5405256d5f2f4
 F ext/jni/jar-dist.make 030aaa4ae71dd86e4ec5e7c1e6cd86f9dfa47c4592c070d2e35157e42498e1fa
 F ext/jni/src/c/sqlite3-jni.c dcd6534b65b732ad927a49185c76c76abbd5ccadfa972d02f699abc45678e329
@@ -245,7 +245,7 @@ F ext/jni/src/org/sqlite/jni/annotation/NotNull.java a99341e88154e70447596b1af6a
 F ext/jni/src/org/sqlite/jni/annotation/Nullable.java 0b1879852707f752512d4db9d7edd0d8db2f0c2612316ce1c832715e012ff6ba
 F ext/jni/src/org/sqlite/jni/annotation/package-info.java 977b374aed9d5853cbf3438ba3b0940abfa2ea4574f702a2448ee143b98ac3ca
 F ext/jni/src/org/sqlite/jni/capi/AbstractCollationCallback.java 1afa90d3f236f79cc7fcd2497e111992644f7596fbc8e8bcf7f1908ae00acd6c
-F ext/jni/src/org/sqlite/jni/capi/AggregateFunction.java bc29e986c866c2ddbbb9f935f5b7264c1c1026864e50a4a735192864f75e37c0
+F ext/jni/src/org/sqlite/jni/capi/AggregateFunction.java 0b72cdff61533b564d65b63418129656daa9a9f30e7e7be982bd5ab394b1dbd0
 F ext/jni/src/org/sqlite/jni/capi/AuthorizerCallback.java 7ed409d5449684616cc924534e22ff6b07d361f12ad904b69ecb10e0568a8013
 F ext/jni/src/org/sqlite/jni/capi/AutoExtensionCallback.java 74cc4998a73d6563542ecb90804a3c4f4e828cb4bd69e61226d1a51f4646e759
 F ext/jni/src/org/sqlite/jni/capi/BusyHandlerCallback.java 7b8e19810c42b0ad21a04b5d8c804b32ee5905d137148703f16a75b612c380ca
@@ -263,11 +263,11 @@ F ext/jni/src/org/sqlite/jni/capi/PreupdateHookCallback.java 819d938e26208adde17
 F ext/jni/src/org/sqlite/jni/capi/ProgressHandlerCallback.java 01bc0c238eed2d5f93c73522cb7849a445cc9098c2ed1e78248fa20ed1cfde5b
 F ext/jni/src/org/sqlite/jni/capi/ResultCode.java 8141171f1bcf9f46eef303b9d3c5dc2537a25ad1628f3638398d8a60cacefa7f
 F ext/jni/src/org/sqlite/jni/capi/RollbackHookCallback.java 105e324d09c207100485e7667ad172e64322c62426bb49b547e9b0dc9c33f5f0
-F ext/jni/src/org/sqlite/jni/capi/SQLFunction.java fef556adbc3624292423083a648bdf97fa8a4f6b3b6577c9660dd7bd6a6d3c4a
+F ext/jni/src/org/sqlite/jni/capi/SQLFunction.java 0d1e9afc9ff8a2adb94a155b72385155fa3b8011a5cca0bb3c28468c7131c1a5
 F ext/jni/src/org/sqlite/jni/capi/SQLTester.java 09bee15aa0eedac68d767ae21d9a6a62a31ade59182a3ccbf036d6463d9e30b1
 F ext/jni/src/org/sqlite/jni/capi/ScalarFunction.java 93b9700fca4c68075ccab12fe0fbbc76c91cafc9f368e835b9bd7cd7732c8615
 F ext/jni/src/org/sqlite/jni/capi/TableColumnMetadata.java addf120e0e76e5be1ff2260daa7ce305ff9b5fafd64153a7a28e9d8f000a815f
-F ext/jni/src/org/sqlite/jni/capi/Tester1.java 5c4e7ba5034aeb5c5be0361b9fa0c23fe993774e634750c775d7ad8fa19b22f3
+F ext/jni/src/org/sqlite/jni/capi/Tester1.java b6b2f3354ba68956a6bcd1c586b8eb25a0bd66eed2b58b340405e1129da15de9
 F ext/jni/src/org/sqlite/jni/capi/TraceV2Callback.java 0a25e117a0daae3394a77f24713e36d7b44c67d6e6d30e9e1d56a63442eef723
 F ext/jni/src/org/sqlite/jni/capi/UpdateHookCallback.java 2766b8526bbffc4f1045f70e79f1bc1b1efe1c3e95ca06cdb8a7391032dda3b4
 F ext/jni/src/org/sqlite/jni/capi/ValueHolder.java 9f9e151f1da017b706c0ee5f40f4c86b54e773d6ae4339723e0cc85a456251ab
@@ -291,13 +291,14 @@ F ext/jni/src/org/sqlite/jni/fts5/fts5_api.java a8e88c3783d21cec51b0748568a96653
 F ext/jni/src/org/sqlite/jni/fts5/fts5_extension_function.java 9e2b954d210d572552b28aca523b272fae14bd41e318921b22f65b728d5bf978
 F ext/jni/src/org/sqlite/jni/fts5/fts5_tokenizer.java 92bdaa3893bd684533004d64ade23d329843f809cd0d0f4f1a2856da6e6b4d90
 F ext/jni/src/org/sqlite/jni/test-script-interpreter.md f9f25126127045d051e918fe59004a1485311c50a13edbf18c79a6ff9160030e
-F ext/jni/src/org/sqlite/jni/wrapper1/AggregateFunction.java 5ad99bd74c85f56bbef324d9ec29b4048f4620547c9a80093d8586c3557f9f9a
+F ext/jni/src/org/sqlite/jni/wrapper1/AggregateFunction.java bbe60ac7fd8718edb215a23dc901771bcedb1df3b46d9cf6caff6f419828587f
 F ext/jni/src/org/sqlite/jni/wrapper1/ScalarFunction.java 43c43adfb7866098aadaaca1620028a6ec82d5193149970019b1cce9eb59fb03
-F ext/jni/src/org/sqlite/jni/wrapper1/SqlFunction.java 92c28b9de358407c8c5e772e0408db528e47eeeb50ffd87b86563a5f078198ad
-F ext/jni/src/org/sqlite/jni/wrapper1/Sqlite.java 882e345d925a79b575b1182efd816dcc72d6814922b4f58e7f4d29f04ece1f64
+F ext/jni/src/org/sqlite/jni/wrapper1/SqlFunction.java 585309311ffce6f39626024bf2ea3add91339f6a146b674720165c1955efbe68
+F ext/jni/src/org/sqlite/jni/wrapper1/Sqlite.java 3e6cdb5fe1b01a592ba5ca6ae7d11681a85d081786ce8d046ef631a08ae82dde
 F ext/jni/src/org/sqlite/jni/wrapper1/SqliteException.java 1386f7b753134fc12253ce2fbbc448ba8c970567fac01a3356cb672e14408d73
-F ext/jni/src/org/sqlite/jni/wrapper1/Tester2.java e224efb77dae2f0abe18f2010c0eb5a09df991f2743597a1aff7f9283f71da7d
+F ext/jni/src/org/sqlite/jni/wrapper1/Tester2.java 13008f8d3c34c1dd55c3afe6dd18dcf94316874cde893ab0661a973fc51a87a4
 F ext/jni/src/org/sqlite/jni/wrapper1/ValueHolder.java 7b89a7391f771692c5b83b0a5b86266abe8d59f1c77d7a0eccc9b79f259d79af
+F ext/jni/src/org/sqlite/jni/wrapper1/WindowFunction.java 1a1afbafbd7406ff67e7d6405541c6347517c731de535a97d7a3df1d4db835b4
 F ext/jni/src/tests/000-000-sanity.test c3427a0e0ac84d7cbe4c95fdc1cd4b61f9ddcf43443408f3000139478c4dc745
 F ext/jni/src/tests/000-001-ignored.test e17e874c6ab3c437f1293d88093cf06286083b65bf162317f91bbfd92f961b70
 F ext/jni/src/tests/900-001-fts.test bf0ce17a8d082773450e91f2388f5bbb2dfa316d0b676c313c637a91198090f0
@@ -2136,8 +2137,8 @@ F vsixtest/vsixtest.tcl 6a9a6ab600c25a91a7acc6293828957a386a8a93
 F vsixtest/vsixtest.vcxproj.data 2ed517e100c66dc455b492e1a33350c1b20fbcdc
 F vsixtest/vsixtest.vcxproj.filters 37e51ffedcdb064aad6ff33b6148725226cd608e
 F vsixtest/vsixtest_TemporaryKey.pfx e5b1b036facdb453873e7084e1cae9102ccc67a0
-P 60a0e82db26270af9d0a5f55c6173e4fd0bdc90a885e838480ed75f8ef193287
-R 5a1e4de3384c2859d02b694c480544f0
+P 89fecf1dd8b97941f9b45130a3c8a67af36ec65cc6f70f5026c569c058a4963f
+R 160b97c24140d26b449a4e16b3331ec4
 U stephan
-Z 0b6f35d12f929147f1a8dbbd84db96f1
+Z 05c9adecf2f51f54547f1d6b2c6aaa3c
 # Remove this line to create a well-formed Fossil manifest.
index a425b289815aa64eb5e2305524eedca5248a18e0..fbcdd1ab297d16a8c8787e959fa70ab25341c251 100644 (file)
@@ -1 +1 @@
-89fecf1dd8b97941f9b45130a3c8a67af36ec65cc6f70f5026c569c058a4963f
\ No newline at end of file
+a27e7471231a24864cbd04b77cbc4b336ce180d738a36ce4318543e2666ed708
\ No newline at end of file