From 580b25a7255f50fcbf35c001c3ff9b2e433e8668 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 22 Jan 2022 22:22:59 -0500 Subject: [PATCH 001/334] Easy warmup: less JNI in Invocation Tweak invocation.c so the stack-allocated space provided by the caller is used to save the prior state rather than to construct the new state. This way, the current state can have a fixed address (currentInvocation is a constant pointer) and can be covered by a single static ByteBuffer that Invocation.java can read/write through without relying on JNI methods. As Invocation isn't a JDBC-specific concept or class, it has never made much sense to have it in the .jdbc package. Move it to .internal. --- pljava-so/src/main/c/Backend.c | 6 +- pljava-so/src/main/c/Invocation.c | 177 +++++++----------- pljava-so/src/main/c/JNICalls.c | 10 +- pljava-so/src/main/c/type/Type.c | 29 +-- .../src/main/include/pljava/Invocation.h | 95 ++++++---- .../pljava/{jdbc => internal}/Invocation.java | 101 +++++----- .../postgresql/pljava/jdbc/SPIConnection.java | 3 +- 7 files changed, 204 insertions(+), 217 deletions(-) rename pljava/src/main/java/org/postgresql/pljava/{jdbc => internal}/Invocation.java (62%) diff --git a/pljava-so/src/main/c/Backend.c b/pljava-so/src/main/c/Backend.c index a02988b48..b06123deb 100644 --- a/pljava-so/src/main/c/Backend.c +++ b/pljava-so/src/main/c/Backend.c @@ -721,7 +721,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... @@ -1400,7 +1400,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; } @@ -1428,7 +1428,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/Invocation.c b/pljava-so/src/main/c/Invocation.c index 8fe4aaff3..113166e99 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,31 @@ 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); + +#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 +161,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 +182,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 +193,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,20 +224,15 @@ 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); } - /* - * Do nativeRelease for any DualState instances scoped to this invocation. - */ - pljava_DualState_nativeRelease(currentInvocation); - /* * Check for any DualState objects that became unreachable and can be freed. */ @@ -235,13 +243,12 @@ void Invocation_popInvocation(bool wasException) JNI_popLocalFrame(0); - if(ctx != 0) + if ( NULL != ctx && NULL != ctx->upperContext ) { MemoryContextSwitchTo(ctx->upperContext); } - currentInvocation = ctx; - --s_callLevel; + *currentInvocation = *ctx; } MemoryContext @@ -251,55 +258,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..9252acb99 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 ( 1 > currentInvocation->nestLevel ) { 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/type/Type.c b/pljava-so/src/main/c/type/Type.c index a0d181e2f..b5b863ae6 100644 --- a/pljava-so/src/main/c/type/Type.c +++ b/pljava-so/src/main/c/type/Type.c @@ -110,14 +110,6 @@ 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 @@ -127,6 +119,17 @@ typedef struct */ MemoryContext spiContext; bool hasConnected; + /* + * Whether an Invocation instance, the Java counterpart to currentInvocation + * the C struct, has been assigned. One isn't unless it gets asked for, then + * if it is, that's noted here, and (in intermediate row-returning calls) + * cleared in the Invocation struct, to suppress its onExit() behavior when + * those intermediate calls return. So, even though the C currentInvocation + * really is new on each entry from PG, Java will see one Invocation + * instance whose onExit() behavior occurs only at the conclusion of the + * whole sequence of calls. + */ + bool hasDual; } CallContextData; /* @@ -141,7 +144,7 @@ static void stashCallContext(CallContextData *ctxData) ctxData->hasConnected = currentInvocation->hasConnected; - ctxData->invocation = currentInvocation->invocation; + ctxData->hasDual = currentInvocation->hasDual; if ( wasConnected ) return; @@ -162,7 +165,7 @@ static void _closeIteration(CallContextData* ctxData) { jobject dummy; currentInvocation->hasConnected = ctxData->hasConnected; - currentInvocation->invocation = ctxData->invocation; + currentInvocation->hasDual = ctxData->hasDual; /* * Why pass 1 as the call_cntr? We won't always have the actual call_cntr @@ -570,7 +573,7 @@ Datum Type_invokeSRF(Type self, Function fn, PG_FUNCTION_ARGS) ctxData = (CallContextData*)context->user_fctx; currCtx = CurrentMemoryContext; /* save executor's per-row context */ currentInvocation->hasConnected = ctxData->hasConnected; - currentInvocation->invocation = ctxData->invocation; + currentInvocation->hasDual = ctxData->hasDual; if(JNI_TRUE == pljava_Function_vpcInvoke(ctxData->fn, ctxData->rowProducer, ctxData->rowCollector, (jlong)context->call_cntr, @@ -580,14 +583,14 @@ Datum Type_invokeSRF(Type self, Function fn, PG_FUNCTION_ARGS) JNI_deleteLocalRef(row); stashCallContext(ctxData); currentInvocation->hasConnected = false; - currentInvocation->invocation = 0; + currentInvocation->hasDual = false; MemoryContextSwitchTo(currCtx); SRF_RETURN_NEXT(context, result); } stashCallContext(ctxData); currentInvocation->hasConnected = false; - currentInvocation->invocation = 0; + currentInvocation->hasDual = false; MemoryContextSwitchTo(currCtx); /* Unregister this callback and call it manually. We do this because diff --git a/pljava-so/src/main/include/pljava/Invocation.h b/pljava-so/src/main/include/pljava/Invocation.h index 28bebe41c..91067b028 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,17 +90,37 @@ 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[]; extern void Invocation_assertConnect(void); @@ -133,7 +148,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/src/main/java/org/postgresql/pljava/jdbc/Invocation.java b/pljava/src/main/java/org/postgresql/pljava/internal/Invocation.java similarity index 62% rename from pljava/src/main/java/org/postgresql/pljava/jdbc/Invocation.java rename to pljava/src/main/java/org/postgresql/pljava/internal/Invocation.java index b06e24727..ef0ba583c 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/Invocation.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/Invocation.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 @@ -10,11 +10,17 @@ * Tada AB * Chapman Flack */ -package org.postgresql.pljava.jdbc; +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.ArrayList; import java.util.logging.Logger; import org.postgresql.pljava.internal.Backend; @@ -40,6 +46,13 @@ */ public class Invocation { + @Native private static final int OFFSET_nestLevel = 0; + @Native private static final int OFFSET_hasDual = 4; + @Native private static final int OFFSET_errorOccurred = 5; + + private static final ByteBuffer s_window = + EarlyNatives._window().order(nativeOrder()); + /** * The current "stack" of invocations. */ @@ -71,7 +84,7 @@ public int getNestingLevel() /** * @return Returns the savePoint. */ - final PgSavepoint getSavepoint() + public final PgSavepoint getSavepoint() { return m_savepoint; } @@ -79,16 +92,16 @@ final PgSavepoint getSavepoint() /** * @param savepoint The savepoint to set. */ - final void setSavepoint(PgSavepoint savepoint) + public final void setSavepoint(PgSavepoint savepoint) { m_savepoint = savepoint; } /** - * Called from the backend when the invokation exits. Should - * not be invoked any other way. + * Called only from the static {@code onExit} below when the invocation + * is popped; should not be invoked any other way. */ - public void onExit(boolean withError) + private void onExit(boolean withError) throws SQLException { try @@ -98,10 +111,21 @@ public void onExit(boolean withError) } finally { - s_levels[m_nestingLevel] = null; + m_savepoint = null; } } + /** + * 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 */ @@ -109,59 +133,34 @@ public static Invocation current() { return doInPG(() -> { - Invocation curr = _getCurrent(); - if(curr != null) - return curr; - - int level = _getNestingLevel(); + Invocation curr; + int level = s_window.getInt(OFFSET_nestLevel); int top = s_levels.length; - if(level < top) - { - curr = s_levels[level]; - if(curr != null) - { - curr._register(); - return curr; - } - } - else + + if(level >= top) { - int newSize = top; - do { newSize <<= 2; } while(newSize <= level); + int newSize = highestOneBit(level) << 1; 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(); + + curr = s_levels[level]; + if ( null == curr ) + s_levels[level] = curr = new Invocation(level); + + s_window.put(OFFSET_hasDual, (byte)1); return curr; }); } - static void clearErrorCondition() + public static void clearErrorCondition() { - doInPG(Invocation::_clearErrorCondition); + doInPG(() -> s_window.put(OFFSET_errorOccurred, (byte)0)); } - /** - * 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(); + private static class EarlyNatives + { + private static native ByteBuffer _window(); + } } 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..6ef2b4b5f 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-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 @@ -50,6 +50,7 @@ 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; From 17a3a26d9998e95c0a475d3e6246df4d23cc36b3 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 22 Jan 2022 22:23:41 -0500 Subject: [PATCH 002/334] These two lines considered redundant Both values have just been stashed by stashCallContext. Both will be restored 14 lines later by _closeIteration. And nothing in those 14 lines cares about them. --- pljava-so/src/main/c/type/Type.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/pljava-so/src/main/c/type/Type.c b/pljava-so/src/main/c/type/Type.c index b5b863ae6..deb38bb59 100644 --- a/pljava-so/src/main/c/type/Type.c +++ b/pljava-so/src/main/c/type/Type.c @@ -589,8 +589,6 @@ Datum Type_invokeSRF(Type self, Function fn, PG_FUNCTION_ARGS) } stashCallContext(ctxData); - currentInvocation->hasConnected = false; - currentInvocation->hasDual = false; MemoryContextSwitchTo(currCtx); /* Unregister this callback and call it manually. We do this because From 9342f68dbace06cfee11ce1857ebc3ae190c72d4 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 22 Jan 2022 22:24:10 -0500 Subject: [PATCH 003/334] Tests for use of upper memory context --- .../example/annotation/MemoryContexts.java | 229 ++++++++++++++++++ 1 file changed, 229 insertions(+) create mode 100644 pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/MemoryContexts.java 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() + { + } + }; + } +} From b2094bae1bbc74429dafeb7f92dcdf8f1245780d Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 22 Jan 2022 22:24:28 -0500 Subject: [PATCH 004/334] Add switchToUpperContext for non-composite SRFs After surveying the code for where function return values can be constructed, add one switchToUpperContext() around the construction of non-composite SRF return values, where it was missing, so such values can be returned correctly after SPI_finish(), and so the former, very hacky, cross-invocation retention of SPI contexts can be sent to pasture. For the record, these are the notes from that survey of the code: Function results, non-set-returning: Type_invoke: the inherited _Type_invoke calls ->coerceObject, within sTUC. sub"class"es that override it: Boolean,Byte,Double,Float,Integer,Long,Short,Void: - overridden in order to use appropriately-typed JNI invoke method - Double,Float,Long have _asDatum that does sTUC; . historical artifact; those types were !byval before PG 8.4 - the rest do not sTUC; should be ok, all byval Coerce: does sTUC Composite: does sTUC around _getTupleAndClear Arrays: createArrayType (extern, in Array.c) does sTUC. So far so good. What about !byval elements stored into the array? the non-primitive/any types don't override _Array_coerceObject, which is where Type_coerceObject on each element, and construct_md_array are called. With no sTUC. Around construct_md_array is really where it's needed. But then, _Array_coerceObject is still being called within sTUC of _Type_invoke. All good. Hmm: !byval elements of values[] are leaked when pfree(values) happens. They should be pfree'd unconditionally; construct_md_array copies them. What about UDTs? They don't override _Type_invoke. So they inherit the one that calls ->coerceObject, within sTUC. That ought to be enough. UDT.c's coerceScalarObject itself also sTUCs, inconsistently, for fixed-length and varlena types but not NUL-terminated. That should be ok, and merely redundant. In coerceTupleObject, no sTUC appears. Again, by inheritance of coerceObject, that should be ok. Absent that, sTUC around the SQLOutputToTuple_getTuple should be adequate; only if that could produce a tuple with TOAST pointers would it also be necessary around the HeapTupleGetDatum. Function results, set-returning: _datumFromSRF is applied to each row result The inherited _datumFromSRF calls Type_coerceObject, NOT within sTUC XXX this, at least, definitely needs a sTUC added. sub"class"es that override it: only Composite: calls _getTupleAndClear, NOT within sTUC. But it works out, just because TupleDesc.java's native _formTuple method uses JavaMemoryContext. Spooky action at a distance? Results from triggers: Function.c's invokeTrigger does sTUC around the getTriggerReturnTuple. --- pljava-so/src/main/c/type/Type.c | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/pljava-so/src/main/c/type/Type.c b/pljava-so/src/main/c/type/Type.c index deb38bb59..70f71b41a 100644 --- a/pljava-so/src/main/c/type/Type.c +++ b/pljava-so/src/main/c/type/Type.c @@ -842,6 +842,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; @@ -873,9 +881,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) From e4417248b244456589039991f158e74e75ae62be Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 22 Jan 2022 22:25:02 -0500 Subject: [PATCH 005/334] Use SPI in the purely nested fashion it expects In passing, fix a long-standing thinko in Invocation_popInvocation: the memory context that was current on entry is stored in upperContext of *this* Invocation, but popInvocation was 'restoring' the one that was saved in the *previous* Invocation. Also in passing, move the cleanEnqueuedInstances step later in the pop sequence, improving its chance of seeing instances that could become unreachable through the release of SPI contexts or the JNI local frame. --- pljava-so/src/main/c/Invocation.c | 23 +++++--- pljava-so/src/main/c/type/Type.c | 92 ++++++++++++++----------------- 2 files changed, 54 insertions(+), 61 deletions(-) diff --git a/pljava-so/src/main/c/Invocation.c b/pljava-so/src/main/c/Invocation.c index 113166e99..c6bc76796 100644 --- a/pljava-so/src/main/c/Invocation.c +++ b/pljava-so/src/main/c/Invocation.c @@ -233,20 +233,25 @@ void Invocation_popInvocation(bool wasException) ? JNI_TRUE : JNI_FALSE); } - /* - * Check for any DualState objects that became unreachable and can be freed. - */ - pljava_DualState_cleanEnqueuedInstances(); - if(currentInvocation->hasConnected) SPI_finish(); JNI_popLocalFrame(0); - if ( NULL != ctx && NULL != ctx->upperContext ) - { - MemoryContextSwitchTo(ctx->upperContext); - } + /* + * Return to the context that was effective at pushInvocation of *this* + * invocation. + */ + 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(); *currentInvocation = *ctx; } diff --git a/pljava-so/src/main/c/type/Type.c b/pljava-so/src/main/c/type/Type.c index 70f71b41a..f7895b93a 100644 --- a/pljava-so/src/main/c/type/Type.c +++ b/pljava-so/src/main/c/type/Type.c @@ -110,15 +110,6 @@ typedef struct Function fn; jobject rowProducer; jobject rowCollector; - /* - * 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; /* * Whether an Invocation instance, the Java counterpart to currentInvocation * the C struct, has been assigned. One isn't unless it gets asked for, then @@ -134,27 +125,13 @@ typedef struct /* * Called during evaluation of a set-returning function, at various points after - * calls into Java code could have instantiated an Invocation, or connected SPI. + * calls into Java code could have instantiated an Invocation. * Does not stash elemType, rowProducer, or rowCollector; those are all - * unconditionally set in the first-call initialization, and spiContext to zero. + * unconditionally set in the first-call initialization. */ -static void stashCallContext(CallContextData *ctxData) +static inline void stashCallContext(CallContextData *ctxData) { - bool wasConnected = ctxData->hasConnected; - - ctxData->hasConnected = currentInvocation->hasConnected; - ctxData->hasDual = currentInvocation->hasDual; - - 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; } /* @@ -164,7 +141,6 @@ static void stashCallContext(CallContextData *ctxData) static void _closeIteration(CallContextData* ctxData) { jobject dummy; - currentInvocation->hasConnected = ctxData->hasConnected; currentInvocation->hasDual = ctxData->hasDual; /* @@ -181,24 +157,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); - } } /* @@ -528,6 +486,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; @@ -565,14 +556,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; + currCtx = CurrentMemoryContext; /* save the supplied per-row context */ currentInvocation->hasDual = ctxData->hasDual; if(JNI_TRUE == pljava_Function_vpcInvoke(ctxData->fn, @@ -582,14 +573,11 @@ 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->hasDual = false; - MemoryContextSwitchTo(currCtx); SRF_RETURN_NEXT(context, result); } stashCallContext(ctxData); - 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 From e380e6bf7b07b42928e95d6722ec22ad68ca8007 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 22 Jan 2022 22:25:41 -0500 Subject: [PATCH 006/334] Add a nested/SPI test to SetOfRecordTest This can reveal issues with the nesting of SPI 'connections' or management of their associated memory contexts. --- .../example/annotation/SetOfRecordTest.java | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) 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", From 2d543596bc6bb6b07836d451ef9c5c8739e0dcb8 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 22 Jan 2022 22:26:23 -0500 Subject: [PATCH 007/334] Also eliminate special SRF Invocation treatment Without the special treatment, the instance of the Java class Invocation, if any, that corresponds to the C Invocation, has its lifetime simply bounded to that of the C Invocation, rather than artificially extended across a sequence of SRF value-per-call invocations. It is simpler, does not break any existing tests, and is less likely to be violating PostgreSQL assumptions on correct behavior. --- pljava-so/src/main/c/type/Type.c | 30 ------------------- .../pljava/internal/Invocation.java | 7 ----- 2 files changed, 37 deletions(-) diff --git a/pljava-so/src/main/c/type/Type.c b/pljava-so/src/main/c/type/Type.c index f7895b93a..698d355b9 100644 --- a/pljava-so/src/main/c/type/Type.c +++ b/pljava-so/src/main/c/type/Type.c @@ -110,30 +110,8 @@ typedef struct Function fn; jobject rowProducer; jobject rowCollector; - /* - * Whether an Invocation instance, the Java counterpart to currentInvocation - * the C struct, has been assigned. One isn't unless it gets asked for, then - * if it is, that's noted here, and (in intermediate row-returning calls) - * cleared in the Invocation struct, to suppress its onExit() behavior when - * those intermediate calls return. So, even though the C currentInvocation - * really is new on each entry from PG, Java will see one Invocation - * instance whose onExit() behavior occurs only at the conclusion of the - * whole sequence of calls. - */ - bool hasDual; } CallContextData; -/* - * Called during evaluation of a set-returning function, at various points after - * calls into Java code could have instantiated an Invocation. - * Does not stash elemType, rowProducer, or rowCollector; those are all - * unconditionally set in the first-call initialization. - */ -static inline void stashCallContext(CallContextData *ctxData) -{ - ctxData->hasDual = currentInvocation->hasDual; -} - /* * Called either at normal completion of a set-returning function, or by the * _endOfSetCB if PostgreSQL doesn't want all the results. @@ -141,7 +119,6 @@ static inline void stashCallContext(CallContextData *ctxData) static void _closeIteration(CallContextData* ctxData) { jobject dummy; - currentInvocation->hasDual = ctxData->hasDual; /* * Why pass 1 as the call_cntr? We won't always have the actual call_cntr @@ -537,8 +514,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( @@ -564,7 +539,6 @@ Datum Type_invokeSRF(Type self, Function fn, PG_FUNCTION_ARGS) context = SRF_PERCALL_SETUP(); ctxData = (CallContextData*)context->user_fctx; currCtx = CurrentMemoryContext; /* save the supplied per-row context */ - currentInvocation->hasDual = ctxData->hasDual; if(JNI_TRUE == pljava_Function_vpcInvoke(ctxData->fn, ctxData->rowProducer, ctxData->rowCollector, (jlong)context->call_cntr, @@ -572,13 +546,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->hasDual = false; SRF_RETURN_NEXT(context, result); } - stashCallContext(ctxData); - /* 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 diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/Invocation.java b/pljava/src/main/java/org/postgresql/pljava/internal/Invocation.java index ef0ba583c..af08e805a 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/Invocation.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/Invocation.java @@ -35,13 +35,6 @@ * 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 From 41eb6152afab26c884baabe5c4b9d6352964ab23 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 22 Jan 2022 22:56:46 -0500 Subject: [PATCH 008/334] Two caching tools: CacheMap and SwitchPointCache CacheMap is a generic class useful for (possibly weak or soft) canonicalizing caches of things that are identified by one or more primitive values. (Writing the key values into a ByteBuffer avoids the allocation involved in boxing them; however, the API as it currently stands might be exceeding that cost with instantiation of lambdas. It should eventually be profiled, and possibly revised into a less tidy, but more efficient, form.) SwitchPointCache is intended for lazily caching numerous values of diverse types, groups of which can be associated with a single SwitchPoint for purposes of invalidation. As currently structured, the SwitchPoints (and their dependent GuardWithTest nodes) do not get stored in static final fields; this may limit HotSpot's ability to optimize them as fully as it could if they did. --- .../postgresql/pljava/internal/Backend.java | 80 +- .../postgresql/pljava/internal/CacheMap.java | 303 ++++++ .../pljava/internal/SwitchPointCache.java | 872 ++++++++++++++++++ 3 files changed, 1254 insertions(+), 1 deletion(-) create mode 100644 pljava/src/main/java/org/postgresql/pljava/internal/CacheMap.java create mode 100644 pljava/src/main/java/org/postgresql/pljava/internal/SwitchPointCache.java 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/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; + } +} From 7655fde084debbaa60e5fa92938d6eb10bd5756c Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 22 Jan 2022 22:57:50 -0500 Subject: [PATCH 009/334] Adapter and org.postgresql.pljava.adt.spi package Adapter is the abstract ancestor of all classes that implement PostgreSQL datatypes for PL/Java, and the adt.spi package contains classes that will be of use to datatype-implementing code: in particular, Datum. PostgreSQL datums are only exposed to Adapters, and the Adapter's job is to reliably convert between the PostgreSQL type and some appropriate Java representation. For some datatypes, there is a single or obvious appropriate Java representation, and an Adapter may be provided that simply produces that. For other datatypes, there may be no single obvious choice of Java representation, either because there is no good match or because there are several; an application might want to map types to specialized classes available in some domain-specific library. To serve those cases, Adapters can be defined in terms of Adapter.Contract subinterfaces, which are simply functional interfaces that document and expose the semantic components of the PostgreSQL type. For example, a contract for PostgreSQL INTERVAL would expose a 64-bit microseconds component, a 32-bit day count, and a 32-bit month count. The division of responsibility is that the Adapter encapsulates how to extract those components given a PostgreSQL datum, but the contract fixes the semantics of what the components are. It is then simple to use the Adapter, with any lambda that conforms to the contract, to produce any desired Java representation of the type. Dummy versions of Attribute, RegClass, RegType, TupleDescriptor, and TupleTableSlot break ground here on the model package, which will consist of a set of classes modeling key PostgreSQL abstractions and a useful subset of the PostgreSQL system catalogs. RegType also implements java.sql.SQLType, making it usable in (a suitable implementation of) JDBC to specify PostgreSQL types precisely. adt.spi.AbstractType needs the specialization() method that was earlier added to internal.Function in anticipation of needing it someday. --- pljava-api/src/main/java/module-info.java | 4 +- .../java/org/postgresql/pljava/Adapter.java | 1181 +++++++++++++++++ .../pljava/adt/spi/AbstractType.java | 795 +++++++++++ .../org/postgresql/pljava/adt/spi/Datum.java | 165 +++ .../pljava/adt/spi/TwosComplement.java | 560 ++++++++ .../postgresql/pljava/adt/spi/Verifier.java | 90 ++ .../pljava/adt/spi/package-info.java | 33 + .../postgresql/pljava/model/Attribute.java | 21 + .../org/postgresql/pljava/model/RegClass.java | 28 + .../org/postgresql/pljava/model/RegType.java | 23 + .../pljava/model/TupleDescriptor.java | 118 ++ .../pljava/model/TupleTableSlot.java | 143 ++ .../postgresql/pljava/model/package-info.java | 55 + .../src/main/resources/pljava.policy | 13 + .../postgresql/pljava/internal/Function.java | 146 +- .../pljava/internal/InstallHelper.java | 5 +- 16 files changed, 3233 insertions(+), 147 deletions(-) create mode 100644 pljava-api/src/main/java/org/postgresql/pljava/Adapter.java create mode 100644 pljava-api/src/main/java/org/postgresql/pljava/adt/spi/AbstractType.java create mode 100644 pljava-api/src/main/java/org/postgresql/pljava/adt/spi/Datum.java create mode 100644 pljava-api/src/main/java/org/postgresql/pljava/adt/spi/TwosComplement.java create mode 100644 pljava-api/src/main/java/org/postgresql/pljava/adt/spi/Verifier.java create mode 100644 pljava-api/src/main/java/org/postgresql/pljava/adt/spi/package-info.java create mode 100644 pljava-api/src/main/java/org/postgresql/pljava/model/Attribute.java create mode 100644 pljava-api/src/main/java/org/postgresql/pljava/model/RegClass.java create mode 100644 pljava-api/src/main/java/org/postgresql/pljava/model/RegType.java create mode 100644 pljava-api/src/main/java/org/postgresql/pljava/model/TupleDescriptor.java create mode 100644 pljava-api/src/main/java/org/postgresql/pljava/model/TupleTableSlot.java create mode 100644 pljava-api/src/main/java/org/postgresql/pljava/model/package-info.java diff --git a/pljava-api/src/main/java/module-info.java b/pljava-api/src/main/java/module-info.java index fbfcb8bd4..c3f3bd299 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-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 @@ -21,7 +21,9 @@ requires transitive java.compiler; exports org.postgresql.pljava; + 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 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..1352fa5b7 --- /dev/null +++ b/pljava-api/src/main/java/org/postgresql/pljava/Adapter.java @@ -0,0 +1,1181 @@ +/* + * 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.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 java.lang.invoke.MethodType; +import static java.lang.invoke.MethodType.methodType; + +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.function.Predicate; + +import org.postgresql.pljava.adt.spi.AbstractType; +import org.postgresql.pljava.adt.spi.AbstractType.Bindings; +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 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 classes that are permitted to instantiate + * this class 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 +{ + final Type m_topType; + /** + * The "under" adapter in the composed case; null in a leaf adapter. + */ + final Adapter m_underAdapter; + 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}. + */ + private , C extends Contract> Adapter( + Configuration configuration, A over, C 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 ) + top = specialization(using.getClass(), Contract.class)[0]; + + MethodHandle mh = leaf.m_fetch.bindTo(this); + Class erased = 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_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; + m_underAdapter = over; + + MethodHandle producer = nonLeaf.m_adapt.bindTo(this); + MethodHandle fetcher = over.m_fetchHandle; + + MethodType mt = producer + .type() + .changeReturnType(erase(top)) + .changeParameterType(1, erase(under)); + + producer = producer.asType(mt); + fetcher = fetcher.asType( + fetcher.type().changeReturnType(mt.parameterType(1))); + + m_fetchHandle = collectArguments(producer, 1, 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(); + } + + public abstract boolean canFetch(RegType pgType); + + public boolean canFetch(Attribute attr) + { + return canFetch(attr.type()); + } + + public abstract boolean canFetchNull(); + + 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. + */ + public Type topType() + { + return m_topType; + } + + 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]; + } + + 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); + } + + 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); + + if ( Primitive.class.isAssignableFrom(cls) ) + { + MethodType mt = methodType(topErased); + assert mt.hasWrappers(); + top = topErased = mt.unwrap().returnType(); + } + + 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().parameterType(0); + 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 a " + fetchName + " method with the " + + "expected signature"); + if ( ! topErased.isAssignableFrom(fetchCandidates[0].getReturnType()) ) + throw new IllegalArgumentException( + cls + " lacks a " + fetchName + " method with the " + + "expected return type"); + + MethodHandle fetcher; + + try + { + fetcher = lookup().unreflect(fetchCandidates[0]); + } + catch ( IllegalAccessException e ) + { + throw new IllegalArgumentException( + cls + " has a " + 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); + } + + Class asFound = fetcher.type().parameterType(1); + 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. + *

+ * If Java's reflection methods on generic types will be 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 + { + 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. + */ + private > As( + Contract.Array using, As adapter, Class 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)); + } + + private static Type refinement( + Contract.Array using, As adapter) + { + Type[] unrefined = + specialization(using.getClass(), Contract.Array.class); + Type result = unrefined[0]; + Type element = unrefined[1]; + return refine(adapter.topType(), element, result)[1]; + } + + 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); + } + } + + @Override + public boolean canFetchNull() + { + return true; + } + + public T fetchNull(Attribute a) throws SQLException + { + return null; + } + } + + public abstract static class Array extends As + { + protected final Contract.Array m_contract; + protected final As m_elementAdapter; + + /** + * Constructor for a leaf array {@code Adapter} that is based on + * a {@code Contract.Array}. + * @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 Class 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, Class 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 + { + private > Primitive(Configuration c, A over) + { + super(c, over, null, null); + } + + @Override + public boolean canFetchNull() + { + return false; + } + } + + public abstract static class AsLong extends Primitive + implements TwosComplement + { + private > AsLong(Configuration c, A over) + { + super(c, over); + } + + 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); + } + } + + public long fetchNull(Attribute a) throws SQLException + { + throw new SQLDataException( + "SQL NULL cannot be returned as Java long", "22002"); + } + + public abstract static class Signed extends AsLong + implements TwosComplement.Signed + { + protected > Signed(Configuration c, A over) + { + super(c, over); + } + } + + public abstract static class Unsigned extends AsLong + implements TwosComplement.Unsigned + { + protected > Unsigned( + Configuration c, A over) + { + super(c, over); + } + } + } + + public abstract static class AsDouble extends Primitive + { + protected > AsDouble(Configuration c, A over) + { + super(c, over); + } + + 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); + } + } + + public double fetchNull(Attribute a) throws SQLException + { + throw new SQLDataException( + "SQL NULL cannot be returned as Java double", "22002"); + } + } + + public abstract static class AsInt extends Primitive + implements TwosComplement + { + private > AsInt(Configuration c, A over) + { + super(c, over); + } + + 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); + } + } + + public int fetchNull(Attribute a) throws SQLException + { + throw new SQLDataException( + "SQL NULL cannot be returned as Java int", "22002"); + } + + public abstract static class Signed extends AsInt + implements TwosComplement.Signed + { + protected > Signed(Configuration c, A over) + { + super(c, over); + } + } + + public abstract static class Unsigned extends AsInt + implements TwosComplement.Unsigned + { + protected > Unsigned( + Configuration c, A over) + { + super(c, over); + } + } + } + + public abstract static class AsFloat extends Primitive + { + protected > AsFloat(Configuration c, A over) + { + super(c, over); + } + + 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); + } + } + + public float fetchNull(Attribute a) throws SQLException + { + throw new SQLDataException( + "SQL NULL cannot be returned as Java float", "22002"); + } + } + + public abstract static class AsShort extends Primitive + implements TwosComplement + { + private > AsShort(Configuration c, A over) + { + super(c, over); + } + + 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); + } + } + + public short fetchNull(Attribute a) throws SQLException + { + throw new SQLDataException( + "SQL NULL cannot be returned as Java short", "22002"); + } + + public abstract static class Signed extends AsShort + implements TwosComplement.Signed + { + protected > Signed(Configuration c, A over) + { + super(c, over); + } + } + + public abstract static class Unsigned extends AsShort + implements TwosComplement.Unsigned + { + protected > Unsigned( + Configuration c, A over) + { + super(c, over); + } + } + } + + public abstract static class AsChar extends Primitive + { + protected > AsChar(Configuration c, A over) + { + super(c, over); + } + + 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); + } + } + + public char fetchNull(Attribute a) throws SQLException + { + throw new SQLDataException( + "SQL NULL cannot be returned as Java char", "22002"); + } + } + + public abstract static class AsByte extends Primitive + implements TwosComplement + { + private > AsByte(Configuration c, A over) + { + super(c, over); + } + + 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); + } + } + + public byte fetchNull(Attribute a) throws SQLException + { + throw new SQLDataException( + "SQL NULL cannot be returned as Java byte", "22002"); + } + + public abstract static class Signed extends AsByte + implements TwosComplement.Signed + { + protected > Signed(Configuration c, A over) + { + super(c, over); + } + } + + public abstract static class Unsigned extends AsByte + implements TwosComplement.Unsigned + { + protected > Unsigned( + Configuration c, A over) + { + super(c, over); + } + } + } + + public abstract static class AsBoolean extends Primitive + { + protected > AsBoolean(Configuration c, A over) + { + super(c, over); + } + + 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); + } + } + + public boolean fetchNull(Attribute a) throws SQLException + { + throw 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 + */ + 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, As 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); + } + + public static class AdapterException extends RuntimeException + { + AdapterException(String message, Throwable cause) + { + super(message, cause, true, false); + } + } + + /** + * 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)); + } + } + } +} 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..54f67707f --- /dev/null +++ b/pljava-api/src/main/java/org/postgresql/pljava/adt/spi/AbstractType.java @@ -0,0 +1,795 @@ +/* + * Copyright (c) 2020-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 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.LinkedList; +import java.util.List; +import java.util.Map; +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)); + } + + /** + * 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)[1]}. + *

+ * 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 + * @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 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(); + } + else if ( rawTypeFound ) + actualArgs = new Type[0]; + + 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 = false; + for ( int i = 0; i < as.length; ++ i ) + { + Type oa = as[i]; + Type na = substitute(b, oa); + if ( na == oa ) + continue; + as[i] = na; + changed = true; + } + + 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 ) + { + throw new UnsupportedOperationException( + "substitute on a wildcard type"); + /* + * Probably just substitute 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 ) + { + /* + * 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()); + } + + 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; + } + } +} 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..69044418e --- /dev/null +++ b/pljava-api/src/main/java/org/postgresql/pljava/adt/spi/Datum.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.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 +{ + /** + * 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() + { + } + + ByteBuffer buffer() throws SQLException; + + 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..84f313665 --- /dev/null +++ b/pljava-api/src/main/java/org/postgresql/pljava/model/Attribute.java @@ -0,0 +1,21 @@ +/* + * 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; + +/** + * An attribute (column), either of a known relation, or of a transient record + * type. + */ +public interface Attribute +{ + RegType type(); +} 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..3be3245a6 --- /dev/null +++ b/pljava-api/src/main/java/org/postgresql/pljava/model/RegClass.java @@ -0,0 +1,28 @@ +/* + * 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.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 +{ +} 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..6f17f5ae7 --- /dev/null +++ b/pljava-api/src/main/java/org/postgresql/pljava/model/RegType.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.model; + +import java.sql.SQLType; + +/** + * Model of a PostgreSQL data type, as defined in the system catalogs. + */ +public interface RegType +extends + SQLType +{ +} 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..ad59eefef --- /dev/null +++ b/pljava-api/src/main/java/org/postgresql/pljava/model/TupleDescriptor.java @@ -0,0 +1,118 @@ +/* + * 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.SQLException; +import java.sql.SQLSyntaxErrorException; // javadoc + +import java.util.List; + +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 +{ + List attributes(); + + 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. + * @throws SQLSyntaxErrorException 42703 if no attribute name matches + */ + Attribute get(Simple name) throws SQLException; + + /** + * 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 + */ + default Attribute get(String name) throws SQLException + { + return get(Simple.fromJava(name)); + } + + /** + * (Convenience method) Retrieve an attribute by its familiar (1-based) + * SQL attribute number. + *

+ * The Java {@link List#get List.get} API uses zero-based numbering, so this + * convenience method is equivalent to {@code attributes().get(attrNum-1)}. + */ + Attribute sqlGet(int attrNum); + + /** + * 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. + *

+ * 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. + *

+ * 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..87fc83170 --- /dev/null +++ b/pljava-api/src/main/java/org/postgresql/pljava/model/TupleTableSlot.java @@ -0,0 +1,143 @@ +/* + * 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.SQLException; + +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; + +/** + * 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(); + + T get(Attribute att, As adapter) throws SQLException; + long get(Attribute att, AsLong adapter) throws SQLException; + double get(Attribute att, AsDouble adapter) throws SQLException; + int get(Attribute att, AsInt adapter) throws SQLException; + float get(Attribute att, AsFloat adapter) throws SQLException; + short get(Attribute att, AsShort adapter) throws SQLException; + char get(Attribute att, AsChar adapter) throws SQLException; + byte get(Attribute att, AsByte adapter) throws SQLException; + boolean get(Attribute att, AsBoolean adapter) throws SQLException; + + T get(int idx, As adapter) throws SQLException; + long get(int idx, AsLong adapter) throws SQLException; + double get(int idx, AsDouble adapter) throws SQLException; + int get(int idx, AsInt adapter) throws SQLException; + float get(int idx, AsFloat adapter) throws SQLException; + short get(int idx, AsShort adapter) throws SQLException; + char get(int idx, AsChar adapter) throws SQLException; + byte get(int idx, AsByte adapter) throws SQLException; + boolean get(int idx, AsBoolean adapter) throws SQLException; + + default T sqlGet(int idx, As adapter) throws SQLException + { + return get(idx - 1, adapter); + } + + default long sqlGet(int idx, AsLong adapter) throws SQLException + { + return get(idx - 1, adapter); + } + + default double sqlGet(int idx, AsDouble adapter) throws SQLException + { + return get(idx - 1, adapter); + } + + default int sqlGet(int idx, AsInt adapter) throws SQLException + { + return get(idx - 1, adapter); + } + + default float sqlGet(int idx, AsFloat adapter) throws SQLException + { + return get(idx - 1, adapter); + } + + default short sqlGet(int idx, AsShort adapter) throws SQLException + { + return get(idx - 1, adapter); + } + + default char sqlGet(int idx, AsChar adapter) throws SQLException + { + return get(idx - 1, adapter); + } + + default byte sqlGet(int idx, AsByte adapter) throws SQLException + { + return get(idx - 1, adapter); + } + + default boolean sqlGet(int idx, AsBoolean adapter) throws SQLException + { + 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(); + } + + /** + * XXX this method is scaffolding for development testing. + */ + Adapter adapterPlease(String clazz, String field) + throws ReflectiveOperationException; +} 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..3e856b924 --- /dev/null +++ b/pljava-api/src/main/java/org/postgresql/pljava/model/package-info.java @@ -0,0 +1,55 @@ +/* + * 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 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. + *

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. + * + * @author Chapman Flack + */ +package org.postgresql.pljava.model; + +import org.postgresql.pljava.Adapter; diff --git a/pljava-packaging/src/main/resources/pljava.policy b/pljava-packaging/src/main/resources/pljava.policy index c360dcdb5..bb63a29fd 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() @@ -86,6 +88,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/src/main/java/org/postgresql/pljava/internal/Function.java b/pljava/src/main/java/org/postgresql/pljava/internal/Function.java index 842ad8153..36eb5c01e 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/Function.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/Function.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 Tada AB and other contributors, as listed below. + * Copyright (c) 2016-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 @@ -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; @@ -1879,145 +1874,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 4e0de5363..476054fa0 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java @@ -164,10 +164,13 @@ public static String hello( System.clearProperty(encodingKey); } - /* 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(); From b35c65a3772c16245c8aadefd4aab55c0fc13cd9 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 22 Jan 2022 22:58:14 -0500 Subject: [PATCH 010/334] Package org.postgresql.pljava.adt: contracts The org.postgresql.pljava.adt package contains 'contracts' (subinterfaces of Adapter.Contract.Scalar or Adapter.Contract.Array), which are functional interfaces that document and expose the exact semantic components of PostgreSQL data types. Adapters are responsible for the internal details of PostgreSQL's representation that aren't semantically important, and code that simply needs to construct some semantically faithful representation of the type only needs to be concerned with the contract. --- pljava-api/src/main/java/module-info.java | 1 + .../java/org/postgresql/pljava/adt/Array.java | 63 ++ .../org/postgresql/pljava/adt/Bitstring.java | 67 ++ .../org/postgresql/pljava/adt/Datetime.java | 596 ++++++++++++++++++ .../org/postgresql/pljava/adt/Geometric.java | 165 +++++ .../org/postgresql/pljava/adt/Internal.java | 41 ++ .../java/org/postgresql/pljava/adt/Money.java | 183 ++++++ .../org/postgresql/pljava/adt/Network.java | 69 ++ .../org/postgresql/pljava/adt/Numeric.java | 96 +++ .../org/postgresql/pljava/adt/Timespan.java | 219 +++++++ .../postgresql/pljava/adt/package-info.java | 75 +++ .../postgresql/pljava/model/package-info.java | 18 + 12 files changed, 1593 insertions(+) create mode 100644 pljava-api/src/main/java/org/postgresql/pljava/adt/Array.java create mode 100644 pljava-api/src/main/java/org/postgresql/pljava/adt/Bitstring.java create mode 100644 pljava-api/src/main/java/org/postgresql/pljava/adt/Datetime.java create mode 100644 pljava-api/src/main/java/org/postgresql/pljava/adt/Geometric.java create mode 100644 pljava-api/src/main/java/org/postgresql/pljava/adt/Internal.java create mode 100644 pljava-api/src/main/java/org/postgresql/pljava/adt/Money.java create mode 100644 pljava-api/src/main/java/org/postgresql/pljava/adt/Network.java create mode 100644 pljava-api/src/main/java/org/postgresql/pljava/adt/Numeric.java create mode 100644 pljava-api/src/main/java/org/postgresql/pljava/adt/Timespan.java create mode 100644 pljava-api/src/main/java/org/postgresql/pljava/adt/package-info.java diff --git a/pljava-api/src/main/java/module-info.java b/pljava-api/src/main/java/module-info.java index c3f3bd299..55667b8d8 100644 --- a/pljava-api/src/main/java/module-info.java +++ b/pljava-api/src/main/java/module-info.java @@ -21,6 +21,7 @@ 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; 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..e474e9a49 --- /dev/null +++ b/pljava-api/src/main/java/org/postgresql/pljava/adt/Array.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.adt; + +import java.sql.SQLException; + +import java.util.ArrayList; +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> + { + /** + * 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(); + List result = new ArrayList<>(n); + for ( int i = 0; i < n; ++ i ) + result.add(slot.get(i, adapter)); + return 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..581dcb4d3 --- /dev/null +++ b/pljava-api/src/main/java/org/postgresql/pljava/adt/Numeric.java @@ -0,0 +1,96 @@ +/* + * 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; + +/** + * 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 +{ + enum Kind { POSITIVE, NEGATIVE, NAN, POSINFINITY, NEGINFINITY } + + /** + * Constructs a representation T from the components + * of the PostgreSQL data type. + * @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); + + /** + * 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); + } +} 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/model/package-info.java b/pljava-api/src/main/java/org/postgresql/pljava/model/package-info.java index 3e856b924..e8ae62451 100644 --- 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 @@ -47,6 +47,24 @@ * 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 */ From 7786fbff92f86bd828320e8f4a0539baf7a9238b Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 22 Jan 2022 23:00:31 -0500 Subject: [PATCH 011/334] CharsetEncoding, CatalogObject, and a few more CharsetEncoding is not really a catalog object (the available encodings in PostgreSQL are hardcoded) but is exposed here as a similar kind of object with useful operations, including encoding and decoding using the corresponding Java codec when known. CatalogObject is, of course, the superinterface of all things that really are catalog objects (identified by a classId, an objectId, and rarely a subId). This commit brings in RegNamespace and RegRole as needed for CatalogObject.Namespaced and CatalogObject.Owned. RolePrincipal is a bridge between a RegRole and Java's Principal interface. CatalogObject.Factory is a service interface 'used' by the API module, and will be 'provided' by the internals module to supply the implementations of these things. --- pljava-api/src/main/java/module-info.java | 2 + .../org/postgresql/pljava/RolePrincipal.java | 234 ++++++++ .../postgresql/pljava/model/Attribute.java | 20 + .../pljava/model/CatalogObject.java | 555 ++++++++++++++++++ .../pljava/model/CharsetEncoding.java | 299 ++++++++++ .../org/postgresql/pljava/model/RegClass.java | 92 +++ .../postgresql/pljava/model/RegNamespace.java | 33 ++ .../org/postgresql/pljava/model/RegRole.java | 150 +++++ .../org/postgresql/pljava/model/RegType.java | 25 + .../postgresql/pljava/model/package-info.java | 69 ++- 10 files changed, 1477 insertions(+), 2 deletions(-) create mode 100644 pljava-api/src/main/java/org/postgresql/pljava/RolePrincipal.java create mode 100644 pljava-api/src/main/java/org/postgresql/pljava/model/CatalogObject.java create mode 100644 pljava-api/src/main/java/org/postgresql/pljava/model/CharsetEncoding.java create mode 100644 pljava-api/src/main/java/org/postgresql/pljava/model/RegNamespace.java create mode 100644 pljava-api/src/main/java/org/postgresql/pljava/model/RegRole.java diff --git a/pljava-api/src/main/java/module-info.java b/pljava-api/src/main/java/module-info.java index 55667b8d8..e1e9dd519 100644 --- a/pljava-api/src/main/java/module-info.java +++ b/pljava-api/src/main/java/module-info.java @@ -32,6 +32,8 @@ 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/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/model/Attribute.java b/pljava-api/src/main/java/org/postgresql/pljava/model/Attribute.java index 84f313665..689d0f058 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/model/Attribute.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/model/Attribute.java @@ -11,11 +11,31 @@ */ 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; + /** * An attribute (column), either of a known relation, or of a transient record * type. */ 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); + RegType type(); } 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..b01959989 --- /dev/null +++ b/pljava-api/src/main/java/org/postgresql/pljava/model/CatalogObject.java @@ -0,0 +1,555 @@ +/* + * 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 java.util.ServiceLoader; + +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 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 + */ + abstract class Factory + { + static final Factory INSTANCE; + + static + { + INSTANCE = ServiceLoader + .load(Factory.class, Factory.class.getClassLoader()) + .iterator().next(); + } + + 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); + } + + 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); + + 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; + + /* + * 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; + } +} 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/RegClass.java b/pljava-api/src/main/java/org/postgresql/pljava/model/RegClass.java index 3be3245a6..6580760f5 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/model/RegClass.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/model/RegClass.java @@ -13,6 +13,10 @@ 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; /** @@ -24,5 +28,93 @@ * 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/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/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 index 6f17f5ae7..38adaf5ed 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/model/RegType.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/model/RegType.java @@ -13,11 +13,36 @@ import java.sql.SQLType; +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 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); } 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 index e8ae62451..e59bc6793 100644 --- 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 @@ -14,8 +14,9 @@ * and related PostgreSQL abstractions for convenient Java access. *

CatalogObject and its subinterfaces

*

- * The bulk of this package consists of interfaces corresponding to various - * database objects represented in the PostgreSQL system catalogs. + * 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 @@ -28,6 +29,70 @@ * {@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. *

TupleTableSlot, TupleDescriptor, and Adapter

*

* {@code TupleTableSlot} in PostgreSQL is a flexible abstraction that can From 42805b19e56a434b49bf408904c41fbf00a0058f Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 22 Jan 2022 23:00:55 -0500 Subject: [PATCH 012/334] Add enough implementation for CharsetEncoding use And convert other code to use CharsetEncoding.SERVER_ENCODING where earlier hacks were used, like the implServerCharset() added to Session in 1.5.1. In passing, fix a bit of overlooked java7ification in SQLXMLImpl. The new CharsetEncodings example provides two functions: SELECT * FROM javatest.charsets(); returns a table of the available PostgreSQL encodings, and what Java encodings they could be matched up with. SELECT * FROM javatest.java_charsets(try_aliases); returns the table of all available Java charsets and the PostgreSQL ones they could be matched up with, where the boolean try_aliases indicates whether to try Java's known aliases for a charset when nothing in PostgreSQL matched its canonical name. False matches happen when try_aliases is true, so that's not a great idea. --- .../org/postgresql/pljava/model/RegType.java | 120 ++++ .../example/annotation/CharsetEncodings.java | 199 ++++++ .../pljava/example/annotation/PassXML.java | 7 +- pljava-so/src/main/c/Backend.c | 4 + pljava-so/src/main/c/ModelConstants.c | 418 +++++++++++++ pljava-so/src/main/c/ModelUtils.c | 172 ++++++ pljava-so/src/main/include/pljava/JNICalls.h | 8 +- .../src/main/include/pljava/ModelConstants.h | 26 + .../src/main/include/pljava/ModelUtils.h | 30 + pljava/src/main/java/module-info.java | 5 +- .../pljava/internal/InstallHelper.java | 18 +- .../postgresql/pljava/internal/Session.java | 25 - .../pljava/internal/VarlenaXMLRenderer.java | 19 +- .../postgresql/pljava/jdbc/SQLXMLImpl.java | 37 +- .../postgresql/pljava/pg/AttributeImpl.java | 167 ++++++ .../pljava/pg/CatalogObjectImpl.java | 565 ++++++++++++++++++ .../pljava/pg/CharsetEncodingImpl.java | 256 ++++++++ .../postgresql/pljava/pg/ModelConstants.java | 445 ++++++++++++++ .../postgresql/pljava/pg/RegClassImpl.java | 159 +++++ .../pljava/pg/RegNamespaceImpl.java | 25 + .../org/postgresql/pljava/pg/RegRoleImpl.java | 110 ++++ .../org/postgresql/pljava/pg/RegTypeImpl.java | 74 +++ .../postgresql/pljava/pg/TupleDescImpl.java | 182 ++++++ .../pljava/pg/TupleTableSlotImpl.java | 285 +++++++++ .../postgresql/pljava/pg/package-info.java | 18 + 25 files changed, 3286 insertions(+), 88 deletions(-) create mode 100644 pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/CharsetEncodings.java create mode 100644 pljava-so/src/main/c/ModelConstants.c create mode 100644 pljava-so/src/main/c/ModelUtils.c create mode 100644 pljava-so/src/main/include/pljava/ModelConstants.h create mode 100644 pljava-so/src/main/include/pljava/ModelUtils.h create mode 100644 pljava/src/main/java/org/postgresql/pljava/pg/AttributeImpl.java create mode 100644 pljava/src/main/java/org/postgresql/pljava/pg/CatalogObjectImpl.java create mode 100644 pljava/src/main/java/org/postgresql/pljava/pg/CharsetEncodingImpl.java create mode 100644 pljava/src/main/java/org/postgresql/pljava/pg/ModelConstants.java create mode 100644 pljava/src/main/java/org/postgresql/pljava/pg/RegClassImpl.java create mode 100644 pljava/src/main/java/org/postgresql/pljava/pg/RegNamespaceImpl.java create mode 100644 pljava/src/main/java/org/postgresql/pljava/pg/RegRoleImpl.java create mode 100644 pljava/src/main/java/org/postgresql/pljava/pg/RegTypeImpl.java create mode 100644 pljava/src/main/java/org/postgresql/pljava/pg/TupleDescImpl.java create mode 100644 pljava/src/main/java/org/postgresql/pljava/pg/TupleTableSlotImpl.java create mode 100644 pljava/src/main/java/org/postgresql/pljava/pg/package-info.java 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 index 38adaf5ed..ed6269283 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/model/RegType.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/model/RegType.java @@ -45,4 +45,124 @@ public interface RegType { 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); + 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); + + /** + * 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-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/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-so/src/main/c/Backend.c b/pljava-so/src/main/c/Backend.c index b06123deb..eca7a3a38 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" @@ -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(); Function_initialize(); Session_initialize(); PgSavepoint_initialize(); diff --git a/pljava-so/src/main/c/ModelConstants.c b/pljava-so/src/main/c/ModelConstants.c new file mode 100644 index 000000000..61604f0ec --- /dev/null +++ b/pljava-so/src/main/c/ModelConstants.c @@ -0,0 +1,418 @@ +/* + * 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 + */ + +#include + +#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 "pljava/PgObject.h" +#include "pljava/ModelConstants.h" + +/* + * 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(SIZEOF_DATUM), + 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)), + + 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(SIZEOF_FORM_PG_ATTRIBUTE, sizeof (FormData_pg_attribute)), + CONSTANT(ATTRIBUTE_FIXED_PART_SIZE), + CONSTANT(CLASS_TUPLE_SIZE), + CONSTANT(HEAPTUPLESIZE), + + 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)), + + 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( Anum_pg_class_reltype ), + + CONSTANTEXPR(SIZEOF_MCTX, sizeof (MemoryContextData)), + TYPEOFFSET(MemoryContextData, MCTX, isReset), + TYPEOFFSET(MemoryContextData, MCTX, mem_allocated), + TYPEOFFSET(MemoryContextData, MCTX, parent), + TYPEOFFSET(MemoryContextData, MCTX, firstchild), + TYPEOFFSET(MemoryContextData, MCTX, prevchild), + TYPEOFFSET(MemoryContextData, MCTX, nextchild), + TYPEOFFSET(MemoryContextData, MCTX, name), + TYPEOFFSET(MemoryContextData, MCTX, ident), + + 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), +}; + +#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 ); + CONFIRMCONST( REGCOLLATIONOID ); + + /* + * 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_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 ); + + CONFIRMCONST( Anum_pg_extension_oid ); + 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 ); + + CONFIRMEXPR( OFFSET_ArrayType_DIMS, + (((char*)ARR_DIMS(0)) - (char *)0) - VARHDRSZ ); + 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 ); + + 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) +{ + 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); +} + +/* + * 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..668db54dc --- /dev/null +++ b/pljava-so/src/main/c/ModelUtils.c @@ -0,0 +1,172 @@ +/* + * 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 + */ + +#include +#include +#include +#include +#include +#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_pg_CharsetEncodingImpl_EarlyNatives.h" + +/* + * 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. + */ + +void pljava_ModelUtils_initialize(void) +{ + jclass cls; + + 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 } + }; + + cls = PgObject_getJavaClass("org/postgresql/pljava/pg/CharsetEncodingImpl$EarlyNatives"); + PgObject_registerNatives2(cls, charsetMethods); + JNI_deleteLocalRef(cls); +} + +/* + * 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; +} 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..bd5bbdfa6 --- /dev/null +++ b/pljava-so/src/main/include/pljava/ModelUtils.h @@ -0,0 +1,30 @@ +/* + * 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_ModelUtils_h +#define __pljava_ModelUtils_h + +#include +#include +#include + +#include "pljava/pljava.h" + +#ifdef __cplusplus +extern "C" { +#endif + +extern void pljava_ModelUtils_initialize(void); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/pljava/src/main/java/module-info.java b/pljava/src/main/java/module-info.java index 68923bbe4..974bed643 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-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,4 +35,7 @@ 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/InstallHelper.java b/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java index 476054fa0..6150579bd 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015-2021 Tada AB and other contributors, as listed below. + * Copyright (c) 2015-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 @@ -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,20 +150,7 @@ 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 they can be granted permissions in the pljava policy */ System.setProperty( "org.postgresql.pljava.codesource", 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/VarlenaXMLRenderer.java b/pljava/src/main/java/org/postgresql/pljava/internal/VarlenaXMLRenderer.java index 11bc27548..d38a415c1 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-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 @@ -20,6 +20,8 @@ import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; +import static org.postgresql.pljava.model.CharsetEncoding.SERVER_ENCODING; + /** * Class adapting a {@code ByteBufferXMLReader} to a * {@code VarlenaWrapper.Input}. @@ -42,20 +44,7 @@ public abstract class VarlenaXMLRenderer public VarlenaXMLRenderer(VarlenaWrapper.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(); } 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..15e815f9d 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-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 @@ -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; @@ -694,7 +697,7 @@ static abstract class Readable 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 @@ -724,14 +727,6 @@ private Readable(V vwi, int oid) throws SQLException { super(vwi); 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 @@ -1225,7 +1220,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 +1239,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() @@ -1541,7 +1524,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 +3453,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 +3611,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/pg/AttributeImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/AttributeImpl.java new file mode 100644 index 000000000..7dd8cfb8c --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/pg/AttributeImpl.java @@ -0,0 +1,167 @@ +/* + * 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.sql.SQLException; + +import static java.util.Objects.requireNonNull; + +import org.postgresql.pljava.model.*; + +import static org.postgresql.pljava.pg.CatalogObjectImpl.*; +import static org.postgresql.pljava.pg.ModelConstants.*; +import static org.postgresql.pljava.pg.TupleDescImpl.Ephemeral; + +import org.postgresql.pljava.sqlgen.Lexicals.Identifier.Simple; + +abstract class AttributeImpl extends Addressed +implements + Nonshared, Named, + AccessControlled, Attribute +{ + @Override + public RegType type() + { + throw notyet(); + } + + boolean foundIn(TupleDescriptor td) + { + return this == td.attributes().get(subId() - 1); + } + + /** + * 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 + { + } + + /** + * 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 final TupleDescriptor m_containingTupleDescriptor; + private final int m_attnum; + + Transient(TupleDescriptor td, int attnum) + { + m_containingTupleDescriptor = requireNonNull(td); + assert 0 < attnum : "nonpositive attnum in transient attribute"; + m_attnum = attnum; + } + + @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 + 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; + } + } +} 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..347bd5b22 --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/pg/CatalogObjectImpl.java @@ -0,0 +1,565 @@ +/* + * 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 org.postgresql.pljava.Adapter; +import org.postgresql.pljava.Adapter.As; + +import org.postgresql.pljava.internal.CacheMap; +import static org.postgresql.pljava.internal.UncheckedException.unchecked; + +import org.postgresql.pljava.model.*; + +import org.postgresql.pljava.sqlgen.Lexicals.Identifier; + +import java.lang.annotation.Native; + +import static java.lang.ref.Reference.reachabilityFence; + +import java.nio.ByteBuffer; +import static java.nio.ByteOrder.nativeOrder; + +import java.sql.SQLException; + +import java.util.List; +import java.util.Set; +import java.util.Optional; +import java.util.function.Consumer; +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) + { + return staticFormObjectId(classId, objId); + } + + @Override + protected RegRole.Grantee publicGranteeImpl() + { + return (RegRole.Grantee)form(AuthIdRelationId, InvalidOid, 0); + } + + @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); + } + + @SuppressWarnings("unchecked") + static > RegClass.Known + staticFormClassId(int classId, Class clazz) + { + return (RegClass.Known)form(RelationRelationId, classId, 0); + } + + @SuppressWarnings("unchecked") + static > + T staticFormObjectId(RegClass.Known classId, int objId) + { + return (T)form(classId.oid(), objId, 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 AuthIdRelationId: + fieldRead = RegRole.CLASSID; + return RegRoleImpl::new; + case NamespaceRelationId: + fieldRead = RegNamespace.CLASSID; + return RegNamespaceImpl::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 + } + } + } + + @SuppressWarnings("unchecked") + static class Addressed> + extends CatalogObjectImpl implements CatalogObject.Addressed + { + @Override + public RegClass.Known classId() + { + return CatalogObjectImpl.Factory.staticFormClassId( + classOid(), (Class)getClass()); + } + + @Override + public boolean exists() + { + throw notyet(); + } + } + + /** + * 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() + { + throw notyet(); + } + } + + interface Namespaced> + extends Named, CatalogObject.Namespaced + { + @Override + default RegNamespace namespace() + { + throw notyet(); + } + } + + interface Owned extends CatalogObject.Owned + { + @Override + default RegRole owner() + { + throw notyet(); + } + } + + interface AccessControlled + extends CatalogObject.AccessControlled + { + @Override + default List grants() + { + throw notyet(); + } + + @Override + default List grants(RegRole grantee) + { + throw notyet(); + } + } + + 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); + } +} 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/ModelConstants.java b/pljava/src/main/java/org/postgresql/pljava/pg/ModelConstants.java new file mode 100644 index 000000000..6dc6bf23e --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/pg/ModelConstants.java @@ -0,0 +1,445 @@ +/* + * 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 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. This native method will return 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. + * + * 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 + { + 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 the indices into the 'statics' array where the various + * wanted values should be placed. Edits should keep them consecutive + * distinct small array indices; the checked() function in the static + * initializer will be checking for gaps or repeats. + */ + @Native private static final int IDX_SIZEOF_DATUM = 0; + @Native private static final int IDX_SIZEOF_SIZE = 1; + + @Native private static final int IDX_ALIGNOF_SHORT = 2; + @Native private static final int IDX_ALIGNOF_INT = 3; + @Native private static final int IDX_ALIGNOF_DOUBLE = 4; + @Native private static final int IDX_MAXIMUM_ALIGNOF = 5; + + @Native private static final int IDX_NAMEDATALEN = 6; + + @Native private static final int IDX_SIZEOF_varatt_indirect = 7; + @Native private static final int IDX_SIZEOF_varatt_expanded = 8; + @Native private static final int IDX_SIZEOF_varatt_external = 9; + + @Native private static final int IDX_OFFSET_TTS_NVALID = 10; + @Native private static final int IDX_SIZEOF_TTS_NVALID = 11; + + @Native private static final int IDX_TTS_FLAG_EMPTY = 12; + @Native private static final int IDX_TTS_FLAG_FIXED = 13; + @Native private static final int IDX_OFFSET_TTS_FLAGS = 14; + + /* + * 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 = 15; + @Native private static final int IDX_OFFSET_TTS_FIXED = 16; + @Native private static final int IDX_OFFSET_TTS_TABLEOID = 17; + + @Native private static final int IDX_SIZEOF_FORM_PG_ATTRIBUTE = 18; + @Native private static final int IDX_ATTRIBUTE_FIXED_PART_SIZE = 19; + @Native private static final int IDX_CLASS_TUPLE_SIZE = 20; + @Native private static final int IDX_HEAPTUPLESIZE = 21; + + @Native private static final int IDX_OFFSET_TUPLEDESC_ATTRS = 22; + @Native private static final int IDX_OFFSET_TUPLEDESC_TDREFCOUNT = 23; + @Native private static final int IDX_SIZEOF_TUPLEDESC_TDREFCOUNT = 24; + @Native private static final int IDX_OFFSET_TUPLEDESC_TDTYPEID = 25; + @Native private static final int IDX_OFFSET_TUPLEDESC_TDTYPMOD = 26; + + @Native private static final int IDX_OFFSET_pg_attribute_atttypid = 27; + @Native private static final int IDX_OFFSET_pg_attribute_attlen = 28; + @Native private static final int IDX_OFFSET_pg_attribute_attcacheoff = 29; + @Native private static final int IDX_OFFSET_pg_attribute_atttypmod = 30; + @Native private static final int IDX_OFFSET_pg_attribute_attbyval = 31; + @Native private static final int IDX_OFFSET_pg_attribute_attalign = 32; + @Native private static final int IDX_OFFSET_pg_attribute_attnotnull = 33; + @Native private static final int IDX_OFFSET_pg_attribute_attisdropped = 34; + + @Native private static final int IDX_Anum_pg_class_reltype = 35; + + @Native private static final int IDX_SIZEOF_MCTX = 36; + @Native private static final int IDX_OFFSET_MCTX_isReset = 37; + @Native private static final int IDX_OFFSET_MCTX_mem_allocated = 38; + @Native private static final int IDX_OFFSET_MCTX_parent = 39; + @Native private static final int IDX_OFFSET_MCTX_firstchild = 40; + @Native private static final int IDX_OFFSET_MCTX_prevchild = 41; + @Native private static final int IDX_OFFSET_MCTX_nextchild = 42; + @Native private static final int IDX_OFFSET_MCTX_name = 43; + @Native private static final int IDX_OFFSET_MCTX_ident = 44; + + /* + * 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 = 45; + @Native private static final int IDX_AUTHMEMMEMROLE = 46; + @Native private static final int IDX_AUTHMEMROLEMEM = 47; + @Native private static final int IDX_AUTHOID = 48; + @Native private static final int IDX_COLLOID = 49; + @Native private static final int IDX_DATABASEOID = 50; + @Native private static final int IDX_LANGOID = 51; + @Native private static final int IDX_NAMESPACEOID = 52; + @Native private static final int IDX_OPEROID = 53; + @Native private static final int IDX_PROCOID = 54; + @Native private static final int IDX_RELOID = 55; + @Native private static final int IDX_TSCONFIGOID = 56; + @Native private static final int IDX_TSDICTOID = 57; + @Native private static final int IDX_TYPEOID = 58; + + /* + * These public statics are the values of interest, set at class + * initialization time by reading them from the buffer returned by _statics. + */ + + public static final int SIZEOF_DATUM; + 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 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 SIZEOF_FORM_PG_ATTRIBUTE; + public static final int ATTRIBUTE_FIXED_PART_SIZE; + public static final int CLASS_TUPLE_SIZE; + public static final int HEAPTUPLESIZE; + + 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 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 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 + + /* + * 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 + + /** + * 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 + { + IntBuffer b = + Natives._statics() + .asReadOnlyBuffer().order(nativeOrder()).asIntBuffer(); + + SIZEOF_DATUM = checked(b, IDX_SIZEOF_DATUM); + SIZEOF_SIZE = checked(b, IDX_SIZEOF_SIZE); + + ALIGNOF_SHORT = checked(b, IDX_ALIGNOF_SHORT); + ALIGNOF_INT = checked(b, IDX_ALIGNOF_INT); + ALIGNOF_DOUBLE = checked(b, IDX_ALIGNOF_DOUBLE); + MAXIMUM_ALIGNOF = checked(b, IDX_MAXIMUM_ALIGNOF); + + int n = checked(b, IDX_NAMEDATALEN); + NAMEDATALEN = (short)n; + assert n == NAMEDATALEN; + + SIZEOF_varatt_indirect = checked(b, IDX_SIZEOF_varatt_indirect); + SIZEOF_varatt_expanded = checked(b, IDX_SIZEOF_varatt_expanded); + SIZEOF_varatt_external = checked(b, IDX_SIZEOF_varatt_external); + + OFFSET_TTS_NVALID = checked(b, IDX_OFFSET_TTS_NVALID); + SIZEOF_TTS_NVALID = checked(b, IDX_SIZEOF_TTS_NVALID); + + TTS_FLAG_EMPTY = checked(b, IDX_TTS_FLAG_EMPTY); + TTS_FLAG_FIXED = checked(b, IDX_TTS_FLAG_FIXED); + OFFSET_TTS_FLAGS = checked(b, IDX_OFFSET_TTS_FLAGS); + + OFFSET_TTS_EMPTY = checked(b, IDX_OFFSET_TTS_EMPTY); + OFFSET_TTS_FIXED = checked(b, IDX_OFFSET_TTS_FIXED); + + OFFSET_TTS_TABLEOID = checked(b, IDX_OFFSET_TTS_TABLEOID); + + SIZEOF_FORM_PG_ATTRIBUTE = checked(b, IDX_SIZEOF_FORM_PG_ATTRIBUTE); + ATTRIBUTE_FIXED_PART_SIZE = checked(b, IDX_ATTRIBUTE_FIXED_PART_SIZE); + CLASS_TUPLE_SIZE = checked(b, IDX_CLASS_TUPLE_SIZE); + HEAPTUPLESIZE = checked(b, IDX_HEAPTUPLESIZE); + + OFFSET_TUPLEDESC_ATTRS = checked(b, IDX_OFFSET_TUPLEDESC_ATTRS); + OFFSET_TUPLEDESC_TDREFCOUNT= checked(b,IDX_OFFSET_TUPLEDESC_TDREFCOUNT); + SIZEOF_TUPLEDESC_TDREFCOUNT= checked(b,IDX_SIZEOF_TUPLEDESC_TDREFCOUNT); + OFFSET_TUPLEDESC_TDTYPEID = checked(b, IDX_OFFSET_TUPLEDESC_TDTYPEID); + OFFSET_TUPLEDESC_TDTYPMOD = checked(b, IDX_OFFSET_TUPLEDESC_TDTYPMOD); + + OFFSET_pg_attribute_atttypid + = checked(b, IDX_OFFSET_pg_attribute_atttypid); + OFFSET_pg_attribute_attlen + = checked(b, IDX_OFFSET_pg_attribute_attlen); + OFFSET_pg_attribute_attcacheoff + = checked(b, IDX_OFFSET_pg_attribute_attcacheoff); + OFFSET_pg_attribute_atttypmod + = checked(b, IDX_OFFSET_pg_attribute_atttypmod); + OFFSET_pg_attribute_attbyval + = checked(b, IDX_OFFSET_pg_attribute_attbyval); + OFFSET_pg_attribute_attalign + = checked(b, IDX_OFFSET_pg_attribute_attalign); + OFFSET_pg_attribute_attnotnull + = checked(b, IDX_OFFSET_pg_attribute_attnotnull); + OFFSET_pg_attribute_attisdropped + = checked(b, IDX_OFFSET_pg_attribute_attisdropped); + + Anum_pg_class_reltype = checked(b, IDX_Anum_pg_class_reltype); + + SIZEOF_MCTX = checked(b, IDX_SIZEOF_MCTX); + OFFSET_MCTX_isReset = checked(b, IDX_OFFSET_MCTX_isReset); + OFFSET_MCTX_mem_allocated = checked(b, IDX_OFFSET_MCTX_mem_allocated); + OFFSET_MCTX_parent = checked(b, IDX_OFFSET_MCTX_parent); + OFFSET_MCTX_firstchild = checked(b, IDX_OFFSET_MCTX_firstchild); + OFFSET_MCTX_prevchild = checked(b, IDX_OFFSET_MCTX_prevchild); + OFFSET_MCTX_nextchild = checked(b, IDX_OFFSET_MCTX_nextchild); + OFFSET_MCTX_name = checked(b, IDX_OFFSET_MCTX_name); + OFFSET_MCTX_ident = checked(b, IDX_OFFSET_MCTX_ident); + + ATTNUM = checked(b, IDX_ATTNUM); + AUTHMEMMEMROLE = checked(b, IDX_AUTHMEMMEMROLE); + AUTHMEMROLEMEM = checked(b, IDX_AUTHMEMROLEMEM); + AUTHOID = checked(b, IDX_AUTHOID); + COLLOID = checked(b, IDX_COLLOID); + DATABASEOID = checked(b, IDX_DATABASEOID); + LANGOID = checked(b, IDX_LANGOID); + NAMESPACEOID = checked(b, IDX_NAMESPACEOID); + OPEROID = checked(b, IDX_OPEROID); + PROCOID = checked(b, IDX_PROCOID); + RELOID = checked(b, IDX_RELOID); + TSCONFIGOID = checked(b, IDX_TSCONFIGOID); + TSDICTOID = checked(b, IDX_TSDICTOID); + TYPEOID = checked(b, IDX_TYPEOID); + + if ( 0 != b.remaining() ) + throw new ConstantsError(); + } + + private static int checked(IntBuffer b, int index) + { + try + { + if ( b.position() != index << 1 || index != b.get() ) + throw new ConstantsError(); + return b.get(); + } + catch ( Exception e ) + { + throw (ConstantsError)new ConstantsError().initCause(e); + } + } + + 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/RegClassImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/RegClassImpl.java new file mode 100644 index 000000000..f1ed7d311 --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/pg/RegClassImpl.java @@ -0,0 +1,159 @@ +/* + * 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.util.List; + +import org.postgresql.pljava.model.*; + +import org.postgresql.pljava.pg.CatalogObjectImpl.*; + +import org.postgresql.pljava.sqlgen.Lexicals.Identifier.Simple; + +/* + * 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 + { + } + + RegClassImpl() + { + } + + @Override + public TupleDescriptor.Interned tupleDescriptor() + { + throw notyet(); + } + + @Override + public RegType type() + { + throw notyet(); + } + + @Override + public RegType ofType() + { + throw notyet(); + } + + // am + // filenode + // tablespace + + /* Of limited interest ... estimates used by planner + * + int pages(); + float tuples(); + int allVisible(); + */ + + @Override + public RegClass toastRelation() + { + throw notyet(); + } + + @Override + public boolean hasIndex() + { + throw notyet(); + } + + @Override + public boolean isShared() + { + throw notyet(); + } + + // persistence + // kind + + @Override + public short nAttributes() + { + throw notyet(); + } + + @Override + public short checks() + { + throw notyet(); + } + + @Override + public boolean hasRules() + { + throw notyet(); + } + + @Override + public boolean hasTriggers() + { + throw notyet(); + } + + @Override + public boolean hasSubclass() + { + throw notyet(); + } + + @Override + public boolean rowSecurity() + { + throw notyet(); + } + + @Override + public boolean forceRowSecurity() + { + throw notyet(); + } + + @Override + public boolean isPopulated() + { + throw notyet(); + } + + // replident + + @Override + public boolean isPartition() + { + throw notyet(); + } + + // rewrite + // frozenxid + // minmxid + + @Override + public List options() + { + throw notyet(); + } + + // partbound +} 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..856e68206 --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/pg/RegNamespaceImpl.java @@ -0,0 +1,25 @@ +/* + * 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 org.postgresql.pljava.model.*; + +import org.postgresql.pljava.pg.CatalogObjectImpl.*; + +import org.postgresql.pljava.sqlgen.Lexicals.Identifier.Simple; + +class RegNamespaceImpl extends Addressed +implements + Nonshared, Named, Owned, + AccessControlled, RegNamespace +{ +} 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..821df6d3f --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/pg/RegRoleImpl.java @@ -0,0 +1,110 @@ +/* + * 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.file.attribute.GroupPrincipal; +import java.nio.file.attribute.UserPrincipal; + +import java.util.List; + +import org.postgresql.pljava.RolePrincipal; + +import org.postgresql.pljava.model.*; + +import org.postgresql.pljava.pg.CatalogObjectImpl.*; + +import org.postgresql.pljava.sqlgen.Lexicals.Identifier.Simple; + +/** + * 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 +{ + /* API methods */ + + @Override + public List memberOf() + { + throw notyet(); + } + + @Override + public boolean superuser() + { + throw notyet(); + } + + @Override + public boolean inherit() + { + throw notyet(); + } + + @Override + public boolean createRole() + { + throw notyet(); + } + + @Override + public boolean createDB() + { + throw notyet(); + } + + @Override + public boolean canLogIn() + { + throw notyet(); + } + + @Override + public boolean replication() + { + throw notyet(); + } + + @Override + public boolean bypassRLS() + { + throw notyet(); + } + + @Override + public int connectionLimit() + { + throw notyet(); + } + + /* 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..e946942a4 --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/pg/RegTypeImpl.java @@ -0,0 +1,74 @@ +/* + * 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.sql.SQLType; + +import org.postgresql.pljava.model.*; + +import org.postgresql.pljava.pg.CatalogObjectImpl.*; + +import org.postgresql.pljava.sqlgen.Lexicals.Identifier.Simple; + +/* + * 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 +{ + + /** + * 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 + { + } + + /** + * 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 + { + Modified(NoModifier base) + { + } + } + + /** + * 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 + { + } +} 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..e79a51e94 --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/pg/TupleDescImpl.java @@ -0,0 +1,182 @@ +/* + * 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 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.pg.CatalogObjectImpl.*; +import static org.postgresql.pljava.pg.ModelConstants.*; + +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; + +/** + * 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 +{ + private final Attribute[] m_attrs = null; + private final Map m_byName = Map.of(); + + @Override + public List attributes() + { + return this; + } + + @Override + public Attribute get(Simple name) throws SQLException + { + /* + * computeIfAbsent would be notationally simple here, but its guarantees + * aren't needed (this isn't a place where uniqueness needs to be + * enforced) and it's tricky to rule out that some name() call in the + * update could recursively end up here. So the longer check, compute, + * putIfAbsent is good enough. + */ + Attribute found = m_byName.get(name); + if ( null != found ) + return found; + + for ( int i = m_byName.size() ; i < m_attrs.length ; ++ i ) + { + Attribute a = m_attrs[i]; + Simple n = a.name(); + Attribute other = m_byName.putIfAbsent(n, a); + assert null == other || found == other + : "TupleDescriptor name cache"; + if ( ! name.equals(n) ) + continue; + found = a; + break; + } + + if ( null == found ) + throw new SQLSyntaxErrorException( + "no such column: " + name, "42703"); + + return found; + } + + @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 + { + @Override + public RegType rowType() + { + throw notyet(); + } + } + + static class Blessed extends TupleDescImpl implements Interned + { + @Override + public RegType rowType() + { + throw notyet(); + } + } + + static class Ephemeral extends TupleDescImpl + implements TupleDescriptor.Ephemeral + { + @Override + public RegType rowType() + { + return RECORD; + } + + @Override + public Interned intern() + { + throw notyet(); + } + } + + static class OfType extends TupleDescImpl + implements TupleDescriptor.Ephemeral + { + OfType(RegType type) + { + } + + @Override + public RegType rowType() + { + return RECORD; + } + + @Override + public Interned intern() + { + throw notyet(); + } + } +} 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..8f53c55f2 --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/pg/TupleTableSlotImpl.java @@ -0,0 +1,285 @@ +/* + * 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.lang.annotation.Native; + +import java.nio.ByteBuffer; + +import java.util.List; + +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.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.CatalogObjectImpl.Factory.staticFormObjectId; + +import static org.postgresql.pljava.pg.ModelConstants.*; + +import java.lang.reflect.Field; + +/* + * 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; + + @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 + + @Override // XXX testing + public Adapter adapterPlease(String cname, String field) + throws ReflectiveOperationException + { + @SuppressWarnings("unchecked") + Class cls = + (Class)Class.forName(cname); + Field f = cls.getField(field); + return (Adapter)f.get(null); + } + + @Override + public RegClass relation() + { + throw notyet(); + } + + @Override + public TupleDescriptor descriptor() + { + throw notyet(); + } + + @Override + public T get(Attribute att, As adapter) throws SQLException + { + throw notyet(); + } + + @Override + public long get(Attribute att, AsLong adapter) throws SQLException + { + throw notyet(); + } + + @Override + public double get(Attribute att, AsDouble adapter) throws SQLException + { + throw notyet(); + } + + @Override + public int get(Attribute att, AsInt adapter) throws SQLException + { + throw notyet(); + } + + @Override + public float get(Attribute att, AsFloat adapter) throws SQLException + { + throw notyet(); + } + + @Override + public short get(Attribute att, AsShort adapter) throws SQLException + { + throw notyet(); + } + + @Override + public char get(Attribute att, AsChar adapter) throws SQLException + { + throw notyet(); + } + + @Override + public byte get(Attribute att, AsByte adapter) throws SQLException + { + throw notyet(); + } + + @Override + public boolean get(Attribute att, AsBoolean adapter) throws SQLException + { + throw notyet(); + } + + @Override + public T get(int idx, As adapter) throws SQLException + { + throw notyet(); + } + + @Override + public long get(int idx, AsLong adapter) throws SQLException + { + throw notyet(); + } + + @Override + public double get(int idx, AsDouble adapter) throws SQLException + { + throw notyet(); + } + + @Override + public int get(int idx, AsInt adapter) throws SQLException + { + throw notyet(); + } + + @Override + public float get(int idx, AsFloat adapter) throws SQLException + { + throw notyet(); + } + + @Override + public short get(int idx, AsShort adapter) throws SQLException + { + throw notyet(); + } + + @Override + public char get(int idx, AsChar adapter) throws SQLException + { + throw notyet(); + } + + @Override + public byte get(int idx, AsByte adapter) throws SQLException + { + throw notyet(); + } + + @Override + public boolean get(int idx, AsBoolean adapter) throws SQLException + { + throw notyet(); + } +} 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; From 712f5d2aa20cb1ad5bb61292bee76fdb6b72a7c7 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 22 Jan 2022 23:01:36 -0500 Subject: [PATCH 013/334] Expose MemoryContext and ResourceOwner in Java These PostgreSQL notions will have to be available to Java code for two reasons. First, even code that has no business poking at them can still need to know which one is current, to set an appropriate lifetime on a Java object that corresponds to something in PostgreSQL allocated in that context or registered to that owner. For that purpose, they both will be exposed as subtypes of Lifespan, and the existing PL/Java DualState class will be reworked to accept any Lifespan to bound the validity of the native state. Second, Adapter code could very well need to poke at such objects (MemoryContexts, anyway): either to make a selected one current for when allocating some object, or even to create and manage one. Methods for that will not be exposed on MemoryContext or ResourceOwner proper, but could be protected methods of Adapter, so that only an Adapter can use them. --- .../java/org/postgresql/pljava/Lifespan.java | 59 +++++++ .../pljava/model/CatalogObject.java | 42 +++++ .../pljava/model/MemoryContext.java | 149 ++++++++++++++++++ .../pljava/model/ResourceOwner.java | 53 +++++++ .../postgresql/pljava/model/package-info.java | 9 ++ .../pljava/pg/CatalogObjectImpl.java | 18 +++ 6 files changed, 330 insertions(+) create mode 100644 pljava-api/src/main/java/org/postgresql/pljava/Lifespan.java create mode 100644 pljava-api/src/main/java/org/postgresql/pljava/model/MemoryContext.java create mode 100644 pljava-api/src/main/java/org/postgresql/pljava/model/ResourceOwner.java 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/model/CatalogObject.java b/pljava-api/src/main/java/org/postgresql/pljava/model/CatalogObject.java index b01959989..c4f10332e 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/model/CatalogObject.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/model/CatalogObject.java @@ -551,5 +551,47 @@ RegClass.Known formClassIdImpl( 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/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/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/package-info.java b/pljava-api/src/main/java/org/postgresql/pljava/model/package-info.java index e59bc6793..abfe63218 100644 --- 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 @@ -93,6 +93,14 @@ * 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 @@ -136,3 +144,4 @@ package org.postgresql.pljava.model; import org.postgresql.pljava.Adapter; +import org.postgresql.pljava.Lifespan; diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/CatalogObjectImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/CatalogObjectImpl.java index 347bd5b22..005a9c7fe 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/CatalogObjectImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/CatalogObjectImpl.java @@ -248,6 +248,24 @@ protected CharsetEncoding encodingFromName(String name) return CharsetEncodingImpl.fromName(name); } + @Override + protected ResourceOwner resourceOwner(int which) + { + throw notyet(); + } + + @Override + protected MemoryContext memoryContext(int which) + { + throw notyet(); + } + + @Override + protected MemoryContext upperMemoryContext() + { + throw notyet(); + } + @SuppressWarnings("unchecked") static > RegClass.Known staticFormClassId(int classId, Class clazz) From 5a9cdf94bb32acc5bb4ea6c84ba0d34b641b45cc Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 22 Jan 2022 23:02:10 -0500 Subject: [PATCH 014/334] Begin MemoryContext/ResourceOwner implementation In addition to MemoryContextImpl and ResourceOwnerImpl proper, this step will require reworking DualState so state lives are bounded by Lifespan instances instead of arbitrary pointer values. Invocation will be made into yet another subtype of Lifespan, appropriate for the life of an object passed by PostgreSQL in a call and presumed good while the call is in progress. The DualState change will have to be rototilled through all of its clients. That will take the next several commits. The DualState.Key requirement that was introduced in 1.5.1 as a way to force DualState-guarded objects to be constructed only in upcalls from C (as a hedge against Java code inadvertently doing it on the wrong thread) will go away. We *want* Adapters to be able to easily construct things without leaving Java. Just don't do it on the wrong thread. --- .../postgresql/pljava/model/Attribute.java | 1 + pljava-so/src/main/c/Backend.c | 2 +- pljava-so/src/main/c/DualState.c | 83 +- pljava-so/src/main/c/ModelUtils.c | 334 +++++ pljava-so/src/main/include/pljava/DualState.h | 9 +- .../src/main/include/pljava/ModelUtils.h | 2 + .../postgresql/pljava/internal/DualState.java | 311 +++-- .../pljava/internal/LifespanImpl.java | 61 + .../pljava/mbeans/DualStateStatistics.java | 4 +- .../postgresql/pljava/pg/AttributeImpl.java | 6 + .../pljava/pg/CatalogObjectImpl.java | 7 +- .../org/postgresql/pljava/pg/DatumImpl.java | 239 ++++ .../org/postgresql/pljava/pg/DatumUtils.java | 1091 +++++++++++++++++ .../pljava/pg/MemoryContextImpl.java | 439 +++++++ .../pljava/pg/ResourceOwnerImpl.java | 210 ++++ 15 files changed, 2534 insertions(+), 265 deletions(-) create mode 100644 pljava/src/main/java/org/postgresql/pljava/internal/LifespanImpl.java create mode 100644 pljava/src/main/java/org/postgresql/pljava/pg/DatumImpl.java create mode 100644 pljava/src/main/java/org/postgresql/pljava/pg/DatumUtils.java create mode 100644 pljava/src/main/java/org/postgresql/pljava/pg/MemoryContextImpl.java create mode 100644 pljava/src/main/java/org/postgresql/pljava/pg/ResourceOwnerImpl.java 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 index 689d0f058..aded68c6b 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/model/Attribute.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/model/Attribute.java @@ -38,4 +38,5 @@ public interface Attribute RegClass CLASS = formObjectId(RegClass.CLASSID, AttributeRelationId); RegType type(); + short length(); } diff --git a/pljava-so/src/main/c/Backend.c b/pljava-so/src/main/c/Backend.c index eca7a3a38..f20bf2d50 100644 --- a/pljava-so/src/main/c/Backend.c +++ b/pljava-so/src/main/c/Backend.c @@ -757,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; } diff --git a/pljava-so/src/main/c/DualState.c b/pljava-so/src/main/c/DualState.c index e557231e5..c4ae6822a 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-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 @@ -20,7 +20,6 @@ #include "org_postgresql_pljava_internal_DualState_SingleSPIcursorClose.h" #include "pljava/DualState.h" -#include "pljava/Backend.h" #include "pljava/Exception.h" #include "pljava/Invocation.h" #include "pljava/PgObject.h" @@ -50,22 +49,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 +64,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[] = { @@ -185,17 +140,9 @@ void pljava_DualState_initialize(void) 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); @@ -231,8 +178,6 @@ void pljava_DualState_initialize(void) PgObject_registerNatives2(clazz, singleSPIcursorCloseMethods); JNI_deleteLocalRef(clazz); - RegisterResourceReleaseCallback(resourceReleaseCB, NULL); - /* * Call initialize() methods of known classes built upon DualState. */ @@ -248,32 +193,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); -} - /* diff --git a/pljava-so/src/main/c/ModelUtils.c b/pljava-so/src/main/c/ModelUtils.c index 668db54dc..84c3890cc 100644 --- a/pljava-so/src/main/c/ModelUtils.c +++ b/pljava-so/src/main/c/ModelUtils.c @@ -31,7 +31,11 @@ #include "pljava/ModelUtils.h" #include "pljava/VarlenaWrapper.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" /* * A compilation unit collecting various native methods used in the pg model @@ -48,6 +52,70 @@ * by these methods won't be shifting underneath them. */ +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 void memoryContextCallback(void *arg) +{ + Ptr2Long p2l; + + p2l.longVal = 0L; + p2l.ptrVal = arg; + JNI_callStaticVoidMethodLocked(s_MemoryContextImpl_class, + s_MemoryContextImpl_callback, + p2l.longVal); +} + +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); +} + +void pljava_ResourceOwner_unregister(void) +{ + UnregisterResourceReleaseCallback(resourceReleaseCB, NULL); +} + void pljava_ModelUtils_initialize(void) { jclass cls; @@ -82,9 +150,85 @@ void pljava_ModelUtils_initialize(void) { 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 } + }; + 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"); + + RegisterResourceReleaseCallback(resourceReleaseCB, NULL); } /* @@ -170,3 +314,193 @@ Java_org_postgresql_pljava_pg_CharsetEncodingImpl_00024EarlyNatives__1ordinalToI 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); + POPULATE(AuxProcess); + +#undef POPULATE + + return r; +} 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/ModelUtils.h b/pljava-so/src/main/include/pljava/ModelUtils.h index bd5bbdfa6..283eea55c 100644 --- a/pljava-so/src/main/include/pljava/ModelUtils.h +++ b/pljava-so/src/main/include/pljava/ModelUtils.h @@ -24,6 +24,8 @@ extern "C" { extern void pljava_ModelUtils_initialize(void); +extern void pljava_ResourceOwner_unregister(void); + #ifdef __cplusplus } #endif 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..25a9dd4c9 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-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 @@ -41,8 +41,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 +81,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 +161,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 +189,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 +240,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 +291,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 +589,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 +620,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 +634,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 +661,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 +677,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 +723,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 +750,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) @@ -1054,8 +1009,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 +1483,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 +1510,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 +1599,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 +1636,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 +1720,7 @@ private static void resourceOwnerRelease(long resourceOwner) } } - s_stats.resourceOwnerPoll(release, total); + s_stats.lifespanPoll(release, total); } /** @@ -1817,22 +1789,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 +1813,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 +1860,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; } @@ -1940,9 +1898,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 +1936,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 +1976,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 +1990,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 +2026,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 +2062,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 @@ -2125,9 +2098,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 +2134,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 @@ -2248,9 +2221,9 @@ public long getNativeReleased() return nativeReleased.sum(); } - public long getResourceOwnerPasses() + public long getLifespanPasses() { - return resourceOwnerPasses.sum(); + return lifespanPasses.sum(); } public long getReferenceQueuePasses() @@ -2297,7 +2270,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 +2286,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/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/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/AttributeImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/AttributeImpl.java index 7dd8cfb8c..589669ab9 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/AttributeImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/AttributeImpl.java @@ -34,6 +34,12 @@ public RegType type() throw notyet(); } + @Override + public short length() + { + throw notyet(); + } + boolean foundIn(TupleDescriptor td) { return this == td.attributes().get(subId() - 1); diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/CatalogObjectImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/CatalogObjectImpl.java index 005a9c7fe..9b3359645 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/CatalogObjectImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/CatalogObjectImpl.java @@ -15,6 +15,7 @@ import org.postgresql.pljava.Adapter.As; import org.postgresql.pljava.internal.CacheMap; +import org.postgresql.pljava.internal.Invocation; import static org.postgresql.pljava.internal.UncheckedException.unchecked; import org.postgresql.pljava.model.*; @@ -251,19 +252,19 @@ protected CharsetEncoding encodingFromName(String name) @Override protected ResourceOwner resourceOwner(int which) { - throw notyet(); + return ResourceOwnerImpl.known(which); } @Override protected MemoryContext memoryContext(int which) { - throw notyet(); + return MemoryContextImpl.known(which); } @Override protected MemoryContext upperMemoryContext() { - throw notyet(); + return Invocation.upperExecutorContext(); } @SuppressWarnings("unchecked") 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..94f393245 --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/pg/DatumImpl.java @@ -0,0 +1,239 @@ +/* + * 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.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 + +/** + * 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 +{ + /** + * Dissociate the datum from Java and return its address to native code. + */ + long adopt() throws SQLException; + + /** + * Implementation of {@link Datum.Input Datum.Input}. + */ + abstract class Input implements Datum.Input, DatumImpl + { + /** + * 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 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"); + } + } + + @Override @SuppressWarnings("unchecked") + public T inputStream() + throws SQLException + { + return (T) new Stream<>(this); + } + + /** + * {@link InputStream InputStream} view of a {@code Datum.Input}. + */ + public static class Stream + 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 Stream(T datum) throws SQLException + { + m_datum = datum; + ByteBuffer b = datum.buffer(); + m_movingBuffer = b.duplicate().order(b.order()); + } + + @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 VarlenaWrapper.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( + "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_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..bc6673f14 --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/pg/DatumUtils.java @@ -0,0 +1,1091 @@ +/* + * 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 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/MemoryContextImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/MemoryContextImpl.java new file mode 100644 index 000000000..854fa4605 --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/pg/MemoryContextImpl.java @@ -0,0 +1,439 @@ +/* + * 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 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.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; + +/* + * 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 = fetchPointer(m_context, OFFSET_MCTX_ident); + + 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/ResourceOwnerImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/ResourceOwnerImpl.java new file mode 100644 index 000000000..ff041d5c7 --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/pg/ResourceOwnerImpl.java @@ -0,0 +1,210 @@ +/* + * 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.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 ) + bs[i] = asReadOnlyNativeOrder(bs[i]); + s_knownOwners = bs; + } + + static ResourceOwner known(int which) + { + ByteBuffer global = s_knownOwners[which]; + 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); + } +} From f387648e118411b6df2fbb8b70da1a519f36d46c Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 22 Jan 2022 23:03:29 -0500 Subject: [PATCH 015/334] Fix an old PgSavepoint bug Never very well publicized upstream, reading the examples of plpgsql, plperl, and plpython, when using BeginInternalSubTransaction, there is a certain pattern of saving and restoring the memory context and resource owner that PL/Java has not been doing. Now it is easy to implement that. https://www.postgresql.org/message-id/619EA06D.9070806%40anastigmatix.net --- .../pljava/internal/PgSavepoint.java | 48 +++++++++++++++++-- 1 file changed, 45 insertions(+), 3 deletions(-) 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); } } From 169b5dc9b5786513e6af0f1c0b09d7f5b984ea2f Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 22 Jan 2022 23:04:07 -0500 Subject: [PATCH 016/334] Now an Invocation is also a Lifespan The current invocation can be the right Lifespan to specify for a DualState that's guarding some object PostgreSQL passed in to the call, which is expected to be good for as long as the call is in progress. In other, but related, news, Invocation can now return the "upper executor" memory context: that is, whatever context was current at entry, even if a later use of SPI changes the context that is current. It can appear tempting to eliminate the special treatment of PgSavepoint in Invocation, and simply make it another DualState client, but because of the strict nesting imposed on savepoints, keeping just the one reference to the first one set suffices, and is more efficient. --- pljava-so/src/main/c/Invocation.c | 1 + .../pljava/internal/Invocation.java | 22 ++++++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/pljava-so/src/main/c/Invocation.c b/pljava-so/src/main/c/Invocation.c index c6bc76796..473e0a948 100644 --- a/pljava-so/src/main/c/Invocation.c +++ b/pljava-so/src/main/c/Invocation.c @@ -101,6 +101,7 @@ StaticAssertStmt(offsetof(Invocation,fld) == \ CONFIRMOFFSET(nestLevel); CONFIRMOFFSET(hasDual); CONFIRMOFFSET(errorOccurred); + CONFIRMOFFSET(upperContext); #undef CONFIRMOFFSET diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/Invocation.java b/pljava/src/main/java/org/postgresql/pljava/internal/Invocation.java index af08e805a..35c2699a4 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/Invocation.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/Invocation.java @@ -26,6 +26,12 @@ 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. @@ -37,11 +43,12 @@ * in the C struct for the duration of the invocation. * @author Thomas Hallgren */ -public class Invocation +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()); @@ -105,6 +112,7 @@ private void onExit(boolean withError) finally { m_savepoint = null; + lifespanRelease(); } } @@ -147,6 +155,18 @@ public static Invocation current() }); } + /** + * 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)); From f7561df9570e48195262ae8f47c34ee2c6f573c8 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 22 Jan 2022 23:04:24 -0500 Subject: [PATCH 017/334] DualState clients that pass no Lifespan Simplify these: their C callers were passing unconditional null as the ResourceOwner before, which their Java constructors passed along unchanged. Now just have the Java constructor pass null as the Lifespan. --- pljava-so/src/main/c/ExecutionPlan.c | 8 +++----- pljava-so/src/main/c/type/ErrorData.c | 12 +++--------- pljava-so/src/main/c/type/Tuple.c | 10 +++------- pljava-so/src/main/c/type/TupleDesc.c | 13 +++---------- .../postgresql/pljava/internal/ErrorData.java | 18 ++++++++++++------ .../pljava/internal/ExecutionPlan.java | 13 ++++++------- .../org/postgresql/pljava/internal/Tuple.java | 18 ++++++++++++------ .../postgresql/pljava/internal/TupleDesc.java | 18 ++++++++++++------ 8 files changed, 54 insertions(+), 56 deletions(-) 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/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/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/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/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); } /** From 6ba4e66cd9643d82bdbde4e194c432a50abff019 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 22 Jan 2022 23:04:53 -0500 Subject: [PATCH 018/334] Clients using Invocation.current as Lifespan These DualState clients were previously passing the address of the current invocation struct as their "resource owner", again from the C code, passed along by the Java constructor. Again simplify to call Invocation.current() right in the Java constructor and use that as the Lifespan. On a side note, the legacy Relation class included here (and its legacy Tuple and TupleDesc) will naturally be among the first candidates for retirement when this new model API is ready. --- pljava-so/src/main/c/SQLInputFromTuple.c | 9 +++------ pljava-so/src/main/c/type/Relation.c | 10 ++-------- pljava-so/src/main/c/type/SingleRowReader.c | 9 +++------ pljava-so/src/main/c/type/TriggerData.c | 11 ++--------- .../postgresql/pljava/internal/Relation.java | 12 ++++++------ .../postgresql/pljava/internal/TriggerData.java | 12 ++++++------ .../pljava/jdbc/SQLInputFromTuple.java | 7 +++---- .../postgresql/pljava/jdbc/SingleRowReader.java | 17 ++++++----------- 8 files changed, 31 insertions(+), 56 deletions(-) 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/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/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/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/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/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/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; } From 901712a640e73a28652f710d8f6668bdf1e11da3 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 22 Jan 2022 23:05:45 -0500 Subject: [PATCH 019/334] Client using its own ResourceOwner as Lifespan This legacy Portal class is called from C and passed the address of the PostgreSQL ResourceOwner associated with the Portal itself. --- pljava-so/src/main/c/type/Portal.c | 6 +++--- .../org/postgresql/pljava/internal/Portal.java | 14 +++++++++----- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/pljava-so/src/main/c/type/Portal.c b/pljava-so/src/main/c/type/Portal.c index 3cf9e5b8f..f5f6892fd 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-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,7 +52,7 @@ jobject pljava_Portal_create(Portal portal, jobject jplan) p2lro.ptrVal = portal->resowner; jportal = JNI_newObjectLocked(s_Portal_class, s_Portal_init, - pljava_DualState_key(), p2lro.longVal, p2l.longVal, jplan); + p2lro.longVal, p2l.longVal, jplan); return jportal; } @@ -104,7 +104,7 @@ 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"); + "(JJLorg/postgresql/pljava/internal/ExecutionPlan;)V"); } /**************************************** 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..7ad127c01 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-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 @@ -15,6 +15,10 @@ import org.postgresql.pljava.internal.SPI; // for javadoc import static org.postgresql.pljava.internal.Backend.doInPG; +import org.postgresql.pljava.Lifespan; + +import org.postgresql.pljava.pg.ResourceOwnerImpl; + import java.sql.SQLException; /** @@ -34,9 +38,9 @@ public class Portal private final State m_state; - Portal(DualState.Key cookie, long ro, long pointer, ExecutionPlan plan) + Portal(long ro, long pointer, ExecutionPlan plan) { - m_state = new State(cookie, this, ro, pointer); + m_state = new State(this, ResourceOwnerImpl.fromAddress(ro), pointer); m_plan = plan; } @@ -44,9 +48,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); } /** From 827f2d6d36e47129195a3f223f06be475d0325c0 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 22 Jan 2022 23:06:15 -0500 Subject: [PATCH 020/334] Client with both ResourceOwner and MemoryContext This is only an intermediate refactoring of VarlenaWrapper. Construction of one is still set in motion from C. Ultimately, it should implement Datum and be something that a Datum.Accessor can construct with a minimum of fuss. --- pljava-so/src/main/c/VarlenaWrapper.c | 18 ++--- .../pljava/internal/VarlenaWrapper.java | 70 +++++++++++-------- 2 files changed, 49 insertions(+), 39 deletions(-) diff --git a/pljava-so/src/main/c/VarlenaWrapper.c b/pljava-so/src/main/c/VarlenaWrapper.c index 5805ace13..21b90e82e 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-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 @@ -258,7 +258,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 +324,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 +348,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_VarlenaWrapper_adopt); #if PG_VERSION_NUM >= 90500 return PointerGetDatum(p2l.ptrVal); #else @@ -456,17 +455,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_VarlenaWrapper_class, "adopt", "()J"); clazz = PgObject_getJavaClass( "org/postgresql/pljava/internal/VarlenaWrapper$Input$State"); 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..27fc324e5 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-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 @@ -36,6 +36,13 @@ import java.util.concurrent.ExecutionException; 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.MemoryContextImpl; +import org.postgresql.pljava.pg.ResourceOwnerImpl; /** * Interface that wraps a PostgreSQL native variable-length ("varlena") datum; @@ -55,7 +62,7 @@ public interface VarlenaWrapper extends Closeable * from Java. * @param cookie Capability held by native code. */ - long adopt(DualState.Key cookie) throws SQLException; + long adopt() throws SQLException; /** * Return a string describing this object in a way useful for debugging, @@ -107,15 +114,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 +175,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 { @@ -323,7 +331,7 @@ protected ByteBuffer buffer() throws IOException } @Override - public long adopt(DualState.Key cookie) throws SQLException + public long adopt() throws SQLException { Input.this.pin(); try @@ -332,7 +340,7 @@ public long adopt(DualState.Key cookie) throws SQLException throw new SQLException( "Cannot adopt VarlenaWrapper.Input after " + "it is closed", "55000"); - return Input.this.adopt(cookie); + return Input.this.adopt(); } finally { @@ -347,14 +355,17 @@ private static class State 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) + 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 +381,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 +394,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 +510,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 +663,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 +672,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 +703,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 +744,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(); } } From 349c3e8ec44c642a6b19ae8d7fd4664b625754a9 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 22 Jan 2022 23:07:18 -0500 Subject: [PATCH 021/334] Eliminate last use of DualState.Key Originally a hedge against coding mistakes during the introduction of DualState for 1.5.1 (which had to support Java < 9), it is less necessary now that the internals are behind JPMS encapsulation, and the former checks for the cookie can be replaced with assertions that the action is happening on the right thread. The CI tests run with assertions enabled, so this should be adequate. --- .../java/org/postgresql/pljava/internal/VarlenaXMLRenderer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 d38a415c1..a0f24e32b 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/VarlenaXMLRenderer.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/VarlenaXMLRenderer.java @@ -49,7 +49,7 @@ public VarlenaXMLRenderer(VarlenaWrapper.Input input) throws SQLException } @Override - public long adopt(DualState.Key cookie) throws SQLException + public long adopt() throws SQLException { throw new UnsupportedOperationException( "adopt() on a synthetic XML rendering"); From fc9bd9c000db39443afbf8a17c7997e90a79d705 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 22 Jan 2022 23:16:56 -0500 Subject: [PATCH 022/334] Adapt SQLXMLImpl to be creatable from Java Fitting it into the new scheme is not entirely completed here; for example, newReadable takes a Datum.Input parameter, but still casts it internally to VarlenaWrapper.Input. Making it interoperate with any Datum.Input may be a bit more work. Likewise, newReadable with synthetic=true still encapsulates all the knowledge of what datatypes there is synthetic-XML coverage for and selecting the right VarlenaXMLRenderer for it (there's that varlena-specificity again!). More of that should be moved out of here and into an Adapter. In passing, fix a couple typos in toString() methods, and add a serviceable, if brute-force, getString() method to Synthetic. It would be better for SyntheticXMLReader to gain the ability to produce character-stream output efficiently, but until that happens, there needs to be something for those moments when you just want a string to look at and shouldn't have to fuss to get it. For now, VarlenaWrapper.Input and .Stream still extend, and add small features like toString(Object) to, DatumImpl. Later work can probably migrate those bits so VarlenaWrapper will only contain logic specific to varlenas. An adt.spi interface Verifier is added, though Datum doesn't yet expose any way to use it; in this commit, only one method accepting Verifier.OfStream is added in DatumImpl.Input.Stream, the minimal change needed to get things working. --- .../pljava/internal/VarlenaWrapper.java | 180 +++--------------- .../postgresql/pljava/jdbc/SQLXMLImpl.java | 81 +++++++- 2 files changed, 98 insertions(+), 163 deletions(-) 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 27fc324e5..34a8dfbda 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/VarlenaWrapper.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/VarlenaWrapper.java @@ -35,12 +35,15 @@ 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.MemoryContextImpl; import org.postgresql.pljava.pg.ResourceOwnerImpl; @@ -55,15 +58,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() 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 @@ -81,8 +77,6 @@ public interface VarlenaWrapper extends Closeable */ String toString(Object o); - - /** * A class by which Java reads the content of a varlena. * @@ -90,7 +84,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; @@ -188,171 +182,40 @@ public long adopt() throws SQLException } } - public class Stream - extends ByteBufferInputStream implements VarlenaWrapper + @Override @SuppressWarnings("unchecked") + public T inputStream() + throws SQLException { - /** - * 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); - } - } + return (T) new Stream(this); + } - /* - * Unpin for use in {@code ByteBufferInputStream} or here; no - * throws-clause difference to blotch things up. - */ - protected void unpin() + public static class Stream + extends DatumImpl.Input.Stream + implements VarlenaWrapper + { + private Stream(VarlenaWrapper.Input datum) throws SQLException { - Input.this.unpin(); + super(datum); } @Override - public void close() throws IOException + public String toString() { - if ( pinUnlessReleased() ) - return; - try - { - super.close(); - Input.this.close(); - } - finally - { - unpin(); - } + return toString(this); } @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() 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(); - } - finally - { - Input.this.unpin(); - } + m_datum.toString(o), m_open ? "open" : "closed"); } } private static class State - extends DualState.SingleMemContextDelete + extends DualState.SingleMemContextDelete { private ByteBuffer m_buf; private long m_resourceOwner; @@ -360,7 +223,8 @@ private static class State private long m_varlena; private State( - Input vr, long resourceOwner, MemoryContext memContext, + VarlenaWrapper.Input vr, long resourceOwner, + MemoryContext memContext, long snapshot, long varlenaPtr, ByteBuffer buf) { super(vr, ResourceOwnerImpl.fromAddress(resourceOwner), 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 15e815f9d..8541e96b0 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java @@ -176,9 +176,17 @@ /* ... 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; + /** * Implementation of {@link SQLXML} for the SPI connection. */ @@ -319,11 +327,31 @@ 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 + { + VarlenaWrapper.Input vwi = (VarlenaWrapper.Input)datum; + int oid = pgType.oid(); + + if ( synthetic ) + return new Readable.Synthetic(vwi, oid); + + return new Readable.PgXML(vwi, 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()); } @@ -925,7 +953,7 @@ 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 "); } @@ -935,7 +963,7 @@ static class PgXML private PgXML(VarlenaWrapper.Input vwi, int oid) throws SQLException { - super(vwi.new Stream(), oid); + super(vwi.inputStream(), oid); } /** @@ -977,7 +1005,7 @@ protected VarlenaWrapper adopt(int oid) throws SQLException if ( null == vw ) backingIfNotFreed(); /* shorthand to throw the exception */ if ( m_pgTypeID != oid ) - vw.verify(new Verifier()); + vw.verify(new Verifier()::verify); return vw; } @@ -1093,6 +1121,7 @@ protected Adjusting.XML.SAXSource toSAXSource( return new AdjustingSAXSource(backing, new InputSource()); } + @Override protected Adjusting.XML.StAXSource toStAXSource( VarlenaXMLRenderer backing) throws SQLException, XMLStreamException, IOException @@ -1102,6 +1131,7 @@ protected Adjusting.XML.StAXSource toStAXSource( "0A000"); } + @Override protected Adjusting.XML.DOMSource toDOMSource( VarlenaXMLRenderer backing) throws @@ -1112,6 +1142,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); + } + } } } @@ -1479,7 +1550,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 "); } } From a0269b80f5ee150a34dd57a94e75799accb85077 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 22 Jan 2022 23:18:20 -0500 Subject: [PATCH 023/334] Build out the TupleDescriptor implementation As before, JNI methods for this 'model' framework continue to be grouped together in ModelUtils.c; their total number and complexity is expected to be low enough for that to be practical, and then they can all be seen in one place. RegClassImpl and RegTypeImpl acquire m_tupDescHolder arrays in this commit, without much explanation; that will come a few commits later. --- .../org/postgresql/pljava/model/RegType.java | 14 + pljava-so/src/main/c/ModelUtils.c | 58 +++ .../src/main/include/pljava/ModelUtils.h | 20 ++ .../postgresql/pljava/pg/AttributeImpl.java | 6 + .../postgresql/pljava/pg/RegClassImpl.java | 2 + .../org/postgresql/pljava/pg/RegTypeImpl.java | 74 ++++ .../postgresql/pljava/pg/TupleDescImpl.java | 340 +++++++++++++++++- 7 files changed, 509 insertions(+), 5 deletions(-) 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 index ed6269283..76143bbf0 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/model/RegType.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/model/RegType.java @@ -105,6 +105,20 @@ public interface RegType RegType REGROLE = formObjectId(CLASSID, REGROLEOID); RegType REGCOLLATION = formObjectId(CLASSID, REGCOLLATIONOID); + RegClass relation(); + RegType modifier(int typmod); + + /** + * 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. diff --git a/pljava-so/src/main/c/ModelUtils.c b/pljava-so/src/main/c/ModelUtils.c index 84c3890cc..76d3ed21b 100644 --- a/pljava-so/src/main/c/ModelUtils.c +++ b/pljava-so/src/main/c/ModelUtils.c @@ -36,6 +36,7 @@ #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" /* * A compilation unit collecting various native methods used in the pg model @@ -61,6 +62,24 @@ 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; + +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; +} + static void memoryContextCallback(void *arg) { Ptr2Long p2l; @@ -200,6 +219,16 @@ void pljava_ModelUtils_initialize(void) { 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 } + }; + cls = PgObject_getJavaClass("org/postgresql/pljava/pg/CharsetEncodingImpl$EarlyNatives"); PgObject_registerNatives2(cls, charsetMethods); JNI_deleteLocalRef(cls); @@ -228,6 +257,17 @@ void pljava_ModelUtils_initialize(void) s_ResourceOwnerImpl_callback = PgObject_getStaticJavaMethod( s_ResourceOwnerImpl_class, "callback", "(J)V"); + 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;"); + RegisterResourceReleaseCallback(resourceReleaseCB, NULL); } @@ -504,3 +544,21 @@ Java_org_postgresql_pljava_pg_ResourceOwnerImpl_00024EarlyNatives__1window(JNIEn 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; +} diff --git a/pljava-so/src/main/include/pljava/ModelUtils.h b/pljava-so/src/main/include/pljava/ModelUtils.h index 283eea55c..0d0e8a244 100644 --- a/pljava-so/src/main/include/pljava/ModelUtils.h +++ b/pljava-so/src/main/include/pljava/ModelUtils.h @@ -26,6 +26,26 @@ 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); + #ifdef __cplusplus } #endif diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/AttributeImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/AttributeImpl.java index 589669ab9..17bd9dc0f 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/AttributeImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/AttributeImpl.java @@ -55,6 +55,12 @@ boolean foundIn(TupleDescriptor td) */ static class Cataloged extends AttributeImpl { + private final RegClassImpl m_relation; + + Cataloged(RegClassImpl relation) + { + m_relation = requireNonNull(relation); + } } /** diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/RegClassImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/RegClassImpl.java index f1ed7d311..7381cd469 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/RegClassImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/RegClassImpl.java @@ -39,6 +39,8 @@ static class Known> { } + TupleDescriptor.Interned[] m_tupDescHolder; + @Override public TupleDescriptor.Interned tupleDescriptor() { diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/RegTypeImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/RegTypeImpl.java index e946942a4..d2c93d70c 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/RegTypeImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/RegTypeImpl.java @@ -38,6 +38,47 @@ abstract class RegTypeImpl extends Addressed Nonshared, Namespaced, Owned, AccessControlled, RegType { + @Override + public TupleDescriptor.Interned tupleDescriptor() + { + throw notyet(); + } + + @Override + public RegClass relation() + { + throw notyet(); + } + + /** + * 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; + } /** * Represents a type that has been mentioned without an accompanying type @@ -45,6 +86,20 @@ abstract class RegTypeImpl extends Addressed */ static class NoModifier extends RegTypeImpl { + @Override + public int modifier() + { + return -1; + } + + @Override + public RegType modifier(int typmod) + { + if ( -1 == typmod ) + return this; + return (RegType) + CatalogObjectImpl.Factory.formMaybeModifiedType(oid(), typmod); + } } /** @@ -56,8 +111,19 @@ static class NoModifier extends RegTypeImpl */ static class Modified extends RegTypeImpl { + private final NoModifier m_base; + Modified(NoModifier base) { + m_base = base; + } + + @Override + public RegType modifier(int typmod) + { + if ( modifier() == typmod ) + return this; + return m_base.modifier(typmod); } } @@ -70,5 +136,13 @@ static class Modified extends RegTypeImpl */ static class Blessed extends RegTypeImpl { + TupleDescriptor.Interned[] m_tupDescHolder; + + @Override + public RegType modifier(int typmod) + { + throw new UnsupportedOperationException( + "may not alter the type modifier of an interned row type"); + } } } diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/TupleDescImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/TupleDescImpl.java index e79a51e94..7899c7f10 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/TupleDescImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/TupleDescImpl.java @@ -16,8 +16,17 @@ 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 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 java.nio.ByteBuffer; +import static java.nio.ByteOrder.nativeOrder; import java.sql.SQLException; import java.sql.SQLSyntaxErrorException; @@ -28,6 +37,10 @@ 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}. *

    @@ -62,8 +75,217 @@ abstract class TupleDescImpl extends AbstractList implements TupleDescriptor { - private final Attribute[] m_attrs = null; - private final Map m_byName = Map.of(); + private final ByteBuffer m_td; + private final Attribute[] m_attrs; + private final Map m_byName; + private final State m_state; + + /** + * 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) + { + int len = SIZEOF_FORM_PG_ATTRIBUTE; + int off = OFFSET_TUPLEDESC_ATTRS + len * index; + len = ATTRIBUTE_FIXED_PART_SIZE; // TupleDesc hasn't got the whole thing + // Java 13: m_td.slice(off, len).order(m_td.order()) + ByteBuffer bnew = m_td.duplicate(); + bnew.position(off).limit(off + len); + return bnew.slice().order(m_td.order()); + } + + 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); + Attribute[] attrs = + new Attribute [ (m_td.capacity() - OFFSET_TUPLEDESC_ATTRS) + / SIZEOF_FORM_PG_ATTRIBUTE ]; + + for ( int i = 0 ; i < attrs.length ; ++ i ) + attrs[i] = ctor.apply(this, 1 + i); + + m_attrs = attrs; + + /* + * A stopgap. There is probably a lighter-weight API to be designed + * that doesn't assume every TupleDescriptor has a HashMap. Application + * code often knows what all the names are of the attributes it will be + * interested in. + */ + m_byName = new ConcurrentHashMap<>(m_attrs.length); + } + + /** + * 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_attrs = new Attribute[] { new AttributeImpl.OfType(this, type) }; + m_byName = new ConcurrentHashMap<>(m_attrs.length); + } + + /** + * 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 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 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 List attributes() @@ -128,25 +350,73 @@ public Attribute get(int index) static class Cataloged extends TupleDescImpl implements Interned { + private final RegClass m_relation; + + Cataloged(ByteBuffer td, RegClassImpl c) + { + /* + * 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; + } + @Override public RegType rowType() { - throw notyet(); + return m_relation.type(); } } static class Blessed extends TupleDescImpl implements Interned { + private final RegType m_rowType; + + Blessed(ByteBuffer td, RegTypeImpl t) + { + /* + * A Blessed tupdesc has no associated RegClass. 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() { - throw notyet(); + 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() { @@ -156,7 +426,25 @@ public RegType rowType() @Override public Interned intern() { - throw notyet(); + return doInPG(() -> + { + TupleDescImpl sup = this; // its m_td is private + + ByteBuffer direct = ByteBuffer.allocateDirect( + sup.m_td.capacity()).put(sup.m_td.rewind()); + + 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(); + }); } } @@ -165,6 +453,7 @@ static class OfType extends TupleDescImpl { OfType(RegType type) { + super(type); } @Override @@ -179,4 +468,45 @@ 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); } From 1f176757aedd93079ccc66c8e00ab7bdbcf25f14 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 22 Jan 2022 23:18:46 -0500 Subject: [PATCH 024/334] Build out TupleTableSlot implementation There are two flavors so far, Deformed and Heap. Deformed works with whatever a real PostgreSQL TupleTableSlot can work with, relying on the PostgreSQL implementation to 'deform' it into separate datum and isnull arrays. (That doesn't have to be a PostgreSQL 'virtual' TupleTableSlot; it can do the deforming independently of the type of slot. When the time comes to implement the reverse direction and produce tuples, a virtual slot will be the way to go for that, using the PostgreSQL C code to 'form' it once populated.) The Heap flavor knows enough about that PostgreSQL tuple format to 'deform' it in Java without the JNI calls (except where some out-of-line value has to be mapped, or for varlena values until VarlenaWrapper sheds more of its remaining JNI-centricity). The Heap implementation does not yet do anything clever to memoize the offsets into the tuple, which makes the retrieval of all the tuple's values an O(n^2) proposition; there is a low-hanging-fruit optimization opportunity there. For now, it gets the job done. It might be interesting to see how the two flavors compare on typical heap tuples: Deformed, making more JNI calls but relying on PostgreSQL's fast native deforming, or Heap, which can avoid more JNI calls, and also avoids deforming something into a fresh native memory allocation if the only thing it will be used for is to immediately construct some Java object. The Heap flavor can do one thing the Deformed flavor definitely cannot: it can operate on heap-tuple-formatted contents of an arbitrary Java byte buffer, which in theory might not even be backed by native memory. (Again, for now, this is slightly science fiction where varlena values are concerned, because VarlenaWrapper retains a lot of its native dependencies. A ByteBuffer "heap tuple" with varlenas in it will have to be native-backed for now.) The selection of the DualState guard by heapTupleGetLightSlot() is currently more hardcoded than that would suggest; it assumes the buffer is mapping memory that can be heap_free_tuple'd. The 'light' in heapTupleGetLightSlot really means that there isn't an underlying PostgreSQL TupleTableSlot constructed. The whole business of how to apply and use DualState guards on these things still needs more attention. There is also Heap.Indexed, which is the thing needed for arrays. When the element type is fixed-length, it achieves O(1) access (plus null-bitmap processing if there are nulls). It uses a "count preceding null bits ahead of time" strategy that could also easily be adopted in Heap. A NullableDatum flavor is also needed, which would be the thing for mapping (as one prominent example) function-call arguments. The HeapTuples8 and HeapTuples4 classes at the end are scaffolding and ought to be factored out into something with a decent API, as hinted at in the comment preceding them. A Heap instance still inherits the values/nulls array fields used in the deformed case, without (at present) making any use of them. It is possible some use could be made (as, again, an underlying PG TupleTableSlot could be used in deforming a heap tuple), but it's also possible that won't ever be needed, and the class could be refactored to a simpler form. --- .../postgresql/pljava/model/Attribute.java | 5 + pljava-so/src/main/c/DualState.c | 35 + pljava-so/src/main/c/ModelUtils.c | 137 +++ .../src/main/include/pljava/ModelUtils.h | 15 + .../postgresql/pljava/internal/DualState.java | 88 ++ .../postgresql/pljava/pg/AttributeImpl.java | 20 + .../org/postgresql/pljava/pg/DatumUtils.java | 7 + .../pljava/pg/TupleTableSlotImpl.java | 874 +++++++++++++++++- 8 files changed, 1161 insertions(+), 20 deletions(-) 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 index aded68c6b..cb88b0d7c 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/model/Attribute.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/model/Attribute.java @@ -15,6 +15,8 @@ import static org.postgresql.pljava.model.CatalogObject.Factory.*; +import org.postgresql.pljava.annotation.BaseUDT.Alignment; + import org.postgresql.pljava.sqlgen.Lexicals.Identifier.Simple; /** @@ -37,6 +39,9 @@ public interface Attribute */ RegClass CLASS = formObjectId(RegClass.CLASSID, AttributeRelationId); + RegClass relation(); RegType type(); short length(); + boolean byValue(); + Alignment alignment(); } diff --git a/pljava-so/src/main/c/DualState.c b/pljava-so/src/main/c/DualState.c index c4ae6822a..6cd089c47 100644 --- a/pljava-so/src/main/c/DualState.c +++ b/pljava-so/src/main/c/DualState.c @@ -18,6 +18,7 @@ #include "org_postgresql_pljava_internal_DualState_SingleFreeErrorData.h" #include "org_postgresql_pljava_internal_DualState_SingleSPIfreeplan.h" #include "org_postgresql_pljava_internal_DualState_SingleSPIcursorClose.h" +#include "org_postgresql_pljava_internal_DualState_BBHeapFreeTuple.h" #include "pljava/DualState.h" #include "pljava/Exception.h" @@ -138,6 +139,16 @@ 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_cleanEnqueuedInstances = PgObject_getStaticJavaMethod( @@ -178,6 +189,11 @@ void pljava_DualState_initialize(void) PgObject_registerNatives2(clazz, singleSPIcursorCloseMethods); JNI_deleteLocalRef(clazz); + 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. */ @@ -345,3 +361,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/ModelUtils.c b/pljava-so/src/main/c/ModelUtils.c index 76d3ed21b..23696037a 100644 --- a/pljava-so/src/main/c/ModelUtils.c +++ b/pljava-so/src/main/c/ModelUtils.c @@ -37,6 +37,7 @@ #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" /* * A compilation unit collecting various native methods used in the pg model @@ -65,6 +66,10 @@ static void resourceReleaseCB(ResourceReleasePhase phase, static jclass s_TupleDescImpl_class; static jmethodID s_TupleDescImpl_fromByteBuffer; +static jclass s_TupleTableSlotImpl_class; +static jmethodID s_TupleTableSlotImpl_newDeformed; +static jmethodID s_TupleTableSlotImpl_supplyHeapTuples; + jobject pljava_TupleDescriptor_create(TupleDesc tupdesc, Oid reloid) { jlong tupdesc_size = (jlong)TupleDescSize(tupdesc); @@ -80,6 +85,48 @@ jobject pljava_TupleDescriptor_create(TupleDesc tupdesc, Oid reloid) return result; } +jobject pljava_TupleTableSlot_create( + TupleDesc tupdesc, 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 jtd = pljava_TupleDescriptor_create(tupdesc, reloid); + + jobject 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; +} + +jobject pljava_TupleTableSlot_fromSPI() +{ + jobject tts = pljava_TupleTableSlot_create( + SPI_tuptable->tupdesc, &TTSOpsHeapTuple, InvalidOid); + + /* + * XXX handle possibility that SPI_processed is way too big. + */ + jobject vals = JNI_newDirectByteBuffer( + SPI_tuptable->vals, (jlong)(SPI_processed*sizeof *SPI_tuptable->vals)); + + jobject list = JNI_callObjectMethodLocked(tts, + s_TupleTableSlotImpl_supplyHeapTuples, vals); + + JNI_deleteLocalRef(vals); + JNI_deleteLocalRef(tts); + + return list; +} + static void memoryContextCallback(void *arg) { Ptr2Long p2l; @@ -229,6 +276,26 @@ void pljava_ModelUtils_initialize(void) { 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 + }, + { + "_testmeSPI", + "()Ljava/util/List;", + Java_org_postgresql_pljava_pg_TupleTableSlotImpl__1testmeSPI + }, + { 0, 0, 0 } + }; + cls = PgObject_getJavaClass("org/postgresql/pljava/pg/CharsetEncodingImpl$EarlyNatives"); PgObject_registerNatives2(cls, charsetMethods); JNI_deleteLocalRef(cls); @@ -268,6 +335,23 @@ void pljava_ModelUtils_initialize(void) "(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;"); + + s_TupleTableSlotImpl_supplyHeapTuples = PgObject_getJavaMethod( + s_TupleTableSlotImpl_class, + "supplyHeapTuples", + "(Ljava/nio/ByteBuffer;)Ljava/util/List;"); + RegisterResourceReleaseCallback(resourceReleaseCB, NULL); } @@ -562,3 +646,56 @@ Java_org_postgresql_pljava_pg_TupleDescImpl__1assign_1record_1type_1typmod(JNIEn 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") +} + +/* + * Class: org_postgresql_pljava_pg_TupleTableSlotImpl + * Method: _testmeSPI + * Signature: ()Ljava/util/List; + */ +JNIEXPORT jobject JNICALL +Java_org_postgresql_pljava_pg_TupleTableSlotImpl__1testmeSPI(JNIEnv* env, jobject _cls) +{ + jobject result; + BEGIN_NATIVE_AND_TRY + result = pljava_TupleTableSlot_fromSPI(); + END_NATIVE_AND_CATCH("_testmeSPI") + return result; +} diff --git a/pljava-so/src/main/include/pljava/ModelUtils.h b/pljava-so/src/main/include/pljava/ModelUtils.h index 0d0e8a244..79a957752 100644 --- a/pljava-so/src/main/include/pljava/ModelUtils.h +++ b/pljava-so/src/main/include/pljava/ModelUtils.h @@ -46,6 +46,21 @@ extern void pljava_ResourceOwner_unregister(void); */ 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. + */ +extern jobject pljava_TupleTableSlot_create( + TupleDesc tupdesc, const TupleTableSlotOps *tts_ops, Oid reloid); + +/* + * Test scaffolding for the time being. + */ +extern jobject pljava_TupleTableSlot_fromSPI(void); + #ifdef __cplusplus } #endif 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 25a9dd4c9..b8618113a 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java @@ -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; @@ -1891,6 +1894,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. @@ -2175,6 +2227,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. diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/AttributeImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/AttributeImpl.java index 17bd9dc0f..a6b0a5e23 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/AttributeImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/AttributeImpl.java @@ -21,6 +21,8 @@ import static org.postgresql.pljava.pg.ModelConstants.*; import static org.postgresql.pljava.pg.TupleDescImpl.Ephemeral; +import org.postgresql.pljava.annotation.BaseUDT.Alignment; + import org.postgresql.pljava.sqlgen.Lexicals.Identifier.Simple; abstract class AttributeImpl extends Addressed @@ -28,6 +30,12 @@ abstract class AttributeImpl extends Addressed Nonshared, Named, AccessControlled, Attribute { + @Override + public RegClass relation() + { + throw notyet(); + } + @Override public RegType type() { @@ -40,6 +48,18 @@ public short length() throw notyet(); } + @Override + public boolean byValue() + { + throw notyet(); + } + + @Override + public Alignment alignment() + { + throw notyet(); + } + boolean foundIn(TupleDescriptor td) { return this == td.attributes().get(subId() - 1); diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/DatumUtils.java b/pljava/src/main/java/org/postgresql/pljava/pg/DatumUtils.java index bc6673f14..3ce77adb5 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/DatumUtils.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/DatumUtils.java @@ -50,6 +50,13 @@ 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() ) diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/TupleTableSlotImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/TupleTableSlotImpl.java index 8f53c55f2..6ddce1b02 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/TupleTableSlotImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/TupleTableSlotImpl.java @@ -14,8 +14,14 @@ 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; @@ -33,6 +39,8 @@ 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; @@ -47,6 +55,16 @@ 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; @@ -163,123 +181,939 @@ public Adapter adapterPlease(String cname, String field) return (Adapter)f.get(null); } + 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.attributes().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 + 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.attributes().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.attributes().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.attributes(); + 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.attributes().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.attributes().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.attributes().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() { - throw notyet(); + 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() { - throw notyet(); + return m_tupdesc; + } + + ByteBuffer values() + { + return m_values; + } + + private List supplyHeapTuples(ByteBuffer htarray) + { + htarray = htarray.asReadOnlyBuffer().order(ByteOrder.nativeOrder()); + if ( 8 == SIZEOF_DATUM ) + return new HeapTuples8(htarray); + return new HeapTuples4(htarray); + } + + private 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); + + private static native List _testmeSPI(); + @Override public T get(Attribute att, As adapter) throws SQLException { - throw notyet(); + 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) throws SQLException { - throw notyet(); + 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) throws SQLException { - throw notyet(); + 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) throws SQLException { - throw notyet(); + 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) throws SQLException { - throw notyet(); + 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) throws SQLException { - throw notyet(); + 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) throws SQLException { - throw notyet(); + 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) throws SQLException { - throw notyet(); + 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) throws SQLException { - throw notyet(); + 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) throws SQLException { - throw notyet(); + 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) throws SQLException { - throw notyet(); + 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) throws SQLException { - throw notyet(); + 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) throws SQLException { - throw notyet(); + 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) throws SQLException { - throw notyet(); + 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) throws SQLException { - throw notyet(); + 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) throws SQLException { - throw notyet(); + 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) throws SQLException { - throw notyet(); + 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) throws SQLException { - throw notyet(); + Attribute att = fromIndex(idx, adapter); + + if ( isNull(idx) ) + return adapter.fetchNull(att); + + int off = toOffset(idx); + return adapter.fetch(accessor(idx), values(), off, att); + } + + /* + * Plan: factor the below out into 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")? + */ + + /** + * A {@code List} built over something like + */ + private class HeapTuples8 extends AbstractList + { + private final LongBuffer m_tuples; + + HeapTuples8(ByteBuffer ht) + { + m_tuples = ht.asLongBuffer(); + } + + @Override + public TupleTableSlot get(int index) + { + store_heaptuple(m_tuples.get(index), false); + return TupleTableSlotImpl.this; + } + + @Override + public int size() + { + return m_tuples.capacity(); + } + } + + private class HeapTuples4 extends AbstractList + { + private final IntBuffer m_tuples; + + HeapTuples4(ByteBuffer ht) + { + m_tuples = ht.asIntBuffer(); + } + + @Override + public TupleTableSlot get(int index) + { + store_heaptuple(m_tuples.get(index), false); + return TupleTableSlotImpl.this; + } + + @Override + public int size() + { + return m_tuples.capacity(); + } + } + + /** + * Temporary test jig during development. + */ + public static List testmeSPI() + { + return doInPG(() -> _testmeSPI()); + } + + private static class HTChunkState + extends DualState.BBHeapFreeTuple + { + private HTChunkState( + TupleTableSlotImpl referent, Lifespan span, ByteBuffer ht) + { + super(referent, span, ht); + } } } From 2e74a6b78476095153c7920d7022b7151067ce4f Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 22 Jan 2022 23:19:09 -0500 Subject: [PATCH 025/334] Build out the CatalogObject implementation Here's how this is going to work. The "exists because mentioned" aspect of a CatalogObject is a lightweight operation, just caching/returning a singleton with the mentioned values of classId/objId/(subId?). For a bare CatalogObject (objId unaccompanied by classId), that's all there is. But for any CatalogObject.Addressed subtype, the classId and objId together identify a tuple in a particular system catalog (or, that is, identify a tuple that could exist in that catalog). And the methods on the Java class that return information about the object get the information by fetching attributes from that tuple, then constructing whatever the Java representation will be. Not to duplicate the work of fetching (the tuple itself, and then an attribute from the tuple) and constructing the Java result, an instance will have an array of SwitchPointCache-managed "slots" that will cache, lazily, the constructed results. Five of those slots have their indices standardized right here in CatalogObjectImpl, to account for the name, namespace, owner, and ACL of objects that have those things. Slot 0 is for the tuple itself. When an uncached value is requested, the "computation method" set up for that slot will execute (always on the PG thread, so it can interact with PostgreSQL with no extra ceremony). Most computation methods will begin by calling cacheTuple() to obtain the tuple itself from slot 0, and then will fetch the wanted attribute from it and construct the result. The computation method for cacheTuple(), in turn, will obtain the tuple if that hasn't happened yet, usually from the PostgreSQL syscache. We copy it to a long-lived memory context where we can keep it until its invalidation. The most common way the cacheTuple is fetched is by a one-argument syscache search by the object's Oid. When that is all that is needed, the Java class need only implement cacheId() to return the number of the PostgreSQL syscache to search in. For exceptional cases (attributes, for example, require a two-argument syscache search), a class should just provide its own cacheTuple computation method. The slots for an object are associated with a Java SwitchPoint, and the mapping from the object to its associated SwitchPoint is a function supplied to the SwitchPointCache.Builder. Some classes, such as RegClass and RegType, will allocate a SwitchPoint per object, and can be selectively invalidated. Otherwise, by default, the s_globalPoint declared here can be used, which will invalidate all values of all slots depending on it. --- .../org/postgresql/pljava/model/RegType.java | 9 + pljava-so/src/main/c/ModelUtils.c | 194 +++++++++ pljava/src/main/java/module-info.java | 2 + .../pljava/pg/CatalogObjectImpl.java | 379 +++++++++++++++++- .../postgresql/pljava/pg/RegClassImpl.java | 7 + .../pljava/pg/RegNamespaceImpl.java | 6 + .../org/postgresql/pljava/pg/RegRoleImpl.java | 7 + .../org/postgresql/pljava/pg/RegTypeImpl.java | 57 ++- .../pljava/pg/adt/ArrayAdapter.java | 193 +++++++++ .../postgresql/pljava/pg/adt/OidAdapter.java | 158 ++++++++ .../postgresql/pljava/pg/adt/TextAdapter.java | 73 ++++ .../pljava/pg/adt/package-info.java | 20 + 12 files changed, 1100 insertions(+), 5 deletions(-) create mode 100644 pljava/src/main/java/org/postgresql/pljava/pg/adt/ArrayAdapter.java create mode 100644 pljava/src/main/java/org/postgresql/pljava/pg/adt/OidAdapter.java create mode 100644 pljava/src/main/java/org/postgresql/pljava/pg/adt/TextAdapter.java create mode 100644 pljava/src/main/java/org/postgresql/pljava/pg/adt/package-info.java 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 index 76143bbf0..d3384f6d7 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/model/RegType.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/model/RegType.java @@ -106,8 +106,17 @@ public interface RegType RegType REGCOLLATION = formObjectId(CLASSID, REGCOLLATIONOID); RegClass relation(); + RegType element(); 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. */ diff --git a/pljava-so/src/main/c/ModelUtils.c b/pljava-so/src/main/c/ModelUtils.c index 23696037a..544cbee21 100644 --- a/pljava-so/src/main/c/ModelUtils.c +++ b/pljava-so/src/main/c/ModelUtils.c @@ -31,6 +31,7 @@ #include "pljava/ModelUtils.h" #include "pljava/VarlenaWrapper.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" @@ -186,6 +187,36 @@ 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 charsetMethods[] = { { @@ -296,6 +327,10 @@ void pljava_ModelUtils_initialize(void) { 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/CharsetEncodingImpl$EarlyNatives"); PgObject_registerNatives2(cls, charsetMethods); JNI_deleteLocalRef(cls); @@ -355,6 +390,165 @@ void pljava_ModelUtils_initialize(void) RegisterResourceReleaseCallback(resourceReleaseCB, NULL); } +/* + * 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], (AttrNumber)oidCol, 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_CharsetEncodingImpl_EarlyNatives * Method: _serverEncoding diff --git a/pljava/src/main/java/module-info.java b/pljava/src/main/java/module-info.java index 974bed643..0fbb52f0b 100644 --- a/pljava/src/main/java/module-info.java +++ b/pljava/src/main/java/module-info.java @@ -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; diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/CatalogObjectImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/CatalogObjectImpl.java index 9b3359645..93526dc08 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/CatalogObjectImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/CatalogObjectImpl.java @@ -14,16 +14,38 @@ import org.postgresql.pljava.Adapter; import org.postgresql.pljava.Adapter.As; +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 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.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; @@ -31,10 +53,12 @@ import java.sql.SQLException; +import java.util.ArrayList; import java.util.List; import java.util.Set; import java.util.Optional; import java.util.function.Consumer; +import java.util.function.UnaryOperator; import java.util.function.Supplier; /** @@ -464,10 +488,210 @@ static Supplier ctorIfKnown( } } + /* + * 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 { + /** + * 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() { @@ -477,9 +701,69 @@ public RegClass.Known classId() @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; + } } /** @@ -521,7 +805,16 @@ interface Named> @Override default T name() { - throw notyet(); + try + { + MethodHandle h = + ((CatalogObjectImpl.Addressed)this).m_slots[SLOT_NAME]; + return (T)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } } } @@ -531,7 +824,16 @@ interface Namespaced> @Override default RegNamespace namespace() { - throw notyet(); + try + { + MethodHandle h = + ((CatalogObjectImpl.Addressed)this).m_slots[SLOT_NAMESPACE]; + return (RegNamespace)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } } } @@ -540,7 +842,16 @@ interface Owned extends CatalogObject.Owned @Override default RegRole owner() { - throw notyet(); + try + { + MethodHandle h = + ((CatalogObjectImpl.Addressed)this).m_slots[SLOT_OWNER]; + return (RegRole)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } } } @@ -550,7 +861,21 @@ interface AccessControlled @Override default List grants() { - throw notyet(); + 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 @@ -560,6 +885,52 @@ default List grants(RegRole grantee) } } + /** + * 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<>(AsFlatList.of(AsFlatList::nullsIncludedCopy), + REGCLASS_INSTANCE); + + ArrayAdapter,?> REGTYPE_LIST_INSTANCE = + new ArrayAdapter<>(AsFlatList.of(AsFlatList::nullsIncludedCopy), + REGTYPE_INSTANCE); + + /** + * List of {@code Identifier.Simple} from an array of {@code TEXT} + * that represents SQL identifiers. + */ + ArrayAdapter,?> TEXT_NAME_LIST_INSTANCE = + new ArrayAdapter<>( + /* + * 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,String>() + { + @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); + } + }, + TextAdapter.INSTANCE); + } + private static final StackWalker s_walker = StackWalker.getInstance(Set.of(), 2); diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/RegClassImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/RegClassImpl.java index 7381cd469..2805154cc 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/RegClassImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/RegClassImpl.java @@ -16,6 +16,7 @@ import org.postgresql.pljava.model.*; import org.postgresql.pljava.pg.CatalogObjectImpl.*; +import static org.postgresql.pljava.pg.ModelConstants.RELOID; // syscache import org.postgresql.pljava.sqlgen.Lexicals.Identifier.Simple; @@ -35,6 +36,12 @@ static class Known> { } + @Override + int cacheId() + { + return RELOID; + } + RegClassImpl() { } diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/RegNamespaceImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/RegNamespaceImpl.java index 856e68206..9495a08c9 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/RegNamespaceImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/RegNamespaceImpl.java @@ -14,6 +14,7 @@ import org.postgresql.pljava.model.*; import org.postgresql.pljava.pg.CatalogObjectImpl.*; +import static org.postgresql.pljava.pg.ModelConstants.NAMESPACEOID; // syscache import org.postgresql.pljava.sqlgen.Lexicals.Identifier.Simple; @@ -22,4 +23,9 @@ class RegNamespaceImpl extends Addressed Nonshared, Named, Owned, AccessControlled, RegNamespace { + @Override + int cacheId() + { + return NAMESPACEOID; + } } diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/RegRoleImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/RegRoleImpl.java index 821df6d3f..1f772ac27 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/RegRoleImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/RegRoleImpl.java @@ -21,6 +21,7 @@ import org.postgresql.pljava.model.*; import org.postgresql.pljava.pg.CatalogObjectImpl.*; +import static org.postgresql.pljava.pg.ModelConstants.AUTHOID; // syscache import org.postgresql.pljava.sqlgen.Lexicals.Identifier.Simple; @@ -35,6 +36,12 @@ class RegRoleImpl extends Addressed Shared, Named, AccessControlled, RegRole.Grantee { + @Override + int cacheId() + { + return AUTHOID; + } + /* API methods */ @Override diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/RegTypeImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/RegTypeImpl.java index d2c93d70c..ccff03a0c 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/RegTypeImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/RegTypeImpl.java @@ -11,11 +11,14 @@ */ package org.postgresql.pljava.pg; +import java.lang.invoke.MethodHandle; + import java.sql.SQLType; import org.postgresql.pljava.model.*; import org.postgresql.pljava.pg.CatalogObjectImpl.*; +import static org.postgresql.pljava.pg.ModelConstants.TYPEOID; // syscache import org.postgresql.pljava.sqlgen.Lexicals.Identifier.Simple; @@ -38,6 +41,28 @@ abstract class RegTypeImpl extends Addressed Nonshared, Namespaced, Owned, AccessControlled, RegType { + @Override + int cacheId() + { + return TYPEOID; + } + + /** + * 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); + } + + /** + * Temporary scaffolding. + */ + RegTypeImpl() + { + } + @Override public TupleDescriptor.Interned tupleDescriptor() { @@ -50,6 +75,12 @@ public RegClass relation() throw notyet(); } + @Override + public RegType element() + { + throw notyet(); + } + /** * Return the expected zero value for {@code subId}. *

    @@ -100,6 +131,11 @@ public RegType modifier(int typmod) return (RegType) CatalogObjectImpl.Factory.formMaybeModifiedType(oid(), typmod); } + + public RegType withoutModifier() + { + return this; + } } /** @@ -115,7 +151,8 @@ static class Modified extends RegTypeImpl Modified(NoModifier base) { - m_base = base; + super(base.m_slots); + m_base = base; // must keep it live, not only share its slots } @Override @@ -125,6 +162,12 @@ public RegType modifier(int typmod) return this; return m_base.modifier(typmod); } + + @Override + public RegType withoutModifier() + { + return m_base; + } } /** @@ -138,11 +181,23 @@ static class Blessed extends RegTypeImpl { TupleDescriptor.Interned[] m_tupDescHolder; + Blessed() + { + super(((RegTypeImpl)RECORD).m_slots); + // RECORD is static final, no other effort needed to keep it live + } + @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; + } } } 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..332420da1 --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/pg/adt/ArrayAdapter.java @@ -0,0 +1,193 @@ +/* + * 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.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 + */ + +/** + * PostgreSQL arrays represented as something or other. + */ +public class ArrayAdapter extends Adapter.Array +{ + private static final Configuration s_config; + + 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<>( + AsFlatList.of(AsFlatList::nullsIncludedCopy), TextAdapter.INSTANCE); + } + + public static ArrayAdapter + arrayAdapter(Contract.Array contract, Adapter.As element) + { + try + { + return new ArrayAdapter<>(contract, element); + } + catch ( Throwable t ) + { + t.printStackTrace(); + throw t; + } + } + + public ArrayAdapter(Contract.Array contract, Adapter.As element) + { + super(contract, element, null, s_config); + } + + @Override + public boolean canFetch(RegType pgType) + { + RegType elementType = pgType.element(); + return elementType.isValid() && m_elementAdapter.canFetch(elementType); + } + + 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; + + 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); + + return m_contract.construct( + nDims, dimsBoundsArray, m_elementAdapter, tti); + } + finally + { + in.unpin(); + in.close(); + } + } +} 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..5188f4cb0 --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/pg/adt/OidAdapter.java @@ -0,0 +1,158 @@ +/* + * 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.*; + +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 REGNAMESPACE_INSTANCE; + public static final Addressed REGROLE_INSTANCE; + public static final Addressed REGTYPE_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) + }); + + INSTANCE = new OidAdapter<>(configs[0], null); + + INT4_INSTANCE = new Int4(configs[1]); + + REGCLASS_INSTANCE = new Addressed<>(configs[2], + RegClass.CLASSID, RegClass.class, RegType.REGCLASS); + + REGNAMESPACE_INSTANCE = new Addressed<>(configs[2], + RegNamespace.CLASSID, RegNamespace.class, RegType.REGNAMESPACE); + + REGROLE_INSTANCE = new Addressed<>(configs[2], + RegRole.CLASSID, RegRole.class, RegType.REGROLE); + + REGTYPE_INSTANCE = new Addressed<>(configs[2], + RegType.CLASSID, RegType.class, RegType.REGTYPE); + } + + /** + * 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 = specificTypes; + } + + @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); + } + } +} 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/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; From a4656f568c25bd6dc3e5c429acd5ae90581dbc93 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 22 Jan 2022 23:19:48 -0500 Subject: [PATCH 026/334] RegClass and RegType have a special relationship They are the two CatalogObjects with tupleDescriptor() methods. You can get strictly more tuple descriptors by asking RegType; a RegType.Blessed can give you a tuple descriptor that has been interned in the PostgreSQL typcache and corresponds to nothing in the system catalogs. But whenever a RegType t is an ordinary cataloged composite type or the row type of a cataloged relation, then there is a RegClass c such that c == t.relation() and t == c.type(), and you will get the same tuple descriptor from the tupleDescriptor() method of either c or t. In all but one such case, c delegates to c.type().tupleDescriptor() and lets the RegType do the work, obtaining the descriptor from the PG typcache. The one exception is when the tuple descriptor for pg_class itself is wanted, in which case the RegClass does the work, obtaining the descriptor from the PG relcache, and RegType delegates to it for that one exceptional case. The reason is that RegClass will see the first request for the pg_class tuple descriptor, and before that is available, c.type() can't be evaluated. In either case, whichever class looked it up, a cataloged tuple descriptor is always stored on the RegClass instance, and RegClass will be responsible for its invalidation if the relation is altered. (A RegType.Blessed has its own field for its tuple descriptor, because there is no corresponding RegClass for one of those.) Because of this close connection between RegClass and RegType, the methods RegClass.type() and RegType.relation() use a handshake protocol to ensure that, whenever either method is called, not only does it cache the result, but its counterpart for that result instance caches the reverse result, so the connection can later be traversed in either direction with no need for a lookup by oid. In the static initializer pattern introduced here, the handful of SwitchPointCache slots that are predefined in CatalogObject.Addressed are added to, by starting an int index at Addressed.NSLOTS, incrementing it to initialize additional slot index constants, then using its final value to define a new NSLOTS that shadows the original. --- .../postgresql/pljava/pg/RegClassImpl.java | 203 +++++++++++++- .../org/postgresql/pljava/pg/RegTypeImpl.java | 263 +++++++++++++++++- 2 files changed, 460 insertions(+), 6 deletions(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/RegClassImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/RegClassImpl.java index 2805154cc..ff3f71bee 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/RegClassImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/RegClassImpl.java @@ -11,15 +11,33 @@ */ 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.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.adt.OidAdapter.REGTYPE_INSTANCE; + import org.postgresql.pljava.sqlgen.Lexicals.Identifier.Simple; +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(). @@ -36,6 +54,14 @@ static class Known> { } + /** + * Per-instance switch point, to be invalidated selectively + * by a relcache callback. + */ + SwitchPoint m_cacheSwitchPoint; + + private static UnaryOperator s_initializer; + @Override int cacheId() { @@ -44,20 +70,193 @@ int cacheId() RegClassImpl() { + super(s_initializer.apply(new MethodHandle[NSLOTS])); + m_cacheSwitchPoint = new SwitchPoint(); } + /** + * 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 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(RegClassImpl.class.getDeclaredMethods()) + .withDependent( "tupleDescriptor", SLOT_TUPLEDESCRIPTOR = i++) + .withDependent( "type", SLOT_TYPE = i++) + .build(); + NSLOTS = i; + } + + /** + * 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; + } + @Override public TupleDescriptor.Interned tupleDescriptor() { - throw notyet(); + 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() { - throw notyet(); + try + { + MethodHandle h = m_slots[SLOT_TYPE]; + return (RegType)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } } @Override diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/RegTypeImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/RegTypeImpl.java index ccff03a0c..151cf2b0c 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/RegTypeImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/RegTypeImpl.java @@ -12,16 +12,31 @@ 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.util.function.UnaryOperator; + +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.adt.OidAdapter.REGCLASS_INSTANCE; + import org.postgresql.pljava.sqlgen.Lexicals.Identifier.Simple; +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. @@ -41,6 +56,14 @@ abstract class RegTypeImpl extends Addressed Nonshared, Namespaced, Owned, AccessControlled, RegType { + /** + * For the time being, punt and return the global switch point. + */ + SwitchPoint cacheSwitchPoint() + { + return s_globalPoint[0]; + } + @Override int cacheId() { @@ -57,22 +80,173 @@ int cacheId() } /** - * Temporary scaffolding. + * Holder for the {@code RegClass} corresponding to {@code relation()}, + * only non-null during a call of {@code dualHandshake}. */ - RegTypeImpl() + private RegClass m_dual = null; + + /** + * 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_RELATION; + 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(RegTypeImpl.class.getDeclaredMethods()) + .withDependent( + "tupleDescriptorCataloged", SLOT_TUPLEDESCRIPTOR = i++) + .withDependent( "relation", SLOT_RELATION = i++) + + .build(); + NSLOTS = i; + } + + /** + * 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 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(t.descriptor().get("typrelid"), REGCLASS_INSTANCE); + + ((RegClassImpl)c).dualHandshake(o); + return c; } @Override public TupleDescriptor.Interned tupleDescriptor() { - throw notyet(); + try + { + MethodHandle h = m_slots[SLOT_TUPLEDESCRIPTOR]; + return ((TupleDescriptor.Interned[])h.invokeExact(this, h))[0]; + } + catch ( Throwable t ) + { + throw unchecked(t); + } } @Override public RegClass relation() { - throw notyet(); + try + { + MethodHandle h = m_slots[SLOT_RELATION]; + return (RegClass)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } } @Override @@ -117,6 +291,11 @@ public int modifier() */ static class NoModifier extends RegTypeImpl { + NoModifier() + { + super(s_initializer.apply(new MethodHandle[NSLOTS])); + } + @Override public int modifier() { @@ -179,12 +358,72 @@ public RegType withoutModifier() */ 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; + } 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 @@ -199,5 +438,21 @@ 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(); + } } } From e742fae751c1a2ca8ab960b90ab250c6dd9ee7d7 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 22 Jan 2022 23:20:17 -0500 Subject: [PATCH 027/334] Attribute, naturally, has complications An Attribute is most often obtained from a TupleDescriptor (in this API, that's how it's done), and the TupleDescriptor can supply a version of Attribute's tuple directly; no need to look it up anywhere else. That copy, however, cuts off at ATTRIBUTE_FIXED_PART_SIZE bytes. The most commonly needed attributes of Attribute are found there, but for others beyond that cutoff, the full tuple has to be fetched from the syscache. So AttributeImpl has the normal SLOT_TUPLE slot, used for the rarely-needed full tuple, and also its own SLOT_PARTIALTUPLE, for the truncated version obtained from the containing tuple descriptor. Most computation methods will fetch from the partial one, with the full one referred to only by the ones that need it. It doesn't end there. A few critical Attribute properties, byValue, alignment, length, and type/typmod, are needed to successfully fetch values from a TupleTableSlotImpl.Heap. So Attribute cannot use that API to fetch those values. For those, it must hardcode their actual offsets and sizes in the raw ByteBuffer that the containing tuple descriptor supplies, and fetch them directly. So there is also a SLOT_RAWBUFFER. This may sound more costly in space than it is. The raw buffer, of course, is just a ByteBuffer sliced off and sharing the larger one in the TupleDescriptor, and the partial tuple is just a TupleTableSlot instance built over that. The full tuple is another complete copy, but only fetched when those less-commonly-needed attributes are requested. With those key values obtained from the raw buffer, the Attribute's name does not require any such contortions, and can be fetched using the civilized TupleTableSlot API, except it can't be done by name, so the attribute number is used for that one. An AttributeImpl.Transient holds a direct reference to the TupleDescriptor it came from, which its containingTupleDescriptor() method returns. An AttributeImpl.Cataloged does not, and instead holds a reference to the RegClass for which it is defined in the system catalogs, and containingTupleDescriptor() delegates to tupleDescriptor() on that. If the relation has been altered, that could return an updated new tuple descriptor. --- .../postgresql/pljava/model/Attribute.java | 10 + .../org/postgresql/pljava/model/RegType.java | 5 + .../postgresql/pljava/pg/AttributeImpl.java | 345 +++++++++++++++++- .../org/postgresql/pljava/pg/RegTypeImpl.java | 72 ++++ .../postgresql/pljava/pg/adt/NameAdapter.java | 149 ++++++++ .../postgresql/pljava/pg/adt/Primitives.java | 222 +++++++++++ 6 files changed, 797 insertions(+), 6 deletions(-) create mode 100644 pljava/src/main/java/org/postgresql/pljava/pg/adt/NameAdapter.java create mode 100644 pljava/src/main/java/org/postgresql/pljava/pg/adt/Primitives.java 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 index cb88b0d7c..33ccf54de 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/model/Attribute.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/model/Attribute.java @@ -44,4 +44,14 @@ public interface Attribute short length(); boolean byValue(); Alignment alignment(); + + /** + * 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/RegType.java b/pljava-api/src/main/java/org/postgresql/pljava/model/RegType.java index d3384f6d7..6ff7c3c0d 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/model/RegType.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/model/RegType.java @@ -17,6 +17,8 @@ import static org.postgresql.pljava.model.CatalogObject.Factory.*; +import org.postgresql.pljava.annotation.BaseUDT.Alignment; + import org.postgresql.pljava.sqlgen.Lexicals.Identifier.Simple; /** @@ -105,8 +107,11 @@ public interface RegType RegType REGROLE = formObjectId(CLASSID, REGROLEOID); RegType REGCOLLATION = formObjectId(CLASSID, REGCOLLATIONOID); + short length(); + boolean byValue(); RegClass relation(); RegType element(); + Alignment alignment(); RegType modifier(int typmod); /** diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/AttributeImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/AttributeImpl.java index a6b0a5e23..cc1dce800 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/AttributeImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/AttributeImpl.java @@ -11,53 +11,333 @@ */ 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 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.NameAdapter; import org.postgresql.pljava.annotation.BaseUDT.Alignment; 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 { + abstract SwitchPoint cacheSwitchPoint(); + + private static UnaryOperator s_initializer; + @Override - public RegClass relation() + 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); + } + + 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); + } + + 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_BYVALUE; + static final int SLOT_ALIGNMENT; + + 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) + + /* + * 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( "byValue", SLOT_BYVALUE = i++) + .withDependent( "alignment", SLOT_ALIGNMENT = i++) + + .build(); + NSLOTS = i; + } + + /* 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 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) { - throw notyet(); + ByteBuffer b = o.rawBuffer(); + assert 1 == SIZEOF_pg_attribute_attalign : "sizeof attalign changed"; + return alignmentFromCatalog(b.get(OFFSET_pg_attribute_attalign)); } + /* 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() { - throw notyet(); + try + { + MethodHandle h = m_slots[SLOT_TYPE]; + return (RegType)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } } @Override public short length() { - throw notyet(); + try + { + MethodHandle h = m_slots[SLOT_LENGTH]; + return (short)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } } @Override public boolean byValue() { - throw notyet(); + try + { + MethodHandle h = m_slots[SLOT_BYVALUE]; + return (boolean)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } } @Override public Alignment alignment() { - throw notyet(); + try + { + MethodHandle h = m_slots[SLOT_ALIGNMENT]; + return (Alignment)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + + @Override + public TupleDescriptor containingTupleDescriptor() + { + return relation().tupleDescriptor(); } boolean foundIn(TupleDescriptor td) @@ -81,6 +361,18 @@ static class Cataloged extends AttributeImpl { m_relation = requireNonNull(relation); } + + @Override + SwitchPoint cacheSwitchPoint() + { + return m_relation.m_cacheSwitchPoint; + } + + @Override + public RegClass relation() + { + return m_relation; + } } /** @@ -98,9 +390,20 @@ static class Cataloged extends AttributeImpl */ 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); @@ -156,6 +459,18 @@ public int hashCode() return super.hashCode(); } + @Override + public RegClass relation() + { + return s_invalidClass; + } + + @Override + public TupleDescriptor containingTupleDescriptor() + { + return m_containingTupleDescriptor; + } + @Override boolean foundIn(TupleDescriptor td) { @@ -195,5 +510,23 @@ public RegType type() { return m_type; } + + @Override + public short length() + { + return m_type.length(); + } + + @Override + public boolean byValue() + { + return m_type.byValue(); + } + + @Override + public Alignment alignment() + { + return m_type.alignment(); + } } } diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/RegTypeImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/RegTypeImpl.java index 151cf2b0c..7b53a8e91 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/RegTypeImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/RegTypeImpl.java @@ -30,8 +30,12 @@ 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.adt.OidAdapter.REGCLASS_INSTANCE; +import static org.postgresql.pljava.pg.adt.Primitives.*; + +import org.postgresql.pljava.annotation.BaseUDT.Alignment; import org.postgresql.pljava.sqlgen.Lexicals.Identifier.Simple; @@ -109,7 +113,10 @@ void dualHandshake(RegClass dual) static final UnaryOperator s_initializer; static final int SLOT_TUPLEDESCRIPTOR; + static final int SLOT_LENGTH; + static final int SLOT_BYVALUE; static final int SLOT_RELATION; + static final int SLOT_ALIGNMENT; static final int NSLOTS; static @@ -123,7 +130,10 @@ void dualHandshake(RegClass dual) .withCandidates(RegTypeImpl.class.getDeclaredMethods()) .withDependent( "tupleDescriptorCataloged", SLOT_TUPLEDESCRIPTOR = i++) + .withDependent( "length", SLOT_LENGTH = i++) + .withDependent( "byValue", SLOT_BYVALUE = i++) .withDependent( "relation", SLOT_RELATION = i++) + .withDependent( "alignment", SLOT_ALIGNMENT = i++) .build(); NSLOTS = i; @@ -198,6 +208,18 @@ private static TupleDescriptor.Interned[] tupleDescriptorBlessed(Blessed o) return o.m_tupDescHolder = r; } + private static short length(RegTypeImpl o) throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return t.get(t.descriptor().get("typlen"), INT2_INSTANCE); + } + + private static boolean byValue(RegTypeImpl o) throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return t.get(t.descriptor().get("typbyval"), BOOLEAN_INSTANCE); + } + private static RegClass relation(RegTypeImpl o) throws SQLException { /* @@ -221,6 +243,13 @@ private static RegClass relation(RegTypeImpl o) throws SQLException return c; } + private static Alignment alignment(RegTypeImpl o) throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return alignmentFromCatalog( + t.get(t.descriptor().get("typalign"), INT1_INSTANCE)); + } + @Override public TupleDescriptor.Interned tupleDescriptor() { @@ -235,6 +264,34 @@ public TupleDescriptor.Interned tupleDescriptor() } } + @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 boolean byValue() + { + try + { + MethodHandle h = m_slots[SLOT_BYVALUE]; + return (boolean)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } + } + @Override public RegClass relation() { @@ -255,6 +312,21 @@ public RegType element() throw notyet(); } + @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 + } + /** * Return the expected zero value for {@code subId}. *

    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/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; + } + } +} From 5adf2c8d72b181520f3b6d8ec780f2210dfc9b21 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 22 Jan 2022 23:20:57 -0500 Subject: [PATCH 028/334] Selectively invalidate RegClass and RegType RegClass is an easy choice, because those invalidations are also the invalidations of TupleDescriptors, and because it has a nice API; we are passed the oid of the relation to invalidate, so we acquire the target in O(1). (Note in passing: AttributeImpl is built on SwitchPointCache in the pattern that's emerged for CatalogObjects in general, and an AttributeImpl.Cataloged uses the SwitchPoint of the RegClass, so it's clear that all the attributes of the associated tuple descriptor will do the right thing upon invalidation. In contrast, TupleDescriptorImpl itself isn't quite built that way, and the question of just how a TupleDescriptor itself should act after invalidation hasn't been fully nailed down yet.) RegType is probably also worth invalidating selectively, as is probably RegProcedure (procedures are mainly what we're about in PL/Java. right?), though only RegType is done here. That API is less convenient; we are passed not the oid but a hash of the oid, and not the hash that Java uses. The solution here is brute force, to get an initial working implementation. There are plenty of opportunities for optimization. One idea would be to use a subclass of SwitchPoint that would set a flag, or invoke a Runnable, the first time its guardWithTest method is called. If that hasn't happened, there is nothing to invalidate. The Runnable could add the containing object into some data structure more easily searched by the supplied hash. Transitions of the data structure between empty and not-empty could be propagated to a boolean in native memory, where the C callback code could avoid the Java upcall entirely if there is nothing to do. This commit contains none of those optimizations. Factory.invalidateType might be misnamed; it could be syscacheInvalidate and take the syscache id as another parameter, and then dispatch to invalidating a RegType or RegProcedure or what have you, as the case may be. At least, that would be a more concise implementation than providing separate Java methods and having the C callback decide which to call. But if some later optimization is tracking anything-to-invalidate? separately for them, then the C code might be the efficient place for the check to be done. PostgreSQL has a limited number of slots for invalidation callbacks, and requires a separate registration (using another slot) for each syscache id for which callbacks are wanted (even though you get the affected syscache id in the callback?!). It would be antisocial to grab one for every sort of CatalogObject supported here, so we will have many relying on CatalogObject.Addressed.s_globalPoint and some strategy for zapping that every so often. That is not included in this commit. (The globalPoint exists, but there is not yet anything that ever zaps it.) Some imperfect strategy that isn't guaranteed conservative might be necessary, and might be tolerable (PL/Java has existed for years with less attention to invalidation). An early idea was to zap the globalPoint on every transaction or subtransaction boundary, or when the command counter has been incremented; those are times when PostgreSQL processes invalidations. However, invalidations are also processed any time locks are acquired, and that doesn't sound as if it would be practical to intercept (or as if the resulting behavior would be practical, even if it could be done). Another solution approach would just be to expose a zapGlobalPoint knob as API; if some code wants to be sure it is not seeing something stale (in any CatalogObject we aren't doing selective invalidation for), it can just say so before fetching it. --- pljava-so/src/main/c/ModelUtils.c | 40 ++++++++++ .../pljava/pg/CatalogObjectImpl.java | 80 +++++++++++++++++++ .../postgresql/pljava/pg/RegClassImpl.java | 32 ++++++++ .../org/postgresql/pljava/pg/RegTypeImpl.java | 58 ++++++++++++-- 4 files changed, 205 insertions(+), 5 deletions(-) diff --git a/pljava-so/src/main/c/ModelUtils.c b/pljava-so/src/main/c/ModelUtils.c index 544cbee21..9bd7b47db 100644 --- a/pljava-so/src/main/c/ModelUtils.c +++ b/pljava-so/src/main/c/ModelUtils.c @@ -55,6 +55,10 @@ * 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); @@ -71,6 +75,9 @@ static jclass s_TupleTableSlotImpl_class; static jmethodID s_TupleTableSlotImpl_newDeformed; static jmethodID s_TupleTableSlotImpl_supplyHeapTuples; +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); @@ -139,6 +146,12 @@ static void memoryContextCallback(void *arg) 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) { @@ -178,6 +191,19 @@ static void resourceReleaseCB(ResourceReleasePhase phase, 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); @@ -331,6 +357,16 @@ void pljava_ModelUtils_initialize(void) PgObject_registerNatives2(cls, catalogObjectAddressedMethods); JNI_deleteLocalRef(cls); + cls = PgObject_getJavaClass("org/postgresql/pljava/pg/CatalogObjectImpl$Factory"); + s_CatalogObjectImpl_Factory_class = JNI_newGlobalRef(cls); + 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); @@ -388,6 +424,10 @@ void pljava_ModelUtils_initialize(void) "(Ljava/nio/ByteBuffer;)Ljava/util/List;"); RegisterResourceReleaseCallback(resourceReleaseCB, NULL); + + CacheRegisterRelcacheCallback(relCacheCB, 0); + + CacheRegisterSyscacheCallback(TYPEOID, sysCacheCB, 0); } /* diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/CatalogObjectImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/CatalogObjectImpl.java index 93526dc08..3bf493c13 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/CatalogObjectImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/CatalogObjectImpl.java @@ -486,6 +486,70 @@ static Supplier ctorIfKnown( 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); + } } /* @@ -952,4 +1016,20 @@ 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/RegClassImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/RegClassImpl.java index ff3f71bee..eb3145dc7 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/RegClassImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/RegClassImpl.java @@ -74,6 +74,38 @@ int cacheId() 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. diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/RegTypeImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/RegTypeImpl.java index 7b53a8e91..6bed4f4d3 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/RegTypeImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/RegTypeImpl.java @@ -21,6 +21,8 @@ import java.sql.SQLType; import java.sql.SQLException; +import java.util.List; + import java.util.function.UnaryOperator; import static org.postgresql.pljava.internal.SwitchPointCache.doNotCache; @@ -61,12 +63,13 @@ abstract class RegTypeImpl extends Addressed AccessControlled, RegType { /** - * For the time being, punt and return the global switch point. + * 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. */ - SwitchPoint cacheSwitchPoint() - { - return s_globalPoint[0]; - } + abstract SwitchPoint cacheSwitchPoint(); @Override int cacheId() @@ -83,6 +86,23 @@ int cacheId() 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}. @@ -363,9 +383,25 @@ public int 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 @@ -400,6 +436,12 @@ 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); @@ -467,6 +509,12 @@ static class Blessed extends RegTypeImpl NSLOTS = i; } + @Override + SwitchPoint cacheSwitchPoint() + { + return ((NoModifier)RECORD).m_sp; + } + Blessed() { super(((RegTypeImpl)RECORD).m_slots); From 33530b50579127aafc502204094529a975d79bb2 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 22 Jan 2022 23:21:26 -0500 Subject: [PATCH 029/334] Here's a simple complete one, a Database object Which means also bringing in a GrantAdapter (needed for the full implementation of the CatalogObject.AccessControlled subtypes) and EncodingAdapter (needed for this and RegCollation, anyway). This also has a static final CURRENT field for convenient access to the instance representing the current database, bringing one more native method to obtain the value. --- .../pljava/model/CatalogObject.java | 8 + .../org/postgresql/pljava/model/Database.java | 60 ++++ pljava-so/src/main/c/ModelConstants.c | 39 +++ pljava-so/src/main/c/ModelUtils.c | 22 ++ .../org/postgresql/pljava/pg/AclItem.java | 262 +++++++++++++++++ .../pljava/pg/CatalogObjectImpl.java | 11 + .../postgresql/pljava/pg/DatabaseImpl.java | 263 ++++++++++++++++++ .../pljava/pg/adt/EncodingAdapter.java | 61 ++++ .../pljava/pg/adt/GrantAdapter.java | 83 ++++++ .../postgresql/pljava/pg/adt/OidAdapter.java | 4 + 10 files changed, 813 insertions(+) create mode 100644 pljava-api/src/main/java/org/postgresql/pljava/model/Database.java create mode 100644 pljava/src/main/java/org/postgresql/pljava/pg/AclItem.java create mode 100644 pljava/src/main/java/org/postgresql/pljava/pg/DatabaseImpl.java create mode 100644 pljava/src/main/java/org/postgresql/pljava/pg/adt/EncodingAdapter.java create mode 100644 pljava/src/main/java/org/postgresql/pljava/pg/adt/GrantAdapter.java 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 index c4f10332e..ac0391def 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/model/CatalogObject.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/model/CatalogObject.java @@ -437,6 +437,11 @@ T formObjectId(RegClass.Known classId, int objId) return INSTANCE.formObjectIdImpl(classId, objId); } + static Database currentDatabase(RegClass.Known classId) + { + return INSTANCE.currentDatabaseImpl(classId); + } + static RegRole.Grantee publicGrantee() { return INSTANCE.publicGranteeImpl(); @@ -449,6 +454,9 @@ RegClass.Known formClassIdImpl( protected abstract > T formObjectIdImpl(RegClass.Known classId, int objId); + protected abstract Database + currentDatabaseImpl(RegClass.Known classId); + protected abstract RegRole.Grantee publicGranteeImpl(); protected abstract CharsetEncoding serverEncoding(); 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-so/src/main/c/ModelConstants.c b/pljava-so/src/main/c/ModelConstants.c index 61604f0ec..0b8ed34fc 100644 --- a/pljava-so/src/main/c/ModelConstants.c +++ b/pljava-so/src/main/c/ModelConstants.c @@ -37,6 +37,9 @@ #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" @@ -267,6 +270,42 @@ StaticAssertStmt((c) == \ #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 ); + CONFIRMCONST( N_ACL_RIGHTS ); + 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 ); + + StaticAssertStmt( + sizeof(AclItem) == org_postgresql_pljava_pg_AclItem_SIZEOF_AclItem, + "Java/C size mismatch for AclItem"); + +#undef CONFIRMCONST +#undef CONFIRMOFFSET + #define CONFIRMCONST(c) \ StaticAssertStmt((c) == \ (org_postgresql_pljava_pg_ModelConstants_##c), \ diff --git a/pljava-so/src/main/c/ModelUtils.c b/pljava-so/src/main/c/ModelUtils.c index 9bd7b47db..2e1c1641e 100644 --- a/pljava-so/src/main/c/ModelUtils.c +++ b/pljava-so/src/main/c/ModelUtils.c @@ -243,6 +243,16 @@ void pljava_ModelUtils_initialize(void) { 0, 0, 0 } }; + JNINativeMethod catalogObjectFactoryMethods[] = + { + { + "_currentDatabase", + "()I", + Java_org_postgresql_pljava_pg_CatalogObjectImpl_00024Factory__1currentDatabase + }, + { 0, 0, 0 } + }; + JNINativeMethod charsetMethods[] = { { @@ -359,6 +369,7 @@ void pljava_ModelUtils_initialize(void) 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( @@ -589,6 +600,17 @@ Java_org_postgresql_pljava_pg_CatalogObjectImpl_00024Addressed__1tupDescBootstra 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 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..cabc3a445 --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/pg/AclItem.java @@ -0,0 +1,262 @@ +/* + * 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 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; + +public abstract class AclItem implements CatalogObject.Grant +{ + @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; + @Native static final int N_ACL_RIGHTS = 12; + @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; + @Native static final int SIZEOF_AclItem = 12; + + /** + * 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. + */ + private static final String s_abbr = "arwdDxtXUCTc"; + + static + { + assert N_ACL_RIGHTS == s_abbr.length() : "AclItem abbreviations"; + assert N_ACL_RIGHTS == 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, + CatalogObject.EXECUTE, CatalogObject.CREATE_TEMP, CatalogObject.CONNECT + { + private final short m_priv; + private final short m_goption; + + public NonRole(ByteBuffer b) + { + super(b.getInt(OFFSET_ai_grantee), b.getInt(OFFSET_ai_grantor)); + int privs = b.getInt(OFFSET_ai_privs); + m_priv = (short)(privs & 0xffff); + m_goption = (short)(privs >>> 16); + } + + private boolean priv(short mask) + { + return 0 != (m_priv & mask); + } + + private boolean goption(short mask) + { + return 0 != (m_goption & mask); + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + if ( to().isValid() ) + sb.append(to().name()); + sb.append('='); + int priv = Short.toUnsignedInt(m_priv); + int goption = Short.toUnsignedInt(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); + } + } +} diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/CatalogObjectImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/CatalogObjectImpl.java index 3bf493c13..464aa7e47 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/CatalogObjectImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/CatalogObjectImpl.java @@ -249,6 +249,14 @@ 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() { @@ -467,6 +475,9 @@ static Supplier ctorIfKnown( case AuthIdRelationId: fieldRead = RegRole.CLASSID; return RegRoleImpl::new; + case DatabaseRelationId: + fieldRead = Database.CLASSID; + return DatabaseImpl::new; case NamespaceRelationId: fieldRead = RegNamespace.CLASSID; return RegNamespaceImpl::new; 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..5e7d1cc8b --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/pg/DatabaseImpl.java @@ -0,0 +1,263 @@ +/* + * 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.lang.invoke.MethodHandle; +import static java.lang.invoke.MethodHandles.lookup; +import java.lang.invoke.SwitchPoint; + +import java.sql.SQLException; + +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(t.descriptor().get("datname"), SIMPLE_INSTANCE); + } + + private static RegRole owner(DatabaseImpl o) throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return t.get(t.descriptor().get("datdba"), REGROLE_INSTANCE); + } + + private static List grants(DatabaseImpl o) + throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return t.get(t.descriptor().get("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; + } + + /* computation methods */ + + private static CharsetEncoding encoding(DatabaseImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(s.descriptor().get("encoding"), EncodingAdapter.INSTANCE); + } + + private static String collate(DatabaseImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(s.descriptor().get("datcollate"), AS_STRING_INSTANCE); + } + + private static String ctype(DatabaseImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(s.descriptor().get("datctype"), AS_STRING_INSTANCE); + } + + private static boolean template(DatabaseImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(s.descriptor().get("datistemplate"), BOOLEAN_INSTANCE); + } + + private static boolean allowConnection(DatabaseImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(s.descriptor().get("datallowconn"), BOOLEAN_INSTANCE); + } + + private static int connectionLimit(DatabaseImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(s.descriptor().get("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/adt/EncodingAdapter.java b/pljava/src/main/java/org/postgresql/pljava/pg/adt/EncodingAdapter.java new file mode 100644 index 000000000..076fab241 --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/pg/adt/EncodingAdapter.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2021 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..e60984bd5 --- /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<>( + AsFlatList.of(AsFlatList::nullsIncludedCopy), INSTANCE); + } + + 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/OidAdapter.java b/pljava/src/main/java/org/postgresql/pljava/pg/adt/OidAdapter.java index 5188f4cb0..03938168d 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/adt/OidAdapter.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/adt/OidAdapter.java @@ -34,6 +34,7 @@ public class OidAdapter public static final Addressed REGNAMESPACE_INSTANCE; public static final Addressed REGROLE_INSTANCE; public static final Addressed REGTYPE_INSTANCE; + public static final Addressed DATABASE_INSTANCE; static { @@ -61,6 +62,9 @@ public class OidAdapter REGTYPE_INSTANCE = new Addressed<>(configs[2], RegType.CLASSID, RegType.class, RegType.REGTYPE); + + DATABASE_INSTANCE = new Addressed<>(configs[2], + Database.CLASSID, Database.class); } /** From 7ffea740cfd48553f895631a11099426549f970f Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 22 Jan 2022 23:21:57 -0500 Subject: [PATCH 030/334] The rest is straight implementation --- .../postgresql/pljava/model/Attribute.java | 30 + .../postgresql/pljava/model/Extension.java | 44 ++ .../pljava/model/ProceduralLanguage.java | 68 ++ .../postgresql/pljava/model/RegCollation.java | 63 ++ .../postgresql/pljava/model/RegConfig.java | 34 + .../pljava/model/RegDictionary.java | 42 + .../postgresql/pljava/model/RegOperator.java | 69 ++ .../postgresql/pljava/model/RegProcedure.java | 160 ++++ .../org/postgresql/pljava/model/RegType.java | 44 ++ pljava-api/src/test/java/CatalogTest.java | 23 + .../postgresql/pljava/pg/AttributeImpl.java | 423 ++++++++++ .../pljava/pg/CatalogObjectImpl.java | 21 + .../postgresql/pljava/pg/ExtensionImpl.java | 266 +++++++ .../pljava/pg/ProceduralLanguageImpl.java | 237 ++++++ .../postgresql/pljava/pg/RegClassImpl.java | 328 +++++++- .../pljava/pg/RegCollationImpl.java | 275 +++++++ .../postgresql/pljava/pg/RegConfigImpl.java | 109 +++ .../pljava/pg/RegDictionaryImpl.java | 109 +++ .../pljava/pg/RegNamespaceImpl.java | 85 ++ .../postgresql/pljava/pg/RegOperatorImpl.java | 397 ++++++++++ .../pljava/pg/RegProcedureImpl.java | 729 ++++++++++++++++++ .../org/postgresql/pljava/pg/RegRoleImpl.java | 244 +++++- .../org/postgresql/pljava/pg/RegTypeImpl.java | 625 ++++++++++++++- .../postgresql/pljava/pg/TupleDescImpl.java | 16 +- .../pljava/pg/adt/ArgModeAdapter.java | 85 ++ .../pljava/pg/adt/DateTimeAdapter.java | 301 ++++++++ .../postgresql/pljava/pg/adt/OidAdapter.java | 58 +- .../postgresql/pljava/pg/adt/UUIDAdapter.java | 111 +++ .../postgresql/pljava/pg/adt/XMLAdapter.java | 103 +++ .../postgresql/pljava/pg/adt/XidAdapter.java | 195 +++++ 30 files changed, 5262 insertions(+), 32 deletions(-) create mode 100644 pljava-api/src/main/java/org/postgresql/pljava/model/Extension.java create mode 100644 pljava-api/src/main/java/org/postgresql/pljava/model/ProceduralLanguage.java create mode 100644 pljava-api/src/main/java/org/postgresql/pljava/model/RegCollation.java create mode 100644 pljava-api/src/main/java/org/postgresql/pljava/model/RegConfig.java create mode 100644 pljava-api/src/main/java/org/postgresql/pljava/model/RegDictionary.java create mode 100644 pljava-api/src/main/java/org/postgresql/pljava/model/RegOperator.java create mode 100644 pljava-api/src/main/java/org/postgresql/pljava/model/RegProcedure.java create mode 100644 pljava-api/src/test/java/CatalogTest.java create mode 100644 pljava/src/main/java/org/postgresql/pljava/pg/ExtensionImpl.java create mode 100644 pljava/src/main/java/org/postgresql/pljava/pg/ProceduralLanguageImpl.java create mode 100644 pljava/src/main/java/org/postgresql/pljava/pg/RegCollationImpl.java create mode 100644 pljava/src/main/java/org/postgresql/pljava/pg/RegConfigImpl.java create mode 100644 pljava/src/main/java/org/postgresql/pljava/pg/RegDictionaryImpl.java create mode 100644 pljava/src/main/java/org/postgresql/pljava/pg/RegOperatorImpl.java create mode 100644 pljava/src/main/java/org/postgresql/pljava/pg/RegProcedureImpl.java create mode 100644 pljava/src/main/java/org/postgresql/pljava/pg/adt/ArgModeAdapter.java create mode 100644 pljava/src/main/java/org/postgresql/pljava/pg/adt/DateTimeAdapter.java create mode 100644 pljava/src/main/java/org/postgresql/pljava/pg/adt/UUIDAdapter.java create mode 100644 pljava/src/main/java/org/postgresql/pljava/pg/adt/XMLAdapter.java create mode 100644 pljava/src/main/java/org/postgresql/pljava/pg/adt/XidAdapter.java 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 index 33ccf54de..d0fc94eca 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/model/Attribute.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/model/Attribute.java @@ -16,12 +16,23 @@ 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 @@ -39,11 +50,30 @@ public interface Attribute */ 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. 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/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/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/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/RegType.java b/pljava-api/src/main/java/org/postgresql/pljava/model/RegType.java index 6ff7c3c0d..ebfa73673 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/model/RegType.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/model/RegType.java @@ -17,8 +17,13 @@ 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; /** @@ -107,11 +112,50 @@ public interface RegType RegType REGROLE = formObjectId(CLASSID, REGROLEOID); RegType REGCOLLATION = formObjectId(CLASSID, REGCOLLATIONOID); + 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(); + // default as pg_node_tree + // default as text RegType modifier(int typmod); /** 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/src/main/java/org/postgresql/pljava/pg/AttributeImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/AttributeImpl.java index cc1dce800..ef61a2e96 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/AttributeImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/AttributeImpl.java @@ -19,6 +19,7 @@ import java.sql.SQLException; +import java.util.List; import static java.util.Objects.requireNonNull; import java.util.function.UnaryOperator; @@ -36,9 +37,13 @@ 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; @@ -50,10 +55,23 @@ abstract class AttributeImpl extends Addressed 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() { @@ -119,6 +137,24 @@ private static TupleTableSlot cacheTuple(AttributeImpl o) 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(); @@ -127,6 +163,15 @@ private static Simple name(AttributeImpl o) throws SQLException NameAdapter.SIMPLE_INSTANCE); } + private static List grants(AttributeImpl o) + throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return t.get(t.descriptor().get("attacl"), GrantAdapter.LIST_INSTANCE); + } + + /* Implementation of Attribute */ + AttributeImpl() { super(s_initializer.apply(new MethodHandle[NSLOTS])); @@ -137,8 +182,24 @@ private static Simple name(AttributeImpl o) throws SQLException 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; @@ -164,6 +225,10 @@ private static Simple name(AttributeImpl o) throws SQLException .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. */ @@ -173,8 +238,19 @@ private static Simple name(AttributeImpl o) throws SQLException .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; @@ -234,6 +310,12 @@ private static short length(AttributeImpl o) return b.getShort(OFFSET_pg_attribute_attlen); } + private static int dimensions(AttributeImpl o) throws SQLException + { + TupleTableSlot s = o.partialTuple(); + return s.get(s.descriptor().get("attndims"), INT4_INSTANCE); + } + private static boolean byValue(AttributeImpl o) { ByteBuffer b = o.rawBuffer(); @@ -248,6 +330,75 @@ private static Alignment alignment(AttributeImpl o) return alignmentFromCatalog(b.get(OFFSET_pg_attribute_attalign)); } + private static Storage storage(AttributeImpl o) throws SQLException + { + TupleTableSlot s = o.partialTuple(); + return + storageFromCatalog( + s.get(s.descriptor().get("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(s.descriptor().get("atthasdef"), BOOLEAN_INSTANCE); + } + + private static boolean hasMissing(AttributeImpl o) throws SQLException + { // not 9.5 + TupleTableSlot s = o.partialTuple(); + return s.get(s.descriptor().get("atthasmissing"), BOOLEAN_INSTANCE); + } + + private static Identity identity(AttributeImpl o) throws SQLException + { // not 9.5 + TupleTableSlot s = o.partialTuple(); + byte v = s.get(s.descriptor().get("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(s.descriptor().get("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(s.descriptor().get("attislocal"), BOOLEAN_INSTANCE); + } + + private static int inheritanceCount(AttributeImpl o) throws SQLException + { + TupleTableSlot s = o.partialTuple(); + return s.get(s.descriptor().get("attinhcount"), INT4_INSTANCE); + } + + private static RegCollation collation(AttributeImpl o) throws SQLException + { + TupleTableSlot s = o.partialTuple(); + return s.get(s.descriptor().get("attcollation"), REGCOLLATION_INSTANCE); + } + /* private methods using cache slots like API methods do */ private ByteBuffer rawBuffer() @@ -306,6 +457,29 @@ public short length() } } + @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() { @@ -334,6 +508,150 @@ public Alignment alignment() } } + @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() { @@ -411,6 +729,16 @@ SwitchPoint cacheSwitchPoint() 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() { @@ -517,6 +845,18 @@ 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() { @@ -528,5 +868,88 @@ 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 index 464aa7e47..4288987ed 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/CatalogObjectImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/CatalogObjectImpl.java @@ -472,15 +472,36 @@ static Supplier ctorIfKnown( 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 : 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..97b394a7f --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/pg/ExtensionImpl.java @@ -0,0 +1,266 @@ +/* + * 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.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.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(t.descriptor().get("extname"), SIMPLE_INSTANCE); + } + + private static RegRole owner(ExtensionImpl o) throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return t.get(t.descriptor().get("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; + } + + /* computation methods */ + + private static RegNamespace namespace(ExtensionImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(s.descriptor().get("extnamespace"), REGNAMESPACE_INSTANCE); + } + + private static boolean relocatable(ExtensionImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(s.descriptor().get("extrelocatable"), BOOLEAN_INSTANCE); + } + + private static String version(ExtensionImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(s.descriptor().get("extversion"), TextAdapter.INSTANCE); + } + + private static List config(ExtensionImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return + s.get(s.descriptor().get("extconfig"), + ArrayAdapters.REGCLASS_LIST_INSTANCE); + } + + private static List condition(ExtensionImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return + s.get(s.descriptor().get("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/ProceduralLanguageImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/ProceduralLanguageImpl.java new file mode 100644 index 000000000..8dff775ac --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/pg/ProceduralLanguageImpl.java @@ -0,0 +1,237 @@ +/* + * 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.lang.invoke.MethodHandle; +import static java.lang.invoke.MethodHandles.lookup; +import java.lang.invoke.SwitchPoint; + +import java.sql.SQLException; + +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(t.descriptor().get("lanname"), SIMPLE_INSTANCE); + } + + private static RegRole owner(ProceduralLanguageImpl o) throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return t.get(t.descriptor().get("lanowner"), REGROLE_INSTANCE); + } + + private static List grants(ProceduralLanguageImpl o) + throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return t.get(t.descriptor().get("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; + } + + /* computation methods */ + + private static PLPrincipal principal(ProceduralLanguageImpl o) + throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + if ( s.get(s.descriptor().get("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(s.descriptor().get("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(s.descriptor().get("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(s.descriptor().get("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 index eb3145dc7..b071a528e 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/RegClassImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/RegClassImpl.java @@ -31,10 +31,21 @@ 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; @@ -62,12 +73,50 @@ static class Known> 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(t.descriptor().get("relname"), NameAdapter.SIMPLE_INSTANCE); + } + + private static RegNamespace namespace(RegClassImpl o) throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return t.get(t.descriptor().get("relnamespace"), REGNAMESPACE_INSTANCE); + } + + private static RegRole owner(RegClassImpl o) throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return t.get(t.descriptor().get("relowner"), REGROLE_INSTANCE); + } + + private static List grants(RegClassImpl o) + throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return t.get(t.descriptor().get("relacl"), GrantAdapter.LIST_INSTANCE); + } + + /* Implementation of RegClass */ + RegClassImpl() { super(s_initializer.apply(new MethodHandle[NSLOTS])); @@ -152,6 +201,20 @@ void dualHandshake(RegType dual) 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 @@ -162,13 +225,48 @@ void dualHandshake(RegType dual) .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; } + /* computation methods */ + /** * Return the tuple descriptor for this relation, wrapped in a one-element * array, which is also stored in {@code m_tupDescHolder}. @@ -263,6 +361,94 @@ private static RegType type(RegClassImpl o) throws SQLException return t; } + private static RegType ofType(RegClassImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(s.descriptor().get("reloftype"), REGTYPE_INSTANCE); + } + + private static RegClass toastRelation(RegClassImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(s.descriptor().get("reltoastrelid"), REGCLASS_INSTANCE); + } + + private static boolean hasIndex(RegClassImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(s.descriptor().get("relhasindex"), BOOLEAN_INSTANCE); + } + + private static boolean isShared(RegClassImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(s.descriptor().get("relisshared"), BOOLEAN_INSTANCE); + } + + private static short nAttributes(RegClassImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(s.descriptor().get("relnatts"), INT2_INSTANCE); + } + + private static short checks(RegClassImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(s.descriptor().get("relchecks"), INT2_INSTANCE); + } + + private static boolean hasRules(RegClassImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(s.descriptor().get("relhasrules"), BOOLEAN_INSTANCE); + } + + private static boolean hasTriggers(RegClassImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(s.descriptor().get("relhastriggers"), BOOLEAN_INSTANCE); + } + + private static boolean hasSubclass(RegClassImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(s.descriptor().get("relhassubclass"), BOOLEAN_INSTANCE); + } + + private static boolean rowSecurity(RegClassImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(s.descriptor().get("relrowsecurity"), BOOLEAN_INSTANCE); + } + + private static boolean forceRowSecurity(RegClassImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return + s.get(s.descriptor().get("relforcerowsecurity"), BOOLEAN_INSTANCE); + } + + private static boolean isPopulated(RegClassImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(s.descriptor().get("relispopulated"), BOOLEAN_INSTANCE); + } + + private static boolean isPartition(RegClassImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(s.descriptor().get("relispartition"), BOOLEAN_INSTANCE); + } + + private static List options(RegClassImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return + s.get(s.descriptor().get("reloptions"), FLAT_STRING_LIST_INSTANCE); + } + + /* API methods */ + @Override public TupleDescriptor.Interned tupleDescriptor() { @@ -294,7 +480,15 @@ public RegType type() @Override public RegType ofType() { - throw notyet(); + try + { + MethodHandle h = m_slots[SLOT_OFTYPE]; + return (RegType)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } } // am @@ -311,19 +505,43 @@ public RegType ofType() @Override public RegClass toastRelation() { - throw notyet(); + try + { + MethodHandle h = m_slots[SLOT_TOASTRELATION]; + return (RegClass)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } } @Override public boolean hasIndex() { - throw notyet(); + try + { + MethodHandle h = m_slots[SLOT_HASINDEX]; + return (boolean)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } } @Override public boolean isShared() { - throw notyet(); + try + { + MethodHandle h = m_slots[SLOT_ISSHARED]; + return (boolean)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } } // persistence @@ -332,49 +550,113 @@ public boolean isShared() @Override public short nAttributes() { - throw notyet(); + try + { + MethodHandle h = m_slots[SLOT_NATTRIBUTES]; + return (short)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } } @Override public short checks() { - throw notyet(); + try + { + MethodHandle h = m_slots[SLOT_CHECKS]; + return (short)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } } @Override public boolean hasRules() { - throw notyet(); + try + { + MethodHandle h = m_slots[SLOT_HASRULES]; + return (boolean)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } } @Override public boolean hasTriggers() { - throw notyet(); + try + { + MethodHandle h = m_slots[SLOT_HASTRIGGERS]; + return (boolean)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } } @Override public boolean hasSubclass() { - throw notyet(); + try + { + MethodHandle h = m_slots[SLOT_HASSUBCLASS]; + return (boolean)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } } @Override public boolean rowSecurity() { - throw notyet(); + try + { + MethodHandle h = m_slots[SLOT_ROWSECURITY]; + return (boolean)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } } @Override public boolean forceRowSecurity() { - throw notyet(); + try + { + MethodHandle h = m_slots[SLOT_FORCEROWSECURITY]; + return (boolean)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } } @Override public boolean isPopulated() { - throw notyet(); + try + { + MethodHandle h = m_slots[SLOT_ISPOPULATED]; + return (boolean)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } } // replident @@ -382,7 +664,15 @@ public boolean isPopulated() @Override public boolean isPartition() { - throw notyet(); + try + { + MethodHandle h = m_slots[SLOT_ISPARTITION]; + return (boolean)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } } // rewrite @@ -392,7 +682,15 @@ public boolean isPartition() @Override public List options() { - throw notyet(); + 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..92de0cc11 --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/pg/RegCollationImpl.java @@ -0,0 +1,275 @@ +/* + * 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.lang.invoke.MethodHandle; +import static java.lang.invoke.MethodHandles.lookup; +import java.lang.invoke.SwitchPoint; + +import java.sql.SQLException; + +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(t.descriptor().get("collname"), SIMPLE_INSTANCE); + } + + private static RegNamespace namespace(RegCollationImpl o) + throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return + t.get(t.descriptor().get("collnamespace"), REGNAMESPACE_INSTANCE); + } + + private static RegRole owner(RegCollationImpl o) throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return t.get(t.descriptor().get("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; + } + + /* computation methods */ + + private static CharsetEncoding encoding(RegCollationImpl o) + throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return + s.get(s.descriptor().get("collencoding"), EncodingAdapter.INSTANCE); + } + + private static String collate(RegCollationImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(s.descriptor().get("collcollate"), AS_STRING_INSTANCE); + } + + private static String ctype(RegCollationImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(s.descriptor().get("collctype"), AS_STRING_INSTANCE); + } + + private static Provider provider(RegCollationImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + byte p = s.get(s.descriptor().get("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(s.descriptor().get("collversion"), TextAdapter.INSTANCE); + } + + private static boolean deterministic(RegCollationImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return + s.get(s.descriptor().get("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..0f63dc02e --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/pg/RegConfigImpl.java @@ -0,0 +1,109 @@ +/* + * 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.lang.invoke.MethodHandle; +import static java.lang.invoke.MethodHandles.lookup; +import java.lang.invoke.SwitchPoint; + +import java.sql.SQLException; + +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(t.descriptor().get("cfgname"), SIMPLE_INSTANCE); + } + + private static RegNamespace namespace(RegConfigImpl o) + throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return + t.get(t.descriptor().get("cfgnamespace"), REGNAMESPACE_INSTANCE); + } + + private static RegRole owner(RegConfigImpl o) throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return t.get(t.descriptor().get("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; + } +} 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..b4fe6c9ec --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/pg/RegDictionaryImpl.java @@ -0,0 +1,109 @@ +/* + * 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.lang.invoke.MethodHandle; +import static java.lang.invoke.MethodHandles.lookup; +import java.lang.invoke.SwitchPoint; + +import java.sql.SQLException; + +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(t.descriptor().get("dictname"), SIMPLE_INSTANCE); + } + + private static RegNamespace namespace(RegDictionaryImpl o) + throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return + t.get(t.descriptor().get("dictnamespace"), REGNAMESPACE_INSTANCE); + } + + private static RegRole owner(RegDictionaryImpl o) throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return t.get(t.descriptor().get("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; + } +} diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/RegNamespaceImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/RegNamespaceImpl.java index 9495a08c9..68d2730e9 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/RegNamespaceImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/RegNamespaceImpl.java @@ -11,21 +11,106 @@ */ 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.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(t.descriptor().get("nspname"), NameAdapter.SIMPLE_INSTANCE); + } + + private static RegRole owner(RegNamespaceImpl o) throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return t.get(t.descriptor().get("nspowner"), REGROLE_INSTANCE); + } + + private static List grants(RegNamespaceImpl o) + throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return t.get(t.descriptor().get("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; + } } 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..1a0422f2b --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/pg/RegOperatorImpl.java @@ -0,0 +1,397 @@ +/* + * 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.lang.invoke.MethodHandle; +import static java.lang.invoke.MethodHandles.lookup; +import java.lang.invoke.SwitchPoint; + +import java.sql.SQLException; + +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(t.descriptor().get("oprname"), OPERATOR_INSTANCE); + } + + private static RegNamespace namespace(RegOperatorImpl o) + throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return + t.get(t.descriptor().get("oprnamespace"), REGNAMESPACE_INSTANCE); + } + + private static RegRole owner(RegOperatorImpl o) throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return t.get(t.descriptor().get("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; + } + + /* computation methods */ + + private static Kind kind(RegOperatorImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + byte b = s.get(s.descriptor().get("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(s.descriptor().get("oprcanmerge"), BOOLEAN_INSTANCE); + } + + private static boolean canHash(RegOperatorImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(s.descriptor().get("oprcanhash"), BOOLEAN_INSTANCE); + } + + private static RegType leftOperand(RegOperatorImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(s.descriptor().get("oprleft"), REGTYPE_INSTANCE); + } + + private static RegType rightOperand(RegOperatorImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(s.descriptor().get("oprright"), REGTYPE_INSTANCE); + } + + private static RegType result(RegOperatorImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(s.descriptor().get("oprresult"), REGTYPE_INSTANCE); + } + + private static RegOperator commutator(RegOperatorImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(s.descriptor().get("oprcom"), REGOPERATOR_INSTANCE); + } + + private static RegOperator negator(RegOperatorImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(s.descriptor().get("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(s.descriptor().get("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(s.descriptor().get("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(s.descriptor().get("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..f999acb71 --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/pg/RegProcedureImpl.java @@ -0,0 +1,729 @@ +/* + * 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.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.List; + +import java.util.function.UnaryOperator; + +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 org.postgresql.pljava.pg.adt.ArgModeAdapter; +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(t.descriptor().get("proname"), SIMPLE_INSTANCE); + } + + private static RegNamespace namespace(RegProcedureImpl o) + throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return t.get(t.descriptor().get("pronamespace"), REGNAMESPACE_INSTANCE); + } + + private static RegRole owner(RegProcedureImpl o) throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return t.get(t.descriptor().get("proowner"), REGROLE_INSTANCE); + } + + private static List grants(RegProcedureImpl o) + throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return t.get(t.descriptor().get("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; + } + + /* computation methods */ + + private static ProceduralLanguage language(RegProcedureImpl o) + throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(s.descriptor().get("prolang"), PLANG_INSTANCE); + } + + private static float cost(RegProcedureImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(s.descriptor().get("procost"), FLOAT4_INSTANCE); + } + + private static float rows(RegProcedureImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(s.descriptor().get("prorows"), FLOAT4_INSTANCE); + } + + private static RegType variadicType(RegProcedureImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(s.descriptor().get("provariadic"), REGTYPE_INSTANCE); + } + + private static RegProcedure support(RegProcedureImpl o) + throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + @SuppressWarnings("unchecked") // XXX add memo magic here + RegProcedure p = (RegProcedure) + s.get(s.descriptor().get("prosupport"), REGPROCEDURE_INSTANCE); + return p; + } + + private static Kind kind(RegProcedureImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + byte b = s.get(s.descriptor().get("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(s.descriptor().get("prosecdef"), BOOLEAN_INSTANCE) ) + return Security.DEFINER; + return Security.INVOKER; + } + + private static boolean leakproof(RegProcedureImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(s.descriptor().get("proleakproof"), BOOLEAN_INSTANCE); + } + + private static OnNullInput onNullInput(RegProcedureImpl o) + throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + if ( s.get(s.descriptor().get("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(s.descriptor().get("proretset"), BOOLEAN_INSTANCE); + } + + private static Effects effects(RegProcedureImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + byte b = s.get(s.descriptor().get("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(s.descriptor().get("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(s.descriptor().get("prorettype"), REGTYPE_INSTANCE); + } + + private static List argTypes(RegProcedureImpl o) + throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return + s.get(s.descriptor().get("proargtypes"), + ArrayAdapters.REGTYPE_LIST_INSTANCE); + } + + private static List allArgTypes(RegProcedureImpl o) + throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return + s.get(s.descriptor().get("proallargtypes"), + ArrayAdapters.REGTYPE_LIST_INSTANCE); + } + + private static List argModes(RegProcedureImpl o) + throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return + s.get(s.descriptor().get("proargmodes"), + ArgModeAdapter.LIST_INSTANCE); + } + + private static List argNames(RegProcedureImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return + s.get(s.descriptor().get("proargnames"), + ArrayAdapters.TEXT_NAME_LIST_INSTANCE); + } + + private static List transformTypes(RegProcedureImpl o) + throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return + s.get(s.descriptor().get("protrftypes"), + ArrayAdapters.REGTYPE_LIST_INSTANCE); + } + + private static String src(RegProcedureImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(s.descriptor().get("prosrc"), TextAdapter.INSTANCE); + } + + private static String bin(RegProcedureImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(s.descriptor().get("probin"), TextAdapter.INSTANCE); + } + + private static List config(RegProcedureImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return + s.get(s.descriptor().get("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(); + + try + { + return + s.get(s.descriptor().get("proargdefaults"), SYNTHETIC_INSTANCE); + } + catch ( SQLException e ) + { + throw unchecked(e); + } + } + + @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. + */ + TupleTableSlot s = cacheTuple(); + + try + { + return + s.get(s.descriptor().get("prosqlbody"), SYNTHETIC_INSTANCE); + } + catch ( SQLException e ) + { + throw unchecked(e); + } + } + + @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 index 1f772ac27..c0f535453 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/RegRoleImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/RegRoleImpl.java @@ -11,19 +11,37 @@ */ 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.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. @@ -36,66 +54,274 @@ class RegRoleImpl extends Addressed 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(t.descriptor().get("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; + } + + /* 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(s.descriptor().get("rolsuper"), BOOLEAN_INSTANCE); + } + + private static boolean inherit(RegRoleImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(s.descriptor().get("rolinherit"), BOOLEAN_INSTANCE); + } + + private static boolean createRole(RegRoleImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(s.descriptor().get("rolcreaterole"), BOOLEAN_INSTANCE); + } + + private static boolean createDB(RegRoleImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(s.descriptor().get("rolcreatedb"), BOOLEAN_INSTANCE); + } + + private static boolean canLogIn(RegRoleImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(s.descriptor().get("rolcanlogin"), BOOLEAN_INSTANCE); + } + + private static boolean replication(RegRoleImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(s.descriptor().get("rolreplication"), BOOLEAN_INSTANCE); + } + + private static boolean bypassRLS(RegRoleImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(s.descriptor().get("rolbypassrls"), BOOLEAN_INSTANCE); + } + + private static int connectionLimit(RegRoleImpl o) throws SQLException + { + TupleTableSlot s = o.cacheTuple(); + return s.get(s.descriptor().get("rolconnlimit"), INT4_INSTANCE); + } + /* API methods */ @Override public List memberOf() { - throw notyet(); + try + { + MethodHandle h = m_slots[SLOT_MEMBEROF]; + return (List)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } } @Override public boolean superuser() { - throw notyet(); + try + { + MethodHandle h = m_slots[SLOT_SUPERUSER]; + return (boolean)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } } @Override public boolean inherit() { - throw notyet(); + try + { + MethodHandle h = m_slots[SLOT_INHERIT]; + return (boolean)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } } @Override public boolean createRole() { - throw notyet(); + try + { + MethodHandle h = m_slots[SLOT_CREATEROLE]; + return (boolean)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } } @Override public boolean createDB() { - throw notyet(); + try + { + MethodHandle h = m_slots[SLOT_CREATEDB]; + return (boolean)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } } @Override public boolean canLogIn() { - throw notyet(); + try + { + MethodHandle h = m_slots[SLOT_CANLOGIN]; + return (boolean)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } } @Override public boolean replication() { - throw notyet(); + try + { + MethodHandle h = m_slots[SLOT_REPLICATION]; + return (boolean)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } } @Override public boolean bypassRLS() { - throw notyet(); + try + { + MethodHandle h = m_slots[SLOT_BYPASSRLS]; + return (boolean)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } } @Override public int connectionLimit() { - throw notyet(); + try + { + MethodHandle h = m_slots[SLOT_CONNECTIONLIMIT]; + return (int)h.invokeExact(this, h); + } + catch ( Throwable t ) + { + throw unchecked(t); + } } /* Implementation of RegRole.Grantee */ diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/RegTypeImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/RegTypeImpl.java index 6bed4f4d3..6afd9d7dd 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/RegTypeImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/RegTypeImpl.java @@ -33,13 +33,24 @@ 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 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 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; @@ -71,12 +82,50 @@ abstract class RegTypeImpl extends Addressed */ 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(t.descriptor().get("typname"), NameAdapter.SIMPLE_INSTANCE); + } + + private static RegNamespace namespace(RegTypeImpl o) throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return t.get(t.descriptor().get("typnamespace"), REGNAMESPACE_INSTANCE); + } + + private static RegRole owner(RegTypeImpl o) throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return t.get(t.descriptor().get("typowner"), REGROLE_INSTANCE); + } + + private static List grants(RegTypeImpl o) + throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return t.get(t.descriptor().get("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. @@ -109,6 +158,12 @@ void invalidate(List sps, List postOps) */ 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. @@ -135,8 +190,28 @@ void dualHandshake(RegClass dual) 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 NSLOTS; static @@ -147,18 +222,67 @@ void dualHandshake(RegClass dual) .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++) .build(); NSLOTS = i; } + /* computation methods */ + /** * Obtain the tuple descriptor for an ordinary cataloged composite type. *

    @@ -240,6 +364,38 @@ private static boolean byValue(RegTypeImpl o) throws SQLException return t.get(t.descriptor().get("typbyval"), BOOLEAN_INSTANCE); } + private static Type type(RegTypeImpl o) throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return typeFromCatalog( + t.get(t.descriptor().get("typtype"), INT1_INSTANCE)); + } + + private static char category(RegTypeImpl o) throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return (char) + (0xff & t.get(t.descriptor().get("typcategory"), INT1_INSTANCE)); + } + + private static boolean preferred(RegTypeImpl o) throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return t.get(t.descriptor().get("typispreferred"), BOOLEAN_INSTANCE); + } + + private static boolean defined(RegTypeImpl o) throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return t.get(t.descriptor().get("typisdefined"), BOOLEAN_INSTANCE); + } + + private static byte delimiter(RegTypeImpl o) throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return t.get(t.descriptor().get("typdelim"), INT1_INSTANCE); + } + private static RegClass relation(RegTypeImpl o) throws SQLException { /* @@ -263,6 +419,99 @@ private static RegClass relation(RegTypeImpl o) throws SQLException return c; } + private static RegType element(RegTypeImpl o) throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return t.get(t.descriptor().get("typelem"), REGTYPE_INSTANCE); + } + + private static RegType array(RegTypeImpl o) throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return t.get(t.descriptor().get("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(t.descriptor().get("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(t.descriptor().get("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(t.descriptor().get("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(t.descriptor().get("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(t.descriptor().get("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(t.descriptor().get("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(t.descriptor().get("typanalyze"), REGPROCEDURE_INSTANCE); + return p; + } + + private static RegProcedure subscript(RegTypeImpl o) + throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + @SuppressWarnings("unchecked") // XXX add memo magic here + RegProcedure p = (RegProcedure) + t.get(t.descriptor().get("typsubscript"), REGPROCEDURE_INSTANCE); + return p; + } + private static Alignment alignment(RegTypeImpl o) throws SQLException { TupleTableSlot t = o.cacheTuple(); @@ -270,6 +519,39 @@ private static Alignment alignment(RegTypeImpl o) throws SQLException t.get(t.descriptor().get("typalign"), INT1_INSTANCE)); } + private static Storage storage(RegTypeImpl o) throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return storageFromCatalog( + t.get(t.descriptor().get("typstorage"), INT1_INSTANCE)); + } + + private static boolean notNull(RegTypeImpl o) throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return t.get(t.descriptor().get("typnotnull"), BOOLEAN_INSTANCE); + } + + private static RegType baseType(RegTypeImpl o) throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return t.get(t.descriptor().get("typbasetype"), REGTYPE_INSTANCE); + } + + private static int dimensions(RegTypeImpl o) throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return t.get(t.descriptor().get("typndims"), INT4_INSTANCE); + } + + private static RegCollation collation(RegTypeImpl o) throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return t.get(t.descriptor().get("typcollation"), REGCOLLATION_INSTANCE); + } + + /* API methods */ + @Override public TupleDescriptor.Interned tupleDescriptor() { @@ -296,6 +578,7 @@ public short length() { throw unchecked(t); } + // also available in the typcache, FWIW } @Override @@ -310,6 +593,78 @@ public boolean byValue() { 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 @@ -324,12 +679,149 @@ public RegClass relation() { throw unchecked(t); } + // also available in the typcache, FWIW } @Override public RegType element() { - throw notyet(); + 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 @@ -347,6 +839,78 @@ public Alignment alignment() // 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 + } + /** * Return the expected zero value for {@code subId}. *

    @@ -377,6 +941,21 @@ public int modifier() 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). @@ -419,6 +998,7 @@ public RegType modifier(int typmod) CatalogObjectImpl.Factory.formMaybeModifiedType(oid(), typmod); } + @Override public RegType withoutModifier() { return this; @@ -461,6 +1041,26 @@ 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() + ")"; + } } /** @@ -574,5 +1174,28 @@ 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/TupleDescImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/TupleDescImpl.java index 7899c7f10..089ae7205 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/TupleDescImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/TupleDescImpl.java @@ -350,13 +350,15 @@ public Attribute get(int index) static class Cataloged extends TupleDescImpl implements Interned { - private final RegClass m_relation; + private final RegClass m_relation;// using its SwitchPoint, keep it live Cataloged(ByteBuffer td, RegClassImpl c) { /* - * Every Cataloged descriptor from the cache had better be - * reference-counted, so unconditional true is passed for useState. + * 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, @@ -364,7 +366,7 @@ static class Cataloged extends TupleDescImpl implements Interned c.oid(), i, () -> new AttributeImpl.Cataloged(c)) ); - m_relation = c; + m_relation = c; // we need it alive for its SwitchPoint } @Override @@ -376,12 +378,14 @@ public RegType rowType() static class Blessed extends TupleDescImpl implements Interned { - private final RegType m_rowType; + private final RegType m_rowType; // using its SwitchPoint, keep it live Blessed(ByteBuffer td, RegTypeImpl t) { /* - * A Blessed tupdesc has no associated RegClass. In fromByteBuffer, + * 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 diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/adt/ArgModeAdapter.java b/pljava/src/main/java/org/postgresql/pljava/pg/adt/ArgModeAdapter.java new file mode 100644 index 000000000..5b95f7137 --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/pg/adt/ArgModeAdapter.java @@ -0,0 +1,85 @@ +/* + * 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 java.util.List; + +import org.postgresql.pljava.Adapter; +import org.postgresql.pljava.adt.Array.AsFlatList; +import org.postgresql.pljava.model.Attribute; +import org.postgresql.pljava.model.RegProcedure; +import org.postgresql.pljava.model.RegProcedure.ArgMode; +import org.postgresql.pljava.model.RegType; + +/** + * {@link ArgMode ArgMode} from a {@code "char"} column. + *

    + * This adapter is arguably too specialized to deserve to exist, but it does, + * to support the {@link RegProcedure#argModes RegProcedure.argModes} array + * property. Once array adapter contracts have been generalized to support + * a primitive-typed element adapter (which has to happen anyway), an + * array-of-{@code ArgMode} type will be trivially achievable with a contract, + * and this adapter can go away. + */ +public class ArgModeAdapter extends Adapter.As +{ + public static final ArgModeAdapter INSTANCE; + + public static final ArrayAdapter,?> LIST_INSTANCE; + + static + { + @SuppressWarnings("removal") // JEP 411 + Configuration config = AccessController.doPrivileged( + (PrivilegedAction)() -> + configure(ArgModeAdapter.class, Via.BYTE)); + + INSTANCE = new ArgModeAdapter(config); + + LIST_INSTANCE = new ArrayAdapter<>( + AsFlatList.of(AsFlatList::nullsIncludedCopy), INSTANCE); + } + + private ArgModeAdapter(Configuration c) + { + super(c, null, null); + } + + @Override + public boolean canFetch(RegType pgType) + { + return RegType.CHAR == pgType; + } + + public ArgMode fetch(Attribute a, byte in) + { + switch ( in ) + { + case (byte)'i': + return ArgMode.IN; + case (byte)'o': + return ArgMode.OUT; + case (byte)'b': + return ArgMode.INOUT; + case (byte)'v': + return ArgMode.VARIADIC; + case (byte)'t': + return ArgMode.TABLE; + default: + throw new UnsupportedOperationException(String.format( + "Unrecognized procedure/function argument mode value %#x", in)); + } + } +} 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..ea3cdc6e4 --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/pg/adt/DateTimeAdapter.java @@ -0,0 +1,301 @@ +/* + * 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 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.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 + { + 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/OidAdapter.java b/pljava/src/main/java/org/postgresql/pljava/pg/adt/OidAdapter.java index 03938168d..38a149a68 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/adt/OidAdapter.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/adt/OidAdapter.java @@ -31,10 +31,17 @@ public class OidAdapter 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 { @@ -44,7 +51,8 @@ public class OidAdapter { configure(OidAdapter.class, Via.INT32ZX), configure( Int4.class, Via.INT32ZX), - configure( Addressed.class, Via.INT32ZX) + configure( Addressed.class, Via.INT32ZX), + configure( Procedure.class, Via.INT32ZX) }); INSTANCE = new OidAdapter<>(configs[0], null); @@ -54,9 +62,24 @@ public class OidAdapter 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); @@ -65,6 +88,12 @@ public class OidAdapter 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); } /** @@ -159,4 +188,31 @@ 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/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..aafccaab3 --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/pg/adt/XMLAdapter.java @@ -0,0 +1,103 @@ +/* + * 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 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(); + } + } + } +} From 8ce2c8729bf33ab52827d4e90c44c3c349c3a86d Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 22 Jan 2022 23:22:20 -0500 Subject: [PATCH 031/334] A test jig that's been used during development There is not much to this, other than something to step through in a debugger. There now needs to be a better story for exercising and testing. --- .../postgresql/pljava/model/SlotTester.java | 22 +++ .../annotation/TupleTableSlotTest.java | 149 ++++++++++++++++++ .../org/postgresql/pljava/internal/SPI.java | 2 +- .../postgresql/pljava/jdbc/SPIConnection.java | 20 ++- 4 files changed, 191 insertions(+), 2 deletions(-) create mode 100644 pljava-api/src/main/java/org/postgresql/pljava/model/SlotTester.java create mode 100644 pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/TupleTableSlotTest.java 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..00f58b8a6 --- /dev/null +++ b/pljava-api/src/main/java/org/postgresql/pljava/model/SlotTester.java @@ -0,0 +1,22 @@ +/* + * 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; + +/** + * A temporary test jig during TupleTableSlot development, not intended to last. + */ +public interface SlotTester +{ + List test(String query); +} 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..6ee32c5ce --- /dev/null +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/TupleTableSlotTest.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.example.annotation; + +import java.sql.Connection; +import static java.sql.DriverManager.getConnection; +import java.sql.SQLException; + +import java.util.List; + +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.annotation.Function; + +import org.postgresql.pljava.model.Attribute; +import org.postgresql.pljava.model.SlotTester; +import org.postgresql.pljava.model.TupleTableSlot; + +/** + * A temporary test jig during TupleTableSlot development; intended + * to be used from a debugger. + */ +public class TupleTableSlotTest +{ + /** + * 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); + + List tups = t.test(query); + + 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 = tts.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().attributes() ) + { + 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 ( SQLException e ) + { + System.out.println(e); + } + } + } + } + } +} 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..29fae03ba 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/SPI.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/SPI.java @@ -64,7 +64,7 @@ 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)); } 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 6ef2b4b5f..7c3fc10e5 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java @@ -44,6 +44,7 @@ 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; @@ -54,6 +55,11 @@ import org.postgresql.pljava.internal.Oid; import org.postgresql.pljava.internal.PgSavepoint; +import org.postgresql.pljava.internal.SPI; +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 @@ -69,8 +75,20 @@ * * @author Thomas Hallgren */ -public class SPIConnection implements Connection +public class SPIConnection implements Connection, SlotTester { + /** + * A temporary test jig during TupleTableSlot development, not intended + * to last. + */ + @Override + @SuppressWarnings("deprecation") + public List test(String query) + { + int result = SPI.exec(query, 0); + return TupleTableSlotImpl.testmeSPI(); + } + /** * The version number of the currently executing PostgreSQL * server. From 4a773cd6c4c56bc5c13aabcb78fe792a1779fa46 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 23 Jan 2022 14:20:25 -0500 Subject: [PATCH 032/334] A couple straggling RegType methods The defaultBin() and defaultText() methods had been, shall we say, left for later, and baseType() was implemented but it neglected the typmod. --- .../org/postgresql/pljava/model/RegType.java | 5 +- .../org/postgresql/pljava/pg/RegTypeImpl.java | 55 ++++++++++++++++++- 2 files changed, 56 insertions(+), 4 deletions(-) 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 index ebfa73673..e23fec045 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/model/RegType.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/model/RegType.java @@ -12,6 +12,7 @@ package org.postgresql.pljava.model; import java.sql.SQLType; +import java.sql.SQLXML; import org.postgresql.pljava.model.CatalogObject.*; @@ -154,8 +155,8 @@ interface TypeSubscript extends Memo { } RegType baseType(); int dimensions(); RegCollation collation(); - // default as pg_node_tree - // default as text + SQLXML defaultBin(); + String defaultText(); RegType modifier(int typmod); /** diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/RegTypeImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/RegTypeImpl.java index 6afd9d7dd..e8c012294 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/RegTypeImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/RegTypeImpl.java @@ -20,6 +20,7 @@ import java.sql.SQLType; import java.sql.SQLException; +import java.sql.SQLXML; import java.util.List; @@ -37,12 +38,15 @@ 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; @@ -212,6 +216,7 @@ void dualHandshake(RegClass dual) 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 @@ -276,6 +281,7 @@ void dualHandshake(RegClass dual) .withDependent( "baseType", SLOT_BASETYPE = i++) .withDependent( "dimensions", SLOT_DIMENSIONS = i++) .withDependent( "collation", SLOT_COLLATION = i++) + .withDependent( "defaultText", SLOT_DEFAULTTEXT = i++) .build(); NSLOTS = i; @@ -535,7 +541,10 @@ private static boolean notNull(RegTypeImpl o) throws SQLException private static RegType baseType(RegTypeImpl o) throws SQLException { TupleTableSlot t = o.cacheTuple(); - return t.get(t.descriptor().get("typbasetype"), REGTYPE_INSTANCE); + TupleDescriptor td = t.descriptor(); + int oid = t.get(td.get("typbasetype"), OidAdapter.INT4_INSTANCE); + int mod = t.get(td.get("typtypmod"), INT4_INSTANCE); + return CatalogObjectImpl.Factory.formMaybeModifiedType(oid, mod); } private static int dimensions(RegTypeImpl o) throws SQLException @@ -550,6 +559,12 @@ private static RegCollation collation(RegTypeImpl o) throws SQLException return t.get(t.descriptor().get("typcollation"), REGCOLLATION_INSTANCE); } + private static String defaultText(RegTypeImpl o) throws SQLException + { + TupleTableSlot t = o.cacheTuple(); + return t.get(t.descriptor().get("typdefault"), TextAdapter.INSTANCE); + } + /* API methods */ @Override @@ -911,6 +926,42 @@ public RegCollation collation() // 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(); + + try + { + return + s.get(s.descriptor().get("typdefaultbin"), SYNTHETIC_INSTANCE); + } + catch ( SQLException e ) + { + throw unchecked(e); + } + } + + @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}. *

    @@ -994,7 +1045,7 @@ public RegType modifier(int typmod) { if ( -1 == typmod ) return this; - return (RegType) + return CatalogObjectImpl.Factory.formMaybeModifiedType(oid(), typmod); } From 9a833d2ecdb2b5c7e4b99a795f97bdb962952d03 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 23 Jan 2022 16:31:23 -0500 Subject: [PATCH 033/334] Spotted a copy-pasto in an assertion message --- .../src/main/java/org/postgresql/pljava/pg/TupleDescImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/TupleDescImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/TupleDescImpl.java index 089ae7205..e282c928f 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/TupleDescImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/TupleDescImpl.java @@ -261,7 +261,7 @@ private static TupleDescriptor fromByteBuffer( if ( null != holder ) { result = holder[0]; - assert null != result : "disagree whether RegClass has desc"; + assert null != result : "disagree whether RegType has desc"; return result; } From 9b639f3992f58044b3f3fbc0fe47a1b75e2de8ad Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 23 Jan 2022 17:22:55 -0500 Subject: [PATCH 034/334] Enable GitHub Actions on REL1_7_STABLE --- .github/workflows/ci-runnerpg.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-runnerpg.yml b/.github/workflows/ci-runnerpg.yml index d77d5a53a..fbe345445 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: From 2aa9eb6947a89f889b7a914bd9d0d768a4618445 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 26 Jan 2022 14:58:49 -0500 Subject: [PATCH 035/334] Update obsolete javadoc information These changes should have been included in pull request #256, but were overlooked at the time. --- .../postgresql/pljava/annotation/BaseUDT.java | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/pljava-api/src/main/java/org/postgresql/pljava/annotation/BaseUDT.java b/pljava-api/src/main/java/org/postgresql/pljava/annotation/BaseUDT.java index e416f2afc..3798c0a3c 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/annotation/BaseUDT.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/annotation/BaseUDT.java @@ -56,13 +56,15 @@ *

    * Other static methods in the class may be exported as SQL functions by * marking them with {@code @Function} in the usual way, and will not have any - * special treatment on account of being in a UDT class. If those function - * declarations will depend on the existence of this type, or the type must + * special treatment on account of being in a UDT class. Those function + * declarations will be correctly ordered before or after this type's, in common + * cases such as when this type appears in their signatures, or the type must * refer to the functions (as it must for * {@link #typeModifierInput typeModifierInput} or - * {@link #typeModifierOutput typeModifierOutput} functions, for example), - * appropriate {@link #provides provides}/{@link #requires requires} labels must - * be used in their {@code @Function} annotations and this annotation, to make + * {@link #typeModifierOutput typeModifierOutput} functions, for example). + * In a case that the automatic ordering does not handle correctly, + * appropriate {@link #provides provides}/{@link #requires requires} labels can + * be used in the {@code @Function} annotations and this annotation, to make * the order come out right. */ @Target(ElementType.TYPE) @Retention(RetentionPolicy.CLASS) @Documented @@ -254,9 +256,8 @@ public interface Code *

    * Even if the method is defined on the UDT class marked by this annotation, * it is not automatically found or used. It will need its own - * {@link Function} annotation giving it a name and a {@code provides} - * label, and this annotation must refer to it by that name and include the - * label in {@code requires} to ensure the SQL is generated in the right + * {@link Function} annotation giving it a name, and this annotation must + * refer to it by that name to ensure the SQL is generated in the right * order. */ String typeModifierInput() default ""; @@ -274,9 +275,8 @@ public interface Code *

    * Even if the method is defined on the UDT class marked by this annotation, * it is not automatically found or used. It will need its own - * {@link Function} annotation giving it a name and a {@code provides} - * label, and this annotation must refer to it by that name and include the - * label in {@code requires} to ensure the SQL is generated in the right + * {@link Function} annotation giving it a name, and this annotation must + * refer to it by that name to ensure the SQL is generated in the right * order. */ String typeModifierOutput() default ""; @@ -288,6 +288,12 @@ public interface Code * The details of the necessary API are in {@code vacuum.h}. + *

    + * Even if the method is defined on the UDT class marked by this annotation, + * it is not automatically found or used. It will need its own + * {@link Function} annotation giving it a name, and this annotation must + * refer to it by that name to ensure the SQL is generated in the right + * order. */ String analyze() default ""; From 62d3694ecbd3924764308afe245b94260d8c9938 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 31 Jan 2022 15:29:32 -0500 Subject: [PATCH 036/334] Fix copyright years that were wrong as committed --- pljava/src/main/java/org/postgresql/pljava/internal/SPI.java | 2 +- .../main/java/org/postgresql/pljava/pg/adt/EncodingAdapter.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 29fae03ba..7afb8c1dd 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-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 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 index 076fab241..d2df9b5da 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/adt/EncodingAdapter.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/adt/EncodingAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 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 From 4fa6a190dbdb42d14b4049f12de2dbb1bfb9a2c2 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 31 Jan 2022 15:29:49 -0500 Subject: [PATCH 037/334] Pacify AppVeyor The "MinGW PG 12" cases were succeeding because the MinGW PacMan environment is really supplying PG 14. Update the "PG" variable for those cases; it's ignored, but may as well reflect (this moment's) reality. The MSVC cases really used PG 12 and failed because of a struct field not present before 13. For now, just consider 13 to be minimum for this work in progress. There are several things that would have to be made conditional to work on PG < 13. So far, only one #include change seems necessary to work on 13. --- appveyor.yml | 56 +++++++++++++-------------- pljava-so/src/main/c/ModelConstants.c | 3 ++ 2 files changed, 31 insertions(+), 28 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index bf404ff0b..86018be22 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -5,55 +5,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-so/src/main/c/ModelConstants.c b/pljava-so/src/main/c/ModelConstants.c index 0b8ed34fc..7f41f459f 100644 --- a/pljava-so/src/main/c/ModelConstants.c +++ b/pljava-so/src/main/c/ModelConstants.c @@ -12,6 +12,9 @@ #include +#if PG_VERSION_NUM < 140000 +#include +#endif #include #include #include From f13513532aa69e0f1221c4aa9949875aed2731bc Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 31 Jan 2022 15:30:01 -0500 Subject: [PATCH 038/334] Update straggling currentInvocation checks Two checks for a current Invocation were missed in 580b25a. The MacOS clang compiler's tautological-pointer-compare warning catches them. Add a HAS_INVOCATION macro to define the proper test in one place. --- pljava-so/src/main/c/DualState.c | 2 +- pljava-so/src/main/c/Function.c | 2 +- pljava-so/src/main/c/JNICalls.c | 2 +- pljava-so/src/main/include/pljava/Invocation.h | 1 + 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/pljava-so/src/main/c/DualState.c b/pljava-so/src/main/c/DualState.c index 6cd089c47..6e295900c 100644 --- a/pljava-so/src/main/c/DualState.c +++ b/pljava-so/src/main/c/DualState.c @@ -350,7 +350,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); } 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/JNICalls.c b/pljava-so/src/main/c/JNICalls.c index 9252acb99..8ed518f77 100644 --- a/pljava-so/src/main/c/JNICalls.c +++ b/pljava-so/src/main/c/JNICalls.c @@ -314,7 +314,7 @@ bool beginNativeNoErrCheck(JNIEnv* env) bool beginNative(JNIEnv* env) { - if ( 1 > currentInvocation->nestLevel ) + if ( ! HAS_INVOCATION ) { env = JNI_setEnv(env); Exception_throw(ERRCODE_INTERNAL_ERROR, diff --git a/pljava-so/src/main/include/pljava/Invocation.h b/pljava-so/src/main/include/pljava/Invocation.h index 91067b028..756fe612a 100644 --- a/pljava-so/src/main/include/pljava/Invocation.h +++ b/pljava-so/src/main/include/pljava/Invocation.h @@ -122,6 +122,7 @@ struct Invocation_ extern Invocation currentInvocation[]; +#define HAS_INVOCATION (0 < currentInvocation->nestLevel) extern void Invocation_assertConnect(void); From 0b0e4dedcf45bec72d4452a9505f3e08a6636935 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 31 Jan 2022 15:30:12 -0500 Subject: [PATCH 039/334] Missing initializer caught by MacOS clang Uniniialized value would only be returned in exceptional case and therefore ignored, but still. --- pljava-so/src/main/c/ModelUtils.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pljava-so/src/main/c/ModelUtils.c b/pljava-so/src/main/c/ModelUtils.c index 2e1c1641e..e1d962387 100644 --- a/pljava-so/src/main/c/ModelUtils.c +++ b/pljava-so/src/main/c/ModelUtils.c @@ -949,7 +949,7 @@ Java_org_postgresql_pljava_pg_TupleTableSlotImpl__1store_1heaptuple(JNIEnv* env, JNIEXPORT jobject JNICALL Java_org_postgresql_pljava_pg_TupleTableSlotImpl__1testmeSPI(JNIEnv* env, jobject _cls) { - jobject result; + jobject result = NULL; BEGIN_NATIVE_AND_TRY result = pljava_TupleTableSlot_fromSPI(); END_NATIVE_AND_CATCH("_testmeSPI") From 04431c4ad352ba9be6d77f04b1295d84331ef68d Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 31 Jan 2022 15:30:29 -0500 Subject: [PATCH 040/334] Look for a way to confirm ARR_DIMS under clang clang is punctilious about refusing the necessary pointer arithmetic when given NULL as the pointer. Find out if it will accept the address of a statically-allocated (permanent waste of 16 bytes) ArrayType struct. --- pljava-so/src/main/c/ModelConstants.c | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/pljava-so/src/main/c/ModelConstants.c b/pljava-so/src/main/c/ModelConstants.c index 7f41f459f..293d777ab 100644 --- a/pljava-so/src/main/c/ModelConstants.c +++ b/pljava-so/src/main/c/ModelConstants.c @@ -176,6 +176,15 @@ static int32 constants[] = { static void dummy() { + /* + * PostgreSQL's array.h defines ARR_DIMS in a way that rules out an easy + * ARR_DIMS(0) on a compiler that treats pointer arithmetic on a null + * pointer as an error. gcc is happy to do the needed arithmetic + * on this symbol even when declared auto, but I am guessing clang will + * insist it be static. Ah well, it's just a small waste of space. + */ + static ArrayType dummyArray; + StaticAssertStmt(SIZEOF_DATUM == SIZEOF_VOID_P, "PostgreSQL SIZEOF_DATUM and SIZEOF_VOID_P no longer equivalent?"); @@ -376,8 +385,8 @@ StaticAssertStmt(offsetof(strct,fld) - VARHDRSZ == \ CONFIRMVLOFFSET( ArrayType, elemtype ); CONFIRMEXPR( OFFSET_ArrayType_DIMS, - (((char*)ARR_DIMS(0)) - (char *)0) - VARHDRSZ ); - CONFIRMEXPR( SIZEOF_ArrayType_DIM, sizeof *ARR_DIMS(0) ); + (((char*)ARR_DIMS(&dummyArray)) - (char *)&dummyArray) - VARHDRSZ ); + CONFIRMEXPR( SIZEOF_ArrayType_DIM, sizeof *ARR_DIMS(&dummyArray) ); #undef CONFIRMSIZEOF #undef CONFIRMVLOFFSET From a9f24a9b1ac0c74ae79c1b0d8fcac53388732837 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 31 Jan 2022 17:32:50 -0500 Subject: [PATCH 041/334] Give up static check of OFFSET_ArrayType_DIMS It appears that ARR_DIMS is defined in such a way as to preclude its use in a constant expression, at least according to clang. In this commit, it is moved to a runtime check. The check of SIZEOF_ArrayType_DIM is not given up on yet; clang did not report that as an error in the last run. --- pljava-so/src/main/c/ModelConstants.c | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/pljava-so/src/main/c/ModelConstants.c b/pljava-so/src/main/c/ModelConstants.c index 293d777ab..15ddb0c00 100644 --- a/pljava-so/src/main/c/ModelConstants.c +++ b/pljava-so/src/main/c/ModelConstants.c @@ -384,8 +384,19 @@ StaticAssertStmt(offsetof(strct,fld) - VARHDRSZ == \ 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(&dummyArray)) - (char *)&dummyArray) - VARHDRSZ ); +#endif + CONFIRMEXPR( SIZEOF_ArrayType_DIM, sizeof *ARR_DIMS(&dummyArray) ); #undef CONFIRMSIZEOF @@ -434,6 +445,7 @@ StaticAssertStmt(offsetof(form,fld) == \ void pljava_ModelConstants_initialize(void) { + ArrayType dummyArray; jclass cls; JNINativeMethod methods[] = @@ -451,6 +463,14 @@ void pljava_ModelConstants_initialize(void) "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"); } /* From cf2f8c56febd10eaba5dfe9c13b7070c12b3d12c Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 31 Jan 2022 18:38:02 -0500 Subject: [PATCH 042/334] See if clang permits sizeof *ARR_DIMS(0) If it will, that eliminates the need to statically allocate a struct. --- pljava-so/src/main/c/ModelConstants.c | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/pljava-so/src/main/c/ModelConstants.c b/pljava-so/src/main/c/ModelConstants.c index 15ddb0c00..ba9badc94 100644 --- a/pljava-so/src/main/c/ModelConstants.c +++ b/pljava-so/src/main/c/ModelConstants.c @@ -176,15 +176,6 @@ static int32 constants[] = { static void dummy() { - /* - * PostgreSQL's array.h defines ARR_DIMS in a way that rules out an easy - * ARR_DIMS(0) on a compiler that treats pointer arithmetic on a null - * pointer as an error. gcc is happy to do the needed arithmetic - * on this symbol even when declared auto, but I am guessing clang will - * insist it be static. Ah well, it's just a small waste of space. - */ - static ArrayType dummyArray; - StaticAssertStmt(SIZEOF_DATUM == SIZEOF_VOID_P, "PostgreSQL SIZEOF_DATUM and SIZEOF_VOID_P no longer equivalent?"); @@ -394,10 +385,10 @@ StaticAssertStmt(offsetof(strct,fld) - VARHDRSZ == \ * detection. */ CONFIRMEXPR( OFFSET_ArrayType_DIMS, - (((char*)ARR_DIMS(&dummyArray)) - (char *)&dummyArray) - VARHDRSZ ); + (((char*)ARR_DIMS(0)) - (char *)0) - VARHDRSZ ); #endif - CONFIRMEXPR( SIZEOF_ArrayType_DIM, sizeof *ARR_DIMS(&dummyArray) ); + CONFIRMEXPR( SIZEOF_ArrayType_DIM, sizeof *ARR_DIMS(0) ); #undef CONFIRMSIZEOF #undef CONFIRMVLOFFSET From 13547a94a56a1a2f727eaa83ebee0dcb1c937e1c Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 31 Jan 2022 19:36:03 -0500 Subject: [PATCH 043/334] More work on array adapters A Contract.Array now has a type parameter for the expected element adapter type, which can range over the reference-returning As and any of the primitive-returning AsFoo types, so it is now straightforward to implement an array contract and adapter over an array whose element type will be mapped to a Java primitive. The ArgModeAdapter goes away, as suggested in its javadoc, now that an adapter for list-of-ArgMode can be easily thrown together from an INT1 element adapter and an anonymous array contract. In passing, also reorder ArrayAdapter constructor arguments, putting the element adapter first. In constructing an ArrayAdapter with an in-place anonymous array contract, the element adapter was becoming hard to see when it came at the end of all that. A reference-returning (As) adapter now has a method to allocate a T[] array, relieving the usual Java headache of getting the expected compile-time typing on an array. AsFlatList.nullsIncludedCopy now uses that and returns an unmodifiable list. A bunch of javadoc filled in. --- .../java/org/postgresql/pljava/Adapter.java | 517 +++++++++++++++++- .../java/org/postgresql/pljava/adt/Array.java | 10 +- .../pljava/adt/spi/AbstractType.java | 48 +- .../pljava/pg/CatalogObjectImpl.java | 70 ++- .../pljava/pg/RegProcedureImpl.java | 3 +- .../pljava/pg/adt/ArgModeAdapter.java | 85 --- .../pljava/pg/adt/ArrayAdapter.java | 131 ++++- .../pljava/pg/adt/GrantAdapter.java | 6 +- 8 files changed, 704 insertions(+), 166 deletions(-) delete mode 100644 pljava/src/main/java/org/postgresql/pljava/pg/adt/ArgModeAdapter.java diff --git a/pljava-api/src/main/java/org/postgresql/pljava/Adapter.java b/pljava-api/src/main/java/org/postgresql/pljava/Adapter.java index 1352fa5b7..7d5079eb6 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/Adapter.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/Adapter.java @@ -19,6 +19,7 @@ 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; @@ -84,7 +85,7 @@ * 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 + * {@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. *

    @@ -99,11 +100,26 @@ */ public abstract class Adapter { + /** + * 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; /** @@ -112,12 +128,15 @@ public abstract class Adapter *

    * 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 , C extends Contract> Adapter( - Configuration configuration, A over, C using, Type witness) + private Adapter( + Configuration configuration, Adapter over, Contract using, Type witness) { requireNonNull(configuration, - getClass() + " instantiated without a Configuration object"); + () -> getClass() + " instantiated without a Configuration object"); if ( getClass() != configuration.m_class ) throw new IllegalArgumentException( getClass() + " instantiated with a Configuration object " + @@ -142,7 +161,10 @@ private , C extends Contract> Adapter( top = specialization(using.getClass(), Contract.class)[0]; MethodHandle mh = leaf.m_fetch.bindTo(this); - Class erased = erase(top); + + @SuppressWarnings("unchecked") + Class erased = (Class)erase(top); + if ( null == witness ) { if ( top instanceof TypeVariable @@ -159,6 +181,7 @@ private , C extends Contract> Adapter( mh = mh.asType(mh.type().changeReturnType(erase(witness))); } m_topType = top; + m_topErased = erased; m_underAdapter = null; m_fetchHandle = mh; return; @@ -192,7 +215,20 @@ private , C extends Contract> Adapter( } m_topType = top; - m_underAdapter = over; + + @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; @@ -271,15 +307,48 @@ public String toString() " to produce " + topType(); } + /** + * Method that an {@code Adapter} must implement to indicate whether it + * is capable of fetching a given PostgreSQL type. + */ public abstract boolean canFetch(RegType pgType); + /** + * 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 + * {@link #canFetch(RegType) canFetch} for the attribute's declared + * PostgreSQL type. + */ public boolean canFetch(Attribute 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. + */ public static Type topType(Class cls) { Type[] params = specialization(cls, Adapter.class); @@ -308,6 +377,14 @@ 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); @@ -317,6 +394,12 @@ public static Type underType(Class cls) 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; @@ -377,6 +460,21 @@ 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. + *

    + * 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) { @@ -522,7 +620,7 @@ protected Container() * composed over another adapter of primitive type, where boxed-class is the * boxed counterpart of the other adapter's primitive type. *

    - * If Java's reflection methods on generic types will be used to compute + * 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. */ @@ -541,7 +639,7 @@ public abstract static class As extends Adapter * 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) + protected As(Configuration c, Adapter over, Class witness) { super(c, over, null, witness); @@ -572,9 +670,13 @@ protected As( /** * 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, As adapter, Class witness, + private As( + Contract.Array using, Adapter adapter, Class witness, Configuration c) { super(c, null, using, @@ -585,16 +687,34 @@ private > As( mh.asType(mh.type().changeReturnType(Object.class)); } - private static Type refinement( - Contract.Array using, As adapter) + /** + * 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) { @@ -609,26 +729,76 @@ public final T fetch( } } + /** + * 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) throws SQLException { 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); + } } - public abstract static class Array extends As + /** + * 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 { - protected final Contract.Array m_contract; - protected final As m_elementAdapter; + /** + * 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}. + * 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 @@ -638,11 +808,163 @@ public abstract static class Array extends As * precisely than the default typing rules. * @param c Configuration instance generated for this class */ + protected Array( + Contract.Array> using, As adapter, + Class 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 c Configuration instance generated for this class + */ protected Array( - Contract.Array using, As adapter, Class witness, + Contract.Array> using, AsLong adapter, Configuration c) { - super(using, adapter, witness, c); + super(using, adapter, null, 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 c Configuration instance generated for this class + */ + protected Array( + Contract.Array> using, AsDouble adapter, + Configuration c) + { + super(using, adapter, null, 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 c Configuration instance generated for this class + */ + protected Array( + Contract.Array> using, AsInt adapter, + Configuration c) + { + super(using, adapter, null, 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 c Configuration instance generated for this class + */ + protected Array( + Contract.Array> using, AsFloat adapter, + Configuration c) + { + super(using, adapter, null, 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 c Configuration instance generated for this class + */ + protected Array( + Contract.Array> using, AsShort adapter, + Configuration c) + { + super(using, adapter, null, 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 c Configuration instance generated for this class + */ + protected Array( + Contract.Array> using, AsChar adapter, + Configuration c) + { + super(using, adapter, null, 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 c Configuration instance generated for this class + */ + protected Array( + Contract.Array> using, AsByte adapter, + Configuration c) + { + super(using, adapter, null, 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 c Configuration instance generated for this class + */ + protected Array( + Contract.Array> using, AsBoolean adapter, + Configuration c) + { + super(using, adapter, null, c); m_contract = using; m_elementAdapter = adapter; } @@ -672,6 +994,11 @@ 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() { @@ -679,6 +1006,10 @@ public boolean canFetchNull() } } + /** + * Abstract superclass of signed and unsigned primitive {@code long} + * adapters. + */ public abstract static class AsLong extends Primitive implements TwosComplement { @@ -687,6 +1018,10 @@ 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) { @@ -701,12 +1036,22 @@ public final long fetch( } } + /** + * 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) throws SQLException { throw 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 { @@ -716,6 +1061,9 @@ protected > Signed(Configuration c, A over) } } + /** + * Abstract superclass of unsigned primitive {@code long} adapters. + */ public abstract static class Unsigned extends AsLong implements TwosComplement.Unsigned { @@ -727,6 +1075,9 @@ protected > Unsigned( } } + /** + * Abstract superclass of primitive {@code double} adapters. + */ public abstract static class AsDouble extends Primitive { protected > AsDouble(Configuration c, A over) @@ -734,6 +1085,10 @@ 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) { @@ -748,6 +1103,13 @@ public final double fetch( } } + /** + * 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) throws SQLException { throw new SQLDataException( @@ -755,6 +1117,10 @@ public double fetchNull(Attribute a) throws SQLException } } + /** + * Abstract superclass of signed and unsigned primitive {@code int} + * adapters. + */ public abstract static class AsInt extends Primitive implements TwosComplement { @@ -763,6 +1129,10 @@ 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) { @@ -777,12 +1147,22 @@ public final int fetch( } } + /** + * 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) throws SQLException { throw 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 { @@ -792,6 +1172,9 @@ protected > Signed(Configuration c, A over) } } + /** + * Abstract superclass of unsigned primitive {@code int} adapters. + */ public abstract static class Unsigned extends AsInt implements TwosComplement.Unsigned { @@ -803,6 +1186,9 @@ protected > Unsigned( } } + /** + * Abstract superclass of primitive {@code float} adapters. + */ public abstract static class AsFloat extends Primitive { protected > AsFloat(Configuration c, A over) @@ -810,6 +1196,10 @@ 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) { @@ -824,6 +1214,13 @@ public final float fetch( } } + /** + * 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) throws SQLException { throw new SQLDataException( @@ -831,6 +1228,10 @@ public float fetchNull(Attribute a) throws SQLException } } + /** + * Abstract superclass of signed and unsigned primitive {@code short} + * adapters. + */ public abstract static class AsShort extends Primitive implements TwosComplement { @@ -839,6 +1240,10 @@ 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) { @@ -853,12 +1258,22 @@ public final short fetch( } } + /** + * 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) throws SQLException { throw 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 { @@ -868,6 +1283,9 @@ protected > Signed(Configuration c, A over) } } + /** + * Abstract superclass of unsigned primitive {@code short} adapters. + */ public abstract static class Unsigned extends AsShort implements TwosComplement.Unsigned { @@ -879,6 +1297,9 @@ protected > Unsigned( } } + /** + * Abstract superclass of primitive {@code char} adapters. + */ public abstract static class AsChar extends Primitive { protected > AsChar(Configuration c, A over) @@ -886,6 +1307,10 @@ 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) { @@ -900,6 +1325,13 @@ public final char fetch( } } + /** + * 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) throws SQLException { throw new SQLDataException( @@ -907,6 +1339,10 @@ public char fetchNull(Attribute a) throws SQLException } } + /** + * Abstract superclass of signed and unsigned primitive {@code byte} + * adapters. + */ public abstract static class AsByte extends Primitive implements TwosComplement { @@ -915,6 +1351,10 @@ 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) { @@ -929,12 +1369,22 @@ public final byte fetch( } } + /** + * 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) throws SQLException { throw 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 { @@ -944,6 +1394,9 @@ protected > Signed(Configuration c, A over) } } + /** + * Abstract superclass of unsigned primitive {@code byte} adapters. + */ public abstract static class Unsigned extends AsByte implements TwosComplement.Unsigned { @@ -955,6 +1408,9 @@ protected > Unsigned( } } + /** + * Abstract superclass of primitive {@code boolean} adapters. + */ public abstract static class AsBoolean extends Primitive { protected > AsBoolean(Configuration c, A over) @@ -962,6 +1418,10 @@ 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) { @@ -976,6 +1436,13 @@ public final boolean fetch( } } + /** + * 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) throws SQLException { throw new SQLDataException( @@ -1007,11 +1474,15 @@ interface Scalar extends Contract * 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 to be returned by an instance of the contract. * @param the type returned by an associated {@code Adapter} for - * the element type + * 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 + public interface Array> extends Contract { /** * Constructs a representation T representing @@ -1026,13 +1497,13 @@ public interface Array extends Contract * 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 + * 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, As adapter, Indexed slot) + int nDims, int[] dimsAndBounds, A adapter, Indexed slot) throws SQLException; } } 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 index e474e9a49..63cee9dbc 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/adt/Array.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/adt/Array.java @@ -13,7 +13,6 @@ import java.sql.SQLException; -import java.util.ArrayList; import java.util.List; import org.postgresql.pljava.Adapter; @@ -31,8 +30,7 @@ public interface Array * with no attention to its specified dimensionality or index bounds. */ @FunctionalInterface - interface AsFlatList - extends Contract.Array,E> + interface AsFlatList extends Contract.Array,E,Adapter.As> { /** * Shorthand for a cast of a suitable method reference to this @@ -54,10 +52,10 @@ static List nullsIncludedCopy( throws SQLException { int n = slot.elements(); - List result = new ArrayList<>(n); + E[] result = adapter.arrayOf(n); for ( int i = 0; i < n; ++ i ) - result.add(slot.get(i, adapter)); - return result; + result[i] = slot.get(i, adapter); + return List.of(result); } } } 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 index 54f67707f..e2197138d 100644 --- 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 @@ -484,16 +484,7 @@ else if ( t instanceof ParameterizedType ) Type oraw = p.getRawType(); assert oraw instanceof Class; - boolean changed = false; - for ( int i = 0; i < as.length; ++ i ) - { - Type oa = as[i]; - Type na = substitute(b, oa); - if ( na == oa ) - continue; - as[i] = na; - changed = true; - } + boolean changed = substituted(b, as); if ( null != oown ) { @@ -511,15 +502,15 @@ else if ( t instanceof ParameterizedType ) } else if ( t instanceof WildcardType ) { - throw new UnsupportedOperationException( - "substitute on a wildcard type"); - /* - * Probably just substitute 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. - */ + 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 ) { @@ -545,6 +536,25 @@ else if ( t instanceof Class ) "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 ) diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/CatalogObjectImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/CatalogObjectImpl.java index 4288987ed..c6c7b27f7 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/CatalogObjectImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/CatalogObjectImpl.java @@ -13,6 +13,7 @@ import org.postgresql.pljava.Adapter; import org.postgresql.pljava.Adapter.As; +import org.postgresql.pljava.Adapter.AsByte; import static org.postgresql.pljava.internal.Backend.threadMayEnterPG; import org.postgresql.pljava.internal.CacheMap; @@ -34,6 +35,7 @@ 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; @@ -988,26 +990,26 @@ default List grants(RegRole grantee) */ public interface ArrayAdapters { - ArrayAdapter,?> REGCLASS_LIST_INSTANCE = - new ArrayAdapter<>(AsFlatList.of(AsFlatList::nullsIncludedCopy), - REGCLASS_INSTANCE); + ArrayAdapter> REGCLASS_LIST_INSTANCE = + new ArrayAdapter<>(REGCLASS_INSTANCE, + AsFlatList.of(AsFlatList::nullsIncludedCopy)); - ArrayAdapter,?> REGTYPE_LIST_INSTANCE = - new ArrayAdapter<>(AsFlatList.of(AsFlatList::nullsIncludedCopy), - REGTYPE_INSTANCE); + 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<>( + 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,String>() + new Adapter.Contract.Array<>() { @Override public List construct( @@ -1023,8 +1025,54 @@ public List construct( slot.get(i, adapter)); return List.of(names); } - }, - TextAdapter.INSTANCE); + }); + + /** + * 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 = diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/RegProcedureImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/RegProcedureImpl.java index f999acb71..3b2e27e15 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/RegProcedureImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/RegProcedureImpl.java @@ -35,7 +35,6 @@ import org.postgresql.pljava.pg.CatalogObjectImpl.*; import static org.postgresql.pljava.pg.ModelConstants.PROCOID; // syscache -import org.postgresql.pljava.pg.adt.ArgModeAdapter; import static org.postgresql.pljava.pg.adt.ArrayAdapter .FLAT_STRING_LIST_INSTANCE; import org.postgresql.pljava.pg.adt.GrantAdapter; @@ -342,7 +341,7 @@ private static List argModes(RegProcedureImpl o) TupleTableSlot s = o.cacheTuple(); return s.get(s.descriptor().get("proargmodes"), - ArgModeAdapter.LIST_INSTANCE); + ArrayAdapters.ARGMODE_LIST_INSTANCE); } private static List argNames(RegProcedureImpl o) throws SQLException diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/adt/ArgModeAdapter.java b/pljava/src/main/java/org/postgresql/pljava/pg/adt/ArgModeAdapter.java deleted file mode 100644 index 5b95f7137..000000000 --- a/pljava/src/main/java/org/postgresql/pljava/pg/adt/ArgModeAdapter.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * 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 java.util.List; - -import org.postgresql.pljava.Adapter; -import org.postgresql.pljava.adt.Array.AsFlatList; -import org.postgresql.pljava.model.Attribute; -import org.postgresql.pljava.model.RegProcedure; -import org.postgresql.pljava.model.RegProcedure.ArgMode; -import org.postgresql.pljava.model.RegType; - -/** - * {@link ArgMode ArgMode} from a {@code "char"} column. - *

    - * This adapter is arguably too specialized to deserve to exist, but it does, - * to support the {@link RegProcedure#argModes RegProcedure.argModes} array - * property. Once array adapter contracts have been generalized to support - * a primitive-typed element adapter (which has to happen anyway), an - * array-of-{@code ArgMode} type will be trivially achievable with a contract, - * and this adapter can go away. - */ -public class ArgModeAdapter extends Adapter.As -{ - public static final ArgModeAdapter INSTANCE; - - public static final ArrayAdapter,?> LIST_INSTANCE; - - static - { - @SuppressWarnings("removal") // JEP 411 - Configuration config = AccessController.doPrivileged( - (PrivilegedAction)() -> - configure(ArgModeAdapter.class, Via.BYTE)); - - INSTANCE = new ArgModeAdapter(config); - - LIST_INSTANCE = new ArrayAdapter<>( - AsFlatList.of(AsFlatList::nullsIncludedCopy), INSTANCE); - } - - private ArgModeAdapter(Configuration c) - { - super(c, null, null); - } - - @Override - public boolean canFetch(RegType pgType) - { - return RegType.CHAR == pgType; - } - - public ArgMode fetch(Attribute a, byte in) - { - switch ( in ) - { - case (byte)'i': - return ArgMode.IN; - case (byte)'o': - return ArgMode.OUT; - case (byte)'b': - return ArgMode.INOUT; - case (byte)'v': - return ArgMode.VARIADIC; - case (byte)'t': - return ArgMode.TABLE; - default: - throw new UnsupportedOperationException(String.format( - "Unrecognized procedure/function argument mode value %#x", in)); - } - } -} 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 index 332420da1..4eaae6f7a 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/adt/ArrayAdapter.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/adt/ArrayAdapter.java @@ -63,12 +63,12 @@ /** * PostgreSQL arrays represented as something or other. */ -public class ArrayAdapter extends Adapter.Array +public class ArrayAdapter extends Adapter.Array { private static final Configuration s_config; public static final - ArrayAdapter,?> FLAT_STRING_LIST_INSTANCE; + ArrayAdapter> FLAT_STRING_LIST_INSTANCE; static { @@ -80,26 +80,114 @@ public class ArrayAdapter extends Adapter.Array s_config = config; FLAT_STRING_LIST_INSTANCE = new ArrayAdapter<>( - AsFlatList.of(AsFlatList::nullsIncludedCopy), TextAdapter.INSTANCE); + TextAdapter.INSTANCE, AsFlatList.of(AsFlatList::nullsIncludedCopy)); } - public static ArrayAdapter - arrayAdapter(Contract.Array contract, Adapter.As element) + /** + * Constructs an array adapter given an adapter that returns a reference + * type {@literal } for the element type, and a corresponding array + * contract. + */ + public ArrayAdapter( + Adapter.As element, Contract.Array> contract) { - try - { - return new ArrayAdapter<>(contract, element); - } - catch ( Throwable t ) - { - t.printStackTrace(); - throw t; - } + super(contract, element, null, s_config); } - public ArrayAdapter(Contract.Array contract, Adapter.As element) + /** + * Constructs an array adapter given an adapter that returns a primitive + * {@code long} for the element type, and a corresponding array + * contract. + */ + public ArrayAdapter( + Adapter.AsLong element, + Contract.Array> contract) { - super(contract, element, null, s_config); + super(contract, element, s_config); + } + + /** + * Constructs an array adapter given an adapter that returns a primitive + * {@code double} for the element type, and a corresponding array + * contract. + */ + public ArrayAdapter( + Adapter.AsDouble element, + Contract.Array> contract) + { + super(contract, element, s_config); + } + + /** + * Constructs an array adapter given an adapter that returns a primitive + * {@code int} for the element type, and a corresponding array + * contract. + */ + public ArrayAdapter( + Adapter.AsInt element, + Contract.Array> contract) + { + super(contract, element, s_config); + } + + /** + * Constructs an array adapter given an adapter that returns a primitive + * {@code float} for the element type, and a corresponding array + * contract. + */ + public ArrayAdapter( + Adapter.AsFloat element, + Contract.Array> contract) + { + super(contract, element, s_config); + } + + /** + * Constructs an array adapter given an adapter that returns a primitive + * {@code short} for the element type, and a corresponding array + * contract. + */ + public ArrayAdapter( + Adapter.AsShort element, + Contract.Array> contract) + { + super(contract, element, s_config); + } + + /** + * Constructs an array adapter given an adapter that returns a primitive + * {@code char} for the element type, and a corresponding array + * contract. + */ + public ArrayAdapter( + Adapter.AsChar element, + Contract.Array> contract) + { + super(contract, element, s_config); + } + + /** + * Constructs an array adapter given an adapter that returns a primitive + * {@code byte} for the element type, and a corresponding array + * contract. + */ + public ArrayAdapter( + Adapter.AsByte element, + Contract.Array> contract) + { + super(contract, element, s_config); + } + + /** + * Constructs an array adapter given an adapter that returns a primitive + * {@code boolean} for the element type, and a corresponding array + * contract. + */ + public ArrayAdapter( + Adapter.AsBoolean element, + Contract.Array> contract) + { + super(contract, element, s_config); } @Override @@ -181,8 +269,17 @@ public T fetch(Attribute a, Datum.Input in) int[] dimsBoundsArray = new int [ dimsAndBounds.capacity() ]; dimsAndBounds.get(dimsBoundsArray); - return m_contract.construct( + /* + * 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 { 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 index e60984bd5..1848710da 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/adt/GrantAdapter.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/adt/GrantAdapter.java @@ -39,7 +39,7 @@ public class GrantAdapter extends Adapter.As { public static final GrantAdapter INSTANCE; - public static final ArrayAdapter,?> LIST_INSTANCE; + public static final ArrayAdapter> LIST_INSTANCE; static { @@ -50,8 +50,8 @@ public class GrantAdapter extends Adapter.As INSTANCE = new GrantAdapter(config); - LIST_INSTANCE = new ArrayAdapter<>( - AsFlatList.of(AsFlatList::nullsIncludedCopy), INSTANCE); + LIST_INSTANCE = new ArrayAdapter<>(INSTANCE, + AsFlatList.of(AsFlatList::nullsIncludedCopy)); } private GrantAdapter(Configuration c) From 4bf4a8dc35ffa2942ae950adaba2ac9439af7f23 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 1 Feb 2022 13:03:45 -0500 Subject: [PATCH 044/334] Some polishing of javadocs and code --- .../java/org/postgresql/pljava/Adapter.java | 24 +++++++---- .../pljava/pg/adt/ArrayAdapter.java | 41 ++++++++++++++----- 2 files changed, 46 insertions(+), 19 deletions(-) diff --git a/pljava-api/src/main/java/org/postgresql/pljava/Adapter.java b/pljava-api/src/main/java/org/postgresql/pljava/Adapter.java index 7d5079eb6..dd4636cec 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/Adapter.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/Adapter.java @@ -94,8 +94,9 @@ * {@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 classes that are permitted to instantiate - * this class will be able to manipulate raw {@code Datum}s. An adapter class + * 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 @@ -348,6 +349,10 @@ public boolean canFetch(Attribute attr) * 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) { @@ -371,6 +376,11 @@ public static Type topType(Class cls) * 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() { @@ -465,6 +475,9 @@ private static void checkAllowed() * 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. @@ -484,13 +497,6 @@ protected static Configuration configure( Class topErased = erase(top); Class underErased = erase(under); - if ( Primitive.class.isAssignableFrom(cls) ) - { - MethodType mt = methodType(topErased); - assert mt.hasWrappers(); - top = topErased = mt.unwrap().returnType(); - } - MethodHandle underFetcher = null; String fetchName; Predicate fetchPredicate; 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 index 4eaae6f7a..89d32c5a7 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/adt/ArrayAdapter.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/adt/ArrayAdapter.java @@ -61,12 +61,20 @@ */ /** - * PostgreSQL arrays represented as something or other. + * 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; @@ -86,7 +94,7 @@ public class ArrayAdapter extends Adapter.Array /** * Constructs an array adapter given an adapter that returns a reference * type {@literal } for the element type, and a corresponding array - * contract. + * contract producing {@literal }. */ public ArrayAdapter( Adapter.As element, Contract.Array> contract) @@ -97,7 +105,7 @@ public ArrayAdapter( /** * Constructs an array adapter given an adapter that returns a primitive * {@code long} for the element type, and a corresponding array - * contract. + * contract producing {@literal }. */ public ArrayAdapter( Adapter.AsLong element, @@ -109,7 +117,7 @@ public ArrayAdapter( /** * Constructs an array adapter given an adapter that returns a primitive * {@code double} for the element type, and a corresponding array - * contract. + * contract producing {@literal }. */ public ArrayAdapter( Adapter.AsDouble element, @@ -121,7 +129,7 @@ public ArrayAdapter( /** * Constructs an array adapter given an adapter that returns a primitive * {@code int} for the element type, and a corresponding array - * contract. + * contract producing {@literal }. */ public ArrayAdapter( Adapter.AsInt element, @@ -133,7 +141,7 @@ public ArrayAdapter( /** * Constructs an array adapter given an adapter that returns a primitive * {@code float} for the element type, and a corresponding array - * contract. + * contract producing {@literal }. */ public ArrayAdapter( Adapter.AsFloat element, @@ -145,7 +153,7 @@ public ArrayAdapter( /** * Constructs an array adapter given an adapter that returns a primitive * {@code short} for the element type, and a corresponding array - * contract. + * contract producing {@literal }. */ public ArrayAdapter( Adapter.AsShort element, @@ -157,7 +165,7 @@ public ArrayAdapter( /** * Constructs an array adapter given an adapter that returns a primitive * {@code char} for the element type, and a corresponding array - * contract. + * contract producing {@literal }. */ public ArrayAdapter( Adapter.AsChar element, @@ -169,7 +177,7 @@ public ArrayAdapter( /** * Constructs an array adapter given an adapter that returns a primitive * {@code byte} for the element type, and a corresponding array - * contract. + * contract producing {@literal }. */ public ArrayAdapter( Adapter.AsByte element, @@ -181,7 +189,7 @@ public ArrayAdapter( /** * Constructs an array adapter given an adapter that returns a primitive * {@code boolean} for the element type, and a corresponding array - * contract. + * contract producing {@literal }. */ public ArrayAdapter( Adapter.AsBoolean element, @@ -190,6 +198,13 @@ public ArrayAdapter( super(contract, element, 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) { @@ -197,6 +212,11 @@ public boolean canFetch(RegType pgType) 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 { @@ -227,6 +247,7 @@ public T fetch(Attribute a, Datum.Input in) 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(); From fe7abe7998bce8b54e72d45172589a50eb0a7b7d Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 8 Feb 2022 12:19:45 -0500 Subject: [PATCH 045/334] Fail validation if TRANSFORM FOR TYPE declared The TRANSFORM FOR TYPE mechanism is only meaningful for a PL whose handler function itself does the work of looking up and applying the transforms. https://www.postgresql.org/message-id/61FDBCFE.3090800%40anastigmatix.net This PL doesn't, so say so when validating a function declaration that includes a TRANSFORM clause. It would arguably be better to say so even earlier, at an attempted CREATE TRANSFORM for the PL, but PostgreSQL does not currently apply any PL-specific validator function at that time. --- .../org/postgresql/pljava/internal/Function.java | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) 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 842ad8153..0c7de5b01 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/Function.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/Function.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 Tada AB and other contributors, as listed below. + * Copyright (c) 2016-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 @@ -55,6 +55,7 @@ import java.sql.ResultSet; import java.sql.SQLData; import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; import java.sql.SQLInput; import java.sql.SQLOutput; import java.sql.SQLNonTransientException; @@ -1318,6 +1319,18 @@ public static Invocable create( { Matcher info = parse(procTup); + /* + * Reject any TRANSFORM FOR TYPE clause at validation time, on + * the grounds that it will get ignored at invocation time anyway. + * The check could be made unconditional, and so catch at invocation + * time any function that might have been declared before this validator + * check was added. But simply ignoring the clause at invocation time + * (as promised...) keeps that path leaner. + */ + if ( forValidator && null != procTup.getObject("protrftypes") ) + throw new SQLFeatureNotSupportedException( + "a PL/Java function will not apply TRANSFORM FOR TYPE","0A000"); + if ( forValidator && ! checkBody ) return null; From 1fed9034d21cefe599ae03767b8ff2495e6330ec Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 15 Feb 2022 17:02:48 -0500 Subject: [PATCH 046/334] Compute supported source version in DDRProcessor Because PL/Java must be compilable on any Java back to release 9, the DDRProcessor cannot refer by name to any SourceVersion enum constant later than RELEASE_9. It also, arguably, should not, because its development and testing rely on the javax.lang.model API as of release 9. Happily, in practice, later Java releases do not often break the DDRProcessor code, so user Java code for releases later than 9 can be compiled with no difficulty, other than a compiler warning about the processor's source version being pegged at 9. But the warning is an obstacle if the user code is being compiled with a fail-on-warning policy, as in issue #403. This patch adopts a compromise position, and keeps track of the latest source version for which the annotation processor at least has been seen to pass the CI tests (fully understanding that such testing is no substitute for fully auditing any release-to-release changes in the javax.lang.model APIs and what impact they could have on the processor!). It will compute its "declared" supported source version to be the earlier of SourceVersion.latestSupported() and that latest tested version. As a result, it should eliminate the compiler warning when running on any Java version in that range. The warning will reappear when running a compile on a later Java version, and it should be easy to alleviate that with a PL/Java release that bumps the latest_tested version. Merely passing the CI tests as normally run isn't enough, because the project is built with a --release 9 option. Before actually bumping latest_tested, a test build (at least of the pljava-examples project) should be done on the Java release in question and without the limiting --release option. --- .../annotation/processing/DDRProcessor.java | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/pljava-api/src/main/java/org/postgresql/pljava/annotation/processing/DDRProcessor.java b/pljava-api/src/main/java/org/postgresql/pljava/annotation/processing/DDRProcessor.java index b78e321f3..6dfaefb7d 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/annotation/processing/DDRProcessor.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/annotation/processing/DDRProcessor.java @@ -158,10 +158,31 @@ "ddr.implementor", // implementor when not annotated, default "PostgreSQL" "ddr.output" // name of ddr file to write }) -@SupportedSourceVersion(SourceVersion.RELEASE_9) public class DDRProcessor extends AbstractProcessor { private DDRProcessorImpl impl; + + @Override + public SourceVersion getSupportedSourceVersion() + { + /* + * Because this must compile on Java versions back to 9, it must not + * mention by name any SourceVersion constant later than RELEASE_9. + * + * Update latest_tested to be the latest Java release on which this + * annotation processor has been tested without problems. + */ + int latest_tested = 17; + int ordinal_9 = SourceVersion.RELEASE_9.ordinal(); + int ordinal_latest = latest_tested - 9 + ordinal_9; + + SourceVersion latestSupported = SourceVersion.latestSupported(); + + if ( latestSupported.ordinal() <= ordinal_latest ) + return latestSupported; + + return SourceVersion.values()[ordinal_latest]; + } @Override public void init( ProcessingEnvironment processingEnv) From 2167527bb874de26c53fd67049d770e3471bb968 Mon Sep 17 00:00:00 2001 From: sincatter Date: Fri, 24 Jun 2022 00:31:44 +0800 Subject: [PATCH 047/334] Fix the problem of invalid timer in _destroyJavaVM After registering the timer with RegisterTimeout(), call enable_timeout_after() to make the timer take effect --- pljava-so/src/main/c/Backend.c | 1 + 1 file changed, 1 insertion(+) diff --git a/pljava-so/src/main/c/Backend.c b/pljava-so/src/main/c/Backend.c index a02988b48..9472e9641 100644 --- a/pljava-so/src/main/c/Backend.c +++ b/pljava-so/src/main/c/Backend.c @@ -1406,6 +1406,7 @@ static void _destroyJavaVM(int status, Datum dummy) #if PG_VERSION_NUM >= 90300 tid = RegisterTimeout(USER_TIMEOUT, terminationTimeoutHandler); + enable_timeout_after(tid, 5000); #else saveSigAlrm = pqsignal(SIGALRM, terminationTimeoutHandler); enable_sig_alarm(5000, false); From be307abe44050e7b89877a534d5b7684e2266e1c Mon Sep 17 00:00:00 2001 From: Francisco Miguel Biete Banon Date: Fri, 21 Oct 2022 13:54:36 +0100 Subject: [PATCH 048/334] Support PostgreSQL v15 String is a PostgreSQL type, rename pljava String to PLJString --- pljava-so/src/main/c/type/String.c | 16 ++++++++-------- pljava-so/src/main/include/pljava/type/String.h | 8 ++++---- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/pljava-so/src/main/c/type/String.c b/pljava-so/src/main/c/type/String.c index 4b58f622f..01e2a18b2 100644 --- a/pljava-so/src/main/c/type/String.c +++ b/pljava-so/src/main/c/type/String.c @@ -59,9 +59,9 @@ jvalue _String_coerceDatum(Type self, Datum arg) { jvalue result; char* tmp = DatumGetCString(FunctionCall3( - &((String)self)->textOutput, + &((PLJString)self)->textOutput, arg, - ObjectIdGetDatum(((String)self)->elementType), + ObjectIdGetDatum(((PLJString)self)->elementType), Int32GetDatum(-1))); result.l = String_createJavaStringFromNTS(tmp); pfree(tmp); @@ -83,19 +83,19 @@ Datum _String_coerceObject(Type self, jobject jstr) JNI_deleteLocalRef(jstr); ret = FunctionCall3( - &((String)self)->textInput, + &((PLJString)self)->textInput, CStringGetDatum(tmp), - ObjectIdGetDatum(((String)self)->elementType), + ObjectIdGetDatum(((PLJString)self)->elementType), Int32GetDatum(-1)); pfree(tmp); return ret; } -static String String_create(TypeClass cls, Oid typeId) +static PLJString String_create(TypeClass cls, Oid typeId) { HeapTuple typeTup = PgObject_getValidTuple(TYPEOID, typeId, "type"); Form_pg_type pgType = (Form_pg_type)GETSTRUCT(typeTup); - String self = (String)TypeClass_allocInstance(cls, typeId); + PLJString self = (PLJString)TypeClass_allocInstance(cls, typeId); MemoryContext ctx = GetMemoryChunkContext(self); fmgr_info_cxt(pgType->typoutput, &self->textOutput, ctx); fmgr_info_cxt(pgType->typinput, &self->textInput, ctx); @@ -109,7 +109,7 @@ Type String_obtain(Oid typeId) return (Type)StringClass_obtain(s_StringClass, typeId); } -String StringClass_obtain(TypeClass self, Oid typeId) +PLJString StringClass_obtain(TypeClass self, Oid typeId) { return String_create(self, typeId); } @@ -126,7 +126,7 @@ jstring String_createJavaString(text* t) Size srcLen = VARSIZE(t) - VARHDRSZ; if(srcLen == 0) return s_the_empty_string; - + if ( s_two_step_conversion ) { utf8 = (char*)pg_do_encoding_conversion((unsigned char*)src, diff --git a/pljava-so/src/main/include/pljava/type/String.h b/pljava-so/src/main/include/pljava/type/String.h index 6abf07520..a03f915d3 100644 --- a/pljava-so/src/main/include/pljava/type/String.h +++ b/pljava-so/src/main/include/pljava/type/String.h @@ -19,9 +19,9 @@ extern "C" { * The String class extends the Type and adds the members necessary to * perform standard Postgres textin/textout conversion. An instance of this * class will be used for all types that are not explicitly mapped. - * + * * The class also has some convenience routings for Java String manipulation. - * + * * @author Thomas Hallgren * **************************************************************************/ @@ -29,7 +29,7 @@ extern "C" { extern jclass s_Object_class; extern jclass s_String_class; struct String_; -typedef struct String_* String; +typedef struct String_* PLJString; /* * Create a Java String object from a null terminated string. Conversion is @@ -73,7 +73,7 @@ extern text* String_createText(jstring javaString); extern Type String_obtain(Oid typeId); -extern String StringClass_obtain(TypeClass self, Oid typeId); +extern PLJString StringClass_obtain(TypeClass self, Oid typeId); #ifdef __cplusplus } From 6fbe91bf975ec032d4bfa4b4105c18cd10963c22 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 29 May 2023 18:48:21 -0400 Subject: [PATCH 049/334] Disable failing CI test until an issue 434 fix In passing, also add semicolons where jshell is (sometimes) lax about them. It is sometimes handy to use copy/paste from these test scripts into a live jshell, and its tolerance of missing semicolons can be diminished then. Refactoring the github and appveyor CI scripts to share one copy of the jshell script would be a fine thing. --- .github/workflows/ci-runnerpg.yml | 42 +++++++++++++++++-------------- appveyor.yml | 40 ++++++++++++++++------------- 2 files changed, 45 insertions(+), 37 deletions(-) diff --git a/.github/workflows/ci-runnerpg.yml b/.github/workflows/ci-runnerpg.yml index d77d5a53a..9155acd60 100644 --- a/.github/workflows/ci-runnerpg.yml +++ b/.github/workflows/ci-runnerpg.yml @@ -200,18 +200,18 @@ jobs: boolean succeeding = false; // begin pessimistic - import static java.nio.file.Files.createTempFile - import static java.nio.file.Files.write - import java.nio.file.Path - import static java.nio.file.Paths.get - import java.sql.Connection - import java.sql.PreparedStatement - import java.sql.ResultSet - import org.postgresql.pljava.packaging.Node - import static org.postgresql.pljava.packaging.Node.q - import static org.postgresql.pljava.packaging.Node.stateMachine - import static org.postgresql.pljava.packaging.Node.isVoidResultSet - import static org.postgresql.pljava.packaging.Node.s_isWindows + import static java.nio.file.Files.createTempFile; + import static java.nio.file.Files.write; + import java.nio.file.Path; + import static java.nio.file.Paths.get; + import java.sql.Connection; + import java.sql.PreparedStatement; + import java.sql.ResultSet; + import org.postgresql.pljava.packaging.Node; + import static org.postgresql.pljava.packaging.Node.q; + import static org.postgresql.pljava.packaging.Node.stateMachine; + import static org.postgresql.pljava.packaging.Node.isVoidResultSet; + import static org.postgresql.pljava.packaging.Node.s_isWindows; String javaHome = System.getProperty("java.home"); @@ -225,12 +225,13 @@ jobs: : javaLibDir.resolve(s_isWindows ? "jvm.dll" : "server/libjvm.so") ); - String vmopts = "-enableassertions:org.postgresql.pljava... -Xcheck:jni" + String vmopts = + "-enableassertions:org.postgresql.pljava... -Xcheck:jni"; - Node n1 = Node.get_new_node("TestNode1") + Node n1 = Node.get_new_node("TestNode1"); if ( s_isWindows ) - n1.use_pg_ctl(true) + n1.use_pg_ctl(true); /* * Keep a tally of the three types of diagnostic notices that may be @@ -241,7 +242,7 @@ jobs: Map results = Stream.of("info", "warning", "error", "ng").collect( LinkedHashMap::new, - (m,k) -> m.put(k, 0), (r,s) -> {}) + (m,k) -> m.put(k, 0), (r,s) -> {}); boolean isDiagnostic(Object o, Set whatIsNG) { @@ -470,13 +471,14 @@ jobs: * pljava.module_path set to the right locations of the jars, and the * correct shared-object path given to LOAD. * - * Also test the after-the-fact packaging up with CREATE EXTENSION + * For now, until issue #434 has a fix, DO NOT + * also test the after-the-fact packaging up with CREATE EXTENSION * FROM unpackaged. That officially goes away in PG 13, where the * equivalent sequence * CREATE EXTENSION pljava VERSION unpackaged * \c * ALTER EXTENSION pljava UPDATE - * should be tested instead. + * should (for now, also NOT) be tested instead. */ try ( Connection c = n1.connect() ) { @@ -515,6 +517,7 @@ jobs: * PG >= 13 CREATE EXTENSION VERSION unpackaged;ALTER EXTENSION UPDATE * sequence) has to happen over a new connection. */ + if ( false ) { // pending issue 434 fix try ( Connection c = n1.connect() ) { int majorVersion = c.getMetaData().getDatabaseMajorVersion(); @@ -548,6 +551,7 @@ jobs: (o,p,q) -> null == o ); } + } // pending issue 434 fix } catch ( Throwable t ) { succeeding = false; @@ -556,5 +560,5 @@ jobs: System.out.println(results); succeeding &= (0 == results.get("ng")); - System.exit(succeeding ? 0 : 1) + System.exit(succeeding ? 0 : 1); ENDJSHELL diff --git a/appveyor.yml b/appveyor.yml index bf404ff0b..4f645a088 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -88,27 +88,27 @@ test_script: @' boolean succeeding = false; // begin pessimistic - import static java.nio.file.Files.createTempFile - import static java.nio.file.Files.write - import java.nio.file.Path - import static java.nio.file.Paths.get - import java.sql.Connection - import java.sql.PreparedStatement - import java.sql.ResultSet - import org.postgresql.pljava.packaging.Node - import static org.postgresql.pljava.packaging.Node.q - import static org.postgresql.pljava.packaging.Node.stateMachine - import static org.postgresql.pljava.packaging.Node.isVoidResultSet + import static java.nio.file.Files.createTempFile; + import static java.nio.file.Files.write; + import java.nio.file.Path; + import static java.nio.file.Paths.get; + import java.sql.Connection; + import java.sql.PreparedStatement; + import java.sql.ResultSet; + import org.postgresql.pljava.packaging.Node; + import static org.postgresql.pljava.packaging.Node.q; + import static org.postgresql.pljava.packaging.Node.stateMachine; + import static org.postgresql.pljava.packaging.Node.isVoidResultSet; System.setErr(System.out); // PowerShell makes a mess of stderr output Node.main(new String[0]); // Extract the files (with output to stdout) - String vmopts = "-enableassertions:org.postgresql.pljava... -Xcheck:jni" + String vmopts = "-enableassertions:org.postgresql.pljava... -Xcheck:jni"; - Node n1 = Node.get_new_node("TestNode1") + Node n1 = Node.get_new_node("TestNode1"); - n1.use_pg_ctl(true) + n1.use_pg_ctl(true); /* * Keep a tally of the three types of diagnostic notices that may be @@ -118,7 +118,8 @@ test_script: */ Map results = Stream.of("info", "warning", "error", "ng").collect( - LinkedHashMap::new, (m,k) -> m.put(k, 0), (r,s) -> {}) + LinkedHashMap::new, + (m,k) -> m.put(k, 0), (r,s) -> {}); boolean isDiagnostic(Object o, Set whatIsNG) { @@ -346,13 +347,14 @@ test_script: * pljava.module_path set to the right locations of the jars, and the * correct shared-object path given to LOAD. * - * Also test the after-the-fact packaging up with CREATE EXTENSION + * For now, until issue #434 has a fix, DO NOT + * also test the after-the-fact packaging up with CREATE EXTENSION * FROM unpackaged. That officially goes away in PG 13, where the * equivalent sequence * CREATE EXTENSION pljava VERSION unpackaged * \c * ALTER EXTENSION pljava UPDATE - * should be tested instead. + * should (for now, also NOT) be tested instead. */ try ( Connection c = n1.connect() ) { @@ -391,6 +393,7 @@ test_script: * PG >= 13 CREATE EXTENSION VERSION unpackaged;ALTER EXTENSION UPDATE * sequence) has to happen over a new connection. */ + if ( false ) { // pending issue 434 fix try ( Connection c = n1.connect() ) { int majorVersion = c.getMetaData().getDatabaseMajorVersion(); @@ -424,6 +427,7 @@ test_script: (o,p,q) -> null == o ); } + } // pending issue 434 fix } catch ( Throwable t ) { succeeding = false; @@ -432,7 +436,7 @@ test_script: System.out.println(results); succeeding &= (0 == results.get("ng")); - System.exit(succeeding ? 0 : 1) + System.exit(succeeding ? 0 : 1); '@ | jshell ` -execution local ` From e658009a09bf576168a20822fef9f6ddd6bd242a Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 29 May 2023 20:33:49 -0400 Subject: [PATCH 050/334] DDRProcessor tests ok with Java 19 Not with Java 20. Likely related to issue #435. --- .../postgresql/pljava/annotation/processing/DDRProcessor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pljava-api/src/main/java/org/postgresql/pljava/annotation/processing/DDRProcessor.java b/pljava-api/src/main/java/org/postgresql/pljava/annotation/processing/DDRProcessor.java index 6dfaefb7d..38046ae49 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/annotation/processing/DDRProcessor.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/annotation/processing/DDRProcessor.java @@ -172,7 +172,7 @@ public SourceVersion getSupportedSourceVersion() * Update latest_tested to be the latest Java release on which this * annotation processor has been tested without problems. */ - int latest_tested = 17; + int latest_tested = 19; int ordinal_9 = SourceVersion.RELEASE_9.ordinal(); int ordinal_latest = latest_tested - 9 + ordinal_9; From 5f34cef0092324ad192057f94ae6ce6fe67c34ff Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 29 May 2023 21:56:07 -0400 Subject: [PATCH 051/334] Overlooked copyright year. --- .../postgresql/pljava/annotation/processing/DDRProcessor.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pljava-api/src/main/java/org/postgresql/pljava/annotation/processing/DDRProcessor.java b/pljava-api/src/main/java/org/postgresql/pljava/annotation/processing/DDRProcessor.java index 38046ae49..1d14ca668 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/annotation/processing/DDRProcessor.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/annotation/processing/DDRProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2022 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 @@ -9,6 +9,7 @@ * Contributors: * Tada AB * Purdue University + * Chapman Flack */ package org.postgresql.pljava.annotation.processing; From 1a32d91d90f276b7e6e9bc2204ffe9c3d2a2757f Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 5 Jun 2023 18:15:33 -0400 Subject: [PATCH 052/334] Unrelated message typo fixed in passing --- .../postgresql/pljava/annotation/processing/DDRProcessor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pljava-api/src/main/java/org/postgresql/pljava/annotation/processing/DDRProcessor.java b/pljava-api/src/main/java/org/postgresql/pljava/annotation/processing/DDRProcessor.java index 1d14ca668..50d6adfb4 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/annotation/processing/DDRProcessor.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/annotation/processing/DDRProcessor.java @@ -2287,7 +2287,7 @@ else if ( MethodShape.PROVIDER == shape ) case ONEOUT | OTHERTYPE: msg( Kind.ERROR, func, "no type= allowed here (the out parameter " + - "declares its own type"); + "declares its own type)"); return; case MOREOUT | RECORDTYPE: case MOREOUT | OTHERTYPE: From 4c420b78540f179473fd5a02708345e126176436 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 5 Jun 2023 18:07:53 -0400 Subject: [PATCH 053/334] Relax the check of function's enclosing type The SQL/JRT standard has always just said class, but there is no technical obstacle to using static methods on an interface, so relax the annotation-processing-time check that was preventing that. Addresses #426. --- .../annotation/processing/DDRProcessor.java | 34 ++++++++++++------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/pljava-api/src/main/java/org/postgresql/pljava/annotation/processing/DDRProcessor.java b/pljava-api/src/main/java/org/postgresql/pljava/annotation/processing/DDRProcessor.java index 50d6adfb4..3a479115d 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/annotation/processing/DDRProcessor.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/annotation/processing/DDRProcessor.java @@ -1089,16 +1089,29 @@ void processFunction( Element e) for ( Element ee = e; null != ( ee = ee.getEnclosingElement() ); ) { - if ( ElementKind.CLASS.equals( ee.getKind()) ) + ElementKind ek = ee.getKind(); + switch ( ek ) { - if ( ! ee.getModifiers().contains( Modifier.PUBLIC) ) - msg( Kind.ERROR, ee, - "A PL/Java function must not have a non-public " + - "enclosing class"); - if ( ((TypeElement)ee).getNestingKind().equals( - NestingKind.TOP_LEVEL) ) - break; + case CLASS: + case INTERFACE: + break; + default: + msg( Kind.ERROR, ee, + "A PL/Java function must not have an enclosing " + ek); + return; } + + // It's a class or interface, represented by TypeElement + TypeElement te = (TypeElement)ee; + mods = ee.getModifiers(); + + if ( ! mods.contains( Modifier.PUBLIC) ) + msg( Kind.ERROR, ee, + "A PL/Java function must not have a non-public " + + "enclosing class"); + + if ( ! te.getNestingKind().isNested() ) + break; // no need to look above top-level class } FunctionImpl f = getSnippet( e, FunctionImpl.class, () -> @@ -2489,10 +2502,7 @@ String makeAS() if ( ! ( complexViaInOut || setof || trigger ) ) sb.append( typu.erasure( func.getReturnType())).append( '='); Element e = func.getEnclosingElement(); - if ( ! e.getKind().equals( ElementKind.CLASS) ) - msg( Kind.ERROR, func, - "Somehow this method got enclosed by something other " + - "than a class"); + // e was earlier checked and ensured to be a class or interface sb.append( e.toString()).append( '.'); sb.append( trigger ? func.getSimpleName() : func.toString()); return sb.toString(); From a1eb0b2a583520ab8504874117396276a18a30b9 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 5 Jun 2023 19:39:33 -0400 Subject: [PATCH 054/334] Example: function on interface or nested type --- .../example/annotation/OnInterface.java | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/OnInterface.java diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/OnInterface.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/OnInterface.java new file mode 100644 index 000000000..e98a8cfb8 --- /dev/null +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/OnInterface.java @@ -0,0 +1,69 @@ +/* + * 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; + +/** + * Illustrates PL/Java functions on an interface instead of a class. + *

    + * The SQL/JRT standard has always just said "class", but there is no technical + * obstacle to permitting a PL/Java function to be a static interface method, so + * that earlier restriction has been relaxed. + */ +public interface OnInterface +{ + /** + * Returns the answer. + */ + @Function(schema = "javatest") + static int answer() + { + return 42; + } + + interface A + { + /** + * Again the answer. + */ + @Function(schema = "javatest") + static int nestedAnswer() + { + return 42; + } + } + + class B + { + /** + * Still the answer. + */ + @Function(schema = "javatest") + public static int nestedClassAnswer() + { + return 42; + } + + public static class C + { + /** + * That answer again. + */ + @Function(schema = "javatest") + public static int moreNestedAnswer() + { + return 42; + } + } + } +} From 2534c98520728e04aa9c4d73cd339c65f87a1275 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 5 Jun 2023 18:48:21 -0400 Subject: [PATCH 055/334] Who ever noticed 1-letter identifiers don't work? The runtime parsing of function AS strings, since 1.6, has been requiring a Java identifier to be a single javaIdentifierStart plus one or more javaIdentifierPart, which of course should be zero or more. The result is an unexpected error if a one-letter identifier appears as a package-name component or a class or method name. Addresses #438. --- .../src/main/java/org/postgresql/pljava/internal/Function.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 0c7de5b01..1b8793edb 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/Function.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/Function.java @@ -1839,7 +1839,7 @@ private static String getAS(ResultSet procTup) throws SQLException * Uncompiled pattern to recognize a Java identifier. */ private static final String javaIdentifier = String.format( - "\\p{%1$sStart}\\p{%1sPart}++", "javaJavaIdentifier" + "\\p{%1$sStart}\\p{%1sPart}*+", "javaJavaIdentifier" ); /** From b7eff644901cfafc9fcf20741e3ff1d687a9259e Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 5 Jun 2023 19:38:41 -0400 Subject: [PATCH 056/334] Emit nested type names properly spelled --- .../pljava/annotation/processing/DDRProcessor.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pljava-api/src/main/java/org/postgresql/pljava/annotation/processing/DDRProcessor.java b/pljava-api/src/main/java/org/postgresql/pljava/annotation/processing/DDRProcessor.java index 3a479115d..aeb3139e4 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/annotation/processing/DDRProcessor.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/annotation/processing/DDRProcessor.java @@ -2503,7 +2503,7 @@ String makeAS() sb.append( typu.erasure( func.getReturnType())).append( '='); Element e = func.getEnclosingElement(); // e was earlier checked and ensured to be a class or interface - sb.append( e.toString()).append( '.'); + sb.append( elmu.getBinaryName((TypeElement)e)).append( '.'); sb.append( trigger ? func.getSimpleName() : func.toString()); return sb.toString(); } @@ -2833,7 +2833,7 @@ public String[] deployStrings() return deployStrings( qnameFrom(name(), schema()), null, // parameter iterable unused in appendParams below - "UDT[" + te + "] " + id.name(), + "UDT[" + elmu.getBinaryName(te) + "] " + id.name(), comment()); } @@ -3053,7 +3053,7 @@ public String[] deployStrings() } al.add( "SELECT sqlj.add_type_mapping(" + DDRWriter.eQuote( qname.toString()) + ", " + - DDRWriter.eQuote( tclass.toString()) + ')'); + DDRWriter.eQuote( elmu.getBinaryName(tclass)) + ')'); addComment( al); return al.toArray( new String [ al.size() ]); } From 8e3b6c96f197529ad932881a62464f6d7d9e3629 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 5 Jun 2023 21:46:14 -0400 Subject: [PATCH 057/334] Don't over-narrow permissions fetching jar URL If an HTTP URL is used, either a SocketPermission or a URLPermission is supposed to work, but HttpURLConnection's getPermission() method predates URLPermission and just returns the other one. That can lead to an empty set of effective permissions if the policy granted one and the set is intersected with the other. Also, for HttpURLConnections, install an Authenticator that will try to find userinfo in the requesting URL, if the remote end sends a password authentication challenge. Beware, though, that sqlj.jar_repository has a jarorigin column, and URLs used to install jars show up there! Both of these items are from issue #425. --- .../pljava/management/Commands.java | 78 ++++++++++++++++++- 1 file changed, 76 insertions(+), 2 deletions(-) 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 519051f5b..2170e240f 100644 --- a/pljava/src/main/java/org/postgresql/pljava/management/Commands.java +++ b/pljava/src/main/java/org/postgresql/pljava/management/Commands.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2021 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 @@ -18,12 +18,18 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.net.Authenticator; +import java.net.HttpURLConnection; +import java.net.PasswordAuthentication; +import java.net.URI; import java.net.URL; import java.net.URLConnection; +import java.net.URLPermission; import java.nio.ByteBuffer; import static java.nio.charset.StandardCharsets.UTF_8; import java.nio.charset.CharsetDecoder; import java.nio.charset.CharacterCodingException; +import java.security.Permission; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; @@ -36,6 +42,7 @@ import java.sql.Statement; import java.text.ParseException; import java.util.ArrayList; +import static java.util.Arrays.fill; import java.util.jar.Attributes; import java.util.jar.JarEntry; import java.util.jar.JarInputStream; @@ -387,6 +394,48 @@ public class Commands private static final Identifier.Simple s_public_schema = Identifier.Simple.fromCatalog("public"); + /** + * An {@link Authenticator} that will try the {@code userinfo} of the + * requesting URL if present. + *

    + * Beware that such URLs will appear in + * {@code sqlj.jar_repository.jarorigin} if used to install a jar! + */ + private static class EmbeddedPwdAuthenticator extends Authenticator + { + private EmbeddedPwdAuthenticator() { } + + static final EmbeddedPwdAuthenticator INSTANCE = + new EmbeddedPwdAuthenticator(); + + @Override + protected PasswordAuthentication getPasswordAuthentication() + { + String userinfo = + URI.create(getRequestingURL().toString()).getUserInfo(); + if ( null == userinfo ) + return null; + int len = userinfo.length(); + int uend = userinfo.indexOf(':'); + int pstart; + if ( -1 == uend ) + uend = pstart = len; + else + pstart = 1 + uend; + String u = userinfo.substring(0, uend); + char[] p = new char[len - pstart]; + try + { + userinfo.getChars(pstart, len, p, 0); + return new PasswordAuthentication(u, p); + } + finally + { + fill(p, '\245'); // PasswordAuthentication clones it + } + } + } + /** * Reads the jar found at the specified URL and stores the entries in the * jar_entry table. @@ -402,6 +451,31 @@ static void addClassImages(int jarId, String urlString) URL url = new URL(urlString); URLConnection uc = url.openConnection(); long[] sz = new long[1]; + Permission[] least = { uc.getPermission() }; + + if ( uc instanceof HttpURLConnection ) + { + /* + * Augment what uc returned as the least privilege set needed + * to connect. HttpURLConnection's getPermission method is older + * than URLPermission, and it only returns a SocketPermission. + * Set up 'least' to include both, so as not to end up with an + * empty permission set when 'least' includes one and the policy + * granted the other. + */ + least = new Permission[] { + least[0], + new URLPermission(urlString, "GET") + }; + + /* + * In case authentication is needed, set an Authenticator that + * will try userinfo from the URL if present. (Beware that jar + * origin URLs are stored in sqlj.jar_repository.jarorigin!) + */ + ((HttpURLConnection)uc).setAuthenticator( + EmbeddedPwdAuthenticator.INSTANCE); + } /* * Do uc.connect() with PL/Java implementation's permissions, but @@ -413,7 +487,7 @@ static void addClassImages(int jarId, String urlString) uc.connect(); sz[0] = uc.getContentLengthLong(); return uc.getInputStream(); - }, null, uc.getPermission()) + }, null, least) ) { addClassImages(jarId, urlStream, sz[0]); From cc5c62d8f574a75e7d1ab825eb8cc30143b4f8e0 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 6 Jun 2023 14:29:42 -0400 Subject: [PATCH 058/334] Node.installSaxonAndExamplesAndPath thinko The method is documented to install Saxon, add it to the classpath, then install the examples, and then add it so the classpath had both. But the path was ending up with only examples. --- pljava-packaging/src/main/java/Node.java | 48 +++++++++++++++++++++--- 1 file changed, 42 insertions(+), 6 deletions(-) diff --git a/pljava-packaging/src/main/java/Node.java b/pljava-packaging/src/main/java/Node.java index fa4e0b007..dd658f9fa 100644 --- a/pljava-packaging/src/main/java/Node.java +++ b/pljava-packaging/src/main/java/Node.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015-2020 Tada AB and other contributors, as listed below. + * Copyright (c) 2015-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 @@ -1102,6 +1102,42 @@ public static Stream setClasspath( return q(ps, ps::execute); } + /** + * Append a jar to a schema's class path if not already included. + * @return a {@link #q(Statement,Callable) result stream} that includes, on + * success, a one-column {@code void} result set with a single row if the + * jar was added to the path, and no rows if the jar was already included. + */ + public static Stream appendClasspathIf( + Connection c, String schema, String jarName) + throws Exception + { + PreparedStatement ps = c.prepareStatement( + "SELECT" + + " sqlj.set_classpath(" + + " schema," + + " pg_catalog.concat_ws(" + + " ':'," + + " VARIADIC oldpath OPERATOR(pg_catalog.||) ARRAY[jar]" + + " )" + + " )" + + "FROM" + + " (VALUES (?, ?)) AS p(schema, jar)," + + " COALESCE(" + + " pg_catalog.regexp_split_to_array(" + + " sqlj.get_classpath(schema)," + + " ':'" + + " )," + + " CAST (ARRAY[] AS pg_catalog.text[])" + + " ) AS t(oldpath)" + + "WHERE" + + " jar OPERATOR(pg_catalog.<>) ALL (oldpath)" + ); + ps.setString(1, schema); + ps.setString(2, jarName); + return q(ps, ps::execute); + } + /** * Execute some arbitrary SQL * @return a {@link #q(Statement,Callable) result stream} from executing @@ -1551,8 +1587,8 @@ public static Stream installExamples(Connection c, boolean deploy) } /** - * Install the examples jar, under the name {@code examples}, and place it - * on the class path for schema {@code public}. + * Install the examples jar, under the name {@code examples}, and append it + * to the class path for schema {@code public}. *

    * The return of a concatenated result stream from two consecutive * statements might be likely to fail in cases where the first @@ -1567,7 +1603,7 @@ public static Stream installExamplesAndPath( throws Exception { Stream s1 = installExamples(c, deploy); - Stream s2 = setClasspath(c, "public", "examples"); + Stream s2 = appendClasspathIf(c, "public", "examples"); return Stream.concat(s1, s2); } @@ -1589,7 +1625,7 @@ public static Stream installSaxon( } /** - * Install a Saxon jar under the name {@code saxon}, and place it on the + * Install a Saxon jar under the name {@code saxon}, and append it to the * class path for schema {@code public}. * @return a combined {@link #q(Statement,Callable) result stream} from * executing the statements @@ -1599,7 +1635,7 @@ public static Stream installSaxonAndPath( throws Exception { Stream s1 = installSaxon(c, repo, version); - Stream s2 = setClasspath(c, "public", "saxon"); + Stream s2 = appendClasspathIf(c, "public", "saxon"); return Stream.concat(s1, s2); } From c732326574e8a5aa8bc6d17e018076ef39e30501 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 6 Jun 2023 14:32:25 -0400 Subject: [PATCH 059/334] Tweaks to CI scripts in passing May as well have them add the -Djava.security.manager=allow for Java > 17. Add a ; in one, because while jshell is normally tolerant of missing ones, it is less so when its input is from terminal paste ... and it's convenient to copy/paste from the CI scripts when testing. --- .github/workflows/ci-runnerpg.yml | 5 ++++- appveyor.yml | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci-runnerpg.yml b/.github/workflows/ci-runnerpg.yml index 9155acd60..be649a755 100644 --- a/.github/workflows/ci-runnerpg.yml +++ b/.github/workflows/ci-runnerpg.yml @@ -215,7 +215,7 @@ jobs: String javaHome = System.getProperty("java.home"); - Path javaLibDir = get(javaHome, s_isWindows ? "bin" : "lib") + Path javaLibDir = get(javaHome, s_isWindows ? "bin" : "lib"); Path libjvm = ( "Mac OS X".equals(System.getProperty("os.name")) @@ -228,6 +228,9 @@ jobs: String vmopts = "-enableassertions:org.postgresql.pljava... -Xcheck:jni"; + if ( 17 < Runtime.version().feature() ) + vmopts += " -Djava.security.manager=allow"; + Node n1 = Node.get_new_node("TestNode1"); if ( s_isWindows ) diff --git a/appveyor.yml b/appveyor.yml index 4f645a088..3ca833317 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -106,6 +106,9 @@ test_script: String vmopts = "-enableassertions:org.postgresql.pljava... -Xcheck:jni"; + if ( 17 < Runtime.version().feature() ) + vmopts += " -Djava.security.manager=allow"; + Node n1 = Node.get_new_node("TestNode1"); n1.use_pg_ctl(true); From 16f5802467fa6ebb8c9f42840a64e49070d7b1bc Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 6 Jun 2023 20:42:33 -0400 Subject: [PATCH 060/334] Test install_jar with an http URL --- .github/workflows/ci-runnerpg.yml | 101 +++++++++++++++++++++++++++++- 1 file changed, 100 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci-runnerpg.yml b/.github/workflows/ci-runnerpg.yml index be649a755..45b05f44d 100644 --- a/.github/workflows/ci-runnerpg.yml +++ b/.github/workflows/ci-runnerpg.yml @@ -192,7 +192,7 @@ jobs: -execution local \ "-J--class-path=$packageJar$pathSep$jdbcJar" \ "--class-path=$packageJar" \ - "-J--add-modules=java.sql.rowset" \ + "-J--add-modules=java.sql.rowset,jdk.httpserver" \ "-J-Dpgconfig=$pgConfig" \ "-J-Dcom.impossibl.shadow.io.netty.noUnsafe=true" \ "-J-DmavenRepo=$mavenRepo" \ @@ -212,6 +212,20 @@ jobs: import static org.postgresql.pljava.packaging.Node.stateMachine; import static org.postgresql.pljava.packaging.Node.isVoidResultSet; import static org.postgresql.pljava.packaging.Node.s_isWindows; + /* + * Imports that will be needed to serve a jar file over http + * when the time comes for testing that. + */ + import static java.nio.charset.StandardCharsets.UTF_8; + import java.util.jar.Attributes; + import java.util.jar.Manifest; + import java.util.jar.JarOutputStream; + import java.util.zip.ZipEntry; + import com.sun.net.httpserver.BasicAuthenticator; + import com.sun.net.httpserver.HttpContext; + import com.sun.net.httpserver.HttpExchange; + import com.sun.net.httpserver.HttpHandler; + import com.sun.net.httpserver.HttpServer; String javaHome = System.getProperty("java.home"); @@ -394,6 +408,91 @@ jobs: // done with connection c2 } + /* + * Spin up an http server with a little jar file to serve, and test + * that install_jar works with an http: url. + * + * First make a little jar empty but for a deployment descriptor. + */ + String ddrName = "foo.ddr"; + Attributes a = new Attributes(); + a.putValue("SQLJDeploymentDescriptor", "TRUE"); + Manifest m = new Manifest(); + m.getEntries().put(ddrName, a); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + JarOutputStream jos = new JarOutputStream(baos, m); + jos.putNextEntry(new ZipEntry(ddrName)); + jos.write( + ( + "SQLActions[]={\n\"BEGIN INSTALL\n" + + "SELECT javatest.logmessage('INFO'," + + " 'jar installed from http');\n" + + "END INSTALL\",\n\"BEGIN REMOVE\n" + + "END REMOVE\"\n}\n" + ).getBytes(UTF_8) + ); + jos.closeEntry(); + jos.close(); + byte[] jar = baos.toByteArray(); + + /* + * Now an http server. + */ + HttpServer hs = + HttpServer.create(new InetSocketAddress("localhost", 0), 0); + + try ( + Connection c2 = n1.connect(); + AutoCloseable t = ((Supplier)() -> + { + hs.start(); + return () -> hs.stop(0); + } + ).get() + ) + { + InetSocketAddress addr = hs.getAddress(); + URL u = new URI( + "http", null, addr.getHostString(), addr.getPort(), + "/foo.jar", null, null + ).toURL(); + + HttpContext hc = hs.createContext( + u.getPath(), + new HttpHandler() + { + @Override + public void handle(HttpExchange t) throws IOException + { + try ( InputStream is = t.getRequestBody() ) { + is.readAllBytes(); + } + t.getResponseHeaders().add( + "Content-Type", "application/java-archive"); + t.sendResponseHeaders(200, jar.length); + try ( OutputStream os = t.getResponseBody() ) { + os.write(jar); + } + } + } + ); + + succeeding &= stateMachine( + "install a jar over http", + null, + + Node.installJar(c2, u.toString(), "foo", true) + .flatMap(Node::semiFlattenDiagnostics) + .peek(Node::peek), + + (o,p,q) -> isDiagnostic(o, Set.of("error", "warning")) ? 1 : -2, + (o,p,q) -> isVoidResultSet(o, 1, 1) ? 3 : false, + (o,p,q) -> null == o + ); + + // done with connection c2 again, and the http server + } + /* * Also confirm that the generated undeploy actions work. */ From 28255017e318f51e6df82456b97ceeffc23de92e Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 6 Jun 2023 21:57:11 -0400 Subject: [PATCH 061/334] Factor out useTrialPolicy in CI script --- .github/workflows/ci-runnerpg.yml | 69 ++++++++++++++++++++----------- 1 file changed, 44 insertions(+), 25 deletions(-) diff --git a/.github/workflows/ci-runnerpg.yml b/.github/workflows/ci-runnerpg.yml index 45b05f44d..e0dc000ac 100644 --- a/.github/workflows/ci-runnerpg.yml +++ b/.github/workflows/ci-runnerpg.yml @@ -276,6 +276,42 @@ jobs: return true; } + /* + * Write a trial policy into a temporary file in n's data_dir, + * and set pljava.vmoptions accordingly over connection c. + * Returns the 'succeeding' flag from the state machine looking + * at the command results. + */ + boolean useTrialPolicy(Node n, Connection c, List contents) + throws Exception + { + Path trialPolicy = + createTempFile(n.data_dir().getParent(), "trial", "policy"); + + write(trialPolicy, contents); + + PreparedStatement setVmOpts = c.prepareStatement( + "SELECT null::pg_catalog.void" + + " FROM pg_catalog.set_config('pljava.vmoptions', ?, false)" + ); + + setVmOpts.setString(1, vmopts + + " -Dorg.postgresql.pljava.policy.trial=" + trialPolicy.toUri()); + + return stateMachine( + "change pljava.vmoptions", + null, + + q(setVmOpts, setVmOpts::execute) + .flatMap(Node::semiFlattenDiagnostics) + .peek(Node::peek), + + (o,p,q) -> isDiagnostic(o, Set.of("error")) ? 1 : -2, + (o,p,q) -> isVoidResultSet(o, 1, 1) ? 3 : false, + (o,p,q) -> null == o + ); + } + try ( AutoCloseable t1 = n1.initialized_cluster(); AutoCloseable t2 = n1.started_server(Map.of( @@ -351,37 +387,13 @@ jobs: */ try ( Connection c2 = n1.connect() ) { - Path trialPolicy = - createTempFile(n1.data_dir().getParent(), "trial", "policy"); - - write(trialPolicy, List.of( + succeeding &= useTrialPolicy(n1, c2, List.of( "grant {", " permission", " org.postgresql.pljava.policy.TrialPolicy$Permission;", "};" )); - PreparedStatement setVmOpts = c2.prepareStatement( - "SELECT null::pg_catalog.void" + - " FROM pg_catalog.set_config('pljava.vmoptions', ?, false)" - ); - - setVmOpts.setString(1, vmopts + - " -Dorg.postgresql.pljava.policy.trial=" + trialPolicy.toUri()); - - succeeding &= stateMachine( - "change pljava.vmoptions", - null, - - q(setVmOpts, setVmOpts::execute) - .flatMap(Node::semiFlattenDiagnostics) - .peek(Node::peek), - - (o,p,q) -> isDiagnostic(o, Set.of("error")) ? 1 : -2, - (o,p,q) -> isVoidResultSet(o, 1, 1) ? 3 : false, - (o,p,q) -> null == o - ); - PreparedStatement tryForbiddenRead = c2.prepareStatement( "SELECT" + " CASE WHEN javatest.java_getsystemproperty('java.home')" + @@ -477,6 +489,13 @@ jobs: } ); + succeeding &= useTrialPolicy(n1, c2, List.of( + "grant codebase \"${org.postgresql.pljava.codesource}\" {", + " permission", + " java.net.URLPermission \"http:*\", \"GET\";", + "};" + )); + succeeding &= stateMachine( "install a jar over http", null, From 5b1dbecfa83fc06becd45c62cdda80bdf42a1a01 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 6 Jun 2023 22:11:53 -0400 Subject: [PATCH 062/334] DDR parser dislikes an empty action group It also has a typo in an error message. --- .github/workflows/ci-runnerpg.yml | 2 ++ .../pljava/management/SQLDeploymentDescriptor.java | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci-runnerpg.yml b/.github/workflows/ci-runnerpg.yml index e0dc000ac..cc2d16076 100644 --- a/.github/workflows/ci-runnerpg.yml +++ b/.github/workflows/ci-runnerpg.yml @@ -440,6 +440,8 @@ jobs: "SELECT javatest.logmessage('INFO'," + " 'jar installed from http');\n" + "END INSTALL\",\n\"BEGIN REMOVE\n" + + "BEGIN dummy\n" + + "END dummy;\n" + "END REMOVE\"\n}\n" ).getBytes(UTF_8) ); diff --git a/pljava/src/main/java/org/postgresql/pljava/management/SQLDeploymentDescriptor.java b/pljava/src/main/java/org/postgresql/pljava/management/SQLDeploymentDescriptor.java index 85c1c7ad0..168ce5d68 100644 --- a/pljava/src/main/java/org/postgresql/pljava/management/SQLDeploymentDescriptor.java +++ b/pljava/src/main/java/org/postgresql/pljava/management/SQLDeploymentDescriptor.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2016 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 @@ -332,7 +332,7 @@ else if(inQuote == c) default: if(inQuote == 0 && Character.isWhitespace((char)c)) { - // Change multiple whitespace into one singe space. + // Change multiple whitespace into one single space. // m_buffer.append(' '); c = this.skipWhite(); @@ -345,7 +345,7 @@ else if(inQuote == c) } } if(inQuote != 0) - throw this.parseError("Untermintated " + (char)inQuote + + throw this.parseError("Unterminated " + (char)inQuote + " starting at position " + startQuotePos); throw this.parseError("Unexpected EOF. Expecting ';' to end command"); From 768e44e99dccb86e49b772992e3a408e4d41f582 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 7 Jun 2023 09:19:52 -0400 Subject: [PATCH 063/334] Add Basic authentication to http test --- .github/workflows/ci-runnerpg.yml | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci-runnerpg.yml b/.github/workflows/ci-runnerpg.yml index cc2d16076..82562c40b 100644 --- a/.github/workflows/ci-runnerpg.yml +++ b/.github/workflows/ci-runnerpg.yml @@ -466,8 +466,11 @@ jobs: ) { InetSocketAddress addr = hs.getAddress(); + + String id = "bar", pw = "baz"; + URL u = new URI( - "http", null, addr.getHostString(), addr.getPort(), + "http", id+':'+pw, addr.getHostString(), addr.getPort(), "/foo.jar", null, null ).toURL(); @@ -491,6 +494,17 @@ jobs: } ); + hc.setAuthenticator( + new BasicAuthenticator("CI realm", UTF_8) + { + @Override + public boolean checkCredentials(String c_id, String c_pw) + { + return id.equals(c_id) && pw.equals(c_pw); + } + } + ); + succeeding &= useTrialPolicy(n1, c2, List.of( "grant codebase \"${org.postgresql.pljava.codesource}\" {", " permission", From b98c40cce861ae85246f1feb740577d11b11c06c Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 7 Jun 2023 09:48:21 -0400 Subject: [PATCH 064/334] Duplicate CI changes into Appveyor script The time is ripe to factor out a common jshell script for the different CI scripts to refer to. But that feels like a different patch. --- appveyor.yml | 182 ++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 158 insertions(+), 24 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 3ca833317..e48a35f4c 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -99,6 +99,20 @@ test_script: import static org.postgresql.pljava.packaging.Node.q; import static org.postgresql.pljava.packaging.Node.stateMachine; import static org.postgresql.pljava.packaging.Node.isVoidResultSet; + /* + * Imports that will be needed to serve a jar file over http + * when the time comes for testing that. + */ + import static java.nio.charset.StandardCharsets.UTF_8; + import java.util.jar.Attributes; + import java.util.jar.Manifest; + import java.util.jar.JarOutputStream; + import java.util.zip.ZipEntry; + import com.sun.net.httpserver.BasicAuthenticator; + import com.sun.net.httpserver.HttpContext; + import com.sun.net.httpserver.HttpExchange; + import com.sun.net.httpserver.HttpHandler; + import com.sun.net.httpserver.HttpServer; System.setErr(System.out); // PowerShell makes a mess of stderr output @@ -138,6 +152,42 @@ test_script: return true; } + /* + * Write a trial policy into a temporary file in n's data_dir, + * and set pljava.vmoptions accordingly over connection c. + * Returns the 'succeeding' flag from the state machine looking + * at the command results. + */ + boolean useTrialPolicy(Node n, Connection c, List contents) + throws Exception + { + Path trialPolicy = + createTempFile(n.data_dir().getParent(), "trial", "policy"); + + write(trialPolicy, contents); + + PreparedStatement setVmOpts = c.prepareStatement( + "SELECT null::pg_catalog.void" + + " FROM pg_catalog.set_config('pljava.vmoptions', ?, false)" + ); + + setVmOpts.setString(1, vmopts + + " -Dorg.postgresql.pljava.policy.trial=" + trialPolicy.toUri()); + + return stateMachine( + "change pljava.vmoptions", + null, + + q(setVmOpts, setVmOpts::execute) + .flatMap(Node::semiFlattenDiagnostics) + .peek(Node::peek), + + (o,p,q) -> isDiagnostic(o, Set.of("error")) ? 1 : -2, + (o,p,q) -> isVoidResultSet(o, 1, 1) ? 3 : false, + (o,p,q) -> null == o + ); + } + try ( AutoCloseable t1 = n1.initialized_cluster( p->p.redirectErrorStream(true)); @@ -213,53 +263,136 @@ test_script: */ try ( Connection c2 = n1.connect() ) { - Path trialPolicy = - createTempFile(n1.data_dir().getParent(), "trial", "policy"); - - write(trialPolicy, List.of( + succeeding &= useTrialPolicy(n1, c2, List.of( "grant {", " permission", " org.postgresql.pljava.policy.TrialPolicy$Permission;", "};" )); - PreparedStatement setVmOpts = c2.prepareStatement( - "SELECT null::pg_catalog.void" + - " FROM pg_catalog.set_config('pljava.vmoptions', ?, false)" + PreparedStatement tryForbiddenRead = c2.prepareStatement( + "SELECT" + + " CASE WHEN javatest.java_getsystemproperty('java.home')" + + " OPERATOR(pg_catalog.=) ?" + + " THEN javatest.logmessage('INFO', 'trial policy test ok')" + + " ELSE javatest.logmessage('WARNING', 'trial policy test ng')" + + " END" ); - setVmOpts.setString(1, vmopts + - " -Dorg.postgresql.pljava.policy.trial=" + trialPolicy.toUri()); + tryForbiddenRead.setString(1, System.getProperty("java.home")); succeeding &= stateMachine( - "change pljava.vmoptions", + "try to read a forbidden property", null, - q(setVmOpts, setVmOpts::execute) + q(tryForbiddenRead, tryForbiddenRead::execute) .flatMap(Node::semiFlattenDiagnostics) .peek(Node::peek), - (o,p,q) -> isDiagnostic(o, Set.of("error")) ? 1 : -2, + (o,p,q) -> isDiagnostic(o, Set.of("error", "warning")) ? 1 : -2, (o,p,q) -> isVoidResultSet(o, 1, 1) ? 3 : false, (o,p,q) -> null == o ); + // done with connection c2 + } - PreparedStatement tryForbiddenRead = c2.prepareStatement( - "SELECT" + - " CASE WHEN javatest.java_getsystemproperty('java.home')" + - " OPERATOR(pg_catalog.=) ?" + - " THEN javatest.logmessage('INFO', 'trial policy test ok')" + - " ELSE javatest.logmessage('WARNING', 'trial policy test ng')" + - " END" + /* + * Spin up an http server with a little jar file to serve, and test + * that install_jar works with an http: url. + * + * First make a little jar empty but for a deployment descriptor. + */ + String ddrName = "foo.ddr"; + Attributes a = new Attributes(); + a.putValue("SQLJDeploymentDescriptor", "TRUE"); + Manifest m = new Manifest(); + m.getEntries().put(ddrName, a); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + JarOutputStream jos = new JarOutputStream(baos, m); + jos.putNextEntry(new ZipEntry(ddrName)); + jos.write( + ( + "SQLActions[]={\n\"BEGIN INSTALL\n" + + "SELECT javatest.logmessage('INFO'," + + " 'jar installed from http');\n" + + "END INSTALL\",\n\"BEGIN REMOVE\n" + + "BEGIN dummy\n" + + "END dummy;\n" + + "END REMOVE\"\n}\n" + ).getBytes(UTF_8) + ); + jos.closeEntry(); + jos.close(); + byte[] jar = baos.toByteArray(); + + /* + * Now an http server. + */ + HttpServer hs = + HttpServer.create(new InetSocketAddress("localhost", 0), 0); + + try ( + Connection c2 = n1.connect(); + AutoCloseable t = ((Supplier)() -> + { + hs.start(); + return () -> hs.stop(0); + } + ).get() + ) + { + InetSocketAddress addr = hs.getAddress(); + + String id = "bar", pw = "baz"; + + URL u = new URI( + "http", id+':'+pw, addr.getHostString(), addr.getPort(), + "/foo.jar", null, null + ).toURL(); + + HttpContext hc = hs.createContext( + u.getPath(), + new HttpHandler() + { + @Override + public void handle(HttpExchange t) throws IOException + { + try ( InputStream is = t.getRequestBody() ) { + is.readAllBytes(); + } + t.getResponseHeaders().add( + "Content-Type", "application/java-archive"); + t.sendResponseHeaders(200, jar.length); + try ( OutputStream os = t.getResponseBody() ) { + os.write(jar); + } + } + } ); - tryForbiddenRead.setString(1, System.getProperty("java.home")); + hc.setAuthenticator( + new BasicAuthenticator("CI realm", UTF_8) + { + @Override + public boolean checkCredentials(String c_id, String c_pw) + { + return id.equals(c_id) && pw.equals(c_pw); + } + } + ); + + succeeding &= useTrialPolicy(n1, c2, List.of( + "grant codebase \"${org.postgresql.pljava.codesource}\" {", + " permission", + " java.net.URLPermission \"http:*\", \"GET\";", + "};" + )); succeeding &= stateMachine( - "try to read a forbidden property", + "install a jar over http", null, - q(tryForbiddenRead, tryForbiddenRead::execute) + Node.installJar(c2, u.toString(), "foo", true) .flatMap(Node::semiFlattenDiagnostics) .peek(Node::peek), @@ -267,7 +400,8 @@ test_script: (o,p,q) -> isVoidResultSet(o, 1, 1) ? 3 : false, (o,p,q) -> null == o ); - // done with connection c2 + + // done with connection c2 again, and the http server } /* @@ -445,7 +579,7 @@ test_script: -execution local ` "-J--class-path=$packageJar;$jdbcJar" ` "--class-path=$packageJar" ` - "-J--add-modules=java.sql.rowset" ` + "-J--add-modules=java.sql.rowset,jdk.httpserver" ` "-J-Dcom.impossibl.shadow.io.netty.noUnsafe=true" ` "-J-Dpgconfig=$pgConfig" ` "-J-DmavenRepo=$mavenRepo" ` From a1ff17bf716c20fe09e3c67975a5c2e28e0d3cf4 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 7 Jun 2023 11:52:47 -0400 Subject: [PATCH 065/334] Bump copyright date. This patch from sincatter addresses #407. --- pljava-so/src/main/c/Backend.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pljava-so/src/main/c/Backend.c b/pljava-so/src/main/c/Backend.c index 9472e9641..287652fd2 100644 --- a/pljava-so/src/main/c/Backend.c +++ b/pljava-so/src/main/c/Backend.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2022 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 From 0311278dd74d85fa7df4e308675bc27aeb997808 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 7 Jun 2023 14:48:30 -0400 Subject: [PATCH 066/334] Fetch the PGDG apt key for GitHub Actions ... thanks to a tip from Rodrigo Alvarado a little tidier than what's on the wiki.postgresql.org Apt page. Also, for now, comment out all the Appveyor tests that have been reliably falling over for some reason of their own. --- .github/workflows/ci-runnerpg.yml | 2 + appveyor.yml | 78 +++++++++++++++---------------- 2 files changed, 41 insertions(+), 39 deletions(-) diff --git a/.github/workflows/ci-runnerpg.yml b/.github/workflows/ci-runnerpg.yml index 9155acd60..e766c6357 100644 --- a/.github/workflows/ci-runnerpg.yml +++ b/.github/workflows/ci-runnerpg.yml @@ -59,6 +59,8 @@ jobs: - name: Obtain PG development files (Ubuntu, PGDG) if: ${{ 'Linux' == runner.os }} run: | + curl https://www.postgresql.org/media/keys/ACCC4CF8.asc | + sudo apt-key add - echo \ deb \ http://apt.postgresql.org/pub/repos/apt \ diff --git a/appveyor.yml b/appveyor.yml index 4f645a088..cd98a1667 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -9,51 +9,51 @@ environment: # - SYS: MINGW # JDK: 10 # PG: 12 - - SYS: MINGW - JDK: 11 - PG: 12 - - SYS: MINGW - JDK: 12 - PG: 12 - - SYS: MINGW - JDK: 13 - PG: 12 - - SYS: MINGW - JDK: 14 - PG: 12 - - SYS: MINGW - JDK: 15 - PG: 12 - - SYS: MSVC - JDK: 15 - PG: 12 - - SYS: MSVC - JDK: 14 - PG: 12 - - SYS: MSVC - JDK: 13 - PG: 12 - - SYS: MSVC - JDK: 12 - PG: 12 - - SYS: MSVC - JDK: 11 - PG: 12 + # - SYS: MINGW + # JDK: 11 + # PG: 12 + # - SYS: MINGW + # JDK: 12 + # PG: 12 + # - SYS: MINGW + # JDK: 13 + # PG: 12 + # - SYS: MINGW + # JDK: 14 + # PG: 12 + # - SYS: MINGW + # JDK: 15 + # PG: 12 + # - SYS: MSVC + # JDK: 15 + # PG: 12 + # - SYS: MSVC + # JDK: 14 + # PG: 12 + # - SYS: MSVC + # JDK: 13 + # PG: 12 + # - SYS: MSVC + # JDK: 12 + # PG: 12 + # - SYS: MSVC + # JDK: 11 + # PG: 12 # - SYS: MSVC # JDK: 10 # PG: 12 # - 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% From 581390889510fc552c2d67c0280400f3dfb72ebc Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 7 Jun 2023 15:00:48 -0400 Subject: [PATCH 067/334] Try another way to hush up app, umm, veyor --- appveyor.yml | 80 +++++++++++++++++++++++++++------------------------- 1 file changed, 41 insertions(+), 39 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index cd98a1667..beb7babca 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,3 +1,5 @@ +only_commits: + message: /appveyor/ image: Visual Studio 2019 environment: APPVEYOR_RDP_PASSWORD: MrRobot@2020 @@ -9,51 +11,51 @@ environment: # - SYS: MINGW # JDK: 10 # PG: 12 - # - SYS: MINGW - # JDK: 11 - # PG: 12 - # - SYS: MINGW - # JDK: 12 - # PG: 12 - # - SYS: MINGW - # JDK: 13 - # PG: 12 - # - SYS: MINGW - # JDK: 14 - # PG: 12 - # - SYS: MINGW - # JDK: 15 - # PG: 12 - # - SYS: MSVC - # JDK: 15 - # PG: 12 - # - SYS: MSVC - # JDK: 14 - # PG: 12 - # - SYS: MSVC - # JDK: 13 - # PG: 12 - # - SYS: MSVC - # JDK: 12 - # PG: 12 - # - SYS: MSVC - # JDK: 11 - # PG: 12 + - SYS: MINGW + JDK: 11 + PG: 12 + - SYS: MINGW + JDK: 12 + PG: 12 + - SYS: MINGW + JDK: 13 + PG: 12 + - SYS: MINGW + JDK: 14 + PG: 12 + - SYS: MINGW + JDK: 15 + PG: 12 + - SYS: MSVC + JDK: 15 + PG: 12 + - SYS: MSVC + JDK: 14 + PG: 12 + - SYS: MSVC + JDK: 13 + PG: 12 + - SYS: MSVC + JDK: 12 + PG: 12 + - SYS: MSVC + JDK: 11 + PG: 12 # - SYS: MSVC # JDK: 10 # PG: 12 # - 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% From 4a168917f94c54d55f007169ffb534f6d95d3e43 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 7 Jun 2023 15:23:54 -0400 Subject: [PATCH 068/334] Add a branches: only: for good measure ... as it would be frustrating to get a quiet commit and then see appveyor spring to life and fail again on a pull request. --- appveyor.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/appveyor.yml b/appveyor.yml index beb7babca..6f800f202 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,5 +1,12 @@ +# These only_commits and branches settings ought to pretty much suppress +# Appveyor, whose runs have all been failing lately because of Maven repository +# connection resets that don't seem reproducible locally. This can be revisited +# later to see if things might be working again. only_commits: message: /appveyor/ +branches: + only: + - appveyor image: Visual Studio 2019 environment: APPVEYOR_RDP_PASSWORD: MrRobot@2020 From 1283031759f7100e0c7642b271f07ed8bd06a667 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 7 Jun 2023 15:53:07 -0400 Subject: [PATCH 069/334] Update Java versions tested Keep 9 because it's the earliest 1.6 supports, 11 because LTS, 12 because some 10/11 weirdness got fixed then, 17 because LTS, and later ones. Copy the pljava.vmoptions tweak from the issue425 branch for Java > 17. --- .github/workflows/ci-runnerpg.yml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci-runnerpg.yml b/.github/workflows/ci-runnerpg.yml index e766c6357..d619778c4 100644 --- a/.github/workflows/ci-runnerpg.yml +++ b/.github/workflows/ci-runnerpg.yml @@ -28,7 +28,7 @@ jobs: # cc: msvc # - os: windows-latest # cc: mingw - java: [9, 11, 12, 13, 14, 15] + java: [9, 11, 12, 17, 18, 19] steps: @@ -59,8 +59,9 @@ jobs: - name: Obtain PG development files (Ubuntu, PGDG) if: ${{ 'Linux' == runner.os }} run: | - curl https://www.postgresql.org/media/keys/ACCC4CF8.asc | - sudo apt-key add - + curl -s -S https://www.postgresql.org/media/keys/ACCC4CF8.asc | + gpg --dearmor | + sudo dd of=/etc/apt/trusted.gpg.d/apt.postgresql.org.gpg echo \ deb \ http://apt.postgresql.org/pub/repos/apt \ @@ -217,7 +218,7 @@ jobs: String javaHome = System.getProperty("java.home"); - Path javaLibDir = get(javaHome, s_isWindows ? "bin" : "lib") + Path javaLibDir = get(javaHome, s_isWindows ? "bin" : "lib"); Path libjvm = ( "Mac OS X".equals(System.getProperty("os.name")) @@ -230,6 +231,9 @@ jobs: String vmopts = "-enableassertions:org.postgresql.pljava... -Xcheck:jni"; + if ( 17 < Runtime.version().feature() ) + vmopts += " -Djava.security.manager=allow"; + Node n1 = Node.get_new_node("TestNode1"); if ( s_isWindows ) From efd1ce5c3b0ec0f28dc7925f748b7805621b57c3 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 7 Jun 2023 16:25:54 -0400 Subject: [PATCH 070/334] The problem may have been a tab in yaml --- .github/workflows/ci-runnerpg.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-runnerpg.yml b/.github/workflows/ci-runnerpg.yml index d619778c4..b7a84cb7e 100644 --- a/.github/workflows/ci-runnerpg.yml +++ b/.github/workflows/ci-runnerpg.yml @@ -61,7 +61,7 @@ jobs: run: | curl -s -S https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | - sudo dd of=/etc/apt/trusted.gpg.d/apt.postgresql.org.gpg + sudo dd of=/etc/apt/trusted.gpg.d/apt.postgresql.org.gpg echo \ deb \ http://apt.postgresql.org/pub/repos/apt \ From e124899bed5197f4b76aaf2298abe0d8f077f070 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 7 Jun 2023 17:25:11 -0400 Subject: [PATCH 071/334] Bump copyright year --- .../src/main/java/org/postgresql/pljava/internal/Function.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 1b8793edb..6015bde7d 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/Function.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/Function.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2022 Tada AB and other contributors, as listed below. + * Copyright (c) 2016-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 From 6cf1b3ab94c5696b2898a0d10a1a54fad22211e2 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 7 Jun 2023 17:48:27 -0400 Subject: [PATCH 072/334] Cherrypick changes to hush app(cough)veyor --- appveyor.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/appveyor.yml b/appveyor.yml index e48a35f4c..dcaae034e 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,3 +1,12 @@ +# These only_commits and branches settings ought to pretty much suppress +# Appveyor, whose runs have all been failing lately because of Maven repository +# connection resets that don't seem reproducible locally. This can be revisited +# later to see if things might be working again. +only_commits: + message: /appveyor/ +branches: + only: + - appveyor image: Visual Studio 2019 environment: APPVEYOR_RDP_PASSWORD: MrRobot@2020 From efe76916f05fc784ba9adf91531c009ed09d3dee Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 7 Jun 2023 18:30:20 -0400 Subject: [PATCH 073/334] Fix CI tests for Java < 14 The BasicAuthenticator constructor that takes a Charset only appeared in Java 14, but hasn't got a Since: in its javadoc. --- .github/workflows/ci-runnerpg.yml | 3 ++- appveyor.yml | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-runnerpg.yml b/.github/workflows/ci-runnerpg.yml index 82562c40b..ee4819c97 100644 --- a/.github/workflows/ci-runnerpg.yml +++ b/.github/workflows/ci-runnerpg.yml @@ -495,7 +495,8 @@ jobs: ); hc.setAuthenticator( - new BasicAuthenticator("CI realm", UTF_8) + new BasicAuthenticator("CI realm") + // ("CI realm", UTF_8) only available in Java 14 or later { @Override public boolean checkCredentials(String c_id, String c_pw) diff --git a/appveyor.yml b/appveyor.yml index dcaae034e..2195e74c4 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -380,7 +380,8 @@ test_script: ); hc.setAuthenticator( - new BasicAuthenticator("CI realm", UTF_8) + new BasicAuthenticator("CI realm") + // ("CI realm", UTF_8) only available in Java 14 or later { @Override public boolean checkCredentials(String c_id, String c_pw) From 9f2e3419c37d07df474c9b9920616a92d2564ccf Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 10 Jun 2023 13:06:42 -0400 Subject: [PATCH 074/334] Document the use of --add-modules PL/Java code loaded with install_jar is treated as unnamed-module, legacy classpath code. Such code is advertised as having access to all readable modules, without having to declare dependencies (as it can't declare dependencies; it would have to be a named module to do that). Ah, but what are "all readable modules"? Only the ones enumerated at JVM startup, using as root modules PL/Java itself and anything named with --add-modules. So that option must be used if any Java modules will be needed beyond the ones PL/Java itself depends on. This added documentation addresses #419. --- src/site/markdown/install/vmoptions.md | 25 ++++++++++++++++ src/site/markdown/use/jpms.md | 40 ++++++++++++++++++++++---- src/site/markdown/use/use.md | 8 ++++++ src/site/markdown/use/variables.md | 6 ++++ 4 files changed, 74 insertions(+), 5 deletions(-) diff --git a/src/site/markdown/install/vmoptions.md b/src/site/markdown/install/vmoptions.md index 87ed91fe1..9a32b203b 100644 --- a/src/site/markdown/install/vmoptions.md +++ b/src/site/markdown/install/vmoptions.md @@ -7,6 +7,31 @@ options are likely to be worth setting. If using [the OpenJ9 JVM][hsj9], be sure to look also at the [VM options specific to OpenJ9][vmoptJ9]. +## Adding to the set of readable modules + +By default, a small set of Java modules (including `java.base`, +`org.postgresql.pljava`, and `java.sql` and its transitive dependencies, +which include `java.xml`) will be readable to any Java code installed with +`install_jar`. + +While those modules may be enough for many uses, other modules are easily added +using `--add-modules` within `pljava.vmoptions`. For example, +`--add-modules=java.net.http,java.rmi` would make the HTTP Client and WebSocket +APIs readable, along with the Remote Method Invocation API. + +For convenience, the module `java.se` simply transitively requires all the +modules that make up the full Java SE API, so `--add-modules=java.se` will make +that full API available to PL/Java code without further thought. The cost, +however, may be that PL/Java uses more memory and starts more slowly than if +only a few needed modules were named. + +Third-party modular code can be made available by adding the modular jars +to `pljava.module_path` (see [configuration variables](../use/variables.html)) +and naming those modules in `--add-modules`. PL/Java currently treats all jars +loaded with `install_jar` as unnamed-module, legacy classpath code. + +For more, see [PL/Java and the Java Platform Module System](../use/jpms.html). + ## Byte order for PL/Java-implemented user-defined types PL/Java is free of byte-order issues except when using its features for building diff --git a/src/site/markdown/use/jpms.md b/src/site/markdown/use/jpms.md index 7c7890a1c..dc635d0d0 100644 --- a/src/site/markdown/use/jpms.md +++ b/src/site/markdown/use/jpms.md @@ -36,10 +36,11 @@ legacy code can be migrated over time: * A jar file containing legacy, non-modular code should be placed on the class path, and is treated as part of an unnamed module that has access - to the exports and opens of any other modules, so it will continue to work - as it did before Java 9. (Even a jar containing Java 9+ modular code - will be treated this way, if found on the class path rather than the - module path.) + to the exports and opens of all other _readable_ modules. Such code will, + therefore, continue to work as it did before Java 9, provided the needed + modules are _readable_, as explained below. (Even a jar containing Java 9+ + modular code will be treated this way, if found on the class path rather + than the module path.) * A jar file can be placed on the module path even if it does not contain an explicit named module. In that case, it becomes an "automatic" module, @@ -55,6 +56,33 @@ that does not include Java module system concepts. Its `sqlj.set_classpath` function manipulates an internal class path, not a module path, and a jar installed with `sqlj.install_jar` behaves as legacy code in an unnamed module. +## Readable versus observable modules + +Using Java's terminology, modules that can be found on the module path are +_observable_. Not all of those are automatically _readable_; the _readable_ +ones in a JVM instance are initially those encountered, at JVM start-up, in +the "recursive enumeration" step of [module resolution][resolution]. + +Recursive enumeration begins with some root modules, and proceeds until all of +the modules on which they (transitively) depend have been added to the readable +set. When PL/Java is launched in a session, PL/Java's own module is a root, +and so the readable modules will include those PL/Java itself depends on, +such as `java.base`, `java.sql`, and the other modules `java.sql` names with +`requires transitive` directives. + +Those modules may be enough for many uses of PL/Java. However, if code for use +in PL/Java will refer to other modules, +[`--add-modules` in `pljava.vmoptions`][addm] can be used to add more roots. +Because of recursive enumeration, it is enough to add just one module, or a few +modules, whose dependencies recursively cover whatever modules will be needed. + +At one extreme for convenience, Java provides a module, `java.se`, that simply +declares dependencies on the other modules that make up the full Java SE API. +Therefore, `--add-modules=java.se` will ensure that any PL/Java code is able to +refer to any of the Java SE API. However, PL/Java instances may use less memory +and start up more quickly if an effort is made to add only modules actually +needed. + ## Configuring the launch-time module path The configuration variable `pljava.module_path` controls the @@ -84,4 +112,6 @@ the usual way. It can be set by adding a `-Djava.class.path=...` in the is simply the jar file pathnames, separated by the platform's path separator character. -[jpms]: http://cr.openjdk.java.net/~mr/jigsaw/spec/ +[jpms]: https://cr.openjdk.java.net/~mr/jigsaw/spec/ +[resolution]: https://docs.oracle.com/javase/9/docs/api/java/lang/module/package-summary.html#resolution +[addm]: ../install/vmoptions.html#Adding_to_the_set_of_readable_modules diff --git a/src/site/markdown/use/use.md b/src/site/markdown/use/use.md index c25bef799..0f486d3b0 100644 --- a/src/site/markdown/use/use.md +++ b/src/site/markdown/use/use.md @@ -34,6 +34,14 @@ Several [configuration variables](variables.html) can affect PL/Java's operation, including some common PostgreSQL variables as well as PL/Java's own. +### Enabling additional Java modules + +By default, PL/Java code can see a small set of Java modules, including +`java.base` and `java.sql` and a few others. To include others, use +[`--add-modules` in `pljava.vmoptions`][addm]. + +[addm]: ../install/vmoptions.html#Adding_to_the_set_of_readable_modules + ## Special topics ### Configuring permissions diff --git a/src/site/markdown/use/variables.md b/src/site/markdown/use/variables.md index 867b847b3..cb465c1fa 100644 --- a/src/site/markdown/use/variables.md +++ b/src/site/markdown/use/variables.md @@ -101,6 +101,11 @@ These PostgreSQL configuration variables can influence PL/Java's operation: PL/Java API jar file and the PL/Java internals jar file. To determine the proper setting, see [finding the files produced by a PL/Java build](../install/locate.html). + + If additional modular jars are added to the module path, + `--add-modules` in [`pljava.vmoptions`][addm] will make them readable by + PL/Java code. + For more on PL/Java's "module path" and "class path", see [PL/Java and the Java Platform Module System](jpms.html). @@ -181,3 +186,4 @@ These PostgreSQL configuration variables can influence PL/Java's operation: [jou]: https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html [vmop]: ../install/vmoptions.html [sqlascii]: charsets.html#Using_PLJava_with_server_encoding_SQL_ASCII +[addm]: ../install/vmoptions.html#Adding_to_the_set_of_readable_modules From b74c56bbb5bd58293dbfb3bc7566016ec15a7f8a Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 10 Jun 2023 17:45:25 -0400 Subject: [PATCH 075/334] Improve exceptions when fetching jar URL Adding the underlying exception to the cause chain won't help much (until there is more progress on the Thoughts on Logging topic, anyway). More useful, for now, will be just including the underlying exception's class name in the new exception's message. Then at least you will see FileNotFoundException for a 404, etc. Manual testing confirms that connections to https URLs will work with no extra ceremony, as long as the server's certificate chains up to a Java-recognized issuer. No such test has been added for CI; too much fuss to spin up a test server with such a certificate. A test could, I suppose, be added to 'install' a nonexistent URL from some well-known https: server and at least confirm the connection is made and 404 is returned, but such use of an unsuspecting server from a CI workflow might be a little antisocial. For debugging an attempt that fails, although it will be hard to get enough useful information into the exception, testing shows that Java's logging at the log_min_messages=debug1 level will include plenty about the certificate chain received, TLS handshake, and request and response headers. --- .../java/org/postgresql/pljava/management/Commands.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 2170e240f..8aa086a7f 100644 --- a/pljava/src/main/java/org/postgresql/pljava/management/Commands.java +++ b/pljava/src/main/java/org/postgresql/pljava/management/Commands.java @@ -495,8 +495,8 @@ static void addClassImages(int jarId, String urlString) } catch(IOException e) { - throw new SQLException("I/O exception reading jar file: " + - e.getMessage()); + throw new SQLException("reading jar file: " + + e.toString(), "58030", e); } } @@ -602,8 +602,8 @@ static void addClassImages(int jarId, InputStream urlStream, long sz) } catch(IOException e) { - throw new SQLException("I/O exception reading jar file: " - + e.getMessage(), "58030", e); + throw new SQLException("reading jar file: " + + e.toString(), "58030", e); } } From 627d1bc7a65b1e35958d59123c55c16cb1f3824a Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 5 Jun 2023 15:28:31 -0400 Subject: [PATCH 076/334] Private methods to bypass buggy namedGroups cache PL/Java builds and passes its tests on Java 20 this way. Of course, this code relies on Java 20 API and can only be built by bumping to 20 in the pljava-api POM. To do: make it reflective so it can build on earlier versions, and apply the workaround only if needed. Begins to address #435. --- .../postgresql/pljava/sqlgen/Lexicals.java | 53 +++++++++++++++---- 1 file changed, 42 insertions(+), 11 deletions(-) diff --git a/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/Lexicals.java b/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/Lexicals.java index 1f6338534..081e5bee4 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/Lexicals.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/Lexicals.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015-2022 Tada AB and other contributors, as listed below. + * Copyright (c) 2015-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 @@ -47,6 +47,37 @@ public abstract class Lexicals { private Lexicals() { } // do not instantiate + /** + * Maps a capturing-group name to its group index, bypassing the buggy cache + * introduced in Java 20 (PL/Java issue 435). + */ + private static int gi(Matcher m, String gn) + { + Integer i = + m.pattern().namedGroups().get(requireNonNull(gn, "group name")); + if ( null != i ) + return i; + throw new IllegalArgumentException("No group with name <" + gn + ">"); + } + + /** + * Returns the equivalent of {@code m.start(gn)} but bypassing the buggy + * cache introduced in Java 20. + */ + private static int start(Matcher m, String gn) + { + return m.start(gi(m, gn)); + } + + /** + * Returns the equivalent of {@code m.group(gn)} but bypassing the buggy + * cache introduced in Java 20. + */ + private static String group(Matcher m, String gn) + { + return m.group(gi(m, gn)); + } + /** Allowed as the first character of a regular identifier by ISO. */ public static final Pattern ISO_REGULAR_IDENTIFIER_START = Pattern.compile( @@ -342,9 +373,9 @@ public static boolean separator(Matcher m, boolean significant) m.usePattern(SEPARATOR); if ( ! m.lookingAt() ) return result; // leave matcher region alone - if ( significant || -1 != m.start("nl") ) + if ( significant || -1 != start(m, "nl") ) result = true; - if ( -1 != m.start("nest") ) + if ( -1 != start(m, "nest") ) { m.region(m.end(0) + 1, m.regionEnd()); // + 1 to eat the * m.usePattern(BRACKETED_COMMENT_INSIDE); @@ -357,7 +388,7 @@ public static boolean separator(Matcher m, boolean significant) case 1: if ( ! m.lookingAt() ) throw new InputMismatchException("unclosed comment"); - if ( -1 != m.start("nest") ) + if ( -1 != start(m, "nest") ) { m.region(m.end(0) + 1, m.regionEnd()); // + 1 to eat the * ++ level; @@ -384,17 +415,17 @@ else if ( 0 == -- level ) */ public static Identifier.Simple identifierFrom(Matcher m) { - String s = m.group("i"); + String s = group(m, "i"); if ( null != s ) return Identifier.Simple.from(s, false); - s = m.group("xd"); + s = group(m, "xd"); if ( null != s ) return Identifier.Simple.from(s.replace("\"\"", "\""), true); - s = m.group("xui"); + s = group(m, "xui"); if ( null == s ) return null; // XXX? s = s.replace("\"\"", "\""); - String uec = m.group("uec"); + String uec = group(m, "uec"); if ( null == uec ) uec = "\\"; int uecp = uec.codePointAt(0); @@ -407,9 +438,9 @@ public static Identifier.Simple identifierFrom(Matcher m) { replacer.appendReplacement(sb, ""); int cp; - String uev = replacer.group("u4d"); + String uev = group(replacer, "u4d"); if ( null == uev ) - uev = replacer.group("u6d"); + uev = group(replacer, "u6d"); if ( null != uev ) cp = Integer.parseInt(uev, 16); else @@ -692,7 +723,7 @@ public static Simple fromJava(String s, Messager msgr) if ( m.find() ) { if ( 0 == m.start() && s.length() == m.end() ) - s = m.group("xd").replace("\"\"", "\""); + s = group(m, "xd").replace("\"\"", "\""); else warn = true; } From e81da5202b2f0236fa45d2c0ea7df258e555f430 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 10 Jun 2023 19:30:55 -0400 Subject: [PATCH 077/334] Simply reject Java with JDK-8309515 bug Trying to include a workaround is simply too brittle; static methods that do the right thing in place of Matcher.{start,end,group} would have to be used not only in PL/Java's own code, but in anybody else's code that might use the Lexicals class; such workaround methods might have to be exposed and clutter the API, and there would still be no guarantee they'd be used everywhere needed. JDK-8309515 has been fixed for Java 21 (in openjdk/jdk@90027ff) and will presumably be backpatched to some build of 20. So simply test for it, and if necessary, advise using a working Java version. --- .../postgresql/pljava/sqlgen/Lexicals.java | 79 ++++++++++--------- 1 file changed, 43 insertions(+), 36 deletions(-) diff --git a/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/Lexicals.java b/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/Lexicals.java index 081e5bee4..85bdc66bc 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/Lexicals.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/sqlgen/Lexicals.java @@ -47,35 +47,42 @@ public abstract class Lexicals { private Lexicals() { } // do not instantiate - /** - * Maps a capturing-group name to its group index, bypassing the buggy cache - * introduced in Java 20 (PL/Java issue 435). - */ - private static int gi(Matcher m, String gn) + static { - Integer i = - m.pattern().namedGroups().get(requireNonNull(gn, "group name")); - if ( null != i ) - return i; - throw new IllegalArgumentException("No group with name <" + gn + ">"); - } + /* + * Reject a Java version affected by JDK-8309515 bug. + */ + Boolean hasBug = null; + Pattern p1 = Pattern.compile("(?.)(?.)"); + Pattern p2 = Pattern.compile("(?.)(?.)"); + Matcher m = p1.matcher("xy"); - /** - * Returns the equivalent of {@code m.start(gn)} but bypassing the buggy - * cache introduced in Java 20. - */ - private static int start(Matcher m, String gn) - { - return m.start(gi(m, gn)); - } + if ( m.matches() && 0 == m.start("a") ) + { + m.usePattern(p2); + if ( m.matches() ) + { + switch ( m.start("a") ) + { + case 0: + hasBug = true; + break; + case 1: + hasBug = false; + break; + } + } + } - /** - * Returns the equivalent of {@code m.group(gn)} but bypassing the buggy - * cache introduced in Java 20. - */ - private static String group(Matcher m, String gn) - { - return m.group(gi(m, gn)); + if ( null == hasBug ) + throw new ExceptionInInitializerError( + "Unexpected result while testing for bug JDK-8309515"); + + if ( hasBug ) + throw new ExceptionInInitializerError( + "Java bug JDK-8309515 affects this version of Java. PL/Java " + + "requires a Java version earlier than 20 (when the bug first " + + "appears) or recent enough to have had the bug fixed."); } /** Allowed as the first character of a regular identifier by ISO. @@ -373,9 +380,9 @@ public static boolean separator(Matcher m, boolean significant) m.usePattern(SEPARATOR); if ( ! m.lookingAt() ) return result; // leave matcher region alone - if ( significant || -1 != start(m, "nl") ) + if ( significant || -1 != m.start("nl") ) result = true; - if ( -1 != start(m, "nest") ) + if ( -1 != m.start("nest") ) { m.region(m.end(0) + 1, m.regionEnd()); // + 1 to eat the * m.usePattern(BRACKETED_COMMENT_INSIDE); @@ -388,7 +395,7 @@ public static boolean separator(Matcher m, boolean significant) case 1: if ( ! m.lookingAt() ) throw new InputMismatchException("unclosed comment"); - if ( -1 != start(m, "nest") ) + if ( -1 != m.start("nest") ) { m.region(m.end(0) + 1, m.regionEnd()); // + 1 to eat the * ++ level; @@ -415,17 +422,17 @@ else if ( 0 == -- level ) */ public static Identifier.Simple identifierFrom(Matcher m) { - String s = group(m, "i"); + String s = m.group("i"); if ( null != s ) return Identifier.Simple.from(s, false); - s = group(m, "xd"); + s = m.group("xd"); if ( null != s ) return Identifier.Simple.from(s.replace("\"\"", "\""), true); - s = group(m, "xui"); + s = m.group("xui"); if ( null == s ) return null; // XXX? s = s.replace("\"\"", "\""); - String uec = group(m, "uec"); + String uec = m.group("uec"); if ( null == uec ) uec = "\\"; int uecp = uec.codePointAt(0); @@ -438,9 +445,9 @@ public static Identifier.Simple identifierFrom(Matcher m) { replacer.appendReplacement(sb, ""); int cp; - String uev = group(replacer, "u4d"); + String uev = replacer.group("u4d"); if ( null == uev ) - uev = group(replacer, "u6d"); + uev = replacer.group("u6d"); if ( null != uev ) cp = Integer.parseInt(uev, 16); else @@ -723,7 +730,7 @@ public static Simple fromJava(String s, Messager msgr) if ( m.find() ) { if ( 0 == m.start() && s.length() == m.end() ) - s = group(m, "xd").replace("\"\"", "\""); + s = m.group("xd").replace("\"\"", "\""); else warn = true; } From a2bf6f72e14150a2006d2a4bf2c304fe225e5371 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 11 Jun 2023 10:10:06 -0400 Subject: [PATCH 078/334] But for JDK-8309515, Java 20's ok for DDRProcessor --- .../postgresql/pljava/annotation/processing/DDRProcessor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pljava-api/src/main/java/org/postgresql/pljava/annotation/processing/DDRProcessor.java b/pljava-api/src/main/java/org/postgresql/pljava/annotation/processing/DDRProcessor.java index 1d14ca668..20f58cb57 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/annotation/processing/DDRProcessor.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/annotation/processing/DDRProcessor.java @@ -173,7 +173,7 @@ public SourceVersion getSupportedSourceVersion() * Update latest_tested to be the latest Java release on which this * annotation processor has been tested without problems. */ - int latest_tested = 19; + int latest_tested = 20; int ordinal_9 = SourceVersion.RELEASE_9.ordinal(); int ordinal_latest = latest_tested - 9 + ordinal_9; From d417a9e516b5727b210069070e77ea5a3dd8e8f4 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 11 Jun 2023 12:37:27 -0400 Subject: [PATCH 079/334] InstallHelper, more java7ification/java8ification In passing, streamline recognizeSchema a bit. --- .../pljava/internal/InstallHelper.java | 167 ++++++++++-------- 1 file changed, 95 insertions(+), 72 deletions(-) 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 4e0de5363..6ec8b4d21 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015-2021 Tada AB and other contributors, as listed below. + * Copyright (c) 2015-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 @@ -705,6 +705,23 @@ private static void deployViaDescriptor( s.execute( "RESET pljava.implementors"); } + /** + * Query the database metadata for existence of a column in a table in the + * {@code sqlj} schema. Pass null for the column to simply check the table's + * existence. + */ + private static boolean hasColumn( + DatabaseMetaData md, String table, String column) + throws SQLException + { + try ( + ResultSet rs = md.getColumns( null, "sqlj", table, column) + ) + { + return rs.next(); + } + } + /** * Detect an existing PL/Java sqlj schema. Tests for changes between schema * variants that have appeared in PL/Java's git history and will return a @@ -723,97 +740,103 @@ private static SchemaVariant recognizeSchema( throws SQLException { DatabaseMetaData md = c.getMetaData(); - ResultSet rs = md.getProcedures( null, "sqlj", "alias_java_language"); - boolean seen = rs.next(); - rs.close(); - if ( seen ) - return SchemaVariant.REL_1_6_0; + try ( + ResultSet rs = + md.getProcedures( null, "sqlj", "alias_java_language") + ) + { + if ( rs.next() ) + return SchemaVariant.REL_1_6_0; + } - rs = md.getColumns( null, "sqlj", "jar_descriptor", null); - seen = rs.next(); - rs.close(); - if ( seen ) + if ( hasColumn( md, "jar_descriptor", null) ) return SchemaVariant.UNREL20130301b; - rs = md.getColumns( null, "sqlj", "jar_descriptors", null); - seen = rs.next(); - rs.close(); - if ( seen ) + if ( hasColumn( md, "jar_descriptors", null) ) return SchemaVariant.UNREL20130301a; - rs = md.getColumns( null, "sqlj", "jar_repository", "jarmanifest"); - seen = rs.next(); - rs.close(); - if ( seen ) + if ( hasColumn( md, "jar_repository", "jarmanifest") ) return SchemaVariant.REL_1_3_0; - rs = md.getColumns( null, "sqlj", "typemap_entry", null); - seen = rs.next(); - rs.close(); - if ( seen ) + if ( hasColumn( md, "typemap_entry", null) ) return SchemaVariant.UNREL20060212; - rs = md.getColumns( null, "sqlj", "jar_repository", "jarowner"); - if ( rs.next() ) + try ( + ResultSet rs = + md.getColumns( null, "sqlj", "jar_repository", "jarowner") + ) { - int t = rs.getInt("DATA_TYPE"); - rs.close(); - if ( VARCHAR == t ) - return SchemaVariant.UNREL20060125; - return SchemaVariant.REL_1_1_0; + if ( rs.next() ) + { + if ( VARCHAR == rs.getInt("DATA_TYPE") ) + return SchemaVariant.UNREL20060125; + return SchemaVariant.REL_1_1_0; + } } - rs.close(); - rs = md.getColumns( null, "sqlj", "jar_repository", "deploymentdesc"); - seen = rs.next(); - rs.close(); - if ( seen ) + if ( hasColumn( md, "jar_repository", "deploymentdesc") ) return SchemaVariant.REL_1_0_0; - rs = md.getColumns( null, "sqlj", "jar_entry", null); - seen = rs.next(); - rs.close(); - if ( seen ) + if ( hasColumn( md, "jar_entry", null) ) return SchemaVariant.UNREL20040121; - rs = md.getColumns( null, "sqlj", "jar_repository", "jarimage"); - seen = rs.next(); - rs.close(); - if ( seen ) + if ( hasColumn( md, "jar_repository", "jarimage") ) return SchemaVariant.UNREL20040120; - PreparedStatement ps = c.prepareStatement( "SELECT count(*) " + - "FROM pg_catalog.pg_depend d, pg_catalog.pg_namespace n " + - "WHERE" + - " refclassid OPERATOR(pg_catalog.=)" + - " 'pg_catalog.pg_namespace'::regclass " + - " AND refobjid OPERATOR(pg_catalog.=) n.oid" + - " AND nspname OPERATOR(pg_catalog.=) 'sqlj' " + - " AND deptype OPERATOR(pg_catalog.=) 'n' " + - " AND NOT EXISTS ( " + - " SELECT 1 FROM " + - " pg_catalog.pg_class sqc JOIN pg_catalog.pg_namespace sqn " + - " ON relnamespace OPERATOR(pg_catalog.=) sqn.oid " + - " WHERE " + - " nspname OPERATOR(pg_catalog.=) 'pg_catalog'" + - " AND relname OPERATOR(pg_catalog.=) 'pg_extension' " + - " AND classid OPERATOR(pg_catalog.=) sqc.oid " + - " OR " + - " nspname OPERATOR(pg_catalog.=) 'sqlj'" + - " AND relname OPERATOR(pg_catalog.=) ?" + - " AND classid OPERATOR(pg_catalog.=)" + - " 'pg_catalog.pg_class'::regclass " + - " AND objid OPERATOR(pg_catalog.=) sqc.oid)"); - ps.setString(1, loadpath_tbl); - rs = ps.executeQuery(); - if ( rs.next() && 0 == rs.getInt(1) ) + try ( + PreparedStatement stmt = Checked.Supplier.use((() -> + { + PreparedStatement ps = c.prepareStatement( + /* + * Is the sqlj schema 'empty'? Count the pg_depend + * type 'n' dependency entries referring to the sqlj + * namespace ... + */ + "SELECT count(*)" + + "FROM" + + " pg_catalog.pg_depend d, pg_catalog.pg_namespace n " + + "WHERE" + + " refclassid OPERATOR(pg_catalog.=)" + + " 'pg_catalog.pg_namespace'::regclass " + + " AND refobjid OPERATOR(pg_catalog.=) n.oid" + + " AND nspname OPERATOR(pg_catalog.=) 'sqlj' " + + " AND deptype OPERATOR(pg_catalog.=) 'n' " + + /* + * ... but exclude from the count, if present: + */ + " AND NOT EXISTS ( " + + " SELECT 1 FROM " + + " pg_catalog.pg_class sqc" + + " JOIN pg_catalog.pg_namespace sqn" + + " ON relnamespace OPERATOR(pg_catalog.=) sqn.oid " + + " WHERE " + + /* + * (1) any dependency that is an extension (d.classid + * identifies pg_catalog.pg_extension) ... + */ + " nspname OPERATOR(pg_catalog.=) 'pg_catalog'" + + " AND" + + " relname OPERATOR(pg_catalog.=) 'pg_extension' " + + " AND d.classid OPERATOR(pg_catalog.=) sqc.oid " + + " OR " + + /* + * (2) any dependency that is the loadpath_tbl table + * we temporarily create in the extension script. + */ + " nspname OPERATOR(pg_catalog.=) 'sqlj'" + + " AND relname OPERATOR(pg_catalog.=) ?" + + " AND classid OPERATOR(pg_catalog.=)" + + " 'pg_catalog.pg_class'::regclass " + + " AND objid OPERATOR(pg_catalog.=) sqc.oid)"); + ps.setString(1, loadpath_tbl); + return ps; + })).get(); + ResultSet rs = stmt.executeQuery(); + ) { - rs.close(); - ps.close(); - return SchemaVariant.EMPTY; + if ( rs.next() && 0 == rs.getInt(1) ) + return SchemaVariant.EMPTY; } - rs.close(); - ps.close(); return null; } From bb86d4dd7b44966b7a0d767e562566e1d10de287 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 11 Jun 2023 16:11:51 -0400 Subject: [PATCH 080/334] Add objects to extension before CREATE OR REPLACE Until postgres/postgres@b9b21ac, CREATE OR REPLACE would silently absorb the object, if preexisting, into the extension being created. Since that change, those CREATE OR REPLACE operations now fail if the object exists but is not yet a member of the extension. Therefore, ALTER EXTENSION ADD them first. Because it's possible to be updating from an older PL/Java version (for example, one without the validator functions), this should be done in Java code where a failure because the expected object isn't there yet can be treated as benign. This special treatment is only needed for the few objects that get specially CREATE OR REPLACEd within the Java code. The rest can be handled with ALTER EXTENSION ADD commands in the extension script, as for any other extension. In passing, also use the tableoid system column to simplify an older query slightly. --- .../pljava/internal/InstallHelper.java | 87 ++++++++++++++++++- 1 file changed, 83 insertions(+), 4 deletions(-) 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 6ec8b4d21..86ef04d19 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java @@ -413,6 +413,9 @@ public static void groundwork( throw new SQLNonTransientException( "sqlj schema not empty for CREATE EXTENSION pljava", "55000"); + if ( asExtension && ! exNihilo ) + preAbsorb(c, s); // handle possible update from unpackaged + handlers(c, s, module_pathname); languages(c, s); deployment(c, s, sv); @@ -432,6 +435,84 @@ public static void groundwork( } } + /** + * Absorb a few key objects into the extension, if they exist, before the + * operations that CREATE OR REPLACE them. + * + * Until postgres/postgres@b9b21ac, CREATE OR REPLACE would silently absorb + * the object, if preexisting, into the extension being created. Since that + * change, those CREATE OR REPLACE operations now fail if the object exists + * but is not yet a member of the extension. Therefore, this method is + * called first, to absorb those objects if they exist. Because this only + * matters when the objects do not yet belong to the extension (the old + * "FROM unpackaged" case), this method first checks and returns with no + * effect if javau_call_handler is already an extension member. + * + * Because it's possible to be updating from an older PL/Java version + * (for example, one without the validator functions), failure to add an + * expected object to the extension because the object doesn't exist yet + * is not treated here as an error. + */ + private static void preAbsorb( Connection c, Statement s) + throws SQLException + { + /* + * Do nothing if javau_call_handler is already an extension member. + */ + try ( + ResultSet rs = s.executeQuery( + "SELECT d.refobjid" + + " FROM" + + " pg_catalog.pg_namespace n" + + " JOIN pg_catalog.pg_proc p" + + " ON pronamespace OPERATOR(pg_catalog.=) n.oid" + + " JOIN pg_catalog.pg_depend d" + + " ON d.classid OPERATOR(pg_catalog.=) p.tableoid" + + " AND d.objid OPERATOR(pg_catalog.=) p.oid" + + " WHERE" + + " nspname OPERATOR(pg_catalog.=) 'sqlj'" + + " AND proname OPERATOR(pg_catalog.=) 'javau_call_handler'" + + " AND deptype OPERATOR(pg_catalog.=) 'e'" + ) + ) + { + if ( rs.next() ) + return; + } + + addExtensionUnless(c, s, "42883", "FUNCTION sqlj.java_call_handler()"); + addExtensionUnless(c, s, "42883", "FUNCTION sqlj.javau_call_handler()"); + addExtensionUnless(c, s, "42883", + "FUNCTION sqlj.java_validator(pg_catalog.oid)"); + addExtensionUnless(c, s, "42883", + "FUNCTION sqlj.javau_validator(pg_catalog.oid)"); + addExtensionUnless(c, s, "42704", "LANGUAGE java"); + addExtensionUnless(c, s, "42704", "LANGUAGE javaU"); + } + + /** + * Absorb obj into the pljava extension, unless it doesn't exist. + * Pass the sqlState expected when an obj of that type doesn't exist. + */ + private static void addExtensionUnless( + Connection c, Statement s, String sqlState, String obj) + throws SQLException + { + Savepoint p = null; + try + { + p = c.setSavepoint(); + s.execute("ALTER EXTENSION pljava ADD " + obj); + c.releaseSavepoint(p); + } + catch ( SQLException sqle ) + { + c.rollback(p); + if ( ! sqlState.equals(sqle.getSQLState()) ) + throw sqle; + } + } + /** * Create the {@code sqlj} schema, adding an appropriate comment and * granting {@code USAGE} to {@code public}. @@ -796,8 +877,7 @@ private static SchemaVariant recognizeSchema( "FROM" + " pg_catalog.pg_depend d, pg_catalog.pg_namespace n " + "WHERE" + - " refclassid OPERATOR(pg_catalog.=)" + - " 'pg_catalog.pg_namespace'::regclass " + + " refclassid OPERATOR(pg_catalog.=) n.tableoid " + " AND refobjid OPERATOR(pg_catalog.=) n.oid" + " AND nspname OPERATOR(pg_catalog.=) 'sqlj' " + " AND deptype OPERATOR(pg_catalog.=) 'n' " + @@ -825,8 +905,7 @@ private static SchemaVariant recognizeSchema( */ " nspname OPERATOR(pg_catalog.=) 'sqlj'" + " AND relname OPERATOR(pg_catalog.=) ?" + - " AND classid OPERATOR(pg_catalog.=)" + - " 'pg_catalog.pg_class'::regclass " + + " AND classid OPERATOR(pg_catalog.=) sqc.tableoid" + " AND objid OPERATOR(pg_catalog.=) sqc.oid)"); ps.setString(1, loadpath_tbl); return ps; From 38e637bc35f9bc6d40bcebabb2cc0c0ac730db54 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 11 Jun 2023 16:26:34 -0400 Subject: [PATCH 081/334] Re-enable the "from unpackaged" CI tests Addresses issue #434. --- .github/workflows/ci-runnerpg.yml | 7 ++----- appveyor.yml | 7 ++----- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci-runnerpg.yml b/.github/workflows/ci-runnerpg.yml index b7a84cb7e..084201427 100644 --- a/.github/workflows/ci-runnerpg.yml +++ b/.github/workflows/ci-runnerpg.yml @@ -477,14 +477,13 @@ jobs: * pljava.module_path set to the right locations of the jars, and the * correct shared-object path given to LOAD. * - * For now, until issue #434 has a fix, DO NOT - * also test the after-the-fact packaging up with CREATE EXTENSION + * Also test the after-the-fact packaging up with CREATE EXTENSION * FROM unpackaged. That officially goes away in PG 13, where the * equivalent sequence * CREATE EXTENSION pljava VERSION unpackaged * \c * ALTER EXTENSION pljava UPDATE - * should (for now, also NOT) be tested instead. + * should be tested instead. */ try ( Connection c = n1.connect() ) { @@ -523,7 +522,6 @@ jobs: * PG >= 13 CREATE EXTENSION VERSION unpackaged;ALTER EXTENSION UPDATE * sequence) has to happen over a new connection. */ - if ( false ) { // pending issue 434 fix try ( Connection c = n1.connect() ) { int majorVersion = c.getMetaData().getDatabaseMajorVersion(); @@ -557,7 +555,6 @@ jobs: (o,p,q) -> null == o ); } - } // pending issue 434 fix } catch ( Throwable t ) { succeeding = false; diff --git a/appveyor.yml b/appveyor.yml index 6f800f202..604c5fc32 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -356,14 +356,13 @@ test_script: * pljava.module_path set to the right locations of the jars, and the * correct shared-object path given to LOAD. * - * For now, until issue #434 has a fix, DO NOT - * also test the after-the-fact packaging up with CREATE EXTENSION + * Also test the after-the-fact packaging up with CREATE EXTENSION * FROM unpackaged. That officially goes away in PG 13, where the * equivalent sequence * CREATE EXTENSION pljava VERSION unpackaged * \c * ALTER EXTENSION pljava UPDATE - * should (for now, also NOT) be tested instead. + * should be tested instead. */ try ( Connection c = n1.connect() ) { @@ -402,7 +401,6 @@ test_script: * PG >= 13 CREATE EXTENSION VERSION unpackaged;ALTER EXTENSION UPDATE * sequence) has to happen over a new connection. */ - if ( false ) { // pending issue 434 fix try ( Connection c = n1.connect() ) { int majorVersion = c.getMetaData().getDatabaseMajorVersion(); @@ -436,7 +434,6 @@ test_script: (o,p,q) -> null == o ); } - } // pending issue 434 fix } catch ( Throwable t ) { succeeding = false; From 49682f7a5ade8d9f4221e993cc5334c6d0427da6 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 11 Jun 2023 18:23:29 -0400 Subject: [PATCH 082/334] Ask for suitable MIME types when fetching a jar Watching DEBUG1 output revealed that the Accept header used by default was asking for other document types. As of pretty recently, application/java-archive is the registered IANA MIME type for a jar. Also accept the "deprecated alias names" listed in the registration. Setting the Accept header requires any URLPermission granted to have an actions string of "GET:Accept". --- .github/workflows/ci-runnerpg.yml | 2 +- appveyor.yml | 2 +- pljava-packaging/src/main/resources/pljava.policy | 5 +++-- .../java/org/postgresql/pljava/management/Commands.java | 8 +++++++- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci-runnerpg.yml b/.github/workflows/ci-runnerpg.yml index ee4819c97..61d406e77 100644 --- a/.github/workflows/ci-runnerpg.yml +++ b/.github/workflows/ci-runnerpg.yml @@ -509,7 +509,7 @@ jobs: succeeding &= useTrialPolicy(n1, c2, List.of( "grant codebase \"${org.postgresql.pljava.codesource}\" {", " permission", - " java.net.URLPermission \"http:*\", \"GET\";", + " java.net.URLPermission \"http:*\", \"GET:Accept\";", "};" )); diff --git a/appveyor.yml b/appveyor.yml index 2195e74c4..e8c602a8d 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -394,7 +394,7 @@ test_script: succeeding &= useTrialPolicy(n1, c2, List.of( "grant codebase \"${org.postgresql.pljava.codesource}\" {", " permission", - " java.net.URLPermission \"http:*\", \"GET\";", + " java.net.URLPermission \"http:*\", \"GET:Accept\";", "};" )); diff --git a/pljava-packaging/src/main/resources/pljava.policy b/pljava-packaging/src/main/resources/pljava.policy index c360dcdb5..4c7c078f1 100644 --- a/pljava-packaging/src/main/resources/pljava.policy +++ b/pljava-packaging/src/main/resources/pljava.policy @@ -78,8 +78,9 @@ grant codebase "${org.postgresql.pljava.codesource}" { // // There would be nothing wrong with restricting this permission to // a specific directory, if all jar files to be loaded will be found there, - // or replacing it with a URLPermission if they will be hosted on a remote - // server, etc. + // or, if they will be hosted on a remote server, a permission like + // java.net.URLPermission "https://example.com/jars/*", "GET:Accept" + // etc. // permission java.io.FilePermission "<>", "read"; 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 8aa086a7f..0cdafa254 100644 --- a/pljava/src/main/java/org/postgresql/pljava/management/Commands.java +++ b/pljava/src/main/java/org/postgresql/pljava/management/Commands.java @@ -450,6 +450,12 @@ static void addClassImages(int jarId, String urlString) { URL url = new URL(urlString); URLConnection uc = url.openConnection(); + uc.setRequestProperty("Accept", + "application/java-archive, " + + "application/jar;q=0.9, application/jar-archive;q=0.9, " + + "application/x-java-archive;q=0.9, " + + "application/*;q=0.3, */*;q=0.2" + ); long[] sz = new long[1]; Permission[] least = { uc.getPermission() }; @@ -465,7 +471,7 @@ static void addClassImages(int jarId, String urlString) */ least = new Permission[] { least[0], - new URLPermission(urlString, "GET") + new URLPermission(urlString, "GET:Accept") }; /* From b302ad37fcbaead70da8216527f82f10a11df439 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 11 Jun 2023 21:08:37 -0400 Subject: [PATCH 083/334] Avoid crash on exception in String initialization Addresses #416, an unlikely scenario that combined a database set for SQL_ASCII with an unworkable setting in pljava.vmoptions, where the combination caused an UnsupportedCharsetException during initialization in String.c, and elogExceptionMessage tried to use the incompletely-initialized String routines, causing a crash. Effort had been put in String.c to avoid such outcomes, by having initial values of uninitialized and s_two_step_conversion that provide a fallback capability before initialization is complete. However, the initial value of s_two_step_conversion could be changed to its runtime-determined value too early, with some other initialization steps yet to be done. --- pljava-so/src/main/c/type/String.c | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/pljava-so/src/main/c/type/String.c b/pljava-so/src/main/c/type/String.c index 63f77c5fb..e380be326 100644 --- a/pljava-so/src/main/c/type/String.c +++ b/pljava-so/src/main/c/type/String.c @@ -423,6 +423,12 @@ static void String_initialize_codec() jclass buffer_class = PgObject_getJavaClass("java/nio/Buffer"); jobject servercs; + /* + * Records what the final state of s_two_step_conversion will be, but the + * static is left at its initial value until all preparations are complete. + */ + bool two_step_when_ready = s_two_step_conversion; + s_server_encoding = GetDatabaseEncoding(); if ( PG_SQL_ASCII == s_server_encoding ) @@ -432,7 +438,7 @@ static void String_initialize_codec() "forName", "(Ljava/lang/String;)Ljava/nio/charset/Charset;"); jstring sql_ascii = JNI_newStringUTF("X-PGSQL_ASCII"); - s_two_step_conversion = false; + two_step_when_ready = false; servercs = JNI_callStaticObjectMethodLocked(charset_class, forname, sql_ascii); @@ -444,7 +450,7 @@ static void String_initialize_codec() jfieldID scharset_UTF_8 = PgObject_getStaticJavaField(scharset_class, "UTF_8", "Ljava/nio/charset/Charset;"); - s_two_step_conversion = PG_UTF8 != s_server_encoding; + two_step_when_ready = PG_UTF8 != s_server_encoding; servercs = JNI_getStaticObjectField(scharset_class, scharset_UTF_8); } @@ -478,5 +484,6 @@ static void String_initialize_codec() s_the_empty_string = JNI_newGlobalRef( JNI_callObjectMethod(empty, string_intern)); + s_two_step_conversion = two_step_when_ready; uninitialized = false; } From a55ec9093b02f2a6679710d167a406ebc5c9c321 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 12 Jun 2023 20:10:45 -0400 Subject: [PATCH 084/334] Pre-PG14, this cast is needed Earlier versions don't supply a text[] || varchar[] operator. --- pljava-packaging/src/main/java/Node.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pljava-packaging/src/main/java/Node.java b/pljava-packaging/src/main/java/Node.java index dd658f9fa..48deda9de 100644 --- a/pljava-packaging/src/main/java/Node.java +++ b/pljava-packaging/src/main/java/Node.java @@ -1122,7 +1122,7 @@ public static Stream appendClasspathIf( " )" + " )" + "FROM" + - " (VALUES (?, ?)) AS p(schema, jar)," + + " (VALUES (?, CAST (? AS pg_catalog.text))) AS p(schema, jar)," + " COALESCE(" + " pg_catalog.regexp_split_to_array(" + " sqlj.get_classpath(schema)," + From 59afb0fea1142a577d295d1aee0af0f12ac07b2d Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 13 Jun 2023 18:38:20 -0400 Subject: [PATCH 085/334] Update release notes, and versions.md --- src/site/markdown/build/versions.md | 16 ++-- src/site/markdown/releasenotes.md.vm | 109 ++++++++++++++++++++++++--- 2 files changed, 108 insertions(+), 17 deletions(-) diff --git a/src/site/markdown/build/versions.md b/src/site/markdown/build/versions.md index d5d36fb9e..a759a20b9 100644 --- a/src/site/markdown/build/versions.md +++ b/src/site/markdown/build/versions.md @@ -1,6 +1,6 @@ # Versions of external packages needed to build and use PL/Java -As of mid-2020, the following version constraints are known. +As of mid-2023, the following version constraints are known. ## Java @@ -24,6 +24,11 @@ itself was built with, as long as that later JRE version is used at run time. That also allows PL/Java to take advantage of recent Java implementation advances such as [class data sharing][cds]. +Some builds of Java 20 are affected by a bug, [JDK-8309515][]. PL/Java will +report an error if detects it is affected by that bug, and the solution can be +to use a Java version earlier than 20, or one recent enough to have the bug +fixed. + PL/Java has been successfully used with [Oracle Java][orj] and with [OpenJDK][], which is available with [either the Hotspot or the OpenJ9 JVM][hsj9]. It can also be built and used @@ -38,6 +43,7 @@ the `mvn` command line. [OpenJDK]: https://adoptopenjdk.net/ [hsj9]: https://www.eclipse.org/openj9/oj9_faq.html [GraalVM]: https://www.graalvm.org/ +[JDK-8309515]: https://bugs.openjdk.org/browse/JDK-8309515 ## Maven @@ -56,13 +62,11 @@ versions 4.3.0 or later are recommended in order to avoid a ## PostgreSQL -PL/Java 1.6.0 does not commit to support PostgreSQL earlier than 9.5. -(Support for 9.4 or even 9.3 might be feasible to add if there is a pressing -need.) +The PL/Java 1.6 series does not commit to support PostgreSQL earlier than 9.5. More current PostgreSQL versions, naturally, are the focus of development and receive more attention in testing. -PL/Java 1.6.0 has been successfully built and run on at least one platform -with PostgreSQL versions from 13 to 9.5, the latest maintenance +PL/Java 1.6.5 has been successfully built and run on at least one platform +with PostgreSQL versions from 15 to 9.5, the latest maintenance release for each. diff --git a/src/site/markdown/releasenotes.md.vm b/src/site/markdown/releasenotes.md.vm index cb10d1dc2..8f1e17562 100644 --- a/src/site/markdown/releasenotes.md.vm +++ b/src/site/markdown/releasenotes.md.vm @@ -10,6 +10,104 @@ #set($ghbug = 'https://github.com/tada/pljava/issues/') #set($ghpull = 'https://github.com/tada/pljava/pull/') +$h2 PL/Java 1.6.5 + +This is the fifth minor update in the PL/Java 1.6 series. It adds support +for PostgreSQL 15 and fixes some bugs, with few other notable changes. Further +information on the changes may be found below. + +$h3 Version compatibility + +PL/Java 1.6.5 can be built against recent PostgreSQL versions including 15, and +older ones back to 9.5, using Java SE 9 or later. The Java version used at +runtime does not have to be the same version used for building. PL/Java itself +can run on any Java version 9 or later. PL/Java functions can be +written for, and use features of, whatever Java version will be loaded at run +time. See [version compatibility][versions] for more detail. + +Some builds of Java 20 are affected by a bug, [JDK-8309515][]. PL/Java will +report an error if detects it is affected by that bug, and the solution can be +to use a Java version earlier than 20, or one recent enough to have the bug +fixed. + +$h3 Changes + +$h4 Changes affecting administration + +$h5 Bugs affecting `install_jar` from http/https URLs fixed + +CI testing now makes sure that http URLs work and the appropriate +`java.net.URLPermission` can be granted in `pljava.policy` where the comments +indicate. + +$h4 Improvements to the annotation-driven SQL generator + +$h5 PL/Java functions can be declared on interfaces as well as classes + +The SQL/JRT specification has always only said 'class', but it could be +debated whether 'class' was intended strictly or inclusively. As there is +no technical obstacle to using static methods declared on an interface, +and PL/Java's runtime already could do so, the SQL generator no longer +disallows `@Function` annotations on them. + +$h5 SQL generator reports compatibility with a more recent Java source version + +Because PL/Java 1.6 retains compatibility for building on Java versions +back to 9, the SQL generator previously reported 9 as the source version +supported. This produced warnings building user code to target a later version +of Java, which were only an issue for sites using a fail-on-warning policy. + +The SQL generator now reports its supported source version as the earlier of: +the Java version being used, or the latest Java version on which it has been +successfully tested. In this release, that is Java 20. + +$h4 Improvements to documentation + +$h5 Use of `--add-modules` to access Java modules not read by default, explained + +By default, PL/Java starts up with a fairly small set of Java modules readable. +The documentation did not explain the use of `--add-modules` in +`pljava.vmoptions` to expand that set when user code will refer to other +modules. That is [now documented][addm]. + +$h3 Enhancement requests addressed + +* [Allow functions from an interface](${ghbug}426) + +$h3 Bugs fixed + +* [Crash on startup with `SQL_ASCII` database and bad `vmoptions`](${ghbug}416) +* [Installed by `LOAD` then packaged as extension broken in recent PostgreSQL updates](${ghbug}434) +* [Java 20 breaks `LexicalsTest.testSeparator`](${ghbug}435) +* [Not found JDK 11 `java.net.http.HttpClient`](${ghbug}419) (documentation added) +* [`SocketPermission` on `install_jar`](${ghbug}425) +* [The timer in `_destroyJavaVM` does not take effect](${ghbug}407) +* PostgreSQL 15 support [410](${ghbug}410), [412](${ghbug}412) +* [Cannot specify java release other than '9' for `maven-compiler-plugin`](${ghbug}403) +* [Fail validation if function declares `TRANSFORM FOR TYPE`](${ghbug}402) +* ["cannot parse AS string" for 1-letter identifiers](${ghbug}438) + +$h3 Credits + +Thanks in release 1.6.5 to Francisco Miguel Biete Banon, Christoph Berg, Frank +Blanning, Stephen Frost, Casey Lai, Krzysztof Nienartowicz, Yuril Rashkovskii, +Tim Van Holder, `aadrian`, `sincatter`, `tayalrun1`. + +[addm]: install/vmoptions.html#Adding_to_the_set_of_readable_modules +[versions]: build/versions.html +[JDK-8309515]: https://bugs.openjdk.org/browse/JDK-8309515 + +$h2 Earlier releases + +## A nice thing about using Velocity is that each release can be entered at +## birth using h2 as its main heading, h3 and below within ... and then, when +## it is moved under 'earlier releases', just define those variables to be +## one heading level finer. Here goes: +#set($h2 = '###') +#set($h3 = '####') +#set($h4 = '#####') +#set($h5 = '######') + $h2 PL/Java 1.6.4 This is the fourth minor update in the PL/Java 1.6 series. It is a minor @@ -79,17 +177,6 @@ $h3 Bugs fixed [exoneout]: pljava-examples/apidocs/org/postgresql/pljava/example/annotation/ReturnComposite.html#method.summary -$h2 Earlier releases - -## A nice thing about using Velocity is that each release can be entered at -## birth using h2 as its main heading, h3 and below within ... and then, when -## it is moved under 'earlier releases', just define those variables to be -## one heading level finer. Here goes: -#set($h2 = '###') -#set($h3 = '####') -#set($h4 = '#####') -#set($h5 = '######') - $h2 PL/Java 1.6.3 (10 October 2021) This is the third minor update in the PL/Java 1.6 series. It adds support From 01761235f8fec8b3fd94333cca3f60a69b6cce3e Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 13 Jun 2023 18:39:58 -0400 Subject: [PATCH 086/334] Poke migration-management versions for 1.6.5 --- .../main/java/org/postgresql/pljava/internal/InstallHelper.java | 1 + 1 file changed, 1 insertion(+) 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 86ef04d19..67c959122 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/InstallHelper.java @@ -991,6 +991,7 @@ void migrateFrom( SchemaVariant sv, Connection c, Statement s) UNREL20040120 ("5e4131738cd095b7ff6367d64f809f6cec6a7ba7"), EMPTY (null); + static final SchemaVariant REL_1_6_5 = REL_1_6_0; static final SchemaVariant REL_1_6_4 = REL_1_6_0; static final SchemaVariant REL_1_6_3 = REL_1_6_0; static final SchemaVariant REL_1_6_2 = REL_1_6_0; From 18147d984041eda4bb7942976031752cc9d1bc95 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 13 Jun 2023 19:11:53 -0400 Subject: [PATCH 087/334] Add control file in preparation for next release Now that 1.6.5 is released, the next release should include an extension SQL file allowing upgrade from 1.6.5. --- pljava-packaging/build.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pljava-packaging/build.xml b/pljava-packaging/build.xml index c85fc4785..0df763e78 100644 --- a/pljava-packaging/build.xml +++ b/pljava-packaging/build.xml @@ -255,6 +255,10 @@ jos.close(); simple update is possible, just repeat the next entry, with the from-version changed. --> + Date: Wed, 14 Jun 2023 16:59:13 -0400 Subject: [PATCH 088/334] New ACL rights appear in PG 15 and in 16 15 introduces two rights that can be granted on GUC settings (where no such thing was formerly possible). postgres/postgres@a0ffa88 16 adds one new right included in OnClass. postgres/postgres@60684dd So N_ACL_RIGHTS can't just be CONFIRMCONSTed; it has to be picked up as a PG-dependent constant. For now, assume that the unused bits (in PG versions that don't use them) will be zero, so no special effort to mask them off will be needed. While this commit adds a Grant.OnSetting interface, it does not add any new interface modeling a setting and its grants. That is left for another day. Also left for later is what the methods for the new rights ought to do in a PG version that doesn't have them. As is, they will just return false. They could be made to do something else, like throw an exception for something unsupported, though that would be a bit more work. --- .../pljava/model/CatalogObject.java | 38 ++++++++++- pljava-so/src/main/c/ModelConstants.c | 12 +++- .../org/postgresql/pljava/pg/AclItem.java | 64 +++++++++++++++++-- .../postgresql/pljava/pg/ModelConstants.java | 15 ++++- 4 files changed, 119 insertions(+), 10 deletions(-) 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 index ac0391def..5182f581d 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/model/CatalogObject.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/model/CatalogObject.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * 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 @@ -277,7 +277,8 @@ 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 { } + interface OnClass + extends OnAttribute, DELETE, TRUNCATE, TRIGGER, MAINTAIN { } /** * Subtype of {@code Grant} representing the privileges that may be @@ -291,6 +292,12 @@ interface OnDatabase extends CONNECT, CREATE, CREATE_TEMP { } */ 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. @@ -411,6 +418,33 @@ interface CONNECT extends Grant boolean connectGrantable(); } + /** + * @hidden + */ + interface SET extends Grant + { + boolean setGranted(); + boolean setGrantable(); + } + + /** + * @hidden + */ + interface ALTER_SYSTEM extends Grant + { + boolean alterSystemGranted(); + boolean alterSystemGrantable(); + } + + /** + * @hidden + */ + interface MAINTAIN extends Grant + { + boolean maintainGranted(); + boolean maintainGrantable(); + } + /** * @hidden */ diff --git a/pljava-so/src/main/c/ModelConstants.c b/pljava-so/src/main/c/ModelConstants.c index ba9badc94..77fe17f92 100644 --- a/pljava-so/src/main/c/ModelConstants.c +++ b/pljava-so/src/main/c/ModelConstants.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * 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 @@ -169,6 +169,8 @@ static int32 constants[] = { CONSTANT(TSCONFIGOID), CONSTANT(TSDICTOID), CONSTANT(TYPEOID), + + CONSTANT(N_ACL_RIGHTS), }; #undef CONSTANT @@ -290,7 +292,13 @@ StaticAssertStmt((c) == \ CONFIRMCONST( ACL_CREATE ); CONFIRMCONST( ACL_CREATE_TEMP ); CONFIRMCONST( ACL_CONNECT ); - CONFIRMCONST( N_ACL_RIGHTS ); +#if PG_VERSION_NUM >= 150000 + CONFIRMCONST( ACL_SET ); + CONFIRMCONST( ACL_ALTER_SYSTEM); +#endif +#if PG_VERSION_NUM >= 160000 + CONFIRMCONST( ACL_MAINTAIN ); +#endif CONFIRMCONST( ACL_ID_PUBLIC ); #define CONFIRMOFFSET(typ,fld) \ diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/AclItem.java b/pljava/src/main/java/org/postgresql/pljava/pg/AclItem.java index cabc3a445..13cefbf88 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/AclItem.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/AclItem.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * 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 @@ -26,8 +26,13 @@ import static org.postgresql.pljava.pg.CatalogObjectImpl.Factory.staticFormObjectId; +import static org.postgresql.pljava.pg.ModelConstants.N_ACL_RIGHTS; + 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; @@ -40,7 +45,14 @@ public abstract class AclItem implements CatalogObject.Grant @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; + // below appearing in PG 16 + @Native static final short ACL_MAINTAIN = 1 << 14; + @Native static final int N_ACL_RIGHTS = 12; + @Native static final int ACL_ID_PUBLIC = 0; @Native static final int OFFSET_ai_grantee = 0; @@ -56,13 +68,25 @@ public abstract class AclItem implements CatalogObject.Grant *

    * 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 = "arwdDxtXUCTc"; + private static final String s_abbr = "arwdDxtXUCTcsAm"; static { - assert N_ACL_RIGHTS == s_abbr.length() : "AclItem abbreviations"; - assert N_ACL_RIGHTS == s_abbr.codePoints().count() : "AclItem abbr BMP"; + /* + * This is not a check for equality, because N_ACL_RIGHTS has grown + * (between PG 14 and 15, and between 15 and 16). 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; @@ -94,7 +118,7 @@ protected AclItem(int grantee, int grantor) */ public static class NonRole extends AclItem implements - OnClass, OnNamespace, + OnClass, OnNamespace, OnSetting, CatalogObject.EXECUTE, CatalogObject.CREATE_TEMP, CatalogObject.CONNECT { private final short m_priv; @@ -258,5 +282,35 @@ public String toString() { 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); + } + + @Override public boolean maintainGranted() + { + return priv(ACL_MAINTAIN); + } + + @Override public boolean maintainGrantable() + { + return goption(ACL_MAINTAIN); + } } } diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/ModelConstants.java b/pljava/src/main/java/org/postgresql/pljava/pg/ModelConstants.java index 6dc6bf23e..a78a890b1 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/ModelConstants.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/ModelConstants.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * 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 @@ -191,6 +191,11 @@ private static class Natives @Native private static final int IDX_TSDICTOID = 57; @Native private static final int IDX_TYPEOID = 58; + /* + * 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 = 59; + /* * These public statics are the values of interest, set at class * initialization time by reading them from the buffer returned by _statics. @@ -273,6 +278,12 @@ private static class Natives public static final int TSDICTOID; // RegDictionaryImpl public static final int TYPEOID; // RegTypeImpl + /* + * The number of meaningful rights bits in an ACL bitmask, imported by + * AclItem. + */ + public static final int N_ACL_RIGHTS; + /** * Value supplied for one of these constants when built in a version of PG * that does not define it. @@ -371,6 +382,8 @@ private static class Natives TSDICTOID = checked(b, IDX_TSDICTOID); TYPEOID = checked(b, IDX_TYPEOID); + N_ACL_RIGHTS = checked(b, IDX_N_ACL_RIGHTS); + if ( 0 != b.remaining() ) throw new ConstantsError(); } From b0b198dd0fe6ef0109db6b6cdb84a92617c09abe Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 14 Jun 2023 17:08:48 -0400 Subject: [PATCH 089/334] Whitespace only --- .../pljava/model/CatalogObject.java | 28 +++++++-------- pljava-so/src/main/c/ModelConstants.c | 30 ++++++++-------- .../org/postgresql/pljava/pg/AclItem.java | 34 +++++++++---------- 3 files changed, 46 insertions(+), 46 deletions(-) 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 index 5182f581d..ab779ffb5 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/model/CatalogObject.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/model/CatalogObject.java @@ -313,7 +313,7 @@ interface OnRole extends Grant /** * @hidden */ - interface INSERT extends Grant + interface INSERT extends Grant { boolean insertGranted(); boolean insertGrantable(); @@ -322,7 +322,7 @@ interface INSERT extends Grant /** * @hidden */ - interface SELECT extends Grant + interface SELECT extends Grant { boolean selectGranted(); boolean selectGrantable(); @@ -331,7 +331,7 @@ interface SELECT extends Grant /** * @hidden */ - interface UPDATE extends Grant + interface UPDATE extends Grant { boolean updateGranted(); boolean updateGrantable(); @@ -340,7 +340,7 @@ interface UPDATE extends Grant /** * @hidden */ - interface DELETE extends Grant + interface DELETE extends Grant { boolean deleteGranted(); boolean deleteGrantable(); @@ -349,7 +349,7 @@ interface DELETE extends Grant /** * @hidden */ - interface TRUNCATE extends Grant + interface TRUNCATE extends Grant { boolean truncateGranted(); boolean truncateGrantable(); @@ -358,7 +358,7 @@ interface TRUNCATE extends Grant /** * @hidden */ - interface REFERENCES extends Grant + interface REFERENCES extends Grant { boolean referencesGranted(); boolean referencesGrantable(); @@ -367,7 +367,7 @@ interface REFERENCES extends Grant /** * @hidden */ - interface TRIGGER extends Grant + interface TRIGGER extends Grant { boolean triggerGranted(); boolean triggerGrantable(); @@ -376,7 +376,7 @@ interface TRIGGER extends Grant /** * @hidden */ - interface EXECUTE extends Grant + interface EXECUTE extends Grant { boolean executeGranted(); boolean executeGrantable(); @@ -385,7 +385,7 @@ interface EXECUTE extends Grant /** * @hidden */ - interface USAGE extends Grant + interface USAGE extends Grant { boolean usageGranted(); boolean usageGrantable(); @@ -394,7 +394,7 @@ interface USAGE extends Grant /** * @hidden */ - interface CREATE extends Grant + interface CREATE extends Grant { boolean createGranted(); boolean createGrantable(); @@ -403,7 +403,7 @@ interface CREATE extends Grant /** * @hidden */ - interface CREATE_TEMP extends Grant + interface CREATE_TEMP extends Grant { boolean create_tempGranted(); boolean create_tempGrantable(); @@ -412,7 +412,7 @@ interface CREATE_TEMP extends Grant /** * @hidden */ - interface CONNECT extends Grant + interface CONNECT extends Grant { boolean connectGranted(); boolean connectGrantable(); @@ -421,7 +421,7 @@ interface CONNECT extends Grant /** * @hidden */ - interface SET extends Grant + interface SET extends Grant { boolean setGranted(); boolean setGrantable(); @@ -439,7 +439,7 @@ interface ALTER_SYSTEM extends Grant /** * @hidden */ - interface MAINTAIN extends Grant + interface MAINTAIN extends Grant { boolean maintainGranted(); boolean maintainGrantable(); diff --git a/pljava-so/src/main/c/ModelConstants.c b/pljava-so/src/main/c/ModelConstants.c index 77fe17f92..800a947bc 100644 --- a/pljava-so/src/main/c/ModelConstants.c +++ b/pljava-so/src/main/c/ModelConstants.c @@ -280,26 +280,26 @@ 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 ); + 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_SET ); CONFIRMCONST( ACL_ALTER_SYSTEM); #endif #if PG_VERSION_NUM >= 160000 - CONFIRMCONST( ACL_MAINTAIN ); + CONFIRMCONST( ACL_MAINTAIN ); #endif - CONFIRMCONST( ACL_ID_PUBLIC ); + CONFIRMCONST( ACL_ID_PUBLIC ); #define CONFIRMOFFSET(typ,fld) \ StaticAssertStmt(offsetof(typ,fld) == \ diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/AclItem.java b/pljava/src/main/java/org/postgresql/pljava/pg/AclItem.java index 13cefbf88..e069ff97f 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/AclItem.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/AclItem.java @@ -33,27 +33,27 @@ 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; + @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 short ACL_SET = 1 << 12; + @Native static final short ACL_ALTER_SYSTEM = 1 << 13; // below appearing in PG 16 - @Native static final short ACL_MAINTAIN = 1 << 14; + @Native static final short ACL_MAINTAIN = 1 << 14; - @Native static final int N_ACL_RIGHTS = 12; + @Native static final int N_ACL_RIGHTS = 12; - @Native static final int ACL_ID_PUBLIC = 0; + @Native static final int ACL_ID_PUBLIC = 0; @Native static final int OFFSET_ai_grantee = 0; @Native static final int OFFSET_ai_grantor = 4; From 52acfdfa0b5f761ded7ea0a0a1388cee716306ae Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 14 Jun 2023 19:43:21 -0400 Subject: [PATCH 090/334] Numeric typmods change in PG 15 It appears Numeric.java had already been written by consulting the upstream code that made it into PG 15. But it mentioned the relevant constants without supplying them, and made no mention of earlier PG versions having different rules, so it is worth updating anyway. Nothing yet checks the constants supplied here against what they should be in PostgreSQL. Once an Adapter is written, in the internal module, it will see these values at compile time and they can be CONFIRMCONSTed from there in the familiar way. --- .../org/postgresql/pljava/adt/Numeric.java | 47 ++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) 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 index 581dcb4d3..0b2ccfdc5 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/adt/Numeric.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/adt/Numeric.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * 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 @@ -42,6 +42,51 @@ @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; + + /** + * Label to distinguish positive, negative, and three kinds of special + * values. + */ enum Kind { POSITIVE, NEGATIVE, NAN, POSINFINITY, NEGINFINITY } /** From acdbb2baa02f3c7bce1fe1fbb5ab8ff08f380738 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 30 Jul 2023 21:31:24 -0400 Subject: [PATCH 091/334] Fix example in AbstractType javadoc --- .../java/org/postgresql/pljava/adt/spi/AbstractType.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 index e2197138d..53e57fe8b 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2022 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 @@ -272,8 +272,8 @@ public static boolean isSubtype(Type sub, Type sup) * 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)[1]}. + * {@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, From 101ae012ecbd7e8ac9ceb60219d617e0132653f2 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 30 Jul 2023 22:24:59 -0400 Subject: [PATCH 092/334] Additions to AbstractType Add an AbstractType.MultiArray to provide manipulations of Java multidimensional arrays, restricted to those matching the shapes of PostgreSQL arrays (non-'jagged', all arrays at a given dimension having the same size, no intermediate arrays missing). In the expected application, a MultiArray will be constructed, with some number of dimensions, over an element type returned by some selected Adapter. The array's type will therefore be that element type with some number of bracket-pairs added. The possibility must be allowed that an Adapter for a PostgreSQL element type also produces a Java array of something. So, the number of bracket pairs in the resulting array's Java type will be the sum of the dimensions of the multi-array and those of the possibly- array type produced by the element adapter. Strictly, only the levels added by the multi-array must conform to the PostgreSQL shape constraints (fully allocated, no jagged sizes). If the element adapter is producing Java array results, those may include nulls, be variable in size, whatever that adapter wants to do. That is, if the final type is [[[[[D, that could be a five-dimension array of double, or a three-dimension array where the element adapter returns a [[D. The MultiArray 'part' is a prefix of the resulting type (when spelled as above, or in the order of dereferencing elements; a suffix when spelled double[][][][][]). When a MultiArray is converted to a Sized, only as many sizes are supplied as its number of prefix dimensions. When that is converted to an Allocated, only that much of it is allocated; the consumer's next task will be to iterate over the arrays at its last level, populating those with whatever the element adapter returns. An iterator is provided to produce those arrays in the right order. Each will be a one-dimensional array of whatever type the element adapter produces, be that a reference or primitive type. By iterating over the final-level arrays rather than individual elements, the hottest work can be done in a simple open-coded loop that may get well optimized; also, further optimization may be possible under detectable conditions (known primitive adapter, fixed size, no nulls), such as direct copy over the array. --- .../pljava/adt/spi/AbstractType.java | 363 +++++++++++++++++- 1 file changed, 362 insertions(+), 1 deletion(-) 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 index 53e57fe8b..af2124253 100644 --- 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 @@ -25,9 +25,11 @@ 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; @@ -267,6 +269,14 @@ 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}. @@ -287,6 +297,8 @@ public static boolean isSubtype(Type sub, Type sup) * {@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 @@ -297,7 +309,8 @@ public static boolean isSubtype(Type sub, Type sup) * 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) + public static Type[] specialization( + Type candidate, Class expected, Type[] rtype) { Type t = requireNonNull(candidate, "candidate is null"); requireNonNull(expected, "expected is null"); @@ -394,9 +407,15 @@ else if ( t instanceof Bindings ) 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 ) @@ -802,4 +821,346 @@ Type substitute(TypeVariable v) 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 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} toa {@link Sized}, only as many + * sizes are supplied as the multiarray's dimensions, and when converting + * that to an {@link 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} 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} that wraps a freshly-allocated array + * with 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. 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; + } + }; + } + } + } + } } From c7e9ae10c3b4ff21171016a38eb1951ca245b22e Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 30 Jul 2023 22:36:13 -0400 Subject: [PATCH 093/334] Add methods to Adapter to derive multiarrays --- .../java/org/postgresql/pljava/Adapter.java | 138 ++++++++++++++++-- 1 file changed, 127 insertions(+), 11 deletions(-) diff --git a/pljava-api/src/main/java/org/postgresql/pljava/Adapter.java b/pljava-api/src/main/java/org/postgresql/pljava/Adapter.java index dd4636cec..2ddca2b00 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/Adapter.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/Adapter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * 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 @@ -44,6 +44,7 @@ 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; @@ -630,7 +631,9 @@ protected Container() * 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 + public abstract static class As + extends Adapter + implements ArrayProto { private final MethodHandle m_fetchHandleErased; @@ -993,7 +996,9 @@ protected Array( * (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 + public abstract static class Primitive + extends Adapter + implements ArrayProto { private > Primitive(Configuration c, A over) { @@ -1016,7 +1021,7 @@ public boolean canFetchNull() * Abstract superclass of signed and unsigned primitive {@code long} * adapters. */ - public abstract static class AsLong extends Primitive + public abstract static class AsLong extends Primitive implements TwosComplement { private > AsLong(Configuration c, A over) @@ -1084,7 +1089,8 @@ protected > Unsigned( /** * Abstract superclass of primitive {@code double} adapters. */ - public abstract static class AsDouble extends Primitive + public abstract static class AsDouble + extends Primitive { protected > AsDouble(Configuration c, A over) { @@ -1127,7 +1133,7 @@ public double fetchNull(Attribute a) throws SQLException * Abstract superclass of signed and unsigned primitive {@code int} * adapters. */ - public abstract static class AsInt extends Primitive + public abstract static class AsInt extends Primitive implements TwosComplement { private > AsInt(Configuration c, A over) @@ -1195,7 +1201,7 @@ protected > Unsigned( /** * Abstract superclass of primitive {@code float} adapters. */ - public abstract static class AsFloat extends Primitive + public abstract static class AsFloat extends Primitive { protected > AsFloat(Configuration c, A over) { @@ -1238,7 +1244,7 @@ public float fetchNull(Attribute a) throws SQLException * Abstract superclass of signed and unsigned primitive {@code short} * adapters. */ - public abstract static class AsShort extends Primitive + public abstract static class AsShort extends Primitive implements TwosComplement { private > AsShort(Configuration c, A over) @@ -1306,7 +1312,7 @@ protected > Unsigned( /** * Abstract superclass of primitive {@code char} adapters. */ - public abstract static class AsChar extends Primitive + public abstract static class AsChar extends Primitive { protected > AsChar(Configuration c, A over) { @@ -1349,7 +1355,7 @@ public char fetchNull(Attribute a) throws SQLException * Abstract superclass of signed and unsigned primitive {@code byte} * adapters. */ - public abstract static class AsByte extends Primitive + public abstract static class AsByte extends Primitive implements TwosComplement { private > AsByte(Configuration c, A over) @@ -1417,7 +1423,8 @@ protected > Unsigned( /** * Abstract superclass of primitive {@code boolean} adapters. */ - public abstract static class AsBoolean extends Primitive + public abstract static class AsBoolean + extends Primitive { protected > AsBoolean(Configuration c, A over) { @@ -1655,4 +1662,113 @@ public Enumeration elements() } } } + + /** + * 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; + } + + 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; + } + } } From 45830dab4cfae1a2ee27eb4b567b055972b80460 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 30 Jul 2023 22:56:23 -0400 Subject: [PATCH 094/334] Add ArrayBuilder.build() and a suitable service ArrayAdapter is in the internal module, so define a service within Adapter (in the API module) to request the construction of an ArrayAdapter with a specialized Contract, derived from the MultiArray computed here. How to handle the resulting type will be tricky. Adapter's existing rules for teasing it out of the contract's type arguments won't be enough. There should be some way for this code to explicitly supply the Type. There is already a 'witness' constructor parameter of type Type (at Adapter's level, anyway, if not yet subclasses). And Type is an unsealed interface. A custom TypeWrapper class, only instantiable here, can be supplied as that argument, passed up through the constructors, and promptly unwrapped to do the right thing. Slightly distasteful, but avoids a slew of new constructors. For consistency, tweak the service loading in CatalogObject to match the slightly more careful usage here. --- .../java/org/postgresql/pljava/Adapter.java | 109 +++++++++++++++++- .../pljava/model/CatalogObject.java | 6 +- 2 files changed, 112 insertions(+), 3 deletions(-) diff --git a/pljava-api/src/main/java/org/postgresql/pljava/Adapter.java b/pljava-api/src/main/java/org/postgresql/pljava/Adapter.java index 2ddca2b00..a98b9f6d3 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/Adapter.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/Adapter.java @@ -39,6 +39,8 @@ 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; @@ -160,7 +162,15 @@ private Adapter( * type precomputed at configuration. */ if ( null != using ) - top = specialization(using.getClass(), Contract.class)[0]; + { + 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); @@ -1663,6 +1673,93 @@ public Enumeration elements() } } + /** + * 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. + */ + 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. @@ -1730,6 +1827,16 @@ public static final class ArrayBuilder m_dimensions = dimensions; } + /** + * Returns an array adapter that will produce arrays with the chosen + * number of dimensions, and this 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); 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 index ab779ffb5..ec97b89bc 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/model/CatalogObject.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/model/CatalogObject.java @@ -12,6 +12,7 @@ package org.postgresql.pljava.model; import java.util.List; +import java.util.ServiceConfigurationError; import java.util.ServiceLoader; import org.postgresql.pljava.sqlgen.Lexicals.Identifier; @@ -455,8 +456,9 @@ abstract class Factory static { INSTANCE = ServiceLoader - .load(Factory.class, Factory.class.getClassLoader()) - .iterator().next(); + .load(Factory.class.getModule().getLayer(), Factory.class) + .findFirst().orElseThrow(() -> new ServiceConfigurationError( + "could not load PL/Java CatalogObject.Factory")); } static > From ff9768b49c0f028784992aff7979f085cd0a766d Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 30 Jul 2023 23:13:54 -0400 Subject: [PATCH 095/334] Implement the multi-array building Rototill the constructors of ArrayAdapter to allow passing the type information up when needed. The ArrayAdapter will be instantiated with one of a handful of different contracts, depending on whether the hottest loop will be dealing with reference or primitive arrays, and which primitives. --- pljava-api/src/main/java/module-info.java | 4 +- .../java/org/postgresql/pljava/Adapter.java | 62 ++-- pljava/src/main/java/module-info.java | 5 +- .../pljava/pg/adt/ArrayAdapter.java | 81 ++++- .../org/postgresql/pljava/pg/adt/Service.java | 305 ++++++++++++++++++ 5 files changed, 428 insertions(+), 29 deletions(-) create mode 100644 pljava/src/main/java/org/postgresql/pljava/pg/adt/Service.java diff --git a/pljava-api/src/main/java/module-info.java b/pljava-api/src/main/java/module-info.java index e1e9dd519..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-2022 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 @@ -30,6 +30,8 @@ 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; diff --git a/pljava-api/src/main/java/org/postgresql/pljava/Adapter.java b/pljava-api/src/main/java/org/postgresql/pljava/Adapter.java index a98b9f6d3..233b63145 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/Adapter.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/Adapter.java @@ -695,7 +695,7 @@ protected As( * parameterized types. */ private As( - Contract.Array using, Adapter adapter, Class witness, + Contract.Array using, Adapter adapter, Type witness, Configuration c) { super(c, null, using, @@ -823,13 +823,13 @@ public abstract static class Array extends As * @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 Class object can specify that more + * 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, - Class witness, Configuration c) + Type witness, Configuration c) { super(using, adapter, witness, c); m_contract = using; @@ -844,13 +844,16 @@ protected Array( * 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, - Configuration c) + Type witness, Configuration c) { - super(using, adapter, null, c); + super(using, adapter, witness, c); m_contract = using; m_elementAdapter = adapter; } @@ -863,13 +866,16 @@ protected Array( * 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, - Configuration c) + Type witness, Configuration c) { - super(using, adapter, null, c); + super(using, adapter, witness, c); m_contract = using; m_elementAdapter = adapter; } @@ -882,13 +888,16 @@ protected Array( * 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, - Configuration c) + Type witness, Configuration c) { - super(using, adapter, null, c); + super(using, adapter, witness, c); m_contract = using; m_elementAdapter = adapter; } @@ -901,13 +910,16 @@ protected Array( * 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, - Configuration c) + Type witness, Configuration c) { - super(using, adapter, null, c); + super(using, adapter, witness, c); m_contract = using; m_elementAdapter = adapter; } @@ -920,13 +932,16 @@ protected Array( * 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, - Configuration c) + Type witness, Configuration c) { - super(using, adapter, null, c); + super(using, adapter, witness, c); m_contract = using; m_elementAdapter = adapter; } @@ -939,13 +954,16 @@ protected Array( * 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, - Configuration c) + Type witness, Configuration c) { - super(using, adapter, null, c); + super(using, adapter, witness, c); m_contract = using; m_elementAdapter = adapter; } @@ -958,13 +976,16 @@ protected Array( * 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, - Configuration c) + Type witness, Configuration c) { - super(using, adapter, null, c); + super(using, adapter, witness, c); m_contract = using; m_elementAdapter = adapter; } @@ -977,13 +998,16 @@ protected Array( * 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, - Configuration c) + Type witness, Configuration c) { - super(using, adapter, null, c); + super(using, adapter, witness, c); m_contract = using; m_elementAdapter = adapter; } diff --git a/pljava/src/main/java/module-info.java b/pljava/src/main/java/module-info.java index 0fbb52f0b..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-2022 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 @@ -35,6 +35,9 @@ 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; 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 index 89d32c5a7..f6f66c935 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/adt/ArrayAdapter.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/adt/ArrayAdapter.java @@ -13,6 +13,8 @@ import java.io.IOException; +import java.lang.reflect.Type; + import java.nio.ByteBuffer; import static java.nio.ByteOrder.nativeOrder; import java.nio.IntBuffer; @@ -111,7 +113,7 @@ public ArrayAdapter( Adapter.AsLong element, Contract.Array> contract) { - super(contract, element, s_config); + super(contract, element, null, s_config); } /** @@ -123,7 +125,7 @@ public ArrayAdapter( Adapter.AsDouble element, Contract.Array> contract) { - super(contract, element, s_config); + super(contract, element, null, s_config); } /** @@ -135,7 +137,7 @@ public ArrayAdapter( Adapter.AsInt element, Contract.Array> contract) { - super(contract, element, s_config); + super(contract, element, null, s_config); } /** @@ -147,7 +149,7 @@ public ArrayAdapter( Adapter.AsFloat element, Contract.Array> contract) { - super(contract, element, s_config); + super(contract, element, null, s_config); } /** @@ -159,7 +161,7 @@ public ArrayAdapter( Adapter.AsShort element, Contract.Array> contract) { - super(contract, element, s_config); + super(contract, element, null, s_config); } /** @@ -171,7 +173,7 @@ public ArrayAdapter( Adapter.AsChar element, Contract.Array> contract) { - super(contract, element, s_config); + super(contract, element, null, s_config); } /** @@ -183,7 +185,7 @@ public ArrayAdapter( Adapter.AsByte element, Contract.Array> contract) { - super(contract, element, s_config); + super(contract, element, null, s_config); } /** @@ -195,7 +197,70 @@ public ArrayAdapter( Adapter.AsBoolean element, Contract.Array> contract) { - super(contract, element, s_config); + 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); } /** 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..50fd2b064 --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/pg/adt/Service.java @@ -0,0 +1,305 @@ +/* + * 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 ) + { + return new ArrayAdapter( + (As)componentAdapter, 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"); + } +} From ecccd542a191a49ba3047d92a1605e1829e62e26 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 30 Jul 2023 21:21:23 -0400 Subject: [PATCH 096/334] Consolidate the temporary test jig methods As long as there is a dedicated SlotTester interface, don't clutter TupleTableSlot with a temporary adapterPlease method. For a bit of sanity, make adapterPlease care a little more about what implementation classes you might name. Have it expect a class that implements SlotTester.Visible. That can go away, of course, as soon as there's a proper adapter manager. --- .../java/org/postgresql/pljava/Adapter.java | 4 ++- .../postgresql/pljava/model/SlotTester.java | 30 ++++++++++++++++++- .../pljava/model/TupleTableSlot.java | 9 +----- .../annotation/TupleTableSlotTest.java | 4 +-- .../postgresql/pljava/jdbc/SPIConnection.java | 20 +++++++++---- .../pljava/pg/TupleTableSlotImpl.java | 15 +--------- .../pljava/pg/adt/DateTimeAdapter.java | 6 ++-- 7 files changed, 54 insertions(+), 34 deletions(-) diff --git a/pljava-api/src/main/java/org/postgresql/pljava/Adapter.java b/pljava-api/src/main/java/org/postgresql/pljava/Adapter.java index 233b63145..b26f795c2 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/Adapter.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/Adapter.java @@ -54,6 +54,8 @@ 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; @@ -102,7 +104,7 @@ * 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 +public abstract class Adapter implements Visible { /** * The full generic type returned by this adapter, as refined at the time 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 index 00f58b8a6..95e181c7f 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/model/SlotTester.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/model/SlotTester.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * 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 @@ -13,10 +13,38 @@ import java.util.List; +import org.postgresql.pljava.Adapter; + /** * A temporary test jig during TupleTableSlot development, not intended to last. */ public interface SlotTester { + /** + * 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/TupleTableSlot.java b/pljava-api/src/main/java/org/postgresql/pljava/model/TupleTableSlot.java index 87fc83170..8447f003d 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/model/TupleTableSlot.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/model/TupleTableSlot.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * 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 @@ -13,7 +13,6 @@ import java.sql.SQLException; -import org.postgresql.pljava.Adapter; import org.postgresql.pljava.Adapter.As; import org.postgresql.pljava.Adapter.AsLong; import org.postgresql.pljava.Adapter.AsDouble; @@ -134,10 +133,4 @@ interface Indexed extends TupleTableSlot */ int elements(); } - - /** - * XXX this method is scaffolding for development testing. - */ - Adapter adapterPlease(String clazz, String field) - throws ReflectiveOperationException; } 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 index 6ee32c5ce..fe1475e63 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * 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 @@ -96,7 +96,7 @@ void testWith(String query, String adpClass, String adpInstance) if ( firstTime ) { firstTime = false; - Adapter a = tts.adapterPlease(adpClass, adpInstance); + Adapter a = t.adapterPlease(adpClass, adpInstance); if ( a instanceof As ) adpL = (As)a; else if ( a instanceof AsLong ) 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 7c3fc10e5..3575ac3cb 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-2022 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 @@ -55,6 +55,8 @@ 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 org.postgresql.pljava.model.SlotTester; import org.postgresql.pljava.model.TupleTableSlot; @@ -77,11 +79,7 @@ */ public class SPIConnection implements Connection, SlotTester { - /** - * A temporary test jig during TupleTableSlot development, not intended - * to last. - */ - @Override + @Override // SlotTester @SuppressWarnings("deprecation") public List test(String query) { @@ -89,6 +87,16 @@ public List test(String query) return TupleTableSlotImpl.testmeSPI(); } + @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/pg/TupleTableSlotImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/TupleTableSlotImpl.java index 6ddce1b02..31b45abed 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/TupleTableSlotImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/TupleTableSlotImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * 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 @@ -70,8 +70,6 @@ import static org.postgresql.pljava.pg.ModelConstants.*; -import java.lang.reflect.Field; - /* * bool always 1 byte (see c.h). * @@ -170,17 +168,6 @@ public abstract class TupleTableSlotImpl @Native private static final int HEAP_HASEXTERNAL = 4; // lives in infomask @Native private static final int HEAP_NATTS_MASK = 0x07FF; // infomask2 - @Override // XXX testing - public Adapter adapterPlease(String cname, String field) - throws ReflectiveOperationException - { - @SuppressWarnings("unchecked") - Class cls = - (Class)Class.forName(cname); - Field f = cls.getField(field); - return (Adapter)f.get(null); - } - protected final ByteBuffer m_tts; /* These can be final only because non-FIXED slots aren't supported yet. */ protected final TupleDescriptor m_tupdesc; 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 index ea3cdc6e4..408828fbf 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/adt/DateTimeAdapter.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/adt/DateTimeAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * 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 @@ -34,6 +34,8 @@ 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; /** @@ -72,7 +74,7 @@ private DateTimeAdapter() // no instances *

    * A holder interface so these won't be instantiated unless wanted. */ - public interface JSR310 + public interface JSR310 extends Visible { Date DATE_INSTANCE = new Date<>(Datetime.Date.AsLocalDate.INSTANCE); From 1423366b909c4bd8b17b9f9753612ca85ed912ae Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 30 Jul 2023 23:40:35 -0400 Subject: [PATCH 097/334] Add an example for multi-array retrieval --- .../annotation/TupleTableSlotTest.java | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) 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 index fe1475e63..4d185982c 100644 --- 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 @@ -15,8 +15,14 @@ import static java.sql.DriverManager.getConnection; import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Arrays; +import static java.util.Arrays.deepToString; +import java.util.Iterator; import java.util.List; +import java.time.OffsetDateTime; + import org.postgresql.pljava.Adapter; import org.postgresql.pljava.Adapter.As; import org.postgresql.pljava.Adapter.AsLong; @@ -40,6 +46,93 @@ */ public class TupleTableSlotTest { + /** + * Test retrieval of a PostgreSQL array as a multidimensional Java array. + */ + @Function(schema="javatest") + public static Iterator javaMultiArrayTest() + throws SQLException, ReflectiveOperationException + { + Connection c = getConnection("jdbc:default:connection"); + SlotTester t = c.unwrap(SlotTester.class); + + /* + * First obtain an Adapter for the component type. For now, the untyped + * adapterPlease method is needed to grovel it out of PL/Java's innards + * and then cast it. An adapter manager with proper generic typing will + * handle that part some day. + */ + AsLong int8 = (AsLong)t.adapterPlease( + "org.postgresql.pljava.pg.adt.Primitives", "INT8_INSTANCE"); + AsInt int4 = (AsInt)t.adapterPlease( + "org.postgresql.pljava.pg.adt.Primitives", "INT4_INSTANCE"); + AsShort int2 = (AsShort)t.adapterPlease( + "org.postgresql.pljava.pg.adt.Primitives", "INT2_INSTANCE"); + AsByte int1 = (AsByte)t.adapterPlease( + "org.postgresql.pljava.pg.adt.Primitives", "INT1_INSTANCE"); + AsDouble float8 = (AsDouble)t.adapterPlease( + "org.postgresql.pljava.pg.adt.Primitives", "FLOAT8_INSTANCE"); + AsFloat float4 = (AsFloat)t.adapterPlease( + "org.postgresql.pljava.pg.adt.Primitives", "FLOAT4_INSTANCE"); + AsBoolean bool = (AsBoolean)t.adapterPlease( + "org.postgresql.pljava.pg.adt.Primitives", "BOOLEAN_INSTANCE"); + + As odt = (As)t.adapterPlease( + "org.postgresql.pljava.pg.adt.DateTimeAdapter$JSR310", + "TIMESTAMPTZ_INSTANCE"); + + /* + * Once properly-typed adapters for component types are in hand, + * getting properly-typed array adapters is straightforward. + * (Java 10+ var can reduce verbosity here.) + */ + As< long[] ,?> i8x1 = int8 .a1().build(); + As< int[][] ,?> i4x2 = int4 .a2() .build(); + As< short[][][] ,?> i2x3 = int2 .a2().a1().build(); + As< byte[][][][] ,?> i1x4 = int1 .a4() .build(); + As< double[][][][][] ,?> f8x5 = float8.a4() .a1().build(); + As< float[][][][][][] ,?> f4x6 = float4.a4().a2() .build(); + As< boolean[][][][][] ,?> bx5 = bool .a4() .a1().build(); + As dtx4 = odt .a4() .build(); + + String query = + "SELECT" + + " '{1,2}'::int8[], " + + " '{{1},{2}}'::int4[], " + + " '{{{1,2,3}}}'::int2[], " + + " '{{{{1},{2},{3}}}}'::\"char\"[], " + + " '{{{{{1,2,3}}}}}'::float8[], " + + " '{{{{{{1},{2},{3}}}}}}'::float4[], " + + " '{{{{{t},{f},{t}}}}}'::boolean[], " + + " '{{{{''now''}}}}'::timestamptz[]"; + + List tups = t.test(query); + + List result = new ArrayList<>(); + + /* + * Then just pass the right adapter to tts.get. + */ + for ( TupleTableSlot tts : tups ) + { + long [] v0 = tts.get(0, i8x1); + int [][] v1 = tts.get(1, i4x2); + short [][][] v2 = tts.get(2, i2x3); + byte [][][][] v3 = tts.get(3, i1x4); + double [][][][][] v4 = tts.get(4, f8x5); + float [][][][][][] v5 = tts.get(5, f4x6); + boolean [][][][][] v6 = tts.get(6, bx5); + OffsetDateTime [][][][] v7 = tts.get(7, dtx4); + + result.addAll(List.of( + Arrays.toString(v0), deepToString(v1), deepToString(v2), + deepToString(v3), deepToString(v4), deepToString(v5), + deepToString(v6), deepToString(v7))); + } + + return result.iterator(); + } + /** * A temporary test jig during TupleTableSlot development; intended * to be used from a debugger. From 87d9389da2310b7aec66659535d46f40cffbb10b Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 31 Jul 2023 17:35:50 -0400 Subject: [PATCH 098/334] Add example of a composing Adapter Well, that couldn't just stay untested forever.... --- .../java/org/postgresql/pljava/Adapter.java | 41 ++++++++++---- .../annotation/TupleTableSlotTest.java | 53 ++++++++++++++++++- 2 files changed, 83 insertions(+), 11 deletions(-) diff --git a/pljava-api/src/main/java/org/postgresql/pljava/Adapter.java b/pljava-api/src/main/java/org/postgresql/pljava/Adapter.java index b26f795c2..1803e6b81 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/Adapter.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/Adapter.java @@ -16,6 +16,7 @@ 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; @@ -249,14 +250,20 @@ private Adapter( MethodType mt = producer .type() - .changeReturnType(erase(top)) + .changeReturnType(erased) .changeParameterType(1, erase(under)); producer = producer.asType(mt); fetcher = fetcher.asType( fetcher.type().changeReturnType(mt.parameterType(1))); - m_fetchHandle = collectArguments(producer, 1, fetcher); + 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; } /** @@ -322,21 +329,34 @@ public String toString() } /** - * Method that an {@code Adapter} must implement to indicate whether it + * 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 abstract boolean canFetch(RegType pgType); + 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 + * 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()); } @@ -535,7 +555,7 @@ protected static Configuration configure( if ( mt.hasWrappers() ) // Void, handled above, won't be seen here { Class underOrig = underErased; - Class underPrim = mt.unwrap().parameterType(0); + Class underPrim = mt.unwrap().returnType(); fetchPredicate = m -> { if ( ! fn.equals(m.getName()) ) @@ -561,11 +581,11 @@ protected static Configuration configure( .filter(m -> ! m.isBridge()).toArray(Method[]::new); if ( 1 != fetchCandidates.length ) throw new IllegalArgumentException( - cls + " lacks a " + fetchName + " method with the " + + cls + " lacks " + fetchName + " method with the " + "expected signature"); if ( ! topErased.isAssignableFrom(fetchCandidates[0].getReturnType()) ) throw new IllegalArgumentException( - cls + " lacks a " + fetchName + " method with the " + + cls + " lacks " + fetchName + " method with the " + "expected return type"); MethodHandle fetcher; @@ -577,7 +597,7 @@ protected static Configuration configure( catch ( IllegalAccessException e ) { throw new IllegalArgumentException( - cls + " has a " + fetchName + " method that is inaccessible", + cls + " has " + fetchName + " method that is inaccessible", e); } @@ -594,7 +614,8 @@ protected static Configuration configure( return new Configuration.Leaf(cls, top, fetcher); } - Class asFound = fetcher.type().parameterType(1); + // unbound virtual handle's type includes receiver; 2nd param is index 2 + Class asFound = fetcher.type().parameterType(2); if ( asFound.isPrimitive() ) under = underErased = asFound; 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 index 4d185982c..20b55e21d 100644 --- 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 @@ -81,6 +81,16 @@ public static Iterator javaMultiArrayTest() "org.postgresql.pljava.pg.adt.DateTimeAdapter$JSR310", "TIMESTAMPTZ_INSTANCE"); + /* + * 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 change + * your data 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. + */ + float8 = new NullReplacingDouble(float8, Double.NaN); + /* * Once properly-typed adapters for component types are in hand, * getting properly-typed array adapters is straightforward. @@ -101,7 +111,7 @@ public static Iterator javaMultiArrayTest() " '{{1},{2}}'::int4[], " + " '{{{1,2,3}}}'::int2[], " + " '{{{{1},{2},{3}}}}'::\"char\"[], " + - " '{{{{{1,2,3}}}}}'::float8[], " + + " '{{{{{1,null,3}}}}}'::float8[], " + " '{{{{{{1},{2},{3}}}}}}'::float4[], " + " '{{{{{t},{f},{t}}}}}'::boolean[], " + " '{{{{''now''}}}}'::timestamptz[]"; @@ -133,6 +143,47 @@ public static Iterator javaMultiArrayTest() 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; + } + } + /** * A temporary test jig during TupleTableSlot development; intended * to be used from a debugger. From b1b7d13d8e05a118f6987e653a9357338c747709 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 31 Jul 2023 18:08:21 -0400 Subject: [PATCH 099/334] Another composing adapter, of reference type --- .../annotation/TupleTableSlotTest.java | 61 +++++++++++++++---- 1 file changed, 49 insertions(+), 12 deletions(-) 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 index 20b55e21d..69923dc16 100644 --- 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 @@ -20,6 +20,7 @@ import static java.util.Arrays.deepToString; import java.util.Iterator; import java.util.List; +import java.util.Optional; import java.time.OffsetDateTime; @@ -91,6 +92,12 @@ public static Iterator javaMultiArrayTest() */ float8 = new NullReplacingDouble(float8, Double.NaN); + /* + * Let's also compose AsOptional over the odt adapter, to get an adapter + * producing Optional instead of possibly nulls. + */ + As,?> oodt = new AsOptional<>(odt); + /* * Once properly-typed adapters for component types are in hand, * getting properly-typed array adapters is straightforward. @@ -103,18 +110,22 @@ public static Iterator javaMultiArrayTest() As< double[][][][][] ,?> f8x5 = float8.a4() .a1().build(); As< float[][][][][][] ,?> f4x6 = float4.a4().a2() .build(); As< boolean[][][][][] ,?> bx5 = bool .a4() .a1().build(); - As dtx4 = odt .a4() .build(); + As[][][][],?> dtx4 = oodt.a4() .build(); String query = - "SELECT" + + "VALUES (" + " '{1,2}'::int8[], " + " '{{1},{2}}'::int4[], " + " '{{{1,2,3}}}'::int2[], " + " '{{{{1},{2},{3}}}}'::\"char\"[], " + - " '{{{{{1,null,3}}}}}'::float8[], " + + " '{{{{{1,2,3}}}}}'::float8[], " + " '{{{{{{1},{2},{3}}}}}}'::float4[], " + " '{{{{{t},{f},{t}}}}}'::boolean[], " + - " '{{{{''now''}}}}'::timestamptz[]"; + " '{{{{''now''}}}}'::timestamptz[]" + + "), (" + + " NULL, NULL, NULL, NULL, '{{{{{1,NULL,3}}}}}', NULL, NULL," + + " '{{{{NULL}}}}'" + + ")"; List tups = t.test(query); @@ -125,14 +136,14 @@ public static Iterator javaMultiArrayTest() */ for ( TupleTableSlot tts : tups ) { - long [] v0 = tts.get(0, i8x1); - int [][] v1 = tts.get(1, i4x2); - short [][][] v2 = tts.get(2, i2x3); - byte [][][][] v3 = tts.get(3, i1x4); - double [][][][][] v4 = tts.get(4, f8x5); - float [][][][][][] v5 = tts.get(5, f4x6); - boolean [][][][][] v6 = tts.get(6, bx5); - OffsetDateTime [][][][] v7 = tts.get(7, dtx4); + long [] v0 = tts.get(0, i8x1); + int [][] v1 = tts.get(1, i4x2); + short [][][] v2 = tts.get(2, i2x3); + byte [][][][] v3 = tts.get(3, i1x4); + double [][][][][] v4 = tts.get(4, f8x5); + float [][][][][][] v5 = tts.get(5, f4x6); + boolean [][][][][] v6 = tts.get(6, bx5); + Optional [][][][] v7 = tts.get(7, dtx4); result.addAll(List.of( Arrays.toString(v0), deepToString(v1), deepToString(v2), @@ -184,6 +195,32 @@ public double adapt(Attribute a, double value) } } + /** + * Another example of a useful composing adapter that should eventually be + * part of a built-in set. + */ + public static class AsOptional extends As,T> + { + @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); + + AsOptional(As over) + { + super(config, over, null); + } + } + /** * A temporary test jig during TupleTableSlot development; intended * to be used from a debugger. From 41473089e1ff1c2c6072dd57efb6c55228aaccef Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 31 Jul 2023 18:27:03 -0400 Subject: [PATCH 100/334] A subtlety with canFetchNull is worth mentioning --- .../pljava/example/annotation/TupleTableSlotTest.java | 9 +++++++++ 1 file changed, 9 insertions(+) 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 index 69923dc16..ab6f0d4b0 100644 --- 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 @@ -201,6 +201,15 @@ public double adapt(Attribute a, double value) */ public static class AsOptional extends As,T> { + /* + * The default for a reference-typed leaf adapter is already true, but + * in case this adapter gets composed over some adapter that has chosen + * to return false for some reason, override here to say true. Clearly + * an AsOptional adapter is meant to be allowed to fetch null. + */ + @Override + public boolean canFetchNull() { return true; } + @Override public Optional fetchNull(Attribute a) { From 536e87fa47623cf5667c7bad55dcfdae680c476e Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 1 Aug 2023 10:25:15 -0400 Subject: [PATCH 101/334] Show autoboxing in Adapter composition Because this is supported, the open-item list entry for "a set composing over primitive adapters to return the boxed forms so they can be nullable" would be satisfied by writing one, very trivial, generically-typed Adapter. --- .../annotation/TupleTableSlotTest.java | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) 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 index ab6f0d4b0..05258c836 100644 --- 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 @@ -98,12 +98,24 @@ public static Iterator javaMultiArrayTest() */ As,?> oodt = new AsOptional<>(odt); + /* + * 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, just one absolutely trivial composing adapter could + * add that behavior over any primitive adapter. + */ + As,?> oint8 = new AsOptional<>(int8); + /* * Once properly-typed adapters for component types are in hand, * getting properly-typed array adapters is straightforward. * (Java 10+ var can reduce verbosity here.) */ - As< long[] ,?> i8x1 = int8 .a1().build(); + As[] ,?> i8x1 = oint8 .a1().build(); As< int[][] ,?> i4x2 = int4 .a2() .build(); As< short[][][] ,?> i2x3 = int2 .a2().a1().build(); As< byte[][][][] ,?> i1x4 = int1 .a4() .build(); @@ -136,7 +148,7 @@ public static Iterator javaMultiArrayTest() */ for ( TupleTableSlot tts : tups ) { - long [] v0 = tts.get(0, i8x1); + Optional [] v0 = tts.get(0, i8x1); int [][] v1 = tts.get(1, i4x2); short [][][] v2 = tts.get(2, i2x3); byte [][][][] v3 = tts.get(3, i1x4); @@ -224,7 +236,13 @@ public Optional adapt(Attribute a, T value) private static final Adapter.Configuration config = Adapter.configure(AsOptional.class, null); - AsOptional(As over) + /* + * 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); } From 3db8e546498f86dafa6b5d037173e882887b9104 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 1 Aug 2023 10:52:39 -0400 Subject: [PATCH 102/334] Tidy up that example SQL query In most other examples I have hewn to the less-abbreviated SQL CAST ( AS ) form. No reason to stop now. 'epoch' instead of 'now' for the timestamp will simplify basing a regression test on this example. --- .../example/annotation/TupleTableSlotTest.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) 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 index 05258c836..93fe13722 100644 --- 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 @@ -126,14 +126,14 @@ public static Iterator javaMultiArrayTest() String query = "VALUES (" + - " '{1,2}'::int8[], " + - " '{{1},{2}}'::int4[], " + - " '{{{1,2,3}}}'::int2[], " + - " '{{{{1},{2},{3}}}}'::\"char\"[], " + - " '{{{{{1,2,3}}}}}'::float8[], " + - " '{{{{{{1},{2},{3}}}}}}'::float4[], " + - " '{{{{{t},{f},{t}}}}}'::boolean[], " + - " '{{{{''now''}}}}'::timestamptz[]" + + " 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 timestamptz [] ) " + "), (" + " NULL, NULL, NULL, NULL, '{{{{{1,NULL,3}}}}}', NULL, NULL," + " '{{{{NULL}}}}'" + From 299b69f8f98fd21f4ebd92e83ec4c498bd16ed1f Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 1 Aug 2023 14:09:01 -0400 Subject: [PATCH 103/334] No, canFetchNull is not that subtle after all The "subtlety ... worth mentioning" in 4147308 was, in fact, a thinko, not a subtlety at all. A composing Adapter's canFetchNull does not involve delegation to the adapter composed over. Nulls are handled directly by the fetchNull method of the last-stacked adapter, and that's the only adapter whose canFetchNull matters. The default canFetchNull, if not overridden, simply returns true in any reference-typed adapter (subclass of As) or false in any subclass of Primitive. --- .../pljava/example/annotation/TupleTableSlotTest.java | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) 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 index 93fe13722..b433def03 100644 --- 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 @@ -213,14 +213,7 @@ public double adapt(Attribute a, double value) */ public static class AsOptional extends As,T> { - /* - * The default for a reference-typed leaf adapter is already true, but - * in case this adapter gets composed over some adapter that has chosen - * to return false for some reason, override here to say true. Clearly - * an AsOptional adapter is meant to be allowed to fetch null. - */ - @Override - public boolean canFetchNull() { return true; } + // canFetchNull isn't needed; its default in As is true. @Override public Optional fetchNull(Attribute a) From 8cbc383fa752af9ee6cbd2ccbccfc17c602bf672 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 2 Aug 2023 09:31:30 -0400 Subject: [PATCH 104/334] Example didn't demonstrate null long case It demonstrated that AsOptional could be composed over a primitive AsLong adapter, and return non-null values, but the query wasn't updated to supply a null array element to demonstrate the other case, which naturally also works, but the example ought to show it. --- .../pljava/example/annotation/TupleTableSlotTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index b433def03..7e643868f 100644 --- 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 @@ -135,7 +135,7 @@ public static Iterator javaMultiArrayTest() " CAST ( '{{{{{t},{f},{t}}}}}' AS boolean [] ), " + " CAST ( '{{{{''epoch''}}}}' AS timestamptz [] ) " + "), (" + - " NULL, NULL, NULL, NULL, '{{{{{1,NULL,3}}}}}', NULL, NULL," + + " '{NULL}', NULL, NULL, NULL, '{{{{{1,NULL,3}}}}}', NULL, NULL," + " '{{{{NULL}}}}'" + ")"; From 79a85693d17a3fbb378784d79af82f195cfccd38 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Thu, 3 Aug 2023 10:15:58 -0400 Subject: [PATCH 105/334] Add PG_VERSION_NUM to ModelConstants For supporting older PG versions, there may be no alternative to some version-dependent logic on the Java side. This might be freely used in the implementation module, and exposed in the API module (there could be user code that also wants to know), but more cautiously used within the API module itself, as it can only be available via the factory service from the implementation. It seems appropriate to have the version number at the very top of the constants, even though it means renumbering them all. --- pljava-so/src/main/c/ModelConstants.c | 2 + .../postgresql/pljava/pg/ModelConstants.java | 139 ++++++++++-------- 2 files changed, 76 insertions(+), 65 deletions(-) diff --git a/pljava-so/src/main/c/ModelConstants.c b/pljava-so/src/main/c/ModelConstants.c index 800a947bc..fdb2db5e0 100644 --- a/pljava-so/src/main/c/ModelConstants.c +++ b/pljava-so/src/main/c/ModelConstants.c @@ -81,6 +81,8 @@ CONSTANTEXPR(OFFSET_##tag##_##fld, offsetof(type,fld)) static int32 constants[] = { + CONSTANT(PG_VERSION_NUM), + CONSTANT(SIZEOF_DATUM), CONSTANTEXPR(SIZEOF_SIZE, sizeof (Size)), diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/ModelConstants.java b/pljava/src/main/java/org/postgresql/pljava/pg/ModelConstants.java index a78a890b1..395751c47 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/ModelConstants.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/ModelConstants.java @@ -110,97 +110,104 @@ private static class Natives * distinct small array indices; the checked() function in the static * initializer will be checking for gaps or repeats. */ - @Native private static final int IDX_SIZEOF_DATUM = 0; - @Native private static final int IDX_SIZEOF_SIZE = 1; + @Native private static final int IDX_PG_VERSION_NUM = 0; - @Native private static final int IDX_ALIGNOF_SHORT = 2; - @Native private static final int IDX_ALIGNOF_INT = 3; - @Native private static final int IDX_ALIGNOF_DOUBLE = 4; - @Native private static final int IDX_MAXIMUM_ALIGNOF = 5; + @Native private static final int IDX_SIZEOF_DATUM = 1; + @Native private static final int IDX_SIZEOF_SIZE = 2; - @Native private static final int IDX_NAMEDATALEN = 6; + @Native private static final int IDX_ALIGNOF_SHORT = 3; + @Native private static final int IDX_ALIGNOF_INT = 4; + @Native private static final int IDX_ALIGNOF_DOUBLE = 5; + @Native private static final int IDX_MAXIMUM_ALIGNOF = 6; - @Native private static final int IDX_SIZEOF_varatt_indirect = 7; - @Native private static final int IDX_SIZEOF_varatt_expanded = 8; - @Native private static final int IDX_SIZEOF_varatt_external = 9; + @Native private static final int IDX_NAMEDATALEN = 7; - @Native private static final int IDX_OFFSET_TTS_NVALID = 10; - @Native private static final int IDX_SIZEOF_TTS_NVALID = 11; + @Native private static final int IDX_SIZEOF_varatt_indirect = 8; + @Native private static final int IDX_SIZEOF_varatt_expanded = 9; + @Native private static final int IDX_SIZEOF_varatt_external = 10; - @Native private static final int IDX_TTS_FLAG_EMPTY = 12; - @Native private static final int IDX_TTS_FLAG_FIXED = 13; - @Native private static final int IDX_OFFSET_TTS_FLAGS = 14; + @Native private static final int IDX_OFFSET_TTS_NVALID = 11; + @Native private static final int IDX_SIZEOF_TTS_NVALID = 12; + + @Native private static final int IDX_TTS_FLAG_EMPTY = 13; + @Native private static final int IDX_TTS_FLAG_FIXED = 14; + @Native private static final int IDX_OFFSET_TTS_FLAGS = 15; /* * 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 = 15; - @Native private static final int IDX_OFFSET_TTS_FIXED = 16; - @Native private static final int IDX_OFFSET_TTS_TABLEOID = 17; - - @Native private static final int IDX_SIZEOF_FORM_PG_ATTRIBUTE = 18; - @Native private static final int IDX_ATTRIBUTE_FIXED_PART_SIZE = 19; - @Native private static final int IDX_CLASS_TUPLE_SIZE = 20; - @Native private static final int IDX_HEAPTUPLESIZE = 21; - - @Native private static final int IDX_OFFSET_TUPLEDESC_ATTRS = 22; - @Native private static final int IDX_OFFSET_TUPLEDESC_TDREFCOUNT = 23; - @Native private static final int IDX_SIZEOF_TUPLEDESC_TDREFCOUNT = 24; - @Native private static final int IDX_OFFSET_TUPLEDESC_TDTYPEID = 25; - @Native private static final int IDX_OFFSET_TUPLEDESC_TDTYPMOD = 26; - - @Native private static final int IDX_OFFSET_pg_attribute_atttypid = 27; - @Native private static final int IDX_OFFSET_pg_attribute_attlen = 28; - @Native private static final int IDX_OFFSET_pg_attribute_attcacheoff = 29; - @Native private static final int IDX_OFFSET_pg_attribute_atttypmod = 30; - @Native private static final int IDX_OFFSET_pg_attribute_attbyval = 31; - @Native private static final int IDX_OFFSET_pg_attribute_attalign = 32; - @Native private static final int IDX_OFFSET_pg_attribute_attnotnull = 33; - @Native private static final int IDX_OFFSET_pg_attribute_attisdropped = 34; - - @Native private static final int IDX_Anum_pg_class_reltype = 35; - - @Native private static final int IDX_SIZEOF_MCTX = 36; - @Native private static final int IDX_OFFSET_MCTX_isReset = 37; - @Native private static final int IDX_OFFSET_MCTX_mem_allocated = 38; - @Native private static final int IDX_OFFSET_MCTX_parent = 39; - @Native private static final int IDX_OFFSET_MCTX_firstchild = 40; - @Native private static final int IDX_OFFSET_MCTX_prevchild = 41; - @Native private static final int IDX_OFFSET_MCTX_nextchild = 42; - @Native private static final int IDX_OFFSET_MCTX_name = 43; - @Native private static final int IDX_OFFSET_MCTX_ident = 44; + @Native private static final int IDX_OFFSET_TTS_EMPTY = 16; + @Native private static final int IDX_OFFSET_TTS_FIXED = 17; + @Native private static final int IDX_OFFSET_TTS_TABLEOID = 18; + + @Native private static final int IDX_SIZEOF_FORM_PG_ATTRIBUTE = 19; + @Native private static final int IDX_ATTRIBUTE_FIXED_PART_SIZE = 20; + @Native private static final int IDX_CLASS_TUPLE_SIZE = 21; + @Native private static final int IDX_HEAPTUPLESIZE = 22; + + @Native private static final int IDX_OFFSET_TUPLEDESC_ATTRS = 23; + @Native private static final int IDX_OFFSET_TUPLEDESC_TDREFCOUNT = 24; + @Native private static final int IDX_SIZEOF_TUPLEDESC_TDREFCOUNT = 25; + @Native private static final int IDX_OFFSET_TUPLEDESC_TDTYPEID = 26; + @Native private static final int IDX_OFFSET_TUPLEDESC_TDTYPMOD = 27; + + @Native private static final int IDX_OFFSET_pg_attribute_atttypid = 28; + @Native private static final int IDX_OFFSET_pg_attribute_attlen = 29; + @Native private static final int IDX_OFFSET_pg_attribute_attcacheoff = 30; + @Native private static final int IDX_OFFSET_pg_attribute_atttypmod = 31; + @Native private static final int IDX_OFFSET_pg_attribute_attbyval = 32; + @Native private static final int IDX_OFFSET_pg_attribute_attalign = 33; + @Native private static final int IDX_OFFSET_pg_attribute_attnotnull = 34; + @Native private static final int IDX_OFFSET_pg_attribute_attisdropped = 35; + + @Native private static final int IDX_Anum_pg_class_reltype = 36; + + @Native private static final int IDX_SIZEOF_MCTX = 37; + @Native private static final int IDX_OFFSET_MCTX_isReset = 38; + @Native private static final int IDX_OFFSET_MCTX_mem_allocated = 39; + @Native private static final int IDX_OFFSET_MCTX_parent = 40; + @Native private static final int IDX_OFFSET_MCTX_firstchild = 41; + @Native private static final int IDX_OFFSET_MCTX_prevchild = 42; + @Native private static final int IDX_OFFSET_MCTX_nextchild = 43; + @Native private static final int IDX_OFFSET_MCTX_name = 44; + @Native private static final int IDX_OFFSET_MCTX_ident = 45; /* * 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 = 45; - @Native private static final int IDX_AUTHMEMMEMROLE = 46; - @Native private static final int IDX_AUTHMEMROLEMEM = 47; - @Native private static final int IDX_AUTHOID = 48; - @Native private static final int IDX_COLLOID = 49; - @Native private static final int IDX_DATABASEOID = 50; - @Native private static final int IDX_LANGOID = 51; - @Native private static final int IDX_NAMESPACEOID = 52; - @Native private static final int IDX_OPEROID = 53; - @Native private static final int IDX_PROCOID = 54; - @Native private static final int IDX_RELOID = 55; - @Native private static final int IDX_TSCONFIGOID = 56; - @Native private static final int IDX_TSDICTOID = 57; - @Native private static final int IDX_TYPEOID = 58; + @Native private static final int IDX_ATTNUM = 46; + @Native private static final int IDX_AUTHMEMMEMROLE = 47; + @Native private static final int IDX_AUTHMEMROLEMEM = 48; + @Native private static final int IDX_AUTHOID = 49; + @Native private static final int IDX_COLLOID = 50; + @Native private static final int IDX_DATABASEOID = 51; + @Native private static final int IDX_LANGOID = 52; + @Native private static final int IDX_NAMESPACEOID = 53; + @Native private static final int IDX_OPEROID = 54; + @Native private static final int IDX_PROCOID = 55; + @Native private static final int IDX_RELOID = 56; + @Native private static final int IDX_TSCONFIGOID = 57; + @Native private static final int IDX_TSDICTOID = 58; + @Native private static final int IDX_TYPEOID = 59; /* * 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 = 59; + @Native private static final int IDX_N_ACL_RIGHTS = 60; /* * These public statics are the values of interest, set at class * initialization time by reading them from the buffer returned by _statics. */ + /** + * Numeric PostgreSQL version compiled in at build time. + */ + public static final int PG_VERSION_NUM; + public static final int SIZEOF_DATUM; public static final int SIZEOF_SIZE; @@ -299,6 +306,8 @@ private static class Natives Natives._statics() .asReadOnlyBuffer().order(nativeOrder()).asIntBuffer(); + PG_VERSION_NUM = checked(b, IDX_PG_VERSION_NUM); + SIZEOF_DATUM = checked(b, IDX_SIZEOF_DATUM); SIZEOF_SIZE = checked(b, IDX_SIZEOF_SIZE); From 716eac5cea5048b1ae0e68faf118e04009067599 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Thu, 3 Aug 2023 10:32:13 -0400 Subject: [PATCH 106/334] Let formObjectId accept a version predicate For compatibility with PG versions older than what is modeled, an object will still be constructed (the API interfaces cannot very well change what statics they export), but with InvalidOid instead of the oid assigned to that object in the later PostgreSQL version. So, the object can be retrieved from the API static, but isValid() will be false, which is about the right thing to see happen in a PG version that hasn't got such an object. Also have OidAdapter.Addressed avoid claiming it canFetch a type with isValid() false; this is the adapter most likely to be affected by PG types (of the reg... variety) absent in older PG versions. (An argument could be made that Adapter should be testing for this, universally.) --- .../pljava/model/CatalogObject.java | 14 +++++++++++-- .../pljava/pg/CatalogObjectImpl.java | 21 ++++++++++++++----- .../postgresql/pljava/pg/adt/OidAdapter.java | 7 +++++-- 3 files changed, 33 insertions(+), 9 deletions(-) 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 index ec97b89bc..bd58132aa 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/model/CatalogObject.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/model/CatalogObject.java @@ -15,6 +15,8 @@ import java.util.ServiceConfigurationError; import java.util.ServiceLoader; +import java.util.function.IntPredicate; + import org.postgresql.pljava.sqlgen.Lexicals.Identifier; /** @@ -470,7 +472,14 @@ RegClass.Known formClassId(int classId, Class clazz) static > T formObjectId(RegClass.Known classId, int objId) { - return INSTANCE.formObjectIdImpl(classId, 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) @@ -488,7 +497,8 @@ RegClass.Known formClassIdImpl( int classId, Class clazz); protected abstract > - T formObjectIdImpl(RegClass.Known classId, int objId); + T formObjectIdImpl( + RegClass.Known classId, int objId, IntPredicate versionTest); protected abstract Database currentDatabaseImpl(RegClass.Known classId); diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/CatalogObjectImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/CatalogObjectImpl.java index c6c7b27f7..3512479fb 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/CatalogObjectImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/CatalogObjectImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * 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 @@ -30,6 +30,7 @@ import static org.postgresql.pljava.model.MemoryContext.JavaMemoryContext; import static org.postgresql.pljava.pg.MemoryContextImpl.allocatingIn; +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; @@ -60,6 +61,7 @@ import java.util.Set; import java.util.Optional; import java.util.function.Consumer; +import java.util.function.IntPredicate; import java.util.function.UnaryOperator; import java.util.function.Supplier; @@ -240,9 +242,10 @@ public Factory() { } @Override protected > - T formObjectIdImpl(RegClass.Known classId, int objId) + T formObjectIdImpl( + RegClass.Known classId, int objId, IntPredicate versionTest) { - return staticFormObjectId(classId, objId); + return staticFormObjectId(classId, objId, versionTest); } @Override @@ -308,11 +311,19 @@ protected MemoryContext upperMemoryContext() return (RegClass.Known)form(RelationRelationId, classId, 0); } - @SuppressWarnings("unchecked") static > T staticFormObjectId(RegClass.Known classId, int objId) { - return (T)form(classId.oid(), objId, 0); + 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") 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 index 38a149a68..f73c69900 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/adt/OidAdapter.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/adt/OidAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * 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 @@ -14,6 +14,8 @@ 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; @@ -171,7 +173,8 @@ private Addressed( { super(c, witness); m_classId = classId; - m_specificTypes = specificTypes; + m_specificTypes = stream(specificTypes) + .filter(RegType::isValid).toArray(RegType[]::new); } @Override From 3887c0548d83649c2079c59f9ddba5c314afc91d Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Fri, 11 Aug 2023 22:00:58 -0400 Subject: [PATCH 107/334] Fix an unchecked warning in array service and another one in the example code --- .../pljava/example/annotation/TupleTableSlotTest.java | 1 + .../src/main/java/org/postgresql/pljava/pg/adt/Service.java | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) 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 index 7e643868f..e3bc87f9a 100644 --- 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 @@ -78,6 +78,7 @@ public static Iterator javaMultiArrayTest() AsBoolean bool = (AsBoolean)t.adapterPlease( "org.postgresql.pljava.pg.adt.Primitives", "BOOLEAN_INSTANCE"); + @SuppressWarnings("unchecked") As odt = (As)t.adapterPlease( "org.postgresql.pljava.pg.adt.DateTimeAdapter$JSR310", "TIMESTAMPTZ_INSTANCE"); 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 index 50fd2b064..0aad7fec7 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/adt/Service.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/adt/Service.java @@ -280,8 +280,11 @@ else if ( componentAdapter instanceof AsBoolean ) } else if ( componentAdapter instanceof As ) { + @SuppressWarnings("unchecked") + As erasedComponent = (As)componentAdapter; + return new ArrayAdapter( - (As)componentAdapter, w, + erasedComponent, w, (int nDims, int[] dimsAndBounds, As adapter, Indexed slot) -> { From 786d5462b1900aedf12b3d23fefa41afe6fed27b Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 12 Aug 2023 19:18:52 -0400 Subject: [PATCH 108/334] Javadoc: typos in MultiArray, tweaks in Adapter --- .../java/org/postgresql/pljava/Adapter.java | 5 ++-- .../pljava/adt/spi/AbstractType.java | 28 ++++++++++--------- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/pljava-api/src/main/java/org/postgresql/pljava/Adapter.java b/pljava-api/src/main/java/org/postgresql/pljava/Adapter.java index 1803e6b81..d53f6446b 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/Adapter.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/Adapter.java @@ -1788,6 +1788,7 @@ protected Adapter adapter(ArrayBuilder builder) *

    * 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 { @@ -1876,8 +1877,8 @@ public static final class ArrayBuilder /** * Returns an array adapter that will produce arrays with the chosen - * number of dimensions, and this adapter's {@link #topType topType} as - * the component type. + * number of dimensions, and the original adapter's + * {@link #topType() topType} as the component type. */ public Array build() { 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 index af2124253..a54d495d4 100644 --- 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 @@ -839,11 +839,12 @@ Type substitute(TypeVariable v) * {@code MultiArray}'s dimensions. Parentheses are used by * {@link #toString toString} to help see what's going on. *

    - * When converting a {@code MultiArray} toa {@link Sized}, only as many - * sizes are supplied as the multiarray's dimensions, and when converting - * that to an {@link 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. + * 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 { @@ -1003,8 +1004,8 @@ public MultiArray refine(Type model) } /** - * Returns a {@link Sized} representing this {@code MultiArray} with - * a size for each of its dimensions. + * Returns a {@link Sized Sized} representing this {@code MultiArray} + * with a size for each of its dimensions. */ public Sized size(int... dims) { @@ -1035,8 +1036,8 @@ public String toString() } /** - * Returns an {@link Allocated} that wraps a freshly-allocated array - * with the sizes recorded here. + * 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 @@ -1058,10 +1059,11 @@ public Allocated allocate() * 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. 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}. + * 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 { From 566edcb7ea9da441bbc7f57d0ffefabbeb86c8bd Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 14 Aug 2023 20:14:01 -0400 Subject: [PATCH 109/334] Get rid of "throws SQLException" on tts.get They were (mostly) getting wrapped, anyway. With nothing remembering to unwrap them.... The get() methods in question should probably be evicted from public API, and kept only as internal methods (perhaps renamed) that a better TargetList API will use under the hood. If there are to be one-shot get methods like these in the API, they can be wrappers that catch the wrapping RuntimeExceptions and throw the original checked exceptions again. --- .../java/org/postgresql/pljava/Adapter.java | 50 +++++++-------- .../pljava/model/TupleTableSlot.java | 61 +++++++++++-------- .../annotation/TupleTableSlotTest.java | 3 +- .../pljava/pg/TupleTableSlotImpl.java | 36 +++++------ 4 files changed, 79 insertions(+), 71 deletions(-) diff --git a/pljava-api/src/main/java/org/postgresql/pljava/Adapter.java b/pljava-api/src/main/java/org/postgresql/pljava/Adapter.java index d53f6446b..636e9a757 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/Adapter.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/Adapter.java @@ -790,7 +790,7 @@ public boolean canFetchNull() *

    * If not overridden, this implementation returns Java null. */ - public T fetchNull(Attribute a) throws SQLException + public T fetchNull(Attribute a) { return null; } @@ -1111,10 +1111,10 @@ public final long fetch( * {@code SQLDataException} with {@code SQLSTATE 22002}, * {@code null_value_no_indicator_parameter}. */ - public long fetchNull(Attribute a) throws SQLException + public long fetchNull(Attribute a) { - throw new SQLDataException( - "SQL NULL cannot be returned as Java long", "22002"); + throw wrapped(new SQLDataException( + "SQL NULL cannot be returned as Java long", "22002")); } /** @@ -1179,10 +1179,10 @@ public final double fetch( * {@code SQLDataException} with {@code SQLSTATE 22002}, * {@code null_value_no_indicator_parameter}. */ - public double fetchNull(Attribute a) throws SQLException + public double fetchNull(Attribute a) { - throw new SQLDataException( - "SQL NULL cannot be returned as Java double", "22002"); + throw wrapped(new SQLDataException( + "SQL NULL cannot be returned as Java double", "22002")); } } @@ -1223,10 +1223,10 @@ public final int fetch( * {@code SQLDataException} with {@code SQLSTATE 22002}, * {@code null_value_no_indicator_parameter}. */ - public int fetchNull(Attribute a) throws SQLException + public int fetchNull(Attribute a) { - throw new SQLDataException( - "SQL NULL cannot be returned as Java int", "22002"); + throw wrapped(new SQLDataException( + "SQL NULL cannot be returned as Java int", "22002")); } /** @@ -1290,10 +1290,10 @@ public final float fetch( * {@code SQLDataException} with {@code SQLSTATE 22002}, * {@code null_value_no_indicator_parameter}. */ - public float fetchNull(Attribute a) throws SQLException + public float fetchNull(Attribute a) { - throw new SQLDataException( - "SQL NULL cannot be returned as Java float", "22002"); + throw wrapped(new SQLDataException( + "SQL NULL cannot be returned as Java float", "22002")); } } @@ -1334,10 +1334,10 @@ public final short fetch( * {@code SQLDataException} with {@code SQLSTATE 22002}, * {@code null_value_no_indicator_parameter}. */ - public short fetchNull(Attribute a) throws SQLException + public short fetchNull(Attribute a) { - throw new SQLDataException( - "SQL NULL cannot be returned as Java short", "22002"); + throw wrapped(new SQLDataException( + "SQL NULL cannot be returned as Java short", "22002")); } /** @@ -1401,10 +1401,10 @@ public final char fetch( * {@code SQLDataException} with {@code SQLSTATE 22002}, * {@code null_value_no_indicator_parameter}. */ - public char fetchNull(Attribute a) throws SQLException + public char fetchNull(Attribute a) { - throw new SQLDataException( - "SQL NULL cannot be returned as Java char", "22002"); + throw wrapped(new SQLDataException( + "SQL NULL cannot be returned as Java char", "22002")); } } @@ -1445,10 +1445,10 @@ public final byte fetch( * {@code SQLDataException} with {@code SQLSTATE 22002}, * {@code null_value_no_indicator_parameter}. */ - public byte fetchNull(Attribute a) throws SQLException + public byte fetchNull(Attribute a) { - throw new SQLDataException( - "SQL NULL cannot be returned as Java byte", "22002"); + throw wrapped(new SQLDataException( + "SQL NULL cannot be returned as Java byte", "22002")); } /** @@ -1513,10 +1513,10 @@ public final boolean fetch( * {@code SQLDataException} with {@code SQLSTATE 22002}, * {@code null_value_no_indicator_parameter}. */ - public boolean fetchNull(Attribute a) throws SQLException + public boolean fetchNull(Attribute a) { - throw new SQLDataException( - "SQL NULL cannot be returned as Java boolean", "22002"); + throw wrapped(new SQLDataException( + "SQL NULL cannot be returned as Java boolean", "22002")); } } 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 index 8447f003d..ba492f308 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/model/TupleTableSlot.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/model/TupleTableSlot.java @@ -50,67 +50,74 @@ public interface TupleTableSlot TupleDescriptor descriptor(); RegClass relation(); - T get(Attribute att, As adapter) throws SQLException; - long get(Attribute att, AsLong adapter) throws SQLException; - double get(Attribute att, AsDouble adapter) throws SQLException; - int get(Attribute att, AsInt adapter) throws SQLException; - float get(Attribute att, AsFloat adapter) throws SQLException; - short get(Attribute att, AsShort adapter) throws SQLException; - char get(Attribute att, AsChar adapter) throws SQLException; - byte get(Attribute att, AsByte adapter) throws SQLException; - boolean get(Attribute att, AsBoolean adapter) throws SQLException; + /* + * 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) throws SQLException; - long get(int idx, AsLong adapter) throws SQLException; - double get(int idx, AsDouble adapter) throws SQLException; - int get(int idx, AsInt adapter) throws SQLException; - float get(int idx, AsFloat adapter) throws SQLException; - short get(int idx, AsShort adapter) throws SQLException; - char get(int idx, AsChar adapter) throws SQLException; - byte get(int idx, AsByte adapter) throws SQLException; - boolean get(int idx, AsBoolean adapter) throws SQLException; + 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) throws SQLException + default T sqlGet(int idx, As adapter) { return get(idx - 1, adapter); } - default long sqlGet(int idx, AsLong adapter) throws SQLException + default long sqlGet(int idx, AsLong adapter) { return get(idx - 1, adapter); } - default double sqlGet(int idx, AsDouble adapter) throws SQLException + default double sqlGet(int idx, AsDouble adapter) { return get(idx - 1, adapter); } - default int sqlGet(int idx, AsInt adapter) throws SQLException + default int sqlGet(int idx, AsInt adapter) { return get(idx - 1, adapter); } - default float sqlGet(int idx, AsFloat adapter) throws SQLException + default float sqlGet(int idx, AsFloat adapter) { return get(idx - 1, adapter); } - default short sqlGet(int idx, AsShort adapter) throws SQLException + default short sqlGet(int idx, AsShort adapter) { return get(idx - 1, adapter); } - default char sqlGet(int idx, AsChar adapter) throws SQLException + default char sqlGet(int idx, AsChar adapter) { return get(idx - 1, adapter); } - default byte sqlGet(int idx, AsByte adapter) throws SQLException + default byte sqlGet(int idx, AsByte adapter) { return get(idx - 1, adapter); } - default boolean sqlGet(int idx, AsBoolean adapter) throws SQLException + default boolean sqlGet(int idx, AsBoolean adapter) { return get(idx - 1, adapter); } 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 index e3bc87f9a..091a6b798 100644 --- 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 @@ -25,6 +25,7 @@ import java.time.OffsetDateTime; 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; @@ -340,7 +341,7 @@ else if ( a instanceof AsBoolean ) case 8: ll = tts.get(att, adpL); break; } } - catch ( SQLException e ) + catch ( AdapterException e ) { System.out.println(e); } diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/TupleTableSlotImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/TupleTableSlotImpl.java index 31b45abed..f0b0e9f03 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/TupleTableSlotImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/TupleTableSlotImpl.java @@ -807,7 +807,7 @@ private static native void _store_heaptuple( private static native List _testmeSPI(); @Override - public T get(Attribute att, As adapter) throws SQLException + public T get(Attribute att, As adapter) { int idx = toIndex(att, adapter); @@ -819,7 +819,7 @@ public T get(Attribute att, As adapter) throws SQLException } @Override - public long get(Attribute att, AsLong adapter) throws SQLException + public long get(Attribute att, AsLong adapter) { int idx = toIndex(att, adapter); @@ -831,7 +831,7 @@ public long get(Attribute att, AsLong adapter) throws SQLException } @Override - public double get(Attribute att, AsDouble adapter) throws SQLException + public double get(Attribute att, AsDouble adapter) { int idx = toIndex(att, adapter); @@ -843,7 +843,7 @@ public double get(Attribute att, AsDouble adapter) throws SQLException } @Override - public int get(Attribute att, AsInt adapter) throws SQLException + public int get(Attribute att, AsInt adapter) { int idx = toIndex(att, adapter); @@ -855,7 +855,7 @@ public int get(Attribute att, AsInt adapter) throws SQLException } @Override - public float get(Attribute att, AsFloat adapter) throws SQLException + public float get(Attribute att, AsFloat adapter) { int idx = toIndex(att, adapter); @@ -867,7 +867,7 @@ public float get(Attribute att, AsFloat adapter) throws SQLException } @Override - public short get(Attribute att, AsShort adapter) throws SQLException + public short get(Attribute att, AsShort adapter) { int idx = toIndex(att, adapter); @@ -879,7 +879,7 @@ public short get(Attribute att, AsShort adapter) throws SQLException } @Override - public char get(Attribute att, AsChar adapter) throws SQLException + public char get(Attribute att, AsChar adapter) { int idx = toIndex(att, adapter); @@ -891,7 +891,7 @@ public char get(Attribute att, AsChar adapter) throws SQLException } @Override - public byte get(Attribute att, AsByte adapter) throws SQLException + public byte get(Attribute att, AsByte adapter) { int idx = toIndex(att, adapter); @@ -903,7 +903,7 @@ public byte get(Attribute att, AsByte adapter) throws SQLException } @Override - public boolean get(Attribute att, AsBoolean adapter) throws SQLException + public boolean get(Attribute att, AsBoolean adapter) { int idx = toIndex(att, adapter); @@ -915,7 +915,7 @@ public boolean get(Attribute att, AsBoolean adapter) throws SQLException } @Override - public T get(int idx, As adapter) throws SQLException + public T get(int idx, As adapter) { Attribute att = fromIndex(idx, adapter); @@ -927,7 +927,7 @@ public T get(int idx, As adapter) throws SQLException } @Override - public long get(int idx, AsLong adapter) throws SQLException + public long get(int idx, AsLong adapter) { Attribute att = fromIndex(idx, adapter); @@ -939,7 +939,7 @@ public long get(int idx, AsLong adapter) throws SQLException } @Override - public double get(int idx, AsDouble adapter) throws SQLException + public double get(int idx, AsDouble adapter) { Attribute att = fromIndex(idx, adapter); @@ -951,7 +951,7 @@ public double get(int idx, AsDouble adapter) throws SQLException } @Override - public int get(int idx, AsInt adapter) throws SQLException + public int get(int idx, AsInt adapter) { Attribute att = fromIndex(idx, adapter); @@ -963,7 +963,7 @@ public int get(int idx, AsInt adapter) throws SQLException } @Override - public float get(int idx, AsFloat adapter) throws SQLException + public float get(int idx, AsFloat adapter) { Attribute att = fromIndex(idx, adapter); @@ -975,7 +975,7 @@ public float get(int idx, AsFloat adapter) throws SQLException } @Override - public short get(int idx, AsShort adapter) throws SQLException + public short get(int idx, AsShort adapter) { Attribute att = fromIndex(idx, adapter); @@ -987,7 +987,7 @@ public short get(int idx, AsShort adapter) throws SQLException } @Override - public char get(int idx, AsChar adapter) throws SQLException + public char get(int idx, AsChar adapter) { Attribute att = fromIndex(idx, adapter); @@ -999,7 +999,7 @@ public char get(int idx, AsChar adapter) throws SQLException } @Override - public byte get(int idx, AsByte adapter) throws SQLException + public byte get(int idx, AsByte adapter) { Attribute att = fromIndex(idx, adapter); @@ -1011,7 +1011,7 @@ public byte get(int idx, AsByte adapter) throws SQLException } @Override - public boolean get(int idx, AsBoolean adapter) throws SQLException + public boolean get(int idx, AsBoolean adapter) { Attribute att = fromIndex(idx, adapter); From 091d8bc96b72828529e7cb77f3815e29203096cd Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 14 Aug 2023 20:14:30 -0400 Subject: [PATCH 110/334] Enter TargetList and its subinterface, Projection The earlier, just-get-something-going API on TupleTableSlot requires the caller to get one value at a time by passing an Attribute and an Adapter to a get() method. That has several practical disadvantages. Passing an Adapter to each get() method is essentially unavoidable; any API that wants to use Java genericity so the values' types can be known at compile time will need to be designed so that the (typed) Adapter the calling code has selected appears as an argument to a method. That creates the possibility, unlikely though it may be, that the calling code could supply different Adapters on different calls, even for the same attribute. There is non-trivial work to check a supplied Adapter's canFetch() against the Attribute / RegType being fetched, and select the corresponding Datum.Accessor. Likewise, the Attribute used in the call must be checked for belonging to the slot's TupleDescriptor. To repeat those checks on every get() call would be to spend cycles needed for a highly unusual use case, while in the common case the caller will be choosing one Attribute and one Adapter per result column and using those consistently. Of course the work can be cached and reused as long as the same Attribute and Adapter are seen, but the question arises where to keep the cache. For now, it is with the TupleTableSlot. That works well enough when PostgreSQL supplies a set of tuples in a form, like SPITupleTable, in which they are bare, and can be associated one-by-one with a single persistent TupleTableSlot. However, some other PostgreSQL APIs exist, and may be worth supporting at some point, in which each tuple arrives in its own (native PG) TupleTableSlot. How to handle that will present questions for another time (creating a fresh Java TupleTableSlot for each native PG one sounds unappealing), but it seems worthwhile to hoist the aforementioned checking and caching out of TupleTableSlot, and into some other class dedicated to selecting the attributes wanted, once, in advance of processing the tuples. Such a class can also lighten the weight of TupleDescriptor. In an API based on one-at-a-time get() calls, which might identify an attribute by name, there is strong incentive for TupleDescriptor to carry a hashtable or the like, just to reduce the cost of repeated name lookups. Again, the cost incurred might be needed in unusual cases, even though in the usual case the same programmer writing the Java code also wrote the query, and probably wants most of those columns in mostly that order. A completely simpleminded method to select several Attributes at once can offer linear behavior for that case without lugging extra data structures around. To that end, enter TargetList and its subinterface, Projection. As in the algebraic usage, Projection is a sequence of Attribute with none appearing more than once. TupleDescriptor is-a Projection, of course, and the only way to obtain another Projection is from an existing one by reordering its attributes or eliminating some. Its project(...) methods allow doing that by name or by index. As a result, the "attributes all belong to the original descriptor" condition is satisfied by construction. The superinterface TargetList relaxes the nonrepetition condition; attributes can appear more than once in a TargetList. That should sound less efficient than simply mentioning the attribute once and letting the Java code copy the fetched value, but there may be cases where it is useful. One would be when the Java code wants different representations of the PG value, produced by different Adapters. Another can be when the Java representation is a one-use-only class like SQLXML. Once a TupleDescriptor has been molded into the Projection (or, more generally, TargetList) with just the attributes the Java code wants to retrieve, the TargetList can be applied over a List of TupleTableSlot at a time, retrieving rows with methods that take a number of Adapters at once, and a lambda with corresponding parameters, taking their compile-time types from the Adapters. Functional interfaces of some likely lengths are provided, and a Cursor object is supplied, both to permit iterating over the rows and to permit the currying of lambda bodies that fit the various available apply(...) methods, in order to fit a TargetList with any number and types of columns. This commit does not apply any of the optimizations contemplated above; the implementation, for now, calls the same old heavyweight get methods on TupleTableSlot. It will, however, pave the way, and those get methods should eventually be deprecated. --- .../org/postgresql/pljava/TargetList.java | 893 ++++++++++++ .../pljava/model/TupleDescriptor.java | 5 +- .../postgresql/pljava/pg/AttributeImpl.java | 5 +- .../postgresql/pljava/pg/TargetListImpl.java | 1232 +++++++++++++++++ .../postgresql/pljava/pg/TupleDescImpl.java | 53 +- 5 files changed, 2183 insertions(+), 5 deletions(-) create mode 100644 pljava-api/src/main/java/org/postgresql/pljava/TargetList.java create mode 100644 pljava/src/main/java/org/postgresql/pljava/pg/TargetListImpl.java 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..8de1db819 --- /dev/null +++ b/pljava-api/src/main/java/org/postgresql/pljava/TargetList.java @@ -0,0 +1,893 @@ +/* + * 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.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); + + /** + * 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; + + /** + * 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; + + /** + * 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. Naturally, this method does + * not return a parallel {@code Stream}. + *

    + * 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/model/TupleDescriptor.java b/pljava-api/src/main/java/org/postgresql/pljava/model/TupleDescriptor.java index ad59eefef..2c706deb3 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/model/TupleDescriptor.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/model/TupleDescriptor.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * 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 @@ -16,6 +16,7 @@ import java.util.List; +import org.postgresql.pljava.TargetList.Projection; import org.postgresql.pljava.sqlgen.Lexicals.Identifier.Simple; /** @@ -36,7 +37,7 @@ * {@code TupleDescriptor}, it will probably be more natural to make them * available by methods on {@code Attribute}. */ -public interface TupleDescriptor +public interface TupleDescriptor extends Projection { List attributes(); diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/AttributeImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/AttributeImpl.java index ef61a2e96..05c6dae1e 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/AttributeImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/AttributeImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * 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 @@ -660,7 +660,8 @@ public TupleDescriptor containingTupleDescriptor() boolean foundIn(TupleDescriptor td) { - return this == td.attributes().get(subId() - 1); + int idx = subId() - 1; + return ( idx < td.size() ) && ( this == td.get(idx) ); } /** 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..943150d5f --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/pg/TargetListImpl.java @@ -0,0 +1,1232 @@ +/* + * 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.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.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 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 + { + return TargetListImpl.applyOver(this, tuples, f); + } + + @Override // TargetList + public R applyOver( + TupleTableSlot tuple, Cursor.Function f) + throws X + { + 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(); + + if ( names.length > n ) + throw new IllegalArgumentException(String.format( + "project() more than %d attribute names supplied", n)); + + 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; + + if ( names.length > n ) + throw new IllegalArgumentException(String.format( + "project() more than %d attribute names supplied", n)); + + 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 + { + return f.apply(new CursorImpl(tl, tuples)); + } + + static R applyOver( + TargetList tl, TupleTableSlot tuple, Cursor.Function f) + throws X + { + return f.apply(new CursorImpl(tl, tuple)); + } + + 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; + + if ( m_slots instanceof Collection ) + spl = Spliterators + .spliterator(itr, ((Collection)m_slots).size(), chr); + else + spl = spliteratorUnknownSize(itr, 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 index e282c928f..55fcc8c3d 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/TupleDescImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/TupleDescImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * 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 @@ -20,6 +20,7 @@ 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; @@ -80,6 +81,56 @@ abstract class TupleDescImpl extends AbstractList private final Map m_byName; 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 + { + return TargetListImpl.applyOver(this, tuples, f); + } + + @Override // TargetList + public R applyOver( + TupleTableSlot tuple, Cursor.Function f) + throws X + { + 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, From 1c96e3a23835d3de235af6197507f8d00abb3891 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 14 Aug 2023 20:15:00 -0400 Subject: [PATCH 111/334] Start on unwrapping ResultSet as Portal The preliminary SlotTester.test method taking just a simple string query is too limiting for much useful testing. The ability to construct a query with parameters may be sorely missed. In a transitional period while the new API is incomplete (and read-only) and the legacy JDBC implementation is still present, it would be useful to allow queries (including parameterized ones) to be issued using the legacy JDBC mechanisms, and then the results processed with the new API. To that end, allow SlotTester.unwrapAsPortal to be applied to the ResultSet obtained from a JDBC query (as long as the ResultSet has not been used to fetch anything yet). The Portal instance will then support retrieving the tuples in the new API. --- .../org/postgresql/pljava/model/Portal.java | 32 +++++++++++++++++++ .../postgresql/pljava/model/SlotTester.java | 12 +++++++ .../annotation/TupleTableSlotTest.java | 10 +++++- pljava-so/src/main/c/type/Portal.c | 29 ++++++++++++++++- .../postgresql/pljava/internal/Portal.java | 29 +++++++++++++++-- .../postgresql/pljava/jdbc/SPIConnection.java | 7 ++++ .../postgresql/pljava/jdbc/SPIResultSet.java | 18 +++++++++-- 7 files changed, 131 insertions(+), 6 deletions(-) create mode 100644 pljava-api/src/main/java/org/postgresql/pljava/model/Portal.java 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..b63a21bb0 --- /dev/null +++ b/pljava-api/src/main/java/org/postgresql/pljava/model/Portal.java @@ -0,0 +1,32 @@ +/* + * 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; + +/** + * 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 +{ + @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; +} 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 index 95e181c7f..3e30f18a2 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/model/SlotTester.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/model/SlotTester.java @@ -11,6 +11,9 @@ */ package org.postgresql.pljava.model; +import java.sql.ResultSet; +import java.sql.SQLException; + import java.util.List; import org.postgresql.pljava.Adapter; @@ -20,6 +23,15 @@ */ 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}. 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 index 091a6b798..aedf8d9be 100644 --- 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 @@ -13,7 +13,9 @@ 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; @@ -39,7 +41,9 @@ import org.postgresql.pljava.annotation.Function; import org.postgresql.pljava.model.Attribute; +import org.postgresql.pljava.model.Portal; import org.postgresql.pljava.model.SlotTester; +import org.postgresql.pljava.model.TupleDescriptor; import org.postgresql.pljava.model.TupleTableSlot; /** @@ -271,7 +275,11 @@ void testWith(String query, String adpClass, String adpInstance) Connection c = getConnection("jdbc:default:connection"); SlotTester t = c.unwrap(SlotTester.class); - List tups = t.test(query); + ResultSet rs = c.createStatement().executeQuery(query); + Portal p = t.unwrapAsPortal(rs); + TupleDescriptor td = p.tupleDescriptor(); + + List tups = List.of(); int ntups = tups.size(); diff --git a/pljava-so/src/main/c/type/Portal.c b/pljava-so/src/main/c/type/Portal.c index f5f6892fd..ce63cd1ca 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-2022 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" @@ -74,6 +75,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 @@ -194,6 +200,27 @@ Java_org_postgresql_pljava_internal_Portal__1getName(JNIEnv* env, jclass clazz, return result; } +/* + * 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: _getTupleDesc 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 7ad127c01..b74a8384f 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-2022 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 @@ -17,6 +17,8 @@ import org.postgresql.pljava.Lifespan; +import org.postgresql.pljava.model.TupleDescriptor; + import org.postgresql.pljava.pg.ResourceOwnerImpl; import java.sql.SQLException; @@ -27,7 +29,7 @@ * * @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 @@ -36,6 +38,8 @@ public class Portal */ private ExecutionPlan m_plan; + private TupleDescriptor m_tupdesc; + private final State m_state; Portal(long ro, long pointer, ExecutionPlan plan) @@ -92,6 +96,7 @@ public void close() { m_state.releaseFromJava(); m_plan = null; + m_tupdesc = null; }); } @@ -120,6 +125,23 @@ public long getPortalPos() return pos; } + /** + * 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; + }); + } + /** * Returns the TupleDesc that describes the row Tuples for this * Portal. @@ -199,6 +221,9 @@ private static native String _getName(long pointer) private static native long _getPortalPos(long pointer) throws SQLException; + private static native TupleDescriptor _getTupleDescriptor(long pointer) + throws SQLException; + private static native TupleDesc _getTupleDesc(long pointer) throws SQLException; 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 3575ac3cb..6747f9dac 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java @@ -58,6 +58,7 @@ import java.lang.reflect.Field; import org.postgresql.pljava.Adapter; import org.postgresql.pljava.internal.SPI; +import org.postgresql.pljava.model.Portal; import org.postgresql.pljava.model.SlotTester; import org.postgresql.pljava.model.TupleTableSlot; import org.postgresql.pljava.pg.TupleTableSlotImpl; @@ -79,6 +80,12 @@ */ 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) 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; From 43987245a7c4bc5de7a8ac78a6b71a0415cd1bab Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 14 Aug 2023 20:15:23 -0400 Subject: [PATCH 112/334] Muck about with SPI class Window some globals through ByteBuffers to reduce JNI calling just to read them. Start on a getTuples() method that will return the List form rather than the legacy TupleTable form. --- pljava-so/src/main/c/Backend.c | 2 +- pljava-so/src/main/c/ModelUtils.c | 54 +++++++++- pljava-so/src/main/c/SPI.c | 42 ++------ .../org/postgresql/pljava/internal/SPI.java | 102 ++++++++++++++++-- .../pljava/pg/TupleTableSlotImpl.java | 3 +- 5 files changed, 161 insertions(+), 42 deletions(-) diff --git a/pljava-so/src/main/c/Backend.c b/pljava-so/src/main/c/Backend.c index 95f06258d..88e0d376a 100644 --- a/pljava-so/src/main/c/Backend.c +++ b/pljava-so/src/main/c/Backend.c @@ -1087,10 +1087,10 @@ static void initPLJavaClasses(void) 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(); diff --git a/pljava-so/src/main/c/ModelUtils.c b/pljava-so/src/main/c/ModelUtils.c index e1d962387..aee822170 100644 --- a/pljava-so/src/main/c/ModelUtils.c +++ b/pljava-so/src/main/c/ModelUtils.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * 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 @@ -31,6 +31,9 @@ #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" @@ -333,6 +336,16 @@ void pljava_ModelUtils_initialize(void) { 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[] = { { @@ -406,6 +419,10 @@ void pljava_ModelUtils_initialize(void) 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); @@ -885,6 +902,41 @@ Java_org_postgresql_pljava_pg_ResourceOwnerImpl_00024EarlyNatives__1window(JNIEn 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 diff --git a/pljava-so/src/main/c/SPI.c b/pljava-so/src/main/c/SPI.c index ea104bd9f..588a6c747 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,6 +23,10 @@ #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) @@ -37,16 +41,6 @@ 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 @@ -57,6 +51,9 @@ void SPI_initialize(void) 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); @@ -106,6 +103,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 +141,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 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 7afb8c1dd..9e27b39ad 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-2022 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,23 @@ */ 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.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 +69,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. @@ -69,6 +101,13 @@ 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 +118,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 +135,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 List 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 List.of(); + + // 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 null; /* XXX */ + }); } /** - * 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 +228,22 @@ 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 native static long _getProcessed(); - private native static int _getResult(); private native static void _freeTupTable(); private native static TupleTable _getTupTable(TupleDesc known); } diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/TupleTableSlotImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/TupleTableSlotImpl.java index f0b0e9f03..2d4b61a75 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/TupleTableSlotImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/TupleTableSlotImpl.java @@ -786,7 +786,8 @@ ByteBuffer values() return m_values; } - private List supplyHeapTuples(ByteBuffer htarray) + // public? or move SPI class into this package? + public List supplyHeapTuples(ByteBuffer htarray) { htarray = htarray.asReadOnlyBuffer().order(ByteOrder.nativeOrder()); if ( 8 == SIZEOF_DATUM ) From 6ec5627f90040ff10b3185cb2b30c5ab9f6acc08 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 14 Aug 2023 20:15:46 -0400 Subject: [PATCH 113/334] Add more methods to Portal --- .../org/postgresql/pljava/model/Portal.java | 34 ++++++ pljava-so/src/main/c/ModelUtils.c | 17 ++- pljava-so/src/main/c/type/Portal.c | 88 ++++++++++---- .../src/main/include/pljava/ModelUtils.h | 10 +- .../postgresql/pljava/internal/Portal.java | 115 ++++++++++++++---- 5 files changed, 216 insertions(+), 48 deletions(-) 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 index b63a21bb0..c4f8528ec 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/model/Portal.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/model/Portal.java @@ -13,6 +13,8 @@ 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 @@ -21,6 +23,18 @@ */ 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 @@ -29,4 +43,24 @@ public interface Portal extends AutoCloseable * 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-so/src/main/c/ModelUtils.c b/pljava-so/src/main/c/ModelUtils.c index aee822170..2ffda5b94 100644 --- a/pljava-so/src/main/c/ModelUtils.c +++ b/pljava-so/src/main/c/ModelUtils.c @@ -96,8 +96,14 @@ jobject pljava_TupleDescriptor_create(TupleDesc tupdesc, Oid reloid) 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, const TupleTableSlotOps *tts_ops, Oid reloid) + TupleDesc tupdesc, jobject jtd, const TupleTableSlotOps *tts_ops, Oid reloid) { int natts = tupdesc->natts; TupleTableSlot *tts = MakeSingleTupleTableSlot(tupdesc, tts_ops); @@ -105,9 +111,12 @@ jobject pljava_TupleTableSlot_create( 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 jtd = pljava_TupleDescriptor_create(tupdesc, reloid); + jobject jtts; + + if ( NULL == jtd ) + jtd = pljava_TupleDescriptor_create(tupdesc, reloid); - jobject jtts = JNI_callStaticObjectMethodLocked(s_TupleTableSlotImpl_class, + jtts = JNI_callStaticObjectMethodLocked(s_TupleTableSlotImpl_class, s_TupleTableSlotImpl_newDeformed, tts_b, jtd, vals_b, nuls_b); JNI_deleteLocalRef(nuls_b); @@ -121,7 +130,7 @@ jobject pljava_TupleTableSlot_create( jobject pljava_TupleTableSlot_fromSPI() { jobject tts = pljava_TupleTableSlot_create( - SPI_tuptable->tupdesc, &TTSOpsHeapTuple, InvalidOid); + SPI_tuptable->tupdesc, NULL, &TTSOpsHeapTuple, InvalidOid); /* * XXX handle possibility that SPI_processed is way too big. diff --git a/pljava-so/src/main/c/type/Portal.c b/pljava-so/src/main/c/type/Portal.c index ce63cd1ca..bfcbdd449 100644 --- a/pljava-so/src/main/c/type/Portal.c +++ b/pljava-so/src/main/c/type/Portal.c @@ -32,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; @@ -64,6 +68,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;", @@ -111,12 +125,65 @@ void pljava_Portal_initialize(void) PgObject_registerNatives2(s_Portal_class, methods); s_Portal_init = PgObject_getJavaMethod(s_Portal_class, "", "(JJLorg/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 @@ -200,27 +267,6 @@ Java_org_postgresql_pljava_internal_Portal__1getName(JNIEnv* env, jclass clazz, return result; } -/* - * 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: _getTupleDesc diff --git a/pljava-so/src/main/include/pljava/ModelUtils.h b/pljava-so/src/main/include/pljava/ModelUtils.h index 79a957752..2f554ba21 100644 --- a/pljava-so/src/main/include/pljava/ModelUtils.h +++ b/pljava-so/src/main/include/pljava/ModelUtils.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * 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 @@ -52,9 +52,15 @@ extern jobject pljava_TupleDescriptor_create(TupleDesc tupdesc, Oid reloid); * * 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, const TupleTableSlotOps *tts_ops, Oid reloid); + TupleDesc tupdesc, jobject jtd, + const TupleTableSlotOps *tts_ops, Oid reloid); /* * Test scaffolding for the time being. 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 b74a8384f..0b128fdef 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/Portal.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/Portal.java @@ -12,17 +12,22 @@ */ 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.TupleDescriptor; +import org.postgresql.pljava.model.TupleTableSlot; 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. @@ -40,8 +45,24 @@ public class Portal implements org.postgresql.pljava.model.Portal private TupleDescriptor m_tupdesc; + private TupleTableSlotImpl m_slot; + private final State m_state; + 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 pointer, ExecutionPlan plan) { m_state = new State(this, ResourceOwnerImpl.fromAddress(ro), pointer); @@ -97,9 +118,74 @@ 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 ) + 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. @@ -125,23 +211,6 @@ public long getPortalPos() return pos; } - /** - * 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; - }); - } - /** * Returns the TupleDesc that describes the row Tuples for this * Portal. @@ -215,13 +284,17 @@ public long move(boolean forward, long count) return moved; } - private static native String _getName(long pointer) + private static native TupleDescriptor _getTupleDescriptor(long pointer) throws SQLException; - private static native long _getPortalPos(long pointer) + private static native TupleTableSlotImpl + _makeTupleTableSlot(long pointer, TupleDescriptor td) throws SQLException; - private static native TupleDescriptor _getTupleDescriptor(long pointer) + private static native String _getName(long pointer) + throws SQLException; + + private static native long _getPortalPos(long pointer) throws SQLException; private static native TupleDesc _getTupleDesc(long pointer) From d8bbd60ad93adada04a52c64da5b59fc268b340e Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 14 Aug 2023 20:16:20 -0400 Subject: [PATCH 114/334] Lose temporary testmeSPI stuff --- pljava-so/src/main/c/ModelUtils.c | 46 ------------------- .../src/main/include/pljava/ModelUtils.h | 5 -- .../postgresql/pljava/jdbc/SPIConnection.java | 14 +++++- .../pljava/pg/TupleTableSlotImpl.java | 19 -------- 4 files changed, 12 insertions(+), 72 deletions(-) diff --git a/pljava-so/src/main/c/ModelUtils.c b/pljava-so/src/main/c/ModelUtils.c index 2ffda5b94..8f6841efe 100644 --- a/pljava-so/src/main/c/ModelUtils.c +++ b/pljava-so/src/main/c/ModelUtils.c @@ -76,7 +76,6 @@ static jmethodID s_TupleDescImpl_fromByteBuffer; static jclass s_TupleTableSlotImpl_class; static jmethodID s_TupleTableSlotImpl_newDeformed; -static jmethodID s_TupleTableSlotImpl_supplyHeapTuples; static void relCacheCB(Datum arg, Oid relid); static void sysCacheCB(Datum arg, int cacheid, uint32 hash); @@ -127,26 +126,6 @@ jobject pljava_TupleTableSlot_create( return jtts; } -jobject pljava_TupleTableSlot_fromSPI() -{ - jobject tts = pljava_TupleTableSlot_create( - SPI_tuptable->tupdesc, NULL, &TTSOpsHeapTuple, InvalidOid); - - /* - * XXX handle possibility that SPI_processed is way too big. - */ - jobject vals = JNI_newDirectByteBuffer( - SPI_tuptable->vals, (jlong)(SPI_processed*sizeof *SPI_tuptable->vals)); - - jobject list = JNI_callObjectMethodLocked(tts, - s_TupleTableSlotImpl_supplyHeapTuples, vals); - - JNI_deleteLocalRef(vals); - JNI_deleteLocalRef(tts); - - return list; -} - static void memoryContextCallback(void *arg) { Ptr2Long p2l; @@ -377,11 +356,6 @@ void pljava_ModelUtils_initialize(void) "(Ljava/nio/ByteBuffer;JZ)V", Java_org_postgresql_pljava_pg_TupleTableSlotImpl__1store_1heaptuple }, - { - "_testmeSPI", - "()Ljava/util/List;", - Java_org_postgresql_pljava_pg_TupleTableSlotImpl__1testmeSPI - }, { 0, 0, 0 } }; @@ -455,11 +429,6 @@ void pljava_ModelUtils_initialize(void) "Ljava/nio/ByteBuffer;Ljava/nio/ByteBuffer;)" "Lorg/postgresql/pljava/pg/TupleTableSlotImpl$Deformed;"); - s_TupleTableSlotImpl_supplyHeapTuples = PgObject_getJavaMethod( - s_TupleTableSlotImpl_class, - "supplyHeapTuples", - "(Ljava/nio/ByteBuffer;)Ljava/util/List;"); - RegisterResourceReleaseCallback(resourceReleaseCB, NULL); CacheRegisterRelcacheCallback(relCacheCB, 0); @@ -1001,18 +970,3 @@ Java_org_postgresql_pljava_pg_TupleTableSlotImpl__1store_1heaptuple(JNIEnv* env, ExecStoreHeapTuple(htp, tts, JNI_TRUE == shouldFree); END_NATIVE_AND_CATCH("_store_heaptuple") } - -/* - * Class: org_postgresql_pljava_pg_TupleTableSlotImpl - * Method: _testmeSPI - * Signature: ()Ljava/util/List; - */ -JNIEXPORT jobject JNICALL -Java_org_postgresql_pljava_pg_TupleTableSlotImpl__1testmeSPI(JNIEnv* env, jobject _cls) -{ - jobject result = NULL; - BEGIN_NATIVE_AND_TRY - result = pljava_TupleTableSlot_fromSPI(); - END_NATIVE_AND_CATCH("_testmeSPI") - return result; -} diff --git a/pljava-so/src/main/include/pljava/ModelUtils.h b/pljava-so/src/main/include/pljava/ModelUtils.h index 2f554ba21..e2eb8e88d 100644 --- a/pljava-so/src/main/include/pljava/ModelUtils.h +++ b/pljava-so/src/main/include/pljava/ModelUtils.h @@ -62,11 +62,6 @@ extern jobject pljava_TupleTableSlot_create( TupleDesc tupdesc, jobject jtd, const TupleTableSlotOps *tts_ops, Oid reloid); -/* - * Test scaffolding for the time being. - */ -extern jobject pljava_TupleTableSlot_fromSPI(void); - #ifdef __cplusplus } #endif 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 6747f9dac..3ee10595d 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SPIConnection.java @@ -58,7 +58,9 @@ 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; @@ -90,8 +92,16 @@ public Portal unwrapAsPortal(ResultSet rs) throws SQLException @SuppressWarnings("deprecation") public List test(String query) { - int result = SPI.exec(query, 0); - return TupleTableSlotImpl.testmeSPI(); + 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 diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/TupleTableSlotImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/TupleTableSlotImpl.java index 2d4b61a75..7c86b4a5a 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/TupleTableSlotImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/TupleTableSlotImpl.java @@ -786,15 +786,6 @@ ByteBuffer values() return m_values; } - // public? or move SPI class into this package? - public List supplyHeapTuples(ByteBuffer htarray) - { - htarray = htarray.asReadOnlyBuffer().order(ByteOrder.nativeOrder()); - if ( 8 == SIZEOF_DATUM ) - return new HeapTuples8(htarray); - return new HeapTuples4(htarray); - } - private void store_heaptuple(long ht, boolean shouldFree) { doInPG(() -> _store_heaptuple(m_tts, ht, shouldFree)); @@ -805,8 +796,6 @@ private void store_heaptuple(long ht, boolean shouldFree) private static native void _store_heaptuple( ByteBuffer tts, long ht, boolean shouldFree); - private static native List _testmeSPI(); - @Override public T get(Attribute att, As adapter) { @@ -1087,14 +1076,6 @@ public int size() } } - /** - * Temporary test jig during development. - */ - public static List testmeSPI() - { - return doInPG(() -> _testmeSPI()); - } - private static class HTChunkState extends DualState.BBHeapFreeTuple { From 9d8e6b72bdca5a6a4b99491fd89054665d0d19ec Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 14 Aug 2023 20:16:50 -0400 Subject: [PATCH 115/334] More notational convenience in DualState --- .../postgresql/pljava/internal/DualState.java | 66 ++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) 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 b8618113a..3d5e99c5f 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-2022 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 @@ -940,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. @@ -960,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}, From d966f7bc026d632b54c17045f3a7c5202574bd54 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 14 Aug 2023 20:17:33 -0400 Subject: [PATCH 116/334] Add TupleList to present different kinds of such Empty and SPI, for now. --- pljava-so/src/main/c/DualState.c | 44 ++++- pljava-so/src/main/c/SPI.c | 42 ++++ .../postgresql/pljava/internal/DualState.java | 36 ++++ .../org/postgresql/pljava/internal/SPI.java | 15 +- .../org/postgresql/pljava/pg/TupleList.java | 185 ++++++++++++++++++ .../pljava/pg/TupleTableSlotImpl.java | 66 +------ 6 files changed, 316 insertions(+), 72 deletions(-) create mode 100644 pljava/src/main/java/org/postgresql/pljava/pg/TupleList.java diff --git a/pljava-so/src/main/c/DualState.c b/pljava-so/src/main/c/DualState.c index 6e295900c..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-2022 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,7 @@ #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" @@ -129,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[] = { { @@ -184,6 +195,11 @@ 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); @@ -329,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 diff --git a/pljava-so/src/main/c/SPI.c b/pljava-so/src/main/c/SPI.c index 588a6c747..79dfb3b13 100644 --- a/pljava-so/src/main/c/SPI.c +++ b/pljava-so/src/main/c/SPI.c @@ -31,6 +31,9 @@ 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) { @@ -46,6 +49,11 @@ void SPI_initialize(void) 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 @@ -57,6 +65,13 @@ void SPI_initialize(void) 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 @@ -159,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/src/main/java/org/postgresql/pljava/internal/DualState.java b/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java index 3d5e99c5f..baa1a4cea 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/DualState.java @@ -2206,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. 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 9e27b39ad..9754bdada 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/SPI.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/SPI.java @@ -24,6 +24,7 @@ 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; @@ -152,7 +153,7 @@ public static int getResult() * ensure that. * @return null if the global SPI_tuptable is null */ - public static List getTuples(TupleTableSlotImpl ttsi) + public static TupleList getTuples(TupleTableSlotImpl ttsi) { return doInPG(() -> { @@ -162,13 +163,13 @@ public static List getTuples(TupleTableSlotImpl ttsi) long count = getProcessed(); if ( 0 == count ) - return List.of(); + 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 null; /* XXX */ + return _mapTupTable(ttsi, p, sizeToMap); }); } @@ -242,8 +243,10 @@ private static native ByteBuffer[] _window( } @Deprecated - private native static int _exec(String command, int rowCount); + private static native int _exec(String command, int rowCount); - 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/pg/TupleList.java b/pljava/src/main/java/org/postgresql/pljava/pg/TupleList.java new file mode 100644 index 000000000..641105c96 --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/pg/TupleList.java @@ -0,0 +1,185 @@ +/* + * 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.List; + +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"); + } + } + + /** + * 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 + { + 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 index 7c86b4a5a..937644302 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/TupleTableSlotImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/TupleTableSlotImpl.java @@ -786,7 +786,7 @@ ByteBuffer values() return m_values; } - private void store_heaptuple(long ht, boolean shouldFree) + void store_heaptuple(long ht, boolean shouldFree) { doInPG(() -> _store_heaptuple(m_tts, ht, shouldFree)); } @@ -1012,70 +1012,6 @@ public boolean get(int idx, AsBoolean adapter) return adapter.fetch(accessor(idx), values(), off, att); } - /* - * Plan: factor the below out into 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")? - */ - - /** - * A {@code List} built over something like - */ - private class HeapTuples8 extends AbstractList - { - private final LongBuffer m_tuples; - - HeapTuples8(ByteBuffer ht) - { - m_tuples = ht.asLongBuffer(); - } - - @Override - public TupleTableSlot get(int index) - { - store_heaptuple(m_tuples.get(index), false); - return TupleTableSlotImpl.this; - } - - @Override - public int size() - { - return m_tuples.capacity(); - } - } - - private class HeapTuples4 extends AbstractList - { - private final IntBuffer m_tuples; - - HeapTuples4(ByteBuffer ht) - { - m_tuples = ht.asIntBuffer(); - } - - @Override - public TupleTableSlot get(int index) - { - store_heaptuple(m_tuples.get(index), false); - return TupleTableSlotImpl.this; - } - - @Override - public int size() - { - return m_tuples.capacity(); - } - } - private static class HTChunkState extends DualState.BBHeapFreeTuple { From e299b4c7ac7b02fcbb260a34a07dd6971f389060 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 14 Aug 2023 20:17:51 -0400 Subject: [PATCH 117/334] Update example --- .../annotation/TupleTableSlotTest.java | 383 +++++++++++++----- 1 file changed, 291 insertions(+), 92 deletions(-) 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 index aedf8d9be..c07a72c5d 100644 --- 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 @@ -24,7 +24,7 @@ import java.util.List; import java.util.Optional; -import java.time.OffsetDateTime; +import java.time.LocalDateTime; import org.postgresql.pljava.Adapter; import org.postgresql.pljava.Adapter.AdapterException;//for now; not planned API @@ -37,11 +37,16 @@ import org.postgresql.pljava.Adapter.AsChar; import org.postgresql.pljava.Adapter.AsByte; import org.postgresql.pljava.Adapter.AsBoolean; +import org.postgresql.pljava.TargetList; +import org.postgresql.pljava.TargetList.Cursor; +import org.postgresql.pljava.TargetList.Projection; import org.postgresql.pljava.annotation.Function; 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; @@ -52,122 +57,316 @@ */ public class TupleTableSlotTest { - /** - * Test retrieval of a PostgreSQL array as a multidimensional Java array. + /* + * 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. */ - @Function(schema="javatest") - public static Iterator javaMultiArrayTest() - throws SQLException, ReflectiveOperationException - { - Connection c = getConnection("jdbc:default:connection"); - SlotTester t = c.unwrap(SlotTester.class); + 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 - /* - * First obtain an Adapter for the component type. For now, the untyped - * adapterPlease method is needed to grovel it out of PL/Java's innards - * and then cast it. An adapter manager with proper generic typing will - * handle that part some day. - */ - AsLong int8 = (AsLong)t.adapterPlease( - "org.postgresql.pljava.pg.adt.Primitives", "INT8_INSTANCE"); - AsInt int4 = (AsInt)t.adapterPlease( - "org.postgresql.pljava.pg.adt.Primitives", "INT4_INSTANCE"); - AsShort int2 = (AsShort)t.adapterPlease( - "org.postgresql.pljava.pg.adt.Primitives", "INT2_INSTANCE"); - AsByte int1 = (AsByte)t.adapterPlease( - "org.postgresql.pljava.pg.adt.Primitives", "INT1_INSTANCE"); - AsDouble float8 = (AsDouble)t.adapterPlease( - "org.postgresql.pljava.pg.adt.Primitives", "FLOAT8_INSTANCE"); - AsFloat float4 = (AsFloat)t.adapterPlease( - "org.postgresql.pljava.pg.adt.Primitives", "FLOAT4_INSTANCE"); - AsBoolean bool = (AsBoolean)t.adapterPlease( - "org.postgresql.pljava.pg.adt.Primitives", "BOOLEAN_INSTANCE"); - - @SuppressWarnings("unchecked") - As odt = (As)t.adapterPlease( - "org.postgresql.pljava.pg.adt.DateTimeAdapter$JSR310", - "TIMESTAMPTZ_INSTANCE"); + /* + * 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 + { /* - * 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 change - * your data 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. + * 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. */ - float8 = new NullReplacingDouble(float8, Double.NaN); + 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); + } /* - * Let's also compose AsOptional over the odt adapter, to get an adapter - * producing Optional instead of possibly nulls. + * Other than those stopgap uses of adapterPlease, the rest is + * not so bad. Instantiate some composing adapters over the leaf + * adapters already obtained: */ - As,?> oodt = new AsOptional<>(odt); + + F8_NaN = new NullReplacingDouble(FLOAT8, Double.NaN); + LDT_O = new AsOptional<>(LDT); + INT8_O = new AsOptional<>(INT8); /* - * 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. + * (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.) * - * 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, just one absolutely trivial composing adapter could - * add that behavior over any primitive adapter. + * Java array-of-array adapters of various dimensionalities are + * easily built from the adapters chosen for their component types. */ - As,?> oint8 = new AsOptional<>(int8); - /* - * Once properly-typed adapters for component types are in hand, - * getting properly-typed array adapters is straightforward. - * (Java 10+ var can reduce verbosity here.) - */ - As[] ,?> i8x1 = oint8 .a1().build(); - As< int[][] ,?> i4x2 = int4 .a2() .build(); - As< short[][][] ,?> i2x3 = int2 .a2().a1().build(); - As< byte[][][][] ,?> i1x4 = int1 .a4() .build(); - As< double[][][][][] ,?> f8x5 = float8.a4() .a1().build(); - As< float[][][][][][] ,?> f4x6 = float4.a4().a2() .build(); - As< boolean[][][][][] ,?> bx5 = bool .a4() .a1().build(); - As[][][][],?> dtx4 = oodt.a4() .build(); + 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 timestamptz [] ) " + + " 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}}}}'" + + " '{{NULL}}'" + ")"; - List tups = t.test(query); + Portal p = t.unwrapAsPortal(conn.createStatement().executeQuery(query)); + Projection proj = p.tupleDescriptor(); + + List tups = p.fetch(FORWARD, ALL); List result = new ArrayList<>(); /* - * Then just pass the right adapter to tts.get. + * Then just use the right adapter for each column. */ - for ( TupleTableSlot tts : tups ) + proj.applyOver(tups, c -> { - Optional [] v0 = tts.get(0, i8x1); - int [][] v1 = tts.get(1, i4x2); - short [][][] v2 = tts.get(2, i2x3); - byte [][][][] v3 = tts.get(3, i1x4); - double [][][][][] v4 = tts.get(4, f8x5); - float [][][][][][] v5 = tts.get(5, f4x6); - boolean [][][][][] v6 = tts.get(6, bx5); - Optional [][][][] v7 = tts.get(7, dtx4); - - result.addAll(List.of( - Arrays.toString(v0), deepToString(v1), deepToString(v2), - deepToString(v3), deepToString(v4), deepToString(v5), - deepToString(v6), deepToString(v7))); - } + 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(); } @@ -279,7 +478,7 @@ void testWith(String query, String adpClass, String adpInstance) Portal p = t.unwrapAsPortal(rs); TupleDescriptor td = p.tupleDescriptor(); - List tups = List.of(); + List tups = p.fetch(FORWARD, ALL); int ntups = tups.size(); From 8a0d912bf2afb87f9895a87f2d7c8f98fad69013 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 14 Aug 2023 20:18:19 -0400 Subject: [PATCH 118/334] Decree: applyOver methods can throw SQLException ... in addition to whatever the lambda bodies appear to throw. Some SQLExceptions, wrapped in the unchecked AdapterException, may not be unwrapped until caught in applyOver. This is probably not the final story for exception handling. For one thing, it means code in the lambda bodies could have catch (SQLException ...) and still miss these, which is weird. But it is also unpleasant to write functional-style Java while dealing with checked exceptions. It also remains to be decided whether the Java-centric, JDBC-defined, checked SQLException hierarchy is the right fit for a more JVM-language-agnostic, PostgreSQL-specific PL/Java API. And the "thoughts on logging" wiki topic has thoughts impinging on that. There are nice things about the SQLException hierarchy. That it carries SQLSTATE codes, and has an exception hierarchy with a defined correspondence to them, is nice. And no API as imagined in "thoughts on logging" exists yet, though things could be much simpler if it did. It is often right at the point of the throw where code knows what the most appropriate SQLSTATE code should be. Maybe an unchecked exception hierarchy paralleling SQLException would be worthwhile. For now, this peculiar hack. --- .../java/org/postgresql/pljava/Adapter.java | 20 +++++++++++++ .../org/postgresql/pljava/TargetList.java | 5 ++-- .../postgresql/pljava/pg/TargetListImpl.java | 29 +++++++++++++++---- .../postgresql/pljava/pg/TupleDescImpl.java | 4 +-- 4 files changed, 48 insertions(+), 10 deletions(-) diff --git a/pljava-api/src/main/java/org/postgresql/pljava/Adapter.java b/pljava-api/src/main/java/org/postgresql/pljava/Adapter.java index 636e9a757..47b40acac 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/Adapter.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/Adapter.java @@ -1609,12 +1609,32 @@ private static RuntimeException wrapped(Throwable 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; + } } /** diff --git a/pljava-api/src/main/java/org/postgresql/pljava/TargetList.java b/pljava-api/src/main/java/org/postgresql/pljava/TargetList.java index 8de1db819..002ee944f 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/TargetList.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/TargetList.java @@ -11,6 +11,7 @@ */ package org.postgresql.pljava; +import java.sql.SQLException; // for javadoc import java.sql.SQLXML; // for javadoc import java.util.Arrays; @@ -159,7 +160,7 @@ default Projection project(CharSequence... names) */ R applyOver( Iterable tuples, Cursor.Function f) - throws X; + throws X, SQLException; /** * Executes the function f, once, supplying a @@ -174,7 +175,7 @@ R applyOver( */ R applyOver( TupleTableSlot tuple, Cursor.Function f) - throws X; + throws X, SQLException; /** * A {@code TargetList} that has been bound to a source of tuples and can diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/TargetListImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/TargetListImpl.java index 943150d5f..f58f443d9 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/TargetListImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/TargetListImpl.java @@ -11,6 +11,7 @@ */ 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; @@ -30,6 +31,8 @@ import java.lang.ref.WeakReference; +import java.sql.SQLException; + import java.util.AbstractList; import java.util.Arrays; import static java.util.Arrays.copyOfRange; @@ -94,7 +97,7 @@ public TargetList subList(int fromIndex, int toIndex) @Override // TargetList public R applyOver( Iterable tuples, Cursor.Function f) - throws X + throws X, SQLException { return TargetListImpl.applyOver(this, tuples, f); } @@ -102,7 +105,7 @@ public R applyOver( @Override // TargetList public R applyOver( TupleTableSlot tuple, Cursor.Function f) - throws X + throws X, SQLException { return TargetListImpl.applyOver(this, tuple, f); } @@ -436,16 +439,30 @@ private static boolean foundIn(Attribute a, TupleDescriptor td) static R applyOver( TargetList tl, Iterable tuples, Cursor.Function f) - throws X + throws X, SQLException { - return f.apply(new CursorImpl(tl, tuples)); + 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 + throws X, SQLException { - return f.apply(new CursorImpl(tl, tuple)); + try + { + return f.apply(new CursorImpl(tl, tuple)); + } + catch ( AdapterException e ) + { + throw e.unwrap(SQLException.class); + } } static class CursorImpl implements TargetList.Cursor, AutoCloseable diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/TupleDescImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/TupleDescImpl.java index 55fcc8c3d..915852691 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/TupleDescImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/TupleDescImpl.java @@ -118,7 +118,7 @@ public Projection project(Attribute... attrs) @Override // TargetList public R applyOver( Iterable tuples, Cursor.Function f) - throws X + throws X, SQLException { return TargetListImpl.applyOver(this, tuples, f); } @@ -126,7 +126,7 @@ public R applyOver( @Override // TargetList public R applyOver( TupleTableSlot tuple, Cursor.Function f) - throws X + throws X, SQLException { return TargetListImpl.applyOver(this, tuple, f); } From ef1f192a74fa514774d6ce5c9e2ece749a233f47 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 14 Aug 2023 20:18:47 -0400 Subject: [PATCH 119/334] Deprecate old hasty TupleDescriptor methods Deprecate attributes() because TupleDescriptor already is-a Projection, which is-a List, so it hardly needs a method that only returns its own receiver. Deprecate the two get() methods that do lookups by name, in favor of the methods of Projection, which can look up multiple attributes by name at one time, addressing the inefficiency of repeated one-name lookups. When these methods have gone, there should be little reason for TupleDescImpl to be hauling around a hashtable. Nothing is wrong with sqlGet, but hoist it out to a default method on TargetList. And deal with the resulting fallout .... --- .../org/postgresql/pljava/TargetList.java | 21 ++ .../pljava/model/TupleDescriptor.java | 56 +++++- .../annotation/TupleTableSlotTest.java | 2 +- .../postgresql/pljava/pg/AttributeImpl.java | 64 +++++- .../postgresql/pljava/pg/DatabaseImpl.java | 61 +++++- .../postgresql/pljava/pg/ExtensionImpl.java | 51 ++++- .../pljava/pg/ProceduralLanguageImpl.java | 51 ++++- .../postgresql/pljava/pg/RegClassImpl.java | 106 ++++++++-- .../pljava/pg/RegCollationImpl.java | 62 +++++- .../postgresql/pljava/pg/RegConfigImpl.java | 32 ++- .../pljava/pg/RegDictionaryImpl.java | 32 ++- .../pljava/pg/RegNamespaceImpl.java | 31 ++- .../postgresql/pljava/pg/RegOperatorImpl.java | 87 ++++++-- .../pljava/pg/RegProcedureImpl.java | 169 +++++++++++----- .../org/postgresql/pljava/pg/RegRoleImpl.java | 61 +++++- .../org/postgresql/pljava/pg/RegTypeImpl.java | 189 ++++++++++++++---- .../postgresql/pljava/pg/TupleDescImpl.java | 50 ----- .../pljava/pg/TupleTableSlotImpl.java | 14 +- 18 files changed, 879 insertions(+), 260 deletions(-) diff --git a/pljava-api/src/main/java/org/postgresql/pljava/TargetList.java b/pljava-api/src/main/java/org/postgresql/pljava/TargetList.java index 002ee944f..d1b877317 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/TargetList.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/TargetList.java @@ -152,6 +152,27 @@ default Projection project(CharSequence... names) @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 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 index 2c706deb3..c5f6e1e85 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/model/TupleDescriptor.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/model/TupleDescriptor.java @@ -39,8 +39,34 @@ */ public interface TupleDescriptor extends Projection { - List attributes(); + /** + * @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. + *

    + * 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(); /** @@ -49,9 +75,21 @@ public interface TupleDescriptor extends Projection * 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}. */ - Attribute get(Simple name) throws SQLException; + @Deprecated(forRemoval=true) + default Attribute get(Simple name) throws SQLException + { + return project(name).get(0); + } /** * Equivalent to {@code get(Simple.fromJava(name))}. @@ -60,21 +98,17 @@ public interface TupleDescriptor extends Projection * 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)); } - /** - * (Convenience method) Retrieve an attribute by its familiar (1-based) - * SQL attribute number. - *

    - * The Java {@link List#get List.get} API uses zero-based numbering, so this - * convenience method is equivalent to {@code attributes().get(attrNum-1)}. - */ - Attribute sqlGet(int attrNum); - /** * Return this descriptor unchanged if it is already interned in * PostgreSQL's type cache, otherwise an equivalent new descriptor with 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 index c07a72c5d..0a823b21a 100644 --- 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 @@ -527,7 +527,7 @@ else if ( a instanceof AsBoolean ) adpZ = (AsBoolean)a; } - for ( Attribute att : tts.descriptor().attributes() ) + for ( Attribute att : tts.descriptor() ) { go = true; while ( go ) diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/AttributeImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/AttributeImpl.java index 05c6dae1e..4d2e0e5d2 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/AttributeImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/AttributeImpl.java @@ -19,6 +19,7 @@ import java.sql.SQLException; +import java.util.Iterator; import java.util.List; import static java.util.Objects.requireNonNull; @@ -167,7 +168,7 @@ private static List grants(AttributeImpl o) throws SQLException { TupleTableSlot t = o.cacheTuple(); - return t.get(t.descriptor().get("attacl"), GrantAdapter.LIST_INSTANCE); + return t.get(Att.ATTACL, GrantAdapter.LIST_INSTANCE); } /* Implementation of Attribute */ @@ -256,6 +257,49 @@ private static List grants(AttributeImpl o) 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 */ /** @@ -313,7 +357,7 @@ private static short length(AttributeImpl o) private static int dimensions(AttributeImpl o) throws SQLException { TupleTableSlot s = o.partialTuple(); - return s.get(s.descriptor().get("attndims"), INT4_INSTANCE); + return s.get(Att.ATTNDIMS, INT4_INSTANCE); } private static boolean byValue(AttributeImpl o) @@ -335,7 +379,7 @@ private static Storage storage(AttributeImpl o) throws SQLException TupleTableSlot s = o.partialTuple(); return storageFromCatalog( - s.get(s.descriptor().get("attstorage"), INT1_INSTANCE)); + s.get(Att.ATTSTORAGE, INT1_INSTANCE)); } private static boolean notNull(AttributeImpl o) @@ -349,26 +393,26 @@ private static boolean notNull(AttributeImpl o) private static boolean hasDefault(AttributeImpl o) throws SQLException { TupleTableSlot s = o.partialTuple(); - return s.get(s.descriptor().get("atthasdef"), BOOLEAN_INSTANCE); + 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(s.descriptor().get("atthasmissing"), BOOLEAN_INSTANCE); + 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(s.descriptor().get("attidentity"), INT1_INSTANCE); + 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(s.descriptor().get("attgenerated"), INT1_INSTANCE); + byte v = s.get(Att.ATTGENERATED, INT1_INSTANCE); return generatedFromCatalog(v); } @@ -384,19 +428,19 @@ private static boolean dropped(AttributeImpl o) private static boolean local(AttributeImpl o) throws SQLException { TupleTableSlot s = o.partialTuple(); - return s.get(s.descriptor().get("attislocal"), BOOLEAN_INSTANCE); + return s.get(Att.ATTISLOCAL, BOOLEAN_INSTANCE); } private static int inheritanceCount(AttributeImpl o) throws SQLException { TupleTableSlot s = o.partialTuple(); - return s.get(s.descriptor().get("attinhcount"), INT4_INSTANCE); + return s.get(Att.ATTINHCOUNT, INT4_INSTANCE); } private static RegCollation collation(AttributeImpl o) throws SQLException { TupleTableSlot s = o.partialTuple(); - return s.get(s.descriptor().get("attcollation"), REGCOLLATION_INSTANCE); + return s.get(Att.ATTCOLLATION, REGCOLLATION_INSTANCE); } /* private methods using cache slots like API methods do */ diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/DatabaseImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/DatabaseImpl.java index 5e7d1cc8b..8c1f609cd 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/DatabaseImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/DatabaseImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * 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 @@ -17,6 +17,7 @@ import java.sql.SQLException; +import java.util.Iterator; import java.util.List; import java.util.function.UnaryOperator; @@ -67,20 +68,20 @@ int cacheId() private static Simple name(DatabaseImpl o) throws SQLException { TupleTableSlot t = o.cacheTuple(); - return t.get(t.descriptor().get("datname"), SIMPLE_INSTANCE); + return t.get(Att.DATNAME, SIMPLE_INSTANCE); } private static RegRole owner(DatabaseImpl o) throws SQLException { TupleTableSlot t = o.cacheTuple(); - return t.get(t.descriptor().get("datdba"), REGROLE_INSTANCE); + return t.get(Att.DATDBA, REGROLE_INSTANCE); } private static List grants(DatabaseImpl o) throws SQLException { TupleTableSlot t = o.cacheTuple(); - return t.get(t.descriptor().get("datacl"), GrantAdapter.LIST_INSTANCE); + return t.get(Att.DATACL, GrantAdapter.LIST_INSTANCE); } /* Implementation of Database */ @@ -137,42 +138,82 @@ private static List grants(DatabaseImpl o) 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(s.descriptor().get("encoding"), EncodingAdapter.INSTANCE); + return s.get(Att.ENCODING, EncodingAdapter.INSTANCE); } private static String collate(DatabaseImpl o) throws SQLException { TupleTableSlot s = o.cacheTuple(); - return s.get(s.descriptor().get("datcollate"), AS_STRING_INSTANCE); + return s.get(Att.DATCOLLATE, AS_STRING_INSTANCE); } private static String ctype(DatabaseImpl o) throws SQLException { TupleTableSlot s = o.cacheTuple(); - return s.get(s.descriptor().get("datctype"), AS_STRING_INSTANCE); + return s.get(Att.DATCTYPE, AS_STRING_INSTANCE); } private static boolean template(DatabaseImpl o) throws SQLException { TupleTableSlot s = o.cacheTuple(); - return s.get(s.descriptor().get("datistemplate"), BOOLEAN_INSTANCE); + return s.get(Att.DATISTEMPLATE, BOOLEAN_INSTANCE); } private static boolean allowConnection(DatabaseImpl o) throws SQLException { TupleTableSlot s = o.cacheTuple(); - return s.get(s.descriptor().get("datallowconn"), BOOLEAN_INSTANCE); + return s.get(Att.DATALLOWCONN, BOOLEAN_INSTANCE); } private static int connectionLimit(DatabaseImpl o) throws SQLException { TupleTableSlot s = o.cacheTuple(); - return s.get(s.descriptor().get("datconnlimit"), INT4_INSTANCE); + return s.get(Att.DATCONNLIMIT, INT4_INSTANCE); } /* API methods */ diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/ExtensionImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/ExtensionImpl.java index 97b394a7f..cb61e9162 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/ExtensionImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/ExtensionImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * 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 @@ -19,6 +19,7 @@ import java.sql.SQLException; +import java.util.Iterator; import java.util.List; import java.util.function.UnaryOperator; @@ -88,13 +89,13 @@ private static Simple name(ExtensionImpl o) throws SQLException { TupleTableSlot t = o.cacheTuple(); return - t.get(t.descriptor().get("extname"), SIMPLE_INSTANCE); + t.get(Att.EXTNAME, SIMPLE_INSTANCE); } private static RegRole owner(ExtensionImpl o) throws SQLException { TupleTableSlot t = o.cacheTuple(); - return t.get(t.descriptor().get("extowner"), REGROLE_INSTANCE); + return t.get(Att.EXTOWNER, REGROLE_INSTANCE); } /* Implementation of Extension */ @@ -156,31 +157,65 @@ private static RegRole owner(ExtensionImpl o) throws SQLException 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(s.descriptor().get("extnamespace"), REGNAMESPACE_INSTANCE); + return s.get(Att.EXTNAMESPACE, REGNAMESPACE_INSTANCE); } private static boolean relocatable(ExtensionImpl o) throws SQLException { TupleTableSlot s = o.cacheTuple(); - return s.get(s.descriptor().get("extrelocatable"), BOOLEAN_INSTANCE); + return s.get(Att.EXTRELOCATABLE, BOOLEAN_INSTANCE); } private static String version(ExtensionImpl o) throws SQLException { TupleTableSlot s = o.cacheTuple(); - return s.get(s.descriptor().get("extversion"), TextAdapter.INSTANCE); + return s.get(Att.EXTVERSION, TextAdapter.INSTANCE); } private static List config(ExtensionImpl o) throws SQLException { TupleTableSlot s = o.cacheTuple(); return - s.get(s.descriptor().get("extconfig"), + s.get(Att.EXTCONFIG, ArrayAdapters.REGCLASS_LIST_INSTANCE); } @@ -188,7 +223,7 @@ private static List condition(ExtensionImpl o) throws SQLException { TupleTableSlot s = o.cacheTuple(); return - s.get(s.descriptor().get("extcondition"), + s.get(Att.EXTCONDITION, FLAT_STRING_LIST_INSTANCE); } diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/ProceduralLanguageImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/ProceduralLanguageImpl.java index 8dff775ac..31ae52417 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/ProceduralLanguageImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/ProceduralLanguageImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * 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 @@ -17,6 +17,7 @@ import java.sql.SQLException; +import java.util.Iterator; import java.util.List; import java.util.function.UnaryOperator; @@ -69,20 +70,20 @@ private static Simple name(ProceduralLanguageImpl o) throws SQLException { TupleTableSlot t = o.cacheTuple(); return - t.get(t.descriptor().get("lanname"), SIMPLE_INSTANCE); + t.get(Att.LANNAME, SIMPLE_INSTANCE); } private static RegRole owner(ProceduralLanguageImpl o) throws SQLException { TupleTableSlot t = o.cacheTuple(); - return t.get(t.descriptor().get("lanowner"), REGROLE_INSTANCE); + return t.get(Att.LANOWNER, REGROLE_INSTANCE); } private static List grants(ProceduralLanguageImpl o) throws SQLException { TupleTableSlot t = o.cacheTuple(); - return t.get(t.descriptor().get("lanacl"), GrantAdapter.LIST_INSTANCE); + return t.get(Att.LANACL, GrantAdapter.LIST_INSTANCE); } /* Implementation of ProceduralLanguage */ @@ -135,13 +136,47 @@ private static List grants(ProceduralLanguageImpl o) 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(s.descriptor().get("lanpltrusted"), BOOLEAN_INSTANCE) ) + if ( s.get(Att.LANPLTRUSTED, BOOLEAN_INSTANCE) ) return new PLPrincipal.Sandboxed(o.name()); return new PLPrincipal.Unsandboxed(o.name()); } @@ -152,7 +187,7 @@ private static RegProcedure handler(ProceduralLanguageImpl o) TupleTableSlot s = o.cacheTuple(); @SuppressWarnings("unchecked") // XXX add memo magic here RegProcedure p = (RegProcedure) - s.get(s.descriptor().get("lanplcallfoid"), REGPROCEDURE_INSTANCE); + s.get(Att.LANPLCALLFOID, REGPROCEDURE_INSTANCE); return p; } @@ -163,7 +198,7 @@ private static RegProcedure inlineHandler( TupleTableSlot s = o.cacheTuple(); @SuppressWarnings("unchecked") // XXX add memo magic here RegProcedure p = (RegProcedure) - s.get(s.descriptor().get("laninline"), REGPROCEDURE_INSTANCE); + s.get(Att.LANINLINE, REGPROCEDURE_INSTANCE); return p; } @@ -173,7 +208,7 @@ private static RegProcedure validator(ProceduralLanguageImpl o) TupleTableSlot s = o.cacheTuple(); @SuppressWarnings("unchecked") // XXX add memo magic here RegProcedure p = (RegProcedure) - s.get(s.descriptor().get("lanvalidator"), REGPROCEDURE_INSTANCE); + s.get(Att.LANVALIDATOR, REGPROCEDURE_INSTANCE); return p; } diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/RegClassImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/RegClassImpl.java index b071a528e..f3c8de2bb 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/RegClassImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/RegClassImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * 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 @@ -20,6 +20,7 @@ import java.sql.SQLException; +import java.util.Iterator; import java.util.List; import java.util.function.UnaryOperator; @@ -93,26 +94,26 @@ private static Simple name(RegClassImpl o) throws SQLException { TupleTableSlot t = o.cacheTuple(); return - t.get(t.descriptor().get("relname"), NameAdapter.SIMPLE_INSTANCE); + t.get(Att.RELNAME, NameAdapter.SIMPLE_INSTANCE); } private static RegNamespace namespace(RegClassImpl o) throws SQLException { TupleTableSlot t = o.cacheTuple(); - return t.get(t.descriptor().get("relnamespace"), REGNAMESPACE_INSTANCE); + return t.get(Att.RELNAMESPACE, REGNAMESPACE_INSTANCE); } private static RegRole owner(RegClassImpl o) throws SQLException { TupleTableSlot t = o.cacheTuple(); - return t.get(t.descriptor().get("relowner"), REGROLE_INSTANCE); + return t.get(Att.RELOWNER, REGROLE_INSTANCE); } private static List grants(RegClassImpl o) throws SQLException { TupleTableSlot t = o.cacheTuple(); - return t.get(t.descriptor().get("relacl"), GrantAdapter.LIST_INSTANCE); + return t.get(Att.RELACL, GrantAdapter.LIST_INSTANCE); } /* Implementation of RegClass */ @@ -265,6 +266,73 @@ void dualHandshake(RegType dual) 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 */ /** @@ -364,87 +432,87 @@ private static RegType type(RegClassImpl o) throws SQLException private static RegType ofType(RegClassImpl o) throws SQLException { TupleTableSlot s = o.cacheTuple(); - return s.get(s.descriptor().get("reloftype"), REGTYPE_INSTANCE); + return s.get(Att.RELOFTYPE, REGTYPE_INSTANCE); } private static RegClass toastRelation(RegClassImpl o) throws SQLException { TupleTableSlot s = o.cacheTuple(); - return s.get(s.descriptor().get("reltoastrelid"), REGCLASS_INSTANCE); + return s.get(Att.RELTOASTRELID, REGCLASS_INSTANCE); } private static boolean hasIndex(RegClassImpl o) throws SQLException { TupleTableSlot s = o.cacheTuple(); - return s.get(s.descriptor().get("relhasindex"), BOOLEAN_INSTANCE); + return s.get(Att.RELHASINDEX, BOOLEAN_INSTANCE); } private static boolean isShared(RegClassImpl o) throws SQLException { TupleTableSlot s = o.cacheTuple(); - return s.get(s.descriptor().get("relisshared"), BOOLEAN_INSTANCE); + return s.get(Att.RELISSHARED, BOOLEAN_INSTANCE); } private static short nAttributes(RegClassImpl o) throws SQLException { TupleTableSlot s = o.cacheTuple(); - return s.get(s.descriptor().get("relnatts"), INT2_INSTANCE); + return s.get(Att.RELNATTS, INT2_INSTANCE); } private static short checks(RegClassImpl o) throws SQLException { TupleTableSlot s = o.cacheTuple(); - return s.get(s.descriptor().get("relchecks"), INT2_INSTANCE); + return s.get(Att.RELCHECKS, INT2_INSTANCE); } private static boolean hasRules(RegClassImpl o) throws SQLException { TupleTableSlot s = o.cacheTuple(); - return s.get(s.descriptor().get("relhasrules"), BOOLEAN_INSTANCE); + return s.get(Att.RELHASRULES, BOOLEAN_INSTANCE); } private static boolean hasTriggers(RegClassImpl o) throws SQLException { TupleTableSlot s = o.cacheTuple(); - return s.get(s.descriptor().get("relhastriggers"), BOOLEAN_INSTANCE); + return s.get(Att.RELHASTRIGGERS, BOOLEAN_INSTANCE); } private static boolean hasSubclass(RegClassImpl o) throws SQLException { TupleTableSlot s = o.cacheTuple(); - return s.get(s.descriptor().get("relhassubclass"), BOOLEAN_INSTANCE); + return s.get(Att.RELHASSUBCLASS, BOOLEAN_INSTANCE); } private static boolean rowSecurity(RegClassImpl o) throws SQLException { TupleTableSlot s = o.cacheTuple(); - return s.get(s.descriptor().get("relrowsecurity"), BOOLEAN_INSTANCE); + return s.get(Att.RELROWSECURITY, BOOLEAN_INSTANCE); } private static boolean forceRowSecurity(RegClassImpl o) throws SQLException { TupleTableSlot s = o.cacheTuple(); return - s.get(s.descriptor().get("relforcerowsecurity"), BOOLEAN_INSTANCE); + s.get(Att.RELFORCEROWSECURITY, BOOLEAN_INSTANCE); } private static boolean isPopulated(RegClassImpl o) throws SQLException { TupleTableSlot s = o.cacheTuple(); - return s.get(s.descriptor().get("relispopulated"), BOOLEAN_INSTANCE); + return s.get(Att.RELISPOPULATED, BOOLEAN_INSTANCE); } private static boolean isPartition(RegClassImpl o) throws SQLException { TupleTableSlot s = o.cacheTuple(); - return s.get(s.descriptor().get("relispartition"), BOOLEAN_INSTANCE); + return s.get(Att.RELISPARTITION, BOOLEAN_INSTANCE); } private static List options(RegClassImpl o) throws SQLException { TupleTableSlot s = o.cacheTuple(); return - s.get(s.descriptor().get("reloptions"), FLAT_STRING_LIST_INSTANCE); + s.get(Att.RELOPTIONS, FLAT_STRING_LIST_INSTANCE); } /* API methods */ diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/RegCollationImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/RegCollationImpl.java index 92de0cc11..533b1c3d4 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/RegCollationImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/RegCollationImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * 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 @@ -17,6 +17,8 @@ import java.sql.SQLException; +import java.util.Iterator; + import java.util.function.UnaryOperator; import org.postgresql.pljava.internal.SwitchPointCache.Builder; @@ -63,7 +65,7 @@ int cacheId() private static Simple name(RegCollationImpl o) throws SQLException { TupleTableSlot t = o.cacheTuple(); - return t.get(t.descriptor().get("collname"), SIMPLE_INSTANCE); + return t.get(Att.COLLNAME, SIMPLE_INSTANCE); } private static RegNamespace namespace(RegCollationImpl o) @@ -71,13 +73,13 @@ private static RegNamespace namespace(RegCollationImpl o) { TupleTableSlot t = o.cacheTuple(); return - t.get(t.descriptor().get("collnamespace"), REGNAMESPACE_INSTANCE); + t.get(Att.COLLNAMESPACE, REGNAMESPACE_INSTANCE); } private static RegRole owner(RegCollationImpl o) throws SQLException { TupleTableSlot t = o.cacheTuple(); - return t.get(t.descriptor().get("collowner"), REGROLE_INSTANCE); + return t.get(Att.COLLOWNER, REGROLE_INSTANCE); } /* Implementation of RegCollation */ @@ -134,6 +136,46 @@ private static RegRole owner(RegCollationImpl o) throws SQLException 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 = CLASSID.tupleDescriptor().project( + "collname", + "collnamespace", + "collowner", + "collencoding", + "collcollate", + "collctype", + "collprovider", + "collversion", + "collisdeterministic" + ).iterator(); + + 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) @@ -141,25 +183,25 @@ private static CharsetEncoding encoding(RegCollationImpl o) { TupleTableSlot s = o.cacheTuple(); return - s.get(s.descriptor().get("collencoding"), EncodingAdapter.INSTANCE); + s.get(Att.COLLENCODING, EncodingAdapter.INSTANCE); } private static String collate(RegCollationImpl o) throws SQLException { TupleTableSlot s = o.cacheTuple(); - return s.get(s.descriptor().get("collcollate"), AS_STRING_INSTANCE); + return s.get(Att.COLLCOLLATE, AS_STRING_INSTANCE); } private static String ctype(RegCollationImpl o) throws SQLException { TupleTableSlot s = o.cacheTuple(); - return s.get(s.descriptor().get("collctype"), AS_STRING_INSTANCE); + return s.get(Att.COLLCTYPE, AS_STRING_INSTANCE); } private static Provider provider(RegCollationImpl o) throws SQLException { TupleTableSlot s = o.cacheTuple(); - byte p = s.get(s.descriptor().get("collprovider"), INT1_INSTANCE); + byte p = s.get(Att.COLLPROVIDER, INT1_INSTANCE); switch ( p ) { case (byte)'d': @@ -177,14 +219,14 @@ private static Provider provider(RegCollationImpl o) throws SQLException private static String version(RegCollationImpl o) throws SQLException { TupleTableSlot s = o.cacheTuple(); - return s.get(s.descriptor().get("collversion"), TextAdapter.INSTANCE); + return s.get(Att.COLLVERSION, TextAdapter.INSTANCE); } private static boolean deterministic(RegCollationImpl o) throws SQLException { TupleTableSlot s = o.cacheTuple(); return - s.get(s.descriptor().get("collisdeterministic"), BOOLEAN_INSTANCE); + s.get(Att.COLLISDETERMINISTIC, BOOLEAN_INSTANCE); } /* API methods */ diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/RegConfigImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/RegConfigImpl.java index 0f63dc02e..ea908b951 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/RegConfigImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/RegConfigImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * 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 @@ -17,6 +17,8 @@ import java.sql.SQLException; +import java.util.Iterator; + import java.util.function.UnaryOperator; import org.postgresql.pljava.internal.SwitchPointCache.Builder; @@ -57,7 +59,7 @@ int cacheId() private static Simple name(RegConfigImpl o) throws SQLException { TupleTableSlot t = o.cacheTuple(); - return t.get(t.descriptor().get("cfgname"), SIMPLE_INSTANCE); + return t.get(Att.CFGNAME, SIMPLE_INSTANCE); } private static RegNamespace namespace(RegConfigImpl o) @@ -65,13 +67,13 @@ private static RegNamespace namespace(RegConfigImpl o) { TupleTableSlot t = o.cacheTuple(); return - t.get(t.descriptor().get("cfgnamespace"), REGNAMESPACE_INSTANCE); + t.get(Att.CFGNAMESPACE, REGNAMESPACE_INSTANCE); } private static RegRole owner(RegConfigImpl o) throws SQLException { TupleTableSlot t = o.cacheTuple(); - return t.get(t.descriptor().get("cfgowner"), REGROLE_INSTANCE); + return t.get(Att.CFGOWNER, REGROLE_INSTANCE); } /* Implementation of RegConfig */ @@ -106,4 +108,26 @@ private static RegRole owner(RegConfigImpl o) throws SQLException .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 index b4fe6c9ec..3841255aa 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/RegDictionaryImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/RegDictionaryImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * 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 @@ -17,6 +17,8 @@ import java.sql.SQLException; +import java.util.Iterator; + import java.util.function.UnaryOperator; import org.postgresql.pljava.internal.SwitchPointCache.Builder; @@ -57,7 +59,7 @@ int cacheId() private static Simple name(RegDictionaryImpl o) throws SQLException { TupleTableSlot t = o.cacheTuple(); - return t.get(t.descriptor().get("dictname"), SIMPLE_INSTANCE); + return t.get(Att.DICTNAME, SIMPLE_INSTANCE); } private static RegNamespace namespace(RegDictionaryImpl o) @@ -65,13 +67,13 @@ private static RegNamespace namespace(RegDictionaryImpl o) { TupleTableSlot t = o.cacheTuple(); return - t.get(t.descriptor().get("dictnamespace"), REGNAMESPACE_INSTANCE); + t.get(Att.DICTNAMESPACE, REGNAMESPACE_INSTANCE); } private static RegRole owner(RegDictionaryImpl o) throws SQLException { TupleTableSlot t = o.cacheTuple(); - return t.get(t.descriptor().get("dictowner"), REGROLE_INSTANCE); + return t.get(Att.DICTOWNER, REGROLE_INSTANCE); } /* Implementation of RegDictionary */ @@ -106,4 +108,26 @@ private static RegRole owner(RegDictionaryImpl o) throws SQLException .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 index 68d2730e9..2b6607410 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/RegNamespaceImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/RegNamespaceImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * 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 @@ -17,6 +17,7 @@ import java.sql.SQLException; +import java.util.Iterator; import java.util.List; import java.util.function.UnaryOperator; @@ -62,20 +63,20 @@ private static Simple name(RegNamespaceImpl o) throws SQLException { TupleTableSlot t = o.cacheTuple(); return - t.get(t.descriptor().get("nspname"), NameAdapter.SIMPLE_INSTANCE); + t.get(Att.NSPNAME, NameAdapter.SIMPLE_INSTANCE); } private static RegRole owner(RegNamespaceImpl o) throws SQLException { TupleTableSlot t = o.cacheTuple(); - return t.get(t.descriptor().get("nspowner"), REGROLE_INSTANCE); + return t.get(Att.NSPOWNER, REGROLE_INSTANCE); } private static List grants(RegNamespaceImpl o) throws SQLException { TupleTableSlot t = o.cacheTuple(); - return t.get(t.descriptor().get("nspacl"), GrantAdapter.LIST_INSTANCE); + return t.get(Att.NSPACL, GrantAdapter.LIST_INSTANCE); } /* Implementation of RegNamespace */ @@ -113,4 +114,26 @@ private static List grants(RegNamespaceImpl o) */ .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 index 1a0422f2b..034c8aaac 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/RegOperatorImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/RegOperatorImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * 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 @@ -17,6 +17,8 @@ import java.sql.SQLException; +import java.util.Iterator; + import java.util.function.UnaryOperator; import org.postgresql.pljava.internal.SwitchPointCache.Builder; @@ -63,7 +65,7 @@ int cacheId() private static Operator name(RegOperatorImpl o) throws SQLException { TupleTableSlot t = o.cacheTuple(); - return t.get(t.descriptor().get("oprname"), OPERATOR_INSTANCE); + return t.get(Att.OPRNAME, OPERATOR_INSTANCE); } private static RegNamespace namespace(RegOperatorImpl o) @@ -71,13 +73,13 @@ private static RegNamespace namespace(RegOperatorImpl o) { TupleTableSlot t = o.cacheTuple(); return - t.get(t.descriptor().get("oprnamespace"), REGNAMESPACE_INSTANCE); + t.get(Att.OPRNAMESPACE, REGNAMESPACE_INSTANCE); } private static RegRole owner(RegOperatorImpl o) throws SQLException { TupleTableSlot t = o.cacheTuple(); - return t.get(t.descriptor().get("oprowner"), REGROLE_INSTANCE); + return t.get(Att.OPROWNER, REGROLE_INSTANCE); } /* Implementation of RegOperator */ @@ -142,12 +144,67 @@ private static RegRole owner(RegOperatorImpl o) throws SQLException 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(s.descriptor().get("oprkind"), INT1_INSTANCE); + byte b = s.get(Att.OPRKIND, INT1_INSTANCE); switch ( b ) { case (byte)'b': @@ -167,43 +224,43 @@ private static Kind kind(RegOperatorImpl o) throws SQLException private static boolean canMerge(RegOperatorImpl o) throws SQLException { TupleTableSlot s = o.cacheTuple(); - return s.get(s.descriptor().get("oprcanmerge"), BOOLEAN_INSTANCE); + return s.get(Att.OPRCANMERGE, BOOLEAN_INSTANCE); } private static boolean canHash(RegOperatorImpl o) throws SQLException { TupleTableSlot s = o.cacheTuple(); - return s.get(s.descriptor().get("oprcanhash"), BOOLEAN_INSTANCE); + return s.get(Att.OPRCANHASH, BOOLEAN_INSTANCE); } private static RegType leftOperand(RegOperatorImpl o) throws SQLException { TupleTableSlot s = o.cacheTuple(); - return s.get(s.descriptor().get("oprleft"), REGTYPE_INSTANCE); + return s.get(Att.OPRLEFT, REGTYPE_INSTANCE); } private static RegType rightOperand(RegOperatorImpl o) throws SQLException { TupleTableSlot s = o.cacheTuple(); - return s.get(s.descriptor().get("oprright"), REGTYPE_INSTANCE); + return s.get(Att.OPRRIGHT, REGTYPE_INSTANCE); } private static RegType result(RegOperatorImpl o) throws SQLException { TupleTableSlot s = o.cacheTuple(); - return s.get(s.descriptor().get("oprresult"), REGTYPE_INSTANCE); + return s.get(Att.OPRRESULT, REGTYPE_INSTANCE); } private static RegOperator commutator(RegOperatorImpl o) throws SQLException { TupleTableSlot s = o.cacheTuple(); - return s.get(s.descriptor().get("oprcom"), REGOPERATOR_INSTANCE); + return s.get(Att.OPRCOM, REGOPERATOR_INSTANCE); } private static RegOperator negator(RegOperatorImpl o) throws SQLException { TupleTableSlot s = o.cacheTuple(); - return s.get(s.descriptor().get("oprnegate"), REGOPERATOR_INSTANCE); + return s.get(Att.OPRNEGATE, REGOPERATOR_INSTANCE); } private static RegProcedure evaluator(RegOperatorImpl o) @@ -212,7 +269,7 @@ private static RegProcedure evaluator(RegOperatorImpl o) TupleTableSlot s = o.cacheTuple(); @SuppressWarnings("unchecked") // XXX add memo magic here RegProcedure p = (RegProcedure) - s.get(s.descriptor().get("oprcode"), REGPROCEDURE_INSTANCE); + s.get(Att.OPRCODE, REGPROCEDURE_INSTANCE); return p; } @@ -224,7 +281,7 @@ private static RegProcedure evaluator(RegOperatorImpl o) @SuppressWarnings("unchecked") // XXX add memo magic here RegProcedure p = (RegProcedure) - s.get(s.descriptor().get("oprrest"), REGPROCEDURE_INSTANCE); + s.get(Att.OPRREST, REGPROCEDURE_INSTANCE); return p; } @@ -235,7 +292,7 @@ private static RegProcedure evaluator(RegOperatorImpl o) TupleTableSlot s = o.cacheTuple(); @SuppressWarnings("unchecked") // XXX add memo magic here RegProcedure p = (RegProcedure) - s.get(s.descriptor().get("oprjoin"), REGPROCEDURE_INSTANCE); + s.get(Att.OPRJOIN, REGPROCEDURE_INSTANCE); return p; } diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/RegProcedureImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/RegProcedureImpl.java index 3b2e27e15..55fd7dd98 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/RegProcedureImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/RegProcedureImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * 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 @@ -18,6 +18,7 @@ import java.sql.SQLException; import java.sql.SQLXML; +import java.util.Iterator; import java.util.List; import java.util.function.UnaryOperator; @@ -80,27 +81,27 @@ int cacheId() private static Simple name(RegProcedureImpl o) throws SQLException { TupleTableSlot t = o.cacheTuple(); - return t.get(t.descriptor().get("proname"), SIMPLE_INSTANCE); + return t.get(Att.PRONAME, SIMPLE_INSTANCE); } private static RegNamespace namespace(RegProcedureImpl o) throws SQLException { TupleTableSlot t = o.cacheTuple(); - return t.get(t.descriptor().get("pronamespace"), REGNAMESPACE_INSTANCE); + return t.get(Att.PRONAMESPACE, REGNAMESPACE_INSTANCE); } private static RegRole owner(RegProcedureImpl o) throws SQLException { TupleTableSlot t = o.cacheTuple(); - return t.get(t.descriptor().get("proowner"), REGROLE_INSTANCE); + return t.get(Att.PROOWNER, REGROLE_INSTANCE); } private static List grants(RegProcedureImpl o) throws SQLException { TupleTableSlot t = o.cacheTuple(); - return t.get(t.descriptor().get("proacl"), GrantAdapter.LIST_INSTANCE); + return t.get(Att.PROACL, GrantAdapter.LIST_INSTANCE); } /* Implementation of RegProcedure */ @@ -189,31 +190,125 @@ private static List grants(RegProcedureImpl o) 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 PROSUPPORT; + static final Attribute PROKIND; + 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 Attribute PROSQLBODY; + + static + { + Iterator itr = CLASSID.tupleDescriptor().project( + "proname", + "pronamespace", + "proowner", + "proacl", + "prolang", + "procost", + "prorows", + "provariadic", + "prosupport", + "prokind", + "prosecdef", + "proleakproof", + "proisstrict", + "proretset", + "provolatile", + "proparallel", + "prorettype", + "proargtypes", + "proallargtypes", + "proargmodes", + "proargnames", + "protrftypes", + "prosrc", + "probin", + "proconfig", + "proargdefaults", + "prosqlbody" + ).iterator(); + + PRONAME = itr.next(); + PRONAMESPACE = itr.next(); + PROOWNER = itr.next(); + PROACL = itr.next(); + PROLANG = itr.next(); + PROCOST = itr.next(); + PROROWS = itr.next(); + PROVARIADIC = itr.next(); + PROSUPPORT = itr.next(); + PROKIND = 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(); + 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(s.descriptor().get("prolang"), PLANG_INSTANCE); + return s.get(Att.PROLANG, PLANG_INSTANCE); } private static float cost(RegProcedureImpl o) throws SQLException { TupleTableSlot s = o.cacheTuple(); - return s.get(s.descriptor().get("procost"), FLOAT4_INSTANCE); + return s.get(Att.PROCOST, FLOAT4_INSTANCE); } private static float rows(RegProcedureImpl o) throws SQLException { TupleTableSlot s = o.cacheTuple(); - return s.get(s.descriptor().get("prorows"), FLOAT4_INSTANCE); + return s.get(Att.PROROWS, FLOAT4_INSTANCE); } private static RegType variadicType(RegProcedureImpl o) throws SQLException { TupleTableSlot s = o.cacheTuple(); - return s.get(s.descriptor().get("provariadic"), REGTYPE_INSTANCE); + return s.get(Att.PROVARIADIC, REGTYPE_INSTANCE); } private static RegProcedure support(RegProcedureImpl o) @@ -222,14 +317,14 @@ private static RegProcedure support(RegProcedureImpl o) TupleTableSlot s = o.cacheTuple(); @SuppressWarnings("unchecked") // XXX add memo magic here RegProcedure p = (RegProcedure) - s.get(s.descriptor().get("prosupport"), REGPROCEDURE_INSTANCE); + s.get(Att.PROSUPPORT, REGPROCEDURE_INSTANCE); return p; } private static Kind kind(RegProcedureImpl o) throws SQLException { TupleTableSlot s = o.cacheTuple(); - byte b = s.get(s.descriptor().get("prokind"), INT1_INSTANCE); + byte b = s.get(Att.PROKIND, INT1_INSTANCE); switch ( b ) { case (byte)'f': @@ -249,7 +344,7 @@ private static Kind kind(RegProcedureImpl o) throws SQLException private static Security security(RegProcedureImpl o) throws SQLException { TupleTableSlot s = o.cacheTuple(); - if ( s.get(s.descriptor().get("prosecdef"), BOOLEAN_INSTANCE) ) + if ( s.get(Att.PROSECDEF, BOOLEAN_INSTANCE) ) return Security.DEFINER; return Security.INVOKER; } @@ -257,14 +352,14 @@ private static Security security(RegProcedureImpl o) throws SQLException private static boolean leakproof(RegProcedureImpl o) throws SQLException { TupleTableSlot s = o.cacheTuple(); - return s.get(s.descriptor().get("proleakproof"), BOOLEAN_INSTANCE); + return s.get(Att.PROLEAKPROOF, BOOLEAN_INSTANCE); } private static OnNullInput onNullInput(RegProcedureImpl o) throws SQLException { TupleTableSlot s = o.cacheTuple(); - if ( s.get(s.descriptor().get("proisstrict"), BOOLEAN_INSTANCE) ) + if ( s.get(Att.PROISSTRICT, BOOLEAN_INSTANCE) ) return OnNullInput.RETURNS_NULL; return OnNullInput.CALLED; } @@ -272,13 +367,13 @@ private static OnNullInput onNullInput(RegProcedureImpl o) private static boolean returnsSet(RegProcedureImpl o) throws SQLException { TupleTableSlot s = o.cacheTuple(); - return s.get(s.descriptor().get("proretset"), BOOLEAN_INSTANCE); + return s.get(Att.PRORETSET, BOOLEAN_INSTANCE); } private static Effects effects(RegProcedureImpl o) throws SQLException { TupleTableSlot s = o.cacheTuple(); - byte b = s.get(s.descriptor().get("provolatile"), INT1_INSTANCE); + byte b = s.get(Att.PROVOLATILE, INT1_INSTANCE); switch ( b ) { case (byte)'i': @@ -296,7 +391,7 @@ private static Effects effects(RegProcedureImpl o) throws SQLException private static Parallel parallel(RegProcedureImpl o) throws SQLException { TupleTableSlot s = o.cacheTuple(); - byte b = s.get(s.descriptor().get("proparallel"), INT1_INSTANCE); + byte b = s.get(Att.PROPARALLEL, INT1_INSTANCE); switch ( b ) { case (byte)'s': @@ -314,7 +409,7 @@ private static Parallel parallel(RegProcedureImpl o) throws SQLException private static RegType returnType(RegProcedureImpl o) throws SQLException { TupleTableSlot s = o.cacheTuple(); - return s.get(s.descriptor().get("prorettype"), REGTYPE_INSTANCE); + return s.get(Att.PRORETTYPE, REGTYPE_INSTANCE); } private static List argTypes(RegProcedureImpl o) @@ -322,7 +417,7 @@ private static List argTypes(RegProcedureImpl o) { TupleTableSlot s = o.cacheTuple(); return - s.get(s.descriptor().get("proargtypes"), + s.get(Att.PROARGTYPES, ArrayAdapters.REGTYPE_LIST_INSTANCE); } @@ -331,7 +426,7 @@ private static List allArgTypes(RegProcedureImpl o) { TupleTableSlot s = o.cacheTuple(); return - s.get(s.descriptor().get("proallargtypes"), + s.get(Att.PROALLARGTYPES, ArrayAdapters.REGTYPE_LIST_INSTANCE); } @@ -340,7 +435,7 @@ private static List argModes(RegProcedureImpl o) { TupleTableSlot s = o.cacheTuple(); return - s.get(s.descriptor().get("proargmodes"), + s.get(Att.PROARGMODES, ArrayAdapters.ARGMODE_LIST_INSTANCE); } @@ -348,7 +443,7 @@ private static List argNames(RegProcedureImpl o) throws SQLException { TupleTableSlot s = o.cacheTuple(); return - s.get(s.descriptor().get("proargnames"), + s.get(Att.PROARGNAMES, ArrayAdapters.TEXT_NAME_LIST_INSTANCE); } @@ -357,27 +452,27 @@ private static List transformTypes(RegProcedureImpl o) { TupleTableSlot s = o.cacheTuple(); return - s.get(s.descriptor().get("protrftypes"), + s.get(Att.PROTRFTYPES, ArrayAdapters.REGTYPE_LIST_INSTANCE); } private static String src(RegProcedureImpl o) throws SQLException { TupleTableSlot s = o.cacheTuple(); - return s.get(s.descriptor().get("prosrc"), TextAdapter.INSTANCE); + return s.get(Att.PROSRC, TextAdapter.INSTANCE); } private static String bin(RegProcedureImpl o) throws SQLException { TupleTableSlot s = o.cacheTuple(); - return s.get(s.descriptor().get("probin"), TextAdapter.INSTANCE); + return s.get(Att.PROBIN, TextAdapter.INSTANCE); } private static List config(RegProcedureImpl o) throws SQLException { TupleTableSlot s = o.cacheTuple(); return - s.get(s.descriptor().get("proconfig"), FLAT_STRING_LIST_INSTANCE); + s.get(Att.PROCONFIG, FLAT_STRING_LIST_INSTANCE); } /* API methods */ @@ -630,16 +725,7 @@ public SQLXML argDefaults() * tuple as needed. */ TupleTableSlot s = cacheTuple(); - - try - { - return - s.get(s.descriptor().get("proargdefaults"), SYNTHETIC_INSTANCE); - } - catch ( SQLException e ) - { - throw unchecked(e); - } + return s.get(Att.PROARGDEFAULTS, SYNTHETIC_INSTANCE); } @Override @@ -694,16 +780,7 @@ public SQLXML sqlBody() * tuple as needed. */ TupleTableSlot s = cacheTuple(); - - try - { - return - s.get(s.descriptor().get("prosqlbody"), SYNTHETIC_INSTANCE); - } - catch ( SQLException e ) - { - throw unchecked(e); - } + return s.get(Att.PROSQLBODY, SYNTHETIC_INSTANCE); } @Override diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/RegRoleImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/RegRoleImpl.java index c0f535453..5902d0fc4 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/RegRoleImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/RegRoleImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * 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 @@ -20,6 +20,7 @@ import java.sql.SQLException; +import java.util.Iterator; import java.util.List; import org.postgresql.pljava.RolePrincipal; @@ -75,7 +76,7 @@ int cacheId() private static Simple name(RegRoleImpl o) throws SQLException { TupleTableSlot t = o.cacheTuple(); - return t.get(t.descriptor().get("rolname"), SIMPLE_INSTANCE); + return t.get(Att.ROLNAME, SIMPLE_INSTANCE); } private static List grants(RegRoleImpl o) @@ -141,6 +142,46 @@ private static List grants(RegRoleImpl o) 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) @@ -151,49 +192,49 @@ private static List memberOf(RegRoleImpl o) private static boolean superuser(RegRoleImpl o) throws SQLException { TupleTableSlot s = o.cacheTuple(); - return s.get(s.descriptor().get("rolsuper"), BOOLEAN_INSTANCE); + return s.get(Att.ROLSUPER, BOOLEAN_INSTANCE); } private static boolean inherit(RegRoleImpl o) throws SQLException { TupleTableSlot s = o.cacheTuple(); - return s.get(s.descriptor().get("rolinherit"), BOOLEAN_INSTANCE); + return s.get(Att.ROLINHERIT, BOOLEAN_INSTANCE); } private static boolean createRole(RegRoleImpl o) throws SQLException { TupleTableSlot s = o.cacheTuple(); - return s.get(s.descriptor().get("rolcreaterole"), BOOLEAN_INSTANCE); + return s.get(Att.ROLCREATEROLE, BOOLEAN_INSTANCE); } private static boolean createDB(RegRoleImpl o) throws SQLException { TupleTableSlot s = o.cacheTuple(); - return s.get(s.descriptor().get("rolcreatedb"), BOOLEAN_INSTANCE); + return s.get(Att.ROLCREATEDB, BOOLEAN_INSTANCE); } private static boolean canLogIn(RegRoleImpl o) throws SQLException { TupleTableSlot s = o.cacheTuple(); - return s.get(s.descriptor().get("rolcanlogin"), BOOLEAN_INSTANCE); + return s.get(Att.ROLCANLOGIN, BOOLEAN_INSTANCE); } private static boolean replication(RegRoleImpl o) throws SQLException { TupleTableSlot s = o.cacheTuple(); - return s.get(s.descriptor().get("rolreplication"), BOOLEAN_INSTANCE); + return s.get(Att.ROLREPLICATION, BOOLEAN_INSTANCE); } private static boolean bypassRLS(RegRoleImpl o) throws SQLException { TupleTableSlot s = o.cacheTuple(); - return s.get(s.descriptor().get("rolbypassrls"), BOOLEAN_INSTANCE); + return s.get(Att.ROLBYPASSRLS, BOOLEAN_INSTANCE); } private static int connectionLimit(RegRoleImpl o) throws SQLException { TupleTableSlot s = o.cacheTuple(); - return s.get(s.descriptor().get("rolconnlimit"), INT4_INSTANCE); + return s.get(Att.ROLCONNLIMIT, INT4_INSTANCE); } /* API methods */ diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/RegTypeImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/RegTypeImpl.java index e8c012294..621ac2732 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/RegTypeImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/RegTypeImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * 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 @@ -22,10 +22,13 @@ 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; @@ -106,26 +109,26 @@ private static Simple name(RegTypeImpl o) throws SQLException { TupleTableSlot t = o.cacheTuple(); return - t.get(t.descriptor().get("typname"), NameAdapter.SIMPLE_INSTANCE); + t.get(Att.TYPNAME, NameAdapter.SIMPLE_INSTANCE); } private static RegNamespace namespace(RegTypeImpl o) throws SQLException { TupleTableSlot t = o.cacheTuple(); - return t.get(t.descriptor().get("typnamespace"), REGNAMESPACE_INSTANCE); + return t.get(Att.TYPNAMESPACE, REGNAMESPACE_INSTANCE); } private static RegRole owner(RegTypeImpl o) throws SQLException { TupleTableSlot t = o.cacheTuple(); - return t.get(t.descriptor().get("typowner"), REGROLE_INSTANCE); + return t.get(Att.TYPOWNER, REGROLE_INSTANCE); } private static List grants(RegTypeImpl o) throws SQLException { TupleTableSlot t = o.cacheTuple(); - return t.get(t.descriptor().get("typacl"), GrantAdapter.LIST_INSTANCE); + return t.get(Att.TYPACL, GrantAdapter.LIST_INSTANCE); } /* Implementation of RegType */ @@ -287,6 +290,114 @@ void dualHandshake(RegClass dual) 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 TYPSUBSCRIPT; + 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 + { + Projection p = CLASSID.tupleDescriptor().project( + "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", + "typsubscript", + "typalign", + "typstorage", + "typnotnull", + "typndims", + "typcollation", + "typdefault", + "typdefaultbin" + ); + + Iterator itr = p.iterator(); + + TYPBASETYPE_TYPTYPMOD = p.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(); + TYPSUBSCRIPT = itr.next(); + TYPALIGN = itr.next(); + TYPSTORAGE = itr.next(); + TYPNOTNULL = itr.next(); + TYPNDIMS = itr.next(); + TYPCOLLATION = itr.next(); + TYPDEFAULT = itr.next(); + TYPDEFAULTBIN = itr.next(); + + assert ! itr.hasNext() : "attribute initialization miscount"; + } + } + /* computation methods */ /** @@ -361,45 +472,45 @@ private static TupleDescriptor.Interned[] tupleDescriptorBlessed(Blessed o) private static short length(RegTypeImpl o) throws SQLException { TupleTableSlot t = o.cacheTuple(); - return t.get(t.descriptor().get("typlen"), INT2_INSTANCE); + return t.get(Att.TYPLEN, INT2_INSTANCE); } private static boolean byValue(RegTypeImpl o) throws SQLException { TupleTableSlot t = o.cacheTuple(); - return t.get(t.descriptor().get("typbyval"), BOOLEAN_INSTANCE); + return t.get(Att.TYPBYVAL, BOOLEAN_INSTANCE); } private static Type type(RegTypeImpl o) throws SQLException { TupleTableSlot t = o.cacheTuple(); return typeFromCatalog( - t.get(t.descriptor().get("typtype"), INT1_INSTANCE)); + t.get(Att.TYPTYPE, INT1_INSTANCE)); } private static char category(RegTypeImpl o) throws SQLException { TupleTableSlot t = o.cacheTuple(); return (char) - (0xff & t.get(t.descriptor().get("typcategory"), INT1_INSTANCE)); + (0xff & t.get(Att.TYPCATEGORY, INT1_INSTANCE)); } private static boolean preferred(RegTypeImpl o) throws SQLException { TupleTableSlot t = o.cacheTuple(); - return t.get(t.descriptor().get("typispreferred"), BOOLEAN_INSTANCE); + return t.get(Att.TYPISPREFERRED, BOOLEAN_INSTANCE); } private static boolean defined(RegTypeImpl o) throws SQLException { TupleTableSlot t = o.cacheTuple(); - return t.get(t.descriptor().get("typisdefined"), BOOLEAN_INSTANCE); + return t.get(Att.TYPISDEFINED, BOOLEAN_INSTANCE); } private static byte delimiter(RegTypeImpl o) throws SQLException { TupleTableSlot t = o.cacheTuple(); - return t.get(t.descriptor().get("typdelim"), INT1_INSTANCE); + return t.get(Att.TYPDELIM, INT1_INSTANCE); } private static RegClass relation(RegTypeImpl o) throws SQLException @@ -419,7 +530,7 @@ private static RegClass relation(RegTypeImpl o) throws SQLException * that may touch both objects, without complicating their code. */ TupleTableSlot t = o.cacheTuple(); - RegClass c = t.get(t.descriptor().get("typrelid"), REGCLASS_INSTANCE); + RegClass c = t.get(Att.TYPRELID, REGCLASS_INSTANCE); ((RegClassImpl)c).dualHandshake(o); return c; @@ -428,13 +539,13 @@ private static RegClass relation(RegTypeImpl o) throws SQLException private static RegType element(RegTypeImpl o) throws SQLException { TupleTableSlot t = o.cacheTuple(); - return t.get(t.descriptor().get("typelem"), REGTYPE_INSTANCE); + return t.get(Att.TYPELEM, REGTYPE_INSTANCE); } private static RegType array(RegTypeImpl o) throws SQLException { TupleTableSlot t = o.cacheTuple(); - return t.get(t.descriptor().get("typarray"), REGTYPE_INSTANCE); + return t.get(Att.TYPARRAY, REGTYPE_INSTANCE); } private static RegProcedure input(RegTypeImpl o) @@ -443,7 +554,7 @@ private static RegProcedure input(RegTypeImpl o) TupleTableSlot t = o.cacheTuple(); @SuppressWarnings("unchecked") // XXX add memo magic here RegProcedure p = (RegProcedure) - t.get(t.descriptor().get("typinput"), REGPROCEDURE_INSTANCE); + t.get(Att.TYPINPUT, REGPROCEDURE_INSTANCE); return p; } @@ -453,7 +564,7 @@ private static RegProcedure output(RegTypeImpl o) TupleTableSlot t = o.cacheTuple(); @SuppressWarnings("unchecked") // XXX add memo magic here RegProcedure p = (RegProcedure) - t.get(t.descriptor().get("typoutput"), REGPROCEDURE_INSTANCE); + t.get(Att.TYPOUTPUT, REGPROCEDURE_INSTANCE); return p; } @@ -463,7 +574,7 @@ private static RegProcedure receive(RegTypeImpl o) TupleTableSlot t = o.cacheTuple(); @SuppressWarnings("unchecked") // XXX add memo magic here RegProcedure p = (RegProcedure) - t.get(t.descriptor().get("typreceive"), REGPROCEDURE_INSTANCE); + t.get(Att.TYPRECEIVE, REGPROCEDURE_INSTANCE); return p; } @@ -473,7 +584,7 @@ private static RegProcedure send(RegTypeImpl o) TupleTableSlot t = o.cacheTuple(); @SuppressWarnings("unchecked") // XXX add memo magic here RegProcedure p = (RegProcedure) - t.get(t.descriptor().get("typsend"), REGPROCEDURE_INSTANCE); + t.get(Att.TYPSEND, REGPROCEDURE_INSTANCE); return p; } @@ -483,7 +594,7 @@ private static RegProcedure modifierInput(RegTypeImpl o) TupleTableSlot t = o.cacheTuple(); @SuppressWarnings("unchecked") // XXX add memo magic here RegProcedure p = (RegProcedure) - t.get(t.descriptor().get("typmodin"), REGPROCEDURE_INSTANCE); + t.get(Att.TYPMODIN, REGPROCEDURE_INSTANCE); return p; } @@ -494,7 +605,7 @@ private static RegProcedure modifierOutput( TupleTableSlot t = o.cacheTuple(); @SuppressWarnings("unchecked") // XXX add memo magic here RegProcedure p = (RegProcedure) - t.get(t.descriptor().get("typmodout"), REGPROCEDURE_INSTANCE); + t.get(Att.TYPMODOUT, REGPROCEDURE_INSTANCE); return p; } @@ -504,7 +615,7 @@ private static RegProcedure analyze(RegTypeImpl o) TupleTableSlot t = o.cacheTuple(); @SuppressWarnings("unchecked") // XXX add memo magic here RegProcedure p = (RegProcedure) - t.get(t.descriptor().get("typanalyze"), REGPROCEDURE_INSTANCE); + t.get(Att.TYPANALYZE, REGPROCEDURE_INSTANCE); return p; } @@ -514,7 +625,7 @@ private static RegProcedure subscript(RegTypeImpl o) TupleTableSlot t = o.cacheTuple(); @SuppressWarnings("unchecked") // XXX add memo magic here RegProcedure p = (RegProcedure) - t.get(t.descriptor().get("typsubscript"), REGPROCEDURE_INSTANCE); + t.get(Att.TYPSUBSCRIPT, REGPROCEDURE_INSTANCE); return p; } @@ -522,47 +633,48 @@ private static Alignment alignment(RegTypeImpl o) throws SQLException { TupleTableSlot t = o.cacheTuple(); return alignmentFromCatalog( - t.get(t.descriptor().get("typalign"), INT1_INSTANCE)); + t.get(Att.TYPALIGN, INT1_INSTANCE)); } private static Storage storage(RegTypeImpl o) throws SQLException { TupleTableSlot t = o.cacheTuple(); return storageFromCatalog( - t.get(t.descriptor().get("typstorage"), INT1_INSTANCE)); + t.get(Att.TYPSTORAGE, INT1_INSTANCE)); } private static boolean notNull(RegTypeImpl o) throws SQLException { TupleTableSlot t = o.cacheTuple(); - return t.get(t.descriptor().get("typnotnull"), BOOLEAN_INSTANCE); + return t.get(Att.TYPNOTNULL, BOOLEAN_INSTANCE); } private static RegType baseType(RegTypeImpl o) throws SQLException { TupleTableSlot t = o.cacheTuple(); - TupleDescriptor td = t.descriptor(); - int oid = t.get(td.get("typbasetype"), OidAdapter.INT4_INSTANCE); - int mod = t.get(td.get("typtypmod"), INT4_INSTANCE); - return CatalogObjectImpl.Factory.formMaybeModifiedType(oid, mod); + 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(t.descriptor().get("typndims"), INT4_INSTANCE); + return t.get(Att.TYPNDIMS, INT4_INSTANCE); } private static RegCollation collation(RegTypeImpl o) throws SQLException { TupleTableSlot t = o.cacheTuple(); - return t.get(t.descriptor().get("typcollation"), REGCOLLATION_INSTANCE); + return t.get(Att.TYPCOLLATION, REGCOLLATION_INSTANCE); } private static String defaultText(RegTypeImpl o) throws SQLException { TupleTableSlot t = o.cacheTuple(); - return t.get(t.descriptor().get("typdefault"), TextAdapter.INSTANCE); + return t.get(Att.TYPDEFAULT, TextAdapter.INSTANCE); } /* API methods */ @@ -936,16 +1048,7 @@ public SQLXML defaultBin() * tuple as needed. */ TupleTableSlot s = cacheTuple(); - - try - { - return - s.get(s.descriptor().get("typdefaultbin"), SYNTHETIC_INSTANCE); - } - catch ( SQLException e ) - { - throw unchecked(e); - } + return s.get(Att.TYPDEFAULTBIN, SYNTHETIC_INSTANCE); } @Override diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/TupleDescImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/TupleDescImpl.java index 915852691..5775d383c 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/TupleDescImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/TupleDescImpl.java @@ -78,7 +78,6 @@ abstract class TupleDescImpl extends AbstractList { private final ByteBuffer m_td; private final Attribute[] m_attrs; - private final Map m_byName; private final State m_state; /* @@ -204,14 +203,6 @@ private TupleDescImpl( attrs[i] = ctor.apply(this, 1 + i); m_attrs = attrs; - - /* - * A stopgap. There is probably a lighter-weight API to be designed - * that doesn't assume every TupleDescriptor has a HashMap. Application - * code often knows what all the names are of the attributes it will be - * interested in. - */ - m_byName = new ConcurrentHashMap<>(m_attrs.length); } /** @@ -223,7 +214,6 @@ private TupleDescImpl(RegType type) m_state = null; m_td = null; m_attrs = new Attribute[] { new AttributeImpl.OfType(this, type) }; - m_byName = new ConcurrentHashMap<>(m_attrs.length); } /** @@ -338,46 +328,6 @@ private static TupleDescriptor fromByteBuffer( return new Ephemeral(copy); } - @Override - public List attributes() - { - return this; - } - - @Override - public Attribute get(Simple name) throws SQLException - { - /* - * computeIfAbsent would be notationally simple here, but its guarantees - * aren't needed (this isn't a place where uniqueness needs to be - * enforced) and it's tricky to rule out that some name() call in the - * update could recursively end up here. So the longer check, compute, - * putIfAbsent is good enough. - */ - Attribute found = m_byName.get(name); - if ( null != found ) - return found; - - for ( int i = m_byName.size() ; i < m_attrs.length ; ++ i ) - { - Attribute a = m_attrs[i]; - Simple n = a.name(); - Attribute other = m_byName.putIfAbsent(n, a); - assert null == other || found == other - : "TupleDescriptor name cache"; - if ( ! name.equals(n) ) - continue; - found = a; - break; - } - - if ( null == found ) - throw new SQLSyntaxErrorException( - "no such column: " + name, "42703"); - - return found; - } - @Override public Attribute sqlGet(int index) { diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/TupleTableSlotImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/TupleTableSlotImpl.java index 937644302..c9143125a 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/TupleTableSlotImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/TupleTableSlotImpl.java @@ -202,7 +202,7 @@ public abstract class TupleTableSlotImpl * From the Heap constructor, it may be null. */ m_isnull = null == isnull ? null : asReadOnlyNativeOrder(isnull); - m_adapters = new Adapter [ m_tupdesc.attributes().size() ]; + m_adapters = new Adapter [ m_tupdesc.size() ]; @SuppressWarnings("unchecked") Object dummy = @@ -306,7 +306,7 @@ static Heap heapTupleGetLightSlot( if ( 0 != ( infomask & HEAP_HASNULL ) ) { - int nlen = ( td.attributes().size() + 7 ) / 8; + int nlen = ( td.size() + 7 ) / 8; if ( nlen + OFFSET_HeapTupleHeaderData_t_bits > hoff ) { int attsReallyPresent = infomask2 & HEAP_NATTS_MASK; @@ -371,7 +371,7 @@ protected int toIndex(Attribute att, Adapter adp) */ protected Attribute fromIndex(int idx, Adapter adp) { - Attribute att = m_tupdesc.attributes().get(idx); + Attribute att = m_tupdesc.get(idx); if ( m_adapters [ idx ] != requireNonNull(adp) ) memoize(idx, att, adp); return att; @@ -536,7 +536,7 @@ protected boolean isNull(int idx) protected int toOffset(int idx) { int offset = 0; - List atts = m_tupdesc.attributes(); + List atts = m_tupdesc; Attribute att; /* @@ -627,13 +627,13 @@ static class Indexed extends Heap implements TupleTableSlot.Indexed TupleDescriptor td, int elements, ByteBuffer nulls, ByteBuffer values) { - super(td.attributes().get(0).relation(), td, values, nulls); + 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.attributes().get(0); + Attribute att = td.get(0); int length = att.length(); int align = alignmentModulus(att.alignment()); assert 0 == values.alignmentOffset(0, align) @@ -666,7 +666,7 @@ public int elements() protected Attribute fromIndex(int idx, Adapter adp) { checkIndex(idx, m_elements); - Attribute att = m_tupdesc.attributes().get(0); + Attribute att = m_tupdesc.get(0); if ( m_adapters [ 0 ] != requireNonNull(adp) ) memoize(0, att, adp); return att; From cb98c804bc49ad056b3c2e127444e8c0eccbdf23 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 14 Aug 2023 20:19:45 -0400 Subject: [PATCH 120/334] Add test for CatalogObject class initializations Now that they project all the expected attribute names in advance, they will break more dependably on PostgreSQL versions that don't have the same attributes, but still only when methods requiring holder class initialization are used, so it is good to have a test that does that. Not departing from the task at hand, add an AttNames utility class in CatalogObjectImpl to simplify assigning these Attribute static finals conditionally, based on the PG version, and use it in RegTypeImpl and RegProcedureImpl to get them working back to PG 13 (as far back as this can otherwise be built without other patches). --- .../example/annotation/CatalogObjects.java | 212 ++++++++++++++++++ .../pljava/pg/CatalogObjectImpl.java | 101 ++++++++- .../pljava/pg/RegProcedureImpl.java | 10 +- .../org/postgresql/pljava/pg/RegTypeImpl.java | 32 ++- .../postgresql/pljava/pg/TargetListImpl.java | 20 +- 5 files changed, 353 insertions(+), 22 deletions(-) create mode 100644 pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/CatalogObjects.java 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/src/main/java/org/postgresql/pljava/pg/CatalogObjectImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/CatalogObjectImpl.java index 3512479fb..bd6f3a1d8 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/CatalogObjectImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/CatalogObjectImpl.java @@ -14,6 +14,7 @@ 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; @@ -30,7 +31,7 @@ import static org.postgresql.pljava.model.MemoryContext.JavaMemoryContext; import static org.postgresql.pljava.pg.MemoryContextImpl.allocatingIn; -import static org.postgresql.pljava.pg.ModelConstants.PG_VERSION_NUM; +import org.postgresql.pljava.pg.ModelConstants; import static org.postgresql.pljava.pg.TupleTableSlotImpl.heapTupleGetLightSlot; import org.postgresql.pljava.pg.adt.ArrayAdapter; @@ -57,9 +58,13 @@ 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; @@ -71,6 +76,8 @@ */ public class CatalogObjectImpl implements CatalogObject { + static final int PG_VERSION_NUM = ModelConstants.PG_VERSION_NUM; + /** * ByteBuffer representing the PostgreSQL object address: {@code classid}, * {@code objid}, {@code objsubid}. @@ -1123,4 +1130,96 @@ static int murmurhash32(int h) h ^= h >>> 16; return h; } + + /** + * Utility class to create a {@link Projection Projection} using attribute + * names that may be conditional (on something like {@code PG_VERSION_NUM}). + *

    + * {@code andIf} 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 andIf(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().andIf(true, names); + } } diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/RegProcedureImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/RegProcedureImpl.java index 55fd7dd98..02c83ecea 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/RegProcedureImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/RegProcedureImpl.java @@ -222,7 +222,7 @@ static class Att static { - Iterator itr = CLASSID.tupleDescriptor().project( + Iterator itr = attNames( "proname", "pronamespace", "proowner", @@ -248,9 +248,10 @@ static class Att "prosrc", "probin", "proconfig", - "proargdefaults", + "proargdefaults" + ).andIf(PG_VERSION_NUM >= 140000, "prosqlbody" - ).iterator(); + ).project(CLASSID.tupleDescriptor()); PRONAME = itr.next(); PRONAMESPACE = itr.next(); @@ -779,6 +780,9 @@ public SQLXML sqlBody() * 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); } diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/RegTypeImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/RegTypeImpl.java index 621ac2732..226cdab43 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/RegTypeImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/RegTypeImpl.java @@ -315,7 +315,6 @@ static class Att static final Attribute TYPMODIN; static final Attribute TYPMODOUT; static final Attribute TYPANALYZE; - static final Attribute TYPSUBSCRIPT; static final Attribute TYPALIGN; static final Attribute TYPSTORAGE; static final Attribute TYPNOTNULL; @@ -323,10 +322,11 @@ static class Att static final Attribute TYPCOLLATION; static final Attribute TYPDEFAULT; static final Attribute TYPDEFAULTBIN; + static final Attribute TYPSUBSCRIPT; static { - Projection p = CLASSID.tupleDescriptor().project( + AttNames itr = attNames( "typbasetype", // these two are wanted "typtypmod", // together, first, below "typname", @@ -350,7 +350,6 @@ static class Att "typmodin", "typmodout", "typanalyze", - "typsubscript", "typalign", "typstorage", "typnotnull", @@ -358,11 +357,11 @@ static class Att "typcollation", "typdefault", "typdefaultbin" - ); - - Iterator itr = p.iterator(); + ).andIf(PG_VERSION_NUM >= 140000, + "typsubscript" + ).project(CLASSID.tupleDescriptor()); - TYPBASETYPE_TYPTYPMOD = p.project(itr.next(), itr.next()); + TYPBASETYPE_TYPTYPMOD = itr.project(itr.next(), itr.next()); TYPNAME = itr.next(); TYPNAMESPACE = itr.next(); @@ -385,7 +384,6 @@ static class Att TYPMODIN = itr.next(); TYPMODOUT = itr.next(); TYPANALYZE = itr.next(); - TYPSUBSCRIPT = itr.next(); TYPALIGN = itr.next(); TYPSTORAGE = itr.next(); TYPNOTNULL = itr.next(); @@ -393,6 +391,7 @@ static class Att TYPCOLLATION = itr.next(); TYPDEFAULT = itr.next(); TYPDEFAULTBIN = itr.next(); + TYPSUBSCRIPT = itr.next(); assert ! itr.hasNext() : "attribute initialization miscount"; } @@ -622,11 +621,20 @@ private static RegProcedure analyze(RegTypeImpl o) private static RegProcedure subscript(RegTypeImpl o) throws SQLException { - TupleTableSlot t = o.cacheTuple(); + 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 p = (RegProcedure) - t.get(Att.TYPSUBSCRIPT, REGPROCEDURE_INSTANCE); - return p; + RegProcedure narrowed = (RegProcedure)p; + + return narrowed; } private static Alignment alignment(RegTypeImpl o) throws SQLException diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/TargetListImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/TargetListImpl.java index f58f443d9..b6f337258 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/TargetListImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/TargetListImpl.java @@ -181,9 +181,13 @@ static Projection project(TupleDescriptor src, Simple... names) int n = src.size(); - if ( names.length > n ) - throw new IllegalArgumentException(String.format( - "project() more than %d attribute names supplied", n)); + /* + * 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); @@ -355,9 +359,13 @@ public Projection project(Simple... names) int n = sup.m_map.length; - if ( names.length > n ) - throw new IllegalArgumentException(String.format( - "project() more than %d attribute names supplied", n)); + /* + * 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); From 630717d3b850be848594f66b53af902b3eb2b916 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 15 Aug 2023 00:33:26 -0400 Subject: [PATCH 121/334] Narrow visibility of CatalogObjectImpl additions The addition in cb98c80 of AttNames, attNames, and a copy of PG_VERSION_NUM in CatalogObjectImpl were intended to make them readily available in subclasses without extra ceremony, when needed to conditionally construct projections of catalog attributes. But it turns out there are compilation units that import static CatalogObjectImpl.* and so also saw these additions. That's a problem if something also does import static ModelConstants.* and then tries to mention PG_VERSION_NUM. Narrow the visibility of the newly added things by puttng them in CatalogObjectImpl.Addressed instead of CatalogObjectImpl. They will still be visible by inheritance exactly where they are wanted (objects that need to project attributes for their catalog tuples must by definition descend from Addressed), but not to things that import CatalogObjectImpl.*, and there is no foreseeable reason anything would import Addressed.*. --- .../pljava/pg/CatalogObjectImpl.java | 200 ++++++++++-------- 1 file changed, 106 insertions(+), 94 deletions(-) diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/CatalogObjectImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/CatalogObjectImpl.java index bd6f3a1d8..649fd1b90 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/CatalogObjectImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/CatalogObjectImpl.java @@ -32,6 +32,7 @@ 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; @@ -76,8 +77,6 @@ */ public class CatalogObjectImpl implements CatalogObject { - static final int PG_VERSION_NUM = ModelConstants.PG_VERSION_NUM; - /** * ByteBuffer representing the PostgreSQL object address: {@code classid}, * {@code objid}, {@code objsubid}. @@ -621,6 +620,16 @@ private static void invalidateType(int oidHash) 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. @@ -880,6 +889,101 @@ public String toString() } 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 andIf} 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 andIf(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().andIf(true, names); + } } /** @@ -1130,96 +1234,4 @@ static int murmurhash32(int h) h ^= h >>> 16; return h; } - - /** - * Utility class to create a {@link Projection Projection} using attribute - * names that may be conditional (on something like {@code PG_VERSION_NUM}). - *

    - * {@code andIf} 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 andIf(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().andIf(true, names); - } } From a0ce0eaeb62de8f9348f0d30100408009d09f942 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 15 Aug 2023 12:32:22 -0400 Subject: [PATCH 122/334] javadoc: a sterner note about the frankenstream A stream() method built this way is something of an abuse of Java streams. It works ok in tested cases, but is probably not yet in the form it should have in a released API. --- .../src/main/java/org/postgresql/pljava/TargetList.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pljava-api/src/main/java/org/postgresql/pljava/TargetList.java b/pljava-api/src/main/java/org/postgresql/pljava/TargetList.java index d1b877317..3ba58d062 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/TargetList.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/TargetList.java @@ -305,7 +305,13 @@ interface Cursor extends Iterator, Iterable * 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. Naturally, this method does - * not return a parallel {@code Stream}. + * not return a parallel {@code Stream}. Do not call + * {@link Stream#parallel() parallel()} on any pipeline built from this + * 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 From 5cfdad8fd517172123a87efa4f1c31c5a450e92d Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 16 Aug 2023 10:17:19 -0400 Subject: [PATCH 123/334] Javadoc: clarify TupleDescriptor.rowType, intern Also a bit more in RegType on the distinction between char and "char". --- .../java/org/postgresql/pljava/model/RegType.java | 4 ++++ .../postgresql/pljava/model/TupleDescriptor.java | 13 ++++++++----- 2 files changed, 12 insertions(+), 5 deletions(-) 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 index e23fec045..50058de0a 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/model/RegType.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/model/RegType.java @@ -74,6 +74,10 @@ public interface RegType 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); 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 index c5f6e1e85..f3cff5a08 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/model/TupleDescriptor.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/model/TupleDescriptor.java @@ -61,7 +61,8 @@ default List attributes() * 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. + * 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 @@ -112,7 +113,8 @@ default Attribute get(String name) throws SQLException /** * 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. + * 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 @@ -121,9 +123,10 @@ default Attribute get(String name) throws SQLException 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. + * 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. From 29bafaba30ca23eff5b58228e9c4b66b52720da3 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Thu, 17 Aug 2023 20:17:55 -0400 Subject: [PATCH 124/334] Further tame the frankenstream To make the stream less of a foot-gun, base it on a Spliterator that never can split. In passing, have TupleList.SPI implement RandomAccess. --- .../org/postgresql/pljava/TargetList.java | 8 +-- .../postgresql/pljava/pg/TargetListImpl.java | 12 ++-- .../org/postgresql/pljava/pg/TupleList.java | 59 ++++++++++++++++++- 3 files changed, 69 insertions(+), 10 deletions(-) diff --git a/pljava-api/src/main/java/org/postgresql/pljava/TargetList.java b/pljava-api/src/main/java/org/postgresql/pljava/TargetList.java index 3ba58d062..3c1619caa 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/TargetList.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/TargetList.java @@ -304,10 +304,10 @@ interface Cursor extends Iterator, Iterable * 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. Naturally, this method does - * not return a parallel {@code Stream}. Do not call - * {@link Stream#parallel() parallel()} on any pipeline built from this - * stream. + * 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 diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/TargetListImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/TargetListImpl.java index b6f337258..e3b41469f 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/TargetListImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/TargetListImpl.java @@ -48,6 +48,7 @@ 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; @@ -552,12 +553,15 @@ public Stream stream() Iterator itr = iterator(); Spliterator spl; int chr = IMMUTABLE | NONNULL | ORDERED; + long est = Long.MAX_VALUE; if ( m_slots instanceof Collection ) - spl = Spliterators - .spliterator(itr, ((Collection)m_slots).size(), chr); - else - spl = spliteratorUnknownSize(itr, chr); + { + est = ((Collection)m_slots).size(); + chr |= SIZED; + } + + spl = new TupleList.IteratorNonSpliterator<>(itr, est, chr); return StreamSupport.stream(spl, false); } diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/TupleList.java b/pljava/src/main/java/org/postgresql/pljava/pg/TupleList.java index 641105c96..3a8c78cc1 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/TupleList.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/TupleList.java @@ -16,8 +16,17 @@ 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; @@ -79,6 +88,51 @@ public TupleTableSlot get(int i) } } + /** + * 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}. *

    @@ -87,7 +141,8 @@ public TupleTableSlot get(int i) * {@code Invocation}. This class merely maps the native tuple table in * place, and so will prevent later access. */ - class SPI extends AbstractList implements TupleList + class SPI extends AbstractList + implements TupleList, RandomAccess { private final State state; private final TupleTableSlotImpl ttSlot; From 1fc0be285dec3dd20aca51d1c37b321f6b47718b Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 20 Aug 2023 17:47:43 -0400 Subject: [PATCH 125/334] Migrate SQLXMLImpl.Readable, VarlenaWrapper->Datum The only mentions of VarlenaWrapper remaining in SQLXMLImpl are the ones for the to-PostgreSQL direction (for which there isn't any support in Datum or TupleTableSlot yet). It should, in principle, be possible now to supply it with Datum.Input instances that are not VarlenaWrapper instances. Some methods that weren't exposed as Datum API are now there where they belong. --- .../org/postgresql/pljava/adt/spi/Datum.java | 49 ++- pljava-so/src/main/c/VarlenaWrapper.c | 16 +- pljava-so/src/main/c/type/SQLXMLImpl.c | 8 +- .../pljava/internal/VarlenaWrapper.java | 33 +- .../pljava/internal/VarlenaXMLRenderer.java | 18 +- .../pljava/jdbc/PgNodeTreeAsXML.java | 6 +- .../postgresql/pljava/jdbc/SQLXMLImpl.java | 76 ++--- .../org/postgresql/pljava/pg/DatumImpl.java | 308 +++++++++++------- 8 files changed, 297 insertions(+), 217 deletions(-) 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 index 69044418e..84fbd669c 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * 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 @@ -29,11 +29,23 @@ */ 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 + interface Input extends Datum { default void pin() throws SQLException { @@ -48,9 +60,40 @@ 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; - T inputStream() 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; } /** diff --git a/pljava-so/src/main/c/VarlenaWrapper.c b/pljava-so/src/main/c/VarlenaWrapper.c index 21b90e82e..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-2022 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; @@ -348,7 +351,7 @@ Datum pljava_VarlenaWrapper_adopt(jobject vlw) void *final_result; #endif - p2l.longVal = JNI_callLongMethodLocked(vlw, s_VarlenaWrapper_adopt); + p2l.longVal = JNI_callLongMethodLocked(vlw, s_DatumImpl_adopt); #if PG_VERSION_NUM >= 90500 return PointerGetDatum(p2l.ptrVal); #else @@ -443,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")); @@ -461,8 +467,8 @@ void pljava_VarlenaWrapper_initialize(void) s_VarlenaWrapper_Output_class, "", "(JJJLjava/nio/ByteBuffer;)V"); - s_VarlenaWrapper_adopt = PgObject_getJavaMethod( - s_VarlenaWrapper_class, "adopt", "()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/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/src/main/java/org/postgresql/pljava/internal/VarlenaWrapper.java b/pljava/src/main/java/org/postgresql/pljava/internal/VarlenaWrapper.java index 34a8dfbda..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-2022 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 @@ -44,6 +44,7 @@ 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; @@ -182,36 +183,6 @@ public long adopt() throws SQLException } } - @Override @SuppressWarnings("unchecked") - public T inputStream() - throws SQLException - { - return (T) new Stream(this); - } - - public static class Stream - extends DatumImpl.Input.Stream - implements VarlenaWrapper - { - private Stream(VarlenaWrapper.Input datum) throws SQLException - { - super(datum); - } - - @Override - public String toString() - { - return toString(this); - } - - @Override - public String toString(Object o) - { - return String.format("%s %s", - m_datum.toString(o), m_open ? "open" : "closed"); - } - } - private static class State 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 a0f24e32b..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-2022 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,28 +20,32 @@ 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 = SERVER_ENCODING.charset(); @@ -64,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/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/SQLXMLImpl.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java index 8541e96b0..3b6e28934 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-2022 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 @@ -186,11 +186,12 @@ 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; @@ -200,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 ) { @@ -338,13 +339,13 @@ public static SQLXML newReadable( Datum.Input datum, RegType pgType, boolean synthetic) throws SQLException { - VarlenaWrapper.Input vwi = (VarlenaWrapper.Input)datum; + Datum.Input di = (Datum.Input)datum; int oid = pgType.oid(); if ( synthetic ) - return new Readable.Synthetic(vwi, oid); + return new Readable.Synthetic(di, oid); - return new Readable.PgXML(vwi, oid); + return new Readable.PgXML(di, oid); } /** @@ -368,12 +369,12 @@ public 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); @@ -389,15 +390,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 @@ -426,7 +427,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(); @@ -536,7 +537,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); @@ -719,7 +720,7 @@ private static boolean useWrappingElement(InputStream is, Reader r) - static abstract class Readable + static abstract class Readable extends SQLXMLImpl { private static final VarHandle s_readableVH; @@ -744,16 +745,16 @@ 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; } @@ -957,13 +958,13 @@ protected String toString(Object o) ? "" : "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.inputStream(), oid); + super(di.inputStream(), oid); } /** @@ -995,18 +996,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()::verify); - return vw; + is.verify(new Verifier()::verify); + return is; } /* @@ -1016,7 +1016,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 }; @@ -1028,7 +1028,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); @@ -1037,7 +1037,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); @@ -1046,7 +1046,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); @@ -1055,7 +1055,7 @@ protected Adjusting.XML.StAXSource toStAXSource( @Override protected Adjusting.XML.DOMSource toDOMSource( - VarlenaWrapper.Input.Stream backing) + T backing) throws SQLException, SAXException, IOException, ParserConfigurationException @@ -1067,19 +1067,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"); @@ -1087,7 +1087,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"); diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/DatumImpl.java b/pljava/src/main/java/org/postgresql/pljava/pg/DatumImpl.java index 94f393245..3e4c906b8 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/DatumImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/DatumImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * 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 @@ -26,6 +26,8 @@ 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}. *

    @@ -36,16 +38,63 @@ */ 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 + 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. @@ -61,6 +110,13 @@ public JavaCopy(ByteBuffer b) 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 { @@ -83,157 +139,157 @@ public long adopt() throws SQLException "XXX Datum JavaCopy.adopt"); } } + } - @Override @SuppressWarnings("unchecked") - public T inputStream() - throws SQLException - { - return (T) new Stream<>(this); - } + /** + * {@link InputStream InputStream} view of a {@code Datum.Input}. + */ + public static class IStream + extends ByteBufferInputStream implements DatumImpl + { + protected final T m_datum; /** - * {@link InputStream InputStream} view of a {@code Datum.Input}. + * 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}). */ - public static class Stream - extends ByteBufferInputStream implements DatumImpl + private final ByteBuffer m_movingBuffer; + + protected IStream(T datum) throws SQLException { - protected final T m_datum; + m_datum = datum; + ByteBuffer b = datum.buffer(); + m_movingBuffer = b.duplicate().order(b.order()); + } - /** - * 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; + @Override + public String toString(Object o) + { + return String.format("%s %s", + m_datum.toString(o), m_open ? "open" : "closed"); + } - protected Stream(T datum) throws SQLException + @Override + protected void pin() throws IOException + { + if ( ! m_open ) + throw new IOException("Read from closed Datum"); + try { - m_datum = datum; - ByteBuffer b = datum.buffer(); - m_movingBuffer = b.duplicate().order(b.order()); + m_datum.pin(); } - - @Override - protected void pin() throws IOException + catch ( SQLException e ) { - if ( ! m_open ) - throw new IOException("Read from closed Datum"); - try - { - m_datum.pin(); - } - catch ( SQLException e ) - { - throw new IOException(e.getMessage(), e); - } + throw new IOException(e.getMessage(), e); } + } - @Override - protected void unpin() + @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 { - m_datum.unpin(); + super.close(); + m_datum.close(); } - - @Override - protected ByteBuffer buffer() + finally { - return m_movingBuffer; + unpin(); } + } - @Override - public void close() throws IOException + @Override + public long adopt() throws SQLException + { + m_datum.pin(); + try { - if ( m_datum.pinUnlessReleased() ) - return; - try - { - super.close(); - m_datum.close(); - } - finally - { - unpin(); - } + if ( ! m_open ) + throw new SQLException( + "Cannot adopt Datum.Input after " + + "it is closed", "55000"); + return m_datum.adopt(); } - - @Override - public long adopt() throws SQLException + finally { - m_datum.pin(); - try - { - if ( ! m_open ) - throw new SQLException( - "Cannot adopt VarlenaWrapper.Input after " + - "it is closed", "55000"); - return m_datum.adopt(); - } - finally - { - m_datum.unpin(); - } + 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. + /** + * 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. */ - 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 + m_datum.pin(); + try + { + ByteBuffer buf = buffer(); + synchronized ( m_lock ) { - 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) { - 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_datum.unpin(); + @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(); + } } } } From 40788c11f29171d12a9ec910d22dc593d5f8dde3 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 20 Aug 2023 20:32:46 -0400 Subject: [PATCH 126/334] Assume dlfcn.h exists except on Windows postgres/postgres@ca1e855 --- pljava-so/src/main/c/Backend.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pljava-so/src/main/c/Backend.c b/pljava-so/src/main/c/Backend.c index 287652fd2..9c60e5c73 100644 --- a/pljava-so/src/main/c/Backend.c +++ b/pljava-so/src/main/c/Backend.c @@ -28,7 +28,7 @@ #include #if PG_VERSION_NUM >= 120000 - #ifdef HAVE_DLOPEN + #if defined(HAVE_DLOPEN) || PG_VERSION_NUM >= 160000 && ! defined(WIN32) #include #endif #define pg_dlopen(f) dlopen((f), RTLD_NOW | RTLD_GLOBAL) From 2511321530cf1ccdb0e4a95cdf35ab8a64c40f78 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sun, 20 Aug 2023 20:41:35 -0400 Subject: [PATCH 127/334] Rely on __func__ being supported postgres/postgres@320f92b --- pljava-so/src/main/c/Function.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pljava-so/src/main/c/Function.c b/pljava-so/src/main/c/Function.c index a46c0e6dd..f975fbe56 100644 --- a/pljava-so/src/main/c/Function.c +++ b/pljava-so/src/main/c/Function.c @@ -34,6 +34,10 @@ #include #include +#if PG_VERSION_NUM >= 160000 +#define PG_FUNCNAME_MACRO __func__ +#endif + #ifdef _MSC_VER # define strcasecmp _stricmp # define strncasecmp _strnicmp From e004f0a25e0b8e503c0c2377ade9def969aff48b Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 22 Aug 2023 16:24:29 -0400 Subject: [PATCH 128/334] Upstream has refactored aclcheck functions postgres/postgres@c727f51 --- pljava-so/src/main/c/Function.c | 2 +- pljava-so/src/main/c/type/AclId.c | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/pljava-so/src/main/c/Function.c b/pljava-so/src/main/c/Function.c index f975fbe56..3a2bd5b10 100644 --- a/pljava-so/src/main/c/Function.c +++ b/pljava-so/src/main/c/Function.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2022 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 diff --git a/pljava-so/src/main/c/type/AclId.c b/pljava-so/src/main/c/type/AclId.c index 504b8cf6a..81b31538d 100644 --- a/pljava-so/src/main/c/type/AclId.c +++ b/pljava-so/src/main/c/type/AclId.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,12 @@ #include "org_postgresql_pljava_internal_AclId.h" #include "pljava/Exception.h" +#if PG_VERSION_NUM >= 160000 +#include +#define pg_namespace_aclcheck(oid,rid,mode) \ + object_aclcheck(NamespaceRelationId, (oid), (rid), (mode)) +#endif + static jclass s_AclId_class; static jmethodID s_AclId_init; static jfieldID s_AclId_m_native; From 5559b8a13b6d91e34136dc59133c1c2042cda4f3 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 23 Aug 2023 11:10:50 -0400 Subject: [PATCH 129/334] Add Java 17 names for XML implementation props Java 17 adds (and prominently documents in the java.xml module-info javadocs), new, standardized, easy-to-remember names for a dozen or so of the XML implementation-specific properties and features. And the standardized names are ... different from the old ones. More names to feed into setFirstSupported{Feature,Property} when trying to adjust things. The naming headaches are now visible outside of just the implementation module, because there are new standardized names for a Transformer property and feature also, which are used in the XSLT example code in PassXML. So this is a good time to factor a setFirstSupported(...) generic function out of the various copies buried in SQLXMLImpl, and expose it in the Adjusting.XML API so client code can use it too, and use it that way in that example. The Adjusting.XML API was added in some haste, and used to say "the adjusting methods are best-effort and do not provide an indication of whether the requested adjustment was made". In fact it was pushed with some debug code doing exception stacktraces to standard error, which may have been an annoyance at any site that was using the feature heavily. Take this opportunity to do something more systematic, and add a lax(boolean) method allowing it to be tailored. Stacktraces will still be logged if no tailoring is done, but may be more concise, as all of the exceptions that might be encountered in a chain of adjustments will be chained together with addSuppressed(), and common stacktrace elements should be elided when those are logged. In passing, copyedit some Adjusting.XML javadoc to use styleguide-favored third-person rather than second-person phrasing. --- .../java/org/postgresql/pljava/Adjusting.java | 200 +++++++++++-- .../pljava/example/annotation/PassXML.java | 58 ++-- .../postgresql/pljava/jdbc/SQLXMLImpl.java | 271 +++++++++++------- 3 files changed, 383 insertions(+), 146 deletions(-) diff --git a/pljava-api/src/main/java/org/postgresql/pljava/Adjusting.java b/pljava-api/src/main/java/org/postgresql/pljava/Adjusting.java index 9a2d379b7..b8e322567 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/Adjusting.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/Adjusting.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 @@ -14,6 +14,9 @@ import java.io.Reader; import java.sql.SQLException; import java.sql.SQLXML; +import java.util.List; +import static java.util.Objects.requireNonNull; +import java.util.function.Consumer; import javax.xml.stream.XMLInputFactory; // for javadoc import javax.xml.stream.XMLResolver; // for javadoc import javax.xml.stream.XMLStreamReader; @@ -126,16 +129,139 @@ public static final class XML { private XML() { } // no instances + /** + * Attempts a given action (typically to set something) using a given + * value, trying one or more supplied keys in order until the action + * succeeds with no exception. + *

    + * This logic is common to the + * {@link Parsing#setFirstSupportedFeature setFirstSupportedFeature} + * and + * {@link Parsing#setFirstSupportedProperty setFirstSupportedProperty} + * methods, and is exposed here because it may be useful for other + * tasks in Java's XML APIs, such as configuring {@code Transformer}s. + *

    + * If any attempt succeeds, null is returned. If no attempt + * succeeds, the first exception caught is returned, with any + * exceptions from the subsequent attempts retrievable from it with + * {@link Exception#getSuppressed getSuppressed}. The return is + * immediate, without any remaining names being tried, if an exception + * is caught that is not assignable to a class in the + * expected list. Such an exception will be passed to the + * onUnexpected handler if that is non-null; otherwise, + * it will be returned (or added to the suppressed list of the + * exception to be returned) just as expected exceptions are. + * @param setter typically a method reference for a method that + * takes a string key and some value. + * @param value the value to pass to the setter + * @param expected a list of exception classes that can be foreseen + * to indicate that a key was not recognized, and the operation + * should be retried with the next possible key. + * @param onUnexpected invoked, if non-null, on an {@code Exception} + * that is caught and matches nothing in the expected list, instead + * of returning it. If this parameter is null, such an exception is + * returned (or added to the suppressed list of the exception to be + * returned), just as for expected exceptions, but the return is + * immediate, without trying remaining names, if any. + * @param names one or more String keys to be tried in order until + * the action succeeds. + * @return null if any attempt succeeded, otherwise an exception, + * which may have further exceptions in its suppressed list. + */ + public static Exception setFirstSupported( + SetMethod setter, V value, + List> expected, + Consumer onUnexpected, String... names) + { + requireNonNull(expected); + Exception caught = null; + for ( String name : names ) + { + try + { + setter.set(name, value); + return null; + } + catch ( Exception e ) + { + boolean benign = + expected.stream().anyMatch(c -> c.isInstance(e)); + + if ( benign || null == onUnexpected ) + { + if ( null == caught ) + caught = e; + else + caught.addSuppressed(e); + } + else + onUnexpected.accept(e); + + if ( ! benign ) + break; + } + } + return caught; + } + + /** + * A functional interface fitting various {@code setFeature} or + * {@code setProperty} methods in Java XML APIs. + *

    + * The XML APIs have a number of methods on various interfaces that can + * be used to set some property or feature, and can generally be + * assigned to this functional interface by bound method reference, and + * used with {@link #setFirstSupported setFirstSupported}. + */ + @FunctionalInterface + public interface SetMethod + { + void set(String key, T value) throws Exception; + } + /** * Interface with methods to adjust the restrictions on XML parsing * that are commonly considered when XML content might be from untrusted * sources. *

    - * The adjusting methods are best-effort and do not provide an - * indication of whether the requested adjustment was made. Not all of + * The adjusting methods are best-effort; not all of * the adjustments are available for all flavors of {@code Source} or * {@code Result} or for all parser implementations or versions the Java - * runtime may supply. + * runtime may supply. Cases where a requested adjustment has not been + * made are handled as follows: + *

    + * Any sequence of adjustment calls will ultimately be followed by a + * {@code get}. During the sequence of adjustments, exceptions caught + * are added to a signaling list or to a quiet list, where "added to" + * means that if either list has a first exception, any caught later are + * attached to that exception with + * {@link Exception#addSuppressed addSuppressed}. + *

    + * For each adjustment (and depending on the type of underlying + * {@code Source} or {@code Result}), one or more exception types will + * be 'expected' as indications that an identifying key or value for + * that adjustment was not recognized. This implementation may continue + * trying to apply the adjustment, using other keys that have at times + * been used to identify it. Expected exceptions caught during these + * attempts form a temporary list (a first exception and those attached + * to it by {@code addSuppressed}). Once any such attempt succeeds, the + * adjustment is considered made, and any temporary expected exceptions + * list from the adjustment is discarded. If no attempt succeeded, the + * temporary list is retained, by adding its head exception to the quiet + * list. + *

    + * Any exceptions caught that are not instances of any of the 'expected' + * types are added to the signaling list. + *

    + * When {@code get} is called, the head exception on the signaling list, + * if any, is thrown. Otherwise, the head exception on the quiet list, + * if any, is logged at [@code WARNING} level. + *

    + * During a chain of adjustments, {@link #lax lax()} can be called to + * tailor the handling of the quiet list. A {@code lax()} call applies + * to whatever exceptions have been added to the quiet list up to that + * point. To discard them, call {@code lax(true)}; to move them to the + * signaling list, call {@code lax(false)}. */ public interface Parsing> { @@ -173,14 +299,14 @@ public interface Parsing> /** * For a feature that may have been identified by more than one URI - * in different parsers or versions, try passing the supplied + * in different parsers or versions, tries passing the supplied * value with each URI from names in order until * one is not rejected by the underlying parser. */ T setFirstSupportedFeature(boolean value, String... names); /** - * Make a best effort to apply the recommended, restrictive + * Makes a best effort to apply the recommended, restrictive * defaults from the OWASP cheat sheet, to the extent they are * supported by the underlying parser, runtime, and version. *

    @@ -196,7 +322,7 @@ public interface Parsing> /** * For a parser property (in DOM parlance, attribute) that may have * been identified by more than one URI in different parsers or - * versions, try passing the supplied value with each URI + * versions, tries passing the supplied value with each URI * from names in order until one is not rejected by the * underlying parser. *

    @@ -278,7 +404,7 @@ public interface Parsing> T accessExternalSchema(String protocols); /** - * Set an {@link EntityResolver} of the type used by SAX and DOM + * Sets an {@link EntityResolver} of the type used by SAX and DOM * (optional operation). *

    * This method only succeeds for a {@code SAXSource} or @@ -297,7 +423,7 @@ public interface Parsing> T entityResolver(EntityResolver resolver); /** - * Set a {@link Schema} to be applied during SAX or DOM parsing + * Sets a {@link Schema} to be applied during SAX or DOM parsing *(optional operation). *

    * This method only succeeds for a {@code SAXSource} or @@ -316,6 +442,31 @@ public interface Parsing> * already. */ T schema(Schema schema); + + /** + * Tailors the treatment of 'quiet' exceptions during a chain of + * best-effort adjustments. + *

    + * See {@link Parsing the class description} for an explanation of + * the signaling and quiet lists. + *

    + * This method applies to whatever exceptions may have been added to + * the quiet list by best-effort adjustments made up to that point. + * They can be moved to the signaling list with {@code lax(false)}, + * or simply discarded with {@code lax(true)}. In either case, the + * quiet list is left empty when {@code lax} returns. + *

    + * At the time a {@code get} method is later called, any exception + * at the head of the signaling list will be thrown (possibly + * wrapped in an exception permitted by {@code get}'s {@code throws} + * clause), with any later exceptions on that list retrievable from + * the head exception with + * {@link Exception#getSuppressed getSuppressed}. Otherwise, any + * exception at the head of the quiet list (again with any later + * ones attached as its suppressed list) will be logged at + * {@code WARNING} level. + */ + T lax(boolean discard); } /** @@ -347,12 +498,17 @@ public interface Source extends Parsing>, javax.xml.transform.Source { /** - * Return an object of the expected {@code Source} subtype + * Returns an object of the expected {@code Source} subtype * reflecting any adjustments made with the other methods. + *

    + * Refer to {@link Parsing the {@code Parsing} class description} + * and the {@link Parsing#lax lax()} method for how any exceptions + * caught while applying best-effort adjustments are handled. * @return an implementing object of the expected Source subtype * @throws SQLException for any reason that {@code getSource} might * have thrown when supplying the corresponding non-Adjusting - * subtype of Source. + * subtype of Source, or for reasons saved while applying + * adjustments. */ T get() throws SQLException; } @@ -392,12 +548,16 @@ public interface Result extends Parsing>, javax.xml.transform.Result { /** - * Return an object of the expected {@code Result} subtype + * Returns an object of the expected {@code Result} subtype * reflecting any adjustments made with the other methods. + * Refer to {@link Parsing the {@code Parsing} class description} + * and the {@link Parsing#lax lax()} method for how any exceptions + * caught while applying best-effort adjustments are handled. * @return an implementing object of the expected Result subtype * @throws SQLException for any reason that {@code getResult} might * have thrown when supplying the corresponding non-Adjusting - * subtype of Result. + * subtype of Result, or for reasons saved while applying + * adjustments. */ T get() throws SQLException; } @@ -428,7 +588,7 @@ public interface Result public interface SourceResult extends Result { /** - * Supply the {@code Source} instance that is the source of the + * Supplies the {@code Source} instance that is the source of the * content. *

    * This method must be called before any of the inherited adjustment @@ -484,7 +644,8 @@ SourceResult set(javax.xml.transform.stax.StAXSource source) throws SQLException; /** - * Provide the content to be copied in the form of a {@code String}. + * Provides the content to be copied in the form of a + * {@code String}. *

    * An exception from the pattern of {@code Source}-typed arguments, * this method simplifies retrofitting adjustments into code that @@ -507,11 +668,14 @@ SourceResult set(javax.xml.transform.dom.DOMSource source) throws SQLException; /** - * Return the result {@code SQLXML} instance ready for handing off + * Returns the result {@code SQLXML} instance ready for handing off * to PostgreSQL. *

    - * This method must be called after any of the inherited adjustment - * methods. + * The handling/logging of exceptions normally handled in a + * {@code get} method happens here for a {@code SourceResult}. + *

    + * Any necessary calls of the inherited adjustment methods must be + * made before this method is called. */ SQLXML getSQLXML() throws SQLException; } 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..3c0605759 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-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 @@ -36,6 +36,7 @@ import java.io.IOException; +import java.util.List; import java.util.Map; import java.util.HashMap; @@ -65,6 +66,7 @@ import javax.xml.validation.SchemaFactory; import org.postgresql.pljava.Adjusting; +import static org.postgresql.pljava.Adjusting.XML.setFirstSupported; import org.postgresql.pljava.annotation.Function; import org.postgresql.pljava.annotation.MappedUDT; import org.postgresql.pljava.annotation.SQLAction; @@ -518,33 +520,51 @@ private static void prepareXMLTransform(String name, SQLXML source, int how, builtin ? TransformerFactory.newDefaultInstance() : TransformerFactory.newInstance(); - String exf = - "http://www.oracle.com/xml/jaxp/properties/enableExtensionFunctions"; - String ecl = "jdk.xml.transform.extensionClassLoader"; + + String legacy_pfx = "http://www.oracle.com/xml/jaxp/properties/"; + String java17_pfx = "jdk.xml."; + String exf_sfx = "enableExtensionFunctions"; + + String ecl_legacy = "jdk.xml.transform.extensionClassLoader"; + String ecl_java17 = "jdk.xml.extensionClassLoader"; + Source src = sxToSource(source, how, adjust); + try { - try - { - tf.setFeature(exf, enableExtensionFunctions); - } - catch ( TransformerConfigurationException e ) + Exception e; + + e = setFirstSupported(tf::setFeature, enableExtensionFunctions, + List.of(TransformerConfigurationException.class), null, + java17_pfx + exf_sfx, legacy_pfx + exf_sfx); + + if ( null != e ) { - logMessage("WARNING", - "non-builtin transformer: ignoring " + e.getMessage()); + if ( builtin ) + throw new SQLException( + "Configuring XML transformation: " + e.getMessage(), e); + else + logMessage("WARNING", + "non-builtin transformer: ignoring " + e.getMessage()); } if ( withJava ) { - try - { - tf.setAttribute(ecl, - Thread.currentThread().getContextClassLoader()); - } - catch ( IllegalArgumentException e ) + e = setFirstSupported(tf::setAttribute, + Thread.currentThread().getContextClassLoader(), + List.of(IllegalArgumentException.class), null, + ecl_java17, ecl_legacy); + + if ( null != e ) { - logMessage("WARNING", - "non-builtin transformer: ignoring " + e.getMessage()); + if ( builtin ) + throw new SQLException( + "Configuring XML transformation: " + + e.getMessage(), e); + else + logMessage("WARNING", + "non-builtin transformer: ignoring " + + e.getMessage()); } } 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..95655ccd7 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 @@ -157,9 +157,13 @@ /* ... for Adjusting API for Source / Result */ import java.io.StringReader; +import java.util.List; +import static javax.xml.XMLConstants.ACCESS_EXTERNAL_DTD; +import static javax.xml.XMLConstants.ACCESS_EXTERNAL_SCHEMA; import javax.xml.parsers.ParserConfigurationException; import javax.xml.validation.Schema; import org.postgresql.pljava.Adjusting; +import static org.postgresql.pljava.Adjusting.XML.setFirstSupported; import org.xml.sax.EntityResolver; import org.xml.sax.SAXNotRecognizedException; import org.xml.sax.SAXNotSupportedException; @@ -4144,14 +4148,72 @@ Writable finish() throws IOException, SQLException AdjustingJAXPParser> implements Adjusting.XML.Parsing { + static final Logger s_logger = + Logger.getLogger("org.postgresql.pljava.jdbc"); + + private static final String JDK17 = "jdk.xml."; private static final String LIMIT = - "http://www.oracle.com/xml/jaxp/properties/"; + "http://www.oracle.com/xml/jaxp/properties/"; // "legacy" since 17 - /* - * Can get these from javax.xml.XMLConstants once assuming Java >= 7. + private Exception m_signaling; + private Exception m_quiet; + + protected void addSignaling(Exception e) + { + if ( null == e ) + return; + if ( null == m_signaling ) + m_signaling = e; + else + m_signaling.addSuppressed(e); + } + + protected void addQuiet(Exception e) + { + if ( null == e ) + return; + if ( null == m_quiet ) + m_quiet = e; + else + m_quiet.addSuppressed(e); + } + + protected boolean anySignaling() + { + return null != m_signaling; + } + + /** + * Returns whatever is on the signaling list, while logging (at + * {@code WARNING} level) whatever is on the quiet list. + *

    + * Both lists are left cleared. + * @return the head exception on the signaling list, or null if none */ - private static final String ACCESS = - "http://javax.xml.XMLConstants/property/accessExternal"; + protected Exception exceptions() + { + Exception e = m_quiet; + m_quiet = null; + if ( null != e ) + s_logger.log(WARNING, + "some XML processing limits were not successfully adjusted", + e); + e = m_signaling; + m_signaling = null; + return e; + } + + @Override + public T lax(boolean discard) + { + if ( null != m_quiet ) + { + if ( ! discard ) + addSignaling(m_quiet); + m_quiet = null; + } + return (T)this; + } @Override public T defaults() @@ -4165,6 +4227,7 @@ public T defaults() public T elementAttributeLimit(int limit) { return setFirstSupportedProperty(limit, + JDK17 + "elementAttributeLimit", LIMIT + "elementAttributeLimit"); } @@ -4172,6 +4235,7 @@ public T elementAttributeLimit(int limit) public T entityExpansionLimit(int limit) { return setFirstSupportedProperty(limit, + JDK17 + "entityExpansionLimit", LIMIT + "entityExpansionLimit"); } @@ -4179,6 +4243,7 @@ public T entityExpansionLimit(int limit) public T entityReplacementLimit(int limit) { return setFirstSupportedProperty(limit, + JDK17 + "entityReplacementLimit", LIMIT + "entityReplacementLimit"); } @@ -4186,6 +4251,7 @@ public T entityReplacementLimit(int limit) public T maxElementDepth(int depth) { return setFirstSupportedProperty(depth, + JDK17 + "maxElementDepth", LIMIT + "maxElementDepth"); } @@ -4193,6 +4259,7 @@ public T maxElementDepth(int depth) public T maxGeneralEntitySizeLimit(int limit) { return setFirstSupportedProperty(limit, + JDK17 + "maxGeneralEntitySizeLimit", LIMIT + "maxGeneralEntitySizeLimit"); } @@ -4200,6 +4267,7 @@ public T maxGeneralEntitySizeLimit(int limit) public T maxParameterEntitySizeLimit(int limit) { return setFirstSupportedProperty(limit, + JDK17 + "maxParameterEntitySizeLimit", LIMIT + "maxParameterEntitySizeLimit"); } @@ -4207,6 +4275,7 @@ public T maxParameterEntitySizeLimit(int limit) public T maxXMLNameLimit(int limit) { return setFirstSupportedProperty(limit, + JDK17 + "maxXMLNameLimit", LIMIT + "maxXMLNameLimit"); } @@ -4214,19 +4283,20 @@ public T maxXMLNameLimit(int limit) public T totalEntitySizeLimit(int limit) { return setFirstSupportedProperty(limit, + JDK17 + "totalEntitySizeLimit", LIMIT + "totalEntitySizeLimit"); } @Override public T accessExternalDTD(String protocols) { - return setFirstSupportedProperty(protocols, ACCESS + "DTD"); + return setFirstSupportedProperty(protocols, ACCESS_EXTERNAL_DTD); } @Override public T accessExternalSchema(String protocols) { - return setFirstSupportedProperty(protocols, ACCESS + "Schema"); + return setFirstSupportedProperty(protocols, ACCESS_EXTERNAL_SCHEMA); } @Override @@ -4314,7 +4384,6 @@ static class SAXDOMErrorHandler implements ErrorHandler */ static final Pattern s_wrapelement = Pattern.compile( "^cvc-elt\\.1(?:\\.a)?+:.*pljava-content-wrap"); - final Logger m_logger = Logger.getLogger("org.postgresql.pljava.jdbc"); private int m_wrapCount; static SAXDOMErrorHandler instance(boolean wrapped) @@ -4364,7 +4433,8 @@ public void fatalError(SAXParseException exception) throws SAXException @Override public void warning(SAXParseException exception) throws SAXException { - m_logger.log(WARNING, exception.getMessage(), exception); + AdjustingJAXPParser.s_logger + .log(WARNING, exception.getMessage(), exception); } } @@ -4526,6 +4596,19 @@ public AdjustingSourceResult set(String source) return set(new StreamSource(new StringReader(source))); } + @Override + public AdjustingSourceResult get() throws SQLException + { + return this; // for this class, get is a noop + } + + @Override + public AdjustingSourceResult lax(boolean discard) + { + theAdjustable().lax(discard); + return this; + } + @Override public SQLXML getSQLXML() throws SQLException { @@ -4535,6 +4618,10 @@ public SQLXML getSQLXML() throws SQLException if ( null == m_copier ) throw new IllegalStateException( "AdjustingSourceResult getSQLXML called before set"); + + // Exception handling/logging for adjustments will happen in + // theAdjustable().get(), during finish() here. + Writable result = null; try { @@ -4578,12 +4665,6 @@ private Adjusting.XML.Source theAdjustable() return m_copier.getAdjustable(); } - @Override - public AdjustingSourceResult get() throws SQLException - { - return this; // for this class, get is a noop - } - @Override public AdjustingSourceResult allowDTD(boolean v) { @@ -4748,8 +4829,11 @@ public StreamResult get() throws SQLException throw new IllegalStateException( "AdjustingStreamResult get() called more than once"); + // Exception handling/logging for theVerifierSource happens here XMLReader xr = theVerifierSource().get().getXMLReader(); + OutputStream os; + try { m_vwo.setVerifier(new Verifier(xr)); @@ -4759,18 +4843,29 @@ public StreamResult get() throws SQLException { throw normalizedException(e); } + StreamResult sr; + if ( m_preferWriter ) sr = new StreamResult( new OutputStreamWriter(os, m_serverCS.newEncoder())); else sr = new StreamResult(os); + m_vwo = null; m_verifierSource = null; m_serverCS = null; + return sr; } + @Override + public AdjustingStreamResult lax(boolean discard) + { + theVerifierSource().lax(discard); + return this; + } + @Override public AdjustingStreamResult allowDTD(boolean v) { @@ -4860,7 +4955,6 @@ static class AdjustingSAXSource private XMLReader m_xr; private InputSource m_is; private boolean m_wrapped; - private SAXException m_except; private boolean m_hasCalledDefaults; static class Dummy extends AdjustingSAXSource @@ -4940,7 +5034,7 @@ private SAXParserFactory theFactory() private XMLReader theReader() { - if ( null != m_except ) + if ( anySignaling() ) return null; if ( null != m_spf ) @@ -4949,16 +5043,12 @@ private XMLReader theReader() { m_xr = m_spf.newSAXParser().getXMLReader(); } - catch ( SAXException e ) - { - m_except = e; - return null; - } - catch ( ParserConfigurationException e ) + catch ( SAXException | ParserConfigurationException e ) { - m_except = new SAXException(e.getMessage(), e); + addSignaling(e); return null; } + m_spf = null; if ( m_wrapped ) m_xr = new SAXUnwrapFilter(m_xr); @@ -4991,9 +5081,11 @@ public SAXSource get() throws SQLException throw new IllegalStateException( "AdjustingSAXSource get() called more than once"); - XMLReader xr; - if ( null != m_except || null == (xr = theReader()) ) - throw normalizedException(m_except); + XMLReader xr = theReader(); + + Exception e = exceptions(); + if ( null != e ) + throw normalizedException(e); SAXSource ss = new SAXSource(xr, m_is); m_xr = null; @@ -5031,18 +5123,11 @@ public AdjustingSAXSource setFirstSupportedFeature( if ( null == r ) // pending exception, nothing to be done return this; - for ( String name : names ) - { - try - { - r.setFeature(name, value); - break; - } - catch ( SAXNotRecognizedException | SAXNotSupportedException e ) - { - e.printStackTrace(); // XXX - } - } + addQuiet(setFirstSupported(r::setFeature, value, + List.of(SAXNotRecognizedException.class, + SAXNotSupportedException.class), + this::addSignaling, names)); + return this; } @@ -5054,22 +5139,11 @@ public AdjustingSAXSource setFirstSupportedProperty( if ( null == r ) // pending exception, nothing to be done return this; - for ( String name : names ) - { - try - { - r.setProperty(name, value); - break; - } - catch ( SAXNotRecognizedException e ) - { - e.printStackTrace(); // XXX - } - catch ( SAXNotSupportedException e ) - { - e.printStackTrace(); // XXX - } - } + addQuiet(setFirstSupported(r::setProperty, value, + List.of(SAXNotRecognizedException.class, + SAXNotSupportedException.class), + this::addSignaling, names)); + return this; } @@ -5227,6 +5301,8 @@ public StAXSource get() throws SQLException if ( null == m_xif ) throw new IllegalStateException( "AdjustingStAXSource get() called more than once"); + + StAXSource ss = null; try { XMLStreamReader xsr = m_xif.createXMLStreamReader( @@ -5234,12 +5310,18 @@ public StAXSource get() throws SQLException if ( m_wrapped ) xsr = new StAXUnwrapFilter(xsr); m_xif = null; // too late for any more adjustments - return new StAXSource(xsr); + ss = new StAXSource(xsr); } catch ( Exception e ) { - throw normalizedException(e); + addSignaling(e); } + + Exception e = exceptions(); + if ( null != e ) + throw normalizedException(e); + + return ss; } @Override @@ -5286,18 +5368,9 @@ public AdjustingStAXSource setFirstSupportedFeature( boolean value, String... names) { XMLInputFactory xif = theFactory(); - for ( String name : names ) - { - try - { - xif.setProperty(name, value); - break; - } - catch ( IllegalArgumentException e ) - { - e.printStackTrace(); // XXX - } - } + addQuiet(setFirstSupported(xif::setProperty, value, + List.of(IllegalArgumentException.class), + this::addSignaling, names)); return this; } @@ -5306,18 +5379,9 @@ public AdjustingStAXSource setFirstSupportedProperty( Object value, String... names) { XMLInputFactory xif = theFactory(); - for ( String name : names ) - { - try - { - xif.setProperty(name, value); - break; - } - catch ( IllegalArgumentException e ) - { - e.printStackTrace(); // XXX - } - } + addQuiet(setFirstSupported(xif::setProperty, value, + List.of(IllegalArgumentException.class), + this::addSignaling, names)); return this; } } @@ -5367,23 +5431,30 @@ public DOMSource get() throws SQLException if ( null == m_dbf ) throw new IllegalStateException( "AdjustingDOMSource get() called more than once"); + + DOMSource ds = null; try { DocumentBuilder db = m_dbf.newDocumentBuilder(); db.setErrorHandler(SAXDOMErrorHandler.instance(m_wrapped)); if ( null != m_resolver ) db.setEntityResolver(m_resolver); - DOMSource ds = new DOMSource(db.parse(m_is)); + ds = new DOMSource(db.parse(m_is)); if ( m_wrapped ) domUnwrap(ds); m_dbf = null; m_is = null; - return ds; } catch ( Exception e ) { - throw normalizedException(e); + addSignaling(e); } + + Exception e = exceptions(); + if ( null != e ) + throw normalizedException(e); + + return ds; } @Override @@ -5405,18 +5476,9 @@ public AdjustingDOMSource setFirstSupportedFeature( boolean value, String... names) { DocumentBuilderFactory dbf = theFactory(); - for ( String name : names ) - { - try - { - dbf.setFeature(name, value); - break; - } - catch ( ParserConfigurationException e ) - { - e.printStackTrace(); // XXX - } - } + addQuiet(setFirstSupported(dbf::setFeature, value, + List.of(ParserConfigurationException.class), + this::addSignaling, names)); return this; } @@ -5425,18 +5487,9 @@ public AdjustingDOMSource setFirstSupportedProperty( Object value, String... names) { DocumentBuilderFactory dbf = theFactory(); - for ( String name : names ) - { - try - { - dbf.setAttribute(name, value); - break; - } - catch ( IllegalArgumentException e ) - { - e.printStackTrace(); // XXX - } - } + addQuiet(setFirstSupported(dbf::setAttribute, value, + List.of(IllegalArgumentException.class), + this::addSignaling, names)); return this; } From 17b7d7cc70a521b490ec09498d68567a9b282761 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 23 Aug 2023 11:53:56 -0400 Subject: [PATCH 130/334] PG compatibility horizon to 9.5 PostgreSQL 9.5 has been advertised as the oldest supported version for PL/Java 1.6.x since 1.6.0. Declutter by removing conditional code lingering from support of older versions. In passing, add a couple SPI constants that showed up later while my back was turned. --- .../example/annotation/ConditionalDDR.java | 49 +-- .../example/annotation/Enumeration.java | 22 +- .../pljava/example/annotation/JDBC42_21.java | 9 +- .../pljava/example/annotation/PGF1010962.java | 9 +- .../pljava/example/annotation/PassXML.java | 24 +- .../pljava/example/annotation/PreJSR310.java | 7 +- .../annotation/RecordParameterDefaults.java | 7 +- .../example/annotation/SetOfRecordTest.java | 8 +- .../pljava/example/annotation/Triggers.java | 8 +- .../example/annotation/TypeRoundTripper.java | 12 +- .../annotation/UnicodeRoundTripTest.java | 9 +- .../example/annotation/VarlenaUDTTest.java | 12 +- .../example/annotation/XMLRenderedTypes.java | 13 +- pljava-so/src/main/c/Backend.c | 354 +++++++----------- pljava-so/src/main/c/DualState.c | 6 +- pljava-so/src/main/c/Exception.c | 10 +- pljava-so/src/main/c/ExecutionPlan.c | 10 +- pljava-so/src/main/c/Function.c | 11 +- pljava-so/src/main/c/InstallHelper.c | 129 +------ pljava-so/src/main/c/SPI.c | 11 +- pljava-so/src/main/c/Session.c | 26 +- pljava-so/src/main/c/TypeOid.c | 6 +- pljava-so/src/main/c/VarlenaWrapper.c | 116 +----- pljava-so/src/main/c/XactListener.c | 4 +- pljava-so/src/main/c/type/AclId.c | 6 +- pljava-so/src/main/c/type/Array.c | 12 +- pljava-so/src/main/c/type/Oid.c | 18 +- pljava-so/src/main/c/type/SQLXMLImpl.c | 18 +- pljava-so/src/main/c/type/String.c | 4 - pljava-so/src/main/c/type/Timestamp.c | 18 +- pljava-so/src/main/c/type/Type.c | 36 +- pljava-so/src/main/c/type/UDT.c | 11 +- pljava-so/src/main/c/type/byte_array.c | 22 +- pljava-so/src/main/include/pljava/Backend.h | 8 +- pljava-so/src/main/include/pljava/Exception.h | 10 +- pljava-so/src/main/include/pljava/pljava.h | 38 +- .../org/postgresql/pljava/internal/SPI.java | 5 +- src/site/markdown/build/versions.md | 2 +- 38 files changed, 255 insertions(+), 825 deletions(-) diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/ConditionalDDR.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/ConditionalDDR.java index 0140f4372..253a03086 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/ConditionalDDR.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/ConditionalDDR.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015-2020 Tada AB and other contributors, as listed below. + * Copyright (c) 2015-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 @@ -64,7 +64,7 @@ * local, so it is reverted when the transaction completes. *

    * In addition to the goodness-of-life examples, this file also generates - * several statements setting PostgreSQL-version-based implementor tags that + * one or more statements setting PostgreSQL-version-based implementor tags that * are relied on by various other examples in this directory. */ @SQLAction(provides={"LifeIsGood","LifeIsNotGood"}, install= @@ -78,51 +78,12 @@ ) @SQLAction(implementor="LifeIsGood", install= - "SELECT javatest.logmessage('INFO', 'Looking good!')" + "SELECT javatest.logmessage('INFO', 'ConditionlDDR looking good!')" ) @SQLAction(implementor="LifeIsNotGood", install= - "SELECT javatest.logmessage('WARNING', 'This should not be executed')" -) - -@SQLAction(provides="postgresql_ge_80300", install= - "SELECT CASE WHEN" + - " 80300 <= CAST(current_setting('server_version_num') AS integer)" + - " THEN set_config('pljava.implementors', 'postgresql_ge_80300,' || " + - " current_setting('pljava.implementors'), true) " + - "END" -) - -@SQLAction(provides="postgresql_ge_80400", install= - "SELECT CASE WHEN" + - " 80400 <= CAST(current_setting('server_version_num') AS integer)" + - " THEN set_config('pljava.implementors', 'postgresql_ge_80400,' || " + - " current_setting('pljava.implementors'), true) " + - "END" -) - -@SQLAction(provides="postgresql_ge_90000", install= - "SELECT CASE WHEN" + - " 90000 <= CAST(current_setting('server_version_num') AS integer)" + - " THEN set_config('pljava.implementors', 'postgresql_ge_90000,' || " + - " current_setting('pljava.implementors'), true) " + - "END" -) - -@SQLAction(provides="postgresql_ge_90100", install= - "SELECT CASE WHEN" + - " 90100 <= CAST(current_setting('server_version_num') AS integer)" + - " THEN set_config('pljava.implementors', 'postgresql_ge_90100,' || " + - " current_setting('pljava.implementors'), true) " + - "END" -) - -@SQLAction(provides="postgresql_ge_90300", install= - "SELECT CASE WHEN" + - " 90300 <= CAST(current_setting('server_version_num') AS integer)" + - " THEN set_config('pljava.implementors', 'postgresql_ge_90300,' || " + - " current_setting('pljava.implementors'), true) " + - "END" + "SELECT javatest.logmessage('WARNING', " + + " 'ConditionalDDR: This should not be executed')" ) @SQLAction(provides="postgresql_ge_100000", install= diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Enumeration.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Enumeration.java index 2fcee7df1..359bb2871 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Enumeration.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Enumeration.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015-2020 Tada AB and other contributors, as listed below. + * Copyright (c) 2015-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,16 +21,12 @@ /** * Confirms the mapping of PG enum and Java String, and arrays of each, as * parameter and return types. - *

    - * This example relies on {@code implementor} tags reflecting the PostgreSQL - * version, set up in the {@link ConditionalDDR} example. PostgreSQL before 8.3 - * did not have enum types. */ -@SQLAction(provides="mood type", implementor="postgresql_ge_80300", +@SQLAction(provides="mood type", install="CREATE TYPE mood AS ENUM ('sad', 'ok', 'happy')", remove="DROP TYPE mood" ) -@SQLAction(implementor="postgresql_ge_80300", +@SQLAction( requires={"textToMood", "moodToText", "textsToMoods", "moodsToTexts"}, install={ "SELECT textToMood('happy')", @@ -41,26 +37,22 @@ ) public class Enumeration { - @Function(requires="mood type", provides="textToMood", type="mood", - implementor="postgresql_ge_80300") + @Function(requires="mood type", provides="textToMood", type="mood") public static String textToMood(String s) { return s; } - @Function(requires="mood type", provides="moodToText", - implementor="postgresql_ge_80300") + @Function(requires="mood type", provides="moodToText") public static String moodToText(@SQLType("mood")String s) { return s; } - @Function(requires="mood type", provides="textsToMoods", type="mood", - implementor="postgresql_ge_80300") + @Function(requires="mood type", provides="textsToMoods", type="mood") public static Iterator textsToMoods(String[] ss) { return Arrays.asList(ss).iterator(); } - @Function(requires="mood type", provides="moodsToTexts", - implementor="postgresql_ge_80300") + @Function(requires="mood type", provides="moodsToTexts") public static Iterator moodsToTexts(@SQLType("mood[]")String[] ss) { return Arrays.asList(ss).iterator(); diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/JDBC42_21.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/JDBC42_21.java index 1b0d35e28..ff2c23116 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/JDBC42_21.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/JDBC42_21.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2022 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 @@ -14,20 +14,15 @@ import org.postgresql.pljava.annotation.Function; import org.postgresql.pljava.annotation.SQLAction; -import org.postgresql.pljava.example.annotation.ConditionalDDR; // for javadoc - /** * Exercise new mappings between date/time types and java.time classes * (JDBC 4.2 change 21). *

    * Defines a method {@link #javaSpecificationGE javaSpecificationGE} that may be * of use for other examples. - *

    - * Relies on PostgreSQL-version-specific implementor tags set up in the - * {@link ConditionalDDR} example. */ @SQLAction( - implementor="postgresql_ge_90300",requires="TypeRoundTripper.roundTrip", + requires="TypeRoundTripper.roundTrip", install={ " SELECT" + " CASE WHEN every(orig = roundtripped)" + diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/PGF1010962.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/PGF1010962.java index 2b06f481e..747f2ef6d 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/PGF1010962.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/PGF1010962.java @@ -10,12 +10,8 @@ /** * A gnarly test of TupleDesc reference management, crafted by Johann Oskarsson * for bug report 1010962 on pgFoundry. - *

    - * This example relies on {@code implementor} tags reflecting the PostgreSQL - * version, set up in the {@link ConditionalDDR} example. Before PostgreSQL 8.4, - * there is no array of {@code RECORD}, which this test requires. */ -@SQLAction(requires="1010962 func", implementor="postgresql_ge_80400", +@SQLAction(requires="1010962 func", install={ "CREATE TYPE javatest.B1010962 AS ( b1_val float8, b2_val int)", @@ -51,8 +47,7 @@ public class PGF1010962 * @param receiver Looks polymorphic, but expects an array of A1010962 * @return 0 */ - @Function(schema="javatest", provides="1010962 func", - implementor="postgresql_ge_80400") + @Function(schema="javatest", provides="1010962 func") public static int complexParam( ResultSet receiver[] ) throws SQLException { 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..d7d8c2a9f 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-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 @@ -112,16 +112,7 @@ "END" ) -@SQLAction(implementor="postgresql_ge_80400", - provides="postgresql_xml_ge84", - install= - "SELECT CASE (SELECT 1 FROM pg_type WHERE typname = 'xml') WHEN 1" + - " THEN set_config('pljava.implementors', 'postgresql_xml_ge84,' || " + - " current_setting('pljava.implementors'), true) " + - "END" -) - -@SQLAction(implementor="postgresql_xml_ge84", requires="echoXMLParameter", +@SQLAction(implementor="postgresql_xml", requires="echoXMLParameter", install= "WITH" + " s(how) AS (SELECT generate_series(1, 7))," + @@ -146,7 +137,7 @@ " r" ) -@SQLAction(implementor="postgresql_xml_ge84", requires="proxiedXMLEcho", +@SQLAction(implementor="postgresql_xml", requires="proxiedXMLEcho", install= "WITH" + " s(how) AS (SELECT unnest('{1,2,4,5,6,7}'::int[]))," + @@ -170,7 +161,7 @@ " r" ) -@SQLAction(implementor="postgresql_xml_ge84", requires="lowLevelXMLEcho", +@SQLAction(implementor="postgresql_xml", requires="lowLevelXMLEcho", install={ "SELECT" + " preparexmlschema('schematest', $$" + @@ -702,7 +693,7 @@ private static SQLXML echoSQLXML(SQLXML sx, int howin, int howout) * still be exercised by calling this method, explicitly passing * {@code adjust => NULL}. */ - @Function(schema="javatest", implementor="postgresql_xml_ge84", + @Function(schema="javatest", implementor="postgresql_xml", provides="lowLevelXMLEcho") public static SQLXML lowLevelXMLEcho( SQLXML sx, int how, @SQLType(defaultValue={}) ResultSet adjust) @@ -1046,12 +1037,9 @@ public static SQLXML mockedXMLEcho(String chars) /** * Text-typed variant of lowLevelXMLEcho (does not require XML type). - *

    - * It does declare a parameter default, limiting it to PostgreSQL 8.4 or - * later. */ @Function(schema="javatest", name="lowLevelXMLEcho", - type="text", implementor="postgresql_ge_80400") + type="text") public static SQLXML lowLevelXMLEcho_(@SQLType("text") SQLXML sx, int how, @SQLType(defaultValue={}) ResultSet adjust) throws SQLException diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/PreJSR310.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/PreJSR310.java index e430e25d1..726d46a5d 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/PreJSR310.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/PreJSR310.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 @@ -29,9 +29,6 @@ * Some tests of pre-JSR 310 date/time/timestamp conversions. *

    * For now, just {@code java.sql.Date}, thanks to issue #199. - *

    - * This example relies on {@code implementor} tags reflecting the PostgreSQL - * version, set up in the {@link ConditionalDDR} example. */ @SQLAction(provides="language java_tzset", install={ "SELECT sqlj.alias_java_language('java_tzset', true)" @@ -39,7 +36,7 @@ "DROP LANGUAGE java_tzset" }) -@SQLAction(implementor="postgresql_ge_90300", // needs LATERAL +@SQLAction( requires="issue199", install={ "SELECT javatest.issue199()" }) diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/RecordParameterDefaults.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/RecordParameterDefaults.java index 09d3dbbe8..34e1aeb75 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/RecordParameterDefaults.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/RecordParameterDefaults.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 @@ -28,9 +28,6 @@ * function. *

    * Also tests the proper DDR generation of defaults for such parameters. - *

    - * This example relies on {@code implementor} tags reflecting the PostgreSQL - * version, set up in the {@link ConditionalDDR} example. */ @SQLAction( provides = "paramtypeinfo type", // created in Triggers.java @@ -64,7 +61,6 @@ public class RecordParameterDefaults implements ResultSetProvider @Function( requires = "paramtypeinfo type", schema = "javatest", - implementor = "postgresql_ge_80400", // supports function param DEFAULTs type = "javatest.paramtypeinfo" ) public static ResultSetProvider paramDefaultsRecord( @@ -87,7 +83,6 @@ public static ResultSetProvider paramDefaultsRecord( */ @Function( requires = "foobar tables", // created in Triggers.java - implementor = "postgresql_ge_80400", // supports function param DEFAULTs schema = "javatest" ) public static String paramDefaultsNamedRow( 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..49abf1382 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-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 @@ -26,12 +26,8 @@ * Example implementing the {@code ResultSetHandle} interface, to return * the {@link ResultSet} from any SQL {@code SELECT} query passed as a string * to the {@link #executeSelect executeSelect} function. - *

    - * This example relies on {@code implementor} tags reflecting the PostgreSQL - * version, set up in the {@link ConditionalDDR} example. Before PostgreSQL 8.4, - * there was no {@code =} or {@code DISTINCT FROM} operator between row types. */ -@SQLAction(requires="selecttorecords fn", implementor="postgresql_ge_80400", +@SQLAction(requires="selecttorecords fn", install= " SELECT " + " CASE WHEN r IS DISTINCT FROM ROW('Foo'::varchar, 1::integer, 1.5::float, " + diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Triggers.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Triggers.java index 804ef9d83..bfdbf8c0f 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Triggers.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Triggers.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 @@ -38,8 +38,8 @@ * also create a function and trigger that uses transition tables. *

    * This example relies on {@code implementor} tags reflecting the PostgreSQL - * version, set up in the {@link ConditionalDDR} example. Constraint triggers - * appear in PG 9.1, transition tables in PG 10. + * version, set up in the {@link ConditionalDDR} example. Transition tables + * appear in PG 10. */ @SQLAction( provides = "foobar tables", @@ -135,10 +135,8 @@ public static void examineRows(TriggerData td) /** * Throw exception if value to be inserted is 44. - * Constraint triggers first became available in PostgreSQL 9.1. */ @Function( - implementor = "postgresql_ge_90100", requires = "foobar tables", provides = "constraint triggers", schema = "javatest", diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/TypeRoundTripper.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/TypeRoundTripper.java index df613c266..8c3151556 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/TypeRoundTripper.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/TypeRoundTripper.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 @@ -36,8 +36,6 @@ import org.postgresql.pljava.annotation.SQLAction; import org.postgresql.pljava.annotation.SQLType; -import org.postgresql.pljava.example.annotation.ConditionalDDR; // for javadoc - /** * A class to simplify testing of PL/Java's mappings between PostgreSQL and * Java/JDBC types. @@ -94,11 +92,8 @@ * (VALUES (timestamptz '2017-08-21 18:25:29.900005Z')) AS p(orig), * roundtrip(p) AS (roundtripped timestamptz); * - *

    - * This example relies on {@code implementor} tags reflecting the PostgreSQL - * version, set up in the {@link ConditionalDDR} example. */ -@SQLAction(implementor = "postgresql_ge_90300", // funcs see earlier FROM items +@SQLAction( requires = {"TypeRoundTripper.roundTrip", "point mirror type"}, install = { " SELECT" + @@ -309,8 +304,7 @@ private TypeRoundTripper() { } @Function( schema = "javatest", type = "RECORD", - provides = "TypeRoundTripper.roundTrip", - implementor = "postgresql_ge_80400" // supports function param DEFAULTs + provides = "TypeRoundTripper.roundTrip" ) public static boolean roundTrip( ResultSet in, @SQLType(defaultValue="") String classname, diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/UnicodeRoundTripTest.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/UnicodeRoundTripTest.java index 4f2c0ec47..6b06d4d9a 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/UnicodeRoundTripTest.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/UnicodeRoundTripTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015-2020 Tada AB and other contributors, as listed below. + * Copyright (c) 2015-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,11 +35,10 @@ * if {@code matched} is false or the original and returned arrays or strings * do not match as seen in SQL. *

    - * This example relies on {@code implementor} tags reflecting the PostgreSQL - * version, set up in the {@link ConditionalDDR} example, and also sets its own. + * This example sets an {@code implementor} tag based on a PostgreSQL condition, + * as further explained in the {@link ConditionalDDR} example. */ -@SQLAction(provides="postgresql_unicodetest", - implementor="postgresql_ge_90000", install= +@SQLAction(provides="postgresql_unicodetest", install= "SELECT CASE" + " WHEN 'UTF8' = current_setting('server_encoding')" + " THEN set_config('pljava.implementors', 'postgresql_unicodetest,' ||" + diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/VarlenaUDTTest.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/VarlenaUDTTest.java index ae1be8991..c9982490e 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/VarlenaUDTTest.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/VarlenaUDTTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015- Tada AB and other contributors, as listed below. + * Copyright (c) 2015-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 @@ -29,14 +29,12 @@ * characters. That makes it easy to test how big a value gets correctly stored * and retrieved. It should be about a GB, but in issue 52 was failing at 32768 * because of a narrowing assignment in the native code. - *

    - * This example relies on {@code implementor} tags reflecting the PostgreSQL - * version, set up in the {@link ConditionalDDR} example. */ -@SQLAction(requires="varlena UDT", implementor="postgresql_ge_80300", install= +@SQLAction(requires="varlena UDT", install= " SELECT CASE v::text = v::javatest.VarlenaUDTTest::text " + -" WHEN true THEN javatest.logmessage('INFO', 'works for ' || v) " + -" ELSE javatest.logmessage('WARNING', 'fails for ' || v) " + +" WHEN true " + +" THEN javatest.logmessage('INFO', 'VarlenaUDTTest works for ' || v) " + +" ELSE javatest.logmessage('WARNING', 'VarlenaUDTTest fails for ' || v) " + " END " + " FROM (VALUES (('32767')), (('32768')), (('65536')), (('1048576'))) " + " AS t ( v )" diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/XMLRenderedTypes.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/XMLRenderedTypes.java index 871e96445..5cd21326f 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/XMLRenderedTypes.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/XMLRenderedTypes.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 @@ -27,19 +27,10 @@ *

    * Everything mentioning the type XML here needs a conditional implementor tag * in case of being loaded into a PostgreSQL instance built without that type. - * The {@code pg_node_tree} type appears in 9.1. */ -@SQLAction(implementor="postgresql_ge_90100", - provides="postgresql_xml_ge91", - install= - "SELECT CASE (SELECT 1 FROM pg_type WHERE typname = 'xml') WHEN 1" + - " THEN set_config('pljava.implementors', 'postgresql_xml_ge91,' || " + - " current_setting('pljava.implementors'), true) " + - "END" -) public class XMLRenderedTypes { - @Function(schema="javatest", implementor="postgresql_xml_ge91") + @Function(schema="javatest", implementor="postgresql_xml") public static SQLXML pgNodeTreeAsXML(@SQLType("pg_node_tree") SQLXML pgt) throws SQLException { diff --git a/pljava-so/src/main/c/Backend.c b/pljava-so/src/main/c/Backend.c index 287652fd2..5a3512d9d 100644 --- a/pljava-so/src/main/c/Backend.c +++ b/pljava-so/src/main/c/Backend.c @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -58,10 +59,6 @@ #include "pljava/SPI.h" #include "pljava/type/String.h" -#if PG_VERSION_NUM >= 90300 -#include "utils/timeout.h" -#endif - /* Include the 'magic block' that PostgreSQL 8.2 and up will use to ensure * that a module is not loaded into an incompatible server. */ @@ -250,152 +247,133 @@ static bool javaGE17 = false; static void initsequencer(enum initstage is, bool tolerant); -#if PG_VERSION_NUM >= 90100 - static bool check_libjvm_location( - char **newval, void **extra, GucSource source); - static bool check_vmoptions( - char **newval, void **extra, GucSource source); - static bool check_modulepath( - char **newval, void **extra, GucSource source); - static bool check_policy_urls( - char **newval, void **extra, GucSource source); - static bool check_enabled( - bool *newval, void **extra, GucSource source); - static bool check_java_thread_pg_entry( - int *newval, void **extra, GucSource source); - - /* Check hooks will always allow "setting" a value that is the same as - * current; otherwise, it would be frustrating to have just found settings - * that work, and be unable to save them with ALTER DATABASE SET ... because - * the check hook is called for that too, and would say it is too late.... - */ +static bool check_libjvm_location( + char **newval, void **extra, GucSource source); +static bool check_vmoptions( + char **newval, void **extra, GucSource source); +static bool check_modulepath( + char **newval, void **extra, GucSource source); +static bool check_policy_urls( + char **newval, void **extra, GucSource source); +static bool check_enabled( + bool *newval, void **extra, GucSource source); +static bool check_java_thread_pg_entry( + int *newval, void **extra, GucSource source); + +/* Check hooks will always allow "setting" a value that is the same as + * current; otherwise, it would be frustrating to have just found settings + * that work, and be unable to save them with ALTER DATABASE SET ... because + * the check hook is called for that too, and would say it is too late.... + */ - static bool check_libjvm_location( - char **newval, void **extra, GucSource source) - { - if ( initstage < IS_CAND_JVMOPENED ) - return true; - if ( libjvmlocation == *newval ) - return true; - if ( libjvmlocation && *newval && 0 == strcmp(libjvmlocation, *newval) ) - return true; - GUC_check_errmsg( - "too late to change \"pljava.libjvm_location\" setting"); - GUC_check_errdetail( - "Changing the setting can have no effect after " - "PL/Java has found and opened the library it points to."); - GUC_check_errhint( - "To try a different value, exit this session and start a new one."); - return false; - } +static bool check_libjvm_location( + char **newval, void **extra, GucSource source) +{ + if ( initstage < IS_CAND_JVMOPENED ) + return true; + if ( libjvmlocation == *newval ) + return true; + if ( libjvmlocation && *newval && 0 == strcmp(libjvmlocation, *newval) ) + return true; + GUC_check_errmsg( + "too late to change \"pljava.libjvm_location\" setting"); + GUC_check_errdetail( + "Changing the setting can have no effect after " + "PL/Java has found and opened the library it points to."); + GUC_check_errhint( + "To try a different value, exit this session and start a new one."); + return false; +} - static bool check_vmoptions( - char **newval, void **extra, GucSource source) - { - if ( initstage < IS_JAVAVM_OPTLIST ) - return true; - if ( vmoptions == *newval ) - return true; - if ( vmoptions && *newval && 0 == strcmp(vmoptions, *newval) ) - return true; - GUC_check_errmsg( - "too late to change \"pljava.vmoptions\" setting"); - GUC_check_errdetail( - "Changing the setting can have no effect after " - "PL/Java has started the Java virtual machine."); - GUC_check_errhint( - "To try a different value, exit this session and start a new one."); - return false; - } +static bool check_vmoptions( + char **newval, void **extra, GucSource source) +{ + if ( initstage < IS_JAVAVM_OPTLIST ) + return true; + if ( vmoptions == *newval ) + return true; + if ( vmoptions && *newval && 0 == strcmp(vmoptions, *newval) ) + return true; + GUC_check_errmsg( + "too late to change \"pljava.vmoptions\" setting"); + GUC_check_errdetail( + "Changing the setting can have no effect after " + "PL/Java has started the Java virtual machine."); + GUC_check_errhint( + "To try a different value, exit this session and start a new one."); + return false; +} - static bool check_modulepath( - char **newval, void **extra, GucSource source) - { - if ( initstage < IS_JAVAVM_OPTLIST ) - return true; - if ( modulepath == *newval ) - return true; - if ( modulepath && *newval && 0 == strcmp(modulepath, *newval) ) - return true; - GUC_check_errmsg( - "too late to change \"pljava.module_path\" setting"); - GUC_check_errdetail( - "Changing the setting has no effect after " - "PL/Java has started the Java virtual machine."); - GUC_check_errhint( - "To try a different value, exit this session and start a new one."); - return false; - } +static bool check_modulepath( + char **newval, void **extra, GucSource source) +{ + if ( initstage < IS_JAVAVM_OPTLIST ) + return true; + if ( modulepath == *newval ) + return true; + if ( modulepath && *newval && 0 == strcmp(modulepath, *newval) ) + return true; + GUC_check_errmsg( + "too late to change \"pljava.module_path\" setting"); + GUC_check_errdetail( + "Changing the setting has no effect after " + "PL/Java has started the Java virtual machine."); + GUC_check_errhint( + "To try a different value, exit this session and start a new one."); + return false; +} - static bool check_policy_urls( - char **newval, void **extra, GucSource source) - { - if ( initstage < IS_JAVAVM_OPTLIST ) - return true; - if ( policy_urls == *newval ) - return true; - if ( policy_urls && *newval && 0 == strcmp(policy_urls, *newval) ) - return true; - GUC_check_errmsg( - "too late to change \"pljava.policy_urls\" setting"); - GUC_check_errdetail( - "Changing the setting has no effect after " - "PL/Java has started the Java virtual machine."); - GUC_check_errhint( - "To try a different value, exit this session and start a new one."); - return false; - } +static bool check_policy_urls( + char **newval, void **extra, GucSource source) +{ + if ( initstage < IS_JAVAVM_OPTLIST ) + return true; + if ( policy_urls == *newval ) + return true; + if ( policy_urls && *newval && 0 == strcmp(policy_urls, *newval) ) + return true; + GUC_check_errmsg( + "too late to change \"pljava.policy_urls\" setting"); + GUC_check_errdetail( + "Changing the setting has no effect after " + "PL/Java has started the Java virtual machine."); + GUC_check_errhint( + "To try a different value, exit this session and start a new one."); + return false; +} - static bool check_enabled( - bool *newval, void **extra, GucSource source) - { - if ( initstage < IS_PLJAVA_ENABLED ) - return true; - if ( *newval ) - return true; - GUC_check_errmsg( - "too late to change \"pljava.enable\" setting"); - GUC_check_errdetail( - "Start-up has progressed past the point where it is checked."); - GUC_check_errhint( - "For another chance, exit this session and start a new one."); - return false; - } +static bool check_enabled( + bool *newval, void **extra, GucSource source) +{ + if ( initstage < IS_PLJAVA_ENABLED ) + return true; + if ( *newval ) + return true; + GUC_check_errmsg( + "too late to change \"pljava.enable\" setting"); + GUC_check_errdetail( + "Start-up has progressed past the point where it is checked."); + GUC_check_errhint( + "For another chance, exit this session and start a new one."); + return false; +} - static bool check_java_thread_pg_entry( - int *newval, void **extra, GucSource source) - { - if ( initstage < IS_PLJAVA_FOUND ) - return true; - if ( java_thread_pg_entry == *newval ) - return true; - GUC_check_errmsg( - "too late to change \"pljava.java_thread_pg_entry\" setting"); - GUC_check_errdetail( - "Start-up has progressed past the point where it is checked."); - GUC_check_errhint( - "For another chance, exit this session and start a new one."); - return false; - } -#endif +static bool check_java_thread_pg_entry( + int *newval, void **extra, GucSource source) +{ + if ( initstage < IS_PLJAVA_FOUND ) + return true; + if ( java_thread_pg_entry == *newval ) + return true; + GUC_check_errmsg( + "too late to change \"pljava.java_thread_pg_entry\" setting"); + GUC_check_errdetail( + "Start-up has progressed past the point where it is checked."); + GUC_check_errhint( + "For another chance, exit this session and start a new one."); + return false; +} -#if PG_VERSION_NUM < 90100 -#define errdetail_internal errdetail -#define ASSIGNHOOK(name,type) \ - static bool \ - CppConcat(assign_,name)(type newval, bool doit, GucSource source); \ - static bool \ - CppConcat(assign_,name)(type newval, bool doit, GucSource source) -#define ASSIGNRETURN(thing) return (thing) -#define ASSIGNRETURNIFCHECK(thing) if (doit) ; else return (thing) -#define ASSIGNRETURNIFNXACT(thing) \ - if (! deferInit && pljavaViableXact()) ; else return (thing) -#define ASSIGNSTRINGHOOK(name) \ - static const char * \ - CppConcat(assign_,name)(const char *newval, bool doit, GucSource source); \ - static const char * \ - CppConcat(assign_,name)(const char *newval, bool doit, GucSource source) -#else #define ASSIGNHOOK(name,type) \ static void \ CppConcat(assign_,name)(type newval, void *extra); \ @@ -406,7 +384,6 @@ static void initsequencer(enum initstage is, bool tolerant); #define ASSIGNRETURNIFNXACT(thing) \ if (! deferInit && pljavaViableXact()) ; else return #define ASSIGNSTRINGHOOK(name) ASSIGNHOOK(name, const char *) -#endif #define ASSIGNENUMHOOK(name) ASSIGNHOOK(name,int) #define ENUMBOOTVAL(entry) ((entry).val) @@ -796,18 +773,13 @@ static void initsequencer(enum initstage is, bool tolerant) * are just function parameters with evaluation order unknown. */ StringInfoData buf; -#if PG_VERSION_NUM >= 90200 -#define MOREHINT \ - appendStringInfo(&buf, \ - "using ALTER DATABASE %s SET ... FROM CURRENT or ", \ - pljavaDbName()), -#else -#define MOREHINT -#endif + ereport(NOTICE, ( errmsg("PL/Java successfully started after adjusting settings"), (initStringInfo(&buf), - MOREHINT + appendStringInfo(&buf, \ + "using ALTER DATABASE %s SET ... FROM CURRENT or ", \ + pljavaDbName()), errhint("The settings that worked should be saved (%s" "in the \"%s\" file). For a reminder of what has been set, " "try: SELECT name, setting FROM pg_settings WHERE name LIKE" @@ -816,7 +788,7 @@ static void initsequencer(enum initstage is, bool tolerant) superuser() ? PG_GETCONFIGOPTION("config_file") : "postgresql.conf")))); -#undef MOREHINT + if ( loadAsExtensionFailed ) { #if PG_VERSION_NUM < 130000 @@ -902,7 +874,7 @@ static void reLogWithChangedLevel(int level) else if ( ERRCODE_WARNING == category || ERRCODE_NO_DATA == category || ERRCODE_SUCCESSFUL_COMPLETION == category ) sqlstate = ERRCODE_INTERNAL_ERROR; -#if PG_VERSION_NUM >= 90500 + edata->elevel = level; edata->sqlerrcode = sqlstate; PG_TRY(); @@ -916,43 +888,6 @@ static void reLogWithChangedLevel(int level) } PG_END_TRY(); FreeErrorData(edata); -#else - if (!errstart(level, edata->filename, edata->lineno, - edata->funcname, NULL)) - { - FreeErrorData(edata); - return; - } - - errcode(sqlstate); - if (edata->message) - errmsg("%s", edata->message); - if (edata->detail) - errdetail("%s", edata->detail); - if (edata->detail_log) - errdetail_log("%s", edata->detail_log); - if (edata->hint) - errhint("%s", edata->hint); - if (edata->context) - errcontext("%s", edata->context); /* this may need to be trimmed */ -#if PG_VERSION_NUM >= 90300 - if (edata->schema_name) - err_generic_string(PG_DIAG_SCHEMA_NAME, edata->schema_name); - if (edata->table_name) - err_generic_string(PG_DIAG_TABLE_NAME, edata->table_name); - if (edata->column_name) - err_generic_string(PG_DIAG_COLUMN_NAME, edata->column_name); - if (edata->datatype_name) - err_generic_string(PG_DIAG_DATATYPE_NAME, edata->datatype_name); - if (edata->constraint_name) - err_generic_string(PG_DIAG_CONSTRAINT_NAME, edata->constraint_name); -#endif - if (edata->internalquery) - internalerrquery(edata->internalquery); - - FreeErrorData(edata); - errfinish(0); -#endif } void _PG_init() @@ -969,8 +904,7 @@ void _PG_init() * preparing the launch options before it is launched. PostgreSQL knows what * it is, but won't directly say; give it some choices and it'll pick one. * Alternatively, let Maven or Ant determine and add a -D at build time from - * the path.separator property. Maybe that's cleaner? This only works for - * PG_VERSION_NUM >= 90100. + * the path.separator property. Maybe that's cleaner? */ sep = first_path_var_separator(":;"); if ( NULL == sep ) @@ -1340,12 +1274,7 @@ static void pljavaQuickDieHandler(int signum) } static sigjmp_buf recoverBuf; -static void terminationTimeoutHandler( -#if PG_VERSION_NUM >= 90300 -#else - int signum -#endif -) +static void terminationTimeoutHandler() { kill(MyProcPid, SIGQUIT); @@ -1387,12 +1316,7 @@ static void _destroyJavaVM(int status, Datum dummy) { Invocation ctx; #ifdef USE_PLJAVA_SIGHANDLERS - -#if PG_VERSION_NUM >= 90300 TimeoutId tid; -#else - pqsigfunc saveSigAlrm; -#endif Invocation_pushBootContext(&ctx); if(sigsetjmp(recoverBuf, 1) != 0) @@ -1404,24 +1328,13 @@ static void _destroyJavaVM(int status, Datum dummy) return; } -#if PG_VERSION_NUM >= 90300 tid = RegisterTimeout(USER_TIMEOUT, terminationTimeoutHandler); enable_timeout_after(tid, 5000); -#else - saveSigAlrm = pqsignal(SIGALRM, terminationTimeoutHandler); - enable_sig_alarm(5000, false); -#endif elog(DEBUG2, "shutting down the Java virtual machine"); JNI_destroyVM(s_javaVM); -#if PG_VERSION_NUM >= 90300 disable_timeout(tid, false); -#else - disable_sig_alarm(false); - pqsignal(SIGALRM, saveSigAlrm); -#endif - #else Invocation_pushBootContext(&ctx); elog(DEBUG2, "shutting down the Java virtual machine"); @@ -1646,12 +1559,7 @@ static jint initializeJavaVM(JVMOptList *optList) #define GUCBOOTVAL(v) (v), #define GUCBOOTASSIGN(a, v) #define GUCFLAGS(f) (f), - -#if PG_VERSION_NUM >= 90100 #define GUCCHECK(h) (h), -#else -#define GUCCHECK(h) -#endif #define BOOL_GUC(name, short_desc, long_desc, valueAddr, bootValue, context, \ flags, check_hook, assign_hook, show_hook) \ @@ -1685,11 +1593,7 @@ static jint initializeJavaVM(JVMOptList *optList) #define PLJAVA_LIBJVMDEFAULT "libjvm" #endif -#if PG_VERSION_NUM >= 90200 #define PLJAVA_ENABLE_DEFAULT true -#else -#define PLJAVA_ENABLE_DEFAULT false -#endif #if PG_VERSION_NUM < 110000 #define PLJAVA_IMPLEMENTOR_FLAGS GUC_LIST_INPUT | GUC_LIST_QUOTE diff --git a/pljava-so/src/main/c/DualState.c b/pljava-so/src/main/c/DualState.c index e557231e5..17d32ab21 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 @@ -44,10 +44,6 @@ extern void pljava_ExecutionPlan_initialize(void); #include "pljava/SQLInputFromTuple.h" #include "pljava/VarlenaWrapper.h" -#if PG_VERSION_NUM < 80400 -#include /* heap_freetuple was there then */ -#endif - static jclass s_DualState_class; static jmethodID s_DualState_resourceOwnerRelease; diff --git a/pljava-so/src/main/c/Exception.c b/pljava-so/src/main/c/Exception.c index 071b8cf49..ccac4e7b8 100644 --- a/pljava-so/src/main/c/Exception.c +++ b/pljava-so/src/main/c/Exception.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2021 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 @@ -59,7 +59,13 @@ Exception_featureNotSupported(const char* requestedFeature, const char* introVer appendStringInfoString(&buf, requestedFeature); appendStringInfoString(&buf, " lacks support in PostgreSQL version "); appendStringInfo(&buf, "%d.%d", - PG_VERSION_NUM / 10000, (PG_VERSION_NUM / 100) % 100); + PG_VERSION_NUM / 10000, +#if PG_VERSION_NUM >= 100000 + (PG_VERSION_NUM) % 10000 +#else + (PG_VERSION_NUM / 100) % 100 +#endif + ); appendStringInfoString(&buf, ". It was introduced in version "); appendStringInfoString(&buf, introVersion); diff --git a/pljava-so/src/main/c/ExecutionPlan.c b/pljava-so/src/main/c/ExecutionPlan.c index 6ff53eb49..ed39bc0fe 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-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 @@ -278,9 +278,7 @@ JNIEXPORT jobject JNICALL Java_org_postgresql_pljava_internal_ExecutionPlan__1prepare(JNIEnv* env, jclass clazz, jobject key, jstring jcmd, jobjectArray paramTypes) { jobject result = 0; -#if PG_VERSION_NUM >= 90200 int spi_ret; -#endif BEGIN_NATIVE STACK_BASE_VARS STACK_BASE_PUSH(env) @@ -321,16 +319,12 @@ Java_org_postgresql_pljava_internal_ExecutionPlan__1prepare(JNIEnv* env, jclass /* Make the plan durable */ p2l.longVal = 0L; /* ensure that the rest is zeroed out */ -#if PG_VERSION_NUM >= 90200 spi_ret = SPI_keepplan(ePlan); if ( 0 == spi_ret ) p2l.ptrVal = ePlan; else Exception_throwSPI("keepplan", spi_ret); -#else - p2l.ptrVal = SPI_saveplan(ePlan); - SPI_freeplan(ePlan); /* Get rid of original, nobody can see it */ -#endif + result = JNI_newObjectLocked( s_ExecutionPlan_class, s_ExecutionPlan_init, /* (jlong)0 as resource owner: the saved plan isn't transient */ diff --git a/pljava-so/src/main/c/Function.c b/pljava-so/src/main/c/Function.c index a46c0e6dd..4428f240f 100644 --- a/pljava-so/src/main/c/Function.c +++ b/pljava-so/src/main/c/Function.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2022 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 @@ -41,15 +41,6 @@ #define PARAM_OIDS(procStruct) (procStruct)->proargtypes.values -#if 90305<=PG_VERSION_NUM || \ - 90209<=PG_VERSION_NUM && PG_VERSION_NUM<90300 || \ - 90114<=PG_VERSION_NUM && PG_VERSION_NUM<90200 || \ - 90018<=PG_VERSION_NUM && PG_VERSION_NUM<90100 || \ - 80422<=PG_VERSION_NUM && PG_VERSION_NUM<90000 -#else -#error "Need fallback for heap_copy_tuple_as_datum" -#endif - #define COUNTCHECK(refs, prims) ((jshort)(((refs) << 8) | ((prims) & 0xff))) jobject pljava_Function_NO_LOADER; diff --git a/pljava-so/src/main/c/InstallHelper.c b/pljava-so/src/main/c/InstallHelper.c index 30af880ae..2cb5a9cb3 100644 --- a/pljava-so/src/main/c/InstallHelper.c +++ b/pljava-so/src/main/c/InstallHelper.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015-2021 Tada AB and other contributors, as listed below. + * Copyright (c) 2015-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 @@ -10,17 +10,11 @@ * Chapman Flack */ #include -#if PG_VERSION_NUM >= 90300 #include -#else -#include -#endif #include #include #include -#if PG_VERSION_NUM >= 90100 #include -#endif #include #include #include @@ -30,20 +24,15 @@ #include #include #include -#if PG_VERSION_NUM >= 80400 #include -#endif #include #if PG_VERSION_NUM >= 120000 #include #define GetNamespaceOid(k1) \ GetSysCacheOid1(NAMESPACENAME, Anum_pg_namespace_oid, k1) -#elif PG_VERSION_NUM >= 90000 -#define GetNamespaceOid(k1) GetSysCacheOid1(NAMESPACENAME, k1) #else -#define SearchSysCache1(cid, k1) SearchSysCache(cid, k1, 0, 0, 0) -#define GetNamespaceOid(k1) GetSysCacheOid(NAMESPACENAME, k1, 0, 0, 0) +#define GetNamespaceOid(k1) GetSysCacheOid1(NAMESPACENAME, k1) #endif #include "pljava/InstallHelper.h" @@ -55,47 +44,17 @@ #include "pljava/type/String.h" /* - * Before 9.1, there was no creating_extension. Before 9.5, it did not have - * PGDLLIMPORT and so was not visible in Windows. In either case, just define - * it to be false, but also define CREATING_EXTENSION_HACK if on Windows and - * it needs to be tested for in some roundabout way. - */ -#if PG_VERSION_NUM < 90100 || defined(_MSC_VER) && PG_VERSION_NUM < 90500 -#define creating_extension false -#if PG_VERSION_NUM >= 90100 -#define CREATING_EXTENSION_HACK -#endif -#endif - -/* - * Before 9.1, there was no IsBinaryUpgrade. Before 9.5, it did not have - * PGDLLIMPORT and so was not visible in Windows. In either case, just define - * it to be false; Windows users may have trouble using pg_upgrade to versions - * earlier than 9.5, but with the current version being 9.6 that should be rare. - */ -#if PG_VERSION_NUM < 90100 || defined(_MSC_VER) && PG_VERSION_NUM < 90500 -#define IsBinaryUpgrade false -#endif - -/* - * Before 9.3, there was no IsBackgroundWorker. As of 9.6.1 it still does not + * As of 9.6.1, IsBackgroundWorker still does not * have PGDLLIMPORT, but MyBgworkerEntry != NULL can be used in MSVC instead. - * However, until 9.3.3, even that did not have PGDLLIMPORT, and there's not - * much to be done about it. BackgroundWorkerness won't be detected in MSVC - * for 9.3.0 through 9.3.2. * * One thing it's needed for is to avoid dereferencing MyProcPort in a * background worker, where it's not set. */ -#if PG_VERSION_NUM < 90300 || defined(_MSC_VER) && PG_VERSION_NUM < 90303 -#define IsBackgroundWorker false -#else #include #if defined(_MSC_VER) #include #define IsBackgroundWorker (MyBgworkerEntry != NULL) #endif -#endif /* * The name of the table the extension scripts will create to pass information @@ -112,7 +71,7 @@ static jfieldID s_InstallHelper_MANAGE_CONTEXT_LOADER; static bool extensionExNihilo = false; -static void checkLoadPath( bool *livecheck); +static void checkLoadPath(void); static void getExtensionLoadPath(void); static char *origUserName(); @@ -153,7 +112,6 @@ static char *origUserName() { if ( IsAutoVacuumWorkerProcess() || IsBackgroundWorker ) { -#if PG_VERSION_NUM >= 90500 char *shortlived; static char *longlived; if ( NULL == longlived ) @@ -163,14 +121,6 @@ static char *origUserName() pfree(shortlived); } return longlived; -#else - ereport(ERROR, ( - errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("PL/Java in a background or autovacuum worker not supported " - "in this PostgreSQL version"), - errhint("PostgreSQL 9.5 is the first version in which " - "such usage is supported."))); -#endif } return MyProcPort->user_name; } @@ -178,22 +128,17 @@ static char *origUserName() char const *pljavaClusterName() { /* - * If PostgreSQL isn't at least 9.5, there can't BE a cluster name, and if - * it is, then there's always one (even if it is an empty string), so - * PG_GETCONFIGOPTION is safe. + * In PostgreSQL of at least 9.5, there's always one (even if it is an empty + * string), so PG_GETCONFIGOPTION is safe. */ -#if PG_VERSION_NUM < 90500 - return ""; -#else return PG_GETCONFIGOPTION("cluster_name"); -#endif } void pljavaCheckExtension( bool *livecheck) { if ( ! creating_extension ) { - checkLoadPath( livecheck); + checkLoadPath(); return; } if ( NULL != livecheck ) @@ -216,29 +161,17 @@ void pljavaCheckExtension( bool *livecheck) * on Windows. So if livecheck isn't null, this function only needs to proceed * as far as the CREATING_EXTENSION_HACK and then return. */ -static void checkLoadPath( bool *livecheck) +static void checkLoadPath() { List *l; Node *ut; LoadStmt *ls; -#if PG_VERSION_NUM >= 80300 PlannedStmt *ps; -#else - Query *ps; -#endif -#ifndef CREATING_EXTENSION_HACK - if ( NULL != livecheck ) - return; -#endif if ( NULL == ActivePortal ) return; - l = ActivePortal-> -#if PG_VERSION_NUM >= 80300 - stmts; -#else - parseTrees; -#endif + l = ActivePortal->stmts; + if ( NULL == l ) return; if ( 1 < list_length( l) ) @@ -249,15 +182,10 @@ static void checkLoadPath( bool *livecheck) elog(DEBUG2, "got null for first statement from ActivePortal"); return; } -#if PG_VERSION_NUM >= 80300 + if ( T_PlannedStmt == nodeTag(ut) ) { ps = (PlannedStmt *)ut; -#else - if ( T_Query == nodeTag(ut) ) - { - ps = (Query *)ut; -#endif if ( CMD_UTILITY != ps->commandType ) { elog(DEBUG2, "ActivePortal has PlannedStmt command type %u", @@ -272,22 +200,8 @@ static void checkLoadPath( bool *livecheck) } } if ( T_LoadStmt != nodeTag(ut) ) -#ifdef CREATING_EXTENSION_HACK - if ( T_CreateExtensionStmt == nodeTag(ut) ) - { - if ( NULL != livecheck ) - { - *livecheck = true; - return; - } - getExtensionLoadPath(); - if ( NULL != pljavaLoadPath ) - pljavaLoadingAsExtension = true; - } -#endif - return; - if ( NULL != livecheck ) return; + ls = (LoadStmt *)ut; if ( NULL == ls->filename ) { @@ -555,14 +469,9 @@ char *InstallHelper_hello() nativeVer = String_createJavaStringFromNTS(SO_VERSION_STRING); serverBuiltVer = String_createJavaStringFromNTS(PG_VERSION_STR); -#if PG_VERSION_NUM >= 90100 InitFunctionCallInfoData(fcinfo, NULL, 0, InvalidOid, /* collation */ NULL, NULL); -#else - InitFunctionCallInfoData(fcinfo, NULL, 0, - NULL, NULL); -#endif runningVer = DatumGetTextP(pgsql_version(&fcinfo)); serverRunningVer = String_createJavaString(runningVer); pfree(runningVer); @@ -614,15 +523,9 @@ void InstallHelper_groundwork() bool snapshot_set = false; Invocation_pushInvocation(&ctx); ctx.function = Function_INIT_WRITER; -#if PG_VERSION_NUM >= 80400 if ( ! ActiveSnapshotSet() ) { PushActiveSnapshot(GetTransactionSnapshot()); -#else - if ( NULL == ActiveSnapshot ) - { - ActiveSnapshot = CopySnapshot(GetTransactionSnapshot()); -#endif snapshot_set = true; } PG_TRY(); @@ -644,11 +547,7 @@ void InstallHelper_groundwork() JNI_deleteLocalRef(jlptq); if ( snapshot_set ) { -#if PG_VERSION_NUM >= 80400 PopActiveSnapshot(); -#else - ActiveSnapshot = NULL; -#endif } Invocation_popInvocation(false); } @@ -656,11 +555,7 @@ void InstallHelper_groundwork() { if ( snapshot_set ) { -#if PG_VERSION_NUM >= 80400 PopActiveSnapshot(); -#else - ActiveSnapshot = NULL; -#endif } Invocation_popInvocation(true); PG_RE_THROW(); diff --git a/pljava-so/src/main/c/SPI.c b/pljava-so/src/main/c/SPI.c index ea104bd9f..3d891b30d 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 @@ -94,14 +94,19 @@ void SPI_initialize(void) CONFIRMCONST(SPI_OK_INSERT_RETURNING); CONFIRMCONST(SPI_OK_DELETE_RETURNING); CONFIRMCONST(SPI_OK_UPDATE_RETURNING); -#if PG_VERSION_NUM >= 80400 CONFIRMCONST(SPI_OK_REWRITTEN); -#endif #if PG_VERSION_NUM >= 100000 CONFIRMCONST(SPI_OK_REL_REGISTER); CONFIRMCONST(SPI_OK_REL_UNREGISTER); CONFIRMCONST(SPI_OK_TD_REGISTER); #endif +#if PG_VERSION_NUM >= 150000 + CONFIRMCONST(SPI_OK_MERGE); +#endif + +#if PG_VERSION_NUM >= 110000 + CONFIRMCONST(SPI_OPT_NONATOMIC); +#endif } /**************************************** diff --git a/pljava-so/src/main/c/Session.c b/pljava-so/src/main/c/Session.c index be4a2ccab..f551cb5b7 100644 --- a/pljava-so/src/main/c/Session.c +++ b/pljava-so/src/main/c/Session.c @@ -1,10 +1,14 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2023 Tada AB and other contributors, as listed below. * - * @author Thomas Hallgren + * 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 */ #include #include @@ -45,9 +49,6 @@ Java_org_postgresql_pljava_internal_Session__1setUser( * a finally block after an exception. */ BEGIN_NATIVE_NO_ERRCHECK -#if 80402<=PG_VERSION_NUM || \ - 80309<=PG_VERSION_NUM && PG_VERSION_NUM<80400 || \ - 80215<=PG_VERSION_NUM && PG_VERSION_NUM<80300 if (InSecurityRestrictedOperation()) ereport(ERROR, (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), errmsg( "cannot set parameter \"%s\" within security-restricted operation", @@ -59,15 +60,6 @@ Java_org_postgresql_pljava_internal_Session__1setUser( else secContext &= ~SECURITY_LOCAL_USERID_CHANGE; SetUserIdAndSecContext(AclId_getAclId(aclId), secContext); -#elif PG_VERSION_NUM>=80206 - (void)secContext; /* away with your unused-variable warnings! */ - GetUserIdAndContext(&dummy, &wasLocalChange); - SetUserIdAndContext(AclId_getAclId(aclId), (bool)isLocalChange); -#else - (void)secContext; - (void)dummy; - SetUserId(AclId_getAclId(aclId)); -#endif END_NATIVE return wasLocalChange ? JNI_TRUE : JNI_FALSE; } diff --git a/pljava-so/src/main/c/TypeOid.c b/pljava-so/src/main/c/TypeOid.c index 0fce9ad17..810053268 100644 --- a/pljava-so/src/main/c/TypeOid.c +++ b/pljava-so/src/main/c/TypeOid.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2021 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 @@ -50,10 +50,6 @@ JNIEXPORT void JNICALL Java_org_postgresql_pljava_jdbc_TypeOid__1dummy(JNIEnv * CONFIRMCONST(VARCHAROID); CONFIRMCONST(OIDOID); CONFIRMCONST(BPCHAROID); - -#if PG_VERSION_NUM >= 90100 CONFIRMCONST(PG_NODE_TREEOID); -#endif - CONFIRMCONST(TRIGGEROID); } diff --git a/pljava-so/src/main/c/VarlenaWrapper.c b/pljava-so/src/main/c/VarlenaWrapper.c index 5805ace13..cb1d9e7a5 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 @@ -21,13 +21,8 @@ #endif #include - -#if PG_VERSION_NUM < 80400 -#define RegisterSnapshotOnOwner(s,o) NULL -#else #include #include -#endif #include "org_postgresql_pljava_internal_VarlenaWrapper_Input_State.h" #include "org_postgresql_pljava_internal_VarlenaWrapper_Output_State.h" @@ -41,37 +36,7 @@ #define GetOldestSnapshot() NULL #endif -#if PG_VERSION_NUM < 90400 -/* - * There aren't 'indirect' varlenas yet, IS_EXTERNAL_ONDISK is just IS_EXTERNAL, - * and VARATT_EXTERNAL_GET_POINTER is private inside tuptoaster.c; copy it here. - */ -#define VARATT_IS_EXTERNAL_ONDISK VARATT_IS_EXTERNAL -#define VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr) \ -do { \ - varattrib_1b_e *attre = (varattrib_1b_e *) (attr); \ - memcpy(&(toast_pointer), VARDATA_EXTERNAL(attre), sizeof(toast_pointer)); \ -} while (0) -#endif - -#if PG_VERSION_NUM < 80300 -#define VARSIZE_ANY(PTR) VARSIZE(PTR) -#define VARSIZE_ANY_EXHDR(PTR) (VARSIZE(PTR) - VARHDRSZ) -#define SET_VARSIZE(PTR, len) VARATT_SIZEP(PTR) = len & VARATT_MASK_SIZE -struct varatt_external -{ - int32 va_extsize; /* the only piece used here */ -}; -#undef VARATT_EXTERNAL_GET_POINTER -#define VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr) \ -do { \ - (toast_pointer).va_extsize = \ - ((varattrib *)(attr))->va_content.va_external.va_extsize; \ -} while (0) -#define _VL_TYPE varattrib * -#else #define _VL_TYPE struct varlena * -#endif #if PG_VERSION_NUM < 140000 #define VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer) ((toast_pointer).va_extsize) @@ -102,37 +67,6 @@ static jfieldID s_VarlenaWrapper_Input_State_varlena; * final reallocation and copy will happen. */ -#if PG_VERSION_NUM < 90500 -/* - * There aren't 'expanded' varlenas yet. Copy some defs (in simplified form) - * and pretend there are. - */ -typedef struct ExpandedObjectHeader ExpandedObjectHeader; - -typedef Size (*EOM_get_flat_size_method) (ExpandedObjectHeader *eohptr); -typedef void (*EOM_flatten_into_method) (ExpandedObjectHeader *eohptr, - void *result, Size allocated_size); - -typedef struct ExpandedObjectMethods -{ - EOM_get_flat_size_method get_flat_size; - EOM_flatten_into_method flatten_into; -} ExpandedObjectMethods; - -struct ExpandedObjectHeader -{ - int32 magic; - MemoryContext eoh_context; -}; - -#define EOH_init_header(eohptr, methods, obj_context) \ - do {(eohptr)->magic = -1; (eohptr)->eoh_context = (obj_context);} while (0) - -#define EOHPGetRWDatum(eohptr) (eohptr) -#define DatumGetEOHP(d) (d) -#define VARATT_IS_EXTERNAL_EXPANDED(attr) false -#endif - static Size VOS_get_flat_size(ExpandedObjectHeader *eohptr); static void VOS_flatten_into(ExpandedObjectHeader *eohptr, void *result, Size allocated_size); @@ -189,7 +123,6 @@ jobject pljava_VarlenaWrapper_Input( vl = (_VL_TYPE) DatumGetPointer(d); -#if PG_VERSION_NUM >= 90400 if ( VARATT_IS_EXTERNAL_INDIRECT(vl) ) /* at most once; can't be nested */ { struct varatt_indirect redirect; @@ -197,7 +130,6 @@ jobject pljava_VarlenaWrapper_Input( vl = (_VL_TYPE)redirect.pointer; d = PointerGetDatum(vl); } -#endif parked = VARSIZE_ANY(vl); actual = toast_raw_datum_size(d) - VARHDRSZ; @@ -342,25 +274,11 @@ jobject pljava_VarlenaWrapper_Output(MemoryContext parent, ResourceOwner ro) Datum pljava_VarlenaWrapper_adopt(jobject vlw) { Ptr2Long p2l; -#if PG_VERSION_NUM < 90500 - ExpandedObjectHeader *eohptr; - Size final_size; - void *final_result; -#endif p2l.longVal = JNI_callLongMethodLocked(vlw, s_VarlenaWrapper_adopt, pljava_DualState_key()); -#if PG_VERSION_NUM >= 90500 + return PointerGetDatum(p2l.ptrVal); -#else - eohptr = p2l.ptrVal; - if ( -1 != eohptr->magic ) - return PointerGetDatum(eohptr); - final_size = VOS_get_flat_size(eohptr); - final_result = MemoryContextAlloc(eohptr->eoh_context, final_size); - VOS_flatten_into(eohptr, final_result, final_size); - return PointerGetDatum(final_result); -#endif } static Size VOS_get_flat_size(ExpandedObjectHeader *eohptr) @@ -376,9 +294,6 @@ static void VOS_flatten_into(ExpandedObjectHeader *eohptr, ExpandedVarlenaOutputStreamHeader *evosh = (ExpandedVarlenaOutputStreamHeader *)eohptr; ExpandedVarlenaOutputStreamNode *node = evosh->tail; -#if PG_VERSION_NUM < 90500 - ExpandedVarlenaOutputStreamNode *next; -#endif Assert(allocated_size == evosh->total_size); SET_VARSIZE(result, allocated_size); @@ -391,25 +306,6 @@ static void VOS_flatten_into(ExpandedObjectHeader *eohptr, result = (char *)result + node->size; } while ( node != evosh->tail ); - -#if PG_VERSION_NUM < 90500 - /* - * It's been flattened into the same context; the original nodes can be - * freed so the 2x memory usage doesn't last longer than necessary. Freeing - * them retail isn't ideal, but this is back-compatibility code. Remember - * the first one wasn't a separate allocation. - */ - node = node->next; /* this is the head, the one that can't be pfreed */ - evosh->tail = node; /* tail is now head, the non-pfreeable node */ - node = node->next; - while ( node != evosh->tail ) - { - next = node->next; - pfree(node); - node = next; - } - pfree(evosh); -#endif } void pljava_VarlenaWrapper_initialize(void) @@ -495,7 +391,6 @@ JNIEXPORT void JNICALL Java_org_postgresql_pljava_internal_VarlenaWrapper_00024Input_00024State__1unregisterSnapshot (JNIEnv *env, jobject _this, jlong snapshot, jlong ro) { -#if PG_VERSION_NUM >= 80400 BEGIN_NATIVE_NO_ERRCHECK Ptr2Long p2lsnap; Ptr2Long p2lro; @@ -503,7 +398,6 @@ Java_org_postgresql_pljava_internal_VarlenaWrapper_00024Input_00024State__1unreg p2lro.longVal = ro; UnregisterSnapshotFromOwner(p2lsnap.ptrVal, p2lro.ptrVal); END_NATIVE -#endif } /* @@ -517,10 +411,8 @@ Java_org_postgresql_pljava_internal_VarlenaWrapper_00024Input_00024State__1detoa { Ptr2Long p2lvl; Ptr2Long p2lcxt; -#if PG_VERSION_NUM >= 80400 Ptr2Long p2lsnap; Ptr2Long p2lro; -#endif Ptr2Long p2ldetoasted; _VL_TYPE detoasted; MemoryContext prevcxt; @@ -530,10 +422,8 @@ Java_org_postgresql_pljava_internal_VarlenaWrapper_00024Input_00024State__1detoa p2lvl.longVal = vl; p2lcxt.longVal = cxt; -#if PG_VERSION_NUM >= 80400 p2lsnap.longVal = snap; p2lro.longVal = resOwner; -#endif prevcxt = MemoryContextSwitchTo((MemoryContext)p2lcxt.ptrVal); @@ -547,10 +437,8 @@ Java_org_postgresql_pljava_internal_VarlenaWrapper_00024Input_00024State__1detoa s_VarlenaWrapper_Input_State_varlena, p2ldetoasted.longVal); pfree(p2lvl.ptrVal); -#if PG_VERSION_NUM >= 80400 if ( 0 != snap ) UnregisterSnapshotFromOwner(p2lsnap.ptrVal, p2lro.ptrVal); -#endif dbb = JNI_newDirectByteBuffer( VARDATA(detoasted), VARSIZE_ANY_EXHDR(detoasted)); diff --git a/pljava-so/src/main/c/XactListener.c b/pljava-so/src/main/c/XactListener.c index 543c2cd46..096326184 100644 --- a/pljava-so/src/main/c/XactListener.c +++ b/pljava-so/src/main/c/XactListener.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 @@ -41,11 +41,9 @@ case XACT_EVENT_##c: \ CASE( PREPARE ); CASE( PRE_COMMIT ); CASE( PRE_PREPARE ); -#if PG_VERSION_NUM >= 90500 CASE( PARALLEL_COMMIT ); CASE( PARALLEL_ABORT ); CASE( PARALLEL_PRE_COMMIT ); -#endif } JNI_callStaticVoidMethod(s_XactListener_class, diff --git a/pljava-so/src/main/c/type/AclId.c b/pljava-so/src/main/c/type/AclId.c index 504b8cf6a..a9f570667 100644 --- a/pljava-so/src/main/c/type/AclId.c +++ b/pljava-so/src/main/c/type/AclId.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 @@ -184,11 +184,7 @@ Java_org_postgresql_pljava_internal_AclId__1getName(JNIEnv* env, jobject aclId) { result = String_createJavaStringFromNTS( GetUserNameFromId( -#if PG_VERSION_NUM >= 90500 AclId_getAclId(aclId), /* noerr= */ false -#else - AclId_getAclId(aclId) -#endif ) ); } diff --git a/pljava-so/src/main/c/type/Array.c b/pljava-so/src/main/c/type/Array.c index 232fc05aa..b2e654482 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 @@ -51,11 +51,7 @@ ArrayType* createArrayType(jsize nElems, size_t elemSize, Oid elemType, bool wit v->dataoffset = (int32)dataoffset; MemoryContextSwitchTo(currCtx); -#if PG_VERSION_NUM < 80300 - ARR_SIZE(v) = nBytes; -#else SET_VARSIZE(v, nBytes); -#endif ARR_NDIM(v) = 1; ARR_ELEMTYPE(v) = elemType; *((int*)ARR_DIMS(v)) = nElems; @@ -88,14 +84,8 @@ static jvalue _Array_coerceDatum(Type self, Datum arg) JNI_setObjectArrayElement(objArray, idx, obj.l); JNI_deleteLocalRef(obj.l); -#if PG_VERSION_NUM < 80300 - values = att_addlength(values, elemLength, PointerGetDatum(values)); - values = (char*)att_align(values, elemAlign); -#else values = att_addlength_datum(values, elemLength, PointerGetDatum(values)); values = (char*)att_align_nominal(values, elemAlign); -#endif - } } result.l = (jobject)objArray; diff --git a/pljava-so/src/main/c/type/Oid.c b/pljava-so/src/main/c/type/Oid.c index aecb67e01..37bb89b2e 100644 --- a/pljava-so/src/main/c/type/Oid.c +++ b/pljava-so/src/main/c/type/Oid.c @@ -1,10 +1,14 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2023 Tada AB and other contributors, as listed below. * - * @author Thomas Hallgren + * 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 */ #include @@ -256,11 +260,7 @@ Java_org_postgresql_pljava_internal_Oid__1forTypeName(JNIEnv* env, jclass cls, j PG_TRY(); { int32 typmod = 0; -#if PG_VERSION_NUM < 90400 - parseTypeString(typeNameOrOid, &typeId, &typmod); -#else parseTypeString(typeNameOrOid, &typeId, &typmod, 0); -#endif } PG_CATCH(); { diff --git a/pljava-so/src/main/c/type/SQLXMLImpl.c b/pljava-so/src/main/c/type/SQLXMLImpl.c index 16b483b37..157f4251b 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 @@ -50,9 +50,7 @@ static bool _SQLXML_canReplaceType(Type self, Type other) #if defined(XMLOID) Type_getOid(other) == XMLOID || #endif -#if PG_VERSION_NUM >= 90100 Type_getOid(other) == PG_NODE_TREEOID || /* a synthetic rendering */ -#endif Type_getOid(other) == TEXTOID; } @@ -86,17 +84,12 @@ static Datum _SQLXML_coerceObject(Type self, jobject sqlxml) s_SQLXML_class, s_SQLXML_adopt, sqlxml, Type_getOid(self)); Datum d = pljava_VarlenaWrapper_adopt(vw); JNI_deleteLocalRef(vw); -#if PG_VERSION_NUM >= 90500 if ( VARATT_IS_EXTERNAL_EXPANDED_RW(DatumGetPointer(d)) ) return TransferExpandedObject(d, CurrentMemoryContext); -#endif -#if PG_VERSION_NUM >= 90200 + MemoryContextSetParent( GetMemoryChunkContext(DatumGetPointer(d)), CurrentMemoryContext); -#else - if ( CurrentMemoryContext != GetMemoryChunkContext(DatumGetPointer(d)) ) - d = PointerGetDatum(PG_DETOAST_DATUM_COPY(d)); -#endif + return d; } @@ -134,18 +127,15 @@ static Type _SQLXML_obtain(Oid typeId) #if defined(XMLOID) static Type xmlInstance; #endif -#if PG_VERSION_NUM >= 90100 static Type pgNodeTreeInstance; -#endif + switch ( typeId ) { -#if PG_VERSION_NUM >= 90100 case PG_NODE_TREEOID: allowedId = PG_NODE_TREEOID; synthetic = true; cache = &pgNodeTreeInstance; break; -#endif default: if ( TEXTOID == typeId ) { diff --git a/pljava-so/src/main/c/type/String.c b/pljava-so/src/main/c/type/String.c index e380be326..8b4623942 100644 --- a/pljava-so/src/main/c/type/String.c +++ b/pljava-so/src/main/c/type/String.c @@ -226,11 +226,7 @@ text* String_createText(jstring javaString) /* Allocate and initialize the text structure. */ result = (text*)palloc(varSize); -#if PG_VERSION_NUM < 80300 - VARATT_SIZEP(result) = varSize; /* Total size of structure, not just data */ -#else SET_VARSIZE(result, varSize); /* Total size of structure, not just data */ -#endif memcpy(VARDATA(result), denc, dencLen); if(denc != sid.data) diff --git a/pljava-so/src/main/c/type/Timestamp.c b/pljava-so/src/main/c/type/Timestamp.c index 2cbdba9ee..7cd76a6c2 100644 --- a/pljava-so/src/main/c/type/Timestamp.c +++ b/pljava-so/src/main/c/type/Timestamp.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 @@ -405,23 +405,7 @@ static Datum _Timestamptz_coerceObject(Type self, jobject ts) */ static int32 Timestamp_getTimeZone(pg_time_t time) { -#if defined(_MSC_VER) && ( \ - 100000<=PG_VERSION_NUM && PG_VERSION_NUM<100002 || \ - 90600<=PG_VERSION_NUM && PG_VERSION_NUM< 90607 || \ - 90500<=PG_VERSION_NUM && PG_VERSION_NUM< 90511 || \ - 90400<=PG_VERSION_NUM && PG_VERSION_NUM< 90416 || \ - PG_VERSION_NUM < 90321 ) - /* This is gross, but pg_tzset has a cache, so not as gross as you think. - * There is some renewed interest on pgsql-hackers to find a good answer for - * the MSVC PGDLLIMPORT nonsense, so this may not have to stay gross. - */ - char const *tzname = PG_GETCONFIGOPTION("timezone"); - struct pg_tm* tx = pg_localtime(&time, pg_tzset(tzname)); -#elif PG_VERSION_NUM < 80300 - struct pg_tm* tx = pg_localtime(&time, global_timezone); -#else struct pg_tm* tx = pg_localtime(&time, session_timezone); -#endif if ( NULL == tx ) ereport(ERROR, ( errcode(ERRCODE_DATA_EXCEPTION), diff --git a/pljava-so/src/main/c/type/Type.c b/pljava-so/src/main/c/type/Type.c index a0d181e2f..188e86e78 100644 --- a/pljava-so/src/main/c/type/Type.c +++ b/pljava-so/src/main/c/type/Type.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2022 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 @@ -30,42 +30,11 @@ #include "pljava/HashMap.h" #include "pljava/SPI.h" -#if PG_VERSION_NUM < 80300 -typedef enum CoercionPathType -{ - COERCION_PATH_NONE, /* failed to find any coercion pathway */ - COERCION_PATH_FUNC, /* apply the specified coercion function */ - COERCION_PATH_RELABELTYPE, /* binary-compatible cast, no function */ - COERCION_PATH_ARRAYCOERCE, /* need an ArrayCoerceExpr node */ - COERCION_PATH_COERCEVIAIO /* need a CoerceViaIO node */ -} CoercionPathType; - -static CoercionPathType fcp(Oid targetTypeId, Oid sourceTypeId, - CoercionContext ccontext, Oid *funcid); -static CoercionPathType fcp(Oid targetTypeId, Oid sourceTypeId, - CoercionContext ccontext, Oid *funcid) -{ - if ( find_coercion_pathway(targetTypeId, sourceTypeId, ccontext, funcid) ) - return *funcid != InvalidOid ? - COERCION_PATH_FUNC : COERCION_PATH_RELABELTYPE; - else - return COERCION_PATH_NONE; -} -#define find_coercion_pathway fcp -#endif - -#if PG_VERSION_NUM < 90500 -#define DomainHasConstraints(x) true -#endif - #if PG_VERSION_NUM < 110000 static Oid BOOLARRAYOID; static Oid CHARARRAYOID; static Oid FLOAT8ARRAYOID; static Oid INT8ARRAYOID; -#if PG_VERSION_NUM < 80400 -static Oid INT2ARRAYOID; -#endif #endif static HashMap s_typeByOid; @@ -1055,9 +1024,6 @@ void Type_initialize(void) CHARARRAYOID = get_array_type(CHAROID); FLOAT8ARRAYOID = get_array_type(FLOAT8OID); INT8ARRAYOID = get_array_type(INT8OID); -#if PG_VERSION_NUM < 80400 - INT2ARRAYOID = get_array_type(INT2OID); -#endif #endif initializeTypeBridges(); diff --git a/pljava-so/src/main/c/type/UDT.c b/pljava-so/src/main/c/type/UDT.c index d351038fc..1f33b0afc 100644 --- a/pljava-so/src/main/c/type/UDT.c +++ b/pljava-so/src/main/c/type/UDT.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2021 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 @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -29,10 +30,6 @@ #include "pljava/SQLInputFromTuple.h" #include "pljava/SQLOutputToTuple.h" -#if PG_VERSION_NUM >= 90000 -#include -#endif - /* * This code, as currently constituted, makes these assumptions that limit how * Java can implement a (scalar) UDT: @@ -183,11 +180,7 @@ static Datum coerceScalarObject(UDT self, jobject value) { /* Assign the correct length. */ -#if PG_VERSION_NUM < 80300 - VARATT_SIZEP(buffer.data) = buffer.len; -#else SET_VARSIZE(buffer.data, buffer.len); -#endif } else if(dataLen != buffer.len) { diff --git a/pljava-so/src/main/c/type/byte_array.c b/pljava-so/src/main/c/type/byte_array.c index d17630842..91e3103a0 100644 --- a/pljava-so/src/main/c/type/byte_array.c +++ b/pljava-so/src/main/c/type/byte_array.c @@ -1,10 +1,14 @@ /* - * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden - * Distributed under the terms shown in the file COPYRIGHT - * found in the root folder of this project or at - * http://eng.tada.se/osprojects/COPYRIGHT.html + * Copyright (c) 2004-2023 Tada AB and other contributors, as listed below. * - * @author Thomas Hallgren + * 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 */ #include "pljava/Exception.h" #include "pljava/type/Type_priv.h" @@ -40,11 +44,7 @@ static Datum _byte_array_coerceObject(Type self, jobject byteArray) int32 byteaSize = length + VARHDRSZ; bytes = (bytea*)palloc(byteaSize); -#if PG_VERSION_NUM < 80300 - VARATT_SIZEP(bytes) = byteaSize; -#else SET_VARSIZE(bytes, byteaSize); -#endif JNI_getByteArrayRegion((jbyteArray)byteArray, 0, length, (jbyte*)VARDATA(bytes)); } else if(JNI_isInstanceOf(byteArray, s_BlobValue_class)) @@ -55,11 +55,7 @@ static Datum _byte_array_coerceObject(Type self, jobject byteArray) byteaSize = (int32)(length + VARHDRSZ); bytes = (bytea*)palloc(byteaSize); -#if PG_VERSION_NUM < 80300 - VARATT_SIZEP(bytes) = byteaSize; -#else SET_VARSIZE(bytes, byteaSize); -#endif byteBuffer = JNI_newDirectByteBuffer((void*)VARDATA(bytes), length); if(byteBuffer != 0) diff --git a/pljava-so/src/main/include/pljava/Backend.h b/pljava-so/src/main/include/pljava/Backend.h index 16c29cdbd..e0e8ca30b 100644 --- a/pljava-so/src/main/include/pljava/Backend.h +++ b/pljava-so/src/main/include/pljava/Backend.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2021 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 @@ -53,13 +53,7 @@ void Backend_warnJEP411(bool isCommit); #error The macro PG_GETCONFIGOPTION needs to be renamed. #endif -#if PG_VERSION_NUM >= 90100 #define PG_GETCONFIGOPTION(key) GetConfigOption(key, false, true) -#elif PG_VERSION_NUM >= 90000 -#define PG_GETCONFIGOPTION(key) GetConfigOption(key, true) -#else -#define PG_GETCONFIGOPTION(key) GetConfigOption(key) -#endif #ifdef __cplusplus } diff --git a/pljava-so/src/main/include/pljava/Exception.h b/pljava-so/src/main/include/pljava/Exception.h index a2edad91a..d524dd062 100644 --- a/pljava-so/src/main/include/pljava/Exception.h +++ b/pljava-so/src/main/include/pljava/Exception.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2021 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 @@ -17,14 +17,6 @@ #include "pljava/PgObject.h" -#if PG_VERSION_NUM < 90500 -#ifdef __GNUC__ -#define pg_attribute_printf(f,a) __attribute__((format(printf, f, a))) -#else -#define pg_attribute_printf(f,a) -#endif -#endif - #ifdef __cplusplus extern "C" { #endif diff --git a/pljava-so/src/main/include/pljava/pljava.h b/pljava-so/src/main/include/pljava/pljava.h index 8da93ff6c..6a2e298c9 100644 --- a/pljava-so/src/main/include/pljava/pljava.h +++ b/pljava-so/src/main/include/pljava/pljava.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2021 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 @@ -37,6 +37,7 @@ extern int vsnprintf(char* buf, size_t count, const char* format, va_list arg); #include #include #include +#include /* * AssertVariableIsOfType appeared in PG9.3. Can test for the macro directly. @@ -65,23 +66,6 @@ extern int vsnprintf(char* buf, size_t count, const char* format, va_list arg); ALLOCSET_SMALL_MINSIZE, ALLOCSET_SMALL_INITSIZE, ALLOCSET_DEFAULT_MAXSIZE #endif -/* - * GETSTRUCT require "access/htup_details.h" to be included in PG9.3 - */ -#if PG_VERSION_NUM >= 90300 -#include "access/htup_details.h" -#endif - -/* - * PG_*_{MIN,MAX} macros (which happen, conveniently, to match Java's datatypes - * (the signed ones, anyway), appear in PG 9.5. Could test for them directly, - * but explicit version conditionals may be easier to find and prune when the - * back-compatibility horizon passes them. Here are only the ones being used. - */ -#if PG_VERSION_NUM < 90500 -#define PG_INT32_MAX (0x7FFFFFFF) -#endif - /* * This symbol was spelled without the underscores prior to PG 14. */ @@ -118,28 +102,10 @@ extern MemoryContext JavaMemoryContext; * stack_base_ptr was static before PG 8.1. By executive decision, PL/Java now * has 8.1 as a back compatibility limit; no empty #defines here for earlier. */ -#if 90104<=PG_VERSION_NUM || \ - 90008<=PG_VERSION_NUM && PG_VERSION_NUM<90100 || \ - 80412<=PG_VERSION_NUM && PG_VERSION_NUM<90000 || \ - 80319<=PG_VERSION_NUM && PG_VERSION_NUM<80400 #define NEED_MISCADMIN_FOR_STACK_BASE #define _STACK_BASE_TYPE pg_stack_base_t #define _STACK_BASE_SET saveStackBasePtr = set_stack_base() #define _STACK_BASE_RESTORE restore_stack_base(saveStackBasePtr) -#else -extern -#if PG_VERSION_NUM < 80300 -DLLIMPORT -#else -PGDLLIMPORT -#endif -char* stack_base_ptr; -#define _STACK_BASE_TYPE char* -#define _STACK_BASE_SET \ - saveStackBasePtr = stack_base_ptr; \ - stack_base_ptr = (char*)&saveMainThreadId -#define _STACK_BASE_RESTORE stack_base_ptr = saveStackBasePtr -#endif #define STACK_BASE_VARS \ void* saveMainThreadId = 0; \ 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..98ac19932 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 @@ -53,6 +53,9 @@ public class SPI public static final int OK_REL_REGISTER = 15; public static final int OK_REL_UNREGISTER = 16; public static final int OK_TD_REGISTER = 17; + public static final int OK_MERGE = 18; + + public static final int OPT_NONATOMIC = 1 << 0; /** * Execute a command using the internal SPI_exec function. diff --git a/src/site/markdown/build/versions.md b/src/site/markdown/build/versions.md index a759a20b9..1c201a21c 100644 --- a/src/site/markdown/build/versions.md +++ b/src/site/markdown/build/versions.md @@ -62,7 +62,7 @@ versions 4.3.0 or later are recommended in order to avoid a ## PostgreSQL -The PL/Java 1.6 series does not commit to support PostgreSQL earlier than 9.5. +The PL/Java 1.6 series does not support PostgreSQL earlier than 9.5. More current PostgreSQL versions, naturally, are the focus of development and receive more attention in testing. From 46a41cd18c95e520ed042916c691fa2b125d735b Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Wed, 23 Aug 2023 12:02:08 -0400 Subject: [PATCH 131/334] Run another example as a regression test Going through all the examples for version-conditional bits showed at least one more deserving inclusion as a regression test. Also tidy a couple examples to use out={...} instead of some throwaway composite type. And put a query in more of the traditional uppercase-keywords form. --- .../annotation/RecordParameterDefaults.java | 19 ++---- .../annotation/UnicodeRoundTripTest.java | 65 +++++++++---------- .../example/annotation/XMLRenderedTypes.java | 15 ++++- 3 files changed, 47 insertions(+), 52 deletions(-) diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/RecordParameterDefaults.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/RecordParameterDefaults.java index 34e1aeb75..291eb990b 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/RecordParameterDefaults.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/RecordParameterDefaults.java @@ -19,7 +19,6 @@ import org.postgresql.pljava.ResultSetProvider; import org.postgresql.pljava.annotation.Function; -import org.postgresql.pljava.annotation.SQLAction; import org.postgresql.pljava.annotation.SQLType; /** @@ -29,17 +28,6 @@ *

    * Also tests the proper DDR generation of defaults for such parameters. */ -@SQLAction( - provides = "paramtypeinfo type", // created in Triggers.java - install = { - "CREATE TYPE javatest.paramtypeinfo AS (" + - " name text, pgtypename text, javaclass text, tostring text" + - ")" - }, - remove = { - "DROP TYPE javatest.paramtypeinfo" - } -) public class RecordParameterDefaults implements ResultSetProvider { /** @@ -59,10 +47,11 @@ public class RecordParameterDefaults implements ResultSetProvider * */ @Function( - requires = "paramtypeinfo type", schema = "javatest", - type = "javatest.paramtypeinfo" - ) + out = { + "name text", "pgtypename text", "javaclass text", "tostring text" + } + ) public static ResultSetProvider paramDefaultsRecord( @SQLType(defaultValue={})ResultSet params) throws SQLException diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/UnicodeRoundTripTest.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/UnicodeRoundTripTest.java index 6b06d4d9a..c317dab25 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/UnicodeRoundTripTest.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/UnicodeRoundTripTest.java @@ -48,50 +48,43 @@ @SQLAction(requires="unicodetest fn", implementor="postgresql_unicodetest", install= -" with " + -" usable_codepoints ( cp ) as ( " + -" select generate_series(1,x'd7ff'::int) " + -" union all " + -" select generate_series(x'e000'::int,x'10ffff'::int) " + +" WITH " + +" usable_codepoints ( cp ) AS ( " + +" SELECT generate_series(1,x'd7ff'::int) " + +" UNION ALL " + +" SELECT generate_series(x'e000'::int,x'10ffff'::int) " + " ), " + -" test_inputs ( groupnum, cparray, s ) as ( " + -" select " + -" cp / 1024 as groupnum, " + -" array_agg(cp order by cp), string_agg(chr(cp), '' order by cp) " + -" from usable_codepoints " + -" group by groupnum " + +" test_inputs ( groupnum, cparray, s ) AS ( " + +" SELECT " + +" cp / 1024 AS groupnum, " + +" array_agg(cp ORDER BY cp), string_agg(chr(cp), '' ORDER BY cp) " + +" FROM usable_codepoints " + +" GROUP BY groupnum " + " ), " + -" test_outputs as ( " + -" select groupnum, cparray, s, unicodetest(s, cparray) as roundtrip " + -" from test_inputs " + +" test_outputs AS ( " + +" SELECT groupnum, cparray, s, unicodetest(s, cparray) AS roundtrip " + +" FROM test_inputs " + " ), " + -" test_failures as ( " + -" select * " + -" from test_outputs " + -" where " + -" cparray != (roundtrip).cparray or s != (roundtrip).s " + -" or not (roundtrip).matched " + +" test_failures AS ( " + +" SELECT * " + +" FROM test_outputs " + +" WHERE " + +" cparray != (roundtrip).cparray OR s != (roundtrip).s " + +" OR NOT (roundtrip).matched " + " ), " + -" test_summary ( n_failing_groups, first_failing_group ) as ( " + -" select count(*), min(groupnum) from test_failures " + +" test_summary ( n_failing_groups, first_failing_group ) AS ( " + +" SELECT count(*), min(groupnum) FROM test_failures " + " ) " + -" select " + -" case when n_failing_groups > 0 then " + +" SELECT " + +" CASE WHEN n_failing_groups > 0 THEN " + " javatest.logmessage('WARNING', n_failing_groups || " + " ' 1k codepoint ranges had mismatches, first is block starting 0x' || " + " to_hex(1024 * first_failing_group)) " + -" else " + +" ELSE " + " javatest.logmessage('INFO', " + " 'all Unicode codepoint ranges roundtripped successfully.') " + -" end " + -" from test_summary" -) -@SQLAction( - install= - "CREATE TYPE unicodetestrow AS " + - "(matched boolean, cparray integer[], s text)", - remove="DROP TYPE unicodetestrow", - provides="unicodetestrow type" +" END " + +" FROM test_summary" ) public class UnicodeRoundTripTest { /** @@ -110,8 +103,8 @@ public class UnicodeRoundTripTest { * @param rs OUT (matched, cparray, s) as described above * @return true to indicate the OUT tuple is not null */ - @Function(type="unicodetestrow", - requires="unicodetestrow type", provides="unicodetest fn") + @Function(out={"matched boolean", "cparray integer[]", "s text"}, + provides="unicodetest fn") public static boolean unicodetest(String s, int[] ints, ResultSet rs) throws SQLException { boolean ok = true; diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/XMLRenderedTypes.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/XMLRenderedTypes.java index 5cd21326f..812233d15 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/XMLRenderedTypes.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/XMLRenderedTypes.java @@ -28,9 +28,22 @@ * Everything mentioning the type XML here needs a conditional implementor tag * in case of being loaded into a PostgreSQL instance built without that type. */ +@SQLAction(implementor="postgresql_xml", requires="pgNodeTreeAsXML", install= +"WITH" + +" a(t) AS (SELECT adbin FROM pg_catalog.pg_attrdef LIMIT 1)" + +" SELECT" + +" CASE WHEN pgNodeTreeAsXML(t) IS DOCUMENT" + +" THEN javatest.logmessage('INFO', 'pgNodeTreeAsXML ok')" + +" ELSE javatest.logmessage('WARNING', 'pgNodeTreeAsXML ng')" + +" END" + +" FROM a" +) public class XMLRenderedTypes { - @Function(schema="javatest", implementor="postgresql_xml") + @Function( + schema="javatest", implementor="postgresql_xml", + provides="pgNodeTreeAsXML" + ) public static SQLXML pgNodeTreeAsXML(@SQLType("pg_node_tree") SQLXML pgt) throws SQLException { From c107f9ae0344446acdb8f96a3b753d22efab144c Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Thu, 24 Aug 2023 16:31:25 -0400 Subject: [PATCH 132/334] Add a set-returning function example When a Portal allocates a TupleTableSlot, it had better do so in a context as enduring as the Portal itself, and not blindly in the current context, which is likely the SPI context that will be reset for each call in the SFRM_ValuePerCall protocol. For now, just allocate it in the Portal's portalContext directly. This is arguably a little incestuous, but the fewest lines of code to change at the moment to get it working (as MemoryContextImpl.java doesn't yet expose any methods for creating and managing a new context), and it has the right lifespan. That is, it has the right lifespan unless a holdable portal is created, and persisted; then stuff should be in its holdContext (or some other context at least as long-lived). So everything is still definitely stopgap in this commit. TupleTableSlotImpl still isn't aware of its memory context or lifespan, even. --- .../annotation/TupleTableSlotTest.java | 145 ++++++++++++++++++ pljava-so/src/main/c/type/Portal.c | 8 +- .../postgresql/pljava/internal/Portal.java | 18 ++- 3 files changed, 166 insertions(+), 5 deletions(-) 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 index 0a823b21a..9448868ef 100644 --- 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 @@ -22,6 +22,7 @@ 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; @@ -37,11 +38,14 @@ 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.model.Attribute; import org.postgresql.pljava.model.Portal; @@ -446,6 +450,147 @@ public Optional adapt(Attribute a, T value) } } + /** + * 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 + ) + 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. diff --git a/pljava-so/src/main/c/type/Portal.c b/pljava-so/src/main/c/type/Portal.c index bfcbdd449..d1bcbbba1 100644 --- a/pljava-so/src/main/c/type/Portal.c +++ b/pljava-so/src/main/c/type/Portal.c @@ -47,6 +47,7 @@ jobject pljava_Portal_create(Portal portal, jobject jplan) jobject jportal; Ptr2Long p2l; Ptr2Long p2lro; + Ptr2Long p2lcxt; if(portal == 0) return NULL; @@ -56,8 +57,11 @@ jobject pljava_Portal_create(Portal portal, jobject jplan) p2lro.longVal = 0L; p2lro.ptrVal = portal->resowner; + p2lcxt.longVal = 0L; + p2lcxt.ptrVal = portal->portalContext; + jportal = JNI_newObjectLocked(s_Portal_class, s_Portal_init, - p2lro.longVal, p2l.longVal, jplan); + p2lro.longVal, p2lcxt.longVal, p2l.longVal, jplan); return jportal; } @@ -124,7 +128,7 @@ 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, "", - "(JJLorg/postgresql/pljava/internal/ExecutionPlan;)V"); + "(JJJLorg/postgresql/pljava/internal/ExecutionPlan;)V"); /* * Statically assert that the Java code has the right values for these. 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 0b128fdef..91bb3530e 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/Portal.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/Portal.java @@ -18,9 +18,12 @@ 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; @@ -49,6 +52,8 @@ public class Portal implements org.postgresql.pljava.model.Portal private final State m_state; + 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; @@ -63,9 +68,10 @@ public class Portal implements org.postgresql.pljava.model.Portal assert FETCH_RELATIVE == Direction.RELATIVE.ordinal(); } - Portal(long ro, long pointer, ExecutionPlan plan) + Portal(long ro, long cxt, long pointer, ExecutionPlan plan) { m_state = new State(this, ResourceOwnerImpl.fromAddress(ro), pointer); + m_context = MemoryContextImpl.fromAddress(cxt); m_plan = plan; } @@ -143,8 +149,14 @@ private TupleTableSlotImpl slot() throws SQLException { assert threadMayEnterPG(); // only call slot() on PG thread if ( null == m_slot ) - m_slot = _makeTupleTableSlot( - m_state.getPortalPtr(), tupleDescriptor()); + { + try ( Checked.AutoCloseable ac = + allocatingIn(m_context) ) + { + m_slot = _makeTupleTableSlot( + m_state.getPortalPtr(), tupleDescriptor()); + } + } return m_slot; } From 59c0ba9e9bf3d7611c27d72f52a8084910ae7699 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Thu, 24 Aug 2023 17:20:40 -0400 Subject: [PATCH 133/334] Upstream has reverted MAINTAIN privilege postgres/postgres@151c22d That's what I get for trying to stay ahead of things. In other news, they made ai_privs wider: postgres/postgres@7b37823 In passing, fix some whitespace, and better comment a use of to().name() where you'd expect to().nameAsGrantee(). Because unexpected ai_privs width will now be detected only by an assertion at runtime, make sure there is a regression test that will make that run. --- .../pljava/model/CatalogObject.java | 12 +--- .../annotation/TupleTableSlotTest.java | 34 ++++++++- pljava-so/src/main/c/ModelConstants.c | 7 -- .../org/postgresql/pljava/pg/AclItem.java | 70 ++++++++++--------- 4 files changed, 70 insertions(+), 53 deletions(-) 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 index bd58132aa..514d5b6aa 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/model/CatalogObject.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/model/CatalogObject.java @@ -280,8 +280,7 @@ 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, MAINTAIN { } + interface OnClass extends OnAttribute, DELETE, TRUNCATE, TRIGGER { } /** * Subtype of {@code Grant} representing the privileges that may be @@ -439,15 +438,6 @@ interface ALTER_SYSTEM extends Grant boolean alterSystemGrantable(); } - /** - * @hidden - */ - interface MAINTAIN extends Grant - { - boolean maintainGranted(); - boolean maintainGrantable(); - } - /** * @hidden */ 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 index 9448868ef..9333ef67f 100644 --- 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 @@ -46,6 +46,7 @@ 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; @@ -59,6 +60,37 @@ * 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 pg_catalog.text ), relacl' ||" + +" ' FROM' ||" + +" ' pg_catalog.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, pg_catalog.translate(cooked, '[] ', '{}') AS cooked" + +" FROM" + +" result" + +" )" + +" SELECT" + +" CASE WHEN pg_catalog.every(raw = cooked )" + +" THEN javatest.logmessage('INFO', 'AclItem[] ok')" + +" ELSE javatest.logmessage('WARNING', 'AclItem[] ng')" + +" END" + +" FROM" + +" conformed" +) public class TupleTableSlotTest { /* @@ -492,7 +524,7 @@ public T adapt(Attribute a, T value) */ @Function( schema = "javatest", type = "pg_catalog.record", variadic = true, - onNullInput = RETURNS_NULL + onNullInput = RETURNS_NULL, provides = "modelToJDBC" ) public static ResultSetProvider modelToJDBC(String query, String[] adapters) throws SQLException, ReflectiveOperationException diff --git a/pljava-so/src/main/c/ModelConstants.c b/pljava-so/src/main/c/ModelConstants.c index fdb2db5e0..cb14c5d80 100644 --- a/pljava-so/src/main/c/ModelConstants.c +++ b/pljava-so/src/main/c/ModelConstants.c @@ -297,9 +297,6 @@ StaticAssertStmt((c) == \ #if PG_VERSION_NUM >= 150000 CONFIRMCONST( ACL_SET ); CONFIRMCONST( ACL_ALTER_SYSTEM); -#endif -#if PG_VERSION_NUM >= 160000 - CONFIRMCONST( ACL_MAINTAIN ); #endif CONFIRMCONST( ACL_ID_PUBLIC ); @@ -312,10 +309,6 @@ StaticAssertStmt(offsetof(typ,fld) == \ CONFIRMOFFSET( AclItem, ai_grantor ); CONFIRMOFFSET( AclItem, ai_privs ); - StaticAssertStmt( - sizeof(AclItem) == org_postgresql_pljava_pg_AclItem_SIZEOF_AclItem, - "Java/C size mismatch for AclItem"); - #undef CONFIRMCONST #undef CONFIRMOFFSET diff --git a/pljava/src/main/java/org/postgresql/pljava/pg/AclItem.java b/pljava/src/main/java/org/postgresql/pljava/pg/AclItem.java index e069ff97f..1cbb9c653 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/AclItem.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/AclItem.java @@ -27,38 +27,34 @@ 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_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_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; - // below appearing in PG 16 - @Native static final short ACL_MAINTAIN = 1 << 14; - - @Native static final int N_ACL_RIGHTS = 12; @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; - @Native static final int SIZEOF_AclItem = 12; + @Native static final int OFFSET_ai_privs = 8; /** * These one-letter abbreviations are to match the order of the bit masks @@ -71,13 +67,13 @@ public abstract class AclItem implements CatalogObject.Grant * It can also be found as {@code ACL_ALL_RIGHTS_STR} in * {@code include/utils/acl.h}. */ - private static final String s_abbr = "arwdDxtXUCTcsAm"; + 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, and between 15 and 16). So the string should + * (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. * @@ -121,23 +117,34 @@ public static class NonRole extends AclItem OnClass, OnNamespace, OnSetting, CatalogObject.EXECUTE, CatalogObject.CREATE_TEMP, CatalogObject.CONNECT { - private final short m_priv; - private final short m_goption; + 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)); - int privs = b.getInt(OFFSET_ai_privs); - m_priv = (short)(privs & 0xffff); - m_goption = (short)(privs >>> 16); + + 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(short mask) + private boolean priv(int mask) { return 0 != (m_priv & mask); } - private boolean goption(short mask) + private boolean goption(int mask) { return 0 != (m_goption & mask); } @@ -146,11 +153,16 @@ private boolean goption(short mask) public String toString() { StringBuilder sb = new StringBuilder(); - if ( to().isValid() ) + /* + * 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 = Short.toUnsignedInt(m_priv); - int goption = Short.toUnsignedInt(m_goption); + int priv = m_priv; + int goption = m_goption; while ( 0 != priv ) { int bit = lowestOneBit(priv); @@ -302,15 +314,5 @@ public String toString() { return goption(ACL_ALTER_SYSTEM); } - - @Override public boolean maintainGranted() - { - return priv(ACL_MAINTAIN); - } - - @Override public boolean maintainGrantable() - { - return goption(ACL_MAINTAIN); - } } } From 57e9609b538c611089c6f2d29e0074ea56560d35 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Thu, 24 Aug 2023 18:02:55 -0400 Subject: [PATCH 134/334] Expose the lax() method in the example code A couple of the supplied examples strive to be usable for real work, so this new method should be available there too. --- .../org/postgresql/pljava/example/annotation/PassXML.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 3c0605759..f2a706585 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 @@ -809,7 +809,9 @@ T applyAdjustments(ResultSet adjust, T axp) for ( int i = 1; i <= n; ++i ) { String k = rsmd.getColumnLabel(i); - if ( "allowDTD".equalsIgnoreCase(k) ) + if ( "lax".equalsIgnoreCase(k) ) + axp.lax(adjust.getBoolean(i)); + else if ( "allowDTD".equalsIgnoreCase(k) ) axp.allowDTD(adjust.getBoolean(i)); else if ( "externalGeneralEntities".equalsIgnoreCase(k) ) axp.externalGeneralEntities(adjust.getBoolean(i)); From e25b7c40bec6f0caa58f9267a76ccb689b5fff96 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Thu, 24 Aug 2023 18:32:00 -0400 Subject: [PATCH 135/334] Clean up a bogus javadoc comment That was one I must have written on far too little coffee. Hardly any of it bore any connection to reality. Ah. In my defense, it had been more coherent once, but did not get updated with c9f6a20. --- .../postgresql/pljava/internal/VarlenaWrapper.java | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) 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..44432000b 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/VarlenaWrapper.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/VarlenaWrapper.java @@ -1182,12 +1182,11 @@ public Thread newThread(Runnable r) * wraps a {@code ByteBuffer} and the {@link Output.State Output.State} * that protects it. *

    - * {@code BufferWrapper} installs itself as the inherited - * {@code m_state} field, so {@code ByteBufferInputStream}'s methods - * synchronize on it rather than the {@code State} object, for no - * interference with the writing thread. The {@code pin} and - * {@code unpin} methods, of course, forward to those of the - * native state object. + * {@code BufferWrapper} installs itself as the + * {@code ByteBufferInputStream}'s lock object, so its methods + * synchronize on this rather than anything that would interfere with + * the writing thread. The {@code pin} and {@code unpin} methods, + * of course, forward to those of the native state object. */ static class BufferWrapper extends ByteBufferInputStream From 8a88cbf40b6cf0b49e98a05d8754f7364b038356 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Fri, 25 Aug 2023 11:30:00 -0400 Subject: [PATCH 136/334] Beat down unchecked warnings from Datum migration These were not caught in 1fc0be2. --- .../main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java | 9 ++++----- .../java/org/postgresql/pljava/pg/adt/XMLAdapter.java | 9 ++++++--- 2 files changed, 10 insertions(+), 8 deletions(-) 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 3b6e28934..6589c9bd7 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/SQLXMLImpl.java @@ -335,17 +335,16 @@ static SQLException normalizedException(Exception e) * 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) + public static SQLXML newReadable( + Datum.Input datum, RegType pgType, boolean synthetic) throws SQLException { - Datum.Input di = (Datum.Input)datum; int oid = pgType.oid(); if ( synthetic ) - return new Readable.Synthetic(di, oid); + return new Readable.Synthetic(datum, oid); - return new Readable.PgXML(di, oid); + return new Readable.PgXML<>(datum, oid); } /** 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 index aafccaab3..7899f00e5 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/adt/XMLAdapter.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/adt/XMLAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Tada AB and other contributors, as listed below. + * 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 @@ -11,6 +11,7 @@ */ package org.postgresql.pljava.pg.adt; +import java.io.InputStream; import java.io.IOException; import java.security.AccessController; @@ -65,7 +66,8 @@ public boolean canFetch(RegType pgType) || RegType.TEXT == pgType; } - public SQLXML fetch(Attribute a, Datum.Input in) + public + SQLXML fetch(Attribute a, Datum.Input in) throws SQLException, IOException { return SQLXMLImpl.newReadable(in, a.type(), false); @@ -94,7 +96,8 @@ public boolean canFetch(RegType pgType) } @Override - public SQLXML fetch(Attribute a, Datum.Input in) + public + SQLXML fetch(Attribute a, Datum.Input in) throws SQLException, IOException { return SQLXMLImpl.newReadable(in, a.type(), true); From 0391e18b20b1b30fb794a7d39c78d10a7c717fb5 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 26 Aug 2023 13:51:56 -0400 Subject: [PATCH 137/334] Some more missing Adapters --- .../pljava/pg/adt/ByteaAdapter.java | 112 +++++++++++++++++ .../pljava/pg/adt/MoneyAdapter.java | 82 +++++++++++++ .../pljava/pg/adt/NumericAdapter.java | 115 ++++++++++++++++++ 3 files changed, 309 insertions(+) create mode 100644 pljava/src/main/java/org/postgresql/pljava/pg/adt/ByteaAdapter.java create mode 100644 pljava/src/main/java/org/postgresql/pljava/pg/adt/MoneyAdapter.java create mode 100644 pljava/src/main/java/org/postgresql/pljava/pg/adt/NumericAdapter.java 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/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/NumericAdapter.java b/pljava/src/main/java/org/postgresql/pljava/pg/adt/NumericAdapter.java new file mode 100644 index 000000000..05a8c9a94 --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/pg/adt/NumericAdapter.java @@ -0,0 +1,115 @@ +/* + * 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.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.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 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(); + } + } +} From 45f965c8910e040efb6a5dee84886519f48fde56 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 26 Aug 2023 14:08:27 -0400 Subject: [PATCH 138/334] A reference NUMERIC -> BigDecimal implementation BigDecimal is nearly a fit, as things in the standard Java library go, but it cannot handle NaN or +/- infinity. At any rate, even if this reference implementation of a contract isn't complete with respect to those values, it could easily be wrapped by another contract that could return a subclass of BigDecimal, or a BigDecimal-or-Something ... or some other type that is easily constructed from a BigDecimal. Because PostgreSQL's numeric.c contains the magic numbers, and they aren't exposed in .h files, a regression test is critical to making sure these bits don't rot. In passing, relax the obsessive schema-qualification in TupleTableSlotTest's SQLActions. These are (a) examples, and (b) when run as CI tests, running in a fresh installation, and not all other examples are obsessively schema-qualified, and this one didn't have the operators qualified anyway, and doing *that* makes an example just plain hard to read. # Conflicts: # pljava/src/main/java/org/postgresql/pljava/pg/adt/NumericAdapter.java --- .../org/postgresql/pljava/adt/Numeric.java | 221 +++++++++++++++++- .../annotation/TupleTableSlotTest.java | 34 ++- .../pljava/pg/adt/NumericAdapter.java | 6 + 3 files changed, 256 insertions(+), 5 deletions(-) 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 index 0b2ccfdc5..538e5fa2c 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/adt/Numeric.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/adt/Numeric.java @@ -11,6 +11,13 @@ */ 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; /** @@ -83,6 +90,21 @@ public interface Numeric extends Contract.Scalar */ 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. @@ -92,6 +114,81 @@ 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 @@ -111,7 +208,7 @@ enum Kind { POSITIVE, NEGATIVE, NAN, POSINFINITY, NEGINFINITY } * unshared and may be used as desired. */ T construct(Kind kind, int displayScale, int weight, - short[] base10000Digits); + short[] base10000Digits) throws SQLException; /** * Functional interface to obtain information from the PostgreSQL type @@ -138,4 +235,126 @@ interface Modifier */ 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, thought, 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-examples/src/main/java/org/postgresql/pljava/example/annotation/TupleTableSlotTest.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/TupleTableSlotTest.java index 9333ef67f..9d07fb192 100644 --- 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 @@ -68,9 +68,9 @@ " FROM" + " javatest.modelToJDBC(" + " 'SELECT DISTINCT' ||" + -" ' CAST ( relacl AS pg_catalog.text ), relacl' ||" + +" ' CAST ( relacl AS text ), relacl' ||" + " ' FROM' ||" + -" ' pg_catalog.pg_class' ||" + +" ' pg_class' ||" + " ' WHERE' ||" + " ' relacl IS NOT NULL'," + " 'org.postgresql.pljava.pg.adt.TextAdapter', 'INSTANCE'," + @@ -79,18 +79,44 @@ " )," + " conformed AS (" + " SELECT" + -" raw, pg_catalog.translate(cooked, '[] ', '{}') AS cooked" + +" raw, translate(cooked, '[] ', '{}') AS cooked" + " FROM" + " result" + " )" + " SELECT" + -" CASE WHEN pg_catalog.every(raw = cooked )" + +" 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 { /* 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 index 05a8c9a94..72ebc8773 100644 --- a/pljava/src/main/java/org/postgresql/pljava/pg/adt/NumericAdapter.java +++ b/pljava/src/main/java/org/postgresql/pljava/pg/adt/NumericAdapter.java @@ -13,6 +13,8 @@ import java.io.IOException; +import java.math.BigDecimal; + import java.nio.ShortBuffer; import static java.nio.ByteOrder.nativeOrder; @@ -24,6 +26,7 @@ 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; @@ -42,6 +45,9 @@ public class NumericAdapter extends Adapter.As (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); From 8353f6fe8eef1b55b9569200c0b3d19303afc346 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Sat, 26 Aug 2023 14:33:05 -0400 Subject: [PATCH 139/334] Ax ancient Java version in PL/Java 1.6.x docs As PL/Java 1.6.x requires Java >= 9, it makes little sense for a lingering javadoc comment to say you need Java 1.6 for something. --- .../postgresql/pljava/annotation/package-info.java | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/pljava-api/src/main/java/org/postgresql/pljava/annotation/package-info.java b/pljava-api/src/main/java/org/postgresql/pljava/annotation/package-info.java index de7309e5b..ca5af21cf 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/annotation/package-info.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/annotation/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015-2016 Tada AB and other contributors, as listed below. + * Copyright (c) 2015-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 @@ -33,11 +33,10 @@ *

    * Automatic descriptor generation requires attention to a few things. *