diff --git a/.github/workflows/ci-runnerpg.yml b/.github/workflows/ci-runnerpg.yml index c534680a1..ab261b3f1 100644 --- a/.github/workflows/ci-runnerpg.yml +++ b/.github/workflows/ci-runnerpg.yml @@ -7,9 +7,9 @@ name: PL/Java CI with PostgreSQL version supplied by the runner on: push: - branches: [ master, REL1_6_STABLE ] + branches: [ master, REL1_7_STABLE, REL1_6_STABLE ] pull_request: - branches: [ master, REL1_6_STABLE ] + branches: [ master, REL1_7_STABLE, REL1_6_STABLE ] jobs: build: diff --git a/appveyor.yml b/appveyor.yml index a0e534ee2..5402ddbb0 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -14,55 +14,55 @@ environment: matrix: # - SYS: MINGW # JDK: 9 -# PG: 12 +# PG: 14 # - SYS: MINGW # JDK: 10 -# PG: 12 +# PG: 14 - SYS: MINGW JDK: 11 - PG: 12 + PG: 14 - SYS: MINGW JDK: 12 - PG: 12 + PG: 14 - SYS: MINGW JDK: 13 - PG: 12 + PG: 14 - SYS: MINGW JDK: 14 - PG: 12 + PG: 14 - SYS: MINGW JDK: 15 - PG: 12 + PG: 14 - SYS: MSVC JDK: 15 - PG: 12 - - SYS: MSVC - JDK: 14 - PG: 12 - - SYS: MSVC - JDK: 13 - PG: 12 - - SYS: MSVC - JDK: 12 - PG: 12 + PG: 13 +# - SYS: MSVC +# JDK: 14 +# PG: 13 +# - SYS: MSVC +# JDK: 13 +# PG: 13 +# - SYS: MSVC +# JDK: 12 +# PG: 13 - SYS: MSVC JDK: 11 - PG: 12 + PG: 13 # - SYS: MSVC # JDK: 10 -# PG: 12 +# PG: 13 # - SYS: MSVC # JDK: 9 # PG: 12 - - SYS: MSVC - JDK: 14 - PG: 11 - - SYS: MSVC - JDK: 14 - PG: 10 - - SYS: MSVC - JDK: 14 - PG: 9.6 +# - SYS: MSVC +# JDK: 14 +# PG: 11 +# - SYS: MSVC +# JDK: 14 +# PG: 10 +# - SYS: MSVC +# JDK: 14 +# PG: 9.6 before_build: - ps: .appveyor/appveyor_download_java.ps1 - set JAVA_HOME=%ProgramFiles%\Java\jdk%JDK% diff --git a/pljava-ant/pom.xml b/pljava-ant/pom.xml index 15e6c6751..f28fa7ee3 100644 --- a/pljava-ant/pom.xml +++ b/pljava-ant/pom.xml @@ -4,7 +4,7 @@ org.postgresql pljava.app - 2-SNAPSHOT + 1.7-SNAPSHOT pljava-ant PL/Java Ant tasks diff --git a/pljava-api/pom.xml b/pljava-api/pom.xml index 314d75238..592b38636 100644 --- a/pljava-api/pom.xml +++ b/pljava-api/pom.xml @@ -4,7 +4,7 @@ org.postgresql pljava.app - 2-SNAPSHOT + 1.7-SNAPSHOT pljava-api PL/Java API diff --git a/pljava-api/src/main/java/module-info.java b/pljava-api/src/main/java/module-info.java index fbfcb8bd4..d501d86a9 100644 --- a/pljava-api/src/main/java/module-info.java +++ b/pljava-api/src/main/java/module-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020 Tada AB and other contributors, as listed below. + * Copyright (c) 2020-2023 Tada AB and other contributors, as listed below. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the The BSD 3-Clause License @@ -21,14 +21,21 @@ requires transitive java.compiler; exports org.postgresql.pljava; + exports org.postgresql.pljava.adt; + exports org.postgresql.pljava.adt.spi; exports org.postgresql.pljava.annotation; + exports org.postgresql.pljava.model; exports org.postgresql.pljava.sqlgen; exports org.postgresql.pljava.annotation.processing to org.postgresql.pljava.internal; + uses org.postgresql.pljava.Adapter.Service; + uses org.postgresql.pljava.Session; + uses org.postgresql.pljava.model.CatalogObject.Factory; + provides javax.annotation.processing.Processor with org.postgresql.pljava.annotation.processing.DDRProcessor; } diff --git a/pljava-api/src/main/java/org/postgresql/pljava/Adapter.java b/pljava-api/src/main/java/org/postgresql/pljava/Adapter.java new file mode 100644 index 000000000..47b40acac --- /dev/null +++ b/pljava-api/src/main/java/org/postgresql/pljava/Adapter.java @@ -0,0 +1,1949 @@ +/* + * Copyright (c) 2022-2023 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles.Lookup; +import static java.lang.invoke.MethodHandles.collectArguments; +import static java.lang.invoke.MethodHandles.dropArguments; +import static java.lang.invoke.MethodHandles.lookup; +import static java.lang.invoke.MethodHandles.permuteArguments; +import java.lang.invoke.MethodType; +import static java.lang.invoke.MethodType.methodType; + +import static java.lang.reflect.Array.newInstance; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; + +import java.security.AccessController; +import java.security.Permission; +import java.security.PermissionCollection; + +import java.sql.SQLException; +import java.sql.SQLDataException; + +import java.util.Arrays; +import static java.util.Arrays.stream; +import static java.util.Collections.emptyEnumeration; +import static java.util.Collections.enumeration; +import java.util.Enumeration; +import java.util.List; +import static java.util.Objects.requireNonNull; +import java.util.ServiceConfigurationError; +import java.util.ServiceLoader; + +import java.util.function.Predicate; + +import org.postgresql.pljava.adt.spi.AbstractType; +import org.postgresql.pljava.adt.spi.AbstractType.Bindings; +import org.postgresql.pljava.adt.spi.AbstractType.MultiArray; +import org.postgresql.pljava.adt.spi.Datum; +import org.postgresql.pljava.adt.spi.TwosComplement; + +import org.postgresql.pljava.model.Attribute; +import org.postgresql.pljava.model.RegType; +import org.postgresql.pljava.model.TupleTableSlot.Indexed; + +import org.postgresql.pljava.model.SlotTester.Visible; // temporary for test jig + +import static org.postgresql.pljava.adt.spi.AbstractType.erase; +import static org.postgresql.pljava.adt.spi.AbstractType.isSubtype; +import static org.postgresql.pljava.adt.spi.AbstractType.refine; +import static org.postgresql.pljava.adt.spi.AbstractType.specialization; +import static org.postgresql.pljava.adt.spi.AbstractType.substitute; + +/** + * Base for classes that implement data types over raw PostgreSQL datums. + *

+ * A PL/Java data type adapter is a concrete subclass of this class that knows + * the structure of one or more PostgreSQL data types and can convert between + * their raw {@code Datum} form and an appropriate Java class or primitive type + * for use in PL/Java code. It will use the {@code Via...} enum declared here + * (to indicate how it will access the PostgreSQL {@code Datum}), and extend + * an {@code As...} abstract class declared here (to indicate the supported + * Java reference or primitive type). + *

+ * An adapter should be stateless and thread-safe. There should be no need to + * instantiate more than one instance of an adapter for a given type mapping. + *

+ * An adapter has a "top" type T, indicating the type it will present to client + * code, and an "under" type U, which client code can generally wildcard and + * ignore; an implementing class that can be composed over another adapter uses + * U to indicate what that "under" adapter's "top" type must be. The Java + * compiler records enough information for both parameters to allow PL/Java to + * reconstruct the type relationships in a stack of composed adapters. + *

+ * An implementing leaf adapter (which will work directly on PostgreSQL Datum + * rather than being composed over another adapter) can declare {@code Void} + * for U by convention. An adapter meant to be composed over another, where the + * "under" adapter has a primitive type, can declare the primitive type's boxed + * counterpart as U. + *

+ * For a primitive-typed adapter, the "top" type is implicit in the class name + * {@code AsLong}, {@code AsInt}, and so on, and the "under" type follows as the + * parameter U. For ease of reading, the type parameters of the two-parameter + * classes like {@code As} are also in that order, T first. + *

+ * The precise meaning of the "top" type T depends on whether an adapter is + * an instance of {@code As} or of {@code Primitive}. In the + * {@code As} case, the top type is a reference type and is given by T directly. + * In the primitive case, T is the boxed counterpart of the actual top type. + *

+ * To preserve type safety, only recognized "leaf" adapters (those registered + * to {@link #configure configure} with a non-null {@link Via via}) + * will be able to manipulate raw {@code Datum}s. An adapter class + * should avoid leaking a {@code Datum} to other code. + */ +public abstract class Adapter implements Visible +{ + /** + * The full generic type returned by this adapter, as refined at the time + * of construction, making use of the type returned by an "under" adapter + * or array contract, if used. + */ + final Type m_topType; + + /** + * The erasure of the type to be returned. + */ + final Class m_topErased; + + /** + * The "under" adapter in the composed case; null in a leaf adapter. + */ + final Adapter m_underAdapter; + + /** + * Method handle constructed for this adapter's fetch operation. + */ + final MethodHandle m_fetchHandle; + + /** + * In this private constructor, witness is declared as + * {@code Type} rather than {@code Class}. + *

+ * It can be invoked that way from {@code As} for array adapters; otherwise, + * the subclass constructors all declare the parameter as {@code Class}. + *

+ * The adapter and contract here are raw types. The accessible subclass + * constructors will constrain their type arguments to be compatible. + */ + private Adapter( + Configuration configuration, Adapter over, Contract using, Type witness) + { + requireNonNull(configuration, + () -> getClass() + " instantiated without a Configuration object"); + if ( getClass() != configuration.m_class ) + throw new IllegalArgumentException( + getClass() + " instantiated with a Configuration object " + + "for the wrong class"); + + if ( configuration instanceof Configuration.Leaf ) + { + if ( null != over ) + throw new IllegalArgumentException( + getClass() + " instantiated with non-null 'over' but is " + + "a leaf adapter"); + + Configuration.Leaf leaf = (Configuration.Leaf)configuration; + + Type top = leaf.m_top; + /* + * If instantiated with a subclass of Contract, the type with + * which it specializes Contract may tell us more than our top + * type precomputed at configuration. + */ + if ( null != using ) + { + if ( witness instanceof TypeWrapper ) + { + top = ((TypeWrapper)witness).wrapped; + witness = null; + } + else + top = specialization(using.getClass(), Contract.class)[0]; + } + + MethodHandle mh = leaf.m_fetch.bindTo(this); + + @SuppressWarnings("unchecked") + Class erased = (Class)erase(top); + + if ( null == witness ) + { + if ( top instanceof TypeVariable + && 1 == ((TypeVariable)top).getBounds().length ) + top = erased; + } + else + { + if ( ! isSubtype(witness, erased) ) + throw new IllegalArgumentException( + "cannot instantiate " + getClass() + " as " + + "adapter producing " + witness); + top = witness; + mh = mh.asType(mh.type().changeReturnType(erase(witness))); + } + m_topType = top; + m_topErased = erased; + m_underAdapter = null; + m_fetchHandle = mh; + return; + } + + /* + * Very well then, it is not a leaf adapter. + */ + + requireNonNull(over, + getClass() + " instantiated with null 'over' but is " + + "a non-leaf adapter"); + if ( null != using ) + throw new IllegalArgumentException( + getClass() + " instantiated with non-null 'using' but is " + + "not a leaf adapter"); + + Configuration.NonLeaf nonLeaf = (Configuration.NonLeaf)configuration; + + Type[] refined = refine(over.m_topType, nonLeaf.m_under, nonLeaf.m_top); + Type under = refined[0]; + Type top = refined[1]; + + if ( null != witness ) + { + if ( ! isSubtype(witness, top) ) + throw new IllegalArgumentException( + "cannot instantiate " + getClass() + " as " + + "adapter producing " + witness); + top = witness; + } + + m_topType = top; + + @SuppressWarnings("unchecked") + Class erased = (Class)erase(top); + m_topErased = erased; + + /* + * 'over' was declared as a raw type to make this constructor also + * usable from the Array subclass constructor. Here, being an ordinary + * composing adapter, we reassert that 'over' is parameterized , as + * the ordinary subclass constructor will have ensured. + */ + @SuppressWarnings("unchecked") + Adapter underAdapter = over; + m_underAdapter = underAdapter; + + MethodHandle producer = nonLeaf.m_adapt.bindTo(this); + MethodHandle fetcher = over.m_fetchHandle; + + MethodType mt = producer + .type() + .changeReturnType(erased) + .changeParameterType(1, erase(under)); + + producer = producer.asType(mt); + fetcher = fetcher.asType( + fetcher.type().changeReturnType(mt.parameterType(1))); + + mt = fetcher + .type() // this is the expected type of a fetcher, but it needs + .changeReturnType(erased); // new return type. After collect we will + fetcher = collectArguments(producer, 1, fetcher); // need 1st arg twice + fetcher = permuteArguments(fetcher, mt, 0, 0, 1, 2, 3, 4); // so do that + + m_fetchHandle = fetcher; + } + + /** + * Specifies, for a leaf adapter (one not composed over a lower adapter), + * the form in which the value fetched from PostgreSQL will be presented to + * it (or how it will produce a value to be stored to PostgreSQL). + *

+ * At this level, an adapter is free to use {@code Via.CHAR} and treat + * {@code char} internally as a 16-bit unsigned integral type with no other + * special meaning. If an adapter will return an unsigned 16-bit + * type, it should extend either {@code AsShort.Unsigned} or {@code AsChar}, + * based on whether the value it returns represents UTF-16 character data. + */ + protected enum Via + { + DATUM ( Datum.Input.class, "getDatum"), + INT64SX ( long.class, "getLongSignExtended"), + INT64ZX ( long.class, "getLongZeroExtended"), + DOUBLE ( double.class, "getDouble"), + INT32SX ( int.class, "getIntSignExtended"), + INT32ZX ( int.class, "getIntZeroExtended"), + FLOAT ( float.class, "getFloat"), + SHORT ( short.class, "getShort"), + CHAR ( char.class, "getChar"), + BYTE ( byte.class, "getByte"), + BOOLEAN ( boolean.class, "getBoolean"); + + Via(Class type, String method) + { + try + { + MethodHandle h; + h = lookup().findVirtual(Datum.Accessor.class, method, + type.isPrimitive() + ? methodType( + type, Object.class, int.class) + : methodType( + type, Object.class, int.class, Attribute.class)); + + if ( type.isPrimitive() ) + h = dropArguments(h, 3, Attribute.class); + + m_handle = h; + } + catch ( ReflectiveOperationException e ) + { + throw wrapped(e); + } + } + + MethodHandle m_handle; + } + + @Override + public String toString() + { + Class c = getClass(); + Module m = c.getModule(); + return + c.getModule().getName() + "/" + + c.getCanonicalName().substring(1 + c.getPackageName().length() ) + + " to produce " + topType(); + } + + /** + * Method that a leaf {@code Adapter} must implement to indicate whether it + * is capable of fetching a given PostgreSQL type. + *

+ * In a composing adapter, this default implementation delegates to + * the adapter beneath. + * @throws UnsupportedOperationException if called in a leaf adapter + */ + public boolean canFetch(RegType pgType) + { + if ( null != m_underAdapter ) + return m_underAdapter.canFetch(pgType); + throw new UnsupportedOperationException( + toString() + " is a leaf adapter and does not override canFetch"); + } + + /** + * Method that an {@code Adapter} may override to indicate whether it + * is capable of fetching a given PostgreSQL attribute. + *

+ * If not overridden, this implementation delegates to the adapter beneath, + * if composed; in a leaf adapter, it delegates to + * {@link #canFetch(RegType) canFetch} for the attribute's declared + * PostgreSQL type. + */ + public boolean canFetch(Attribute attr) + { + if ( null != m_underAdapter ) + return m_underAdapter.canFetch(attr); + return canFetch(attr.type()); + } + + /** + * Method that an {@code Adapter} must implement to indicate whether it + * is capable of returning some usable representation of SQL null values. + *

+ * An {@code Adapter} that cannot should only be used with values that + * are known never to be null; it will throw an exception if asked to fetch + * a value that is null. + *

+ * An adapter usable with null values can be formed by composing, for + * example, an adapter producing {@code Optional} over an adapter that + * cannot fetch nulls. + */ + public abstract boolean canFetchNull(); + + /** + * A static method to indicate the type returned by a given {@code Adapter} + * subclass, based only on the type information recorded for it by the Java + * compiler. + *

+ * The type returned could contain free type variables that may be given + * concrete values when the instance {@link #topType() topType} method is + * called on a particular instance of the class. + *

+ * When cls is a subclass of {@code Primitive}, this method + * returns the {@code Class} object for the actual primitive type, + * not the boxed type. + */ + public static Type topType(Class cls) + { + Type[] params = specialization(cls, Adapter.class); + if ( null == params ) + throw new IllegalArgumentException( + cls + " does not extend Adapter"); + Type top = params[0]; + if ( Primitive.class.isAssignableFrom(cls) ) + { + top = methodType((Class)top).unwrap().returnType(); + assert ((Class)top).isPrimitive(); + } + return top; + } + + /** + * The full generic {@link Type Type} this Adapter presents to Java. + *

+ * Unlike the static method, this instance method, on an adapter formed + * by composition, returns the actual type obtained by unifying + * the "under" adapter's top type with the top adapter's "under" type, then + * making the indicated substitutions in the top adapter's "top" type. + *

+ * Likewise, for an adapter constructed with an array contract and an + * adapter for the element type, the element adapter's "top" type is unified + * with the contract's element type, and this method returns the contract's + * result type with the same substitutions made. + */ + public Type topType() + { + return m_topType; + } + + /** + * A static method to indicate the "under" type expected by a given + * {@code Adapter} subclass that is intended for composition over another + * adapter, based only on the type information recorded for it by the Java + * compiler. + *

+ * The type returned could contain free type variables. + */ + public static Type underType(Class cls) + { + Type[] params = specialization(cls, Adapter.class); + if ( null == params ) + throw new IllegalArgumentException( + cls + " does not extend Adapter"); + return params[1]; + } + + /** + * A class that is returned by the {@link #configure configure} method, + * intended for use during an {@code Adapter} subclass's static + * initialization, and must be supplied to the constructor when instances + * of the class are created. + */ + protected static abstract class Configuration + { + final Class m_class; + /** + * In the case of a primitive-typed adapter, this will really be the + * primitive Class object, not the corresponding boxed class. + */ + final Type m_top; + + Configuration(Class cls, Type top) + { + m_class = cls; + m_top = top; + } + + static class Leaf extends Configuration + { + final MethodHandle m_fetch; + + Leaf(Class cls, Type top, MethodHandle fetcher) + { + super(cls, top); + m_fetch = fetcher; + } + } + + static class NonLeaf extends Configuration + { + /** + * For an adapter meant to compose over a primitive-typed one, this + * is the actual primitive class object for the under-adapter's + * expected return type, not the boxed counterpart. + */ + final Type m_under; + final MethodHandle m_adapt; + + NonLeaf( + Class cls, Type top, Type under, + MethodHandle fetcher) + { + super(cls, top); + m_under = under; + m_adapt = fetcher; + } + } + } + + /** + * Throws a security exception if permission to configure an adapter + * isn't held. + *

+ * For the time being, there is only Permission("*", "fetch"), so this needs + * no parameters and can use a static instance of the permission. + */ + @SuppressWarnings("removal") // JEP 411 + private static void checkAllowed() + { + AccessController.checkPermission(Permission.INSTANCE); + } + + /** + * Method that must be called in static initialization of an {@code Adapter} + * subclass, producing a {@code Configuration} object that must be passed + * to the constructor when creating an instance. + *

+ * If the adapter class is in a named module, its containing package must be + * exported to at least {@code org.postgresql.pljava}. + *

+ * When a leaf adapter (one that does not compose over some other adapter, + * but acts directly on PostgreSQL datums) is configured, the necessary + * {@link Permission Permission} is checked. + * @param cls The Adapter subclass being configured. + * @param via null for a composing (non-leaf) adapter; otherwise a value + * of the {@link Via} enumeration, indicating how the underlying PostgreSQL + * datum will be presented to the adapter. + * @throws SecurityException if the class being configured represents a leaf + * adapter and the necessary permission is not held. + */ + protected static Configuration configure( + Class cls, Via via) + { + Adapter.class.getModule().addReads(cls.getModule()); + Type top = topType(cls); + Type under = underType(cls); + Class topErased = erase(top); + Class underErased = erase(under); + + MethodHandle underFetcher = null; + String fetchName; + Predicate fetchPredicate; + + if ( Void.class == underErased ) + { + checkAllowed(); + requireNonNull(via, "a leaf Adapter must have a non-null Via"); + underFetcher = via.m_handle; + underErased = underFetcher.type().returnType(); + Class[] params = { Attribute.class, underErased }; + final String fn = fetchName = "fetch"; + fetchPredicate = m -> fn.equals(m.getName()) + && Arrays.equals(m.getParameterTypes(), params); + } + else + { + if ( null != via ) + throw new IllegalArgumentException( + "a non-leaf (U is not Void) adapter must have null Via"); + final String fn = fetchName = "adapt"; + MethodType mt = methodType(underErased); + if ( mt.hasWrappers() ) // Void, handled above, won't be seen here + { + Class underOrig = underErased; + Class underPrim = mt.unwrap().returnType(); + fetchPredicate = m -> + { + if ( ! fn.equals(m.getName()) ) + return false; + Class[] ptypes = m.getParameterTypes(); + return + 2 == ptypes.length && Attribute.class == ptypes[0] && + ( underOrig == ptypes[1] || underPrim == ptypes[1] ); + }; + } + else + { + Class[] params = { Attribute.class, underErased }; + fetchPredicate = m -> fn.equals(m.getName()) + && Arrays.equals(m.getParameterTypes(), params); + } + } + + Method[] fetchCandidates = stream(cls.getMethods()) + .filter(fetchPredicate).toArray(Method[]::new); + if ( 1 < fetchCandidates.length ) + fetchCandidates = stream(fetchCandidates) + .filter(m -> ! m.isBridge()).toArray(Method[]::new); + if ( 1 != fetchCandidates.length ) + throw new IllegalArgumentException( + cls + " lacks " + fetchName + " method with the " + + "expected signature"); + if ( ! topErased.isAssignableFrom(fetchCandidates[0].getReturnType()) ) + throw new IllegalArgumentException( + cls + " lacks " + fetchName + " method with the " + + "expected return type"); + + MethodHandle fetcher; + + try + { + fetcher = lookup().unreflect(fetchCandidates[0]); + } + catch ( IllegalAccessException e ) + { + throw new IllegalArgumentException( + cls + " has " + fetchName + " method that is inaccessible", + e); + } + + /* + * Adjust the return type. isAssignableFrom was already checked, so + * this can only be a no-op or a widening, to make sure the handle + * will fit invokeExact with the expected return type. + */ + fetcher = fetcher.asType(fetcher.type().changeReturnType(topErased)); + + if ( null != via ) + { + fetcher = collectArguments(fetcher, 2, underFetcher); + return new Configuration.Leaf(cls, top, fetcher); + } + + // unbound virtual handle's type includes receiver; 2nd param is index 2 + Class asFound = fetcher.type().parameterType(2); + if ( asFound.isPrimitive() ) + under = underErased = asFound; + + return new Configuration.NonLeaf(cls, top, under, fetcher); + } + + /** + * Provided to serve as a superclass for a 'container' class that is used + * to group several related adapters without being instantiable + * as an adapter itself. + *

+ * By being technically a subclass of {@code Adapter}, the container class + * will have access to the protected {@code Configuration} class and + * {@code configure} method. + */ + public static abstract class Container extends Adapter + { + protected Container() + { + super(null, null, null, null); + } + } + + /** + * Superclass for adapters that fetch something and return it as a reference + * type T. + *

+ * The type variable U for the thing consumed gets no enforcement from + * the compiler, because any extending adapter class provides its own + * {@code T fetch(Attribute,something)} method, with no abstract version + * inherited from this class to constrain it. The method will be found + * reflectively by name and parameter types, so the "something" only has to + * match the type of the accessor method specified with {@code Via}, or the + * type returned by an underlying adapter that this one will be composed + * over. + *

+ * In particular, that means this is the class to extend even if using a + * primitive accessor method, or composing over an adapter that returns a + * primitive type, as long as this adapter will return a reference type T. + * Such an adapter simply declares that it extends {@code As} when + * based on a primitive accessor method, or {@code As} when + * composed over another adapter of primitive type, where boxed-class is the + * boxed counterpart of the other adapter's primitive type. + *

+ * When Java's reflection methods on generic types are used to compute + * the (non-erased) result type of a stack of composed adapters, the type + * variable U can be used in relating the input to the output type of each. + */ + public abstract static class As + extends Adapter + implements ArrayProto + { + private final MethodHandle m_fetchHandleErased; + + /** + * Constructor for a simple leaf {@code Adapter}, or a composing + * (non-leaf) {@code Adapter} when passed another adapter over which + * it should be composed. + * @param c Configuration instance generated for this class + * @param over null for a leaf Adapter, otherwise another Adapter + * to compose this one over + * @param witness if not null, the top type the resulting + * adapter will produce, if a Class object can specify that more + * precisely than the default typing rules. + */ + protected As(Configuration c, Adapter over, Class witness) + { + super(c, over, null, witness); + + MethodHandle mh = m_fetchHandle; + m_fetchHandleErased = + mh.asType(mh.type().changeReturnType(Object.class)); + } + + /** + * Constructor for a leaf {@code Adapter} that is based on + * a {@code Contract}. + * @param using the scalar Contract that will be used to produce + * the value returned + * @param witness if not null, the top type the resulting + * adapter will produce, if a Class object can specify that more + * precisely than the default typing rules. + * @param c Configuration instance generated for this class + */ + protected As( + Contract.Scalar using, Class witness, Configuration c) + { + super(c, null, using, witness); + + MethodHandle mh = m_fetchHandle; + m_fetchHandleErased = + mh.asType(mh.type().changeReturnType(Object.class)); + } + + /** + * Used only by the {@code Array} subclass below. + *

+ * The contract and element adapter here are raw types. The accessible + * subclass constructors will permit only compatible combinations of + * parameterized types. + */ + private As( + Contract.Array using, Adapter adapter, Type witness, + Configuration c) + { + super(c, null, using, + witness != null ? witness : refinement(using, adapter)); + + MethodHandle mh = m_fetchHandle; + m_fetchHandleErased = + mh.asType(mh.type().changeReturnType(Object.class)); + } + + /** + * Returns the type that will be produced by the array contract + * using when applied to the element-type adapter + * adapter. + *

+ * Determined by unifying the contract's element type with + * the result type of adapter, then repeating any resulting + * substitutions in the contract's result type. + */ + private static Type refinement(Contract.Array using, Adapter adapter) + { + Type[] unrefined = + specialization(using.getClass(), Contract.Array.class); + Type result = unrefined[0]; + Type element = unrefined[1]; + /* + * A Contract that expects a primitive-typed adapter must already be + * specialized to one primitive type, so there is nothing to refine. + */ + if ( adapter instanceof Primitive ) + return result; + return refine(adapter.topType(), element, result)[1]; + } + + /** + * Method invoked internally when this {@code Adapter} is used to fetch + * a value; not intended for use in application code. + */ + public final T fetch( + Datum.Accessor acc, B buffer, int offset, Attribute a) + { + try + { + return (T) + m_fetchHandleErased.invokeExact(a, acc, buffer, offset, a); + } + catch ( Throwable t ) + { + throw wrapped(t); + } + } + + /** + * A default implementation of {@code canFetchNull} that unconditionally + * returns true. + *

+ * An adapter that extends this class, if it does not override + * {@link #fetchNull fetchNull}, will simply map any SQL null value + * to a Java null. + */ + @Override + public boolean canFetchNull() + { + return true; + } + + /** + * Determines the value to which SQL null should be mapped. + *

+ * If not overridden, this implementation returns Java null. + */ + public T fetchNull(Attribute a) + { + return null; + } + + /** + * Allocate an array of the given length with this adapter's + * result type as its component type. + */ + @SuppressWarnings("unchecked") + public T[] arrayOf(int length) + { + return (T[])newInstance(m_topErased, length); + } + } + + /** + * Abstract supertype of array adapters. + *

+ * Instantiating an array adapter requires supplying an array contract + * and a compatible adapter for the element type, to be stored in the + * corresponding final fields here, which are declared with raw types. + * The several accessible constructors enforce the various compatible + * parameterizations for the two arguments. + */ + public abstract static class Array extends As + { + /** + * The {@code Contract.Array} that this array adapter will use, + * together with the supplied element-type adapter. + *

+ * Declared here as the raw type. The accessible constructors enforce + * the compatibility requirements between this and the supplied + * element adapter. + */ + protected final Contract.Array m_contract; + + /** + * The {@code Adapter} that this array adapter will use for the array's + * element type, together with the supplied contract. + *

+ * Declared here as the raw type. The accessible constructors enforce + * the compatibility requirements between this and the supplied + * contract. + */ + protected final Adapter m_elementAdapter; + + /** + * Constructor for a leaf array {@code Adapter} that is based on + * a {@code Contract.Array} and a reference-returning {@code Adapter} + * for the element type. + * @param using the array Contract that will be used to produce + * the value returned + * @param adapter an Adapter producing a representation of the array's + * element type + * @param witness if not null, the top type the resulting + * adapter will produce, if a Type object can specify that more + * precisely than the default typing rules. + * @param c Configuration instance generated for this class + */ + protected Array( + Contract.Array> using, As adapter, + Type witness, Configuration c) + { + super(using, adapter, witness, c); + m_contract = using; + m_elementAdapter = adapter; + } + + /** + * Constructor for a leaf array {@code Adapter} that is based on + * a {@code Contract.Array} and a long-returning {@code Adapter} + * for the element type. + * @param using the array Contract that will be used to produce + * the value returned + * @param adapter an Adapter producing a representation of the array's + * element type + * @param witness if not null, the top type the resulting + * adapter will produce, if a Type object can specify that more + * precisely than the default typing rules. + * @param c Configuration instance generated for this class + */ + protected Array( + Contract.Array> using, AsLong adapter, + Type witness, Configuration c) + { + super(using, adapter, witness, c); + m_contract = using; + m_elementAdapter = adapter; + } + + /** + * Constructor for a leaf array {@code Adapter} that is based on + * a {@code Contract.Array} and a double-returning {@code Adapter} + * for the element type. + * @param using the array Contract that will be used to produce + * the value returned + * @param adapter an Adapter producing a representation of the array's + * element type + * @param witness if not null, the top type the resulting + * adapter will produce, if a Type object can specify that more + * precisely than the default typing rules. + * @param c Configuration instance generated for this class + */ + protected Array( + Contract.Array> using, AsDouble adapter, + Type witness, Configuration c) + { + super(using, adapter, witness, c); + m_contract = using; + m_elementAdapter = adapter; + } + + /** + * Constructor for a leaf array {@code Adapter} that is based on + * a {@code Contract.Array} and an int-returning {@code Adapter} + * for the element type. + * @param using the array Contract that will be used to produce + * the value returned + * @param adapter an Adapter producing a representation of the array's + * element type + * @param witness if not null, the top type the resulting + * adapter will produce, if a Type object can specify that more + * precisely than the default typing rules. + * @param c Configuration instance generated for this class + */ + protected Array( + Contract.Array> using, AsInt adapter, + Type witness, Configuration c) + { + super(using, adapter, witness, c); + m_contract = using; + m_elementAdapter = adapter; + } + + /** + * Constructor for a leaf array {@code Adapter} that is based on + * a {@code Contract.Array} and a float-returning {@code Adapter} + * for the element type. + * @param using the array Contract that will be used to produce + * the value returned + * @param adapter an Adapter producing a representation of the array's + * element type + * @param witness if not null, the top type the resulting + * adapter will produce, if a Type object can specify that more + * precisely than the default typing rules. + * @param c Configuration instance generated for this class + */ + protected Array( + Contract.Array> using, AsFloat adapter, + Type witness, Configuration c) + { + super(using, adapter, witness, c); + m_contract = using; + m_elementAdapter = adapter; + } + + /** + * Constructor for a leaf array {@code Adapter} that is based on + * a {@code Contract.Array} and a short-returning {@code Adapter} + * for the element type. + * @param using the array Contract that will be used to produce + * the value returned + * @param adapter an Adapter producing a representation of the array's + * element type + * @param witness if not null, the top type the resulting + * adapter will produce, if a Type object can specify that more + * precisely than the default typing rules. + * @param c Configuration instance generated for this class + */ + protected Array( + Contract.Array> using, AsShort adapter, + Type witness, Configuration c) + { + super(using, adapter, witness, c); + m_contract = using; + m_elementAdapter = adapter; + } + + /** + * Constructor for a leaf array {@code Adapter} that is based on + * a {@code Contract.Array} and a char-returning {@code Adapter} + * for the element type. + * @param using the array Contract that will be used to produce + * the value returned + * @param adapter an Adapter producing a representation of the array's + * element type + * @param witness if not null, the top type the resulting + * adapter will produce, if a Type object can specify that more + * precisely than the default typing rules. + * @param c Configuration instance generated for this class + */ + protected Array( + Contract.Array> using, AsChar adapter, + Type witness, Configuration c) + { + super(using, adapter, witness, c); + m_contract = using; + m_elementAdapter = adapter; + } + + /** + * Constructor for a leaf array {@code Adapter} that is based on + * a {@code Contract.Array} and a byte-returning {@code Adapter} + * for the element type. + * @param using the array Contract that will be used to produce + * the value returned + * @param adapter an Adapter producing a representation of the array's + * element type + * @param witness if not null, the top type the resulting + * adapter will produce, if a Type object can specify that more + * precisely than the default typing rules. + * @param c Configuration instance generated for this class + */ + protected Array( + Contract.Array> using, AsByte adapter, + Type witness, Configuration c) + { + super(using, adapter, witness, c); + m_contract = using; + m_elementAdapter = adapter; + } + + /** + * Constructor for a leaf array {@code Adapter} that is based on + * a {@code Contract.Array} and a boolean-returning {@code Adapter} + * for the element type. + * @param using the array Contract that will be used to produce + * the value returned + * @param adapter an Adapter producing a representation of the array's + * element type + * @param witness if not null, the top type the resulting + * adapter will produce, if a Type object can specify that more + * precisely than the default typing rules. + * @param c Configuration instance generated for this class + */ + protected Array( + Contract.Array> using, AsBoolean adapter, + Type witness, Configuration c) + { + super(using, adapter, witness, c); + m_contract = using; + m_elementAdapter = adapter; + } + } + + /** + * Ancestor class for adapters that fetch something and return it as + * a Java primitive type. + *

+ * Subclasses for integral types, namely {@code AsLong}, {@code asInt}, + * and {@code AsShort}, cannot be extended directly, but only via their + * {@code Signed} or {@code Unsigned} nested subclasses, according to how + * the value is meant to be used. Nothing can change how Java treats the + * primitive types (always as signed), but the {@code Signed} and + * {@code Unsigned} subclasses here offer methods for the operations that + * differ, allowing the right behavior to be achieved if those methods + * are used. + *

+ * Whether an adapter extends {@code AsShort.Unsigned} or {@code AsChar} + * (also an unsigned 16-bit type) should be determined based on whether + * the resulting value is meant to have a UTF-16 character meaning. + */ + public abstract static class Primitive + extends Adapter + implements ArrayProto + { + private > Primitive(Configuration c, A over) + { + super(c, over, null, null); + } + + /** + * Implementation of {@code canFetchNull} that unconditionally returns + * false, as primitive adapters have no reliably distinguishable values + * to which SQL null can be mapped. + */ + @Override + public boolean canFetchNull() + { + return false; + } + } + + /** + * Abstract superclass of signed and unsigned primitive {@code long} + * adapters. + */ + public abstract static class AsLong extends Primitive + implements TwosComplement + { + private > AsLong(Configuration c, A over) + { + super(c, over); + } + + /** + * Method invoked internally when this {@code Adapter} is used to fetch + * a value; not intended for use in application code. + */ + public final long fetch( + Datum.Accessor acc, B buffer, int offset, Attribute a) + { + try + { + return (long) + m_fetchHandle.invokeExact(a, acc, buffer, offset, a); + } + catch ( Throwable t ) + { + throw wrapped(t); + } + } + + /** + * Determines the mapping of SQL null. + *

+ * If not overridden, this implementation throws an + * {@code SQLDataException} with {@code SQLSTATE 22002}, + * {@code null_value_no_indicator_parameter}. + */ + public long fetchNull(Attribute a) + { + throw wrapped(new SQLDataException( + "SQL NULL cannot be returned as Java long", "22002")); + } + + /** + * Abstract superclass of signed primitive {@code long} adapters. + */ + public abstract static class Signed extends AsLong + implements TwosComplement.Signed + { + protected > Signed(Configuration c, A over) + { + super(c, over); + } + } + + /** + * Abstract superclass of unsigned primitive {@code long} adapters. + */ + public abstract static class Unsigned extends AsLong + implements TwosComplement.Unsigned + { + protected > Unsigned( + Configuration c, A over) + { + super(c, over); + } + } + } + + /** + * Abstract superclass of primitive {@code double} adapters. + */ + public abstract static class AsDouble + extends Primitive + { + protected > AsDouble(Configuration c, A over) + { + super(c, over); + } + + /** + * Method invoked internally when this {@code Adapter} is used to fetch + * a value; not intended for use in application code. + */ + public final double fetch( + Datum.Accessor acc, B buffer, int offset, Attribute a) + { + try + { + return (double) + m_fetchHandle.invokeExact(a, acc, buffer, offset, a); + } + catch ( Throwable t ) + { + throw wrapped(t); + } + } + + /** + * Determines the mapping of SQL null. + *

+ * If not overridden, this implementation throws an + * {@code SQLDataException} with {@code SQLSTATE 22002}, + * {@code null_value_no_indicator_parameter}. + */ + public double fetchNull(Attribute a) + { + throw wrapped(new SQLDataException( + "SQL NULL cannot be returned as Java double", "22002")); + } + } + + /** + * Abstract superclass of signed and unsigned primitive {@code int} + * adapters. + */ + public abstract static class AsInt extends Primitive + implements TwosComplement + { + private > AsInt(Configuration c, A over) + { + super(c, over); + } + + /** + * Method invoked internally when this {@code Adapter} is used to fetch + * a value; not intended for use in application code. + */ + public final int fetch( + Datum.Accessor acc, B buffer, int offset, Attribute a) + { + try + { + return (int) + m_fetchHandle.invokeExact(a, acc, buffer, offset, a); + } + catch ( Throwable t ) + { + throw wrapped(t); + } + } + + /** + * Determines the mapping of SQL null. + *

+ * If not overridden, this implementation throws an + * {@code SQLDataException} with {@code SQLSTATE 22002}, + * {@code null_value_no_indicator_parameter}. + */ + public int fetchNull(Attribute a) + { + throw wrapped(new SQLDataException( + "SQL NULL cannot be returned as Java int", "22002")); + } + + /** + * Abstract superclass of signed primitive {@code int} adapters. + */ + public abstract static class Signed extends AsInt + implements TwosComplement.Signed + { + protected > Signed(Configuration c, A over) + { + super(c, over); + } + } + + /** + * Abstract superclass of unsigned primitive {@code int} adapters. + */ + public abstract static class Unsigned extends AsInt + implements TwosComplement.Unsigned + { + protected > Unsigned( + Configuration c, A over) + { + super(c, over); + } + } + } + + /** + * Abstract superclass of primitive {@code float} adapters. + */ + public abstract static class AsFloat extends Primitive + { + protected > AsFloat(Configuration c, A over) + { + super(c, over); + } + + /** + * Method invoked internally when this {@code Adapter} is used to fetch + * a value; not intended for use in application code. + */ + public final float fetch( + Datum.Accessor acc, B buffer, int offset, Attribute a) + { + try + { + return (float) + m_fetchHandle.invokeExact(a, acc, buffer, offset, a); + } + catch ( Throwable t ) + { + throw wrapped(t); + } + } + + /** + * Determines the mapping of SQL null. + *

+ * If not overridden, this implementation throws an + * {@code SQLDataException} with {@code SQLSTATE 22002}, + * {@code null_value_no_indicator_parameter}. + */ + public float fetchNull(Attribute a) + { + throw wrapped(new SQLDataException( + "SQL NULL cannot be returned as Java float", "22002")); + } + } + + /** + * Abstract superclass of signed and unsigned primitive {@code short} + * adapters. + */ + public abstract static class AsShort extends Primitive + implements TwosComplement + { + private > AsShort(Configuration c, A over) + { + super(c, over); + } + + /** + * Method invoked internally when this {@code Adapter} is used to fetch + * a value; not intended for use in application code. + */ + public final short fetch( + Datum.Accessor acc, B buffer, int offset, Attribute a) + { + try + { + return (short) + m_fetchHandle.invokeExact(a, acc, buffer, offset, a); + } + catch ( Throwable t ) + { + throw wrapped(t); + } + } + + /** + * Determines the mapping of SQL null. + *

+ * If not overridden, this implementation throws an + * {@code SQLDataException} with {@code SQLSTATE 22002}, + * {@code null_value_no_indicator_parameter}. + */ + public short fetchNull(Attribute a) + { + throw wrapped(new SQLDataException( + "SQL NULL cannot be returned as Java short", "22002")); + } + + /** + * Abstract superclass of signed primitive {@code short} adapters. + */ + public abstract static class Signed extends AsShort + implements TwosComplement.Signed + { + protected > Signed(Configuration c, A over) + { + super(c, over); + } + } + + /** + * Abstract superclass of unsigned primitive {@code short} adapters. + */ + public abstract static class Unsigned extends AsShort + implements TwosComplement.Unsigned + { + protected > Unsigned( + Configuration c, A over) + { + super(c, over); + } + } + } + + /** + * Abstract superclass of primitive {@code char} adapters. + */ + public abstract static class AsChar extends Primitive + { + protected > AsChar(Configuration c, A over) + { + super(c, over); + } + + /** + * Method invoked internally when this {@code Adapter} is used to fetch + * a value; not intended for use in application code. + */ + public final char fetch( + Datum.Accessor acc, B buffer, int offset, Attribute a) + { + try + { + return (char) + m_fetchHandle.invokeExact(a, acc, buffer, offset, a); + } + catch ( Throwable t ) + { + throw wrapped(t); + } + } + + /** + * Determines the mapping of SQL null. + *

+ * If not overridden, this implementation throws an + * {@code SQLDataException} with {@code SQLSTATE 22002}, + * {@code null_value_no_indicator_parameter}. + */ + public char fetchNull(Attribute a) + { + throw wrapped(new SQLDataException( + "SQL NULL cannot be returned as Java char", "22002")); + } + } + + /** + * Abstract superclass of signed and unsigned primitive {@code byte} + * adapters. + */ + public abstract static class AsByte extends Primitive + implements TwosComplement + { + private > AsByte(Configuration c, A over) + { + super(c, over); + } + + /** + * Method invoked internally when this {@code Adapter} is used to fetch + * a value; not intended for use in application code. + */ + public final byte fetch( + Datum.Accessor acc, B buffer, int offset, Attribute a) + { + try + { + return (byte) + m_fetchHandle.invokeExact(a, acc, buffer, offset, a); + } + catch ( Throwable t ) + { + throw wrapped(t); + } + } + + /** + * Determines the mapping of SQL null. + *

+ * If not overridden, this implementation throws an + * {@code SQLDataException} with {@code SQLSTATE 22002}, + * {@code null_value_no_indicator_parameter}. + */ + public byte fetchNull(Attribute a) + { + throw wrapped(new SQLDataException( + "SQL NULL cannot be returned as Java byte", "22002")); + } + + /** + * Abstract superclass of signed primitive {@code byte} adapters. + */ + public abstract static class Signed extends AsByte + implements TwosComplement.Signed + { + protected > Signed(Configuration c, A over) + { + super(c, over); + } + } + + /** + * Abstract superclass of unsigned primitive {@code byte} adapters. + */ + public abstract static class Unsigned extends AsByte + implements TwosComplement.Unsigned + { + protected > Unsigned( + Configuration c, A over) + { + super(c, over); + } + } + } + + /** + * Abstract superclass of primitive {@code boolean} adapters. + */ + public abstract static class AsBoolean + extends Primitive + { + protected > AsBoolean(Configuration c, A over) + { + super(c, over); + } + + /** + * Method invoked internally when this {@code Adapter} is used to fetch + * a value; not intended for use in application code. + */ + public final boolean fetch( + Datum.Accessor acc, B buffer, int offset, Attribute a) + { + try + { + return (boolean) + m_fetchHandle.invokeExact(a, acc, buffer, offset, a); + } + catch ( Throwable t ) + { + throw wrapped(t); + } + } + + /** + * Determines the mapping of SQL null. + *

+ * If not overridden, this implementation throws an + * {@code SQLDataException} with {@code SQLSTATE 22002}, + * {@code null_value_no_indicator_parameter}. + */ + public boolean fetchNull(Attribute a) + { + throw wrapped(new SQLDataException( + "SQL NULL cannot be returned as Java boolean", "22002")); + } + } + + /** + * A marker interface to be extended by functional interfaces that + * serve as ADT contracts. + *

+ * It facilitates the declaration of "dispenser" interfaces by which + * one contract can rely on others. + * @param the type to be returned by an instance of the contract + */ + public interface Contract + { + /** + * Marker interface for contracts for simple scalar types. + */ + interface Scalar extends Contract + { + } + + /** + * Base for functional interfaces that serve as contracts + * for array-like types. + *

+ * The distinguishing feature is an associated {@code Adapter} handling + * the element type of the array-like type. This form of contract may + * be useful for range and multirange types as well as for arrays. + * @param the type to be returned by an instance of the contract. + * @param the type returned by an associated {@code Adapter} for + * the element type (or the boxed type, if the adapter returns + * a primitive type). + * @param The subtype of {@code Adapter} that the contract requires; + * reference-returning ({@code As}) and all of the primitive-returning + * types must be distinguished. + */ + public interface Array> extends Contract + { + /** + * Constructs a representation T representing + * a PostgreSQL array. + * @param nDims the number of array dimensions (always one half of + * {@code dimsAndBounds.length}, but passed separately for + * convenience) + * @param dimsAndBounds the first nDims elements + * represent the total number of valid indices for each dimension, + * and the next nDims elements represent the first valid index for each + * dimension. For example, if nDims is 3, dimsAndBounds[1] is 6, and + * dimsAndBounds[4] is -2, then the array's second dimension uses + * indices in [-2,4). The array is a copy and may be used freely. + * @param adapter an Adapter producing a representation of + * the array's element type. + * @param slot A TupleTableSlot with multiple components accessible + * by a (single, flat) index, all of the same type, described by + * a one-element TupleDescriptor. + */ + T construct( + int nDims, int[] dimsAndBounds, A adapter, Indexed slot) + throws SQLException; + } + } + + /** + * Functional interface able to dispense one instance of an ADT by passing + * its constituent values to a supplied {@code Contract} and returning + * whatever that returns. + */ + @FunctionalInterface + public interface Dispenser> + { + T get(U constructor); + } + + /** + * Functional interface able to dispense multiple instances of an ADT + * identified by a zero-based index, passing the its constituent values + * to a supplied {@code Contract} and returning whatever that returns. + */ + @FunctionalInterface + public interface PullDispenser> + { + T get(int index, U constructor); + } + + private static RuntimeException wrapped(Throwable t) + { + if ( t instanceof RuntimeException ) + return (RuntimeException)t; + if ( t instanceof Error ) + throw (Error)t; + return new AdapterException(t.getMessage(), t); + } + + /** + * A lightweight unchecked exception used to wrap checked ones + * (often {@link SQLException}) in settings where checked ones are a bother. + *

+ * The idea may or may not be worth keeping, and either way, this particular + * exception might not be part of any final API. + */ + public static class AdapterException extends RuntimeException + { + AdapterException(String message, Throwable cause) + { + super(message, cause, true, false); + } + + /** + * Unwraps this wrapper's cause and returns it, if it is an instance of + * the exception type declared; otherwise, just throws this + * wrapper again. + */ + public X unwrap(Class declared) + { + Throwable t = getCause(); + if ( declared.isInstance(t) ) + return declared.cast(t); + throw this; + } + } + + /** + * A permission allowing the creation of a leaf {@code Adapter}. + *

+ * The proper spelling in a policy file is + * {@code org.postgresql.pljava.Adapter$Permission}. + *

+ * For the time being, only {@code "*"} is allowed as the name, + * and only {@code "fetch"} as the actions. + *

+ * Only a "leaf" adapter (one that will interact with PostgreSQL datum + * values directly) requires permission. Definition of composing adapters + * (those that can be applied over another adapter and transform the Java + * values somehow) is unrestricted. + */ + public static final class Permission extends java.security.Permission + { + private static final long serialVersionUID = 1L; + + /** + * An instance of this permission (not a singleton, merely one among + * possible others). + */ + static final Permission INSTANCE = new Permission("*", "fetch"); + + public Permission(String name, String actions) + { + super("*"); + if ( ! ( "*".equals(name) && "fetch".equals(actions) ) ) + throw new IllegalArgumentException( + "the only currently-allowed name and actions are " + + "* and fetch, not " + name + " and " + actions); + } + + @Override + public boolean equals(Object other) + { + return other instanceof Permission; + } + + @Override + public int hashCode() + { + return 131129; + } + + @Override + public String getActions() + { + return "fetch"; + } + + @Override + public PermissionCollection newPermissionCollection() + { + return new Collection(); + } + + @Override + public boolean implies(java.security.Permission p) + { + return p instanceof Permission; + } + + static class Collection extends PermissionCollection + { + private static final long serialVersionUID = 1L; + + Permission the_permission = null; + + @Override + public void add(java.security.Permission p) + { + if ( isReadOnly() ) + throw new SecurityException( + "attempt to add a Permission to a readonly " + + "PermissionCollection"); + + if ( ! (p instanceof Permission) ) + throw new IllegalArgumentException( + "invalid in homogeneous PermissionCollection: " + p); + + if ( null == the_permission ) + the_permission = (Permission) p; + } + + @Override + public boolean implies(java.security.Permission p) + { + if ( null == the_permission ) + return false; + return the_permission.implies(p); + } + + @Override + public Enumeration elements() + { + if ( null == the_permission ) + return emptyEnumeration(); + return enumeration(List.of(the_permission)); + } + } + } + + /** + * Specification of a service supplied by the internals module for certain + * operations, such as specially instantiating array adapters based on + * {@code ArrayBuilder}s constructed here. + */ + public static abstract class Service + { + static final Service INSTANCE; + + static + { + INSTANCE = ServiceLoader.load( + Service.class.getModule().getLayer(), Service.class) + .findFirst().orElseThrow(() -> new ServiceConfigurationError( + "could not load PL/Java Adapter.Service")); + } + + static + Array buildArrayAdapter( + ArrayBuilder builder, TypeWrapper w) + { + return INSTANCE.buildArrayAdapterImpl(builder, w); + } + + /** + * Builds an array adapter, given an {@code ArrayBuilder} (which wraps + * this {@code Adapter} and can describe the resulting array type), and + * an {@code TypeWrapper}. + *

+ * The {@code TypeWrapper} is a contrivance so that the computed array + * type can be passed back up through the constructors in a non-racy + * way. + */ + protected abstract + Array buildArrayAdapterImpl( + ArrayBuilder builder, TypeWrapper w); + + /** + * An upcall from the implementation layer to obtain the + * {@code MultiArray} from an {@code ArrayBuilder} without cluttering + * the latter's exposed API. + */ + protected MultiArray multiArray(ArrayBuilder builder) + { + return builder.multiArray(); + } + + /** + * An upcall from the implementation layer to obtain the + * {@code Adapter} wrapped by an {@code ArrayBuilder} without cluttering + * the latter's exposed API. + */ + protected Adapter adapter(ArrayBuilder builder) + { + return builder.m_adapter; + } + } + + /** + * A class that sneakily implements {@link Type} just so it can be passed + * up through the witness parameter of existing constructors, + * and carry the computed type of an array adapter to be constructed. + *

+ * Can only be instantiated here, to limit the ability for arbitrary code + * to supply computed (or miscomputed) types. + *

+ * The implementation layer will call {@link #setWrappedType setWrappedType} + * and then pass the wrapper to the appropriate adapter constructor. + * @hidden + */ + public static class TypeWrapper implements Type + { + @Override + public String getTypeName() + { + return "(a PL/Java TypeWrapper)"; + } + + private Type wrapped; + + private TypeWrapper() { } + + public void setWrappedType(Type t) + { + wrapped = t; + } + } + + /** + * Mixin allowing properly-typed array adapters of various dimensionalities + * to be derived from an adapter for the array component type. + *

+ * If a is an adapter producing type T, then + * {@code a.a4().a2()} is an {@code ArrayBuilder} that can build a + * six-dimensional array adapter producing type T[][][][][][]. + * + * @param Type of a one-dimension array of the component type; the type + * a builder obtained with a1() would build. + */ + public interface ArrayProto + { + /** + * Returns a builder that will make an array adapter returning + * a one-dimension Java array of this {@code Adapter}'s Java type. + */ + default ArrayBuilder a1() + { + return new ArrayBuilder(this, 1); + } + + /** + * Returns a builder that will make an array adapter returning + * a two-dimension Java array of this {@code Adapter}'s Java type. + */ + default ArrayBuilder a2() + { + return new ArrayBuilder(this, 2); + } + + /** + * Returns a builder that will make an array adapter returning + * a four-dimension Java array of this {@code Adapter}'s Java type. + */ + default ArrayBuilder a4() + { + return new ArrayBuilder(this, 4); + } + } + + /** + * Builder to derive properly-typed array adapters of various + * dimensionalities, first obtained from an {@link ArrayProto}. + * + * @param The array type represented by this builder. a1() will produce + * a builder for TA[], and so on. + * @param The type of a one-dimension array of the original component + * type; remains unchanged by increases to the dimensionality of TA. + */ + @SuppressWarnings("unchecked") + public static final class ArrayBuilder + { + final Adapter m_adapter; + private int m_dimensions; + + /** + * Records the adapter for the component type (necessarily an instance + * of {@code Adapter} but here typed as {@code ArrayProto} to simplify + * call sites), and the dimensionality of array to be built. + */ + ArrayBuilder(ArrayProto adapter, int dimensions) + { + m_adapter = (Adapter)requireNonNull(adapter); + m_dimensions = dimensions; + } + + /** + * Returns an array adapter that will produce arrays with the chosen + * number of dimensions, and the original adapter's + * {@link #topType() topType} as the component type. + */ + public Array build() + { + return Service.buildArrayAdapter(this, new TypeWrapper()); + } + + MultiArray multiArray() + { + return new MultiArray(m_adapter.topType(), m_dimensions); + } + + /** + * Adds one to the result-array dimensions of the {@code Adapter} this + * builder will build. + * @return this builder, with dimensions increased, and a sneaky + * unchecked cast to the corresponding generic type. + */ + public ArrayBuilder a1() + { + m_dimensions += 1; + return (ArrayBuilder)this; + } + + /** + * Adds two to the result-array dimensions of the {@code Adapter} this + * builder will build. + * @return this builder, with dimensions increased, and a sneaky + * unchecked cast to the corresponding generic type. + */ + public ArrayBuilder a2() + { + m_dimensions += 2; + return (ArrayBuilder)this; + } + + /** + * Adds four to the result-array dimensions of the {@code Adapter} this + * builder will build. + * @return this builder, with dimensions increased, and a sneaky + * unchecked cast to the corresponding generic type. + */ + public ArrayBuilder a4() + { + m_dimensions += 4; + return (ArrayBuilder)this; + } + } +} diff --git a/pljava-api/src/main/java/org/postgresql/pljava/Lifespan.java b/pljava-api/src/main/java/org/postgresql/pljava/Lifespan.java new file mode 100644 index 000000000..672079d5c --- /dev/null +++ b/pljava-api/src/main/java/org/postgresql/pljava/Lifespan.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava; + +import org.postgresql.pljava.model.MemoryContext; // javadoc +import org.postgresql.pljava.model.ResourceOwner; // javadoc + +/** + * Model of any notional object in PostgreSQL or PL/Java that has a definite + * temporal existence, with a detectable end, and so can be used to scope the + * lifetime of any PL/Java object that has corresponding native resources. + *

+ * A {@code Lifespan} generalizes over assorted classes that can play that role, + * such as PostgreSQL's {@link ResourceOwner ResourceOwner} and + * {@link MemoryContext MemoryContext}. {@code MemoryContext} may see the most + * use in PL/Java, as the typical reason to scope the lifetime of some PL/Java + * object is that it refers to some allocation of native memory. + *

+ * The invocation of a PL/Java function is also usefully treated as a resource + * owner. It is reasonable to depend on the objects passed in the function call + * to remain usable as long as the call is on the stack, if no other explicit + * lifespan applies. + *

+ * Java's incubating foreign function and memory API will bring a + * {@code ResourceScope} object for which some relation to a PL/Java + * {@code Lifespan} can probably be defined. + *

+ * The history of PostgreSQL MemoryContexts + * (the older mechanism, appearing in PostgreSQL 7.1), and ResourceOwners + * (introduced in 8.0) is interesting. As the latter's {@code README} puts it, + * The design of the ResourceOwner API is modeled on our MemoryContext API, + * which has proven very flexible and successful ... It is tempting to consider + * unifying ResourceOwners and MemoryContexts into a single object type, but + * their usage patterns are sufficiently different ...." + *

+ * Only later, in PostgreSQL 9.5, did {@code MemoryContext} gain a callback + * mechanism for detecting reset or delete, with which it also becomes usable + * as a kind of lifespan under PL/Java's broadened view of the concept. + * While not unifying ResourceOwners and MemoryContexts into a single + * object type, PL/Java here makes them both available as subtypes of a + * common interface, so either can be chosen to place an appropriate temporal + * scope on a PL/Java object. + */ +public interface Lifespan +{ +} diff --git a/pljava-api/src/main/java/org/postgresql/pljava/RolePrincipal.java b/pljava-api/src/main/java/org/postgresql/pljava/RolePrincipal.java new file mode 100644 index 000000000..cd68813de --- /dev/null +++ b/pljava-api/src/main/java/org/postgresql/pljava/RolePrincipal.java @@ -0,0 +1,234 @@ +/* + * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava; + +import java.io.InvalidObjectException; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectStreamException; +import java.io.Serializable; + +import java.nio.file.attribute.GroupPrincipal; + +import java.util.function.Function; + +import org.postgresql.pljava.sqlgen.Lexicals.Identifier; +import org.postgresql.pljava.sqlgen.Lexicals.Identifier.Pseudo; +import org.postgresql.pljava.sqlgen.Lexicals.Identifier.Simple; + +public abstract class RolePrincipal extends BasePrincipal +{ + private static final long serialVersionUID = 5650953533699613976L; + + RolePrincipal(String name) + { + super(name); + constrain(IllegalArgumentException::new); + } + + RolePrincipal(Simple name) + { + super(name); + constrain(IllegalArgumentException::new); + /* + * Ensure the subclasses' PUBLIC singletons really are, by rejecting the + * Pseudo.PUBLIC identifier in this constructor. The subclasses use + * private constructors that call the specialized one below when + * initializing their singletons. + */ + if ( s_public == name ) + throw new IllegalArgumentException( + "attempt to create non-singleton PUBLIC RolePrincipal"); + } + + RolePrincipal(Pseudo name) + { + super(name); + constrain(IllegalArgumentException::new); + } + + private final void constrain(Function exc) + throws E + { + Class c = getClass(); + if ( c != Authenticated.class && c != Session.class + && c != Outer.class && c != Current.class ) + throw exc.apply( + "forbidden to create unknown RolePrincipal subclass: " + + c.getName()); + + /* + * Unlike many cases where a delimited identifier can be used whose + * regular-identifier form is a reserved word, PostgreSQL in fact + * forbids giving any role a name that the regular identifier public + * would match, even if the name is quoted. + */ + if ( ( "public".equals(m_name.nonFolded()) + || "public".equals(m_name.pgFolded()) ) && m_name != s_public ) + throw exc.apply( + "forbidden to create a RolePrincipal with name " + + "that matches \"public\" by PostgreSQL rules"); + } + + private void readObject(ObjectInputStream in) + throws IOException, ClassNotFoundException + { + in.defaultReadObject(); + constrain(InvalidObjectException::new); + } + + static final Pseudo s_public = Pseudo.PUBLIC; + + /** + * Compare two {@code RolePrincipal}s for equality, with special treatment + * for the {@code PUBLIC} ones. + *

+ * Each concrete subclass of {@code RolePrincipal} has a singleton + * {@code PUBLIC} instance, which will only compare equal to itself (this + * method is not the place to say everything matches {@code PUBLIC}, because + * {@code equals} should be symmetric, and security checks should not be). + * Otherwise, the result is that of + * {@link Identifier#equals(Object) Identifier.equals}. + *

+ * Note that these {@code PUBLIC} instances are distinct from the wild-card + * principal names that can appear in the Java policy file: those are + * handled without ever instantiating the class, and simply match any + * principal with the identically-spelled class name. + */ + @Override + public final boolean equals(Object other) + { + if ( this == other ) + return true; + /* + * Because the pseudo "PUBLIC" instances are restricted to being + * singletons (one per RolePrincipal subclass), the above test will have + * already handled the matching case for those. Below, if either one is + * a PUBLIC instance, its m_name won't match anything else, which is ok + * because of the PostgreSQL rule that no role can have a potentially + * matching name anyway. + */ + if ( ! getClass().isInstance(other) ) + return false; + RolePrincipal o = (RolePrincipal)other; + return m_name.equals(o.m_name); + } + + public static final class Authenticated extends RolePrincipal + { + private static final long serialVersionUID = -4558155344619605758L; + + public static final Authenticated PUBLIC = new Authenticated(s_public); + + public Authenticated(String name) + { + super(name); + } + + public Authenticated(Simple name) + { + super(name); + } + + private Authenticated(Pseudo name) + { + super(name); + } + + private Object readResolve() throws ObjectStreamException + { + return m_name == s_public ? PUBLIC : this; + } + } + + public static final class Session extends RolePrincipal + { + private static final long serialVersionUID = -598305505864518470L; + + public static final Session PUBLIC = new Session(s_public); + + public Session(String name) + { + super(name); + } + + public Session(Simple name) + { + super(name); + } + + private Session(Pseudo name) + { + super(name); + } + + private Object readResolve() throws ObjectStreamException + { + return m_name == s_public ? PUBLIC : this; + } + } + + public static final class Outer extends RolePrincipal + { + private static final long serialVersionUID = 2177159367185354785L; + + public static final Outer PUBLIC = new Outer(s_public); + + public Outer(String name) + { + super(name); + } + + public Outer(Simple name) + { + super(name); + } + + private Outer(Pseudo name) + { + super(name); + } + + private Object readResolve() throws ObjectStreamException + { + return m_name == s_public ? PUBLIC : this; + } + } + + public static final class Current extends RolePrincipal + implements GroupPrincipal + { + private static final long serialVersionUID = 2816051825662188997L; + + public static final Current PUBLIC = new Current(s_public); + + public Current(String name) + { + super(name); + } + + public Current(Simple name) + { + super(name); + } + + private Current(Pseudo name) + { + super(name); + } + + private Object readResolve() throws ObjectStreamException + { + return m_name == s_public ? PUBLIC : this; + } + } +} diff --git a/pljava-api/src/main/java/org/postgresql/pljava/TargetList.java b/pljava-api/src/main/java/org/postgresql/pljava/TargetList.java new file mode 100644 index 000000000..3c1619caa --- /dev/null +++ b/pljava-api/src/main/java/org/postgresql/pljava/TargetList.java @@ -0,0 +1,921 @@ +/* + * Copyright (c) 2023 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava; + +import java.sql.SQLException; // for javadoc +import java.sql.SQLXML; // for javadoc + +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; + +import java.util.stream.Stream; + +import org.postgresql.pljava.Adapter.As; +import org.postgresql.pljava.Adapter.AsBoolean; +import org.postgresql.pljava.Adapter.AsByte; +import org.postgresql.pljava.Adapter.AsChar; +import org.postgresql.pljava.Adapter.AsDouble; +import org.postgresql.pljava.Adapter.AsFloat; +import org.postgresql.pljava.Adapter.AsInt; +import org.postgresql.pljava.Adapter.AsLong; +import org.postgresql.pljava.Adapter.AsShort; + +import org.postgresql.pljava.model.Attribute; +import org.postgresql.pljava.model.Portal; // for javadoc +import org.postgresql.pljava.model.TupleDescriptor; // for javadoc +import org.postgresql.pljava.model.TupleTableSlot; + +import org.postgresql.pljava.sqlgen.Lexicals.Identifier.Simple; + +/** + * Identifies attributes to be retrieved from a set of tuples. + *

+ * {@code TargetList} is more general than {@link Projection Projection}: in a + * {@code Projection}, no attribute can appear more than once, but repetition + * is possible in a {@code TargetList}. + *

+ * In general, it will be more efficient, if processing logic requires more than + * one copy of some attribute's value, to simply mention the attribute once in a + * {@code Projection}, and have the Java logic then copy the value, rather than + * fetching and converting it twice from the database native form. But there + * may be cases where that isn't workable, such as when the value is needed in + * different Java representations from different {@link Adapter}s, or when the + * Java representation is a type like {@link SQLXML} that can only be used once. + * Such cases call for a {@code TargetList} in which the attribute is mentioned + * more than once, to be separately fetched. + *

+ * Given a {@code TargetList}, query results can be processed by supplying a + * lambda body to {@link #applyOver(Iterable,Cursor.Function) applyOver}. The + * lambda will be supplied a {@link Cursor Cursor} whose {@code apply} methods + * can be used to break out the wanted values on each row, in the + * {@code TargetList} order. + */ +public interface TargetList extends List +{ + /** + * A {@code TargetList} in which no one attribute may appear more than once. + *

+ * The prime example of a {@code Projection} is a {@link TupleDescriptor} as + * obtained, for example, from the {@link Portal} for a query result. + *

+ * To preserve the "no attribute appears more than once" property, the only + * new {@code Projection}s derivable from an existing one involve selecting + * a subset of its attributes, and possibly changing their order. The + * {@code project} methods taking attribute names, attribute indices, or the + * attributes themselves can be used to do so, as can the {@code subList} + * method. + */ + interface Projection extends TargetList + { + /** + * From this {@code Projection}, returns a {@code Projection} containing + * only the attributes matching the supplied names and in the + * order of the argument list. + * @throws IllegalArgumentException if more names are supplied than this + * Projection has attributes, or if any remain unmatched after matching + * each attribute in this Projection at most once. + */ + Projection project(Simple... names); + + /** + * From this {@code Projection}, returns a {@code Projection} containing + * only the attributes matching the supplied names and in the + * order of the argument list. + *

+ * The names will be converted to {@link Simple Identifier.Simple} by + * its {@link Simple#fromJava fromJava} method before comparison. + * @throws IllegalArgumentException if more names are supplied than this + * Projection has attributes, or if any remain unmatched after matching + * each attribute in this Projection at most once. + */ + default Projection project(CharSequence... names) + { + return project( + Arrays.stream(names) + .map(CharSequence::toString) + .map(Simple::fromJava) + .toArray(Simple[]::new) + ); + } + + /** + * Returns a {@code Projection} containing only the attributes found + * at the supplied indices in this {@code Projection}, and in + * the order of the argument list. + *

+ * The index of the first attribute is zero. + * @throws IllegalArgumentException if more indices are supplied than + * this Projection has attributes, if any index is negative or beyond + * the last index in this Projection, or if any index appears more than + * once. + */ + Projection project(int... indices); + + /** + * Like {@link #project(int...) project(int...)} but using SQL's 1-based + * indexing convention. + *

+ * The index of the first attribute is 1. + * @throws IllegalArgumentException if more indices are supplied than + * this Projection has attributes, if any index is nonpositive or beyond + * the last 1-based index in this Projection, or if any index appears + * more than once. + */ + Projection sqlProject(int... indices); + + /** + * Returns a {@code Projection} containing only attributes + * and in the order of the argument list. + *

+ * The attributes must be found in this {@code Projection} by exact + * reference identity. + * @throws IllegalArgumentException if more attributes are supplied than + * this Projection has, or if any remain unmatched after matching + * each attribute in this Projection at most once. + */ + Projection project(Attribute... attributes); + + @Override + Projection subList(int fromIndex, int toIndex); + } + + @Override + TargetList subList(int fromIndex, int toIndex); + + /** + * Like {@link #get(int) get} but following the SQL convention where the + * first element has index 1. + */ + default Attribute sqlGet(int oneBasedIndex) + { + try + { + return get(oneBasedIndex - 1); + } + catch ( IndexOutOfBoundsException e ) + { + throw (IndexOutOfBoundsException) + new IndexOutOfBoundsException(String.format( + "sqlGet() one-based index %d should be > 0 and <= %d", + oneBasedIndex, size() + )) + .initCause(e); + } + } + + /** + * Executes the function f, once, supplying a + * {@link Cursor Cursor} that can be iterated over the supplied + * tuples and used to process each tuple. + * @return whatever f returns. + */ + R applyOver( + Iterable tuples, Cursor.Function f) + throws X, SQLException; + + /** + * Executes the function f, once, supplying a + * {@link Cursor Cursor} that can be used to process the tuple. + *

+ * The {@code Cursor} can be iterated, just as if a one-row + * {@code Iterable} had been passed to + * {@link #applyOver(Iterable,Cursor.Function) applyOver(tuples, f)}, but it + * need not be; it will already have the single supplied tuple as + * its current row, ready for its {@code apply} methods to be used. + * @return whatever f returns. + */ + R applyOver( + TupleTableSlot tuple, Cursor.Function f) + throws X, SQLException; + + /** + * A {@code TargetList} that has been bound to a source of tuples and can + * execute code with the wanted attribute values available. + *

+ * Being derived from a {@link TargetList}, a {@code Cursor} serves directly + * as an {@code Iterator}, supplying the attributes in the + * {@code TargetList} order. + *

+ * Being bound to a source of tuples, a {@code Cursor} also implements + * {@code Iterable}, and can supply an iterator over the bound tuples in + * order. The {@code Cursor} is mutated during the iteration, having a + * current row that becomes each tuple in turn. The object returned by that + * iterator is the {@code Cursor} itself, so the caller has no need for the + * iteration variable, and can use the "unnamed variable" {@code _} for it, + * in Java versions including that feature (which appears in Java 21 but + * only with {@code --enable-preview}). In older Java versions it can be + * given some other obviously throwaway name. + *

+ * When a {@code Cursor} has a current row, its {@code apply} methods can be + * used to execute a lambda body with its parameters mapped to the row's + * values, in {@code TargetList} order, or to a prefix of those, should + * a lambda with fewer parameters be supplied. + *

+ * Each overload of {@code apply} takes some number of + * {@link Adapter Adapter} instances, each of which must be suited to the + * PostgreSQL type at its corresponding position, followed by a lambda body + * with the same number of parameters, each of which will receive the value + * from the corresponding {@code Adapter}, and have an inferred type + * matching what that {@code Adapter} produces. + *

+ * Within a lambda body with fewer parameters than the length of the + * {@code TargetList}, the {@code Cursor}'s attribute iterator has been + * advanced by the number of columns consumed. It can be used again to apply + * an inner lambda body to remaining columns. This "curried" style can be + * useful when the number or types of values to be processed will not + * directly fit any available {@code apply} signature. + *

+	 *  overall_result = targetlist.applyOver(tuples, c ->
+	 *  {
+	 *      var resultCollector = ...;
+	 *      for ( Cursor _ : c )
+	 *      {
+	 *          var oneResult = c.apply(
+	 *              adap0, adap1,
+	 *             ( val0,  val1 ) -> c.apply(
+	 *                  adap2, adap3,
+	 *                 ( val2,  val3 ) -> process(val0, val1, val2, val3)));
+     *          resultCollector.collect(oneResult);
+	 *      }
+	 *      return resultCollector;
+	 *  });
+	 *
+ *

+ * As the {@code apply} overloads for reference-typed values and those for + * primitive values are separate, currying must be used when processing a + * mix of reference and primitive types. + *

+ * The {@code Cursor}'s attribute iterator is reset each time the tuple + * iterator moves to a new tuple. It is also reset on return (normal or + * exceptional) from an outermost {@code apply}, in case another function + * should then be applied to the row. + *

+ * The attribute iterator is not reset on return from an inner (curried) + * {@code apply}. Therefore, it is possible to process a tuple having + * repeating groups of attributes with matching types, reusing an inner + * lambda and its matching adapters for each occurrence of the group. + *

+ * If the tuple is nothing but repeating groups, the effect can still be + * achieved by using the zero-parameter {@code apply} overload as the + * outermost. + */ + interface Cursor extends Iterator, Iterable + { + /** + * Returns an {@link Iterator} that will return this {@code Cursor} + * instance itself, repeatedly, mutated each time to represent the next + * of the bound list of tuples. + *

+ * Because the {@code Iterator} will produce the same {@code Cursor} + * instance on each iteration, and the instance is mutated, saving + * values the iterator returns will not have effects one might expect, + * and no more than one iteration should be in progress at a time. + *

+ * The {@code Iterator} that this {@code Cursor} represents + * will be reset to the first attribute each time a new tuple is + * presented by the {@code Iterator}. + * @throws IllegalStateException within the code body passed to any + * {@code apply} method. Within any such code body, the cursor simply + * represents its current tuple. Only outside of any {@code apply()} may + * {@code iterator()} be called. + */ + @Override // Iterable + Iterator iterator(); + + /** + * Returns a {@link Stream} that will present this {@code Cursor} + * instance itself, repeatedly, mutated each time to represent the next + * of the bound list of tuples. + *

+ * The stream should be used within the scope of the + * {@link #applyOver(Iterable,Function) applyOver} that has made + * this {@code Cursor} available. + *

+ * Because the {@code Stream} will produce the same {@code Cursor} + * instance repeatedly, and the instance is mutated, saving instances + * will not have effects one might expect, and no more than one + * stream should be in progress at a time. Stateful operations such as + * {@code distinct} or {@code sorted} will make no sense applied to + * these instances. Naturally, this method does not return a parallel + * {@code Stream}. + *

+ * These restrictions do not satisfy all expectations of a + * {@code Stream}, and may be topics for future work as this API is + * refined. + *

+ * The {@code Iterator} that this {@code Cursor} represents + * will be reset to the first attribute each time a new tuple is + * presented by the {@code Stream}. + * @throws IllegalStateException within the code body passed to any + * {@code apply} method. Within any such code body, the cursor simply + * represents its current tuple. Only outside of any {@code apply()} may + * {@code stream()} be called. + */ + Stream stream(); + + R apply( + L0 f) + throws X; + + R apply( + As a0, + L1 f) + throws X; + + R apply( + As a0, As a1, + L2 f) + throws X; + + R apply( + As a0, As a1, As a2, + L3 f) + throws X; + + R apply( + As a0, As a1, As a2, As a3, + L4 f) + throws X; + + R apply( + As a0, As a1, As a2, As a3, + As a4, + L5 f) + throws X; + + R apply( + As a0, As a1, As a2, As a3, + As a4, As a5, + L6 f) + throws X; + + R apply( + As a0, As a1, As a2, As a3, + As a4, As a5, As a6, + L7 f) + throws X; + + R apply( + As a0, As a1, As a2, As a3, + As a4, As a5, As a6, As a7, + L8 f) + throws X; + + R apply( + As a0, As a1, As a2, As a3, + As a4, As a5, As a6, As a7, + As a8, As a9, As aa, As ab, + As ac, As ad, As ae, As af, + L16 f) + throws X; + + R apply( + AsLong a0, + J1 f) + throws X; + + R apply( + AsLong a0, AsLong a1, + J2 f) + throws X; + + R apply( + AsLong a0, AsLong a1, AsLong a2, + J3 f) + throws X; + + R apply( + AsLong a0, AsLong a1, AsLong a2, AsLong a3, + J4 f) + throws X; + + R apply( + AsDouble a0, + D1 f) + throws X; + + R apply( + AsDouble a0, AsDouble a1, + D2 f) + throws X; + + R apply( + AsDouble a0, AsDouble a1, AsDouble a2, + D3 f) + throws X; + + R apply( + AsDouble a0, AsDouble a1, AsDouble a2, AsDouble a3, + D4 f) + throws X; + + R apply( + AsInt a0, + I1 f) + throws X; + + R apply( + AsInt a0, AsInt a1, + I2 f) + throws X; + + R apply( + AsInt a0, AsInt a1, AsInt a2, + I3 f) + throws X; + + R apply( + AsInt a0, AsInt a1, AsInt a2, AsInt a3, + I4 f) + throws X; + + R apply( + AsFloat a0, + F1 f) + throws X; + + R apply( + AsFloat a0, AsFloat a1, + F2 f) + throws X; + + R apply( + AsFloat a0, AsFloat a1, AsFloat a2, + F3 f) + throws X; + + R apply( + AsFloat a0, AsFloat a1, AsFloat a2, AsFloat a3, + F4 f) + throws X; + + R apply( + AsShort a0, + S1 f) + throws X; + + R apply( + AsShort a0, AsShort a1, + S2 f) + throws X; + + R apply( + AsShort a0, AsShort a1, AsShort a2, + S3 f) + throws X; + + R apply( + AsShort a0, AsShort a1, AsShort a2, AsShort a3, + S4 f) + throws X; + + R apply( + AsChar a0, + C1 f) + throws X; + + R apply( + AsChar a0, AsChar a1, + C2 f) + throws X; + + R apply( + AsChar a0, AsChar a1, AsChar a2, + C3 f) + throws X; + + R apply( + AsChar a0, AsChar a1, AsChar a2, AsChar a3, + C4 f) + throws X; + + R apply( + AsByte a0, + B1 f) + throws X; + + R apply( + AsByte a0, AsByte a1, + B2 f) + throws X; + + R apply( + AsByte a0, AsByte a1, AsByte a2, + B3 f) + throws X; + + R apply( + AsByte a0, AsByte a1, AsByte a2, AsByte a3, + B4 f) + throws X; + + R apply( + AsBoolean a0, + Z1 f) + throws X; + + R apply( + AsBoolean a0, AsBoolean a1, + Z2 f) + throws X; + + R apply( + AsBoolean a0, AsBoolean a1, AsBoolean a2, + Z3 f) + throws X; + + R apply( + AsBoolean a0, AsBoolean a1, AsBoolean a2, AsBoolean a3, + Z4 f) + throws X; + + @FunctionalInterface + interface Function + { + R apply(Cursor c); + } + + @FunctionalInterface + interface L0 + { + R apply() throws X; + } + + @FunctionalInterface + interface L1 + { + R apply(A v0) throws X; + } + + @FunctionalInterface + interface L2 + { + R apply(A v0, B v1) throws X; + } + + /** + * @hidden + */ + @FunctionalInterface + interface L3 + { + R apply(A v0, B v1, C v2) throws X; + } + + /** + * @hidden + */ + @FunctionalInterface + interface L4 + { + R apply(A v0, B v1, C v2, D v3) throws X; + } + + /** + * @hidden + */ + @FunctionalInterface + interface L5 + { + R apply(A v0, B v1, C v2, D v3, E v4) throws X; + } + + /** + * @hidden + */ + @FunctionalInterface + interface L6 + { + R apply(A v0, B v1, C v2, D v3, E v4, F v5) throws X; + } + + /** + * @hidden + */ + @FunctionalInterface + interface L7 + { + R apply(A v0, B v1, C v2, D v3, E v4, F v5, G v6) throws X; + } + + /** + * @hidden + */ + @FunctionalInterface + interface L8 + { + R apply(A v0, B v1, C v2, D v3, E v4, F v5, G v6, H v7) + throws X; + } + + /** + * @hidden + */ + @FunctionalInterface + interface L16 + { + R apply( + A v0, B v1, C v2, D v3, E v4, F v5, G v6, H v7, + I v8, J v9, K va, L vb, M vc, N vd, O ve, P vf) + throws X; + } + + /** + * @hidden + */ + @FunctionalInterface + interface J1 + { + R apply(long v0) throws X; + } + + /** + * @hidden + */ + @FunctionalInterface + interface J2 + { + R apply(long v0, long v1) throws X; + } + + /** + * @hidden + */ + @FunctionalInterface + interface J3 + { + R apply(long v0, long v1, long v2) throws X; + } + + /** + * @hidden + */ + @FunctionalInterface + interface J4 + { + R apply(long v0, long v1, long v2, long v3) throws X; + } + + /** + * @hidden + */ + @FunctionalInterface + interface D1 + { + R apply(double v0) throws X; + } + + /** + * @hidden + */ + @FunctionalInterface + interface D2 + { + R apply(double v0, double v1) throws X; + } + + /** + * @hidden + */ + @FunctionalInterface + interface D3 + { + R apply(double v0, double v1, double v2) throws X; + } + + /** + * @hidden + */ + @FunctionalInterface + interface D4 + { + R apply(double v0, double v1, double v2, double v3) throws X; + } + + /** + * @hidden + */ + @FunctionalInterface + interface I1 + { + R apply(int v0) throws X; + } + + /** + * @hidden + */ + @FunctionalInterface + interface I2 + { + R apply(int v0, int v1) throws X; + } + + /** + * @hidden + */ + @FunctionalInterface + interface I3 + { + R apply(int v0, int v1, int v2) throws X; + } + + /** + * @hidden + */ + @FunctionalInterface + interface I4 + { + R apply(int v0, int v1, int v2, int v3) throws X; + } + + /** + * @hidden + */ + @FunctionalInterface + interface F1 + { + R apply(float v0) throws X; + } + + /** + * @hidden + */ + @FunctionalInterface + interface F2 + { + R apply(float v0, float v1) throws X; + } + + /** + * @hidden + */ + @FunctionalInterface + interface F3 + { + R apply(float v0, float v1, float v2) throws X; + } + + /** + * @hidden + */ + @FunctionalInterface + interface F4 + { + R apply(float v0, float v1, float v2, float v3) throws X; + } + + /** + * @hidden + */ + @FunctionalInterface + interface S1 + { + R apply(short v0) throws X; + } + + /** + * @hidden + */ + @FunctionalInterface + interface S2 + { + R apply(short v0, short v1) throws X; + } + + /** + * @hidden + */ + @FunctionalInterface + interface S3 + { + R apply(short v0, short v1, short v2) throws X; + } + + /** + * @hidden + */ + @FunctionalInterface + interface S4 + { + R apply(short v0, short v1, short v2, short v3) throws X; + } + + /** + * @hidden + */ + @FunctionalInterface + interface C1 + { + R apply(char v0) throws X; + } + + /** + * @hidden + */ + @FunctionalInterface + interface C2 + { + R apply(char v0, char v1) throws X; + } + + /** + * @hidden + */ + @FunctionalInterface + interface C3 + { + R apply(char v0, char v1, char v2) throws X; + } + + /** + * @hidden + */ + @FunctionalInterface + interface C4 + { + R apply(char v0, char v1, char v2, char v3) throws X; + } + + /** + * @hidden + */ + @FunctionalInterface + interface B1 + { + R apply(byte v0) throws X; + } + + /** + * @hidden + */ + @FunctionalInterface + interface B2 + { + R apply(byte v0, byte v1) throws X; + } + + /** + * @hidden + */ + @FunctionalInterface + interface B3 + { + R apply(byte v0, byte v1, byte v2) throws X; + } + + /** + * @hidden + */ + @FunctionalInterface + interface B4 + { + R apply(byte v0, byte v1, byte v2, byte v3) throws X; + } + + /** + * @hidden + */ + @FunctionalInterface + interface Z1 + { + R apply(boolean v0) throws X; + } + + /** + * @hidden + */ + @FunctionalInterface + interface Z2 + { + R apply(boolean v0, boolean v1) throws X; + } + + /** + * @hidden + */ + @FunctionalInterface + interface Z3 + { + R apply(boolean v0, boolean v1, boolean v2) throws X; + } + + /** + * @hidden + */ + @FunctionalInterface + interface Z4 + { + R apply(boolean v0, boolean v1, boolean v2, boolean v3) throws X; + } + } +} diff --git a/pljava-api/src/main/java/org/postgresql/pljava/adt/Array.java b/pljava-api/src/main/java/org/postgresql/pljava/adt/Array.java new file mode 100644 index 000000000..63cee9dbc --- /dev/null +++ b/pljava-api/src/main/java/org/postgresql/pljava/adt/Array.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.adt; + +import java.sql.SQLException; + +import java.util.List; + +import org.postgresql.pljava.Adapter; +import org.postgresql.pljava.Adapter.Contract; + +import org.postgresql.pljava.model.TupleTableSlot.Indexed; + +/** + * Container for functional interfaces presenting a PostgreSQL array. + */ +public interface Array +{ + /** + * A contract whereby an array is returned flattened into a Java list, + * with no attention to its specified dimensionality or index bounds. + */ + @FunctionalInterface + interface AsFlatList extends Contract.Array,E,Adapter.As> + { + /** + * Shorthand for a cast of a suitable method reference to this + * functional interface type. + */ + static AsFlatList of(AsFlatList instance) + { + return instance; + } + + /** + * An implementation that produces a Java list eagerly copied from the + * PostgreSQL array, which is then no longer needed; null elements in + * the array are included in the list. + */ + static List nullsIncludedCopy( + int nDims, int[] dimsAndBounds, Adapter.As adapter, + Indexed slot) + throws SQLException + { + int n = slot.elements(); + E[] result = adapter.arrayOf(n); + for ( int i = 0; i < n; ++ i ) + result[i] = slot.get(i, adapter); + return List.of(result); + } + } +} diff --git a/pljava-api/src/main/java/org/postgresql/pljava/adt/Bitstring.java b/pljava-api/src/main/java/org/postgresql/pljava/adt/Bitstring.java new file mode 100644 index 000000000..8b43b3b5a --- /dev/null +++ b/pljava-api/src/main/java/org/postgresql/pljava/adt/Bitstring.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.adt; + +import java.nio.ByteBuffer; + +import java.util.OptionalInt; + +import org.postgresql.pljava.Adapter.Contract; + +/** + * Container for abstract-type functional interfaces in PostgreSQL's + * {@code BITSTRING} type category. + */ +public interface Bitstring +{ + /** + * The {@code BIT} and {@code VARBIT} types' PostgreSQL semantics: the + * number of bits, and the sequence of bytes they're packed into. + */ + @FunctionalInterface + public interface Bit extends Contract.Scalar + { + /** + * Constructs a representation T from the components + * of the PostgreSQL data type. + * @param nBits the actual number of bits in the value, not necessarily + * a multiple of 8. For type BIT, must equal the modifier nBits if + * specified; for VARBIT, must be equal or smaller. + * @param bytes a buffer of ceiling(nBits/8) bytes, not aliasing any + * internal storage, so safely readable (and writable, if useful for + * format conversion). Before accessing it in wider units, its byte + * order should be explicitly set. Within each byte, the logical order + * of the bits is from MSB to LSB; beware that this within-byte bit + * order is the reverse of what java.util.BitSet.valueOf(...) expects. + * When nBits is not a multiple of 8, the unused low-order bits of + * the final byte must be zero. + */ + T construct(int nBits, ByteBuffer bytes); + + /** + * Functional interface to obtain information from the PostgreSQL type + * modifier applied to the type. + */ + @FunctionalInterface + interface Modifier + { + /** + * Returns a {@code Bit} function possibly tailored ("curried") + * with the values from a PostgreSQL type modifier on the type. + * @param nBits for the BIT type, the exact number of bits the + * value must have; for VARBIT, the maximum. When not specified, + * the meaning is 1 for BIT, and unlimited for VARBIT. + */ + Bit modify(OptionalInt nBits); + } + } +} diff --git a/pljava-api/src/main/java/org/postgresql/pljava/adt/Datetime.java b/pljava-api/src/main/java/org/postgresql/pljava/adt/Datetime.java new file mode 100644 index 000000000..0ac42e41d --- /dev/null +++ b/pljava-api/src/main/java/org/postgresql/pljava/adt/Datetime.java @@ -0,0 +1,596 @@ +/* + * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.adt; + +import java.sql.SQLException; +import java.sql.SQLDataException; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.OffsetTime; +import java.time.ZoneOffset; + +import static java.time.ZoneOffset.UTC; +import static java.time.temporal.ChronoUnit.DAYS; +import static java.time.temporal.ChronoUnit.MICROS; +import static java.time.temporal.JulianFields.JULIAN_DAY; + +import java.util.OptionalInt; + +import org.postgresql.pljava.Adapter.Contract; + +/** + * Container for abstract-type functional interfaces in PostgreSQL's + * {@code DATETIME} type category. + */ +public interface Datetime +{ + /** + * PostgreSQL "infinitely early" date, as a value of what would otherwise be + * days from the PostgreSQL epoch. + */ + int DATEVAL_NOBEGIN = Integer.MIN_VALUE; + + /** + * PostgreSQL "infinitely late" date, as a value of what would otherwise be + * days from the PostgreSQL epoch. + */ + int DATEVAL_NOEND = Integer.MAX_VALUE; + + /** + * PostgreSQL "infinitely early" timestamp, as a value of what would + * otherwise be microseconds from the PostgreSQL epoch. + */ + long DT_NOBEGIN = Long.MIN_VALUE; + + /** + * PostgreSQL "infinitely late" timestamp, as a value of what would + * otherwise be microseconds from the PostgreSQL epoch. + */ + long DT_NOEND = Long.MAX_VALUE; + + /** + * The PostgreSQL "epoch", 1 January 2000, as a Julian day; the date + * represented by a {@code DATE}, {@code TIMESTAMP}, or {@code TIMESTAMPTZ} + * with a stored value of zero. + */ + int POSTGRES_EPOCH_JDATE = 2451545; + + /** + * Maximum value allowed for a type modifier specifying the seconds digits + * to the right of the decimal point for a {@code TIME} or {@code TIMETZ}. + */ + int MAX_TIME_PRECISION = 6; + + /** + * Maximum value allowed for a type modifier specifying the seconds digits + * to the right of the decimal point for a {@code TIMESTAMP} or + * {@code TIMESTAMPTZ}. + */ + int MAX_TIMESTAMP_PRECISION = 6; + + /** + * The maximum allowed value, inclusive, for a {@code TIME} or the time + * portion of a {@code TIMETZ}. + *

+ * The limit is inclusive; PostgreSQL officially accepts 24:00:00 + * as a valid time value. + */ + long USECS_PER_DAY = 86400000000L; + + /** + * The {@code DATE} type's PostgreSQL semantics: a signed number of days + * since the "Postgres epoch". + */ + @FunctionalInterface + public interface Date extends Contract.Scalar + { + /** + * The PostgreSQL "epoch" as a {@code java.time.LocalDate}. + */ + LocalDate POSTGRES_EPOCH = + LocalDate.EPOCH.with(JULIAN_DAY, POSTGRES_EPOCH_JDATE); + + /** + * Constructs a representation T from the components + * of the PostgreSQL data type. + *

+ * The argument represents days since + * {@link #POSTGRES_EPOCH POSTGRES_EPOCH}, unless it is one of + * the special values {@link #DATEVAL_NOBEGIN DATEVAL_NOBEGIN} or + * {@link #DATEVAL_NOEND DATEVAL_NOEND}. + *

+ * When constructing a representation that lacks notions of positive or + * negative "infinity", one option is to simply map the above special + * values no differently than ordinary ones, and remember the two + * resulting representations as the "infinite" ones. If that is done + * without wraparound, the resulting "-infinity" value will precede all + * other PostgreSQL-representable dates and the resulting "+infinity" + * will follow them. + *

+ * The older {@code java.util.Date} cannot represent those values + * without wraparound; the two resulting values can still be saved as + * representing -infinity and +infinity, but will not have the expected + * ordering with respect to other values. They will both be quite far + * from the present. + */ + T construct(int daysSincePostgresEpoch); + + /** + * A reference implementation that maps to {@link LocalDate LocalDate}. + *

+ * The PostgreSQL "-infinity" and "+infinity" values are mapped to + * {@code LocalDate} instances matching (by {@code equals}) the special + * instances {@code NOBEGIN} and {@code NOEND} here, respectively. + */ + static class AsLocalDate implements Date + { + private AsLocalDate() // I am a singleton + { + } + + public static final AsLocalDate INSTANCE = new AsLocalDate(); + + /** + * {@code LocalDate} representing PostgreSQL's "infinitely early" + * date. + */ + public static final LocalDate NOBEGIN = + INSTANCE.construct(DATEVAL_NOBEGIN); + + /** + * {@code LocalDate} representing PostgreSQL's "infinitely late" + * date. + */ + public static final LocalDate NOEND = + INSTANCE.construct(DATEVAL_NOEND); + + @Override + public LocalDate construct(int daysSincePostgresEpoch) + { + return POSTGRES_EPOCH.plusDays(daysSincePostgresEpoch); + } + + public T store(LocalDate d, Date f) throws SQLException + { + if ( NOBEGIN.isAfter(d) || d.isAfter(NOEND) ) + throw new SQLDataException(String.format( + "date out of range: \"%s\"", d), "22008"); + + return f.construct((int)POSTGRES_EPOCH.until(d, DAYS)); + } + } + } + + /** + * The {@code TIME} type's PostgreSQL semantics: microseconds since + * midnight. + */ + @FunctionalInterface + public interface Time extends Contract.Scalar + { + /** + * Constructs a representation T from the components + * of the PostgreSQL data type. + *

+ * The argument represents microseconds since midnight, nonnegative + * and not exceeding {@code USECS_PER_DAY}. + *

+ * PostgreSQL does allow the value to exactly equal + * {@code USECS_PER_DAY}. 24:00:00 is considered a valid value. That + * may need extra attention if the representation to be constructed + * doesn't allow that. + */ + T construct(long microsecondsSinceMidnight); + + /** + * Functional interface to obtain information from the PostgreSQL type + * modifier applied to the type. + */ + @FunctionalInterface + interface Modifier + { + /** + * Returns a {@code Time} function possibly tailored ("curried") + * with the values from a PostgreSQL type modifier on the type. + *

+ * The precision indicates the number of seconds digits desired + * to the right of the decimal point, and must be positive and + * no greater than {@code MAX_TIME_PRECISION}. + */ + Time modify(OptionalInt precision); + } + + /** + * A reference implementation that maps to {@link LocalTime LocalTime}. + *

+ * While PostgreSQL allows 24:00:00 as a valid time, {@code LocalTime} + * maxes out at the preceding nanosecond. That is still a value that + * can be distinguished, because PostgreSQL's time resolution is only + * to microseconds, so the PostgreSQL 24:00:00 value will be mapped + * to that. + *

+ * In the other direction, nanoseconds will be rounded to microseconds, + * so any value within the half-microsecond preceding {@code HOUR24} + * will become the PostgreSQL 24:00:00 value. + */ + static class AsLocalTime implements Time + { + private AsLocalTime() // I am a singleton + { + } + + public static final AsLocalTime INSTANCE = new AsLocalTime(); + + /** + * {@code LocalTime} representing the 24:00:00 time that PostgreSQL + * accepts but {@code LocalTime} does not. + *

+ * This {@code LocalTime} represents the immediately preceding + * nanosecond. That is still distinguishable from any other + * PostgreSQL time, because those have only microsecond + * resolution. + */ + public static final LocalTime HOUR24 = + LocalTime.ofNanoOfDay(1000L * USECS_PER_DAY - 1L); + + @Override + public LocalTime construct(long microsecondsSinceMidnight) + { + if ( USECS_PER_DAY == microsecondsSinceMidnight ) + return HOUR24; + + return LocalTime.ofNanoOfDay(1000L * microsecondsSinceMidnight); + } + + public T store(LocalTime t, Time f) + { + long nanos = t.toNanoOfDay(); + + return f.construct((500L + nanos) / 1000L); + } + } + } + + /** + * The {@code TIMETZ} type's PostgreSQL semantics: microseconds since + * midnight, accompanied by a time zone offset expressed in seconds. + */ + @FunctionalInterface + public interface TimeTZ extends Contract.Scalar + { + /** + * Constructs a representation T from the components + * of the PostgreSQL data type. + *

+ * The first argument represents microseconds since midnight, + * nonnegative and not exceeding {@code USECS_PER_DAY}, and + * the second is a time zone offset expressed in seconds, positive + * for locations west of the prime meridian. + *

+ * It should be noted that other common conventions, such as ISO 8601 + * and {@code java.time.ZoneOffset}, use positive offsets for locations + * east of the prime meridian, requiring a sign flip. + *

+ * Also noteworthy, as with {@link Time Time}, is that the first + * argument may exactly equal {@code USECS_PER_DAY}; 24:00:00 + * is a valid value to PostgreSQL. That may need extra attention if + * the representation to be constructed doesn't allow that. + * @param microsecondsSinceMidnight the time of day, in the zone + * indicated by the second argument + * @param secondsWestOfPrimeMeridian note that the sign of this time + * zone offset will be the opposite of that used in other common systems + * using positive values for offsets east of the prime meridian. + */ + T construct( + long microsecondsSinceMidnight, int secondsWestOfPrimeMeridian); + + /** + * Functional interface to obtain information from the PostgreSQL type + * modifier applied to the type. + */ + @FunctionalInterface + interface Modifier + { + /** + * Returns a {@code TimeTZ} function possibly tailored ("curried") + * with the values from a PostgreSQL type modifier on the type. + *

+ * The precision indicates the number of seconds digits desired + * to the right of the decimal point, and must be positive and + * no greater than {@code MAX_TIME_PRECISION}. + */ + TimeTZ modify(OptionalInt precision); + } + + /** + * A reference implementation that maps to + * {@link OffsetTime OffsetTime}. + *

+ * While PostgreSQL allows 24:00:00 as a valid time, Java's rules + * max out at the preceding nanosecond. That is still a value that + * can be distinguished, because PostgreSQL's time resolution is only + * to microseconds, so the PostgreSQL 24:00:00 value will be mapped + * to a value whose {@code LocalTime} component matches (with + * {@code equals}) {@link Time.AsLocalTime#HOUR24 AsLocalTime.HOUR24}, + * which is really one nanosecond shy of 24 hours. + *

+ * In the other direction, nanoseconds will be rounded to microseconds, + * so any value within the half-microsecond preceding {@code HOUR24} + * will become the PostgreSQL 24:00:00 value. + */ + static class AsOffsetTime implements TimeTZ + { + private AsOffsetTime() // I am a singleton + { + } + + public static final AsOffsetTime INSTANCE = new AsOffsetTime(); + + @Override + public OffsetTime construct( + long microsecondsSinceMidnight, int secondsWestOfPrimeMeridian) + { + ZoneOffset offset = + ZoneOffset.ofTotalSeconds( - secondsWestOfPrimeMeridian); + + LocalTime local = Time.AsLocalTime.INSTANCE + .construct(microsecondsSinceMidnight); + + return OffsetTime.of(local, offset); + } + + public T store(OffsetTime t, TimeTZ f) + { + int secondsWest = - t.getOffset().getTotalSeconds(); + + LocalTime local = t.toLocalTime(); + + return Time.AsLocalTime.INSTANCE + .store(local, micros -> f.construct(micros, secondsWest)); + } + } + } + + /** + * The {@code TIMESTAMP} type's PostgreSQL semantics: microseconds since + * midnight of the PostgreSQL epoch, without an assumed time zone. + */ + @FunctionalInterface + public interface Timestamp extends Contract.Scalar + { + /** + * The PostgreSQL "epoch" as a {@code java.time.LocalDateTime}. + */ + LocalDateTime POSTGRES_EPOCH = Date.POSTGRES_EPOCH.atStartOfDay(); + + /** + * Constructs a representation T from the components + * of the PostgreSQL data type. + *

+ * The argument represents microseconds since midnight on + * {@link #POSTGRES_EPOCH POSTGRES_EPOCH}. + *

+ * Because no particular time zone is understood to apply, the exact + * corresponding point on a standard timeline cannot be identified, + * absent outside information. It is typically used to represent + * a timestamp in the local zone, whatever that is. + *

+ * The argument represents microseconds since + * {@link #POSTGRES_EPOCH POSTGRES_EPOCH}, unless it is one of + * the special values {@link #DT_NOBEGIN DT_NOBEGIN} or + * {@link #DT_NOEND DT_NOEND}. + *

+ * When constructing a representation that lacks notions of positive or + * negative "infinity", one option is to simply map the above special + * values no differently than ordinary ones, and remember the two + * resulting representations as the "infinite" ones. If that is done + * without wraparound, the resulting "-infinity" value will precede all + * other PostgreSQL-representable dates and the resulting "+infinity" + * will follow them. + *

+ * The older {@code java.util.Date} cannot represent those values + * without wraparound; the two resulting values can still be saved as + * representing -infinity and +infinity, but will not have the expected + * ordering with respect to other values. They will both be quite far + * from the present. + */ + T construct(long microsecondsSincePostgresEpoch); + + /** + * Functional interface to obtain information from the PostgreSQL type + * modifier applied to the type. + */ + @FunctionalInterface + interface Modifier + { + /** + * Returns a {@code Timestamp} function possibly tailored + * ("curried") with the values from a PostgreSQL type modifier + * on the type. + *

+ * The precision indicates the number of seconds digits desired + * to the right of the decimal point, and must be positive and + * no greater than {@code MAX_TIMESTAMP_PRECISION}. + */ + Timestamp modify(OptionalInt precision); + } + + /** + * A reference implementation that maps to + * {@link LocalDateTime LocalDateTime}. + *

+ * The PostgreSQL "-infinity" and "+infinity" values are mapped to + * {@code LocalDateTime} instances matching (by {@code equals}) + * the special instances {@code NOBEGIN} and {@code NOEND} here, + * respectively. + */ + static class AsLocalDateTime implements Timestamp + { + private AsLocalDateTime() // I am a singleton + { + } + + public static final AsLocalDateTime INSTANCE = + new AsLocalDateTime(); + + /** + * {@code LocalDateTime} representing PostgreSQL's "infinitely + * early" timestamp. + */ + public static final LocalDateTime NOBEGIN = + INSTANCE.construct(DT_NOBEGIN); + + /** + * {@code LocalDateTime} representing PostgreSQL's "infinitely + * late" timestamp. + */ + public static final LocalDateTime NOEND = + INSTANCE.construct(DT_NOEND); + + @Override + public LocalDateTime construct(long microsecondsSincePostgresEpoch) + { + return + POSTGRES_EPOCH.plus(microsecondsSincePostgresEpoch, MICROS); + } + + public T store(LocalDateTime d, Timestamp f) + throws SQLException + { + try + { + return f.construct(POSTGRES_EPOCH.until(d, MICROS)); + } + catch ( ArithmeticException e ) + { + throw new SQLDataException(String.format( + "timestamp out of range: \"%s\"", d), "22008", e); + } + } + } + } + + /** + * The {@code TIMESTAMPTZ} type's PostgreSQL semantics: microseconds since + * midnight UTC of the PostgreSQL epoch. + */ + @FunctionalInterface + public interface TimestampTZ extends Contract.Scalar + { + /** + * The PostgreSQL "epoch" as a {@code java.time.OffsetDateTime}. + */ + OffsetDateTime POSTGRES_EPOCH = + OffsetDateTime.of(Timestamp.POSTGRES_EPOCH, UTC); + + /** + * Constructs a representation T from the components + * of the PostgreSQL data type. + *

+ * The argument represents microseconds since midnight UTC on + * {@link #POSTGRES_EPOCH POSTGRES_EPOCH}. + *

+ * Given any desired local time zone, conversion to/from this value + * is possible if the rules for that time zone as of the represented + * date are available. + *

+ * The argument represents microseconds since + * {@link #POSTGRES_EPOCH POSTGRES_EPOCH}, unless it is one of + * the special values {@link #DT_NOBEGIN DT_NOBEGIN} or + * {@link #DT_NOEND DT_NOEND}. + *

+ * When constructing a representation that lacks notions of positive or + * negative "infinity", one option is to simply map the above special + * values no differently than ordinary ones, and remember the two + * resulting representations as the "infinite" ones. If that is done + * without wraparound, the resulting "-infinity" value will precede all + * other PostgreSQL-representable dates and the resulting "+infinity" + * will follow them. + *

+ * The older {@code java.util.Date} cannot represent those values + * without wraparound; the two resulting values can still be saved as + * representing -infinity and +infinity, but will not have the expected + * ordering with respect to other values. They will both be quite far + * from the present. + */ + T construct(long microsecondsSincePostgresEpochUTC); + + /** + * Functional interface to obtain information from the PostgreSQL type + * modifier applied to the type. + */ + @FunctionalInterface + interface Modifier + { + /** + * Returns a {@code TimestampTZ} function possibly tailored + * ("curried") with the values from a PostgreSQL type modifier + * on the type. + *

+ * The precision indicates the number of seconds digits desired + * to the right of the decimal point, and must be positive and + * no greater than {@code MAX_TIMESTAMP_PRECISION}. + */ + TimestampTZ modify(OptionalInt precision); + } + + /** + * A reference implementation that maps to + * {@link OffsetDateTime OffsetDateTime}. + *

+ * A value from PostgreSQL is always understood to be at UTC, and + * will be mapped always to an {@code OffsetDateTime} with UTC as + * its offset. + *

+ * A value from Java is adjusted by its offset so that PostgreSQL will + * always be passed {@code microsecondsSincePostgresEpochUTC}. + *

+ * The PostgreSQL "-infinity" and "+infinity" values are mapped to + * instances whose corresponding {@code LocalDateTime} at UTC will match + * (by {@code equals}) the constants {@code NOBEGIN} and {@code NOEND} + * of {@code AsLocalDateTime}, respectively. + */ + static class AsOffsetDateTime implements TimestampTZ + { + private AsOffsetDateTime() // I am a singleton + { + } + + public static final AsOffsetDateTime INSTANCE = + new AsOffsetDateTime(); + + @Override + public OffsetDateTime construct(long microsecondsSincePostgresEpoch) + { + return + POSTGRES_EPOCH.plus(microsecondsSincePostgresEpoch, MICROS); + } + + public T store(OffsetDateTime d, TimestampTZ f) + throws SQLException + { + try + { + return f.construct(POSTGRES_EPOCH.until(d, MICROS)); + } + catch ( ArithmeticException e ) + { + throw new SQLDataException(String.format( + "timestamp out of range: \"%s\"", d), "22008", e); + } + } + } + } +} diff --git a/pljava-api/src/main/java/org/postgresql/pljava/adt/Geometric.java b/pljava-api/src/main/java/org/postgresql/pljava/adt/Geometric.java new file mode 100644 index 000000000..a07052d6b --- /dev/null +++ b/pljava-api/src/main/java/org/postgresql/pljava/adt/Geometric.java @@ -0,0 +1,165 @@ +/* + * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.adt; + +import org.postgresql.pljava.Adapter.Contract; +import org.postgresql.pljava.Adapter.Dispenser; +import org.postgresql.pljava.Adapter.PullDispenser; + +/** + * Container for abstract-type functional interfaces in PostgreSQL's + * {@code GEOMETRIC} type category. + */ +public interface Geometric +{ + /** + * The {@code POINT} type's PostgreSQL semantics: a pair of {@code float8} + * coordinates. + */ + @FunctionalInterface + public interface Point extends Contract.Scalar + { + /** + * Constructs a representation T from the components + * of the PostgreSQL data type. + */ + T construct(double x, double y); + } + + /** + * The {@code LSEG} type's PostgreSQL semantics: two endpoints. + * @param the type returned by the constructor + * @param internal parameter that consumers of this interface should + * wildcard; an implementor may bound this parameter to get stricter type + * checking of the {@code Dispenser} uses within the implementing body. + */ + @FunctionalInterface + public interface LSeg extends Contract.Scalar + { + /** + * Constructs a representation T from the components + * of the PostgreSQL data type. + * @param endpoints a dispenser that will dispense a {@code Point} for + * index 0 and index 1. + */ + T construct(PullDispenser> endpoints); + } + + /** + * The {@code PATH} type's PostgreSQL semantics: vertex points and whether + * closed. + * @param the type returned by the constructor + * @param internal parameter that consumers of this interface should + * wildcard; an implementor may bound this parameter to get stricter type + * checking of the {@code Dispenser} uses within the implementing body. + */ + @FunctionalInterface + public interface Path extends Contract.Scalar + { + /** + * Constructs a representation T from the components + * of the PostgreSQL data type. + * @param nPoints the number of points on the path + * @param closed whether the path should be understood to include + * a segment joining the last point to the first one. + * @param points a dispenser that will dispense a {@code Point} for + * each index 0 through nPoint - 1. + */ + T construct( + int nPoints, boolean closed, PullDispenser> points); + } + + /** + * The {@code LINE} type's PostgreSQL semantics: coefficients of its + * general equation Ax+By+C=0. + */ + @FunctionalInterface + public interface Line extends Contract.Scalar + { + /** + * Constructs a representation T from the components + * of the PostgreSQL data type. + */ + T construct(double A, double B, double C); + } + + /** + * The {@code BOX} type's PostgreSQL semantics: two corner points. + * @param the type returned by the constructor + * @param internal parameter that consumers of this interface should + * wildcard; an implementor may bound this parameter to get stricter type + * checking of the {@code Dispenser} uses within the implementing body. + */ + @FunctionalInterface + public interface Box extends Contract.Scalar + { + /** + * Constructs a representation T from the components + * of the PostgreSQL data type. + *

+ * As stored, the corner point at index 0 is never below or to the left + * of that at index 1. This may be achieved by permuting the points + * or their coordinates obtained as input, in any way that preserves + * the box. + * @param corners a dispenser that will dispense a {@code Point} for + * index 0 and at index 1. + */ + T construct(PullDispenser> corners); + } + + /** + * The {@code POLYGON} type's PostgreSQL semantics: vertex points and + * a bounding box. + * @param the type returned by the constructor + * @param internal parameter that consumers of this interface should + * wildcard; an implementor may bound this parameter to get stricter type + * checking of the boundingBox dispenser used within + * the implementing body. + * @param internal parameter that consumers of this interface should + * wildcard; an implementor may bound this parameter to get stricter type + * checking of the vertices dispenser used within + * the implementing body. + */ + @FunctionalInterface + public interface Polygon extends Contract.Scalar + { + /** + * Constructs a representation T from the components + * of the PostgreSQL data type. + * @param nVertices the number of vertices in the polygon + * @param boundingBox a dispenser from which the bounding box may be + * obtained. + * @param vertices a dispenser from which a vertex {@code Point} may be + * obtained for each index 0 through nVertices - 1. + */ + T construct( + int nVertices, Dispenser> boundingBox, + PullDispenser> vertices); + } + + /** + * The {@code CIRCLE} type's PostgreSQL semantics: center point and radius. + * @param the type returned by the constructor + * @param internal parameter that consumers of this interface should + * wildcard; an implementor may bound this parameter to get stricter type + * checking of the {@code Dispenser} uses within the implementing body. + */ + @FunctionalInterface + public interface Circle extends Contract.Scalar + { + /** + * Constructs a representation T from the components + * of the PostgreSQL data type. + */ + T construct(Dispenser> center, double radius); + } +} diff --git a/pljava-api/src/main/java/org/postgresql/pljava/adt/Internal.java b/pljava-api/src/main/java/org/postgresql/pljava/adt/Internal.java new file mode 100644 index 000000000..49e344471 --- /dev/null +++ b/pljava-api/src/main/java/org/postgresql/pljava/adt/Internal.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.adt; + +import org.postgresql.pljava.Adapter.Contract; + +/** + * Container for abstract-type functional interfaces, not quite exactly + * corresponding to PostgreSQL's {@code INTERNAL} category; there are some + * fairly "internal" types that ended up in the {@code USER} category too, + * for whatever reason. + */ +public interface Internal +{ + /** + * The {@code tid} type's PostgreSQL semantics: a block ID and + * a row index within that block. + */ + @FunctionalInterface + public interface Tid extends Contract.Scalar + { + /** + * Constructs a representation T from the components + * of the PostgreSQL data type. + * @param blockId (treat as unsigned) identifies the block in a table + * containing the target row + * @param offsetNumber (treat as unsigned) the index of the target row + * within the identified block + */ + T construct(int blockId, short offsetNumber); + } +} diff --git a/pljava-api/src/main/java/org/postgresql/pljava/adt/Money.java b/pljava-api/src/main/java/org/postgresql/pljava/adt/Money.java new file mode 100644 index 000000000..d4f35ef19 --- /dev/null +++ b/pljava-api/src/main/java/org/postgresql/pljava/adt/Money.java @@ -0,0 +1,183 @@ +/* + * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.adt; + +import java.math.BigDecimal; // javadoc + +import java.text.NumberFormat; // javadoc + +import java.util.Currency; // javadoc +import java.util.Locale; // javadoc + +import org.postgresql.pljava.Adapter.Contract; + +/** + * The {@code MONEY} type's PostgreSQL semantics: an integer value, whose + * scaling, display format, and currency are all determined by a + * user-settable configuration setting. + *

+ * This type is a strange duck in PostgreSQL. It is stored + * as a (64 bit) integer, and must have a scaling applied on input and + * output to the appropriate number of decimal places. + *

+ * The appropriate scaling, the symbols for decimal point and grouping + * separators, how the sign is shown, and even what currency it + * represents and the currency symbol to use, are all determined + * from the locale specified by the {@code lc_monetary} configuration + * setting, which can be changed within any session with no special + * privilege at any time. That may make {@code MONEY} the only data type + * in PostgreSQL where a person can use a single {@code SET} command to + * instantly change what an entire table of data means. + *

+ * For example, this little catalog of products: + *

+ * => SELECT * FROM products;
+ *  product |       price
+ * ---------+--------------------
+ *  widget  |             $19.00
+ *  tokamak | $19,000,000,000.00
+ *
+ *

+ * can be instantly marked down by about 12 percent (at the exchange + * rates looked up at this writing): + *

+ * => SET lc_monetary TO 'ja_JP';
+ * SET
+ * => SELECT * FROM products;
+ *  product |        price
+ * ---------+---------------------
+ *  widget  |             ï¿¥1,900
+ *  tokamak | ï¿¥1,900,000,000,000
+ *
+ *

+ * or marked up by roughly the same amount: + *

+ * => SET lc_monetary TO 'de_DE@euro';
+ * SET
+ * => SELECT * FROM products;
+ *  product |        price
+ * ---------+---------------------
+ *  widget  |             19,00 €
+ *  tokamak | 19.000.000.000,00 €
+ *
+ *

+ * or marked up even further (as of this writing, 26%): + *

+ * => SET lc_monetary TO 'en_GB';
+ * SET
+ * => SELECT * FROM products;
+ *  product |       price
+ * ---------+--------------------
+ *  widget  |             £19.00
+ *  tokamak | £19,000,000,000.00
+ *
+ *

+ * Obtaining the locale information in Java + *

+ * Before the integer value provided here can be correctly scaled or + * interpreted, the locale-dependent information must be obtained. + * In Java, that can be done in six steps: + *

    + *
  1. Obtain the string value of PostgreSQL's {@code lc_monetary} + * configuration setting. + *
  2. Let's not talk about step 2 just yet. + *
  3. Obtain a {@code Locale} object by passing the BCP 47 tag to + * {@link Locale#forLanguageTag Locale.forLanguageTag}. + *
  4. Pass the {@code Locale} object to + * {@link NumberFormat#getCurrencyInstance(Locale) + NumberFormat.getCurrencyInstance}. + *
  5. From that, obtain an actual instance of {@code Currency} with + * {@link NumberFormat#getCurrency NumberFormat.getCurrency}. + *
  6. Obtain the correct power of ten for scaling from + * {@link Currency#getDefaultFractionDigits + Currency.getDefaultFractionDigits}. + *
+ *

+ * The {@code NumberFormat} obtained in step 4 knows all the appropriate + * formatting details, but will not automatically scale the integer + * value here by the proper power of ten. That must be done explicitly, + * and to avoid compromising the precision objectives of the + * {@code MONEY} type, should be done with something like a + * {@link BigDecimal BigDecimal}. If fmt was obtained + * in step 4 above and scale is the value from step 6: + *

+ * BigDecimal bd =
+ *     BigDecimal.valueOf(scaledToInteger).movePointLeft(scale);
+ * String s = fmt.format(bd);
+ *
+ *

+ * would produce the correctly-formatted value, where + * scaledToInteger is the parameter supplied to this interface + * method. + *

+ * If the format is not needed, the scale can be obtained in fewer steps + * by passing the {@code Locale} from step 3 directly to + * {@link Currency#getInstance(Locale) Currency.getInstance}. + * That would be enough to build a simple reference implementation for + * this data type that would return a {@code BigDecimal} with its point + * moved left by the scale. + *

+ * Now let's talk about step 2. + *

+ * Java's locale support is based on BCP 47, a format for identifiers + * standardized by + * IETF to ensure that they are reliable and specific. + *

+ * The string obtained from the {@code lc_monetary} setting in step 1 + * above is, most often, a string that makes sense to the underlying + * operating system's C library, using some syntax that predated BCP 47, + * and likely demonstrates all of the problems BCP 47 was created to + * overcome. + *

+ * From a first glance at a few simple examples, it can appear that + * replacing some underscores with hyphens could turn some simple + * OS-library strings into BCP 47 tags, but that is far from the general + * case, which is full of nonobvious rules, special cases, and + * grandfather clauses. + *

+ * A C library, {@code liblangtag}, is available to perform exactly that + * mapping, and weighs in at about two and a half megabytes. The library + * might be present on the system where PostgreSQL is running, in which + * case it could be used in step 2, at the cost of a native call. + *

+ * If PostgreSQL was built with ICU, a native method could accomplish + * the same (as nearly as practical) thing by calling + * {@code uloc_canonicalize} followed by {@code uloc_toLanguageTag}; or, + * if the ICU4J Java library is available, + * {@code ULocale.createCanonical}could be used to the same effect. + *

+ * It might be simplest to just use a native call to obtain the + * scaling and other needed details from the underlying operating system + * library. + *

+ * Because of step 2's complexity, PL/Java does not here supply the + * simple reference implementation to {@code BigDecimal} proposed above. + */ +@FunctionalInterface +public interface Money extends Contract.Scalar +{ + /** + * Constructs a representation T from the components + * of the PostgreSQL data type. + *

+ * It might be necessary to extend this interface with extra parameters + * (or to use the {@code Modifier} mechanism) to receive the needed + * scaling and currency details, and require the corresponding + * {@code Adapter} (which could no longer be pure Java) to make + * the needed native calls to obtain those. + * @param scaledToInteger integer value that must be scaled according + * to the setting of the lc_monetary configuration setting, + * and represents a value in the currency also determined by that + * setting. + */ + T construct(long scaledToInteger); +} diff --git a/pljava-api/src/main/java/org/postgresql/pljava/adt/Network.java b/pljava-api/src/main/java/org/postgresql/pljava/adt/Network.java new file mode 100644 index 000000000..9bfb2e542 --- /dev/null +++ b/pljava-api/src/main/java/org/postgresql/pljava/adt/Network.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.adt; + +import java.net.StandardProtocolFamily; + +import org.postgresql.pljava.Adapter.Contract; + +/** + * Container for abstract-type functional interfaces in PostgreSQL's + * {@code NETWORK} type category (and MAC addresses, which, for arcane reasons, + * are not in that category). + */ +public interface Network +{ + /** + * The {@code INET} and {@code CIDR} types' PostgreSQL semantics: the + * family ({@code INET} or {@code INET6}), the number of network prefix + * bits, and the address bytes in network byte order. + */ + @FunctionalInterface + public interface Inet extends Contract.Scalar + { + /** + * Constructs a representation T from the components + * of the PostgreSQL data type. + * @param addressFamily INET or INET6 + * @param networkPrefixBits nonnegative, not greater than 32 for INET + * or 128 for INET6 (either maximum value indicates the address is for + * a single host rather than a network) + * @param networkOrderAddress the address bytes in network order. When + * the type is CIDR, only the leftmost networkPrefixBits bits are + * allowed to be nonzero. The array does not alias any internal storage + * and may be used as desired. + */ + T construct( + StandardProtocolFamily addressFamily, int networkPrefixBits, + byte[] networkOrderAddress); + } + + /** + * The {@code macaddr} and {@code macaddr8} types' PostgreSQL semantics: + * a byte array (6 or 8 bytes, respectively)., of which byte 0 is the one + * appearing first in the text representation (and stored in the member + * named a of the C struct). + */ + @FunctionalInterface + public interface MAC extends Contract.Scalar + { + /** + * Constructs a representation T from the components + * of the PostgreSQL data type. + * @param address array of 6 (macaddr) or 8 (macaddr8) bytes, of which + * byte 0 is the one appearing first in the text representation (and + * stored in the member named a of the C struct). The array + * does not alias any internal storage and may be used as desired. + */ + T construct(byte[] address); + } +} diff --git a/pljava-api/src/main/java/org/postgresql/pljava/adt/Numeric.java b/pljava-api/src/main/java/org/postgresql/pljava/adt/Numeric.java new file mode 100644 index 000000000..4abaa6eaf --- /dev/null +++ b/pljava-api/src/main/java/org/postgresql/pljava/adt/Numeric.java @@ -0,0 +1,360 @@ +/* + * Copyright (c) 2022-2023 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.adt; + +import static java.lang.Math.multiplyExact; + +import java.math.BigDecimal; + +import java.sql.SQLException; +import java.sql.SQLDataException; + +import org.postgresql.pljava.Adapter.Contract; + +/** + * The {@code NUMERIC} type's PostgreSQL semantics: a sign (or indication + * that the value is NaN, + infinity, or - infinity), a display scale, + * a weight, and zero or more base-ten-thousand digits. + *

+ * This data type can have a type modifier that specifies a maximum + * precision (total number of base-ten digits to retain) and a maximum scale + * (how many of those base-ten digits are right of the decimal point). + *

+ * A curious feature of the type is that, when a type modifier is specified, + * the value becomes "anchored" to the decimal point: all of its decimal + * digits must be within precision places of the decimal point, + * or an error is reported. This rules out the kind of values that can crop + * up in physics, for example, where there might be ten digits of precision + * but those are twenty places away from the decimal point. This limitation + * apparently follows from the ISO SQL definitions of the precision and + * scale. + *

+ * However, when PostgreSQL {@code NUMERIC} is used with no type modifier, + * such values are not rejected, and are stored efficiently, just as you + * would expect, keeping only the digits that are needed and adjusting + * weight for the distance to the decimal point. + *

+ * In mapping to and from a Java representation, extra care may be needed + * if that capability is to be preserved. + */ +@FunctionalInterface +public interface Numeric extends Contract.Scalar +{ + /** + * The maximum precision that may be specified in a {@code numeric} type + * modifier. + *

+ * Without a modifier, the type is subject only to its implementation + * limits, which are much larger. + */ + int NUMERIC_MAX_PRECISION = 1000; + + /** + * The minimum 'scale' that may be specified in a {@code numeric} type + * modifier in PostgreSQL 15 or later. + *

+ * Negative scale indicates rounding left of the decimal point. A scale of + * -1000 indicates rounding to a multiple of 101000. + *

+ * Prior to PostgreSQL 15, a type modifier did not allow a negative + * scale. + *

+ * Without a modifier, the type is subject only to its implementation + * limits. + */ + int NUMERIC_MIN_SCALE = -1000; + + /** + * The maximum 'scale' that may be specified in a {@code numeric} type + * modifier in PostgreSQL 15 or later. + *

+ * When scale is positive, the digits string represents a value smaller by + * the indicated power of ten. When scale exceeds precision, the digits + * string represents digits that appear following (scale - precision) zeros + * to the right of the decimal point. + *

+ * Prior to PostgreSQL 15, a type modifier did not allow a scale greater + * than the specified precision. + *

+ * Without a modifier, the type is subject only to its implementation + * limits. + */ + int NUMERIC_MAX_SCALE = 1000; + + /** + * The base of the 'digit' elements supplied by PostgreSQL. + *

+ * This is also built into the parameter name base10000Digits and + * is highly unlikely to change; a comment in the PostgreSQL code since 2015 + * confirms "values of {@code NBASE} other than 10000 are considered of + * historical interest only and are no longer supported in any sense". + */ + int NBASE = 10000; + + /** + * Decimal digits per {@code NBASE} digit. + */ + int DEC_DIGITS = 4; + + /** + * Label to distinguish positive, negative, and three kinds of special + * values. + */ + enum Kind { POSITIVE, NEGATIVE, NAN, POSINFINITY, NEGINFINITY } + + /** + * Constructs a representation T from the components + * of the PostgreSQL data type. + *

+ * A note about displayScale: when positive, it is information, + * stored with the PostgreSQL value, that conveys how far (right of the + * units place) the least significant decimal digit of the intended value + * falls. + *

+ * An apparentScale can also be computed: + *

+	 *  apparentScale = (1 + weight - base10000Digits.length) * (- DEC_DIGITS)
+	 *
+ * This computation has a simple meaning, and gives the distance, right of + * the units place, of the least-significant decimal digit in the stored + * representation. When negative, of course, it means that least stored + * digit falls left of the units place. + *

+ * Because of the {@code DEC_DIGITS} factor, apparentScale + * computed this way will always be a multiple of four, the next such (in + * the direction of more significant digits) from the position of the + * actual least significant digit in the value. So apparentScale + * may exceed displayScale by as much as three, and, if so, + * displayScale should be used in preference, to avoid + * overstating the value's significant figures. + *

+ * Likewise, if displayScale is positive, it should be used even + * if it exceeds apparentScale. In that case, it conveys that + * PostgreSQL knows additional digits are significant, even though they were + * zero and it did not store them. + *

+ * However, the situation when displayScale is zero is less + * clear-cut, because PostgreSQL simply disallows it ever to be negative. + * This clamping of displayScale loses information, such that a + * value with displayScale zero and apparentScale + * negative may represent any of: + *

    + *
  • A limited-precision value with non-significant trailing zeros (from + * -apparentScale to as many as -apparentScale+3 of + * them)
  • + *
  • A precise integer, all of whose -apparentScale non-stored + * significant digits just happened to be zeros
  • + *
  • or anything in between.
  • + *
+ *

+ * That these cases can't be distinguished is inherent in PostgreSQL's + * representation of the type, and any implementation of this interface will + * need to make and document a choice of how to proceed. If the choice is + * to rely on apparentScale, then the fact that it is a multiple + * of four and may overstate, by up to three, the number of significant + * digits (as known, perhaps, to a human who assigned the value) has to be + * lived with; when displayScale is clamped to zero there simply + * isn't enough information to do better. + *

+ * For example, consider this adapter applied to the result of: + *

+	 * SELECT 6.62607015e-34 AS planck, 6.02214076e23 AS avogadro;
+	 *
+ *

+ * Planck's constant (a small number defined with nine significant places) + * will be presented with displayScale=42, weight=-9, + * and base10000Digits=[662, 6070, 1500]. + * Because apparentScale works out to 44 (placing the least + * stored digit 44 places right of the decimal point, a multiple of 4) but + * displayScale is only 42, it is clear that the two trailing + * zeroes in the last element are non-significant, and the value has not + * eleven but only nine significant figures. + *

+ * In contrast, Avogadro's number (a large one, defined also with nine + * significant places) will arrive with weight=5 and + * base10000Digits=[6022, 1407, 6000], but + * displayScale will not be -15; it is clamped to zero instead. + * If an implementation of this contract chooses to compute + * apparentScale, that will be -12 (the next larger multiple of + * four) and the value will seem to have gained three extra significant + * figures. On the other hand, in an implementation that takes the + * clamped-to-zero displayScale at face value, the number will + * seem to have gained fifteen extra significant figures. + * @param kind POSITIVE, NEGATIVE, POSINFINITY, NEGINFINITY, or NAN + * @param displayScale nominal precision, nonnegative; the number of + * base ten digits right of the decimal point. If this exceeds + * the number of right-of-decimal digits determined by the stored value, + * the excess represents a number of trailing decimal zeroes that are + * significant but trimmed from storage. + * @param weight indicates the power of ten thousand which the first + * base ten-thousand digit is taken is taken to represent. If the array + * base10000Digits has length one, and that one digit has the + * value 3, and weight is zero, the value is 3. If + * weight is 1, the value is 30000, and if weight + * is -1, the value is 0.0003. + * @param base10000Digits each array element is a nonnegative value not + * above 9999, representing a single digit of a base-ten-thousand + * number. The element at index zero is the most significant. The caller + * may pass a zero-length array, but may not pass null. The array is + * unshared and may be used as desired. + */ + T construct(Kind kind, int displayScale, int weight, + short[] base10000Digits) throws SQLException; + + /** + * Functional interface to obtain information from the PostgreSQL type + * modifier applied to the type. + */ + @FunctionalInterface + interface Modifier + { + /** + * Returns a {@code Numeric} function possibly tailored + * ("curried") with the values from a PostgreSQL type modifier + * on the type. + *

+ * If specified, precision must be at least one and + * not greater than {@code NUMERIC_MAX_PRECISION}, and scale + * must be not less than {@code NUMERIC_MIN_SCALE} nor more than + * {@code NUMERIC_MAX_SCALE}. + * @param specified true if a type modifier was specified, false if + * omitted + * @param precision the maximum number of base-ten digits to be + * retained, counting those on both sides of the decimal point + * @param scale maximum number of base-ten digits to be retained + * to the right of the decimal point. + */ + Numeric modify(boolean specified, int precision, int scale); + } + + /** + * A reference implementation that maps to {@link BigDecimal BigDecimal} + * (but cannot represent all possible values). + *

+ * A Java {@code BigDecimal} cannot represent the not-a-number or positive + * or negative infinity values possible for a PostgreSQL {@code NUMERIC}. + */ + static class AsBigDecimal implements Numeric + { + private AsBigDecimal() // I am a singleton + { + } + + public static final AsBigDecimal INSTANCE = new AsBigDecimal(); + + /** + * Produces a {@link BigDecimal} representation of the {@code NUMERIC} + * value, or throws an exception if the value is not-a-number or + * positive or negative infinity. + *

+ * In resolving the ambiguity when displayScale is zero, + * this implementation constructs a {@code BigDecimal} with significant + * figures inferred from the base10000Digits array's length, + * where decimal digits are grouped in fours, and therefore the + * {@code BigDecimal}'s {@link BigDecimal#scale() scale} method will + * always return a multiple of four in such cases. Therefore, from the + * query + *

+		 * SELECT 6.62607015e-34 AS planck, 6.02214076e23 AS avogadro;
+		 *
+ * this conversion will produce the {@code BigDecimal} 6.62607015E-34 + * for planck ({@code scale} will return 42, as expected), + * but will produce 6.02214076000E+23 for avogadro, showing + * three unexpected trailing zeros; {@code scale()} will not return -15 + * as expected, but the next larger multiple of four, -12. + * @throws SQLException 22000 if the value is NaN or +/- infinity. + */ + @Override + public BigDecimal construct( + Kind kind, int displayScale, int weight, short[] base10000Digits) + throws SQLException + { + switch ( kind ) + { + case NAN: + case POSINFINITY: + case NEGINFINITY: + throw new SQLDataException( + "cannot represent PostgreSQL numeric " + kind + + " as Java BigDecimal", "22000"); + default: + } + + int scale = multiplyExact(weight, - DEC_DIGITS); + + if ( 0 == base10000Digits.length ) + return BigDecimal.valueOf(0L, scale); + + // check that the final value also won't wrap around + multiplyExact(1 + weight - base10000Digits.length, - DEC_DIGITS); + + BigDecimal bd = BigDecimal.valueOf(base10000Digits[0], scale); + + for ( int i = 1 ; i < base10000Digits.length ; ++ i ) + { + scale += DEC_DIGITS; + bd = bd.add(BigDecimal.valueOf(base10000Digits[i], scale)); + } + + /* + * The final value of scale from the loop above is + * (1 + weight - base10000Digits.length) * (- DEC_DIGITS), so + * will always be a multiple of DEC_DIGITS (i.e. 4). It's also + * the scale of the BigDecimal constructed so far, and represents + * the position, right of the decimal point, of the least stored + * digit. Because of that DEC-DIGITS granularity, though, it may + * reflect up to three trailing zeros from the last element of + * base10000Digits that are not really significant. When scale and + * displayScale are positive (the value extends right of the decimal + * point), we can use displayScale to correct the scale of the + * BigDecimal. (This 'correction' applies even when displayScale + * is greater than scale; that means PostgreSQL knows even more + * trailing zeros are significant, and simply avoided storing them.) + * + * When scale ends up negative, though (the least stored digit falls + * somewhere left of the units place), and displayScale is zero, + * we get no such help, because PostgreSQL simply clamps that value + * to zero. We are on our own to decide whether we are looking at + * + * a) a value of limited precision, with (- scale) non-significant + * trailing zeros (and possibly up to three more) + * b) a precise integer value, all of whose (- scale) trailing + * digits happen to be zero (figure the odds...) + * c) anything in between. + * + * The Java BigDecimal will believe whatever we tell it and use the + * corresponding amount of memory, so on efficiency as well as + * plausibility grounds, we'll tell it (a). The scale will still be + * that multiple of four, though, so we may still have bestowed + * significance upon up to three trailing zeros, compared to what a + * human who assigned the value might think. That cannot affect + * roundtripping of the value back to PostgreSQL, because indeed the + * corresponding PostgreSQL forms are identical, so PostgreSQL can't + * notice any difference; that's how we got into this mess. + */ + if ( displayScale > 0 || scale > displayScale ) + { + assert displayScale >= 1 + scale - DEC_DIGITS; + bd = bd.setScale(displayScale); + } + + return Kind.POSITIVE == kind ? bd : bd.negate(); + } + + public T store(BigDecimal bd, Numeric f) + throws SQLException + { + throw new UnsupportedOperationException( + "no BigDecimal->NUMERIC store for now"); + } + } +} diff --git a/pljava-api/src/main/java/org/postgresql/pljava/adt/Timespan.java b/pljava-api/src/main/java/org/postgresql/pljava/adt/Timespan.java new file mode 100644 index 000000000..000412da7 --- /dev/null +++ b/pljava-api/src/main/java/org/postgresql/pljava/adt/Timespan.java @@ -0,0 +1,219 @@ +/* + * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.adt; + +import java.util.EnumSet; +import static java.util.EnumSet.of; +import static java.util.EnumSet.noneOf; +import static java.util.EnumSet.range; +import java.util.OptionalInt; +import java.util.Set; + +import org.postgresql.pljava.Adapter.Contract; + +/** + * Container for abstract-type functional interfaces in PostgreSQL's + * {@code TIMESPAN} type category (which, at present, includes the single + * type {@code INTERVAL}). + */ +public interface Timespan +{ + /** + * The {@code INTERVAL} type's PostgreSQL semantics: separate microseconds, + * days, and months components, independently signed. + *

+ * A type modifier can specify field-presence bits, and precision (number of + * seconds digits to the right of the decimal point). An empty fields set + * indicates that fields were not specified. + *

Why no reference implementation?

+ *

+ * The types in the {@link Datetime Datetime} interface come with reference + * implementations returning Java's JSR310 {@code java.time} types. + *

+ * For PostgreSQL {@code INTERVAL}, there are two candidate JSR310 types, + * {@code Period} and {@code Duration}, each of which would be appropriate + * for a different subset of PostgreSQL {@code INTERVAL} values. + *

+ * {@code Period} is appropriate for the months and days components. + * A {@code Period} treats the length of a day as subject to daylight + * adjustments following time zone rules, as does PostgreSQL. + *

+ * {@code Duration} is suitable for the sub-day components. It also allows + * access to a "day" field, but treats that as having invariant 24-hour + * width. + *

+ * Both share the superinterface {@code TemporalAmount}. That interface + * itself is described as "a framework-level interface that should not + * be widely used in application code", recommending instead that new + * concrete types can be created that implement it. + *

+ * In the datatype library that comes with the PGJDBC-NG driver, there is + * a class {@code com.impossibl.postgres.api.data.Interval} that takes that + * approach exactly; it implements {@code TemporalAmount} and represents + * all three components of the PostgreSQL interval with their PostgreSQL + * semantics. An application with that library available could use an + * implementation of this functional interface that would return instances + * of that class. + *

+ * The PGJDBC driver includes a {@code org.postgresql.util.PGInterval} class + * for the same purpose; that one does not derive from any JSR310 type. + *

Related notes from the ISO SQL/XML specification

+ *

+ * SQL/XML specifies how to map SQL {@code INTERVAL} types and values to + * the XML Schema types {@code xs:yearMonthDuration} and + * {@code xs:dayTimeDuration}, which were added in XML Schema 1.1 as + * distinct subtypes of the broader {@code xs:duration} type from XML Schema + * 1.0. That Schema 1.0 supertype has a corresponding class in the standard + * Java library, {@code javax.xml.datatype.Duration}, so an implementation + * of this functional interface returning that type would also be easy. + *

+ * These XML Schema types do not perfectly align with the PostgreSQL + * {@code INTERVAL} type, because they group the day with the sub-day + * components and treat it as having invariant width. (The only time zone + * designations supported in XML Schema are fixed offsets, for which no + * daylight rules apply). The XML Schema types allow one overall sign, + * positive or negative, but do not allow the individual components to have + * signs that differ, as PostgreSQL does. + *

+ * Java's JSR310 types can be used with equal convenience in the PostgreSQL + * way (by assigning days to the {@code Period} and the smaller + * components to the {@code Duration}) or in the XML Schema way (by storing + * days in the {@code Duration} along with the smaller + * components), but of course those choices have different implications. + *

+ * A related consideration is, in a scheme like SQL/XML's where the SQL + * {@code INTERVAL} can be mapped to a choice of types, whether that choice + * is made statically (i.e. by looking at the declared type modifier such as + * {@code YEAR TO MONTH} or {@code HOUR TO SECOND} for a column) or + * per-value (by looking at which fields are nonzero in each value + * encountered). + *

+ * The SQL/XML rule is to choose a static mapping at analysis time according + * to the type modifier. {@code YEAR}, {@code MONTH}, or + * {@code YEAR TO MONTH} call for a mapping to {@code xs:yearMonthDuration}, + * while any of the finer modifiers call for mapping to + * {@code xs:dayTimeDuration}, and no mapping is defined for an + * {@code INTERVAL} lacking a type modifier to constrain its fields in one + * of those ways. Again, those specified mappings assume that days are not + * subject to daylight rules, contrary to the behavior of the PostgreSQL + * type. + *

+ * In view of those considerations, there seems to be no single mapping of + * PostgreSQL {@code INTERVAL} to a common Java type that is sufficiently + * free of caveats to stand as a reference implementation. An application + * ought to choose an implementation of this functional interface to create + * whatever representation of an {@code INTERVAL} will suit that + * application's purposes. + */ + @FunctionalInterface + public interface Interval extends Contract.Scalar + { + enum Field + { + YEAR, MONTH, DAY, HOUR, MINUTE, SECOND + } + + EnumSet YEAR = of(Field.YEAR); + EnumSet MONTH = of(Field.MONTH); + EnumSet DAY = of(Field.DAY); + EnumSet HOUR = of(Field.HOUR); + EnumSet MINUTE = of(Field.MINUTE); + EnumSet SECOND = of(Field.SECOND); + + EnumSet YEAR_TO_MONTH = range(Field.YEAR, Field.MONTH); + EnumSet DAY_TO_HOUR = range(Field.DAY, Field.HOUR); + EnumSet DAY_TO_MINUTE = range(Field.DAY, Field.MINUTE); + EnumSet DAY_TO_SECOND = range(Field.DAY, Field.SECOND); + EnumSet HOUR_TO_MINUTE = range(Field.HOUR, Field.MINUTE); + EnumSet HOUR_TO_SECOND = range(Field.HOUR, Field.SECOND); + EnumSet MINUTE_TO_SECOND = range(Field.HOUR, Field.SECOND); + + Set> ALLOWED_FIELDS = + Set.of( + noneOf(Field.class), YEAR, MONTH, DAY, HOUR, MINUTE, SECOND, + YEAR_TO_MONTH, DAY_TO_HOUR, DAY_TO_MINUTE, DAY_TO_SECOND, + HOUR_TO_MINUTE, HOUR_TO_SECOND, MINUTE_TO_SECOND); + + int MAX_INTERVAL_PRECISION = 6; + + /** + * Constructs a representation T from the components + * of the PostgreSQL data type. + *

+ * PostgreSQL allows the three components to have independent signs. + * They are stored separately because the results of combining them with + * a date or a timestamp cannot be precomputed without knowing the other + * operand. + *

+ * In arithmetic involving an interval and a timestamp, the width of one + * unit in days can depend on the other operand if a timezone + * applies and has daylight savings rules: + *

+		 * SELECT (t + i) - t
+		 * FROM (VALUES (interval '1' DAY)) AS s(i),
+		 * (VALUES (timestamptz '12 mar 2022'), ('13 mar 2022'), ('6 nov 2022')) AS v(t);
+		 * ----------------
+		 *  1 day
+		 *  23:00:00
+		 *  1 day 01:00:00
+		 *
+ *

+ * In arithmetic involving an interval and a date or timestamp, the + * width of one unit in months can depend on the calendar + * month of the other operand, as well as on timezone shifts as for + * days: + *

+		 * SELECT (t + i) - t
+		 * FROM (VALUES (interval '1' MONTH)) AS s(i),
+		 * (VALUES (timestamptz '1 feb 2022'), ('1 mar 2022'), ('1 nov 2022')) AS v(t);
+		 * ------------------
+		 *  28 days
+		 *  30 days 23:00:00
+		 *  30 days 01:00:00
+		 *
+ */ + T construct(long microseconds, int days, int months); + + /** + * Functional interface to obtain information from the PostgreSQL type + * modifier applied to the type. + */ + @FunctionalInterface + interface Modifier + { + /** + * Returns an {@code Interval} function possibly tailored + * ("curried") with the values from a PostgreSQL type modifier + * applied to the type. + *

+ * The notional fields to be present in the interval are indicated + * by fields; the SQL standard defines more than three of + * these, which PostgreSQL combines into the three components + * actually stored. In a valid type modifier, the fields + * set must equal one of the members of {@code ALLOWED_FIELDS}: one + * of the named constants in this interface or the empty set. If it + * is empty, the type modifier does not constrain the fields that + * may be present. In practice, it is the finest field allowed in + * the type modifier that matters; PostgreSQL rounds away portions + * of an interval finer than that, but applies no special treatment + * based on the coarsest field the type modifier mentions. + *

+ * The desired number of seconds digits to the right of the decimal + * point is indicated by precision if present, which must + * be between 0 and {@code MAX_INTERVAL_PRECISION} inclusive. In + * a valid type modifier, when this is specified, fields + * must either include {@code SECONDS}, or be unspecified. + */ + Interval modify(EnumSet fields, OptionalInt precision); + } + } +} diff --git a/pljava-api/src/main/java/org/postgresql/pljava/adt/package-info.java b/pljava-api/src/main/java/org/postgresql/pljava/adt/package-info.java new file mode 100644 index 000000000..d97b70340 --- /dev/null +++ b/pljava-api/src/main/java/org/postgresql/pljava/adt/package-info.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +/** + * Package containing functional interfaces that document and present + * PostgreSQL data types abstractly, but clearly enough for faithful mapping. + *

+ * Interfaces in this package are meant to occupy a level between a PL/Java + * {@link Adapter Adapter} (responsible for PostgreSQL internal details that + * properly remain encapsulated) and some intended Java representation class + * (which may encapsulate details of its own). + *

Example

+ *

+ * Suppose an application would like to manipulate + * a PostgreSQL {@code TIME WITH TIME ZONE} in the form of a Java + * {@link OffsetTime OffsetTime} instance. + *

+ * The application selects a PL/Java {@link Adapter Adapter} that handles the + * PostgreSQL {@code TIME WITH TIME ZONE} type and presents it via the + * functional interface {@link Datetime.TimeTZ Datetime.TimeTZ} in this package. + *

+ * The application can instantiate that {@code Adapter} with some implementation + * (possibly just a lambda) of that functional interface, which will construct + * an {@code OffsetTime} instance. That {@code Adapter} instance now maps + * {@code TIME WITH TIME ZONE} to {@code OffsetTime}, as desired. + *

+ * The PostgreSQL internal details are handled by the {@code Adapter}. The + * internal details of {@code OffsetTime} are {@code OffsetTime}'s business. + * In between those two sits the {@link Datetime.TimeTZ Datetime.TimeTZ} + * interface in this package, with its one simple role: it presents the value + * in a clear, documented form as consisting of: + *

    + *
  • microseconds since midnight, and + *
  • a time zone offset in seconds west of the prime meridian + *
+ *

+ * It serves as a contract for the {@code Adapter} and as a clear starting point + * for constructing the wanted Java representation. + *

+ * It is important that the interfaces here serve as documentation as + * well as code, as it turns out that {@code OffsetTime} expects its + * time zone offsets to be positive east of the prime meridian, + * so a sign flip is needed. Interfaces in this package must be + * documented with enough detail to allow a developer to make correct + * use of the exposed values. + *

+ * The division of labor between what is exposed in these interfaces and what + * is encapsulated within {@code Adapter}s calls for a judgment of which + * details are semantically significant. If PostgreSQL somehow changes the + * internal details needed to retrieve a {@code timetz} value, it should be the + * {@code Adapter}'s job to make that transparent. If PostgreSQL ever changes + * the fact that a {@code timetz} is microseconds since midnight with + * seconds-west as a zone offset, that would require versioning the + * corresponding interface here; it is something a developer would need to know. + *

Reference implementations

+ * A few simple reference implementations (including the + * {@code timetz}-as-{@code OffsetTime} used as the example) can also be found + * in this package, and {@code Adapter} instances using them are available, + * so an application would not really have to follow the steps of the example + * to obtain one. + * @author Chapman Flack + */ +package org.postgresql.pljava.adt; + +import java.time.OffsetTime; + +import org.postgresql.pljava.Adapter; diff --git a/pljava-api/src/main/java/org/postgresql/pljava/adt/spi/AbstractType.java b/pljava-api/src/main/java/org/postgresql/pljava/adt/spi/AbstractType.java new file mode 100644 index 000000000..0feece20c --- /dev/null +++ b/pljava-api/src/main/java/org/postgresql/pljava/adt/spi/AbstractType.java @@ -0,0 +1,1168 @@ +/* + * Copyright (c) 2020-2023 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.adt.spi; + +import static java.lang.System.identityHashCode; + +import java.lang.reflect.Array; +import java.lang.reflect.Type; +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.GenericDeclaration; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.TypeVariable; +import java.lang.reflect.WildcardType; + +import static java.util.Arrays.stream; +import static java.util.Collections.addAll; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Set; +import static java.util.Objects.requireNonNull; + +import static java.util.stream.Stream.concat; +import static java.util.stream.Collectors.joining; + +/** + * Custom implementations of Java's {@link Type Type} interfaces, with methods + * for a couple useful manipulations. + *

+ * The implementations returned from Java reflection methods are internal, with + * no way to instantiate arbitrary new ones to represent the results of + * computations with them. + *

+ * Note: the implementations here do not override {@code equals} and + * {@code hashCode} inherited from {@code Object}. The JDK internal ones do, + * but not with documented behaviors, so it didn't seem worthwhile to try + * to match them. (The API specifies an {@code equals} behavior only for + * {@code ParameterizedType}, and no corresponding {@code hashCode} even for + * that, so good luck matching it.) Results from methods in this class can + * include new objects (instances of these classes) and original ones + * constructed by Java; don't assume anything sane will happen using + * {@code equals} or {@code hashCode} between them. There is a + * {@code typesEqual} static method defined here to do that job. + */ +public abstract class AbstractType implements Type +{ + enum TypeKind + { + ARRAY(GenericArrayType.class), + PT(ParameterizedType.class), + TV(TypeVariable.class), + WILDCARD(WildcardType.class), + CLASS(Class.class); + + private Class m_class; + + TypeKind(Class cls) + { + m_class = cls; + } + + static TypeKind of(Class cls) + { + for ( TypeKind k : values() ) + if ( k.m_class.isAssignableFrom(cls) ) + return k; + throw new AssertionError("TypeKind nonexhaustive: " + cls); + } + } + + /** + * Compare two Types for equality without relying on their own + * {@code equals} methods. + */ + static boolean typesEqual(Type a, Type b) + { + if ( a == b ) + return true; + + if ( null == a || null == b ) + return false; + + TypeKind ak = TypeKind.of(a.getClass()); + TypeKind bk = TypeKind.of(b.getClass()); + + if ( ak != bk ) + return false; + + switch ( ak ) + { + case ARRAY: + GenericArrayType gaa = (GenericArrayType)a; + GenericArrayType gab = (GenericArrayType)b; + return typesEqual(gaa, gab); + case PT: + ParameterizedType pta = (ParameterizedType)a; + ParameterizedType ptb = (ParameterizedType)b; + if ( ! typesEqual(pta.getRawType(), ptb.getRawType()) ) + return false; + Type[] taa = pta.getActualTypeArguments(); + Type[] tab = ptb.getActualTypeArguments(); + if ( taa.length != tab.length ) + return false; + for ( int i = 0; i < taa.length; ++ i ) + if ( ! typesEqual(taa[i], tab[i]) ) + return false; + return true; + case TV: + TypeVariable tva = (TypeVariable)a; + TypeVariable tvb = (TypeVariable)b; + return tva.getGenericDeclaration() == tvb.getGenericDeclaration() + && tva.getName().equals(tvb.getName()); + case WILDCARD: + WildcardType wa = (WildcardType)a; + WildcardType wb = (WildcardType)b; + Type[] ua = wa.getUpperBounds(); + Type[] ub = wb.getUpperBounds(); + Type[] la = wa.getLowerBounds(); + Type[] lb = wb.getLowerBounds(); + if ( ua.length != ub.length || la.length != lb.length ) + return false; + for ( int i = 0; i < ua.length; ++ i ) + if ( ! typesEqual(ua[i], ub[i]) ) + return false; + for ( int i = 0; i < la.length; ++ i ) + if ( ! typesEqual(la[i], lb[i]) ) + return false; + return true; + case CLASS: + return false; // they failed the == test at the very top + } + + return false; // unreachable, but tell that to javac + } + + /** + * Refines some {@code Type}s in by unifying the first of them + * with using. + *

+ * The variadic array of in arguments is returned, modified + * in place. + *

+ * The type using is unified with {@code in[0]} and then used to + * replace {@code in[0]}, while any variable substitutions made in + * the unification are repeated in the remaining in elements. + */ + public static Type[] refine(Type using, Type... in) + { + Map bindings = new HashMap<>(); + unify(bindings, using, in[0]); + + TypeVariable[] vars = new TypeVariable[bindings.size()]; + Type [] args = new Type [bindings.size()]; + + int i = 0; + for ( Map.Entry e : bindings.entrySet() ) + { + vars[i] = e.getKey().get(); + args[i] = e.getValue(); + ++ i; + } + Bindings b = new Bindings(vars, args); + + in[0] = using; + for ( i = 1; i < in.length; ++ i ) + in[i] = substitute(b, in[i]); + + return in; + } + + /** + * A simpleminded unify that assumes one argument is always + * the more-specific one, should resolve type variables found in the other, + * and that this can be done for cases of interest without generating and + * then solving constraints. + */ + static void unify(Map bindings, Type specific, Type general) + { + Type element1; + Type element2; + + while ( null != (element1 = toElementIfArray(specific)) + && null != (element2 = toElementIfArray(general)) ) + { + specific = element1; + general = element2; + } + + if ( general instanceof TypeVariable ) + { + // XXX verify here that specific satisfies the variable's bounds + Type wasBound = + bindings.put(new VKey((TypeVariable)general), specific); + if ( null != wasBound && ! typesEqual(specific, wasBound) ) + throw new UnsupportedOperationException( + "unimplemented case in AbstractType.unify: binding again"); + return; + } + + if ( general instanceof ParameterizedType ) + { + ParameterizedType t = (ParameterizedType)general; + Type[] oldActuals = t.getActualTypeArguments(); + Class raw = (Class)t.getRawType(); + Type[] newActuals = specialization(specific, raw); + if ( null != newActuals ) + { + for ( int i = 0; i < oldActuals.length; ++ i ) + unify(bindings, newActuals[i], oldActuals[i]); + return; + } + } + else if ( general instanceof Class ) + { + Class c = (Class)general; + TypeVariable[] formals = c.getTypeParameters(); + Type[] actuals = specialization(specific, c); + if ( null != actuals ) + { + for ( int i = 0; i < formals.length; ++ i ) + unify(bindings, actuals[i], formals[i]); + return; + } + } + + throw new IllegalArgumentException( + "failed to unify " + specific + " with " + general); + } + + /** + * Returns the component type of either a {@code GenericArrayType} or + * an array {@code Class}, otherwise null. + */ + private static Type toElementIfArray(Type possibleArray) + { + if ( possibleArray instanceof GenericArrayType ) + return ((GenericArrayType)possibleArray).getGenericComponentType(); + if ( ! (possibleArray instanceof Class) ) + return null; + return ((Class)possibleArray).getComponentType(); // null if !array + } + + /** + * Needed: test whether sub is a subtype of sup. + *

+ * XXX For the time being, this is nothing but a test of + * erased subtyping, hastily implemented by requiring that + * {@code specialization(sub, erase(sup))} does not return null. + *

+ * This must sooner or later be replaced with an implementation of + * the subtyping rules from Java Language Specification 4.10, taking + * also type parameterization into account. + */ + public static boolean isSubtype(Type sub, Type sup) + { + return null != specialization(sub, erase(sup)); + } + + /** + * Equivalent to {@code specialization(candidate, expected, null)}. + */ + public static Type[] specialization(Type candidate, Class expected) + { + return specialization(candidate, expected, null); + } + + /** + * Test whether the type {@code candidate} is, directly or indirectly, + * a specialization of generic type {@code expected}. + *

+ * For example, the Java type T of a particular adapter A that extends + * {@code Adapter.As} can be retrieved with + * {@code specialization(A.class, As.class)[0]}. + *

+ * More generally, this method can retrieve the generic type information + * from any "super type token", as first proposed by Neal Gafter in 2006, + * where a super type token is generally an instance of an anonymous + * subclass that specializes a certain generic type. Although the idea has + * been often used, the usages have not settled on one agreed name for the + * generic type. This method will work with any of them, by supplying the + * expected generic type itself as the second parameter. For example, a + * super type token {@code foo} derived from Gafter's suggested class + * {@code TypeReference} can be unpacked with + * {@code specialization(foo.getClass(), TypeReference.class)}. + * @param candidate a type to be checked + * @param expected known (normally generic) type to check for + * @param rtype array to receive (if non-null) the corresponding + * (parameterized or raw) type if the result is non-null. + * @return null if candidate does not extend expected, + * otherwise the array of type arguments with which it specializes + * expected + * @throws IllegalArgumentException if passed a Type that is not a + * Class or a ParameterizedType + * @throws NullPointerException if either argument is null + * @throws UnsupportedOperationException if candidate does extend + * expected but does not carry the needed parameter bindings (such as + * when the raw expected Class itself is passed) + */ + public static Type[] specialization( + Type candidate, Class expected, Type[] rtype) + { + Type t = requireNonNull(candidate, "candidate is null"); + requireNonNull(expected, "expected is null"); + boolean superinterfaces = expected.isInterface(); + Class c; + ParameterizedType pt = null; + Bindings latestBindings = null; + boolean ptFound = false; + boolean rawTypeFound = false; + + if ( t instanceof Class ) + { + c = (Class)t; + if ( ! expected.isAssignableFrom(c) ) + return null; + if ( expected == c ) + rawTypeFound = true; + else + latestBindings = // trivial, non-null initial value + new Bindings(new TypeVariable[0], new Type[0]); + } + else if ( t instanceof ParameterizedType ) + { + pt = (ParameterizedType)t; + c = (Class)pt.getRawType(); + if ( ! expected.isAssignableFrom(c) ) + return null; + if ( expected == c ) + ptFound = true; + else + latestBindings = new Bindings(latestBindings, pt); + } + else + throw new IllegalArgumentException( + "expected Class or ParameterizedType, got: " + t); + + if ( ! ptFound && ! rawTypeFound ) + { + List pending = new LinkedList<>(); + pending.add(c.getGenericSuperclass()); + if ( superinterfaces ) + addAll(pending, c.getGenericInterfaces()); + + while ( ! pending.isEmpty() ) + { + t = pending.remove(0); + if ( null == t ) + continue; + if ( t instanceof Class ) + { + c = (Class)t; + if ( expected == c ) + { + rawTypeFound = true; + break; + } + if ( ! expected.isAssignableFrom(c) ) + continue; + pending.add(latestBindings); + } + else if ( t instanceof ParameterizedType ) + { + pt = (ParameterizedType)t; + c = (Class)pt.getRawType(); + if ( expected == c ) + { + ptFound = true; + break; + } + if ( ! expected.isAssignableFrom(c) ) + continue; + pending.add(new Bindings(latestBindings, pt)); + } + else if ( t instanceof Bindings ) + { + latestBindings = (Bindings)t; + continue; + } + else + throw new AssertionError( + "expected Class or ParameterizedType, got: " + t); + + pending.add(c.getGenericSuperclass()); + if ( superinterfaces ) + addAll(pending, c.getGenericInterfaces()); + } + } + + Type[] actualArgs = null; + + if ( ptFound ) + { + if ( null != latestBindings ) + pt = (ParameterizedType) + AbstractType.substitute(latestBindings, pt); + actualArgs = pt.getActualTypeArguments(); + if ( null != rtype ) + rtype[0] = pt; + } + else if ( rawTypeFound ) + { + actualArgs = new Type[0]; + if ( null != rtype ) + rtype[0] = expected; + } + + if ( null == actualArgs + || actualArgs.length != expected.getTypeParameters().length ) + throw new UnsupportedOperationException( + "failed checking whether " + candidate + + " specializes " + expected); + + return actualArgs; + } + + /** + * Returns the erasure of a type. + *

+ * If t is a {@code Class}, it is returned unchanged. + */ + public static Class erase(Type t) + { + if ( t instanceof Class ) + { + return (Class)t; + } + else if ( t instanceof GenericArrayType ) + { + int dims = 0; + do + { + ++ dims; + GenericArrayType a = (GenericArrayType)t; + t = a.getGenericComponentType(); + } while ( t instanceof GenericArrayType ); + Class c = (Class)erase(t); + // in Java 12+ see TypeDescriptor.ofField.arrayType(int) + return Array.newInstance(c, new int [ dims ]).getClass(); + } + else if ( t instanceof ParameterizedType ) + { + return (Class)((ParameterizedType)t).getRawType(); + } + else if ( t instanceof WildcardType ) + { + throw new UnsupportedOperationException("erase on wildcard type"); + /* + * Probably just resolve all the lower and/or upper bounds, as long + * as b is known to be the right set of bindings for the type that + * contains the member declaration, but I'm not convinced at present + * that wouldn't require more work keeping track of bindings. + */ + } + else if ( t instanceof TypeVariable ) + { + return erase(((TypeVariable)t).getBounds()[0]); + } + else + throw new UnsupportedOperationException( + "erase on unknown Type " + t.getClass()); + } + + /** + * Recursively descend t substituting any occurrence of a type variable + * found in b, returning a new object, or t unchanged if no substitutions + * were made. + *

+ * Currently throws {@code UnsupportedOperationException} if t is + * a wildcard, as that case shouldn't be needed for the analysis of + * class/interface inheritance hierarchies that {@code specialization} + * is concerned with. + *

+ */ + public static Type substitute(Bindings b, Type t) + { + if ( t instanceof GenericArrayType ) + { + GenericArrayType a = (GenericArrayType)t; + Type oc = a.getGenericComponentType(); + Type nc = substitute(b, oc); + if ( nc == oc ) + return t; + return new GenericArray(nc); + } + else if ( t instanceof ParameterizedType ) + { + ParameterizedType p = (ParameterizedType)t; + Type[] as = p.getActualTypeArguments(); + Type oown = p.getOwnerType(); + Type oraw = p.getRawType(); + assert oraw instanceof Class; + + boolean changed = substituted(b, as); + + if ( null != oown ) + { + Type nown = substitute(b, oown); + if ( nown != oown ) + { + oown = nown; + changed = true; + } + } + + if ( changed ) + return new Parameterized(as, oown, oraw); + return t; + } + else if ( t instanceof WildcardType ) + { + WildcardType w = (WildcardType)t; + Type[] lbs = w.getLowerBounds(); + Type[] ubs = w.getUpperBounds(); + + boolean changed = substituted(b, lbs) | substituted(b, ubs); + + if ( changed ) + return new Wildcard(lbs, ubs); + return t; + } + else if ( t instanceof TypeVariable ) + { + /* + * First the bad news: there isn't a reimplementation of + * TypeVariable here, to handle returning a changed version with + * substitutions in its bounds. Doesn't seem worth the effort, as + * the classes that hold/supply TypeVariables are Class/Method/ + * Constructor, and we're not going to be reimplementing *them*. + * + * Next the good news: TypeVariable bounds are the places where + * a good story for terminating recursion would be needed, so + * if we can't substitute in them anyway, that's a non-concern. + */ + return b.substitute((TypeVariable)t); + } + else if ( t instanceof Class ) + { + return t; + } + else + throw new UnsupportedOperationException( + "substitute on unknown Type " + t.getClass()); + } + + /** + * Applies substitutions in b to each type in types, + * updating them in place, returning true if any change resulted. + */ + private static boolean substituted(Bindings b, Type[] types) + { + boolean changed = false; + for ( int i = 0; i < types.length; ++ i ) + { + Type ot = types[i]; + Type nt = substitute(b, ot); + if ( nt == ot ) + continue; + types[i] = nt; + changed = true; + } + return changed; + } + + static String toString(Type t) + { + if ( t instanceof Class ) + return ((Class)t).getCanonicalName(); + return t.toString(); + } + + /** + * A key class for entering {@code TypeVariable}s in hash structures, + * without relying on the undocumented behavior of the Java implementation. + *

+ * Assumes that object identity is significant for + * {@code GenericDeclaration} instances ({@code Class} instances are chiefly + * what will be of interest here), just as {@code typesEqual} does. + */ + static final class VKey + { + private final TypeVariable m_tv; + + VKey(TypeVariable tv) + { + m_tv = tv; + } + + @Override + public int hashCode() + { + return + m_tv.getName().hashCode() + ^ identityHashCode(m_tv.getGenericDeclaration()); + } + + @Override + public boolean equals(Object other) + { + if ( this == other ) + return true; + if ( ! (other instanceof VKey) ) + return false; + return typesEqual(m_tv, ((VKey)other).m_tv); + } + + TypeVariable get() + { + return m_tv; + } + } + + public static TypeVariable[] freeVariables(Type t) + { + Set result = new HashSet<>(); + freeVariables(result, t); + return result.stream().map(VKey::get).toArray(TypeVariable[]::new); + } + + private static void freeVariables(Set s, Type t) + { + if ( t instanceof Class ) + return; + if ( t instanceof GenericArrayType ) + { + GenericArrayType a = (GenericArrayType)t; + freeVariables(s, a.getGenericComponentType()); + return; + } + if ( t instanceof ParameterizedType ) + { + ParameterizedType p = (ParameterizedType)t; + freeVariables(s, p.getOwnerType()); + stream(p.getActualTypeArguments()) + .forEach(tt -> freeVariables(s, tt)); + return; + } + if ( t instanceof TypeVariable ) + { + TypeVariable v = (TypeVariable)t; + if ( s.add(new VKey(v)) ) + stream(v.getBounds()).forEach(tt -> freeVariables(s, tt)); + return; + } + if ( t instanceof WildcardType ) + { + WildcardType w = (WildcardType)t; + concat(stream(w.getUpperBounds()), stream(w.getLowerBounds())) + .forEach(tt -> freeVariables(s, tt)); + return; + } + } + + @Override + public String getTypeName() + { + return toString(); + } + + static class GenericArray extends AbstractType implements GenericArrayType + { + private final Type component; + + GenericArray(Type component) + { + this.component = component; + } + + @Override + public Type getGenericComponentType() + { + return component; + } + + @Override + public String toString() + { + return toString(component) + "[]"; + } + } + + static class Parameterized extends AbstractType implements ParameterizedType + { + private final Type[] arguments; + private final Type owner; + private final Type raw; + + Parameterized(Type[] arguments, Type owner, Type raw) + { + this.arguments = arguments; + this.owner = owner; + this.raw = raw; + } + + @Override + public Type[] getActualTypeArguments() + { + return arguments; + } + + @Override + public Type getOwnerType() + { + return owner; + } + + @Override + public Type getRawType() + { + return raw; + } + + @Override + public String toString() + { + if ( 0 == arguments.length ) + return toString(raw); + return toString(raw) + stream(arguments) + .map(AbstractType::toString).collect(joining(",", "<", ">")); + } + } + + static class Wildcard extends AbstractType implements WildcardType + { + private final Type[] lbounds; + private final Type[] ubounds; + + Wildcard(Type[] lbounds, Type[] ubounds) + { + this.lbounds = lbounds; + this.ubounds = ubounds; + } + + @Override + public Type[] getLowerBounds() + { + return lbounds; + } + + @Override + public Type[] getUpperBounds() + { + return ubounds; + } + + @Override + public String toString() + { + if ( 0 < lbounds.length ) + return "? super " + stream(lbounds) + .map(AbstractType::toString).collect(joining(" & ")); + else if ( 0 < ubounds.length && Object.class != ubounds[0] ) + return "? extends " + stream(ubounds) + .map(AbstractType::toString).collect(joining(" & ")); + else + return "?"; + } + } + + /** + * A class recording the bindings made in a ParameterizedType to the type + * parameters in a GenericDeclaration<Class>. Implements {@code Type} + * so it can be added to the {@code pending} queue in + * {@code specialization}. + *

+ * In {@code specialization}, the tree of superclasses/superinterfaces will + * be searched breadth-first, with all of a node's immediate supers enqueued + * before any from the next level. By recording a node's type variable to + * type argument bindings in an object of this class, and enqueueing it + * before any of the node's supers, any type variables encountered as actual + * type arguments to any of those supers should be resolvable in the object + * of this class most recently dequeued. + */ + public static class Bindings implements Type + { + private final TypeVariable[] formalTypeParams; + private final Type[] actualTypeArgs; + + public Bindings(TypeVariable[] formalParams, Type[] actualArgs) + { + actualTypeArgs = actualArgs; + formalTypeParams = formalParams; + if ( actualTypeArgs.length != formalTypeParams.length ) + throw new IllegalArgumentException( + "formalParams and actualArgs differ in length"); + // XXX check actualTypeArgs against bounds of the formalParams + } + + Bindings(Bindings prior, ParameterizedType pt) + { + actualTypeArgs = pt.getActualTypeArguments(); + formalTypeParams = + ((GenericDeclaration)pt.getRawType()).getTypeParameters(); + assert actualTypeArgs.length == formalTypeParams.length; + + if ( 0 == prior.actualTypeArgs.length ) + return; + + for ( int i = 0; i < actualTypeArgs.length; ++ i ) + actualTypeArgs[i] = + AbstractType.substitute(prior, actualTypeArgs[i]); + } + + Type substitute(TypeVariable v) + { + for ( int i = 0; i < formalTypeParams.length; ++ i ) + if ( typesEqual(formalTypeParams[i], v) ) + return actualTypeArgs[i]; + return v; + } + } + + /** + * A class dedicated to manipulating the types of multidimensional Java + * arrays, and their instances, that conform to PostgreSQL array constraints + * (non-'jagged', each dimension's arrays all equal size, no intermediate + * nulls). + *

+ * Construct a {@code MultiArray} by supplying a component {@link Type} and + * a number of dimensions. The resulting {@code MultiArray} represents the + * Java array type, and has a number of bracket pairs equal to the supplied + * dimensions argument plus those of the component type if it is itself a + * Java array. (There could be an {@code Adapter} for some PostgreSQL scalar + * type that presents it as a Java array, and then there could be a + * PostgreSQL array of that type.) So the type reported by + * {@link #arrayType arrayType} may have more bracket pairs than the + * {@code MultiArray}'s dimensions. Parentheses are used by + * {@link #toString toString} to help see what's going on. + *

+ * When converting a {@code MultiArray} to a {@link Sized Sized}, only as + * many sizes are supplied as the multiarray's dimensions, and when + * converting that to an {@link Sized.Allocated Allocated}, only that much + * allocation is done. Populating the arrays at that last allocated level + * with the converted elements of the PostgreSQL array is the work left + * for the caller. + */ + public static class MultiArray + { + public final Type component; + public final int dimensions; + + /** + * Constructs a description of a multiarray with a given component type + * and dimensions. + * @param component the type of the component (which may itself be an + * array) + * @param dimensions dimensions of the multiarray (if the component type + * is an array, the final resulting type will have the sum of its + * dimensions and these) + */ + public MultiArray(Type component, int dimensions) + { + if ( 1 > dimensions ) + throw new IllegalArgumentException( + "dimensions must be positive: " + dimensions); + this.component = component; + this.dimensions = dimensions; + } + + /** + * Returns a representation of the resulting Java array type, with + * parentheses around the component type (which may itself be an array + * type) and around the array brackets corresponding to this + * multiarray's dimensions. + */ + @Override + public String toString() + { + return "MultiArray: (" + component + ")([])*" + dimensions; + } + + /** + * Returns the resulting Java array type (which, if the component type + * is also an array, does not distinguish between its dimensions and + * those of this multiarray). + */ + public Type arrayType() + { + Type t = component; + + if ( t instanceof Class ) + t = Array.newInstance((Class)t, new int[dimensions]) + .getClass(); + else + for ( int i = 0 ; i < dimensions ; ++ i ) + t = new GenericArray(t); + + return t; + } + + /** + * Returns a {@code MultiArray} representing an array type t + * in a canonical form, with its ultimate non-array type as the + * component type, and all of its array dimensions belonging to the + * multiarray. + */ + public static MultiArray canonicalize(Type t) + { + Type t1 = requireNonNull(t); + int dims = 0; + + for ( ;; ) + { + t1 = toElementIfArray(t1); + if ( null == t1 ) + break; + t = t1; + ++ dims; + } + + if ( 0 == dims ) + throw new IllegalArgumentException("not an array type: " + t); + + return new MultiArray(t, dims); + } + + /** + * Returns a new {@code MultiArray} with the same Java array type but + * where {@link #component} is a non-array type and {@link #dimensions} + * holds the total number of dimensions. + */ + public MultiArray canonicalize() + { + if ( null == toElementIfArray(component) ) + return this; + + MultiArray a = canonicalize(component); + return new MultiArray(a.component, dimensions + a.dimensions); + } + + /** + * Returns this {@code MultiArray} as a 'prefix' of suffix + * (which must have the same ultimate non-array type but a smaller + * number of dimensions). + *

+ * The result will have the array type of suffix as its + * component type, and the dimensions required to have the same overall + * Java {@link #arrayType arrayType} as the receiver. + */ + public MultiArray asPrefixOf(MultiArray suffix) + { + MultiArray pfx = canonicalize(); + MultiArray sfx = suffix.canonicalize(); + + if ( 1 + sfx.dimensions > pfx.dimensions ) + throw new IllegalArgumentException( + "suffix too long: ("+ this +").asPrefixOf("+ suffix +")"); + + if ( ! typesEqual(pfx.component, sfx.component) ) + throw new IllegalArgumentException( + "asPrefixOf with different component types: " + + pfx.component + ", " + sfx.component); + + Type c = sfx.arrayType(); + + return new MultiArray(c, pfx.dimensions - sfx.dimensions); + } + + /** + * Returns a new {@code MultiArray} with this one's type (possibly a + * raw, or parameterized type) refined according to the known type of + * model. + */ + public MultiArray refine(Type model) + { + int modelDims = 0; + + if ( null != toElementIfArray(model) ) + { + MultiArray cmodel = canonicalize(model); + modelDims = cmodel.dimensions; + model = cmodel.component; + } + + MultiArray canon = canonicalize(); + + Type[] rtype = new Type[1]; + if ( null == specialization(model, erase(canon.component), rtype) ) + throw new IllegalArgumentException( + "refine: " + model + " does not specialize " + + canon.component); + + MultiArray result = new MultiArray(rtype[0], canon.dimensions); + + if ( 0 < modelDims ) + { + MultiArray suffix = new MultiArray(rtype[0], modelDims); + result = result.asPrefixOf(suffix); + } + + return result; + } + + /** + * Returns a {@link Sized Sized} representing this {@code MultiArray} + * with a size for each of its dimensions. + */ + public Sized size(int... dims) + { + return new Sized(dims); + } + + /** + * Represents a {@code MultiArray} for which sizes for its dimensions + * have been specified, so that an instance can be allocated. + */ + public class Sized + { + private final int[] lengths; + + private Sized(int[] dims) + { + if ( dims.length != dimensions ) + throw new IllegalArgumentException( + "("+ this +").size(passed " + + dims.length +" dimensions)"); + lengths = dims.clone(); + } + + @Override + public String toString() + { + return MultiArray.this.toString(); + } + + /** + * Returns an {@link Allocated Allocated} that wraps a + * freshly-allocated array having the sizes recorded here. + *

+ * The result is returned with wildcard types. If the caller code + * has been written so as to have type variables with the proper + * types at compile time, it may do an unchecked cast on the result, + * which may make later operations more concise. + */ + public Allocated allocate() + { + Class c = erase(component); + Object a = Array.newInstance(c, lengths); + + return new Allocated(a); + } + + /** + * Wraps an existing instance of the multiarray type in question. + * + * @param the overall Java type of the whole array, which + * can be retrieved with array() + * @param the type of the arrays at the final level + * (one-dimensional arrays of the component type) that can be + * iterated, in order, to be populated or read out. <TI> is + * always an array type, but can be a reference array or any + * primitive array type, and therefore not as convenient as it might + * be, because the least upper bound of those types is + * {@code Object}. + */ + public class Allocated implements Iterable + { + final Object array; + + private Allocated(Object a) + { + array = requireNonNull(a); + } + + /** + * Returns the resulting array. + */ + public TA array() + { + @SuppressWarnings("unchecked") + TA result = (TA)array; + return result; + } + + @Override + public String toString() + { + return MultiArray.this.toString(); + } + + /** + * Returns an {@code Iterator} over the array(s) at the bottom + * level of this multiarray, the ones that are one-dimensional + * arrays of the component type. + *

+ * They are returned in order, so that a simple loop to copy the + * component values into or out of each array in turn will + * amount to a row-major traversal (same as PostgreSQL's storage + * order) of the whole array. + */ + @Override + public Iterator iterator() + { + final Object[][] arrays = new Object [ dimensions ] []; + final int[] indices = new int [ dimensions ]; + final int rightmost = dimensions - 1; + + arrays[0] = new Object[] { array }; + + for ( int i = 1; i < arrays.length; ++ i ) + { + Object[] a = arrays[i-1]; + if ( 0 == a.length ) + { + ++ indices[0]; + break; + } + arrays[i] = (Object[])requireNonNull(a[0]); + } + + return new Iterator() + { + @Override + public boolean hasNext() + { + return 0 == indices[0]; + } + + @Override + public TI next() + { + if ( 0 < indices[0] ) + throw new NoSuchElementException(); + + @SuppressWarnings("unchecked") + TI o = (TI)arrays[rightmost][indices[rightmost]++]; + + if (indices[rightmost] >= arrays[rightmost].length) + { + int i = rightmost - 1; + while ( 0 <= i ) + { + if ( ++ indices[i] < arrays[i].length ) + break; + -- i; + } + if ( 0 <= i ) + { + while ( i < rightmost ) + { + Object a = arrays[i][indices[i]]; + ++ i; + arrays[i] = (Object[])requireNonNull(a); + indices[i] = 0; + } + } + } + + return o; + } + }; + } + } + } + } +} diff --git a/pljava-api/src/main/java/org/postgresql/pljava/adt/spi/Datum.java b/pljava-api/src/main/java/org/postgresql/pljava/adt/spi/Datum.java new file mode 100644 index 000000000..84fbd669c --- /dev/null +++ b/pljava-api/src/main/java/org/postgresql/pljava/adt/spi/Datum.java @@ -0,0 +1,208 @@ +/* + * Copyright (c) 2022-2023 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.adt.spi; + +import java.io.Closeable; +import java.io.InputStream; + +import java.nio.ByteBuffer; + +import java.sql.SQLException; + +import org.postgresql.pljava.Adapter; // for javadoc +import org.postgresql.pljava.model.Attribute; + +/** + * Raw access to the contents of a PostgreSQL datum. + *

+ * For type safety, only {@link Adapter Adapter} implementations should be + * able to obtain a {@code Datum}, and should avoid leaking it to other code. + */ +public interface Datum extends Closeable +{ + /** + * Use the given {@link Verifier} to confirm that the {@code Datum} content + * is well-formed, throwing an exception if not. + */ + void verify(Verifier.OfBuffer v) throws SQLException; + + /** + * Use the given {@link Verifier} to confirm that the {@code Datum} content + * is well-formed, throwing an exception if not. + */ + void verify(Verifier.OfStream v) throws SQLException; + + /** + * Interface through which PL/Java code reads the content of an existing + * PostgreSQL datum. + */ + interface Input extends Datum + { + default void pin() throws SQLException + { + } + + default boolean pinUnlessReleased() + { + return false; + } + + default void unpin() + { + } + + /** + * Returns a read-only {@link ByteBuffer} covering the content of the + * datum. + *

+ * When the datum is a {@code varlena}, the "content" does not include + * the four-byte header. When implementing an adapter for a varlena + * datatype, note carefully whether offsets used in the PostgreSQL C + * code are relative to the start of the content or the start of the + * varlena overall. If the latter, they will need adjustment when + * indexing into the {@code ByteBuffer}. + *

+ * If the byte order of the buffer will matter, it should be explicitly + * set. + *

+ * The buffer may window native memory allocated by PostgreSQL, so + * {@link #pin pin()} and {@link #unpin unpin()} should surround + * accesses through it. Like {@code Datum} itself, the + * {@code ByteBuffer} should be used only within an {@code Adapter}, and + * not exposed to other code. + */ + ByteBuffer buffer() throws SQLException; + + /** + * Returns an {@link InputStream} that presents the same bytes contained + * in the buffer returned by {@link #buffer buffer()}. + *

+ * When necessary, the {@code InputStream} will handle pinning the + * buffer when reading, so the {@code InputStream} can safely be exposed + * to other code, if it is a reasonable way to present the contents of + * the datatype in question. + *

+ * The stream supports {@code mark} and {@code reset}. + */ + T inputStream() throws SQLException; + } + + /** + * Empty superinterface of {@code Accessor.Deformed} and + * {@code Accessor.Heap}, which are erased at run time but help distinguish, + * in source code, which memory layout convention an {@code Accessor} + * is tailored for. + */ + interface Layout + { + } + + /** + * Accessor for a {@code Datum} located, at some offset, in + * memory represented by a {@code } object. + *

+ * {@code } is a type variable to anticipate future memory abstractions + * like the incubating {@code MemorySegment} from JEP 412. The present + * implementation will work with any {@code } that you want as long + * as it is {@code java.nio.ByteBuffer}. + *

+ * Given an {@code Accessor} instance properly selected for the memory + * layout, datum width, type length, and by-value/by-reference passing + * convention declared for a given {@link Attribute Attribute}, methods on + * the {@code Accessor} are available to retrieve the individual datum + * in {@code Datum} form (essentially another {@code } of exactly + * the length of the datum, wrapped with methods to avoid access outside + * of its lifetime), or as any Java primitive type appropriate to + * the datum's width. A {@code get} method of the datum's exact width or + * wider may be used (except for {@code float} and {@code double}, which + * only work for width exactly 4 or 8 bytes, respectively). + *

+ * PostgreSQL only allows power-of-two widths up to {@code SIZEOF_DATUM} for + * a type that specifies the by-value convention, and so an {@code Accessor} + * for the by-value case only supports those widths. An {@code Accessor} for + * the by-reference case supports any size, with direct access as a Java + * primitive supported for any size up to the width of a Java long. + *

+ * {@code getBoolean} can be used for any width the {@code Accessor} + * supports up to the width of Java long, and the result will be true + * if the value has any 1 bits. + *

+ * Java {@code long} and {@code int} are always treated as + * signed by the language (though unsigned operations are available as + * methods), but have paired methods here to explicitly indicate which + * treatment is intended. The choice can affect the returned value when + * fetching a value as a primitive type that is wider than its type's + * declared length. Paired methods for {@code byte} are not provided because + * a byte is not wider than any type's length. When a type narrower than + * {@code SIZEOF_DATUM} is stored (in the {@code Deformed} layout), unused + * high bits are stored as zero. This should not strictly matter, as + * PostgreSQL strictly ignores the unused high bits, but it is consistent + * with the way PostgreSQL declares {@code Datum} as an unsigned integral + * type. + * + * @param type of the memory abstraction used. Accessors will be + * available supporting {@code ByteBuffer}, and may be available supporting + * a newer abstraction like {@code MemorySegment}. + * @param a subinterface of {@code Layout}, either {@code Deformed} or + * {@code Heap}, indicating which {@code TupleTableSlot} layout the + * {@code Accessor} is intended for, chiefly as a tool for compile-time + * checking that they haven't been mixed up. + */ + interface Accessor + { + Datum.Input getDatum(B buffer, int offset, Attribute a); + + long getLongSignExtended(B buffer, int offset); + + long getLongZeroExtended(B buffer, int offset); + + double getDouble(B buffer, int offset); + + int getIntSignExtended(B buffer, int offset); + + int getIntZeroExtended(B buffer, int offset); + + float getFloat(B buffer, int offset); + + short getShort(B buffer, int offset); + + char getChar(B buffer, int offset); + + byte getByte(B buffer, int offset); + + boolean getBoolean(B buffer, int offset); + + /** + * An accessor for use with a 'deformed' (array-of-{@code Datum}) + * memory layout. + *

+ * When using a 'deformed' accessor, the caller is responsible for + * passing an {@code offset} value that is an integral multiple of + * {@code SIZEOF_DATUM} from where the array-of-{@code Datum} starts. + */ + interface Deformed extends Layout + { + } + + /** + * An accessor for use with a heap-tuple styled, flattened, + * memory layout. + *

+ * When using a heap accessor, the caller is responsible for passing an + * {@code offset} value properly computed from the sizes of preceding + * members and the alignment of the member to be accessed. + */ + interface Heap extends Layout + { + } + } +} diff --git a/pljava-api/src/main/java/org/postgresql/pljava/adt/spi/TwosComplement.java b/pljava-api/src/main/java/org/postgresql/pljava/adt/spi/TwosComplement.java new file mode 100644 index 000000000..d2323ed96 --- /dev/null +++ b/pljava-api/src/main/java/org/postgresql/pljava/adt/spi/TwosComplement.java @@ -0,0 +1,560 @@ +/* + * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.adt.spi; + +/** + * Methods that have variants on twos-complement Java types that might be signed + * or unsigned. + *

+ * The {@code Signed} or {@code Unsigned} subinterface below, as appropriate, + * can be used as a mixin on a class where the right treatment of a Java + * {@code long}, {@code int}, {@code short}, or {@code byte} might be + * class-specific. + *

+ * The semantic difference between a {@code short} treated as unsigned and a + * {@code char} (also an unsigned 16-bit type) is whether the value is expected + * to mean what UTF-16 says it means. + */ +public interface TwosComplement +{ + boolean unsigned(); + + /* + * Methods for long + */ + + int compare(long x, long y); + + long divide(long dividend, long divisor); + + long remainder(long dividend, long divisor); + + long parseLong(CharSequence s, int beginIndex, int endIndex, int radix); + + String deparse(long i, int radix); + + default long parseLong(CharSequence s, int radix) + { + return parseLong(s, 0, s.length(), radix); + } + + default long parseLong(CharSequence s) + { + return parseLong(s, 0, s.length(), 10); + } + + default String deparse(long i) + { + return deparse(i, 10); + } + + /* + * Methods for int + */ + + int compare(int x, int y); + + int divide(int dividend, int divisor); + + int remainder(int dividend, int divisor); + + long toLong(int i); + + int parseInt(CharSequence s, int beginIndex, int endIndex, int radix); + + String deparse(int i, int radix); + + default int parseInt(CharSequence s, int radix) + { + return parseInt(s, 0, s.length(), radix); + } + + default int parseInt(CharSequence s) + { + return parseInt(s, 0, s.length(), 10); + } + + default String deparse(int i) + { + return deparse(i, 10); + } + + /* + * Methods for short + */ + + int compare(short x, short y); + + short divide(short dividend, short divisor); + + short remainder(short dividend, short divisor); + + long toLong(short i); + + int toInt(short i); + + short parseShort(CharSequence s, int beginIndex, int endIndex, int radix); + + String deparse(short i, int radix); + + default short parseShort(CharSequence s, int radix) + { + return parseShort(s, 0, s.length(), radix); + } + + default short parseShort(CharSequence s) + { + return parseShort(s, 0, s.length(), 10); + } + + default String deparse(short i) + { + return deparse(i, 10); + } + + /* + * Methods for byte + */ + + int compare(byte x, byte y); + + byte divide(byte dividend, byte divisor); + + byte remainder(byte dividend, byte divisor); + + long toLong(byte i); + + int toInt(byte i); + + short toShort(byte i); + + byte parseByte(CharSequence s, int beginIndex, int endIndex, int radix); + + String deparse(byte i, int radix); + + default byte parseByte(CharSequence s, int radix) + { + return parseByte(s, 0, s.length(), radix); + } + + default byte parseByte(CharSequence s) + { + return parseByte(s, 0, s.length(), 10); + } + + default String deparse(byte i) + { + return deparse(i, 10); + } + + /** + * Mixin with default signed implementations of the interface methods. + */ + interface Signed extends TwosComplement + { + @Override + default boolean unsigned() + { + return false; + } + + /* + * Methods for long + */ + + @Override + default int compare(long x, long y) + { + return Long.compare(x, y); + } + + @Override + default long divide(long dividend, long divisor) + { + return dividend / divisor; + } + + @Override + default long remainder(long dividend, long divisor) + { + return dividend % divisor; + } + + @Override + default long parseLong( + CharSequence s, int beginIndex, int endIndex, int radix) + { + return Long.parseLong(s, beginIndex, endIndex, radix); + } + + @Override + default String deparse(long i, int radix) + { + return Long.toString(i, radix); + } + + /* + * Methods for int + */ + + @Override + default int compare(int x, int y) + { + return Integer.compare(x, y); + } + + @Override + default int divide(int dividend, int divisor) + { + return dividend / divisor; + } + + @Override + default int remainder(int dividend, int divisor) + { + return dividend % divisor; + } + + @Override + default long toLong(int i) + { + return i; + } + + @Override + default int parseInt( + CharSequence s, int beginIndex, int endIndex, int radix) + { + return Integer.parseInt(s, beginIndex, endIndex, radix); + } + + @Override + default String deparse(int i, int radix) + { + return Integer.toString(i, radix); + } + + /* + * Methods for short + */ + + @Override + default int compare(short x, short y) + { + return Short.compare(x, y); + } + + @Override + default short divide(short dividend, short divisor) + { + return (short)(dividend / divisor); + } + + @Override + default short remainder(short dividend, short divisor) + { + return (short)(dividend % divisor); + } + + @Override + default long toLong(short i) + { + return i; + } + + @Override + default int toInt(short i) + { + return i; + } + + @Override + default short parseShort( + CharSequence s, int beginIndex, int endIndex, int radix) + { + int i = Integer.parseInt(s, beginIndex, endIndex, radix); + if ( Short.MIN_VALUE <= i && i <= Short.MAX_VALUE ) + return (short)i; + throw new NumberFormatException(String.format( + "Value out of range. Value:\"%s\" Radix:%d", + s.subSequence(beginIndex, endIndex), radix)); + } + + @Override + default String deparse(short i, int radix) + { + return Integer.toString(i, radix); + } + + /* + * Methods for byte + */ + + @Override + default int compare(byte x, byte y) + { + return Byte.compare(x, y); + } + + @Override + default byte divide(byte dividend, byte divisor) + { + return (byte)(dividend / divisor); + } + + @Override + default byte remainder(byte dividend, byte divisor) + { + return (byte)(dividend % divisor); + } + + @Override + default long toLong(byte i) + { + return i; + } + + @Override + default int toInt(byte i) + { + return i; + } + + @Override + default short toShort(byte i) + { + return i; + } + + @Override + default byte parseByte( + CharSequence s, int beginIndex, int endIndex, int radix) + { + int i = Integer.parseInt(s, beginIndex, endIndex, radix); + if ( Byte.MIN_VALUE <= i && i <= Byte.MAX_VALUE ) + return (byte)i; + throw new NumberFormatException(String.format( + "Value out of range. Value:\"%s\" Radix:%d", + s.subSequence(beginIndex, endIndex), radix)); + } + + @Override + default String deparse(byte i, int radix) + { + return Integer.toString(i, radix); + } + } + + /** + * Mixin with default unsigned implementations of the interface methods. + */ + interface Unsigned extends TwosComplement + { + @Override + default boolean unsigned() + { + return true; + } + + /* + * Methods for long + */ + + @Override + default int compare(long x, long y) + { + return Long.compareUnsigned(x, y); + } + + @Override + default long divide(long dividend, long divisor) + { + return Long.divideUnsigned(dividend, divisor); + } + + @Override + default long remainder(long dividend, long divisor) + { + return Long.remainderUnsigned(dividend, divisor); + } + + @Override + default long parseLong( + CharSequence s, int beginIndex, int endIndex, int radix) + { + return Long.parseUnsignedLong(s, beginIndex, endIndex, radix); + } + + @Override + default String deparse(long i, int radix) + { + return Long.toUnsignedString(i, radix); + } + + /* + * Methods for int + */ + + @Override + default int compare(int x, int y) + { + return Integer.compareUnsigned(x, y); + } + + @Override + default int divide(int dividend, int divisor) + { + return Integer.divideUnsigned(dividend, divisor); + } + + @Override + default int remainder(int dividend, int divisor) + { + return Integer.remainderUnsigned(dividend, divisor); + } + + @Override + default long toLong(int i) + { + return Integer.toUnsignedLong(i); + } + + @Override + default int parseInt( + CharSequence s, int beginIndex, int endIndex, int radix) + { + return Integer.parseUnsignedInt(s, beginIndex, endIndex, radix); + } + + @Override + default String deparse(int i, int radix) + { + return Integer.toUnsignedString(i, radix); + } + + /* + * Methods for short + */ + + @Override + default int compare(short x, short y) + { + return Short.compareUnsigned(x, y); + } + + @Override + default short divide(short dividend, short divisor) + { + return (short) + Integer.divideUnsigned(toInt(dividend), toInt(divisor)); + } + + @Override + default short remainder(short dividend, short divisor) + { + return (short) + Integer.remainderUnsigned(toInt(dividend), toInt(divisor)); + } + + @Override + default long toLong(short i) + { + return Short.toUnsignedLong(i); + } + + @Override + default int toInt(short i) + { + return Short.toUnsignedInt(i); + } + + @Override + default short parseShort( + CharSequence s, int beginIndex, int endIndex, int radix) + { + int i = + Integer.parseUnsignedInt(s, beginIndex, endIndex, radix); + if ( 0 <= i && i <= 0xffff ) + return (short)i; + throw new NumberFormatException(String.format( + "Value out of range. Value:\"%s\" Radix:%d", + s.subSequence(beginIndex, endIndex), radix)); + } + + @Override + default String deparse(short i, int radix) + { + return Integer.toUnsignedString(toInt(i), radix); + } + + /* + * Methods for byte + */ + + @Override + default int compare(byte x, byte y) + { + return Byte.compareUnsigned(x, y); + } + + @Override + default byte divide(byte dividend, byte divisor) + { + return (byte) + Integer.divideUnsigned(toInt(dividend), toInt(divisor)); + } + + @Override + default byte remainder(byte dividend, byte divisor) + { + return (byte) + Integer.remainderUnsigned(toInt(dividend), toInt(divisor)); + } + + @Override + default long toLong(byte i) + { + return Byte.toUnsignedLong(i); + } + + @Override + default int toInt(byte i) + { + return Byte.toUnsignedInt(i); + } + + @Override + default short toShort(byte i) + { + return (short)Byte.toUnsignedInt(i); + } + + @Override + default byte parseByte( + CharSequence s, int beginIndex, int endIndex, int radix) + { + int i = + Integer.parseUnsignedInt(s, beginIndex, endIndex, radix); + if ( 0 <= i && i <= 0xff ) + return (byte)i; + throw new NumberFormatException(String.format( + "Value out of range. Value:\"%s\" Radix:%d", + s.subSequence(beginIndex, endIndex), radix)); + } + + @Override + default String deparse(byte i, int radix) + { + return Integer.toUnsignedString(toInt(i), radix); + } + } +} diff --git a/pljava-api/src/main/java/org/postgresql/pljava/adt/spi/Verifier.java b/pljava-api/src/main/java/org/postgresql/pljava/adt/spi/Verifier.java new file mode 100644 index 000000000..7ee581a4a --- /dev/null +++ b/pljava-api/src/main/java/org/postgresql/pljava/adt/spi/Verifier.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.adt.spi; + +import java.io.InputStream; + +import java.nio.ByteBuffer; + +/** + * A {@code Verifier} verifies the proper form of content written to a + * {@code Datum}. + *

+ * This is necessary only when the correctness of the written stream may be + * doubtful, as when an API spec requires exposing a method for client code + * to write arbitrary bytes. If a type implementation exposes only + * type-appropriate operations to client code, and always controls the byte + * stream written to the varlena, the {@code NOOP} verifier can be used. + *

+ * There are no methods accepting an unextended {@code Verifier}, only those + * accepting one of its contained functional interfaces + * {@link OfBuffer OfBuffer} and {@link OfStream OfStream}. + *

+ * A type-specific verifier must supply a {@code verify} method that reads all + * of the content and completes normally if it is a complete and well-formed + * representation of the type. Otherwise, it must throw an exception. + *

+ * An {@code OfBuffer} verifier must leave the buffer's position equal to the + * value of the buffer's limit when the verifier was entered. An + * {@code OfStream} verifier must leave the stream at end of input. An + * {@code OfStream} verifier may assume that the supplied {@code InputStream} + * supports {@code mark} and {@code reset} efficiently. + *

+ * An {@code OfStream} verifier may execute in another thread concurrently with + * the writing of the content by the adapter. + * Its {@code verify} method must not interact with PostgreSQL. + */ +public interface Verifier +{ + /** + * A verifier interface to be used when the {@code ByteBuffer} API provides + * the most natural interface for manipulating the content. + *

+ * Such a verifier will be run only when the content has been completely + * produced. + */ + @FunctionalInterface + interface OfBuffer extends Verifier + { + /** + * Completes normally if the verification succeeds, otherwise throwing + * an exception. + *

+ * The buffer's {@code position} when this method returns must equal the + * value of the buffer's {@code limit} when the method was called. + */ + void verify(ByteBuffer b) throws Exception; + } + + /** + * A verifier interface to be used when the {@code InputStream} API provides + * the most natural interface for manipulating the content. + *

+ * Such a verifier may be run concurrently in another thread while the + * data type adapter is writing the content. It must therefore be able to + * verify the content without interacting with PostgreSQL. + */ + @FunctionalInterface + interface OfStream extends Verifier + { + /** + * Completes normally if the verification succeeds, otherwise throwing + * an exception. + *

+ * The method must leave the stream at end-of-input. It may assume that + * the stream supports {@code mark} and {@code reset} efficiently. + * It must avoid interacting with PostgreSQL, in case it is run in + * another thread concurrently with the production of the content. + */ + void verify(InputStream s) throws Exception; + } +} diff --git a/pljava-api/src/main/java/org/postgresql/pljava/adt/spi/package-info.java b/pljava-api/src/main/java/org/postgresql/pljava/adt/spi/package-info.java new file mode 100644 index 000000000..de2fe153f --- /dev/null +++ b/pljava-api/src/main/java/org/postgresql/pljava/adt/spi/package-info.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +/** + * Types that will be of interest in the implementation of {@code Adapter}s. + *

+ * First-class PL/Java support for a new PostgreSQL data type entails + * implementation of an {@link Adapter Adapter}. Unlike non-{@code Adapter} + * code, an {@code Adapter} implementation may have to concern itself with + * the facilities in this package, {@code Datum} in particular. An + * {@code Adapter} should avoid leaking a {@code Datum} to non-{@code Adapter} + * code. + *

Adapter manager

+ *

+ * There needs to be an {@code Adapter}-manager service to accept application + * requests to connect x PostgreSQL type with y Java type + * and find or compose available {@code Adapter}s (built-in or by service + * loader) to do so. There is some work in that direction (the methods in + * {@link AbstractType AbstractType} should be helpful), but no such manager + * yet. + * @author Chapman Flack + */ +package org.postgresql.pljava.adt.spi; + +import org.postgresql.pljava.Adapter; diff --git a/pljava-api/src/main/java/org/postgresql/pljava/model/Attribute.java b/pljava-api/src/main/java/org/postgresql/pljava/model/Attribute.java new file mode 100644 index 000000000..d0fc94eca --- /dev/null +++ b/pljava-api/src/main/java/org/postgresql/pljava/model/Attribute.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.model; + +import org.postgresql.pljava.model.CatalogObject.*; + +import static org.postgresql.pljava.model.CatalogObject.Factory.*; + +import org.postgresql.pljava.annotation.BaseUDT.Alignment; +import org.postgresql.pljava.annotation.BaseUDT.Storage; + +import org.postgresql.pljava.sqlgen.Lexicals.Identifier.Simple; + +/** + * An attribute (column), either of a known relation, or of a transient record + * type. + *

+ * Instances of the transient kind may be retrieved from a + * {@link TupleDescriptor TupleDescriptor} and will compare unequal to other + * {@code Attribute} instances even with the same {@code classId}, + * {@code subId}, and {@code oid} (which will be {@code InvalidOid}); for such + * instances, {@link #containingTupleDescriptor() containingTupleDescriptor} + * will return the specific transient {@code TupleDescriptor} to which + * the attribute belongs. Such 'virtual' instances will appear to have + * the invalid {@code RegClass} as {@code relation()}, and all access granted + * to {@code public}. + */ +public interface Attribute +extends + Addressed, Component, Named, + AccessControlled +{ + /** + * CLASS rather than CLASSID because Attribute isn't an object class + * in its own right. + *

+ * This simply identifies the table in the catalog that holds attribute + * definitions. An Attribute is not regarded as an object of that 'class'; + * it is a subId of whatever other RegClass object it defines an attribute + * of. + */ + RegClass CLASS = formObjectId(RegClass.CLASSID, AttributeRelationId); + + enum Identity { INAPPLICABLE, GENERATED_ALWAYS, GENERATED_BY_DEFAULT } + + enum Generated { INAPPLICABLE, STORED } + + RegClass relation(); + RegType type(); + short length(); + int dimensions(); + int cachedOffset(); + boolean byValue(); + Alignment alignment(); + Storage storage(); + boolean notNull(); + boolean hasDefault(); + boolean hasMissing(); + Identity identity(); + Generated generated(); + boolean dropped(); + boolean local(); + int inheritanceCount(); + RegCollation collation(); + // options + // fdwoptions + // missingValue + + /** + * Returns the tuple descriptor to which this attribute belongs. + *

+ * For a 'cataloged' attribute corresponding to a known relation + * or row type, returns a {@code TupleDescriptor} for that. For a 'virtual' + * attribute obtained from some non-cataloged tuple descriptor, returns + * whatever {@code TupleDescriptor} it came from. + */ + TupleDescriptor containingTupleDescriptor(); +} diff --git a/pljava-api/src/main/java/org/postgresql/pljava/model/CatalogObject.java b/pljava-api/src/main/java/org/postgresql/pljava/model/CatalogObject.java new file mode 100644 index 000000000..6e8cf0908 --- /dev/null +++ b/pljava-api/src/main/java/org/postgresql/pljava/model/CatalogObject.java @@ -0,0 +1,641 @@ +/* + * Copyright (c) 2022-2023 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.model; + +import java.util.List; +import java.util.ServiceConfigurationError; +import java.util.ServiceLoader; + +import java.util.function.IntPredicate; + +import org.postgresql.pljava.sqlgen.Lexicals.Identifier; + +/** + * Base interface representing some object in the PostgreSQL catalogs, + * identified by its {@link #oid() oid}. + *

+ * The {@link #oid() oid} by itself does not constitute an object address until + * combined with a {@code classId} identifying the catalog to which it belongs. + * This topmost interface, therefore, represents a catalog object when only + * the {@code oid} is known, and the {@code classId} is: unknown, or simply + * understood from context. An instance of this interface can be explicitly + * combined with a {@code classId}, using the {@link #of of(classId)} method, + * which will yield an instance of an interface that extends {@link Addressed} + * and is specific to catalog objects of that class. + *

+ * A {@code classId}, in turn, is simply an instance of + * {@link RegClass RegClass} (the catalog of relations, whose name "class" + * reflects PostgreSQL's object-relational origins). It identifies the specific + * relation in the PostgreSQL catalogs where objects with that {@code classId} + * can be looked up. + *

+ * Every user relation, of course, is also represented by a {@code RegClass} + * instance, but not one that can be used to form a catalog object address. + * For that matter, not every class in the PostgreSQL catalogs is modeled by + * a class in PL/Java. Therefore, not just any {@code RegClass} instance can be + * passed to {@link #of of(classId)} as a {@code classId}. Those that can be + * have the more-specific type {@code RegClass.Known}, which also identifies + * the Java model class T that will be returned. + */ +public interface CatalogObject +{ + /** + * The distinct integer value that {@link #oid oid()} will return when + * {@link #isValid isValid()} is false. + *

+ * PostgreSQL catalogs typically use this value (rather than a nullable + * column and a null value) in cases where an object may or may not be + * specified and has not been. + */ + int InvalidOid = 0; + + /** + * This catalog object's object ID; the integer value that identifies the + * object to PostgreSQL when the containing catalog is known. + */ + int oid(); + + /** + * Whether this catalog object has a valid {@code oid} + * (any value other than {@code InvalidOid}). + *

+ * This is not the same as whether any corresponding catalog object actually + * exists. This question can be answered directly from the value of + * {@code oid()}. The existence question (which can be asked sensibly only + * of an {@link Addressed Addressed} instance with its + * {@link Addressed#exists exists()} method} can be answered only through + * a lookup attempt for the {@code oid} in the corresponding catalog. + *

+ * There is not a unique singleton invalid catalog object instance. Rather, + * there can be distinct {@link Addressed Addressed} instances that have + * the invalid {@code oid} and distinct {@code classId}s, as well as one + * singleton {@code CatalogObject} that has the invalid {@code oid} and + * no valid {@code classId}. + *

+ * When applied to a {@link RegRole.Grantee RegRole.Grantee}, this method + * simply returns the negation of {@link RegRole.Grantee#isPublic isPublic}, + * which is the method that should be preferred for clarity in that case. + */ + boolean isValid(); + + /** + * Return a catalog object as an {@code Addressed} instance in a known + * class. + *

+ * For example, if a {@code CatalogObject o} is read from an {@code oid} + * column known to represent a namespace, {@code o.of(RegNamespace.CLASSID)} + * will return a {@code RegNamespace} instance. + *

+ * An instance whose class id is already the desired one will return itself. + * On an instance that lacks a valid class id, {@code of} can apply any + * desired class id (a different instance will be returned). The invalid + * instance of any class can be converted to the (distinct) invalid instance + * of any other class. On an instance that is valid and already has a valid + * class id, {@code of} will throw an exception if the desired class id + * differs. + * @param classId A known class id, often from the CLASSID field of a known + * CatalogObject subclass. + * @param Specific subtype of Addressed that represents catalog objects + * with the given class id. + * @return An instance with this instance's oid and the desired class id + * (this instance, if the class id matches). + */ + > T of(RegClass.Known classId); + + /** + * A catalog object that has both {@code oid} and {@code classId} specified, + * and can be looked up in the PostgreSQL catalogs (where it may, or may + * not, be found). + * @param Specific subtype of Addressed that represents catalog objects + * with the given class id. + */ + interface Addressed> extends CatalogObject + { + /** + * Returns the {@code classId} (which is an instance of + * {@link RegClass.Known RegClass.Known} of this addressed catalog + * object. + */ + RegClass.Known classId(); + + /** + * Whether a catalog object with this address in fact exists in + * the PostgreSQL catalogs. + *

+ * Unlike {@link #isValid isValid()}, which depends only on the value + * of {@code oid()}, this reflects the result of a catalog lookup. + */ + boolean exists(); + + /** + * Whether this catalog object is shared across all databases in the + * cluster. + *

+ * Contrast {@link RegClass#isShared() isShared()}, a method found only + * on {@code RegClass}, which indicates whether that {@code RegClass} + * instance represents a shared relation. Catalog objects formed with + * that {@code RegClass} instance as their {@code classId} will have + * {@code shared() == true}, though the {@code RegClass} instance itself + * will have {@code shared() == false} (because it models a row in + * {@code pg_class} itself, a catalog that isn't shared). + * @return classId().isShared() + */ + default boolean shared() + { + return classId().isShared(); + } + } + + /** + * Interface for an object that is regarded as a component of some, other, + * addressed catalog object, and is identified by that other object's + * {@code classId} and {@code oid} along with an integer {@code subId}. + *

+ * The chief (only?) example is an {@link Attribute Attribute}, which is + * identified by the {@code classId} and {@code oid} of its containing + * relation, plus a {@code subId}. + */ + interface Component + { + int subId(); + } + + /** + * Interface for any catalog object that has a name, which can be + * an {@link Identifier.Simple Identifier.Simple} or an + * {@link Identifier.Operator Identifier.Operator}. + */ + interface Named> + { + T name(); + } + + /** + * Interface for any catalog object that has a name and also a namespace + * or schema (an associated instance of {@link RegNamespace RegNamespace}). + */ + interface Namespaced> + extends Named + { + RegNamespace namespace(); + + default Identifier.Qualified qualifiedName() + { + return name().withQualifier(namespaceName()); + } + + default Identifier.Simple namespaceName() + { + return namespace().name(); + } + } + + /** + * Interface for any catalog object that has an owner (an associated + * instance of {@link RegRole RegRole}. + */ + interface Owned + { + RegRole owner(); + } + + /** + * Interface for any catalog object with an access control list + * (a list of some type of {@code Grant}). + * @param The subtype of {@link Grant Grant} that applies to catalog + * objects of this type. + */ + interface AccessControlled + { + /** + * Simple list of direct grants. + *

+ * For any T except {@code Grant.OnRole}, simply returns the list of + * grants directly found in this catalog object's ACL. When T is + * {@code Grant.OnRole}, this catalog object is a {@code RegRole}, and + * the result contains a {@code Grant.OnRole} for every role R that is + * directly a member of the role this catalog object represents; each + * such grant has {@code maySetRole()} by definition, and + * {@code mayExercisePrivileges()} if and only if R has {@code inherit}. + */ + List grants(); + + /** + * Computed list of (possibly transitive) grants to grantee. + *

+ * For any T except {@code Grant.OnRole}, a list of grants to + * grantee assembled from: direct grants in this object's ACL + * to {@code PUBLIC}, or to grantee, or to any role R for which + * {@code R.grants(grantee).mayExercisePrivileges()} is true. + *

+ * When T is {@code Grant.OnRole}, this catalog object is a + * {@code RegRole}, and the result contains a {@code Grant.OnRole} for + * which {@code maySetRole()} is true if a membership path from + * grantee to this role exists, and + * {@code mayExercisePrivileges()} is true if such a path exists using + * only roles with {@code inherit()} true. (The {@code inherit()} status + * of this object itself is not considered.) + */ + List grants(RegRole grantee); // transitive closure when on RegRole + // aclitem[] acl(); + // { Oid grantee; Oid grantor; AclMode bits; } see nodes/parsenodes.h + } + + /** + * Interface representing any single {@code Grant} (or ACL item), a grant + * of some set of possible privileges, to some role, granted by some role. + */ + interface Grant + { + /** + * Role to which the accompanying privileges are granted. + *

+ * There is no actual role named {@code public}, but there is + * a distinguished instance {@link RegRole.Grantee#PUBLIC PUBLIC} of + * {@link RegRole.Grantee RegRole.Grantee}. + */ + RegRole.Grantee to(); + + /** + * Role responsible for granting these privileges. + */ + RegRole by(); + + /** + * Subtype of {@code Grant} representing the privileges that may be + * granted on an attribute (or column). + */ + interface OnAttribute extends SELECT, INSERT, UPDATE, REFERENCES { } + + /** + * Subtype of {@code Grant} representing the privileges that may be + * granted on a class (or relation, table, view). + */ + interface OnClass extends OnAttribute, DELETE, TRUNCATE, TRIGGER { } + + /** + * Subtype of {@code Grant} representing the privileges that may be + * granted on a database. + */ + interface OnDatabase extends CONNECT, CREATE, CREATE_TEMP { } + + /** + * Subtype of {@code Grant} representing the privileges that may be + * granted on a namespace (or schema). + */ + interface OnNamespace extends CREATE, USAGE { } + + /** + * Subtype of {@code Grant} representing the privileges that may be + * granted on a configuration setting. + */ + interface OnSetting extends SET, ALTER_SYSTEM { } + + /** + * Subtype of {@code Grant} representing the grants (of membership in, + * and/or privileges of, other roles) that may be made to a role. + */ + interface OnRole extends Grant + { + boolean mayExercisePrivileges(); + boolean maySetRole(); + boolean mayAdmin(); + } + } + + /** + * @hidden + */ + interface INSERT extends Grant + { + boolean insertGranted(); + boolean insertGrantable(); + } + + /** + * @hidden + */ + interface SELECT extends Grant + { + boolean selectGranted(); + boolean selectGrantable(); + } + + /** + * @hidden + */ + interface UPDATE extends Grant + { + boolean updateGranted(); + boolean updateGrantable(); + } + + /** + * @hidden + */ + interface DELETE extends Grant + { + boolean deleteGranted(); + boolean deleteGrantable(); + } + + /** + * @hidden + */ + interface TRUNCATE extends Grant + { + boolean truncateGranted(); + boolean truncateGrantable(); + } + + /** + * @hidden + */ + interface REFERENCES extends Grant + { + boolean referencesGranted(); + boolean referencesGrantable(); + } + + /** + * @hidden + */ + interface TRIGGER extends Grant + { + boolean triggerGranted(); + boolean triggerGrantable(); + } + + /** + * @hidden + */ + interface EXECUTE extends Grant + { + boolean executeGranted(); + boolean executeGrantable(); + } + + /** + * @hidden + */ + interface USAGE extends Grant + { + boolean usageGranted(); + boolean usageGrantable(); + } + + /** + * @hidden + */ + interface CREATE extends Grant + { + boolean createGranted(); + boolean createGrantable(); + } + + /** + * @hidden + */ + interface CREATE_TEMP extends Grant + { + boolean create_tempGranted(); + boolean create_tempGrantable(); + } + + /** + * @hidden + */ + interface CONNECT extends Grant + { + boolean connectGranted(); + boolean connectGrantable(); + } + + /** + * @hidden + */ + interface SET extends Grant + { + boolean setGranted(); + boolean setGrantable(); + } + + /** + * @hidden + */ + interface ALTER_SYSTEM extends Grant + { + boolean alterSystemGranted(); + boolean alterSystemGrantable(); + } + + /** + * @hidden + */ + abstract class Factory + { + static final Factory INSTANCE; + + static + { + INSTANCE = ServiceLoader + .load(Factory.class.getModule().getLayer(), Factory.class) + .findFirst().orElseThrow(() -> new ServiceConfigurationError( + "could not load PL/Java CatalogObject.Factory")); + } + + static > + RegClass.Known formClassId(int classId, Class clazz) + { + return INSTANCE.formClassIdImpl(classId, clazz); + } + + static > + T formObjectId(RegClass.Known classId, int objId) + { + return INSTANCE.formObjectIdImpl(classId, objId, v -> true); + } + + static > + T formObjectId( + RegClass.Known classId, int objId, IntPredicate versionTest) + { + return INSTANCE.formObjectIdImpl(classId, objId, versionTest); + } + + static Database currentDatabase(RegClass.Known classId) + { + return INSTANCE.currentDatabaseImpl(classId); + } + + static RegRole.Grantee publicGrantee() + { + return INSTANCE.publicGranteeImpl(); + } + + protected abstract > + RegClass.Known formClassIdImpl( + int classId, Class clazz); + + protected abstract > + T formObjectIdImpl( + RegClass.Known classId, int objId, IntPredicate versionTest); + + protected abstract Database + currentDatabaseImpl(RegClass.Known classId); + + protected abstract RegRole.Grantee publicGranteeImpl(); + + protected abstract CharsetEncoding serverEncoding(); + protected abstract CharsetEncoding clientEncoding(); + protected abstract CharsetEncoding encodingFromOrdinal(int ordinal); + protected abstract CharsetEncoding encodingFromName(String name); + + /* + * These magic numbers are hardcoded here inside the pljava-api project + * so they can be used in static initializers in API interfaces. The + * verification that they are the right magic numbers takes place in + * compilation of the pljava and pljava-so projects, where they are + * included from here, exported in JNI .h files, and compared using + * StaticAssertStmt to the corresponding values from PostgreSQL headers. + * + * Within groups here, numerical order is as good as any. When adding a + * constant here, add a corresponding CONFIRMCONST in ModelConstants.c. + */ + protected static final int TypeRelationId = 1247; + protected static final int AttributeRelationId = 1249; + protected static final int ProcedureRelationId = 1255; + protected static final int RelationRelationId = 1259; + protected static final int AuthIdRelationId = 1260; + protected static final int DatabaseRelationId = 1262; + protected static final int LanguageRelationId = 2612; + protected static final int NamespaceRelationId = 2615; + protected static final int OperatorRelationId = 2617; + protected static final int ExtensionRelationId = 3079; + protected static final int CollationRelationId = 3456; + protected static final int TSDictionaryRelationId = 3600; + protected static final int TSConfigRelationId = 3602; + + /* + * PG types good to have around because of corresponding JDBC types. + */ + protected static final int BOOLOID = 16; + protected static final int BYTEAOID = 17; + protected static final int CHAROID = 18; + protected static final int INT8OID = 20; + protected static final int INT2OID = 21; + protected static final int INT4OID = 23; + protected static final int XMLOID = 142; + protected static final int FLOAT4OID = 700; + protected static final int FLOAT8OID = 701; + protected static final int BPCHAROID = 1042; + protected static final int VARCHAROID = 1043; + protected static final int DATEOID = 1082; + protected static final int TIMEOID = 1083; + protected static final int TIMESTAMPOID = 1114; + protected static final int TIMESTAMPTZOID = 1184; + protected static final int TIMETZOID = 1266; + protected static final int BITOID = 1560; + protected static final int VARBITOID = 1562; + protected static final int NUMERICOID = 1700; + + /* + * PG types not mentioned in JDBC but bread-and-butter to PG devs. + */ + protected static final int TEXTOID = 25; + protected static final int UNKNOWNOID = 705; + protected static final int RECORDOID = 2249; + protected static final int CSTRINGOID = 2275; + protected static final int VOIDOID = 2278; + + /* + * PG types used in modeling PG types themselves. + */ + protected static final int NAMEOID = 19; + protected static final int REGPROCOID = 24; + protected static final int OIDOID = 26; + protected static final int PG_NODE_TREEOID = 194; + protected static final int ACLITEMOID = 1033; + protected static final int REGPROCEDUREOID = 2202; + protected static final int REGOPEROID = 2203; + protected static final int REGOPERATOROID = 2204; + protected static final int REGCLASSOID = 2205; + protected static final int REGTYPEOID = 2206; + protected static final int REGCONFIGOID = 3734; + protected static final int REGDICTIONARYOID = 3769; + protected static final int REGNAMESPACEOID = 4089; + protected static final int REGROLEOID = 4096; + protected static final int REGCOLLATIONOID = 4191; // v >= 130000 + + /* + * The well-known, pinned procedural languages. + */ + protected static final int INTERNALlanguageId = 12; + protected static final int ClanguageId = 13; + protected static final int SQLlanguageId = 14; + + /* + * The well-known, pinned namespaces. + */ + protected static final int PG_CATALOG_NAMESPACE = 11; + protected static final int PG_TOAST_NAMESPACE = 99; + + /* + * The well-known, pinned collations. + */ + protected static final int DEFAULT_COLLATION_OID = 100; + protected static final int C_COLLATION_OID = 950; + protected static final int POSIX_COLLATION_OID = 951; + + /* + * These magic numbers are assigned here to allow the various well-known + * PostgreSQL ResourceOwners to be retrieved without a proliferation of + * methods on the factory interface. These are arbitrary array indices, + * visible also to JNI code through the generated headers just as + * described above. The native initialization method may create, + * for example, an array of ByteBuffers that window the corresponding + * PostgreSQL globals, ordered according to these indices. The Java code + * implementing resourceOwner() can be ignorant of these specific values + * and simply use them to index the array. HOWEVER, it does know that + * the first one, index 0, refers to the current resource owner. + */ + protected static final int RSO_Current = 0; // must be index 0 + protected static final int RSO_CurTransaction = 1; + protected static final int RSO_TopTransaction = 2; + protected static final int RSO_AuxProcess = 3; + + protected abstract ResourceOwner resourceOwner(int which); + + /* + * Same as above but for the well-known PostgreSQL MemoryContexts. + * Again, the implementing code knows index 0 is for the current one. + */ + protected static final int MCX_CurrentMemory = 0; // must be index 0 + protected static final int MCX_TopMemory = 1; + protected static final int MCX_Error = 2; + protected static final int MCX_Postmaster = 3; + protected static final int MCX_CacheMemory = 4; + protected static final int MCX_Message = 5; + protected static final int MCX_TopTransaction = 6; + protected static final int MCX_CurTransaction = 7; + protected static final int MCX_Portal = 8; + /* + * A long-lived, never-reset context created by PL/Java as a child of + * TopMemoryContext. + */ + protected static final int MCX_JavaMemory = 9; + + protected abstract MemoryContext memoryContext(int which); + + protected abstract MemoryContext upperMemoryContext(); + } +} diff --git a/pljava-api/src/main/java/org/postgresql/pljava/model/CharsetEncoding.java b/pljava-api/src/main/java/org/postgresql/pljava/model/CharsetEncoding.java new file mode 100644 index 000000000..6e5543893 --- /dev/null +++ b/pljava-api/src/main/java/org/postgresql/pljava/model/CharsetEncoding.java @@ -0,0 +1,299 @@ +/* + * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.model; + +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; + +import java.nio.ByteBuffer; +import java.nio.CharBuffer; + +import java.nio.charset.Charset; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.CharsetEncoder; + +import java.nio.charset.CharacterCodingException; + +import java.sql.SQLException; + +import static org.postgresql.pljava.model.CatalogObject.Factory; + +import org.postgresql.pljava.adt.spi.Datum; + +/** + * Represents one of PostgreSQL's available character set encodings. + *

+ * Not all of the encodings that PostgreSQL supports for communication with + * the client are also supported for use in the backend and in storage. + * The {@link #usableOnServer usableOnServer} method identifies which ones + * are suitable as server encodings. + *

+ * The encoding that is in use for the current database cannot change during + * a session, and is found in the final {@link #SERVER_ENCODING SERVER_ENCODING} + * field. + *

+ * The encoding currently in use by the connected client may change during + * a session, and is returned by the {@link #clientEncoding clientEncoding} + * method. + *

+ * The {@link #charset charset} method returns the corresponding Java + * {@link Charset Charset} if that can be identified, and several convenience + * methods are provided to decode or encode values accordingly. + */ +public interface CharsetEncoding +{ + CharsetEncoding SERVER_ENCODING = Factory.INSTANCE.serverEncoding(); + + /** + * A distinguished {@code CharsetEncoding} representing uses such as + * {@code -1} in the {@code collencoding} column of {@code pg_collation}, + * indicating the collation is usable with any encoding. + *

+ * This is its only instance. + */ + Any ANY = new Any(); + + /** + * Returns the encoding currently selected by the connected client. + */ + static CharsetEncoding clientEncoding() + { + return Factory.INSTANCE.clientEncoding(); + } + + /** + * Returns the {@code CharsetEncoding} for the given PostgreSQL encoding + * number (as used in the {@code encoding} columns of some system catalogs). + * @throws IllegalArgumentException if the argument is not the ordinal of + * some known encoding + */ + static CharsetEncoding fromOrdinal(int ordinal) + { + return Factory.INSTANCE.encodingFromOrdinal(ordinal); + } + + /** + * Returns the {@code CharsetEncoding} for the given PostgreSQL encoding + * name. + * @throws IllegalArgumentException if the argument is not the name of + * some known encoding + */ + static CharsetEncoding fromName(String name) + { + return Factory.INSTANCE.encodingFromName(name); + } + + /** + * Returns the PostgreSQL encoding number (as used in the {@code encoding} + * columns of some system catalogs) for this encoding. + */ + int ordinal(); + + /** + * Returns the PostgreSQL name for this encoding. + *

+ * The PostgreSQL encoding names have a long history and may not match + * cleanly with more standardized names in modern libraries. + */ + String name(); + + /** + * Returns the name identifying this encoding in ICU (international + * components for Unicode), or null if its implementation in PostgreSQL + * does not define one. + *

+ * When present, the ICU name can be a better choice for matching encodings + * in other libraries. + */ + String icuName(); + + /** + * Indicates whether this encoding is usable as a server encoding. + */ + boolean usableOnServer(); + + /** + * Returns the corresponding Java {@link Charset Charset}, or null if none + * can be identified. + */ + Charset charset(); + + /** + * Returns a {@link CharsetDecoder CharsetDecoder}, configured to report + * all decoding errors (rather than silently substituting data), if + * {@link #charset charset()} would return a non-null value. + */ + default CharsetDecoder newDecoder() + { + return charset().newDecoder(); + } + + /** + * Returns a {@link CharsetEncoder CharsetEncoder}, configured to report + * all encoding errors (rather than silently substituting data), if + * {@link #charset charset()} would return a non-null value. + */ + default CharsetEncoder newEncoder() + { + return charset().newEncoder(); + } + + /** + * Decode bytes to characters, with exceptions reported. + *

+ * Unlike the corresponding convenience method on {@link Charset Charset}, + * this method will throw exceptions rather than silently substituting + * characters. This is a database system; it doesn't go changing your data + * without telling you. + *

+ * Other behaviors can be obtained by calling {@link #newDecoder newDecoder} + * and configuring it as desired. + */ + default CharBuffer decode(ByteBuffer bb) throws CharacterCodingException + { + return newDecoder().decode(bb); + } + + /** + * Encode characters to bytes, with exceptions reported. + *

+ * Unlike the corresponding convenience method on {@link Charset Charset}, + * this method will throw exceptions rather than silently substituting + * characters. This is a database system; it doesn't go changing your data + * without telling you. + *

+ * Other behaviors can be obtained by calling {@link #newEncoder newEncoder} + * and configuring it as desired. + */ + default ByteBuffer encode(CharBuffer cb) throws CharacterCodingException + { + return newEncoder().encode(cb); + } + + /** + * Encode characters to bytes, with exceptions reported. + *

+ * Unlike the corresponding convenience method on {@link Charset Charset}, + * this method will throw exceptions rather than silently substituting + * characters. This is a database system; it doesn't go changing your data + * without telling you. + *

+ * Other behaviors can be obtained by calling {@link #newEncoder newEncoder} + * and configuring it as desired. + */ + default ByteBuffer encode(String s) throws CharacterCodingException + { + return encode(CharBuffer.wrap(s)); + } + + /** + * Decode bytes to characters, with exceptions reported. + *

+ * The input {@link Datum Datum} is pinned around the decoding operation. + */ + default CharBuffer decode(Datum.Input in, boolean close) + throws SQLException, IOException + { + in.pin(); + try + { + return decode(in.buffer()); + } + finally + { + in.unpin(); + if ( close ) + in.close(); + } + } + + /** + * Return an {@link InputStreamReader InputStreamReader} that reports + * exceptions. + *

+ * Other behaviors can be obtained by calling {@link #newDecoder newDecoder} + * and configuring it as desired before constructing an + * {@code InputStreamReader}. + */ + default InputStreamReader reader(InputStream in) + { + return new InputStreamReader(in, newDecoder()); + } + + /** + * Return an {@link OutputStreamWriter OutputStreamWriter} that reports + * exceptions. + *

+ * Other behaviors can be obtained by calling {@link #newEncoder newEncoder} + * and configuring it as desired before constructing an + * {@code OutputStreamWriter}. + */ + default OutputStreamWriter writer(OutputStream out) + { + return new OutputStreamWriter(out, newEncoder()); + } + + /** + * A distinguished {@code CharsetEncoding} representing uses such as + * {@code -1} in the {@code collencoding} column of {@code pg_collation}, + * indicating the collation is usable with any encoding. + *

+ * This returns -1 from {@code ordinal()} and {@code null} or {@code false} + * from the other non-default methods according to their types. The only + * instance of this class is {@code CharsetEncoding.ANY}. + */ + class Any implements CharsetEncoding + { + private Any() + { + } + + @Override + public int ordinal() + { + return -1; + } + + @Override + public String name() + { + return null; + } + + @Override + public String icuName() + { + return null; + } + + @Override + public boolean usableOnServer() + { + return false; + } + + @Override + public Charset charset() + { + return null; + } + + @Override + public String toString() + { + return "CharsetEncoding.ANY"; + } + } +} diff --git a/pljava-api/src/main/java/org/postgresql/pljava/model/Database.java b/pljava-api/src/main/java/org/postgresql/pljava/model/Database.java new file mode 100644 index 000000000..f50a58907 --- /dev/null +++ b/pljava-api/src/main/java/org/postgresql/pljava/model/Database.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.model; + +import org.postgresql.pljava.model.CatalogObject.*; + +import static org.postgresql.pljava.model.CatalogObject.Factory.*; + +import org.postgresql.pljava.sqlgen.Lexicals.Identifier.Simple; + +/** + * Model of a database defined within the PostgreSQL cluster. + */ +public interface Database +extends + Addressed, Named, Owned, + AccessControlled +{ + RegClass.Known CLASSID = + formClassId(DatabaseRelationId, Database.class); + + Database CURRENT = currentDatabase(CLASSID); + + CharsetEncoding encoding(); + + /** + * A string identifying the collation rules for use in this database (when + * not overridden for a specific column or expression). + *

+ * At least through PostgreSQL 14, this is always the identifier of an + * operating system ("libc") collation, even in builds with ICU available. + */ + String collate(); + + /** + * A string identifying the collation rules for use in this database (when + * not overridden for a specific column or expression). + *

+ * At least through PostgreSQL 14, this is always the identifier of an + * operating system ("libc") collation, even in builds with ICU available. + */ + String ctype(); + + boolean template(); + boolean allowConnection(); + int connectionLimit(); + // oid lastsysoid + // xid frozenxid + // xid minmxid + // oid tablespace +} diff --git a/pljava-api/src/main/java/org/postgresql/pljava/model/Extension.java b/pljava-api/src/main/java/org/postgresql/pljava/model/Extension.java new file mode 100644 index 000000000..0bf782037 --- /dev/null +++ b/pljava-api/src/main/java/org/postgresql/pljava/model/Extension.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.model; + +import java.util.List; + +import org.postgresql.pljava.model.CatalogObject.*; + +import static org.postgresql.pljava.model.CatalogObject.Factory.*; + +import org.postgresql.pljava.sqlgen.Lexicals.Identifier.Simple; + +/** + * Model of a PostgreSQL extension that has been installed for the current + * database. + */ +public interface Extension +extends Addressed, Named, Owned +{ + RegClass.Known CLASSID = + formClassId(ExtensionRelationId, Extension.class); + + /** + * Namespace in which most (or all, for a relocatable extension) of the + * namespace-qualified objects belonging to the extension are installed. + *

+ * Not a namespace qualifying the extension's name; extensions are not + * namespace-qualified. + */ + RegNamespace namespace(); + boolean relocatable(); + String version(); + List config(); + List condition(); +} diff --git a/pljava-api/src/main/java/org/postgresql/pljava/model/MemoryContext.java b/pljava-api/src/main/java/org/postgresql/pljava/model/MemoryContext.java new file mode 100644 index 000000000..917012e4c --- /dev/null +++ b/pljava-api/src/main/java/org/postgresql/pljava/model/MemoryContext.java @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.model; + +import org.postgresql.pljava.Lifespan; + +import static org.postgresql.pljava.model.CatalogObject.Factory.*; + +/** + * A PostgreSQL {@code MemoryContext}, which is usable as a PL/Java + * {@link Lifespan Lifespan} to scope the lifetimes of PL/Java objects + * (as when they depend on native memory allocated in the underlying context). + *

+ * The {@code MemoryContext} API in PostgreSQL is described here. + *

+ * Static getters for the globally known contexts are spelled and capitalized + * as they are in PostgreSQL. + */ +public interface MemoryContext extends Lifespan +{ + /** + * The top level of the context tree, of which every other context is + * a descendant. + *

+ * Used as described here. + */ + MemoryContext TopMemoryContext = + INSTANCE.memoryContext(MCX_TopMemory); + + /** + * The "current" memory context, which supplies all allocations made by + * PostgreSQL {@code palloc} and related functions that do not explicitly + * specify a context. + *

+ * Used as described here. + */ + static MemoryContext CurrentMemoryContext() + { + return INSTANCE.memoryContext(MCX_CurrentMemory); + } + + /** + * Getter method equivalent to the final + * {@link #TopMemoryContext TopMemoryContext} field, for consistency with + * the other static getters. + */ + static MemoryContext TopMemoryContext() + { + return TopMemoryContext; + } + + /** + * Holds everything that lives until end of the top-level transaction. + *

+ * Can be appropriate when a specification, for example JDBC, provides that + * an object should remain valid for the life of the transaction. + *

+ * Uses are described here. + */ + static MemoryContext TopTransactionContext() + { + return INSTANCE.memoryContext(MCX_TopTransaction); + } + + /** + * The same as {@link #TopTransactionContext() TopTransactionContext} when + * in a top-level transaction, but different in subtransactions (such as + * those associated with PL/Java savepoints). + *

+ * Used as described here. + */ + static MemoryContext CurTransactionContext() + { + return INSTANCE.memoryContext(MCX_CurTransaction); + } + + /** + * Context of the currently active execution portal. + *

+ * Used as described here. + */ + static MemoryContext PortalContext() + { + return INSTANCE.memoryContext(MCX_Portal); + } + + /** + * A permanent context switched into for error recovery processing. + *

+ * Used as described here. + */ + static MemoryContext ErrorContext() + { + return INSTANCE.memoryContext(MCX_Error); + } + + /** + * A long-lived, never-reset context created by PL/Java as a child of + * {@code TopMemoryContext}. + *

+ * Perhaps useful for PL/Java-related allocations that will be long-lived, + * or managed only from the Java side, as a way of accounting for them + * separately, as opposed to just putting them in {@code TopMemoryContext}. + * It hasn't been used consistently even in the historical PL/Java + * code base, and should perhaps be a candidate for deprecation (or for + * a thorough code review to establish firmer guidelines for its use). + */ + static MemoryContext JavaMemoryContext() + { + return INSTANCE.memoryContext(MCX_JavaMemory); + } + + /** + * The "upper executor" memory context (that is, the context on entry, prior + * to any use of SPI) associated with the current (innermost) PL/Java + * function invocation. + *

+ * This is "precisely the right context for a value returned" from a + * function that uses SPI, as described + * here. + */ + static MemoryContext UpperMemoryContext() + { + return INSTANCE.upperMemoryContext(); + } +} diff --git a/pljava-api/src/main/java/org/postgresql/pljava/model/Portal.java b/pljava-api/src/main/java/org/postgresql/pljava/model/Portal.java new file mode 100644 index 000000000..c4f8528ec --- /dev/null +++ b/pljava-api/src/main/java/org/postgresql/pljava/model/Portal.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2023 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.model; + +import java.sql.SQLException; + +import java.util.List; + +/** + * Models a PostgreSQL {@code Portal}, an object representing the ongoing + * execution of a query and capable of returning a {@link TupleDescriptor} for + * the result, and fetching tuples of the result, either all at once, or in + * smaller batches. + */ +public interface Portal extends AutoCloseable +{ + /** + * The direction modes that can be used with {@link #fetch fetch} + * and {@link #move move}. + */ + enum Direction { FORWARD, BACKWARD, ABSOLUTE, RELATIVE } + + /** + * A distinguished value for the count argument to + * {@link #fetch fetch} or {@link #move move}. + */ + long ALL = Long.MAX_VALUE; + + @Override + void close(); // AutoCloseable without checked exceptions + + /** + * Returns the {@link TupleDescriptor} describing any tuples that may be + * fetched from this {@code Portal}. + */ + TupleDescriptor tupleDescriptor() throws SQLException; + + /** + * Fetches count more tuples (or {@link #ALL ALL} of them) in the + * specified direction. + * @return a notional List of the fetched tuples. Iterating through the list + * may return the same TupleTableSlot repeatedly, with each tuple in turn + * stored in the slot. + * @see "PostgreSQL documentation for SPI_scroll_cursor_fetch" + */ + List fetch(Direction dir, long count) + throws SQLException; + + /** + * Moves the {@code Portal}'s current position count rows (or + * {@link #ALL ALL} possible) in the specified direction. + * @return the number of rows by which the position actually moved + * @see "PostgreSQL documentation for SPI_scroll_cursor_move" + */ + long move(Direction dir, long count) + throws SQLException; +} diff --git a/pljava-api/src/main/java/org/postgresql/pljava/model/ProceduralLanguage.java b/pljava-api/src/main/java/org/postgresql/pljava/model/ProceduralLanguage.java new file mode 100644 index 000000000..291be54da --- /dev/null +++ b/pljava-api/src/main/java/org/postgresql/pljava/model/ProceduralLanguage.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.model; + +import org.postgresql.pljava.model.CatalogObject.*; + +import static org.postgresql.pljava.model.CatalogObject.Factory.*; + +import org.postgresql.pljava.model.RegProcedure.Memo; + +import org.postgresql.pljava.sqlgen.Lexicals.Identifier.Simple; + +import org.postgresql.pljava.PLPrincipal; + +import org.postgresql.pljava.annotation.Function.Trust; + +/** + * Model of a PostgreSQL procedural language, including (for non-built-in + * languages, like PL/Java) the handler functions used in its implementation. + */ +public interface ProceduralLanguage +extends + Addressed, Named, Owned, AccessControlled +{ + RegClass.Known CLASSID = + formClassId(LanguageRelationId, ProceduralLanguage.class); + + /** + * The well-known language "internal", for routines implemented within + * PostgreSQL itself. + */ + ProceduralLanguage INTERNAL = formObjectId(CLASSID, INTERNALlanguageId); + + /** + * The well-known language "c", for extension routines implemented using + * PostgreSQL's C language conventions. + */ + ProceduralLanguage C = formObjectId(CLASSID, ClanguageId); + + /** + * The well-known language "sql", for routines in that PostgreSQL + * built-in language. + */ + ProceduralLanguage SQL = formObjectId(CLASSID, SQLlanguageId); + + interface Handler extends Memo { } + interface InlineHandler extends Memo { } + interface Validator extends Memo { } + + default Trust trust() + { + return principal().trust(); + } + + PLPrincipal principal(); + RegProcedure handler(); + RegProcedure inlineHandler(); + RegProcedure validator(); +} diff --git a/pljava-api/src/main/java/org/postgresql/pljava/model/RegClass.java b/pljava-api/src/main/java/org/postgresql/pljava/model/RegClass.java new file mode 100644 index 000000000..6580760f5 --- /dev/null +++ b/pljava-api/src/main/java/org/postgresql/pljava/model/RegClass.java @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.model; + +import java.util.List; + +import org.postgresql.pljava.model.CatalogObject.*; + +import static org.postgresql.pljava.model.CatalogObject.Factory.*; + +import org.postgresql.pljava.sqlgen.Lexicals.Identifier.Simple; + +/** + * Model of PostgreSQL relations/"classes"/tables. + *

+ * Instances of {@code RegClass} also serve as the "class ID" values for + * objects within the catalog (including for {@code RegClass} objects, which + * are no different from others in being defined by rows that appear in a + * catalog table; there is a row in {@code pg_class} for {@code pg_class}). + */ +public interface RegClass +extends + Addressed, Namespaced, Owned, + AccessControlled +{ + Known CLASSID = formClassId(RelationRelationId, RegClass.class); + + /** + * A more-specifically-typed subinterface of {@code RegClass}, used in the + * {@code CLASSID} static fields of interfaces in this package. + * @param identifies the specific CatalogObject.Addressed subinterface + * to result when this is applied as the {@code classId} to a bare + * {@code CatalogObject}. + */ + interface Known> extends RegClass + { + } + + /** + * The PostgreSQL type that is associated with this relation as its + * "row type". + *

+ * This is the type that will be found in a + * {@link TupleDescriptor TupleDescriptor} for this relation. + */ + RegType type(); + + /** + * Only for a relation that was created with {@code CREATE TABLE ... OF} + * type, this will be that type; the invalid {@code RegType} + * otherwise. + *

+ * Even though the tuple structure will match, this is not the same type + * returned by {@link #type() type()}; that will still be a type distinctly + * associated with this relation. + */ + RegType ofType(); + // am + // filenode + // tablespace + + /* Of limited interest ... estimates used by planner + * + int pages(); + float tuples(); + int allVisible(); + */ + + RegClass toastRelation(); + boolean hasIndex(); + + /** + * Whether this relation is shared across all databases in the cluster. + *

+ * Contrast {@link shared()}, which indicates, for any catalog object, + * whether that object is shared across the cluster. For any + * {@code RegClass} instance, {@code shared()} will be false (the + * {@code pg_class} catalog is not shared), but if the instance represents + * a shared class, {@code isShared()} will be true (and {@code shared()} + * will be true for any catalog object formed with that instance as its + * {@code classId}). + * @return whether the relation represented by this RegClass instance is + * shared across all databases in the cluster. + */ + boolean isShared(); + // persistence + // kind + short nAttributes(); + short checks(); + boolean hasRules(); + boolean hasTriggers(); + boolean hasSubclass(); + boolean rowSecurity(); + boolean forceRowSecurity(); + boolean isPopulated(); + // replident + boolean isPartition(); + // rewrite + // frozenxid + // minmxid + /** + * This is a list of {@code keyword=value} pairs and ought to have + * a more specific return type. + *

+ * XXX + */ + List options(); + // partbound + + TupleDescriptor.Interned tupleDescriptor(); +} diff --git a/pljava-api/src/main/java/org/postgresql/pljava/model/RegCollation.java b/pljava-api/src/main/java/org/postgresql/pljava/model/RegCollation.java new file mode 100644 index 000000000..136890a62 --- /dev/null +++ b/pljava-api/src/main/java/org/postgresql/pljava/model/RegCollation.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.model; + +import org.postgresql.pljava.model.CatalogObject.*; + +import static org.postgresql.pljava.model.CatalogObject.Factory.*; + +import org.postgresql.pljava.sqlgen.Lexicals.Identifier.Simple; + +/** + * Model of a registered PostgreSQL collation, consisting of a provider and + * version, {@code collate} and {@code ctype} strings meaningful to that + * provider, and a {@code CharsetEncoding} (or {@code ANY} if the collation + * is usable with any encoding). + */ +public interface RegCollation +extends Addressed, Namespaced, Owned +{ + RegClass.Known CLASSID = + formClassId(CollationRelationId, RegCollation.class); + + RegCollation DEFAULT = formObjectId(CLASSID, DEFAULT_COLLATION_OID); + RegCollation C = formObjectId(CLASSID, C_COLLATION_OID); + RegCollation POSIX = formObjectId(CLASSID, POSIX_COLLATION_OID); + + /* + * Static lc_messages/lc_monetary/lc_numeric/lc_time getters? They are not + * components of RegCollation, but simply GUCs. They don't have PGDLLIMPORT, + * so on Windows they'd have to be retrieved through the GUC machinery + * by name. At least they're strings anyway. + */ + + enum Provider { DEFAULT, LIBC, ICU } + + CharsetEncoding encoding(); + String collate(); + String ctype(); + + /** + * @since PG 10 + */ + Provider provider(); + + /** + * @since PG 10 + */ + String version(); + + /** + * @since PG 12 + */ + boolean deterministic(); +} diff --git a/pljava-api/src/main/java/org/postgresql/pljava/model/RegConfig.java b/pljava-api/src/main/java/org/postgresql/pljava/model/RegConfig.java new file mode 100644 index 000000000..ee923ff89 --- /dev/null +++ b/pljava-api/src/main/java/org/postgresql/pljava/model/RegConfig.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.model; + +import org.postgresql.pljava.model.CatalogObject.*; + +import static org.postgresql.pljava.model.CatalogObject.Factory.*; + +import org.postgresql.pljava.sqlgen.Lexicals.Identifier.Simple; + +/** + * A PostgreSQL text search configuration. + *

+ * This interface is included in the model per the (arguably arbitrary) goal of + * covering all the catalog classes for which a {@code Reg...} type is provided + * in PostgreSQL. However, completing its implementation (to include a + * {@code parser()} method) would require also defining an interface to + * represent a text search parser. + */ +public interface RegConfig +extends Addressed, Namespaced, Owned +{ + RegClass.Known CLASSID = + formClassId(TSConfigRelationId, RegConfig.class); +} diff --git a/pljava-api/src/main/java/org/postgresql/pljava/model/RegDictionary.java b/pljava-api/src/main/java/org/postgresql/pljava/model/RegDictionary.java new file mode 100644 index 000000000..3e70d8a98 --- /dev/null +++ b/pljava-api/src/main/java/org/postgresql/pljava/model/RegDictionary.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.model; + +import org.postgresql.pljava.model.CatalogObject.*; + +import static org.postgresql.pljava.model.CatalogObject.Factory.*; + +import org.postgresql.pljava.sqlgen.Lexicals.Identifier.Simple; + +/** + * A PostgreSQL text search dictionary. + *

+ * This interface is included in the model per the (arguably arbitrary) goal of + * covering all the catalog classes for which a {@code Reg...} type is provided + * in PostgreSQL. However, completing its implementation (to include a + * {@code template()} method) would require also defining an interface to + * represent a text search template. + */ +public interface RegDictionary +extends Addressed, Namespaced, Owned +{ + RegClass.Known CLASSID = + formClassId(TSDictionaryRelationId, RegDictionary.class); + + /* + * dictinitoption is a text column, but it clearly (see CREATE TEXT SEARCH + * DICTIONARY and examples in the catalog) has an option = value , ... + * structure. An appropriate return type for a method could be a map, + * and the implementation would have to match the quoting/escaping/parsing + * rules used by PG. + */ +} diff --git a/pljava-api/src/main/java/org/postgresql/pljava/model/RegNamespace.java b/pljava-api/src/main/java/org/postgresql/pljava/model/RegNamespace.java new file mode 100644 index 000000000..8dc28cc40 --- /dev/null +++ b/pljava-api/src/main/java/org/postgresql/pljava/model/RegNamespace.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.model; + +import org.postgresql.pljava.model.CatalogObject.*; + +import static org.postgresql.pljava.model.CatalogObject.Factory.*; + +import org.postgresql.pljava.sqlgen.Lexicals.Identifier.Simple; + +/** + * Model of a namespace (named schema) entry in the PostgreSQL catalogs. + */ +public interface RegNamespace +extends + Addressed, Named, Owned, + AccessControlled +{ + RegClass.Known CLASSID = + formClassId(NamespaceRelationId, RegNamespace.class); + + RegNamespace PG_CATALOG = formObjectId(CLASSID, PG_CATALOG_NAMESPACE); + RegNamespace PG_TOAST = formObjectId(CLASSID, PG_TOAST_NAMESPACE); +} diff --git a/pljava-api/src/main/java/org/postgresql/pljava/model/RegOperator.java b/pljava-api/src/main/java/org/postgresql/pljava/model/RegOperator.java new file mode 100644 index 000000000..ee6f66b6c --- /dev/null +++ b/pljava-api/src/main/java/org/postgresql/pljava/model/RegOperator.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.model; + +import org.postgresql.pljava.model.CatalogObject.*; + +import static org.postgresql.pljava.model.CatalogObject.Factory.*; + +import org.postgresql.pljava.model.RegProcedure.Memo; + +import org.postgresql.pljava.sqlgen.Lexicals.Identifier.Operator; + +/** + * Model of a PostgreSQL operator as defined in the system catalogs, including + * its kind (infix or prefix), operand and result types, and a number of + * properties helpful in query planning. + */ +public interface RegOperator +extends Addressed, Namespaced, Owned +{ + RegClass.Known CLASSID = + formClassId(OperatorRelationId, RegOperator.class); + + enum Kind + { + /** + * An operator used between a left and a right operand. + */ + INFIX, + + /** + * An operator used to the left of a single right operand. + */ + PREFIX, + + /** + * An operator used to the right of a single left operand. + * @deprecated Postfix operators are deprecated since PG 13 and + * unsupported since PG 14. + */ + @Deprecated(since="PG 13") + POSTFIX + } + + interface Evaluator extends Memo { } + interface RestrictionSelectivity extends Memo { } + interface JoinSelectivity extends Memo { } + + Kind kind(); + boolean canMerge(); + boolean canHash(); + RegType leftOperand(); + RegType rightOperand(); + RegType result(); + RegOperator commutator(); + RegOperator negator(); + RegProcedure evaluator(); + RegProcedure restrictionEstimator(); + RegProcedure joinEstimator(); +} diff --git a/pljava-api/src/main/java/org/postgresql/pljava/model/RegProcedure.java b/pljava-api/src/main/java/org/postgresql/pljava/model/RegProcedure.java new file mode 100644 index 000000000..ed9048151 --- /dev/null +++ b/pljava-api/src/main/java/org/postgresql/pljava/model/RegProcedure.java @@ -0,0 +1,160 @@ +/* + * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.model; + +import java.sql.SQLXML; + +import java.util.List; + +import org.postgresql.pljava.model.CatalogObject.*; + +import static org.postgresql.pljava.model.CatalogObject.Factory.*; + +import org.postgresql.pljava.sqlgen.Lexicals.Identifier.Simple; + +import org.postgresql.pljava.annotation.Function.Effects; +import org.postgresql.pljava.annotation.Function.OnNullInput; +import org.postgresql.pljava.annotation.Function.Parallel; +import org.postgresql.pljava.annotation.Function.Security; + +/** + * Model of a PostgreSQL "routine" (which in late versions can include + * procedures and functions of various kinds) as defined in the system catalogs, + * including its parameter and result types and many other properties. + * @param distinguishes {@code RegProcedure} instances used for different + * known purposes, by specifying the type of a 'memo' that could be attached to + * the instance, perhaps with extra information helpful for the intended use. + * At present, such memo interfaces are all empty, but still this parameter can + * serve a compile-time role to discourage mixing different procedures up. + */ +public interface RegProcedure> +extends + Addressed>, Namespaced, Owned, + AccessControlled +{ + RegClass.Known> CLASSID = + formClassId(ProcedureRelationId, (Class>)null); + + ProceduralLanguage language(); + + float cost(); + + float rows(); + + RegType variadicType(); + + /** + * A planner-support function that may transform call sites of + * this function. + *

+ * In PG 9.5 to 11, there was a similar, but less flexible, "transform" + * function that this method can return when running on those versions. + * @since PG 12 + */ + RegProcedure support(); + + /** + * The kind of procedure or function. + *

+ * Before PG 11, there were separate booleans to indicate an aggregate or + * window function, which this method can consult when running on earlier + * versions. + * @since PG 11 + */ + Kind kind(); + + Security security(); + + boolean leakproof(); + + OnNullInput onNullInput(); + + boolean returnsSet(); + + Effects effects(); + + Parallel parallel(); + + RegType returnType(); + + List argTypes(); + + List allArgTypes(); + + /** + * Modes corresponding 1-for-1 to the arguments in {@code allArgTypes}. + */ + List argModes(); + + /** + * Names corresponding 1-for-1 to the arguments in {@code allArgTypes}. + */ + List argNames(); + + /** + * A {@code pg_node_tree} representation of a list of n + * expression trees, corresponding to the last n input arguments + * (that is, the last n returned by {@code argTypes}). + */ + SQLXML argDefaults(); + + List transformTypes(); + + String src(); + + String bin(); + + /** + * A {@code pg_node_tree} representation of a pre-parsed SQL function body, + * used when it is given in SQL-standard notation rather than as a string + * literal, otherwise null. + * @since PG 14 + */ + SQLXML sqlBody(); + + /** + * This is surely a list of {@code guc=value} pairs and ought to have + * a more specific return type. + *

+ * XXX + */ + List config(); + + enum ArgMode { IN, OUT, INOUT, VARIADIC, TABLE }; + + enum Kind { FUNCTION, PROCEDURE, AGGREGATE, WINDOW }; + + /** + * Obtain memo attached to this {@code RegProcedure}, if any. + *

+ * A {@code RegProcedure} may have an implementation of {@link Memo Memo} + * attached, providing additional information on what sort of procedure + * it is and how to use it. Many catalog getters that return + * {@code RegProcedure} specialize the return type to indicate + * an expected subinterface of {@code Memo}. + */ + M memo(); + + interface Memo> + { + RegProcedure apply(RegProcedure bare); + } + + interface PlannerSupport extends Memo { } + + interface PLJava extends Memo + { + // MethodHandleInfo methodInfo() ? \ + // MethodHandle method() ? } need a RegNamespace parameter? + // AccessControlContext acc() ? / + } +} diff --git a/pljava-api/src/main/java/org/postgresql/pljava/model/RegRole.java b/pljava-api/src/main/java/org/postgresql/pljava/model/RegRole.java new file mode 100644 index 000000000..acb8e55c3 --- /dev/null +++ b/pljava-api/src/main/java/org/postgresql/pljava/model/RegRole.java @@ -0,0 +1,150 @@ +/* + * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.model; + +import java.util.List; + +import org.postgresql.pljava.RolePrincipal; + +import org.postgresql.pljava.model.CatalogObject.*; + +import static org.postgresql.pljava.model.CatalogObject.Factory.*; + +import org.postgresql.pljava.sqlgen.Lexicals.Identifier.Simple; + +import org.postgresql.pljava.sqlgen.Lexicals.Identifier.Pseudo; + +/** + * Model of a PostgreSQL role. + *

+ * In addition to the methods returning the information in the {@code pg_authid} + * system catalog, there are methods to return four different flavors of + * {@link RolePrincipal RolePrincipal}, all representing this role. + *

+ * The {@code ...Principal()} methods should not be confused with environment + * accessors returning actual information about the execution context. Each of + * the methods simply returns an instance of the corresponding class that would + * be appropriate to find in the execution context if this role were, + * respectively, the authenticated, session, outer, or current role. + *

+ * {@link RolePrincipal.Current} implements the + * {@code UserPrincipal/GroupPrincipal} interfaces of + * {@code java.nio.file.attribute}, so + * {@link #currentPrincipal() currentPrincipal()} can also be used to obtain + * {@code Principal}s that will work in the Java NIO.2 filesystem API. + *

+ * The {@code ...Principal} methods only succeed when {@code name()} does, + * therefore not when {@code isValid} is false. The {@code RegRole.Grantee} + * representing {@code PUBLIC} is, for all other purposes, not a valid role, + * including for its {@code ...Principal} methods. + */ +public interface RegRole +extends Addressed, Named, AccessControlled +{ + RegClass.Known CLASSID = + formClassId(AuthIdRelationId, RegRole.class); + + /** + * A {@code RegRole.Grantee} representing {@code PUBLIC}; not a valid + * {@code RegRole} for other purposes. + */ + RegRole.Grantee PUBLIC = publicGrantee(); + + /** + * Subinterface of {@code RegRole} returned by methods of + * {@link CatalogObject.AccessControlled CatalogObject.AccessControlled} + * identifying the role to which a privilege has been granted. + *

+ * A {@code RegRole} appearing as a grantee can be {@link #PUBLIC PUBLIC}, + * unlike a {@code RegRole} in any other context, so the + * {@link #isPublic isPublic()} method appears only on this subinterface, + * as well as the {@link #nameAsGrantee nameAsGrantee} method, which will + * return the correct name even in that case (the ordinary {@code name} + * method will not). + */ + interface Grantee extends RegRole + { + /** + * In the case of a {@code RegRole} obtained as the {@code grantee} of a + * {@link Grant}, indicate whether it is a grant to "public". + */ + default boolean isPublic() + { + return ! isValid(); + } + + /** + * Like {@code name()}, but also returns the expected name for a + * {@code Grantee} representing {@code PUBLIC}. + */ + Simple nameAsGrantee(); + } + + /** + * Return a {@code RolePrincipal} that would represent this role as a + * session's authenticated identity (which was established at connection + * time and will not change for the life of a session). + */ + default RolePrincipal.Authenticated authenticatedPrincipal() + { + return new RolePrincipal.Authenticated(name()); + } + + /** + * Return a {@code RolePrincipal} that would represent this role as a + * session's "session" identity (which can be changed during a session + * by {@code SET SESSION AUTHORIZATION}). + */ + default RolePrincipal.Session sessionPrincipal() + { + return new RolePrincipal.Session(name()); + } + + /** + * Return a {@code RolePrincipal} that would represent this role as the one + * last established by {@code SET ROLE}, and outside of any + * {@code SECURITY DEFINER} function. + */ + default RolePrincipal.Outer outerPrincipal() + { + return new RolePrincipal.Outer(name()); + } + + /** + * Return a {@code RolePrincipal} that would represent this role as the + * effective one for normal privilege checks, usually the same as the + * session or outer, but changed during {@code SECURITY DEFINER} functions. + *

+ * This method can also be used to obtain a {@code Principal} that will work + * in the Java NIO.2 filesystem API. + */ + default RolePrincipal.Current currentPrincipal() + { + return new RolePrincipal.Current(name()); + } + + /** + * Roles of which this role is directly a member. + *

+ * For the other direction, see {@link #grants() grants()}. + */ + List memberOf(); + + boolean superuser(); + boolean inherit(); + boolean createRole(); + boolean createDB(); + boolean canLogIn(); + boolean replication(); + boolean bypassRLS(); + int connectionLimit(); +} diff --git a/pljava-api/src/main/java/org/postgresql/pljava/model/RegType.java b/pljava-api/src/main/java/org/postgresql/pljava/model/RegType.java new file mode 100644 index 000000000..b67dfc721 --- /dev/null +++ b/pljava-api/src/main/java/org/postgresql/pljava/model/RegType.java @@ -0,0 +1,246 @@ +/* + * Copyright (c) 2022-2023 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.model; + +import java.sql.SQLType; +import java.sql.SQLXML; + +import org.postgresql.pljava.model.CatalogObject.*; + +import static org.postgresql.pljava.model.CatalogObject.Factory.*; + +import org.postgresql.pljava.model.RegProcedure.Memo; + +import org.postgresql.pljava.annotation.BaseUDT.Alignment; +import org.postgresql.pljava.annotation.BaseUDT.PredefinedCategory; // javadoc +import org.postgresql.pljava.annotation.BaseUDT.Storage; + +import org.postgresql.pljava.sqlgen.Lexicals.Identifier; // javadoc +import org.postgresql.pljava.sqlgen.Lexicals.Identifier.Simple; + +/** + * Model of a PostgreSQL data type, as defined in the system catalogs. + *

+ * This class also has static final fields for a selection of commonly used + * {@code RegType}s, such as those that correspond to types mentioned in JDBC, + * and others that are just ubiquitous when working in PostgreSQL in general, + * or are used in this model package. + *

+ * An instance of {@code RegType} also implements the JDBC + * {@link SQLType SQLType} interface, with the intention that it could be used + * with a suitably-aware JDBC implementation to identify any type available + * in PostgreSQL. + *

+ * A type can have a 'modifier' (think {@code NUMERIC(4)} versus plain + * {@code NUMERIC}). In PostgreSQL's C code, a type oid and modifier have to + * be passed around in tandem. Here, you apply + * {@link #modifier(int) modifier(int)} to the unmodified {@code RegType} and + * obtain a distinct {@code RegType} instance incorporating the modifier. + */ +public interface RegType +extends + Addressed, Namespaced, Owned, AccessControlled, + SQLType +{ + RegClass.Known CLASSID = + formClassId(TypeRelationId, RegType.class); + + /* + * PG types good to have around because of corresponding JDBC types. + */ + RegType BOOL = formObjectId(CLASSID, BOOLOID); + RegType BYTEA = formObjectId(CLASSID, BYTEAOID); + /** + * The PostgreSQL type {@code "char"} (the quotes are needed to distinguish + * it from the different SQL type named {@code CHAR}), which is an eight-bit + * signed value with no associated character encoding (though it is often + * used in the catalogs with ASCII-letter values as an ersatz enum). + *

+ * It can be mapped to the JDBC type {@code TINYINT}, or Java {@code byte}. + */ + RegType CHAR = formObjectId(CLASSID, CHAROID); + RegType INT8 = formObjectId(CLASSID, INT8OID); + RegType INT2 = formObjectId(CLASSID, INT2OID); + RegType INT4 = formObjectId(CLASSID, INT4OID); + RegType XML = formObjectId(CLASSID, XMLOID); + RegType FLOAT4 = formObjectId(CLASSID, FLOAT4OID); + RegType FLOAT8 = formObjectId(CLASSID, FLOAT8OID); + /** + * "Blank-padded CHAR", the PostgreSQL type that corresponds to the SQL + * standard {@code CHAR} (spelled without quotes) type. + */ + RegType BPCHAR = formObjectId(CLASSID, BPCHAROID); + RegType VARCHAR = formObjectId(CLASSID, VARCHAROID); + RegType DATE = formObjectId(CLASSID, DATEOID); + RegType TIME = formObjectId(CLASSID, TIMEOID); + RegType TIMESTAMP = formObjectId(CLASSID, TIMESTAMPOID); + RegType TIMESTAMPTZ = formObjectId(CLASSID, TIMESTAMPTZOID); + RegType TIMETZ = formObjectId(CLASSID, TIMETZOID); + RegType BIT = formObjectId(CLASSID, BITOID); + RegType VARBIT = formObjectId(CLASSID, VARBITOID); + RegType NUMERIC = formObjectId(CLASSID, NUMERICOID); + + /* + * PG types not mentioned in JDBC but bread-and-butter to PG devs. + */ + RegType TEXT = formObjectId(CLASSID, TEXTOID); + RegType UNKNOWN = formObjectId(CLASSID, UNKNOWNOID); + RegType RECORD = formObjectId(CLASSID, RECORDOID); + RegType CSTRING = formObjectId(CLASSID, CSTRINGOID); + RegType VOID = formObjectId(CLASSID, VOIDOID); + + /* + * PG types used in modeling PG types themselves. + */ + RegType NAME = formObjectId(CLASSID, NAMEOID); + RegType REGPROC = formObjectId(CLASSID, REGPROCOID); + RegType OID = formObjectId(CLASSID, OIDOID); + RegType PG_NODE_TREE = formObjectId(CLASSID, PG_NODE_TREEOID); + RegType ACLITEM = formObjectId(CLASSID, ACLITEMOID); + RegType REGPROCEDURE = formObjectId(CLASSID, REGPROCEDUREOID); + RegType REGOPER = formObjectId(CLASSID, REGOPEROID); + RegType REGOPERATOR = formObjectId(CLASSID, REGOPERATOROID); + RegType REGCLASS = formObjectId(CLASSID, REGCLASSOID); + RegType REGTYPE = formObjectId(CLASSID, REGTYPEOID); + RegType REGCONFIG = formObjectId(CLASSID, REGCONFIGOID); + RegType REGDICTIONARY = formObjectId(CLASSID, REGDICTIONARYOID); + RegType REGNAMESPACE = formObjectId(CLASSID, REGNAMESPACEOID); + RegType REGROLE = formObjectId(CLASSID, REGROLEOID); + RegType REGCOLLATION = formObjectId(CLASSID, REGCOLLATIONOID, + v -> v >= 130000 ); + + enum Type { BASE, COMPOSITE, DOMAIN, ENUM, PSEUDO, RANGE, MULTIRANGE } + + interface TypeInput extends Memo { } + interface TypeOutput extends Memo { } + interface TypeReceive extends Memo { } + interface TypeSend extends Memo { } + interface TypeModifierInput extends Memo { } + interface TypeModifierOutput extends Memo { } + interface TypeAnalyze extends Memo { } + interface TypeSubscript extends Memo { } + + short length(); + boolean byValue(); + Type type(); + /** + * A one-character code representing the type's 'category'. + *

+ * Custom categories are possible, so not every value here need correspond + * to a {@link PredefinedCategory PredefinedCategory}, but common ones will, + * and can be 'decoded' with {@link PredefinedCategory#valueOf(char)}. + */ + char category(); + boolean preferred(); + boolean defined(); + byte delimiter(); + RegClass relation(); + RegType element(); + RegType array(); + RegProcedure input(); + RegProcedure output(); + RegProcedure receive(); + RegProcedure send(); + RegProcedure modifierInput(); + RegProcedure modifierOutput(); + RegProcedure analyze(); + RegProcedure subscript(); + Alignment alignment(); + Storage storage(); + boolean notNull(); + RegType baseType(); + int dimensions(); + RegCollation collation(); + SQLXML defaultBin(); + String defaultText(); + RegType modifier(int typmod); + + /** + * Returns the {@code RegType} for this type with no modifier, if this + * instance has one. + *

+ * If not, simply returns {@code this}. + */ + RegType withoutModifier(); + + /** + * Returns the modifier if this instance has one, else -1. + */ + int modifier(); + + /** + * The corresponding {@link TupleDescriptor TupleDescriptor}, non-null only + * for composite types. + */ + TupleDescriptor.Interned tupleDescriptor(); + + /** + * The name of this type as a {@code String}, as the JDBC + * {@link SQLType SQLType} interface requires. + *

+ * The string produced here is as would be produced by + * {@link Identifier#deparse deparse(StandardCharsets.UTF_8)} applied to + * the result of {@link #qualifiedName qualifiedName()}. + * The returned string may include double-quote marks, which affect its case + * sensitivity and the characters permitted within it. If an application is + * not required to use this method for JDBC compatibility, it can avoid + * needing to fuss with those details by using {@code qualifiedName} + * instead. + */ + @Override + default String getName() + { + return qualifiedName().toString(); + } + + /** + * A string identifying the "vendor" for which the type name and number here + * are meaningful, as the JDBC {@link SQLType SQLType} interface requires. + *

+ * The JDBC API provides that the result "typically is the package name for + * this vendor", and this method returns {@code org.postgresql} as + * a constant string. + *

+ * Note, however, that every type that is defined in the current PostgreSQL + * database can be represented by an instance of this interface, whether + * built in to PostgreSQL, installed with an extension, or user-defined. + * Therefore, not every instance with this "vendor" string can be assumed + * to be a type known to all PostgreSQL databases. Moreover, even if + * the same extension-provided or user-defined type is present in different + * PostgreSQL databases, it need not be installed with the same + * {@link #qualifiedName qualifiedName} in each, and will almost certainly + * have different object IDs, so {@link #getName getName} and + * {@link #getVendorTypeNumber getVendorTypeNumber} may not in general + * identify the same type across unrelated PostgreSQL databases. + */ + @Override + default String getVendor() + { + return "org.postgresql"; + } + + /** + * A vendor-specific type number identifying this type, as the JDBC + * {@link SQLType SQLType} interface requires. + *

+ * This implementation returns the {@link #oid oid} of the type in + * the current database. However, except for the subset of types that are + * built in to PostgreSQL with oid values that are fixed, the result of this + * method should only be relied on to identify a type within the current + * database. + */ + @Override + default Integer getVendorTypeNumber() + { + return oid(); + } +} diff --git a/pljava-api/src/main/java/org/postgresql/pljava/model/ResourceOwner.java b/pljava-api/src/main/java/org/postgresql/pljava/model/ResourceOwner.java new file mode 100644 index 000000000..919a05e00 --- /dev/null +++ b/pljava-api/src/main/java/org/postgresql/pljava/model/ResourceOwner.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.model; + +import org.postgresql.pljava.Lifespan; + +import org.postgresql.pljava.model.CatalogObject.Factory; + +import static org.postgresql.pljava.model.CatalogObject.Factory.*; + +/** + * The representation of a PostgreSQL {@code ResourceOwner}, usable as + * a PL/Java {@link Lifespan Lifespan}. + *

+ * The {@code ResourceOwner} API in PostgreSQL is described here. + *

+ * PostgreSQL invokes callbacks in phases when a {@code ResourceOwner} + * is released, and all of its built-in consumers get notified before + * loadable modules (like PL/Java) for each phase in turn. The release + * behavior of this PL/Java instance is tied to the + * {@code RESOURCE_RELEASE_LOCKS} phase of the underlying PostgreSQL object, + * and therefore occurs after all of the built-in PostgreSQL lock-related + * releases, but before any of the built-in stuff released in the + * {@code RESOURCE_RELEASE_AFTER_LOCKS} phase. + */ +public interface ResourceOwner extends Lifespan +{ + static ResourceOwner CurrentResourceOwner() + { + return INSTANCE.resourceOwner(RSO_Current); + } + + static ResourceOwner CurTransactionResourceOwner() + { + return INSTANCE.resourceOwner(RSO_CurTransaction); + } + + static ResourceOwner TopTransactionResourceOwner() + { + return INSTANCE.resourceOwner(RSO_TopTransaction); + } +} diff --git a/pljava-api/src/main/java/org/postgresql/pljava/model/SlotTester.java b/pljava-api/src/main/java/org/postgresql/pljava/model/SlotTester.java new file mode 100644 index 000000000..3e30f18a2 --- /dev/null +++ b/pljava-api/src/main/java/org/postgresql/pljava/model/SlotTester.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2022-2023 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.model; + +import java.sql.ResultSet; +import java.sql.SQLException; + +import java.util.List; + +import org.postgresql.pljava.Adapter; + +/** + * A temporary test jig during TupleTableSlot development, not intended to last. + */ +public interface SlotTester +{ + /** + * Unwrap a {@link ResultSet} instance from the legacy JDBC layer as a + * {@link Portal} instance so results can be retrieved using new API. + * @param rs a ResultSet, which can only be an SPIResultSet obtained from + * the legacy JDBC implementation, not yet closed or used to fetch anything, + * and will be closed. + */ + Portal unwrapAsPortal(ResultSet rs) throws SQLException; + + /** + * Execute query, returning its complete result as a {@code List} + * of {@link TupleTableSlot}. + */ + List test(String query); + + /** + * Return one of the predefined {@link Adapter} instances, given knowledge + * of the class name and static final field name within that class inside + * PL/Java's implementation module. + *

+ * Example: + *

+	 * adapterPlease(
+	 *  "org.postgresql.pljava.pg.adt.Primitives", "FLOAT8_INSTANCE");
+	 *
+ */ + Adapter adapterPlease(String clazz, String field) + throws ReflectiveOperationException; + + /** + * A temporary marker interface used on classes or interfaces whose + * static final fields should be visible to {@code adapterPlease}. + */ + interface Visible + { + } +} diff --git a/pljava-api/src/main/java/org/postgresql/pljava/model/TupleDescriptor.java b/pljava-api/src/main/java/org/postgresql/pljava/model/TupleDescriptor.java new file mode 100644 index 000000000..f3cff5a08 --- /dev/null +++ b/pljava-api/src/main/java/org/postgresql/pljava/model/TupleDescriptor.java @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2022-2023 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.model; + +import java.sql.SQLException; +import java.sql.SQLSyntaxErrorException; // javadoc + +import java.util.List; + +import org.postgresql.pljava.TargetList.Projection; +import org.postgresql.pljava.sqlgen.Lexicals.Identifier.Simple; + +/** + * Conceptually, a {@code TupleDescriptor} is a list of {@code Attribute}, with + * a {@code RegType} that identifies its corresponding row type. + *

+ * The row type might be just {@code RECORD}, though, representing a + * transient, unregistered type. + *

+ * The {@code Attribute} instances may then correspond to nothing that exists in + * {@code pg_attribute}, in which case they will be 'virtual' instances whose + * {@code CatalogObject.Addressed} methods don't work, but which simply hold a + * reference to the {@code TupleDescriptor} they came from instead. + *

+ * A {@code TupleDescriptor} may also contain attribute defaults and/or + * constraints. These would be less often of interest in Java; if there is + * a need to make them available, rather than complicating + * {@code TupleDescriptor}, it will probably be more natural to make them + * available by methods on {@code Attribute}. + */ +public interface TupleDescriptor extends Projection +{ + /** + * @deprecated As a subinterface of {@link Projection Projection}, + * a {@code TupleDescriptor} already is a {@code List}, and there + * is no need for this method to simply return its own receiver. + */ + @Deprecated(forRemoval=true) + default List attributes() + { + return this; + } + + /** + * If this tuple descriptor is not ephemeral, returns the PostgreSQL type + * that identifies it. + *

+ * If the descriptor is for a known composite type in the PostgreSQL + * catalog, this method returns that type. + *

+ * If the descriptor has been created programmatically and interned, this + * method returns the type + * {@link RegType#RECORD RECORD}.{@link RegType#modifier(int) modifier(n)} + * where n was uniquely assigned by PostgreSQL when the + * descriptor was interned, and will reliably refer to this tuple descriptor + * for the duration of the session. + *

+ * For any ephemeral descriptor passed around in code without being + * interned, this method returns plain {@link RegType#RECORD RECORD}, which + * is useless for identifying the tuple structure. + */ + RegType rowType(); + + /** + * Gets an attribute by name. + *

+ * This API should be considered scaffolding or preliminary, until an API + * can be designed that might offer a convenient usage idiom without + * presupposing something like a name-to-attribute map in every decriptor. + *

+ * This default implementation simply does {@code project(name).get(0)}. + * Code that will do so repeatedly might be improved by doing so once and + * retaining the result. + * @throws SQLSyntaxErrorException 42703 if no attribute name matches + * @deprecated A one-by-one lookup-by-name API forces the implementation to + * cater to an inefficient usage pattern, when callers will often have a + * number of named attributes to look up, which can be done more efficiently + * in one go; see the methods of {@link Projection Projection}. + */ + @Deprecated(forRemoval=true) + default Attribute get(Simple name) throws SQLException + { + return project(name).get(0); + } + + /** + * Equivalent to {@code get(Simple.fromJava(name))}. + *

+ * This API should be considered scaffolding or preliminary, until an API + * can be designed that might offer a convenient usage idiom without + * presupposing something like a name-to-attribute map in every descriptor. + * @throws SQLSyntaxErrorException 42703 if no attribute name matches + * @deprecated A one-by-one lookup-by-name API forces the implementation to + * cater to an inefficient usage pattern, when callers will often have a + * number of named attributes to look up, which can be done more efficiently + * in one go; see the methods of {@link Projection Projection}. + */ + @Deprecated(forRemoval=true) + default Attribute get(String name) throws SQLException + { + return get(Simple.fromJava(name)); + } + + /** + * Return this descriptor unchanged if it is already interned in + * PostgreSQL's type cache, otherwise an equivalent new descriptor with + * a different {@link #rowType rowType} uniquely assigned to identify it + * for the duration of the session. + *

+ * PostgreSQL calls this operation "BlessTupleDesc", which updates the + * descriptor in place; in PL/Java code, the descriptor returned by this + * method should be used in place of the original. + */ + Interned intern(); + + /** + * A descriptor that either describes a known composite type in the + * catalogs, or has been interned in PostgreSQL's type cache, and has + * a distinct {@link #rowType rowType} that can be used to identify it + * for the duration of the session. + *

+ * Some operations, such as constructing a composite value for a function + * to return, require this. + */ + interface Interned extends TupleDescriptor + { + @Override + default Interned intern() + { + return this; + } + } + + /** + * A descriptor that has been constructed on the fly and has not been + * interned. + *

+ * For all such descriptors, {@link #rowType rowType} returns + * {@link RegType#RECORD RECORD}, which is of no use for identification. + * For some purposes (such as constructing a composite value for a function + * to return), an ephemeral descriptor must be interned before it can + * be used. + */ + interface Ephemeral extends TupleDescriptor + { + } +} diff --git a/pljava-api/src/main/java/org/postgresql/pljava/model/TupleTableSlot.java b/pljava-api/src/main/java/org/postgresql/pljava/model/TupleTableSlot.java new file mode 100644 index 000000000..ba492f308 --- /dev/null +++ b/pljava-api/src/main/java/org/postgresql/pljava/model/TupleTableSlot.java @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2022-2023 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.model; + +import java.sql.SQLException; + +import org.postgresql.pljava.Adapter.As; +import org.postgresql.pljava.Adapter.AsLong; +import org.postgresql.pljava.Adapter.AsDouble; +import org.postgresql.pljava.Adapter.AsInt; +import org.postgresql.pljava.Adapter.AsFloat; +import org.postgresql.pljava.Adapter.AsShort; +import org.postgresql.pljava.Adapter.AsChar; +import org.postgresql.pljava.Adapter.AsByte; +import org.postgresql.pljava.Adapter.AsBoolean; + +/** + * A PostgreSQL abstraction that can present a variety of underlying tuple + * representations in a common way. + *

+ * PL/Java may take the liberty of extending this class to present even some + * other tuple-like things that are not native tuple forms to PostgreSQL. + *

+ * A readable instance that relies on PostgreSQL's "deforming" can be + * constructed over any supported flavor of underlying tuple. Retrieving + * its values can involve JNI calls to the support functions in PostgreSQL. + * Its writable counterpart is also what must be used for constructing a tuple + * on the fly; after its values/nulls have been set (pure Java), it can be + * flattened (at the cost of a JNI call) to return a pass-by-reference + * {@code Datum} usable as a composite function argument or return value. + *

+ * A specialized instance, with support only for reading, can be constructed + * over a PostgreSQL tuple in its widely-used 'heap' form. PL/Java knows that + * form well enough to walk it and retrieve values mostly without JNI calls. + *

+ * A {@code TupleTableSlot} is not safe for concurrent use by multiple threads, + * in the absence of appropriate synchronization. + */ +public interface TupleTableSlot +{ + TupleDescriptor descriptor(); + RegClass relation(); + + /* + * Idea: move these methods out of public API, as they aren't very + * efficient. Make them invocable internally via TargetList. As an interim + * measure, remove their "throws SQLException" clauses; the implementation + * hasn't been throwing those anyway, but wrapping them in a runtime + * version. (Which needs to get unwrapped eventually, somewhere suitable.) + */ + T get(Attribute att, As adapter); + long get(Attribute att, AsLong adapter); + double get(Attribute att, AsDouble adapter); + int get(Attribute att, AsInt adapter); + float get(Attribute att, AsFloat adapter); + short get(Attribute att, AsShort adapter); + char get(Attribute att, AsChar adapter); + byte get(Attribute att, AsByte adapter); + boolean get(Attribute att, AsBoolean adapter); + + T get(int idx, As adapter); + long get(int idx, AsLong adapter); + double get(int idx, AsDouble adapter); + int get(int idx, AsInt adapter); + float get(int idx, AsFloat adapter); + short get(int idx, AsShort adapter); + char get(int idx, AsChar adapter); + byte get(int idx, AsByte adapter); + boolean get(int idx, AsBoolean adapter); + + default T sqlGet(int idx, As adapter) + { + return get(idx - 1, adapter); + } + + default long sqlGet(int idx, AsLong adapter) + { + return get(idx - 1, adapter); + } + + default double sqlGet(int idx, AsDouble adapter) + { + return get(idx - 1, adapter); + } + + default int sqlGet(int idx, AsInt adapter) + { + return get(idx - 1, adapter); + } + + default float sqlGet(int idx, AsFloat adapter) + { + return get(idx - 1, adapter); + } + + default short sqlGet(int idx, AsShort adapter) + { + return get(idx - 1, adapter); + } + + default char sqlGet(int idx, AsChar adapter) + { + return get(idx - 1, adapter); + } + + default byte sqlGet(int idx, AsByte adapter) + { + return get(idx - 1, adapter); + } + + default boolean sqlGet(int idx, AsBoolean adapter) + { + return get(idx - 1, adapter); + } + + /** + * A form of {@code TupleTableSlot} consisting of a number of indexable + * elements all of the same type, described by the single {@code Attribute} + * of a one-element {@code TupleDescriptor}. + *

+ * This is one form in which a PostgreSQL array can be accessed. + *

+ * The {@code get} methods that take an {@code Attribute} are not especially + * useful with this type of slot, and will simply return its first element. + */ + interface Indexed extends TupleTableSlot + { + /** + * Count of the slot's elements (one greater than the maximum index + * that may be passed to {@code get}). + */ + int elements(); + } +} diff --git a/pljava-api/src/main/java/org/postgresql/pljava/model/package-info.java b/pljava-api/src/main/java/org/postgresql/pljava/model/package-info.java new file mode 100644 index 000000000..abfe63218 --- /dev/null +++ b/pljava-api/src/main/java/org/postgresql/pljava/model/package-info.java @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +/** + * Interfaces that model a useful subset of the PostgreSQL system catalogs + * and related PostgreSQL abstractions for convenient Java access. + *

CatalogObject and its subinterfaces

+ *

+ * The bulk of this package consists of interfaces extending + * {@link CatalogObject CatalogObject}, corresponding to various database + * objects represented in the PostgreSQL system catalogs. + *

+ * In many of the PostgreSQL catalog tables, each row is identified by an + * integer {@code oid}. When a row in a catalog table represents an object of + * some kind, the {@code oid} of that row (plus an identifier for which table + * it is defined in) will be enough to identify that object. + *

CatalogObject

+ *

+ * In most of the catalog tables, reference to another object is by its bare + * {@code oid}; the containing table is understood. For example, the + * {@code prorettype} attribute of a row in {@code pg_proc} (the catalog of + * procedures and functions) is a bare {@code oid}, understood to identify a row + * in {@code pg_type}, namely, the data type that the function returns. + *

+ * Such an {@code oid} standing alone, when the containing catalog is only + * implied in context, is represented in PL/Java by an instance of the root + * class {@link CatalogObject CatalogObject} itself. Such an object does not + * carry much information; it can be asked for its {@code oid}, and it can be + * combined with the {@code oid} of some catalog table to produce a + * {@link CatalogObject.Addressed CatalogObject.Addressed}. + *

CatalogObject.Addressed

+ *

+ * When the {@code oid} of a row in some catalog table is combined with an + * identifier for which catalog table, the result is the explicit + * address of an object. Because catalog tables themselves are defined by rows + * in one particular catalog table ({@code pg_class}), all that is needed to + * identify one is the {@code oid} of its defining row in {@code pg_class}. + * Therefore, a pair of numbers {@code (classId, objectId)} is a complete + * "object address" for most types of object in PostgreSQL. The {@code classId} + * identifies a catalog table (by its row in {@code pg_class}), and therefore + * what kind of object is intended, and the {@code objectId} identifies + * the specific row in that catalog table, and therefore the specific object. + *

+ * Such an {@code oid} pair is represented in PL/Java by an instance of + * {@link CatalogObject.Addressed CatalogObject.Addressed}—or, more + * likely, one of its specific subinterfaces in this package corresponding to + * the type of object. A function, for example, may be identified by a + * {@link RegProcedure RegProcedure} instance ({@code classId} identifies the + * {@code pg_proc} table, {@code objectId} is the row for the function), and its + * return type by a {@link RegType RegType} instance ({@code classId} identifies + * the {@code pg_type} table, and {@code objectId} the row defining the data + * type). + *

CatalogObject.Component

+ *

+ * The only current exception in PostgreSQL to the + * two-{@code oid}s-identify-an-object rule is for attributes (columns of tables + * or components of composite types), which are identified by three numbers, + * the {@code classId} and {@code objectId} of the parent object, plus a third + * number {@code subId} for the component's position in the parent. + * {@link Attribute Attribute}, therefore, is that rare subinterface that also + * implements {@link CatalogObject.Component CatalogObject.Component}. + *

+ * For the most part, that detail should be of no consequence to a user of this + * package, who will probably only ever obtain {@code Attribute} instances + * from a {@link TupleDescriptor TupleDescriptor}. + *

CatalogObject instances are singletons

+ *

+ * Object instances in this catalog model are lazily-populated singletons + * that exist upon being mentioned, and thereafter reliably identify the same + * {@code (classId,objectId)} in the PostgreSQL catalogs. (Whether that + * {@code (classId,objectId)} continues to identify the "same" thing in + * PostgreSQL can be affected by data-definition commands being issued in + * the same or some other session.) An instance is born lightweight, with only + * its identifying triple of numbers. Its methods that further expose properties + * of the addressed object (including whether any such object even exists) + * do not obtain that information from the PostgreSQL system caches until + * requested, and may then cache it in Java until signaled by PostgreSQL that + * some catalog change has invalidated it. + *

CharsetEncoding

+ *

+ * While not strictly a catalog object (PostgreSQL's supported encodings are + * a hard-coded set, not represented in the catalogs), they are exposed by + * {@link CharsetEncoding CharsetEncoding} instances that otherwise behave much + * like the modeled catalog objects, and are returned by the {@code encoding()} + * methods on {@link Database Database} and {@link RegCollation RegCollation}. + * The one in use on the server (an often-needed value) is exposed by the + * {@link CharsetEncoding#SERVER_ENCODING SERVER_ENCODING} static. + *

Lifespan subinterfaces

+ * Some PL/Java objects correspond to certain native structures in PostgreSQL + * and therefore must not be used beyond the native structures' lifespan. + * {@link Lifespan Lifespan} abstractly models any object in PostgreSQL that + * can be used to define, and detect the end of, a native-object lifespan. + * Two interfaces in this package that extend it and model specific PostgreSQL + * objects with that ability are {@link MemoryContext MemoryContext} and + * {@link ResourceOwner ResourceOwner}. + *

TupleTableSlot, TupleDescriptor, and Adapter

+ *

+ * {@code TupleTableSlot} in PostgreSQL is a flexible abstraction that can + * present several variant forms of native tuples to be manipulated with + * a common API. Modeled on that, {@link TupleTableSlot TupleTableSlot} is + * further abstracted, and can present a uniform API in PL/Java even to + * tuple-like things—anything with a sequence of typed, possibly named + * values—that might not be in the form of PostgreSQL native tuples. + *

+ * The key to the order, types, and names of the components of a tuple is + * its {@link TupleDescriptor TupleDescriptor}, which in broad strokes is little + * more than a {@code List} of {@link Attribute Attribute}. + *

+ * Given a tuple, and an {@code Attribute} that identifies its PostgreSQL data + * type, the job of accessing that value as some appropriate Java type falls to + * an {@link Adapter Adapter}, of which PL/Java provides a selection to cover + * common types, and there is + * a {@link org.postgresql.pljava.adt.spi service-provider interface} allowing + * independent development of others. + *

+ * PL/Java supplies simple adapters when a Java primitive or some existing + * standard Java class is clearly the appropriate mapping for a PostgreSQL type. + * Other than that (and excepting the model classes in this package), PL/Java + * avoids defining new Java classes to represent other PostgreSQL types. Such + * classes may already have been developed for an application, or may be found + * in existing Java driver libraries for PostgreSQL, such as PGJDBC or + * PGJDBC-NG. It would be unhelpful for PL/Java to offer another such, + * independent and incompatible, set. + *

+ * Instead, for PostgreSQL types that might not have an obvious, appropriate + * mapping to a standard Java type, or that might have more than one plausible + * mapping, PL/Java provides a set of functional interfaces in the + * package {@link org.postgresql.pljava.adt}. An {@code Adapter} (encapsulating + * internal details of a data type) can then expose the content in a documented, + * semantically clear form, to a simple application-supplied functional + * interface implementation or lambda that will produce a result of whatever + * Java type the application may already wish to use. + * + * @author Chapman Flack + */ +package org.postgresql.pljava.model; + +import org.postgresql.pljava.Adapter; +import org.postgresql.pljava.Lifespan; diff --git a/pljava-api/src/test/java/CatalogTest.java b/pljava-api/src/test/java/CatalogTest.java new file mode 100644 index 000000000..15aea321f --- /dev/null +++ b/pljava-api/src/test/java/CatalogTest.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava; + +import org.postgresql.pljava.model.RegNamespace; + +public class CatalogTest +{ + public boolean whatbits(RegNamespace n) + { + return n.grants().stream().anyMatch( + g -> g.usageGranted() && g.createGranted() ); + } +} diff --git a/pljava-examples/pom.xml b/pljava-examples/pom.xml index 973cf4bf5..f371bedf3 100644 --- a/pljava-examples/pom.xml +++ b/pljava-examples/pom.xml @@ -4,7 +4,7 @@ org.postgresql pljava.app - 2-SNAPSHOT + 1.7-SNAPSHOT pljava-examples PL/Java examples diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/CatalogObjects.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/CatalogObjects.java new file mode 100644 index 000000000..34cbd75d1 --- /dev/null +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/CatalogObjects.java @@ -0,0 +1,212 @@ +/* + * Copyright (c) 2023 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.example.annotation; + +import java.sql.Connection; +import static java.sql.DriverManager.getConnection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Statement; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.Optional; + +import java.util.logging.Logger; +import java.util.logging.Level; +import static java.util.logging.Level.INFO; +import static java.util.logging.Level.WARNING; + +import static java.util.stream.Collectors.joining; +import static java.util.stream.Collectors.toList; + +import org.postgresql.pljava.Adapter.As; +import org.postgresql.pljava.TargetList.Cursor; +import org.postgresql.pljava.TargetList.Projection; + +import org.postgresql.pljava.annotation.Function; +import org.postgresql.pljava.annotation.SQLAction; + +import org.postgresql.pljava.model.CatalogObject; +import org.postgresql.pljava.model.CatalogObject.Addressed; +import org.postgresql.pljava.model.CatalogObject.Named; +import org.postgresql.pljava.model.Portal; +import static org.postgresql.pljava.model.Portal.ALL; +import static org.postgresql.pljava.model.Portal.Direction.FORWARD; +import org.postgresql.pljava.model.RegClass; +import org.postgresql.pljava.model.RegClass.Known; +import org.postgresql.pljava.model.SlotTester; +import org.postgresql.pljava.model.TupleTableSlot; + +/** + * A test that PL/Java's various {@link CatalogObject} implementations are + * usable. + *

+ * They rely on named attributes, in PostgreSQL's system catalogs, that are + * looked up at class initialization, so on a PostgreSQL version that may not + * supply all the expected attributes, the issue may not be detected until + * an affected {@code CatalogObject} subclass is first used. This test uses as + * many of them as it can. + */ +@SQLAction(requires="catalogClasses function", install= + "SELECT javatest.catalogClasses()" +) +public class CatalogObjects { + static final Logger logr = Logger.getAnonymousLogger(); + + static void log(Level v, String m, Object... p) + { + logr.log(v, m, p); + } + + static final As CatObjAdapter; + static final As RegClsAdapter; + + static + { + try + { + Connection conn = getConnection("jdbc:default:connection"); + + // Get access to the hacked-together interim testing API + SlotTester t = conn.unwrap(SlotTester.class); + + String cls = "org.postgresql.pljava.pg.adt.OidAdapter"; + + @SuppressWarnings("unchecked") Object _1 = + CatObjAdapter = + (As)t.adapterPlease(cls, "INSTANCE"); + @SuppressWarnings("unchecked") Object _2 = + RegClsAdapter = + (As)t.adapterPlease(cls, "REGCLASS_INSTANCE"); + } + catch ( SQLException | ReflectiveOperationException e ) + { + throw new ExceptionInInitializerError(e); + } + } + + @Function(provides="catalogClasses function") + public static void catalogClasses() throws SQLException + { + String catalogRelationsQuery = + "SELECT" + + " oid" + + " FROM" + + " pg_catalog.pg_class" + + " WHERE" + + " relnamespace = CAST ('pg_catalog' AS pg_catalog.regnamespace)" + + " AND" + + " relkind = 'r'"; + + try ( + Connection conn = getConnection("jdbc:default:connection"); + Statement s = conn.createStatement(); + ) + { + SlotTester st = conn.unwrap(SlotTester.class); + + List knownRegClasses; + + try ( + Portal p = + st.unwrapAsPortal(s.executeQuery(catalogRelationsQuery)) + ) + { + Projection proj = p.tupleDescriptor(); + List tups = p.fetch(FORWARD, ALL); + + Class knownCls = Known.class; + + knownRegClasses = + proj.applyOver(tups, c0 -> c0.stream() + .map(c -> c.apply(RegClsAdapter, regcls -> regcls)) + .filter(knownCls::isInstance) + .map(knownCls::cast) + .collect(toList()) + ); + } + + int passed = 0; + int untested = 0; + + for ( Known regc : knownRegClasses ) + { + String objectQuery = + "SELECT oid FROM " + regc.qualifiedName() + " LIMIT 1"; + + Class classUnderTest = null; + + try ( + Portal p = + st.unwrapAsPortal(s.executeQuery(objectQuery)) + ) + { + Projection proj = p.tupleDescriptor(); + List tups = p.fetch(FORWARD, ALL); + Optional cobj = + proj.applyOver(tups, c0 -> c0.stream() + .map(c -> c.apply(CatObjAdapter, o -> o)) + .findAny()); + + if ( ! cobj.isPresent() ) + { + log(INFO, + "database has no {0} objects " + + " for representation test", regc.name()); + ++ untested; + continue; + } + + Addressed aobj = cobj.get().of(regc); + + classUnderTest = aobj.getClass(); + + if ( aobj instanceof Named ) + { + ((Named)aobj).name(); + ++ passed; + continue; + } + + log(INFO, + "{0} untested, not instance of Named " + + "(does implement {1})", + classUnderTest.getCanonicalName().substring( + 1 + classUnderTest.getPackageName().length()), + Arrays.stream(classUnderTest.getInterfaces()) + .map(Class::getSimpleName) + .collect(joining(", ")) + ); + } + catch ( LinkageError e ) + { + Throwable t = e.getCause(); + if ( null == t ) + t = e; + log(WARNING, + "{0} failed initialization: {1}", + classUnderTest.getName().substring( + 1 + classUnderTest.getPackageName().length()), + t.getMessage()); + } + } + + log((knownRegClasses.size() == passed + untested)? INFO : WARNING, + "of {0} catalog representations, {1} worked " + + "and {2} could not be tested", + knownRegClasses.size(), passed, untested); + } + } +} diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/CharsetEncodings.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/CharsetEncodings.java new file mode 100644 index 000000000..dfed6caa7 --- /dev/null +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/CharsetEncodings.java @@ -0,0 +1,199 @@ +/* + * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.example.annotation; + +import java.nio.charset.Charset; + +import java.sql.ResultSet; +import java.sql.SQLException; + +import java.util.Iterator; + +import org.postgresql.pljava.ResultSetProvider; +import org.postgresql.pljava.annotation.Function; +import static + org.postgresql.pljava.annotation.Function.OnNullInput.RETURNS_NULL; +import static org.postgresql.pljava.annotation.Function.Effects.IMMUTABLE; +import org.postgresql.pljava.annotation.SQLAction; + +import org.postgresql.pljava.model.CharsetEncoding; +import static org.postgresql.pljava.model.CharsetEncoding.SERVER_ENCODING; +import static org.postgresql.pljava.model.CharsetEncoding.clientEncoding; + +/** + * Example using the {@link CharsetEncoding CharsetEncoding} interface. + */ +public class CharsetEncodings implements ResultSetProvider.Large +{ + /** + * Enumerate PostgreSQL's known character set encodings, indicating for + * each one whether it is the server encoding, whether it's the client + * encoding, its PostgreSQL name, its corresponding Java + * {@link Charset Charset} name, and the Java module that provides it. + */ + @Function( + schema = "javatest", + out = { + "server boolean", "client boolean", "server_usable boolean", + "ordinal int", "pg_name text", "icu_name text", + "java_name text", "module text" + } + ) + public static ResultSetProvider charsets() + { + return new CharsetEncodings(); + } + + /** + * Enumerate Java's known character set encodings, trying to map them to + * PostgreSQL encodings, and indicating for + * each one whether it is the server encoding, whether it's the client + * encoding, its PostgreSQL name, its corresponding Java + * {@link Charset Charset} name, and the Java module that provides it. + */ + @Function( + schema = "javatest", + out = { + "server boolean", "client boolean", "server_usable boolean", + "ordinal int", "pg_name text", "icu_name text", + "java_name text", "module text" + } + ) + public static ResultSetProvider java_charsets(boolean try_aliases) + { + return new JavaEncodings(try_aliases); + } + + @Override + public void close() + { + } + + @Override + public boolean assignRowValues(ResultSet receiver, long currentRow) + throws SQLException + { + /* + * Shamelessly exploit the fact that currentRow will be passed as + * consecutive values starting at zero and that's the same way PG + * encodings are numbered. + */ + + CharsetEncoding cse; + + try + { + cse = CharsetEncoding.fromOrdinal((int)currentRow); + } + catch ( IllegalArgumentException e ) + { + return false; + } + + if ( SERVER_ENCODING == cse ) + receiver.updateBoolean("server", true); + if ( clientEncoding() == cse ) + receiver.updateBoolean("client", true); + if ( cse.usableOnServer() ) + receiver.updateBoolean("server_usable", true); + receiver.updateInt("ordinal", cse.ordinal()); + receiver.updateString("pg_name", cse.name()); + receiver.updateString("icu_name", cse.icuName()); + + Charset cs = cse.charset(); + if ( null == cs ) + return true; + + receiver.updateString("java_name", cs.name()); + receiver.updateString("module", cs.getClass().getModule().getName()); + + return true; + } + + static class JavaEncodings implements ResultSetProvider.Large + { + final Iterator iter = + Charset.availableCharsets().values().iterator(); + final boolean tryAliases; + + JavaEncodings(boolean tryAliases) + { + this.tryAliases = tryAliases; + } + + @Override + public void close() + { + } + + @Override + public boolean assignRowValues(ResultSet receiver, long currentRow) + throws SQLException + { + if ( ! iter.hasNext() ) + return false; + + Charset cs = iter.next(); + + receiver.updateString("java_name", cs.name()); + receiver.updateString("module", + cs.getClass().getModule().getName()); + + CharsetEncoding cse = null; + + try + { + cse = CharsetEncoding.fromName(cs.name()); + } + catch ( IllegalArgumentException e ) + { + } + + /* + * If the canonical Java name didn't match up with a PG encoding, + * try the first match found for any of the Java charset's aliases. + * This is not an especially dependable idea: the aliases are a Set, + * so they don't enumerate in a reproducible order, and some Java + * aliases are PG aliases for different charsets. + */ + if ( null == cse && tryAliases ) + { + for ( String alias : cs.aliases() ) + { + try + { + cse = CharsetEncoding.fromName(alias); + break; + } + catch ( IllegalArgumentException e ) + { + } + } + } + + if ( null == cse ) + return true; + + if ( SERVER_ENCODING == cse ) + receiver.updateBoolean("server", true); + if ( clientEncoding() == cse ) + receiver.updateBoolean("client", true); + if ( cse.usableOnServer() ) + receiver.updateBoolean("server_usable", true); + receiver.updateInt("ordinal", cse.ordinal()); + receiver.updateString("pg_name", cse.name()); + receiver.updateString("icu_name", cse.icuName()); + + return true; + } + } +} diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/DArray2.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/DArray2.java new file mode 100644 index 000000000..8f4212f08 --- /dev/null +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/DArray2.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2023 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.example.annotation; + +import org.postgresql.pljava.annotation.Function; +import org.postgresql.pljava.annotation.SQLType; + +/** + * Example to return a 2D array of {@code double}. + */ +public class DArray2 { + private DArray2() { } // do not instantiate + + /** + * Returns null as a {@code double[][]}. + */ + @Function( + schema = "javatest", type="double precision[]" + ) + public static double[][] darray2() + { + return null; + } +} diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/MemoryContexts.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/MemoryContexts.java new file mode 100644 index 000000000..2e6be6496 --- /dev/null +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/MemoryContexts.java @@ -0,0 +1,229 @@ +/* + * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.example.annotation; + +import java.sql.Connection; +import static java.sql.DriverManager.getConnection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.Savepoint; +import java.sql.Statement; +import java.sql.SQLException; + +import java.util.Iterator; + +import java.util.stream.Stream; + +import org.postgresql.pljava.ResultSetProvider; + +import org.postgresql.pljava.annotation.Function; +import org.postgresql.pljava.annotation.SQLAction; +import org.postgresql.pljava.annotation.SQLType; + +/** + * Functions to check that allocations are being made in the "upper" memory + * context as necessary when SPI has been used. + */ +public class MemoryContexts { + private MemoryContexts() + { + } + + private static Connection ensureSPIConnected() throws SQLException + { + Connection c = getConnection("jdbc:default:connection"); + try ( Statement s = c.createStatement() ) + { + s.execute("UPDATE javatest.foobar_1 SET stuff = 'a' WHERE FALSE"); + } + return c; + } + + /** + * Return an array result after connecting SPI, to ensure the result isn't + * allocated in SPI's short-lived memory context. + */ + @Function(schema = "javatest") + public static String[] nonSetArrayResult() throws SQLException + { + ensureSPIConnected(); + return new String[] { "Hello", "world" }; + } + + /** + * Return a coerced result after connecting SPI, to ensure the result isn't + * allocated in SPI's short-lived memory context. + *

+ * The mismatch of the Java type {@code int} and the PostgreSQL type + * {@code numeric} forces PL/Java to create a {@code Coerce} node applying + * a cast, the correct allocation of which is tested here. + */ + @Function(schema = "javatest", type = "numeric") + public static int nonSetCoercedResult() throws SQLException + { + ensureSPIConnected(); + return 42; + } + + /** + * Return a composite result after connecting SPI, to ensure the result + * isn't allocated in SPI's short-lived memory context. + */ + @Function(schema = "javatest", out = { "a text", "b text" }) + public static boolean nonSetCompositeResult(ResultSet out) + throws SQLException + { + ensureSPIConnected(); + out.updateString(1, "Hello"); + out.updateString(2, "world"); + return true; + } + + /** + * Return a fixed-length base UDT result after connecting SPI, to ensure + * the result isn't allocated in SPI's short-lived memory context. + */ + @Function(schema = "javatest") + public static ComplexScalar nonSetFixedUDTResult() throws SQLException + { + ensureSPIConnected(); + return new ComplexScalar(1.2, 3.4, "javatest.complexscalar"); + } + + /** + * Return a composite UDT result after connecting SPI, to ensure + * the result isn't allocated in SPI's short-lived memory context. + */ + @Function(schema = "javatest") + public static ComplexTuple nonSetCompositeUDTResult() throws SQLException + { + Connection c = ensureSPIConnected(); + try ( + Statement s = c.createStatement(); + ResultSet r = s.executeQuery( + "SELECT CAST ( '(1.2,3.4)' AS javatest.complextuple )") + ) + { + r.next(); + return r.getObject(1, ComplexTuple.class); + } + } + + /** + * Return a set-of (non-composite) result after connecting SPI, to ensure + * the result isn't allocated in SPI's short-lived memory context. + */ + @Function(schema = "javatest") + public static Iterator setNonCompositeResult() + { + final Iterator it = Stream.of("a", "b", "c").iterator(); + return new Iterator<>() + { + @Override + public boolean hasNext() + { + try + { + ensureSPIConnected(); + return it.hasNext(); + } + catch ( SQLException e ) + { + throw new RuntimeException(e.getMessage(), e); + } + } + + @Override + public String next() + { + try + { + ensureSPIConnected(); + return it.next(); + } + catch ( SQLException e ) + { + throw new RuntimeException(e.getMessage(), e); + } + } + }; + } + + /** + * Return a set-of composite result after connecting SPI, to ensure + * the result isn't allocated in SPI's short-lived memory context. + */ + @Function(schema = "javatest", out = {"a text", "b text"}) + public static ResultSetProvider setCompositeResult() + { + return new ResultSetProvider.Large() + { + @Override + public boolean assignRowValues(ResultSet out, long currentRow) + throws SQLException + { + ensureSPIConnected(); + if ( currentRow > 2 ) + return false; + out.updateString(1, "a"); + out.updateString(2, "b"); + return true; + } + + @Override + public void close() + { + } + }; + } + + /** + * Prepare a statement after connecting SPI and use it later, to ensure + * important allocations are not in SPI's short-lived memory context. + */ + @Function(schema = "javatest", out = {"a text", "b text"}) + public static ResultSetProvider preparedStatementContext() + throws SQLException + { + Connection c = ensureSPIConnected(); + final PreparedStatement ps = c.prepareStatement( + "SELECT " + + " to_char( " + + " extract(microseconds FROM statement_timestamp()) % 3999, " + + " ?)"); + ps.setString(1, "RN"); + + return new ResultSetProvider.Large() + { + @Override + public boolean assignRowValues(ResultSet out, long currentRow) + throws SQLException + { + ensureSPIConnected(); + if ( currentRow > 2 ) + return false; + try ( ResultSet rs = ps.executeQuery() ) + { + rs.next(); + out.updateString(1, rs.getString(1)); + ps.setString(1, "RN"); + return true; + } + } + + @Override + public void close() + { + } + }; + } +} diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/PassXML.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/PassXML.java index e735376c2..a33627856 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/PassXML.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/PassXML.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2021 Tada AB and other contributors, as listed below. + * Copyright (c) 2018-2022 Tada AB and other contributors, as listed below. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the The BSD 3-Clause License @@ -70,6 +70,8 @@ import org.postgresql.pljava.annotation.SQLAction; import org.postgresql.pljava.annotation.SQLType; +import static org.postgresql.pljava.model.CharsetEncoding.SERVER_ENCODING; + import static org.postgresql.pljava.example.LoggerTest.logMessage; /* Imports needed just for the SAX flavor of "low-level XML echo" below */ @@ -663,8 +665,7 @@ private static SQLXML echoSQLXML(SQLXML sx, int howin, int howout) * for setting the Transformer to use the server encoding. */ if ( howout < 5 ) - t.setOutputProperty(ENCODING, - System.getProperty("org.postgresql.server.encoding")); + t.setOutputProperty(ENCODING, SERVER_ENCODING.charset().name()); t.transform(src, rlt); } catch ( TransformerException te ) diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/SetOfRecordTest.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/SetOfRecordTest.java index 13fce44d4..5ecd6975d 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/SetOfRecordTest.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/SetOfRecordTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2020 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2022 Tada AB and other contributors, as listed below. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the The BSD 3-Clause License @@ -32,7 +32,7 @@ * there was no {@code =} or {@code DISTINCT FROM} operator between row types. */ @SQLAction(requires="selecttorecords fn", implementor="postgresql_ge_80400", -install= +install={ " SELECT " + " CASE WHEN r IS DISTINCT FROM ROW('Foo'::varchar, 1::integer, 1.5::float, " + " 23.67::decimal(8,2), '2005-06-01'::date, '20:56'::time, " + @@ -45,8 +45,20 @@ " 'select ''Foo'', 1, 1.5::float, 23.67, ''2005-06-01'', " + " ''20:56''::time, ''192.168.0''') " + " AS r(t_varchar varchar, t_integer integer, t_float float, " + -" t_decimal decimal(8,2), t_date date, t_time time, t_cidr cidr)" -) +" t_decimal decimal(8,2), t_date date, t_time time, t_cidr cidr)", + +" SELECT " + +" CASE WHEN every(a IS NOT DISTINCT FROM b) " + +" THEN javatest.logmessage('INFO', 'nested/SPI SetOfRecordTest ok') " + +" ELSE javatest.logmessage('WARNING', 'nested/SPI SetOfRecordTest not ok') " + +" END " + +" FROM " + +" javatest.executeselecttorecords('" + +" SELECT " + +" javatest.executeselect(''select generate_series(1,1)''), " + +" javatest.executeselect(''select generate_series(1,1)'') " + +" ') AS t(a text, b text)" +}) public class SetOfRecordTest implements ResultSetHandle { @Function(schema="javatest", name="executeselecttorecords", diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/TupleTableSlotTest.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/TupleTableSlotTest.java new file mode 100644 index 000000000..9b2bb9754 --- /dev/null +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/TupleTableSlotTest.java @@ -0,0 +1,762 @@ +/* + * Copyright (c) 2022-2023 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.example.annotation; + +import java.sql.Connection; +import static java.sql.DriverManager.getConnection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import java.util.ArrayList; +import java.util.Arrays; +import static java.util.Arrays.deepToString; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import java.time.LocalDateTime; + +import org.postgresql.pljava.Adapter; +import org.postgresql.pljava.Adapter.AdapterException;//for now; not planned API +import org.postgresql.pljava.Adapter.As; +import org.postgresql.pljava.Adapter.AsLong; +import org.postgresql.pljava.Adapter.AsDouble; +import org.postgresql.pljava.Adapter.AsInt; +import org.postgresql.pljava.Adapter.AsFloat; +import org.postgresql.pljava.Adapter.AsShort; +import org.postgresql.pljava.Adapter.AsChar; +import org.postgresql.pljava.Adapter.AsByte; +import org.postgresql.pljava.Adapter.AsBoolean; +import org.postgresql.pljava.ResultSetProvider; +import org.postgresql.pljava.TargetList; +import org.postgresql.pljava.TargetList.Cursor; +import org.postgresql.pljava.TargetList.Projection; + +import org.postgresql.pljava.annotation.Function; +import static + org.postgresql.pljava.annotation.Function.OnNullInput.RETURNS_NULL; +import org.postgresql.pljava.annotation.SQLAction; + +import org.postgresql.pljava.model.Attribute; +import org.postgresql.pljava.model.Portal; +import static org.postgresql.pljava.model.Portal.ALL; +import static org.postgresql.pljava.model.Portal.Direction.FORWARD; +import org.postgresql.pljava.model.SlotTester; +import org.postgresql.pljava.model.TupleDescriptor; +import org.postgresql.pljava.model.TupleTableSlot; + +/** + * A temporary test jig during TupleTableSlot development; intended + * to be used from a debugger. + */ +@SQLAction(requires = "modelToJDBC", install = +"WITH" + +" result AS (" + +" SELECT" + +" * " + +" FROM" + +" javatest.modelToJDBC(" + +" 'SELECT DISTINCT" + +" CAST ( relacl AS text ), relacl" + +" FROM" + +" pg_class" + +" WHERE" + +" relacl IS NOT NULL'," + +" 'org.postgresql.pljava.pg.adt.TextAdapter', 'INSTANCE'," + +" 'org.postgresql.pljava.pg.adt.GrantAdapter', 'LIST_INSTANCE'" + +" ) AS r(raw text, cooked text)" + +" )," + +" conformed AS (" + +" SELECT" + +" raw, translate(cooked, '[] ', '{}') AS cooked" + +" FROM" + +" result" + +" )" + +" SELECT" + +" CASE WHEN every(raw = cooked)" + +" THEN javatest.logmessage('INFO', 'AclItem[] ok')" + +" ELSE javatest.logmessage('WARNING', 'AclItem[] ng')" + +" END" + +" FROM" + +" conformed" +) +@SQLAction(requires = "modelToJDBC", install = +"WITH" + +" result AS (" + +" SELECT" + +" raw, cooked, CAST ( cooked AS numeric ) AS refried" + +" FROM" + +" javatest.modeltojdbc(" + +" 'SELECT" + +" CAST ( pow AS text ) AS txt, pow AS bin" + +" FROM" + +" generate_series(-20., 20., 1.) AS gs(p)," + +" (VALUES (1e-16), (1e-65)) AS pf(f)," + +" (VALUES (1.), (-1.)) AS sf(sgn)," + +" LATERAL (SELECT sgn*(37.821637 ^ (p + f))) AS s(pow)'," + +" 'org.postgresql.pljava.pg.adt.TextAdapter', 'INSTANCE'," + +" 'org.postgresql.pljava.pg.adt.NumericAdapter', 'BIGDECIMAL_INSTANCE'" + +" ) AS j(raw text, cooked text)" + +" )" + +" SELECT" + +" CASE WHEN every(raw = cooked OR raw = CAST ( refried AS text ))" + +" THEN javatest.logmessage('INFO', 'NUMERIC ok')" + +" ELSE javatest.logmessage('WARNING', 'NUMERIC ng')" + +" END" + +" FROM" + +" result" +) +public class TupleTableSlotTest +{ + /* + * Collect some Adapter instances that are going to be useful in the code + * below. Is it necessary they be static final? No, they can be obtained at + * any time, but collecting these here will keep the example methods tidier + * below. + * + * These are "leaf" adapters: they work from the PostgreSQL types directly. + */ + static final AsLong < ?> INT8; + static final AsInt < ?> INT4; + static final AsShort < ?> INT2; + static final AsByte < ?> INT1; + static final AsDouble < ?> FLOAT8; + static final AsFloat < ?> FLOAT4; + static final AsBoolean< ?> BOOL; + + static final As TEXT; + static final As LDT; // for the PostgreSQL TIMESTAMP type + + /* + * Now some adapters that can be derived from leaf adapters by composing + * non-leaf adapters over them. + * + * By default, the Adapters for primitive types can't fetch a null + * value. There is no value in the primitive's value space that could + * unambiguously represent null, and a DBMS should not go and reuse an + * otherwise-valid value to also mean null, if you haven't said to. But in + * a case where that is what you want, it is simple to write an adapter with + * the wanted behavior and compose it over the original one. + */ + static final AsDouble F8_NaN; // primitive double using NaN for null + + /* + * Reference-typed adapters have no trouble with null values by default; + * they'll just produce Java null. But suppose it is more convenient to get + * an Optional instead of a LocalDateTime that might be null. + * An Adapter for that can be obtained by composition. + */ + static final As,?> LDT_O; + + /* + * A composing adapter expecting a reference type can also be composed + * over one that produces a primitive type. It will see the values + * automatically boxed. + * + * Corollary: should the desired behavior be not to produce Optional, + * but simply to enable null handling for a primitive type by producing + * its boxed form or null, just one absolutely trivial composing adapter + * could add that behavior over any primitive adapter. + */ + static final As ,?> INT8_O; + + /* + * Once properly-typed adapters for component types are in hand, + * getting properly-typed array adapters is straightforward. (In Java 10+, + * a person might prefer to set these up at run time in local variables, + * where var could be used instead of these longwinded declarations.) + * + * For fun, I8x1 will be built over INT8_O, so it will really produce + * Optional[] instead of long[]. F8x5 will be built over F8_NaN, so it + * will produce double[][][][][], but null elements won't be rejected, + * and will appear as NaN. DTx2 will be built over LDT_O, so it will really + * produce Optional[][]. + */ + static final As[] ,?> I8x1; + static final As< int[][] ,?> I4x2; + static final As< short[][][] ,?> I2x3; + static final As< byte[][][][] ,?> I1x4; + static final As< double[][][][][] ,?> F8x5; + static final As< float[][][][][][] ,?> F4x6; + static final As< boolean[][][][][] ,?> Bx5; + static final As[][],?> DTx2; + + static + { + /* + * This is the very untidy part, while the planned Adapter manager API + * is not yet implemented. The extremely temporary adapterPlease method + * can be used to grovel some adapters out of PL/Java's innards, as long + * as the name of a class and a static final field is known. + * + * The adapter manager will have generic methods to obtain adapters with + * specific compile-time types. The adapterPlease method, not so much. + * It needs to be used with ugly casts. + */ + try + { + Connection conn = getConnection("jdbc:default:connection"); + SlotTester t = conn.unwrap(SlotTester.class); + + String cls = "org.postgresql.pljava.pg.adt.Primitives"; + INT8 = (AsLong )t.adapterPlease(cls, "INT8_INSTANCE"); + INT4 = (AsInt )t.adapterPlease(cls, "INT4_INSTANCE"); + INT2 = (AsShort )t.adapterPlease(cls, "INT2_INSTANCE"); + INT1 = (AsByte )t.adapterPlease(cls, "INT1_INSTANCE"); + FLOAT8 = (AsDouble )t.adapterPlease(cls, "FLOAT8_INSTANCE"); + FLOAT4 = (AsFloat )t.adapterPlease(cls, "FLOAT4_INSTANCE"); + BOOL = (AsBoolean)t.adapterPlease(cls, "BOOLEAN_INSTANCE"); + + cls = "org.postgresql.pljava.pg.adt.TextAdapter"; + + /* + * SuppressWarnings must appear on a declaration, making it hard to + * apply here, an initial assignment to a final field declared + * earlier. But making this the declaration of a new local variable, + * with the actual wanted assignment as a "side effect", works. + * (The "unnamed variable" _ previewed in Java 21 would be ideal.) + */ + @SuppressWarnings("unchecked") Object _1 = + TEXT = (As)t.adapterPlease(cls, "INSTANCE"); + + cls = "org.postgresql.pljava.pg.adt.DateTimeAdapter$JSR310"; + + @SuppressWarnings("unchecked") Object _2 = + LDT = + (As)t.adapterPlease(cls, "TIMESTAMP_INSTANCE"); + } + catch ( SQLException | ReflectiveOperationException e ) + { + throw new ExceptionInInitializerError(e); + } + + /* + * Other than those stopgap uses of adapterPlease, the rest is + * not so bad. Instantiate some composing adapters over the leaf + * adapters already obtained: + */ + + F8_NaN = new NullReplacingDouble(FLOAT8, Double.NaN); + LDT_O = new AsOptional<>(LDT); + INT8_O = new AsOptional<>(INT8); + + /* + * (Those composing adapters should be provided by PL/Java and known + * to the adapter manager so it can compose them for you. For now, + * they are just defined in this example file, showing that client + * code can easily supply its own.) + * + * Java array-of-array adapters of various dimensionalities are + * easily built from the adapters chosen for their component types. + */ + + I8x1 = INT8_O .a1() .build(); // array of Optional + I4x2 = INT4 .a2() .build(); + I2x3 = INT2 .a2() .a1() .build(); + I1x4 = INT1 .a4() .build(); + F8x5 = F8_NaN .a4() .a1() .build(); // 5D F8 array, null <-> NaN + F4x6 = FLOAT4 .a4() .a2() .build(); + Bx5 = BOOL .a4() .a1() .build(); + DTx2 = LDT_O .a2() .build(); // 2D of optional LDT + } + + /** + * Test {@link TargetList} and its functional API for retrieving values. + */ + @Function(schema="javatest") + public static Iterator targetListTest() + throws SQLException, ReflectiveOperationException + { + try ( + Connection conn = getConnection("jdbc:default:connection"); + Statement s = conn.createStatement(); + ) + { + SlotTester t = conn.unwrap(SlotTester.class); + + String query = + "SELECT" + + " to_char(stamp, 'DAY') AS day," + + " stamp" + + " FROM" + + " generate_series(" + + " timestamp 'epoch', timestamp 'epoch' + interval 'P6D'," + + " interval 'P1D'" + + " ) AS s(stamp)"; + + try ( Portal p = t.unwrapAsPortal(s.executeQuery(query)) ) + { + Projection proj = p.tupleDescriptor(); + + /* + * A quick glance shows this project(...) to be unneeded, as the + * query's TupleDescriptor already has exactly these columns in + * this order, and could be used below directly. On the other + * hand, this line will keep things working if someone later + * changes the query, reordering these columns or adding + * to them, and it may give a more explanatory exception if + * a change to the query does away with an expected column. + */ + proj = proj.project("day", "stamp"); + + List fetched = p.fetch(FORWARD, ALL); + + List results = new ArrayList<>(); + + proj.applyOver(fetched, c -> + { + /* + * This loop demonstrates a straightforward use of two + * Adapters and a lambda with two parameters to go through + * the retrieved rows. + * + * Note that applyOver does not, itself, iterate over the + * rows; it supplies a Cursor object that can be iterated to + * do that. This gives the lambda body of applyOver more + * control over how that will happen. + * + * The Cursor object is mutated during iteration so the + * same object represents each row in turn; the iteration + * variable is simply the Cursor object itself, so does not + * need to be used. Once the "unnamed variable" _ is more + * widely available (Java 21 has it, with --enable-preview), + * it will be the obvious choice for the iteration variable + * here. + * + * Within the loop, the cursor represents the single current + * row as far as its apply(...) methods are concerned. + * + * Other patterns, such as the streams API, can also be used + * (starting with a stream of the cursor object itself, + * again for each row), but can involve more fuss when + * checked exceptions are involved. + */ + for ( Cursor __ : c ) + { + c.apply(TEXT, LDT, // the adapters + ( v0, v1 ) -> // the fetched values + results.add(v0 + " | " + v1.getDayOfWeek()) + ); + } + + /* + * This equivalent loop uses two lambdas in curried style + * to do the same processing of the same two columns. That + * serves no practical need in this example; a perfectly + * good method signature for two reference columns was seen + * above. This loop illustrates the technique for combining + * the available methods when there isn't one that exactly + * fits the number and types of the target columns. + */ + for ( Cursor __ : c ) + { + c.apply(TEXT, + v0 -> + c.apply(LDT, + v1 -> + results.add(v0 + " | " + v1.getDayOfWeek()) + ) + ); + } + + return null; + }); + + return results.iterator(); + } + } + } + + /** + * Test retrieval of a PostgreSQL array as a multidimensional Java array. + */ + @Function(schema="javatest") + public static Iterator javaMultiArrayTest() + throws SQLException, ReflectiveOperationException + { + Connection conn = getConnection("jdbc:default:connection"); + SlotTester t = conn.unwrap(SlotTester.class); + + String query = + "VALUES (" + + " CAST ( '{1,2}' AS int8 [] ), " + + " CAST ( '{{1},{2}}' AS int4 [] ), " + + " CAST ( '{{{1,2,3}}}' AS int2 [] ), " + + " CAST ( '{{{{1},{2},{3}}}}' AS \"char\" [] ), " + // ASCII + " CAST ( '{{{{{1,2,3}}}}}' AS float8 [] ), " + + " CAST ( '{{{{{{1},{2},{3}}}}}}' AS float4 [] ), " + + " CAST ( '{{{{{t},{f},{t}}}}}' AS boolean [] ), " + + " CAST ( '{{''epoch''}}' AS timestamp [] ) " + + "), (" + + " '{NULL}', NULL, NULL, NULL, '{{{{{1,NULL,3}}}}}', NULL, NULL," + + " '{{NULL}}'" + + ")"; + + Portal p = t.unwrapAsPortal(conn.createStatement().executeQuery(query)); + Projection proj = p.tupleDescriptor(); + + List tups = p.fetch(FORWARD, ALL); + + List result = new ArrayList<>(); + + /* + * Then just use the right adapter for each column. + */ + proj.applyOver(tups, c -> + { + for ( Cursor __ : c ) + { + c.apply(I8x1, I4x2, I2x3, I1x4, F8x5, F4x6, Bx5, DTx2, + ( v0, v1, v2, v3, v4, v5, v6, v7 ) -> + result.addAll(List.of( + Arrays.toString(v0), deepToString(v1), deepToString(v2), + deepToString(v3), deepToString(v4), deepToString(v5), + deepToString(v6), deepToString(v7), + v7[0][0].orElse(LocalDateTime.MAX).getMonth() + "" + )) + ); + } + return null; + }); + + return result.iterator(); + } + + /** + * An adapter to compose over another one, adding some wanted behavior. + * + * There should eventually be a built-in set of composing adapters like + * this available for ready use, and automatically composed for you by an + * adapter manager when you say "I want an adapter for this PG type to this + * Java type and behaving this way." + * + * Until then, let this illustrate the simplicity of writing one. + */ + public static class NullReplacingDouble extends AsDouble + { + private final double replacement; + + @Override + public boolean canFetchNull() { return true; } + + @Override + public double fetchNull(Attribute a) + { + return replacement; + } + + // It would be nice to let this method be omitted and this behavior + // assumed, in a composing adapter with the same type for return and + // parameter. Maybe someday. + public double adapt(Attribute a, double value) + { + return value; + } + + private static final Adapter.Configuration config = + Adapter.configure(NullReplacingDouble.class, null); + + NullReplacingDouble(AsDouble over, double valueForNull) + { + super(config, over); + replacement = valueForNull; + } + } + + /** + * Another example of a useful composing adapter that should eventually be + * part of a built-in set. + */ + public static class AsOptional extends As,T> + { + // canFetchNull isn't needed; its default in As is true. + + @Override + public Optional fetchNull(Attribute a) + { + return Optional.empty(); + } + + public Optional adapt(Attribute a, T value) + { + return Optional.of(value); + } + + private static final Adapter.Configuration config = + Adapter.configure(AsOptional.class, null); + + /* + * This adapter may be composed over any Adapter, including those + * of primitive types as well as the reference-typed As. When + * constructed over a primitive-returning adapter, values will be boxed + * when passed to adapt(). + */ + AsOptional(Adapter over) + { + super(config, over, null); + } + } + + /** + * A surprisingly useful composing adapter that should eventually be + * part of a built-in set. + *

+ * Surprisingly useful, because although it "does" nothing, composing it + * over any primitive adapter produces one that returns the boxed form, and + * Java null for SQL null. + */ + public static class Identity extends As + { + // the inherited fetchNull returns null, which is just right + + public T adapt(Attribute a, T value) + { + return value; + } + + private static final Adapter.Configuration config = + Adapter.configure(Identity.class, null); + + /* + * Another choice could be to restrict 'over' to extend Primitive, as + * there isn't much point composing this adapter over one of reference + * type ... unless you want Java null for SQL null and the 'over' + * adapter produces something else. + */ + Identity(Adapter over) + { + super(config, over, null); + } + } + + /** + * Test retrieving results from a query using the PG-model API and returning + * them to the caller using the legacy JDBC API. + * @param query a query producing some number of columns + * @param adapters an array of strings, twice the number of columns, + * supplying a class name and static field name for the ugly temporary + * {@code adapterPlease} method, one such pair for each result column + */ + @Function( + schema = "javatest", type = "pg_catalog.record", variadic = true, + onNullInput = RETURNS_NULL, provides = "modelToJDBC" + ) + public static ResultSetProvider modelToJDBC(String query, String[] adapters) + throws SQLException, ReflectiveOperationException + { + Connection conn = getConnection("jdbc:default:connection"); + SlotTester t = conn.unwrap(SlotTester.class); + Portal p = t.unwrapAsPortal(conn.createStatement().executeQuery(query)); + TupleDescriptor td = p.tupleDescriptor(); + + if ( adapters.length != 2 * td.size() ) + throw new SQLException(String.format( + "query makes %d columns so 'adapters' should have %d " + + "elements, not %d", td.size(), 2*td.size(), adapters.length)); + + if ( Arrays.stream(adapters).anyMatch(Objects::isNull) ) + throw new SQLException("adapters array has null element"); + + As[] resolved = new As[ td.size() ]; + + for ( int i = 0 ; i < resolved.length ; ++ i ) + { + Adapter a = + t.adapterPlease(adapters[i<<1], adapters[(i<<1) + 1]); + if ( a instanceof As ) + resolved[i] = (As)a; + else + resolved[i] = new Identity(a); + } + + return new ResultSetProvider.Large() + { + @Override + public boolean assignRowValues(ResultSet out, long currentRow) + throws SQLException + { + if ( 0 == currentRow ) + { + int rcols = out.getMetaData().getColumnCount(); + if ( td.size() != rcols ) + throw new SQLException(String.format( + "query makes %d columns but result descriptor " + + "has %d", td.size(), rcols)); + } + + /* + * This example will fetch one tuple at a time here in the + * ResultSetProvider. This is a low-level interface to Postgres. + * In the SFRM_ValuePerCall protocol that ResultSetProvider + * supports, a fresh call from Postgres is made to retrieve each + * row. The Portal lives in a memory context that persists + * across the multiple calls, but the fetch result tups only + * exist in a child of the SPI context set up for each call. + * So here we only fetch as many tups as we can use to make one + * result row. + * + * If the logic involved fetching a bunch of rows and processing + * those into Java representations with no further dependence on + * the native tuples, then of course that could be done all in + * advance. + */ + List tups = p.fetch(FORWARD, 1); + if ( 0 == tups.size() ) + return false; + + TupleTableSlot tts = tups.get(0); + + for ( int i = 0 ; i < resolved.length ; ++ i ) + { + Object o = tts.get(i, resolved[i]); + try + { + out.updateObject(1 + i, o); + } + catch ( SQLException e ) + { + try + { + out.updateObject(1 + i, o.toString()); + } + catch ( SQLException e2 ) + { + e.addSuppressed(e2); + throw e; + } + } + } + + return true; + } + + @Override + public void close() + { + p.close(); + } + }; + } + + /** + * A temporary test jig during TupleTableSlot development; intended + * to be used from a debugger. + */ + @Function(schema="javatest") + public static void tupleTableSlotTest( + String query, String adpClass, String adpInstance) + throws SQLException, ReflectiveOperationException + { + new TupleTableSlotTest().testWith(query, adpClass, adpInstance); + } + + As adpL; + AsLong adpJ; + AsDouble adpD; + AsInt adpI; + AsFloat adpF; + AsShort adpS; + AsChar adpC; + AsByte adpB; + AsBoolean adpZ; + + void testWith(String query, String adpClass, String adpInstance) + throws SQLException, ReflectiveOperationException + { + Connection c = getConnection("jdbc:default:connection"); + SlotTester t = c.unwrap(SlotTester.class); + + ResultSet rs = c.createStatement().executeQuery(query); + Portal p = t.unwrapAsPortal(rs); + TupleDescriptor td = p.tupleDescriptor(); + + List tups = p.fetch(FORWARD, ALL); + + int ntups = tups.size(); + + boolean firstTime = true; + + int form = 8; // set with debugger, 8 selects reference-typed adpL + + boolean go; // true until set false by debugger each time through loop + + /* + * Results from adapters of assorted types. + */ + long jj = 0; + double dd = 0; + int ii = 0; + float ff = 0; + short ss = 0; + char cc = 0; + byte bb = 0; + boolean zz = false; + Object ll = null; + + for ( TupleTableSlot tts : tups ) + { + if ( firstTime ) + { + firstTime = false; + Adapter a = t.adapterPlease(adpClass, adpInstance); + if ( a instanceof As ) + adpL = (As)a; + else if ( a instanceof AsLong ) + adpJ = (AsLong)a; + else if ( a instanceof AsDouble ) + adpD = (AsDouble)a; + else if ( a instanceof AsInt ) + adpI = (AsInt)a; + else if ( a instanceof AsFloat ) + adpF = (AsFloat)a; + else if ( a instanceof AsShort ) + adpS = (AsShort)a; + else if ( a instanceof AsChar ) + adpC = (AsChar)a; + else if ( a instanceof AsByte ) + adpB = (AsByte)a; + else if ( a instanceof AsBoolean ) + adpZ = (AsBoolean)a; + } + + for ( Attribute att : tts.descriptor() ) + { + go = true; + while ( go ) + { + go = false; + try + { + switch ( form ) + { + case 0: jj = tts.get(att, adpJ); break; + case 1: dd = tts.get(att, adpD); break; + case 2: ii = tts.get(att, adpI); break; + case 3: ff = tts.get(att, adpF); break; + case 4: ss = tts.get(att, adpS); break; + case 5: cc = tts.get(att, adpC); break; + case 6: bb = tts.get(att, adpB); break; + case 7: zz = tts.get(att, adpZ); break; + case 8: ll = tts.get(att, adpL); break; + } + } + catch ( AdapterException e ) + { + System.out.println(e); + } + } + } + } + } +} diff --git a/pljava-packaging/pom.xml b/pljava-packaging/pom.xml index 0d2d6b1d2..8348726f8 100644 --- a/pljava-packaging/pom.xml +++ b/pljava-packaging/pom.xml @@ -4,7 +4,7 @@ org.postgresql pljava.app - 2-SNAPSHOT + 1.7-SNAPSHOT pljava-packaging PL/Java packaging diff --git a/pljava-packaging/src/main/resources/pljava.policy b/pljava-packaging/src/main/resources/pljava.policy index 4c7c078f1..da3ab5846 100644 --- a/pljava-packaging/src/main/resources/pljava.policy +++ b/pljava-packaging/src/main/resources/pljava.policy @@ -71,6 +71,8 @@ grant codebase "${org.postgresql.pljava.codesource}" { "control"; permission java.security.SecurityPermission "createAccessControlContext"; + permission org.postgresql.pljava.Adapter$Permission + "*", "fetch"; // This gives the PL/Java implementation code permission to read // any file, which it only exercises on behalf of sqlj.install_jar() @@ -87,6 +89,17 @@ grant codebase "${org.postgresql.pljava.codesource}" { }; +// +// This grant is specific to the API classes of PL/Java itself; the data type +// Adapter class is there (so user code can create adapters) and must be able +// to pass its own permission check. +// +grant codebase "${org.postgresql.pljava.codesource.api}" { + permission org.postgresql.pljava.Adapter$Permission + "*", "fetch"; +}; + + // // This grant defines the mapping onto Java of PostgreSQL's "trusted language" // category. When PL/Java executes a function whose SQL declaration names diff --git a/pljava-pgxs/pom.xml b/pljava-pgxs/pom.xml index 19896dbde..cde9d904f 100644 --- a/pljava-pgxs/pom.xml +++ b/pljava-pgxs/pom.xml @@ -5,7 +5,7 @@ org.postgresql pljava.app - 2-SNAPSHOT + 1.7-SNAPSHOT pljava-pgxs diff --git a/pljava-so/pom.xml b/pljava-so/pom.xml index dbd52812e..0072cf4c8 100644 --- a/pljava-so/pom.xml +++ b/pljava-so/pom.xml @@ -4,7 +4,7 @@ org.postgresql pljava.app - 2-SNAPSHOT + 1.7-SNAPSHOT pljava-so PL/Java backend native code @@ -91,6 +91,10 @@ compile : function(cc, files, output_dir, includes, defines, flags) { includes.add(java_include.resolve("linux").toString()); defines.put("Linux", null); + defines.put("PGXC", null); + defines.put("XCP", null); + defines.put("XZ", null); + defines.put("__TBASE__", null); flags.add("-c"); if(isDebugEnabled) flags.add("-g"); @@ -321,6 +325,7 @@ function execute() compile_flags.addAll(pgxs.getPgConfigPropertyAsList(cflags)); compile_flags.addAll(pgxs.getPgConfigPropertyAsList(cppflags)); compile_flags.addAll(pgxs.getPgConfigPropertyAsList(cflags_sl)); + compile_flags.remove('-dFRONTEND'); var exitCode = pgxs.compile( cc, files, target_path, base_includes, base_defines, compile_flags); if (exitCode != 0) diff --git a/pljava-so/src/main/c/Backend.c b/pljava-so/src/main/c/Backend.c index 287652fd2..88e0d376a 100644 --- a/pljava-so/src/main/c/Backend.c +++ b/pljava-so/src/main/c/Backend.c @@ -47,6 +47,8 @@ #include "org_postgresql_pljava_internal_Backend.h" #include "org_postgresql_pljava_internal_Backend_EarlyNatives.h" +#include "pljava/ModelConstants.h" +#include "pljava/ModelUtils.h" #include "pljava/DualState.h" #include "pljava/Invocation.h" #include "pljava/InstallHelper.h" @@ -721,7 +723,7 @@ static void initsequencer(enum initstage is, bool tolerant) } PG_CATCH(); { - MemoryContextSwitchTo(ctx.upperContext); /* leave ErrorContext */ + Invocation_switchToUpperContext(); /* leave ErrorContext */ Invocation_popBootContext(); initstage = IS_MISC_ONCE_DONE; /* We can't stay here... @@ -755,7 +757,7 @@ static void initsequencer(enum initstage is, bool tolerant) "and \"pljava-api.jar\" files, separated by the correct " "path separator for this platform.") )); - pljava_DualState_unregister(); + pljava_ResourceOwner_unregister(); _destroyJavaVM(0, 0); goto check_tolerant; } @@ -1082,11 +1084,13 @@ static void initPLJavaClasses(void) "THREADLOCK", "Ljava/lang/Object;"); JNI_setThreadLock(JNI_getStaticObjectField(s_Backend_class, fID)); + pljava_ModelConstants_initialize(); Invocation_initialize(); Exception_initialize2(); - SPI_initialize(); Type_initialize(); pljava_DualState_initialize(); + pljava_ModelUtils_initialize(); + SPI_initialize(); Function_initialize(); Session_initialize(); PgSavepoint_initialize(); @@ -1400,7 +1404,7 @@ static void _destroyJavaVM(int status, Datum dummy) elog(DEBUG2, "needed to forcibly shut down the Java virtual machine"); s_javaVM = 0; - currentInvocation = 0; + *currentInvocation = ctx; /* popBootContext but VM is gone */ return; } @@ -1429,7 +1433,7 @@ static void _destroyJavaVM(int status, Datum dummy) #endif elog(DEBUG2, "done shutting down the Java virtual machine"); s_javaVM = 0; - currentInvocation = 0; + *currentInvocation = ctx; /* popBootContext but VM is gone */ } } diff --git a/pljava-so/src/main/c/DualState.c b/pljava-so/src/main/c/DualState.c index e557231e5..8646b1c93 100644 --- a/pljava-so/src/main/c/DualState.c +++ b/pljava-so/src/main/c/DualState.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2019 Tada AB and other contributors, as listed below. + * Copyright (c) 2018-2023 Tada AB and other contributors, as listed below. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the The BSD 3-Clause License @@ -17,10 +17,11 @@ #include "org_postgresql_pljava_internal_DualState_SingleHeapFreeTuple.h" #include "org_postgresql_pljava_internal_DualState_SingleFreeErrorData.h" #include "org_postgresql_pljava_internal_DualState_SingleSPIfreeplan.h" +#include "org_postgresql_pljava_internal_DualState_SingleSPIfreetuptable.h" #include "org_postgresql_pljava_internal_DualState_SingleSPIcursorClose.h" +#include "org_postgresql_pljava_internal_DualState_BBHeapFreeTuple.h" #include "pljava/DualState.h" -#include "pljava/Backend.h" #include "pljava/Exception.h" #include "pljava/Invocation.h" #include "pljava/PgObject.h" @@ -50,22 +51,8 @@ extern void pljava_ExecutionPlan_initialize(void); static jclass s_DualState_class; -static jmethodID s_DualState_resourceOwnerRelease; static jmethodID s_DualState_cleanEnqueuedInstances; -static jobject s_DualState_key; - -static void resourceReleaseCB(ResourceReleasePhase phase, - bool isCommit, bool isTopLevel, void *arg); - -/* - * Return a capability that is only expected to be accessible to native code. - */ -jobject pljava_DualState_key(void) -{ - return s_DualState_key; -} - /* * Rather than using finalizers (deprecated in recent Java anyway), which can * increase the number of threads needing to interact with PG, DualState objects @@ -79,39 +66,9 @@ void pljava_DualState_cleanEnqueuedInstances(void) s_DualState_cleanEnqueuedInstances); } -/* - * Called when the lifespan/scope of a particular PG resource owner is about to - * expire, to make the associated DualState objects inaccessible from Java. As - * described in DualState.java, the argument will often be a PG ResourceOwner - * (when this function is called by resourceReleaseCB), but pointers to other - * structures can also be used (such a pointer clearly can't be confused with a - * ResourceOwner existing at the same time). In PG 9.5+, it could be a - * MemoryContext, with a MemoryContextCallback established to call this - * function. For items whose scope is limited to a single PL/Java function - * invocation, this can be a pointer to the Invocation. - */ -void pljava_DualState_nativeRelease(void *ro) -{ - Ptr2Long p2l; - - /* - * This static assertion does not need to be in every file - * that uses Ptr2Long, but it should be somewhere once, so here it is. - */ - StaticAssertStmt(sizeof p2l.ptrVal <= sizeof p2l.longVal, - "Pointer will not fit in long on this platform"); - - p2l.longVal = 0L; - p2l.ptrVal = ro; - JNI_callStaticVoidMethodLocked(s_DualState_class, - s_DualState_resourceOwnerRelease, - p2l.longVal); -} - void pljava_DualState_initialize(void) { jclass clazz; - jmethodID ctor; JNINativeMethod singlePfreeMethods[] = { @@ -173,6 +130,16 @@ void pljava_DualState_initialize(void) { 0, 0, 0 } }; + JNINativeMethod singleSPIfreetuptableMethods[] = + { + { + "_spiFreeTupTable", + "(J)V", + Java_org_postgresql_pljava_internal_DualState_00024SingleSPIfreetuptable__1spiFreeTupTable + }, + { 0, 0, 0 } + }; + JNINativeMethod singleSPIcursorCloseMethods[] = { { @@ -183,19 +150,21 @@ void pljava_DualState_initialize(void) { 0, 0, 0 } }; + JNINativeMethod bbHeapFreeTupleMethods[] = + { + { + "_heapFreeTuple", + "(Ljava/nio/ByteBuffer;)V", + Java_org_postgresql_pljava_internal_DualState_00024BBHeapFreeTuple__1heapFreeTuple + }, + { 0, 0, 0 } + }; + s_DualState_class = (jclass)JNI_newGlobalRef(PgObject_getJavaClass( "org/postgresql/pljava/internal/DualState")); - s_DualState_resourceOwnerRelease = PgObject_getStaticJavaMethod( - s_DualState_class, "resourceOwnerRelease", "(J)V"); s_DualState_cleanEnqueuedInstances = PgObject_getStaticJavaMethod( s_DualState_class, "cleanEnqueuedInstances", "()V"); - clazz = (jclass)PgObject_getJavaClass( - "org/postgresql/pljava/internal/DualState$Key"); - ctor = PgObject_getJavaMethod(clazz, "", "()V"); - s_DualState_key = JNI_newGlobalRef(JNI_newObject(clazz, ctor)); - JNI_deleteLocalRef(clazz); - clazz = (jclass)PgObject_getJavaClass( "org/postgresql/pljava/internal/DualState$SinglePfree"); PgObject_registerNatives2(clazz, singlePfreeMethods); @@ -226,12 +195,20 @@ void pljava_DualState_initialize(void) PgObject_registerNatives2(clazz, singleSPIfreeplanMethods); JNI_deleteLocalRef(clazz); + clazz = (jclass)PgObject_getJavaClass( + "org/postgresql/pljava/internal/DualState$SingleSPIfreetuptable"); + PgObject_registerNatives2(clazz, singleSPIfreetuptableMethods); + JNI_deleteLocalRef(clazz); + clazz = (jclass)PgObject_getJavaClass( "org/postgresql/pljava/internal/DualState$SingleSPIcursorClose"); PgObject_registerNatives2(clazz, singleSPIcursorCloseMethods); JNI_deleteLocalRef(clazz); - RegisterResourceReleaseCallback(resourceReleaseCB, NULL); + clazz = (jclass)PgObject_getJavaClass( + "org/postgresql/pljava/internal/DualState$BBHeapFreeTuple"); + PgObject_registerNatives2(clazz, bbHeapFreeTupleMethods); + JNI_deleteLocalRef(clazz); /* * Call initialize() methods of known classes built upon DualState. @@ -248,32 +225,6 @@ void pljava_DualState_initialize(void) pljava_VarlenaWrapper_initialize(); } -void pljava_DualState_unregister(void) -{ - UnregisterResourceReleaseCallback(resourceReleaseCB, NULL); -} - -static void resourceReleaseCB(ResourceReleasePhase phase, - bool isCommit, bool isTopLevel, void *arg) -{ - /* - * The way ResourceOwnerRelease is implemented, callbacks to loadable - * modules (like us!) happen /after/ all of the built-in releasey actions - * for a particular phase. So, by looking for RESOURCE_RELEASE_LOCKS here, - * we actually end up executing after all the built-in lock-related stuff - * has been released, but before any of the built-in stuff released in the - * RESOURCE_RELEASE_AFTER_LOCKS phase. Which, at least for the currently - * implemented DualState subclasses, is about the right time. - */ - if ( RESOURCE_RELEASE_LOCKS != phase ) - return; - - pljava_DualState_nativeRelease(CurrentResourceOwner); - - if ( isTopLevel ) - Backend_warnJEP411(isCommit); -} - /* @@ -394,6 +345,32 @@ Java_org_postgresql_pljava_internal_DualState_00024SingleSPIfreeplan__1spiFreePl +/* + * Class: org_postgresql_pljava_internal_DualState_SingleSPIfreetuptable + * Method: _spiFreeTupTable + * Signature: (J)V + */ +JNIEXPORT void JNICALL +Java_org_postgresql_pljava_internal_DualState_00024SingleSPIfreetuptable__1spiFreeTupTable( + JNIEnv* env, jobject _this, jlong pointer) +{ + BEGIN_NATIVE_NO_ERRCHECK + Ptr2Long p2l; + p2l.longVal = pointer; + PG_TRY(); + { + SPI_freetuptable(p2l.ptrVal); + } + PG_CATCH(); + { + Exception_throw_ERROR("SPI_freeplan"); + } + PG_END_TRY(); + END_NATIVE +} + + + /* * Class: org_postgresql_pljava_internal_DualState_SingleSPIcursorClose * Method: _spiCursorClose @@ -415,7 +392,7 @@ Java_org_postgresql_pljava_internal_DualState_00024SingleSPIcursorClose__1spiCur * does nothing if the current Invocation's errorOccurred flag is set, * or during an end-of-expression-context callback from the executor. */ - if ( NULL != currentInvocation && ! currentInvocation->errorOccurred + if ( HAS_INVOCATION && ! currentInvocation->errorOccurred && ! currentInvocation->inExprContextCB ) SPI_cursor_close(p2l.ptrVal); } @@ -426,3 +403,22 @@ Java_org_postgresql_pljava_internal_DualState_00024SingleSPIcursorClose__1spiCur PG_END_TRY(); END_NATIVE } + + + +/* + * Class: org_postgresql_pljava_internal_DualState_BBHeapFreeTuple + * Method: _heapFreeTuple + * Signature: (Ljava/nio/ByteBuffer;)V + */ +JNIEXPORT void JNICALL +Java_org_postgresql_pljava_internal_DualState_00024BBHeapFreeTuple__1heapFreeTuple( + JNIEnv* env, jobject _this, jobject bb) +{ + HeapTuple tup = (*env)->GetDirectBufferAddress(env, bb); + if ( NULL == tup ) + return; + BEGIN_NATIVE_NO_ERRCHECK + heap_freetuple(tup); + END_NATIVE +} diff --git a/pljava-so/src/main/c/ExecutionPlan.c b/pljava-so/src/main/c/ExecutionPlan.c index 6ff53eb49..b38dd4ccd 100644 --- a/pljava-so/src/main/c/ExecutionPlan.c +++ b/pljava-so/src/main/c/ExecutionPlan.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2019 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2022 Tada AB and other contributors, as listed below. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the The BSD 3-Clause License @@ -78,8 +78,7 @@ void pljava_ExecutionPlan_initialize(void) "org/postgresql/pljava/internal/ExecutionPlan")); s_ExecutionPlan_init = PgObject_getJavaMethod(s_ExecutionPlan_class, "", - "(Lorg/postgresql/pljava/internal/DualState$Key;J" - "Ljava/lang/Object;J)V"); + "(Ljava/lang/Object;J)V"); } static bool coerceObjects(void* ePlan, jobjectArray jvalues, Datum** valuesPtr, char** nullsPtr) @@ -333,8 +332,7 @@ Java_org_postgresql_pljava_internal_ExecutionPlan__1prepare(JNIEnv* env, jclass #endif result = JNI_newObjectLocked( s_ExecutionPlan_class, s_ExecutionPlan_init, - /* (jlong)0 as resource owner: the saved plan isn't transient */ - pljava_DualState_key(), (jlong)0, key, p2l.longVal); + key, p2l.longVal); } } PG_CATCH(); diff --git a/pljava-so/src/main/c/Function.c b/pljava-so/src/main/c/Function.c index a46c0e6dd..dab6fc544 100644 --- a/pljava-so/src/main/c/Function.c +++ b/pljava-so/src/main/c/Function.c @@ -1115,7 +1115,7 @@ jobject Function_currentLoader(void) { Function f; - if ( NULL == currentInvocation ) + if ( ! HAS_INVOCATION ) return NULL; f = currentInvocation->function; if ( NULL == f ) diff --git a/pljava-so/src/main/c/Invocation.c b/pljava-so/src/main/c/Invocation.c index 8fe4aaff3..473e0a948 100644 --- a/pljava-so/src/main/c/Invocation.c +++ b/pljava-so/src/main/c/Invocation.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2021 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2022 Tada AB and other contributors, as listed below. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the The BSD 3-Clause License @@ -13,7 +13,8 @@ #include #include -#include "org_postgresql_pljava_jdbc_Invocation.h" +#include "org_postgresql_pljava_internal_Invocation.h" +#include "org_postgresql_pljava_internal_Invocation_EarlyNatives.h" #include "pljava/Invocation.h" #include "pljava/Function.h" #include "pljava/PgObject.h" @@ -24,10 +25,33 @@ #define LOCAL_FRAME_SIZE 128 -static jmethodID s_Invocation_onExit; -static unsigned int s_callLevel = 0; +static jclass s_Invocation_class; +static jmethodID s_Invocation_onExit; -Invocation* currentInvocation; +/** + * All of these initial values are as were formerly set in pushBootContext, + * leaving it to set only upperContext (a value that's not statically known). + * When nestLevel is zero, no call into a PL/Java function is in progress. + */ +Invocation currentInvocation[] = +{ + { + .nestLevel = 0, + .hasDual = false, + .errorOccurred = false, + .hasConnected = false, + .inExprContextCB = false, + .upperContext = NULL, + .savedLoader = NULL, + .function = NULL, +#if PG_VERSION_NUM >= 100000 + .triggerData = NULL, +#endif + .previous = NULL, + .primSlot0.j = 0L, + .frameLimits = 0 + } +}; /* * Two features of the calling convention for PL/Java functions will be handled @@ -62,31 +86,32 @@ void Invocation_initialize(void) JNINativeMethod invocationMethods[] = { { - "_getCurrent", - "()Lorg/postgresql/pljava/jdbc/Invocation;", - Java_org_postgresql_pljava_jdbc_Invocation__1getCurrent - }, - { - "_getNestingLevel", - "()I", - Java_org_postgresql_pljava_jdbc_Invocation__1getNestingLevel - }, - { - "_clearErrorCondition", - "()V", - Java_org_postgresql_pljava_jdbc_Invocation__1clearErrorCondition - }, - { - "_register", - "()V", - Java_org_postgresql_pljava_jdbc_Invocation__1register + "_window", + "()Ljava/nio/ByteBuffer;", + Java_org_postgresql_pljava_internal_Invocation_00024EarlyNatives__1window }, { 0, 0, 0 } }; - cls = PgObject_getJavaClass("org/postgresql/pljava/jdbc/Invocation"); +#define CONFIRMOFFSET(fld) \ +StaticAssertStmt(offsetof(Invocation,fld) == \ +(org_postgresql_pljava_internal_Invocation_OFFSET_##fld), \ + "Java/C offset mismatch for " #fld) + + CONFIRMOFFSET(nestLevel); + CONFIRMOFFSET(hasDual); + CONFIRMOFFSET(errorOccurred); + CONFIRMOFFSET(upperContext); + +#undef CONFIRMOFFSET + + cls = PgObject_getJavaClass("org/postgresql/pljava/internal/Invocation$EarlyNatives"); PgObject_registerNatives2(cls, invocationMethods); - s_Invocation_onExit = PgObject_getJavaMethod(cls, "onExit", "(Z)V"); + JNI_deleteLocalRef(cls); + + cls = PgObject_getJavaClass("org/postgresql/pljava/internal/Invocation"); + s_Invocation_class = JNI_newGlobalRef(cls); + s_Invocation_onExit = PgObject_getStaticJavaMethod(cls, "onExit", "(IZ)V"); JNI_deleteLocalRef(cls); } @@ -137,28 +162,16 @@ jobject Invocation_getTypeMap(void) void Invocation_pushBootContext(Invocation* ctx) { JNI_pushLocalFrame(LOCAL_FRAME_SIZE); - ctx->invocation = 0; - ctx->function = 0; - ctx->frameLimits = 0; - ctx->primSlot0.j = 0L; - ctx->savedLoader = 0; - ctx->hasConnected = false; - ctx->upperContext = CurrentMemoryContext; - ctx->errorOccurred = false; - ctx->inExprContextCB = false; - ctx->previous = 0; -#if PG_VERSION_NUM >= 100000 - ctx->triggerData = 0; -#endif - currentInvocation = ctx; - ++s_callLevel; + *ctx = *currentInvocation; + currentInvocation->previous = ctx; + currentInvocation->upperContext = CurrentMemoryContext; + ++ currentInvocation->nestLevel; } void Invocation_popBootContext(void) { JNI_popLocalFrame(0); - currentInvocation = 0; - --s_callLevel; + *currentInvocation = *currentInvocation->previous; /* * Nothing is done here with savedLoader. It is just set to 0 in * pushBootContext (uses can precede allocation of the sentinel value), @@ -170,7 +183,9 @@ void Invocation_popBootContext(void) void Invocation_pushInvocation(Invocation* ctx) { JNI_pushLocalFrame(LOCAL_FRAME_SIZE); - ctx->invocation = 0; + *ctx = *currentInvocation; + currentInvocation->previous = ctx; + ctx = currentInvocation; /* just to keep the notation compact below */ ctx->function = 0; ctx->frameLimits = *s_frameLimits; ctx->primSlot0 = *s_primSlot0; @@ -179,12 +194,11 @@ void Invocation_pushInvocation(Invocation* ctx) ctx->upperContext = CurrentMemoryContext; ctx->errorOccurred = false; ctx->inExprContextCB = false; - ctx->previous = currentInvocation; #if PG_VERSION_NUM >= 100000 ctx->triggerData = 0; #endif - currentInvocation = ctx; - ++s_callLevel; + ctx->hasDual = false; + ++ ctx->nestLevel; } void Invocation_popInvocation(bool wasException) @@ -211,37 +225,36 @@ void Invocation_popInvocation(bool wasException) * invocation, delete the reference (after calling its onExit method, * indicating whether the return is exceptional or not). */ - if(currentInvocation->invocation != 0) + if ( currentInvocation->hasDual ) { - JNI_callVoidMethodLocked( - currentInvocation->invocation, s_Invocation_onExit, + JNI_callStaticVoidMethodLocked( + s_Invocation_class, s_Invocation_onExit, + (jint)currentInvocation->nestLevel, (wasException || currentInvocation->errorOccurred) ? JNI_TRUE : JNI_FALSE); - JNI_deleteGlobalRef(currentInvocation->invocation); } + if(currentInvocation->hasConnected) + SPI_finish(); + + JNI_popLocalFrame(0); + /* - * Do nativeRelease for any DualState instances scoped to this invocation. + * Return to the context that was effective at pushInvocation of *this* + * invocation. */ - pljava_DualState_nativeRelease(currentInvocation); + MemoryContextSwitchTo(currentInvocation->upperContext); /* * Check for any DualState objects that became unreachable and can be freed. + * In this late position, it might find things that became unreachable with + * the release of SPI contexts or JNI local frame references; having first + * switched back to the upperContext, the chance that any contexts possibly + * released in cleaning could be the current one are minimized. */ pljava_DualState_cleanEnqueuedInstances(); - if(currentInvocation->hasConnected) - SPI_finish(); - - JNI_popLocalFrame(0); - - if(ctx != 0) - { - MemoryContextSwitchTo(ctx->upperContext); - } - - currentInvocation = ctx; - --s_callLevel; + *currentInvocation = *ctx; } MemoryContext @@ -251,55 +264,13 @@ Invocation_switchToUpperContext(void) } /* - * Class: org_postgresql_pljava_jdbc_Invocation - * Method: _getNestingLevel - * Signature: ()I - */ -JNIEXPORT jint JNICALL -Java_org_postgresql_pljava_jdbc_Invocation__1getNestingLevel(JNIEnv* env, jclass cls) -{ - return s_callLevel; -} - -/* - * Class: org_postgresql_pljava_jdbc_Invocation - * Method: _getCurrent - * Signature: ()Lorg/postgresql/pljava/jdbc/Invocation; + * Class: org_postgresql_pljava_internal_Invocation_EarlyNatives + * Method: _window + * Signature: ()Ljava/nio/ByteBuffer; */ JNIEXPORT jobject JNICALL -Java_org_postgresql_pljava_jdbc_Invocation__1getCurrent(JNIEnv* env, jclass cls) +Java_org_postgresql_pljava_internal_Invocation_00024EarlyNatives__1window(JNIEnv* env, jobject _cls) { - return currentInvocation->invocation; -} - -/* - * Class: org_postgresql_pljava_jdbc_Invocation - * Method: _clearErrorCondition - * Signature: ()V - */ -JNIEXPORT void JNICALL -Java_org_postgresql_pljava_jdbc_Invocation__1clearErrorCondition(JNIEnv* env, jclass cls) -{ - currentInvocation->errorOccurred = false; -} - -/* - * Class: org_postgresql_pljava_jdbc_Invocation - * Method: _register - * Signature: ()V - */ -JNIEXPORT void JNICALL -Java_org_postgresql_pljava_jdbc_Invocation__1register(JNIEnv* env, jobject _this) -{ - if ( NULL == currentInvocation->invocation ) - { - currentInvocation->invocation = (*env)->NewGlobalRef(env, _this); - return; - } - if ( (*env)->IsSameObject(env, currentInvocation->invocation, _this) ) - return; - BEGIN_NATIVE - Exception_throw(ERRCODE_INTERNAL_ERROR, - "mismanaged PL/Java invocation stack"); - END_NATIVE + return (*env)->NewDirectByteBuffer(env, + currentInvocation, sizeof *currentInvocation); } diff --git a/pljava-so/src/main/c/JNICalls.c b/pljava-so/src/main/c/JNICalls.c index ba807c516..8ed518f77 100644 --- a/pljava-so/src/main/c/JNICalls.c +++ b/pljava-so/src/main/c/JNICalls.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2021 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2022 Tada AB and other contributors, as listed below. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the The BSD 3-Clause License @@ -314,10 +314,14 @@ bool beginNativeNoErrCheck(JNIEnv* env) bool beginNative(JNIEnv* env) { - if (!currentInvocation) + if ( ! HAS_INVOCATION ) { env = JNI_setEnv(env); - Exception_throw(ERRCODE_INTERNAL_ERROR, "An attempt was made to call a PostgreSQL backend function in a transaction callback. At the end of a transaction you may not access the database any longer."); + Exception_throw(ERRCODE_INTERNAL_ERROR, + "An attempt was made to call a PostgreSQL backend function " + "when no PL/Java function was active (such as in a transaction " + "callback. At the end of a transaction you may not access " + "the database any longer."); JNI_setEnv(env); return false; } diff --git a/pljava-so/src/main/c/ModelConstants.c b/pljava-so/src/main/c/ModelConstants.c new file mode 100644 index 000000000..1d2544ca5 --- /dev/null +++ b/pljava-so/src/main/c/ModelConstants.c @@ -0,0 +1,527 @@ +/* + * Copyright (c) 2022-2023 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ + +#include + +#if PG_VERSION_NUM < 140000 +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include + +#include "org_postgresql_pljava_pg_CatalogObjectImpl_Factory.h" +#include "org_postgresql_pljava_pg_ModelConstants.h" +#include "org_postgresql_pljava_pg_ModelConstants_Natives.h" +#include "org_postgresql_pljava_pg_TupleTableSlotImpl.h" + +#include +#include "org_postgresql_pljava_pg_AclItem.h" + +#include "pljava/PgObject.h" +#include "pljava/ModelConstants.h" + +#if PG_VERSION_NUM < 120000 +#define TupleDescData tupleDesc +#endif + +/* + * A compilation unit collecting various machine- or PostgreSQL-related + * constants that have to be known in Java code. Those that are expected to be + * stable can be defined in Java code, included from the Java-generated .h files + * and simply confirmed here (in the otherwise-unused dummy() method) by static + * assertions comparing them to the real values. Those that are expected to vary + * (between PostgreSQL versions, or target platforms, or both) are a bit more + * effort: their values are compiled into the constants[] array here, at indexes + * known to the Java code, and the _statics() native method will return a direct + * ByteBuffer through which the Java code can read them. + * + * To confirm the expected order of the array elements, each constant gets two + * consecutive array members, first the expected index, then the value. The + * CONSTANT macro below generates both, for the common case where the constant + * is known here by the name FOO and the Java index is in static field IDX_FOO + * in the ModelConstants class. CONSTANTEXPR is for the cases without that + * direct name correspondence. + * + * NOCONSTANT supplies the value ModelConstants.NOCONSTANT, intended for when + * the version of PG being built for does not define the constant in question + * (and when the NOCONSTANT value wouldn't be a valid value of the constant!). + */ + +#define CONSTANT(c) (org_postgresql_pljava_pg_ModelConstants_IDX_##c), (c) +#define CONSTANTEXPR(c,v) (org_postgresql_pljava_pg_ModelConstants_IDX_##c), (v) +#define NOCONSTANT(c) \ + CONSTANTEXPR(c,org_postgresql_pljava_pg_ModelConstants_NOCONSTANT) + +#define FORMOFFSET(form,fld) \ + CONSTANTEXPR(OFFSET_##form##_##fld, offsetof(FormData_##form,fld)) + +#define TYPEOFFSET(type,tag,fld) \ + CONSTANTEXPR(OFFSET_##tag##_##fld, offsetof(type,fld)) + +static int32 constants[] = { + CONSTANT(PG_VERSION_NUM), + + CONSTANT(SIZEOF_DATUM), + CONSTANTEXPR(SIZEOF_INT, sizeof (int)), + CONSTANTEXPR(SIZEOF_SIZE, sizeof (Size)), + + CONSTANT(ALIGNOF_SHORT), + CONSTANT(ALIGNOF_INT), + CONSTANT(ALIGNOF_DOUBLE), + CONSTANT(MAXIMUM_ALIGNOF), + + CONSTANT(NAMEDATALEN), + + + + CONSTANTEXPR(SIZEOF_varatt_indirect, sizeof (varatt_indirect)), + CONSTANTEXPR(SIZEOF_varatt_expanded, sizeof (varatt_expanded)), + CONSTANTEXPR(SIZEOF_varatt_external, sizeof (varatt_external)), + + + + CONSTANT(HEAPTUPLESIZE), + CONSTANTEXPR(OFFSET_TTS_NVALID, offsetof(TupleTableSlot, tts_nvalid)), + CONSTANTEXPR(SIZEOF_TTS_NVALID, sizeof ((TupleTableSlot *)0)->tts_nvalid), + +#if PG_VERSION_NUM >= 120000 + CONSTANT(TTS_FLAG_EMPTY), + CONSTANT(TTS_FLAG_FIXED), + CONSTANTEXPR(OFFSET_TTS_FLAGS, offsetof(TupleTableSlot, tts_flags)), + NOCONSTANT(OFFSET_TTS_EMPTY), + NOCONSTANT(OFFSET_TTS_FIXED), + CONSTANTEXPR(OFFSET_TTS_TABLEOID, offsetof(TupleTableSlot, tts_tableOid)), +#else + NOCONSTANT(TTS_FLAG_EMPTY), + NOCONSTANT(TTS_FLAG_FIXED), + NOCONSTANT(OFFSET_TTS_FLAGS), + CONSTANTEXPR(OFFSET_TTS_EMPTY, offsetof(TupleTableSlot, tts_isempty)), +#if PG_VERSION_NUM >= 110000 + CONSTANTEXPR(OFFSET_TTS_FIXED, + offsetof(TupleTableSlot, tts_fixedTupleDescriptor)), +#else + NOCONSTANT(OFFSET_TTS_FIXED), +#endif /* 110000 */ + NOCONSTANT(OFFSET_TTS_TABLEOID), +#endif /* 120000 */ + + + + CONSTANTEXPR(OFFSET_TUPLEDESC_ATTRS, offsetof(struct TupleDescData, attrs)), + CONSTANTEXPR(OFFSET_TUPLEDESC_TDREFCOUNT, + offsetof(struct TupleDescData, tdrefcount)), + CONSTANTEXPR(SIZEOF_TUPLEDESC_TDREFCOUNT, + sizeof ((struct TupleDescData *)0)->tdrefcount), + CONSTANTEXPR(OFFSET_TUPLEDESC_TDTYPEID, + offsetof(struct TupleDescData, tdtypeid)), + CONSTANTEXPR(OFFSET_TUPLEDESC_TDTYPMOD, + offsetof(struct TupleDescData, tdtypmod)), + + + + CONSTANTEXPR(SIZEOF_FORM_PG_ATTRIBUTE, sizeof (FormData_pg_attribute)), + CONSTANT(ATTRIBUTE_FIXED_PART_SIZE), + FORMOFFSET( pg_attribute, atttypid ), + FORMOFFSET( pg_attribute, attlen ), + FORMOFFSET( pg_attribute, attcacheoff ), + FORMOFFSET( pg_attribute, atttypmod ), + FORMOFFSET( pg_attribute, attbyval ), + FORMOFFSET( pg_attribute, attalign ), + FORMOFFSET( pg_attribute, attnotnull ), + FORMOFFSET( pg_attribute, attisdropped ), + + + + CONSTANT(CLASS_TUPLE_SIZE), + CONSTANT( Anum_pg_class_reltype ), + + + + CONSTANTEXPR(SIZEOF_MCTX, sizeof (MemoryContextData)), + TYPEOFFSET(MemoryContextData, MCTX, isReset), +#if PG_VERSION_NUM >= 130000 + TYPEOFFSET(MemoryContextData, MCTX, mem_allocated), +#else + NOCONSTANT(OFFSET_MCTX_mem_allocated), +#endif + TYPEOFFSET(MemoryContextData, MCTX, parent), + TYPEOFFSET(MemoryContextData, MCTX, firstchild), + TYPEOFFSET(MemoryContextData, MCTX, prevchild), + TYPEOFFSET(MemoryContextData, MCTX, nextchild), + TYPEOFFSET(MemoryContextData, MCTX, name), +#if PG_VERSION_NUM >= 110000 + TYPEOFFSET(MemoryContextData, MCTX, ident), +#else + NOCONSTANT(OFFSET_MCTX_ident), +#endif + + + + CONSTANT(N_ACL_RIGHTS), + + + + CONSTANT(ATTNUM), + CONSTANT(AUTHMEMMEMROLE), + CONSTANT(AUTHMEMROLEMEM), + CONSTANT(AUTHOID), + CONSTANT(COLLOID), + CONSTANT(DATABASEOID), + CONSTANT(LANGOID), + CONSTANT(NAMESPACEOID), + CONSTANT(OPEROID), + CONSTANT(PROCOID), + CONSTANT(RELOID), + CONSTANT(TSCONFIGOID), + CONSTANT(TSDICTOID), + CONSTANT(TYPEOID), + + + + // TBASE + TYPEOFFSET(HeapTupleHeaderData, HeapTupleHeaderData, t_infomask), + TYPEOFFSET(HeapTupleHeaderData, HeapTupleHeaderData, t_infomask2), + TYPEOFFSET(HeapTupleHeaderData, HeapTupleHeaderData, t_hoff), + TYPEOFFSET(HeapTupleHeaderData, HeapTupleHeaderData, t_bits), + + + +}; + +#undef CONSTANT +#undef CONSTANTEXPR + +static void dummy() +{ + StaticAssertStmt(SIZEOF_DATUM == SIZEOF_VOID_P, + "PostgreSQL SIZEOF_DATUM and SIZEOF_VOID_P no longer equivalent?"); + +#define CONFIRMCONST(c) \ +StaticAssertStmt((c) == \ +(org_postgresql_pljava_pg_CatalogObjectImpl_Factory_##c), \ + "Java/C value mismatch for " #c) + + CONFIRMCONST( InvalidOid ); + + CONFIRMCONST( TypeRelationId ); + CONFIRMCONST( AttributeRelationId ); + CONFIRMCONST( ProcedureRelationId ); + CONFIRMCONST( RelationRelationId ); + CONFIRMCONST( AuthIdRelationId ); + CONFIRMCONST( DatabaseRelationId ); + CONFIRMCONST( LanguageRelationId ); + CONFIRMCONST( NamespaceRelationId ); + CONFIRMCONST( OperatorRelationId ); + CONFIRMCONST( ExtensionRelationId ); + CONFIRMCONST( CollationRelationId ); + CONFIRMCONST( TSDictionaryRelationId ); + CONFIRMCONST( TSConfigRelationId ); + + /* + * PG types good to have around because of corresponding JDBC types. + */ + CONFIRMCONST( BOOLOID ); + CONFIRMCONST( BYTEAOID ); + CONFIRMCONST( CHAROID ); + CONFIRMCONST( INT8OID ); + CONFIRMCONST( INT2OID ); + CONFIRMCONST( INT4OID ); + CONFIRMCONST( XMLOID ); + CONFIRMCONST( FLOAT4OID ); + CONFIRMCONST( FLOAT8OID ); + CONFIRMCONST( BPCHAROID ); + CONFIRMCONST( VARCHAROID ); + CONFIRMCONST( DATEOID ); + CONFIRMCONST( TIMEOID ); + CONFIRMCONST( TIMESTAMPOID ); + CONFIRMCONST( TIMESTAMPTZOID ); + CONFIRMCONST( TIMETZOID ); + CONFIRMCONST( BITOID ); + CONFIRMCONST( VARBITOID ); + CONFIRMCONST( NUMERICOID ); + + /* + * PG types not mentioned in JDBC but bread-and-butter to PG devs. + */ + CONFIRMCONST( TEXTOID ); + CONFIRMCONST( UNKNOWNOID ); + CONFIRMCONST( RECORDOID ); + CONFIRMCONST( CSTRINGOID ); + CONFIRMCONST( VOIDOID ); + + /* + * PG types used in modeling PG types themselves. + */ + CONFIRMCONST( NAMEOID ); + CONFIRMCONST( REGPROCOID ); + CONFIRMCONST( OIDOID ); + CONFIRMCONST( PG_NODE_TREEOID ); + CONFIRMCONST( ACLITEMOID ); + CONFIRMCONST( REGPROCEDUREOID ); + CONFIRMCONST( REGOPEROID ); + CONFIRMCONST( REGOPERATOROID ); + CONFIRMCONST( REGCLASSOID ); + CONFIRMCONST( REGTYPEOID ); + CONFIRMCONST( REGCONFIGOID ); + CONFIRMCONST( REGDICTIONARYOID ); + CONFIRMCONST( REGNAMESPACEOID ); + CONFIRMCONST( REGROLEOID ); +#if PG_VERSION_NUM >= 130000 + CONFIRMCONST( REGCOLLATIONOID ); +#endif + + /* + * The well-known, pinned procedural languages. + */ + CONFIRMCONST( INTERNALlanguageId ); + CONFIRMCONST( ClanguageId ); + CONFIRMCONST( SQLlanguageId ); + + /* + * The well-known, pinned namespaces. + */ + CONFIRMCONST( PG_CATALOG_NAMESPACE ); + CONFIRMCONST( PG_TOAST_NAMESPACE ); + + /* + * The well-known, pinned collations. + */ + CONFIRMCONST( DEFAULT_COLLATION_OID ); + CONFIRMCONST( C_COLLATION_OID ); + CONFIRMCONST( POSIX_COLLATION_OID ); + +#undef CONFIRMCONST + +#define CONFIRMCONST(c) \ +StaticAssertStmt((c) == \ +(org_postgresql_pljava_pg_AclItem_##c), \ + "Java/C value mismatch for " #c) + + CONFIRMCONST( ACL_INSERT ); + CONFIRMCONST( ACL_SELECT ); + CONFIRMCONST( ACL_UPDATE ); + CONFIRMCONST( ACL_DELETE ); + CONFIRMCONST( ACL_TRUNCATE ); + CONFIRMCONST( ACL_REFERENCES ); + CONFIRMCONST( ACL_TRIGGER ); + CONFIRMCONST( ACL_EXECUTE ); + CONFIRMCONST( ACL_USAGE ); + CONFIRMCONST( ACL_CREATE ); + CONFIRMCONST( ACL_CREATE_TEMP ); + CONFIRMCONST( ACL_CONNECT ); +#if PG_VERSION_NUM >= 150000 + CONFIRMCONST( ACL_SET ); + CONFIRMCONST( ACL_ALTER_SYSTEM); +#endif + CONFIRMCONST( ACL_ID_PUBLIC ); + +#define CONFIRMOFFSET(typ,fld) \ +StaticAssertStmt(offsetof(typ,fld) == \ +(org_postgresql_pljava_pg_AclItem_OFFSET_##fld), \ + "Java/C offset mismatch for " #fld) + + CONFIRMOFFSET( AclItem, ai_grantee ); + CONFIRMOFFSET( AclItem, ai_grantor ); + CONFIRMOFFSET( AclItem, ai_privs ); + +#undef CONFIRMCONST +#undef CONFIRMOFFSET + +#define CONFIRMCONST(c) \ +StaticAssertStmt((c) == \ +(org_postgresql_pljava_pg_ModelConstants_##c), \ + "Java/C value mismatch for " #c) +#define CONFIRMSIZEOF(form,fld) \ +StaticAssertStmt((sizeof ((FormData_##form *)0)->fld) == \ +(org_postgresql_pljava_pg_ModelConstants_SIZEOF_##form##_##fld), \ + "Java/C sizeof mismatch for " #form "." #fld) +#define CONFIRMOFFSET(form,fld) \ +StaticAssertStmt(offsetof(FormData_##form,fld) == \ +(org_postgresql_pljava_pg_ModelConstants_OFFSET_##form##_##fld), \ + "Java/C offset mismatch for " #form "." #fld) +#define CONFIRMATTNUM(form,fld) \ +StaticAssertStmt(Anum_##form##_##fld == \ +(org_postgresql_pljava_pg_ModelConstants_Anum_##form##_##fld), \ + "Java/C attribute number mismatch for " #form "." #fld) +#define CONFIRMEXPR(c,expr) \ +StaticAssertStmt((expr) == \ +(org_postgresql_pljava_pg_ModelConstants_##c), \ + "Java/C value mismatch for " #c) + + CONFIRMCONST( PG_SQL_ASCII ); + CONFIRMCONST( PG_UTF8 ); + CONFIRMCONST( PG_LATIN1 ); + CONFIRMCONST( PG_ENCODING_BE_LAST ); + + CONFIRMCONST( VARHDRSZ ); + CONFIRMCONST( VARHDRSZ_EXTERNAL ); + CONFIRMCONST( VARTAG_INDIRECT ); + CONFIRMCONST( VARTAG_EXPANDED_RO ); + CONFIRMCONST( VARTAG_EXPANDED_RW ); + CONFIRMCONST( VARTAG_ONDISK ); + + CONFIRMATTNUM( pg_attribute, attname ); + + CONFIRMSIZEOF( pg_attribute, atttypid ); + CONFIRMSIZEOF( pg_attribute, attlen ); + CONFIRMSIZEOF( pg_attribute, attcacheoff ); + CONFIRMSIZEOF( pg_attribute, atttypmod ); + CONFIRMSIZEOF( pg_attribute, attbyval ); + CONFIRMSIZEOF( pg_attribute, attalign ); + CONFIRMSIZEOF( pg_attribute, attnotnull ); + CONFIRMSIZEOF( pg_attribute, attisdropped ); + +#if PG_VERSION_NUM >= 120000 + CONFIRMATTNUM( pg_extension, oid ); +#endif + CONFIRMCONST( ExtensionOidIndexId ); + +#undef CONFIRMSIZEOF +#undef CONFIRMOFFSET +#define CONFIRMSIZEOF(strct,fld) \ +StaticAssertStmt((sizeof ((strct *)0)->fld) == \ +(org_postgresql_pljava_pg_ModelConstants_SIZEOF_##strct##_##fld), \ + "Java/C sizeof mismatch for " #strct "." #fld) +#define CONFIRMVLOFFSET(strct,fld) \ +StaticAssertStmt(offsetof(strct,fld) - VARHDRSZ == \ +(org_postgresql_pljava_pg_ModelConstants_OFFSET_##strct##_##fld), \ + "Java/C offset mismatch for " #strct "." #fld) + + CONFIRMSIZEOF( ArrayType, ndim ); + CONFIRMSIZEOF( ArrayType, dataoffset ); + CONFIRMSIZEOF( ArrayType, elemtype ); + + CONFIRMVLOFFSET( ArrayType, ndim ); + CONFIRMVLOFFSET( ArrayType, dataoffset ); + CONFIRMVLOFFSET( ArrayType, elemtype ); + +#if 0 + /* + * Given the way ARR_DIMS is defined in PostgreSQL's array.h, there seems + * to be no way to construct a static assertion for this offset acceptable + * to a compiler that forbids "the conversions of a reinterpret_cast" in + * a constant expression. This will have to be checked in an old-fashioned + * runtime assertion in _initialize, losing the benefit of compile-time + * detection. + */ + CONFIRMEXPR( OFFSET_ArrayType_DIMS, + (((char*)ARR_DIMS(0)) - (char *)0) - VARHDRSZ ); +#endif + + CONFIRMEXPR( SIZEOF_ArrayType_DIM, sizeof *ARR_DIMS(0) ); + +#undef CONFIRMSIZEOF +#undef CONFIRMVLOFFSET +#undef CONFIRMCONST +#undef CONFIRMATTNUM +#undef CONFIRMEXPR + +#define CONFIRMCONST(c) \ +StaticAssertStmt((c) == \ +(org_postgresql_pljava_pg_TupleTableSlotImpl_##c), \ + "Java/C value mismatch for " #c) +#define CONFIRMSIZEOF(form,fld) \ +StaticAssertStmt((sizeof ((form *)0)->fld) == \ +(org_postgresql_pljava_pg_TupleTableSlotImpl_SIZEOF_##form##_##fld), \ + "Java/C sizeof mismatch for " #form "." #fld) +#define CONFIRMOFFSET(form,fld) \ +StaticAssertStmt(offsetof(form,fld) == \ +(org_postgresql_pljava_pg_TupleTableSlotImpl_OFFSET_##form##_##fld), \ + "Java/C offset mismatch for " #form "." #fld) + + CONFIRMOFFSET( HeapTupleData, t_len ); + CONFIRMOFFSET( HeapTupleData, t_tableOid ); + + CONFIRMSIZEOF( HeapTupleData, t_len ); + CONFIRMSIZEOF( HeapTupleData, t_tableOid ); + +/* TBASE + CONFIRMOFFSET( HeapTupleHeaderData, t_infomask ); + CONFIRMOFFSET( HeapTupleHeaderData, t_infomask2 ); + CONFIRMOFFSET( HeapTupleHeaderData, t_hoff ); + CONFIRMOFFSET( HeapTupleHeaderData, t_bits ); +*/ + CONFIRMSIZEOF( HeapTupleHeaderData, t_infomask ); + CONFIRMSIZEOF( HeapTupleHeaderData, t_infomask2 ); + CONFIRMSIZEOF( HeapTupleHeaderData, t_hoff ); + + CONFIRMCONST( HEAP_HASNULL ); + CONFIRMCONST( HEAP_HASEXTERNAL ); + CONFIRMCONST( HEAP_NATTS_MASK ); + +#undef CONFIRMCONST +#undef CONFIRMSIZEOF +#undef CONFIRMOFFSET + +} + +void pljava_ModelConstants_initialize(void) +{ + ArrayType dummyArray; + jclass cls; + + JNINativeMethod methods[] = + { + { + "_statics", + "()Ljava/nio/ByteBuffer;", + Java_org_postgresql_pljava_pg_ModelConstants_00024Natives__1statics + }, + { 0, 0, 0 }, + { 0, 0, dummy } /* so C compiler won't warn that dummy is unused */ + }; + + cls = PgObject_getJavaClass( + "org/postgresql/pljava/pg/ModelConstants$Natives"); + PgObject_registerNatives2(cls, methods); + JNI_deleteLocalRef(cls); + + /* + * Don't really use PostgreSQL Assert for this; it goes behind elog's back. + */ + if (org_postgresql_pljava_pg_ModelConstants_OFFSET_ArrayType_DIMS != + (((char*)ARR_DIMS(&dummyArray)) - (char *)&dummyArray) - VARHDRSZ ) + elog(ERROR, + "PL/Java built with mismatched value for OFFSET_ArrayType_DIMS"); +} + +/* + * Class: org_postgresql_pljava_pg_ModelConstants_Natives + * Method: _statics + * Signature: ()Ljava/nio/ByteBuffer; + */ +JNIEXPORT jobject JNICALL +Java_org_postgresql_pljava_pg_ModelConstants_00024Natives__1statics(JNIEnv* env, jobject _cls) +{ + /* + * None of the usual PL/Java BEGIN_NATIVE fencing here, because this is not + * a call into PostgreSQL; it's pure JNI to grab a static constant address. + */ + return (*env)->NewDirectByteBuffer(env, constants, sizeof constants); +} diff --git a/pljava-so/src/main/c/ModelUtils.c b/pljava-so/src/main/c/ModelUtils.c new file mode 100644 index 000000000..28bd05d46 --- /dev/null +++ b/pljava-so/src/main/c/ModelUtils.c @@ -0,0 +1,1003 @@ +/* + * Copyright (c) 2022-2023 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ + +#include +#include +#include +#if PG_VERSION_NUM >= 130000 +#include +#else +#include +#endif +#if PG_VERSION_NUM >= 120000 +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "pljava/Backend.h" +#include "pljava/Exception.h" +#include "pljava/PgObject.h" +#include "pljava/ModelUtils.h" +#include "pljava/VarlenaWrapper.h" + +#include "org_postgresql_pljava_internal_SPI.h" +#include "org_postgresql_pljava_internal_SPI_EarlyNatives.h" + +#include "org_postgresql_pljava_pg_CatalogObjectImpl_Addressed.h" +#include "org_postgresql_pljava_pg_CatalogObjectImpl_Factory.h" +#include "org_postgresql_pljava_pg_CharsetEncodingImpl_EarlyNatives.h" +#include "org_postgresql_pljava_pg_DatumUtils.h" +#include "org_postgresql_pljava_pg_MemoryContextImpl_EarlyNatives.h" +#include "org_postgresql_pljava_pg_ResourceOwnerImpl_EarlyNatives.h" +#include "org_postgresql_pljava_pg_TupleDescImpl.h" +#include "org_postgresql_pljava_pg_TupleTableSlotImpl.h" + +#if PG_VERSION_NUM < 120000 +#include /* for ObjectIdAttributeNumber */ +#define MakeSingleTupleTableSlot(tupdesc, slotops) \ + MakeSingleTupleTableSlot(tupdesc) +struct TupleTableSlotOps { }; +const TupleTableSlotOps TTSOpsHeapTuple = { }; +#define slot_getsomeattrs_int slot_getsomeattrs +#define ExecStoreHeapTuple(tuple, slot, shouldFree) \ + ExecStoreTuple((tuple), (slot), 0, (shouldFree)) +#endif + +#if PG_VERSION_NUM < 110000 +/* in older versions, the attribute area is allocated separately */ +#define TupleDescSize(src) sizeof(struct tupleDesc) +#endif + +/* + * A compilation unit collecting various native methods used in the pg model + * implementation classes. This is something of a break with past PL/Java + * practice of having a correspondingly-named C file for a Java class, made on + * the belief that there won't be that many new methods here, and they will make + * more sense collected together. + * + * Some of the native methods here may *not* include the elaborate fencing seen + * in other PL/Java native methods, if they involve trivially simple functions + * that do not require calling into PostgreSQL or other non-thread-safe code. + * This is, of course, a careful exception made to the general rule. The calling + * Java code is expected to have good reason to believe any state to be examined + * by these methods won't be shifting underneath them. + */ + +static jclass s_CatalogObjectImpl_Factory_class; +static jmethodID s_CatalogObjectImpl_Factory_invalidateRelation; +static jmethodID s_CatalogObjectImpl_Factory_invalidateType; + +static jclass s_MemoryContextImpl_class; +static jmethodID s_MemoryContextImpl_callback; +static void memoryContextCallback(void *arg); + +static jclass s_ResourceOwnerImpl_class; +static jmethodID s_ResourceOwnerImpl_callback; +static void resourceReleaseCB(ResourceReleasePhase phase, + bool isCommit, bool isTopLevel, void *arg); + +static jclass s_TupleDescImpl_class; +static jmethodID s_TupleDescImpl_fromByteBuffer; + +static jclass s_TupleTableSlotImpl_class; +static jmethodID s_TupleTableSlotImpl_newDeformed; + +static void relCacheCB(Datum arg, Oid relid); +static void sysCacheCB(Datum arg, int cacheid, uint32 hash); + +jobject pljava_TupleDescriptor_create(TupleDesc tupdesc, Oid reloid) +{ + jlong tupdesc_size = (jlong)TupleDescSize(tupdesc); + jobject td_b = JNI_newDirectByteBuffer(tupdesc, tupdesc_size); + + jobject result = JNI_callStaticObjectMethodLocked(s_TupleDescImpl_class, + s_TupleDescImpl_fromByteBuffer, + td_b, + (jint)tupdesc->tdtypeid, (jint)tupdesc->tdtypmod, + (jint)reloid, (jint)tupdesc->tdrefcount); + + JNI_deleteLocalRef(td_b); + return result; +} + +/* + * If NULL is passed for jtd, a Java TupleDescriptor will be created here from + * tupdesc. Otherwise, the passed jtd must be a JNI local reference to an + * existing Java TupleDescriptor corresponding to tupdesc, and on return, the + * JNI local reference will have been deleted. + */ +jobject pljava_TupleTableSlot_create( + TupleDesc tupdesc, jobject jtd, const TupleTableSlotOps *tts_ops, Oid reloid) +{ + int natts = tupdesc->natts; + TupleTableSlot *tts = MakeSingleTupleTableSlot(tupdesc, tts_ops); + jobject tts_b = JNI_newDirectByteBuffer(tts, (jlong)sizeof *tts); + jobject vals_b = JNI_newDirectByteBuffer(tts->tts_values, + (jlong)(natts * sizeof *tts->tts_values)); + jobject nuls_b = JNI_newDirectByteBuffer(tts->tts_isnull, (jlong)natts); + jobject jtts; + + if ( NULL == jtd ) + jtd = pljava_TupleDescriptor_create(tupdesc, reloid); + + jtts = JNI_callStaticObjectMethodLocked(s_TupleTableSlotImpl_class, + s_TupleTableSlotImpl_newDeformed, tts_b, jtd, vals_b, nuls_b); + + JNI_deleteLocalRef(nuls_b); + JNI_deleteLocalRef(vals_b); + JNI_deleteLocalRef(jtd); + JNI_deleteLocalRef(tts_b); + + return jtts; +} + +static void memoryContextCallback(void *arg) +{ + Ptr2Long p2l; + + p2l.longVal = 0L; + p2l.ptrVal = arg; + JNI_callStaticVoidMethodLocked(s_MemoryContextImpl_class, + s_MemoryContextImpl_callback, + p2l.longVal); +} + +static void relCacheCB(Datum arg, Oid relid) +{ + JNI_callStaticObjectMethodLocked(s_CatalogObjectImpl_Factory_class, + s_CatalogObjectImpl_Factory_invalidateRelation, (jint)relid); +} + +static void resourceReleaseCB(ResourceReleasePhase phase, + bool isCommit, bool isTopLevel, void *arg) +{ + Ptr2Long p2l; + + /* + * This static assertion does not need to be in every file + * that uses Ptr2Long, but it should be somewhere once, so here it is. + */ + StaticAssertStmt(sizeof p2l.ptrVal <= sizeof p2l.longVal, + "Pointer will not fit in long on this platform"); + + /* + * The way ResourceOwnerRelease is implemented, callbacks to loadable + * modules (like us!) happen /after/ all of the built-in releasey actions + * for a particular phase. So, by looking for RESOURCE_RELEASE_LOCKS here, + * we actually end up executing after all the built-in lock-related stuff + * has been released, but before any of the built-in stuff released in the + * RESOURCE_RELEASE_AFTER_LOCKS phase. Which, at least for the currently + * implemented DualState subclasses, is about the right time. + */ + if ( RESOURCE_RELEASE_LOCKS != phase ) + return; + + /* + * The void *arg is the NULL we supplied at registration time. The resource + * manager arranges for CurrentResourceOwner to be the one that is being + * released. + */ + p2l.longVal = 0L; + p2l.ptrVal = CurrentResourceOwner; + JNI_callStaticVoidMethodLocked(s_ResourceOwnerImpl_class, + s_ResourceOwnerImpl_callback, + p2l.longVal); + + if ( isTopLevel ) + Backend_warnJEP411(isCommit); +} + +static void sysCacheCB(Datum arg, int cacheid, uint32 hash) +{ + switch ( cacheid ) + { + case TYPEOID: + JNI_callStaticObjectMethodLocked(s_CatalogObjectImpl_Factory_class, + s_CatalogObjectImpl_Factory_invalidateType, (jint)hash); + break; + default: + break; + } +} + +void pljava_ResourceOwner_unregister(void) +{ + UnregisterResourceReleaseCallback(resourceReleaseCB, NULL); +} + +void pljava_ModelUtils_initialize(void) +{ + jclass cls; + + JNINativeMethod catalogObjectAddressedMethods[] = + { + { + "_lookupRowtypeTupdesc", + "(II)Ljava/nio/ByteBuffer;", + Java_org_postgresql_pljava_pg_CatalogObjectImpl_00024Addressed__1lookupRowtypeTupdesc + }, + { + "_searchSysCacheCopy1", + "(II)Ljava/nio/ByteBuffer;", + Java_org_postgresql_pljava_pg_CatalogObjectImpl_00024Addressed__1searchSysCacheCopy1 + }, + { + "_searchSysCacheCopy2", + "(III)Ljava/nio/ByteBuffer;", + Java_org_postgresql_pljava_pg_CatalogObjectImpl_00024Addressed__1searchSysCacheCopy2 + }, + { + "_sysTableGetByOid", + "(IIIIJ)Ljava/nio/ByteBuffer;", + Java_org_postgresql_pljava_pg_CatalogObjectImpl_00024Addressed__1sysTableGetByOid + }, + { + "_tupDescBootstrap", + "()Ljava/nio/ByteBuffer;", + Java_org_postgresql_pljava_pg_CatalogObjectImpl_00024Addressed__1tupDescBootstrap + }, + { 0, 0, 0 } + }; + + JNINativeMethod catalogObjectFactoryMethods[] = + { + { + "_currentDatabase", + "()I", + Java_org_postgresql_pljava_pg_CatalogObjectImpl_00024Factory__1currentDatabase + }, + { 0, 0, 0 } + }; + + JNINativeMethod charsetMethods[] = + { + { + "_serverEncoding", + "()I", + Java_org_postgresql_pljava_pg_CharsetEncodingImpl_00024EarlyNatives__1serverEncoding + }, + { + "_clientEncoding", + "()I", + Java_org_postgresql_pljava_pg_CharsetEncodingImpl_00024EarlyNatives__1clientEncoding + }, + { + "_nameToOrdinal", + "(Ljava/nio/ByteBuffer;)I", + Java_org_postgresql_pljava_pg_CharsetEncodingImpl_00024EarlyNatives__1nameToOrdinal + }, + { + "_ordinalToName", + "(I)Ljava/nio/ByteBuffer;", + Java_org_postgresql_pljava_pg_CharsetEncodingImpl_00024EarlyNatives__1ordinalToName + }, + { + "_ordinalToIcuName", + "(I)Ljava/nio/ByteBuffer;", + Java_org_postgresql_pljava_pg_CharsetEncodingImpl_00024EarlyNatives__1ordinalToIcuName + }, + { 0, 0, 0 } + }; + + JNINativeMethod datumMethods[] = + { + { + "_addressOf", + "(Ljava/nio/ByteBuffer;)J", + Java_org_postgresql_pljava_pg_DatumUtils__1addressOf + }, + { + "_map", + "(JI)Ljava/nio/ByteBuffer;", + Java_org_postgresql_pljava_pg_DatumUtils__1map + }, + { + "_mapCString", + "(J)Ljava/nio/ByteBuffer;", + Java_org_postgresql_pljava_pg_DatumUtils__1mapCString + }, + { + "_mapVarlena", + "(Ljava/nio/ByteBuffer;JJJ)Lorg/postgresql/pljava/adt/spi/Datum$Input;", + Java_org_postgresql_pljava_pg_DatumUtils__1mapVarlena + }, + { 0, 0, 0 } + }; + + JNINativeMethod memoryContextMethods[] = + { + { + "_registerCallback", + "(J)V", + Java_org_postgresql_pljava_pg_MemoryContextImpl_00024EarlyNatives__1registerCallback + }, + { + "_window", + "(Ljava/lang/Class;)[Ljava/nio/ByteBuffer;", + Java_org_postgresql_pljava_pg_MemoryContextImpl_00024EarlyNatives__1window + }, + { 0, 0, 0 } + }; + + JNINativeMethod resourceOwnerMethods[] = + { + { + "_window", + "(Ljava/lang/Class;)[Ljava/nio/ByteBuffer;", + Java_org_postgresql_pljava_pg_ResourceOwnerImpl_00024EarlyNatives__1window + }, + { 0, 0, 0 } + }; + + JNINativeMethod spiMethods[] = + { + { + "_window", + "(Ljava/lang/Class;)[Ljava/nio/ByteBuffer;", + Java_org_postgresql_pljava_internal_SPI_00024EarlyNatives__1window + }, + { 0, 0, 0 } + }; + + JNINativeMethod tdiMethods[] = + { + { + "_assign_record_type_typmod", + "(Ljava/nio/ByteBuffer;)I", + Java_org_postgresql_pljava_pg_TupleDescImpl__1assign_1record_1type_1typmod + }, + { 0, 0, 0 } + }; + + JNINativeMethod ttsiMethods[] = + { + { + "_getsomeattrs", + "(Ljava/nio/ByteBuffer;I)V", + Java_org_postgresql_pljava_pg_TupleTableSlotImpl__1getsomeattrs + }, + { + "_store_heaptuple", + "(Ljava/nio/ByteBuffer;JZ)V", + Java_org_postgresql_pljava_pg_TupleTableSlotImpl__1store_1heaptuple + }, + { 0, 0, 0 } + }; + + cls = PgObject_getJavaClass("org/postgresql/pljava/pg/CatalogObjectImpl$Addressed"); + PgObject_registerNatives2(cls, catalogObjectAddressedMethods); + JNI_deleteLocalRef(cls); + + cls = PgObject_getJavaClass("org/postgresql/pljava/pg/CatalogObjectImpl$Factory"); + s_CatalogObjectImpl_Factory_class = JNI_newGlobalRef(cls); + PgObject_registerNatives2(cls, catalogObjectFactoryMethods); + JNI_deleteLocalRef(cls); + s_CatalogObjectImpl_Factory_invalidateRelation = + PgObject_getStaticJavaMethod( + s_CatalogObjectImpl_Factory_class, "invalidateRelation", "(I)V"); + s_CatalogObjectImpl_Factory_invalidateType = + PgObject_getStaticJavaMethod( + s_CatalogObjectImpl_Factory_class, "invalidateType", "(I)V"); + + cls = PgObject_getJavaClass("org/postgresql/pljava/pg/CharsetEncodingImpl$EarlyNatives"); + PgObject_registerNatives2(cls, charsetMethods); + JNI_deleteLocalRef(cls); + + cls = PgObject_getJavaClass("org/postgresql/pljava/pg/DatumUtils"); + PgObject_registerNatives2(cls, datumMethods); + JNI_deleteLocalRef(cls); + + cls = PgObject_getJavaClass("org/postgresql/pljava/pg/MemoryContextImpl$EarlyNatives"); + PgObject_registerNatives2(cls, memoryContextMethods); + JNI_deleteLocalRef(cls); + + cls = PgObject_getJavaClass("org/postgresql/pljava/pg/MemoryContextImpl"); + s_MemoryContextImpl_class = JNI_newGlobalRef(cls); + JNI_deleteLocalRef(cls); + s_MemoryContextImpl_callback = PgObject_getStaticJavaMethod( + s_MemoryContextImpl_class, "callback", "(J)V"); + + cls = PgObject_getJavaClass("org/postgresql/pljava/pg/ResourceOwnerImpl$EarlyNatives"); + PgObject_registerNatives2(cls, resourceOwnerMethods); + JNI_deleteLocalRef(cls); + + cls = PgObject_getJavaClass("org/postgresql/pljava/pg/ResourceOwnerImpl"); + s_ResourceOwnerImpl_class = JNI_newGlobalRef(cls); + JNI_deleteLocalRef(cls); + s_ResourceOwnerImpl_callback = PgObject_getStaticJavaMethod( + s_ResourceOwnerImpl_class, "callback", "(J)V"); + + cls = PgObject_getJavaClass("org/postgresql/pljava/internal/SPI$EarlyNatives"); + PgObject_registerNatives2(cls, spiMethods); + JNI_deleteLocalRef(cls); + + cls = PgObject_getJavaClass("org/postgresql/pljava/pg/TupleDescImpl"); + s_TupleDescImpl_class = JNI_newGlobalRef(cls); + PgObject_registerNatives2(cls, tdiMethods); + JNI_deleteLocalRef(cls); + + s_TupleDescImpl_fromByteBuffer = PgObject_getStaticJavaMethod( + s_TupleDescImpl_class, + "fromByteBuffer", + "(Ljava/nio/ByteBuffer;IIII)" + "Lorg/postgresql/pljava/model/TupleDescriptor;"); + + cls = PgObject_getJavaClass("org/postgresql/pljava/pg/TupleTableSlotImpl"); + s_TupleTableSlotImpl_class = JNI_newGlobalRef(cls); + PgObject_registerNatives2(cls, ttsiMethods); + JNI_deleteLocalRef(cls); + + s_TupleTableSlotImpl_newDeformed = PgObject_getStaticJavaMethod( + s_TupleTableSlotImpl_class, + "newDeformed", + "(Ljava/nio/ByteBuffer;Lorg/postgresql/pljava/model/TupleDescriptor;" + "Ljava/nio/ByteBuffer;Ljava/nio/ByteBuffer;)" + "Lorg/postgresql/pljava/pg/TupleTableSlotImpl$Deformed;"); + + RegisterResourceReleaseCallback(resourceReleaseCB, NULL); + + CacheRegisterRelcacheCallback(relCacheCB, 0); + + CacheRegisterSyscacheCallback(TYPEOID, sysCacheCB, 0); +} + +/* + * Class: org_postgresql_pljava_pg_CatalogObjectImpl_Addressed + * Method: _lookupRowtypeTupdesc + * Signature: (II)Ljava/nio/ByteBuffer; + */ +JNIEXPORT jobject JNICALL +Java_org_postgresql_pljava_pg_CatalogObjectImpl_00024Addressed__1lookupRowtypeTupdesc(JNIEnv* env, jobject _cls, jint typeid, jint typmod) +{ + TupleDesc td; + jlong length; + jobject result = NULL; + BEGIN_NATIVE_AND_TRY + td = lookup_rowtype_tupdesc_noerror(typeid, typmod, true); + if ( NULL != td ) + { + /* + * Per contract, we return the tuple descriptor with its reference count + * incremented, but not registered with a resource owner for descriptor + * leak warnings. l_r_t_n() will have incremented already, but also + * registered for warnings. The proper dance is a second pure increment + * here, followed by a DecrTupleDescRefCount to undo what l_r_t_n() did. + * And none of that, of course, if the descriptor is not refcounted. + */ + if ( td->tdrefcount >= 0 ) + { + ++ td->tdrefcount; + DecrTupleDescRefCount(td); + } + length = (jlong)TupleDescSize(td); + result = JNI_newDirectByteBuffer((void *)td, length); + } + END_NATIVE_AND_CATCH("_lookupRowtypeTupdesc") + return result; +} + +/* + * Class: org_postgresql_pljava_pg_CatalogObjectImpl_Addressed + * Method: _searchSysCacheCopy1 + * Signature: (II)Ljava/nio/ByteBuffer; + */ +JNIEXPORT jobject JNICALL +Java_org_postgresql_pljava_pg_CatalogObjectImpl_00024Addressed__1searchSysCacheCopy1(JNIEnv *env, jclass cls, jint cacheId, jint key1) +{ + jobject result = NULL; + HeapTuple ht; + BEGIN_NATIVE_AND_TRY + ht = SearchSysCacheCopy1(cacheId, Int32GetDatum(key1)); + if ( HeapTupleIsValid(ht) ) + { + result = JNI_newDirectByteBuffer(ht, HEAPTUPLESIZE + ht->t_len); + } + END_NATIVE_AND_CATCH("_searchSysCacheCopy1") + return result; +} + +/* + * Class: org_postgresql_pljava_pg_CatalogObjectImpl_Addressed + * Method: _searchSysCacheCopy2 + * Signature: (III)Ljava/nio/ByteBuffer; + */ +JNIEXPORT jobject JNICALL +Java_org_postgresql_pljava_pg_CatalogObjectImpl_00024Addressed__1searchSysCacheCopy2(JNIEnv *env, jclass cls, jint cacheId, jint key1, jint key2) +{ + jobject result = NULL; + HeapTuple ht; + BEGIN_NATIVE_AND_TRY + ht = SearchSysCacheCopy2(cacheId, Int32GetDatum(key1), Int32GetDatum(key2)); + if ( HeapTupleIsValid(ht) ) + { + result = JNI_newDirectByteBuffer(ht, HEAPTUPLESIZE + ht->t_len); + } + END_NATIVE_AND_CATCH("_searchSysCacheCopy2") + return result; +} + +/* + * Class: org_postgresql_pljava_pg_CatalogObjectImpl_Addressed + * Method: _sysTableGetByOid + * Signature: (IIIIJ)Ljava/nio/ByteBuffer; + */ +JNIEXPORT jobject JNICALL +Java_org_postgresql_pljava_pg_CatalogObjectImpl_00024Addressed__1sysTableGetByOid(JNIEnv *env, jclass cls, jint relOid, jint objOid, jint oidCol, jint indexOid, jlong tupleDesc) +{ + jobject result = NULL; + HeapTuple ht; + Relation rel; + SysScanDesc scandesc; + ScanKeyData entry[1]; + Ptr2Long p2l; + + p2l.longVal = tupleDesc; + + BEGIN_NATIVE_AND_TRY + rel = relation_open((Oid)relOid, AccessShareLock); + + ScanKeyInit(&entry[0], +#if PG_VERSION_NUM >= 120000 + (AttrNumber)oidCol, +#else + ObjectIdAttributeNumber, +#endif + BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum((Oid)objOid)); + + scandesc = systable_beginscan( + rel, (Oid)indexOid, InvalidOid != indexOid, NULL, 1, entry); + + ht = systable_getnext(scandesc); + + /* + * As in the extension.c code from which this is brazenly copied, we assume + * there can be at most one matching tuple. (Oid ought to be the primary key + * of a catalog table we care about, so it's not a daring assumption.) + */ + if ( HeapTupleIsValid(ht) ) + { + /* + * We wish to return a tuple satisfying the same conditions as if it had + * been obtained from the syscache, including that it has no external + * TOAST pointers. (Inline-compressed values, it could still have.) + */ + if ( HeapTupleHasExternal(ht) ) + ht = toast_flatten_tuple(ht, p2l.ptrVal); + else + ht = heap_copytuple(ht); + result = JNI_newDirectByteBuffer(ht, HEAPTUPLESIZE + ht->t_len); + } + + systable_endscan(scandesc); + relation_close(rel, AccessShareLock); + END_NATIVE_AND_CATCH("_sysTableGetByOid") + return result; +} + +/* + * Class: org_postgresql_pljava_pg_CatalogObjectImpl_Addressed + * Method: _tupDescBootstrap + * Signature: ()Ljava/nio/ByteBuffer; + */ +JNIEXPORT jobject JNICALL +Java_org_postgresql_pljava_pg_CatalogObjectImpl_00024Addressed__1tupDescBootstrap(JNIEnv* env, jobject _cls) +{ + Relation rel; + TupleDesc td; + jlong length; + jobject result = NULL; + BEGIN_NATIVE_AND_TRY + rel = relation_open(RelationRelationId, AccessShareLock); + td = RelationGetDescr(rel); + /* + * Per contract, we return the tuple descriptor with its reference count + * incremented, without registering it with a resource owner for descriptor + * leak warnings. + */ + ++ td->tdrefcount; + /* + * Can close the relation now that the td reference count is bumped. + */ + relation_close(rel, AccessShareLock); + length = (jlong)TupleDescSize(td); + result = JNI_newDirectByteBuffer((void *)td, length); + END_NATIVE_AND_CATCH("_tupDescBootstrap") + return result; +} + +/* + * Class: org_postgresql_pljava_pg_CatalogObjectImpl_Factory + * Method: _currentDatabase + * Signature: ()I + */ +JNIEXPORT jint JNICALL +Java_org_postgresql_pljava_pg_CatalogObjectImpl_00024Factory__1currentDatabase(JNIEnv *env, jclass cls) +{ + return MyDatabaseId; +} + +/* + * Class: org_postgresql_pljava_pg_CharsetEncodingImpl_EarlyNatives + * Method: _serverEncoding + * Signature: ()I + */ +JNIEXPORT jint JNICALL +Java_org_postgresql_pljava_pg_CharsetEncodingImpl_00024EarlyNatives__1serverEncoding(JNIEnv *env, jclass cls) +{ + int result = -1; + BEGIN_NATIVE_AND_TRY + result = GetDatabaseEncoding(); + END_NATIVE_AND_CATCH("_serverEncoding") + return result; +} + +/* + * Class: org_postgresql_pljava_pg_CharsetEncodingImpl_EarlyNatives + * Method: _clientEncoding + * Signature: ()I + */ +JNIEXPORT jint JNICALL +Java_org_postgresql_pljava_pg_CharsetEncodingImpl_00024EarlyNatives__1clientEncoding(JNIEnv *env, jclass cls) +{ + int result = -1; + BEGIN_NATIVE_AND_TRY + result = pg_get_client_encoding(); + END_NATIVE_AND_CATCH("_clientEncoding") + return result; +} + +/* + * Class: org_postgresql_pljava_pg_CharsetEncodingImpl_EarlyNatives + * Method: _nameToOrdinal + * Signature: (Ljava/nio/ByteBuffer;)I + */ +JNIEXPORT jint JNICALL +Java_org_postgresql_pljava_pg_CharsetEncodingImpl_00024EarlyNatives__1nameToOrdinal(JNIEnv *env, jclass cls, jobject bb) +{ + int result = -1; + char const *name = (*env)->GetDirectBufferAddress(env, bb); + if ( NULL == name ) + return result; + BEGIN_NATIVE_AND_TRY + result = pg_char_to_encoding(name); + END_NATIVE_AND_CATCH("_nameToOrdinal") + return result; +} + +/* + * Class: org_postgresql_pljava_pg_CharsetEncodingImpl_EarlyNatives + * Method: _ordinalToName + * Signature: (I)Ljava/nio/ByteBuffer; + */ +JNIEXPORT jobject JNICALL +Java_org_postgresql_pljava_pg_CharsetEncodingImpl_00024EarlyNatives__1ordinalToName(JNIEnv *env, jclass cls, jint ordinal) +{ + jobject result = NULL; + char const *name; + BEGIN_NATIVE_AND_TRY + name = pg_encoding_to_char(ordinal); + if ( '\0' != *name ) + result = JNI_newDirectByteBuffer((void *)name, (jint)strlen(name)); + END_NATIVE_AND_CATCH("_ordinalToName") + return result; +} + +/* + * Class: org_postgresql_pljava_pg_CharsetEncodingImpl_EarlyNatives + * Method: _ordinalToIcuName + * Signature: (I)Ljava/nio/ByteBuffer; + */ +JNIEXPORT jobject JNICALL +Java_org_postgresql_pljava_pg_CharsetEncodingImpl_00024EarlyNatives__1ordinalToIcuName(JNIEnv *env, jclass cls, jint ordinal) +{ + jobject result = NULL; + char const *name; + BEGIN_NATIVE_AND_TRY + name = get_encoding_name_for_icu(ordinal); + if ( NULL != name ) + result = JNI_newDirectByteBuffer((void *)name, (jint)strlen(name)); + END_NATIVE_AND_CATCH("_ordinalToIcuName") + return result; +} + +/* + * Class: org_postgresql_pljava_pg_DatumUtils + * Method: _addressOf + * Signature: (Ljava/nio/ByteBuffer;)J + */ +JNIEXPORT jlong JNICALL +Java_org_postgresql_pljava_pg_DatumUtils__1addressOf(JNIEnv* env, jobject _cls, jobject bb) +{ + Ptr2Long p2l; + p2l.longVal = 0; + p2l.ptrVal = (*env)->GetDirectBufferAddress(env, bb); + return p2l.longVal; +} + +/* + * Class: org_postgresql_pljava_pg_DatumUtils + * Method: _map + * Signature: (JI)Ljava/nio/ByteBuffer; + */ +JNIEXPORT jobject JNICALL +Java_org_postgresql_pljava_pg_DatumUtils__1map(JNIEnv* env, jobject _cls, jlong nativeAddress, jint length) +{ + Ptr2Long p2l; + p2l.longVal = nativeAddress; + return (*env)->NewDirectByteBuffer(env, p2l.ptrVal, length); +} + +/* + * Class: org_postgresql_pljava_pg_DatumUtils + * Method: _mapCString + * Signature: (J)Ljava/nio/ByteBuffer; + */ +JNIEXPORT jobject JNICALL +Java_org_postgresql_pljava_pg_DatumUtils__1mapCString(JNIEnv* env, jobject _cls, jlong nativeAddress) +{ + jlong length; + void *base; + Ptr2Long p2l; + + p2l.longVal = nativeAddress; + base = p2l.ptrVal; + length = (jlong)strlen(base); + return (*env)->NewDirectByteBuffer(env, base, length); +} + +/* + * Class: org_postgresql_pljava_pg_DatumUtils + * Method: _mapVarlena + * Signature: (Ljava/nio/ByteBuffer;JJJ)Lorg/postgresql/pljava/adt/spi/Datum$Input; + */ +JNIEXPORT jobject JNICALL +Java_org_postgresql_pljava_pg_DatumUtils__1mapVarlena(JNIEnv* env, jobject _cls, jobject bb, jlong offset, jlong resowner, jlong memcontext) +{ + Ptr2Long p2lvl; + Ptr2Long p2lro; + Ptr2Long p2lmc; + jobject result = NULL; + + p2lvl.longVal = 0; + if ( NULL != bb ) + { + p2lvl.ptrVal = (*env)->GetDirectBufferAddress(env, bb); + if ( NULL == p2lvl.ptrVal ) + return NULL; + } + p2lvl.longVal += offset; + + p2lro.longVal = resowner; + p2lmc.longVal = memcontext; + + BEGIN_NATIVE_AND_TRY + result = pljava_VarlenaWrapper_Input(PointerGetDatum(p2lvl.ptrVal), + (MemoryContext)p2lmc.ptrVal, (ResourceOwner)p2lro.ptrVal); + END_NATIVE_AND_CATCH("_mapVarlena") + return result; +} + + +/* + * Class: org_postgresql_pljava_pg_MemoryContext_EarlyNatives + * Method: _registerCallback + * Signature: (J)V; + */ +JNIEXPORT void JNICALL +Java_org_postgresql_pljava_pg_MemoryContextImpl_00024EarlyNatives__1registerCallback(JNIEnv* env, jobject _cls, jlong nativeAddress) +{ + Ptr2Long p2l; + MemoryContext cxt; + MemoryContextCallback *cb; + + p2l.longVal = nativeAddress; + cxt = p2l.ptrVal; + BEGIN_NATIVE_AND_TRY + /* + * Optimization? Use MemoryContextAllocExtended with NO_OOM, and do without + * the AND_TRY/AND_CATCH to catch a PostgreSQL ereport. + */ + cb = MemoryContextAlloc(cxt, sizeof *cb); + cb->func = memoryContextCallback; + cb->arg = cxt; + MemoryContextRegisterResetCallback(cxt, cb); + END_NATIVE_AND_CATCH("_registerCallback") +} + +/* + * Class: org_postgresql_pljava_pg_MemoryContext_EarlyNatives + * Method: _window + * Signature: ()[Ljava/nio/ByteBuffer; + * + * Return an array of ByteBuffers constructed to window the PostgreSQL globals + * holding the well-known memory contexts. The indices into the array are + * assigned arbitrarily in the API class CatalogObject.Factory and inherited + * from it in CatalogObjectImpl.Factory, from which the native .h makes them + * visible here. A peculiar consequence is that the code in MemoryContextImpl + * can be ignorant of them, and just fetch the array element at the index passed + * from the API class. + */ +JNIEXPORT jobject JNICALL +Java_org_postgresql_pljava_pg_MemoryContextImpl_00024EarlyNatives__1window(JNIEnv* env, jobject _cls, jclass component) +{ + jobject r = (*env)->NewObjectArray(env, (jsize)10, component, NULL); + if ( NULL == r ) + return NULL; + +#define POPULATE(tag) do {\ + jobject b = (*env)->NewDirectByteBuffer(env, \ + &tag##Context, sizeof tag##Context);\ + if ( NULL == b )\ + return NULL;\ + (*env)->SetObjectArrayElement(env, r, \ + (jsize)org_postgresql_pljava_pg_CatalogObjectImpl_Factory_MCX_##tag, \ + b);\ +} while (0) + + POPULATE(CurrentMemory); + POPULATE(TopMemory); + POPULATE(Error); + POPULATE(Postmaster); + POPULATE(CacheMemory); + POPULATE(Message); + POPULATE(TopTransaction); + POPULATE(CurTransaction); + POPULATE(Portal); + POPULATE(JavaMemory); + +#undef POPULATE + + return r; +} + +/* + * Class: org_postgresql_pljava_pg_ResourceOwnerImpl_EarlyNatives + * Method: _window + * Signature: ()[Ljava/nio/ByteBuffer; + * + * Return an array of ByteBuffers constructed to window the PostgreSQL globals + * holding the well-known resource owners. The indices into the array are + * assigned arbitrarily in the API class CatalogObject.Factory and inherited + * from it in CatalogObjectImpl.Factory, from which the native .h makes them + * visible here. A peculiar consequence is that the code in ResourceOwnerImpl + * can be ignorant of them, and just fetch the array element at the index passed + * from the API class. + */ +JNIEXPORT jobject JNICALL +Java_org_postgresql_pljava_pg_ResourceOwnerImpl_00024EarlyNatives__1window(JNIEnv* env, jobject _cls, jclass component) +{ + jobject r = (*env)->NewObjectArray(env, (jsize)4, component, NULL); + if ( NULL == r ) + return NULL; + +#define POPULATE(tag) do {\ + jobject b = (*env)->NewDirectByteBuffer(env, \ + &tag##ResourceOwner, sizeof tag##ResourceOwner);\ + if ( NULL == b )\ + return NULL;\ + (*env)->SetObjectArrayElement(env, r, \ + (jsize)org_postgresql_pljava_pg_CatalogObjectImpl_Factory_RSO_##tag, \ + b);\ +} while (0) + + POPULATE(Current); + POPULATE(CurTransaction); + POPULATE(TopTransaction); +#if PG_VERSION_NO >= 120000 + POPULATE(AuxProcess); +#endif + +#undef POPULATE + + return r; +} + +/* + * Class: org_postgresql_pljava_internal_SPI_EarlyNatives + * Method: _window + * Signature: ()[Ljava/nio/ByteBuffer; + * + * Return an array of ByteBuffers constructed to window the PostgreSQL globals + * SPI_result, SPI_processed, and SPI_tuptable. The indices into the array are + * assigned arbitrarily in the internal class SPI, from which the native .h + * makes them visible here. + */ +JNIEXPORT jobject JNICALL +Java_org_postgresql_pljava_internal_SPI_00024EarlyNatives__1window(JNIEnv* env, jobject _cls, jclass component) +{ + jobject r = (*env)->NewObjectArray(env, (jsize)3, component, NULL); + if ( NULL == r ) + return NULL; + +#define POPULATE(tag) do {\ + jobject b = (*env)->NewDirectByteBuffer(env, &tag, sizeof tag);\ + if ( NULL == b )\ + return NULL;\ + (*env)->SetObjectArrayElement(env, r, \ + (jsize)org_postgresql_pljava_internal_SPI_##tag, \ + b);\ +} while (0) + + POPULATE(SPI_result); + POPULATE(SPI_processed); + POPULATE(SPI_tuptable); + +#undef POPULATE + + return r; +} + +/* + * Class: org_postgresql_pljava_pg_TupleDescImpl + * Method: _assign_record_type_typmod + * Signature: (Ljava/nio/ByteBuffer)I + */ +JNIEXPORT jint JNICALL +Java_org_postgresql_pljava_pg_TupleDescImpl__1assign_1record_1type_1typmod(JNIEnv* env, jobject _cls, jobject td_b) +{ + TupleDesc td = (*env)->GetDirectBufferAddress(env, td_b); + if ( NULL == td ) + return -1; + + BEGIN_NATIVE_AND_TRY + assign_record_type_typmod(td); + END_NATIVE_AND_CATCH("_assign_record_type_typmod") + return td->tdtypmod; +} + +/* + * Class: org_postgresql_pljava_pg_TupleTableSlotImpl + * Method: _getsomeattrs + * Signature: (Ljava/nio/ByteBuffer;I)V + */ +JNIEXPORT void JNICALL +Java_org_postgresql_pljava_pg_TupleTableSlotImpl__1getsomeattrs(JNIEnv* env, jobject _cls, jobject tts_b, jint attnum) +{ + TupleTableSlot *tts = (*env)->GetDirectBufferAddress(env, tts_b); + if ( NULL == tts ) + return; + + BEGIN_NATIVE_AND_TRY + slot_getsomeattrs_int(tts, attnum); + END_NATIVE_AND_CATCH("_getsomeattrs") +} + +/* + * Class: org_postgresql_pljava_pg_TupleTableSlotImpl + * Method: _store_heaptuple + * Signature: (Ljava/nio/ByteBuffer;JZ)V + */ +JNIEXPORT void JNICALL +Java_org_postgresql_pljava_pg_TupleTableSlotImpl__1store_1heaptuple(JNIEnv* env, jobject _cls, jobject tts_b, jlong ht, jboolean shouldFree) +{ + Ptr2Long p2l; + HeapTuple htp; + TupleTableSlot *tts = (*env)->GetDirectBufferAddress(env, tts_b); + if ( NULL == tts ) + return; + + BEGIN_NATIVE_AND_TRY + p2l.longVal = ht; + htp = p2l.ptrVal; + ExecStoreHeapTuple(htp, tts, JNI_TRUE == shouldFree); + END_NATIVE_AND_CATCH("_store_heaptuple") +} diff --git a/pljava-so/src/main/c/SPI.c b/pljava-so/src/main/c/SPI.c index ea104bd9f..79dfb3b13 100644 --- a/pljava-so/src/main/c/SPI.c +++ b/pljava-so/src/main/c/SPI.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2019 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2023 Tada AB and other contributors, as listed below. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the The BSD 3-Clause License @@ -23,10 +23,17 @@ #include #endif +/* + * Yes, this macro works because the class's simple name happens to be SPI + * and it defines constants named without the SPI_ prefix the PG source uses. + */ #define CONFIRMCONST(c) \ StaticAssertStmt((c) == (org_postgresql_pljava_internal_##c), \ "Java/C value mismatch for " #c) +static jclass s_TupleList_SPI_class; +static jmethodID s_TupleList_SPI_init; + extern void SPI_initialize(void); void SPI_initialize(void) { @@ -37,29 +44,34 @@ void SPI_initialize(void) Java_org_postgresql_pljava_internal_SPI__1exec }, { - "_getProcessed", - "()J", - Java_org_postgresql_pljava_internal_SPI__1getProcessed - }, - { - "_getResult", - "()I", - Java_org_postgresql_pljava_internal_SPI__1getResult - }, - { "_getTupTable", "(Lorg/postgresql/pljava/internal/TupleDesc;)Lorg/postgresql/pljava/internal/TupleTable;", Java_org_postgresql_pljava_internal_SPI__1getTupTable }, { + "_mapTupTable", + "(Lorg/postgresql/pljava/pg/TupleTableSlotImpl;JI)Lorg/postgresql/pljava/pg/TupleList;", + Java_org_postgresql_pljava_internal_SPI__1mapTupTable + }, + { "_freeTupTable", "()V", Java_org_postgresql_pljava_internal_SPI__1freeTupTable }, { 0, 0, 0 }}; + /* + * See also ModelUtils.c for newer methods associated with SPI.EarlyNatives. + */ PgObject_registerNatives("org/postgresql/pljava/internal/SPI", methods); + s_TupleList_SPI_class = JNI_newGlobalRef( + PgObject_getJavaClass("org/postgresql/pljava/pg/TupleList$SPI")); + s_TupleList_SPI_init = PgObject_getJavaMethod(s_TupleList_SPI_class, + "", + "(Lorg/postgresql/pljava/pg/TupleTableSlotImpl;JLjava/nio/ByteBuffer;)V" + ); + /* * Statically assert that the Java code has the right values for these. * I would rather have this at the top, but these count as statements and @@ -106,6 +118,7 @@ void SPI_initialize(void) /**************************************** * JNI methods + * See also ModelUtils.c for newer methods associated with SPI.EarlyNatives. ****************************************/ /* * Class: org_postgresql_pljava_internal_SPI @@ -143,28 +156,6 @@ Java_org_postgresql_pljava_internal_SPI__1exec(JNIEnv* env, jclass cls, jstring return result; } -/* - * Class: org_postgresql_pljava_internal_SPI - * Method: _getProcessed - * Signature: ()J - */ -JNIEXPORT jlong JNICALL -Java_org_postgresql_pljava_internal_SPI__1getProcessed(JNIEnv* env, jclass cls) -{ - return (jlong)SPI_processed; -} - -/* - * Class: org_postgresql_pljava_internal_SPI - * Method: _getResult - * Signature: ()I - */ -JNIEXPORT jint JNICALL -Java_org_postgresql_pljava_internal_SPI__1getResult(JNIEnv* env, jclass cls) -{ - return (jint)SPI_result; -} - /* * Class: org_postgresql_pljava_internal_SPI * Method: _getTupTable @@ -183,6 +174,33 @@ Java_org_postgresql_pljava_internal_SPI__1getTupTable(JNIEnv* env, jclass cls, j return tupleTable; } +/* + * Class: org_postgresql_pljava_internal_SPI + * Method: _mapTupTable + * Signature: (Lorg/postgresql/pljava/pg/TupleTableSlotImpl;JI)Lorg/postgresql/pljava/pg/TupleList; + */ +JNIEXPORT jobject JNICALL +Java_org_postgresql_pljava_internal_SPI__1mapTupTable(JNIEnv* env, jclass cls, jobject ttsi, jlong p, jint sizeToMap) +{ + jobject tupleList = NULL; + Ptr2Long p2l; + SPITupleTable *tuptbl; + jobject bb; + if ( p != 0 ) + { + BEGIN_NATIVE_AND_TRY + p2l.longVal = p; + tuptbl = (SPITupleTable *)p2l.ptrVal; + bb = JNI_newDirectByteBuffer(tuptbl->vals, sizeToMap); + tupleList = JNI_newObjectLocked( + s_TupleList_SPI_class, s_TupleList_SPI_init, ttsi, p, bb); + END_NATIVE_AND_CATCH("_mapTupleTable") + } + if ( 0 != tupleList && SPI_tuptable == tuptbl ) + SPI_tuptable = NULL; /* protect from legacy _freetuptable below */ + return tupleList; +} + /* * Class: org_postgresql_pljava_internal_SPI * Method: _freeTupTable diff --git a/pljava-so/src/main/c/SQLInputFromTuple.c b/pljava-so/src/main/c/SQLInputFromTuple.c index 72a1bf822..f98919f95 100644 --- a/pljava-so/src/main/c/SQLInputFromTuple.c +++ b/pljava-so/src/main/c/SQLInputFromTuple.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2019 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2022 Tada AB and other contributors, as listed below. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the The BSD 3-Clause License @@ -25,19 +25,16 @@ static jmethodID s_SQLInputFromTuple_init; jobject pljava_SQLInputFromTuple_create(HeapTupleHeader hth) { Ptr2Long p2lht; - Ptr2Long p2lro; jobject result; jobject jtd = pljava_SingleRowReader_getTupleDesc(hth); p2lht.longVal = 0L; - p2lro.longVal = 0L; p2lht.ptrVal = hth; - p2lro.ptrVal = currentInvocation; result = JNI_newObjectLocked(s_SQLInputFromTuple_class, s_SQLInputFromTuple_init, - pljava_DualState_key(), p2lro.longVal, p2lht.longVal, jtd); + p2lht.longVal, jtd); JNI_deleteLocalRef(jtd); return result; @@ -50,7 +47,7 @@ void pljava_SQLInputFromTuple_initialize(void) jclass cls = PgObject_getJavaClass("org/postgresql/pljava/jdbc/SQLInputFromTuple"); s_SQLInputFromTuple_init = PgObject_getJavaMethod(cls, "", - "(Lorg/postgresql/pljava/internal/DualState$Key;JJLorg/postgresql/pljava/internal/TupleDesc;)V"); + "(JLorg/postgresql/pljava/internal/TupleDesc;)V"); s_SQLInputFromTuple_class = JNI_newGlobalRef(cls); JNI_deleteLocalRef(cls); } diff --git a/pljava-so/src/main/c/VarlenaWrapper.c b/pljava-so/src/main/c/VarlenaWrapper.c index 5805ace13..ab19e46c3 100644 --- a/pljava-so/src/main/c/VarlenaWrapper.c +++ b/pljava-so/src/main/c/VarlenaWrapper.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2021 Tada AB and other contributors, as listed below. + * Copyright (c) 2018-2023 Tada AB and other contributors, as listed below. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the The BSD 3-Clause License @@ -79,8 +79,11 @@ do { \ #define INITIALSIZE 1024 +static jclass s_DatumImpl_class; + +static jmethodID s_DatumImpl_adopt; + static jclass s_VarlenaWrapper_class; -static jmethodID s_VarlenaWrapper_adopt; static jclass s_VarlenaWrapper_Input_class; static jclass s_VarlenaWrapper_Output_class; @@ -258,7 +261,7 @@ jobject pljava_VarlenaWrapper_Input( p2ldatum.ptrVal = vl; vr = JNI_newObjectLocked(s_VarlenaWrapper_Input_class, - s_VarlenaWrapper_Input_init, pljava_DualState_key(), + s_VarlenaWrapper_Input_init, p2lro.longVal, p2lcxt.longVal, p2lpin.longVal, p2ldatum.longVal, (jlong)parked, (jlong)actual, dbb); @@ -324,7 +327,7 @@ jobject pljava_VarlenaWrapper_Output(MemoryContext parent, ResourceOwner ro) dbb = JNI_newDirectByteBuffer(evosh->tail + 1, INITIALSIZE); vos = JNI_newObjectLocked(s_VarlenaWrapper_Output_class, - s_VarlenaWrapper_Output_init, pljava_DualState_key(), + s_VarlenaWrapper_Output_init, p2lro.longVal, p2lcxt.longVal, p2ldatum.longVal, dbb); JNI_deleteLocalRef(dbb); @@ -348,8 +351,7 @@ Datum pljava_VarlenaWrapper_adopt(jobject vlw) void *final_result; #endif - p2l.longVal = JNI_callLongMethodLocked(vlw, s_VarlenaWrapper_adopt, - pljava_DualState_key()); + p2l.longVal = JNI_callLongMethodLocked(vlw, s_DatumImpl_adopt); #if PG_VERSION_NUM >= 90500 return PointerGetDatum(p2l.ptrVal); #else @@ -444,6 +446,9 @@ void pljava_VarlenaWrapper_initialize(void) { 0, 0, 0 } }; + s_DatumImpl_class = + (jclass)JNI_newGlobalRef(PgObject_getJavaClass( + "org/postgresql/pljava/pg/DatumImpl")); s_VarlenaWrapper_class = (jclass)JNI_newGlobalRef(PgObject_getJavaClass( "org/postgresql/pljava/internal/VarlenaWrapper")); @@ -456,17 +461,14 @@ void pljava_VarlenaWrapper_initialize(void) s_VarlenaWrapper_Input_init = PgObject_getJavaMethod( s_VarlenaWrapper_Input_class, "", - "(Lorg/postgresql/pljava/internal/DualState$Key;" - "JJJJJJLjava/nio/ByteBuffer;)V"); + "(JJJJJJLjava/nio/ByteBuffer;)V"); s_VarlenaWrapper_Output_init = PgObject_getJavaMethod( s_VarlenaWrapper_Output_class, "", - "(Lorg/postgresql/pljava/internal/DualState$Key;" - "JJJLjava/nio/ByteBuffer;)V"); + "(JJJLjava/nio/ByteBuffer;)V"); - s_VarlenaWrapper_adopt = PgObject_getJavaMethod( - s_VarlenaWrapper_class, "adopt", - "(Lorg/postgresql/pljava/internal/DualState$Key;)J"); + s_DatumImpl_adopt = PgObject_getJavaMethod( + s_DatumImpl_class, "adopt", "()J"); clazz = PgObject_getJavaClass( "org/postgresql/pljava/internal/VarlenaWrapper$Input$State"); diff --git a/pljava-so/src/main/c/type/Array.c b/pljava-so/src/main/c/type/Array.c index 232fc05aa..e4dec0e04 100644 --- a/pljava-so/src/main/c/type/Array.c +++ b/pljava-so/src/main/c/type/Array.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2020 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2023 Tada AB and other contributors, as listed below. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the The BSD 3-Clause License @@ -157,7 +157,8 @@ static bool _Array_canReplaceType(Type self, Type other) if ( oe == 0 ) return false; return Type_canReplaceType(Type_getElementType(self), oe) - || Type_getObjectType(self) == other; + || Type_getObjectType(self) == other + || Type_getElementType(Type_getElementType(self)) == oe; } Type Array_fromOid(Oid typeId, Type elementType) diff --git a/pljava-so/src/main/c/type/ErrorData.c b/pljava-so/src/main/c/type/ErrorData.c index 2b2ca6758..bb895464a 100644 --- a/pljava-so/src/main/c/type/ErrorData.c +++ b/pljava-so/src/main/c/type/ErrorData.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2021 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2022 Tada AB and other contributors, as listed below. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the The BSD 3-Clause License @@ -34,14 +34,8 @@ jobject pljava_ErrorData_getCurrentError(void) p2l.longVal = 0L; /* ensure that the rest is zeroed out */ p2l.ptrVal = errorData; - /* - * Passing (jlong)0 as the ResourceOwner means this will never be matched by - * a nativeRelease call; that's appropriate (for now) as the ErrorData copy - * is being made into JavaMemoryContext, which never gets reset, so only - * unreachability from the Java side will free it. - */ jed = JNI_newObjectLocked(s_ErrorData_class, s_ErrorData_init, - pljava_DualState_key(), (jlong)0, p2l.longVal); + p2l.longVal); return jed; } @@ -143,7 +137,7 @@ void pljava_ErrorData_initialize(void) s_ErrorData_class = JNI_newGlobalRef(PgObject_getJavaClass("org/postgresql/pljava/internal/ErrorData")); PgObject_registerNatives2(s_ErrorData_class, methods); s_ErrorData_init = PgObject_getJavaMethod(s_ErrorData_class, "", - "(Lorg/postgresql/pljava/internal/DualState$Key;JJ)V"); + "(J)V"); s_ErrorData_getNativePointer = PgObject_getJavaMethod(s_ErrorData_class, "getNativePointer", "()J"); } diff --git a/pljava-so/src/main/c/type/Portal.c b/pljava-so/src/main/c/type/Portal.c index 3cf9e5b8f..a9c4338c9 100644 --- a/pljava-so/src/main/c/type/Portal.c +++ b/pljava-so/src/main/c/type/Portal.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2019 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2023 Tada AB and other contributors, as listed below. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the The BSD 3-Clause License @@ -22,6 +22,7 @@ #include "pljava/Exception.h" #include "pljava/Invocation.h" #include "pljava/HashMap.h" +#include "pljava/ModelUtils.h" #include "pljava/type/Type_priv.h" #include "pljava/type/TupleDesc.h" #include "pljava/type/Portal.h" @@ -31,6 +32,10 @@ #include #endif +#define CONFIRMCONST(c) \ +StaticAssertStmt((c) == (org_postgresql_pljava_internal_Portal_##c), \ + "Java/C value mismatch for " #c) + static jclass s_Portal_class; static jmethodID s_Portal_init; @@ -42,6 +47,7 @@ jobject pljava_Portal_create(Portal portal, jobject jplan) jobject jportal; Ptr2Long p2l; Ptr2Long p2lro; + Ptr2Long p2lcxt; if(portal == 0) return NULL; @@ -51,8 +57,16 @@ jobject pljava_Portal_create(Portal portal, jobject jplan) p2lro.longVal = 0L; p2lro.ptrVal = portal->resowner; + p2lcxt.longVal = 0L; + p2lcxt.ptrVal = +#if PG_VERSION_NUM >= 110000 + portal->portalContext; +#else + PortalGetHeapMemory(portal); +#endif + jportal = JNI_newObjectLocked(s_Portal_class, s_Portal_init, - pljava_DualState_key(), p2lro.longVal, p2l.longVal, jplan); + p2lro.longVal, p2lcxt.longVal, p2l.longVal, jplan); return jportal; } @@ -63,6 +77,16 @@ void pljava_Portal_initialize(void) { JNINativeMethod methods[] = { + { + "_getTupleDescriptor", + "(J)Lorg/postgresql/pljava/model/TupleDescriptor;", + Java_org_postgresql_pljava_internal_Portal__1getTupleDescriptor + }, + { + "_makeTupleTableSlot", + "(JLorg/postgresql/pljava/model/TupleDescriptor;)Lorg/postgresql/pljava/pg/TupleTableSlotImpl;", + Java_org_postgresql_pljava_internal_Portal__1makeTupleTableSlot + }, { "_getName", "(J)Ljava/lang/String;", @@ -74,6 +98,11 @@ void pljava_Portal_initialize(void) Java_org_postgresql_pljava_internal_Portal__1getPortalPos }, { + "_getTupleDescriptor", + "(J)Lorg/postgresql/pljava/model/TupleDescriptor;", + Java_org_postgresql_pljava_internal_Portal__1getTupleDescriptor + }, + { "_getTupleDesc", "(J)Lorg/postgresql/pljava/internal/TupleDesc;", Java_org_postgresql_pljava_internal_Portal__1getTupleDesc @@ -104,13 +133,66 @@ void pljava_Portal_initialize(void) s_Portal_class = JNI_newGlobalRef(PgObject_getJavaClass("org/postgresql/pljava/internal/Portal")); PgObject_registerNatives2(s_Portal_class, methods); s_Portal_init = PgObject_getJavaMethod(s_Portal_class, "", - "(Lorg/postgresql/pljava/internal/DualState$Key;JJLorg/postgresql/pljava/internal/ExecutionPlan;)V"); + "(JJJLorg/postgresql/pljava/internal/ExecutionPlan;)V"); + + /* + * Statically assert that the Java code has the right values for these. + * I would rather have this at the top, but these count as statements and + * would trigger a declaration-after-statment warning. + */ + CONFIRMCONST(FETCH_FORWARD); + CONFIRMCONST(FETCH_BACKWARD); + CONFIRMCONST(FETCH_ABSOLUTE); + CONFIRMCONST(FETCH_RELATIVE); + CONFIRMCONST(FETCH_ALL); } /**************************************** * JNI methods ****************************************/ +/* + * Class: org_postgresql_pljava_internal_Portal + * Method: _getTupleDescriptor + * Signature: (J)Lorg/postgresql/pljava/model/TupleDescriptor; + */ +JNIEXPORT jobject JNICALL +Java_org_postgresql_pljava_internal_Portal__1getTupleDescriptor(JNIEnv* env, jclass clazz, jlong _this) +{ + jobject result = 0; + if(_this != 0) + { + BEGIN_NATIVE + Ptr2Long p2l; + p2l.longVal = _this; + result = pljava_TupleDescriptor_create( + ((Portal)p2l.ptrVal)->tupDesc, InvalidOid); + END_NATIVE + } + return result; +} + +/* + * Class: org_postgresql_pljava_internal_Portal + * Method: _makeTupleTableSlot + * Signature: (JLorg/postgresql/pljava/model/TupleDescriptor;)Lorg/postgresql/pljava/pg/TupleTableSlotImpl; + */ +JNIEXPORT jobject JNICALL +Java_org_postgresql_pljava_internal_Portal__1makeTupleTableSlot(JNIEnv* env, jclass clazz, jlong _this, jobject jtd) +{ + jobject result = 0; + if(_this != 0) + { + BEGIN_NATIVE + Ptr2Long p2l; + p2l.longVal = _this; + result = pljava_TupleTableSlot_create( + ((Portal)p2l.ptrVal)->tupDesc, jtd, &TTSOpsHeapTuple, InvalidOid); + END_NATIVE + } + return result; +} + /* * Class: org_postgresql_pljava_internal_Portal * Method: _getPortalPos diff --git a/pljava-so/src/main/c/type/Relation.c b/pljava-so/src/main/c/type/Relation.c index 2e8ee4807..da3299736 100644 --- a/pljava-so/src/main/c/type/Relation.c +++ b/pljava-so/src/main/c/type/Relation.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2019 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2022 Tada AB and other contributors, as listed below. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the The BSD 3-Clause License @@ -35,7 +35,6 @@ static jmethodID s_Relation_init; jobject pljava_Relation_create(Relation r) { Ptr2Long p2lr; - Ptr2Long p2lro; if ( NULL == r ) return NULL; @@ -43,14 +42,9 @@ jobject pljava_Relation_create(Relation r) p2lr.longVal = 0L; p2lr.ptrVal = r; - p2lro.longVal = 0L; - p2lro.ptrVal = currentInvocation; - return JNI_newObjectLocked( s_Relation_class, s_Relation_init, - pljava_DualState_key(), - p2lro.longVal, p2lr.longVal); } @@ -84,7 +78,7 @@ void pljava_Relation_initialize(void) s_Relation_class = JNI_newGlobalRef(PgObject_getJavaClass("org/postgresql/pljava/internal/Relation")); PgObject_registerNatives2(s_Relation_class, methods); s_Relation_init = PgObject_getJavaMethod(s_Relation_class, "", - "(Lorg/postgresql/pljava/internal/DualState$Key;JJ)V"); + "(J)V"); } /**************************************** diff --git a/pljava-so/src/main/c/type/SQLXMLImpl.c b/pljava-so/src/main/c/type/SQLXMLImpl.c index 16b483b37..9f2314038 100644 --- a/pljava-so/src/main/c/type/SQLXMLImpl.c +++ b/pljava-so/src/main/c/type/SQLXMLImpl.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2021 Tada AB and other contributors, as listed below. + * Copyright (c) 2018-2023 Tada AB and other contributors, as listed below. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the The BSD 3-Clause License @@ -203,19 +203,19 @@ void pljava_SQLXMLImpl_initialize(void) s_SQLXML_class = JNI_newGlobalRef(PgObject_getJavaClass( "org/postgresql/pljava/jdbc/SQLXMLImpl")); s_SQLXML_adopt = PgObject_getStaticJavaMethod(s_SQLXML_class, "adopt", - "(Ljava/sql/SQLXML;I)Lorg/postgresql/pljava/internal/VarlenaWrapper;"); + "(Ljava/sql/SQLXML;I)Lorg/postgresql/pljava/adt/spi/Datum;"); s_SQLXML_Readable_PgXML_class = JNI_newGlobalRef(PgObject_getJavaClass( "org/postgresql/pljava/jdbc/SQLXMLImpl$Readable$PgXML")); s_SQLXML_Readable_PgXML_init = PgObject_getJavaMethod( s_SQLXML_Readable_PgXML_class, - "", "(Lorg/postgresql/pljava/internal/VarlenaWrapper$Input;I)V"); + "", "(Lorg/postgresql/pljava/adt/spi/Datum$Input;I)V"); s_SQLXML_Readable_Synthetic_class = JNI_newGlobalRef(PgObject_getJavaClass( "org/postgresql/pljava/jdbc/SQLXMLImpl$Readable$Synthetic")); s_SQLXML_Readable_Synthetic_init = PgObject_getJavaMethod( s_SQLXML_Readable_Synthetic_class, - "", "(Lorg/postgresql/pljava/internal/VarlenaWrapper$Input;I)V"); + "", "(Lorg/postgresql/pljava/adt/spi/Datum$Input;I)V"); s_SQLXML_Writable_class = JNI_newGlobalRef(PgObject_getJavaClass( "org/postgresql/pljava/jdbc/SQLXMLImpl$Writable")); diff --git a/pljava-so/src/main/c/type/SingleRowReader.c b/pljava-so/src/main/c/type/SingleRowReader.c index f65c1b269..6e1103365 100644 --- a/pljava-so/src/main/c/type/SingleRowReader.c +++ b/pljava-so/src/main/c/type/SingleRowReader.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2019 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2022 Tada AB and other contributors, as listed below. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the The BSD 3-Clause License @@ -48,19 +48,16 @@ jobject pljava_SingleRowReader_getTupleDesc(HeapTupleHeader ht) jobject pljava_SingleRowReader_create(HeapTupleHeader ht) { Ptr2Long p2lht; - Ptr2Long p2lro; jobject result; jobject jtd = pljava_SingleRowReader_getTupleDesc(ht); p2lht.longVal = 0L; - p2lro.longVal = 0L; p2lht.ptrVal = ht; - p2lro.ptrVal = currentInvocation; result = JNI_newObjectLocked(s_SingleRowReader_class, s_SingleRowReader_init, - pljava_DualState_key(), p2lro.longVal, p2lht.longVal, jtd); + p2lht.longVal, jtd); JNI_deleteLocalRef(jtd); return result; @@ -83,7 +80,7 @@ void pljava_SingleRowReader_initialize(void) PgObject_getJavaClass("org/postgresql/pljava/jdbc/SingleRowReader"); PgObject_registerNatives2(cls, methods); s_SingleRowReader_init = PgObject_getJavaMethod(cls, "", - "(Lorg/postgresql/pljava/internal/DualState$Key;JJLorg/postgresql/pljava/internal/TupleDesc;)V"); + "(JLorg/postgresql/pljava/internal/TupleDesc;)V"); s_SingleRowReader_class = JNI_newGlobalRef(cls); JNI_deleteLocalRef(cls); } diff --git a/pljava-so/src/main/c/type/TriggerData.c b/pljava-so/src/main/c/type/TriggerData.c index 9f05542c6..3e880db5b 100644 --- a/pljava-so/src/main/c/type/TriggerData.c +++ b/pljava-so/src/main/c/type/TriggerData.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2019 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2022 Tada AB and other contributors, as listed below. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the The BSD 3-Clause License @@ -32,7 +32,6 @@ static jmethodID s_TriggerData_getTriggerReturnTuple; jobject pljava_TriggerData_create(TriggerData* triggerData) { Ptr2Long p2ltd; - Ptr2Long p2lro; if ( NULL == triggerData ) return NULL; @@ -40,14 +39,9 @@ jobject pljava_TriggerData_create(TriggerData* triggerData) p2ltd.longVal = 0L; p2ltd.ptrVal = triggerData; - p2lro.longVal = 0L; - p2lro.ptrVal = currentInvocation; - return JNI_newObjectLocked( s_TriggerData_class, s_TriggerData_init, - pljava_DualState_key(), - p2lro.longVal, p2ltd.longVal); } @@ -137,8 +131,7 @@ void pljava_TriggerData_initialize(void) jcls = PgObject_getJavaClass("org/postgresql/pljava/internal/TriggerData"); PgObject_registerNatives2(jcls, methods); - s_TriggerData_init = PgObject_getJavaMethod(jcls, "", - "(Lorg/postgresql/pljava/internal/DualState$Key;JJ)V"); + s_TriggerData_init = PgObject_getJavaMethod(jcls, "", "(J)V"); s_TriggerData_getTriggerReturnTuple = PgObject_getJavaMethod( jcls, "getTriggerReturnTuple", "()J"); s_TriggerData_class = JNI_newGlobalRef(jcls); diff --git a/pljava-so/src/main/c/type/Tuple.c b/pljava-so/src/main/c/type/Tuple.c index 7cfc197e9..cc810ae24 100644 --- a/pljava-so/src/main/c/type/Tuple.c +++ b/pljava-so/src/main/c/type/Tuple.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2019 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2022 Tada AB and other contributors, as listed below. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the The BSD 3-Clause License @@ -65,14 +65,10 @@ jobject pljava_Tuple_internalCreate(HeapTuple ht, bool mustCopy) htH.longVal = 0L; /* ensure that the rest is zeroed out */ htH.ptrVal = ht; /* - * Passing (jlong)0 as the ResourceOwner means this will never be matched by a - * nativeRelease call; that's appropriate (for now) as the Tuple copy is - * being made into JavaMemoryContext, which never gets reset, so only - * unreachability from the Java side will free it. * XXX? this seems like a lot of tuple copying. */ jht = JNI_newObjectLocked(s_Tuple_class, s_Tuple_init, - pljava_DualState_key(), (jlong)0, htH.longVal); + htH.longVal); return jht; } @@ -100,7 +96,7 @@ void pljava_Tuple_initialize(void) s_Tuple_class = JNI_newGlobalRef(PgObject_getJavaClass("org/postgresql/pljava/internal/Tuple")); PgObject_registerNatives2(s_Tuple_class, methods); s_Tuple_init = PgObject_getJavaMethod(s_Tuple_class, "", - "(Lorg/postgresql/pljava/internal/DualState$Key;JJ)V"); + "(J)V"); cls = TypeClass_alloc("type.Tuple"); cls->JNISignature = "Lorg/postgresql/pljava/internal/Tuple;"; diff --git a/pljava-so/src/main/c/type/TupleDesc.c b/pljava-so/src/main/c/type/TupleDesc.c index 20376967d..118eb9224 100644 --- a/pljava-so/src/main/c/type/TupleDesc.c +++ b/pljava-so/src/main/c/type/TupleDesc.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2020 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2022 Tada AB and other contributors, as listed below. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the The BSD 3-Clause License @@ -54,15 +54,8 @@ jobject pljava_TupleDesc_internalCreate(TupleDesc td) td = CreateTupleDescCopyConstr(td); tdH.longVal = 0L; /* ensure that the rest is zeroed out */ tdH.ptrVal = td; - /* - * Passing (jlong)0 as the ResourceOwner means this will never be matched by a - * nativeRelease call; that's appropriate (for now) as the TupleDesc copy is - * being made into JavaMemoryContext, which never gets reset, so only - * unreachability from the Java side will free it. - * XXX what about invalidating if DDL alters the column layout? - */ jtd = JNI_newObjectLocked(s_TupleDesc_class, s_TupleDesc_init, - pljava_DualState_key(), (jlong)0, tdH.longVal, (jint)td->natts); + tdH.longVal, (jint)td->natts); return jtd; } @@ -125,7 +118,7 @@ void pljava_TupleDesc_initialize(void) s_TupleDesc_class = JNI_newGlobalRef(PgObject_getJavaClass("org/postgresql/pljava/internal/TupleDesc")); PgObject_registerNatives2(s_TupleDesc_class, methods); s_TupleDesc_init = PgObject_getJavaMethod(s_TupleDesc_class, "", - "(Lorg/postgresql/pljava/internal/DualState$Key;JJI)V"); + "(JI)V"); cls = TypeClass_alloc("type.TupleDesc"); cls->JNISignature = "Lorg/postgresql/pljava/internal/TupleDesc;"; diff --git a/pljava-so/src/main/c/type/Type.c b/pljava-so/src/main/c/type/Type.c index a0d181e2f..a9ec56e9b 100644 --- a/pljava-so/src/main/c/type/Type.c +++ b/pljava-so/src/main/c/type/Type.c @@ -62,7 +62,9 @@ static CoercionPathType fcp(Oid targetTypeId, Oid sourceTypeId, static Oid BOOLARRAYOID; static Oid CHARARRAYOID; static Oid FLOAT8ARRAYOID; +#ifndef __TBASE__ static Oid INT8ARRAYOID; +#endif #if PG_VERSION_NUM < 80400 static Oid INT2ARRAYOID; #endif @@ -110,50 +112,8 @@ typedef struct Function fn; jobject rowProducer; jobject rowCollector; - /* - * Invocation instance, if any, the Java counterpart to currentInvocation - * the C struct. There isn't one unless it gets asked for, then if it is, - * it's saved here, so even though the C currentInvocation really is new on - * each entry from PG, Java will see one Invocation instance throughout the - * sequence of calls. - */ - jobject invocation; - /* - * Two pieces of state from Invocation.c's management of SPI connection, - * effectively keeping one such connection alive through the sequence of - * calls. I could easily be led to question the advisability of even doing - * that, but it has a long history in PL/Java, so changing it might call for - * some careful analysis. - */ - MemoryContext spiContext; - bool hasConnected; } CallContextData; -/* - * Called during evaluation of a set-returning function, at various points after - * calls into Java code could have instantiated an Invocation, or connected SPI. - * Does not stash elemType, rowProducer, or rowCollector; those are all - * unconditionally set in the first-call initialization, and spiContext to zero. - */ -static void stashCallContext(CallContextData *ctxData) -{ - bool wasConnected = ctxData->hasConnected; - - ctxData->hasConnected = currentInvocation->hasConnected; - - ctxData->invocation = currentInvocation->invocation; - - if ( wasConnected ) - return; - - /* - * If SPI has been connected for the first time, capture the memory context - * it imposed. Curiously, this is not used again except in _closeIteration. - */ - if(ctxData->hasConnected) - ctxData->spiContext = CurrentMemoryContext; -} - /* * Called either at normal completion of a set-returning function, or by the * _endOfSetCB if PostgreSQL doesn't want all the results. @@ -161,8 +121,6 @@ static void stashCallContext(CallContextData *ctxData) static void _closeIteration(CallContextData* ctxData) { jobject dummy; - currentInvocation->hasConnected = ctxData->hasConnected; - currentInvocation->invocation = ctxData->invocation; /* * Why pass 1 as the call_cntr? We won't always have the actual call_cntr @@ -178,24 +136,6 @@ static void _closeIteration(CallContextData* ctxData) JNI_deleteGlobalRef(ctxData->rowProducer); if(ctxData->rowCollector != 0) JNI_deleteGlobalRef(ctxData->rowCollector); - - if(ctxData->hasConnected && ctxData->spiContext != 0) - { - /* - * SPI was connected. We will (1) switch back to the memory context that - * was imposed by SPI_connect, then (2) disconnect. SPI_finish will have - * switched back to whatever memory context was current when SPI_connect - * was called, and that context had better still be valid. It might be - * the executor's multi_call_memory_ctx, if the SPI_connect happened - * during initialization of the rowProducer or rowCollector, or the - * executor's per-row context, if it happened later. Both of those are - * still valid at this point. The final step (3) is to switch back to - * the context we had before (1) and (2) happened. - */ - MemoryContext currCtx = MemoryContextSwitchTo(ctxData->spiContext); - Invocation_assertDisconnect(); - MemoryContextSwitchTo(currCtx); - } } /* @@ -525,6 +465,39 @@ Datum Type_invokeSRF(Type self, Function fn, PG_FUNCTION_ARGS) SRF_RETURN_DONE(context); } + /* + * If the set-up function called above did not connect SPI, we are + * (unless the function changed it in some other arbitrary way) still + * in the multi_call_memory_ctx. We will return to currCtx (the executor + * per-row context) at the end of this set-up block, in preparation for + * producing the first row, if any. + * + * If the set-up function did connect SPI, we are now in the SPI Proc + * memory context (which will go away in SPI_finish when this call + * returns). That's not very much different from currCtx, the one the + * executor supplied us, which will be reset by the executor after the + * return of this call and before the next invocation. Here, we will + * switch back to the multi_call_memory_ctx for the remainder of this + * set-up block. As always, this block will end with a switch to currCtx + * and be ready to produce the first row. + * + * Two choices are possible here: 1) leave currCtx unchanged, so we + * end up in the executor's per-row context; 2) assign the SPI Proc + * context to it, so we end up in that. Because the contexts have very + * similar lifecycles, the choice does not seem critical. Of note, + * though, is that any SPI function that operates in the SPI Exec + * context will unconditionally leave the SPI Proc context as + * the current context when it returns; it will not save and restore + * its context on entry. Given that behavior, the choice here of (2) + * reassigning currCtx to mean the SPI Proc context would seem to create + * the situation with the least potential for surprises. + */ + if ( currentInvocation->hasConnected ) + currCtx = MemoryContextSwitchTo(context->multi_call_memory_ctx); + + /* + * This palloc depends on being made in the multi_call_memory_ctx. + */ ctxData = (CallContextData*)palloc0(sizeof(CallContextData)); context->user_fctx = ctxData; @@ -543,8 +516,6 @@ Datum Type_invokeSRF(Type self, Function fn, PG_FUNCTION_ARGS) JNI_deleteLocalRef(tmp); } - stashCallContext(ctxData); - /* Register callback to be called when the function ends */ RegisterExprContextCallback( @@ -562,15 +533,14 @@ Datum Type_invokeSRF(Type self, Function fn, PG_FUNCTION_ARGS) /* * Invariant: whether this is the first call and the SRF_IS_FIRSTCALL block * above just completed, or this is a subsequent call, at this point, the - * memory context is the per-row one supplied by the executor (which gets - * reset between calls). + * memory context is one that gets reset between calls: either the per-row + * context supplied by the executor, or (if this is the first call and the + * setup code used SPI) the "SPI Proc" context. */ context = SRF_PERCALL_SETUP(); ctxData = (CallContextData*)context->user_fctx; - currCtx = CurrentMemoryContext; /* save executor's per-row context */ - currentInvocation->hasConnected = ctxData->hasConnected; - currentInvocation->invocation = ctxData->invocation; + currCtx = CurrentMemoryContext; /* save the supplied per-row context */ if(JNI_TRUE == pljava_Function_vpcInvoke(ctxData->fn, ctxData->rowProducer, ctxData->rowCollector, (jlong)context->call_cntr, @@ -578,18 +548,9 @@ Datum Type_invokeSRF(Type self, Function fn, PG_FUNCTION_ARGS) { Datum result = Type_datumFromSRF(self, row, ctxData->rowCollector); JNI_deleteLocalRef(row); - stashCallContext(ctxData); - currentInvocation->hasConnected = false; - currentInvocation->invocation = 0; - MemoryContextSwitchTo(currCtx); SRF_RETURN_NEXT(context, result); } - stashCallContext(ctxData); - currentInvocation->hasConnected = false; - currentInvocation->invocation = 0; - MemoryContextSwitchTo(currCtx); - /* Unregister this callback and call it manually. We do this because * otherwise it will be called when the backend is in progress of * cleaning up Portals. If we close cursors (i.e. drop portals) in @@ -841,6 +802,14 @@ bool _Type_canReplaceType(Type self, Type other) return self->typeClass == other->typeClass; } +/* + * The Type_invoke implementation that is 'inherited' by all type classes + * except Coerce, Composite, and those corresponding to Java primitives. + * This implementation unconditionally switches to the "upper memory context" + * recorded in the Invocation before coercing the Java result to a Datum, + * in case SPI has been connected (which would have switched to a context that + * is reset too soon for the caller to use the result). + */ Datum _Type_invoke(Type self, Function fn, PG_FUNCTION_ARGS) { MemoryContext currCtx; @@ -872,9 +841,24 @@ static jobject _Type_getSRFCollector(Type self, PG_FUNCTION_ARGS) return 0; } +/* + * The Type_datumFromSRF implementation that is 'inherited' by all type classes + * except Composite. This implementation makes no use of the rowCollector + * parameter, and unconditionally switches to the "upper memory context" + * recorded in the Invocation before coercing the Java result to a Datum, in + * case SPI has been connected (which would have switched to a context that is + * reset too soon for the caller to use the result). + */ static Datum _Type_datumFromSRF(Type self, jobject row, jobject rowCollector) { - return Type_coerceObject(self, row); + MemoryContext currCtx; + Datum ret; + + currCtx = Invocation_switchToUpperContext(); + ret = Type_coerceObject(self, row); + MemoryContextSwitchTo(currCtx); + + return ret; } jobject Type_getSRFCollector(Type self, PG_FUNCTION_ARGS) @@ -1054,7 +1038,9 @@ void Type_initialize(void) BOOLARRAYOID = get_array_type(BOOLOID); CHARARRAYOID = get_array_type(CHAROID); FLOAT8ARRAYOID = get_array_type(FLOAT8OID); +#ifndef __TBASE__ INT8ARRAYOID = get_array_type(INT8OID); +#endif #if PG_VERSION_NUM < 80400 INT2ARRAYOID = get_array_type(INT2OID); #endif diff --git a/pljava-so/src/main/include/pljava/DualState.h b/pljava-so/src/main/include/pljava/DualState.h index 64e111c76..a736b8a24 100644 --- a/pljava-so/src/main/include/pljava/DualState.h +++ b/pljava-so/src/main/include/pljava/DualState.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2019 Tada AB and other contributors, as listed below. + * Copyright (c) 2018-2022 Tada AB and other contributors, as listed below. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the The BSD 3-Clause License @@ -13,7 +13,6 @@ #define __pljava_DualState_h #include -#include #include "pljava/pljava.h" @@ -21,16 +20,10 @@ extern "C" { #endif -extern jobject pljava_DualState_key(void); - extern void pljava_DualState_cleanEnqueuedInstances(void); extern void pljava_DualState_initialize(void); -extern void pljava_DualState_unregister(void); - -extern void pljava_DualState_nativeRelease(void *); - #ifdef __cplusplus } #endif diff --git a/pljava-so/src/main/include/pljava/Invocation.h b/pljava-so/src/main/include/pljava/Invocation.h index 28bebe41c..756fe612a 100644 --- a/pljava-so/src/main/include/pljava/Invocation.h +++ b/pljava-so/src/main/include/pljava/Invocation.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2021 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2022 Tada AB and other contributors, as listed below. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the The BSD 3-Clause License @@ -28,48 +28,51 @@ extern "C" { struct Invocation_ { /** - * A Java object representing the current invocation. This - * field will be NULL if no such object has been requested. + * The level of nested call into PL/Java represented by this Invocation. + * Including it in this struct is slightly redundant (it can be "saved" and + * "restored" just by increment/decrement), but allows it to be read with no + * additional fuss by the Java code through a single ByteBuffer window over + * the currentInvocation struct. */ - jobject invocation; + int32 nestLevel; + + /** + * Set if the Java Invocation instance corresponding to this invocation + * has been requested and assigned. If so, its onExit method will be called + * when this invocation is popped. + */ + bool hasDual; /** - * The context to use when allocating values that are to be - * returned from the call. + * Set to true if an elog with a severity >= ERROR + * has occured. All calls from Java to the backend will + * be prevented until this flag is reset (by a rollback + * of a savepoint or function exit). */ - MemoryContext upperContext; + bool errorOccurred; /** * Set when an SPI_connect is issued. Ensures that SPI_finish * is called when the function exits. */ - bool hasConnected; + bool hasConnected; /** * Set to true if the call originates from an ExprContextCallback. When - * it does, we should not close any cursors. - */ - bool inExprContextCB; - - /** - * The saved limits reserved in Function.c's static parameter frame, as a - * count of reference and primitive parameters combined in a short. - * FRAME_LIMITS_PUSHED is an otherwise invalid value used to record that the - * more heavyweight saving of the frame as a Java ParameterFrame instance - * has occurred. Otherwise, this value (and the primitive slot 0 value - * below) are simply restored when this Invocation is exited normally or - * exceptionally. + * it does, we should not close any cursors. Such a callback is registered + * in the setup of a value-per-call set-returning function, and used to + * detect when no further values of the set will be wanted. */ - jshort frameLimits; -#define FRAME_LIMITS_PUSHED ((jshort)-1) + bool inExprContextCB; /** - * The saved value of the first primitive slot in Function's static - * parameter frame. Unless frameLimits above is FRAME_LIMITS_PUSHED, this - * value is simply restored when this Invocation is exited normally or - * exceptionally. + * The context to use when allocating values that are to be + * returned from the call. Copied from CurrentMemoryContext on invocation + * entry. If SPI_connect is later called (which changes the context to + * a local one), this is the same as what SPI calls the "upper executor + * context" and uses in functions like SPI_palloc. */ - jvalue primSlot0; + MemoryContext upperContext; /** * The saved thread context classloader from before this invocation @@ -79,15 +82,7 @@ struct Invocation_ /** * The currently executing Function. */ - Function function; - - /** - * Set to true if an elog with a severity >= ERROR - * has occured. All calls from Java to the backend will - * be prevented until this flag is reset (by a rollback - * of a savepoint or function exit). - */ - bool errorOccurred; + Function function; #if PG_VERSION_NUM >= 100000 /** @@ -95,18 +90,39 @@ struct Invocation_ * so it can be passed to SPI_register_trigger_data if the function connects * to SPI. */ - TriggerData* triggerData; + TriggerData* triggerData; #endif /** * The previous call context when nested function calls * are made or 0 if this call is at the top level. */ - Invocation* previous; + Invocation* previous; + + /** + * The saved value of the first primitive slot in Function's static + * parameter frame. Unless frameLimits above is FRAME_LIMITS_PUSHED, this + * value is simply restored when this Invocation is exited normally or + * exceptionally. + */ + jvalue primSlot0; + + /** + * The saved limits reserved in Function.c's static parameter frame, as a + * count of reference and primitive parameters combined in a short. + * FRAME_LIMITS_PUSHED is an otherwise invalid value used to record that the + * more heavyweight saving of the frame as a Java ParameterFrame instance + * has occurred. Otherwise, this value (and the primitive slot 0 value + * below) are simply restored when this Invocation is exited normally or + * exceptionally. + */ + jshort frameLimits; +#define FRAME_LIMITS_PUSHED ((jshort)-1) }; -extern Invocation* currentInvocation; +extern Invocation currentInvocation[]; +#define HAS_INVOCATION (0 < currentInvocation->nestLevel) extern void Invocation_assertConnect(void); @@ -133,7 +149,7 @@ extern jobject Invocation_getTypeMap(void); * Switch memory context to a context that is durable between calls to * the call manager but not durable between queries. The old context is * returned. This method can be used when creating values that will be - * returned from the Pl/Java routines. Once the values have been created + * returned from the PL/Java routines. Once the values have been created * a call to MemoryContextSwitchTo(oldContext) must follow where oldContext * is the context returned from this call. */ diff --git a/pljava-so/src/main/include/pljava/JNICalls.h b/pljava-so/src/main/include/pljava/JNICalls.h index 98cf10ff0..a1e8aa028 100644 --- a/pljava-so/src/main/include/pljava/JNICalls.h +++ b/pljava-so/src/main/include/pljava/JNICalls.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2021 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2022 Tada AB and other contributors, as listed below. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the The BSD 3-Clause License @@ -25,9 +25,13 @@ extern "C" { extern jint (JNICALL *pljava_createvm)(JavaVM **, void **, void *); #define BEGIN_NATIVE_NO_ERRCHECK if(beginNativeNoErrCheck(env)) { -#define BEGIN_NATIVE if(beginNative(env)) { +#define BEGIN_NATIVE if(!beginNative(env)) ; else { #define END_NATIVE JNI_setEnv(0); } +#define BEGIN_NATIVE_AND_TRY BEGIN_NATIVE PG_TRY(); { +#define END_NATIVE_AND_CATCH(shortfunc) } PG_CATCH(); { \ + Exception_throw_ERROR(shortfunc); } PG_END_TRY(); END_NATIVE + /*********************************************************************** * All calls to and from the JVM uses this header. The calls are implemented * using a fence mechanism that prevents multiple threads to access diff --git a/pljava-so/src/main/include/pljava/ModelConstants.h b/pljava-so/src/main/include/pljava/ModelConstants.h new file mode 100644 index 000000000..60593094f --- /dev/null +++ b/pljava-so/src/main/include/pljava/ModelConstants.h @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +#ifndef __pljava_ModelConstants_h +#define __pljava_ModelConstants_h + +#include "pljava/pljava.h" + +#ifdef __cplusplus +extern "C" { +#endif + +extern void pljava_ModelConstants_initialize(void); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/pljava-so/src/main/include/pljava/ModelUtils.h b/pljava-so/src/main/include/pljava/ModelUtils.h new file mode 100644 index 000000000..a0021d80f --- /dev/null +++ b/pljava-so/src/main/include/pljava/ModelUtils.h @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2022-2023 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +#ifndef __pljava_ModelUtils_h +#define __pljava_ModelUtils_h + +#include +#include +#include + +#include "pljava/pljava.h" + +#if PG_VERSION_NUM < 120000 +struct TupleTableSlotOps; +typedef struct TupleTableSlotOps TupleTableSlotOps; +extern const TupleTableSlotOps TTSOpsHeapTuple; +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +extern void pljava_ModelUtils_initialize(void); + +extern void pljava_ResourceOwner_unregister(void); + +/* + * Return a Java TupleDescriptor based on a PostgreSQL one. + * + * If the descriptor's tdtypeid is not RECORDOID (meaning the descriptor is + * for a named composite type), passing the relation oid here, if handy, will + * save a lookup in the Java code. In other cases, or if it simply is not + * handily available, InvalidOid can be passed, and the relation will be looked + * up if needed. + * + * If there is already a cached Java representation, the existing one + * is returned, and the supplied one's reference count (if it is counted) is + * untouched. If the supplied one is used to create a cached Java version, its + * reference count is incremented (without registering it for descriptor leak + * warnings), and it will be released upon removal from PL/Java's cache for + * invalidation or unreachability. If the descriptor is non-reference-counted, + * the returned Java object will not depend on it, and it is expendable + * after this function returns. + */ +extern jobject pljava_TupleDescriptor_create(TupleDesc tupdesc, Oid reloid); + +/* + * Create a PostgreSQL TupleTableSlot (of the specific type specified by + * tts_ops) and return a Java TupleTableSlot wrapping it. + * + * reloid is simply passed along to pljava_TupleDescriptor_create, so may be + * passed as InvalidOid with the same effects described there. + * + * If jtd is not NULL, it must be a JNI local reference to an existing Java + * TupleDescriptor that corresponds to the native tupdesc, and will be used + * instead of calling pljava_TupleDescriptor_create. On return, the local + * reference will have been deleted. + */ +extern jobject pljava_TupleTableSlot_create( + TupleDesc tupdesc, jobject jtd, + const TupleTableSlotOps *tts_ops, Oid reloid); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/pljava/pom.xml b/pljava/pom.xml index fb9b23f10..83b415529 100644 --- a/pljava/pom.xml +++ b/pljava/pom.xml @@ -4,7 +4,7 @@ org.postgresql pljava.app - 2-SNAPSHOT + 1.7-SNAPSHOT pljava PL/Java backend Java code diff --git a/pljava/src/main/java/module-info.java b/pljava/src/main/java/module-info.java index 68923bbe4..856d6b319 100644 --- a/pljava/src/main/java/module-info.java +++ b/pljava/src/main/java/module-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020 Tada AB and other contributors, as listed below. + * Copyright (c) 2020-2023 Tada AB and other contributors, as listed below. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the The BSD 3-Clause License @@ -19,6 +19,8 @@ requires java.management; requires org.postgresql.pljava; + exports org.postgresql.pljava.pg.adt to org.postgresql.pljava; + exports org.postgresql.pljava.mbeans; // bothers me, but only interfaces exports org.postgresql.pljava.elog to java.logging; @@ -33,6 +35,12 @@ provides java.sql.Driver with org.postgresql.pljava.jdbc.SPIDriver; + provides org.postgresql.pljava.Adapter.Service + with org.postgresql.pljava.pg.adt.Service; + provides org.postgresql.pljava.Session with org.postgresql.pljava.internal.Session; + + provides org.postgresql.pljava.model.CatalogObject.Factory + with org.postgresql.pljava.pg.CatalogObjectImpl.Factory; } diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/Backend.java b/pljava/src/main/java/org/postgresql/pljava/internal/Backend.java index 47bfe9051..2d2f294d0 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/Backend.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/Backend.java @@ -173,7 +173,7 @@ public static int doInPG(Checked.IntSupplier op) /** * Specialization of {@link #doInPG(Supplier) doInPG} for operations that * return a long result. This method need not be present: without it, the - * Java compiler will happily match int lambdas or method references to + * Java compiler will happily match long lambdas or method references to * the generic method, at the small cost of some boxing/unboxing; providing * this method simply allows that to be avoided. */ @@ -189,6 +189,84 @@ public static long doInPG(Checked.LongSupplier op) return op.getAsLong(); } + /** + * Specialization of {@link #doInPG(Supplier) doInPG} for operations that + * return a float result. This method need not be present: without it, the + * Java compiler will happily match float lambdas or method references to + * the generic method, at the small cost of some boxing/unboxing; providing + * this method simply allows that to be avoided. + */ + public static float doInPG( + Checked.FloatSupplier op) + throws E + { + if ( null != THREADLOCK ) + synchronized(THREADLOCK) + { + return op.getAsFloat(); + } + assertThreadMayEnterPG(); + return op.getAsFloat(); + } + + /** + * Specialization of {@link #doInPG(Supplier) doInPG} for operations that + * return a short result. This method need not be present: without it, the + * Java compiler will happily match short lambdas or method references to + * the generic method, at the small cost of some boxing/unboxing; providing + * this method simply allows that to be avoided. + */ + public static short doInPG( + Checked.ShortSupplier op) + throws E + { + if ( null != THREADLOCK ) + synchronized(THREADLOCK) + { + return op.getAsShort(); + } + assertThreadMayEnterPG(); + return op.getAsShort(); + } + + /** + * Specialization of {@link #doInPG(Supplier) doInPG} for operations that + * return a char result. This method need not be present: without it, the + * Java compiler will happily match char lambdas or method references to + * the generic method, at the small cost of some boxing/unboxing; providing + * this method simply allows that to be avoided. + */ + public static char doInPG(Checked.CharSupplier op) + throws E + { + if ( null != THREADLOCK ) + synchronized(THREADLOCK) + { + return op.getAsChar(); + } + assertThreadMayEnterPG(); + return op.getAsChar(); + } + + /** + * Specialization of {@link #doInPG(Supplier) doInPG} for operations that + * return a byte result. This method need not be present: without it, the + * Java compiler will happily match int lambdas or method references to + * the generic method, at the small cost of some boxing/unboxing; providing + * this method simply allows that to be avoided. + */ + public static byte doInPG(Checked.ByteSupplier op) + throws E + { + if ( null != THREADLOCK ) + synchronized(THREADLOCK) + { + return op.getAsByte(); + } + assertThreadMayEnterPG(); + return op.getAsByte(); + } + /** * Return true if the current thread may JNI-call into Postgres. *

diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/CacheMap.java b/pljava/src/main/java/org/postgresql/pljava/internal/CacheMap.java new file mode 100644 index 000000000..730888ec2 --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/internal/CacheMap.java @@ -0,0 +1,303 @@ +/* + * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.internal; + +import java.lang.ref.Reference; +import static java.lang.ref.Reference.reachabilityFence; +import java.lang.ref.ReferenceQueue; +import java.lang.ref.SoftReference; +import java.lang.ref.WeakReference; + +import java.nio.ByteBuffer; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import java.util.concurrent.ConcurrentHashMap; + +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import static java.util.stream.Collectors.joining; + +public class CacheMap +{ + private final Map> m_map; + private final ThreadLocal> m_holder; + private final ThreadLocal> m_holderWithBuffer; + private final ReferenceQueue m_queue = new ReferenceQueue<>(); + + private CacheMap( + Map> map, + Supplier keyBufferSupplier) + { + m_map = map; + m_holder = ThreadLocal.withInitial(() -> new KVHolder()); + m_holderWithBuffer = ThreadLocal.withInitial(() -> + { + KVHolder h = m_holder.get(); + h.key = keyBufferSupplier.get(); + return h; + }); + } + + /** + * Construct a {@code CacheMap} based on a concurrent map. + */ + public static CacheMap newConcurrent( + Supplier keyBufferSupplier) + { + return new CacheMap<>( + new ConcurrentHashMap>(), + keyBufferSupplier); + } + + /** + * Construct a {@code CacheMap} based on a non-thread-safe map, for cases + * where concurrent access from multiple threads can be ruled out. + */ + public static CacheMap newThreadConfined( + Supplier keyBufferSupplier) + { + return new CacheMap<>( + new HashMap>(), + keyBufferSupplier); + } + + private void poll() + { + for ( KeyedEntry e; null != (e = (KeyedEntry)m_queue.poll()); ) + m_map.remove(e.key(), e); + /* + * Reference objects (of which e is one) do not override equals() from + * Object, which is good, because Map's remove(k,v) actually uses + * v.equals(...) and could therefore remove a different object than + * intended, if the object had other than == semantics for equals(). + */ + } + + public T softlyCache( + Checked.Consumer keyer, + Checked.Function cacher) + throws E + { + BiFunction> wrapper = + (k,v) -> new SoftEntry<>(k, v, m_queue); + return cache(keyer, cacher, wrapper); + } + + public T weaklyCache( + Checked.Consumer keyer, + Checked.Function cacher) + throws E + { + BiFunction> wrapper = + (k,v) -> new WeakEntry<>(k, v, m_queue); + return cache(keyer, cacher, wrapper); + } + + public T stronglyCache( + Checked.Consumer keyer, + Checked.Function cacher) + throws E + { + BiFunction> wrapper = + (k,v) -> new StrongEntry<>(k, v, m_map); + return cache(keyer, cacher, wrapper); + } + + @Override + public String toString() + { + return m_map.values().stream() + .map(Entry::get) + .filter(Objects::nonNull) + .map(Object::toString) + .collect(joining(", ", "{", "}")); + } + + private T cache( + Checked.Consumer keyer, + Checked.Function cacher, + BiFunction> wrapper) + throws E + { + poll(); + KVHolder h = m_holderWithBuffer.get(); + ByteBuffer b = h.key; + b.clear(); + keyer.accept(b); + b.flip(); + KeyedEntry w; + for ( ;; ) + { + w = cacher.inReturning(Checked.Function.use( + (c) -> m_map.computeIfAbsent(b, + (k) -> + { + m_holderWithBuffer.remove(); + T v = c.apply(k); + h.value = v; // keep it live while returning through ref + return null == v ? null : wrapper.apply(k,v); + } + ) + )); + + if ( null == w ) + return null; + T v = w.get(); + reachabilityFence(h.value); + h.value = null; // no longer needed now that v is a strong reference + if ( null != v ) + return v; + m_map.remove(w.key(), w); + } + } + + /** + * Simple lookup, with no way to cache a new entry; returns null if no such + * entry is present. + *

+ * Returns an {@link Entry Entry} if found, which provides a method to + * remove the entry if appropriate. + */ + public Entry find( + Checked.Consumer keyer) + throws E + { + poll(); + KVHolder h = m_holderWithBuffer.get(); + ByteBuffer b = h.key; + b.clear(); + keyer.accept(b); + b.flip(); + return m_map.get(b); + } + + public void forEachValue(Consumer action) + { + if ( m_map instanceof ConcurrentHashMap ) + { + ConcurrentHashMap> m = + (ConcurrentHashMap>)m_map; + m.forEachValue(Long.MAX_VALUE, Entry::get, action); + return; + } + m_map.values().stream().map(Entry::get).filter(Objects::nonNull) + .forEach(action); + } + + public interface Entry + { + T get(); + void remove(); + } + + interface KeyedEntry extends Entry + { + ByteBuffer key(); + } + + static class SoftEntry extends SoftReference implements KeyedEntry + { + final ByteBuffer m_key; + + SoftEntry(ByteBuffer k, T v, ReferenceQueue q) + { + super(v, q); + m_key = k; + } + + @Override + public ByteBuffer key() + { + return m_key; + } + + @Override + public void remove() + { + clear(); + enqueue(); + } + } + + static class WeakEntry extends WeakReference implements KeyedEntry + { + final ByteBuffer m_key; + + WeakEntry(ByteBuffer k, T v, ReferenceQueue q) + { + super(v, q); + m_key = k; + } + + @Override + public ByteBuffer key() + { + return m_key; + } + + @Override + public void remove() + { + clear(); + enqueue(); + } + } + + static class StrongEntry implements KeyedEntry + { + final ByteBuffer m_key; + T m_value; + final Map> m_map; + + StrongEntry(ByteBuffer k, T v, Map> map) + { + m_key = k; + m_value = v; + m_map = map; + } + + @Override + public ByteBuffer key() + { + return m_key; + } + + @Override + public T get() + { + return m_value; + } + + @Override + public void remove() + { + m_value = null; + m_map.remove(m_key, this); + } + } + + /* + * Hold a ByteBuffer for key use, and any new value briefly between + * construction and return (to avoid any chance of its being found + * weakly reachable before its return). + */ + static class KVHolder + { + ByteBuffer key; + T value; + } +} diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java b/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java index a08f7f0f3..baa1a4cea 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2020 Tada AB and other contributors, as listed below. + * Copyright (c) 2018-2023 Tada AB and other contributors, as listed below. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the The BSD 3-Clause License @@ -17,6 +17,8 @@ import java.lang.ref.ReferenceQueue; import java.lang.ref.WeakReference; +import java.nio.ByteBuffer; + import java.sql.SQLException; import java.util.ArrayDeque; @@ -27,6 +29,7 @@ import java.util.List; import java.util.Map; import java.util.NoSuchElementException; +import static java.util.Objects.requireNonNull; import java.util.Queue; import java.util.concurrent.CancellationException; @@ -41,8 +44,15 @@ import javax.management.ObjectName; import javax.management.JMException; +import org.postgresql.pljava.Lifespan; + +import static org.postgresql.pljava.internal.Backend.threadMayEnterPG; +import org.postgresql.pljava.internal.LifespanImpl.Addressed; + import org.postgresql.pljava.mbeans.DualStateStatistics; +import org.postgresql.pljava.model.MemoryContext; + /** * Base class for object state with corresponding Java and native components. *

@@ -74,25 +84,18 @@ *

* A subclass calls {@link #releaseFromJava releaseFromJava} to signal an event * of the first kind. Events of the second kind are, naturally, detected by the - * Java garbage collector. To detect events of the third kind, a resource owner + * Java garbage collector. To detect events of the third kind, a lifespan * must be associated with the instance. *

- * A parameter to the {@code DualState} constructor is a {@code ResourceOwner}, - * a PostgreSQL implementation concept introduced in PG 8.0. A + * A parameter to the {@code DualState} constructor is a {@code Lifespan}. A * {@code nativeStateReleased} event occurs when the corresponding - * {@code ResourceOwner} is released in PostgreSQL. - *

- * However, this class does not require the {@code resourceOwner} parameter to - * be, in all cases, a pointer to a PostgreSQL {@code ResourceOwner}. It is - * treated simply as an opaque {@code long} value, to be compared to a value - * passed at release time (as if in a {@code ResourceOwner} callback). Other - * values (such as pointers to other allocated structures, which of course - * cannot match any PG {@code ResourceOwner} existing at the same time) can also - * be used. In PostgreSQL 9.5 and later, a {@code MemoryContext} could be used, - * with its address passed to a {@code MemoryContextCallback} for release. For - * state that is scoped to a single invocation of a PL/Java function, the - * address of the {@code Invocation} can be used. Such references can be - * considered "generalized" resource owners. + * {@code Lifespan} is released in PostgreSQL. PostgreSQL {@code ResourceOwner} + * and {@code MemoryContext} are two types of object that can serve + * as lifespans. A PL/Java {@code Invocation} object may also be used, to mark + * the lifespans of function arguments and other data expected to live at least + * for the duration of a function call. The lifespan argument can be null, + * for an object allocated in an immortal context and managed only by its + * {@code javaStateReleased} or {@code javaStateUnreachable} methods. *

* Java code may execute in multiple threads, but PostgreSQL is not * multi-threaded; at any given time, there is no more than one thread that may @@ -161,7 +164,7 @@ * native state until the pin is released. *

* If either the native state or the Java state has been released already (by - * the resource owner callback or an explicit call to {@code releaseFromJava}, + * the lifespan callback or an explicit call to {@code releaseFromJava}, * respectively), {@code pin()} will detect that and throw the appropriate * exception. Otherwise, the state is safe to make use of until {@code unpin}. * A subclass can customize the messages or {@code SQLSTATE} codes for the @@ -189,8 +192,8 @@ * The exclusive counterparts to {@code pin} and {@code unpin} are * {@link #lock lock} and {@link #unlock(int,boolean) unlock}, which are not * expected to be used as widely. The chief use of {@code lock}/{@code unlock} - * is around the call to {@code nativeStateReleased} when handling a resource - * owner callback from PostgreSQL. They can be used in subclasses to surround + * is around the call to {@code nativeStateReleased} when handling a lifespan + * callback from PostgreSQL. They can be used in subclasses to surround * modifications to the state, as needed. A {@code lock} will block until all * earlier-acquired pins are released; subsequent pins block until the lock is * released. Only the PG thread may use {@code lock}/{@code unlock}. An @@ -240,17 +243,14 @@ *

  • Instance construction *
  • Reference queue processing (instances found unreachable by Java's * garbage collector, or enqueued following {@code releaseFromJava}) - *
  • Exit of a resource owner's scope + *
  • Exit of a lifespan's scope * *
  • There is only one PG thread, or only one at a time. *
  • Construction of any {@code DualState} instance is to take place only on - * the PG thread. The requirement to pass any - * constructor a {@code DualState.Key} instance, obtainable by native code, is - * intended to reinforce that convention. It is not abuse-proof, or intended as - * a security mechanism, but only a guard against programming mistakes. + * the PG thread. *
  • Reference queue processing takes place only at chosen points where a * thread enters or exits native code, on the PG thread. - *
  • Resource-owner callbacks originate in native code, on the PG thread. + *
  • Lifespan callbacks originate in native code, on the PG thread. * */ public abstract class DualState extends WeakReference @@ -294,23 +294,10 @@ public abstract class DualState extends WeakReference private static final IdentityHashMap s_unscopedInstances = new IdentityHashMap<>(); - /** - * All native-scoped instances are added to this structure upon creation. - *

    - * The hash map takes a resource owner to the doubly-linked list of - * instances it owns. The list is implemented directly with the two list - * fields here (rather than by a Collections class), so that an instance can - * be unlinked with no searching in the case of {@code javaStateUnreachable} - * or {@code javaStateReleased}, where the instance to be unlinked is - * already at hand. The list head is of a dummy {@code DualState} subclass. - */ - private static final Map s_scopedInstances = - new HashMap<>(); - - /** Backward link in per-resource-owner list. */ + /** Backward link in per-lifespan list. */ private DualState m_prev; - /** Forward link in per-resource-owner list. */ + /** Forward link in per-lifespan list. */ private DualState m_next; /** @@ -605,23 +592,6 @@ boolean inCleanup() catch ( JMException e ) { /* XXX */ } } - /** - * Pointer value of the {@code ResourceOwner} this instance belongs to, - * if any. - */ - protected final long m_resourceOwner; - - /** - * Check that a cookie is valid, throwing an unchecked exception otherwise. - */ - protected static void checkCookie(Key cookie) - { - assert Backend.threadMayEnterPG(); - if ( ! Key.class.isInstance(cookie) ) - throw new UnsupportedOperationException( - "Operation on DualState instance without cookie"); - } - /** Flag held in lock state showing the native state has been released. */ private static final int NATIVE_RELEASED = 0x80000000; /** Flag held in lock state showing the Java state has been released. */ @@ -653,7 +623,7 @@ protected static void checkCookie(Key cookie) /** * Return the argument; convenient breakpoint target for failed assertions. */ - static T m(T detail) + public static T m(T detail) { return detail; } @@ -667,30 +637,24 @@ static T m(T detail) * some confidence that constructor parameters representing native values * are for real, and also that the construction is taking place on a thread * holding the native lock, keeping the concurrency story simple. - * @param cookie Capability held by native code to invoke {@code DualState} - * constructors. * @param referent The Java object whose state this instance represents. - * @param resourceOwner Pointer value of the native {@code ResourceOwner} + * @param lifespan {@link Lifespan Lifespan} * whose release callback will indicate that this object's native state is - * no longer valid. If zero (a NULL pointer in C), it indicates that the + * no longer valid. If null, it indicates that the * state is held in long-lived native memory (such as JavaMemoryContext), * and can only be released via {@code javaStateUnreachable} or * {@code javaStateReleased}. */ - protected DualState(Key cookie, T referent, long resourceOwner) + protected DualState(T referent, Lifespan lifespan) { super(referent, s_releasedInstances); - checkCookie(cookie); - long scoped = 0L; - m_resourceOwner = resourceOwner; - assert Backend.threadMayEnterPG() : m("DualState construction"); /* * The following stanza publishes 'this' into one of the static data - * structures, for resource-owner-scoped or non-native-scoped instances, + * structures, for lifespan-scoped or non-native-scoped instances, * respectively. That may look like escape of 'this' from an unfinished * constructor, but the structures are private, and only manipulated * during construction and release, always on the thread cleared to @@ -700,15 +664,10 @@ protected DualState(Key cookie, T referent, long resourceOwner) * That will happen after this constructor returns, so the reference is * safely published. */ - if ( 0 != resourceOwner ) + if ( null != lifespan ) { scoped = 1L; - DualState.ListHead head = s_scopedInstances.get(resourceOwner); - if ( null == head ) - { - head = new DualState.ListHead(resourceOwner); - s_scopedInstances.put(resourceOwner, head); - } + ListHead head = (ListHead)lifespan; m_prev = head; m_next = ((DualState)head).m_next; m_prev.m_next = m_next.m_prev = this; @@ -721,25 +680,24 @@ protected DualState(Key cookie, T referent, long resourceOwner) /** * Private constructor only for dummy instances to use as the list heads - * for per-resource-owner lists. + * for per-lifespan lists. */ - private DualState(T referent, long resourceOwner) + private DualState(T referent) { super(referent); // as a WeakReference subclass, must have a referent super.clear(); // but nobody ever said for how long. - m_resourceOwner = resourceOwner; m_prev = m_next = this; m_waiters = null; } /** - * Method that will be called when the associated {@code ResourceOwner} + * Method that will be called when the associated {@code Lifespan} * is released, indicating that the native portion of the state * is no longer valid. The implementing class should clean up * whatever is appropriate to that event. *

    * This object's exclusive {@code lock()} will always be held when this - * method is called during resource owner release. The class whose state + * method is called during lifespan release. The class whose state * this is must use {@link #pin() pin()}, followed by * {@link #unpin() unpin()} in a {@code finally} block, around every * (ideally short) block of code that could refer to the native state. @@ -768,7 +726,7 @@ protected void nativeStateReleased(boolean javaStateLive) * live-instances data structures; that will have been done just before * this method is called. * @param nativeStateLive true is passed if the instance's "native state" is - * still considered live, that is, no resource-owner callback has been + * still considered live, that is, no lifespan callback has been * invoked to stamp it invalid (nor has it been "adopted"). */ protected void javaStateUnreachable(boolean nativeStateLive) @@ -795,7 +753,7 @@ protected void javaStateUnreachable(boolean nativeStateLive) * This default implementation calls {@code javaStateUnreachable}, which, in * typical cases, will have the same cleanup to do. * @param nativeStateLive true is passed if the instance's "native state" is - * still considered live, that is, no resource-owner callback has been + * still considered live, that is, no lifespan callback has been * invoked to stamp it invalid (nor has it been "adopted"). */ protected void javaStateReleased(boolean nativeStateLive) @@ -982,6 +940,49 @@ public final void pin() throws SQLException throw new SQLException(releasedMessage(), releasedSqlState()); } + /** + * Obtains a pin on this state, returning an + * {@link AutoCloseable AutoCloseable} instance that can be used in a + * {@code try}-with resources statement to ensure it is unpinned. + * @throws SQLException if the native state or the Java state has been + * released. + */ + public final Pinned pinned() throws SQLException + { + pin(); + return this::unpin; + } + + /** + * Obtains a pin on this state, returning an + * {@link AutoCloseable AutoCloseable} instance that can be used in a + * {@code try}-with resources statement to ensure it is unpinned. + * @throws IllegalStateException for use where a checked exception is not + * wanted, any resulting SQLException will be wrapped in + * IllegalStateException + */ + public final Pinned pinnedNoChecked() + { + try + { + return pinned(); + } + catch ( SQLException e ) + { + throw new IllegalStateException(e.getMessage(), e); + } + } + + /** + * A subinterface of {@link AutoCloseable AutoCloseable} whose {@code close} + * method throws no checked exceptions. + */ + @FunctionalInterface + public interface Pinned extends AutoCloseable + { + public void close(); + } + /** * Obtain a pin on this state, if it is still valid, blocking if necessary * until release of a lock. @@ -1002,6 +1003,27 @@ public final boolean pinUnlessReleased() return !z(_pin()); } + /** + * Runs r with this state pinned, unless the state has already + * been released, completing normally without running r in that + * case. + */ + public final void unlessReleased( + Checked.Runnable r) + throws E + { + if ( pinUnlessReleased() ) + return; + try + { + r.run(); + } + finally + { + unpin(); + } + } + /** * Workhorse for {@code pin()} and {@code pinUnlessReleased()}. * @return zero if the pin was obtained, otherwise {@code NATIVE_RELEASED}, @@ -1054,8 +1076,9 @@ private final int _pin() * null for most DualState instances, and be 'inflated' by having a * queue installed when first needed. That requires a null check here. */ - if ( null != m_waiters ) - m_waiters.add(thr); + Queue queue = m_waiters; + if ( null != queue ) + queue.add(thr); else { /* @@ -1527,12 +1550,10 @@ protected final void unlock(int s, boolean isNativeRelease) * nor {@code JAVA_RELEASED} flag may be set. This method is non-blocking * and will simply throw an exception if these preconditions are not * satisfied. - * @param cookie Capability held by native code to invoke special - * {@code DualState} methods. */ - protected final void adoptionLock(Key cookie) throws SQLException + protected final void adoptionLock() throws SQLException { - checkCookie(cookie); + assert threadMayEnterPG() : m("adoptionLock thread"); s_mutatorThread = Thread.currentThread(); assert pinnedByCurrentThread() : m("adoptionLock without pin"); int s = 1; // must be: quiescent (our pin only), unreleased @@ -1556,12 +1577,10 @@ protected final void adoptionLock(Key cookie) throws SQLException * and {@code JAVA_RELEASED} flags set. When the calling code releases the * prior pin it was expected to hold, the {@code javaStateReleased} callback * will execute. A value of false will be passed to both callbacks. - * @param cookie Capability held by native code to invoke special - * {@code DualState} methods. */ - protected final void adoptionUnlock(Key cookie) throws SQLException + protected final void adoptionUnlock() throws SQLException { - checkCookie(cookie); + assert threadMayEnterPG() : m("adoptionUnlock thread"); int s = NATIVE_RELEASED | JAVA_RELEASED | MUTATOR_HOLDS | 1 << WAITERS_SHIFT; int t = NATIVE_RELEASED | JAVA_RELEASED | 1; @@ -1647,7 +1666,7 @@ protected String releasedSqlState() /** * Produce a string describing this state object in a way useful for - * debugging, with such information as the associated {@code ResourceOwner} + * debugging, with such information as the associated {@code Lifespan} * and whether the state is fresh or stale. *

    * This method calls {@link #toString(Object)} passing {@code this}. @@ -1684,40 +1703,60 @@ public String toString(Object o) Class c = (null == o ? this : o).getClass(); String cn = c.getCanonicalName(); int pnl = c.getPackageName().length(); - return String.format("%s owner:%x %s", - cn.substring(1 + pnl), m_resourceOwner, + return String.format("%s lifespan:%s %s", + cn.substring(1 + pnl), lifespan(), z((int)s_stateVH.getVolatile(this) & NATIVE_RELEASED) ? "fresh" : "stale"); } /** - * Called only from native code by the {@code ResourceOwner} callback when a - * resource owner is being released. Must identify the live instances that - * have been registered to that owner, if any, and call their + * Return the {@code Lifespan} with which this instance is associated. + *

    + * As it is only needed for infrequent operations like {@code toString}, + * this is implemented simply by walking the circular list of owned objects + * back to the list head. + * @return the owning Lifespan, or null for an unscoped instance + */ + private Lifespan lifespan() + { + if ( this instanceof ListHead ) + return (Lifespan)this; + if ( null == m_prev ) + return null; + for ( DualState t = m_prev; t != this; t = t.m_prev ) + if ( t instanceof ListHead ) + return (Lifespan)t; + throw new AssertionError(m("degenerate owned-object list")); + } + + /** + * Called only on the PG thread when a + * lifespan is being released. Must identify the live instances that + * have been registered to that lifespan, if any, and call their * {@link #nativeStateReleased nativeStateReleased} methods. - * @param resourceOwner Pointer value identifying the resource owner being - * released. Calls can be received for resource owners to which no instances - * here have been registered. + * @param lifespan The lifespan being released, whose implementation must + * extend {@link ListHead ListHead}. Calls can be received for lifespans + * to which no instances here have been registered. *

    * Some state subclasses may have their nativeStateReleased methods called * from Java code, when it is clear the native state is no longer needed in - * Java. That doesn't remove the state instance from s_scopedInstances, + * Java. That doesn't unlink the state instance from its lifespan (if any) * though, so it will still eventually be seen by this loop and efficiently * removed by the iterator. Hence the {@code NATIVE_RELEASED} test, to avoid * invoking nativeStateReleased more than once. */ - private static void resourceOwnerRelease(long resourceOwner) + private static void lifespanRelease(Lifespan lifespan) { long total = 0L, release = 0L; - assert Backend.threadMayEnterPG() : m("resourceOwnerRelease thread"); + assert Backend.threadMayEnterPG() : m("lifespanRelease thread"); - DualState head = s_scopedInstances.remove(resourceOwner); + DualState head = (ListHead)lifespan; if ( null == head ) return; DualState t = head.m_next; - head.m_prev = head.m_next = null; + head.m_prev = head.m_next = head; for ( DualState s = t ; s != head ; s = t ) { t = s.m_next; @@ -1748,7 +1787,7 @@ private static void resourceOwnerRelease(long resourceOwner) } } - s_stats.resourceOwnerPoll(release, total); + s_stats.lifespanPoll(release, total); } /** @@ -1817,22 +1856,21 @@ else if ( z(NATIVE_RELEASED & state) ) /** * Remove this instance from the data structure holding it, for scoped - * instances if it has a non-zero resource owner, otherwise for unscoped + * instances if it is linked on a scoped list, otherwise for unscoped * instances. */ private void delist() { assert Backend.threadMayEnterPG() : m("DualState delist thread"); - if ( 0 == m_resourceOwner ) + if ( null == m_next ) { if ( null != s_unscopedInstances.remove(this) ) s_stats.delistUnscoped(); return; } - if ( null == m_prev || null == m_next ) - return; + // m_next is non-null, so m_prev had better be also. if ( this == m_prev.m_next ) m_prev.m_next = m_next; if ( this == m_next.m_prev ) @@ -1842,47 +1880,34 @@ private void delist() } /** - * Magic cookie needed as a constructor parameter to confirm that - * {@code DualState} subclass instances are being constructed from - * native code. - */ - public static final class Key - { - private static boolean constructed = false; - private Key() - { - synchronized ( Key.class ) - { - if ( constructed ) - throw new IllegalStateException("Duplicate DualState.Key"); - constructed = true; - } - } - } - - /** - * Dummy DualState concrete class whose instances only serve as list - * headers in per-resource-owner lists of instances. + * An otherwise nonfunctional DualState subclass whose instances only serve + * as list headers in per-lifespan lists of instances. + *

    + * Implementations of {@link Lifespan Lifespan} extend this. */ - private static class ListHead extends DualState // because why not? + public static abstract class ListHead + extends DualState // because why not? { /** - * Construct a {@code ListHead} instance. As a subclass of - * {@code DualState}, it can't help having a resource owner field, so - * may as well use it to store the resource owner that the list is for, - * in case it's of interest in debugging. - * @param owner The resource owner + * Construct a {@code ListHead} instance. + *

    + * The instance must be a concrete subtype of {@code Lifespan}. */ - private ListHead(long owner) + protected ListHead() { - super("", owner); // An instance needs an object to be its referent + super(""); // An instance needs some object to be its referent + assert this instanceof Lifespan : m( + getClass() + " does not implement Lifespan and may not " + + "extend DualState.ListHead"); } - @Override - public String toString(Object o) + /** + * Walk the chain of objects owned by this lifespan, signaling + * their release from native code. + */ + protected void lifespanRelease() { - return String.format( - "DualState.ListHead for resource owner %x", m_resourceOwner); + DualState.lifespanRelease((Lifespan)this); } } @@ -1902,9 +1927,9 @@ public static abstract class SingleGuardedLong extends DualState private final long m_guardedLong; protected SingleGuardedLong( - Key cookie, T referent, long resourceOwner, long guardedLong) + T referent, Lifespan span, long guardedLong) { - super(cookie, referent, resourceOwner); + super(referent, span); m_guardedLong = guardedLong; } @@ -1933,6 +1958,55 @@ protected final long guardedLong() } } + /** + * A {@code DualState} subclass serving only to guard access to a single + * nonnull {@code ByteBuffer} value. + *

    + * Nothing in particular is done to the native resource at the time of + * {@code javaStateReleased} or {@code javaStateUnreachable}; if it is + * subject to reclamation, this class assumes it will be shortly, in the + * normal operation of the native code. This can be appropriate for native + * state that was set up by a native caller for a short lifetime, such as a + * single function invocation. + */ + public static abstract class SingleGuardedBB extends DualState + { + private final ByteBuffer m_guardedBuffer; + + protected SingleGuardedBB( + T referent, Lifespan span, ByteBuffer guardedBuffer) + { + super(referent, span); + m_guardedBuffer = requireNonNull(guardedBuffer); + assert guardedBuffer.isDirect() : "GuardedBB is not direct"; + } + + @Override + public String toString(Object o) + { + return + String.format( + formatString(), super.toString(o), m_guardedBuffer); + } + + /** + * Return a {@code printf} format string resembling + * {@code "%s something(%s)"} where the second {@code %s} will be + * the value being guarded; the "something" should indicate what the + * value represents, or what will be done with it when released by Java. + */ + protected String formatString() + { + return "%s GuardedBB(%s)"; + } + + protected final ByteBuffer guardedBuffer() + { + assert pinnedByCurrentThread() : m("guardedBuffer() without pin"); + return m_guardedBuffer; + } + } + /** * A {@code DualState} subclass whose only native resource releasing action * needed is {@code pfree} of a single pointer. @@ -1940,9 +2014,9 @@ protected final long guardedLong() public static abstract class SinglePfree extends SingleGuardedLong { protected SinglePfree( - Key cookie, T referent, long resourceOwner, long pfreeTarget) + T referent, Lifespan span, long pfreeTarget) { - super(cookie, referent, resourceOwner, pfreeTarget); + super(referent, span, pfreeTarget); } @Override @@ -1978,18 +2052,33 @@ protected void javaStateUnreachable(boolean nativeStateLive) * native code is responsible for whatever happens to it next. */ public static abstract class SingleMemContextDelete - extends SingleGuardedLong + extends DualState { + private final MemoryContext m_context; + protected SingleMemContextDelete( - Key cookie, T referent, long resourceOwner, long memoryContext) + T referent, Lifespan span, MemoryContext cxt) { - super(cookie, referent, resourceOwner, memoryContext); + super(referent, span); + m_context = cxt; } @Override + public String toString(Object o) + { + return + String.format(formatString(), super.toString(o), m_context); + } + public String formatString() { - return "%s MemoryContextDelete(%x)"; + return "%s MemoryContextDelete(%s)"; + } + + protected final MemoryContext memoryContext() + { + assert pinnedByCurrentThread() : m("memoryContext() without pin"); + return m_context; } /** @@ -2003,7 +2092,7 @@ protected void javaStateUnreachable(boolean nativeStateLive) { assert Backend.threadMayEnterPG(); if ( nativeStateLive ) - _memContextDelete(guardedLong()); + _memContextDelete(((Addressed)memoryContext()).address()); } private native void _memContextDelete(long pointer); @@ -2017,9 +2106,9 @@ public static abstract class SingleFreeTupleDesc extends SingleGuardedLong { protected SingleFreeTupleDesc( - Key cookie, T referent, long resourceOwner, long ftdTarget) + T referent, Lifespan span, long ftdTarget) { - super(cookie, referent, resourceOwner, ftdTarget); + super(referent, span, ftdTarget); } @Override @@ -2053,9 +2142,9 @@ public static abstract class SingleHeapFreeTuple extends SingleGuardedLong { protected SingleHeapFreeTuple( - Key cookie, T referent, long resourceOwner, long hftTarget) + T referent, Lifespan span, long hftTarget) { - super(cookie, referent, resourceOwner, hftTarget); + super(referent, span, hftTarget); } @Override @@ -2089,9 +2178,9 @@ public static abstract class SingleFreeErrorData extends SingleGuardedLong { protected SingleFreeErrorData( - Key cookie, T referent, long resourceOwner, long fedTarget) + T referent, Lifespan span, long fedTarget) { - super(cookie, referent, resourceOwner, fedTarget); + super(referent, span, fedTarget); } @Override @@ -2117,6 +2206,42 @@ protected void javaStateUnreachable(boolean nativeStateLive) private native void _freeErrorData(long pointer); } + /** + * A {@code DualState} subclass whose only native resource releasing action + * needed is {@code SPI_freetuptable} of a single pointer. + */ + public static abstract class SingleSPIfreetuptable + extends SingleGuardedLong + { + protected SingleSPIfreetuptable( + T referent, Lifespan span, long fttTarget) + { + super(referent, span, fttTarget); + } + + @Override + public String formatString() + { + return "%s SPI_freetuptable(%x)"; + } + + /** + * When the Java state is released or unreachable, an + * {@code SPI_freetuptable} + * call is made so the native memory is released without having to wait + * for release of its containing context. + */ + @Override + protected void javaStateUnreachable(boolean nativeStateLive) + { + assert Backend.threadMayEnterPG(); + if ( nativeStateLive ) + _spiFreeTupTable(guardedLong()); + } + + private native void _spiFreeTupTable(long pointer); + } + /** * A {@code DualState} subclass whose only native resource releasing action * needed is {@code SPI_freeplan} of a single pointer. @@ -2125,9 +2250,9 @@ public static abstract class SingleSPIfreeplan extends SingleGuardedLong { protected SingleSPIfreeplan( - Key cookie, T referent, long resourceOwner, long fpTarget) + T referent, Lifespan span, long fpTarget) { - super(cookie, referent, resourceOwner, fpTarget); + super(referent, span, fpTarget); } @Override @@ -2161,9 +2286,9 @@ public static abstract class SingleSPIcursorClose extends SingleGuardedLong { protected SingleSPIcursorClose( - Key cookie, T referent, long resourceOwner, long ccTarget) + T referent, Lifespan span, long ccTarget) { - super(cookie, referent, resourceOwner, ccTarget); + super(referent, span, ccTarget); } @Override @@ -2202,6 +2327,42 @@ protected void javaStateUnreachable(boolean nativeStateLive) private native void _spiCursorClose(long pointer); } + /** + * A {@code DualState} subclass whose only native resource releasing action + * needed is {@code heap_freetuple} of the address of a direct byte buffer. + */ + public static abstract class BBHeapFreeTuple + extends SingleGuardedBB + { + protected BBHeapFreeTuple( + T referent, Lifespan span, ByteBuffer hftTarget) + { + super(referent, span, hftTarget); + } + + @Override + public String formatString() + { + return"%s heap_freetuple(%s)"; + } + + /** + * When the Java state is released or unreachable, a + * {@code heap_freetuple} + * call is made so the native memory is released without having to wait + * for release of its containing context. + */ + @Override + protected void javaStateUnreachable(boolean nativeStateLive) + { + assert Backend.threadMayEnterPG(); + if ( nativeStateLive ) + _heapFreeTuple(guardedBuffer()); + } + + private native void _heapFreeTuple(ByteBuffer tuple); + } + /** * Bean exposing some {@code DualState} allocation and lifecycle statistics * for viewing in a JMX management client. @@ -2248,9 +2409,9 @@ public long getNativeReleased() return nativeReleased.sum(); } - public long getResourceOwnerPasses() + public long getLifespanPasses() { - return resourceOwnerPasses.sum(); + return lifespanPasses.sum(); } public long getReferenceQueuePasses() @@ -2297,7 +2458,7 @@ public long getReleaseReleaseRaces() private LongAdder javaUnreachable = new LongAdder(); private LongAdder javaReleased = new LongAdder(); private LongAdder nativeReleased = new LongAdder(); - private LongAdder resourceOwnerPasses = new LongAdder(); + private LongAdder lifespanPasses = new LongAdder(); private LongAdder referenceQueuePasses = new LongAdder(); private LongAdder referenceQueueItems = new LongAdder(); private LongAdder contendedLocks = new LongAdder(); @@ -2313,9 +2474,9 @@ final void construct(long scoped) enlistedUnscoped.add(1L - scoped); } - final void resourceOwnerPoll(long released, long total) + final void lifespanPoll(long released, long total) { - resourceOwnerPasses.increment(); + lifespanPasses.increment(); nativeReleased.add(released); delistedScoped.add(total); } diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/ErrorData.java b/pljava/src/main/java/org/postgresql/pljava/internal/ErrorData.java index e667d23f8..e0cc20e29 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/ErrorData.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/ErrorData.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2021 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2022 Tada AB and other contributors, as listed below. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the The BSD 3-Clause License @@ -27,18 +27,24 @@ public class ErrorData { private final State m_state; - ErrorData(DualState.Key cookie, long resourceOwner, long pointer) + ErrorData(long pointer) { - m_state = new State(cookie, this, resourceOwner, pointer); + m_state = new State(this, pointer); } private static class State extends DualState.SingleFreeErrorData { - private State( - DualState.Key cookie, ErrorData ed, long ro, long ht) + private State(ErrorData ed, long ht) { - super(cookie, ed, ro, ht); + /* + * Passing null as the Lifespan means this will never be + * matched by a lifespanRelease call; that's appropriate (for now) as + * the ErrorData copy is being made into JavaMemoryContext, which + * never gets reset, so only unreachability from the Java side + * will free it. + */ + super(ed, null, ht); } /** diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/ExecutionPlan.java b/pljava/src/main/java/org/postgresql/pljava/internal/ExecutionPlan.java index 86ac4c418..3e1291917 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/ExecutionPlan.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/ExecutionPlan.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2019 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2022 Tada AB and other contributors, as listed below. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the The BSD 3-Clause License @@ -79,10 +79,10 @@ public class ExecutionPlan private static class State extends DualState.SingleSPIfreeplan { - private State( - DualState.Key cookie, ExecutionPlan jep, long ro, long ep) + private State(ExecutionPlan jep, long ep) { - super(cookie, jep, ro, ep); + /* null as Lifespan: the saved plan isn't transient */ + super(jep, null, ep); } /** @@ -200,11 +200,10 @@ public int hashCode() : cacheSize)); } - private ExecutionPlan(DualState.Key cookie, long resourceOwner, - Object planKey, long spiPlan) + private ExecutionPlan(Object planKey, long spiPlan) { m_key = planKey; - m_state = new State(cookie, this, resourceOwner, spiPlan); + m_state = new State(this, spiPlan); } /** diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/Function.java b/pljava/src/main/java/org/postgresql/pljava/internal/Function.java index 6015bde7d..710264a27 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/Function.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/Function.java @@ -37,11 +37,6 @@ import java.lang.invoke.WrongMethodTypeException; import java.lang.reflect.Array; -import java.lang.reflect.Method; -import java.lang.reflect.GenericDeclaration; -import java.lang.reflect.ParameterizedType; -import java.lang.reflect.Type; -import java.lang.reflect.TypeVariable; import java.nio.ByteBuffer; import java.nio.ByteOrder; @@ -1892,145 +1887,6 @@ private static String getAS(ResultSet procTup) throws SQLException "(" + javaTypeName + ")(" + arrayDims + ")?+" ); - /** - * Test whether the type {@code t0} is, directly or indirectly, - * a specialization of generic type {@code c0}. - * @param t0 a type to be checked - * @param c0 known generic type to check for - * @return null if {@code t0} does not extend {@code c0}, otherwise the - * array of type arguments with which it specializes {@code c0} - */ - private static Type[] specialization(Type t0, Class c0) - { - Type t = t0; - Class c; - ParameterizedType pt = null; - TypeBindings latestBindings = null; - Type[] actualArgs = null; - - if ( t instanceof Class ) - { - c = (Class)t; - if ( ! c0.isAssignableFrom(c) ) - return null; - if ( c0 == c ) - return new Type[0]; - } - else if ( t instanceof ParameterizedType ) - { - pt = (ParameterizedType)t; - c = (Class)pt.getRawType(); - if ( ! c0.isAssignableFrom(c) ) - return null; - if ( c0 == c ) - actualArgs = pt.getActualTypeArguments(); - else - latestBindings = new TypeBindings(null, pt); - } - else - throw new AssertionError( - "expected Class or ParameterizedType, got: " + t); - - if ( null == actualArgs ) - { - List pending = new LinkedList<>(); - pending.add(c.getGenericSuperclass()); - addAll(pending, c.getGenericInterfaces()); - - while ( ! pending.isEmpty() ) - { - t = pending.remove(0); - if ( null == t ) - continue; - if ( t instanceof Class ) - { - c = (Class)t; - if ( c0 == c ) - return new Type[0]; - } - else if ( t instanceof ParameterizedType ) - { - pt = (ParameterizedType)t; - c = (Class)pt.getRawType(); - if ( c0 == c ) - { - actualArgs = pt.getActualTypeArguments(); - break; - } - if ( c0.isAssignableFrom(c) ) - pending.add(new TypeBindings(latestBindings, pt)); - } - else if ( t instanceof TypeBindings ) - { - latestBindings = (TypeBindings)t; - continue; - } - else - throw new AssertionError( - "expected Class or ParameterizedType, got: " + t); - if ( ! c0.isAssignableFrom(c) ) - continue; - pending.add(c.getGenericSuperclass()); - addAll(pending, c.getGenericInterfaces()); - } - } - if ( null == actualArgs ) - throw new AssertionError( - "failed checking whether " + t0 + " specializes " + c0); - - for ( int i = 0; i < actualArgs.length; ++ i ) - if ( actualArgs[i] instanceof TypeVariable ) - actualArgs[i] = - latestBindings.resolve((TypeVariable)actualArgs[i]); - - return actualArgs; - } - - /** - * A class recording the bindings made in a ParameterizedType to the type - * parameters in a GenericDeclaration. Implements {@code Type} so it - * can be added to the {@code pending} queue in {@code specialization}. - *

    - * In {@code specialization}, the tree of superclasses/superinterfaces will - * be searched breadth-first, with all of a node's immediate supers enqueued - * before any from the next level. By recording a node's type variable to - * type argument bindings in an object of this class, and enqueueing it - * before any of the node's supers, any type variables encountered as actual - * type arguments to any of those supers should be resolvable in the object - * of this class most recently dequeued. - */ - static class TypeBindings implements Type - { - private final TypeVariable[] formalTypeParams; - private final Type[] actualTypeArgs; - - TypeBindings(TypeBindings prior, ParameterizedType pt) - { - actualTypeArgs = pt.getActualTypeArguments(); - formalTypeParams = - ((GenericDeclaration)pt.getRawType()).getTypeParameters(); - assert actualTypeArgs.length == formalTypeParams.length; - - if ( null == prior ) - return; - - for ( int i = 0; i < actualTypeArgs.length; ++ i ) - { - Type t = actualTypeArgs[i]; - if ( actualTypeArgs[i] instanceof TypeVariable ) - actualTypeArgs[i] = prior.resolve((TypeVariable)t); - } - } - - Type resolve(TypeVariable v) - { - for ( int i = 0; i < formalTypeParams.length; ++ i ) - if ( formalTypeParams[i].equals(v) ) - return actualTypeArgs[i]; - throw new AssertionError("type binding not found for " + v); - } - } - /** * Wrap the native method to store the values computed in Java, for a * non-UDT function, into the C {@code Function} structure. Returns an array diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java b/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java index 67c959122..d1c175746 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java @@ -43,6 +43,7 @@ import org.postgresql.pljava.policy.TrialPolicy; import static org.postgresql.pljava.annotation.processing.DDRWriter.eQuote; import static org.postgresql.pljava.elog.ELogHandler.LOG_WARNING; +import static org.postgresql.pljava.model.CharsetEncoding.SERVER_ENCODING; import static org.postgresql.pljava.sqlgen.Lexicals.Identifier.Simple; /** @@ -149,25 +150,15 @@ public static String hello( System.clearProperty(orderKey + ".scalar"); System.clearProperty(orderKey + ".mirror"); - String encodingKey = "org.postgresql.server.encoding"; - String encName = System.getProperty(encodingKey); - if ( null == encName ) - encName = Backend.getConfigOption( "server_encoding"); - try - { - Charset cs = Charset.forName(encName); - org.postgresql.pljava.internal.Session.s_serverCharset = cs; // poke - System.setProperty(encodingKey, cs.name()); - } - catch ( IllegalArgumentException iae ) - { - System.clearProperty(encodingKey); - } + SERVER_ENCODING.charset(); // this must be set before beginEnforcing() - /* so it can be granted permissions in the pljava policy */ + /* so they can be granted permissions in the pljava policy */ System.setProperty( "org.postgresql.pljava.codesource", InstallHelper.class.getProtectionDomain().getCodeSource() .getLocation().toString()); + System.setProperty( "org.postgresql.pljava.codesource.api", + Simple.class.getProtectionDomain().getCodeSource() + .getLocation().toString()); setPolicyURLs(); diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/Invocation.java b/pljava/src/main/java/org/postgresql/pljava/internal/Invocation.java new file mode 100644 index 000000000..35c2699a4 --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/internal/Invocation.java @@ -0,0 +1,179 @@ +/* + * Copyright (c) 2004-2022 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Tada AB + * Chapman Flack + */ +package org.postgresql.pljava.internal; + +import java.lang.annotation.Native; + +import static java.lang.Integer.highestOneBit; + +import java.nio.ByteBuffer; +import static java.nio.ByteOrder.nativeOrder; + +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.logging.Logger; + +import org.postgresql.pljava.internal.Backend; +import static org.postgresql.pljava.internal.Backend.doInPG; +import org.postgresql.pljava.internal.PgSavepoint; +import org.postgresql.pljava.internal.LifespanImpl; + +import org.postgresql.pljava.model.MemoryContext; + +import static org.postgresql.pljava.pg.DatumUtils.fetchPointer; +import org.postgresql.pljava.pg.MemoryContextImpl; + +/** + * One invocation, from PostgreSQL, of functionality implemented using PL/Java. + *

    + * This class is the Java counterpart of the {@code struct Invocation_} in the + * C code, but while there is a new stack-allocated C structure on every entry + * from PG to PL/Java, no instance of this class is created unless requested + * (with {@link #current current()}; once requested, a reference to it is saved + * in the C struct for the duration of the invocation. + * @author Thomas Hallgren + */ +public class Invocation extends LifespanImpl +{ + @Native private static final int OFFSET_nestLevel = 0; + @Native private static final int OFFSET_hasDual = 4; + @Native private static final int OFFSET_errorOccurred = 5; + @Native private static final int OFFSET_upperContext = 8; + + private static final ByteBuffer s_window = + EarlyNatives._window().order(nativeOrder()); + + /** + * The current "stack" of invocations. + */ + private static Invocation[] s_levels = new Invocation[10]; + + /** + * Nesting level for this invocation + */ + private final int m_nestingLevel; + + /** + * Top level savepoint relative to this invocation. + */ + private PgSavepoint m_savepoint; + + private Invocation(int level) + { + m_nestingLevel = level; + } + + /** + * @return The nesting level of this invocation + */ + public int getNestingLevel() + { + return m_nestingLevel; + } + + /** + * @return Returns the savePoint. + */ + public final PgSavepoint getSavepoint() + { + return m_savepoint; + } + + /** + * @param savepoint The savepoint to set. + */ + public final void setSavepoint(PgSavepoint savepoint) + { + m_savepoint = savepoint; + } + + /** + * Called only from the static {@code onExit} below when the invocation + * is popped; should not be invoked any other way. + */ + private void onExit(boolean withError) + throws SQLException + { + try + { + if(m_savepoint != null) + m_savepoint.onInvocationExit(withError); + } + finally + { + m_savepoint = null; + lifespanRelease(); + } + } + + /** + * The actual entry point from JNI, which passes a valid nestLevel. + *

    + * Forwards to the instance method at the corresponding level. + */ + private static void onExit(int nestLevel, boolean withError) + throws SQLException + { + s_levels[nestLevel].onExit(withError); + } + + /** + * @return The current invocation + */ + public static Invocation current() + { + return doInPG(() -> + { + Invocation curr; + int level = s_window.getInt(OFFSET_nestLevel); + int top = s_levels.length; + + if(level >= top) + { + int newSize = highestOneBit(level) << 1; + Invocation[] levels = new Invocation[newSize]; + System.arraycopy(s_levels, 0, levels, 0, top); + s_levels = levels; + } + + curr = s_levels[level]; + if ( null == curr ) + s_levels[level] = curr = new Invocation(level); + + s_window.put(OFFSET_hasDual, (byte)1); + return curr; + }); + } + + /** + * The "upper executor" memory context (that is, the context on entry, prior + * to any {@code SPI_connect}) associated with the current (innermost) + * invocation. + */ + public static MemoryContext upperExecutorContext() + { + return + doInPG(() -> MemoryContextImpl.fromAddress( + fetchPointer(s_window, OFFSET_upperContext))); + } + + public static void clearErrorCondition() + { + doInPG(() -> s_window.put(OFFSET_errorOccurred, (byte)0)); + } + + private static class EarlyNatives + { + private static native ByteBuffer _window(); + } +} diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/LifespanImpl.java b/pljava/src/main/java/org/postgresql/pljava/internal/LifespanImpl.java new file mode 100644 index 000000000..d64f15774 --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/internal/LifespanImpl.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.internal; + +import java.lang.ref.Reference; // for javadoc + +import org.postgresql.pljava.Lifespan; + +import org.postgresql.pljava.internal.DualState; + +/** + * Implements PL/Java's generalized notion of lifespans. + *

    + * Subclasses are likely to maintain cache mappings from addresses of PostgreSQL + * native objects to instances. Such mappings must hold strong references to the + * instances, because any {@code LifespanImpl} instance can serve as the + * head of a list of {@code DualState} objects, which are + * {@link Reference Reference} instances, and the Java runtime will cease + * tracking those if they themselves are not kept strongly reachable. This + * requirement is acceptable, because all instances represent bounded lifespans + * that end with explicit invalidation and decaching; that's what they're for, + * after all. + */ +public class LifespanImpl extends DualState.ListHead implements Lifespan +{ + public interface Addressed + { + long address(); + } + + /** + * Overrides the version provided by {@code DualState} to simply call + * the niladic {@code toString}, as a resource owner isn't directly + * associated with another object the way a {@code DualState} instance + * generally is. + */ + @Override + public String toString(Object o) + { + assert null == o || this == o; + return toString(); + } + + @Override + public String toString() + { + Class c = getClass(); + String cn = c.getCanonicalName(); + int pnl = c.getPackageName().length(); + return cn.substring(1 + pnl); + } +} diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/PgSavepoint.java b/pljava/src/main/java/org/postgresql/pljava/internal/PgSavepoint.java index cbe5e8e6c..5cca9d889 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/PgSavepoint.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/PgSavepoint.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2020 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2022 Tada AB and other contributors, as listed below. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the The BSD 3-Clause License @@ -13,6 +13,10 @@ package org.postgresql.pljava.internal; import static org.postgresql.pljava.internal.Backend.doInPG; +import org.postgresql.pljava.internal.LifespanImpl; + +import org.postgresql.pljava.pg.MemoryContextImpl; +import org.postgresql.pljava.pg.ResourceOwnerImpl; import java.sql.Connection; import java.sql.SQLException; @@ -71,6 +75,28 @@ private static void forgetNestLevelsGE(int nestLevel) */ private final String m_name; + /* + * Not especially well documented upstream, but following the example of + * plpgsql/perl/python, the current memory context must be saved before + * calling BeginInternalSubTransaction, and then reimposed afterward, and + * reimposed again later after release or rollback-and-release. During the + * subtransaction, its associated context will of course be available as + * CurTransactionMemoryContext, but we will avoid surprising the caller + * with changes to CurrentMemoryContext. + */ + private final long m_priorMemoryContext; + + /* + * Not especially well documented upstream, but following the example of + * plpgsql/perl/python, the current resource owner must be saved before + * calling BeginInternalSubTransaction, and then reimposed later, after + * release or rollback-and-release. Unlike the memory context, the owner is + * not reimposed immediately after entering the subtransaction, so the newly + * established owner is the CurrentResourceOwner during the subtransaction, + * and the former one is reimposed only at its (normal or abnormal) end. + */ + private final long m_priorResourceOwner; + /* * The transaction ID assigned during BeginInternalSubTransaction is really * the identifier that matters. An instance will briefly have the default @@ -119,9 +145,12 @@ private static void forgetNestLevelsGE(int nestLevel) */ private static PgSavepoint s_nursery; - private PgSavepoint(String name) + private PgSavepoint( + String name, long priorMemoryContext, long priorResourceOwner) { m_name = name; + m_priorMemoryContext = priorMemoryContext; + m_priorResourceOwner = priorResourceOwner; } /** @@ -135,7 +164,9 @@ public static PgSavepoint set(String name) { return doInPG(() -> { - PgSavepoint sp = new PgSavepoint(name); + long mc = MemoryContextImpl.getCurrentRaw(); + long ro = ResourceOwnerImpl.getCurrentRaw(); + PgSavepoint sp = new PgSavepoint(name, mc, ro); s_nursery = sp; try { @@ -148,6 +179,7 @@ public static PgSavepoint set(String name) finally { s_nursery = null; + MemoryContextImpl.setCurrentRaw(mc); } s_knownSavepoints.put(sp, Boolean.TRUE); return sp; @@ -165,6 +197,7 @@ static PgSavepoint forId(int savepointId) PgSavepoint sp = s_nursery; sp.m_xactId = savepointId; s_nursery = null; + MemoryContextImpl.setCurrentRaw(sp.m_priorMemoryContext); return sp; } for ( PgSavepoint sp : s_knownSavepoints.keySet() ) @@ -204,6 +237,8 @@ public void release() " that is no longer valid", "3B001"); _release(m_xactId, m_nestLevel); + MemoryContextImpl.setCurrentRaw(m_priorMemoryContext); + ResourceOwnerImpl.setCurrentRaw(m_priorResourceOwner); forgetNestLevelsGE(m_nestLevel); }); } @@ -230,6 +265,8 @@ public void rollback() " that is no longer valid", "3B001"); _rollback(m_xactId, m_nestLevel); + MemoryContextImpl.setCurrentRaw(m_priorMemoryContext); + ResourceOwnerImpl.setCurrentRaw(m_priorResourceOwner); /* Forget only more-deeply-nested savepoints, NOT this one */ forgetNestLevelsGE(1 + m_nestLevel); @@ -248,6 +285,7 @@ public void rollback() finally { s_nursery = null; + MemoryContextImpl.setCurrentRaw(m_priorMemoryContext); } }); } @@ -286,6 +324,8 @@ public void onInvocationExit(boolean withError) */ _release(m_xactId, m_nestLevel); forgetNestLevelsGE(m_nestLevel); + MemoryContextImpl.setCurrentRaw(m_priorMemoryContext); + ResourceOwnerImpl.setCurrentRaw(m_priorResourceOwner); } else { @@ -299,6 +339,8 @@ public void onInvocationExit(boolean withError) */ _rollback(m_xactId, m_nestLevel); forgetNestLevelsGE(m_nestLevel); + MemoryContextImpl.setCurrentRaw(m_priorMemoryContext); + ResourceOwnerImpl.setCurrentRaw(m_priorResourceOwner); } } diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/Portal.java b/pljava/src/main/java/org/postgresql/pljava/internal/Portal.java index 790581382..91bb3530e 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/Portal.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/Portal.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2019 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2023 Tada AB and other contributors, as listed below. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the The BSD 3-Clause License @@ -12,18 +12,32 @@ */ package org.postgresql.pljava.internal; -import org.postgresql.pljava.internal.SPI; // for javadoc import static org.postgresql.pljava.internal.Backend.doInPG; +import static org.postgresql.pljava.internal.Backend.threadMayEnterPG; +import org.postgresql.pljava.internal.SPI; + +import org.postgresql.pljava.Lifespan; + +import org.postgresql.pljava.model.MemoryContext; +import org.postgresql.pljava.model.TupleDescriptor; +import org.postgresql.pljava.model.TupleTableSlot; + +import org.postgresql.pljava.pg.MemoryContextImpl; +import static org.postgresql.pljava.pg.MemoryContextImpl.allocatingIn; +import org.postgresql.pljava.pg.ResourceOwnerImpl; +import org.postgresql.pljava.pg.TupleTableSlotImpl; import java.sql.SQLException; +import java.util.List; + /** * The Portal correspons to the internal PostgreSQL * Portal type. * * @author Thomas Hallgren */ -public class Portal +public class Portal implements org.postgresql.pljava.model.Portal { /* * Hold a reference to the Java ExecutionPlan object as long as we might be @@ -32,11 +46,32 @@ public class Portal */ private ExecutionPlan m_plan; + private TupleDescriptor m_tupdesc; + + private TupleTableSlotImpl m_slot; + private final State m_state; - Portal(DualState.Key cookie, long ro, long pointer, ExecutionPlan plan) + private final MemoryContext m_context; + + private static final int FETCH_FORWARD = 0; + private static final int FETCH_BACKWARD = 1; + private static final int FETCH_ABSOLUTE = 2; + private static final int FETCH_RELATIVE = 3; + private static final long FETCH_ALL = ALL; + + static + { + assert FETCH_FORWARD == Direction.FORWARD .ordinal(); + assert FETCH_BACKWARD == Direction.BACKWARD.ordinal(); + assert FETCH_ABSOLUTE == Direction.ABSOLUTE.ordinal(); + assert FETCH_RELATIVE == Direction.RELATIVE.ordinal(); + } + + Portal(long ro, long cxt, long pointer, ExecutionPlan plan) { - m_state = new State(cookie, this, ro, pointer); + m_state = new State(this, ResourceOwnerImpl.fromAddress(ro), pointer); + m_context = MemoryContextImpl.fromAddress(cxt); m_plan = plan; } @@ -44,9 +79,9 @@ private static class State extends DualState.SingleSPIcursorClose { private State( - DualState.Key cookie, Portal referent, long ro, long portal) + Portal referent, Lifespan span, long portal) { - super(cookie, referent, ro, portal); + super(referent, span, portal); } /** @@ -88,9 +123,81 @@ public void close() { m_state.releaseFromJava(); m_plan = null; + m_tupdesc = null; + m_slot = null; }); } + /** + * Returns the {@link TupleDescriptor} that describes the row tuples for + * this {@code Portal}. + * @throws SQLException if the handle to the native structure is stale. + */ + @Override + public TupleDescriptor tupleDescriptor() + throws SQLException + { + return doInPG(() -> + { + if ( null == m_tupdesc ) + m_tupdesc = _getTupleDescriptor(m_state.getPortalPtr()); + return m_tupdesc; + }); + } + + private TupleTableSlotImpl slot() throws SQLException + { + assert threadMayEnterPG(); // only call slot() on PG thread + if ( null == m_slot ) + { + try ( Checked.AutoCloseable ac = + allocatingIn(m_context) ) + { + m_slot = _makeTupleTableSlot( + m_state.getPortalPtr(), tupleDescriptor()); + } + } + return m_slot; + } + + @Override + public List fetch(Direction dir, long count) + throws SQLException + { + boolean forward; + switch ( dir ) + { + case FORWARD : forward = true ; break; + case BACKWARD: forward = false; break; + default: + throw new UnsupportedOperationException( + dir + " Portal mode not yet supported"); + } + + return doInPG(() -> + { + fetch(forward, count); // for now; it's already implemented + return SPI.getTuples(slot()); + }); + } + + @Override + public long move(Direction dir, long count) + throws SQLException + { + boolean forward; + switch ( dir ) + { + case FORWARD : forward = true ; break; + case BACKWARD: forward = false; break; + default: + throw new UnsupportedOperationException( + dir + " Portal mode not yet supported"); + } + + return move(forward, count); // for now; it's already implemented + } + /** * Returns the name of this Portal. * @throws SQLException if the handle to the native structure is stale. @@ -189,6 +296,13 @@ public long move(boolean forward, long count) return moved; } + private static native TupleDescriptor _getTupleDescriptor(long pointer) + throws SQLException; + + private static native TupleTableSlotImpl + _makeTupleTableSlot(long pointer, TupleDescriptor td) + throws SQLException; + private static native String _getName(long pointer) throws SQLException; diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/Relation.java b/pljava/src/main/java/org/postgresql/pljava/internal/Relation.java index f405ed92c..4860d88d4 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/Relation.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/Relation.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2019 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2022 Tada AB and other contributors, as listed below. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the The BSD 3-Clause License @@ -13,6 +13,7 @@ package org.postgresql.pljava.internal; import static org.postgresql.pljava.internal.Backend.doInPG; +import org.postgresql.pljava.internal.Invocation; import java.sql.SQLException; @@ -27,18 +28,17 @@ public class Relation private TupleDesc m_tupleDesc; private final State m_state; - Relation(DualState.Key cookie, long resourceOwner, long pointer) + Relation(long pointer) { - m_state = new State(cookie, this, resourceOwner, pointer); + m_state = new State(this, pointer); } private static class State extends DualState.SingleGuardedLong { - private State( - DualState.Key cookie, Relation r, long ro, long hth) + private State(Relation r, long hth) { - super(cookie, r, ro, hth); + super(r, Invocation.current(), hth); } /** diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/SPI.java b/pljava/src/main/java/org/postgresql/pljava/internal/SPI.java index 9151ad100..9754bdada 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/SPI.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/SPI.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2019 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2023 Tada AB and other contributors, as listed below. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the The BSD 3-Clause License @@ -12,8 +12,24 @@ */ package org.postgresql.pljava.internal; +import static java.lang.Math.multiplyExact; +import static java.lang.Math.toIntExact; + +import java.nio.ByteBuffer; + +import java.util.List; + import static org.postgresql.pljava.internal.Backend.doInPG; +import org.postgresql.pljava.model.TupleTableSlot; + +import static org.postgresql.pljava.pg.ModelConstants.SIZEOF_DATUM; +import org.postgresql.pljava.pg.TupleList; +import org.postgresql.pljava.pg.TupleTableSlotImpl; + +import static org.postgresql.pljava.pg.DatumUtils.asReadOnlyNativeOrder; +import static org.postgresql.pljava.pg.DatumUtils.fetchPointer; + /** * The SPI class provides access to some global * variables used by SPI. @@ -54,6 +70,23 @@ public class SPI public static final int OK_REL_UNREGISTER = 16; public static final int OK_TD_REGISTER = 17; + /* + * Indices into window array. + */ + private static final int SPI_result = 0; + private static final int SPI_processed = 1; + private static final int SPI_tuptable = 2; + + private static final ByteBuffer[] s_windows; + + static + { + ByteBuffer[] bs = EarlyNatives._window(ByteBuffer.class); + for ( int i = 0; i < bs.length; ++ i ) + bs[i] = asReadOnlyNativeOrder(bs[i]); + s_windows = bs; + } + /** * Execute a command using the internal SPI_exec function. * @param command The command to execute. @@ -64,11 +97,18 @@ public class SPI * @deprecated This seems never to have been used in git history of project. */ @Deprecated - private static int exec(String command, int rowCount) + public static int exec(String command, int rowCount) { return doInPG(() -> _exec(command, rowCount)); } + /** + * Frees a tuple table returned by SPI. + *

    + * This legacy method has no parameter, and frees whatever tuple table the + * {@code SPI_tuptable} global points to at the moment; beware if SPI has + * returned any newer result since the one you might think you are freeing! + */ public static void freeTupTable() { doInPG(SPI::_freeTupTable); @@ -79,7 +119,12 @@ public static void freeTupTable() */ public static long getProcessed() { - long count = doInPG(SPI::_getProcessed); + long count = doInPG(() -> + { + assert 8 == s_windows[SPI_processed].capacity() : + "SPI_processed width change"; + return s_windows[SPI_processed].getLong(0); + }); if ( count < 0 ) throw new ArithmeticException( "too many rows processed to count in a Java signed long"); @@ -91,11 +136,46 @@ public static long getProcessed() */ public static int getResult() { - return doInPG(SPI::_getResult); + return doInPG(() -> + { + assert 4 == s_windows[SPI_result].capacity() : + "SPI_result width change"; + return s_windows[SPI_result].getInt(0); + }); + } + + /** + * Returns a List of the supplied TupleTableSlot covering the tuples pointed + * to from the pointer array that the global {@code SPI_tuptable} points to. + *

    + * This is an internal, not an API, method, and it does nothing to check + * that the supplied ttsi fits the tuples SPI has returned. The caller is to + * ensure that. + * @return null if the global SPI_tuptable is null + */ + public static TupleList getTuples(TupleTableSlotImpl ttsi) + { + return doInPG(() -> + { + long p = fetchPointer(s_windows[SPI_tuptable], 0); + if ( 0 == p ) + return null; + + long count = getProcessed(); + if ( 0 == count ) + return TupleList.EMPTY; + + // An assertion in the C code checks SIZEOF_DATUM == SIZEOF_VOID_P + // XXX catch ArithmeticException, report a "program limit exceeded" + int sizeToMap = toIntExact(multiplyExact(count, SIZEOF_DATUM)); + + return _mapTupTable(ttsi, p, sizeToMap); + }); } /** - * Returns the value of the global variable SPI_tuptable. + * Returns the tuples located by the global variable {@code SPI_tuptable} + * as an instance of the legacy {@code TupleTable} class. */ public static TupleTable getTupTable(TupleDesc known) { @@ -149,11 +229,24 @@ public static String getResultText(int resultCode) } } + private static class EarlyNatives + { + /** + * Returns an array of ByteBuffer, one covering SPI_result, one for + * SPI_processed, and one for the SPI_tuptable pointer. + *

    + * Takes a {@code Class} argument, to save the native + * code a lookup. + */ + private static native ByteBuffer[] _window( + Class component); + } + @Deprecated - private native static int _exec(String command, int rowCount); + private static native int _exec(String command, int rowCount); - private native static long _getProcessed(); - private native static int _getResult(); - private native static void _freeTupTable(); - private native static TupleTable _getTupTable(TupleDesc known); + private static native void _freeTupTable(); + private static native TupleTable _getTupTable(TupleDesc known); + private static native TupleList _mapTupTable( + TupleTableSlotImpl ttsi, long p, int sizeToMap); } diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/Session.java b/pljava/src/main/java/org/postgresql/pljava/internal/Session.java index cb44c12b7..95b692081 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/Session.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/Session.java @@ -61,31 +61,6 @@ private static class Holder @SuppressWarnings("removal") private final TransactionalMap m_attributes = new TransactionalMap(new HashMap()); - /** - * The Java charset corresponding to the server encoding, or null if none - * such was found. Put here by InstallHelper via package access at startup. - */ - static Charset s_serverCharset; - - /** - * A static method (not part of the API-exposed Session interface) by which - * pljava implementation classes can get hold of the server charset without - * the indirection of getting a Session instance. If there turns out to be - * demand for client code to obtain it through the API, an interface method - * {@code serverCharset} can easily be added later. - * @return The Java Charset corresponding to the server's encoding, or null - * if no matching Java charset was found. That can happen if a corresponding - * Java charset really does exist but is not successfully found using the - * name reported by PostgreSQL. That can be worked around by giving the - * right name explicitly as the system property - * {@code org.postgresql.server.encoding} in {@code pljava.vmoptions} for - * the affected database (or cluster-wide, if the same encoding is used). - */ - public static Charset implServerCharset() - { - return s_serverCharset; - } - /** * Adds the specified listener to the list of listeners that will * receive transactional events. diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/SwitchPointCache.java b/pljava/src/main/java/org/postgresql/pljava/internal/SwitchPointCache.java new file mode 100644 index 000000000..646b7d2b8 --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/internal/SwitchPointCache.java @@ -0,0 +1,872 @@ +/* + * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.internal; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles.Lookup; +import java.lang.invoke.MethodHandles; +import static java.lang.invoke.MethodHandles.collectArguments; +import static java.lang.invoke.MethodHandles.constant; +import static java.lang.invoke.MethodHandles.dropArguments; +import static java.lang.invoke.MethodHandles.empty; +import static java.lang.invoke.MethodHandles.filterArguments; +import static java.lang.invoke.MethodHandles.insertArguments; +import static java.lang.invoke.MethodHandles.lookup; +import static java.lang.invoke.MethodHandles.permuteArguments; +import java.lang.invoke.MethodType; +import static java.lang.invoke.MethodType.methodType; +import java.lang.invoke.SwitchPoint; +import java.lang.invoke.VarHandle; +import static java.lang.invoke.VarHandle.AccessMode.GET; +import static java.lang.invoke.VarHandle.AccessMode.SET; + +import java.lang.reflect.Method; +import static java.lang.reflect.Modifier.isStatic; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import static java.util.Objects.requireNonNull; + +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.UnaryOperator; +import static java.util.function.UnaryOperator.identity; + +import static java.util.stream.Collectors.groupingBy; +import static java.util.stream.Collectors.toMap; +import java.util.stream.Stream; + +import static org.postgresql.pljava.internal.Backend.doInPG; +import org.postgresql.pljava.internal.DualState; // for JavaDoc +import static org.postgresql.pljava.internal.UncheckedException.unchecked; + +/** + * Tool for implementing objects or families of objects with methods that lazily + * compute various values and then return the same values until invalidated, + * after which new values will be lazily computed when next requested. + *

    Synchronization

    + *

    + * Items that have been cached are returned directly, until invalidated by + * the action of {@link SwitchPoint SwitchPoint}. + *

    + * When an item has not been cached or the cached value has been invalidated, + * its lazy recomputation at next use takes place within {@code doInPG}, + * that is, "on the PG thread". (An extended discussion of what that really + * means can be found at {@link DualState DualState}.) The PG thread must be the + * only thread where the {@code SwitchPoint}s will be invalidated, and an old + * {@code SwitchPoint} must be replaced in its field by a newly-constructed one + * before the old one is invalidated. + */ +public class SwitchPointCache +{ + private SwitchPointCache() // not to be instantiated + { + } + + /** + * Whether to cache the value returned by a computation method; true unless + * the method has called {@code doNotCache}. + *

    + * Because all computation methods are constrained to run on the PG thread, + * a simple static suffices. + */ + private static boolean cache = true; + + /** + * Called from a computation method to prevent caching of the value being + * returned. + *

    + * This can be useful in cases where a not-yet-determined value should not + * 'stick'. Whatever the computation method returns will be returned to the + * caller, but the computation method will be reinvoked the next time + * a caller wants the value. + *

    + * This state is reset on entry and after return of any computation method. + * Therefore, if there are actions within a computation method that could + * involve calling other {@code SwitchPointCache}-based methods, this method + * must be called after all of those to have reliable effect. By convention, + * it should be called immediately before returning. + */ + public static void doNotCache() + { + cache = false; + } + + /** + * Transform a {@code MethodHandle} into one with a reference to itself. + * @param m MethodHandle with methodType(r,MethodHandle,p0,...,pk) where the + * expected first parameter is a MethodHandle h of methodType(r,p0,...,pk) + * that invokes m with inserted first argument h. + * @return h + */ + public static MethodHandle fix(MethodHandle m) + { + MethodHandle[] a = new MethodHandle[1]; + a[0] = m.asSpreader(0, MethodHandle[].class, 1).bindTo(a); + return a[0]; + } + + /** + * Replace {@code slots[index]} with a constant returning {@code v} forever, + * immune to invalidation. + *

    + * The slot must already be populated, as by the initializer created by + * a {@link Builder Builder}; this method adapts the supplied constant to + * the slot's existing {@code methodType}. + */ + public static void setConstant(MethodHandle[] slots, int index, Object v) + { + MethodHandle h = slots[index]; + MethodType t = h.type(); + MethodHandle c = constant(t.returnType(), v); + c = dropArguments(c, 0, t.parameterArray()); + slots[index] = c; + } + + /** + * Builder for use during the static initialization of a class that uses + * {@code SwitchPointCache}. + *

    + * The builder's constructor is passed information about the class, and + * about a {@link SwitchPoint SwitchPoint} that will be used when the + * dependent values need to be invalidated. To accommodate invalidation + * schemes with different granularity, the {@code SwitchPoint} used may be + * kept in an instance field of the class, or in a static field and + * governing all instances of the class, or even somewhere else entirely + * and used for widespread or global invalidation. + *

    + * The builder's {@link #withDependent withDependent} method is then used to + * declare each value that can be computed and cached in an instance of the + * class, by giving the name of a static method that computes the + * value (given one argument, an instance of the class) and functions to get + * and set a {@code MethodHandle}-typed per-instance slot where the + * computation result will be cached. + *

    + * Finally, the builder's {@link #build build} method returns + * a {@code Consumer} that can be saved in a static final field and + * invoked in the object's constructor; it will initialize all of the new + * instance's fields that were declared in {@code withDependent} calls to + * their initial, uncomputed states. + */ + public static class Builder + { + private final Class m_class; + private Function m_describer; + private UnaryOperator m_initializer; + private Lookup m_lookup; + private Map m_candidates; + private Function m_spGetter; + private Function m_slotGetter; + private Class m_receiver; + private Class m_return; + + /** + * Create a builder that will be used to declare dependent values + * controlled by a single {@code SwitchPoint} and to create an + * initializer for the per-instance slots that will hold their states. + * @param c the class being configured by this Builder + */ + public Builder(Class c) + { + m_receiver = m_class = requireNonNull(c); + m_describer = Object::toString; + m_initializer = identity(); + } + + /** + * @param describer function, with a signature like that of + * {@code Object.toString}, that will produce a useful description of + * the object if needed in an exception message. The default if this + * method is not called is {@code Object::toString}; a different + * describer can be supplied if the output of {@code toString} isn't + * well suited for an exception message. If null, any exception will + * have its bare constant message with nothing added about the specific + * receiver object. + */ + public Builder withDescriber(Function describer) + { + if ( null == describer ) + m_describer = o -> ""; + else + m_describer = o -> ": " + describer.apply(o); + return this; + } + + /** + * Supply the {@code Lookup} object to be used in resolving dependent + * methods. + * @param l a {@link Lookup Lookup} object obtained by the caller and + * able to introspect in the class + */ + public Builder withLookup(Lookup l) + { + m_lookup = requireNonNull(l); + return this; + } + + /** + * Supply the candidate methods to be available to subsequent + * {@link #withDependent withDependent} calls. + * @param ms array of methods such as the caller may obtain with + * {@link Class#getDeclaredMethods getDeclaredMethods}, avoiding the + * access complications of having this class do it. The methods will be + * filtered to only those that are static with a non-void return type + * and exactly one parameter, assignable from the target class, and + * uniquely named within that set. Only such methods can be named in + * later {@link #withDependent withDependent} calls. No reference to + * the array will be retained. + */ + public Builder withCandidates(Method[] ms) + { + m_candidates = candidatesAmong(Arrays.stream(ms)); + return this; + } + + /** + * Supply a function mapping a receiver object instance to the + * {@code SwitchPoint} to be associated with subsequently declared + * slots. + * @param spGetter a function usable to fetch the SwitchPoint + * that controls this cache. It is passed an instance of T but need not + * use it (in the case, for example, of a single controlling SwitchPoint + * held in a static). + */ + public Builder withSwitchPoint(Function spGetter) + { + m_spGetter = requireNonNull(spGetter); + return this; + } + + /** + * Supply a function mapping a receiver object instance to the + * per-instance {@code MethodHandle} array whose elements will be + * the slots. + * @param slotGetter a function usable to fetch the slot array + * for an instance. + */ + public Builder withSlots(Function slotGetter) + { + m_slotGetter = requireNonNull(slotGetter); + return this; + } + + /** + * Adjust the static return type of subsequently declared dependents + * that return references. + *

    + * This can be a more compact notation if compute methods or API methods + * from a superclass or subclass will be reused and the return type + * needs to be adjusted to match the static type in the API method + * (possibly erased from a generic type). + * @param t Class to serve as the following dependents' static return + * type. Pass null to discontinue adjusting return types for following + * dependents. + * @throws IllegalArgumentException if t represents a primitive type. + */ + public Builder withReturnType(Class t) + { + if ( null != t && t.isPrimitive() ) + throw new IllegalArgumentException( + "return type adjustment cannot accept primitive type " + t); + m_return = t; + return this; + } + + /** + * Adjust the static receiver type of subsequently declared dependents. + *

    + * This can be a more compact notation if compute methods or API methods + * from a superclass or subclass will be reused and the receiver type + * needs to be adjusted to match the static type in the API method + * (possibly erased from a generic type). + * @param t Class to serve as the following dependents' static receiver + * type. Pass null to discontinue adjusting receiver types for following + * dependents. + * @throws IllegalArgumentException if t is neither a widening nor a + * narrowing of the receiver type specified for this builder. + */ + public Builder withReceiverType(Class t) + { + if ( null != t + && ! t.isAssignableFrom(m_class) + && ! m_class.isAssignableFrom(t) ) + throw new IllegalArgumentException( + "receiver type " + m_class + " cannot be adjusted to " + t); + m_receiver = null == t ? m_class : t; + return this; + } + + /** + * Return a {@code UnaryOperator} to be invoked + * in the constructor of a client object, applied to a newly-allocated + * array of the right number of slots, which will initialize all of the + * array's elements with the corresponding fallback method handles + * and return the initialized array. + *

    + * The initializer can be used conveniently in a constructor that + * assigns the array to a final field, or calls a superclass constructor + * that does so, to arrange that the array's elements are written + * in advance of Java's freeze of the final array reference field. + */ + public UnaryOperator build() + { + return m_initializer; + } + + /** + * Declare one dependent item that will be controlled by this builder's + * {@code SwitchPoint}. + *

    + * An item is declared by naming the static method that can + * compute its value when needed, and the index into the per-instance + * {@code MethodHandle[]} "slots" array that will be used to cache the + * value. Typically, these will be private, and there will be an API + * method for retrieving the value, by fetching the method handle from + * the array index given here, and invoking it. + *

    + * The method handle that will be found in the named slot has a return + * type matching the compute method named here, and two parameters; it + * expects the receiver object as the first parameter, and itself as + * the second. So the typical API method is simply: + *

    +		 * MethodHandle h = slots[MY_SLOT];
    +		 * return (cast)h.invokeExact(this, h);
    +		 *
    + *

    + * When there is a cached value and the {@code SwitchPoint} has not been + * invalidated, the two arguments are ignored and the cached value + * is returned. + * @param methodName name of the static method that will be used to + * compute values for this item. It must be found among the methods + * that were passed to the Builder constructor, only considering those + * that are static, with a non-void return and one argument of + * the class type. + * @param index index into the per-instance slot arrray where the cached + * state will be maintained. + */ + public Builder withDependent(String methodName, int index) + { + MethodHandle m; + MethodHandle recompute; + + try + { + m = m_lookup.unreflect(m_candidates.get(methodName)); + } + catch ( ReflectiveOperationException e ) + { + throw unchecked(e); + } + + final MethodHandle only_p_erased = eraseP0(m); + MethodType mt = only_p_erased.type(); + Class rt = mt.returnType(); + Class pt = m_receiver; + Function spGetter = m_spGetter; + Function slotGetter = m_slotGetter; + Function describer = m_describer; + + if ( ! rt.isPrimitive() ) + { + Class rtfinal = null == m_return ? rt : m_return; + + final MethodHandle p_and_r_erased = + m.asType(mt.changeReturnType(Object.class)); + recompute = AS_MH.bindTo((As)(h,o,g) -> doInPG(() -> + { + MethodHandle[] slots = slotGetter.apply(o); + MethodHandle gwt = slots[index]; + if ( gwt != g ) // somebody else refreshed it already + return gwt.invoke(o, gwt); + /* + * Still the same invalidated g, so the task of computing + * a fresh value and replacing it has fallen to us. + */ + SwitchPoint sp = spGetter.apply(o); + if ( null == sp || sp.hasBeenInvalidated() ) + throw new IllegalStateException( + "function call after invalidation of object" + + describer.apply(o)); + Object v; + try + { + cache = true; + v = p_and_r_erased.invokeExact(o); + if ( cache ) + { + MethodHandle c = constant(rtfinal, v); + c = dropArguments(c, 0, pt, MethodHandle.class); + slots[index] = sp.guardWithTest(c, h); + } + } + finally + { + cache = true; + } + return v; + })); + recompute = recompute.asType( + recompute.type().changeReturnType(rtfinal)); + } + else if ( int.class == rt ) + { + recompute = ASINT_MH.bindTo((AsInt)(h,o,g) -> doInPG(() -> + { + MethodHandle[] slots = slotGetter.apply(o); + MethodHandle gwt = slots[index]; + if ( gwt != g ) // somebody else refreshed it already + return (int)gwt.invoke(o, gwt); + /* + * Still the same invalidated g, so the task of computing + * a fresh value and replacing it has fallen to us. + */ + SwitchPoint sp = spGetter.apply(o); + if ( null == sp || sp.hasBeenInvalidated() ) + throw new IllegalStateException( + "function call after invalidation of object" + + describer.apply(o)); + int v; + try + { + cache = true; + v = (int)only_p_erased.invokeExact(o); + if ( cache ) + { + MethodHandle c = constant(rt, v); + c = dropArguments(c, 0, pt, MethodHandle.class); + slots[index] = sp.guardWithTest(c, h); + } + } + finally + { + cache = true; + } + return v; + })); + } + else if ( long.class == rt ) + { + recompute = ASLONG_MH.bindTo((AsLong)(h,o,g) -> doInPG(() -> + { + MethodHandle[] slots = slotGetter.apply(o); + MethodHandle gwt = slots[index]; + if ( gwt != g ) // somebody else refreshed it already + return (long)gwt.invoke(o, gwt); + /* + * Still the same invalidated g, so the task of computing + * a fresh value and replacing it has fallen to us. + */ + SwitchPoint sp = spGetter.apply(o); + if ( null == sp || sp.hasBeenInvalidated() ) + throw new IllegalStateException( + "function call after invalidation of object" + + describer.apply(o)); + long v; + try + { + cache = true; + v = (long)only_p_erased.invokeExact(o); + if ( cache ) + { + MethodHandle c = constant(rt, v); + c = dropArguments(c, 0, pt, MethodHandle.class); + slots[index] = sp.guardWithTest(c, h); + } + } + finally + { + cache = true; + } + return v; + })); + } + else if ( boolean.class == rt ) + { + recompute = + ASBOOLEAN_MH.bindTo((AsBoolean)(h,o,g) -> doInPG(() -> + { + MethodHandle[] slots = slotGetter.apply(o); + MethodHandle gwt = slots[index]; + if ( gwt != g ) // somebody else refreshed it already + return (boolean)gwt.invoke(o, gwt); + /* + * Still the same invalidated g, so the task of computing + * a fresh value and replacing it has fallen to us. + */ + SwitchPoint sp = spGetter.apply(o); + if ( null == sp || sp.hasBeenInvalidated() ) + throw new IllegalStateException( + "function call after invalidation of object" + + describer.apply(o)); + boolean v; + try + { + cache = true; + v = (boolean)only_p_erased.invokeExact(o); + if ( cache ) + { + MethodHandle c = constant(rt, v); + c = dropArguments(c, 0, pt, MethodHandle.class); + slots[index] = sp.guardWithTest(c, h); + } + } + finally + { + cache = true; + } + return v; + })); + } + else if ( double.class == rt ) + { + recompute = + ASDOUBLE_MH.bindTo((AsDouble)(h,o,g) -> doInPG(() -> + { + MethodHandle[] slots = slotGetter.apply(o); + MethodHandle gwt = slots[index]; + if ( gwt != g ) // somebody else refreshed it already + return (double)gwt.invoke(o, gwt); + /* + * Still the same invalidated g, so the task of computing + * a fresh value and replacing it has fallen to us. + */ + SwitchPoint sp = spGetter.apply(o); + if ( null == sp || sp.hasBeenInvalidated() ) + throw new IllegalStateException( + "function call after invalidation of object" + + describer.apply(o)); + double v; + try + { + cache = true; + v = (double)only_p_erased.invokeExact(o); + if ( cache ) + { + MethodHandle c = constant(rt, v); + c = dropArguments(c, 0, pt, MethodHandle.class); + slots[index] = sp.guardWithTest(c, h); + } + } + finally + { + cache = true; + } + return v; + })); + } + else if ( float.class == rt ) + { + recompute = + ASFLOAT_MH.bindTo((AsFloat)(h,o,g) -> doInPG(() -> + { + MethodHandle[] slots = slotGetter.apply(o); + MethodHandle gwt = slots[index]; + if ( gwt != g ) // somebody else refreshed it already + return (float)gwt.invoke(o, gwt); + /* + * Still the same invalidated g, so the task of computing + * a fresh value and replacing it has fallen to us. + */ + SwitchPoint sp = spGetter.apply(o); + if ( null == sp || sp.hasBeenInvalidated() ) + throw new IllegalStateException( + "function call after invalidation of object" + + describer.apply(o)); + float v; + try + { + cache = true; + v = (float)only_p_erased.invokeExact(o); + if ( cache ) + { + MethodHandle c = constant(rt, v); + c = dropArguments(c, 0, pt, MethodHandle.class); + slots[index] = sp.guardWithTest(c, h); + } + } + finally + { + cache = true; + } + return v; + })); + } + else if ( short.class == rt ) + { + recompute = + ASSHORT_MH.bindTo((AsShort)(h,o,g) -> doInPG(() -> + { + MethodHandle[] slots = slotGetter.apply(o); + MethodHandle gwt = slots[index]; + if ( gwt != g ) // somebody else refreshed it already + return (short)gwt.invoke(o, gwt); + /* + * Still the same invalidated g, so the task of computing + * a fresh value and replacing it has fallen to us. + */ + SwitchPoint sp = spGetter.apply(o); + if ( null == sp || sp.hasBeenInvalidated() ) + throw new IllegalStateException( + "function call after invalidation of object" + + describer.apply(o)); + short v; + try + { + cache = true; + v = (short)only_p_erased.invokeExact(o); + if ( cache ) + { + MethodHandle c = constant(rt, v); + c = dropArguments(c, 0, pt, MethodHandle.class); + slots[index] = sp.guardWithTest(c, h); + } + } + finally + { + cache = true; + } + return v; + })); + } + else if ( char.class == rt ) + { + recompute = ASCHAR_MH.bindTo((AsChar)(h,o,g) -> doInPG(() -> + { + MethodHandle[] slots = slotGetter.apply(o); + MethodHandle gwt = slots[index]; + if ( gwt != g ) // somebody else refreshed it already + return (char)gwt.invoke(o, gwt); + /* + * Still the same invalidated g, so the task of computing + * a fresh value and replacing it has fallen to us. + */ + SwitchPoint sp = spGetter.apply(o); + if ( null == sp || sp.hasBeenInvalidated() ) + throw new IllegalStateException( + "function call after invalidation of object" + + describer.apply(o)); + char v; + try + { + cache = true; + v = (char)only_p_erased.invokeExact(o); + if ( cache ) + { + MethodHandle c = constant(rt, v); + c = dropArguments(c, 0, pt, MethodHandle.class); + slots[index] = sp.guardWithTest(c, h); + } + } + finally + { + cache = true; + } + return v; + })); + } + else if ( byte.class == rt ) + { + recompute = ASBYTE_MH.bindTo((AsByte)(h,o,g) -> doInPG(() -> + { + MethodHandle[] slots = slotGetter.apply(o); + MethodHandle gwt = slots[index]; + if ( gwt != g ) // somebody else refreshed it already + return (byte)gwt.invoke(o, gwt); + /* + * Still the same invalidated g, so the task of computing + * a fresh value and replacing it has fallen to us. + */ + SwitchPoint sp = spGetter.apply(o); + if ( null == sp || sp.hasBeenInvalidated() ) + throw new IllegalStateException( + "function call after invalidation of object" + + describer.apply(o)); + byte v; + try + { + cache = true; + v = (byte)only_p_erased.invokeExact(o); + if ( cache ) + { + MethodHandle c = constant(rt, v); + c = dropArguments(c, 0, pt, MethodHandle.class); + slots[index] = sp.guardWithTest(c, h); + } + } + finally + { + cache = true; + } + return v; + })); + } + else + throw new AssertionError("unhandled primitive"); // pacify javac + + recompute = recompute.asType( + recompute.type().changeParameterType(1, pt)); + + MethodHandle init = fix(recompute); + + m_initializer = m_initializer.andThen(s -> + { + s[index] = init; + return s; + })::apply; + + return this; + } + + /** + * Return a map from name to {@code Method} for all methods in ms that + * are static with a non-void return type and exactly one parameter, + * assignable from the target class, and uniquely named within that set. + */ + private Map candidatesAmong(Stream ms) + { + Map> m1 = ms + .filter(m -> + isStatic(m.getModifiers()) && + void.class != m.getReturnType() && + 1 == m.getParameterCount() && + m.getParameterTypes()[0].isAssignableFrom(m_class)) + .collect(groupingBy(Method::getName)); + + return m1.values().stream() + .filter(list -> 1 == list.size()) + .map(list -> list.get(0)) + .collect(toMap(Method::getName, identity())); + } + + private static MethodHandle eraseP0(MethodHandle m) + { + MethodType mt = m.type().changeParameterType(0, Object.class); + return m.asType(mt); + } + } + + private static final MethodHandle AS_MH; + private static final MethodHandle ASLONG_MH; + private static final MethodHandle ASDOUBLE_MH; + private static final MethodHandle ASINT_MH; + private static final MethodHandle ASFLOAT_MH; + private static final MethodHandle ASSHORT_MH; + private static final MethodHandle ASCHAR_MH; + private static final MethodHandle ASBYTE_MH; + private static final MethodHandle ASBOOLEAN_MH; + + static + { + Lookup lu = lookup(); + MethodType mt = + methodType(Object.class, + MethodHandle.class, Object.class, MethodHandle.class); + + try + { + AS_MH = lu.findVirtual(As.class, "compute", mt); + + ASLONG_MH = lu.findVirtual(AsLong.class, "compute", + mt.changeReturnType(long.class)); + + ASDOUBLE_MH = lu.findVirtual(AsDouble.class, "compute", + mt.changeReturnType(double.class)); + + ASINT_MH = lu.findVirtual(AsInt.class, "compute", + mt.changeReturnType(int.class)); + + ASFLOAT_MH = lu.findVirtual(AsFloat.class, "compute", + mt.changeReturnType(float.class)); + + ASSHORT_MH = lu.findVirtual(AsShort.class, "compute", + mt.changeReturnType(short.class)); + + ASCHAR_MH= lu.findVirtual(AsChar.class, "compute", + mt.changeReturnType(char.class)); + + ASBYTE_MH = lu.findVirtual(AsByte.class, "compute", + mt.changeReturnType(byte.class)); + + ASBOOLEAN_MH = lu.findVirtual(AsBoolean.class, "compute", + mt.changeReturnType(boolean.class)); + } + catch ( ReflectiveOperationException e ) + { + throw unchecked(e); + } + } + + @FunctionalInterface + private interface As + { + R compute(MethodHandle h, T instance, MethodHandle gwt) + throws Throwable; + } + + @FunctionalInterface + private interface AsLong + { + long compute(MethodHandle h, T instance, MethodHandle gwt) + throws Throwable; + } + + @FunctionalInterface + private interface AsDouble + { + double compute(MethodHandle h, T instance, MethodHandle gwt) + throws Throwable; + } + + @FunctionalInterface + private interface AsInt + { + int compute(MethodHandle h, T instance, MethodHandle gwt) + throws Throwable; + } + + @FunctionalInterface + private interface AsFloat + { + float compute(MethodHandle h, T instance, MethodHandle gwt) + throws Throwable; + } + + @FunctionalInterface + private interface AsShort + { + short compute(MethodHandle h, T instance, MethodHandle gwt) + throws Throwable; + } + + @FunctionalInterface + private interface AsChar + { + char compute(MethodHandle h, T instance, MethodHandle gwt) + throws Throwable; + } + + @FunctionalInterface + private interface AsByte + { + byte compute(MethodHandle h, T instance, MethodHandle gwt) + throws Throwable; + } + + @FunctionalInterface + private interface AsBoolean + { + boolean compute(MethodHandle h, T instance, MethodHandle gwt) + throws Throwable; + } +} diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/TriggerData.java b/pljava/src/main/java/org/postgresql/pljava/internal/TriggerData.java index 74368e0fb..f70586b9c 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/TriggerData.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/TriggerData.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2019 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2022 Tada AB and other contributors, as listed below. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the The BSD 3-Clause License @@ -16,6 +16,7 @@ import java.sql.SQLException; import static org.postgresql.pljava.internal.Backend.doInPG; +import org.postgresql.pljava.internal.Invocation; import org.postgresql.pljava.TriggerException; import org.postgresql.pljava.jdbc.TriggerResultSet; @@ -35,18 +36,17 @@ public class TriggerData implements org.postgresql.pljava.TriggerData private boolean m_suppress = false; private final State m_state; - TriggerData(DualState.Key cookie, long resourceOwner, long pointer) + TriggerData(long pointer) { - m_state = new State(cookie, this, resourceOwner, pointer); + m_state = new State(this, pointer); } private static class State extends DualState.SingleGuardedLong { - private State( - DualState.Key cookie, TriggerData td, long ro, long hth) + private State(TriggerData td, long hth) { - super(cookie, td, ro, hth); + super(td, Invocation.current(), hth); } /** diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/Tuple.java b/pljava/src/main/java/org/postgresql/pljava/internal/Tuple.java index ac4fc417f..7d4d29022 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/Tuple.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/Tuple.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2019 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2022 Tada AB and other contributors, as listed below. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the The BSD 3-Clause License @@ -26,18 +26,24 @@ public class Tuple { private final State m_state; - Tuple(DualState.Key cookie, long resourceOwner, long pointer) + Tuple(long pointer) { - m_state = new State(cookie, this, resourceOwner, pointer); + m_state = new State(this, pointer); } private static class State extends DualState.SingleHeapFreeTuple { - private State( - DualState.Key cookie, Tuple t, long ro, long ht) + private State(Tuple t, long ht) { - super(cookie, t, ro, ht); + /* + * Passing null as the Lifespan means this will never be + * matched by a lifespanRelease call; that's appropriate (for now) as + * the Tuple copy is being made into JavaMemoryContext, which never + * gets reset, so only unreachability from the Java side + * will free it. + */ + super(t, null, ht); } /** diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/TupleDesc.java b/pljava/src/main/java/org/postgresql/pljava/internal/TupleDesc.java index 8dd5b343b..482ce2568 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/TupleDesc.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/TupleDesc.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2019 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2022 Tada AB and other contributors, as listed below. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the The BSD 3-Clause License @@ -28,20 +28,26 @@ public class TupleDesc private final int m_size; private Class[] m_columnClasses; - TupleDesc(DualState.Key cookie, long resourceOwner, long pointer, int size) + TupleDesc(long pointer, int size) throws SQLException { - m_state = new State(cookie, this, resourceOwner, pointer); + m_state = new State(this, pointer); m_size = size; } private static class State extends DualState.SingleFreeTupleDesc { - private State( - DualState.Key cookie, TupleDesc td, long ro, long hth) + private State(TupleDesc td, long hth) { - super(cookie, td, ro, hth); + /* + * Passing null as the Lifespan means this will never be + * matched by a lifespanRelease call; that's appropriate (for now) as + * the TupleDesc copy is being made into JavaMemoryContext, which + * never gets reset, so only unreachability from the Java side + * will free it. + */ + super(td, null, hth); } /** diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/VarlenaWrapper.java b/pljava/src/main/java/org/postgresql/pljava/internal/VarlenaWrapper.java index 89594488e..30a07cf47 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/VarlenaWrapper.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/VarlenaWrapper.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2020 Tada AB and other contributors, as listed below. + * Copyright (c) 2019-2023 Tada AB and other contributors, as listed below. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the The BSD 3-Clause License @@ -35,7 +35,18 @@ import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; +import org.postgresql.pljava.adt.spi.Datum; + import static org.postgresql.pljava.internal.Backend.doInPG; +import org.postgresql.pljava.internal.LifespanImpl.Addressed; + +import org.postgresql.pljava.model.MemoryContext; +import org.postgresql.pljava.model.ResourceOwner; + +import org.postgresql.pljava.pg.DatumImpl; +import org.postgresql.pljava.pg.DatumImpl.IStream; +import org.postgresql.pljava.pg.MemoryContextImpl; +import org.postgresql.pljava.pg.ResourceOwnerImpl; /** * Interface that wraps a PostgreSQL native variable-length ("varlena") datum; @@ -48,15 +59,8 @@ * Java code has written and closed it), after which it is no longer accessible * from Java. */ -public interface VarlenaWrapper extends Closeable +public interface VarlenaWrapper extends Closeable, DatumImpl { - /** - * Return the varlena address to native code and dissociate the varlena - * from Java. - * @param cookie Capability held by native code. - */ - long adopt(DualState.Key cookie) throws SQLException; - /** * Return a string describing this object in a way useful for debugging, * prefixed with the name (abbreviated for comfort) of the class of the @@ -74,8 +78,6 @@ public interface VarlenaWrapper extends Closeable */ String toString(Object o); - - /** * A class by which Java reads the content of a varlena. * @@ -83,7 +85,7 @@ public interface VarlenaWrapper extends Closeable * the native reference; the chosen resource owner must be one that will be * released no later than the memory context containing the varlena. */ - public static class Input implements VarlenaWrapper + public static class Input extends DatumImpl.Input implements VarlenaWrapper { private long m_parkedSize; private long m_bufferSize; @@ -107,15 +109,16 @@ public static class Input implements VarlenaWrapper * @param buf Readable direct {@code ByteBuffer} constructed over the * varlena's data bytes. */ - private Input(DualState.Key cookie, long resourceOwner, + private Input(long resourceOwner, long context, long snapshot, long varlenaPtr, long parkedSize, long bufferSize, ByteBuffer buf) { m_parkedSize = parkedSize; m_bufferSize = bufferSize; m_state = new State( - cookie, this, resourceOwner, - context, snapshot, varlenaPtr, buf); + this, resourceOwner, + MemoryContextImpl.fromAddress(context), + snapshot, varlenaPtr, buf); } public void pin() throws SQLException @@ -167,12 +170,12 @@ public String toString(Object o) } @Override - public long adopt(DualState.Key cookie) throws SQLException + public long adopt() throws SQLException { m_state.pin(); try { - return m_state.adopt(cookie); + return m_state.adopt(); } finally { @@ -180,181 +183,24 @@ public long adopt(DualState.Key cookie) throws SQLException } } - public class Stream - extends ByteBufferInputStream implements VarlenaWrapper - { - /** - * A duplicate of the {@code VarlenaWrapper.Input}'s byte buffer, - * so its {@code position} and {@code mark} can be updated by the - * {@code InputStream} operations without affecting the original - * (therefore multiple {@code Stream}s may read one {@code Input}). - */ - private ByteBuffer m_movingBuffer; - - /* - * Overrides {@code ByteBufferInputStream} method and throws the - * exception type declared there. For other uses of pin in this - * class where SQLException is expected, just use - * {@code m_state.pin} directly. - */ - @Override - protected void pin() throws IOException - { - if ( ! m_open ) - throw new IOException("Read from closed VarlenaWrapper"); - try - { - Input.this.pin(); - } - catch ( SQLException e ) - { - throw new IOException(e.getMessage(), e); - } - } - - /* - * Unpin for use in {@code ByteBufferInputStream} or here; no - * throws-clause difference to blotch things up. - */ - protected void unpin() - { - Input.this.unpin(); - } - - @Override - public void close() throws IOException - { - if ( pinUnlessReleased() ) - return; - try - { - super.close(); - Input.this.close(); - } - finally - { - unpin(); - } - } - - @Override - public String toString(Object o) - { - return String.format("%s %s", - Input.this.toString(o), m_open ? "open" : "closed"); - } - - /** - * Apply a {@code Verifier} to the input data. - *

    - * This should only be necessary if the input wrapper is being used - * directly as an output item, and needs verification that it - * conforms to the format of the target type. - *

    - * The current position must be at the beginning of the stream. The - * verifier must leave it at the end to confirm the entire stream - * was examined. There should be no need to reset the position here, - * as the only anticipated use is during an {@code adopt}, and the - * native code will only care about the varlena's address. - */ - public void verify(Verifier v) throws SQLException - { - /* - * This is only called from some client code's adopt() method, - * calls to which are serialized through Backend.THREADLOCK - * anyway, so holding a pin here for the duration doesn't - * further limit concurrency. Hold m_state's monitor also to - * block any extraneous reading interleaved with the verifier. - */ - m_state.pin(); - try - { - ByteBuffer buf = buffer(); - synchronized ( m_state ) - { - if ( 0 != buf.position() ) - throw new SQLException( - "Variable-length input data to be verified " + - " not positioned at start", - "55000"); - InputStream dontCloseMe = new FilterInputStream(this) - { - @Override - public void close() throws IOException { } - }; - v.verify(dontCloseMe); - if ( 0 != buf.remaining() ) - throw new SQLException( - "Verifier finished prematurely"); - } - } - catch ( SQLException | RuntimeException e ) - { - throw e; - } - catch ( Exception e ) - { - throw new SQLException( - "Exception verifying variable-length data: " + - e.getMessage(), "XX000", e); - } - finally - { - m_state.unpin(); - } - } - - @Override - protected ByteBuffer buffer() throws IOException - { - try - { - if ( null == m_movingBuffer ) - { - ByteBuffer b = Input.this.buffer(); - m_movingBuffer = b.duplicate().order(b.order()); - } - return m_movingBuffer; - } - catch ( SQLException sqe ) - { - throw new IOException("Read from varlena failed", sqe); - } - } - - @Override - public long adopt(DualState.Key cookie) throws SQLException - { - Input.this.pin(); - try - { - if ( ! m_open ) - throw new SQLException( - "Cannot adopt VarlenaWrapper.Input after " + - "it is closed", "55000"); - return Input.this.adopt(cookie); - } - finally - { - Input.this.unpin(); - } - } - } - private static class State - extends DualState.SingleMemContextDelete + extends DualState.SingleMemContextDelete { private ByteBuffer m_buf; + private long m_resourceOwner; private long m_snapshot; private long m_varlena; private State( - DualState.Key cookie, Input vr, long resourceOwner, - long memContext, long snapshot, long varlenaPtr, ByteBuffer buf) + VarlenaWrapper.Input vr, long resourceOwner, + MemoryContext memContext, + long snapshot, long varlenaPtr, ByteBuffer buf) { - super(cookie, vr, resourceOwner, memContext); + super(vr, ResourceOwnerImpl.fromAddress(resourceOwner), + memContext); + m_resourceOwner = resourceOwner; // keep that address handy m_snapshot = snapshot; m_varlena = varlenaPtr; m_buf = null == buf ? buf : buf.asReadOnlyBuffer(); @@ -370,7 +216,8 @@ private ByteBuffer buffer() throws SQLException doInPG(() -> { m_buf = _detoast( - m_varlena, guardedLong(), m_snapshot, + m_varlena, + ((Addressed)memoryContext()).address(), m_snapshot, m_resourceOwner).asReadOnlyBuffer(); m_snapshot = 0; }); @@ -382,21 +229,22 @@ m_varlena, guardedLong(), m_snapshot, } } - private long adopt(DualState.Key cookie) throws SQLException + private long adopt() throws SQLException { - adoptionLock(cookie); + adoptionLock(); try { if ( 0 != m_snapshot ) { /* fetch, before snapshot released */ - m_varlena = _fetch(m_varlena, guardedLong()); + m_varlena = _fetch( + m_varlena, ((Addressed)memoryContext()).address()); } return m_varlena; } finally { - adoptionUnlock(cookie); + adoptionUnlock(); } } @@ -497,11 +345,12 @@ public class Output extends OutputStream implements VarlenaWrapper * @param buf Writable direct {@code ByteBuffer} constructed over (an * initial region of) the varlena's data bytes. */ - private Output(DualState.Key cookie, long resourceOwner, + private Output(long resourceOwner, long context, long varlenaPtr, ByteBuffer buf) { m_state = new State( - cookie, this, resourceOwner, context, varlenaPtr, buf); + this, ResourceOwnerImpl.fromAddress(resourceOwner), + MemoryContextImpl.fromAddress(context), varlenaPtr, buf); } /** @@ -649,7 +498,7 @@ public void free() throws IOException } @Override - public long adopt(DualState.Key cookie) throws SQLException + public long adopt() throws SQLException { m_state.pin(); try @@ -658,7 +507,7 @@ public long adopt(DualState.Key cookie) throws SQLException throw new SQLException( "Writing of VarlenaWrapper.Output not yet complete", "55000"); - return m_state.adopt(cookie); + return m_state.adopt(); } finally { @@ -689,11 +538,11 @@ private static class State private Verifier m_verifier; private State( - DualState.Key cookie, Output vr, - long resourceOwner, long memContext, long varlenaPtr, - ByteBuffer buf) + Output vr, + ResourceOwner resourceOwner, MemoryContext memContext, + long varlenaPtr, ByteBuffer buf) { - super(cookie, vr, resourceOwner, memContext); + super(vr, resourceOwner, memContext); m_varlena = varlenaPtr; m_buf = buf; } @@ -730,16 +579,16 @@ private ByteBuffer buffer(int desiredCapacity) throws SQLException } } - private long adopt(DualState.Key cookie) throws SQLException + private long adopt() throws SQLException { - adoptionLock(cookie); + adoptionLock(); try { return m_varlena; } finally { - adoptionUnlock(cookie); + adoptionUnlock(); } } diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/VarlenaXMLRenderer.java b/pljava/src/main/java/org/postgresql/pljava/internal/VarlenaXMLRenderer.java index 11bc27548..2b58a8f24 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/VarlenaXMLRenderer.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/VarlenaXMLRenderer.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2020 Tada AB and other contributors, as listed below. + * Copyright (c) 2019-2023 Tada AB and other contributors, as listed below. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the The BSD 3-Clause License @@ -20,47 +20,40 @@ import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; +import org.postgresql.pljava.adt.spi.Datum; + +import static org.postgresql.pljava.model.CharsetEncoding.SERVER_ENCODING; + +import org.postgresql.pljava.pg.DatumImpl; + /** * Class adapting a {@code ByteBufferXMLReader} to a - * {@code VarlenaWrapper.Input}. + * {@code Datum.Input}. */ public abstract class VarlenaXMLRenderer -extends ByteBufferXMLReader implements VarlenaWrapper +extends ByteBufferXMLReader implements DatumImpl { - private final VarlenaWrapper.Input m_input; + private final Datum.Input m_input; protected final CharsetDecoder m_decoder; /** - * A duplicate of the {@code VarlenaWrapper.Input}'s byte buffer, + * A duplicate of the {@code Datum.Input}'s byte buffer, * so its {@code position} can be updated by the * {@code XMLEventReader} operations without affecting the original * (therefore multiple streams may read one {@code Input}). */ private ByteBuffer m_movingBuffer; - public VarlenaXMLRenderer(VarlenaWrapper.Input input) throws SQLException + public VarlenaXMLRenderer(Datum.Input input) throws SQLException { m_input = input; - Charset cs = Session.implServerCharset(); - if ( null == cs ) - { - try - { - input.close(); - } - catch ( IOException e ) { } - throw new SQLFeatureNotSupportedException("SQLXML: no Java " + - "Charset found to match server encoding; perhaps set " + - "org.postgresql.server.encoding system property to a " + - "valid Java charset name for the same encoding?", "0A000"); - - } + Charset cs = SERVER_ENCODING.charset(); m_decoder = cs.newDecoder(); } @Override - public long adopt(DualState.Key cookie) throws SQLException + public long adopt() throws SQLException { throw new UnsupportedOperationException( "adopt() on a synthetic XML rendering"); @@ -75,7 +68,7 @@ public String toString() @Override public String toString(Object o) { - return m_input.toString(o); + return ((DatumImpl)m_input).toString(o); } @Override diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/Invocation.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/Invocation.java deleted file mode 100644 index b06e24727..000000000 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/Invocation.java +++ /dev/null @@ -1,167 +0,0 @@ -/* - * Copyright (c) 2004-2019 Tada AB and other contributors, as listed below. - * - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the The BSD 3-Clause License - * which accompanies this distribution, and is available at - * http://opensource.org/licenses/BSD-3-Clause - * - * Contributors: - * Tada AB - * Chapman Flack - */ -package org.postgresql.pljava.jdbc; - -import java.sql.PreparedStatement; -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.logging.Logger; - -import org.postgresql.pljava.internal.Backend; -import static org.postgresql.pljava.internal.Backend.doInPG; -import org.postgresql.pljava.internal.PgSavepoint; - -/** - * One invocation, from PostgreSQL, of functionality implemented using PL/Java. - *

    - * This class is the Java counterpart of the {@code struct Invocation_} in the - * C code, but while there is a new stack-allocated C structure on every entry - * from PG to PL/Java, no instance of this class is created unless requested - * (with {@link #current current()}; once requested, a reference to it is saved - * in the C struct for the duration of the invocation. - *

    - * One further piece of magic applies to set-returning functions. Under the - * value-per-call protocol, there is technically a new entry into PL/Java, and - * a new C {@code Invocation_} struct, for every row to be returned, but that - * low-level complication is hidden at this level: a single instance of this - * class, if once requested, will be remembered throughout the value-per-call - * sequence of calls. - * @author Thomas Hallgren - */ -public class Invocation -{ - /** - * The current "stack" of invocations. - */ - private static Invocation[] s_levels = new Invocation[10]; - - /** - * Nesting level for this invocation - */ - private final int m_nestingLevel; - - /** - * Top level savepoint relative to this invocation. - */ - private PgSavepoint m_savepoint; - - private Invocation(int level) - { - m_nestingLevel = level; - } - - /** - * @return The nesting level of this invocation - */ - public int getNestingLevel() - { - return m_nestingLevel; - } - - /** - * @return Returns the savePoint. - */ - final PgSavepoint getSavepoint() - { - return m_savepoint; - } - - /** - * @param savepoint The savepoint to set. - */ - final void setSavepoint(PgSavepoint savepoint) - { - m_savepoint = savepoint; - } - - /** - * Called from the backend when the invokation exits. Should - * not be invoked any other way. - */ - public void onExit(boolean withError) - throws SQLException - { - try - { - if(m_savepoint != null) - m_savepoint.onInvocationExit(withError); - } - finally - { - s_levels[m_nestingLevel] = null; - } - } - - /** - * @return The current invocation - */ - public static Invocation current() - { - return doInPG(() -> - { - Invocation curr = _getCurrent(); - if(curr != null) - return curr; - - int level = _getNestingLevel(); - int top = s_levels.length; - if(level < top) - { - curr = s_levels[level]; - if(curr != null) - { - curr._register(); - return curr; - } - } - else - { - int newSize = top; - do { newSize <<= 2; } while(newSize <= level); - Invocation[] levels = new Invocation[newSize]; - System.arraycopy(s_levels, 0, levels, 0, top); - s_levels = levels; - } - curr = new Invocation(level); - s_levels[level] = curr; - curr._register(); - return curr; - }); - } - - static void clearErrorCondition() - { - doInPG(Invocation::_clearErrorCondition); - } - - /** - * Register this Invocation so that it receives the onExit callback - */ - private native void _register(); - - /** - * Returns the current invocation or null if no invocation has been - * registered yet. - */ - private native static Invocation _getCurrent(); - - /** - * Returns the current nesting level - */ - private native static int _getNestingLevel(); - - /** - * Clears the error condition set by elog(ERROR) - */ - private native static void _clearErrorCondition(); -} diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/PgNodeTreeAsXML.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/PgNodeTreeAsXML.java index 8d5de171b..731d7c4c6 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/PgNodeTreeAsXML.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/PgNodeTreeAsXML.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2020 Tada AB and other contributors, as listed below. + * Copyright (c) 2019-2023 Tada AB and other contributors, as listed below. * Portions Copyright (c) 1996-2019, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * @@ -28,7 +28,7 @@ import org.xml.sax.SAXException; -import org.postgresql.pljava.internal.VarlenaWrapper; +import org.postgresql.pljava.adt.spi.Datum; import org.postgresql.pljava.internal.VarlenaXMLRenderer; /** @@ -39,7 +39,7 @@ */ public class PgNodeTreeAsXML extends VarlenaXMLRenderer { - PgNodeTreeAsXML(VarlenaWrapper.Input vwi) throws SQLException + PgNodeTreeAsXML(Datum.Input vwi) throws SQLException { super(vwi); } diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java index ad1df5c5c..3ee10595d 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2020 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2023 Tada AB and other contributors, as listed below. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the The BSD 3-Clause License @@ -44,15 +44,27 @@ import java.util.Calendar; import java.util.HashMap; import java.util.Iterator; +import java.util.List; // for SlotTester import java.util.Map; import java.util.Properties; import java.util.concurrent.Executor; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; +import org.postgresql.pljava.internal.Invocation; import org.postgresql.pljava.internal.Oid; import org.postgresql.pljava.internal.PgSavepoint; +import java.lang.reflect.Field; +import org.postgresql.pljava.Adapter; +import org.postgresql.pljava.internal.SPI; +import static org.postgresql.pljava.internal.UncheckedException.unchecked; +import org.postgresql.pljava.model.Portal; +import static org.postgresql.pljava.model.Portal.Direction.FORWARD; +import org.postgresql.pljava.model.SlotTester; +import org.postgresql.pljava.model.TupleTableSlot; +import org.postgresql.pljava.pg.TupleTableSlotImpl; + /** * Provides access to the current connection (session) the Java stored * procedure is running in. It is returned from the driver manager @@ -68,8 +80,40 @@ * * @author Thomas Hallgren */ -public class SPIConnection implements Connection +public class SPIConnection implements Connection, SlotTester { + @Override // SlotTester + public Portal unwrapAsPortal(ResultSet rs) throws SQLException + { + return ((SPIResultSet)rs).unwrapAsPortal(); + } + + @Override // SlotTester + @SuppressWarnings("deprecation") + public List test(String query) + { + try ( Statement s = createStatement() ) + { + ResultSet rs = s.executeQuery(query); + Portal p = unwrapAsPortal(rs); + return p.fetch(FORWARD, Portal.ALL); + } + catch ( SQLException e ) + { + throw unchecked(e); + } + } + + @Override // SlotTester + public Adapter adapterPlease(String cname, String field) + throws ReflectiveOperationException + { + Class cls = + Class.forName(cname).asSubclass(SlotTester.Visible.class); + Field f = cls.getField(field); + return (Adapter)f.get(null); + } + /** * The version number of the currently executing PostgreSQL * server. diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIResultSet.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIResultSet.java index 673fe5eeb..a25c0e2b8 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIResultSet.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIResultSet.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2018 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2023 Tada AB and other contributors, as listed below. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the The BSD 3-Clause License @@ -45,6 +45,8 @@ public class SPIResultSet extends ResultSetBase private boolean m_open; + private boolean m_portalUnwrapped; + SPIResultSet(SPIStatement statement, Portal portal, long maxRows) throws SQLException { @@ -57,6 +59,17 @@ public class SPIResultSet extends ResultSetBase m_open = true; } + public Portal unwrapAsPortal() throws SQLException + { + if ( ! m_open || null != m_table || null != m_currentRow + || null != m_nextRow || -1 != m_tableRow ) + throw new IllegalStateException( + "too late to unwrap SPIResultSet to Portal"); + m_portalUnwrapped = true; + close(); + return m_portal; + } + @Override public void close() throws SQLException @@ -64,7 +77,8 @@ public void close() if(m_open) { m_open = false; - m_portal.close(); + if ( ! m_portalUnwrapped ) + m_portal.close(); m_statement.resultSetClosed(this); m_table = null; m_tableRow = -1; diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLInputFromTuple.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLInputFromTuple.java index 2b99e01ba..005097748 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLInputFromTuple.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLInputFromTuple.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2019 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2022 Tada AB and other contributors, as listed below. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the The BSD 3-Clause License @@ -52,11 +52,10 @@ public class SQLInputFromTuple extends SingleRowReader implements SQLInput * {@code HeapTupleHeader}, as well as the TupleDesc (Java object this time) * describing its structure. */ - public SQLInputFromTuple(DualState.Key cookie, long resourceOwner, - long heapTupleHeaderPointer, TupleDesc tupleDesc) + public SQLInputFromTuple(long heapTupleHeaderPointer, TupleDesc tupleDesc) throws SQLException { - super(cookie, resourceOwner, heapTupleHeaderPointer, tupleDesc); + super(heapTupleHeaderPointer, tupleDesc); m_index = 0; m_columns = tupleDesc.size(); } diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java index 1b3bb2a00..6589c9bd7 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2021 Tada AB and other contributors, as listed below. + * Copyright (c) 2018-2023 Tada AB and other contributors, as listed below. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the The BSD 3-Clause License @@ -80,7 +80,8 @@ import org.w3c.dom.Node; import org.w3c.dom.Text; -import static org.postgresql.pljava.internal.Session.implServerCharset; +import static org.postgresql.pljava.model.CharsetEncoding.SERVER_ENCODING; + import org.postgresql.pljava.internal.VarlenaWrapper; import java.sql.SQLFeatureNotSupportedException; @@ -104,6 +105,8 @@ import java.io.FilterOutputStream; import java.io.OutputStreamWriter; +import static java.nio.charset.StandardCharsets.UTF_8; + import static javax.xml.transform.OutputKeys.ENCODING; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerFactory; @@ -173,13 +176,22 @@ /* ... for SQLXMLImpl.Readable.Synthetic */ +import java.io.StringWriter; +import javax.xml.transform.TransformerConfigurationException; +import static org.postgresql.pljava.internal.UncheckedException.unchecked; import org.postgresql.pljava.internal.VarlenaXMLRenderer; import static org.postgresql.pljava.jdbc.TypeOid.PG_NODE_TREEOID; +/* ... for new model / adapter interoperability */ + +import org.postgresql.pljava.adt.spi.Datum; +import org.postgresql.pljava.model.RegType; +import org.postgresql.pljava.pg.DatumImpl; + /** * Implementation of {@link SQLXML} for the SPI connection. */ -public abstract class SQLXMLImpl implements SQLXML +public abstract class SQLXMLImpl implements SQLXML { private static final VarHandle s_backingVH; protected volatile V m_backing; @@ -189,7 +201,7 @@ public abstract class SQLXMLImpl implements SQLXML try { s_backingVH = lookup().findVarHandle( - SQLXMLImpl.class, "m_backing", VarlenaWrapper.class); + SQLXMLImpl.class, "m_backing", Datum.class); } catch ( ReflectiveOperationException e ) { @@ -316,11 +328,30 @@ static SQLException normalizedException(Exception e) + e.getMessage(), "XX000", e); } + /** + * Create readable SQLXML instance over a {@code Datum.Input}, recording + * the source type. + *

    + * The source type can be used to detect efforts to store this value into + * a destination of a different type, and apply a verifier for type safety. + */ + public static SQLXML newReadable( + Datum.Input datum, RegType pgType, boolean synthetic) + throws SQLException + { + int oid = pgType.oid(); + + if ( synthetic ) + return new Readable.Synthetic(datum, oid); + + return new Readable.PgXML<>(datum, oid); + } + /** * Create a new, initially empty and writable, SQLXML instance, whose * backing memory will in a transaction-scoped PostgreSQL memory context. */ - static SQLXML newWritable() + public static SQLXML newWritable() { return doInPG(() -> _newWritable()); } @@ -337,12 +368,12 @@ static SQLXML newWritable() * @param sx The SQLXML object to be adopted. * @param oid The PostgreSQL type ID the native code is expecting; * see Readable.adopt for why that can matter. - * @return The underlying {@code VarlenaWrapper} (which has its own + * @return The underlying {@code Datum} (which has its own * {@code adopt} method the native code will call next. * @throws SQLException if this {@code SQLXML} instance is not in the * proper state to be adoptable. */ - private static VarlenaWrapper adopt(SQLXML sx, int oid) throws SQLException + private static Datum adopt(SQLXML sx, int oid) throws SQLException { if ( sx instanceof Readable.PgXML || sx instanceof Writable ) return ((SQLXMLImpl)sx).adopt(oid); @@ -358,15 +389,15 @@ private static VarlenaWrapper adopt(SQLXML sx, int oid) throws SQLException /** * Allow native code to claim complete control over the - * underlying {@code VarlenaWrapper} and dissociate it from Java. + * underlying {@code Datum} and dissociate it from Java. * @param oid The PostgreSQL type ID the native code is expecting; * see Readable.adopt for why that can matter. - * @return The underlying {@code VarlenaWrapper} (which has its own + * @return The underlying {@code Datum} (which has its own * {@code adopt} method the native code will call next. * @throws SQLException if this {@code SQLXML} instance is not in the * proper state to be adoptable. */ - protected abstract VarlenaWrapper adopt(int oid) throws SQLException; + protected abstract Datum adopt(int oid) throws SQLException; /** * Return a description of this object useful for debugging (not the raw @@ -395,7 +426,7 @@ protected String toString(Object o) o = this; V backing = (V)s_backingVH.getAcquire(this); if ( null != backing ) - return backing.toString(o); + return ((DatumImpl)backing).toString(o); Class c = o.getClass(); String cn = c.getCanonicalName(); int pnl = c.getPackageName().length(); @@ -505,7 +536,7 @@ static InputStream correctedDeclStream( int markLimit = 1048576; // don't assume a markable stream's economical if ( ! is.markSupported() ) is = new BufferedInputStream(is); - else if ( is instanceof VarlenaWrapper ) // a VarlenaWrapper is, though + else if ( is instanceof Datum ) // a Datum is, though markLimit = Integer.MAX_VALUE; InputStream msis = new MarkableSequenceInputStream(pfis, rais, is); @@ -688,13 +719,13 @@ private static boolean useWrappingElement(InputStream is, Reader r) - static abstract class Readable + static abstract class Readable extends SQLXMLImpl { private static final VarHandle s_readableVH; protected volatile boolean m_readable = true; protected final int m_pgTypeID; - protected Charset m_serverCS = implServerCharset(); + protected Charset m_serverCS = SERVER_ENCODING.charset(); protected boolean m_wrapped = false; static @@ -713,25 +744,17 @@ static abstract class Readable /** * Create a readable instance, when called by native code (the * constructor is otherwise private, after all), passing an initialized - * {@code VarlenaWrapper} and the PostgreSQL type ID from which it has + * {@code Datum} and the PostgreSQL type ID from which it has * been created. - * @param vwi The already-created wrapper for reading the varlena from + * @param di The already-created wrapper for reading the varlena from * native memory. * @param oid The PostgreSQL type ID from which this instance is being * created (for why it matters, see {@code adopt}). */ - private Readable(V vwi, int oid) throws SQLException + private Readable(V di, int oid) throws SQLException { - super(vwi); + super(di); m_pgTypeID = oid; - if ( null == m_serverCS ) - { - free(); - throw new SQLFeatureNotSupportedException("SQLXML: no Java " + - "Charset found to match server encoding; perhaps set " + - "org.postgresql.server.encoding system property to a " + - "valid Java charset name for the same encoding?", "0A000"); - } } private V backingAndClearReadable() throws SQLException @@ -930,17 +953,17 @@ public T getSource(Class sourceClass) protected String toString(Object o) { return String.format("%s %sreadable %swrapped", - super.toString(o), (boolean)s_readableVH.getAcquire() + super.toString(o), (boolean)s_readableVH.getAcquire(this) ? "" : "not ", m_wrapped ? "" : "not "); } - static class PgXML - extends Readable + static class PgXML + extends Readable { - private PgXML(VarlenaWrapper.Input vwi, int oid) + private PgXML(Datum.Input di, int oid) throws SQLException { - super(vwi.new Stream(), oid); + super(di.inputStream(), oid); } /** @@ -972,18 +995,17 @@ private PgXML(VarlenaWrapper.Input vwi, int oid) * with the PostgreSQL types. */ @Override - protected VarlenaWrapper adopt(int oid) throws SQLException + protected Datum adopt(int oid) throws SQLException { - VarlenaWrapper.Input.Stream vw = (VarlenaWrapper.Input.Stream) - s_backingVH.getAndSet(this, null); + T is = (T)s_backingVH.getAndSet(this, null); if ( ! (boolean)s_readableVH.getAcquire(this) ) throw new SQLNonTransientException( "SQLXML object has already been read from", "55000"); - if ( null == vw ) + if ( null == is ) backingIfNotFreed(); /* shorthand to throw the exception */ if ( m_pgTypeID != oid ) - vw.verify(new Verifier()); - return vw; + is.verify(new Verifier()::verify); + return is; } /* @@ -993,7 +1015,7 @@ protected VarlenaWrapper adopt(int oid) throws SQLException */ @Override protected InputStream toBinaryStream( - VarlenaWrapper.Input.Stream backing, boolean neverWrap) + T backing, boolean neverWrap) throws SQLException, IOException { boolean[] wrapped = { false }; @@ -1005,7 +1027,7 @@ protected InputStream toBinaryStream( @Override protected Reader toCharacterStream( - VarlenaWrapper.Input.Stream backing, boolean neverWrap) + T backing, boolean neverWrap) throws SQLException, IOException { InputStream is = toBinaryStream(backing, neverWrap); @@ -1014,7 +1036,7 @@ protected Reader toCharacterStream( @Override protected Adjusting.XML.SAXSource toSAXSource( - VarlenaWrapper.Input.Stream backing) + T backing) throws SQLException, SAXException, IOException { InputStream is = toBinaryStream(backing, false); @@ -1023,7 +1045,7 @@ protected Adjusting.XML.SAXSource toSAXSource( @Override protected Adjusting.XML.StAXSource toStAXSource( - VarlenaWrapper.Input.Stream backing) + T backing) throws SQLException, XMLStreamException, IOException { InputStream is = toBinaryStream(backing, false); @@ -1032,7 +1054,7 @@ protected Adjusting.XML.StAXSource toStAXSource( @Override protected Adjusting.XML.DOMSource toDOMSource( - VarlenaWrapper.Input.Stream backing) + T backing) throws SQLException, SAXException, IOException, ParserConfigurationException @@ -1044,19 +1066,19 @@ protected Adjusting.XML.DOMSource toDOMSource( static class Synthetic extends Readable { - private Synthetic(VarlenaWrapper.Input vwi, int oid) + private Synthetic(Datum.Input di, int oid) throws SQLException { - super(xmlRenderer(oid, vwi), oid); + super(xmlRenderer(oid, di), oid); } private static VarlenaXMLRenderer xmlRenderer( - int oid, VarlenaWrapper.Input vwi) + int oid, Datum.Input di) throws SQLException { switch ( oid ) { - case PG_NODE_TREEOID: return new PgNodeTreeAsXML(vwi); + case PG_NODE_TREEOID: return new PgNodeTreeAsXML(di); default: throw new SQLNonTransientException( "no synthetic SQLXML support for Oid " + oid, "0A000"); @@ -1064,7 +1086,7 @@ private static VarlenaXMLRenderer xmlRenderer( } @Override - protected VarlenaWrapper adopt(int oid) throws SQLException + protected Datum adopt(int oid) throws SQLException { throw new SQLFeatureNotSupportedException( "adopt() on a synthetic SQLXML not yet supported", "0A000"); @@ -1098,6 +1120,7 @@ protected Adjusting.XML.SAXSource toSAXSource( return new AdjustingSAXSource(backing, new InputSource()); } + @Override protected Adjusting.XML.StAXSource toStAXSource( VarlenaXMLRenderer backing) throws SQLException, XMLStreamException, IOException @@ -1107,6 +1130,7 @@ protected Adjusting.XML.StAXSource toStAXSource( "0A000"); } + @Override protected Adjusting.XML.DOMSource toDOMSource( VarlenaXMLRenderer backing) throws @@ -1117,6 +1141,47 @@ protected Adjusting.XML.DOMSource toDOMSource( "synthetic SQLXML as DOMSource not yet supported", "0A000"); } + + /** + * Until there is better support for {@code toBinaryStream} and + * {@code toCharacterStream}, at least supply a working brute-force + * {@code toString} to support quick examination of values. + */ + @Override + public String getString() throws SQLException + { + XMLReader backing = + ((Readable)this) + .backingAndClearReadable(); + if ( null == backing ) + throw new SQLNonTransientException( + "Attempted use of getString on " + + "an unreadable SQLXML object", "55000"); + + SAXTransformerFactory saxtf = (SAXTransformerFactory) + SAXTransformerFactory.newDefaultInstance(); + try + { + TransformerHandler th = saxtf.newTransformerHandler(); + StringWriter w = new StringWriter(); + th.setResult(new StreamResult(w)); + + backing.setContentHandler(th); + backing.setDTDHandler(th); + backing.setProperty( + SAX2PROPERTY.LEXICAL_HANDLER.propertyUri(), th); + backing.parse(new InputSource()); + return w.toString(); + } + catch ( TransformerConfigurationException | IOException | + SAXException e ) + { + /* + * None of the above should really happen here. + */ + throw unchecked(e); + } + } } } @@ -1225,7 +1290,7 @@ static class Writable extends SQLXMLImpl { private static final VarHandle s_writableVH; private volatile boolean m_writable = true; - private Charset m_serverCS = implServerCharset(); + private Charset m_serverCS = SERVER_ENCODING.charset(); private DOMResult m_domResult; static @@ -1244,18 +1309,6 @@ static class Writable extends SQLXMLImpl private Writable(VarlenaWrapper.Output vwo) throws SQLException { super(vwo); - if ( null == m_serverCS ) - { - try - { - vwo.free(); - } - catch ( IOException ioe ) { } - throw new SQLFeatureNotSupportedException("SQLXML: no Java " + - "Charset found to match server encoding; perhaps set " + - "org.postgresql.server.encoding system property to a " + - "valid Java charset name for the same encoding?", "0A000"); - } } private VarlenaWrapper.Output backingAndClearWritable() @@ -1496,7 +1549,7 @@ protected VarlenaWrapper adopt(int oid) throws SQLException protected String toString(Object o) { return String.format("%s %swritable", super.toString(o), - (boolean)s_writableVH.getAcquire() ? "" : "not "); + (boolean)s_writableVH.getAcquire(this) ? "" : "not "); } } @@ -1541,7 +1594,7 @@ protected void verify(InputStream is) throws Exception { boolean[] wrapped = { false }; is = correctedDeclStream( - is, false, implServerCharset(), wrapped); + is, false, SERVER_ENCODING.charset(), wrapped); /* * The supplied XMLReader is never set up to do unwrapping, which is @@ -3470,7 +3523,7 @@ byte[] prefix(Charset serverCharset) throws IOException boolean canOmitVersion = true; // no declaration => 1.0 byte[] version = new byte[] { '1', '.', '0' }; boolean canOmitEncoding = - null == serverCharset || "UTF-8".equals(serverCharset.name()); + null == serverCharset || UTF_8.equals(serverCharset); boolean canOmitStandalone = true; byte[] parseResult = m_save.toByteArray(); @@ -3628,7 +3681,7 @@ void checkEncoding(Charset serverCharset, boolean strict) } } - if ( ! strict || "UTF-8".equals(serverCharset.name()) ) + if ( ! strict || UTF_8.equals(serverCharset) ) return; throw new SQLDataException( "XML does not declare a character set, and server encoding " + diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/SingleRowReader.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SingleRowReader.java index 2b5d62cfa..7db34ad16 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SingleRowReader.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SingleRowReader.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2019 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2022 Tada AB and other contributors, as listed below. * Copyright (c) 2010, 2011 PostgreSQL Global Development Group * * All rights reserved. This program and the accompanying materials @@ -18,6 +18,7 @@ import static org.postgresql.pljava.internal.Backend.doInPG; import org.postgresql.pljava.internal.DualState; +import org.postgresql.pljava.internal.Invocation; import org.postgresql.pljava.internal.TupleDesc; /** @@ -35,10 +36,9 @@ public class SingleRowReader extends SingleRowResultSet private static class State extends DualState.SingleGuardedLong { - private State( - DualState.Key cookie, SingleRowReader srr, long ro, long hth) + private State(SingleRowReader srr, long hth) { - super(cookie, srr, ro, hth); + super(srr, Invocation.current(), hth); } /** @@ -73,18 +73,13 @@ private long getHeapTupleHeaderPtr() throws SQLException /** * Construct a {@code SingleRowReader} from a {@code HeapTupleHeader} * and a {@link TupleDesc TupleDesc}. - * @param cookie Capability obtained from native code to construct a - * {@code SingleRowReader} instance. - * @param resourceOwner Value identifying a scope in PostgreSQL during which - * the native state encapsulated here will be valid. * @param hth Native pointer to a PG {@code HeapTupleHeader} * @param tupleDesc A {@code TupleDesc}; the Java class this time. */ - public SingleRowReader(DualState.Key cookie, long resourceOwner, long hth, - TupleDesc tupleDesc) + public SingleRowReader(long hth, TupleDesc tupleDesc) throws SQLException { - m_state = new State(cookie, this, resourceOwner, hth); + m_state = new State(this, hth); m_tupleDesc = tupleDesc; } diff --git a/pljava/src/main/java/org/postgresql/pljava/management/Commands.java b/pljava/src/main/java/org/postgresql/pljava/management/Commands.java index 0cdafa254..2e8dad9db 100644 --- a/pljava/src/main/java/org/postgresql/pljava/management/Commands.java +++ b/pljava/src/main/java/org/postgresql/pljava/management/Commands.java @@ -327,7 +327,7 @@ " jarOrigin CHARACTER VARYING(500) NOT NULL," + " jarOwner pg_catalog.NAME NOT NULL," + " jarManifest pg_catalog.TEXT" + -" )", +" ) DISTRIBUTE BY REPLICATION", " COMMENT ON TABLE sqlj.jar_repository IS" + " 'Information on jars loaded by PL/Java, one row per jar.'", " GRANT SELECT ON sqlj.jar_repository TO public", @@ -339,7 +339,7 @@ " REFERENCES sqlj.jar_repository ON DELETE CASCADE," + " entryImage pg_catalog.BYTEA NOT NULL," + " UNIQUE(jarId, entryName)" + -" )", +" ) DISTRIBUTE BY REPLICATION", " COMMENT ON TABLE sqlj.jar_entry IS" + " 'Name and content of each entry in every jar loaded by PL/Java.'", " GRANT SELECT ON sqlj.jar_entry TO public", @@ -349,7 +349,7 @@ " ordinal pg_catalog.INT2," + " PRIMARY KEY (jarId, ordinal)," + " entryId INT NOT NULL REFERENCES sqlj.jar_entry ON DELETE CASCADE" + -" )", +" ) DISTRIBUTE BY REPLICATION", " COMMENT ON TABLE sqlj.jar_descriptor IS" + " 'Associates each jar with zero-or-more deployment descriptors (a row " + "for each), with ordinal indicating their order of mention in the " + @@ -362,7 +362,7 @@ " jarId INT NOT NULL" + " REFERENCES sqlj.jar_repository ON DELETE CASCADE," + " PRIMARY KEY(schemaName, ordinal)" + -" )", +" ) DISTRIBUTE BY REPLICATION", " COMMENT ON TABLE sqlj.classpath_entry IS" + " 'Associates each schema with zero-or-more jars (a row " + "for each), with ordinal indicating their order of precedence in the " + @@ -373,7 +373,7 @@ " mapId SERIAL PRIMARY KEY," + " javaName CHARACTER VARYING(200) NOT NULL," + " sqlName pg_catalog.NAME NOT NULL" + -" )", +" ) DISTRIBUTE BY REPLICATION", " COMMENT ON TABLE sqlj.typemap_entry IS" + " 'A row for each SQL type <-> Java type custom mapping.'", " GRANT SELECT ON sqlj.typemap_entry TO public" diff --git a/pljava/src/main/java/org/postgresql/pljava/mbeans/DualStateStatistics.java b/pljava/src/main/java/org/postgresql/pljava/mbeans/DualStateStatistics.java index f259bbc14..826190a8b 100644 --- a/pljava/src/main/java/org/postgresql/pljava/mbeans/DualStateStatistics.java +++ b/pljava/src/main/java/org/postgresql/pljava/mbeans/DualStateStatistics.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020 Tada AB and other contributors, as listed below. + * Copyright (c) 2022 Tada AB and other contributors, as listed below. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the The BSD 3-Clause License @@ -30,7 +30,7 @@ public interface DualStateStatistics long getJavaUnreachable(); long getJavaReleased(); long getNativeReleased(); - long getResourceOwnerPasses(); + long getLifespanPasses(); long getReferenceQueuePasses(); long getReferenceQueueItems(); long getContendedLocks(); diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/AclItem.java b/pljava/src/main/java/org/postgresql/pljava/pg/AclItem.java new file mode 100644 index 000000000..1cbb9c653 --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/pg/AclItem.java @@ -0,0 +1,318 @@ +/* + * Copyright (c) 2022-2023 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.pg; + +import static java.lang.Integer.lowestOneBit; +import static java.lang.Integer.numberOfTrailingZeros; + +import java.lang.annotation.Native; +import java.util.List; + +import java.nio.ByteBuffer; + +import org.postgresql.pljava.model.*; + +import org.postgresql.pljava.pg.CatalogObjectImpl.*; + +import static + org.postgresql.pljava.pg.CatalogObjectImpl.Factory.staticFormObjectId; + +import static org.postgresql.pljava.pg.ModelConstants.N_ACL_RIGHTS; +import static org.postgresql.pljava.pg.ModelConstants.PG_VERSION_NUM; + +public abstract class AclItem implements CatalogObject.Grant +{ + /* + * PostgreSQL defines these in include/nodes/parsenodes.h + */ + @Native static final short ACL_INSERT = 1 << 0; + @Native static final short ACL_SELECT = 1 << 1; + @Native static final short ACL_UPDATE = 1 << 2; + @Native static final short ACL_DELETE = 1 << 3; + @Native static final short ACL_TRUNCATE = 1 << 4; + @Native static final short ACL_REFERENCES = 1 << 5; + @Native static final short ACL_TRIGGER = 1 << 6; + @Native static final short ACL_EXECUTE = 1 << 7; + @Native static final short ACL_USAGE = 1 << 8; + @Native static final short ACL_CREATE = 1 << 9; + @Native static final short ACL_CREATE_TEMP = 1 << 10; + @Native static final short ACL_CONNECT = 1 << 11; + // below appearing in PG 15 + @Native static final short ACL_SET = 1 << 12; + @Native static final short ACL_ALTER_SYSTEM = 1 << 13; + + @Native static final int ACL_ID_PUBLIC = 0; + + @Native static final int OFFSET_ai_grantee = 0; + @Native static final int OFFSET_ai_grantor = 4; + @Native static final int OFFSET_ai_privs = 8; + + /** + * These one-letter abbreviations are to match the order of the bit masks + * declared above, following the {@code PRIVILEGE-ABBREVS-TABLE} in the + * PostgreSQL documentation, under Privileges, in the Data Definition + * chapter. + *

    + * Note that the order of the table in the documentation need not match + * the order of the bits above. This string must be ordered like the bits. + * It can also be found as {@code ACL_ALL_RIGHTS_STR} in + * {@code include/utils/acl.h}. + */ + private static final String s_abbr = "arwdDxtXUCTcsA"; + + static + { + /* + * This is not a check for equality, because N_ACL_RIGHTS has grown + * (between PG 14 and 15). So the string should + * include all the letters that might be used, and the assertion will + * catch if a new PG version has grown the count again. + * + * For now, assume that, in older versions, unused bits will be zero + * and we won't have to bother masking them off. + */ + assert N_ACL_RIGHTS <= s_abbr.length() : "AclItem abbreviations"; + assert + s_abbr.length() == s_abbr.codePoints().count() : "AclItem abbr BMP"; + } + + private final RegRole.Grantee m_grantee; + private final int m_grantor; // less often interesting + + protected AclItem(int grantee, int grantor) + { + m_grantee = + (RegRole.Grantee) staticFormObjectId(RegRole.CLASSID, grantee); + m_grantor = grantor; + } + + @Override public RegRole.Grantee to() + { + return m_grantee; + } + + @Override public RegRole by() + { + return staticFormObjectId(RegRole.CLASSID, m_grantor); + } + + /** + * Implementation of all non-OnRole subinterfaces of Grant. + *

    + * The distinct interfaces in the API are a type-safety veneer to help + * clients remember what privileges apply to what object types. Underneath, + * this class implements them all. + */ + public static class NonRole extends AclItem + implements + OnClass, OnNamespace, OnSetting, + CatalogObject.EXECUTE, CatalogObject.CREATE_TEMP, CatalogObject.CONNECT + { + private final int m_priv; + private final int m_goption; + + public NonRole(ByteBuffer b) + { + super(b.getInt(OFFSET_ai_grantee), b.getInt(OFFSET_ai_grantor)); + + if ( PG_VERSION_NUM < 160000 ) + { + assert OFFSET_ai_privs + Integer.BYTES == b.limit(); + int privs = b.getInt(OFFSET_ai_privs); + m_priv = (privs & 0xffff); + m_goption = (privs >>> 16); + return; + } + + assert OFFSET_ai_privs + Long.BYTES == b.limit(); + long privs = b.getLong(OFFSET_ai_privs); + m_priv = (int)(privs & 0xffffffff); + m_goption = (int)(privs >>> 32); + } + + private boolean priv(int mask) + { + return 0 != (m_priv & mask); + } + + private boolean goption(int mask) + { + return 0 != (m_goption & mask); + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + /* + * Should this not be sb.append(to().nameAsGrantee()) ? You'd think, + * but to match the text representation from PostgreSQL itself, the + * bare = is the right thing to show for public. + */ + if ( ! to().isPublic() ) + sb.append(to().name()); + sb.append('='); + int priv = m_priv; + int goption = m_goption; + while ( 0 != priv ) + { + int bit = lowestOneBit(priv); + priv ^= bit; + sb.append(s_abbr.charAt(numberOfTrailingZeros(bit))); + if ( 0 != (goption & bit) ) + sb.append('*'); + } + sb.append('/').append(by().name()); + return sb.toString(); + } + + @Override public boolean selectGranted() + { + return priv(ACL_SELECT); + } + + @Override public boolean selectGrantable() + { + return goption(ACL_SELECT); + } + + @Override public boolean insertGranted() + { + return priv(ACL_INSERT); + } + + @Override public boolean insertGrantable() + { + return goption(ACL_INSERT); + } + + @Override public boolean updateGranted() + { + return priv(ACL_UPDATE); + } + + @Override public boolean updateGrantable() + { + return goption(ACL_UPDATE); + } + + @Override public boolean referencesGranted() + { + return priv(ACL_REFERENCES); + } + + @Override public boolean referencesGrantable() + { + return goption(ACL_REFERENCES); + } + + @Override public boolean deleteGranted() + { + return priv(ACL_DELETE); + } + + @Override public boolean deleteGrantable() + { + return goption(ACL_DELETE); + } + + @Override public boolean truncateGranted() + { + return priv(ACL_TRUNCATE); + } + + @Override public boolean truncateGrantable() + { + return goption(ACL_TRUNCATE); + } + + @Override public boolean triggerGranted() + { + return priv(ACL_TRIGGER); + } + + @Override public boolean triggerGrantable() + { + return goption(ACL_TRIGGER); + } + + @Override public boolean createGranted() + { + return priv(ACL_CREATE); + } + + @Override public boolean createGrantable() + { + return goption(ACL_CREATE); + } + + @Override public boolean usageGranted() + { + return priv(ACL_USAGE); + } + + @Override public boolean usageGrantable() + { + return goption(ACL_USAGE); + } + + @Override public boolean executeGranted() + { + return priv(ACL_EXECUTE); + } + + @Override public boolean executeGrantable() + { + return goption(ACL_EXECUTE); + } + + @Override public boolean create_tempGranted() + { + return priv(ACL_CREATE_TEMP); + } + + @Override public boolean create_tempGrantable() + { + return goption(ACL_CREATE_TEMP); + } + + @Override public boolean connectGranted() + { + return priv(ACL_CONNECT); + } + + @Override public boolean connectGrantable() + { + return goption(ACL_CONNECT); + } + + @Override public boolean setGranted() + { + return priv(ACL_SET); + } + + @Override public boolean setGrantable() + { + return goption(ACL_SET); + } + + @Override public boolean alterSystemGranted() + { + return priv(ACL_ALTER_SYSTEM); + } + + @Override public boolean alterSystemGrantable() + { + return goption(ACL_ALTER_SYSTEM); + } + } +} diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/AttributeImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/AttributeImpl.java new file mode 100644 index 000000000..4d2e0e5d2 --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/pg/AttributeImpl.java @@ -0,0 +1,1000 @@ +/* + * Copyright (c) 2022-2023 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.pg; + +import java.lang.invoke.MethodHandle; +import static java.lang.invoke.MethodHandles.lookup; +import java.lang.invoke.SwitchPoint; + +import java.nio.ByteBuffer; + +import java.sql.SQLException; + +import java.util.Iterator; +import java.util.List; +import static java.util.Objects.requireNonNull; + +import java.util.function.UnaryOperator; + +import org.postgresql.pljava.internal.Checked; +import org.postgresql.pljava.internal.SwitchPointCache.Builder; +import static org.postgresql.pljava.internal.SwitchPointCache.setConstant; + +import org.postgresql.pljava.model.*; +import static org.postgresql.pljava.model.MemoryContext.JavaMemoryContext; + +import static org.postgresql.pljava.pg.CatalogObjectImpl.*; +import static org.postgresql.pljava.pg.MemoryContextImpl.allocatingIn; +import static org.postgresql.pljava.pg.ModelConstants.*; +import static org.postgresql.pljava.pg.TupleDescImpl.Ephemeral; +import static org.postgresql.pljava.pg.TupleTableSlotImpl.heapTupleGetLightSlot; + +import org.postgresql.pljava.pg.adt.GrantAdapter; +import org.postgresql.pljava.pg.adt.NameAdapter; +import static org.postgresql.pljava.pg.adt.OidAdapter.REGCOLLATION_INSTANCE; +import static org.postgresql.pljava.pg.adt.Primitives.*; + +import org.postgresql.pljava.annotation.BaseUDT.Alignment; +import org.postgresql.pljava.annotation.BaseUDT.Storage; + +import org.postgresql.pljava.sqlgen.Lexicals.Identifier.Simple; +import org.postgresql.pljava.sqlgen.Lexicals.Identifier.Unqualified; + +import static org.postgresql.pljava.internal.UncheckedException.unchecked; + +abstract class AttributeImpl extends Addressed +implements + Nonshared, Named, + AccessControlled, Attribute +{ + // syscache id is ATTNUM; two key components: attrelid, attnum + // remember to account for ATTRIBUTE_FIXED_PART_SIZE when from tupledesc + + abstract SwitchPoint cacheSwitchPoint(); + + private static UnaryOperator s_initializer; + + /* Implementation of CatalogObject */ + + @Override + public > T of(RegClass.Known c) + { + throw new UnsupportedOperationException("of() on an Attribute"); + } + + /* Implementation of Addressed */ + + @Override + public RegClass.Known classId() + { + return RegClass.CLASSID; + } + + /** + * Overrides {@code cacheDescriptor} to correctly return the descriptor + * for {@code pg_attribute}. + *

    + * Because of the unusual addressing scheme for attributes, where + * the {@code classId} refers to {@code pg_class}, the inherited method + * would return the wrong descriptor. + */ + @Override + TupleDescriptor cacheDescriptor() + { + return CLASS.tupleDescriptor(); + } + + /** + * An attribute exists for as long as it has a non-invalidated containing + * tuple descriptor that can supply a byte buffer, whether or not it appears + * in the catalog. + */ + @Override + public boolean exists() + { + try + { + return null != rawBuffer(); + } + catch ( IllegalStateException e ) + { + return false; + } + } + + /** + * Fetch the entire tuple for this attribute from the PG {@code syscache}. + *

    + * The containing {@code TupleDescriptor} supplies a + * {@link #partialTuple partialTuple} covering the first + * {@code ATTRIBUTE_FIXED_PART_SIZE} bytes of this, where most often-needed + * properties are found, so this will be called only on requests for + * the properties that aren't found in that prefix. + */ + private static TupleTableSlot cacheTuple(AttributeImpl o) + { + ByteBuffer heapTuple; + + /* + * See this method in CatalogObjectImpl.Addressed for more on the choice + * of memory context and lifespan. + */ + try ( Checked.AutoCloseable ac = + allocatingIn(JavaMemoryContext()) ) + { + heapTuple = _searchSysCacheCopy2(ATTNUM, o.oid(), o.subId()); + if ( null == heapTuple ) + return null; + } + return heapTupleGetLightSlot(o.cacheDescriptor(), heapTuple, null); + } + + /* + * The super implementation nulls the TUPLE slot permanently; this + * class has RAWBUFFER and PARTIALTUPLE slots used similarly, so null those + * too. Transient will in turn override this and null nothing at all; its + * instances have the invalid Oid as a matter of course. + * + * It may well be that no circumstances exist where this version is called. + */ + @Override + void makeInvalidInstance(MethodHandle[] slots) + { + super.makeInvalidInstance(slots); + setConstant(slots, SLOT_RAWBUFFER, null); + setConstant(slots, SLOT_PARTIALTUPLE, null); + } + + /* Implementation of Named and AccessControlled */ + + private static Simple name(AttributeImpl o) throws SQLException + { + TupleTableSlot t = o.partialTuple(); + return + t.get(t.descriptor().sqlGet(Anum_pg_attribute_attname), + NameAdapter.SIMPLE_INSTANCE); + } + + private static List grants(AttributeImpl o) + throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return t.get(Att.ATTACL, GrantAdapter.LIST_INSTANCE); + } + + /* Implementation of Attribute */ + + AttributeImpl() + { + super(s_initializer.apply(new MethodHandle[NSLOTS])); + } + + static final int SLOT_RAWBUFFER; + static final int SLOT_PARTIALTUPLE; + + static final int SLOT_TYPE; + static final int SLOT_LENGTH; + static final int SLOT_DIMENSIONS; + // static final int SLOT_CACHEDOFFSET; -- read fresh every time, no slot + static final int SLOT_BYVALUE; + static final int SLOT_ALIGNMENT; + static final int SLOT_STORAGE; + // static final int SLOT_COMPRESSION; -- add this + static final int SLOT_NOTNULL; + static final int SLOT_HASDEFAULT; + static final int SLOT_HASMISSING; + static final int SLOT_IDENTITY; + static final int SLOT_GENERATED; + static final int SLOT_DROPPED; + static final int SLOT_LOCAL; + static final int SLOT_INHERITANCECOUNT; + static final int SLOT_COLLATION; + // static final int SLOT_OPTIONS; -- add this + // static final int SLOT_FDWOPTIONS; -- add this + // static final int SLOT_MISSINGVALUE; -- add this + + static final int NSLOTS; + + static + { + int i = CatalogObjectImpl.Addressed.NSLOTS; + s_initializer = + new Builder<>(AttributeImpl.class) + .withLookup(lookup()) + .withSwitchPoint(AttributeImpl::cacheSwitchPoint) + .withSlots(o -> o.m_slots) + .withCandidates(AttributeImpl.class.getDeclaredMethods()) + + /* + * First declare some slots whose consuming API methods are found + * on inherited interfaces. This requires some adjustment of method + * types so that run-time adaptation isn't needed. + */ + .withReceiverType(CatalogObjectImpl.Addressed.class) + .withDependent("cacheTuple", SLOT_TUPLE) + + .withReceiverType(CatalogObjectImpl.Named.class) + .withReturnType(Unqualified.class) + .withDependent( "name", SLOT_NAME) + + .withReceiverType(CatalogObjectImpl.AccessControlled.class) + .withReturnType(null) // cancel adjustment from above + .withDependent( "grants", SLOT_ACL) + + /* + * Next come slots where the compute and API methods are here. + */ + .withReceiverType(null) + .withDependent( "rawBuffer", SLOT_RAWBUFFER = i++) + .withDependent("partialTuple", SLOT_PARTIALTUPLE = i++) + + .withDependent( "type", SLOT_TYPE = i++) + .withDependent( "length", SLOT_LENGTH = i++) + .withDependent( "dimensions", SLOT_DIMENSIONS = i++) + .withDependent( "byValue", SLOT_BYVALUE = i++) + .withDependent( "alignment", SLOT_ALIGNMENT = i++) + .withDependent( "storage", SLOT_STORAGE = i++) + .withDependent( "notNull", SLOT_NOTNULL = i++) + .withDependent( "hasDefault", SLOT_HASDEFAULT = i++) + .withDependent( "hasMissing", SLOT_HASMISSING = i++) + .withDependent( "identity", SLOT_IDENTITY = i++) + .withDependent( "generated", SLOT_GENERATED = i++) + .withDependent( "dropped", SLOT_DROPPED = i++) + .withDependent( "local", SLOT_LOCAL = i++) + .withDependent("inheritanceCount", SLOT_INHERITANCECOUNT = i++) + .withDependent( "collation", SLOT_COLLATION = i++) + + .build(); + NSLOTS = i; + } + + static class Att + { + static final Attribute ATTACL; + static final Attribute ATTNDIMS; + static final Attribute ATTSTORAGE; + static final Attribute ATTHASDEF; + static final Attribute ATTHASMISSING; + static final Attribute ATTIDENTITY; + static final Attribute ATTGENERATED; + static final Attribute ATTISLOCAL; + static final Attribute ATTINHCOUNT; + static final Attribute ATTCOLLATION; + + static + { + Iterator itr = CLASS.tupleDescriptor().project( + "attacl", + "attndims", + "attstorage", + "atthasdef", + "atthasmissing", + "attidentity", + "attgenerated", + "attislocal", + "attinhcount", + "attcollation" + ).iterator(); + + ATTACL = itr.next(); + ATTNDIMS = itr.next(); + ATTSTORAGE = itr.next(); + ATTHASDEF = itr.next(); + ATTHASMISSING = itr.next(); + ATTIDENTITY = itr.next(); + ATTGENERATED = itr.next(); + ATTISLOCAL = itr.next(); + ATTINHCOUNT = itr.next(); + ATTCOLLATION = itr.next(); + + assert ! itr.hasNext() : "attribute initialization miscount"; + } + } + + /* computation methods */ + + /** + * Obtain the raw, heap-formatted readable byte buffer over this attribute. + *

    + * Because this is the {@code AttributeImpl} class, a few of the critical + * properties will be read directly via ByteBuffer methods, rather than + * using the {@code TupleTableSlot.get} API where a working + * {@code Attribute} must be supplied. + *

    + * The raw buffer is what the containing {@code TupleDescImpl} supplies, and + * it cuts off at {@code ATTRIBUTE_FIXED_PART_SIZE}. Retrieving properties + * beyond that point will require using {@code cacheTuple()} to fetch + * the whole tuple from the {@code syscache}. + */ + private static ByteBuffer rawBuffer(AttributeImpl o) + { + return + ((TupleDescImpl)o.containingTupleDescriptor()).slice(o.subId() - 1); + } + + /** + * A {@code TupleTableSlot} formed over the {@link #rawBuffer rawBuffer}, + * which holds only the first {@code ATTRIBUTE_FIXED_PART_SIZE} bytes of + * the full {@code pg_attribute} tuple. + *

    + * Supports the regular {@code TupleTableSlot.get} API for most properties + * (the ones that appear in the first {@code ATTRIBUTE_FIXED_PART_SIZE} + * bytes, and aren't needed for {@code TupleTableSlot.get} itself to work). + */ + private static TupleTableSlot partialTuple(AttributeImpl o) + { + return new TupleTableSlotImpl.Heap( + CLASS, o.cacheDescriptor(), o.rawBuffer(), null); + } + + private static RegType type(AttributeImpl o) + { + ByteBuffer b = o.rawBuffer(); + assert 4 == SIZEOF_pg_attribute_atttypid : "sizeof atttypid changed"; + assert 4 == SIZEOF_pg_attribute_atttypmod : "sizeof atttypmod changed"; + return + CatalogObjectImpl.Factory.formMaybeModifiedType( + b.getInt(OFFSET_pg_attribute_atttypid), + b.getInt(OFFSET_pg_attribute_atttypmod)); + } + + private static short length(AttributeImpl o) + { + ByteBuffer b = o.rawBuffer(); + assert 2 == SIZEOF_pg_attribute_attlen : "sizeof attlen changed"; + return b.getShort(OFFSET_pg_attribute_attlen); + } + + private static int dimensions(AttributeImpl o) throws SQLException + { + TupleTableSlot s = o.partialTuple(); + return s.get(Att.ATTNDIMS, INT4_INSTANCE); + } + + private static boolean byValue(AttributeImpl o) + { + ByteBuffer b = o.rawBuffer(); + assert 1 == SIZEOF_pg_attribute_attbyval : "sizeof attbyval changed"; + return 0 != b.get(OFFSET_pg_attribute_attbyval); + } + + private static Alignment alignment(AttributeImpl o) + { + ByteBuffer b = o.rawBuffer(); + assert 1 == SIZEOF_pg_attribute_attalign : "sizeof attalign changed"; + return alignmentFromCatalog(b.get(OFFSET_pg_attribute_attalign)); + } + + private static Storage storage(AttributeImpl o) throws SQLException + { + TupleTableSlot s = o.partialTuple(); + return + storageFromCatalog( + s.get(Att.ATTSTORAGE, INT1_INSTANCE)); + } + + private static boolean notNull(AttributeImpl o) + { + ByteBuffer b = o.rawBuffer(); + assert + 1 == SIZEOF_pg_attribute_attnotnull : "sizeof attnotnull changed"; + return 0 != b.get(OFFSET_pg_attribute_attnotnull); + } + + private static boolean hasDefault(AttributeImpl o) throws SQLException + { + TupleTableSlot s = o.partialTuple(); + return s.get(Att.ATTHASDEF, BOOLEAN_INSTANCE); + } + + private static boolean hasMissing(AttributeImpl o) throws SQLException + { // not 9.5 + TupleTableSlot s = o.partialTuple(); + return s.get(Att.ATTHASMISSING, BOOLEAN_INSTANCE); + } + + private static Identity identity(AttributeImpl o) throws SQLException + { // not 9.5 + TupleTableSlot s = o.partialTuple(); + byte v = s.get(Att.ATTIDENTITY, INT1_INSTANCE); + return identityFromCatalog(v); + } + + private static Generated generated(AttributeImpl o) throws SQLException + { // not 9.5 + TupleTableSlot s = o.partialTuple(); + byte v = s.get(Att.ATTGENERATED, INT1_INSTANCE); + return generatedFromCatalog(v); + } + + private static boolean dropped(AttributeImpl o) + { + ByteBuffer b = o.rawBuffer(); + assert + 1 == SIZEOF_pg_attribute_attisdropped + : "sizeof attisdropped changed"; + return 0 != b.get(OFFSET_pg_attribute_attisdropped); + } + + private static boolean local(AttributeImpl o) throws SQLException + { + TupleTableSlot s = o.partialTuple(); + return s.get(Att.ATTISLOCAL, BOOLEAN_INSTANCE); + } + + private static int inheritanceCount(AttributeImpl o) throws SQLException + { + TupleTableSlot s = o.partialTuple(); + return s.get(Att.ATTINHCOUNT, INT4_INSTANCE); + } + + private static RegCollation collation(AttributeImpl o) throws SQLException + { + TupleTableSlot s = o.partialTuple(); + return s.get(Att.ATTCOLLATION, REGCOLLATION_INSTANCE); + } + + /* private methods using cache slots like API methods do */ + + private ByteBuffer rawBuffer() + { + try + { + MethodHandle h = m_slots[SLOT_RAWBUFFER]; + return (ByteBuffer)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + private TupleTableSlot partialTuple() + { + try + { + MethodHandle h = m_slots[SLOT_PARTIALTUPLE]; + return (TupleTableSlot)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + /* API methods */ + + @Override + public RegType type() + { + try + { + MethodHandle h = m_slots[SLOT_TYPE]; + return (RegType)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public short length() + { + try + { + MethodHandle h = m_slots[SLOT_LENGTH]; + return (short)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public int dimensions() + { + try + { + MethodHandle h = m_slots[SLOT_DIMENSIONS]; + return (int)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public int cachedOffset() // perhaps useful for heap case? + { + ByteBuffer b = rawBuffer(); + assert 4 == SIZEOF_pg_attribute_attcacheoff + : "sizeof attcacheoff changed"; + return b.getInt(OFFSET_pg_attribute_attcacheoff); + } + + @Override + public boolean byValue() + { + try + { + MethodHandle h = m_slots[SLOT_BYVALUE]; + return (boolean)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public Alignment alignment() + { + try + { + MethodHandle h = m_slots[SLOT_ALIGNMENT]; + return (Alignment)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public Storage storage() + { + try + { + MethodHandle h = m_slots[SLOT_STORAGE]; + return (Storage)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public boolean notNull() + { + try + { + MethodHandle h = m_slots[SLOT_NOTNULL]; + return (boolean)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public boolean hasDefault() + { + try + { + MethodHandle h = m_slots[SLOT_HASDEFAULT]; + return (boolean)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public boolean hasMissing() // not 9.5 + { + try + { + MethodHandle h = m_slots[SLOT_HASMISSING]; + return (boolean)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public Identity identity() // not 9.5 + { + try + { + MethodHandle h = m_slots[SLOT_IDENTITY]; + return (Identity)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public Generated generated() // not 9.5 + { + try + { + MethodHandle h = m_slots[SLOT_GENERATED]; + return (Generated)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public boolean dropped() + { + try + { + MethodHandle h = m_slots[SLOT_HASMISSING]; + return (boolean)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public boolean local() + { + try + { + MethodHandle h = m_slots[SLOT_LOCAL]; + return (boolean)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public int inheritanceCount() + { + try + { + MethodHandle h = m_slots[SLOT_INHERITANCECOUNT]; + return (int)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public RegCollation collation() + { + try + { + MethodHandle h = m_slots[SLOT_COLLATION]; + return (RegCollation)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + // options + // fdwoptions + // missingValue + + @Override + public TupleDescriptor containingTupleDescriptor() + { + return relation().tupleDescriptor(); + } + + boolean foundIn(TupleDescriptor td) + { + int idx = subId() - 1; + return ( idx < td.size() ) && ( this == td.get(idx) ); + } + + /** + * An attribute that belongs to a full-fledged cataloged composite type. + *

    + * It holds a reference to the relation that defines the composite type + * layout. While that can always be found from the class and object IDs + * of the object address, that is too much fuss for as often as + * {@code relation()} is called. + */ + static class Cataloged extends AttributeImpl + { + private final RegClassImpl m_relation; + + Cataloged(RegClassImpl relation) + { + m_relation = requireNonNull(relation); + } + + @Override + SwitchPoint cacheSwitchPoint() + { + return m_relation.m_cacheSwitchPoint; + } + + @Override + public RegClass relation() + { + return m_relation; + } + } + + /** + * An attribute that belongs to a transient {@code TupleDescriptor}, not + * to any relation in the catalog (and therefore isn't really + * a {@code CatalogObject}, though it still pretends to be one). + *

    + * For now, this is simply a subclass of {@code AttributeImpl} to inherit + * most of the same machinery, and simply overrides and disables the methods + * of a real {@code CatalogObject}. In an alternative, it could be an + * independent implementation of the {@code Attribute} interface, but that + * could require more duplication of implementation. A cost of this + * implementation is that every instance will carry around one unused + * {@code CatalogObjectImpl.m_objectAddress} field. + */ + static class Transient extends AttributeImpl + { + private static final RegClass s_invalidClass = + CatalogObjectImpl.Factory.staticFormObjectId( + RegClass.CLASSID, InvalidOid); + + private final TupleDescriptor m_containingTupleDescriptor; + private final int m_attnum; + + SwitchPoint cacheSwitchPoint() + { + return + ((RegTypeImpl)m_containingTupleDescriptor.rowType()) + .cacheSwitchPoint(); + } + + Transient(TupleDescriptor td, int attnum) + { + m_containingTupleDescriptor = requireNonNull(td); + assert 0 < attnum : "nonpositive attnum in transient attribute"; + m_attnum = attnum; + } + + /* + * Do no nulling of slots (not even what the superclass method does) + * when created with the invalid Oid. *All* Transient instances have + * the invalid Oid! + */ + @Override + void makeInvalidInstance(MethodHandle[] slots) + { + } + + @Override + public int oid() + { + return InvalidOid; + } + + @Override + public int classOid() + { + return RegClass.CLASSID.oid(); + } + + @Override + public int subId() + { + return m_attnum; + } + + /** + * Returns true for an attribute of a transient {@code TupleDescriptor}, + * even though {@code oid()} will return {@code InvalidOid}. + *

    + * It's not clear any other convention would be less weird. + */ + @Override + public boolean isValid() + { + return true; + } + + @Override + public boolean equals(Object other) + { + if ( this == other ) + return true; + if ( ! super.equals(other) ) + return false; + return ! ( m_containingTupleDescriptor instanceof Ephemeral ); + } + + @Override + public int hashCode() + { + if ( m_containingTupleDescriptor instanceof Ephemeral ) + return System.identityHashCode(this); + return super.hashCode(); + } + + @Override + public RegClass relation() + { + return s_invalidClass; + } + + @Override + public TupleDescriptor containingTupleDescriptor() + { + return m_containingTupleDescriptor; + } + + @Override + boolean foundIn(TupleDescriptor td) + { + return m_containingTupleDescriptor == td; + } + } + + /** + * A transient attribute belonging to a synthetic tuple descriptor with + * one element of a specified {@code RegType}. + *

    + * Such a singleton tuple descriptor allows the {@code TupleTableSlot} API + * to be used as-is for related applications like array element access. + *

    + * Most methods simply delegate to the associated RegType. + */ + static class OfType extends Transient + { + private static final Simple s_anonymous = Simple.fromJava("?column?"); + + private final RegType m_type; + + OfType(TupleDescriptor td, RegType type) + { + super(td, 1); + m_type = requireNonNull(type); + } + + @Override + public Simple name() + { + return s_anonymous; + } + + @Override + public RegType type() + { + return m_type; + } + + @Override + public short length() + { + return m_type.length(); + } + + @Override + public int dimensions() + { + return m_type.dimensions(); + } + + @Override + public int cachedOffset() // perhaps useful for heap case? + { + return -1; + } + + @Override + public boolean byValue() + { + return m_type.byValue(); + } + + @Override + public Alignment alignment() + { + return m_type.alignment(); + } + + @Override + public Storage storage() + { + return m_type.storage(); + } + + @Override + public boolean notNull() + { + return m_type.notNull(); + } + + @Override + public boolean hasDefault() + { + return false; + } + + @Override + public boolean hasMissing() // not 9.5 + { + return false; + } + + @Override + public Identity identity() // not 9.5 + { + return Identity.INAPPLICABLE; + } + + @Override + public Generated generated() // not 9.5 + { + return Generated.INAPPLICABLE; + } + + @Override + public boolean dropped() + { + return false; + } + + @Override + public boolean local() + { + return true; + } + + @Override + public int inheritanceCount() + { + return 0; + } + + @Override + public RegCollation collation() + { + return m_type.collation(); + } + } + + private static Identity identityFromCatalog(byte b) + { + switch ( b ) + { + case (byte)'\0': return Identity.INAPPLICABLE; + case (byte) 'a': return Identity.GENERATED_ALWAYS; + case (byte) 'd': return Identity.GENERATED_BY_DEFAULT; + } + throw unchecked(new SQLException( + "unrecognized Identity '" + (char)b + "' in catalog", "XX000")); + } + + private static Generated generatedFromCatalog(byte b) + { + switch ( b ) + { + case (byte)'\0': return Generated.INAPPLICABLE; + case (byte) 's': return Generated.STORED; + } + throw unchecked(new SQLException( + "unrecognized Generated '" + (char)b + "' in catalog", "XX000")); + } +} diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/CatalogObjectImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/CatalogObjectImpl.java new file mode 100644 index 000000000..135edfed3 --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/pg/CatalogObjectImpl.java @@ -0,0 +1,1237 @@ +/* + * Copyright (c) 2022-2023 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.pg; + +import org.postgresql.pljava.Adapter; +import org.postgresql.pljava.Adapter.As; +import org.postgresql.pljava.Adapter.AsByte; +import org.postgresql.pljava.TargetList.Projection; + +import static org.postgresql.pljava.internal.Backend.threadMayEnterPG; +import org.postgresql.pljava.internal.CacheMap; +import org.postgresql.pljava.internal.Checked; +import org.postgresql.pljava.internal.Invocation; +import org.postgresql.pljava.internal.SwitchPointCache.Builder; +import static org.postgresql.pljava.internal.SwitchPointCache.setConstant; +import static org.postgresql.pljava.internal.UncheckedException.unchecked; + +import org.postgresql.pljava.adt.Array.AsFlatList; +import org.postgresql.pljava.adt.spi.Datum; + +import org.postgresql.pljava.model.*; +import static org.postgresql.pljava.model.MemoryContext.JavaMemoryContext; + +import static org.postgresql.pljava.pg.MemoryContextImpl.allocatingIn; +import org.postgresql.pljava.pg.ModelConstants; +import static org.postgresql.pljava.pg.ModelConstants.PG_VERSION_NUM; +import static org.postgresql.pljava.pg.TupleTableSlotImpl.heapTupleGetLightSlot; + +import org.postgresql.pljava.pg.adt.ArrayAdapter; +import static org.postgresql.pljava.pg.adt.OidAdapter.REGCLASS_INSTANCE; +import static org.postgresql.pljava.pg.adt.OidAdapter.REGTYPE_INSTANCE; +import org.postgresql.pljava.pg.adt.Primitives; +import org.postgresql.pljava.pg.adt.TextAdapter; + +import org.postgresql.pljava.sqlgen.Lexicals.Identifier; + +import java.io.IOException; + +import java.lang.annotation.Native; + +import java.lang.invoke.MethodHandle; +import static java.lang.invoke.MethodHandles.lookup; +import java.lang.invoke.SwitchPoint; + +import static java.lang.ref.Reference.reachabilityFence; + +import java.nio.ByteBuffer; +import static java.nio.ByteOrder.nativeOrder; + +import java.sql.SQLException; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.Optional; +import java.util.function.BooleanSupplier; +import java.util.function.Consumer; +import java.util.function.IntPredicate; +import java.util.function.UnaryOperator; +import java.util.function.Supplier; + +/** + * Implementation of the {@link CatalogObject CatalogObject} API for the + * PL/Java case of JVM running in the PostgreSQL backend process. + */ +public class CatalogObjectImpl implements CatalogObject +{ + /** + * ByteBuffer representing the PostgreSQL object address: {@code classid}, + * {@code objid}, {@code objsubid}. + *

    + * This buffer has to be retained as a key in the lookup data structure + * anyway, so this class will keep just one reference to the buffer, and + * read the values from it as needed. + *

    + * From the moment of construction here, the buffer must be treated as + * immutable. It may not actually be immutable: there is no way to alter an + * existing ByteBuffer to be readonly, but only to obtain a readonly copy, + * and the lookup data structure may have no API to reliably replace the key + * of an entry. But no reference to it should escape the lookup structure + * and this object, where it should be treated as if it cannot be written. + */ + private final ByteBuffer m_objectAddress; + + /** + * Hold the address during construction so it can be retrieved by this + * constructor without having to fuss with it in every subclass. + *

    + * Largely a notation convenience; it can be done the longwinded way if it + * proves a bottleneck. + */ + private static final ThreadLocal + s_address = new ThreadLocal<>(); + + private CatalogObjectImpl() + { + ByteBuffer b = s_address.get(); + + /* + * Here is a bit of a hack. No CatalogObjectImpl should ever be without + * its address buffer, with AttributeImpl.Transient being the sole + * exception. It supplies null, and overrides all the methods that rely + * on it. Perhaps it should simply be an independent implementation of + * the Attribute interface, rather than extending this class and wasting + * the address slot, but for now, this is the way it works. + */ + if ( null != b ) + assert saneByteBuffer(b, 12, "CatalogObjectImpl address"); + else if ( ! (this instanceof AttributeImpl.Transient) ) + throw new IllegalStateException( + "CatalogObjectImpl constructed without its address buffer"); + + m_objectAddress = b; + } + + public static CatalogObject of(int objId) + { + return Factory.form(InvalidOid, objId, 0); + } + + public static > + T of(RegClass.Known classId, int objId) + { + return Factory.staticFormObjectId(classId, objId); + } + + static boolean saneByteBuffer(ByteBuffer bb, int cap, String tag) + { + assert null != bb : tag + " null"; + assert cap == bb.capacity() : tag + " unexpected size"; + assert 0 == bb.position() : tag + " unexpected position"; + assert nativeOrder() == bb.order() : tag + " unexpected byte order"; + return true; + } + + @Override + protected final CatalogObjectImpl clone() throws CloneNotSupportedException + { + throw new CloneNotSupportedException(); + } + + @Override + public int oid() + { + return m_objectAddress.getInt(4); + } + + @Override + @SuppressWarnings("unchecked") + public > T of(RegClass.Known c) + { + if ( classOid() == c.oid() ) + return (T) this; + if ( classValid() && isValid() ) + throw new RuntimeException("XXX I'm not one of those"); + return Factory.staticFormObjectId(c, oid()); + } + + public int classOid() + { + return m_objectAddress.getInt(0); + } + + public int subId() + { + return m_objectAddress.getInt(8); + } + + @Override + public boolean isValid() + { + return InvalidOid != oid(); + } + + public boolean classValid() + { + return InvalidOid != classOid(); + } + + @Override + public boolean equals(Object other) + { + if ( this == other ) + return true; + if ( ! (other instanceof CatalogObjectImpl) ) + return false; + return + m_objectAddress.equals(((CatalogObjectImpl)other).m_objectAddress); + } + + @Override + public int hashCode() + { + return m_objectAddress.hashCode(); + } + + @Override + public String toString() + { + Class c = getClass(); + String pfx = c.getCanonicalName(); + return pfx.substring(1 + c.getPackageName().length()) + '[' + + Integer.toUnsignedString(classOid()) + ',' + + Integer.toUnsignedString(oid()) + ',' + + Integer.toUnsignedString(subId()) + ']'; + } + + /** + * Provider of the {@link CatalogObject.Factory CatalogObject.Factory} + * service, linking the {@link org.postgresql.pljava.model} API to the + * implementations in this package. + */ + public static final class Factory extends CatalogObject.Factory + { + public Factory() { } + + /* + * Include one @Native-annotated constant here to trigger header + * generation for this class. The generated header also includes + * all the static primitive constants inherited from Factory, so + * they all can be statically checked against the PostgreSQL values + * in ModelConstants.c. + */ + @Native static final int InvalidOid = CatalogObject.InvalidOid; + + private static final CacheMap + s_map = CacheMap.newConcurrent( + () -> ByteBuffer.allocate(12).order(nativeOrder())); + + @Override + protected > RegClass.Known + formClassIdImpl(int classId, Class clazz) + { + return staticFormClassId(classId, clazz); + } + + @Override + protected > + T formObjectIdImpl( + RegClass.Known classId, int objId, IntPredicate versionTest) + { + return staticFormObjectId(classId, objId, versionTest); + } + + @Override + protected RegRole.Grantee publicGranteeImpl() + { + return (RegRole.Grantee)form(AuthIdRelationId, InvalidOid, 0); + } + + @Override + protected Database currentDatabaseImpl(RegClass.Known classId) + { + return staticFormObjectId(classId, _currentDatabase()); + } + + private static native int _currentDatabase(); + + @Override + protected CharsetEncoding serverEncoding() + { + return CharsetEncodingImpl.serverEncoding(); + } + + @Override + protected CharsetEncoding clientEncoding() + { + return CharsetEncodingImpl.clientEncoding(); + } + + @Override + protected CharsetEncoding encodingFromOrdinal(int ordinal) + { + return CharsetEncodingImpl.fromOrdinal(ordinal); + } + + @Override + protected CharsetEncoding encodingFromName(String name) + { + return CharsetEncodingImpl.fromName(name); + } + + @Override + protected ResourceOwner resourceOwner(int which) + { + return ResourceOwnerImpl.known(which); + } + + @Override + protected MemoryContext memoryContext(int which) + { + return MemoryContextImpl.known(which); + } + + @Override + protected MemoryContext upperMemoryContext() + { + return Invocation.upperExecutorContext(); + } + + @SuppressWarnings("unchecked") + static > RegClass.Known + staticFormClassId(int classId, Class clazz) + { + return (RegClass.Known)form(RelationRelationId, classId, 0); + } + + static > + T staticFormObjectId(RegClass.Known classId, int objId) + { + return staticFormObjectId(classId, objId, v -> true); + } + + @SuppressWarnings("unchecked") + static > + T staticFormObjectId( + RegClass.Known classId, int objId, IntPredicate versionTest) + { + return (T)form(classId.oid(), + versionTest.test(PG_VERSION_NUM) ? objId : InvalidOid, 0); + } + + @SuppressWarnings("unchecked") + static > + T findObjectId(RegClass.Known classId, int objId) + { + CacheMap.Entry e = s_map.find(k -> + k.putInt(classId.oid()).putInt(objId).putInt(0)); + if ( null == e ) + return null; + return (T)e.get(); // may be null if it's been found unreachable + } + + static void forEachValue(Consumer action) + { + s_map.forEachValue(action); + } + + static RegType formMaybeModifiedType(int typeId, int typmod) + { + if ( -1 == typmod ) + return (RegType)form(TypeRelationId, typeId, 0); + + int subId = (0 == typmod) ? -1 : typmod; + + RegType result = + (RegType)s_map.weaklyCache( + b -> b.putInt(TypeRelationId).putInt(typeId).putInt(subId), + b -> + { + if ( RECORDOID == typeId ) + return + constructWith(RegTypeImpl.Blessed::new, b); + /* + * Look up the unmodified base type. This is a plain + * find(), not a cache(), because ConcurrentHashMap's + * computeIfAbsent contract requires that the action + * "must not attempt to update any other mappings of + * this map." If not found, we will have to return null + * from this attempt, then retry after caching the base. + */ + CacheMap.Entry e = s_map.find(k -> + k.putInt(TypeRelationId).putInt(typeId).putInt(0)); + if ( null == e ) + return null; + RegTypeImpl.NoModifier base = + (RegTypeImpl.NoModifier)e.get(); + if ( null == base ) // e isn't a strong reference + return null; + + return constructWith( + () -> new RegTypeImpl.Modified(base), b); + } + ); + + if ( null != result ) + return result; + + RegTypeImpl.NoModifier base = + (RegTypeImpl.NoModifier)form(TypeRelationId, typeId, 0); + + return + (RegType)s_map.weaklyCache( + b -> b.putInt(TypeRelationId).putInt(typeId).putInt(subId), + b -> constructWith( + () -> new RegTypeImpl.Modified(base), b)); + } + + static CatalogObject form(int classId, int objId, int objSubId) + { + assert classId != TypeRelationId || 0 == objSubId : + "nonzero objSubId passed to form() for a type"; + + /* + * As attributes aren't built here anymore, there is now no valid + * use of this method with a nonzero objSubId. See formAttribute. + */ + if ( 0 != objSubId ) + throw new UnsupportedOperationException( + "CatalogObjectImpl.Factory.form with nonzero objSubId"); + + Supplier ctor = + Optional.ofNullable(ctorIfKnown(classId, objId, objSubId)) + .orElseGet(() -> + InvalidOid == classId + ? CatalogObjectImpl::new : Addressed::new); + + return + s_map.weaklyCache( + b -> b.putInt(classId).putInt(objId).putInt(objSubId), + b -> constructWith(ctor, b) + ); + } + + /** + * Called only by {@code TupleDescImpl}, which is the only way + * cataloged attribute instances should be formed. + *

    + * {@code TupleDescImpl} is expected and trusted to supply only valid + * (positive) attribute numbers, and a {@code Supplier} that will + * construct the attribute with a reference to its correct corresponding + * {@code RegClass} (not checked here). Because {@code TupleDescImpl} + * constructs a bunch of attributes at once, that reduces overhead. + */ + static Attribute formAttribute( + int relId, int attNum, Supplier ctor) + { + assert attNum > 0 : "formAttribute attribute number validity"; + return (Attribute) + s_map.weaklyCache( + b -> b.putInt(RelationRelationId) + .putInt(relId).putInt(attNum), + b -> constructWith(ctor, b) + ); + } + + /** + * Invokes a supplied {@code CatalogObjectImpl} constructor, with the + * {@code ByteBuffer} containing its address in thread-local storage, + * so it isn't necessary for all constructors of all subtypes to pass + * the thing all the way up. + */ + static CatalogObjectImpl constructWith( + Supplier ctor, ByteBuffer b) + { + try + { + s_address.set(b); + return ctor.get(); + } + finally + { + s_address.remove(); + } + } + + /** + * Returns the constructor for the right subtype of + * {@code CatalogObject} if the classId identifies one + * for which an implementation is available; null otherwise. + */ + static Supplier ctorIfKnown( + int classId, int objId, int objSubId) + { + /* + * Used to read a static field of whatever class we will return + * a constructor for, to ensure its static initializer has already + * run and cannot be triggered by the instance creation, which + * happens within the CacheMap's computeIfAbsent and therefore could + * pose a risk of deadlock if the class must also create instances + * to populate its own statics. + */ + RegClass fieldRead = null; + + try + { + switch ( classId ) + { + case TypeRelationId: + fieldRead = RegType.CLASSID; + return RegTypeImpl.NoModifier::new; + case ProcedureRelationId: + fieldRead = RegProcedure.CLASSID; + return RegProcedureImpl::new; + case AuthIdRelationId: + fieldRead = RegRole.CLASSID; + return RegRoleImpl::new; + case DatabaseRelationId: + fieldRead = Database.CLASSID; + return DatabaseImpl::new; + case LanguageRelationId: + fieldRead = ProceduralLanguage.CLASSID; + return ProceduralLanguageImpl::new; + case NamespaceRelationId: + fieldRead = RegNamespace.CLASSID; + return RegNamespaceImpl::new; + case OperatorRelationId: + fieldRead = RegOperator.CLASSID; + return RegOperatorImpl::new; + case ExtensionRelationId: + fieldRead = Extension.CLASSID; + return ExtensionImpl::new; + case CollationRelationId: + fieldRead = RegCollation.CLASSID; + return RegCollationImpl::new; + case TSDictionaryRelationId: + fieldRead = RegDictionary.CLASSID; + return RegDictionaryImpl::new; + case TSConfigRelationId: + fieldRead = RegConfig.CLASSID; + return RegConfigImpl::new; + case RelationRelationId: + fieldRead = RegClass.CLASSID; + assert 0 == objSubId : + "CatalogObjectImpl.Factory.form attribute"; + if ( null != ctorIfKnown(objId, InvalidOid, 0) ) + return RegClassImpl.Known::new; + return RegClassImpl::new; + default: + return null; + } + } + finally + { + reachabilityFence(fieldRead); // insist the read really happens + } + } + + /** + * Called from native code with a relation oid when one relation's + * metadata has been invalidated, or with {@code InvalidOid} to flush + * all relation metadata. + */ + private static void invalidateRelation(int relOid) + { + assert threadMayEnterPG() : "RegClass invalidate thread"; + + List sps = new ArrayList<>(); + List postOps = new ArrayList<>(); + + if ( InvalidOid != relOid ) + { + RegClassImpl c = (RegClassImpl) + findObjectId(RegClass.CLASSID, relOid); + if ( null != c ) + c.invalidate(sps, postOps); + } + else // invalidate all RegClass instances + { + forEachValue(o -> + { + if ( o instanceof RegClassImpl ) + ((RegClassImpl)o).invalidate(sps, postOps); + }); + } + + if ( sps.isEmpty() ) + return; + + SwitchPoint.invalidateAll(sps.stream().toArray(SwitchPoint[]::new)); + + postOps.forEach(Runnable::run); + } + + /** + * Called from native code with the {@code catcache} hash of the type + * Oid (inconvenient, as that is likely different from the hash Java + * uses), or zero to flush metadata for all cached types. + */ + private static void invalidateType(int oidHash) + { + assert threadMayEnterPG() : "RegType invalidate thread"; + + List sps = new ArrayList<>(); + List postOps = new ArrayList<>(); + + forEachValue(o -> + { + if ( ! ( o instanceof RegTypeImpl ) ) + return; + if ( 0 == oidHash || oidHash == murmurhash32(o.oid()) ) + ((RegTypeImpl)o).invalidate(sps, postOps); + }); + + if ( sps.isEmpty() ) + return; + + SwitchPoint.invalidateAll(sps.stream().toArray(SwitchPoint[]::new)); + + postOps.forEach(Runnable::run); + } + } + + /* + * Go ahead and reserve fixed slot offsets for the common tuple/name/ + * namespace/owner/acl slots all within Addressed; those that + * correspond to interfaces a given subclass doesn't implement won't + * get used. Being fussier about it here would only complicate the code. + */ + static final int SLOT_TUPLE = 0; + static final int SLOT_NAME = 1; + static final int SLOT_NAMESPACE = 2; + static final int SLOT_OWNER = 3; + static final int SLOT_ACL = 4; + static final int NSLOTS = 5; + + @SuppressWarnings("unchecked") + static class Addressed> + extends CatalogObjectImpl implements CatalogObject.Addressed + { + /** + * Copy this constant here so it can be inherited without ceremony + * by subclasses of Addressed, which may need it when initializing + * attribute projections. Putting the copy up in CatalogObject itself + * is a problem if another compilation unit does import static of both + * CatalogObjectImpl.* and ModelConstants.* but there is little reason + * anyone would import CatalogObjectImpl.Addressed.*. + */ + static final int PG_VERSION_NUM = ModelConstants.PG_VERSION_NUM; + + /** + * Invalidation {@code SwitchPoint} for catalog objects that do not have + * their own selective invalidation callbacks. + *

    + * PostgreSQL only has a limited number of callback slots, so we do not + * consume one for every type of catalog object. Many will simply depend + * on this {@code SwitchPoint}, which will be invalidated at every + * transaction, subtransaction, or command counter change. + *

    + * XXX This is not strictly conservative: those are common points where + * PostgreSQL processes invalidations, but there are others (such as + * lock acquisitions) less easy to predict or intercept. + */ + static final SwitchPoint[] s_globalPoint = { new SwitchPoint() }; + static final UnaryOperator s_initializer; + final MethodHandle[] m_slots; + + static + { + s_initializer = + new Builder<>(CatalogObjectImpl.Addressed.class) + .withLookup(lookup()) + .withSwitchPoint(o -> s_globalPoint[0]) + .withCandidates( + CatalogObjectImpl.Addressed.class.getDeclaredMethods()) + .withSlots(o -> o.m_slots) + .withDependent("cacheTuple", SLOT_TUPLE) + .build(); + } + + static TupleTableSlot cacheTuple(CatalogObjectImpl.Addressed o) + { + ByteBuffer heapTuple; + + /* + * The longest we can hold a tuple (non-copied) from syscache is + * for the life of CurrentResourceOwner. We may want to cache the + * thing for longer, if we can snag invalidation messages for it. + * So, call _searchSysCacheCopy, in the JavaMemoryContext, which is + * immortal; we'll arrange below to explicitly free our copy later. + */ + try ( Checked.AutoCloseable ac = + allocatingIn(JavaMemoryContext()) ) + { + heapTuple = _searchSysCacheCopy1(o.cacheId(), o.oid()); + if ( null == heapTuple ) + return null; + } + + /* + * Because our copy is in an immortal memory context, we can + * pass null as the lifespan below. The DualState manager + * created for the TupleTableSlot will therefore not have + * any nativeStateReleased action; on javaStateUnreachable or + * javaStateReleased, it will free the tuple copy. + */ + return heapTupleGetLightSlot(o.cacheDescriptor(), heapTuple, null); + } + + /** + * Find a tuple in the PostgreSQL {@code syscache}, returning a copy + * made in the current memory context. + *

    + * The key(s) in PostgreSQL are really {@code Datum}; perhaps this + * should be refined to rely on {@link Datum.Accessor Datum.Accessor} + * somehow, once that implements store methods. For present purposes, + * we only need to support 32-bit integers, which will be zero-extended + * to {@code Datum} width. + */ + static native ByteBuffer _searchSysCacheCopy1(int cacheId, int key1); + + /** + * Find a tuple in the PostgreSQL {@code syscache}, returning a copy + * made in the current memory context. + *

    + * The key(s) in PostgreSQL are really {@code Datum}; perhaps this + * should be refined to rely on {@link Datum.Accessor Datum.Accessor} + * somehow, once that implements store methods. For present purposes, + * we only need to support 32-bit integers, which will be zero-extended + * to {@code Datum} width. + */ + static native ByteBuffer _searchSysCacheCopy2( + int cacheId, int key1, int key2); + + /** + * Search the table classId for at most one row with the Oid + * objId in column oidCol, using the index + * indexOid if it is not {@code InvalidOid}, returning null + * or a copy of the tuple in the current memory context. + *

    + * The returned tuple should be like one obtained from {@code syscache} + * in having no external TOAST pointers. The tuple descriptor is passed + * so that {@code toast_flatten_tuple} can be called if necessary. + */ + static native ByteBuffer _sysTableGetByOid( + int classId, int objId, int oidCol, int indexOid, long tupleDesc); + + /** + * Calls {@code lookup_rowtype_tupdesc_noerror} in the PostgreSQL + * {@code typcache}, returning a byte buffer over the result, or null + * if there isn't one (such as when called with a type oid that doesn't + * represent a composite type). + *

    + * Beware that "noerror" does not prevent an ugly {@code ereport} if + * the oid doesn't represent an existing type at all. + *

    + * Only to be called by {@code RegTypeImpl}. Declaring it here allows + * that class to be kept pure Java. + *

    + * This is used when we know we will be caching the result, so + * the native code will already have further incremented + * the reference count (for a counted descriptor) and released the pin + * {@code lookup_rowtype_tupdesc} took, thereby waiving leaked-reference + * warnings. We will hold on to the result until an invalidation message + * tells us not to. + *

    + * If the descriptor is not reference-counted, ordinarily it would be of + * dubious longevity, but when obtained from the {@code typcache}, + * such a descriptor is good for the life of the process (clarified + * in upstream commit bbc227e). + */ + static native ByteBuffer _lookupRowtypeTupdesc(int oid, int typmod); + + /** + * Return a byte buffer mapping the tuple descriptor + * for {@code pg_class} itself, using only the PostgreSQL + * {@code relcache}. + *

    + * Only to be called by {@code RegClassImpl}. Declaring it here allows + * that class to be kept pure Java. + *

    + * Other descriptor lookups on a {@code RegClass} are done by handing + * off to its associated row {@code RegType}, which will look in + * the {@code typcache}. But finding the associated row {@code RegType} + * isn't something {@code RegClass} can do before it has obtained this + * crucial tuple descriptor for its own structure. + *

    + * This method shall increment the reference count; the caller will pass + * the byte buffer directly to a {@code TupleDescImpl} constructor, + * which assumes that has already happened. The reference count shall be + * incremented without registering the descriptor for leak warnings. + */ + static native ByteBuffer _tupDescBootstrap(); + + /* XXX private */ Addressed() + { + this(s_initializer.apply(new MethodHandle[NSLOTS])); + } + + /** + * Constructor for use by a subclass that supplies a slots array + * (assumed to have length at least NSLOTS). + *

    + * It is the responsibility of the subclass to initialize the slots + * (including the first NSLOTS ones defined here; s_initializer can be + * used for those, if the default global-switchpoint behavior it offers + * is appropriate). + *

    + * Some subclasses may do oddball things, such as RegTypeImpl.Modified + * sharing the slots array of its base NoModifier instance. + *

    + * Any class that will do such a thing must also hold a strong reference + * to whatever instance the slots array 'belongs' to; a reference to + * just the array can't be counted on to keep the other instance live. + */ + Addressed(MethodHandle[] slots) + { + if ( InvalidOid == oid() ) + makeInvalidInstance(slots); + m_slots = slots; + } + + /** + * Adjust cache slots when constructing an invalid instance. + *

    + * This implementation stores a permanent null (insensitive to + * invalidation) in {@code SLOT_TUPLE}, which will cause {@code exists} + * to return false and other dependent methods to fail. + *

    + * An instance method because {@code AttributeImpl.Transient} will + * have to override it; those things have the invalid Oid in real life. + */ + void makeInvalidInstance(MethodHandle[] slots) + { + setConstant(slots, SLOT_TUPLE, null); + } + + @Override + public RegClass.Known classId() + { + return CatalogObjectImpl.Factory.staticFormClassId( + classOid(), (Class)getClass()); + } + + @Override + public boolean exists() + { + return null != cacheTuple(); + } + + TupleDescriptor cacheDescriptor() + { + return classId().tupleDescriptor(); + } + + TupleTableSlot cacheTuple() + { + try + { + MethodHandle h = m_slots[SLOT_TUPLE]; + return (TupleTableSlot)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + /** + * Inheritable placeholder to throw + * {@code UnsupportedOperationException} during development. + */ + int cacheId() + { + throw notyet(); + } + + @Override + public String toString() + { + String prefix = super.toString(); + if ( this instanceof CatalogObject.Named ) + { + try + { + CatalogObject.Named named = (CatalogObject.Named)this; + if ( ! exists() ) + return prefix; + if ( this instanceof CatalogObject.Namespaced ) + { + CatalogObject.Namespaced spaced = + (CatalogObject.Namespaced)this; + RegNamespace ns = spaced.namespace(); + if ( ns.exists() ) + return prefix + spaced.qualifiedName(); + return prefix + "(" + ns + ")." + named.name(); + } + return prefix + named.name(); + } + catch ( LinkageError e ) + { + /* + * Do nothing; LinkageError is expected when testing in, + * for example, jshell, and not in a PostgreSQL backend. + */ + } + } + return prefix; + } + + /** + * Utility class to create a {@link Projection Projection} using + * attribute names that may be conditional (on something like + * {@code PG_VERSION_NUM}). + *

    + * {@code alsoIf} adds strings to the list, if the condition is true, or + * the same number of nulls of the condition is false. + *

    + * {@code project} filters the list to only the non-null values, using + * those to form a {@code Projection} and obtain its iterator of + * attributes. + *

    + * This class then implements its own iterator of attributes, iterating + * for the length of the original name list, drawing from the + * Projection's iterator where a non-null name was saved, or producing + * null (and not incrementing the Projection's iterator) where a null + * was saved. + *

    + * The iterator can be used in a sequence of static final initializers, + * such that the final fields will end up containing the wanted + * Attribute instances where applicable, or null where not. + */ + static class AttNames implements Iterator + { + private ArrayList strings = new ArrayList<>(); + + private Iterator myItr; + private Projection it; + private Iterator itsItr; + + AttNames alsoIf(boolean p, String... toAdd) + { + if ( p ) + for ( String s : toAdd ) + strings.add(s); + else + for ( String s : toAdd ) + strings.add(null); + return this; + } + + AttNames project(Projection p) + { + String[] filtered = strings + .stream().filter(Objects::nonNull).toArray(String[]::new); + it = p.project(filtered); + itsItr = it.iterator(); + myItr = strings.iterator(); + return this; + } + + /** + * Returns a further projection of the one derived from the names. + *

    + * Caters to cases (so far only one in RegTypeImpl) where a + * computation method will want a projection of multiple attributes, + * instead of a single attribute. + *

    + * In the expected usage, the attribute arguments will have been + * supplied from the iterator, and will be null where the expected + * attributes do not exist. In that case, null must be returned for + * the projection. + */ + Projection project(Attribute... atts) + { + if ( Arrays.stream(atts).anyMatch(Objects::isNull) ) + return null; + return it.project(atts); + } + + @Override + public boolean hasNext() + { + return myItr.hasNext(); + } + + @Override + public Attribute next() + { + String myNext = myItr.next(); + if ( null == myNext ) + return null; + return itsItr.next(); + } + } + + /** + * Constructs a new {@link AttNames AttNames} instance and begins + * populating it, adding names unconditionally. + */ + static AttNames attNames(String... names) + { + return new AttNames().alsoIf(true, names); + } + } + + /** + * Mixin supplying a {@code shared()} method that returns false without + * having to materialize the {@code classId}. + */ + interface Nonshared> + extends CatalogObject.Addressed + { + @Override + default boolean shared() + { + return false; + } + } + + /** + * Mixin supplying a {@code shared()} method that returns true without + * having to materialize the {@code classId}. + */ + interface Shared> + extends CatalogObject.Addressed + { + @Override + default boolean shared() + { + return true; + } + } + + /* + * Note to self: name() should, of course, fail or return null + * when ! isValid(). That seems generally sensible, but code + * in interface RegRole contains the first conscious reliance on it. + */ + interface Named> + extends CatalogObject.Named + { + @Override + default T name() + { + try + { + MethodHandle h = + ((CatalogObjectImpl.Addressed)this).m_slots[SLOT_NAME]; + return (T)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + } + + interface Namespaced> + extends Named, CatalogObject.Namespaced + { + @Override + default RegNamespace namespace() + { + try + { + MethodHandle h = + ((CatalogObjectImpl.Addressed)this).m_slots[SLOT_NAMESPACE]; + return (RegNamespace)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + } + + interface Owned extends CatalogObject.Owned + { + @Override + default RegRole owner() + { + try + { + MethodHandle h = + ((CatalogObjectImpl.Addressed)this).m_slots[SLOT_OWNER]; + return (RegRole)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + } + + interface AccessControlled + extends CatalogObject.AccessControlled + { + @Override + default List grants() + { + try + { + MethodHandle h = + ((CatalogObjectImpl.Addressed)this).m_slots[SLOT_ACL]; + /* + * The value stored in the slot comes from GrantAdapter, which + * returns undifferentiated List, to be confidently + * narrowed here to List. + */ + return (List)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + default List grants(RegRole grantee) + { + throw notyet(); + } + } + + /** + * Instances of {@code ArrayAdapter} for types used in the catalogs. + *

    + * A holder interface so these won't be instantiated unless wanted. + */ + public interface ArrayAdapters + { + ArrayAdapter> REGCLASS_LIST_INSTANCE = + new ArrayAdapter<>(REGCLASS_INSTANCE, + AsFlatList.of(AsFlatList::nullsIncludedCopy)); + + ArrayAdapter> REGTYPE_LIST_INSTANCE = + new ArrayAdapter<>(REGTYPE_INSTANCE, + AsFlatList.of(AsFlatList::nullsIncludedCopy)); + + /** + * List of {@code Identifier.Simple} from an array of {@code TEXT} + * that represents SQL identifiers. + */ + ArrayAdapter> TEXT_NAME_LIST_INSTANCE = + new ArrayAdapter<>(TextAdapter.INSTANCE, + /* + * A custom array contract is an anonymous class, not just a + * lambda, so the compiler will record the actual type arguments + * with which it specializes the generic contract. + */ + new Adapter.Contract.Array<>() + { + @Override + public List construct( + int nDims, int[] dimsAndBounds, As adapter, + TupleTableSlot.Indexed slot) + throws SQLException + { + int n = slot.elements(); + Identifier.Simple[] names = new Identifier.Simple[n]; + for ( int i = 0; i < n; ++ i ) + names[i] = + Identifier.Simple.fromCatalog( + slot.get(i, adapter)); + return List.of(names); + } + }); + + /** + * List of {@code RegProcedure.ArgMode} from an array of {@code "char"}. + */ + ArrayAdapter> ARGMODE_LIST_INSTANCE = + new ArrayAdapter<>(Primitives.INT1_INSTANCE, + new Adapter.Contract.Array<>() + { + @Override + public List construct( + int nDims, int[] dimsAndBounds, AsByte adapter, + TupleTableSlot.Indexed slot) + throws SQLException + { + int n = slot.elements(); + RegProcedure.ArgMode[] modes = + new RegProcedure.ArgMode[n]; + for ( int i = 0; i < n; ++ i ) + { + byte in = slot.get(i, adapter); + switch ( in ) + { + case (byte)'i': + modes[i] = RegProcedure.ArgMode.IN; + break; + case (byte)'o': + modes[i] = RegProcedure.ArgMode.OUT; + break; + case (byte)'b': + modes[i] = RegProcedure.ArgMode.INOUT; + break; + case (byte)'v': + modes[i] = RegProcedure.ArgMode.VARIADIC; + break; + case (byte)'t': + modes[i] = RegProcedure.ArgMode.TABLE; + break; + default: + throw new UnsupportedOperationException( + String.format("Unrecognized " + + "procedure/function argument mode " + + "value %#x", in)); + } + } + return List.of(modes); + } + }); + } + + private static final StackWalker s_walker = + StackWalker.getInstance(Set.of(), 2); + + static UnsupportedOperationException notyet() + { + String what = s_walker.walk(s -> s + .skip(1) + .map(StackWalker.StackFrame::toStackTraceElement) + .findFirst() + .map(e -> " " + e.getClassName() + "." + e.getMethodName()) + .orElse("") + ); + return new UnsupportedOperationException( + "CatalogObject API" + what); + } + + static UnsupportedOperationException notyet(String what) + { + return new UnsupportedOperationException( + "CatalogObject API " + what); + } + + /** + * The Oid hash function used by the backend's Oid-based catalog caches + * to identify the entries affected by invalidation events. + *

    + * From hashutils.h. + */ + static int murmurhash32(int h) + { + h ^= h >>> 16; + h *= 0x85ebca6b; + h ^= h >>> 13; + h *= 0xc2b2ae35; + h ^= h >>> 16; + return h; + } +} diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/CharsetEncodingImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/CharsetEncodingImpl.java new file mode 100644 index 000000000..8e6692c14 --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/pg/CharsetEncodingImpl.java @@ -0,0 +1,256 @@ +/* + * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.pg; + +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; +import static java.nio.ByteOrder.nativeOrder; +import java.nio.CharBuffer; + +import java.nio.charset.CharacterCodingException; +import java.nio.charset.Charset; +import java.nio.charset.CharsetEncoder; +import java.nio.charset.CoderResult; +import static java.nio.charset.StandardCharsets.ISO_8859_1; +import static java.nio.charset.StandardCharsets.US_ASCII; +import static java.nio.charset.StandardCharsets.UTF_8; + +import java.util.regex.Pattern; + +import static org.postgresql.pljava.internal.Backend.doInPG; +import static org.postgresql.pljava.internal.Backend.threadMayEnterPG; +import org.postgresql.pljava.internal.CacheMap; + +import org.postgresql.pljava.model.CharsetEncoding; + +import static org.postgresql.pljava.pg.ModelConstants.NAMEDATALEN; +import static org.postgresql.pljava.pg.ModelConstants.PG_ENCODING_BE_LAST; +import static org.postgresql.pljava.pg.ModelConstants.PG_LATIN1; +import static org.postgresql.pljava.pg.ModelConstants.PG_SQL_ASCII; +import static org.postgresql.pljava.pg.ModelConstants.PG_UTF8; + +class CharsetEncodingImpl implements CharsetEncoding +{ + private static final CacheMap s_byOrdinal = + CacheMap.newConcurrent( + () -> ByteBuffer.allocate(4).order(nativeOrder())); + + private static final ByteBuffer s_nameWindow = + ByteBuffer.allocateDirect(NAMEDATALEN); + + private static final Pattern s_name_sqlascii = Pattern.compile( + "(?i)(?:X[-_]?+)?+(?:PG)?+SQL[-_]?+ASCII"); + + private static final String s_property = "org.postgresql.server.encoding"; + + /** + * Only called once to initialize the {@code SERVER_ENCODING} static. + *

    + * Doesn't use {@code fromOrdinal}, because that method will check against + * {@code SERVER_ENCODING}. + */ + static CharsetEncoding serverEncoding() + { + String charsetOverride = System.getProperty(s_property); + CharsetEncoding result = doInPG(() -> + { + int ordinal = EarlyNatives._serverEncoding(); + return s_byOrdinal.softlyCache( + b -> b.putInt(ordinal), + b -> new CharsetEncodingImpl(ordinal, charsetOverride) + ); + }); + if ( null != result.charset() ) + { + System.setProperty(s_property, result.charset().name()); + return result; + } + throw new UnsupportedOperationException( + "No Java Charset found for PostgreSQL server encoding " + + "\"" + result.name() + "\" (" + result.ordinal() +"). Consider " + + "adding -D" + s_property + "=... in pljava.vmoptions."); + } + + static CharsetEncoding clientEncoding() + { + return doInPG(() -> fromOrdinal(EarlyNatives._clientEncoding())); + } + + static CharsetEncoding fromOrdinal(int ordinal) + { + if ( SERVER_ENCODING.ordinal() == ordinal ) + return SERVER_ENCODING; + return s_byOrdinal.softlyCache( + b -> b.putInt(ordinal), + b -> doInPG(() -> new CharsetEncodingImpl(ordinal, null)) + ); + } + + static CharsetEncoding fromName(String name) + { + try + { + return doInPG(() -> + { + s_nameWindow.clear(); + /* + * Charset names should all be ASCII, according to IANA, + * which neatly skirts a "how do I find the encoder for + * the name of my encoding?" conundrum. + */ + CharsetEncoder e = US_ASCII.newEncoder(); + CoderResult r = e.encode( + CharBuffer.wrap(name), s_nameWindow, true); + if ( r.isUnderflow() ) + r = e.flush(s_nameWindow); + if ( ! r.isUnderflow() ) + r.throwException(); + /* + * PG will want a NUL-terminated string (and yes, the NAME + * datatype is limited to NAMEDATALEN - 1 encoded octets + * plus the NUL, so if this doesn't fit, overflow exception + * is the right outcome). + */ + s_nameWindow.put((byte)0).flip(); + int o = EarlyNatives._nameToOrdinal(s_nameWindow); + if ( -1 != o ) + return fromOrdinal(o); + if ( s_name_sqlascii.matcher(name).matches() ) + return fromOrdinal(PG_SQL_ASCII); + throw new IllegalArgumentException( + "no such PostgreSQL character encoding: \"" + + name + "\""); + } + ); + } + catch ( BufferOverflowException | CharacterCodingException e ) + { + throw new IllegalArgumentException( + "no such PostgreSQL character encoding: \"" + name + "\"", e); + } + } + + private final int m_ordinal; + private final String m_name; + private final String m_icuName; + private final Charset m_charset; + + private CharsetEncodingImpl(int ordinal, String charsetOverride) + { + assert threadMayEnterPG(); + ByteBuffer b = EarlyNatives._ordinalToName(ordinal); + if ( null == b ) + throw new IllegalArgumentException( + "no such PostgreSQL character encoding: " + ordinal); + + m_ordinal = ordinal; + + try + { + m_name = US_ASCII.newDecoder().decode(b).toString(); + } + catch ( CharacterCodingException e ) + { + throw new AssertionError( + "PG encoding " + ordinal + " has a non-ASCII name"); + } + + String altName = null; + if ( usableOnServer() ) + { + b = EarlyNatives._ordinalToIcuName(ordinal); + if ( null != b ) + { + try + { + altName = US_ASCII.newDecoder().decode(b).toString(); + } + catch ( CharacterCodingException e ) + { + throw new AssertionError( + "PG encoding " + ordinal + " has a non-ASCII ICU name"); + } + } + } + m_icuName = altName; + + Charset c = null; + if ( null == charsetOverride ) + { + switch ( ordinal ) + { + case PG_LATIN1 : c = ISO_8859_1; break; + case PG_UTF8 : c = UTF_8 ; break; + default: + } + } + else + altName = charsetOverride; + + if ( null == c ) + { + try + { + c = Charset.forName(null != altName ? altName : m_name); + } + catch ( IllegalArgumentException e ) + { + } + } + m_charset = c; + } + + @Override + public int ordinal() + { + return m_ordinal; + } + + @Override + public String name() + { + return m_name; + } + + @Override + public String icuName() + { + return m_icuName; + } + + @Override + public boolean usableOnServer() + { + return 0 <= m_ordinal && m_ordinal <= PG_ENCODING_BE_LAST; + } + + @Override + public Charset charset() + { + return m_charset; + } + + @Override + public String toString() + { + return "CharsetEncoding[" + m_ordinal + "]" + m_name; + } + + private static class EarlyNatives + { + private static native int _serverEncoding(); + private static native int _clientEncoding(); + private static native int _nameToOrdinal(ByteBuffer nulTerminated); + private static native ByteBuffer _ordinalToName(int ordinal); + private static native ByteBuffer _ordinalToIcuName(int ordinal); + } +} diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/DatabaseImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/DatabaseImpl.java new file mode 100644 index 000000000..8c1f609cd --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/pg/DatabaseImpl.java @@ -0,0 +1,304 @@ +/* + * Copyright (c) 2022-2023 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.pg; + +import java.lang.invoke.MethodHandle; +import static java.lang.invoke.MethodHandles.lookup; +import java.lang.invoke.SwitchPoint; + +import java.sql.SQLException; + +import java.util.Iterator; +import java.util.List; + +import java.util.function.UnaryOperator; + +import org.postgresql.pljava.internal.SwitchPointCache.Builder; + +import org.postgresql.pljava.model.*; + +import org.postgresql.pljava.pg.CatalogObjectImpl.*; +import static org.postgresql.pljava.pg.ModelConstants.DATABASEOID; // syscache + +import org.postgresql.pljava.pg.adt.EncodingAdapter; +import org.postgresql.pljava.pg.adt.GrantAdapter; +import static org.postgresql.pljava.pg.adt.NameAdapter.SIMPLE_INSTANCE; +import static org.postgresql.pljava.pg.adt.NameAdapter.AS_STRING_INSTANCE; +import static org.postgresql.pljava.pg.adt.OidAdapter.REGROLE_INSTANCE; +import static org.postgresql.pljava.pg.adt.Primitives.BOOLEAN_INSTANCE; +import static org.postgresql.pljava.pg.adt.Primitives.INT4_INSTANCE; + +import org.postgresql.pljava.sqlgen.Lexicals.Identifier.Simple; +import org.postgresql.pljava.sqlgen.Lexicals.Identifier.Unqualified; + +import static org.postgresql.pljava.internal.UncheckedException.unchecked; + +class DatabaseImpl extends Addressed +implements + Shared, Named, Owned, + AccessControlled, Database +{ + private static UnaryOperator s_initializer; + + /* Implementation of Addressed */ + + @Override + public RegClass.Known classId() + { + return CLASSID; + } + + @Override + int cacheId() + { + return DATABASEOID; + } + + /* Implementation of Named, Owned, AccessControlled */ + + private static Simple name(DatabaseImpl o) throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return t.get(Att.DATNAME, SIMPLE_INSTANCE); + } + + private static RegRole owner(DatabaseImpl o) throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return t.get(Att.DATDBA, REGROLE_INSTANCE); + } + + private static List grants(DatabaseImpl o) + throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return t.get(Att.DATACL, GrantAdapter.LIST_INSTANCE); + } + + /* Implementation of Database */ + + /** + * Merely passes the supplied slots array to the superclass constructor; all + * initialization of the slots will be the responsibility of the subclass. + */ + DatabaseImpl() + { + super(s_initializer.apply(new MethodHandle[NSLOTS])); + } + + static final int SLOT_ENCODING; + static final int SLOT_COLLATE; + static final int SLOT_CTYPE; + static final int SLOT_TEMPLATE; + static final int SLOT_ALLOWCONNECTION; + static final int SLOT_CONNECTIONLIMIT; + static final int NSLOTS; + + static + { + int i = CatalogObjectImpl.Addressed.NSLOTS; + s_initializer = + new Builder<>(DatabaseImpl.class) + .withLookup(lookup()) + .withSwitchPoint(o -> s_globalPoint[0]) + .withSlots(o -> o.m_slots) + .withCandidates(DatabaseImpl.class.getDeclaredMethods()) + + .withReceiverType(CatalogObjectImpl.Named.class) + .withReturnType(Unqualified.class) + .withDependent( "name", SLOT_NAME) + .withReturnType(null) + .withReceiverType(CatalogObjectImpl.Owned.class) + .withDependent( "owner", SLOT_OWNER) + .withReceiverType(CatalogObjectImpl.AccessControlled.class) + .withDependent( "grants", SLOT_ACL) + + .withReceiverType(null) + .withDependent( "encoding", SLOT_ENCODING = i++) + .withDependent( "collate", SLOT_COLLATE = i++) + .withDependent( "ctype", SLOT_CTYPE = i++) + .withDependent( "template", SLOT_TEMPLATE = i++) + .withDependent("allowConnection", SLOT_ALLOWCONNECTION = i++) + .withDependent("connectionLimit", SLOT_CONNECTIONLIMIT = i++) + + .build() + /* + * Add these slot initializers after what Addressed does. + */ + .compose(CatalogObjectImpl.Addressed.s_initializer)::apply; + NSLOTS = i; + } + + static class Att + { + static final Attribute DATNAME; + static final Attribute DATDBA; + static final Attribute DATACL; + static final Attribute ENCODING; + static final Attribute DATCOLLATE; + static final Attribute DATCTYPE; + static final Attribute DATISTEMPLATE; + static final Attribute DATALLOWCONN; + static final Attribute DATCONNLIMIT; + + static + { + Iterator itr = CLASSID.tupleDescriptor().project( + "datname", + "datdba", + "datacl", + "encoding", + "datcollate", + "datctype", + "datistemplate", + "datallowconn", + "datconnlimit" + ).iterator(); + + DATNAME = itr.next(); + DATDBA = itr.next(); + DATACL = itr.next(); + ENCODING = itr.next(); + DATCOLLATE = itr.next(); + DATCTYPE = itr.next(); + DATISTEMPLATE = itr.next(); + DATALLOWCONN = itr.next(); + DATCONNLIMIT = itr.next(); + + assert ! itr.hasNext() : "attribute initialization miscount"; + } + } + + /* computation methods */ + + private static CharsetEncoding encoding(DatabaseImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(Att.ENCODING, EncodingAdapter.INSTANCE); + } + + private static String collate(DatabaseImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(Att.DATCOLLATE, AS_STRING_INSTANCE); + } + + private static String ctype(DatabaseImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(Att.DATCTYPE, AS_STRING_INSTANCE); + } + + private static boolean template(DatabaseImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(Att.DATISTEMPLATE, BOOLEAN_INSTANCE); + } + + private static boolean allowConnection(DatabaseImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(Att.DATALLOWCONN, BOOLEAN_INSTANCE); + } + + private static int connectionLimit(DatabaseImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(Att.DATCONNLIMIT, INT4_INSTANCE); + } + + /* API methods */ + + @Override + public CharsetEncoding encoding() + { + try + { + MethodHandle h = m_slots[SLOT_ENCODING]; + return (CharsetEncoding)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public String collate() + { + try + { + MethodHandle h = m_slots[SLOT_COLLATE]; + return (String)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public String ctype() + { + try + { + MethodHandle h = m_slots[SLOT_CTYPE]; + return (String)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public boolean template() + { + try + { + MethodHandle h = m_slots[SLOT_TEMPLATE]; + return (boolean)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public boolean allowConnection() + { + try + { + MethodHandle h = m_slots[SLOT_ALLOWCONNECTION]; + return (boolean)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public int connectionLimit() + { + try + { + MethodHandle h = m_slots[SLOT_CONNECTIONLIMIT]; + return (int)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } +} diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/DatumImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/DatumImpl.java new file mode 100644 index 000000000..3e4c906b8 --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/pg/DatumImpl.java @@ -0,0 +1,295 @@ +/* + * Copyright (c) 2022-2023 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.pg; + +import java.io.Closeable; +import java.io.FilterInputStream; +import java.io.InputStream; +import java.io.IOException; + +import java.nio.ByteBuffer; + +import java.sql.SQLException; + +import org.postgresql.pljava.adt.spi.Datum; +import org.postgresql.pljava.adt.spi.Verifier; + +import org.postgresql.pljava.internal.ByteBufferInputStream; +import org.postgresql.pljava.internal.VarlenaWrapper; // javadoc + +import static org.postgresql.pljava.pg.CatalogObjectImpl.notyet; + +/** + * Contains implementation for {@link Datum Datum}. + *

    + * This is also implemented by {@link VarlenaWrapper VarlenaWrapper}, which is + * carried over from PL/Java 1.5.x, where it could originally be constructed + * only from native code. It has been minimally adapted to fit into this new + * scheme, and in future should fit more cleanly. + */ +public interface DatumImpl extends Datum +{ + @Override + default void verify(Verifier.OfBuffer v) throws SQLException + { + throw notyet(); + } + + @Override + default void verify(Verifier.OfStream v) throws SQLException + { + throw notyet(); + } + + /** + * Dissociate the datum from Java and return its address to native code. + */ + long adopt() throws SQLException; + + default String toString(Object o) + { + Class c = (null == o ? this : o).getClass(); + String cn = c.getCanonicalName(); + int pnl = c.getPackageName().length(); + return cn.substring(1 + pnl); + } + + /** + * Implementation of {@link Datum.Input Datum.Input}. + */ + abstract class Input implements Datum.Input, DatumImpl + { + @Override + public String toString() + { + return toString(this); + } + + @Override + public IStream inputStream() throws SQLException + { + return new IStream<>(this); + } + + @Override + public void verify(Verifier.OfStream v) throws SQLException + { + try ( IStream is = inputStream() ) + { + is.verify(v); + } + catch ( IOException e ) + { + throw new SQLException( + "Exception verifying Datum content: " + + e.getMessage(), "XX000", e); + } + } + + /** + * A Datum.Input copied onto the Java heap to depend on no native state, + * so {@code pin} and {@code unpin} are no-ops. + */ + static class JavaCopy extends DatumImpl.Input + { + private ByteBuffer m_buffer; + + public JavaCopy(ByteBuffer b) + { + assert ! b.isDirect() : + "ByteBuffer passed to a JavaCopy Datum constructor is direct"; + m_buffer = b; + } + + @Override + public String toString(Object o) + { + return String.format("%s %s", + super.toString(o), m_buffer); + } + + @Override + public ByteBuffer buffer() throws SQLException + { + ByteBuffer b = m_buffer; + if ( b == null ) + throw new SQLException("Datum used after close", "55000"); + return b; + } + + @Override + public void close() throws IOException + { + m_buffer = null; + } + + @Override + public long adopt() throws SQLException + { + throw new UnsupportedOperationException( + "XXX Datum JavaCopy.adopt"); + } + } + } + + /** + * {@link InputStream InputStream} view of a {@code Datum.Input}. + */ + public static class IStream + extends ByteBufferInputStream implements DatumImpl + { + protected final T m_datum; + + /** + * A duplicate of the {@code Datum.Input}'s byte buffer, + * so its {@code position} and {@code mark} can be updated by the + * {@code InputStream} operations without affecting the original + * (therefore multiple {@code Stream}s may read one {@code Input}). + */ + private final ByteBuffer m_movingBuffer; + + protected IStream(T datum) throws SQLException + { + m_datum = datum; + ByteBuffer b = datum.buffer(); + m_movingBuffer = b.duplicate().order(b.order()); + } + + @Override + public String toString(Object o) + { + return String.format("%s %s", + m_datum.toString(o), m_open ? "open" : "closed"); + } + + @Override + protected void pin() throws IOException + { + if ( ! m_open ) + throw new IOException("Read from closed Datum"); + try + { + m_datum.pin(); + } + catch ( SQLException e ) + { + throw new IOException(e.getMessage(), e); + } + } + + @Override + protected void unpin() + { + m_datum.unpin(); + } + + @Override + protected ByteBuffer buffer() + { + return m_movingBuffer; + } + + @Override + public void close() throws IOException + { + if ( m_datum.pinUnlessReleased() ) + return; + try + { + super.close(); + m_datum.close(); + } + finally + { + unpin(); + } + } + + @Override + public long adopt() throws SQLException + { + m_datum.pin(); + try + { + if ( ! m_open ) + throw new SQLException( + "Cannot adopt Datum.Input after " + + "it is closed", "55000"); + return m_datum.adopt(); + } + finally + { + m_datum.unpin(); + } + } + + /** + * Apply a {@code Verifier} to the input data. + *

    + * This should only be necessary if the input wrapper is being used + * directly as an output item, and needs verification that it + * conforms to the format of the target type. + *

    + * The current position must be at the beginning of the stream. The + * verifier must leave it at the end to confirm the entire stream + * was examined. There should be no need to reset the position here, + * as the only anticipated use is during an {@code adopt}, and the + * native code will only care about the varlena's address. + */ + public void verify(Verifier.OfStream v) throws SQLException + { + /* + * This is only called from some client code's adopt() method, + * calls to which are serialized through Backend.THREADLOCK + * anyway, so holding a pin here for the duration doesn't + * further limit concurrency. Hold m_lock's monitor also to + * block any extraneous reading interleaved with the verifier. + */ + m_datum.pin(); + try + { + ByteBuffer buf = buffer(); + synchronized ( m_lock ) + { + if ( 0 != buf.position() ) + throw new SQLException( + "Input data to be verified " + + " not positioned at start", + "55000"); + InputStream dontCloseMe = new FilterInputStream(this) + { + @Override + public void close() throws IOException { } + }; + v.verify(dontCloseMe); + if ( 0 != buf.remaining() ) + throw new SQLException( + "Verifier finished prematurely"); + } + } + catch ( SQLException | RuntimeException e ) + { + throw e; + } + catch ( Exception e ) + { + throw new SQLException( + "Exception verifying Datum content: " + + e.getMessage(), "XX000", e); + } + finally + { + m_datum.unpin(); + } + } + } +} diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/DatumUtils.java b/pljava/src/main/java/org/postgresql/pljava/pg/DatumUtils.java new file mode 100644 index 000000000..3ce77adb5 --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/pg/DatumUtils.java @@ -0,0 +1,1098 @@ +/* + * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.pg; + +import java.io.Closeable; +import java.io.IOException; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.BufferUnderflowException; + +import java.sql.SQLException; + +import java.util.List; + +import org.postgresql.pljava.adt.spi.Datum; + +import org.postgresql.pljava.model.Attribute; +import org.postgresql.pljava.model.MemoryContext; +import org.postgresql.pljava.model.RegType; +import org.postgresql.pljava.model.ResourceOwner; +import static org.postgresql.pljava.model.MemoryContext.TopTransactionContext; +import static + org.postgresql.pljava.model.ResourceOwner.TopTransactionResourceOwner; +import org.postgresql.pljava.model.TupleDescriptor; +import org.postgresql.pljava.model.TupleTableSlot; + +import static org.postgresql.pljava.internal.Backend.doInPG; +import org.postgresql.pljava.internal.DualState; +import org.postgresql.pljava.internal.LifespanImpl.Addressed; + +import static org.postgresql.pljava.pg.CatalogObjectImpl.notyet; +import static org.postgresql.pljava.pg.ModelConstants.*; + +/** + * Implementations of {@link Datum.Accessor} and a collection of related + * static methods. + */ +public /*XXX*/ class DatumUtils +{ + static final boolean BIG_ENDIAN = + ByteOrder.BIG_ENDIAN == ByteOrder.nativeOrder(); + + public static TupleTableSlot.Indexed indexedTupleSlot( + RegType type, int elements, ByteBuffer nulls, ByteBuffer values) + { + TupleDescriptor td = new TupleDescImpl.OfType(type); + return new TupleTableSlotImpl.Heap.Indexed(td, elements, nulls, values); + } + + public static long addressOf(ByteBuffer bb) + { + if ( bb.isDirect() ) + return _addressOf(bb); + throw new IllegalArgumentException( + "addressOf(non-direct " + bb + ")"); + } + + public static long fetchPointer(ByteBuffer bb, int offset) + { + return Accessor.ByReference.Deformed.s_pointerAccessor + .getLongZeroExtended(bb, offset); + } + + public static void storePointer(ByteBuffer bb, int offset, long value) + { + /* + * Stopgap implementation; use s_pointer_accessor as above once + * accessors have store methods. + */ + if ( 4 == SIZEOF_DATUM ) + bb.putInt(offset, (int)value); + else + bb.putLong(offset, value); + } + + public static ByteBuffer asReadOnlyNativeOrder(ByteBuffer bb) + { + if ( ! bb.isReadOnly() ) + bb = bb.asReadOnlyBuffer(); + return bb.order(ByteOrder.nativeOrder()); + } + + static ByteBuffer mapFixedLength(long nativeAddress, int length) + { + if ( 0 == nativeAddress ) + return null; + ByteBuffer bb = _map(nativeAddress, length); + return asReadOnlyNativeOrder(bb); + } + + public static ByteBuffer mapFixedLength( + ByteBuffer bb, int offset, int length) + { + // Java 13: bb.slice(offset, length).order(bb.order()) + ByteBuffer bnew = bb.duplicate(); + bnew.position(offset).limit(offset + length); + return bnew.slice().order(bb.order()); + } + + static ByteBuffer mapCString(long nativeAddress) + { + if ( 0 == nativeAddress ) + return null; + ByteBuffer bb = _mapCString(nativeAddress); + if ( null == bb ) + { + /* + * This may seem an odd exception to throw in this case (the + * native code found no NUL terminator within the maximum size + * allowed for a ByteBuffer), but it is the same exception that + * would be thrown by the mapCString(ByteBuffer,int) flavor if + * it found no NUL within the bounds of its source buffer. + */ + throw new BufferUnderflowException(); + } + return asReadOnlyNativeOrder(bb); + } + + public static ByteBuffer mapCString(ByteBuffer bb, int offset) + { + ByteBuffer bnew = bb.duplicate(); + int i = offset; + while ( 0 != bnew.get(i) ) + ++i; + bnew.position(offset).limit(i); + return bnew.slice().order(bb.order()); + } + + static Datum.Input mapVarlena(long nativeAddress, + ResourceOwner resowner, MemoryContext memcontext) + { + long ro = ((Addressed)resowner).address(); + long mc = ((Addressed)memcontext).address(); + return doInPG(() -> _mapVarlena(null, nativeAddress, ro, mc)); + } + + static Datum.Input mapVarlena(ByteBuffer bb, long offset, + ResourceOwner resowner, MemoryContext memcontext) + { + long ro = ((Addressed)resowner).address(); + long mc = ((Addressed)memcontext).address(); + return doInPG(() -> _mapVarlena(bb, offset, ro, mc)); + } + + /** + * For now, just return the inline size (the size to be skipped if stepping + * over this varlena in a heap tuple). + *

    + * This is a reimplementation of some of the top of {@code postgres.h}, so + * that this common operation can be done without a JNI call to the C code. + */ + public static int inspectVarlena(ByteBuffer bb, int offset) + { + byte b1 = bb.get(offset); + byte shortbit; + int tagsize; + + if ( BIG_ENDIAN ) + { + shortbit = (byte)(b1 & 0x80); + if ( 0 == shortbit ) // it has a four-byte header and we're aligned + { + // here is where to discern if it's inline compressed if we care + return bb.getInt(offset) & 0x3FFFFFFF; + } + if ( shortbit != b1 ) // it is inline and short + return b1 & 0x7F; + } + else // little endian + { + shortbit = (byte)(b1 & 0x01); + if ( 0 == shortbit ) // it has a four-byte header and we're aligned + { + // here is where to discern if it's inline compressed if we care + return bb.getInt(offset) >>> 2; + } + if ( shortbit != b1 ) // it is inline and short + return b1 >>> 1 & 0x7F; + } + + /* + * If we got here, it is a TOAST pointer of some kind. Its identifying + * tag is the next byte, and its total size is VARHDRSZ_EXTERNAL plus + * something that depends on the tag. + */ + switch ( bb.get(offset + 1) ) + { + case VARTAG_INDIRECT: + tagsize = SIZEOF_varatt_indirect; + break; + case VARTAG_EXPANDED_RO: + case VARTAG_EXPANDED_RW: + tagsize = SIZEOF_varatt_expanded; + break; + case VARTAG_ONDISK: + tagsize = SIZEOF_varatt_external; + break; + default: + throw new AssertionError("unrecognized TOAST vartag"); + } + + return VARHDRSZ_EXTERNAL + tagsize; + } + + static Datum.Input asAlwaysCopiedDatum( + ByteBuffer bb, int offset, int length) + { + byte[] bytes = new byte [ length ]; + // Java 13: bb.get(offset, bytes); + ((ByteBuffer)bb.duplicate().position(offset)).get(bytes); + ByteBuffer copy = ByteBuffer.wrap(bytes); + return new DatumImpl.Input.JavaCopy(asReadOnlyNativeOrder(copy)); + } + + private static native long _addressOf(ByteBuffer bb); + + private static native ByteBuffer _map(long nativeAddress, int length); + + private static native ByteBuffer _mapCString(long nativeAddress); + + /* + * Uses offset as address directly if bb is null. + */ + private static native Datum.Input _mapVarlena( + ByteBuffer bb, long offset, long resowner, long memcontext); + + abstract static class Accessor + implements Datum.Accessor + /* + * Accessors handle fixed-length types no wider than a Datum; for such + * types, they support access as all suitable Java primitive types as well + * as Datum. For wider or variable-length types, only Datum access applies. + * For by-value, only power-of-2 sizes and corresponding alignments allowed: + * https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=82a1f09 + * + * The primitive-typed methods all have SignExtended and ZeroExtended + * flavors (except for short and char where the flavor is explicit, and byte + * which has no narrower type to extend). The get methods return the + * specified type, which means the choice of flavor will have no detectable + * effect on the return value when the value being read is exactly that + * width (as always in Java, a long, int, or byte will be treated as + * signed); the flavor will make a difference if the method is used to read + * a value that is actually narrower (say, getLongZeroExtended or + * getLongSignExtended on an int-sized field). + */ + { + static Datum.Accessor forDeformed( + boolean byValue, short length) + { + if ( byValue ) + return ByValue.Deformed.ACCESSORS [ length ]; + if ( 0 <= length ) + { + /* + * specific by-reference accessors are always available for + * lengths up to Long.BYTES, even in 4-byte-datum builds. The + * by-reference value doesn't have to fit in a Datum, and it + * may be useful to access it as a Java primitive. + */ + if ( Long.BYTES >= length ) + return ByReference.Deformed.ACCESSORS [ length ]; + return ByReference.Deformed.ACCESSORS [ + ByReference.FIXED_ACCESSOR_INDEX + ]; + } + if ( -1 == length ) + return ByReference.Deformed.ACCESSORS [ + ByReference.VARLENA_ACCESSOR_INDEX + ]; + if ( -2 == length ) + return ByReference.Deformed.ACCESSORS [ + ByReference.CSTRING_ACCESSOR_INDEX + ]; + throw new IllegalArgumentException( + "invalid attribute length: "+length); + } + + static Datum.Accessor forHeap( + boolean byValue, short length) + { + if ( byValue ) + return ByValue.Heap.ACCESSORS [ length ]; + if ( 0 <= length ) + { + /* + * specific by-reference accessors are always available for + * lengths up to Long.BYTES, even in 4-byte-datum builds. The + * by-reference value doesn't have to fit in a Datum, and it + * may be useful to access it as a Java primitive. + */ + if ( Long.BYTES >= length ) + return ByReference.Heap.ACCESSORS [ length ]; + return ByReference.Heap.ACCESSORS [ + ByReference.FIXED_ACCESSOR_INDEX + ]; + } + if ( -1 == length ) + return ByReference.Heap.ACCESSORS [ + ByReference.VARLENA_ACCESSOR_INDEX + ]; + if ( -2 == length ) + return ByReference.Heap.ACCESSORS [ + ByReference.CSTRING_ACCESSOR_INDEX + ]; + throw new IllegalArgumentException( + "invalid attribute length: "+length); + } + + @Override + public long getLongSignExtended(B buf, int off) + { + return getIntSignExtended(buf, off); + } + + @Override + public long getLongZeroExtended(B buf, int off) + { + return Integer.toUnsignedLong(getIntZeroExtended(buf, off)); + } + + @Override + public double getDouble(B buf, int off) + { + return getFloat(buf, off); + } + + @Override + public int getIntSignExtended(B buf, int off) + { + return getShort(buf, off); + } + + @Override + public int getIntZeroExtended(B buf, int off) + { + return getChar(buf, off); + } + + @Override + public float getFloat(B buf, int off) + { + throw new AccessorWidthException(); + } + + @Override + public short getShort(B buf, int off) + { + return getByte(buf, off); + } + + @Override + public char getChar(B buf, int off) + { + return (char)Byte.toUnsignedInt(getByte(buf, off)); + } + + @Override + public byte getByte(B buf, int off) + { + throw new AccessorWidthException(); + } + + @Override + public boolean getBoolean(B buf, int off) + { + return 0 != getLongZeroExtended(buf, off); + } + + @Override + public Datum.Input getDatum(B buf, int off, Attribute a) + { + throw new AccessorWidthException(); + } + + static class ByValue + extends Accessor + { + /* + * Convention: when invoking a deformed accessor method, the offset + * shall be a multiple of SIZEOF_DATUM. + */ + static class Deformed extends ByValue + { + @SuppressWarnings("unchecked") + static final ByValue[] ACCESSORS = + new ByValue[ 1 + SIZEOF_DATUM ]; + static + { + ByValue none = new ByValue<>(); + ( + (8 == SIZEOF_DATUM) + ? List.>of( + none, + new DV81(), new DV82(), none, new DV84(), + none, none, none, new DV88() + ) + : List.>of( + none, + new DV41(), new DV42(), none, new DV44() + ) + ).toArray(ACCESSORS); + } + } + + /* + * Convention: when invoking a heap accessor method, the offset + * shall already have been adjusted for alignment (according to + * PostgreSQL's alignment rules, that is, so the right value will be + * accessed). Java's ByteBuffer API will still check and possibly + * split accesses according to the hardware's rules; there's no way + * to talk it out of that, so there's little to gain by being more + * clever here. + */ + static class Heap extends ByValue + { + @SuppressWarnings("unchecked") + static final ByValue[] ACCESSORS = + new ByValue[ 1 + SIZEOF_DATUM ]; + static + { + ByValue none = new ByValue<>(); + ( + (8 == SIZEOF_DATUM) + ? List.>of( + none, + new HV1(), new HV2(), none, new HV4(), + none, none, none, new HV8() + ) + : List.>of( + none, + new HV1(), new HV2(), none, new HV4() + ) + ).toArray(ACCESSORS); + } + } + } + + static class DV88 extends ByValue.Deformed + { + @Override + public long getLongSignExtended(ByteBuffer bb, int off) + { + return bb.getLong(off); + } + @Override + public long getLongZeroExtended(ByteBuffer bb, int off) + { + return bb.getLong(off); + } + @Override + public double getDouble(ByteBuffer bb, int off) + { + return bb.getDouble(off); + } + @Override + public Datum.Input getDatum(ByteBuffer bb, int off, Attribute a) + { + return asAlwaysCopiedDatum(bb, off, 8); + } + } + + static class DV84 extends ByValue.Deformed + { + @Override + public int getIntSignExtended(ByteBuffer bb, int off) + { + long r = bb.getLong(off); + return (int)r; + } + @Override + public int getIntZeroExtended(ByteBuffer bb, int off) + { + long r = bb.getLong(off); + return (int)r; + } + @Override + public float getFloat(ByteBuffer bb, int off) + { + return bb.getFloat(off); + } + @Override + public Datum.Input getDatum(ByteBuffer bb, int off, Attribute a) + { + if ( BIG_ENDIAN ) + off += SIZEOF_DATUM - 4; + return asAlwaysCopiedDatum(bb, off, 4); + } + } + + static class DV82 extends ByValue.Deformed + { + @Override + public short getShort(ByteBuffer bb, int off) + { + long r = bb.getLong(off); + return (short)r; + } + @Override + public char getChar(ByteBuffer bb, int off) + { + long r = bb.getLong(off); + return (char)r; + } + @Override + public Datum.Input getDatum(ByteBuffer bb, int off, Attribute a) + { + if ( BIG_ENDIAN ) + off += SIZEOF_DATUM - 2; + return asAlwaysCopiedDatum(bb, off, 2); + } + } + + static class DV81 extends ByValue.Deformed + { + @Override + public byte getByte(ByteBuffer bb, int off) + { + long r = bb.getLong(off); + return (byte)r; + } + @Override + public Datum.Input getDatum(ByteBuffer bb, int off, Attribute a) + { + if ( BIG_ENDIAN ) + off += SIZEOF_DATUM - 1; + return asAlwaysCopiedDatum(bb, off, 1); + } + } + + static class DV44 extends ByValue.Deformed + { + @Override + public int getIntSignExtended(ByteBuffer bb, int off) + { + return bb.getInt(off); + } + @Override + public int getIntZeroExtended(ByteBuffer bb, int off) + { + return bb.getInt(off); + } + @Override + public float getFloat(ByteBuffer bb, int off) + { + return bb.getFloat(off); + } + @Override + public Datum.Input getDatum(ByteBuffer bb, int off, Attribute a) + { + return asAlwaysCopiedDatum(bb, off, 4); + } + } + + static class DV42 extends ByValue.Deformed + { + @Override + public short getShort(ByteBuffer bb, int off) + { + int r = bb.getInt(off); + return (short)r; + } + @Override + public char getChar(ByteBuffer bb, int off) + { + int r = bb.getInt(off); + return (char)r; + } + @Override + public Datum.Input getDatum(ByteBuffer bb, int off, Attribute a) + { + if ( BIG_ENDIAN ) + off += SIZEOF_DATUM - 2; + return asAlwaysCopiedDatum(bb, off, 2); + } + } + + static class DV41 extends ByValue.Deformed + { + @Override + public byte getByte(ByteBuffer bb, int off) + { + int r = bb.getInt(off); + return (byte)r; + } + @Override + public Datum.Input getDatum(ByteBuffer bb, int off, Attribute a) + { + if ( BIG_ENDIAN ) + off += SIZEOF_DATUM - 1; + return asAlwaysCopiedDatum(bb, off, 1); + } + } + + static class HV8 extends ByValue.Heap + { + @Override + public long getLongSignExtended(ByteBuffer bb, int off) + { + return bb.getLong(off); + } + @Override + public long getLongZeroExtended(ByteBuffer bb, int off) + { + return bb.getLong(off); + } + @Override + public double getDouble(ByteBuffer bb, int off) + { + return bb.getDouble(off); + } + @Override + public Datum.Input getDatum(ByteBuffer bb, int off, Attribute a) + { + return asAlwaysCopiedDatum(bb, off, 8); + } + } + + static class HV4 extends ByValue.Heap + { + @Override + public int getIntSignExtended(ByteBuffer bb, int off) + { + return bb.getInt(off); + } + @Override + public int getIntZeroExtended(ByteBuffer bb, int off) + { + return bb.getInt(off); + } + @Override + public float getFloat(ByteBuffer bb, int off) + { + return bb.getFloat(off); + } + @Override + public Datum.Input getDatum(ByteBuffer bb, int off, Attribute a) + { + return asAlwaysCopiedDatum(bb, off, 4); + } + } + + static class HV2 extends ByValue.Heap + { + @Override + public short getShort(ByteBuffer bb, int off) + { + return bb.getShort(off); + } + @Override + public char getChar(ByteBuffer bb, int off) + { + return bb.getChar(off); + } + @Override + public Datum.Input getDatum(ByteBuffer bb, int off, Attribute a) + { + return asAlwaysCopiedDatum(bb, off, 2); + } + } + + static class HV1 extends ByValue.Heap + { + @Override + public byte getByte(ByteBuffer bb, int off) + { + return bb.get(off); + } + @Override + public Datum.Input getDatum(ByteBuffer bb, int off, Attribute a) + { + return asAlwaysCopiedDatum(bb, off, 1); + } + } + + /* + * In the ByReference case, the accessors for Deformed and Heap differ + * only in what the map*Reference() methods do, so the accessors are + * all made inner classes of Impl, and instantiated in the + * constructors of its subclasses Deformed and Heap, so they have access + * to the right copy/map methods by enclosure rather than inheritance. + * The constructor of each (Heap and Deformed) is invoked just once, + * statically, to populate the ACCESSORS arrays. + * + * There are always length-specific accessors for each length through 8, + * even in 4-byte-datum builds, plus accessors for fixed lengths greater + * than 8, cstrings, and varlenas. + */ + abstract static class ByReference + extends Accessor + { + static final int FIXED_ACCESSOR_INDEX = 9; + static final int CSTRING_ACCESSOR_INDEX = 10; + static final int VARLENA_ACCESSOR_INDEX = 11; + static final int ACCESSORS_ARRAY_LENGTH = 12; + + static final class Deformed extends Impl + { + @SuppressWarnings("unchecked") + static final ByReference[] ACCESSORS = + new ByReference[ACCESSORS_ARRAY_LENGTH]; + + static final ByValue s_pointerAccessor; + + static + { + new Deformed(); + s_pointerAccessor = + ByValue.Deformed.ACCESSORS[SIZEOF_DATUM]; + } + + private Deformed() + { + List.>of( + this, + new R1<>(), new R2<>(), new R3<>(), new R4<>(), + new R5<>(), new R6<>(), new R7<>(), new R8<>(), + new Fixed<>(), new CString<>(), new Varlena<>() + ).toArray(ACCESSORS); + } + + @Override + protected ByteBuffer mapFixedLengthReference( + ByteBuffer bb, int off, int len) + { + long p = s_pointerAccessor.getLongZeroExtended(bb, off); + return mapFixedLength(p, len); + } + + @Override + protected ByteBuffer mapCStringReference(ByteBuffer bb, int off) + { + long p = s_pointerAccessor.getLongZeroExtended(bb, off); + return mapCString(p); + } + + @Override + protected Datum.Input mapVarlenaReference(ByteBuffer b, int off, + ResourceOwner ro, MemoryContext mc) + { + long p = s_pointerAccessor.getLongZeroExtended(b, off); + return mapVarlena(p, ro, mc); + } + } + + /* + * Convention: when invoking a heap accessor method, the offset + * shall already have been adjusted for alignment (according to + * PostgreSQL's alignment rules, that is, so the right value will be + * accessed). Java's ByteBuffer API will still check and possibly + * split accesses according to the hardware's rules; there's no way + * to talk it out of that, so there's little to gain by being more + * clever here. + * + * The ByReference case includes accessors for non-power-of-two + * sizes. To keep things simple here, they just put the widest + * accesses first, which should be as good as it gets in the most + * expected case where the initial offset is aligned, and Java will + * make other cases work too. + */ + static class Heap extends Impl + { + @SuppressWarnings("unchecked") + static final ByReference[] ACCESSORS = + new ByReference[ACCESSORS_ARRAY_LENGTH]; + + static + { + new Heap(); + } + + private Heap() + { + List.>of( + this, + new R1<>(), new R2<>(), new R3<>(), new R4<>(), + new R5<>(), new R6<>(), new R7<>(), new R8<>(), + new Fixed<>(), new CString<>(), new Varlena<>() + ).toArray(ACCESSORS); + } + + @Override + protected ByteBuffer mapFixedLengthReference( + ByteBuffer bb, int off, int len) + { + return mapFixedLength(bb, off, len); + } + + @Override + protected ByteBuffer mapCStringReference(ByteBuffer bb, int off) + { + return mapCString(bb, off); + } + + @Override + protected Datum.Input mapVarlenaReference(ByteBuffer b, int off, + ResourceOwner ro, MemoryContext mc) + { + return mapVarlena(b, off, ro, mc); + } + } + + abstract static class Impl + extends ByReference + { + abstract ByteBuffer mapFixedLengthReference( + ByteBuffer bb, int off, int len); + + abstract ByteBuffer mapCStringReference(ByteBuffer bb, int off); + + /* + * If the varlena is a TOAST pointer and can be parked until + * needed by pinning a snapshot, ro is the ResourceOwner it will + * be pinned to. If the content gets fetched, uncompressed, or + * copied, it will be into a new memory context with mc as its + * parent. + */ + abstract Datum.Input mapVarlenaReference(ByteBuffer bb, int off, + ResourceOwner ro, MemoryContext mc); + + Datum.Input copyFixedLengthReference( + ByteBuffer b, int off, int len) + { + return + asAlwaysCopiedDatum( + mapFixedLengthReference(b, off, len), 0, len); + } + + class R8 extends ByReference + { + @Override + public long getLongSignExtended(ByteBuffer bb, int off) + { + return mapFixedLengthReference(bb, off, 8).getLong(); + } + @Override + public long getLongZeroExtended(ByteBuffer bb, int off) + { + return mapFixedLengthReference(bb, off, 8).getLong(); + } + @Override + public double getDouble(ByteBuffer bb, int off) + { + return mapFixedLengthReference(bb, off, 8).getDouble(); + } + @Override + public Datum.Input getDatum( + ByteBuffer bb, int off, Attribute a) + { + return copyFixedLengthReference(bb, off, 8); + } + } + + class R7 extends ByReference + { + @Override + public long getLongSignExtended(ByteBuffer bb, int off) + { + long r = getLongZeroExtended(bb, off); + return r | (0L - ((r & 0x80_0000_0000_0000L) << 1)); + } + @Override + public long getLongZeroExtended(ByteBuffer bb, int off) + { + ByteBuffer mb = mapFixedLengthReference(bb, off, 7); + long r; + if ( BIG_ENDIAN ) + { + r = Integer.toUnsignedLong(mb.getInt()) << 24; + r |= (long)mb.getChar() << 8; + r |= Byte.toUnsignedLong(mb.get()); + return r; + } + r = Integer.toUnsignedLong(mb.getInt()); + r |= (long)mb.getChar() << 32; + r |= Byte.toUnsignedLong(mb.get()) << 48; + return r; + } + @Override + public Datum.Input getDatum( + ByteBuffer bb, int off, Attribute a) + { + return copyFixedLengthReference(bb, off, 7); + } + } + + class R6 extends ByReference + { + @Override + public long getLongSignExtended(ByteBuffer bb, int off) + { + long r = getLongZeroExtended(bb, off); + return r | (0L - ((r & 0x8000_0000_0000L) << 1)); + } + @Override + public long getLongZeroExtended(ByteBuffer bb, int off) + { + ByteBuffer mb = mapFixedLengthReference(bb, off, 6); + long r; + if ( BIG_ENDIAN ) + { + r = Integer.toUnsignedLong(mb.getInt()) << 16; + r |= (long)mb.getChar(); + return r; + } + r = Integer.toUnsignedLong(mb.getInt()); + r |= (long)mb.getChar() << 32; + return r; + } + @Override + public Datum.Input getDatum( + ByteBuffer bb, int off, Attribute a) + { + return copyFixedLengthReference(bb, off, 6); + } + } + + class R5 extends ByReference + { + @Override + public long getLongSignExtended(ByteBuffer bb, int off) + { + long r = getLongZeroExtended(bb, off); + return r | (0L - ((r & 0x80_0000_0000L) << 1)); + } + @Override + public long getLongZeroExtended(ByteBuffer bb, int off) + { + ByteBuffer mb = mapFixedLengthReference(bb, off, 5); + long r; + if ( BIG_ENDIAN ) + { + r = Integer.toUnsignedLong(mb.getInt()) << 8; + r |= Byte.toUnsignedLong(mb.get()); + return r; + } + r = Integer.toUnsignedLong(mb.getInt()); + r |= Byte.toUnsignedLong(mb.get()) << 32; + return r; + } + @Override + public Datum.Input getDatum( + ByteBuffer bb, int off, Attribute a) + { + return copyFixedLengthReference(bb, off, 5); + } + } + + class R4 extends ByReference + { + @Override + public int getIntSignExtended(ByteBuffer bb, int off) + { + return mapFixedLengthReference(bb, off, 4).getInt(); + } + @Override + public int getIntZeroExtended(ByteBuffer bb, int off) + { + return mapFixedLengthReference(bb, off, 4).getInt(); + } + @Override + public float getFloat(ByteBuffer bb, int off) + { + return mapFixedLengthReference(bb, off, 4).getFloat(); + } + @Override + public Datum.Input getDatum( + ByteBuffer bb, int off, Attribute a) + { + return copyFixedLengthReference(bb, off, 4); + } + } + + class R3 extends ByReference + { + @Override + public int getIntSignExtended(ByteBuffer bb, int off) + { + int r = getIntZeroExtended(bb, off); + return r | (0 - ((r & 0x80_0000) << 1)); + } + @Override + public int getIntZeroExtended(ByteBuffer bb, int off) + { + ByteBuffer mb = mapFixedLengthReference(bb, off, 3); + int r; + if ( BIG_ENDIAN ) + { + r = (int)mb.getChar() << 8; + r |= Byte.toUnsignedInt(mb.get()); + return r; + } + r = (int)mb.getChar(); + r |= Byte.toUnsignedInt(mb.get()) << 16; + return r; + } + @Override + public Datum.Input getDatum( + ByteBuffer bb, int off, Attribute a) + { + return copyFixedLengthReference(bb, off, 3); + } + } + + class R2 extends ByReference + { + @Override + public short getShort(ByteBuffer bb, int off) + { + return mapFixedLengthReference(bb, off, 2).getShort(); + } + @Override + public char getChar(ByteBuffer bb, int off) + { + return mapFixedLengthReference(bb, off, 2).getChar(); + } + @Override + public Datum.Input getDatum( + ByteBuffer bb, int off, Attribute a) + { + return copyFixedLengthReference(bb, off, 2); + } + } + + class R1 extends ByReference + { + @Override + public byte getByte(ByteBuffer bb, int off) + { + return mapFixedLengthReference(bb, off, 1).get(); + } + @Override + public Datum.Input getDatum( + ByteBuffer bb, int off, Attribute a) + { + return copyFixedLengthReference(bb, off, 1); + } + } + + class Fixed extends ByReference + { + @Override + public Datum.Input getDatum( + ByteBuffer bb, int off, Attribute a) + { + int len = a.length(); + if ( len <= NAMEDATALEN ) + return copyFixedLengthReference(bb, off, len); + // XXX even copy bigger ones, for now + return copyFixedLengthReference(bb, off, len); + } + } + + class CString extends ByReference + { + @Override + public Datum.Input getDatum( + ByteBuffer bb, int off, Attribute a) + { + ByteBuffer bnew = mapCStringReference(bb, off); + // XXX for now, return a Java copy regardless of size + return asAlwaysCopiedDatum(bnew, 0, bnew.remaining()); + } + } + + class Varlena extends ByReference + { + @Override + public Datum.Input getDatum( + ByteBuffer bb, int off, Attribute a) + { + // XXX no control over resowner and context for now + return mapVarlenaReference(bb, off, + TopTransactionResourceOwner(), + TopTransactionContext()); + } + } + } + } + } + + private static class AccessorWidthException extends RuntimeException + { + AccessorWidthException() + { + super(null, null, false, false); + } + } +} diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/ExtensionImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/ExtensionImpl.java new file mode 100644 index 000000000..cb61e9162 --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/pg/ExtensionImpl.java @@ -0,0 +1,301 @@ +/* + * Copyright (c) 2022-2023 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.pg; + +import java.lang.invoke.MethodHandle; +import static java.lang.invoke.MethodHandles.lookup; +import java.lang.invoke.SwitchPoint; + +import java.nio.ByteBuffer; + +import java.sql.SQLException; + +import java.util.Iterator; +import java.util.List; + +import java.util.function.UnaryOperator; + +import org.postgresql.pljava.internal.Checked; +import org.postgresql.pljava.internal.SwitchPointCache.Builder; +import static org.postgresql.pljava.internal.UncheckedException.unchecked; + +import org.postgresql.pljava.model.*; +import static org.postgresql.pljava.model.MemoryContext.JavaMemoryContext; + +import org.postgresql.pljava.pg.CatalogObjectImpl.*; +import static org.postgresql.pljava.pg.MemoryContextImpl.allocatingIn; +import static org.postgresql.pljava.pg.ModelConstants.Anum_pg_extension_oid; +import static org.postgresql.pljava.pg.ModelConstants.ExtensionOidIndexId; +import static org.postgresql.pljava.pg.TupleTableSlotImpl.heapTupleGetLightSlot; + +import static org.postgresql.pljava.pg.adt.ArrayAdapter + .FLAT_STRING_LIST_INSTANCE; +import static org.postgresql.pljava.pg.adt.NameAdapter.SIMPLE_INSTANCE; +import static org.postgresql.pljava.pg.adt.OidAdapter.REGNAMESPACE_INSTANCE; +import static org.postgresql.pljava.pg.adt.OidAdapter.REGROLE_INSTANCE; +import static org.postgresql.pljava.pg.adt.Primitives.BOOLEAN_INSTANCE; +import org.postgresql.pljava.pg.adt.TextAdapter; + +import org.postgresql.pljava.sqlgen.Lexicals.Identifier.Simple; +import org.postgresql.pljava.sqlgen.Lexicals.Identifier.Unqualified; + +class ExtensionImpl extends Addressed +implements Nonshared, Named, Owned, Extension +{ + private static UnaryOperator s_initializer; + + /* Implementation of Addressed */ + + @Override + public RegClass.Known classId() + { + return CLASSID; + } + + private static TupleTableSlot cacheTuple(ExtensionImpl o) + throws SQLException + { + ByteBuffer heapTuple; + TupleDescImpl td = (TupleDescImpl)o.cacheDescriptor(); + + /* + * See this method in CatalogObjectImpl.Addressed for more on the choice + * of memory context and lifespan. + */ + try ( Checked.AutoCloseable ac = + allocatingIn(JavaMemoryContext()) ) + { + heapTuple = _sysTableGetByOid( + o.classId().oid(), o.oid(), Anum_pg_extension_oid, + ExtensionOidIndexId, td.address()); + if ( null == heapTuple ) + return null; + } + return heapTupleGetLightSlot(td, heapTuple, null); + } + + /* Implementation of Named, Owned */ + + private static Simple name(ExtensionImpl o) throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return + t.get(Att.EXTNAME, SIMPLE_INSTANCE); + } + + private static RegRole owner(ExtensionImpl o) throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return t.get(Att.EXTOWNER, REGROLE_INSTANCE); + } + + /* Implementation of Extension */ + + /** + * Merely passes the supplied slots array to the superclass constructor; all + * initialization of the slots will be the responsibility of the subclass. + */ + ExtensionImpl() + { + super(s_initializer.apply(new MethodHandle[NSLOTS])); + } + + static final int SLOT_TARGETNAMESPACE; + static final int SLOT_RELOCATABLE; + static final int SLOT_VERSION; + static final int SLOT_CONFIG; + static final int SLOT_CONDITION; + static final int NSLOTS; + + static + { + int i = CatalogObjectImpl.Addressed.NSLOTS; + s_initializer = + new Builder<>(ExtensionImpl.class) + .withLookup(lookup()) + .withSwitchPoint(o -> s_globalPoint[0]) + .withSlots(o -> o.m_slots) + .withCandidates(ExtensionImpl.class.getDeclaredMethods()) + + /* + * First declare some slots whose consuming API methods are found + * on inherited interfaces. This requires some adjustment of method + * types so that run-time adaptation isn't needed. + */ + .withReceiverType(CatalogObjectImpl.Addressed.class) + .withDependent("cacheTuple", SLOT_TUPLE) + + .withReceiverType(CatalogObjectImpl.Named.class) + .withReturnType(Unqualified.class) + .withDependent( "name", SLOT_NAME) + + .withReceiverType(CatalogObjectImpl.Owned.class) + .withReturnType(null) // cancel adjustment from above + .withDependent( "owner", SLOT_OWNER) + + /* + * Next come slots where the compute and API methods are here. + */ + .withReceiverType(null) + + .withDependent( "namespace", SLOT_TARGETNAMESPACE = i++) + .withDependent("relocatable", SLOT_RELOCATABLE = i++) + .withDependent( "version", SLOT_VERSION = i++) + .withDependent( "config", SLOT_CONFIG = i++) + .withDependent( "condition", SLOT_CONDITION = i++) + + .build(); + NSLOTS = i; + } + + static class Att + { + static final Attribute EXTNAME; + static final Attribute EXTOWNER; + static final Attribute EXTNAMESPACE; + static final Attribute EXTRELOCATABLE; + static final Attribute EXTVERSION; + static final Attribute EXTCONFIG; + static final Attribute EXTCONDITION; + + static + { + Iterator itr = CLASSID.tupleDescriptor().project( + "extname", + "extowner", + "extnamespace", + "extrelocatable", + "extversion", + "extconfig", + "extcondition" + ).iterator(); + + EXTNAME = itr.next(); + EXTOWNER = itr.next(); + EXTNAMESPACE = itr.next(); + EXTRELOCATABLE = itr.next(); + EXTVERSION = itr.next(); + EXTCONFIG = itr.next(); + EXTCONDITION = itr.next(); + + assert ! itr.hasNext() : "attribute initialization miscount"; + } + } + + /* computation methods */ + + private static RegNamespace namespace(ExtensionImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(Att.EXTNAMESPACE, REGNAMESPACE_INSTANCE); + } + + private static boolean relocatable(ExtensionImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(Att.EXTRELOCATABLE, BOOLEAN_INSTANCE); + } + + private static String version(ExtensionImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(Att.EXTVERSION, TextAdapter.INSTANCE); + } + + private static List config(ExtensionImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return + s.get(Att.EXTCONFIG, + ArrayAdapters.REGCLASS_LIST_INSTANCE); + } + + private static List condition(ExtensionImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return + s.get(Att.EXTCONDITION, + FLAT_STRING_LIST_INSTANCE); + } + + /* API methods */ + + @Override + public RegNamespace namespace() + { + try + { + MethodHandle h = m_slots[SLOT_TARGETNAMESPACE]; + return (RegNamespace)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public boolean relocatable() + { + try + { + MethodHandle h = m_slots[SLOT_RELOCATABLE]; + return (boolean)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public String version() + { + try + { + MethodHandle h = m_slots[SLOT_VERSION]; + return (String)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public List config() + { + try + { + MethodHandle h = m_slots[SLOT_CONFIG]; + return (List)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public List condition() + { + try + { + MethodHandle h = m_slots[SLOT_CONDITION]; + return (List)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } +} diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/MemoryContextImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/MemoryContextImpl.java new file mode 100644 index 000000000..2bcda3cda --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/pg/MemoryContextImpl.java @@ -0,0 +1,445 @@ +/* + * Copyright (c) 2022-2023 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.pg; + +import static java.lang.Integer.toUnsignedLong; + +import java.nio.ByteBuffer; +import static java.nio.ByteOrder.nativeOrder; + +import java.nio.charset.CharacterCodingException; + +import static org.postgresql.pljava.internal.Backend.doInPG; +import static org.postgresql.pljava.internal.Backend.threadMayEnterPG; + +import org.postgresql.pljava.internal.CacheMap; +import org.postgresql.pljava.internal.Checked; +import static org.postgresql.pljava.internal.DualState.m; +import org.postgresql.pljava.internal.LifespanImpl; + +import static org.postgresql.pljava.model.CharsetEncoding.SERVER_ENCODING; +import org.postgresql.pljava.model.MemoryContext; + +import static org.postgresql.pljava.pg.DatumUtils.addressOf; +import static org.postgresql.pljava.pg.DatumUtils.asReadOnlyNativeOrder; +import static org.postgresql.pljava.pg.DatumUtils.fetchPointer; +import static org.postgresql.pljava.pg.DatumUtils.mapCString; +import static org.postgresql.pljava.pg.DatumUtils.mapFixedLength; +import static org.postgresql.pljava.pg.DatumUtils.storePointer; + +import static org.postgresql.pljava.pg.ModelConstants.NOCONSTANT; +import static org.postgresql.pljava.pg.ModelConstants.SIZEOF_DATUM; +import static org.postgresql.pljava.pg.ModelConstants.SIZEOF_MCTX; +import static org.postgresql.pljava.pg.ModelConstants.OFFSET_MCTX_name; +import static org.postgresql.pljava.pg.ModelConstants.OFFSET_MCTX_ident; +import static org.postgresql.pljava.pg.ModelConstants.OFFSET_MCTX_firstchild; + +// be aware that this will be NOCONSTANT in PG < 13 +import static org.postgresql.pljava.pg.ModelConstants.OFFSET_MCTX_mem_allocated; + +/* + * CurrentMemoryContext is declared in utils/palloc.h and defined in + * utils/mmgr/mcxt.c along with the rest of these, which are puzzlingly + * declared in utils/memutils.h instead. + * + * TopMemoryContext // can be made a static + * ErrorContext + * PostmasterContext + * CacheMemoryContext + * MessageContext + * TopTransactionContext + * CurTransactionContext + * PortalContext // transient; for active portal + * + * The structure of a context is in nodes/memnodes.h + */ + +/** + * A lazily-created mirror of a PostgreSQL MemoryContext. + *

    + * PostgreSQL is creating, resetting, and deleting memory contexts all the time, + * and most of them will never be visible in PL/Java; one of these objects only + * gets created when Java code specifically requests a reference to a particular + * context, generally to make it the {@code Lifespan} of some PL/Java object + * that should be invalidated when the context goes away. + *

    + * Once an instance of this class has been instantiated and before it escapes to + * calling Java code, it must be registered for a reset/delete callback on the + * underlying PostgreSQL context so it can track its life cycle and invalidate, + * when the time comes, any objects it has been used as the "owner" of. + * (Instances that might be transiently created here, say in traversing the + * context tree, and won't escape, don't need the full registration treatment.) + * All creation, traversal, and mutation has to happen on the PG thread. Once + * published and while valid, an instance can be observed by other threads. + *

    + * Events that can occur in the life of a memory context: + *

    + *
    SetParent
    It can be made a child of a context other than its original + * parent. (It can also be given the null parent, immediately before being + * deleted; this happens after invocation of the callback, though, so + * gives the callback routine no help in determining what is happening.) + *
    Reset
    It can have all of its descendant contexts deleted and its own + * allocations freed, but remain in existence itself. + *
    ResetOnly
    It can have its own allocations freed, with no effect on + * descendant contexts. + *
    ResetChildren
    All of its children can recursively get + * the ResetChildren treatment and in addition be ResetOnly themselves, but + * with no effect on this context itself. + *
    Delete
    All of its descendants, and last this context itself, go away. + *
    DeleteChildren
    All of its descendants go away, with no other effect + * on this context. + *
    + *

    + * Complicating the lifecycle tracking, PostgreSQL will invoke exactly the same + * callback, with exactly the same parameter, whether the context in question + * is being deleted or reset. In the reset case, the context is still valid + * after the callback; in the delete case, it is not. The difference is not + * important for the objects "owned" by this context; they're to be invalidated + * in either case. But it leaves the callback with a puzzle to solve regarding + * what to do with this object itself. + *

    + * A few related observations: + *

      + *
    • Within the callback itself, the context is still valid; its native struct + * may still be accessed safely, and its parent, child, and sibling links + * are sane. + *
    • If the {@code firstchild} link is non-null, this is definitely a reset + * and not a delete. In any delete case, all children will already be gone. + *
    • Conversely, though, absence of children does not prove this is deletion. + *
    • Hence, the callback will leave this mirror in either a definitely-valid + * or a maybe-deleted state. + *
    • In either state, its callback will have been deregistered. It must + * re-register the callback in the definitely-valid state. In the maybe-deleted + * state, it will receive no further callbacks, unless it can later be found + * revivifiable and the callback is re-registered. + *
    • Because the callback can only proceed when none of this ResourceOwner's + * owned objects are pinned, and they all will be invalidated and delinked from + * it, it will always be the owner of no objects when the callback completes. + * A possible approach then is to treat maybe-deleted as definitely-deleted + * always, invalidate and unpublish this object, and require a Java caller to + * obtain a new mirror of the same context if indeed it still exists and is + * wanted. Efforts to retain and possibly revivify the mirror could be viewed + * as optimizations. (They could have API consequences, though; without + * revivification, the object would have to be made invalid and throw an + * exception if used by Java code that had held on to a reference, even if only + * a reset was intended. Revivification could allow the retained reference + * to remain usable.) + *
    • Once the callback completes, the maybe-deleted state must be treated as + * completely forbidding any access to the mapped memory. If there is any + * information that could be useful in a later revivification decision, it must + * be collected by the callback and saved in the Java object state. + *
    • If the callback for a maybe-deleted mirror saves a reference to (a + * published Java mirror of) its parent at callback time and, at a later + * attempt to use the object, the parent is found to be valid and have this + * object as a child, revivification is supported. + *
    • That child-of-valid parent test can be applied recursively if the parent + * is also found to be maybe-deleted. But the test can spuriously fail if a + * (reset-but-still-valid) context was reparented after the callback saved its + * parent reference. + *
    • Obtaining the reference again from one of the PostgreSQL globals or from + * a valid PostgreSQL data structure clearly re-establishes that it is valid. + * (Whether it is "the same" context is more a philosophical point; whether + * reset or deleted, it was left with no allocations and no owned objects at + * that point, so questions of its "identity" may not be critical. Its name and + * ident may have changed. Its operations (the 'type' of context) may also have + * changed, but may be a lower-level detail than needs attention here. + *
    + */ +public class MemoryContextImpl extends LifespanImpl +implements MemoryContext, LifespanImpl.Addressed +{ + static final ByteBuffer[] s_knownContexts; + + /** + * Map from native address of a PostgreSQL MemoryContext to an instance + * of this class. + *

    + * A non-concurrent map suffices, as the uses are only on the PG thread + * (in known() within a doInPG(), and in callback() invoked from PG). + */ + static final CacheMap s_map = + CacheMap.newThreadConfined( + () -> ByteBuffer.allocate(SIZEOF_DATUM).order(nativeOrder())); + + static + { + ByteBuffer[] bs = EarlyNatives._window(ByteBuffer.class); + /* + * The first one windows CurrentMemoryContext. Set the correct byte + * order but do not make it read-only; operations may be provided + * for setting it. + */ + bs[0] = bs[0].order(nativeOrder()); + /* + * The rest are made native-ordered and read-only. + */ + for ( int i = 1; i < bs.length; ++ i ) + bs[i] = asReadOnlyNativeOrder(bs[i]); + s_knownContexts = bs; + } + + static MemoryContext known(int which) + { + ByteBuffer global = s_knownContexts[which]; + return doInPG(() -> + { + long ctx = fetchPointer(global, 0); + if ( 0 == ctx ) + return null; + return fromAddress(ctx); + }); + } + + public static MemoryContext fromAddress(long address) + { + assert threadMayEnterPG() : m("MemoryContext thread"); + + /* + * Cache strongly; see LifespanImpl javadoc. + */ + return s_map.stronglyCache( + b -> + { + if ( 4 == SIZEOF_DATUM ) + b.putInt((int)address); + else + b.putLong(address); + }, + b -> + { + MemoryContextImpl c = new MemoryContextImpl(address); + EarlyNatives._registerCallback(address); + return c; + } + ); + } + + /** + * Specialized method intended, so far, only for {@code PgSavepoint}'s use. + *

    + * Only to be called on the PG thread. + */ + public static long getCurrentRaw() + { + assert threadMayEnterPG() : m("MemoryContext thread"); + return fetchPointer(s_knownContexts[0], 0); + } + + /** + * Even more specialized method intended, so far, only for + * {@code PgSavepoint}'s use. + *

    + * Only to be called on the PG thread. + */ + public static void setCurrentRaw(long context) + { + assert threadMayEnterPG() : m("MemoryContext thread"); + storePointer(s_knownContexts[0], 0, context); + } + + /** + * Change the current memory context to c, for use in + * a {@code try}-with-resources to restore the prior context on exit + * of the block. + */ + public static Checked.AutoCloseable + allocatingIn(MemoryContext c) + { + assert threadMayEnterPG() : m("MemoryContext thread"); + MemoryContextImpl ci = (MemoryContextImpl)c; + long prior = getCurrentRaw(); + Checked.AutoCloseable ac = () -> setCurrentRaw(prior); + setCurrentRaw(ci.m_address); + return ac; + } + + /* + * Called only from JNI. + * + * See EarlyNatives._registerCallback below for discussion of why the native + * context address is used as the callback argument. + * + * Deregistering the callback is a non-issue: that has already happened + * when this call is made. + */ + private static void callback(long ctx) + { + CacheMap.Entry e = s_map.find( + b -> + { + if ( 4 == SIZEOF_DATUM ) + b.putInt((int)ctx); + else + b.putLong(ctx); + } + ); + + if ( null == e ) + return; + + MemoryContextImpl c = e.get(); + if ( null == c ) + return; + + /* + * invalidate() has to make a (conservative) judgment whether this + * callback reflects a 'reset' or 'delete' operation, and return true + * if the mapping should be removed from the cache. It should return + * false only if the case is provably a reset only, or (possible future + * work) if it can be placed in a maybe-deleted state and possibly + * revivified later. Otherwise, the instance must be conservatively + * marked invalid, and dropped from the cache. + */ + if ( c.invalidate() ) + e.remove(); + } + + private final ByteBuffer m_context; + /** + * The address of the context, even though technically redundant. + *

    + * A JNI function can easily retrieve it from the {@code ByteBuffer}, but + * by keeping the value here, sometimes a JNI call can be avoided. + */ + private final long m_address; + private String m_ident; + private final String m_name; + + private MemoryContextImpl(long context) + { + m_address = context; + m_context = mapFixedLength(context, SIZEOF_MCTX); + String s; + + long p = (NOCONSTANT != OFFSET_MCTX_ident) + ? fetchPointer(m_context, OFFSET_MCTX_ident) + : 0; + + try + { + if ( 0 == p ) + s = null; + else + s = SERVER_ENCODING.decode(mapCString(p)).toString(); + } + catch ( CharacterCodingException e ) + { + s = "[unexpected encoding]"; + } + m_ident = s; + + p = fetchPointer(m_context, OFFSET_MCTX_name); + + try + { + if ( 0 == p ) + s = null; + else + s = SERVER_ENCODING.decode(mapCString(p)).toString(); + } + catch ( CharacterCodingException e ) + { + s = "[unexpected encoding]"; + } + m_name = s; + } + + @Override + public long address() + { + if ( 0 == m_context.limit() ) + throw new IllegalStateException( + "address may not be taken of invalidated MemoryContext"); + return m_address; + } + + @Override + public String toString() + { + return String.format("MemoryContext[%s,%s]", m_name, m_ident); + } + + /* + * Determine (or conservatively guess) whether this context is being deleted + * or merely reset, and perform (in either case) the nativeRelease() actions + * for dependent objects. + * + * Return false only if this is provably a reset only, or (possible future + * work) if it can be placed in a maybe-deleted state and possibly + * revivified later. Otherwise, the instance must be conservatively + * marked invalid, and true returned to drop it from the cache. + */ + private boolean invalidate() + { + lifespanRelease(); + + /* + * The one easy rule is that if there is any child, the case can only be + * 'reset'. + */ + if ( 0 != fetchPointer(m_context, OFFSET_MCTX_firstchild) ) + return false; + + /* + * Rather than a separate field to record invalidation status, set the + * windowing ByteBuffer's limit to zero. This will ensure an + * IndexOutOfBoundsException on future attempts to read through it, + * without cluttering the code with additional tests. + */ + m_context.limit(0); + return true; + } + + private static class EarlyNatives + { + /** + * Returns an array of ByteBuffer, one covering each PostgreSQL known + * memory context global, in the same order as the arbitrary indices + * defined in the API class CatalogObject.Factory, which are what will + * be passed to the known() method. + *

    + * Takes a {@code Class} argument, to save the native code + * a lookup. + */ + private static native ByteBuffer[] _window(Class component); + + /** + * Register a memory context callback for the context with the given + * native address in PostgreSQL. + *

    + * A callback is allowed one {@code void *}-sized argument to receive + * when called back. If that were a JNI global reference, for example, + * we could arrange for {@link #callback callback} to be invoked with + * the affected Java instance directly. But {@code callback} will be + * wanting the native address anyway in order to look it up and remove + * it from the CacheMap, and JNI's global references surely involve + * their own layer of mapping under the JVM's hood. So we may as well + * keep it simple and use our one allowed arg to hold the context + * address itself, which is necessary anyway, and sufficient. + */ + private static native void _registerCallback(long nativeAddress); + } + + //possibly useful operations: + //MemoryContext parent(); + // B palloc(Class api, long size); // ByteBuffer.class for now + // flags HUGE 1 NO_OOM 2 ZERO 4 + // B repalloc(B chunk, long size); + // others from palloc.h ? + // AutoCloseable switchedTo(); + // reset/delete/resetonly/resetchildren/deletechildren/setparent + // from utils/memutils.h? only if PL/Java created the context? + // require intent to delete/reset to be declared on creation, and prevent + // such a context being used in switchedTo()? + // AllocSetContextCreate/SlabContextCreate/GenerationContextCreate + // protect some operations as protected methods of Adapter? +} diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/ModelConstants.java b/pljava/src/main/java/org/postgresql/pljava/pg/ModelConstants.java new file mode 100644 index 000000000..314c3a0e1 --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/pg/ModelConstants.java @@ -0,0 +1,620 @@ +/* + * Copyright (c) 2022-2023 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.pg; + +import org.postgresql.pljava.annotation.BaseUDT.Alignment; +import org.postgresql.pljava.annotation.BaseUDT.Storage; + +import static org.postgresql.pljava.internal.UncheckedException.unchecked; + +import java.lang.annotation.Native; + +import java.nio.ByteBuffer; +import static java.nio.ByteOrder.nativeOrder; +import java.nio.IntBuffer; + +import java.sql.SQLException; + +/** + * Supply static values that can vary between PostgreSQL versions/builds. + */ +public abstract class ModelConstants +{ + /* + * C code will contain a static array of int initialized to the values + * that are needed. The native method in this class obtains a ByteBuffer + * windowing that static array. + * + * To detect fat-finger mistakes, the array will include alternating indices + * and values { IDX_SIZEOF_DATUM, SIZEOF_DATUM, ... }, so when windowed as + * an IntBuffer, get(2*IDX_FOO) should equal IDX_FOO and get(1 + 2*IDX_FOO) + * is then the value; this can be done without hairy preprocessor logic on + * the C side, and checked here (not statically, but still cheaply). C99's + * designated array initializers would offer a simpler, all-static approach, + * but PostgreSQL strives for C89 compatibility before PostgreSQL 12. + * + * As a practical matter, the sequence of IDX_... values is allowed to have + * gaps, so that new constants can be added as needed, coherently grouped, + * without requiring extensive renumbering of otherwise unaffected lines. + * The array cells remain consecutive, and this class simply tracks a gap + * between the IDX_... value and the physical position. This, of course, + * would complicate any move to C99 designated initializers. + * + * Starting with PostgreSQL 11, LLVM bitcode for the server might be found + * in $pkglibdir/bitcode/postgres, and that could one day pose opportunities + * for a PL/Java using an LLVM library, or depending on GraalVM, to access + * these values (and do much more) without this tedious hand coding. But for + * now, the goal is to support earlier versions and not require LLVM or + * GraalVM, and hope that the bootstrapping needed here does not become too + * burdensome. + */ + private static class Natives implements AutoCloseable + { + private IntBuffer b = _statics() + .asReadOnlyBuffer().order(nativeOrder()).asIntBuffer(); + private int gap = 0; + + /** + * Returns the next constant from the windowed array. + *

    + * The next constant is determined by the buffer's current position, + * not by index, which is only used for sanity checking: + *

      + *
    1. adjusted by the current "gap", it should be half the buffer's + * current position, and + *
    2. unadjusted, it should equal the {@code int} read at + * that position. + *
    + * The {@code int} read at the next consecutive position is returned. + * @param index the expected index of the next constant to be read + * @return the next constant + * @throws ConstantsError if any sanity check fails + */ + int get(int index) + { + try + { + if ( b.position() != (index - gap) << 1 || index != b.get() ) + throw new ConstantsError(); + return b.get(); + } + catch ( Exception e ) + { + throw (ConstantsError)new ConstantsError().initCause(e); + } + } + + /** + * Conforms the internal sanity checking to a gap in assigned indices. + *

    + * The supplied index must be greater than that implied by + * the buffer's current position (and previously recorded gap, if any), + * and the difference is remembered as the new gap. + * @param index the assigned index of the next constant to be read + * @throws ConstantsError if the newly-computed gap is not larger than + * the remembered value + */ + void gap(int index) + { + int pos = b.position(); + assert 0 == (pos & 1); // expected as get() always advances by two + index -= pos >>> 1; + if ( index <= gap ) + throw new ConstantsError(); + gap = index; + } + + @Override + public void close() + { + if ( 0 < b.remaining() ) + throw new ConstantsError(); + } + + private static native ByteBuffer _statics(); + } + + /* + * These constants (which will be included in a generated header available + * to the C code) have historically stable values that aren't expected to + * change. The C code simply asserts statically at build time that they + * are right. If a new PG version conflicts with the assertion, move the + * constant from here to the list further below of constants that get their + * values *from* the C code at class initialization time. (When doing that, + * also check uses of the constant for any assumptions that might no longer + * hold.) + */ + + @Native public static final int PG_SQL_ASCII = 0; + @Native public static final int PG_UTF8 = 6; + @Native public static final int PG_LATIN1 = 8; + @Native public static final int PG_ENCODING_BE_LAST = 34; + + @Native public static final int VARHDRSZ = 4; + @Native public static final int VARHDRSZ_EXTERNAL = 2; + @Native public static final byte VARTAG_INDIRECT = 1; + @Native public static final byte VARTAG_EXPANDED_RO = 2; + @Native public static final byte VARTAG_EXPANDED_RW = 3; + @Native public static final byte VARTAG_ONDISK = 18; + + @Native public static final int Anum_pg_attribute_attname = 2; + + @Native public static final int SIZEOF_pg_attribute_atttypid = 4; + @Native public static final int SIZEOF_pg_attribute_attlen = 2; + @Native public static final int SIZEOF_pg_attribute_attcacheoff = 4; + @Native public static final int SIZEOF_pg_attribute_atttypmod = 4; + @Native public static final int SIZEOF_pg_attribute_attbyval = 1; + @Native public static final int SIZEOF_pg_attribute_attalign = 1; + @Native public static final int SIZEOF_pg_attribute_attnotnull = 1; + @Native public static final int SIZEOF_pg_attribute_attisdropped = 1; + + @Native public static final int Anum_pg_extension_oid = 1; + @Native public static final int ExtensionOidIndexId = 3080; + + @Native public static final int SIZEOF_ArrayType_ndim = 4; + @Native public static final int SIZEOF_ArrayType_dataoffset = 4; + @Native public static final int SIZEOF_ArrayType_elemtype = 4; + + @Native public static final int OFFSET_ArrayType_ndim = 0; + @Native public static final int OFFSET_ArrayType_dataoffset = 4; + @Native public static final int OFFSET_ArrayType_elemtype = 8; + + @Native public static final int OFFSET_ArrayType_DIMS = 12; + @Native public static final int SIZEOF_ArrayType_DIM = 4; + + /* + * These constants (which will be included in a generated header available + * to the C code) are (almost) indices into the 'statics' array where the + * various wanted values should be placed. Edits should keep them distinct + * consecutive small array indices within related groups; gaps are allowed + * (and encouraged) between groups, so additions can be made without mass + * renumbering. The get() method of Natives, used in the static initializer, + * will be checking for gaps or repeats; the gap() method must be called + * where each gap occurs, to advise what the next expected IDX_... value + * is to be. + */ + @Native private static final int IDX_PG_VERSION_NUM = 0; + + @Native private static final int IDX_SIZEOF_DATUM = 1; + @Native private static final int IDX_SIZEOF_INT = 2; + @Native private static final int IDX_SIZEOF_SIZE = 3; + + @Native private static final int IDX_ALIGNOF_SHORT = 4; + @Native private static final int IDX_ALIGNOF_INT = 5; + @Native private static final int IDX_ALIGNOF_DOUBLE = 6; + @Native private static final int IDX_MAXIMUM_ALIGNOF = 7; + + @Native private static final int IDX_NAMEDATALEN = 8; + + + + @Native private static final int IDX_SIZEOF_varatt_indirect = 10; + @Native private static final int IDX_SIZEOF_varatt_expanded = 11; + @Native private static final int IDX_SIZEOF_varatt_external = 12; + + + + @Native private static final int IDX_HEAPTUPLESIZE = 20; + @Native private static final int IDX_OFFSET_TTS_NVALID = 21; + @Native private static final int IDX_SIZEOF_TTS_NVALID = 22; + + @Native private static final int IDX_TTS_FLAG_EMPTY = 23; + @Native private static final int IDX_TTS_FLAG_FIXED = 24; + @Native private static final int IDX_OFFSET_TTS_FLAGS = 25; + + /* + * Before PG 12, TTS had no flags field with bit flags, but instead + * distinct boolean (1-byte) fields. + */ + @Native private static final int IDX_OFFSET_TTS_EMPTY = 26; + @Native private static final int IDX_OFFSET_TTS_FIXED = 27; + @Native private static final int IDX_OFFSET_TTS_TABLEOID = 28; + + + + @Native private static final int IDX_OFFSET_TUPLEDESC_ATTRS = 40; + @Native private static final int IDX_OFFSET_TUPLEDESC_TDREFCOUNT = 41; + @Native private static final int IDX_SIZEOF_TUPLEDESC_TDREFCOUNT = 42; + @Native private static final int IDX_OFFSET_TUPLEDESC_TDTYPEID = 43; + @Native private static final int IDX_OFFSET_TUPLEDESC_TDTYPMOD = 44; + + + + @Native private static final int IDX_SIZEOF_FORM_PG_ATTRIBUTE = 50; + @Native private static final int IDX_ATTRIBUTE_FIXED_PART_SIZE = 51; + @Native private static final int IDX_OFFSET_pg_attribute_atttypid = 52; + @Native private static final int IDX_OFFSET_pg_attribute_attlen = 53; + @Native private static final int IDX_OFFSET_pg_attribute_attcacheoff = 54; + @Native private static final int IDX_OFFSET_pg_attribute_atttypmod = 55; + @Native private static final int IDX_OFFSET_pg_attribute_attbyval = 56; + @Native private static final int IDX_OFFSET_pg_attribute_attalign = 57; + @Native private static final int IDX_OFFSET_pg_attribute_attnotnull = 58; + @Native private static final int IDX_OFFSET_pg_attribute_attisdropped = 59; + + + + @Native private static final int IDX_CLASS_TUPLE_SIZE = 70; + @Native private static final int IDX_Anum_pg_class_reltype = 71; + + + + @Native private static final int IDX_SIZEOF_MCTX = 80; + @Native private static final int IDX_OFFSET_MCTX_isReset = 81; + @Native private static final int IDX_OFFSET_MCTX_mem_allocated = 82; + @Native private static final int IDX_OFFSET_MCTX_parent = 83; + @Native private static final int IDX_OFFSET_MCTX_firstchild = 84; + @Native private static final int IDX_OFFSET_MCTX_prevchild = 85; + @Native private static final int IDX_OFFSET_MCTX_nextchild = 86; + @Native private static final int IDX_OFFSET_MCTX_name = 87; + @Native private static final int IDX_OFFSET_MCTX_ident = 88; + + + + /* + * N_ACL_RIGHTS was stable for a long time, but changes in PG 15 and in 16 + */ + @Native private static final int IDX_N_ACL_RIGHTS = 100; + + + + /* + * Identifiers of different caches in PG's syscache, utils/cache/syscache.c. + * As upstream adds new caches, the enum is kept in alphabetical order, so + * they belong in this section to have their effective values picked up. + */ + @Native private static final int IDX_ATTNUM = 500; + @Native private static final int IDX_AUTHMEMMEMROLE = 501; + @Native private static final int IDX_AUTHMEMROLEMEM = 502; + @Native private static final int IDX_AUTHOID = 503; + @Native private static final int IDX_COLLOID = 504; + @Native private static final int IDX_DATABASEOID = 505; + @Native private static final int IDX_LANGOID = 506; + @Native private static final int IDX_NAMESPACEOID = 507; + @Native private static final int IDX_OPEROID = 508; + @Native private static final int IDX_PROCOID = 509; + @Native private static final int IDX_RELOID = 510; + @Native private static final int IDX_TSCONFIGOID = 511; + @Native private static final int IDX_TSDICTOID = 512; + @Native private static final int IDX_TYPEOID = 513; + + + + @Native private static final int + IDX_OFFSET_HeapTupleHeaderData_t_infomask = 1000; + @Native private static final int + IDX_OFFSET_HeapTupleHeaderData_t_infomask2 = 1001; + @Native private static final int + IDX_OFFSET_HeapTupleHeaderData_t_hoff = 1002; + @Native private static final int + IDX_OFFSET_HeapTupleHeaderData_t_bits = 1003; + + + + /* + * These public statics are the values of interest, set at class + * initialization time by reading them from the buffer managed by Natives. + */ + + /** + * Numeric PostgreSQL version compiled in at build time. + */ + public static final int PG_VERSION_NUM; + + public static final int SIZEOF_DATUM; + /* + * In backporting, can be useful when the git history shows something was + * always of 'int' type, so it doesn't need a dedicated SIZEOF_FOO, but does + * need to notice if a platform has an unexpected 'int' width. + */ + public static final int SIZEOF_INT; + public static final int SIZEOF_SIZE; + + public static final int ALIGNOF_SHORT; + public static final int ALIGNOF_INT; + public static final int ALIGNOF_DOUBLE; + public static final int MAXIMUM_ALIGNOF; + + public static final short NAMEDATALEN; + + + + public static final int SIZEOF_varatt_indirect; + public static final int SIZEOF_varatt_expanded; + public static final int SIZEOF_varatt_external; + + + + public static final int HEAPTUPLESIZE; + public static final int OFFSET_TTS_NVALID; + public static final int SIZEOF_TTS_NVALID; // int or int16 per pg version + + public static final int TTS_FLAG_EMPTY; + public static final int TTS_FLAG_FIXED; + public static final int OFFSET_TTS_FLAGS; + + public static final int OFFSET_TTS_EMPTY; + public static final int OFFSET_TTS_FIXED; + + public static final int OFFSET_TTS_TABLEOID; // NOCONSTANT unless PG >= 12 + + + + public static final int OFFSET_TUPLEDESC_ATTRS; + public static final int OFFSET_TUPLEDESC_TDREFCOUNT; + public static final int SIZEOF_TUPLEDESC_TDREFCOUNT; + public static final int OFFSET_TUPLEDESC_TDTYPEID; + public static final int OFFSET_TUPLEDESC_TDTYPMOD; + + + + public static final int SIZEOF_FORM_PG_ATTRIBUTE; + public static final int ATTRIBUTE_FIXED_PART_SIZE; + public static final int OFFSET_pg_attribute_atttypid; + public static final int OFFSET_pg_attribute_attlen; + public static final int OFFSET_pg_attribute_attcacheoff; + public static final int OFFSET_pg_attribute_atttypmod; + public static final int OFFSET_pg_attribute_attbyval; + public static final int OFFSET_pg_attribute_attalign; + public static final int OFFSET_pg_attribute_attnotnull; + public static final int OFFSET_pg_attribute_attisdropped; + + + + public static final int CLASS_TUPLE_SIZE; + public static final int Anum_pg_class_reltype; + + + + public static final int SIZEOF_MCTX; + public static final int OFFSET_MCTX_isReset; + public static final int OFFSET_MCTX_mem_allocated; // since PG 13 + public static final int OFFSET_MCTX_parent; + public static final int OFFSET_MCTX_firstchild; + public static final int OFFSET_MCTX_prevchild; // since PG 9.6 + public static final int OFFSET_MCTX_nextchild; + public static final int OFFSET_MCTX_name; + public static final int OFFSET_MCTX_ident; // since PG 11 + + + + /* + * The number of meaningful rights bits in an ACL bitmask, imported by + * AclItem. + */ + public static final int N_ACL_RIGHTS; + + + + /* + * These identify different caches in the PostgreSQL syscache. + * The indicated classes import them. + */ + public static final int ATTNUM; // AttributeImpl + public static final int AUTHMEMMEMROLE; // RegRoleImpl + public static final int AUTHMEMROLEMEM; // " + public static final int AUTHOID; // " + public static final int COLLOID; // RegCollationImpl + public static final int DATABASEOID; // DatabaseImpl + public static final int LANGOID; // ProceduralLanguageImpl + public static final int NAMESPACEOID; // RegNamespaceImpl + public static final int OPEROID; // RegOperatorImpl + public static final int PROCOID; // RegProcedureImpl + public static final int RELOID; // RegClassImpl + public static final int TSCONFIGOID; // RegConfigImpl + public static final int TSDICTOID; // RegDictionaryImpl + public static final int TYPEOID; // RegTypeImpl + + + + // TBASE + public static final int OFFSET_HeapTupleHeaderData_t_infomask; + public static final int OFFSET_HeapTupleHeaderData_t_infomask2; + public static final int OFFSET_HeapTupleHeaderData_t_hoff; + public static final int OFFSET_HeapTupleHeaderData_t_bits; + + + + /** + * Value supplied for one of these constants when built in a version of PG + * that does not define it. + *

    + * Clearly not useful if the value could be valid for the constant + * in question. + */ + @Native public static final int NOCONSTANT = -1; + + static + { + try ( Natives n = new Natives() ) + { + PG_VERSION_NUM = n.get(IDX_PG_VERSION_NUM); + + SIZEOF_DATUM = n.get(IDX_SIZEOF_DATUM); + SIZEOF_INT = n.get(IDX_SIZEOF_INT); + SIZEOF_SIZE = n.get(IDX_SIZEOF_SIZE); + + ALIGNOF_SHORT = n.get(IDX_ALIGNOF_SHORT); + ALIGNOF_INT = n.get(IDX_ALIGNOF_INT); + ALIGNOF_DOUBLE = n.get(IDX_ALIGNOF_DOUBLE); + MAXIMUM_ALIGNOF = n.get(IDX_MAXIMUM_ALIGNOF); + + int c = n.get(IDX_NAMEDATALEN); + NAMEDATALEN = (short)c; + assert c == NAMEDATALEN; + + + + n.gap(IDX_SIZEOF_varatt_indirect); + SIZEOF_varatt_indirect = n.get(IDX_SIZEOF_varatt_indirect); + SIZEOF_varatt_expanded = n.get(IDX_SIZEOF_varatt_expanded); + SIZEOF_varatt_external = n.get(IDX_SIZEOF_varatt_external); + + + + n.gap(IDX_HEAPTUPLESIZE); + HEAPTUPLESIZE = n.get(IDX_HEAPTUPLESIZE); + OFFSET_TTS_NVALID = n.get(IDX_OFFSET_TTS_NVALID); + SIZEOF_TTS_NVALID = n.get(IDX_SIZEOF_TTS_NVALID); + + TTS_FLAG_EMPTY = n.get(IDX_TTS_FLAG_EMPTY); + TTS_FLAG_FIXED = n.get(IDX_TTS_FLAG_FIXED); + OFFSET_TTS_FLAGS = n.get(IDX_OFFSET_TTS_FLAGS); + + OFFSET_TTS_EMPTY = n.get(IDX_OFFSET_TTS_EMPTY); + OFFSET_TTS_FIXED = n.get(IDX_OFFSET_TTS_FIXED); + + OFFSET_TTS_TABLEOID = n.get(IDX_OFFSET_TTS_TABLEOID); + + + + n.gap(IDX_OFFSET_TUPLEDESC_ATTRS); + OFFSET_TUPLEDESC_ATTRS = n.get(IDX_OFFSET_TUPLEDESC_ATTRS); + OFFSET_TUPLEDESC_TDREFCOUNT= n.get(IDX_OFFSET_TUPLEDESC_TDREFCOUNT); + SIZEOF_TUPLEDESC_TDREFCOUNT= n.get(IDX_SIZEOF_TUPLEDESC_TDREFCOUNT); + OFFSET_TUPLEDESC_TDTYPEID = n.get(IDX_OFFSET_TUPLEDESC_TDTYPEID); + OFFSET_TUPLEDESC_TDTYPMOD = n.get(IDX_OFFSET_TUPLEDESC_TDTYPMOD); + + + + n.gap(IDX_SIZEOF_FORM_PG_ATTRIBUTE); + SIZEOF_FORM_PG_ATTRIBUTE = n.get(IDX_SIZEOF_FORM_PG_ATTRIBUTE); + ATTRIBUTE_FIXED_PART_SIZE = n.get(IDX_ATTRIBUTE_FIXED_PART_SIZE); + OFFSET_pg_attribute_atttypid + = n.get(IDX_OFFSET_pg_attribute_atttypid); + OFFSET_pg_attribute_attlen + = n.get(IDX_OFFSET_pg_attribute_attlen); + OFFSET_pg_attribute_attcacheoff + = n.get(IDX_OFFSET_pg_attribute_attcacheoff); + OFFSET_pg_attribute_atttypmod + = n.get(IDX_OFFSET_pg_attribute_atttypmod); + OFFSET_pg_attribute_attbyval + = n.get(IDX_OFFSET_pg_attribute_attbyval); + OFFSET_pg_attribute_attalign + = n.get(IDX_OFFSET_pg_attribute_attalign); + OFFSET_pg_attribute_attnotnull + = n.get(IDX_OFFSET_pg_attribute_attnotnull); + OFFSET_pg_attribute_attisdropped + = n.get(IDX_OFFSET_pg_attribute_attisdropped); + + + + n.gap(IDX_CLASS_TUPLE_SIZE); + CLASS_TUPLE_SIZE = n.get(IDX_CLASS_TUPLE_SIZE); + Anum_pg_class_reltype = n.get(IDX_Anum_pg_class_reltype); + + + + n.gap(IDX_SIZEOF_MCTX); + SIZEOF_MCTX = n.get(IDX_SIZEOF_MCTX); + OFFSET_MCTX_isReset = n.get(IDX_OFFSET_MCTX_isReset); + OFFSET_MCTX_mem_allocated = n.get(IDX_OFFSET_MCTX_mem_allocated); + OFFSET_MCTX_parent = n.get(IDX_OFFSET_MCTX_parent); + OFFSET_MCTX_firstchild = n.get(IDX_OFFSET_MCTX_firstchild); + OFFSET_MCTX_prevchild = n.get(IDX_OFFSET_MCTX_prevchild); + OFFSET_MCTX_nextchild = n.get(IDX_OFFSET_MCTX_nextchild); + OFFSET_MCTX_name = n.get(IDX_OFFSET_MCTX_name); + OFFSET_MCTX_ident = n.get(IDX_OFFSET_MCTX_ident); + + + + n.gap(IDX_N_ACL_RIGHTS); + N_ACL_RIGHTS = n.get(IDX_N_ACL_RIGHTS); + + + + n.gap(IDX_ATTNUM); + ATTNUM = n.get(IDX_ATTNUM); + AUTHMEMMEMROLE = n.get(IDX_AUTHMEMMEMROLE); + AUTHMEMROLEMEM = n.get(IDX_AUTHMEMROLEMEM); + AUTHOID = n.get(IDX_AUTHOID); + COLLOID = n.get(IDX_COLLOID); + DATABASEOID = n.get(IDX_DATABASEOID); + LANGOID = n.get(IDX_LANGOID); + NAMESPACEOID = n.get(IDX_NAMESPACEOID); + OPEROID = n.get(IDX_OPEROID); + PROCOID = n.get(IDX_PROCOID); + RELOID = n.get(IDX_RELOID); + TSCONFIGOID = n.get(IDX_TSCONFIGOID); + TSDICTOID = n.get(IDX_TSDICTOID); + TYPEOID = n.get(IDX_TYPEOID); + + + + n.gap(IDX_OFFSET_HeapTupleHeaderData_t_infomask); + OFFSET_HeapTupleHeaderData_t_infomask = + n.get(IDX_OFFSET_HeapTupleHeaderData_t_infomask); + OFFSET_HeapTupleHeaderData_t_infomask2 = + n.get(IDX_OFFSET_HeapTupleHeaderData_t_infomask2); + OFFSET_HeapTupleHeaderData_t_hoff = + n.get(IDX_OFFSET_HeapTupleHeaderData_t_hoff); + OFFSET_HeapTupleHeaderData_t_bits = + n.get(IDX_OFFSET_HeapTupleHeaderData_t_bits); + + + + } + } + + static class ConstantsError extends ExceptionInInitializerError + { + ConstantsError() + { + super("PL/Java native constants jumbled; " + + "are jar and shared object same version?"); + } + } + + /* + * Some static methods used by more than one model class, here because they + * are sort of related to constants. For example, Alignment appears both in + * RegType and in Attribute. + */ + + static Alignment alignmentFromCatalog(byte b) + { + switch ( b ) + { + case (byte)'c': return Alignment.CHAR; + case (byte)'s': return Alignment.INT2; + case (byte)'i': return Alignment.INT4; + case (byte)'d': return Alignment.DOUBLE; + } + throw unchecked(new SQLException( + "unrecognized alignment '" + (char)b + "' in catalog", "XX000")); + } + + static int alignmentModulus(Alignment a) + { + switch ( a ) + { + case CHAR: return 1; + case INT2: return ALIGNOF_SHORT; + case INT4: return ALIGNOF_INT; + case DOUBLE: return ALIGNOF_DOUBLE; + } + throw unchecked(new SQLException( + "expected alignment, got " + a, "XX000")); + } + + static Storage storageFromCatalog(byte b) + { + switch ( b ) + { + case (byte)'x': return Storage.EXTENDED; + case (byte)'e': return Storage.EXTERNAL; + case (byte)'m': return Storage.MAIN; + case (byte)'p': return Storage.PLAIN; + } + throw unchecked(new SQLException( + "unrecognized storage '" + (char)b + "' in catalog", "XX000")); + } +} diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/ProceduralLanguageImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/ProceduralLanguageImpl.java new file mode 100644 index 000000000..31ae52417 --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/pg/ProceduralLanguageImpl.java @@ -0,0 +1,272 @@ +/* + * Copyright (c) 2022-2023 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.pg; + +import java.lang.invoke.MethodHandle; +import static java.lang.invoke.MethodHandles.lookup; +import java.lang.invoke.SwitchPoint; + +import java.sql.SQLException; + +import java.util.Iterator; +import java.util.List; + +import java.util.function.UnaryOperator; + +import org.postgresql.pljava.model.*; + +import org.postgresql.pljava.PLPrincipal; + +import org.postgresql.pljava.annotation.Function.Trust; + +import org.postgresql.pljava.internal.SwitchPointCache.Builder; +import static org.postgresql.pljava.internal.UncheckedException.unchecked; + +import org.postgresql.pljava.pg.CatalogObjectImpl.*; +import static org.postgresql.pljava.pg.ModelConstants.LANGOID; // syscache + +import org.postgresql.pljava.pg.adt.GrantAdapter; +import static org.postgresql.pljava.pg.adt.NameAdapter.SIMPLE_INSTANCE; +import static org.postgresql.pljava.pg.adt.OidAdapter.REGPROCEDURE_INSTANCE; +import static org.postgresql.pljava.pg.adt.OidAdapter.REGROLE_INSTANCE; +import static org.postgresql.pljava.pg.adt.Primitives.BOOLEAN_INSTANCE; + +import org.postgresql.pljava.sqlgen.Lexicals.Identifier.Simple; +import org.postgresql.pljava.sqlgen.Lexicals.Identifier.Unqualified; + +class ProceduralLanguageImpl extends Addressed +implements + Nonshared, Named, Owned, + AccessControlled, ProceduralLanguage +{ + private static UnaryOperator s_initializer; + + /* Implementation of Addressed */ + + @Override + public RegClass.Known classId() + { + return CLASSID; + } + + @Override + int cacheId() + { + return LANGOID; + } + + /* Implementation of Named, Owned, AccessControlled */ + + private static Simple name(ProceduralLanguageImpl o) throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return + t.get(Att.LANNAME, SIMPLE_INSTANCE); + } + + private static RegRole owner(ProceduralLanguageImpl o) throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return t.get(Att.LANOWNER, REGROLE_INSTANCE); + } + + private static List grants(ProceduralLanguageImpl o) + throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return t.get(Att.LANACL, GrantAdapter.LIST_INSTANCE); + } + + /* Implementation of ProceduralLanguage */ + + /** + * Merely passes the supplied slots array to the superclass constructor; all + * initialization of the slots will be the responsibility of the subclass. + */ + ProceduralLanguageImpl() + { + super(s_initializer.apply(new MethodHandle[NSLOTS])); + } + + static final int SLOT_PRINCIPAL; + static final int SLOT_HANDLER; + static final int SLOT_INLINEHANDLER; + static final int SLOT_VALIDATOR; + static final int NSLOTS; + + static + { + int i = CatalogObjectImpl.Addressed.NSLOTS; + s_initializer = + new Builder<>(ProceduralLanguageImpl.class) + .withLookup(lookup()) + .withSwitchPoint(o -> s_globalPoint[0]) + .withSlots(o -> o.m_slots) + .withCandidates(ProceduralLanguageImpl.class.getDeclaredMethods()) + + .withReceiverType(CatalogObjectImpl.Named.class) + .withReturnType(Unqualified.class) + .withDependent( "name", SLOT_NAME) + .withReturnType(null) + .withReceiverType(CatalogObjectImpl.Owned.class) + .withDependent( "owner", SLOT_OWNER) + .withReceiverType(CatalogObjectImpl.AccessControlled.class) + .withDependent( "grants", SLOT_ACL) + + .withReceiverType(null) + .withDependent( "principal", SLOT_PRINCIPAL = i++) + .withDependent( "handler", SLOT_HANDLER = i++) + .withDependent("inlineHandler", SLOT_INLINEHANDLER = i++) + .withDependent( "validator", SLOT_VALIDATOR = i++) + + .build() + /* + * Add these slot initializers after what Addressed does. + */ + .compose(CatalogObjectImpl.Addressed.s_initializer)::apply; + NSLOTS = i; + } + + static class Att + { + static final Attribute LANNAME; + static final Attribute LANOWNER; + static final Attribute LANACL; + static final Attribute LANPLTRUSTED; + static final Attribute LANPLCALLFOID; + static final Attribute LANINLINE; + static final Attribute LANVALIDATOR; + + static + { + Iterator itr = CLASSID.tupleDescriptor().project( + "lanname", + "lanowner", + "lanacl", + "lanpltrusted", + "lanplcallfoid", + "laninline", + "lanvalidator" + ).iterator(); + + LANNAME = itr.next(); + LANOWNER = itr.next(); + LANACL = itr.next(); + LANPLTRUSTED = itr.next(); + LANPLCALLFOID = itr.next(); + LANINLINE = itr.next(); + LANVALIDATOR = itr.next(); + + assert ! itr.hasNext() : "attribute initialization miscount"; + } + } + + /* computation methods */ + + private static PLPrincipal principal(ProceduralLanguageImpl o) + throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + if ( s.get(Att.LANPLTRUSTED, BOOLEAN_INSTANCE) ) + return new PLPrincipal.Sandboxed(o.name()); + return new PLPrincipal.Unsandboxed(o.name()); + } + + private static RegProcedure handler(ProceduralLanguageImpl o) + throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + @SuppressWarnings("unchecked") // XXX add memo magic here + RegProcedure p = (RegProcedure) + s.get(Att.LANPLCALLFOID, REGPROCEDURE_INSTANCE); + return p; + } + + private static RegProcedure inlineHandler( + ProceduralLanguageImpl o) + throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + @SuppressWarnings("unchecked") // XXX add memo magic here + RegProcedure p = (RegProcedure) + s.get(Att.LANINLINE, REGPROCEDURE_INSTANCE); + return p; + } + + private static RegProcedure validator(ProceduralLanguageImpl o) + throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + @SuppressWarnings("unchecked") // XXX add memo magic here + RegProcedure p = (RegProcedure) + s.get(Att.LANVALIDATOR, REGPROCEDURE_INSTANCE); + return p; + } + + /* API methods */ + + @Override + public PLPrincipal principal() + { + try + { + MethodHandle h = m_slots[SLOT_PRINCIPAL]; + return (PLPrincipal)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public RegProcedure handler() + { + try + { + MethodHandle h = m_slots[SLOT_HANDLER]; + return (RegProcedure)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public RegProcedure inlineHandler() + { + try + { + MethodHandle h = m_slots[SLOT_INLINEHANDLER]; + return (RegProcedure)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public RegProcedure validator() + { + try + { + MethodHandle h = m_slots[SLOT_VALIDATOR]; + return (RegProcedure)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } +} diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/RegClassImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/RegClassImpl.java new file mode 100644 index 000000000..f3c8de2bb --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/pg/RegClassImpl.java @@ -0,0 +1,765 @@ +/* + * Copyright (c) 2022-2023 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.pg; + +import java.lang.invoke.MethodHandle; +import static java.lang.invoke.MethodHandles.lookup; +import java.lang.invoke.SwitchPoint; + +import java.nio.ByteBuffer; +import static java.nio.ByteOrder.nativeOrder; + +import java.sql.SQLException; + +import java.util.Iterator; +import java.util.List; + +import java.util.function.UnaryOperator; + +import org.postgresql.pljava.internal.SwitchPointCache.Builder; + +import org.postgresql.pljava.model.*; + +import org.postgresql.pljava.pg.CatalogObjectImpl.*; +import static org.postgresql.pljava.pg.ModelConstants.Anum_pg_class_reltype; +import static org.postgresql.pljava.pg.ModelConstants.RELOID; // syscache +import static org.postgresql.pljava.pg.ModelConstants.CLASS_TUPLE_SIZE; + +import static org.postgresql.pljava.pg.adt.ArrayAdapter + .FLAT_STRING_LIST_INSTANCE; +import org.postgresql.pljava.pg.adt.GrantAdapter; +import org.postgresql.pljava.pg.adt.NameAdapter; +import static org.postgresql.pljava.pg.adt.OidAdapter.REGCLASS_INSTANCE; +import static org.postgresql.pljava.pg.adt.OidAdapter.REGNAMESPACE_INSTANCE; +import static org.postgresql.pljava.pg.adt.OidAdapter.REGROLE_INSTANCE; +import static org.postgresql.pljava.pg.adt.OidAdapter.REGTYPE_INSTANCE; +import static org.postgresql.pljava.pg.adt.Primitives.*; + +import org.postgresql.pljava.sqlgen.Lexicals.Identifier.Qualified; +import org.postgresql.pljava.sqlgen.Lexicals.Identifier.Simple; +import org.postgresql.pljava.sqlgen.Lexicals.Identifier.Unqualified; + +import static org.postgresql.pljava.internal.UncheckedException.unchecked; + +/* + * Can get lots of information, including Form_pg_class rd_rel and + * TupleDesc rd_att, from the relcache. See CacheRegisterRelcacheCallback(). + * However, the relcache copy of the class tuple is cut off at CLASS_TUPLE_SIZE. + */ + +class RegClassImpl extends Addressed +implements + Nonshared, Namespaced, Owned, + AccessControlled, RegClass +{ + static class Known> + extends RegClassImpl implements RegClass.Known + { + } + + /** + * Per-instance switch point, to be invalidated selectively + * by a relcache callback. + */ + SwitchPoint m_cacheSwitchPoint; + + private static UnaryOperator s_initializer; + + /* Implementation of Addressed */ + + @Override + public RegClass.Known classId() + { + return CLASSID; + } + + @Override + int cacheId() + { + return RELOID; + } + + /* Implementation of Named, Namespaced, Owned, AccessControlled */ + + private static Simple name(RegClassImpl o) throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return + t.get(Att.RELNAME, NameAdapter.SIMPLE_INSTANCE); + } + + private static RegNamespace namespace(RegClassImpl o) throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return t.get(Att.RELNAMESPACE, REGNAMESPACE_INSTANCE); + } + + private static RegRole owner(RegClassImpl o) throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return t.get(Att.RELOWNER, REGROLE_INSTANCE); + } + + private static List grants(RegClassImpl o) + throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return t.get(Att.RELACL, GrantAdapter.LIST_INSTANCE); + } + + /* Implementation of RegClass */ + + RegClassImpl() + { + super(s_initializer.apply(new MethodHandle[NSLOTS])); + m_cacheSwitchPoint = new SwitchPoint(); + } + + /** + * Called from {@code Factory}'s {@code invalidateRelation} to set up + * the invalidation of this relation's metadata. + *

    + * Adds this relation's {@code SwitchPoint} to the caller's list so that, + * if more than one is to be invalidated, that can be done in bulk. Adds to + * postOps any operations the caller should conclude with + * after invalidating the {@code SwitchPoint}. + */ + void invalidate(List sps, List postOps) + { + TupleDescriptor.Interned[] oldTDH = m_tupDescHolder; + sps.add(m_cacheSwitchPoint); + + /* + * Before invalidating the SwitchPoint, line up a new one (and a newly + * nulled tupDescHolder) for value-computing methods to find once the + * old SwitchPoint is invalidated. + */ + m_cacheSwitchPoint = new SwitchPoint(); + m_tupDescHolder = null; + + /* + * After the old SwitchPoint gets invalidated, the old tupDescHolder, + * if any, can have its element nulled so the old TupleDescriptor can + * be collected without having to wait for the 'guardWithTest's it is + * bound into to be recomputed. + */ + if ( null != oldTDH ) + postOps.add(() -> oldTDH[0] = null); + } + + /** + * Associated tuple descriptor, redundantly kept accessible here as well as + * opaquely bound into a {@code SwitchPointCache} method handle. + *

    + * This one-element array containing the descriptor is what gets bound into + * the handle, so the descriptor can be freed for GC at invalidation time + * (rather than waiting for the next tuple-descriptor request to replace + * the handle). Only accessed from {@code SwitchPointCache} computation + * methods or {@code TupleDescImpl} factory methods, all of which execute + * on the PG thread; no synchronization fuss needed. + *

    + * When null, no computation method has run (or none since invalidation), + * and the state is not known. Otherwise, the single element is the result + * to be returned by the {@code tupleDescriptor()} API method. + */ + TupleDescriptor.Interned[] m_tupDescHolder; + + /** + * Holder for the {@code RegType} corresponding to {@code type()}, + * only non-null during a call of {@code dualHandshake}. + */ + private RegType m_dual = null; + + /** + * Called by the corresponding {@code RegType} instance if it has just + * looked us up. + *

    + * Because the {@code SwitchPointCache} recomputation methods always execute + * on the PG thread, plain access to an instance field suffices here. + */ + void dualHandshake(RegType dual) + { + try + { + m_dual = dual; + dual = type(); + assert dual == m_dual : "RegType/RegClass handshake outcome"; + } + finally + { + m_dual = null; + } + } + + static final int SLOT_TUPLEDESCRIPTOR; + static final int SLOT_TYPE; + static final int SLOT_OFTYPE; + static final int SLOT_TOASTRELATION; + static final int SLOT_HASINDEX; + static final int SLOT_ISSHARED; + static final int SLOT_NATTRIBUTES; + static final int SLOT_CHECKS; + static final int SLOT_HASRULES; + static final int SLOT_HASTRIGGERS; + static final int SLOT_HASSUBCLASS; + static final int SLOT_ROWSECURITY; + static final int SLOT_FORCEROWSECURITY; + static final int SLOT_ISPOPULATED; + static final int SLOT_ISPARTITION; + static final int SLOT_OPTIONS; + static final int NSLOTS; + + static + { + int i = CatalogObjectImpl.Addressed.NSLOTS; + s_initializer = + new Builder<>(RegClassImpl.class) + .withLookup(lookup()) + .withSwitchPoint(o -> o.m_cacheSwitchPoint) + .withSlots(o -> o.m_slots) + + .withCandidates( + CatalogObjectImpl.Addressed.class.getDeclaredMethods()) + .withReceiverType(CatalogObjectImpl.Addressed.class) + .withDependent("cacheTuple", SLOT_TUPLE) + + .withCandidates(RegClassImpl.class.getDeclaredMethods()) + .withReceiverType(CatalogObjectImpl.Named.class) + .withReturnType(Unqualified.class) + .withDependent( "name", SLOT_NAME) + .withReceiverType(CatalogObjectImpl.Namespaced.class) + .withReturnType(null) + .withDependent( "namespace", SLOT_NAMESPACE) + .withReceiverType(CatalogObjectImpl.Owned.class) + .withDependent( "owner", SLOT_OWNER) + .withReceiverType(CatalogObjectImpl.AccessControlled.class) + .withDependent( "grants", SLOT_ACL) + + .withReceiverType(null) + .withDependent( "tupleDescriptor", SLOT_TUPLEDESCRIPTOR = i++) + .withDependent( "type", SLOT_TYPE = i++) + .withDependent( "ofType", SLOT_OFTYPE = i++) + .withDependent( "toastRelation", SLOT_TOASTRELATION = i++) + .withDependent( "hasIndex", SLOT_HASINDEX = i++) + .withDependent( "isShared", SLOT_ISSHARED = i++) + .withDependent( "nAttributes", SLOT_NATTRIBUTES = i++) + .withDependent( "checks", SLOT_CHECKS = i++) + .withDependent( "hasRules", SLOT_HASRULES = i++) + .withDependent( "hasTriggers", SLOT_HASTRIGGERS = i++) + .withDependent( "hasSubclass", SLOT_HASSUBCLASS = i++) + .withDependent( "rowSecurity", SLOT_ROWSECURITY = i++) + .withDependent("forceRowSecurity", SLOT_FORCEROWSECURITY = i++) + .withDependent( "isPopulated", SLOT_ISPOPULATED = i++) + .withDependent( "isPartition", SLOT_ISPARTITION = i++) + .withDependent( "options", SLOT_OPTIONS = i++) + + .build(); + NSLOTS = i; + } + + static class Att + { + static final Attribute RELNAME; + static final Attribute RELNAMESPACE; + static final Attribute RELOWNER; + static final Attribute RELACL; + static final Attribute RELOFTYPE; + static final Attribute RELTOASTRELID; + static final Attribute RELHASINDEX; + static final Attribute RELISSHARED; + static final Attribute RELNATTS; + static final Attribute RELCHECKS; + static final Attribute RELHASRULES; + static final Attribute RELHASTRIGGERS; + static final Attribute RELHASSUBCLASS; + static final Attribute RELROWSECURITY; + static final Attribute RELFORCEROWSECURITY; + static final Attribute RELISPOPULATED; + static final Attribute RELISPARTITION; + static final Attribute RELOPTIONS; + + static + { + Iterator itr = CLASSID.tupleDescriptor().project( + "relname", + "relnamespace", + "relowner", + "relacl", + "reloftype", + "reltoastrelid", + "relhasindex", + "relisshared", + "relnatts", + "relchecks", + "relhasrules", + "relhastriggers", + "relhassubclass", + "relrowsecurity", + "relforcerowsecurity", + "relispopulated", + "relispartition", + "reloptions" + ).iterator(); + + RELNAME = itr.next(); + RELNAMESPACE = itr.next(); + RELOWNER = itr.next(); + RELACL = itr.next(); + RELOFTYPE = itr.next(); + RELTOASTRELID = itr.next(); + RELHASINDEX = itr.next(); + RELISSHARED = itr.next(); + RELNATTS = itr.next(); + RELCHECKS = itr.next(); + RELHASRULES = itr.next(); + RELHASTRIGGERS = itr.next(); + RELHASSUBCLASS = itr.next(); + RELROWSECURITY = itr.next(); + RELFORCEROWSECURITY = itr.next(); + RELISPOPULATED = itr.next(); + RELISPARTITION = itr.next(); + RELOPTIONS = itr.next(); + + assert ! itr.hasNext() : "attribute initialization miscount"; + } + } + + /* computation methods */ + + /** + * Return the tuple descriptor for this relation, wrapped in a one-element + * array, which is also stored in {@code m_tupDescHolder}. + *

    + * The tuple descriptor for a relation can be retrieved from the PostgreSQL + * {@code relcache} or {@code typcache}; it's the same descriptor, and the + * latter gets it from the former. Going through the {@code relcache} is + * fussier, involving the lock manager every time, while using the + * {@code typcache} can avoid that except in its cache-miss case. + *

    + * Here, for every relation other than {@code pg_class} itself, we will + * rely on the corresponding {@code RegType} to do the work. There is a bit + * of incest involved; it will construct the descriptor to rely on our + * {@code SwitchPoint} for invalidation, and will poke the wrapper array + * into our {@code m_tupDescHolder}. + *

    + * It does that last bit so that, even if the first query for a type's + * tuple descriptor is made through the {@code RegType}, we will also return + * it if a later request is made here, and all of the invalidation logic + * lives here; it is relation-cache invalidation that obsoletes a cataloged + * tuple descriptor. + *

    + * However, when the relation is {@code pg_class} itself, we rely + * on a bespoke JNI method to get the descriptor from the {@code relcache}. + * The case occurs when we are looking up the descriptor to interpret our + * own cache tuples, and the normal case's {@code type()} call won't work + * before that's available. + */ + private static TupleDescriptor.Interned[] tupleDescriptor(RegClassImpl o) + { + TupleDescriptor.Interned[] r = o.m_tupDescHolder; + + /* + * If not null, r is a value placed here by an invocation of + * tupleDescriptor() on the associated RegType, and we have not seen an + * invalidation since that happened (invalidations run on the PG thread, + * as do computation methods like this, so we've not missed anything). + * It is the value to return. + */ + if ( null != r ) + return r; + + /* + * In any case other than looking up our own tuple descriptor, we can + * use type() to find the associated RegType and let it do the work. + */ + if ( CLASSID != o ) + { + o.type().tupleDescriptor(); // side effect: writes o.m_tupDescHolder + return o.m_tupDescHolder; + } + + /* + * It is the bootstrap case, looking up the pg_class tuple descriptor. + * If we got here we need it, so we can call the Cataloged constructor + * directly, rather than fromByteBuffer (which would first check whether + * we need it, and bump its reference count only if so). Called + * directly, the constructor expects the count already bumped, which + * the _tupDescBootstrap method will have done for us. + */ + ByteBuffer bb = _tupDescBootstrap(); + bb.order(nativeOrder()); + r = new TupleDescriptor.Interned[] {new TupleDescImpl.Cataloged(bb, o)}; + return o.m_tupDescHolder = r; + } + + private static RegType type(RegClassImpl o) throws SQLException + { + /* + * If this is a handshake occurring when the corresponding RegType + * has just looked *us* up, we are done. + */ + if ( null != o.m_dual ) + return o.m_dual; + + /* + * Otherwise, look up the corresponding RegType, and do the same + * handshake in reverse. Either way, the connection is set up + * bidirectionally with one cache lookup starting from either. That + * can avoid extra work in operations (like TupleDescriptor caching) + * that may touch both objects, without complicating their code. + * + * Because the fetching of pg_attribute's tuple descriptor + * necessarily passes through this point, and attributes don't know + * what their names are until it has, use the attribute number here. + */ + TupleTableSlot s = o.cacheTuple(); + RegType t = s.get( + s.descriptor().sqlGet(Anum_pg_class_reltype), REGTYPE_INSTANCE); + + ((RegTypeImpl)t).dualHandshake(o); + return t; + } + + private static RegType ofType(RegClassImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(Att.RELOFTYPE, REGTYPE_INSTANCE); + } + + private static RegClass toastRelation(RegClassImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(Att.RELTOASTRELID, REGCLASS_INSTANCE); + } + + private static boolean hasIndex(RegClassImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(Att.RELHASINDEX, BOOLEAN_INSTANCE); + } + + private static boolean isShared(RegClassImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(Att.RELISSHARED, BOOLEAN_INSTANCE); + } + + private static short nAttributes(RegClassImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(Att.RELNATTS, INT2_INSTANCE); + } + + private static short checks(RegClassImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(Att.RELCHECKS, INT2_INSTANCE); + } + + private static boolean hasRules(RegClassImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(Att.RELHASRULES, BOOLEAN_INSTANCE); + } + + private static boolean hasTriggers(RegClassImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(Att.RELHASTRIGGERS, BOOLEAN_INSTANCE); + } + + private static boolean hasSubclass(RegClassImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(Att.RELHASSUBCLASS, BOOLEAN_INSTANCE); + } + + private static boolean rowSecurity(RegClassImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(Att.RELROWSECURITY, BOOLEAN_INSTANCE); + } + + private static boolean forceRowSecurity(RegClassImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return + s.get(Att.RELFORCEROWSECURITY, BOOLEAN_INSTANCE); + } + + private static boolean isPopulated(RegClassImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(Att.RELISPOPULATED, BOOLEAN_INSTANCE); + } + + private static boolean isPartition(RegClassImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(Att.RELISPARTITION, BOOLEAN_INSTANCE); + } + + private static List options(RegClassImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return + s.get(Att.RELOPTIONS, FLAT_STRING_LIST_INSTANCE); + } + + /* API methods */ + + @Override + public TupleDescriptor.Interned tupleDescriptor() + { + try + { + MethodHandle h = m_slots[SLOT_TUPLEDESCRIPTOR]; + return ((TupleDescriptor.Interned[])h.invokeExact(this, h))[0]; + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public RegType type() + { + try + { + MethodHandle h = m_slots[SLOT_TYPE]; + return (RegType)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public RegType ofType() + { + try + { + MethodHandle h = m_slots[SLOT_OFTYPE]; + return (RegType)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + // am + // filenode + // tablespace + + /* Of limited interest ... estimates used by planner + * + int pages(); + float tuples(); + int allVisible(); + */ + + @Override + public RegClass toastRelation() + { + try + { + MethodHandle h = m_slots[SLOT_TOASTRELATION]; + return (RegClass)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public boolean hasIndex() + { + try + { + MethodHandle h = m_slots[SLOT_HASINDEX]; + return (boolean)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public boolean isShared() + { + try + { + MethodHandle h = m_slots[SLOT_ISSHARED]; + return (boolean)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + // persistence + // kind + + @Override + public short nAttributes() + { + try + { + MethodHandle h = m_slots[SLOT_NATTRIBUTES]; + return (short)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public short checks() + { + try + { + MethodHandle h = m_slots[SLOT_CHECKS]; + return (short)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public boolean hasRules() + { + try + { + MethodHandle h = m_slots[SLOT_HASRULES]; + return (boolean)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public boolean hasTriggers() + { + try + { + MethodHandle h = m_slots[SLOT_HASTRIGGERS]; + return (boolean)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public boolean hasSubclass() + { + try + { + MethodHandle h = m_slots[SLOT_HASSUBCLASS]; + return (boolean)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public boolean rowSecurity() + { + try + { + MethodHandle h = m_slots[SLOT_ROWSECURITY]; + return (boolean)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public boolean forceRowSecurity() + { + try + { + MethodHandle h = m_slots[SLOT_FORCEROWSECURITY]; + return (boolean)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public boolean isPopulated() + { + try + { + MethodHandle h = m_slots[SLOT_ISPOPULATED]; + return (boolean)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + // replident + + @Override + public boolean isPartition() + { + try + { + MethodHandle h = m_slots[SLOT_ISPARTITION]; + return (boolean)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + // rewrite + // frozenxid + // minmxid + + @Override + public List options() + { + try + { + MethodHandle h = m_slots[SLOT_OPTIONS]; + return (List)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + // partbound +} diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/RegCollationImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/RegCollationImpl.java new file mode 100644 index 000000000..255ed3dd8 --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/pg/RegCollationImpl.java @@ -0,0 +1,321 @@ +/* + * Copyright (c) 2022-2023 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.pg; + +import java.lang.invoke.MethodHandle; +import static java.lang.invoke.MethodHandles.lookup; +import java.lang.invoke.SwitchPoint; + +import java.sql.SQLException; + +import java.util.Iterator; + +import java.util.function.UnaryOperator; + +import org.postgresql.pljava.internal.SwitchPointCache.Builder; +import static org.postgresql.pljava.internal.UncheckedException.unchecked; + +import org.postgresql.pljava.model.*; + +import org.postgresql.pljava.pg.CatalogObjectImpl.*; +import static org.postgresql.pljava.pg.ModelConstants.COLLOID; // syscache + +import org.postgresql.pljava.pg.adt.EncodingAdapter; +import static org.postgresql.pljava.pg.adt.NameAdapter.SIMPLE_INSTANCE; +import static org.postgresql.pljava.pg.adt.NameAdapter.AS_STRING_INSTANCE; +import static org.postgresql.pljava.pg.adt.OidAdapter.REGNAMESPACE_INSTANCE; +import static org.postgresql.pljava.pg.adt.OidAdapter.REGROLE_INSTANCE; +import static org.postgresql.pljava.pg.adt.Primitives.BOOLEAN_INSTANCE; +import static org.postgresql.pljava.pg.adt.Primitives.INT1_INSTANCE; +import org.postgresql.pljava.pg.adt.TextAdapter; + +import org.postgresql.pljava.sqlgen.Lexicals.Identifier.Simple; +import org.postgresql.pljava.sqlgen.Lexicals.Identifier.Unqualified; + +class RegCollationImpl extends Addressed +implements Nonshared, Namespaced, Owned, RegCollation +{ + private static UnaryOperator s_initializer; + + /* Implementation of Addressed */ + + @Override + public RegClass.Known classId() + { + return CLASSID; + } + + @Override + int cacheId() + { + return COLLOID; + } + + /* Implementation of Named, Namespaced, Owned */ + + private static Simple name(RegCollationImpl o) throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return t.get(Att.COLLNAME, SIMPLE_INSTANCE); + } + + private static RegNamespace namespace(RegCollationImpl o) + throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return + t.get(Att.COLLNAMESPACE, REGNAMESPACE_INSTANCE); + } + + private static RegRole owner(RegCollationImpl o) throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return t.get(Att.COLLOWNER, REGROLE_INSTANCE); + } + + /* Implementation of RegCollation */ + + /** + * Merely passes the supplied slots array to the superclass constructor; all + * initialization of the slots will be the responsibility of the subclass. + */ + RegCollationImpl() + { + super(s_initializer.apply(new MethodHandle[NSLOTS])); + } + + static final int SLOT_ENCODING; + static final int SLOT_COLLATE; + static final int SLOT_CTYPE; + static final int SLOT_PROVIDER; + static final int SLOT_VERSION; + static final int SLOT_DETERMINISTIC; + static final int NSLOTS; + + static + { + int i = CatalogObjectImpl.Addressed.NSLOTS; + s_initializer = + new Builder<>(RegCollationImpl.class) + .withLookup(lookup()) + .withSwitchPoint(o -> s_globalPoint[0]) + .withSlots(o -> o.m_slots) + .withCandidates(RegCollationImpl.class.getDeclaredMethods()) + + .withReceiverType(CatalogObjectImpl.Named.class) + .withReturnType(Unqualified.class) + .withDependent( "name", SLOT_NAME) + .withReturnType(null) + .withReceiverType(CatalogObjectImpl.Namespaced.class) + .withDependent( "namespace", SLOT_NAMESPACE) + .withReceiverType(CatalogObjectImpl.Owned.class) + .withDependent( "owner", SLOT_OWNER) + + .withReceiverType(null) + .withDependent( "encoding", SLOT_ENCODING = i++) + .withDependent( "collate", SLOT_COLLATE = i++) + .withDependent( "ctype", SLOT_CTYPE = i++) + .withDependent( "provider", SLOT_PROVIDER = i++) + .withDependent( "version", SLOT_VERSION = i++) + .withDependent("deterministic", SLOT_DETERMINISTIC = i++) + + .build() + /* + * Add these slot initializers after what Addressed does. + */ + .compose(CatalogObjectImpl.Addressed.s_initializer)::apply; + NSLOTS = i; + } + + static class Att + { + static final Attribute COLLNAME; + static final Attribute COLLNAMESPACE; + static final Attribute COLLOWNER; + static final Attribute COLLENCODING; + static final Attribute COLLCOLLATE; + static final Attribute COLLCTYPE; + static final Attribute COLLPROVIDER; + static final Attribute COLLVERSION; + static final Attribute COLLISDETERMINISTIC; + + static + { + Iterator itr = attNames( + "collname", + "collnamespace", + "collowner", + "collencoding", + "collcollate", + "collctype", + "collprovider", + "collversion" + ).alsoIf(PG_VERSION_NUM >= 120000, + "collisdeterministic" + ).project(CLASSID.tupleDescriptor()); + + COLLNAME = itr.next(); + COLLNAMESPACE = itr.next(); + COLLOWNER = itr.next(); + COLLENCODING = itr.next(); + COLLCOLLATE = itr.next(); + COLLCTYPE = itr.next(); + COLLPROVIDER = itr.next(); + COLLVERSION = itr.next(); + COLLISDETERMINISTIC = itr.next(); + + assert ! itr.hasNext() : "attribute initialization miscount"; + } + } + + /* computation methods */ + + private static CharsetEncoding encoding(RegCollationImpl o) + throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return + s.get(Att.COLLENCODING, EncodingAdapter.INSTANCE); + } + + private static String collate(RegCollationImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(Att.COLLCOLLATE, AS_STRING_INSTANCE); + } + + private static String ctype(RegCollationImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(Att.COLLCTYPE, AS_STRING_INSTANCE); + } + + private static Provider provider(RegCollationImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + byte p = s.get(Att.COLLPROVIDER, INT1_INSTANCE); + switch ( p ) + { + case (byte)'d': + return Provider.DEFAULT; + case (byte)'c': + return Provider.LIBC; + case (byte)'i': + return Provider.ICU; + default: + throw new UnsupportedOperationException(String.format( + "Unrecognized collation provider value %#x", p)); + } + } + + private static String version(RegCollationImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(Att.COLLVERSION, TextAdapter.INSTANCE); + } + + private static boolean deterministic(RegCollationImpl o) throws SQLException + { + if ( null == Att.COLLISDETERMINISTIC ) + return true; // before PG 12, there were only deterministic ones + + TupleTableSlot s = o.cacheTuple(); + return + s.get(Att.COLLISDETERMINISTIC, BOOLEAN_INSTANCE); + } + + /* API methods */ + + @Override + public CharsetEncoding encoding() + { + try + { + MethodHandle h = m_slots[SLOT_ENCODING]; + return (CharsetEncoding)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public String collate() + { + try + { + MethodHandle h = m_slots[SLOT_COLLATE]; + return (String)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public String ctype() + { + try + { + MethodHandle h = m_slots[SLOT_CTYPE]; + return (String)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public Provider provider() // since PG 10 + { + try + { + MethodHandle h = m_slots[SLOT_PROVIDER]; + return (Provider)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public String version() // since PG 10 + { + try + { + MethodHandle h = m_slots[SLOT_VERSION]; + return (String)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public boolean deterministic() // since PG 12 + { + try + { + MethodHandle h = m_slots[SLOT_DETERMINISTIC]; + return (boolean)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } +} diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/RegConfigImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/RegConfigImpl.java new file mode 100644 index 000000000..ea908b951 --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/pg/RegConfigImpl.java @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2022-2023 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.pg; + +import java.lang.invoke.MethodHandle; +import static java.lang.invoke.MethodHandles.lookup; +import java.lang.invoke.SwitchPoint; + +import java.sql.SQLException; + +import java.util.Iterator; + +import java.util.function.UnaryOperator; + +import org.postgresql.pljava.internal.SwitchPointCache.Builder; + +import org.postgresql.pljava.model.*; + +import org.postgresql.pljava.pg.CatalogObjectImpl.*; +import static org.postgresql.pljava.pg.ModelConstants.TSCONFIGOID; // syscache + +import static org.postgresql.pljava.pg.adt.NameAdapter.SIMPLE_INSTANCE; +import static org.postgresql.pljava.pg.adt.OidAdapter.REGNAMESPACE_INSTANCE; +import static org.postgresql.pljava.pg.adt.OidAdapter.REGROLE_INSTANCE; + +import org.postgresql.pljava.sqlgen.Lexicals.Identifier.Simple; +import org.postgresql.pljava.sqlgen.Lexicals.Identifier.Unqualified; + +class RegConfigImpl extends Addressed +implements Nonshared, Namespaced, Owned, RegConfig +{ + private static UnaryOperator s_initializer; + + /* Implementation of Addressed */ + + @Override + public RegClass.Known classId() + { + return CLASSID; + } + + @Override + int cacheId() + { + return TSCONFIGOID; + } + + /* Implementation of Named, Namespaced, Owned */ + + private static Simple name(RegConfigImpl o) throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return t.get(Att.CFGNAME, SIMPLE_INSTANCE); + } + + private static RegNamespace namespace(RegConfigImpl o) + throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return + t.get(Att.CFGNAMESPACE, REGNAMESPACE_INSTANCE); + } + + private static RegRole owner(RegConfigImpl o) throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return t.get(Att.CFGOWNER, REGROLE_INSTANCE); + } + + /* Implementation of RegConfig */ + + /** + * Merely passes the supplied slots array to the superclass constructor; all + * initialization of the slots will be the responsibility of the subclass. + */ + RegConfigImpl() + { + super(s_initializer.apply(new MethodHandle[NSLOTS])); + } + + static + { + s_initializer = + new Builder<>(RegConfigImpl.class) + .withLookup(lookup()) + .withSwitchPoint(o -> s_globalPoint[0]) + .withSlots(o -> o.m_slots) + .withCandidates(RegConfigImpl.class.getDeclaredMethods()) + + .withReceiverType(CatalogObjectImpl.Named.class) + .withReturnType(Unqualified.class) + .withDependent( "name", SLOT_NAME) + .withReturnType(null) + .withReceiverType(CatalogObjectImpl.Namespaced.class) + .withDependent( "namespace", SLOT_NAMESPACE) + .withReceiverType(CatalogObjectImpl.Owned.class) + .withDependent( "owner", SLOT_OWNER) + + .build() + .compose(CatalogObjectImpl.Addressed.s_initializer)::apply; + } + + static class Att + { + static final Attribute CFGNAME; + static final Attribute CFGNAMESPACE; + static final Attribute CFGOWNER; + + static + { + Iterator itr = CLASSID.tupleDescriptor().project( + "cfgname", + "cfgnamespace", + "cfgowner" + ).iterator(); + + CFGNAME = itr.next(); + CFGNAMESPACE = itr.next(); + CFGOWNER = itr.next(); + + assert ! itr.hasNext() : "attribute initialization miscount"; + } + } +} diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/RegDictionaryImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/RegDictionaryImpl.java new file mode 100644 index 000000000..3841255aa --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/pg/RegDictionaryImpl.java @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2022-2023 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.pg; + +import java.lang.invoke.MethodHandle; +import static java.lang.invoke.MethodHandles.lookup; +import java.lang.invoke.SwitchPoint; + +import java.sql.SQLException; + +import java.util.Iterator; + +import java.util.function.UnaryOperator; + +import org.postgresql.pljava.internal.SwitchPointCache.Builder; + +import org.postgresql.pljava.model.*; + +import org.postgresql.pljava.pg.CatalogObjectImpl.*; +import static org.postgresql.pljava.pg.ModelConstants.TSDICTOID; // syscache + +import static org.postgresql.pljava.pg.adt.NameAdapter.SIMPLE_INSTANCE; +import static org.postgresql.pljava.pg.adt.OidAdapter.REGNAMESPACE_INSTANCE; +import static org.postgresql.pljava.pg.adt.OidAdapter.REGROLE_INSTANCE; + +import org.postgresql.pljava.sqlgen.Lexicals.Identifier.Simple; +import org.postgresql.pljava.sqlgen.Lexicals.Identifier.Unqualified; + +class RegDictionaryImpl extends Addressed +implements Nonshared, Namespaced, Owned, RegDictionary +{ + private static UnaryOperator s_initializer; + + /* Implementation of Addressed */ + + @Override + public RegClass.Known classId() + { + return CLASSID; + } + + @Override + int cacheId() + { + return TSDICTOID; + } + + /* Implementation of Named, Namespaced, Owned */ + + private static Simple name(RegDictionaryImpl o) throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return t.get(Att.DICTNAME, SIMPLE_INSTANCE); + } + + private static RegNamespace namespace(RegDictionaryImpl o) + throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return + t.get(Att.DICTNAMESPACE, REGNAMESPACE_INSTANCE); + } + + private static RegRole owner(RegDictionaryImpl o) throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return t.get(Att.DICTOWNER, REGROLE_INSTANCE); + } + + /* Implementation of RegDictionary */ + + /** + * Merely passes the supplied slots array to the superclass constructor; all + * initialization of the slots will be the responsibility of the subclass. + */ + RegDictionaryImpl() + { + super(s_initializer.apply(new MethodHandle[NSLOTS])); + } + + static + { + s_initializer = + new Builder<>(RegDictionaryImpl.class) + .withLookup(lookup()) + .withSwitchPoint(o -> s_globalPoint[0]) + .withSlots(o -> o.m_slots) + .withCandidates(RegDictionaryImpl.class.getDeclaredMethods()) + + .withReceiverType(CatalogObjectImpl.Named.class) + .withReturnType(Unqualified.class) + .withDependent( "name", SLOT_NAME) + .withReturnType(null) + .withReceiverType(CatalogObjectImpl.Namespaced.class) + .withDependent( "namespace", SLOT_NAMESPACE) + .withReceiverType(CatalogObjectImpl.Owned.class) + .withDependent( "owner", SLOT_OWNER) + + .build() + .compose(CatalogObjectImpl.Addressed.s_initializer)::apply; + } + + static class Att + { + static final Attribute DICTNAME; + static final Attribute DICTNAMESPACE; + static final Attribute DICTOWNER; + + static + { + Iterator itr = CLASSID.tupleDescriptor().project( + "dictname", + "dictnamespace", + "dictowner" + ).iterator(); + + DICTNAME = itr.next(); + DICTNAMESPACE = itr.next(); + DICTOWNER = itr.next(); + + assert ! itr.hasNext() : "attribute initialization miscount"; + } + } +} diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/RegNamespaceImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/RegNamespaceImpl.java new file mode 100644 index 000000000..2b6607410 --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/pg/RegNamespaceImpl.java @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2022-2023 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.pg; + +import java.lang.invoke.MethodHandle; +import static java.lang.invoke.MethodHandles.lookup; +import java.lang.invoke.SwitchPoint; + +import java.sql.SQLException; + +import java.util.Iterator; +import java.util.List; + +import java.util.function.UnaryOperator; + +import org.postgresql.pljava.internal.SwitchPointCache.Builder; + +import org.postgresql.pljava.model.*; + +import org.postgresql.pljava.pg.CatalogObjectImpl.*; +import static org.postgresql.pljava.pg.ModelConstants.NAMESPACEOID; // syscache + +import org.postgresql.pljava.pg.adt.GrantAdapter; +import org.postgresql.pljava.pg.adt.NameAdapter; +import static org.postgresql.pljava.pg.adt.OidAdapter.REGROLE_INSTANCE; + +import org.postgresql.pljava.sqlgen.Lexicals.Identifier.Simple; +import org.postgresql.pljava.sqlgen.Lexicals.Identifier.Unqualified; + +class RegNamespaceImpl extends Addressed +implements + Nonshared, Named, Owned, + AccessControlled, RegNamespace +{ + private static UnaryOperator s_initializer; + + /* Implementation of Addressed */ + + @Override + public RegClass.Known classId() + { + return CLASSID; + } + + @Override + int cacheId() + { + return NAMESPACEOID; + } + + /* Implementation of Named, Owned, AccessControlled */ + + private static Simple name(RegNamespaceImpl o) throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return + t.get(Att.NSPNAME, NameAdapter.SIMPLE_INSTANCE); + } + + private static RegRole owner(RegNamespaceImpl o) throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return t.get(Att.NSPOWNER, REGROLE_INSTANCE); + } + + private static List grants(RegNamespaceImpl o) + throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return t.get(Att.NSPACL, GrantAdapter.LIST_INSTANCE); + } + + /* Implementation of RegNamespace */ + + /** + * Merely passes the supplied slots array to the superclass constructor; all + * initialization of the slots will be the responsibility of the subclass. + */ + RegNamespaceImpl() + { + super(s_initializer.apply(new MethodHandle[NSLOTS])); + } + + static + { + s_initializer = + new Builder<>(RegNamespaceImpl.class) + .withLookup(lookup()) + .withSwitchPoint(o -> s_globalPoint[0]) + .withSlots(o -> o.m_slots) + .withCandidates(RegNamespaceImpl.class.getDeclaredMethods()) + + .withReceiverType(CatalogObjectImpl.Named.class) + .withReturnType(Unqualified.class) + .withDependent( "name", SLOT_NAME) + .withReturnType(null) + .withReceiverType(CatalogObjectImpl.Owned.class) + .withDependent( "owner", SLOT_OWNER) + .withReceiverType(CatalogObjectImpl.AccessControlled.class) + .withDependent( "grants", SLOT_ACL) + + .build() + /* + * Add these slot initializers after what Addressed does. + */ + .compose(CatalogObjectImpl.Addressed.s_initializer)::apply; + } + + static class Att + { + static final Attribute NSPNAME; + static final Attribute NSPOWNER; + static final Attribute NSPACL; + + static + { + Iterator itr = CLASSID.tupleDescriptor().project( + "nspname", + "nspowner", + "nspacl" + ).iterator(); + + NSPNAME = itr.next(); + NSPOWNER = itr.next(); + NSPACL = itr.next(); + + assert ! itr.hasNext() : "attribute initialization miscount"; + } + } +} diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/RegOperatorImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/RegOperatorImpl.java new file mode 100644 index 000000000..034c8aaac --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/pg/RegOperatorImpl.java @@ -0,0 +1,454 @@ +/* + * Copyright (c) 2022-2023 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.pg; + +import java.lang.invoke.MethodHandle; +import static java.lang.invoke.MethodHandles.lookup; +import java.lang.invoke.SwitchPoint; + +import java.sql.SQLException; + +import java.util.Iterator; + +import java.util.function.UnaryOperator; + +import org.postgresql.pljava.internal.SwitchPointCache.Builder; +import static org.postgresql.pljava.internal.UncheckedException.unchecked; + +import org.postgresql.pljava.model.*; + +import org.postgresql.pljava.pg.CatalogObjectImpl.*; +import static org.postgresql.pljava.pg.ModelConstants.OPEROID; // syscache + +import static org.postgresql.pljava.pg.adt.NameAdapter.OPERATOR_INSTANCE; +import static org.postgresql.pljava.pg.adt.OidAdapter.REGNAMESPACE_INSTANCE; +import static org.postgresql.pljava.pg.adt.OidAdapter.REGOPERATOR_INSTANCE; +import static org.postgresql.pljava.pg.adt.OidAdapter.REGPROCEDURE_INSTANCE; +import static org.postgresql.pljava.pg.adt.OidAdapter.REGROLE_INSTANCE; +import static org.postgresql.pljava.pg.adt.OidAdapter.REGTYPE_INSTANCE; +import static org.postgresql.pljava.pg.adt.Primitives.INT1_INSTANCE; +import static org.postgresql.pljava.pg.adt.Primitives.BOOLEAN_INSTANCE; + +import org.postgresql.pljava.sqlgen.Lexicals.Identifier.Operator; +import org.postgresql.pljava.sqlgen.Lexicals.Identifier.Unqualified; + +class RegOperatorImpl extends Addressed +implements Nonshared, Namespaced, Owned, RegOperator +{ + private static UnaryOperator s_initializer; + + /* Implementation of Addressed */ + + @Override + public RegClass.Known classId() + { + return CLASSID; + } + + @Override + int cacheId() + { + return OPEROID; + } + + /* Implementation of Named, Namespaced, Owned */ + + private static Operator name(RegOperatorImpl o) throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return t.get(Att.OPRNAME, OPERATOR_INSTANCE); + } + + private static RegNamespace namespace(RegOperatorImpl o) + throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return + t.get(Att.OPRNAMESPACE, REGNAMESPACE_INSTANCE); + } + + private static RegRole owner(RegOperatorImpl o) throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return t.get(Att.OPROWNER, REGROLE_INSTANCE); + } + + /* Implementation of RegOperator */ + + /** + * Merely passes the supplied slots array to the superclass constructor; all + * initialization of the slots will be the responsibility of the subclass. + */ + RegOperatorImpl() + { + super(s_initializer.apply(new MethodHandle[NSLOTS])); + } + + static final int SLOT_KIND; + static final int SLOT_CANMERGE; + static final int SLOT_CANHASH; + static final int SLOT_LEFTOPERAND; + static final int SLOT_RIGHTOPERAND; + static final int SLOT_RESULT; + static final int SLOT_COMMUTATOR; + static final int SLOT_NEGATOR; + static final int SLOT_EVALUATOR; + static final int SLOT_RESTRICTIONESTIMATOR; + static final int SLOT_JOINESTIMATOR; + static final int NSLOTS; + + static + { + int i = CatalogObjectImpl.Addressed.NSLOTS; + s_initializer = + new Builder<>(RegOperatorImpl.class) + .withLookup(lookup()) + .withSwitchPoint(o -> s_globalPoint[0]) + .withSlots(o -> o.m_slots) + .withCandidates(RegOperatorImpl.class.getDeclaredMethods()) + + .withReceiverType(CatalogObjectImpl.Named.class) + .withReturnType(Unqualified.class) + .withDependent( "name", SLOT_NAME) + .withReturnType(null) + .withReceiverType(CatalogObjectImpl.Namespaced.class) + .withDependent( "namespace", SLOT_NAMESPACE) + .withReceiverType(CatalogObjectImpl.Owned.class) + .withDependent( "owner", SLOT_OWNER) + + .withReceiverType(null) + .withDependent( "kind", SLOT_KIND = i++) + .withDependent( "canMerge", SLOT_CANMERGE = i++) + .withDependent( "canHash", SLOT_CANHASH = i++) + .withDependent( "leftOperand", SLOT_LEFTOPERAND = i++) + .withDependent( "rightOperand", SLOT_RIGHTOPERAND = i++) + .withDependent( "result", SLOT_RESULT = i++) + .withDependent( "commutator", SLOT_COMMUTATOR = i++) + .withDependent( "negator", SLOT_NEGATOR = i++) + .withDependent( "evaluator", SLOT_EVALUATOR = i++) + .withDependent( + "restrictionEstimator", SLOT_RESTRICTIONESTIMATOR = i++) + .withDependent("joinEstimator", SLOT_JOINESTIMATOR = i++) + + .build() + .compose(CatalogObjectImpl.Addressed.s_initializer)::apply; + NSLOTS = i; + } + + static class Att + { + static final Attribute OPRNAME; + static final Attribute OPRNAMESPACE; + static final Attribute OPROWNER; + static final Attribute OPRKIND; + static final Attribute OPRCANMERGE; + static final Attribute OPRCANHASH; + static final Attribute OPRLEFT; + static final Attribute OPRRIGHT; + static final Attribute OPRRESULT; + static final Attribute OPRCOM; + static final Attribute OPRNEGATE; + static final Attribute OPRCODE; + static final Attribute OPRREST; + static final Attribute OPRJOIN; + + static + { + Iterator itr = CLASSID.tupleDescriptor().project( + "oprname", + "oprnamespace", + "oprowner", + "oprkind", + "oprcanmerge", + "oprcanhash", + "oprleft", + "oprright", + "oprresult", + "oprcom", + "oprnegate", + "oprcode", + "oprrest", + "oprjoin" + ).iterator(); + + OPRNAME = itr.next(); + OPRNAMESPACE = itr.next(); + OPROWNER = itr.next(); + OPRKIND = itr.next(); + OPRCANMERGE = itr.next(); + OPRCANHASH = itr.next(); + OPRLEFT = itr.next(); + OPRRIGHT = itr.next(); + OPRRESULT = itr.next(); + OPRCOM = itr.next(); + OPRNEGATE = itr.next(); + OPRCODE = itr.next(); + OPRREST = itr.next(); + OPRJOIN = itr.next(); + + assert ! itr.hasNext() : "attribute initialization miscount"; + } + } + + /* computation methods */ + + private static Kind kind(RegOperatorImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + byte b = s.get(Att.OPRKIND, INT1_INSTANCE); + switch ( b ) + { + case (byte)'b': + return Kind.INFIX; + case (byte)'l': + return Kind.PREFIX; + case (byte)'r': + @SuppressWarnings("deprecation") + Kind k = Kind.POSTFIX; + return k; + default: + throw new UnsupportedOperationException(String.format( + "Unrecognized operator kind value %#x", b)); + } + } + + private static boolean canMerge(RegOperatorImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(Att.OPRCANMERGE, BOOLEAN_INSTANCE); + } + + private static boolean canHash(RegOperatorImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(Att.OPRCANHASH, BOOLEAN_INSTANCE); + } + + private static RegType leftOperand(RegOperatorImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(Att.OPRLEFT, REGTYPE_INSTANCE); + } + + private static RegType rightOperand(RegOperatorImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(Att.OPRRIGHT, REGTYPE_INSTANCE); + } + + private static RegType result(RegOperatorImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(Att.OPRRESULT, REGTYPE_INSTANCE); + } + + private static RegOperator commutator(RegOperatorImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(Att.OPRCOM, REGOPERATOR_INSTANCE); + } + + private static RegOperator negator(RegOperatorImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(Att.OPRNEGATE, REGOPERATOR_INSTANCE); + } + + private static RegProcedure evaluator(RegOperatorImpl o) + throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + @SuppressWarnings("unchecked") // XXX add memo magic here + RegProcedure p = (RegProcedure) + s.get(Att.OPRCODE, REGPROCEDURE_INSTANCE); + return p; + } + + private static RegProcedure + restrictionEstimator(RegOperatorImpl o) + throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + @SuppressWarnings("unchecked") // XXX add memo magic here + RegProcedure p = + (RegProcedure) + s.get(Att.OPRREST, REGPROCEDURE_INSTANCE); + return p; + } + + private static RegProcedure + joinEstimator(RegOperatorImpl o) + throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + @SuppressWarnings("unchecked") // XXX add memo magic here + RegProcedure p = (RegProcedure) + s.get(Att.OPRJOIN, REGPROCEDURE_INSTANCE); + return p; + } + + /* API methods */ + + @Override + public Kind kind() + { + try + { + MethodHandle h = m_slots[SLOT_KIND]; + return (Kind)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public boolean canMerge() + { + try + { + MethodHandle h = m_slots[SLOT_CANMERGE]; + return (boolean)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public boolean canHash() + { + try + { + MethodHandle h = m_slots[SLOT_CANHASH]; + return (boolean)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public RegType leftOperand() + { + try + { + MethodHandle h = m_slots[SLOT_LEFTOPERAND]; + return (RegType)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public RegType rightOperand() + { + try + { + MethodHandle h = m_slots[SLOT_RIGHTOPERAND]; + return (RegType)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public RegType result() + { + try + { + MethodHandle h = m_slots[SLOT_RESULT]; + return (RegType)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public RegOperator commutator() + { + try + { + MethodHandle h = m_slots[SLOT_COMMUTATOR]; + return (RegOperator)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public RegOperator negator() + { + try + { + MethodHandle h = m_slots[SLOT_NEGATOR]; + return (RegOperator)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public RegProcedure evaluator() + { + try + { + MethodHandle h = m_slots[SLOT_EVALUATOR]; + return (RegProcedure)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public RegProcedure restrictionEstimator() + { + try + { + MethodHandle h = m_slots[SLOT_RESTRICTIONESTIMATOR]; + return (RegProcedure)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public RegProcedure joinEstimator() + { + try + { + MethodHandle h = m_slots[SLOT_JOINESTIMATOR]; + return (RegProcedure)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } +} diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/RegProcedureImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/RegProcedureImpl.java new file mode 100644 index 000000000..f1e197f0b --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/pg/RegProcedureImpl.java @@ -0,0 +1,839 @@ +/* + * Copyright (c) 2022-2023 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.pg; + +import java.lang.invoke.MethodHandle; +import static java.lang.invoke.MethodHandles.lookup; +import java.lang.invoke.SwitchPoint; + +import java.sql.SQLException; +import java.sql.SQLXML; + +import java.util.Iterator; +import java.util.List; + +import java.util.function.UnaryOperator; + +import org.postgresql.pljava.TargetList.Projection; + +import org.postgresql.pljava.annotation.Function.Effects; +import org.postgresql.pljava.annotation.Function.OnNullInput; +import org.postgresql.pljava.annotation.Function.Parallel; +import org.postgresql.pljava.annotation.Function.Security; + +import org.postgresql.pljava.internal.SwitchPointCache.Builder; +import static org.postgresql.pljava.internal.UncheckedException.unchecked; + +import org.postgresql.pljava.model.*; + +import org.postgresql.pljava.pg.CatalogObjectImpl.*; +import static org.postgresql.pljava.pg.ModelConstants.PROCOID; // syscache + +import static org.postgresql.pljava.pg.adt.ArrayAdapter + .FLAT_STRING_LIST_INSTANCE; +import org.postgresql.pljava.pg.adt.GrantAdapter; +import static org.postgresql.pljava.pg.adt.NameAdapter.SIMPLE_INSTANCE; +import static org.postgresql.pljava.pg.adt.OidAdapter.PLANG_INSTANCE; +import static org.postgresql.pljava.pg.adt.OidAdapter.REGNAMESPACE_INSTANCE; +import static org.postgresql.pljava.pg.adt.OidAdapter.REGPROCEDURE_INSTANCE; +import static org.postgresql.pljava.pg.adt.OidAdapter.REGROLE_INSTANCE; +import static org.postgresql.pljava.pg.adt.OidAdapter.REGTYPE_INSTANCE; +import static org.postgresql.pljava.pg.adt.Primitives.BOOLEAN_INSTANCE; +import static org.postgresql.pljava.pg.adt.Primitives.FLOAT4_INSTANCE; +import static org.postgresql.pljava.pg.adt.Primitives.INT1_INSTANCE; +import org.postgresql.pljava.pg.adt.TextAdapter; +import static org.postgresql.pljava.pg.adt.XMLAdapter.SYNTHETIC_INSTANCE; + +import org.postgresql.pljava.sqlgen.Lexicals.Identifier.Simple; +import org.postgresql.pljava.sqlgen.Lexicals.Identifier.Unqualified; + +class RegProcedureImpl> +extends Addressed> +implements + Nonshared>, Namespaced, Owned, + AccessControlled, RegProcedure +{ + private static UnaryOperator s_initializer; + + /* Implementation of Addressed */ + + @Override + public RegClass.Known> classId() + { + return CLASSID; + } + + @Override + int cacheId() + { + return PROCOID; + } + + /* Implementation of Named, Namespaced, Owned, AccessControlled */ + + private static Simple name(RegProcedureImpl o) throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return t.get(Att.PRONAME, SIMPLE_INSTANCE); + } + + private static RegNamespace namespace(RegProcedureImpl o) + throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return t.get(Att.PRONAMESPACE, REGNAMESPACE_INSTANCE); + } + + private static RegRole owner(RegProcedureImpl o) throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return t.get(Att.PROOWNER, REGROLE_INSTANCE); + } + + private static List grants(RegProcedureImpl o) + throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return t.get(Att.PROACL, GrantAdapter.LIST_INSTANCE); + } + + /* Implementation of RegProcedure */ + + /** + * Merely passes the supplied slots array to the superclass constructor; all + * initialization of the slots will be the responsibility of the subclass. + */ + RegProcedureImpl() + { + super(s_initializer.apply(new MethodHandle[NSLOTS])); + } + + static final int SLOT_LANGUAGE; + static final int SLOT_COST; + static final int SLOT_ROWS; + static final int SLOT_VARIADICTYPE; + static final int SLOT_SUPPORT; + static final int SLOT_KIND; + static final int SLOT_SECURITY; + static final int SLOT_LEAKPROOF; + static final int SLOT_ONNULLINPUT; + static final int SLOT_RETURNSSET; + static final int SLOT_EFFECTS; + static final int SLOT_PARALLEL; + static final int SLOT_RETURNTYPE; + static final int SLOT_ARGTYPES; + static final int SLOT_ALLARGTYPES; + static final int SLOT_ARGMODES; + static final int SLOT_ARGNAMES; + static final int SLOT_TRANSFORMTYPES; + static final int SLOT_SRC; + static final int SLOT_BIN; + static final int SLOT_CONFIG; + static final int NSLOTS; + + static + { + int i = CatalogObjectImpl.Addressed.NSLOTS; + s_initializer = + new Builder<>(RegProcedureImpl.class) + .withLookup(lookup()) + .withSwitchPoint(o -> s_globalPoint[0]) + .withSlots(o -> o.m_slots) + .withCandidates(RegProcedureImpl.class.getDeclaredMethods()) + + .withReceiverType(CatalogObjectImpl.Named.class) + .withReturnType(Unqualified.class) + .withDependent( "name", SLOT_NAME) + .withReturnType(null) + .withReceiverType(CatalogObjectImpl.Namespaced.class) + .withDependent( "namespace", SLOT_NAMESPACE) + .withReceiverType(CatalogObjectImpl.Owned.class) + .withDependent( "owner", SLOT_OWNER) + .withReceiverType(CatalogObjectImpl.AccessControlled.class) + .withDependent( "grants", SLOT_ACL) + + .withReceiverType(null) + .withDependent( "language", SLOT_LANGUAGE = i++) + .withDependent( "cost", SLOT_COST = i++) + .withDependent( "rows", SLOT_ROWS = i++) + .withDependent( "variadicType", SLOT_VARIADICTYPE = i++) + .withDependent( "support", SLOT_SUPPORT = i++) + .withDependent( "kind", SLOT_KIND = i++) + .withDependent( "security", SLOT_SECURITY = i++) + .withDependent( "leakproof", SLOT_LEAKPROOF = i++) + .withDependent( "onNullInput", SLOT_ONNULLINPUT = i++) + .withDependent( "returnsSet", SLOT_RETURNSSET = i++) + .withDependent( "effects", SLOT_EFFECTS = i++) + .withDependent( "parallel", SLOT_PARALLEL = i++) + .withDependent( "returnType", SLOT_RETURNTYPE = i++) + .withDependent( "argTypes", SLOT_ARGTYPES = i++) + .withDependent( "allArgTypes", SLOT_ALLARGTYPES = i++) + .withDependent( "argModes", SLOT_ARGMODES = i++) + .withDependent( "argNames", SLOT_ARGNAMES = i++) + .withDependent("transformTypes", SLOT_TRANSFORMTYPES = i++) + .withDependent( "src", SLOT_SRC = i++) + .withDependent( "bin", SLOT_BIN = i++) + .withDependent( "config", SLOT_CONFIG = i++) + + .build() + /* + * Add these slot initializers after what Addressed does. + */ + .compose(CatalogObjectImpl.Addressed.s_initializer)::apply; + NSLOTS = i; + } + + static class Att + { + static final Attribute PRONAME; + static final Attribute PRONAMESPACE; + static final Attribute PROOWNER; + static final Attribute PROACL; + static final Attribute PROLANG; + static final Attribute PROCOST; + static final Attribute PROROWS; + static final Attribute PROVARIADIC; + static final Attribute PROSECDEF; + static final Attribute PROLEAKPROOF; + static final Attribute PROISSTRICT; + static final Attribute PRORETSET; + static final Attribute PROVOLATILE; + static final Attribute PROPARALLEL; + static final Attribute PRORETTYPE; + static final Attribute PROARGTYPES; + static final Attribute PROALLARGTYPES; + static final Attribute PROARGMODES; + static final Attribute PROARGNAMES; + static final Attribute PROTRFTYPES; + static final Attribute PROSRC; + static final Attribute PROBIN; + static final Attribute PROCONFIG; + static final Attribute PROARGDEFAULTS; + static final Projection PROISAGG_PROISWINDOW; + static final Attribute PROKIND; + static final Attribute PROTRANSFORM; + static final Attribute PROSUPPORT; + static final Attribute PROSQLBODY; + + static + { + AttNames itr = attNames( + "proname", + "pronamespace", + "proowner", + "proacl", + "prolang", + "procost", + "prorows", + "provariadic", + "prosecdef", + "proleakproof", + "proisstrict", + "proretset", + "provolatile", + "proparallel", + "prorettype", + "proargtypes", + "proallargtypes", + "proargmodes", + "proargnames", + "protrftypes", + "prosrc", + "probin", + "proconfig", + "proargdefaults" + ).alsoIf(PG_VERSION_NUM < 110000, + "proisagg", + "proiswindow" + ).alsoIf(PG_VERSION_NUM >= 110000, + "prokind" + ).alsoIf(PG_VERSION_NUM < 120000, + "protransform" // early internal version of prosupport + ).alsoIf(PG_VERSION_NUM >= 120000, + "prosupport" + ).alsoIf(PG_VERSION_NUM >= 140000, + "prosqlbody" + ).project(CLASSID.tupleDescriptor()); + + PRONAME = itr.next(); + PRONAMESPACE = itr.next(); + PROOWNER = itr.next(); + PROACL = itr.next(); + PROLANG = itr.next(); + PROCOST = itr.next(); + PROROWS = itr.next(); + PROVARIADIC = itr.next(); + PROSECDEF = itr.next(); + PROLEAKPROOF = itr.next(); + PROISSTRICT = itr.next(); + PRORETSET = itr.next(); + PROVOLATILE = itr.next(); + PROPARALLEL = itr.next(); + PRORETTYPE = itr.next(); + PROARGTYPES = itr.next(); + PROALLARGTYPES = itr.next(); + PROARGMODES = itr.next(); + PROARGNAMES = itr.next(); + PROTRFTYPES = itr.next(); + PROSRC = itr.next(); + PROBIN = itr.next(); + PROCONFIG = itr.next(); + PROARGDEFAULTS = itr.next(); + PROISAGG_PROISWINDOW = itr.project(itr.next(), itr.next()); + PROKIND = itr.next(); + PROTRANSFORM = itr.next(); + PROSUPPORT = itr.next(); + PROSQLBODY = itr.next(); + + assert ! itr.hasNext() : "attribute initialization miscount"; + } + } + + /* computation methods */ + + private static ProceduralLanguage language(RegProcedureImpl o) + throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(Att.PROLANG, PLANG_INSTANCE); + } + + private static float cost(RegProcedureImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(Att.PROCOST, FLOAT4_INSTANCE); + } + + private static float rows(RegProcedureImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(Att.PROROWS, FLOAT4_INSTANCE); + } + + private static RegType variadicType(RegProcedureImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(Att.PROVARIADIC, REGTYPE_INSTANCE); + } + + private static RegProcedure support(RegProcedureImpl o) + throws SQLException + { + RegProcedure p; + Attribute a = Att.PROSUPPORT; + + if ( null == a ) // missing in this PG version + a = Att.PROTRANSFORM; // use earlier internal-only equivalent + + TupleTableSlot t = o.cacheTuple(); + p = t.get(a, REGPROCEDURE_INSTANCE); + + @SuppressWarnings("unchecked") // XXX add memo magic here + RegProcedure narrowed = (RegProcedure)p; + + return narrowed; + } + + private static Kind kind(RegProcedureImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + + if ( null == Att.PROKIND ) // before PG 11, there were separate booleans + { + return Att.PROISAGG_PROISWINDOW.applyOver(s, c -> + c.apply(BOOLEAN_INSTANCE, BOOLEAN_INSTANCE, (agg, win) -> + agg ? Kind.AGGREGATE : win ? Kind.WINDOW : Kind.FUNCTION) + ); + } + + byte b = s.get(Att.PROKIND, INT1_INSTANCE); + switch ( b ) + { + case (byte)'f': + return Kind.FUNCTION; + case (byte)'p': + return Kind.PROCEDURE; + case (byte)'a': + return Kind.AGGREGATE; + case (byte)'w': + return Kind.WINDOW; + default: + throw new UnsupportedOperationException(String.format( + "Unrecognized procedure/function kind value %#x", b)); + } + } + + private static Security security(RegProcedureImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + if ( s.get(Att.PROSECDEF, BOOLEAN_INSTANCE) ) + return Security.DEFINER; + return Security.INVOKER; + } + + private static boolean leakproof(RegProcedureImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(Att.PROLEAKPROOF, BOOLEAN_INSTANCE); + } + + private static OnNullInput onNullInput(RegProcedureImpl o) + throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + if ( s.get(Att.PROISSTRICT, BOOLEAN_INSTANCE) ) + return OnNullInput.RETURNS_NULL; + return OnNullInput.CALLED; + } + + private static boolean returnsSet(RegProcedureImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(Att.PRORETSET, BOOLEAN_INSTANCE); + } + + private static Effects effects(RegProcedureImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + byte b = s.get(Att.PROVOLATILE, INT1_INSTANCE); + switch ( b ) + { + case (byte)'i': + return Effects.IMMUTABLE; + case (byte)'s': + return Effects.STABLE; + case (byte)'v': + return Effects.VOLATILE; + default: + throw new UnsupportedOperationException(String.format( + "Unrecognized procedure/function volatility value %#x", b)); + } + } + + private static Parallel parallel(RegProcedureImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + byte b = s.get(Att.PROPARALLEL, INT1_INSTANCE); + switch ( b ) + { + case (byte)'s': + return Parallel.SAFE; + case (byte)'r': + return Parallel.RESTRICTED; + case (byte)'u': + return Parallel.UNSAFE; + default: + throw new UnsupportedOperationException(String.format( + "Unrecognized procedure/function parallel safety value %#x",b)); + } + } + + private static RegType returnType(RegProcedureImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(Att.PRORETTYPE, REGTYPE_INSTANCE); + } + + private static List argTypes(RegProcedureImpl o) + throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return + s.get(Att.PROARGTYPES, + ArrayAdapters.REGTYPE_LIST_INSTANCE); + } + + private static List allArgTypes(RegProcedureImpl o) + throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return + s.get(Att.PROALLARGTYPES, + ArrayAdapters.REGTYPE_LIST_INSTANCE); + } + + private static List argModes(RegProcedureImpl o) + throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return + s.get(Att.PROARGMODES, + ArrayAdapters.ARGMODE_LIST_INSTANCE); + } + + private static List argNames(RegProcedureImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return + s.get(Att.PROARGNAMES, + ArrayAdapters.TEXT_NAME_LIST_INSTANCE); + } + + private static List transformTypes(RegProcedureImpl o) + throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return + s.get(Att.PROTRFTYPES, + ArrayAdapters.REGTYPE_LIST_INSTANCE); + } + + private static String src(RegProcedureImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(Att.PROSRC, TextAdapter.INSTANCE); + } + + private static String bin(RegProcedureImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(Att.PROBIN, TextAdapter.INSTANCE); + } + + private static List config(RegProcedureImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return + s.get(Att.PROCONFIG, FLAT_STRING_LIST_INSTANCE); + } + + /* API methods */ + + @Override + public ProceduralLanguage language() + { + try + { + MethodHandle h = m_slots[SLOT_LANGUAGE]; + return (ProceduralLanguage)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public float cost() + { + try + { + MethodHandle h = m_slots[SLOT_COST]; + return (float)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public float rows() + { + try + { + MethodHandle h = m_slots[SLOT_ROWS]; + return (float)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public RegType variadicType() + { + try + { + MethodHandle h = m_slots[SLOT_VARIADICTYPE]; + return (RegType)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public RegProcedure support() + { + try + { + MethodHandle h = m_slots[SLOT_SUPPORT]; + return (RegProcedure)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public Kind kind() + { + try + { + MethodHandle h = m_slots[SLOT_KIND]; + return (Kind)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public Security security() + { + try + { + MethodHandle h = m_slots[SLOT_SECURITY]; + return (Security)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public boolean leakproof() + { + try + { + MethodHandle h = m_slots[SLOT_LEAKPROOF]; + return (boolean)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public OnNullInput onNullInput() + { + try + { + MethodHandle h = m_slots[SLOT_ONNULLINPUT]; + return (OnNullInput)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public boolean returnsSet() + { + try + { + MethodHandle h = m_slots[SLOT_RETURNSSET]; + return (boolean)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public Effects effects() + { + try + { + MethodHandle h = m_slots[SLOT_EFFECTS]; + return (Effects)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public Parallel parallel() + { + try + { + MethodHandle h = m_slots[SLOT_PARALLEL]; + return (Parallel)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public RegType returnType() + { + try + { + MethodHandle h = m_slots[SLOT_RETURNTYPE]; + return (RegType)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public List argTypes() + { + try + { + MethodHandle h = m_slots[SLOT_ARGTYPES]; + return (List)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public List allArgTypes() + { + try + { + MethodHandle h = m_slots[SLOT_ALLARGTYPES]; + return (List)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public List argModes() + { + try + { + MethodHandle h = m_slots[SLOT_ARGMODES]; + return (List)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public List argNames() + { + try + { + MethodHandle h = m_slots[SLOT_ARGNAMES]; + return (List)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public SQLXML argDefaults() + { + /* + * Because of the JDBC rules that an SQLXML instance lasts no longer + * than one transaction and can only be read once, it is not a good + * candidate for caching. We will just fetch a new one from the cached + * tuple as needed. + */ + TupleTableSlot s = cacheTuple(); + return s.get(Att.PROARGDEFAULTS, SYNTHETIC_INSTANCE); + } + + @Override + public List transformTypes() + { + try + { + MethodHandle h = m_slots[SLOT_TRANSFORMTYPES]; + return (List)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public String src() + { + try + { + MethodHandle h = m_slots[SLOT_SRC]; + return (String)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public String bin() + { + try + { + MethodHandle h = m_slots[SLOT_BIN]; + return (String)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public SQLXML sqlBody() + { + /* + * Because of the JDBC rules that an SQLXML instance lasts no longer + * than one transaction and can only be read once, it is not a good + * candidate for caching. We will just fetch a new one from the cached + * tuple as needed. + */ + if ( null == Att.PROSQLBODY ) // missing in this PG version + return null; + + TupleTableSlot s = cacheTuple(); + return s.get(Att.PROSQLBODY, SYNTHETIC_INSTANCE); + } + + @Override + public List config() + { + try + { + MethodHandle h = m_slots[SLOT_CONFIG]; + return (List)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public M memo() + { + throw notyet(); + } +} diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/RegRoleImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/RegRoleImpl.java new file mode 100644 index 000000000..5902d0fc4 --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/pg/RegRoleImpl.java @@ -0,0 +1,384 @@ +/* + * Copyright (c) 2022-2023 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.pg; + +import java.lang.invoke.MethodHandle; +import static java.lang.invoke.MethodHandles.lookup; +import java.lang.invoke.SwitchPoint; + +import java.nio.file.attribute.GroupPrincipal; +import java.nio.file.attribute.UserPrincipal; + +import java.sql.SQLException; + +import java.util.Iterator; +import java.util.List; + +import org.postgresql.pljava.RolePrincipal; + +import java.util.function.UnaryOperator; + +import org.postgresql.pljava.internal.SwitchPointCache.Builder; +import static org.postgresql.pljava.internal.UncheckedException.unchecked; + +import org.postgresql.pljava.model.*; + +import org.postgresql.pljava.pg.CatalogObjectImpl.*; +import static org.postgresql.pljava.pg.ModelConstants.AUTHOID; // syscache +import static org.postgresql.pljava.pg.ModelConstants.AUTHMEMMEMROLE; +import static org.postgresql.pljava.pg.ModelConstants.AUTHMEMROLEMEM; + +import static org.postgresql.pljava.pg.adt.NameAdapter.SIMPLE_INSTANCE; +import static org.postgresql.pljava.pg.adt.Primitives.BOOLEAN_INSTANCE; +import static org.postgresql.pljava.pg.adt.Primitives.INT4_INSTANCE; + +import org.postgresql.pljava.sqlgen.Lexicals.Identifier.Simple; +import org.postgresql.pljava.sqlgen.Lexicals.Identifier.Unqualified; + +/** + * Implementation of the {@link RegRole RegRole} interface. + *

    + * That this class can in fact be cast to {@link RegRole.Grantee Grantee} is an + * unadvertised implementation detail. + */ +class RegRoleImpl extends Addressed +implements + Shared, Named, + AccessControlled, RegRole.Grantee +{ + private static UnaryOperator s_initializer; + + /* Implementation of Addressed */ + + @Override + public RegClass.Known classId() + { + return CLASSID; + } + + @Override + int cacheId() + { + return AUTHOID; + } + + /* Implementation of Named, AccessControlled */ + + private static Simple name(RegRoleImpl o) throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return t.get(Att.ROLNAME, SIMPLE_INSTANCE); + } + + private static List grants(RegRoleImpl o) + { + throw notyet("CatCList support needed"); + } + + /* Implementation of RegRole */ + + /** + * Merely passes the supplied slots array to the superclass constructor; all + * initialization of the slots will be the responsibility of the subclass. + */ + RegRoleImpl() + { + super(s_initializer.apply(new MethodHandle[NSLOTS])); + } + + static final int SLOT_MEMBEROF; + static final int SLOT_SUPERUSER; + static final int SLOT_INHERIT; + static final int SLOT_CREATEROLE; + static final int SLOT_CREATEDB; + static final int SLOT_CANLOGIN; + static final int SLOT_REPLICATION; + static final int SLOT_BYPASSRLS; + static final int SLOT_CONNECTIONLIMIT; + static final int NSLOTS; + + static + { + int i = CatalogObjectImpl.Addressed.NSLOTS; + s_initializer = + new Builder<>(RegRoleImpl.class) + .withLookup(lookup()) + .withSwitchPoint(o -> s_globalPoint[0]) + .withSlots(o -> o.m_slots) + .withCandidates(RegRoleImpl.class.getDeclaredMethods()) + + .withReceiverType(CatalogObjectImpl.Named.class) + .withReturnType(Unqualified.class) + .withDependent( "name", SLOT_NAME) + .withReturnType(null) + .withReceiverType(CatalogObjectImpl.AccessControlled.class) + .withDependent( "grants", SLOT_ACL) + + .withReceiverType(null) + .withDependent( "memberOf", SLOT_MEMBEROF = i++) + .withDependent( "superuser", SLOT_SUPERUSER = i++) + .withDependent( "inherit", SLOT_INHERIT = i++) + .withDependent( "createRole", SLOT_CREATEROLE = i++) + .withDependent( "createDB", SLOT_CREATEDB = i++) + .withDependent( "canLogIn", SLOT_CANLOGIN = i++) + .withDependent( "replication", SLOT_REPLICATION = i++) + .withDependent( "bypassRLS", SLOT_BYPASSRLS = i++) + .withDependent("connectionLimit", SLOT_CONNECTIONLIMIT = i++) + + .build() + /* + * Add these slot initializers after what Addressed does. + */ + .compose(CatalogObjectImpl.Addressed.s_initializer)::apply; + NSLOTS = i; + } + + static class Att + { + static final Attribute ROLNAME; + static final Attribute ROLSUPER; + static final Attribute ROLINHERIT; + static final Attribute ROLCREATEROLE; + static final Attribute ROLCREATEDB; + static final Attribute ROLCANLOGIN; + static final Attribute ROLREPLICATION; + static final Attribute ROLBYPASSRLS; + static final Attribute ROLCONNLIMIT; + + static + { + Iterator itr = CLASSID.tupleDescriptor().project( + "rolname", + "rolsuper", + "rolinherit", + "rolcreaterole", + "rolcreatedb", + "rolcanlogin", + "rolreplication", + "rolbypassrls", + "rolconnlimit" + ).iterator(); + + ROLNAME = itr.next(); + ROLSUPER = itr.next(); + ROLINHERIT = itr.next(); + ROLCREATEROLE = itr.next(); + ROLCREATEDB = itr.next(); + ROLCANLOGIN = itr.next(); + ROLREPLICATION = itr.next(); + ROLBYPASSRLS = itr.next(); + ROLCONNLIMIT = itr.next(); + + assert ! itr.hasNext() : "attribute initialization miscount"; + } + } + + /* computation methods */ + + private static List memberOf(RegRoleImpl o) + { + throw notyet("CatCList support needed"); + } + + private static boolean superuser(RegRoleImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(Att.ROLSUPER, BOOLEAN_INSTANCE); + } + + private static boolean inherit(RegRoleImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(Att.ROLINHERIT, BOOLEAN_INSTANCE); + } + + private static boolean createRole(RegRoleImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(Att.ROLCREATEROLE, BOOLEAN_INSTANCE); + } + + private static boolean createDB(RegRoleImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(Att.ROLCREATEDB, BOOLEAN_INSTANCE); + } + + private static boolean canLogIn(RegRoleImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(Att.ROLCANLOGIN, BOOLEAN_INSTANCE); + } + + private static boolean replication(RegRoleImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(Att.ROLREPLICATION, BOOLEAN_INSTANCE); + } + + private static boolean bypassRLS(RegRoleImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(Att.ROLBYPASSRLS, BOOLEAN_INSTANCE); + } + + private static int connectionLimit(RegRoleImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(Att.ROLCONNLIMIT, INT4_INSTANCE); + } + + /* API methods */ + + @Override + public List memberOf() + { + try + { + MethodHandle h = m_slots[SLOT_MEMBEROF]; + return (List)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public boolean superuser() + { + try + { + MethodHandle h = m_slots[SLOT_SUPERUSER]; + return (boolean)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public boolean inherit() + { + try + { + MethodHandle h = m_slots[SLOT_INHERIT]; + return (boolean)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public boolean createRole() + { + try + { + MethodHandle h = m_slots[SLOT_CREATEROLE]; + return (boolean)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public boolean createDB() + { + try + { + MethodHandle h = m_slots[SLOT_CREATEDB]; + return (boolean)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public boolean canLogIn() + { + try + { + MethodHandle h = m_slots[SLOT_CANLOGIN]; + return (boolean)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public boolean replication() + { + try + { + MethodHandle h = m_slots[SLOT_REPLICATION]; + return (boolean)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public boolean bypassRLS() + { + try + { + MethodHandle h = m_slots[SLOT_BYPASSRLS]; + return (boolean)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public int connectionLimit() + { + try + { + MethodHandle h = m_slots[SLOT_CONNECTIONLIMIT]; + return (int)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + /* Implementation of RegRole.Grantee */ + + /* + * As it turns out, PostgreSQL doesn't use a notion like Identifier.Pseudo + * for the name of the public grantee. It uses the ordinary, folding name + * "public" and reserves it, forbidding that any actual role have any name + * that matches it according to the usual folding rules. So, construct that + * name here. + */ + private static final Simple s_public_name = Simple.fromCatalog("public"); + + @Override + public Simple nameAsGrantee() + { + return isPublic() ? s_public_name : name(); + } +} diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/RegTypeImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/RegTypeImpl.java new file mode 100644 index 000000000..19b2c3da2 --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/pg/RegTypeImpl.java @@ -0,0 +1,1363 @@ +/* + * Copyright (c) 2022-2023 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.pg; + +import java.lang.invoke.MethodHandle; +import static java.lang.invoke.MethodHandles.lookup; +import java.lang.invoke.SwitchPoint; + +import java.nio.ByteBuffer; +import static java.nio.ByteOrder.nativeOrder; + +import java.sql.SQLType; +import java.sql.SQLException; +import java.sql.SQLXML; + +import java.util.Iterator; +import java.util.List; + +import java.util.function.UnaryOperator; + +import org.postgresql.pljava.TargetList.Projection; + +import static org.postgresql.pljava.internal.SwitchPointCache.doNotCache; +import org.postgresql.pljava.internal.SwitchPointCache.Builder; + +import org.postgresql.pljava.model.*; + +import org.postgresql.pljava.pg.CatalogObjectImpl.*; +import static org.postgresql.pljava.pg.ModelConstants.TYPEOID; // syscache +import static org.postgresql.pljava.pg.ModelConstants.alignmentFromCatalog; +import static org.postgresql.pljava.pg.ModelConstants.storageFromCatalog; + +import org.postgresql.pljava.pg.adt.GrantAdapter; +import org.postgresql.pljava.pg.adt.NameAdapter; +import org.postgresql.pljava.pg.adt.OidAdapter; +import static org.postgresql.pljava.pg.adt.OidAdapter.REGCLASS_INSTANCE; +import static org.postgresql.pljava.pg.adt.OidAdapter.REGCOLLATION_INSTANCE; +import static org.postgresql.pljava.pg.adt.OidAdapter.REGNAMESPACE_INSTANCE; +import static org.postgresql.pljava.pg.adt.OidAdapter.REGPROCEDURE_INSTANCE; +import static org.postgresql.pljava.pg.adt.OidAdapter.REGROLE_INSTANCE; +import static org.postgresql.pljava.pg.adt.OidAdapter.REGTYPE_INSTANCE; +import org.postgresql.pljava.pg.adt.TextAdapter; +import static org.postgresql.pljava.pg.adt.XMLAdapter.SYNTHETIC_INSTANCE; +import static org.postgresql.pljava.pg.adt.Primitives.*; + +import org.postgresql.pljava.annotation.BaseUDT.Alignment; +import org.postgresql.pljava.annotation.BaseUDT.Storage; + +import org.postgresql.pljava.sqlgen.Lexicals.Identifier.Qualified; +import org.postgresql.pljava.sqlgen.Lexicals.Identifier.Simple; +import org.postgresql.pljava.sqlgen.Lexicals.Identifier.Unqualified; + +import static org.postgresql.pljava.internal.UncheckedException.unchecked; + +/* + * Can get lots of information, including TupleDesc, domain constraints, etc., + * from the typcache. A typcache entry is immortal but bits of it can change. + * So it may be safe to keep a reference to the entry forever, but detect when + * bits have changed. See in particular tupDesc_identifier. + * + * Many of the attributes of pg_type are available in the typcache. But + * lookup_type_cache() does not have a _noerror version. If there is any doubt + * about the existence of a type to be looked up, one must either do a syscache + * lookup first anyway, or have a plan to catch an undefined_object error. + * Same if you happen to look up a type still in the "only a shell" stage. + * At that rate, may as well rely on the syscache for all the pg_type info. + */ + +abstract class RegTypeImpl extends Addressed +implements + Nonshared, Namespaced, Owned, + AccessControlled, RegType +{ + /** + * Per-instance switch point, to be invalidated selectively + * by a syscache callback. + *

    + * Only {@link NoModifier NoModifier} carries one; derived instances of + * {@link Modified Modified} or {@link Blessed Blessed} return that one. + */ + abstract SwitchPoint cacheSwitchPoint(); + + /* Implementation of Addressed */ + + @Override + public RegClass.Known classId() + { + return CLASSID; + } + + @Override + int cacheId() + { + return TYPEOID; + } + + /* Implementation of Named, Namespaced, Owned, AccessControlled */ + + private static Simple name(RegTypeImpl o) throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return + t.get(Att.TYPNAME, NameAdapter.SIMPLE_INSTANCE); + } + + private static RegNamespace namespace(RegTypeImpl o) throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return t.get(Att.TYPNAMESPACE, REGNAMESPACE_INSTANCE); + } + + private static RegRole owner(RegTypeImpl o) throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return t.get(Att.TYPOWNER, REGROLE_INSTANCE); + } + + private static List grants(RegTypeImpl o) + throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return t.get(Att.TYPACL, GrantAdapter.LIST_INSTANCE); + } + + /* Implementation of RegType */ + + /** + * Merely passes the supplied slots array to the superclass constructor; all + * initialization of the slots will be the responsibility of the subclass. + */ + RegTypeImpl(MethodHandle[] slots) + { + super(slots); + } + + /** + * Called from {@code Factory}'s {@code invalidateType} to set up + * the invalidation of this type's metadata. + *

    + * Adds this type's {@code SwitchPoint} to the caller's list so that, + * if more than one is to be invalidated, that can be done in bulk. Adds to + * postOps any operations the caller should conclude with + * after invalidating the {@code SwitchPoint}. + */ + void invalidate(List sps, List postOps) + { + /* + * We don't expect invalidations for any flavor except NoModifier, so + * this no-op version will be overridden there only. + */ + } + + /** + * Holder for the {@code RegClass} corresponding to {@code relation()}, + * only non-null during a call of {@code dualHandshake}. + */ + private RegClass m_dual = null; + + /** + * A lazily-populated synthetic tuple descriptor with a single element + * of this type. + */ + private TupleDescriptor m_singleton; + + /** + * Called by the corresponding {@code RegClass} instance if it has just + * looked us up. + *

    + * Because the {@code SwitchPointCache} recomputation methods always execute + * on the PG thread, plain access to an instance field does the trick here. + */ + void dualHandshake(RegClass dual) + { + try + { + m_dual = dual; + dual = relation(); + assert dual == m_dual : "RegClass/RegType handshake outcome"; + } + finally + { + m_dual = null; + } + } + + static final UnaryOperator s_initializer; + + static final int SLOT_TUPLEDESCRIPTOR; + static final int SLOT_LENGTH; + static final int SLOT_BYVALUE; + static final int SLOT_TYPE; + static final int SLOT_CATEGORY; + static final int SLOT_PREFERRED; + static final int SLOT_DEFINED; + static final int SLOT_DELIMITER; + static final int SLOT_RELATION; + static final int SLOT_ELEMENT; + static final int SLOT_ARRAY; + static final int SLOT_INPUT; + static final int SLOT_OUTPUT; + static final int SLOT_RECEIVE; + static final int SLOT_SEND; + static final int SLOT_MODIFIERINPUT; + static final int SLOT_MODIFIEROUTPUT; + static final int SLOT_ANALYZE; + static final int SLOT_SUBSCRIPT; + static final int SLOT_ALIGNMENT; + static final int SLOT_STORAGE; + static final int SLOT_NOTNULL; + static final int SLOT_BASETYPE; + static final int SLOT_DIMENSIONS; + static final int SLOT_COLLATION; + static final int SLOT_DEFAULTTEXT; + static final int NSLOTS; + + static + { + int i = CatalogObjectImpl.Addressed.NSLOTS; + s_initializer = + new Builder<>(RegTypeImpl.class) + .withLookup(lookup().in(RegTypeImpl.class)) + .withSwitchPoint(RegTypeImpl::cacheSwitchPoint) + .withSlots(o -> o.m_slots) + + .withCandidates( + CatalogObjectImpl.Addressed.class.getDeclaredMethods()) + .withReceiverType(CatalogObjectImpl.Addressed.class) + .withDependent("cacheTuple", SLOT_TUPLE) + + .withCandidates(RegTypeImpl.class.getDeclaredMethods()) + .withReceiverType(CatalogObjectImpl.Named.class) + .withReturnType(Unqualified.class) + .withDependent("name", SLOT_NAME) + .withReceiverType(CatalogObjectImpl.Namespaced.class) + .withReturnType(null) + .withDependent("namespace", SLOT_NAMESPACE) + .withReceiverType(CatalogObjectImpl.Owned.class) + .withDependent("owner", SLOT_OWNER) + .withReceiverType(CatalogObjectImpl.AccessControlled.class) + .withDependent("grants", SLOT_ACL) + + .withReceiverType(null) + .withSwitchPoint(o -> + { + RegClassImpl c = (RegClassImpl)o.relation(); + if ( c.isValid() ) + return c.m_cacheSwitchPoint; + return o.cacheSwitchPoint(); + }) + .withDependent( + "tupleDescriptorCataloged", SLOT_TUPLEDESCRIPTOR = i++) + + .withSwitchPoint(RegTypeImpl::cacheSwitchPoint) + .withDependent( "length", SLOT_LENGTH = i++) + .withDependent( "byValue", SLOT_BYVALUE = i++) + .withDependent( "type", SLOT_TYPE = i++) + .withDependent( "category", SLOT_CATEGORY = i++) + .withDependent( "preferred", SLOT_PREFERRED = i++) + .withDependent( "defined", SLOT_DEFINED = i++) + .withDependent( "delimiter", SLOT_DELIMITER = i++) + .withDependent( "relation", SLOT_RELATION = i++) + .withDependent( "element", SLOT_ELEMENT = i++) + .withDependent( "array", SLOT_ARRAY = i++) + .withDependent( "input", SLOT_INPUT = i++) + .withDependent( "output", SLOT_OUTPUT = i++) + .withDependent( "receive", SLOT_RECEIVE = i++) + .withDependent( "send", SLOT_SEND = i++) + .withDependent( "modifierInput", SLOT_MODIFIERINPUT = i++) + .withDependent( "modifierOutput", SLOT_MODIFIEROUTPUT = i++) + .withDependent( "analyze", SLOT_ANALYZE = i++) + .withDependent( "subscript", SLOT_SUBSCRIPT = i++) + .withDependent( "alignment", SLOT_ALIGNMENT = i++) + .withDependent( "storage", SLOT_STORAGE = i++) + .withDependent( "notNull", SLOT_NOTNULL = i++) + .withDependent( "baseType", SLOT_BASETYPE = i++) + .withDependent( "dimensions", SLOT_DIMENSIONS = i++) + .withDependent( "collation", SLOT_COLLATION = i++) + .withDependent( "defaultText", SLOT_DEFAULTTEXT = i++) + + .build(); + NSLOTS = i; + } + + static class Att + { + static final Projection TYPBASETYPE_TYPTYPMOD; + + static final Attribute TYPNAME; + static final Attribute TYPNAMESPACE; + static final Attribute TYPOWNER; + static final Attribute TYPACL; + static final Attribute TYPLEN; + static final Attribute TYPBYVAL; + static final Attribute TYPTYPE; + static final Attribute TYPCATEGORY; + static final Attribute TYPISPREFERRED; + static final Attribute TYPISDEFINED; + static final Attribute TYPDELIM; + static final Attribute TYPRELID; + static final Attribute TYPELEM; + static final Attribute TYPARRAY; + static final Attribute TYPINPUT; + static final Attribute TYPOUTPUT; + static final Attribute TYPRECEIVE; + static final Attribute TYPSEND; + static final Attribute TYPMODIN; + static final Attribute TYPMODOUT; + static final Attribute TYPANALYZE; + static final Attribute TYPALIGN; + static final Attribute TYPSTORAGE; + static final Attribute TYPNOTNULL; + static final Attribute TYPNDIMS; + static final Attribute TYPCOLLATION; + static final Attribute TYPDEFAULT; + static final Attribute TYPDEFAULTBIN; + static final Attribute TYPSUBSCRIPT; + + static + { + AttNames itr = attNames( + "typbasetype", // these two are wanted + "typtypmod", // together, first, below + "typname", + "typnamespace", + "typowner", + "typacl", + "typlen", + "typbyval", + "typtype", + "typcategory", + "typispreferred", + "typisdefined", + "typdelim", + "typrelid", + "typelem", + "typarray", + "typinput", + "typoutput", + "typreceive", + "typsend", + "typmodin", + "typmodout", + "typanalyze", + "typalign", + "typstorage", + "typnotnull", + "typndims", + "typcollation", + "typdefault", + "typdefaultbin" + ).alsoIf(PG_VERSION_NUM >= 140000, + "typsubscript" + ).project(CLASSID.tupleDescriptor()); + + TYPBASETYPE_TYPTYPMOD = itr.project(itr.next(), itr.next()); + + TYPNAME = itr.next(); + TYPNAMESPACE = itr.next(); + TYPOWNER = itr.next(); + TYPACL = itr.next(); + TYPLEN = itr.next(); + TYPBYVAL = itr.next(); + TYPTYPE = itr.next(); + TYPCATEGORY = itr.next(); + TYPISPREFERRED = itr.next(); + TYPISDEFINED = itr.next(); + TYPDELIM = itr.next(); + TYPRELID = itr.next(); + TYPELEM = itr.next(); + TYPARRAY = itr.next(); + TYPINPUT = itr.next(); + TYPOUTPUT = itr.next(); + TYPRECEIVE = itr.next(); + TYPSEND = itr.next(); + TYPMODIN = itr.next(); + TYPMODOUT = itr.next(); + TYPANALYZE = itr.next(); + TYPALIGN = itr.next(); + TYPSTORAGE = itr.next(); + TYPNOTNULL = itr.next(); + TYPNDIMS = itr.next(); + TYPCOLLATION = itr.next(); + TYPDEFAULT = itr.next(); + TYPDEFAULTBIN = itr.next(); + TYPSUBSCRIPT = itr.next(); + + assert ! itr.hasNext() : "attribute initialization miscount"; + } + } + + /* computation methods */ + + /** + * Obtain the tuple descriptor for an ordinary cataloged composite type. + *

    + * Every such type has a corresponding {@link RegClass RegClass}, which has + * the {@code SwitchPoint} that will govern the descriptor's invalidation, + * and a one-element array in which the descriptor should be stored. This + * method returns the array. + */ + private static TupleDescriptor.Interned[] + tupleDescriptorCataloged(RegTypeImpl o) + { + RegClassImpl c = (RegClassImpl)o.relation(); + + /* + * If this is not a composite type, c won't be valid, and our API + * contract is to return null (which means, here, return {null}). + */ + if ( ! c.isValid() ) + return new TupleDescriptor.Interned[] { null }; + + TupleDescriptor.Interned[] r = c.m_tupDescHolder; + + /* + * If c is RegClass.CLASSID itself, it has the descriptor by now + * (bootstrapped at the latest during the above relation() call, + * if it wasn't there already). + */ + if ( RegClass.CLASSID == c ) + { + assert null != r && null != r[0] : + "RegClass TupleDescriptor bootstrap outcome"; + return r; + } + + assert null == r : "RegClass has tuple descriptor when RegType doesn't"; + + /* + * Otherwise, do the work here, and store the descriptor in r. + * Can pass -1 for the modifier; Blessed types do not use this method. + */ + + ByteBuffer b = _lookupRowtypeTupdesc(o.oid(), -1); + assert null != b : "cataloged composite type tupdesc lookup"; + b.order(nativeOrder()); + r = new TupleDescriptor.Interned[]{ new TupleDescImpl.Cataloged(b, c) }; + return c.m_tupDescHolder = r; + } + + private static TupleDescriptor.Interned[] tupleDescriptorBlessed(Blessed o) + { + TupleDescriptor.Interned[] r = new TupleDescriptor.Interned[1]; + ByteBuffer b = _lookupRowtypeTupdesc(o.oid(), o.modifier()); + + /* + * If there is no registered tuple descriptor for this typmod, return an + * empty value to the current caller, but do not cache it; a later call + * could find one has been registered. + */ + if ( null == b ) + { + doNotCache(); + return r; + } + + b.order(nativeOrder()); + r[0] = new TupleDescImpl.Blessed(b, o); + return o.m_tupDescHolder = r; + } + + private static short length(RegTypeImpl o) throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return t.get(Att.TYPLEN, INT2_INSTANCE); + } + + private static boolean byValue(RegTypeImpl o) throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return t.get(Att.TYPBYVAL, BOOLEAN_INSTANCE); + } + + private static Type type(RegTypeImpl o) throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return typeFromCatalog( + t.get(Att.TYPTYPE, INT1_INSTANCE)); + } + + private static char category(RegTypeImpl o) throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return (char) + (0xff & t.get(Att.TYPCATEGORY, INT1_INSTANCE)); + } + + private static boolean preferred(RegTypeImpl o) throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return t.get(Att.TYPISPREFERRED, BOOLEAN_INSTANCE); + } + + private static boolean defined(RegTypeImpl o) throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return t.get(Att.TYPISDEFINED, BOOLEAN_INSTANCE); + } + + private static byte delimiter(RegTypeImpl o) throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return t.get(Att.TYPDELIM, INT1_INSTANCE); + } + + private static RegClass relation(RegTypeImpl o) throws SQLException + { + /* + * If this is a handshake occurring when the corresponding RegClass + * has just looked *us* up, we are done. + */ + if ( null != o.m_dual ) + return o.m_dual; + + /* + * Otherwise, look up the corresponding RegClass, and do the same + * handshake in reverse. Either way, the connection is set up + * bidirectionally with one cache lookup starting from either. That + * can avoid extra work in operations (like TupleDescriptor caching) + * that may touch both objects, without complicating their code. + */ + TupleTableSlot t = o.cacheTuple(); + RegClass c = t.get(Att.TYPRELID, REGCLASS_INSTANCE); + + ((RegClassImpl)c).dualHandshake(o); + return c; + } + + private static RegType element(RegTypeImpl o) throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return t.get(Att.TYPELEM, REGTYPE_INSTANCE); + } + + private static RegType array(RegTypeImpl o) throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return t.get(Att.TYPARRAY, REGTYPE_INSTANCE); + } + + private static RegProcedure input(RegTypeImpl o) + throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + @SuppressWarnings("unchecked") // XXX add memo magic here + RegProcedure p = (RegProcedure) + t.get(Att.TYPINPUT, REGPROCEDURE_INSTANCE); + return p; + } + + private static RegProcedure output(RegTypeImpl o) + throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + @SuppressWarnings("unchecked") // XXX add memo magic here + RegProcedure p = (RegProcedure) + t.get(Att.TYPOUTPUT, REGPROCEDURE_INSTANCE); + return p; + } + + private static RegProcedure receive(RegTypeImpl o) + throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + @SuppressWarnings("unchecked") // XXX add memo magic here + RegProcedure p = (RegProcedure) + t.get(Att.TYPRECEIVE, REGPROCEDURE_INSTANCE); + return p; + } + + private static RegProcedure send(RegTypeImpl o) + throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + @SuppressWarnings("unchecked") // XXX add memo magic here + RegProcedure p = (RegProcedure) + t.get(Att.TYPSEND, REGPROCEDURE_INSTANCE); + return p; + } + + private static RegProcedure modifierInput(RegTypeImpl o) + throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + @SuppressWarnings("unchecked") // XXX add memo magic here + RegProcedure p = (RegProcedure) + t.get(Att.TYPMODIN, REGPROCEDURE_INSTANCE); + return p; + } + + private static RegProcedure modifierOutput( + RegTypeImpl o) + throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + @SuppressWarnings("unchecked") // XXX add memo magic here + RegProcedure p = (RegProcedure) + t.get(Att.TYPMODOUT, REGPROCEDURE_INSTANCE); + return p; + } + + private static RegProcedure analyze(RegTypeImpl o) + throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + @SuppressWarnings("unchecked") // XXX add memo magic here + RegProcedure p = (RegProcedure) + t.get(Att.TYPANALYZE, REGPROCEDURE_INSTANCE); + return p; + } + + private static RegProcedure subscript(RegTypeImpl o) + throws SQLException + { + RegProcedure p; + + if ( null == Att.TYPSUBSCRIPT ) // missing in this PG version + p = of(RegProcedure.CLASSID, InvalidOid); + else + { + TupleTableSlot t = o.cacheTuple(); + p = t.get(Att.TYPSUBSCRIPT, REGPROCEDURE_INSTANCE); + } + + @SuppressWarnings("unchecked") // XXX add memo magic here + RegProcedure narrowed = (RegProcedure)p; + + return narrowed; + } + + private static Alignment alignment(RegTypeImpl o) throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return alignmentFromCatalog( + t.get(Att.TYPALIGN, INT1_INSTANCE)); + } + + private static Storage storage(RegTypeImpl o) throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return storageFromCatalog( + t.get(Att.TYPSTORAGE, INT1_INSTANCE)); + } + + private static boolean notNull(RegTypeImpl o) throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return t.get(Att.TYPNOTNULL, BOOLEAN_INSTANCE); + } + + private static RegType baseType(RegTypeImpl o) throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return Att.TYPBASETYPE_TYPTYPMOD + .applyOver(t, c -> + c.apply(OidAdapter.INT4_INSTANCE, INT4_INSTANCE, + ( oid, mod ) -> + CatalogObjectImpl.Factory.formMaybeModifiedType(oid, mod))); + } + + private static int dimensions(RegTypeImpl o) throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return t.get(Att.TYPNDIMS, INT4_INSTANCE); + } + + private static RegCollation collation(RegTypeImpl o) throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return t.get(Att.TYPCOLLATION, REGCOLLATION_INSTANCE); + } + + private static String defaultText(RegTypeImpl o) throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return t.get(Att.TYPDEFAULT, TextAdapter.INSTANCE); + } + + /* API methods */ + + @Override + public TupleDescriptor.Interned tupleDescriptor() + { + try + { + MethodHandle h = m_slots[SLOT_TUPLEDESCRIPTOR]; + return ((TupleDescriptor.Interned[])h.invokeExact(this, h))[0]; + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public short length() + { + try + { + MethodHandle h = m_slots[SLOT_LENGTH]; + return (short)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + // also available in the typcache, FWIW + } + + @Override + public boolean byValue() + { + try + { + MethodHandle h = m_slots[SLOT_BYVALUE]; + return (boolean)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + // also available in the typcache, FWIW + } + + @Override + public Type type() + { + try + { + MethodHandle h = m_slots[SLOT_TYPE]; + return (Type)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + // also available in the typcache, FWIW + } + + @Override + public char category() + { + try + { + MethodHandle h = m_slots[SLOT_CATEGORY]; + return (char)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public boolean preferred() + { + try + { + MethodHandle h = m_slots[SLOT_PREFERRED]; + return (boolean)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public boolean defined() + { + try + { + MethodHandle h = m_slots[SLOT_DEFINED]; + return (boolean)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public byte delimiter() + { + try + { + MethodHandle h = m_slots[SLOT_DELIMITER]; + return (byte)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public RegClass relation() + { + try + { + MethodHandle h = m_slots[SLOT_RELATION]; + return (RegClass)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + // also available in the typcache, FWIW + } + + @Override + public RegType element() + { + try + { + MethodHandle h = m_slots[SLOT_ELEMENT]; + return (RegType)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + // also available in the typcache, FWIW + } + + @Override + public RegType array() + { + try + { + MethodHandle h = m_slots[SLOT_ARRAY]; + return (RegType)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public RegProcedure input() + { + try + { + MethodHandle h = m_slots[SLOT_INPUT]; + return (RegProcedure)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public RegProcedure output() + { + try + { + MethodHandle h = m_slots[SLOT_OUTPUT]; + return (RegProcedure)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public RegProcedure receive() + { + try + { + MethodHandle h = m_slots[SLOT_RECEIVE]; + return (RegProcedure)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public RegProcedure send() + { + try + { + MethodHandle h = m_slots[SLOT_SEND]; + return (RegProcedure)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public RegProcedure modifierInput() + { + try + { + MethodHandle h = m_slots[SLOT_MODIFIERINPUT]; + return (RegProcedure)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public RegProcedure modifierOutput() + { + try + { + MethodHandle h = m_slots[SLOT_MODIFIEROUTPUT]; + return (RegProcedure)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public RegProcedure analyze() + { + try + { + MethodHandle h = m_slots[SLOT_ANALYZE]; + return (RegProcedure)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public RegProcedure subscript() + { + try + { + MethodHandle h = m_slots[SLOT_SUBSCRIPT]; + return (RegProcedure)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + // also available in the typcache, FWIW + } + + @Override + public Alignment alignment() + { + try + { + MethodHandle h = m_slots[SLOT_ALIGNMENT]; + return (Alignment)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + // also available in the typcache, FWIW + } + + @Override + public Storage storage() + { + try + { + MethodHandle h = m_slots[SLOT_STORAGE]; + return (Storage)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + // also available in the typcache, FWIW + } + + @Override + public boolean notNull() + { + try + { + MethodHandle h = m_slots[SLOT_NOTNULL]; + return (boolean)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public RegType baseType() + { + try + { + MethodHandle h = m_slots[SLOT_BASETYPE]; + return (RegType)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public int dimensions() + { + try + { + MethodHandle h = m_slots[SLOT_DIMENSIONS]; + return (int)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public RegCollation collation() + { + try + { + MethodHandle h = m_slots[SLOT_COLLATION]; + return (RegCollation)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + // also available in the typcache, FWIW + } + + @Override + public SQLXML defaultBin() + { + /* + * Because of the JDBC rules that an SQLXML instance lasts no longer + * than one transaction and can only be read once, it is not a good + * candidate for caching. We will just fetch a new one from the cached + * tuple as needed. + */ + TupleTableSlot s = cacheTuple(); + return s.get(Att.TYPDEFAULTBIN, SYNTHETIC_INSTANCE); + } + + @Override + public String defaultText() + { + try + { + MethodHandle h = m_slots[SLOT_DEFAULTTEXT]; + return (String)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + /** + * Return the expected zero value for {@code subId}. + *

    + * For keying the {@code CacheMap}, we sneak type modifiers in there + * (PG types do not otherwise use {@code subId}), but that's an + * implementation detail that could be done a different way if upstream + * ever decided to have subIds for types, and having it show in the address + * triple of a modified type could be surprising to an old PostgreSQL hand. + */ + @Override + public int subId() + { + return 0; + } + + /** + * Return the type modifier. + *

    + * In this implementation, where we snuck it in as the third component + * of the cache key, sneak it back out. + */ + @Override + public int modifier() + { + int m = super.subId(); + if ( -1 == m ) + return 0; + return m; + } + + /** + * Return a synthetic tuple descriptor with a single element of this type. + */ + public TupleDescriptor singletonTupleDescriptor() + { + TupleDescriptor td = m_singleton; + if ( null != td ) + return td; + /* + * In case of a race, the synthetic tuple descriptors will be + * equivalent anyway. + */ + return m_singleton = new TupleDescImpl.OfType(this); + } + + /** + * Represents a type that has been mentioned without an accompanying type + * modifier (or with the 'unspecified' value -1 for its type modifier). + */ + static class NoModifier extends RegTypeImpl + { + private SwitchPoint m_sp; + + @Override + SwitchPoint cacheSwitchPoint() + { + return m_sp; + } + + NoModifier() + { + super(s_initializer.apply(new MethodHandle[NSLOTS])); + m_sp = new SwitchPoint(); + } + + @Override + void invalidate(List sps, List postOps) + { + sps.add(m_sp); + m_sp = new SwitchPoint(); + } + + @Override + public int modifier() + { + return -1; + } + + @Override + public RegType modifier(int typmod) + { + if ( -1 == typmod ) + return this; + return + CatalogObjectImpl.Factory.formMaybeModifiedType(oid(), typmod); + } + + @Override + public RegType withoutModifier() + { + return this; + } + } + + /** + * Represents a type that is not {@code RECORD} and has a type modifier that + * is not the unspecified value. + *

    + * When the {@code RECORD} type appears in PostgreSQL with a type modifier, + * that is a special case; see {@link Blessed Blessed}. + */ + static class Modified extends RegTypeImpl + { + private final NoModifier m_base; + + @Override + SwitchPoint cacheSwitchPoint() + { + return m_base.m_sp; + } + + Modified(NoModifier base) + { + super(base.m_slots); + m_base = base; // must keep it live, not only share its slots + } + + @Override + public RegType modifier(int typmod) + { + if ( modifier() == typmod ) + return this; + return m_base.modifier(typmod); + } + + @Override + public RegType withoutModifier() + { + return m_base; + } + + /** + * Whether a just-mentioned modified type "exists" depends on whether + * its unmodified type exists and has a modifier input function. + *

    + * No attempt is made here to verify that the modifier value is one that + * the modifier input/output functions would produce or accept. + */ + @Override + public boolean exists() + { + return m_base.exists() && modifierInput().isValid(); + } + + @Override + public String toString() + { + String prefix = super.toString(); + return prefix + "(" + modifier() + ")"; + } + } + + /** + * Represents the "row type" of a {@link TupleDescriptor TupleDescriptor} + * that has been programmatically constructed and interned ("blessed"). + *

    + * Such a type is represented in PostgreSQL as the type {@code RECORD} + * with a type modifier assigned uniquely for the life of the backend. + */ + static class Blessed extends RegTypeImpl + { + /** + * Associated tuple descriptor, redundantly kept accessible here as well + * as opaquely bound into a {@code SwitchPointCache} method handle. + *

    + * A {@code Blessed} descriptor has no associated {@code RegClass}, so + * a slot for the descriptor is provided here. No invalidation events + * are expected for a blessed type, but the one-element array form here + * matches that used in {@code RegClass} for cataloged descriptors, to + * avoid multiple cases in the code. Only accessed from + * {@code SwitchPointCache} computation methods and + * {@code TupleDescImpl} factory methods, all of which execute on the PG + * thread; no synchronization fuss needed. + *

    + * When null, no computation method has run, and the state is not known. + * Otherwise, the single element is the result to be returned by + * the {@code tupleDescriptor()} API method. + */ + TupleDescriptor.Interned[] m_tupDescHolder; + private final MethodHandle[] m_moreSlots; + private static final UnaryOperator s_initializer; + private static final int SLOT_TDBLESSED; + private static final int NSLOTS; + + static + { + int i = 0; + s_initializer = + new Builder<>(Blessed.class) + .withLookup(lookup().in(RegTypeImpl.class)) + .withSwitchPoint(Blessed::cacheSwitchPoint) + .withSlots(o -> o.m_moreSlots) + .withCandidates(RegTypeImpl.class.getDeclaredMethods()) + .withDependent("tupleDescriptorBlessed", SLOT_TDBLESSED = i++) + .build(); + NSLOTS = i; + } + + @Override + SwitchPoint cacheSwitchPoint() + { + return ((NoModifier)RECORD).m_sp; + } + + Blessed() + { + super(((RegTypeImpl)RECORD).m_slots); + // RECORD is static final, no other effort needed to keep it live + m_moreSlots = s_initializer.apply(new MethodHandle[NSLOTS]); + } + + /** + * The tuple descriptor registered in the type cache for this 'blessed' + * type, or null if none. + *

    + * A null value is not sticky; it would be possible to 'mention' a + * blessed type with a not-yet-used typmod, which could then later exist + * after a tuple descriptor has been interned. (Such usage would be odd, + * though; typically one will obtain a blessed instance from an existing + * tuple descriptor.) + */ + @Override + public TupleDescriptor.Interned tupleDescriptor() + { + try + { + MethodHandle h = m_moreSlots[SLOT_TDBLESSED]; + return ((TupleDescriptor.Interned[])h.invokeExact(this, h))[0]; + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public RegType modifier(int typmod) + { + throw new UnsupportedOperationException( + "may not alter the type modifier of an interned row type"); + } + + @Override + public RegType withoutModifier() + { + return RECORD; + } + + /** + * Whether a just-mentioned blessed type "exists" depends on whether + * there is a tuple descriptor registered for it in the type cache. + *

    + * A false value is not sticky; it would be possible to 'mention' a + * blessed type with a not-yet-used typmod, which could then later exist + * after a tuple descriptor has been interned. (Such usage would be odd, + * though; typically one will obtain a blessed instance from an existing + * tuple descriptor.) + */ + @Override + public boolean exists() + { + return null != tupleDescriptor(); + } + + @Override + public String toString() + { + String prefix = super.toString(); + return prefix + "[" + modifier() + "]"; + } + } + + private static Type typeFromCatalog(byte b) + { + switch ( b ) + { + case (byte)'b': return Type.BASE; + case (byte)'c': return Type.COMPOSITE; + case (byte)'d': return Type.DOMAIN; + case (byte)'e': return Type.ENUM; + case (byte)'m': return Type.MULTIRANGE; + case (byte)'p': return Type.PSEUDO; + case (byte)'r': return Type.RANGE; + } + throw unchecked(new SQLException( + "unrecognized Type type '" + (char)b + "' in catalog", "XX000")); + } +} diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/ResourceOwnerImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/ResourceOwnerImpl.java new file mode 100644 index 000000000..bdbbd2ad7 --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/pg/ResourceOwnerImpl.java @@ -0,0 +1,213 @@ +/* + * Copyright (c) 2022-2023 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.pg; + +import java.nio.ByteBuffer; +import static java.nio.ByteOrder.nativeOrder; + +import static org.postgresql.pljava.internal.Backend.doInPG; +import static org.postgresql.pljava.internal.Backend.threadMayEnterPG; + +import org.postgresql.pljava.internal.CacheMap; +import org.postgresql.pljava.internal.DualState; +import static org.postgresql.pljava.internal.DualState.m; +import org.postgresql.pljava.internal.LifespanImpl; + +import org.postgresql.pljava.model.ResourceOwner; + +import static org.postgresql.pljava.pg.DatumUtils.asReadOnlyNativeOrder; +import static org.postgresql.pljava.pg.DatumUtils.fetchPointer; +import static org.postgresql.pljava.pg.DatumUtils.storePointer; + +import static org.postgresql.pljava.pg.ModelConstants.SIZEOF_DATUM; + +/** + * A PostgreSQL {@code ResourceOwner}, one of the things that can serve as + * a PL/Java {@code Lifespan}. + *

    + * The designer of this PostgreSQL object believed strongly in encapsulation, + * so very strongly that there is not any C header exposing its structure, + * and any operations to be exposed here will have to be calls through JNI. + * While a {@code ResourceOwner} does have a name (which will appear in log + * messages involving it), there's not even an exposed API to retrieve that. + * So this object will be not much more than a stub, known by its address + * and capable of serving as a PL/Java lifespan. + */ +public class ResourceOwnerImpl extends LifespanImpl +implements ResourceOwner, LifespanImpl.Addressed +{ + static final ByteBuffer[] s_knownOwners; + + static final CacheMap s_map = + CacheMap.newThreadConfined(() -> ByteBuffer.allocate(SIZEOF_DATUM)); + + static + { + ByteBuffer[] bs = EarlyNatives._window(ByteBuffer.class); + /* + * The first one windows CurrentResourceOwner. Set the correct byte + * order but do not make it read-only; operations may be provided + * for setting it. + */ + bs[0] = bs[0].order(nativeOrder()); + /* + * The rest are made native-ordered and read-only. + */ + for ( int i = 1; i < bs.length; ++ i ) + if ( null != bs[i] ) // older PG versions may not have every owner + bs[i] = asReadOnlyNativeOrder(bs[i]); + s_knownOwners = bs; + } + + static ResourceOwner known(int which) + { + ByteBuffer global = s_knownOwners[which]; + if ( null == global ) // older PG versions may not have every owner + return null; + return doInPG(() -> + { + long rso = fetchPointer(global, 0); + if ( 0 == rso ) + return null; + + return fromAddress(rso); + }); + } + + public static ResourceOwner fromAddress(long address) + { + assert threadMayEnterPG() : m("ResourceOwner thread"); + + /* + * Cache strongly; see LifespanImpl javadoc. + */ + return s_map.stronglyCache( + b -> + { + if ( 4 == SIZEOF_DATUM ) + b.putInt((int)address); + else + b.putLong(address); + }, + b -> new ResourceOwnerImpl(b) + ); + } + + /** + * Specialized method intended, so far, only for + * {@code PgSavepoint}'s use. + *

    + * Only to be called on the PG thread. + */ + public static long getCurrentRaw() + { + assert threadMayEnterPG() : m("ResourceOwner thread"); + return fetchPointer(s_knownOwners[0], 0); + } + + /** + * Even more specialized method intended, so far, only for + * {@code PgSavepoint}'s use. + *

    + * Only to be called on the PG thread. + */ + public static void setCurrentRaw(long owner) + { + assert threadMayEnterPG() : m("ResourceOwner thread"); + storePointer(s_knownOwners[0], 0, owner); + } + + /* + * Called only from JNI. + */ + private static void callback(long nativePointer) + { + CacheMap.Entry e = s_map.find( + b -> + { + if ( 4 == SIZEOF_DATUM ) + b.putInt((int)nativePointer); + else + b.putLong(nativePointer); + } + ); + + if ( null == e ) + return; + + ResourceOwnerImpl r = e.get(); + if ( null == r ) + return; + + r.invalidate(); + e.remove(); + } + + /** + * The {@code ByteBuffer} keying this object. + *

    + * As described for {@code CatalogObjectImpl}, as we'd like to be able + * to retrieve the address, and that's what's in the ByteBuffer that is + * held as the key in the CacheMap anyway, just keep a reference to that + * here. We must treat it as read-only, even if it hasn't officially + * been made that way. + *

    + * The contents are needed only for non-routine operations like + * {@code toString}, where an extra {@code fetchPointer} doesn't + * break the bank. + */ + private final ByteBuffer m_key; + private boolean m_valid = true; + + private ResourceOwnerImpl(ByteBuffer key) + { + m_key = key; + } + + @Override // Addressed + public long address() + { + if ( m_valid ) + return fetchPointer(m_key, 0); + throw new IllegalStateException( + "address may not be taken of invalidated ResourceOwner"); + } + + @Override + public String toString() + { + return String.format("%s[%#x]", + super.toString(), fetchPointer(m_key, 0)); + } + + private void invalidate() + { + lifespanRelease(); + m_valid = false; + // nothing else to do here. + } + + private static class EarlyNatives + { + /** + * Returns an array of ByteBuffer, one covering each PostgreSQL + * known resource owner global, in the same order as the arbitrary + * indices defined in the API class CatalogObject.Factory, which are + * what will be passed to the known() method. + *

    + * Takes a {@code Class} argument, to save the native + * code a lookup. + */ + private static native ByteBuffer[] _window( + Class component); + } +} diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/TargetListImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/TargetListImpl.java new file mode 100644 index 000000000..e3b41469f --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/pg/TargetListImpl.java @@ -0,0 +1,1261 @@ +/* + * Copyright (c) 2023 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.pg; + +import org.postgresql.pljava.Adapter.AdapterException; +import org.postgresql.pljava.Adapter.As; +import org.postgresql.pljava.Adapter.AsBoolean; +import org.postgresql.pljava.Adapter.AsByte; +import org.postgresql.pljava.Adapter.AsChar; +import org.postgresql.pljava.Adapter.AsDouble; +import org.postgresql.pljava.Adapter.AsFloat; +import org.postgresql.pljava.Adapter.AsInt; +import org.postgresql.pljava.Adapter.AsLong; +import org.postgresql.pljava.Adapter.AsShort; +import org.postgresql.pljava.TargetList; + +import org.postgresql.pljava.model.Attribute; +import org.postgresql.pljava.model.TupleDescriptor; +import org.postgresql.pljava.model.TupleTableSlot; + +import org.postgresql.pljava.sqlgen.Lexicals.Identifier.Simple; + +import java.lang.ref.WeakReference; + +import java.sql.SQLException; + +import java.util.AbstractList; +import java.util.Arrays; +import static java.util.Arrays.copyOfRange; +import java.util.BitSet; +import java.util.Collection; +import java.util.IntSummaryStatistics; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; +import static java.util.Objects.checkFromToIndex; +import static java.util.Objects.requireNonNull; +import java.util.Spliterator; +import static java.util.Spliterator.IMMUTABLE; +import static java.util.Spliterator.NONNULL; +import static java.util.Spliterator.ORDERED; +import static java.util.Spliterator.SIZED; +import java.util.Spliterators; +import static java.util.Spliterators.spliteratorUnknownSize; + +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +/** + * Implementation of {@link TargetList TargetList}. + */ +class TargetListImpl extends AbstractList implements TargetList +{ + private static final Projected EMPTY = new Projected(null, new short[0]); + + private final TupleDescriptor m_tdesc; + private final short[] m_map; + + private TargetListImpl(TupleDescriptor tdesc, short[] map) + { + m_tdesc = tdesc; + m_map = map; // not cloned here; caller should ensure no aliasing + } + + @Override // List + public Attribute get(int index) + { + return m_tdesc.get(m_map[index]); + } + + @Override // List + public int size() + { + return m_map.length; + } + + @Override // TargetList + public TargetList subList(int fromIndex, int toIndex) + { + if ( 0 == fromIndex && m_map.length == toIndex ) + return this; + checkFromToIndex(fromIndex, toIndex, m_map.length); + if ( fromIndex == toIndex ) + return EMPTY; + return new TargetListImpl( + m_tdesc, copyOfRange(m_map, fromIndex, toIndex)); + } + + @Override // TargetList + public R applyOver( + Iterable tuples, Cursor.Function f) + throws X, SQLException + { + return TargetListImpl.applyOver(this, tuples, f); + } + + @Override // TargetList + public R applyOver( + TupleTableSlot tuple, Cursor.Function f) + throws X, SQLException + { + return TargetListImpl.applyOver(this, tuple, f); + } + + static class Projected extends TargetListImpl implements Projection + { + Projected(TupleDescriptor tdesc, short[] map) + { + super(tdesc, map); + } + + static Projection project(TupleDescriptor src, int... indices) + { + if ( requireNonNull(indices, "project() indices null").length == 0 ) + return EMPTY; + + int n = src.size(); + + IntSummaryStatistics s = + Arrays.stream(indices).distinct().summaryStatistics(); + + if ( s.getCount() < indices.length || indices.length > n + || 0 > s.getMin() || s.getMax() > n - 1 ) + throw new IllegalArgumentException(String.format( + "project() indices must be distinct, >= 0, and < %d: %s", + n, Arrays.toString(indices) + )); + + if ( ( indices.length == n ) + && Arrays.stream(indices).allMatch(i -> i == indices[i]) ) + return src; + + short[] map = new short [ indices.length ]; + for ( int i = 0 ; i < indices.length ; ++ i ) + map[i] = (short)indices[i]; + + return new Projected(src, map); + } + + static Projection sqlProject(TupleDescriptor src, int... indices) + { + if ( requireNonNull(indices, "sqlProject() indices null").length + == 0 ) + return EMPTY; + + int n = src.size(); + + IntSummaryStatistics s = + Arrays.stream(indices).distinct().summaryStatistics(); + + if ( s.getCount() < indices.length || indices.length > n + || 1 > s.getMin() || s.getMax() > n ) + throw new IllegalArgumentException(String.format( + "sqlProject() indices must be distinct, > 0, and <= %d: %s", + n, Arrays.toString(indices) + )); + + if ( ( indices.length == src.size() ) + && Arrays.stream(indices).allMatch(i -> i == indices[i-1]) ) + return src; + + short[] map = new short [ indices.length ]; + for ( int i = 0 ; i < indices.length ; ++ i ) + map[i] = (short)(indices[i] - 1); + + return new Projected(src, map); + } + + static Projection project(TupleDescriptor src, Simple... names) + { + if ( requireNonNull(names, "project() names null").length == 0 ) + return EMPTY; + + int n = src.size(); + + /* + * An exception could be thrown here if names.length > n, but that + * condition ensures the later exception for names left unmatched + * will have to be thrown, and as long as that's going to happen + * anyway, the extra work to see just what names didn't match + * produces a more helpful message. + */ + + BitSet pb = new BitSet(names.length); + pb.set(0, names.length); + + short[] map = new short [ names.length ]; + + for ( int i = 0 ; i < n ; ++ i ) + { + Attribute attr = src.get(i); + Simple name = attr.name(); + + for ( int j = pb.nextSetBit(0); 0 <= j; j = pb.nextSetBit(++j) ) + { + if ( ! name.equals(names[j]) ) + continue; + map[j] = (short)i; + pb.clear(j); + if ( pb.isEmpty() ) + return new Projected(src, map); + break; + } + } + + throw new IllegalArgumentException( + "project() left unmatched by name: " + Arrays.toString( + pb.stream().mapToObj(i->names[i]).toArray(Simple[]::new))); + } + + static Projection project(TupleDescriptor src, Attribute... attrs) + { + if ( requireNonNull(attrs, "project() attrs null").length == 0 ) + return EMPTY; + + int n = src.size(); + + if ( attrs.length > n ) + throw new IllegalArgumentException(String.format( + "project() more than %d attributes supplied", n)); + + BitSet pb = new BitSet(attrs.length); + pb.set(0, attrs.length); + + BitSet sb = new BitSet(src.size()); // to detect repetition + + short[] map = new short [ attrs.length ]; + + for ( int i = 0 ; i < attrs.length ; ++ i ) + { + Attribute attr = attrs[i]; + int idx = attr.subId() - 1; + if ( sb.get(idx) ) // repetition? + continue; + if ( ! foundIn(attr, src) ) + continue; + sb.set(idx); + map[i] = (short)idx; + pb.clear(i); + } + + if ( pb.isEmpty() ) + return new Projected(src, map); + + throw new IllegalArgumentException( + "project() extraneous attributes: " + Arrays.toString( + pb.stream() + .mapToObj(i->attrs[i]).toArray(Attribute[]::new))); + } + + static Projection subList( + TupleDescriptor src, int fromIndex, int toIndex) + { + int n = src.size(); + + if ( 0 == fromIndex && n == toIndex ) + return src; + checkFromToIndex(fromIndex, toIndex, n); + if ( fromIndex == toIndex ) + return EMPTY; + short[] map = new short [ toIndex - fromIndex ]; + for ( int i = 0; i < map.length ; ++ i ) + map[i] = (short)(i + fromIndex); + return new Projected(src, map); + } + + @Override // Projection + public Projection subList(int fromIndex, int toIndex) + { + TargetListImpl sup = (TargetListImpl)this; // m_tdesc/m-map private + + if ( 0 == fromIndex && sup.m_map.length == toIndex ) + return this; + checkFromToIndex(fromIndex, toIndex, sup.m_map.length); + if ( fromIndex == toIndex ) + return EMPTY; + return new Projected( + sup.m_tdesc, copyOfRange(sup.m_map, fromIndex, toIndex)); + } + + @Override // Projection + public Projection project(int... indices) + { + if ( requireNonNull(indices, "project() indices null").length == 0 ) + return EMPTY; + + TargetListImpl sup = (TargetListImpl)this; // m_tdesc/m-map private + + int n = sup.m_map.length; + + IntSummaryStatistics s = + Arrays.stream(indices).distinct().summaryStatistics(); + + if ( s.getCount() < indices.length || indices.length > n + || 0 > s.getMin() || s.getMax() > n - 1 ) + throw new IllegalArgumentException(String.format( + "project() indices must be distinct, >= 0, and < %d: %s", + n, Arrays.toString(indices) + )); + + if ( ( indices.length == n ) + && Arrays.stream(indices).allMatch(i -> i == indices[i]) ) + return this; + + short[] map = new short [ indices.length ]; + for ( int i = 0 ; i < indices.length ; ++ i ) + map[i] = sup.m_map[indices[i]]; + + return new Projected(sup.m_tdesc, map); + } + + @Override // Projection + public Projection sqlProject(int... indices) + { + if ( requireNonNull(indices, "sqlProject() indices null").length + == 0 ) + return EMPTY; + + TargetListImpl sup = (TargetListImpl)this; // m_tdesc/m-map private + + int n = sup.m_map.length; + + IntSummaryStatistics s = + Arrays.stream(indices).distinct().summaryStatistics(); + + if ( s.getCount() < indices.length || indices.length > n + || 1 > s.getMin() || s.getMax() > n ) + throw new IllegalArgumentException(String.format( + "sqlProject() indices must be distinct, > 0, and <= %d: %s", + n, Arrays.toString(indices) + )); + + if ( ( indices.length == n ) + && Arrays.stream(indices).allMatch(i -> i == indices[i-1]) ) + return this; + + short[] map = new short [ indices.length ]; + for ( int i = 0 ; i < indices.length ; ++ i ) + map[i] = sup.m_map[indices[i] - 1]; + + return new Projected(sup.m_tdesc, map); + } + + @Override // Projection + public Projection project(Simple... names) + { + if ( requireNonNull(names, "project() names null").length == 0 ) + return EMPTY; + + TargetListImpl sup = (TargetListImpl)this; // m_tdesc/m-map private + + int n = sup.m_map.length; + + /* + * An exception could be thrown here if names.length > n, but that + * condition ensures the later exception for names left unmatched + * will have to be thrown, and as long as that's going to happen + * anyway, the extra work to see just what names didn't match + * produces a more helpful message. + */ + + BitSet pb = new BitSet(names.length); + pb.set(0, names.length); + + short[] map = new short [ names.length ]; + + for ( int i = 0 ; i < n ; ++ i ) + { + short mapped = sup.m_map[i]; + Simple name = sup.m_tdesc.get(mapped).name(); + + for ( int j = pb.nextSetBit(0); 0 <= j; j = pb.nextSetBit(++j) ) + { + if ( ! name.equals(names[j]) ) + continue; + map[j] = mapped; + pb.clear(j); + if ( pb.isEmpty() ) + return new Projected(sup.m_tdesc, map); + break; + } + } + + throw new IllegalArgumentException( + "project() left unmatched by name: " + Arrays.toString( + pb.stream().mapToObj(i->names[i]).toArray(Simple[]::new))); + } + + @Override // Projection + public Projection project(Attribute... attrs) + { + if ( requireNonNull(attrs, "project() attrs null").length == 0 ) + return EMPTY; + + TargetListImpl sup = (TargetListImpl)this; // m_tdesc/m-map private + + int n = sup.m_map.length; + + if ( attrs.length > n ) + throw new IllegalArgumentException(String.format( + "project() more than %d attributes supplied", n)); + + BitSet pb = new BitSet(attrs.length); + pb.set(0, attrs.length); + + BitSet sb = new BitSet(sup.m_tdesc.size()); + for ( short i : sup.m_map ) + sb.set(i); + + short[] map = new short [ attrs.length ]; + + for ( int i = 0 ; i < attrs.length ; ++ i ) + { + Attribute attr = attrs[i]; + int idx = attr.subId() - 1; + if ( ! sb.get(idx) ) + continue; + if ( ! foundIn(attr, sup.m_tdesc) ) + continue; + map[i] = (short)idx; + pb.clear(i); + sb.clear(idx); + } + + if ( pb.isEmpty() ) + return new Projected(sup.m_tdesc, map); + + throw new IllegalArgumentException( + "project() extraneous attributes: " + Arrays.toString( + pb.stream() + .mapToObj(i->attrs[i]).toArray(Attribute[]::new))); + } + } + + private static boolean foundIn(Attribute a, TupleDescriptor td) + { + return ((AttributeImpl)a).foundIn(td); + } + + static R applyOver( + TargetList tl, Iterable tuples, Cursor.Function f) + throws X, SQLException + { + try + { + return f.apply(new CursorImpl(tl, tuples)); + } + catch ( AdapterException e ) + { + throw e.unwrap(SQLException.class); + } + } + + static R applyOver( + TargetList tl, TupleTableSlot tuple, Cursor.Function f) + throws X, SQLException + { + try + { + return f.apply(new CursorImpl(tl, tuple)); + } + catch ( AdapterException e ) + { + throw e.unwrap(SQLException.class); + } + } + + static class CursorImpl implements TargetList.Cursor, AutoCloseable + { + private final TargetList m_tlist; + private final int m_targets; + private Iterable m_slots; + private TupleTableSlot m_currentSlot; + private int m_currentTarget; + private int m_nestLevel; + private WeakReference m_activeIterator; + + CursorImpl(TargetList tlist, Iterable slots) + { + m_tlist = tlist; + m_targets = tlist.size(); + m_slots = requireNonNull(slots, "applyOver() tuples null"); + } + + CursorImpl(TargetList tlist, TupleTableSlot slot) + { + m_tlist = tlist; + m_targets = tlist.size(); + m_currentSlot = requireNonNull(slot, "applyOver() tuple null"); + } + + @Override // Iterable + public Iterator iterator() + { + if ( 0 < m_nestLevel ) + throw new IllegalStateException( + "Cursor.iterator() called within a curried CursorFunction"); + + /* + * Only one Iterator should be active at a time. There is nothing in + * Iterator's API to indicate when one is no longer active (its user + * might just stop iterating it), so just keep track of whether an + * earlier-created one is still around and, if so, sabotage it. + */ + WeakReference iRef = m_activeIterator; + if ( null != iRef ) + { + Itr i = iRef.get(); + if ( null != i ) + { + i.slot_iter = new Iterator() + { + @Override + public boolean hasNext() + { + throw new IllegalStateException( + "another iterator for this Cursor has been " + + "started"); + } + @Override + public TupleTableSlot next() + { + hasNext(); + return null; + } + }; + } + } + + if ( null == m_slots ) + { + m_slots = List.of(m_currentSlot); + m_currentSlot = null; + } + + Itr i = new Itr(); + m_activeIterator = new WeakReference<>(i); + return i; + } + + @Override // Cursor + public Stream stream() + { + Iterator itr = iterator(); + Spliterator spl; + int chr = IMMUTABLE | NONNULL | ORDERED; + long est = Long.MAX_VALUE; + + if ( m_slots instanceof Collection ) + { + est = ((Collection)m_slots).size(); + chr |= SIZED; + } + + spl = new TupleList.IteratorNonSpliterator<>(itr, est, chr); + + return StreamSupport.stream(spl, false); + } + + class Itr implements Iterator + { + private Iterator slot_iter = m_slots.iterator(); + + @Override + public boolean hasNext() + { + return slot_iter.hasNext(); + } + + @Override + public Cursor next() + { + m_currentSlot = slot_iter.next(); + m_currentTarget = 0; + return CursorImpl.this; + } + } + + @Override // Iterator + public boolean hasNext() + { + return m_currentTarget < m_targets; + } + + @Override // Iterator + public Attribute next() + { + if ( m_currentTarget < m_targets ) + return m_tlist.get(m_currentTarget++); + + throw new NoSuchElementException( + "fewer Attributes in TargetList than parameters to assign"); + } + + private CursorImpl nest() + { + ++ m_nestLevel; + return this; + } + + @Override // AutoCloseable + public void close() + { + if ( 0 == -- m_nestLevel ) + m_currentTarget = 0; + } + + @Override + public R apply( + L0 f) + throws X + { + try ( CursorImpl unnest = nest() ) + { + return f.apply(); + } + } + + @Override + public R apply( + As a0, + L1 f) + throws X + { + try ( CursorImpl unnest = nest() ) + { + A v0 = m_currentSlot.get(next(), a0); + return f.apply(v0); + } + } + + @Override + public R apply( + As a0, As a1, + L2 f) + throws X + { + try ( CursorImpl unnest = nest() ) + { + A v0 = m_currentSlot.get(next(), a0); + B v1 = m_currentSlot.get(next(), a1); + return f.apply(v0, v1); + } + } + + @Override + public R apply( + As a0, As a1, As a2, + L3 f) + throws X + { + try ( CursorImpl unnest = nest() ) + { + A v0 = m_currentSlot.get(next(), a0); + B v1 = m_currentSlot.get(next(), a1); + C v2 = m_currentSlot.get(next(), a2); + return f.apply(v0, v1, v2); + } + } + + @Override + public R apply( + As a0, As a1, As a2, As a3, + L4 f) + throws X + { + try ( CursorImpl unnest = nest() ) + { + A v0 = m_currentSlot.get(next(), a0); + B v1 = m_currentSlot.get(next(), a1); + C v2 = m_currentSlot.get(next(), a2); + D v3 = m_currentSlot.get(next(), a3); + return f.apply(v0, v1, v2, v3); + } + } + + @Override + public R apply( + As a0, As a1, As a2, As a3, + As a4, + L5 f) + throws X + { + try ( CursorImpl unnest = nest() ) + { + A v0 = m_currentSlot.get(next(), a0); + B v1 = m_currentSlot.get(next(), a1); + C v2 = m_currentSlot.get(next(), a2); + D v3 = m_currentSlot.get(next(), a3); + E v4 = m_currentSlot.get(next(), a4); + return f.apply(v0, v1, v2, v3, v4); + } + } + + @Override + public R apply( + As a0, As a1, As a2, As a3, + As a4, As a5, + L6 f) + throws X + { + try ( CursorImpl unnest = nest() ) + { + A v0 = m_currentSlot.get(next(), a0); + B v1 = m_currentSlot.get(next(), a1); + C v2 = m_currentSlot.get(next(), a2); + D v3 = m_currentSlot.get(next(), a3); + E v4 = m_currentSlot.get(next(), a4); + F v5 = m_currentSlot.get(next(), a5); + return f.apply(v0, v1, v2, v3, v4, v5); + } + } + + @Override + public R apply( + As a0, As a1, As a2, As a3, + As a4, As a5, As a6, + L7 f) + throws X + { + try ( CursorImpl unnest = nest() ) + { + A v0 = m_currentSlot.get(next(), a0); + B v1 = m_currentSlot.get(next(), a1); + C v2 = m_currentSlot.get(next(), a2); + D v3 = m_currentSlot.get(next(), a3); + E v4 = m_currentSlot.get(next(), a4); + F v5 = m_currentSlot.get(next(), a5); + G v6 = m_currentSlot.get(next(), a6); + return f.apply(v0, v1, v2, v3, v4, v5, v6); + } + } + + @Override + public R apply( + As a0, As a1, As a2, As a3, + As a4, As a5, As a6, As a7, + L8 f) + throws X + { + try ( CursorImpl unnest = nest() ) + { + A v0 = m_currentSlot.get(next(), a0); + B v1 = m_currentSlot.get(next(), a1); + C v2 = m_currentSlot.get(next(), a2); + D v3 = m_currentSlot.get(next(), a3); + E v4 = m_currentSlot.get(next(), a4); + F v5 = m_currentSlot.get(next(), a5); + G v6 = m_currentSlot.get(next(), a6); + H v7 = m_currentSlot.get(next(), a7); + return f.apply(v0, v1, v2, v3, v4, v5, v6, v7); + } + } + + @Override + public R apply( + As a0, As a1, As a2, As a3, + As a4, As a5, As a6, As a7, + As a8, As a9, As aa, As ab, + As ac, As ad, As ae, As af, + L16 f) + throws X + { + try ( CursorImpl unnest = nest() ) + { + A v0 = m_currentSlot.get(next(), a0); + B v1 = m_currentSlot.get(next(), a1); + C v2 = m_currentSlot.get(next(), a2); + D v3 = m_currentSlot.get(next(), a3); + E v4 = m_currentSlot.get(next(), a4); + F v5 = m_currentSlot.get(next(), a5); + G v6 = m_currentSlot.get(next(), a6); + H v7 = m_currentSlot.get(next(), a7); + I v8 = m_currentSlot.get(next(), a8); + J v9 = m_currentSlot.get(next(), a9); + K va = m_currentSlot.get(next(), aa); + L vb = m_currentSlot.get(next(), ab); + M vc = m_currentSlot.get(next(), ac); + N vd = m_currentSlot.get(next(), ad); + O ve = m_currentSlot.get(next(), ae); + P vf = m_currentSlot.get(next(), af); + return f.apply( + v0, v1, v2, v3, v4, v5, v6, v7, + v8, v9, va, vb, vc, vd, ve, vf); + } + } + + @Override + public R apply( + AsLong a0, + J1 f) + throws X + { + try ( CursorImpl unnest = nest() ) + { + long v0 = m_currentSlot.get(next(), a0); + return f.apply(v0); + } + } + + @Override + public R apply( + AsLong a0, AsLong a1, + J2 f) + throws X + { + try ( CursorImpl unnest = nest() ) + { + long v0 = m_currentSlot.get(next(), a0); + long v1 = m_currentSlot.get(next(), a1); + return f.apply(v0, v1); + } + } + + @Override + public R apply( + AsLong a0, AsLong a1, AsLong a2, + J3 f) + throws X + { + try ( CursorImpl unnest = nest() ) + { + long v0 = m_currentSlot.get(next(), a0); + long v1 = m_currentSlot.get(next(), a1); + long v2 = m_currentSlot.get(next(), a2); + return f.apply(v0, v1, v2); + } + } + + @Override + public R apply( + AsLong a0, AsLong a1, AsLong a2, AsLong a3, + J4 f) + throws X + { + try ( CursorImpl unnest = nest() ) + { + long v0 = m_currentSlot.get(next(), a0); + long v1 = m_currentSlot.get(next(), a1); + long v2 = m_currentSlot.get(next(), a2); + long v3 = m_currentSlot.get(next(), a3); + return f.apply(v0, v1, v2, v3); + } + } + + @Override + public R apply( + AsDouble a0, + D1 f) + throws X + { + try ( CursorImpl unnest = nest() ) + { + double v0 = m_currentSlot.get(next(), a0); + return f.apply(v0); + } + } + + @Override + public R apply( + AsDouble a0, AsDouble a1, + D2 f) + throws X + { + try ( CursorImpl unnest = nest() ) + { + double v0 = m_currentSlot.get(next(), a0); + double v1 = m_currentSlot.get(next(), a1); + return f.apply(v0, v1); + } + } + + @Override + public R apply( + AsDouble a0, AsDouble a1, AsDouble a2, + D3 f) + throws X + { + try ( CursorImpl unnest = nest() ) + { + double v0 = m_currentSlot.get(next(), a0); + double v1 = m_currentSlot.get(next(), a1); + double v2 = m_currentSlot.get(next(), a2); + return f.apply(v0, v1, v2); + } + } + + @Override + public R apply( + AsDouble a0, AsDouble a1, AsDouble a2, AsDouble a3, + D4 f) + throws X + { + try ( CursorImpl unnest = nest() ) + { + double v0 = m_currentSlot.get(next(), a0); + double v1 = m_currentSlot.get(next(), a1); + double v2 = m_currentSlot.get(next(), a2); + double v3 = m_currentSlot.get(next(), a3); + return f.apply(v0, v1, v2, v3); + } + } + + @Override + public R apply( + AsInt a0, + I1 f) + throws X + { + try ( CursorImpl unnest = nest() ) + { + int v0 = m_currentSlot.get(next(), a0); + return f.apply(v0); + } + } + + @Override + public R apply( + AsInt a0, AsInt a1, + I2 f) + throws X + { + try ( CursorImpl unnest = nest() ) + { + int v0 = m_currentSlot.get(next(), a0); + int v1 = m_currentSlot.get(next(), a1); + return f.apply(v0, v1); + } + } + + @Override + public R apply( + AsInt a0, AsInt a1, AsInt a2, + I3 f) + throws X + { + try ( CursorImpl unnest = nest() ) + { + int v0 = m_currentSlot.get(next(), a0); + int v1 = m_currentSlot.get(next(), a1); + int v2 = m_currentSlot.get(next(), a2); + return f.apply(v0, v1, v2); + } + } + + @Override + public R apply( + AsInt a0, AsInt a1, AsInt a2, AsInt a3, + I4 f) + throws X + { + try ( CursorImpl unnest = nest() ) + { + int v0 = m_currentSlot.get(next(), a0); + int v1 = m_currentSlot.get(next(), a1); + int v2 = m_currentSlot.get(next(), a2); + int v3 = m_currentSlot.get(next(), a3); + return f.apply(v0, v1, v2, v3); + } + } + + @Override + public R apply( + AsFloat a0, + F1 f) + throws X + { + try ( CursorImpl unnest = nest() ) + { + float v0 = m_currentSlot.get(next(), a0); + return f.apply(v0); + } + } + + @Override + public R apply( + AsFloat a0, AsFloat a1, + F2 f) + throws X + { + try ( CursorImpl unnest = nest() ) + { + float v0 = m_currentSlot.get(next(), a0); + float v1 = m_currentSlot.get(next(), a1); + return f.apply(v0, v1); + } + } + + @Override + public R apply( + AsFloat a0, AsFloat a1, AsFloat a2, + F3 f) + throws X + { + try ( CursorImpl unnest = nest() ) + { + float v0 = m_currentSlot.get(next(), a0); + float v1 = m_currentSlot.get(next(), a1); + float v2 = m_currentSlot.get(next(), a2); + return f.apply(v0, v1, v2); + } + } + + @Override + public R apply( + AsFloat a0, AsFloat a1, AsFloat a2, AsFloat a3, + F4 f) + throws X + { + try ( CursorImpl unnest = nest() ) + { + float v0 = m_currentSlot.get(next(), a0); + float v1 = m_currentSlot.get(next(), a1); + float v2 = m_currentSlot.get(next(), a2); + float v3 = m_currentSlot.get(next(), a3); + return f.apply(v0, v1, v2, v3); + } + } + + @Override + public R apply( + AsShort a0, + S1 f) + throws X + { + try ( CursorImpl unnest = nest() ) + { + short v0 = m_currentSlot.get(next(), a0); + return f.apply(v0); + } + } + + @Override + public R apply( + AsShort a0, AsShort a1, + S2 f) + throws X + { + try ( CursorImpl unnest = nest() ) + { + short v0 = m_currentSlot.get(next(), a0); + short v1 = m_currentSlot.get(next(), a1); + return f.apply(v0, v1); + } + } + + @Override + public R apply( + AsShort a0, AsShort a1, AsShort a2, + S3 f) + throws X + { + try ( CursorImpl unnest = nest() ) + { + short v0 = m_currentSlot.get(next(), a0); + short v1 = m_currentSlot.get(next(), a1); + short v2 = m_currentSlot.get(next(), a2); + return f.apply(v0, v1, v2); + } + } + + @Override + public R apply( + AsShort a0, AsShort a1, AsShort a2, AsShort a3, + S4 f) + throws X + { + try ( CursorImpl unnest = nest() ) + { + short v0 = m_currentSlot.get(next(), a0); + short v1 = m_currentSlot.get(next(), a1); + short v2 = m_currentSlot.get(next(), a2); + short v3 = m_currentSlot.get(next(), a3); + return f.apply(v0, v1, v2, v3); + } + } + + @Override + public R apply( + AsChar a0, + C1 f) + throws X + { + try ( CursorImpl unnest = nest() ) + { + char v0 = m_currentSlot.get(next(), a0); + return f.apply(v0); + } + } + + @Override + public R apply( + AsChar a0, AsChar a1, + C2 f) + throws X + { + try ( CursorImpl unnest = nest() ) + { + char v0 = m_currentSlot.get(next(), a0); + char v1 = m_currentSlot.get(next(), a1); + return f.apply(v0, v1); + } + } + + @Override + public R apply( + AsChar a0, AsChar a1, AsChar a2, + C3 f) + throws X + { + try ( CursorImpl unnest = nest() ) + { + char v0 = m_currentSlot.get(next(), a0); + char v1 = m_currentSlot.get(next(), a1); + char v2 = m_currentSlot.get(next(), a2); + return f.apply(v0, v1, v2); + } + } + + @Override + public R apply( + AsChar a0, AsChar a1, AsChar a2, AsChar a3, + C4 f) + throws X + { + try ( CursorImpl unnest = nest() ) + { + char v0 = m_currentSlot.get(next(), a0); + char v1 = m_currentSlot.get(next(), a1); + char v2 = m_currentSlot.get(next(), a2); + char v3 = m_currentSlot.get(next(), a3); + return f.apply(v0, v1, v2, v3); + } + } + + @Override + public R apply( + AsByte a0, + B1 f) + throws X + { + try ( CursorImpl unnest = nest() ) + { + byte v0 = m_currentSlot.get(next(), a0); + return f.apply(v0); + } + } + + @Override + public R apply( + AsByte a0, AsByte a1, + B2 f) + throws X + { + try ( CursorImpl unnest = nest() ) + { + byte v0 = m_currentSlot.get(next(), a0); + byte v1 = m_currentSlot.get(next(), a1); + return f.apply(v0, v1); + } + } + + @Override + public R apply( + AsByte a0, AsByte a1, AsByte a2, + B3 f) + throws X + { + try ( CursorImpl unnest = nest() ) + { + byte v0 = m_currentSlot.get(next(), a0); + byte v1 = m_currentSlot.get(next(), a1); + byte v2 = m_currentSlot.get(next(), a2); + return f.apply(v0, v1, v2); + } + } + + @Override + public R apply( + AsByte a0, AsByte a1, AsByte a2, AsByte a3, + B4 f) + throws X + { + try ( CursorImpl unnest = nest() ) + { + byte v0 = m_currentSlot.get(next(), a0); + byte v1 = m_currentSlot.get(next(), a1); + byte v2 = m_currentSlot.get(next(), a2); + byte v3 = m_currentSlot.get(next(), a3); + return f.apply(v0, v1, v2, v3); + } + } + + @Override + public R apply( + AsBoolean a0, + Z1 f) + throws X + { + try ( CursorImpl unnest = nest() ) + { + boolean v0 = m_currentSlot.get(next(), a0); + return f.apply(v0); + } + } + + @Override + public R apply( + AsBoolean a0, AsBoolean a1, + Z2 f) + throws X + { + try ( CursorImpl unnest = nest() ) + { + boolean v0 = m_currentSlot.get(next(), a0); + boolean v1 = m_currentSlot.get(next(), a1); + return f.apply(v0, v1); + } + } + + @Override + public R apply( + AsBoolean a0, AsBoolean a1, AsBoolean a2, + Z3 f) + throws X + { + try ( CursorImpl unnest = nest() ) + { + boolean v0 = m_currentSlot.get(next(), a0); + boolean v1 = m_currentSlot.get(next(), a1); + boolean v2 = m_currentSlot.get(next(), a2); + return f.apply(v0, v1, v2); + } + } + + @Override + public R apply( + AsBoolean a0, AsBoolean a1, AsBoolean a2, AsBoolean a3, + Z4 f) + throws X + { + try ( CursorImpl unnest = nest() ) + { + boolean v0 = m_currentSlot.get(next(), a0); + boolean v1 = m_currentSlot.get(next(), a1); + boolean v2 = m_currentSlot.get(next(), a2); + boolean v3 = m_currentSlot.get(next(), a3); + return f.apply(v0, v1, v2, v3); + } + } + } +} diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/TupleDescImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/TupleDescImpl.java new file mode 100644 index 000000000..3221bc70b --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/pg/TupleDescImpl.java @@ -0,0 +1,649 @@ +/* + * Copyright (c) 2022-2023 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.pg; + +import org.postgresql.pljava.model.*; +import static org.postgresql.pljava.model.RegType.RECORD; + +import org.postgresql.pljava.sqlgen.Lexicals.Identifier.Simple; + +import static org.postgresql.pljava.internal.Backend.doInPG; +import static org.postgresql.pljava.internal.Backend.threadMayEnterPG; +import org.postgresql.pljava.internal.DualState; + +import org.postgresql.pljava.pg.TargetListImpl.Projected; +import static org.postgresql.pljava.pg.CatalogObjectImpl.*; +import static org.postgresql.pljava.pg.ModelConstants.*; +import static org.postgresql.pljava.pg.DatumUtils.addressOf; +import static org.postgresql.pljava.pg.DatumUtils.asReadOnlyNativeOrder; +import static org.postgresql.pljava.pg.DatumUtils.fetchPointer; +import static org.postgresql.pljava.pg.DatumUtils.mapFixedLength; +import static org.postgresql.pljava.pg.DatumUtils.storePointer; + +import java.nio.ByteBuffer; +import static java.nio.ByteOrder.nativeOrder; + +import java.sql.SQLException; +import java.sql.SQLSyntaxErrorException; + +import java.util.AbstractList; +import java.util.List; +import java.util.Map; + +import java.util.concurrent.ConcurrentHashMap; + +import java.util.function.BiFunction; +import java.util.function.IntSupplier; +import java.util.function.ToIntBiFunction; + +/** + * Implementation of {@link TupleDescriptor TupleDescriptor}. + *

    + * A {@link Cataloged Cataloged} descriptor corresponds to a known composite + * type declared in the PostgreSQL catalogs; its {@link #rowType rowType} method + * returns that type. A {@link Blessed Blessed} descriptor has been constructed + * on the fly and then interned in the type cache, such that the type + * {@code RECORD} and its type modifier value will identify it uniquely for + * the life of the backend; {@code rowType} will return the corresponding + * {@link RegTypeImpl.Blessed} instance. An {@link Ephemeral Ephemeral} + * descriptor has been constructed ad hoc and not interned; {@code rowType} will + * return {@link RegType#RECORD RECORD} itself, which isn't a useful identifier + * (many such ephemeral descriptors, all different, could exist at once). + * An ephemeral descriptor is only useful as long as a reference to it is held. + *

    + * A {@code Cataloged} descriptor can be obtained from the PG {@code relcache} + * or the {@code typcache}, should respond to cache invalidation for + * the corresponding relation, and is reference-counted, so the count should be + * incremented when cached here, and decremented/released if this instance + * goes unreachable from Java. + *

    + * A {@code Blessed} descriptor can be obtained from the PG {@code typcache} + * by {@code lookup_rowtype_tupdesc}. No invalidation logic is needed, as it + * will persist, and its identifying typmod will remain unique, for the life of + * the backend. It may or may not be reference-counted. + *

    + * An {@code Ephemeral} tuple descriptor may need to be copied out of + * a short-lived memory context where it is found, either into a longer-lived + * context (and invalidated when that context is), or onto the Java heap and + * used until GC'd. + */ +abstract class TupleDescImpl extends AbstractList +implements TupleDescriptor +{ + @FunctionalInterface + interface Slicer + { + ByteBuffer slice(TupleDescImpl o, int index); + } + + private final ByteBuffer m_td; + private final Slicer m_slicer; + private final Attribute[] m_attrs; + private final State m_state; + + /* + * Implementation of Projection + */ + + @Override // Projection + public Projection subList(int fromIndex, int toIndex) + { + return Projected.subList(this, fromIndex, toIndex); + } + + @Override // Projection + public Projection project(Simple... names) + { + return Projected.project(this, names); + } + + @Override // Projection + public Projection project(int... indices) + { + return Projected.project(this, indices); + } + + @Override // Projection + public Projection sqlProject(int... indices) + { + return Projected.sqlProject(this, indices); + } + + @Override // Projection + public Projection project(Attribute... attrs) + { + return Projected.project(this, attrs); + } + + @Override // TargetList + public R applyOver( + Iterable tuples, Cursor.Function f) + throws X, SQLException + { + return TargetListImpl.applyOver(this, tuples, f); + } + + @Override // TargetList + public R applyOver( + TupleTableSlot tuple, Cursor.Function f) + throws X, SQLException + { + return TargetListImpl.applyOver(this, tuple, f); + } + + /** + * A "getAndAdd" (with just plain memory effects, as it will only be used on + * the PG thread) tailored to the width of the tdrefcount field (which is, + * oddly, declared as C int rather than a specific-width type). + */ + private static final ToIntBiFunction s_getAndAddPlain; + + static + { + if ( 4 == SIZEOF_TUPLEDESC_TDREFCOUNT ) + { + s_getAndAddPlain = (b,i) -> + { + int count = b.getInt(OFFSET_TUPLEDESC_TDREFCOUNT); + b.putInt(OFFSET_TUPLEDESC_TDREFCOUNT, count + i); + return count; + }; + } + else + throw new ExceptionInInitializerError( + "Implementation needed for platform with " + + "sizeof TupleDesc->tdrefcount = " +SIZEOF_TUPLEDESC_TDREFCOUNT); + } + + /** + * Address of the native tuple descriptor (not supported on + * an {@code Ephemeral} instance). + */ + long address() throws SQLException + { + try + { + m_state.pin(); + return m_state.address(); + } + finally + { + m_state.unpin(); + } + } + + /** + * Slice off the portion of the buffer representing one attribute. + *

    + * Only called by {@code AttributeImpl}. + */ + ByteBuffer slice(int index) + { + return m_slicer.slice(this, index); + } + + private TupleDescImpl( + ByteBuffer td, boolean useState, + BiFunction ctor) + { + assert threadMayEnterPG() : "TupleDescImpl construction thread"; + + m_state = useState ? new State(this, td) : null; + m_td = asReadOnlyNativeOrder(td); + + int natts; + Slicer slicer; + + if ( PG_VERSION_NUM >= 110000 ) + { + /* + * In PG 11 and up, the attrs are allocated right at the end of + * the tupledesc struct. Use a slicer that gets them from there. + */ + natts = (m_td.capacity() - OFFSET_TUPLEDESC_ATTRS) + / SIZEOF_FORM_PG_ATTRIBUTE; + + slicer = (o,i) -> + { + int len = SIZEOF_FORM_PG_ATTRIBUTE; + int off = OFFSET_TUPLEDESC_ATTRS + len * i; + len = ATTRIBUTE_FIXED_PART_SIZE; + ByteBuffer bnew = o.m_td.duplicate(); + bnew.position(off).limit(off + len); + return bnew.slice().order(m_td.order()); + }; + } + else // < 110000 + { + /* + * In PG 10 and earlier, they were allocated separately, and the + * attrs member pointed to an array of pointers to them. The + * tupledesc struct had a fixed size, so we can't compute the + * number of attributes from that, have to read the natts member. + * Git shows it was always at offset 0 in historical versions, and + * declared as int, so we don't need to clutter ModelConstants with + * offset/sizeof for it specifically, but should check sizeof int. + */ + assert 4 == SIZEOF_INT : "sizeof int != 4 on this platform"; + natts = m_td.getInt(0); + + long p = fetchPointer(m_td, OFFSET_TUPLEDESC_ATTRS); + ByteBuffer pointers = mapFixedLength(p, natts * SIZEOF_DATUM); + + slicer = (o,i) -> + { + long ap = fetchPointer(pointers, i * SIZEOF_DATUM); + return mapFixedLength(ap, ATTRIBUTE_FIXED_PART_SIZE); + }; + + if ( this instanceof Ephemeral ) + { + /* + * We need to copy them; fromByteBuffer only copied + * the tupledesc struct itself. + */ + ByteBuffer copy = + ByteBuffer.allocate(natts * ATTRIBUTE_FIXED_PART_SIZE); + + for ( int i = 0 ; i < natts ; ++ i ) + copy.put(slicer.slice(this, i).rewind()); + + ByteBuffer bound = asReadOnlyNativeOrder(copy); + + slicer = (o,i) -> + { + int len = ATTRIBUTE_FIXED_PART_SIZE; + int off = len * i; + ByteBuffer bnew = bound.duplicate(); + bnew.position(off).limit(off + len); + return bnew.slice().order(bound.order()); + }; + } + } + + m_slicer = slicer; + + Attribute[] attrs = new Attribute [ natts ]; + + for ( int i = 0 ; i < attrs.length ; ++ i ) + attrs[i] = ctor.apply(this, 1 + i); + + m_attrs = attrs; + } + + /** + * Constructor used only by OfType to produce a synthetic tuple descriptor + * with one element of a specified RegType. + */ + private TupleDescImpl(RegType type) + { + m_state = null; + m_td = null; + m_slicer = null; + m_attrs = new Attribute[] { new AttributeImpl.OfType(this, type) }; + } + + /** + * Return a {@code TupleDescImpl} given a byte buffer that maps a PostgreSQL + * {@code TupleDesc} structure. + *

    + * This method is called from native code, and assumes the caller has not + * (or not knowingly) obtained the descriptor directly from the type cache, + * so if it is not reference-counted (its count is -1) it will be assumed + * unsafe to directly cache. In that case, if it represents a cataloged + * or interned ("blessed") descriptor, we will get one directly from the + * cache and return that, or if it is ephemeral, we will return one based + * on a defensive copy. + *

    + * If the descriptor is reference-counted, and we use it (that is, we do not + * find an existing version in our cache), we increment the reference count + * here. That does not have the effect of requesting leak warnings + * at the exit of PostgreSQL's current resource owner, because we have every + * intention of hanging on to it longer, until GC or an invalidation + * callback tells us not to. + *

    + * While we can just read the type oid, typmod, and reference count through + * the byte buffer, as long as the only caller is C code, it saves some fuss + * just to have it pass those values. If the C caller has the relation oid + * handy also, it can pass that as well and save a lookup here. + */ + private static TupleDescriptor fromByteBuffer( + ByteBuffer td, int typoid, int typmod, int reloid, int refcount) + { + TupleDescriptor.Interned result; + + td.order(nativeOrder()); + + /* + * Case 1: if the type is not RECORD, it's a cataloged composite type. + * Build an instance of Cataloged (unless the implicated RegClass has + * already got one). + */ + if ( RECORD.oid() != typoid ) + { + RegTypeImpl t = + (RegTypeImpl)Factory.formMaybeModifiedType(typoid, typmod); + + RegClassImpl c = + (RegClassImpl)( InvalidOid == reloid ? t.relation() + : Factory.staticFormObjectId(RegClass.CLASSID, reloid) ); + + assert c.isValid() : "Cataloged row type without matching RegClass"; + + if ( -1 == refcount ) // don't waste time on an ephemeral copy. + return c.tupleDescriptor(); // just go get the real one. + + TupleDescriptor.Interned[] holder = c.m_tupDescHolder; + if ( null != holder ) + { + result = holder[0]; + assert null != result : "disagree whether RegClass has desc"; + return result; + } + + holder = new TupleDescriptor.Interned[1]; + /* + * The constructor assumes the reference count has already been + * incremented to account for the reference constructed here. + */ + s_getAndAddPlain.applyAsInt(td, 1); + holder[0] = result = new Cataloged(td, c); + c.m_tupDescHolder = holder; + return result; + } + + /* + * Case 2: if RECORD with a modifier, it's an interned tuple type. + * Build an instance of Blessed (unless the implicated RegType has + * already got one). + */ + if ( -1 != typmod ) + { + RegTypeImpl.Blessed t = + (RegTypeImpl.Blessed)RECORD.modifier(typmod); + + if ( -1 == refcount ) // don't waste time on an ephemeral copy. + return t.tupleDescriptor(); // just go get the real one. + + TupleDescriptor.Interned[] holder = t.m_tupDescHolder; + if ( null != holder ) + { + result = holder[0]; + assert null != result : "disagree whether RegType has desc"; + return result; + } + + holder = new TupleDescriptor.Interned[1]; + /* + * The constructor assumes the reference count has already been + * incremented to account for the reference constructed here. + */ + s_getAndAddPlain.applyAsInt(td, 1); + holder[0] = result = new Blessed(td, t); + t.m_tupDescHolder = holder; + return result; + } + + /* + * Case 3: it's RECORD with no modifier, an ephemeral tuple type. + * Build an instance of Ephemeral unconditionally, defensively copying + * the descriptor if it isn't reference-counted (which we assert it + * isn't). + */ + assert -1 == refcount : "can any ephemeral TupleDesc be refcounted?"; + ByteBuffer copy = ByteBuffer.allocate(td.capacity()).put(td); + return new Ephemeral(copy); + } + + @Override + public Attribute sqlGet(int index) + { + return m_attrs[index - 1]; + } + + /* + * AbstractList implementation + */ + @Override + public int size() + { + return m_attrs.length; + } + + @Override + public Attribute get(int index) + { + return m_attrs[index]; + } + + static class Cataloged extends TupleDescImpl implements Interned + { + private final RegClass m_relation;// using its SwitchPoint, keep it live + + Cataloged(ByteBuffer td, RegClassImpl c) + { + /* + * Invalidation of a Cataloged tuple descriptor happens with the + * SwitchPoint attached to the RegClass. Every Cataloged descriptor + * from the cache had better be reference-counted, so unconditional + * true is passed for useState. + */ + super( + td, true, + (o, i) -> CatalogObjectImpl.Factory.formAttribute( + c.oid(), i, () -> new AttributeImpl.Cataloged(c)) + ); + + m_relation = c; // we need it alive for its SwitchPoint + } + + @Override + public RegType rowType() + { + return m_relation.type(); + } + } + + static class Blessed extends TupleDescImpl implements Interned + { + private final RegType m_rowType; // using its SwitchPoint, keep it live + + Blessed(ByteBuffer td, RegTypeImpl t) + { + /* + * A Blessed tuple descriptor has no associated RegClass, so we grab + * the SwitchPoint from the associated RegType, even though no + * invalidation event for it is ever expected. In fromByteBuffer, + * if we see a non-reference-counted descriptor, we grab one + * straight from the type cache instead. But sometimes, the one + * in PostgreSQL's type cache is non-reference counted, and that's + * ok, because that one will be good for the life of the process. + * So we do need to check, in this constructor, whether to pass true + * or false for useState. (Checking with getAndAddPlain(0) is a bit + * goofy, but it was already set up, matched to the field width, + * does the job.) + */ + super( + td, -1 != s_getAndAddPlain.applyAsInt(td, 0), + (o, i) -> new AttributeImpl.Transient(o, i) + ); + + m_rowType = t; + } + + @Override + public RegType rowType() + { + return m_rowType; + } + } + + static class Ephemeral extends TupleDescImpl + implements TupleDescriptor.Ephemeral + { + private Ephemeral(ByteBuffer td) + { + super( + td, false, + (o, i) -> new AttributeImpl.Transient(o, i) + ); + } + + @Override + public RegType rowType() + { + return RECORD; + } + + @Override + public Interned intern() + { + return doInPG(() -> + { + TupleDescImpl sup = this; // its m_td is private + + ByteBuffer direct; + + if ( PG_VERSION_NUM >= 110000 ) + { + direct = ByteBuffer.allocateDirect( + sup.m_td.capacity() + MAXIMUM_ALIGNOF - 1) + .alignedSlice(MAXIMUM_ALIGNOF).put(sup.m_td.rewind()); + } + else // < 110000 + { + /* + * May as well just make one big allocation and copy all + * the pieces into it; it's our job to free it, not PG's + * (PG will simply take a copy in its accustomed way), so + * PG needn't care how it was allocated. GC can have this + * as soon as we're done interning it, so some extra space + * for ensuring alignment is ok even if it could be figured + * more precisely. + */ + assert 4 == SIZEOF_INT : "sizeof int != 4 on this platform"; + int natts = sup.m_td.getInt(0); + + int len = + sup.m_td.capacity() + + natts * SIZEOF_DATUM // the pointer array + + natts * SIZEOF_FORM_PG_ATTRIBUTE // all the attrs + + 3 * MAXIMUM_ALIGNOF - 3; // some align gaps + + direct = ByteBuffer.allocateDirect(len) + .alignedSlice(MAXIMUM_ALIGNOF) // possible gap 1 + .put(sup.m_td.rewind()); // tupledesc itself + + int pos = direct.position(); + int alignmask = MAXIMUM_ALIGNOF - 1; + int misalign = direct.alignmentOffset(pos, MAXIMUM_ALIGNOF); + pos += - misalign & alignmask; // posible gap 2 + + int ptrs = pos; + long base = addressOf(direct); + direct.order(nativeOrder()); + storePointer(direct, OFFSET_TUPLEDESC_ATTRS, base + ptrs); + + pos += natts * SIZEOF_DATUM; // skip pointer array + misalign = direct.alignmentOffset(pos, MAXIMUM_ALIGNOF); + pos += - misalign & alignmask; // possible gap 3 + + int pad = + SIZEOF_FORM_PG_ATTRIBUTE - ATTRIBUTE_FIXED_PART_SIZE; + + for ( int i = 0; i < natts ; ++ i ) + { + storePointer( + direct, ptrs + i * SIZEOF_DATUM, base + pos); + direct.position(pos).put( + sup.m_slicer.slice(sup, i).rewind()); + pos = direct.position() + pad; + } + } + + int assigned = _assign_record_type_typmod(direct); + + /* + * That will have saved in the typcache an authoritative + * new copy of the descriptor. It will also have written + * the assigned modifier into the 'direct' copy of this + * descriptor, but this is still an Ephemeral instance, + * the wrong Java type. We need to return a new instance + * over the authoritative typcache copy. + */ + return RECORD.modifier(assigned).tupleDescriptor(); + }); + } + } + + static class OfType extends TupleDescImpl + implements TupleDescriptor.Ephemeral + { + OfType(RegType type) + { + super(type); + } + + @Override + public RegType rowType() + { + return RECORD; + } + + @Override + public Interned intern() + { + throw notyet(); + } + } + + /** + * Based on {@code SingleFreeTupleDesc}, but really does + * {@code ReleaseTupleDesc}. + *

    + * Decrements the reference count and, if it was 1 before decrementing, + * proceeds to the superclass method to free the descriptor. + */ + private static class State + extends DualState.SingleFreeTupleDesc + { + private final IntSupplier m_getAndDecrPlain; + + private State(TupleDescImpl referent, ByteBuffer td) + { + super(referent, null, addressOf(td)); + /* + * The only reference to this non-readonly ByteBuffer retained here + * is what's bound into this getAndDecr for the reference count. + */ + m_getAndDecrPlain = () -> s_getAndAddPlain.applyAsInt(td, -1); + } + + @Override + protected void javaStateUnreachable(boolean nativeStateLive) + { + if ( nativeStateLive && 1 == m_getAndDecrPlain.getAsInt() ) + super.javaStateUnreachable(nativeStateLive); + } + + private long address() + { + return guardedLong(); + } + } + + /** + * Call the PostgreSQL {@code typcache} function of the same name, but + * return the assigned typmod rather than {@code void}. + */ + private static native int _assign_record_type_typmod(ByteBuffer bb); +} diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/TupleList.java b/pljava/src/main/java/org/postgresql/pljava/pg/TupleList.java new file mode 100644 index 000000000..3a8c78cc1 --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/pg/TupleList.java @@ -0,0 +1,240 @@ +/* + * Copyright (c) 2023 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.pg; + +import java.nio.ByteBuffer; +import java.nio.IntBuffer; +import java.nio.LongBuffer; + +import java.util.AbstractList; +import java.util.Iterator; +import java.util.List; +import java.util.RandomAccess; +import java.util.Spliterator; +import static java.util.Spliterator.IMMUTABLE; +import static java.util.Spliterator.NONNULL; +import static java.util.Spliterator.ORDERED; +import static java.util.Spliterator.SIZED; +import java.util.Spliterators.AbstractSpliterator; + +import java.util.function.Consumer; +import java.util.function.IntToLongFunction; + +import org.postgresql.pljava.internal.DualState; +import org.postgresql.pljava.internal.DualState.Pinned; +import org.postgresql.pljava.internal.Invocation; + +import org.postgresql.pljava.model.MemoryContext; // for javadoc +import org.postgresql.pljava.model.TupleTableSlot; + +import static org.postgresql.pljava.pg.DatumUtils.asReadOnlyNativeOrder; +import static org.postgresql.pljava.pg.ModelConstants.SIZEOF_DATUM; + +/* + * Plan: a group (maybe a class or interface with nested classes) of + * implementations that look like lists of TupleTableSlot over different kinds + * of result: + * - SPITupleTable (these: a tupdesc, and vals array of HeapTuple pointers) + * - CatCList (n_members and a members array of CatCTup pointers, where each + * CatCTup has a HeapTupleData and HeapTupleHeader nearly but not quite + * adjacent), must find tupdesc + * - Tuplestore ? (is this visible, or concealed behind SPI's cursors?) + * - Tuplesort ? (") + * - SFRM results? (Ah, SFRM_Materialize makes a Tuplestore.) + * - will we ever see a "tuple table" ("which is a List of independent + * TupleTableSlots")? + */ + +/** + * Superinterface of one or more classes that can present a sequence of tuples, + * working from the forms in which PostgreSQL can present them. + */ +public interface TupleList extends List, AutoCloseable +{ + @Override + default void close() + { + } + + TupleList EMPTY = new Empty(); + + final static class Empty + extends AbstractList implements TupleList + { + private Empty() + { + } + + @Override + public int size() + { + return 0; + } + + @Override + public TupleTableSlot get(int i) + { + throw new IndexOutOfBoundsException( + "Index " + i + " out of bounds for length 0"); + } + } + + /** + * Returns a {@code Spliterator} that never splits. + *

    + * Because a {@code TupleList} is typically built on a single + * {@code TupleTableSlot} holding each tuple in turn, there can be no + * thought of parallel stream execution. + *

    + * Also, because a {@code TupleList} iterator may return the same + * {@code TupleTableSlot} repeatedly, stateful {@code Stream} operations + * such as {@code distinct} or {@code sorted} will make no sense applied + * to those objects. + */ + @Override + default public Spliterator spliterator() + { + return new IteratorNonSpliterator<>(iterator(), size(), + IMMUTABLE | NONNULL | ORDERED | SIZED); + } + + static class IteratorNonSpliterator extends AbstractSpliterator + { + private Iterator it; + + IteratorNonSpliterator(Iterator it, long est, int characteristics) + { + super(est, characteristics); + this.it = it; + } + + @Override + public boolean tryAdvance(Consumer action) + { + if ( ! it.hasNext() ) + return false; + action.accept(it.next()); + return true; + } + + @Override + public Spliterator trySplit() + { + return null; + } + } + + /** + * A {@code TupleList} constructed atop a PostgreSQL {@code SPITupleTable}. + *

    + * The native table is allocated in a {@link MemoryContext} that will be + * deleted when {@code SPI_finish} is called on exit of the current + * {@code Invocation}. This class merely maps the native tuple table in + * place, and so will prevent later access. + */ + class SPI extends AbstractList + implements TupleList, RandomAccess + { + private final State state; + private final TupleTableSlotImpl ttSlot; + private final int nTuples; + private final IntToLongFunction indexToPointer; + + private static class State + extends DualState.SingleSPIfreetuptable + { + private State(SPI r, long tt) + { + /* + * Each SPITupleTable is constructed in a context of its own + * that is a child of the SPI Proc context, and is used by + * SPI_freetuptable to efficiently free it. By rights, that + * context should be the Lifespan here, but that member of + * SPITupleTable is declared a private member "not intended for + * external callers" in the documentation. + * + * If that admonition is to be obeyed, a next-best choice is the + * current Invocation. As long as SPI connection continues to be + * managed automatically and disconnected when the invocation + * exits (and it makes its lifespanRelease call before + * disconnecting SPI, which it does), it should be safe enough. + */ + super(r, Invocation.current(), tt); + } + + private void close() + { + unlessReleased(() -> + { + releaseFromJava(); + }); + } + } + + /** + * Constructs an instance over an {@code SPITupleTable}. + * @param slot a TupleTableSlotImpl to use. The constructed object's + * iterator will return this slot repeatedly, with each tuple in turn + * stored into it. + * @param spiStructP address of the SPITupleTable structure itself, + * saved here to be freed if this object is closed or garbage-collected. + * @param htarray ByteBuffer over the consecutive HeapTuple pointers at + * spiStructP->vals. + */ + SPI(TupleTableSlotImpl slot, long spiStructP, ByteBuffer htarray) + { + ttSlot = slot; + htarray = asReadOnlyNativeOrder(htarray); + state = new State(this, spiStructP); + + if ( 8 == SIZEOF_DATUM ) + { + LongBuffer tuples = htarray.asLongBuffer(); + nTuples = tuples.capacity(); + indexToPointer = tuples::get; + return; + } + else if ( 4 == SIZEOF_DATUM ) + { + IntBuffer tuples = htarray.asIntBuffer(); + nTuples = tuples.capacity(); + indexToPointer = tuples::get; + return; + } + else + throw new AssertionError("unsupported SIZEOF_DATUM"); + } + + @Override + public TupleTableSlot get(int index) + { + try ( Pinned p = state.pinnedNoChecked() ) + { + ttSlot.store_heaptuple( + indexToPointer.applyAsLong(index), false); + return ttSlot; + } + } + + @Override + public int size() + { + return nTuples; + } + + @Override + public void close() + { + state.close(); + } + } +} diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/TupleTableSlotImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/TupleTableSlotImpl.java new file mode 100644 index 000000000..2d9e9e985 --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/pg/TupleTableSlotImpl.java @@ -0,0 +1,1037 @@ +/* + * Copyright (c) 2022-2023 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.pg; + +import java.lang.annotation.Native; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.IntBuffer; +import java.nio.LongBuffer; + +import java.util.List; +import java.util.AbstractList; + +import java.util.function.IntUnaryOperator; + +import java.sql.SQLException; + +import static java.util.Objects.checkIndex; +import static java.util.Objects.requireNonNull; + +import org.postgresql.pljava.Adapter; +import org.postgresql.pljava.Adapter.As; +import org.postgresql.pljava.Adapter.AsLong; +import org.postgresql.pljava.Adapter.AsDouble; +import org.postgresql.pljava.Adapter.AsInt; +import org.postgresql.pljava.Adapter.AsFloat; +import org.postgresql.pljava.Adapter.AsShort; +import org.postgresql.pljava.Adapter.AsChar; +import org.postgresql.pljava.Adapter.AsByte; +import org.postgresql.pljava.Adapter.AsBoolean; + +import org.postgresql.pljava.Lifespan; + +import org.postgresql.pljava.adt.spi.Datum; +import org.postgresql.pljava.adt.spi.Datum.Accessor; + +import static org.postgresql.pljava.internal.Backend.doInPG; +import org.postgresql.pljava.internal.DualState; +import static org.postgresql.pljava.internal.UncheckedException.unchecked; + +import org.postgresql.pljava.model.Attribute; +import org.postgresql.pljava.model.RegClass; +import org.postgresql.pljava.model.TupleDescriptor; +import org.postgresql.pljava.model.TupleTableSlot; + +import static org.postgresql.pljava.pg.CatalogObjectImpl.notyet; + +import static org.postgresql.pljava.pg.DatumUtils.mapFixedLength; +import static org.postgresql.pljava.pg.DatumUtils.mapCString; +import static org.postgresql.pljava.pg.DatumUtils.asAlwaysCopiedDatum; +import static org.postgresql.pljava.pg.DatumUtils.asReadOnlyNativeOrder; +import static org.postgresql.pljava.pg.DatumUtils.inspectVarlena; +import static org.postgresql.pljava.pg.DatumUtils.Accessor.forDeformed; +import static org.postgresql.pljava.pg.DatumUtils.Accessor.forHeap; + +import static org.postgresql.pljava.pg.ModelConstants.HEAPTUPLESIZE; + +import static + org.postgresql.pljava.pg.CatalogObjectImpl.Factory.staticFormObjectId; + +import static org.postgresql.pljava.pg.ModelConstants.*; + +/* + * bool always 1 byte (see c.h). + * + * From PG 12: + * type, flags, nvalid, tupleDescriptor, *values, *isnull, mcxt, tid, tableOid + * flags: EMPTY SHOULDFREE SLOW FIXED + * + * Pre-PG 12 (= for fields present in both): + * type + * individual bool flags + * isempty, shouldFree, shouldFreeMin, slow, fixedTupleDescriptor + * HeapTuple tuple + * =tupleDescriptor + * =mcxt + * buffer + * =nvalid + * =*values + * =*isnull + * mintuple, minhdr, off + * + * tableOid is tuple->t_tableOid, tid is tuple->t_self. + * Can a tuple from a different descendant table then get loaded in the slot? + * Answer: yes. So tableOid can change per tuple. (See ExecStoreHeapTuple.) + * Fetching the tableOid is easy starting with PG 12 (it's right in the TTS + * struct). For PG < 12, a native method will be needed to inspect 'tuple' (or + * just return a ByteBuffer windowing it, to be inspected here). That native + * method will not need to be serialized onto the PG thread, as it only looks at + * an existing struct in memory. + * FWIW, *HeapTuple is a HeapTupleData, and a HeapTupleData has a t_len. + * heap_copytuple allocates HEAPTUPLESIZE + tuple->t_len. The HEAPTUPLESIZE + * covers the HeapTupleData that precedes the HeapTupleHeader; from the start + * of that it's t_len. They could be allocated separately but typically aren't. + * (A HeapTuple in the form of a Datum is without the HeapTupleData part; see + * ExecStoreHeapTupleDatum, which just puts a transient HeapTupleData struct + * on the stack to point to the thing during the operation, deforms it, and + * stores it in virtual form.) + * + * (Also FWIW, to make a MinimalTuple from a HeapTuple, subract + * MINIMAL_TUPLE_OFFSET from the latter's t_len; the result is the amount to + * allocate and the amount to copy and what goes in the result's t_len.) + * + * For now: support only FIXED/fixedTupleDescriptor slots. For those, the native + * code can create ByteBuffers and pass them all at once to the constructor for: + * the TTS struct itself, the values array, the isnull array, and the TupleDesc + * (this constructor can pass that straight to the TupleDesc constructor). If it + * later makes sense to support non-fixed slots, that will mean checking for + * changes, and possibly creating a new TupleDesc and new values/isnull buffers + * on the fly. + * + * A PostgreSQL TupleTableSlot can be configured with TTSOpsVirtual or + * TTSOpsHeapTuple (or others, not contemplated here). The Heap and Deformed + * subclasses here don't exactly mirror that distinction. What they are really + * distinguishing is which flavor of DatumUtils.Accessor will be used. + * + * That is, the Deformed subclass here relies on getsomeattrs and the + * tts_values/tts_isnull arrays of the slot (which are in fact available for any + * flavor of slot). The Heap subclass here overloads m_values and m_isnull to + * directly map the tuple data, rather than relying on tts_values and + * tts_isnull, so it can only work for slot flavors where such regions exist in + * the expected formats. In other words, a Deformed can be constructed over any + * flavor of PostgreSQL slot (and is the only choice if the slot is + * TTSOpsVirtual); a Heap is an alternative choice only available if the + * underlying slot is known to have the expected null bitmap and data layout, + * and may save the overhead of populating tts_isnull and tts_values arrays from + * the underlying tuple. It would still be possible in principle to exploit + * those arrays in the Heap case if they have been populated, to avoid + * repeatedly walking the tuple, but the Heap implementation here, as of this + * writing, doesn't. Perhaps some refactoring / renaming is needed, so Heap has + * its own instance fields for the directly accessed tuple regions, and the + * m_values / m_isnull in the superclass always map the tts_values / tts_isnull + * arrays? + */ + +/** + * Implementation of {@link TupleTableSlot TupleTableSlot}. + */ +public abstract class TupleTableSlotImpl +implements TupleTableSlot +{ + @Native private static final int OFFSET_HeapTupleData_t_len = 0; + @Native private static final int OFFSET_HeapTupleData_t_tableOid = 12; + + @Native private static final int SIZEOF_HeapTupleData_t_len = 4; + @Native private static final int SIZEOF_HeapTupleData_t_tableOid = 4; +/* TBASE + @Native private static final int OFFSET_HeapTupleHeaderData_t_infomask2= 18; + @Native private static final int OFFSET_HeapTupleHeaderData_t_infomask = 20; + @Native private static final int OFFSET_HeapTupleHeaderData_t_hoff = 22; + @Native private static final int OFFSET_HeapTupleHeaderData_t_bits = 23; +*/ + @Native private static final int SIZEOF_HeapTupleHeaderData_t_infomask2 = 2; + @Native private static final int SIZEOF_HeapTupleHeaderData_t_infomask = 2; + @Native private static final int SIZEOF_HeapTupleHeaderData_t_hoff = 1; + + @Native private static final int HEAP_HASNULL = 1; // lives in infomask + @Native private static final int HEAP_HASEXTERNAL = 4; // lives in infomask + @Native private static final int HEAP_NATTS_MASK = 0x07FF; // infomask2 + + protected final ByteBuffer m_tts; + /* These can be final only because non-FIXED slots aren't supported yet. */ + protected final TupleDescriptor m_tupdesc; + protected final ByteBuffer m_values; + protected final ByteBuffer m_isnull; + protected final Accessor[] m_accessors; + protected final Adapter[] m_adapters; + + /* + * Experimenting with yet another pattern for use of DualState. We will + * keep one here and be agnostic about its exact subtype. Methods that + * install a tuple in the slot will be expected to provide a DualState + * instance with this slot as its referent and encapsulating whatever object + * and behavior it needs for cleaning up. Pin/unpin should be done at + * outermost API-exposed methods, not by internal ones. + */ + DualState m_state; + + TupleTableSlotImpl( + ByteBuffer tts, TupleDescriptor tupleDesc, + ByteBuffer values, ByteBuffer isnull) + { + m_tts = null == tts ? null : asReadOnlyNativeOrder(tts); + m_tupdesc = tupleDesc; + /* + * From the Deformed constructor, this is the array of Datum elements. + * From the Heap constructor, it may be null. + */ + m_values = null == values ? null : asReadOnlyNativeOrder(values); + /* + * From the Deformed constructor, this is an array of one-byte booleans. + * From the Heap constructor, it may be null. + */ + m_isnull = null == isnull ? null : asReadOnlyNativeOrder(isnull); + m_adapters = new Adapter [ m_tupdesc.size() ]; + + @SuppressWarnings("unchecked") + Object dummy = + m_accessors = new Accessor [ m_adapters.length ]; + + /* + * A subclass constructor other than Deformed could pass null for tts, + * provided it overrides the inherited relation(), which relies on it. + */ + if ( null == m_tts ) + return; + + /* + * Verify (for now) that this is a FIXED TupleTableSlot. + * JIT will specialize to the test that applies in this PG version + */ + if ( NOCONSTANT != OFFSET_TTS_FLAGS ) + { + if ( 0 != (TTS_FLAG_FIXED & m_tts.getChar(OFFSET_TTS_FLAGS)) ) + return; + } + else if ( NOCONSTANT != OFFSET_TTS_FIXED ) + { + if ( 0 != m_tts.get(OFFSET_TTS_FIXED) ) + return; + } + else if ( null != tupleDesc ) + /* + * This is an old PG version that lacks the flag to indicate that + * a slot has a fixed tuple descriptor that won't change between + * rows. The case of a non-fixed tuple descriptor has not been + * implemented here. For now, though, simply *assume* that when a + * descriptor has been passed to this constructor, it will be good + * for all rows, that is, that the slot is effectively fixed, even + * without the flag to say so. Cases where PostgreSQL could make a + * non-fixed slot through the APIs we're likely to use are *assumed* + * to be rare or even nonexistent. + */ + return; // hold my beer and watch this + else + throw new UnsupportedOperationException( + "Cannot construct non-fixed TupleTableSlot (PG < 11)"); + throw new UnsupportedOperationException( + "Cannot construct non-fixed TupleTableSlot"); + } + + static Deformed newDeformed( + ByteBuffer tts, TupleDescriptor tupleDesc, + ByteBuffer values, ByteBuffer isnull) + { + return new Deformed(tts, tupleDesc, values, isnull); + } + + /** + * Allocate a 'light' (no native TupleTableSlot struct) + * {@code TupleTableSlotImpl.Heap} object, given a tuple descriptor and + * a byte buffer that maps a single-chunk-allocated {@code HeapTuple} (one + * where the {@code HeapTupleHeader} directly follows the + * {@code HeapTupleData}) that's to be passed to {@code heap_freetuple} when + * no longer needed. + *

    + * If an optional {@code Lifespan} is supplied, the slot will be linked + * to it and invalidated when it expires. Otherwise, the tuple will be + * assumed allocated in an immortal memory context and freed upon the + * {@code javaStateUnreachable} or {@code javaStateReleased} events. + */ + static Heap heapTupleGetLightSlot( + TupleDescriptor td, ByteBuffer ht, Lifespan lifespan) + { + ht = asReadOnlyNativeOrder(ht); + + assert 4 == SIZEOF_HeapTupleData_t_len + : "sizeof HeapTupleData.t_len changed"; + int len = ht.getInt(OFFSET_HeapTupleData_t_len); + + assert ht.capacity() == len + HEAPTUPLESIZE + : "unexpected length for single-chunk HeapTuple"; + + int relOid = ht.getInt(OFFSET_HeapTupleData_t_tableOid); + + boolean disallowExternal = true; + + /* + * Following offsets are relative to the HeapTupleHeaderData struct. + * Could slice off a new ByteBuffer from HEAPTUPLESIZE here and use + * the offsets directly, but we'll just add HEAPTUPLESIZE to the offsets + * and save constructing that intermediate object. We will slice off + * values and nulls ByteBuffers further below. + */ + + assert 2 == SIZEOF_HeapTupleHeaderData_t_infomask + : "sizeof HeapTupleHeaderData.t_infomask changed"; + short infomask = ht.getShort( + HEAPTUPLESIZE + OFFSET_HeapTupleHeaderData_t_infomask); + + assert 2 == SIZEOF_HeapTupleHeaderData_t_infomask2 + : "sizeof HeapTupleHeaderData.t_infomask2 changed"; + short infomask2 = ht.getShort( + HEAPTUPLESIZE + OFFSET_HeapTupleHeaderData_t_infomask2); + + assert 1 == SIZEOF_HeapTupleHeaderData_t_hoff + : "sizeof HeapTupleHeaderData.t_hoff changed"; + int hoff = + Byte.toUnsignedInt(ht.get( + HEAPTUPLESIZE + OFFSET_HeapTupleHeaderData_t_hoff)); + + if ( disallowExternal && 0 != ( infomask & HEAP_HASEXTERNAL ) ) + throw notyet("heapTupleGetLightSlot with external values in tuple"); + + int voff = hoff + HEAPTUPLESIZE; + + ByteBuffer values = mapFixedLength(ht, voff, ht.capacity() - voff); + ByteBuffer nulls = null; + + if ( 0 != ( infomask & HEAP_HASNULL ) ) + { + int nlen = ( td.size() + 7 ) / 8; + if ( nlen + OFFSET_HeapTupleHeaderData_t_bits > hoff ) + { + int attsReallyPresent = infomask2 & HEAP_NATTS_MASK; + nlen = ( attsReallyPresent + 7 ) / 8; + assert nlen + OFFSET_HeapTupleHeaderData_t_bits <= hoff + : "heap null bitmap length"; + } + nulls = mapFixedLength(ht, + HEAPTUPLESIZE + OFFSET_HeapTupleHeaderData_t_bits, nlen); + } + + Heap slot = new Heap( + staticFormObjectId(RegClass.CLASSID, relOid), td, values, nulls); + + slot.m_state = new HTChunkState(slot, lifespan, ht); + + return slot; + } + + /** + * Return the index into {@code m_accessors} for this attribute, + * ensuring the elements at that index of {@code m_accessors} and + * {@code m_adapters} are set, or throw an exception if + * this {@code Attribute} doesn't belong to this slot's + * {@code TupleDescriptor}, or if the supplied {@code Adapter} can't + * fetch it. + *

    + * Most tests are skipped if the index is in range and {@code m_adapters} + * at that index already contains the supplied {@code Adapter}. + */ + protected int toIndex(Attribute att, Adapter adp) + { + int idx = att.subId() - 1; + + if ( 0 > idx || idx >= m_adapters.length + || m_adapters [ idx ] != requireNonNull(adp) ) + { + if ( ! (att instanceof AttributeImpl) + || ! ((AttributeImpl)att).foundIn(m_tupdesc) ) + { + throw new IllegalArgumentException( + "attribute " + att + " does not go with slot " + this); + } + + memoize(idx, att, adp); + } + + return idx; + } + + /** + * Return the {@code Attribute} at this index into the associated + * {@code TupleDescriptor}, + * ensuring the elements at that index of {@code m_accessors} and + * {@code m_adapters} are set, or throw an exception if + * this {@code Attribute} doesn't belong to this slot's + * {@code TupleDescriptor}, or if the supplied {@code Adapter} can't + * fetch it. + *

    + * Most tests are skipped if the index is in range and {@code m_adapters} + * at that index already contains the supplied {@code Adapter}. + */ + protected Attribute fromIndex(int idx, Adapter adp) + { + Attribute att = m_tupdesc.get(idx); + if ( m_adapters [ idx ] != requireNonNull(adp) ) + memoize(idx, att, adp); + return att; + } + + /** + * Called after verifying that att belongs to this slot's + * {@code TupleDescriptor}, that idx is its corresponding + * (zero-based) index, and that {@code m_adapters[idx]} does not already + * contain adp. + */ + protected void memoize(int idx, Attribute att, Adapter adp) + { + if ( ! adp.canFetch(att) ) + { + throw new IllegalArgumentException(String.format( + "cannot fetch attribute %s of type %s using %s", + att, att.type(), adp)); + } + + m_adapters [ idx ] = adp; + + if ( null == m_accessors [ idx ] ) + { + boolean byValue = att.byValue(); + short length = att.length(); + + m_accessors [ idx ] = selectAccessor(byValue, length); + } + } + + /** + * Selects appropriate {@code Accessor} for this {@code Layout} given + * byValue and length. + */ + protected abstract Accessor selectAccessor( + boolean byValue, short length); + + /** + * Returns the previously-selected {@code Accessor} for the item at the + * given index. + *

    + * The indirection's cost may be regrettable, but it simplifies the + * implementation of {@code Indexed}. + */ + protected Accessor accessor(int idx) + { + return m_accessors[idx]; + } + + /** + * Only to be called after idx is known valid + * from calling {@code toIndex}. + */ + protected abstract boolean isNull(int idx); + + /** + * Only to be called after idx is known valid + * from calling {@code toIndex}. + */ + protected abstract int toOffset(int idx); + + static class Deformed extends TupleTableSlotImpl + { + Deformed( + ByteBuffer tts, TupleDescriptor tupleDesc, + ByteBuffer values, ByteBuffer isnull) + { + super(tts, tupleDesc, values, requireNonNull(isnull)); + } + + @Override + protected int toIndex(Attribute att, Adapter adp) + { + int idx = super.toIndex(att, adp); + + getsomeattrs(idx); + return idx; + } + + @Override + protected Attribute fromIndex(int idx, Adapter adp) + { + Attribute att = super.fromIndex(idx, adp); + + getsomeattrs(idx); + return att; + } + + @Override + protected Accessor selectAccessor( + boolean byValue, short length) + { + return forDeformed(byValue, length); + } + + @Override + protected boolean isNull(int idx) + { + return 0 != m_isnull.get(idx); + } + + @Override + protected int toOffset(int idx) + { + return idx * SIZEOF_DATUM; + } + + /** + * Like PostgreSQL's {@code slot_getsomeattrs}, but {@code idx} here is + * zero-based (one will be added when it is passed to PostgreSQL). + */ + private void getsomeattrs(int idx) + { + int nValid; + if ( 2 == SIZEOF_TTS_NVALID ) + nValid = m_tts.getShort(OFFSET_TTS_NVALID); + else + { + assert 4 == SIZEOF_TTS_NVALID : "unexpected SIZEOF_TTS_NVALID"; + nValid = m_tts.getInt(OFFSET_TTS_NVALID); + } + if ( nValid <= idx ) + doInPG(() -> _getsomeattrs(m_tts, 1 + idx)); + } + } + + static class Heap extends TupleTableSlotImpl + { + protected final ByteBuffer m_hValues; + protected final ByteBuffer m_hIsNull; + protected final RegClass m_relation; + + Heap( + RegClass relation, TupleDescriptor tupleDesc, + ByteBuffer hValues, ByteBuffer hIsNull) + { + super(null, tupleDesc, null, null); + m_relation = requireNonNull(relation); + m_hValues = requireNonNull(hValues); + m_hIsNull = hIsNull; + } + + @Override + protected Accessor selectAccessor( + boolean byValue, short length) + { + return forHeap(byValue, length); + } + + @Override + protected boolean isNull(int idx) + { + if ( null == m_hIsNull ) + return false; + + // XXX we could have actual natts < m_tupdesc.size() + return 0 == ( m_hIsNull.get(idx >>> 3) & (1 << (idx & 7)) ); + } + + @Override + protected int toOffset(int idx) + { + int offset = 0; + List atts = m_tupdesc; + Attribute att; + + /* + * This logic is largely duplicated in Heap.Indexed.toOffsetNonFixed + * and will probably need to be changed there too if anything is + * changed here. + */ + for ( int i = 0 ; i < idx ; ++ i ) + { + if ( isNull(i) ) + continue; + + att = atts.get(i); + + int align = alignmentModulus(att.alignment()); + int len = att.length(); + + /* + * Skip the fuss of aligning if align isn't greater than 1. + * More interestingly, whether to align in the varlena case + * (length of -1) depends on whether the byte at the current + * offset is zero. Each outcome includes two subcases, for one + * of which it doesn't matter whether we align or not because + * the offset is already aligned, and for the other of which it + * does matter, so that determines the choice. If the byte seen + * there is zero, it might be a pad byte and require aligning, + * so align. See att_align_pointer in PG's access/tupmacs.h. + */ + if ( align > 1 && ( -1 != len || 0 == m_hValues.get(offset) ) ) + offset += + - m_hValues.alignmentOffset(offset, align) & (align-1); + + if ( 0 <= len ) // a nonnegative length is used directly + offset += len; + else if ( -1 == len ) // find and skip the length of the varlena + offset += inspectVarlena(m_hValues, offset); + else if ( -2 == len ) // NUL-terminated value, skip past the NUL + { + while ( 0 != m_hValues.get(offset) ) + ++ offset; + ++ offset; + } + else + throw new AssertionError( + "cannot skip attribute with weird length " + len); + } + + att = atts.get(idx); + + int align = alignmentModulus(att.alignment()); + int len = att.length(); + /* + * Same alignment logic as above. + */ + if ( align > 1 && ( -1 != len || 0 == m_hValues.get(offset) ) ) + offset += -m_hValues.alignmentOffset(offset, align) & (align-1); + + return offset; + } + + @Override + ByteBuffer values() + { + return m_hValues; + } + + @Override + public RegClass relation() + { + return m_relation; + } + + /** + * Something that resembles a {@code Heap} tuple, but consists of + * a number of elements all of the same type, distinguished by index. + *

    + * Constructed with a one-element {@code TupleDescriptor} whose single + * {@code Attribute} describes the type of all elements. + *

    + * + */ + static class Indexed extends Heap implements TupleTableSlot.Indexed + { + private final int m_elements; + private final IntUnaryOperator m_toOffset; + + Indexed( + TupleDescriptor td, int elements, + ByteBuffer nulls, ByteBuffer values) + { + super(td.get(0).relation(), td, values, nulls); + assert elements >= 0 : "negative element count"; + assert null == nulls || nulls.capacity() == (elements+7)/8 + : "nulls length element count mismatch"; + m_elements = elements; + + Attribute att = td.get(0); + int length = att.length(); + int align = alignmentModulus(att.alignment()); + assert 0 == values.alignmentOffset(0, align) + : "misaligned ByteBuffer passed"; + int mask = align - 1; // make it a mask + if ( length < 0 ) // the non-fixed case + /* + * XXX without offset memoization of some kind, this will be + * a quadratic way of accessing elements, but that can be + * improved later. + */ + m_toOffset = i -> toOffsetNonFixed(i, length, mask); + else + { + int stride = length + ( -(length & mask) & mask ); + if ( null == nulls ) + m_toOffset = i -> i * stride; + else + m_toOffset = i -> (i - nullsPreceding(i)) * stride; + } + } + + @Override + public int elements() + { + return m_elements; + } + + @Override + protected Attribute fromIndex(int idx, Adapter adp) + { + checkIndex(idx, m_elements); + Attribute att = m_tupdesc.get(0); + if ( m_adapters [ 0 ] != requireNonNull(adp) ) + memoize(0, att, adp); + return att; + } + + @Override + protected int toOffset(int idx) + { + return m_toOffset.applyAsInt(idx); + } + + @Override + protected Accessor accessor(int idx) + { + return m_accessors[0]; + } + + private int nullsPreceding(int idx) + { + int targetByte = idx >>> 3; + int targetBit = 1 << ( idx & 7 ); + byte b = m_hIsNull.get(targetByte); + /* + * The nulls bitmask has 1 bits where values are *not* null. + * Java has a bitCount method that counts 1 bits. So the loop + * below will have an invert step before counting bits. That + * means we want to modify *this* byte to have 1 at the target + * position *and above*, so all those bits will invert to zero + * before we count them. The next step does that. + */ + b |= - targetBit; + int count = Integer.bitCount(Byte.toUnsignedInt(b) ^ 0xff); + for ( int i = 0; i < targetByte; ++ i ) + { + b = m_hIsNull.get(i); + count += Integer.bitCount(Byte.toUnsignedInt(b) ^ 0xff); + } + return count; + } + + /** + * Largely duplicates the superclass {@code toOffset} but + * specialized to only a single attribute type that is repeated. + *

    + * Only covers the non-fixed-length cases (length of -1 or -2). + * Assumes the byte buffer is already aligned such that offset 0 + * satisfies the alignment constraint. + *

    + * Important: align here is a mask; the caller + * has subtracted 1 from it, compared to the align value + * seen in the superclass implementation. + */ + private int toOffsetNonFixed(int idx, int len, int align) + { + int offset = 0; + + if ( null != m_hIsNull ) + idx -= nullsPreceding(idx); + + /* + * The following code is very similar to that in the superclass, + * other than having already converted align to a mask (changing + * the test below to align>0 where the superclass has align>1), + * and having already reduced idx by the preceding nulls. If any + * change is needed here, it is probably needed there too. + */ + for ( int i = 0 ; i < idx ; ++ i ) + { + if ( align > 0 + && ( -1 != len || 0 == m_hValues.get(offset) ) ) + offset += - (offset & align) & align; + + if ( -1 == len ) // find and skip the length of the varlena + offset += inspectVarlena(m_hValues, offset); + else if ( -2 == len ) // NUL-terminated, skip past the NUL + { + while ( 0 != m_hValues.get(offset) ) + ++ offset; + ++ offset; + } + else + throw new AssertionError( + "cannot skip attribute with weird length " + len); + } + + /* + * Same alignment logic as above. + */ + if ( align > 0 && ( -1 != len || 0 == m_hValues.get(offset) ) ) + offset += - (offset & align) & align; + + return offset; + } + } + } + + @Override + public RegClass relation() + { + int tableOid; + + if ( NOCONSTANT == OFFSET_TTS_TABLEOID ) + throw notyet("table Oid from TupleTableSlot in PostgreSQL < 12"); + + tableOid = m_tts.getInt(OFFSET_TTS_TABLEOID); + return staticFormObjectId(RegClass.CLASSID, tableOid); + } + + @Override + public TupleDescriptor descriptor() + { + return m_tupdesc; + } + + ByteBuffer values() + { + return m_values; + } + + void store_heaptuple(long ht, boolean shouldFree) + { + doInPG(() -> _store_heaptuple(m_tts, ht, shouldFree)); + } + + private static native void _getsomeattrs(ByteBuffer tts, int idx); + + private static native void _store_heaptuple( + ByteBuffer tts, long ht, boolean shouldFree); + + @Override + public T get(Attribute att, As adapter) + { + int idx = toIndex(att, adapter); + + if ( isNull(idx) ) + return adapter.fetchNull(att); + + int off = toOffset(idx); + return adapter.fetch(m_accessors[idx], values(), off, att); + } + + @Override + public long get(Attribute att, AsLong adapter) + { + int idx = toIndex(att, adapter); + + if ( isNull(idx) ) + return adapter.fetchNull(att); + + int off = toOffset(idx); + return adapter.fetch(m_accessors[idx], values(), off, att); + } + + @Override + public double get(Attribute att, AsDouble adapter) + { + int idx = toIndex(att, adapter); + + if ( isNull(idx) ) + return adapter.fetchNull(att); + + int off = toOffset(idx); + return adapter.fetch(m_accessors[idx], values(), off, att); + } + + @Override + public int get(Attribute att, AsInt adapter) + { + int idx = toIndex(att, adapter); + + if ( isNull(idx) ) + return adapter.fetchNull(att); + + int off = toOffset(idx); + return adapter.fetch(m_accessors[idx], values(), off, att); + } + + @Override + public float get(Attribute att, AsFloat adapter) + { + int idx = toIndex(att, adapter); + + if ( isNull(idx) ) + return adapter.fetchNull(att); + + int off = toOffset(idx); + return adapter.fetch(m_accessors[idx], values(), off, att); + } + + @Override + public short get(Attribute att, AsShort adapter) + { + int idx = toIndex(att, adapter); + + if ( isNull(idx) ) + return adapter.fetchNull(att); + + int off = toOffset(idx); + return adapter.fetch(m_accessors[idx], values(), off, att); + } + + @Override + public char get(Attribute att, AsChar adapter) + { + int idx = toIndex(att, adapter); + + if ( isNull(idx) ) + return adapter.fetchNull(att); + + int off = toOffset(idx); + return adapter.fetch(m_accessors[idx], values(), off, att); + } + + @Override + public byte get(Attribute att, AsByte adapter) + { + int idx = toIndex(att, adapter); + + if ( isNull(idx) ) + return adapter.fetchNull(att); + + int off = toOffset(idx); + return adapter.fetch(m_accessors[idx], values(), off, att); + } + + @Override + public boolean get(Attribute att, AsBoolean adapter) + { + int idx = toIndex(att, adapter); + + if ( isNull(idx) ) + return adapter.fetchNull(att); + + int off = toOffset(idx); + return adapter.fetch(m_accessors[idx], values(), off, att); + } + + @Override + public T get(int idx, As adapter) + { + Attribute att = fromIndex(idx, adapter); + + if ( isNull(idx) ) + return adapter.fetchNull(att); + + int off = toOffset(idx); + return adapter.fetch(accessor(idx), values(), off, att); + } + + @Override + public long get(int idx, AsLong adapter) + { + Attribute att = fromIndex(idx, adapter); + + if ( isNull(idx) ) + return adapter.fetchNull(att); + + int off = toOffset(idx); + return adapter.fetch(accessor(idx), values(), off, att); + } + + @Override + public double get(int idx, AsDouble adapter) + { + Attribute att = fromIndex(idx, adapter); + + if ( isNull(idx) ) + return adapter.fetchNull(att); + + int off = toOffset(idx); + return adapter.fetch(accessor(idx), values(), off, att); + } + + @Override + public int get(int idx, AsInt adapter) + { + Attribute att = fromIndex(idx, adapter); + + if ( isNull(idx) ) + return adapter.fetchNull(att); + + int off = toOffset(idx); + return adapter.fetch(accessor(idx), values(), off, att); + } + + @Override + public float get(int idx, AsFloat adapter) + { + Attribute att = fromIndex(idx, adapter); + + if ( isNull(idx) ) + return adapter.fetchNull(att); + + int off = toOffset(idx); + return adapter.fetch(accessor(idx), values(), off, att); + } + + @Override + public short get(int idx, AsShort adapter) + { + Attribute att = fromIndex(idx, adapter); + + if ( isNull(idx) ) + return adapter.fetchNull(att); + + int off = toOffset(idx); + return adapter.fetch(accessor(idx), values(), off, att); + } + + @Override + public char get(int idx, AsChar adapter) + { + Attribute att = fromIndex(idx, adapter); + + if ( isNull(idx) ) + return adapter.fetchNull(att); + + int off = toOffset(idx); + return adapter.fetch(accessor(idx), values(), off, att); + } + + @Override + public byte get(int idx, AsByte adapter) + { + Attribute att = fromIndex(idx, adapter); + + if ( isNull(idx) ) + return adapter.fetchNull(att); + + int off = toOffset(idx); + return adapter.fetch(accessor(idx), values(), off, att); + } + + @Override + public boolean get(int idx, AsBoolean adapter) + { + Attribute att = fromIndex(idx, adapter); + + if ( isNull(idx) ) + return adapter.fetchNull(att); + + int off = toOffset(idx); + return adapter.fetch(accessor(idx), values(), off, att); + } + + private static class HTChunkState + extends DualState.BBHeapFreeTuple + { + private HTChunkState( + TupleTableSlotImpl referent, Lifespan span, ByteBuffer ht) + { + super(referent, span, ht); + } + } +} diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/adt/ArrayAdapter.java b/pljava/src/main/java/org/postgresql/pljava/pg/adt/ArrayAdapter.java new file mode 100644 index 000000000..f6f66c935 --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/pg/adt/ArrayAdapter.java @@ -0,0 +1,376 @@ +/* + * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.pg.adt; + +import java.io.IOException; + +import java.lang.reflect.Type; + +import java.nio.ByteBuffer; +import static java.nio.ByteOrder.nativeOrder; +import java.nio.IntBuffer; + +import java.security.AccessController; +import java.security.PrivilegedAction; + +import java.sql.SQLException; + +import java.util.List; +import static java.util.Objects.requireNonNull; + +import java.util.stream.IntStream; + +import org.postgresql.pljava.Adapter; +import org.postgresql.pljava.Adapter.Contract; + +import org.postgresql.pljava.adt.Array.AsFlatList; +import org.postgresql.pljava.adt.spi.Datum; + +import org.postgresql.pljava.model.Attribute; +import org.postgresql.pljava.model.RegClass; +import org.postgresql.pljava.model.RegType; +import org.postgresql.pljava.model.TupleDescriptor; +import org.postgresql.pljava.model.TupleTableSlot; + +import static org.postgresql.pljava.pg.CatalogObjectImpl.of; +import static org.postgresql.pljava.pg.DatumUtils.indexedTupleSlot; +import static org.postgresql.pljava.pg.DatumUtils.mapFixedLength; +import static org.postgresql.pljava.pg.ModelConstants.SIZEOF_ArrayType_ndim; +import static org.postgresql.pljava.pg.ModelConstants.OFFSET_ArrayType_ndim; +import static org.postgresql.pljava.pg.ModelConstants.SIZEOF_ArrayType_elemtype; +import static org.postgresql.pljava.pg.ModelConstants.OFFSET_ArrayType_elemtype; +import static + org.postgresql.pljava.pg.ModelConstants.SIZEOF_ArrayType_dataoffset; +import static + org.postgresql.pljava.pg.ModelConstants.OFFSET_ArrayType_dataoffset; +import static org.postgresql.pljava.pg.ModelConstants.OFFSET_ArrayType_DIMS; +import static org.postgresql.pljava.pg.ModelConstants.SIZEOF_ArrayType_DIM; +import static org.postgresql.pljava.pg.ModelConstants.VARHDRSZ; + +import static org.postgresql.pljava.pg.ModelConstants.MAXIMUM_ALIGNOF; + +/* + * The representation details are found in include/utils/array.h + */ + +/** + * Ancestor of adapters that can map a PostgreSQL array to some representation + * {@literal }. + * @param Java type to represent the entire array. + */ +public class ArrayAdapter extends Adapter.Array +{ + private static final Configuration s_config; + + /** + * An {@code ArrayAdapter} that maps any PostgreSQL array with element type + * compatible with {@link TextAdapter TextAdapter} to flat (disregarding the + * PostgreSQL array's dimensionality) {@code List} of {@code String}, + * with any null elements mapped to Java null. + */ + public static final + ArrayAdapter> FLAT_STRING_LIST_INSTANCE; + + static + { + @SuppressWarnings("removal") // JEP 411 + Configuration config = AccessController.doPrivileged( + (PrivilegedAction)() -> + configure(ArrayAdapter.class, Via.DATUM)); + + s_config = config; + + FLAT_STRING_LIST_INSTANCE = new ArrayAdapter<>( + TextAdapter.INSTANCE, AsFlatList.of(AsFlatList::nullsIncludedCopy)); + } + + /** + * Constructs an array adapter given an adapter that returns a reference + * type {@literal } for the element type, and a corresponding array + * contract producing {@literal }. + */ + public ArrayAdapter( + Adapter.As element, Contract.Array> contract) + { + super(contract, element, null, s_config); + } + + /** + * Constructs an array adapter given an adapter that returns a primitive + * {@code long} for the element type, and a corresponding array + * contract producing {@literal }. + */ + public ArrayAdapter( + Adapter.AsLong element, + Contract.Array> contract) + { + super(contract, element, null, s_config); + } + + /** + * Constructs an array adapter given an adapter that returns a primitive + * {@code double} for the element type, and a corresponding array + * contract producing {@literal }. + */ + public ArrayAdapter( + Adapter.AsDouble element, + Contract.Array> contract) + { + super(contract, element, null, s_config); + } + + /** + * Constructs an array adapter given an adapter that returns a primitive + * {@code int} for the element type, and a corresponding array + * contract producing {@literal }. + */ + public ArrayAdapter( + Adapter.AsInt element, + Contract.Array> contract) + { + super(contract, element, null, s_config); + } + + /** + * Constructs an array adapter given an adapter that returns a primitive + * {@code float} for the element type, and a corresponding array + * contract producing {@literal }. + */ + public ArrayAdapter( + Adapter.AsFloat element, + Contract.Array> contract) + { + super(contract, element, null, s_config); + } + + /** + * Constructs an array adapter given an adapter that returns a primitive + * {@code short} for the element type, and a corresponding array + * contract producing {@literal }. + */ + public ArrayAdapter( + Adapter.AsShort element, + Contract.Array> contract) + { + super(contract, element, null, s_config); + } + + /** + * Constructs an array adapter given an adapter that returns a primitive + * {@code char} for the element type, and a corresponding array + * contract producing {@literal }. + */ + public ArrayAdapter( + Adapter.AsChar element, + Contract.Array> contract) + { + super(contract, element, null, s_config); + } + + /** + * Constructs an array adapter given an adapter that returns a primitive + * {@code byte} for the element type, and a corresponding array + * contract producing {@literal }. + */ + public ArrayAdapter( + Adapter.AsByte element, + Contract.Array> contract) + { + super(contract, element, null, s_config); + } + + /** + * Constructs an array adapter given an adapter that returns a primitive + * {@code boolean} for the element type, and a corresponding array + * contract producing {@literal }. + */ + public ArrayAdapter( + Adapter.AsBoolean element, + Contract.Array> contract) + { + super(contract, element, null, s_config); + } + + ArrayAdapter( + Adapter.As element, Type witness, + Contract.Array> contract) + { + super(contract, element, witness, s_config); + } + + ArrayAdapter( + Adapter.AsLong element, Type witness, + Contract.Array> contract) + { + super(contract, element, witness, s_config); + } + + ArrayAdapter( + Adapter.AsDouble element, Type witness, + Contract.Array> contract) + { + super(contract, element, witness, s_config); + } + + ArrayAdapter( + Adapter.AsInt element, Type witness, + Contract.Array> contract) + { + super(contract, element, witness, s_config); + } + + ArrayAdapter( + Adapter.AsFloat element, Type witness, + Contract.Array> contract) + { + super(contract, element, witness, s_config); + } + + ArrayAdapter( + Adapter.AsShort element, Type witness, + Contract.Array> contract) + { + super(contract, element, witness, s_config); + } + + ArrayAdapter( + Adapter.AsChar element, Type witness, + Contract.Array> contract) + { + super(contract, element, witness, s_config); + } + + ArrayAdapter( + Adapter.AsByte element, Type witness, + Contract.Array> contract) + { + super(contract, element, witness, s_config); + } + + ArrayAdapter( + Adapter.AsBoolean element, Type witness, + Contract.Array> contract) + { + super(contract, element, witness, s_config); + } + + /** + * Whether this adapter can be applied to the given PostgreSQL type. + *

    + * If not overridden, simply requires that pgType is an array + * type and that its declared element type is acceptable to {@code canFetch} + * of the configured element adapter. + */ + @Override + public boolean canFetch(RegType pgType) + { + RegType elementType = pgType.element(); + return elementType.isValid() && m_elementAdapter.canFetch(elementType); + } + + /** + * Returns the result of applying the configured element adapter and + * {@link Contract.Array array contract} to the contents of the array + * in. + */ + public T fetch(Attribute a, Datum.Input in) + throws SQLException, IOException + { + try + { + in.pin(); + ByteBuffer bb = in.buffer().order(nativeOrder()); + + assert 4 == SIZEOF_ArrayType_ndim : "ArrayType.ndim size change"; + int nDims = bb.getInt(OFFSET_ArrayType_ndim); + + assert 4 == SIZEOF_ArrayType_elemtype + : "ArrayType.elemtype size change"; + RegType elementType = + of(RegType.CLASSID, bb.getInt(OFFSET_ArrayType_elemtype)); + + if ( ! m_elementAdapter.canFetch(elementType) ) + throw new IllegalArgumentException(String.format( + "cannot fetch array element of type %s using %s", + elementType, m_elementAdapter)); + + assert 4 == SIZEOF_ArrayType_dataoffset + : "ArrayType.dataoffset size change"; + int dataOffset = bb.getInt(OFFSET_ArrayType_dataoffset); + + boolean hasNulls = 0 != dataOffset; + + int dimsOffset = OFFSET_ArrayType_DIMS; + int dimsBoundsLength = 2 * nDims * SIZEOF_ArrayType_DIM; + + assert 4 == SIZEOF_ArrayType_DIM : "ArrayType dim size change"; + IntBuffer dimsAndBounds = + mapFixedLength(bb, dimsOffset, dimsBoundsLength).asIntBuffer(); + + int nItems = + IntStream.range(0, nDims).map(dimsAndBounds::get) + .reduce(1, Math::multiplyExact); + + ByteBuffer nulls; + + if ( hasNulls ) + { + int nullsOffset = dimsOffset + dimsBoundsLength; + int nullsLength = (nItems + 7) / 8; + nulls = mapFixedLength(bb, nullsOffset, nullsLength); + /* + * In the with-nulls case, PostgreSQL has supplied dataOffset. + * But it includes VARHDRSZ, and a VarlenaWrapper doesn't + * include that first word. + */ + dataOffset -= VARHDRSZ; + } + else + { + nulls = null; + /* + * In the no-nulls case, computing dataOffset is up to us. + */ + dataOffset = dimsOffset + dimsBoundsLength; + dataOffset += + - bb.alignmentOffset(dataOffset, MAXIMUM_ALIGNOF) + & (MAXIMUM_ALIGNOF - 1); + } + + ByteBuffer values = + mapFixedLength(bb, dataOffset, bb.capacity() - dataOffset); + + TupleTableSlot.Indexed tti = + indexedTupleSlot(elementType, nItems, nulls, values); + + int[] dimsBoundsArray = new int [ dimsAndBounds.capacity() ]; + dimsAndBounds.get(dimsBoundsArray); + + /* + * The accessible constructors ensured that m_elementAdapter and + * m_contract have compatible parameterized types. They were stored + * as raw types to avoid having extra type parameters on array + * adapters that are of no interest to code that makes use of them. + */ + @SuppressWarnings("unchecked") + T result = (T)m_contract.construct( + nDims, dimsBoundsArray, m_elementAdapter, tti); + + return result; + } + finally + { + in.unpin(); + in.close(); + } + } +} diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/adt/ByteaAdapter.java b/pljava/src/main/java/org/postgresql/pljava/pg/adt/ByteaAdapter.java new file mode 100644 index 000000000..b0fbed3f6 --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/pg/adt/ByteaAdapter.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2023 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.pg.adt; + +import java.io.InputStream; +import java.io.IOException; + +import java.nio.ByteBuffer; + +import java.security.AccessController; +import java.security.PrivilegedAction; + +import java.sql.SQLException; + +import org.postgresql.pljava.Adapter; +import org.postgresql.pljava.adt.spi.Datum; +import org.postgresql.pljava.model.Attribute; +import org.postgresql.pljava.model.RegType; + +/** + * PostgreSQL {@code bytea}. + */ +public abstract class ByteaAdapter extends Adapter.Container +{ + private ByteaAdapter() // no instances + { + } + + public static final Bytes ARRAY_INSTANCE; + public static final Stream STREAM_INSTANCE; + + static + { + @SuppressWarnings("removal") // JEP 411 + Configuration[] configs = AccessController.doPrivileged( + (PrivilegedAction)() -> new Configuration[] + { + configure( Bytes.class, Via.DATUM), + configure(Stream.class, Via.DATUM) + }); + + ARRAY_INSTANCE = new Bytes(configs[0]); + STREAM_INSTANCE = new Stream(configs[1]); + } + + /** + * Adapter producing a Java byte array. + */ + public static class Bytes extends Adapter.As + { + private Bytes(Configuration c) + { + super(c, null, null); + } + + @Override + public boolean canFetch(RegType pgType) + { + return RegType.BYTEA == pgType; + } + + public byte[] fetch(Attribute a, Datum.Input in) + throws SQLException + { + in.pin(); + try + { + ByteBuffer b = in.buffer(); + byte[] array = new byte [ b.limit() ]; + // Java >= 13: b.get(0, array) + b.rewind().get(array); + return array; + } + finally + { + in.unpin(); + } + } + } + + /** + * Adapter producing an {@code InputStream}. + */ + public static class Stream extends Adapter.As + { + private Stream(Configuration c) + { + super(c, null, null); + } + + @Override + public boolean canFetch(RegType pgType) + { + return RegType.BYTEA == pgType; + } + + public InputStream fetch(Attribute a, Datum.Input in) + throws SQLException + { + return in.inputStream(); + } + } +} diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/adt/DateTimeAdapter.java b/pljava/src/main/java/org/postgresql/pljava/pg/adt/DateTimeAdapter.java new file mode 100644 index 000000000..408828fbf --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/pg/adt/DateTimeAdapter.java @@ -0,0 +1,303 @@ +/* + * Copyright (c) 2022-2023 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.pg.adt; + +import java.io.IOException; + +import java.nio.ByteBuffer; + +import java.security.AccessController; +import java.security.PrivilegedAction; + +import java.sql.SQLException; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.OffsetTime; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; + +import org.postgresql.pljava.Adapter; +import org.postgresql.pljava.adt.Datetime; +import org.postgresql.pljava.adt.Timespan; +import org.postgresql.pljava.adt.spi.Datum; +import org.postgresql.pljava.model.Attribute; +import static org.postgresql.pljava.model.RegNamespace.PG_CATALOG; +import org.postgresql.pljava.model.RegType; + +import org.postgresql.pljava.model.SlotTester.Visible; // temporary for test jig + +import org.postgresql.pljava.sqlgen.Lexicals.Identifier.Simple; + +/** + * PostgreSQL date, time, timestamp, and interval types, available in various + * representations by implementing the corresponding functional interfaces + * to construct them. + */ +public abstract class DateTimeAdapter extends Adapter.Container +{ + private DateTimeAdapter() // no instances + { + } + + private static final Configuration[] s_configs; + + static + { + @SuppressWarnings("removal") // JEP 411 + Configuration[] configs = AccessController.doPrivileged( + (PrivilegedAction)() -> new Configuration[] + { + configure( Date.class, Via.INT32SX), + configure( Time.class, Via.INT64SX), + configure( TimeTZ.class, Via.DATUM ), + configure( Timestamp.class, Via.INT64SX), + configure(TimestampTZ.class, Via.INT64SX), + configure( Interval.class, Via.DATUM ) + }); + + s_configs = configs; + } + + /** + * Instances of the date/time/timestamp adapters using the JSR310 + * {@code java.time} types. + *

    + * A holder interface so these won't be instantiated unless wanted. + */ + public interface JSR310 extends Visible + { + Date DATE_INSTANCE = + new Date<>(Datetime.Date.AsLocalDate.INSTANCE); + + Time TIME_INSTANCE = + new Time<>(Datetime.Time.AsLocalTime.INSTANCE); + + TimeTZ TIMETZ_INSTANCE = + new TimeTZ<>(Datetime.TimeTZ.AsOffsetTime.INSTANCE); + + Timestamp TIMESTAMP_INSTANCE = + new Timestamp<>(Datetime.Timestamp.AsLocalDateTime.INSTANCE); + + TimestampTZ TIMESTAMPTZ_INSTANCE = + new TimestampTZ<>(Datetime.TimestampTZ.AsOffsetDateTime.INSTANCE); + + /* + * See org.postgresql.pljava.adt.Timespan.Interval for why a reference + * implementation for that type is missing here. + */ + } + + /** + * Adapter for the {@code DATE} type to the functional interface + * {@link Datetime.Date Datetime.Date}. + */ + public static class Date extends Adapter.As + { + private Datetime.Date m_ctor; + public Date(Datetime.Date ctor) + { + super(ctor, null, s_configs[0]); + m_ctor = ctor; + } + + @Override + public boolean canFetch(RegType pgType) + { + return RegType.DATE == pgType; + } + + public T fetch(Attribute a, int in) + { + return m_ctor.construct(in); + } + } + + /** + * Adapter for the {@code TIME} type to the functional interface + * {@link Datetime.Time Datetime.Time}. + */ + public static class Time extends Adapter.As + { + private Datetime.Time m_ctor; + public Time(Datetime.Time ctor) + { + super(ctor, null, s_configs[1]); + m_ctor = ctor; + } + + @Override + public boolean canFetch(RegType pgType) + { + return RegType.TIME == pgType; + } + + public T fetch(Attribute a, long in) + { + return m_ctor.construct(in); + } + } + + /** + * Adapter for the {@code TIME WITH TIME ZONE} type to the functional + * interface {@link Datetime.TimeTZ Datetime.TimeTZ}. + */ + public static class TimeTZ extends Adapter.As + { + private Datetime.TimeTZ m_ctor; + public TimeTZ(Datetime.TimeTZ ctor) + { + super(ctor, null, s_configs[2]); + m_ctor = ctor; + } + + @Override + public boolean canFetch(RegType pgType) + { + return RegType.TIMETZ == pgType; + } + + public T fetch(Attribute a, Datum.Input in) + throws IOException, SQLException + { + try + { + in.pin(); + ByteBuffer bb = in.buffer(); + long microsecondsSincePostgresEpoch = bb.getLong(); + int secondsWestOfPrimeMeridian = bb.getInt(); + return m_ctor.construct( + microsecondsSincePostgresEpoch, secondsWestOfPrimeMeridian); + } + finally + { + in.unpin(); + in.close(); + } + } + } + + /** + * Adapter for the {@code TIMESTAMP} type to the functional + * interface {@link Datetime.Timestamp Datetime.Timestamp}. + */ + public static class Timestamp extends Adapter.As + { + private Datetime.Timestamp m_ctor; + public Timestamp(Datetime.Timestamp ctor) + { + super(ctor, null, s_configs[3]); + m_ctor = ctor; + } + + @Override + public boolean canFetch(RegType pgType) + { + return RegType.TIMESTAMP == pgType; + } + + public T fetch(Attribute a, long in) + { + return m_ctor.construct(in); + } + } + + /** + * Adapter for the {@code TIMESTAMP WITH TIME ZONE} type to the functional + * interface {@link Datetime.TimestampTZ Datetime.TimestampTZ}. + */ + public static class TimestampTZ extends Adapter.As + { + private Datetime.TimestampTZ m_ctor; + public TimestampTZ(Datetime.TimestampTZ ctor) + { + super(ctor, null, s_configs[4]); + m_ctor = ctor; + } + + @Override + public boolean canFetch(RegType pgType) + { + return RegType.TIMESTAMPTZ == pgType; + } + + public T fetch(Attribute a, long in) + { + return m_ctor.construct(in); + } + } + + /** + * Adapter for the {@code INTERVAL} type to the functional + * interface {@link Timespan.Interval Timespan.Interval}. + */ + public static class Interval extends Adapter.As + { + private static final Simple + s_name_INTERVAL = Simple.fromJava("interval"); + + private static RegType s_intervalType; + + private Timespan.Interval m_ctor; + public Interval(Timespan.Interval ctor) + { + super(ctor, null, s_configs[5]); + m_ctor = ctor; + } + + @Override + public boolean canFetch(RegType pgType) + { + /* + * There has to be some kind of rule for which data types deserve + * their own RegType constants. The date/time/timestamp ones all do + * because JDBC mentions them, but it doesn't mention interval. + * So just compare it by name here, unless the decision is made + * to have a RegType constant for it too. + */ + RegType intervalType = s_intervalType; + if ( null != intervalType ) // did we match the type and cache it? + return intervalType == pgType; + + if ( ! s_name_INTERVAL.equals(pgType.name()) + || PG_CATALOG != pgType.namespace() ) + return false; + + /* + * Hang onto this matching RegType for faster future checks. + * Because RegTypes are singletons, and reference writes can't + * be torn, this isn't evil as data races go. + */ + s_intervalType = pgType; + return true; + } + + public T fetch(Attribute a, Datum.Input in) + throws IOException, SQLException + { + try + { + in.pin(); + ByteBuffer bb = in.buffer(); + long microseconds = bb.getLong(); + int days = bb.getInt(); + int months = bb.getInt(); + return m_ctor.construct(microseconds, days, months); + } + finally + { + in.unpin(); + in.close(); + } + } + } +} diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/adt/EncodingAdapter.java b/pljava/src/main/java/org/postgresql/pljava/pg/adt/EncodingAdapter.java new file mode 100644 index 000000000..d2df9b5da --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/pg/adt/EncodingAdapter.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.pg.adt; + +import java.io.IOException; + +import java.security.AccessController; +import java.security.PrivilegedAction; + +import java.sql.SQLException; + +import org.postgresql.pljava.Adapter; + +import org.postgresql.pljava.model.Attribute; +import org.postgresql.pljava.model.CharsetEncoding; +import org.postgresql.pljava.model.RegType; + +/** + * PostgreSQL character set encoding ({@code int4} in the catalogs) represented + * as {@code CharsetEncoding}. + */ +public class EncodingAdapter extends Adapter.As +{ + public static final EncodingAdapter INSTANCE; + + static + { + @SuppressWarnings("removal") // JEP 411 + Configuration config = AccessController.doPrivileged( + (PrivilegedAction)() -> + configure(EncodingAdapter.class, Via.INT32SX)); + + INSTANCE = new EncodingAdapter(config); + } + + EncodingAdapter(Configuration c) + { + super(c, null, null); + } + + @Override + public boolean canFetch(RegType pgType) + { + return RegType.INT4 == pgType; + } + + public CharsetEncoding fetch(Attribute a, int in) + throws SQLException, IOException + { + return -1 == in ? CharsetEncoding.ANY : CharsetEncoding.fromOrdinal(in); + } +} diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/adt/GrantAdapter.java b/pljava/src/main/java/org/postgresql/pljava/pg/adt/GrantAdapter.java new file mode 100644 index 000000000..1848710da --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/pg/adt/GrantAdapter.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.pg.adt; + +import java.io.IOException; + +import java.nio.ByteBuffer; +import static java.nio.ByteOrder.nativeOrder; + +import java.security.AccessController; +import java.security.PrivilegedAction; + +import java.sql.SQLException; + +import java.util.List; + +import org.postgresql.pljava.Adapter; +import org.postgresql.pljava.adt.Array.AsFlatList; +import org.postgresql.pljava.adt.spi.Datum; +import org.postgresql.pljava.model.Attribute; +import org.postgresql.pljava.model.CatalogObject.Grant; +import org.postgresql.pljava.model.RegType; + +import org.postgresql.pljava.pg.AclItem; + +/** + * PostgreSQL {@code aclitem} represented as {@link Grant Grant}. + */ +public class GrantAdapter extends Adapter.As +{ + public static final GrantAdapter INSTANCE; + + public static final ArrayAdapter> LIST_INSTANCE; + + static + { + @SuppressWarnings("removal") // JEP 411 + Configuration config = AccessController.doPrivileged( + (PrivilegedAction)() -> + configure(GrantAdapter.class, Via.DATUM)); + + INSTANCE = new GrantAdapter(config); + + LIST_INSTANCE = new ArrayAdapter<>(INSTANCE, + AsFlatList.of(AsFlatList::nullsIncludedCopy)); + } + + private GrantAdapter(Configuration c) + { + super(c, null, null); + } + + @Override + public boolean canFetch(RegType pgType) + { + return RegType.ACLITEM == pgType; + } + + public Grant fetch(Attribute a, Datum.Input in) + throws IOException, SQLException + { + in.pin(); + try + { + ByteBuffer b = in.buffer().order(nativeOrder()); + return new AclItem.NonRole(b); + } + finally + { + in.unpin(); + in.close(); + } + } +} diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/adt/MoneyAdapter.java b/pljava/src/main/java/org/postgresql/pljava/pg/adt/MoneyAdapter.java new file mode 100644 index 000000000..4694a354e --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/pg/adt/MoneyAdapter.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2023 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.pg.adt; + +import java.io.IOException; + +import java.security.AccessController; +import java.security.PrivilegedAction; + +import java.sql.SQLException; + +import org.postgresql.pljava.Adapter; +import org.postgresql.pljava.adt.Money; +import org.postgresql.pljava.model.Attribute; +import static org.postgresql.pljava.model.RegNamespace.PG_CATALOG; +import org.postgresql.pljava.model.RegType; + +import org.postgresql.pljava.sqlgen.Lexicals.Identifier.Simple; + +/** + * Adapter for the {@code MONEY} type to the functional interface {@link Money}. + */ +public abstract class MoneyAdapter extends Adapter.As +{ + private static final Simple s_name_MONEY = Simple.fromJava("money"); + private static RegType s_moneyType; + private final Money m_ctor; + + @SuppressWarnings("removal") // JEP 411 + private static final Configuration s_config = + AccessController.doPrivileged( + (PrivilegedAction)() -> + configure(MoneyAdapter.class, Via.INT64SX)); + + public MoneyAdapter(Money ctor) + { + super(ctor, null, s_config); + m_ctor = ctor; + } + + @Override + public boolean canFetch(RegType pgType) + { + /* + * There has to be some kind of rule for which data types deserve + * their own RegType constants. The date/time/timestamp ones all do + * because JDBC mentions them, but it doesn't mention interval. + * So just compare it by name here, unless the decision is made + * to have a RegType constant for it too. + */ + RegType moneyType = s_moneyType; + if ( null != moneyType ) // did we match the type and cache it? + return moneyType == pgType; + + if ( ! s_name_MONEY.equals(pgType.name()) + || PG_CATALOG != pgType.namespace() ) + return false; + + /* + * Hang onto this matching RegType for faster future checks. + * Because RegTypes are singletons, and reference writes can't + * be torn, this isn't evil as data races go. + */ + s_moneyType = pgType; + return true; + } + + public T fetch(Attribute a, long scaledToInteger) + throws IOException, SQLException + { + return m_ctor.construct(scaledToInteger); + } +} diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/adt/NameAdapter.java b/pljava/src/main/java/org/postgresql/pljava/pg/adt/NameAdapter.java new file mode 100644 index 000000000..b06fa76c3 --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/pg/adt/NameAdapter.java @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.pg.adt; + +import java.io.IOException; + +import java.nio.ByteBuffer; + +import java.security.AccessController; +import java.security.PrivilegedAction; + +import java.sql.SQLException; + +import org.postgresql.pljava.Adapter; +import org.postgresql.pljava.adt.spi.Datum; +import org.postgresql.pljava.model.Attribute; +import static org.postgresql.pljava.model.CharsetEncoding.SERVER_ENCODING; + +import org.postgresql.pljava.model.RegType; + +import static org.postgresql.pljava.pg.DatumUtils.mapCString; + +import org.postgresql.pljava.sqlgen.Lexicals.Identifier; +import org.postgresql.pljava.sqlgen.Lexicals.Identifier.Unqualified; + +/** + * PostgreSQL {@code name} type represented as + * {@code Lexicals.Identifier.Simple} or {@code Lexicals.Identifier.Operator}. + */ +public abstract class NameAdapter +extends Adapter.As +{ + public static final Simple SIMPLE_INSTANCE; + public static final Operator OPERATOR_INSTANCE; + public static final AsString AS_STRING_INSTANCE; + + static + { + @SuppressWarnings("removal") // JEP 411 + Configuration[] configs = AccessController.doPrivileged( + (PrivilegedAction)() -> new Configuration[] + { + configure( Simple.class, Via.DATUM), + configure(Operator.class, Via.DATUM), + configure(AsString.class, Via.DATUM) + }); + + SIMPLE_INSTANCE = new Simple(configs[0]); + OPERATOR_INSTANCE = new Operator(configs[1]); + AS_STRING_INSTANCE = new AsString(configs[2]); + } + + NameAdapter(Configuration c) + { + super(c, null, null); + } + + @Override + public boolean canFetch(RegType pgType) + { + return RegType.NAME == pgType; + } + + /** + * Adapter for the {@code name} type, returning an + * {@link Identifier.Simple Identifier.Simple}. + */ + public static class Simple extends NameAdapter + { + private Simple(Configuration c) + { + super(c); + } + + public Identifier.Simple fetch(Attribute a, Datum.Input in) + throws SQLException, IOException + { + return Identifier.Simple.fromCatalog(decoded(in)); + } + } + + /** + * Adapter for the {@code name} type, returning an + * {@link Identifier.Operator Identifier.Operator}. + */ + public static class Operator extends NameAdapter + { + private Operator(Configuration c) + { + super(c); + } + + public Identifier.Operator fetch(Attribute a, Datum.Input in) + throws SQLException, IOException + { + return Identifier.Operator.from(decoded(in)); + } + } + + /** + * Adapter for the {@code name} type, returning a Java {@code String}. + *

    + * This may be convenient for some casual uses, but a Java string will not + * observe any of the peculiar case-sensitivity rules of SQL identifiers. + */ + public static class AsString extends Adapter.As + { + private AsString(Configuration c) + { + super(c, null, null); + } + + @Override + public boolean canFetch(RegType pgType) + { + return RegType.NAME == pgType; + } + + public String fetch(Attribute a, Datum.Input in) + throws SQLException, IOException + { + return decoded(in); + } + } + + static final String decoded(Datum.Input in) throws SQLException, IOException + { + in.pin(); + try + { + ByteBuffer bnew = mapCString(in.buffer(), 0); + return SERVER_ENCODING.decode(bnew).toString(); + } + finally + { + in.unpin(); + in.close(); + } + } +} diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/adt/NumericAdapter.java b/pljava/src/main/java/org/postgresql/pljava/pg/adt/NumericAdapter.java new file mode 100644 index 000000000..35f871763 --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/pg/adt/NumericAdapter.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2023 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.pg.adt; + +import java.io.IOException; + +import java.math.BigDecimal; + +import java.nio.ShortBuffer; +import static java.nio.ByteOrder.nativeOrder; + +import java.security.AccessController; +import java.security.PrivilegedAction; + +import java.sql.SQLException; + +import org.postgresql.pljava.Adapter; +import org.postgresql.pljava.adt.Numeric; +import org.postgresql.pljava.adt.Numeric.Kind; +import org.postgresql.pljava.adt.Numeric.AsBigDecimal; +import org.postgresql.pljava.adt.spi.Datum; +import org.postgresql.pljava.model.Attribute; +import org.postgresql.pljava.model.RegType; + +/** + * Adapter for the {@code NUMERIC} type to the functional interface + * {@link Numeric}. + */ +public class NumericAdapter extends Adapter.As +{ + private final Numeric m_ctor; + + @SuppressWarnings("removal") // JEP 411 + private static final Configuration s_config = + AccessController.doPrivileged( + (PrivilegedAction)() -> + configure(NumericAdapter.class, Via.DATUM)); + + public static final NumericAdapter BIGDECIMAL_INSTANCE = + new NumericAdapter<>(AsBigDecimal.INSTANCE); + + public NumericAdapter(Numeric ctor) + { + super(ctor, null, s_config); + m_ctor = ctor; + } + + @Override + public boolean canFetch(RegType pgType) + { + return RegType.NUMERIC == pgType; + } + + public T fetch(Attribute a, Datum.Input in) throws SQLException + { + in.pin(); + try + { + ShortBuffer b = + in.buffer().order(nativeOrder()).asShortBuffer(); + + /* + * Magic numbers used below are not exposed in .h files, but + * only found in PostgreSQL's utils/adt/numeric.c. Most are used + * naked here, rather than named, if they aren't needed in many + * places and the usage is clear in context. Regression tests + * are the only way to confirm they are right anyway. + */ + + short header = b.get(); + + boolean isShort = 0 != (header & 0x8000); + + Kind k; + + switch ( header & 0xF000 ) + { + case 0xC000: k = Kind.NAN; break; + case 0xD000: k = Kind.POSINFINITY; break; + case 0xF000: k = Kind.NEGINFINITY; break; + default: + int displayScale; + int weight; + + if ( isShort ) + { + k = 0 != (header & 0x2000) ? Kind.NEGATIVE : Kind.POSITIVE; + displayScale = (header & 0x1F80) >>> 7; + weight = ( (header & 0x007F) ^ 0x0040 ) - 0x0040;// sign ext + } + else + { + k = 0 != (header & 0x4000) ? Kind.NEGATIVE : Kind.POSITIVE; + displayScale = header & 0x3FFF; + weight = b.get(); + } + + short[] base10000Digits = new short [ b.remaining() ]; + b.get(base10000Digits); + + return m_ctor.construct( + k, displayScale, weight, base10000Digits); + } + + return m_ctor.construct(k, 0, 0, new short[0]); + } + finally + { + in.unpin(); + } + } +} diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/adt/OidAdapter.java b/pljava/src/main/java/org/postgresql/pljava/pg/adt/OidAdapter.java new file mode 100644 index 000000000..f73c69900 --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/pg/adt/OidAdapter.java @@ -0,0 +1,221 @@ +/* + * Copyright (c) 2022-2023 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.pg.adt; + +import java.security.AccessController; +import java.security.PrivilegedAction; + +import static java.util.Arrays.stream; + +import org.postgresql.pljava.Adapter; +import org.postgresql.pljava.model.Attribute; + +import org.postgresql.pljava.model.*; + +import static org.postgresql.pljava.pg.CatalogObjectImpl.of; + +/** + * PostgreSQL {@code oid} type represented as + * {@code CatalogObject} or one of its {@code Addressed} subtypes. + */ +public class OidAdapter +extends Adapter.As +{ + public static final OidAdapter INSTANCE; + public static final Int4 INT4_INSTANCE; + public static final Addressed REGCLASS_INSTANCE; + public static final Addressed REGCOLLATION_INSTANCE; + public static final Addressed REGCONFIG_INSTANCE; + public static final Addressed REGDICTIONARY_INSTANCE; + public static final Addressed REGNAMESPACE_INSTANCE; + public static final Addressed REGOPERATOR_INSTANCE; + public static final Procedure REGPROCEDURE_INSTANCE; + public static final Addressed REGROLE_INSTANCE; + public static final Addressed REGTYPE_INSTANCE; + public static final Addressed DATABASE_INSTANCE; + public static final Addressed EXTENSION_INSTANCE; + public static final Addressed PLANG_INSTANCE; + + static + { + @SuppressWarnings("removal") // JEP 411 + Configuration[] configs = AccessController.doPrivileged( + (PrivilegedAction)() -> new Configuration[] + { + configure(OidAdapter.class, Via.INT32ZX), + configure( Int4.class, Via.INT32ZX), + configure( Addressed.class, Via.INT32ZX), + configure( Procedure.class, Via.INT32ZX) + }); + + INSTANCE = new OidAdapter<>(configs[0], null); + + INT4_INSTANCE = new Int4(configs[1]); + + REGCLASS_INSTANCE = new Addressed<>(configs[2], + RegClass.CLASSID, RegClass.class, RegType.REGCLASS); + + REGCOLLATION_INSTANCE = new Addressed<>(configs[2], + RegCollation.CLASSID, RegCollation.class, RegType.REGCOLLATION); + + REGCONFIG_INSTANCE = new Addressed<>(configs[2], + RegConfig.CLASSID, RegConfig.class, RegType.REGCONFIG); + + REGDICTIONARY_INSTANCE = new Addressed<>(configs[2], + RegDictionary.CLASSID, RegDictionary.class, RegType.REGDICTIONARY); + + REGNAMESPACE_INSTANCE = new Addressed<>(configs[2], + RegNamespace.CLASSID, RegNamespace.class, RegType.REGNAMESPACE); + + REGOPERATOR_INSTANCE = new Addressed<>(configs[2], + RegOperator.CLASSID, RegOperator.class, + RegType.REGOPER, RegType.REGOPERATOR); + + REGPROCEDURE_INSTANCE = new Procedure(configs[3]); + + REGROLE_INSTANCE = new Addressed<>(configs[2], + RegRole.CLASSID, RegRole.class, RegType.REGROLE); + + REGTYPE_INSTANCE = new Addressed<>(configs[2], + RegType.CLASSID, RegType.class, RegType.REGTYPE); + + DATABASE_INSTANCE = new Addressed<>(configs[2], + Database.CLASSID, Database.class); + + EXTENSION_INSTANCE = new Addressed<>(configs[2], + Extension.CLASSID, Extension.class); + + PLANG_INSTANCE = new Addressed<>(configs[2], + ProceduralLanguage.CLASSID, ProceduralLanguage.class); + } + + /** + * Types for which the non-specific {@code OidAdapter} or {@code Int4} will + * allow itself to be applied. + *

    + * Some halfhearted effort is put into ordering this with less commonly + * sought entries later. + */ + private static final RegType[] s_oidTypes = + { + RegType.OID, RegType.REGPROC, RegType.REGPROCEDURE, RegType.REGTYPE, + RegType.REGNAMESPACE, RegType.REGOPER, RegType.REGOPERATOR, + RegType.REGROLE, RegType.REGCLASS, RegType.REGCOLLATION, + RegType.REGCONFIG, RegType.REGDICTIONARY + }; + + private OidAdapter(Configuration c, Class witness) + { + super(c, null, witness); + } + + @Override + public boolean canFetch(RegType pgType) + { + for ( RegType t : s_oidTypes ) + if ( t == pgType ) + return true; + return false; + } + + public CatalogObject fetch(Attribute a, int in) + { + return of(in); + } + + /** + * Adapter for the {@code oid} type, returned as a primitive {@code int}. + */ + public static class Int4 extends Adapter.AsInt.Unsigned + { + private Int4(Configuration c) + { + super(c, null); + } + + @Override + public boolean canFetch(RegType pgType) + { + for ( RegType t : s_oidTypes ) + if ( t == pgType ) + return true; + return false; + } + + public int fetch(Attribute a, int in) + { + return in; + } + } + + /** + * Adapter for the {@code oid} type, able to return most of the + * {@link CatalogObject.Addressed CatalogObject.Addressed} subinterfaces. + */ + public static class Addressed> + extends OidAdapter + { + private final RegClass.Known m_classId; + private final RegType[] m_specificTypes; + + private Addressed( + Configuration c, RegClass.Known classId, Class witness, + RegType... specificTypes) + { + super(c, witness); + m_classId = classId; + m_specificTypes = stream(specificTypes) + .filter(RegType::isValid).toArray(RegType[]::new); + } + + @Override + public boolean canFetch(RegType pgType) + { + for ( RegType t : m_specificTypes ) + if ( t == pgType ) + return true; + return RegType.OID == pgType; + } + + public T fetch(Attribute a, int in) + { + return of(m_classId, in); + } + } + + /** + * A distinct adapter class is needed here because the parameterized + * {@code RegProcedure} type can't be indicated with a class literal + * argument to {@code Addressed}. + */ + public static class Procedure + extends OidAdapter> + { + private Procedure(Configuration c) + { + super(c, null); + } + + @Override + public boolean canFetch(RegType pgType) + { + if ( RegType.REGPROC == pgType || RegType.REGPROCEDURE == pgType ) + return true; + return RegType.OID == pgType; + } + + public RegProcedure fetch(Attribute a, int in) + { + return of(RegProcedure.CLASSID, in); + } + } +} diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/adt/Primitives.java b/pljava/src/main/java/org/postgresql/pljava/pg/adt/Primitives.java new file mode 100644 index 000000000..35d70fd56 --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/pg/adt/Primitives.java @@ -0,0 +1,222 @@ +/* + * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.pg.adt; + +import java.security.AccessController; +import java.security.PrivilegedAction; + +import org.postgresql.pljava.Adapter; +import org.postgresql.pljava.model.Attribute; +import org.postgresql.pljava.model.RegType; + +/** + * PostgreSQL primitive numeric and boolean, as the corresponding Java + * primitive types. + */ +public abstract class Primitives extends Adapter.Container +{ + private Primitives() // no instances + { + } + + public static final Int8 INT8_INSTANCE; + public static final Int4 INT4_INSTANCE; + public static final Int2 INT2_INSTANCE; + /** + * The PostgreSQL type {@code "char"} (with the quotes, to distinguish it + * from the different, standard SQL type), an 8-bit signed value with no + * associated character encoding (though often used in PostgreSQL catalogs + * with ASCII letters as values). + */ + public static final Int1 INT1_INSTANCE; + public static final Float8 FLOAT8_INSTANCE; + public static final Float4 FLOAT4_INSTANCE; + public static final Boolean BOOLEAN_INSTANCE; + + static + { + @SuppressWarnings("removal") // JEP 411 + Configuration[] configs = AccessController.doPrivileged( + (PrivilegedAction)() -> new Configuration[] + { + configure( Int8.class, Via.INT64SX), + configure( Int4.class, Via.INT32SX), + configure( Int2.class, Via.SHORT), + configure( Int1.class, Via.BYTE), + configure( Float8.class, Via.DOUBLE), + configure( Float4.class, Via.FLOAT), + configure(Boolean.class, Via.BOOLEAN) + }); + + INT8_INSTANCE = new Int8(configs[0]); + INT4_INSTANCE = new Int4(configs[1]); + INT2_INSTANCE = new Int2(configs[2]); + INT1_INSTANCE = new Int1(configs[3]); + FLOAT8_INSTANCE = new Float8(configs[4]); + FLOAT4_INSTANCE = new Float4(configs[5]); + BOOLEAN_INSTANCE = new Boolean(configs[6]); + } + + /** + * Adapter for the {@code int8} type. + */ + public static class Int8 extends Adapter.AsLong.Signed + { + private Int8(Configuration c) + { + super(c, null); + } + + @Override + public boolean canFetch(RegType pgType) + { + return RegType.INT8 == pgType; + } + + public long fetch(Attribute a, long in) + { + return in; + } + } + + /** + * Adapter for the {@code int4} type. + */ + public static class Int4 extends Adapter.AsInt.Signed + { + private Int4(Configuration c) + { + super(c, null); + } + + @Override + public boolean canFetch(RegType pgType) + { + return RegType.INT4 == pgType; + } + + public int fetch(Attribute a, int in) + { + return in; + } + } + + /** + * Adapter for the {@code int2} type. + */ + public static class Int2 extends Adapter.AsShort.Signed + { + private Int2(Configuration c) + { + super(c, null); + } + + @Override + public boolean canFetch(RegType pgType) + { + return RegType.INT2 == pgType; + } + + public short fetch(Attribute a, short in) + { + return in; + } + } + + /** + * Adapter for the {@code "char"} type. + */ + public static class Int1 extends Adapter.AsByte.Signed + { + private Int1(Configuration c) + { + super(c, null); + } + + @Override + public boolean canFetch(RegType pgType) + { + return RegType.CHAR == pgType; + } + + public byte fetch(Attribute a, byte in) + { + return in; + } + } + + /** + * Adapter for the {@code float8} type. + */ + public static class Float8 extends Adapter.AsDouble + { + private Float8(Configuration c) + { + super(c, null); + } + + @Override + public boolean canFetch(RegType pgType) + { + return RegType.FLOAT8 == pgType; + } + + public double fetch(Attribute a, double in) + { + return in; + } + } + + /** + * Adapter for the {@code float4} type. + */ + public static class Float4 extends Adapter.AsFloat + { + private Float4(Configuration c) + { + super(c, null); + } + + @Override + public boolean canFetch(RegType pgType) + { + return RegType.FLOAT4 == pgType; + } + + public float fetch(Attribute a, float in) + { + return in; + } + } + + /** + * Adapter for the {@code boolean} type. + */ + public static class Boolean extends Adapter.AsBoolean + { + private Boolean(Configuration c) + { + super(c, null); + } + + @Override + public boolean canFetch(RegType pgType) + { + return RegType.BOOL == pgType; + } + + public boolean fetch(Attribute a, boolean in) + { + return in; + } + } +} diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/adt/Service.java b/pljava/src/main/java/org/postgresql/pljava/pg/adt/Service.java new file mode 100644 index 000000000..0aad7fec7 --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/pg/adt/Service.java @@ -0,0 +1,308 @@ +/* + * Copyright (c) 2023 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.pg.adt; + +import java.lang.reflect.Type; + +import java.sql.SQLException; +import java.sql.SQLDataException; + +import static java.util.Arrays.copyOf; +import static java.util.Objects.requireNonNull; + +import org.postgresql.pljava.Adapter; +import org.postgresql.pljava.Adapter.Array; +import org.postgresql.pljava.Adapter.ArrayBuilder; +import org.postgresql.pljava.Adapter.As; +import org.postgresql.pljava.Adapter.AsBoolean; +import org.postgresql.pljava.Adapter.AsByte; +import org.postgresql.pljava.Adapter.AsChar; +import org.postgresql.pljava.Adapter.AsDouble; +import org.postgresql.pljava.Adapter.AsFloat; +import org.postgresql.pljava.Adapter.AsInt; +import org.postgresql.pljava.Adapter.AsLong; +import org.postgresql.pljava.Adapter.AsShort; +import org.postgresql.pljava.Adapter.TypeWrapper; + +import org.postgresql.pljava.adt.spi.AbstractType.MultiArray; +import org.postgresql.pljava.adt.spi.AbstractType.MultiArray.Sized.Allocated; + +import org.postgresql.pljava.model.TupleTableSlot.Indexed; + +/** + * Implementation of a service defined by {@link Adapter} for data types. + *

    + * Handles operations such as creating a properly-typed {@link ArrayAdapter} + * with dimensions and types computed from an adapter for the component type. + */ +public final class Service extends Adapter.Service +{ + @Override + protected Array + buildArrayAdapterImpl(ArrayBuilder builder, TypeWrapper w) + { + return staticBuildArrayAdapter( + builder, adapter(builder), multiArray(builder), requireNonNull(w)); + } + + /** + * Functional interface representing the initial logic of multiarray + * creation, verifying that the dimensions match, and allocating the Java + * array using the sizes from the PostgreSQL array datum. + */ + @FunctionalInterface + private interface MultiArrayBuilder + { + Allocated + build(int nDims, int[] dimsAndBounds) throws SQLException; + } + + /** + * Instantiate an array adapter, given the builder, and the component + * adapter and the {@link MultiArray} representing the desired array shape, + * both extracted from the builder in the protected caller above. + * + * A {@link TypeWrapper} has been supplied, to be populated here with the + * computed type, and passed as the 'witness' to the appropriate + * {@code ArrayAdapter} constructor. + */ + private static Array staticBuildArrayAdapter( + ArrayBuilder builder, + Adapter componentAdapter, + MultiArray shape, + TypeWrapper w) + { + w.setWrappedType(shape.arrayType()); + + /* + * Build an 'init' lambda that closes over 'shape'. + */ + final MultiArrayBuilder init = (nDims, dimsAndBounds) -> + { + if ( shape.dimensions != nDims ) + throw new SQLDataException( + shape.dimensions + "-dimension array adapter " + + "applied to " + nDims + "-dimension value", "2202E"); + + return shape.size(copyOf(dimsAndBounds, nDims)).allocate(); + }; + + /* + * A lambda implementing the rest of the array contract (closed over + * the 'init' created above) has to be specialized to the component type + * (reference or one of the primitives) that its inner loop will have to + * contend with. That can be determined from the subclass of Adapter. + */ + if ( componentAdapter instanceof AsLong ) + { + return new ArrayAdapter( + (AsLong)componentAdapter, w, + (int nDims, int[] dimsAndBounds, AsLong adapter, + Indexed slot) -> + { + @SuppressWarnings("unchecked") + Allocated multi = (Allocated) + init.build(nDims, dimsAndBounds); + + int n = slot.elements(); + int i = 0; + + for ( long[] a : multi ) + for ( int j = 0; j < a.length; ++ j ) + a[j] = slot.get(i++, adapter); + assert i == n; + return multi.array(); + } + ); + } + else if ( componentAdapter instanceof AsDouble ) + { + return new ArrayAdapter( + (AsDouble)componentAdapter, w, + (int nDims, int[] dimsAndBounds, AsDouble adapter, + Indexed slot) -> + { + @SuppressWarnings("unchecked") + Allocated multi = (Allocated) + init.build(nDims, dimsAndBounds); + + int n = slot.elements(); + int i = 0; + + for ( double[] a : multi ) + for ( int j = 0; j < a.length; ++ j ) + a[j] = slot.get(i++, adapter); + assert i == n; + return multi.array(); + } + ); + } + else if ( componentAdapter instanceof AsInt ) + { + return new ArrayAdapter( + (AsInt)componentAdapter, w, + (int nDims, int[] dimsAndBounds, AsInt adapter, + Indexed slot) -> + { + @SuppressWarnings("unchecked") + Allocated multi = (Allocated) + init.build(nDims, dimsAndBounds); + + int n = slot.elements(); + int i = 0; + + for ( int[] a : multi ) + for ( int j = 0; j < a.length; ++ j ) + a[j] = slot.get(i++, adapter); + assert i == n; + return multi.array(); + } + ); + } + else if ( componentAdapter instanceof AsFloat ) + { + return new ArrayAdapter( + (AsFloat)componentAdapter, w, + (int nDims, int[] dimsAndBounds, AsFloat adapter, + Indexed slot) -> + { + @SuppressWarnings("unchecked") + Allocated multi = (Allocated) + init.build(nDims, dimsAndBounds); + + int n = slot.elements(); + int i = 0; + + for ( float[] a : multi ) + for ( int j = 0; j < a.length; ++ j ) + a[j] = slot.get(i++, adapter); + assert i == n; + return multi.array(); + } + ); + } + else if ( componentAdapter instanceof AsShort ) + { + return new ArrayAdapter( + (AsShort)componentAdapter, w, + (int nDims, int[] dimsAndBounds, AsShort adapter, + Indexed slot) -> + { + @SuppressWarnings("unchecked") + Allocated multi = (Allocated) + init.build(nDims, dimsAndBounds); + + int n = slot.elements(); + int i = 0; + + for ( short[] a : multi ) + for ( int j = 0; j < a.length; ++ j ) + a[j] = slot.get(i++, adapter); + assert i == n; + return multi.array(); + } + ); + } + else if ( componentAdapter instanceof AsChar ) + { + return new ArrayAdapter( + (AsChar)componentAdapter, w, + (int nDims, int[] dimsAndBounds, AsChar adapter, + Indexed slot) -> + { + @SuppressWarnings("unchecked") + Allocated multi = (Allocated) + init.build(nDims, dimsAndBounds); + + int n = slot.elements(); + int i = 0; + + for ( char[] a : multi ) + for ( int j = 0; j < a.length; ++ j ) + a[j] = slot.get(i++, adapter); + assert i == n; + return multi.array(); + } + ); + } + else if ( componentAdapter instanceof AsByte ) + { + return new ArrayAdapter( + (AsByte)componentAdapter, w, + (int nDims, int[] dimsAndBounds, AsByte adapter, + Indexed slot) -> + { + @SuppressWarnings("unchecked") + Allocated multi = (Allocated) + init.build(nDims, dimsAndBounds); + + int n = slot.elements(); + int i = 0; + + for ( byte[] a : multi ) + for ( int j = 0; j < a.length; ++ j ) + a[j] = slot.get(i++, adapter); + assert i == n; + return multi.array(); + } + ); + } + else if ( componentAdapter instanceof AsBoolean ) + { + return new ArrayAdapter( + (AsBoolean)componentAdapter, w, + (int nDims, int[] dimsAndBounds, AsBoolean adapter, + Indexed slot) -> + { + @SuppressWarnings("unchecked") + Allocated multi = (Allocated) + init.build(nDims, dimsAndBounds); + + int n = slot.elements(); + int i = 0; + + for ( boolean[] a : multi ) + for ( int j = 0; j < a.length; ++ j ) + a[j] = slot.get(i++, adapter); + assert i == n; + return multi.array(); + } + ); + } + else if ( componentAdapter instanceof As ) + { + @SuppressWarnings("unchecked") + As erasedComponent = (As)componentAdapter; + + return new ArrayAdapter( + erasedComponent, w, + (int nDims, int[] dimsAndBounds, As adapter, + Indexed slot) -> + { + @SuppressWarnings("unchecked") + Allocated multi = (Allocated) + init.build(nDims, dimsAndBounds); + + int n = slot.elements(); + int i = 0; + + for ( Object[] a : multi ) + for ( int j = 0; j < a.length; ++ j ) + a[j] = slot.get(i++, adapter); + assert i == n; + return multi.array(); + } + ); + } + throw new AssertionError("unhandled type building array adapter"); + } +} diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/adt/TextAdapter.java b/pljava/src/main/java/org/postgresql/pljava/pg/adt/TextAdapter.java new file mode 100644 index 000000000..089657e66 --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/pg/adt/TextAdapter.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.pg.adt; + +import java.io.IOException; + +import java.security.AccessController; +import java.security.PrivilegedAction; + +import java.sql.SQLException; + +import org.postgresql.pljava.Adapter; +import org.postgresql.pljava.adt.spi.Datum; +import org.postgresql.pljava.model.Attribute; +import static org.postgresql.pljava.model.CharsetEncoding.SERVER_ENCODING; +import org.postgresql.pljava.model.RegType; + +/** + * PostgreSQL {@code text}, {@code varchar}, and similar types represented as + * Java {@code String}. + */ +public class TextAdapter extends Adapter.As +{ + public static final TextAdapter INSTANCE; + + static + { + @SuppressWarnings("removal") // JEP 411 + Configuration config = AccessController.doPrivileged( + (PrivilegedAction)() -> + configure(TextAdapter.class, Via.DATUM)); + + INSTANCE = new TextAdapter(config); + } + + private TextAdapter(Configuration c) + { + super(c, null, null); + } + + @Override + public boolean canFetch(RegType pgType) + { + if ( RegType.TEXT == pgType || RegType.CSTRING == pgType ) + return true; + + pgType = pgType.withoutModifier(); + + return RegType.VARCHAR == pgType + || RegType.BPCHAR == pgType; + + /* [comment re: typmod copied from upstream utils/adt/varchar.c:] + * For largely historical reasons, the typmod is VARHDRSZ plus the number + * of characters; there is enough client-side code that knows about that + * that we'd better not change it. + */ + } + + public String fetch(Attribute a, Datum.Input in) + throws SQLException, IOException + { + return SERVER_ENCODING.decode(in, /* close */ true).toString(); + } +} diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/adt/UUIDAdapter.java b/pljava/src/main/java/org/postgresql/pljava/pg/adt/UUIDAdapter.java new file mode 100644 index 000000000..d1d47ddad --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/pg/adt/UUIDAdapter.java @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.pg.adt; + +import java.io.IOException; + +import java.nio.ByteBuffer; +import static java.nio.ByteOrder.BIG_ENDIAN; + +import java.security.AccessController; +import java.security.PrivilegedAction; + +import java.sql.SQLException; + +import java.util.UUID; + +import org.postgresql.pljava.Adapter; + +import org.postgresql.pljava.adt.spi.Datum; + +import org.postgresql.pljava.model.Attribute; +import static org.postgresql.pljava.model.RegNamespace.PG_CATALOG; +import org.postgresql.pljava.model.RegType; + +import org.postgresql.pljava.sqlgen.Lexicals.Identifier.Simple; + +/** + * PostgreSQL {@code uuid} type represented + * as {@code java.util.UUID}. + */ +public class UUIDAdapter extends Adapter.As +{ + public static final UUIDAdapter INSTANCE; + + private static final Simple s_name_UUID = Simple.fromJava("uuid"); + + private static RegType s_uuidType; + + static + { + @SuppressWarnings("removal") // JEP 411 + Configuration config = AccessController.doPrivileged( + (PrivilegedAction)() -> + configure(UUIDAdapter.class, Via.DATUM)); + + INSTANCE = new UUIDAdapter(config); + } + + UUIDAdapter(Configuration c) + { + super(c, null, null); + } + + @Override + public boolean canFetch(RegType pgType) + { + /* + * Compare by name and namespace rather than requiring RegType to have + * a static field for the UUID type; more popular ones, sure, but a line + * has to be drawn somewhere. + */ + RegType uuidType = s_uuidType; + if ( null != uuidType ) // have we matched it before and cached it? + return uuidType == pgType; + + if ( ! s_name_UUID.equals(pgType.name()) + || PG_CATALOG != pgType.namespace() ) + return false; + + /* + * Hang onto this matching RegType for faster future checks. + * Because RegTypes are singletons, and reference writes can't + * be torn, this isn't evil as data races go. + */ + s_uuidType = pgType; + return true; + } + + public UUID fetch(Attribute a, Datum.Input in) + throws SQLException, IOException + { + try + { + in.pin(); + ByteBuffer bb = in.buffer(); + /* + * The storage is laid out byte by byte in the order PostgreSQL + * prints them (irrespective of architecture). Java's UUID type + * prints the MSB first. + */ + bb.order(BIG_ENDIAN); + long high64 = bb.getLong(); + long low64 = bb.getLong(); + return new UUID(high64, low64); + } + finally + { + in.unpin(); + in.close(); + } + } +} diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/adt/XMLAdapter.java b/pljava/src/main/java/org/postgresql/pljava/pg/adt/XMLAdapter.java new file mode 100644 index 000000000..7899f00e5 --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/pg/adt/XMLAdapter.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2022-2023 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.pg.adt; + +import java.io.InputStream; +import java.io.IOException; + +import java.security.AccessController; +import java.security.PrivilegedAction; + +import java.sql.SQLException; +import java.sql.SQLXML; + +import org.postgresql.pljava.Adapter; +import org.postgresql.pljava.adt.spi.Datum; + +import org.postgresql.pljava.jdbc.SQLXMLImpl; +import org.postgresql.pljava.model.Attribute; +import org.postgresql.pljava.model.RegType; + +/** + * PostgreSQL {@code xml} type represented as {@code java.sql.SQLXML}. + */ +public class XMLAdapter extends Adapter.As +{ + public static final XMLAdapter INSTANCE; + public static final XMLAdapter SYNTHETIC_INSTANCE; + + static + { + @SuppressWarnings("removal") // JEP 411 + Configuration[] configs = AccessController.doPrivileged( + (PrivilegedAction)() -> new Configuration[] + { + configure(XMLAdapter.class, Via.DATUM), + configure(Synthetic.class, Via.DATUM) + }); + + INSTANCE = new XMLAdapter(configs[0]); + SYNTHETIC_INSTANCE = new Synthetic(configs[1]); + } + + XMLAdapter(Configuration c) + { + super(c, null, null); + } + + /* + * This preserves the convention, since SQLXML came to PL/Java 1.5.1, that + * you can use the SQLXML API over text values (such as in a database built + * without the XML type, though who would do that nowadays?). + */ + @Override + public boolean canFetch(RegType pgType) + { + return RegType.XML == pgType + || RegType.TEXT == pgType; + } + + public + SQLXML fetch(Attribute a, Datum.Input in) + throws SQLException, IOException + { + return SQLXMLImpl.newReadable(in, a.type(), false); + } + + /** + * Adapter for use when the PostgreSQL type is not actually XML, but + * to be synthetically rendered as XML (such as {@code pg_node_tree}). + *

    + * This is, for now, a very thin wrapper over + * {@code SQLXMLImpl.newReadable}, which (so far) is still where the + * type-specific rendering logic gets chosen, but that can be refactored + * eventually. + */ + public static class Synthetic extends XMLAdapter + { + Synthetic(Configuration c) + { + super(c); + } + + @Override + public boolean canFetch(RegType pgType) + { + return RegType.PG_NODE_TREE == pgType; + } + + @Override + public + SQLXML fetch(Attribute a, Datum.Input in) + throws SQLException, IOException + { + return SQLXMLImpl.newReadable(in, a.type(), true); + } + } +} diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/adt/XidAdapter.java b/pljava/src/main/java/org/postgresql/pljava/pg/adt/XidAdapter.java new file mode 100644 index 000000000..204067a4d --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/pg/adt/XidAdapter.java @@ -0,0 +1,195 @@ +/* + * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.pg.adt; + +import java.io.IOException; + +import java.nio.ByteBuffer; + +import java.security.AccessController; +import java.security.PrivilegedAction; + +import java.sql.SQLException; + +import org.postgresql.pljava.Adapter; +import org.postgresql.pljava.adt.Internal; +import org.postgresql.pljava.adt.spi.Datum; +import org.postgresql.pljava.model.Attribute; +import org.postgresql.pljava.model.RegType; +import static org.postgresql.pljava.model.RegNamespace.PG_CATALOG; + +import org.postgresql.pljava.sqlgen.Lexicals.Identifier.Simple; + +/** + * PostgreSQL {@code cid}, {@code tid}, {@code xid}, and {@code xid8} types. + */ +public abstract class XidAdapter extends Adapter.Container +{ + private XidAdapter() // no instances + { + } + + private static final Configuration s_tid_config; + + public static final CidXid CID_INSTANCE; + public static final CidXid XID_INSTANCE; + public static final Xid8 XID8_INSTANCE; + + static + { + @SuppressWarnings("removal") // JEP 411 + Configuration[] configs = AccessController.doPrivileged( + (PrivilegedAction)() -> new Configuration[] + { + configure( CidXid.class, Via.INT32ZX), + configure( Xid8.class, Via.INT64ZX), + configure( Tid.class, Via.DATUM ) + }); + + CID_INSTANCE = new CidXid(configs[0], "cid"); + XID_INSTANCE = new CidXid(configs[0], "xid"); + XID8_INSTANCE = new Xid8(configs[1]); + + s_tid_config = configs[2]; + } + + /** + * Adapter for the {@code cid} or {@code xid} type, returned as + * a primitive {@code int}. + */ + public static class CidXid extends Adapter.AsInt.Unsigned + { + private final Simple m_typeName; + private RegType m_type; + + private CidXid(Configuration c, String typeName) + { + super(c, null); + m_typeName = Simple.fromJava(typeName); + } + + @Override + public boolean canFetch(RegType pgType) + { + RegType myType = m_type; + if ( null != myType ) + return myType == pgType; + if ( ! m_typeName.equals(pgType.name()) + || PG_CATALOG != pgType.namespace() ) + return false; + /* + * Reference writes are atomic and RegTypes are singletons, + * so this race isn't evil. + */ + m_type = pgType; + return true; + } + + public int fetch(Attribute a, int in) + { + return in; + } + } + + /** + * Adapter for the {@code xid8} type, returned as a primitive {@code long}. + */ + public static class Xid8 extends Adapter.AsLong.Unsigned + { + private static final Simple s_typeName = Simple.fromJava("xid8"); + private static RegType s_type; + + private Xid8(Configuration c) + { + super(c, null); + } + + @Override + public boolean canFetch(RegType pgType) + { + RegType myType = s_type; + if ( null != myType ) + return myType == pgType; + if ( ! s_typeName.equals(pgType.name()) + || PG_CATALOG != pgType.namespace() ) + return false; + /* + * Reference writes are atomic and RegTypes are singletons, + * so this race isn't evil. + */ + s_type = pgType; + return true; + } + + public long fetch(Attribute a, long in) + { + return in; + } + } + + /** + * Adapter for the {@code tid} type using the functional interface + * {@link Internal.Tid Internal.Tid}. + */ + public static class Tid extends Adapter.As + { + private static final Simple s_typeName = Simple.fromJava("tid"); + private static RegType s_type; + private Internal.Tid m_ctor; + + public Tid(Configuration c, Internal.Tid ctor) + { + super(ctor, null, c); + m_ctor = ctor; + } + + @Override + public boolean canFetch(RegType pgType) + { + RegType myType = s_type; + if ( null != myType ) + return myType == pgType; + if ( ! s_typeName.equals(pgType.name()) + || PG_CATALOG != pgType.namespace() ) + return false; + /* + * Reference writes are atomic and RegTypes are singletons, + * so this race isn't evil. + */ + s_type = pgType; + return true; + } + + public T fetch(Attribute a, Datum.Input in) + throws IOException, SQLException + { + try + { + in.pin(); + ByteBuffer bb = in.buffer(); + /* + * The following read could be unaligned; the C code declares + * BlockIdData trickily to allow it to be short-aligned. + * Java ByteBuffers will break up unaligned accesses as needed. + */ + int blockId = bb.getInt(); + short offsetNumber = bb.getShort(); + return m_ctor.construct(blockId, offsetNumber); + } + finally + { + in.unpin(); + in.close(); + } + } + } +} diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/adt/package-info.java b/pljava/src/main/java/org/postgresql/pljava/pg/adt/package-info.java new file mode 100644 index 000000000..02e2c7cdf --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/pg/adt/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +/** + * Built-in implementations of {@link Adapter Adapter} for common PostgreSQL + * data types. + * + * @author Chapman Flack + */ +package org.postgresql.pljava.pg.adt; + +import org.postgresql.pljava.Adapter; diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/package-info.java b/pljava/src/main/java/org/postgresql/pljava/pg/package-info.java new file mode 100644 index 000000000..0b6109730 --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/pg/package-info.java @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +/** + * Package that provides the running-directly-in-PG-backend implementations + * for the API in {@link org.postgresql.pljava.model}. + * + * @author Chapman Flack + */ +package org.postgresql.pljava.pg; diff --git a/pom.xml b/pom.xml index 5a883ab72..0749b4acf 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 org.postgresql pljava.app - 2-SNAPSHOT + 1.7-SNAPSHOT pom PostgreSQL PL/Java https://tada.github.io/pljava/