diff --git a/pljava-api/src/main/java/org/postgresql/pljava/fdw/FDWExplainState.java b/pljava-api/src/main/java/org/postgresql/pljava/fdw/FDWExplainState.java new file mode 100644 index 000000000..db1fbbd1a --- /dev/null +++ b/pljava-api/src/main/java/org/postgresql/pljava/fdw/FDWExplainState.java @@ -0,0 +1,9 @@ +package org.postgresql.pljava.fdw; + +/** + * Additional content for `EXPLAIN x...` + * + * No details yet. + */ +public interface FDWExplainState { +} diff --git a/pljava-api/src/main/java/org/postgresql/pljava/fdw/FDWForeignDataWrapper.java b/pljava-api/src/main/java/org/postgresql/pljava/fdw/FDWForeignDataWrapper.java new file mode 100644 index 000000000..97a26efe3 --- /dev/null +++ b/pljava-api/src/main/java/org/postgresql/pljava/fdw/FDWForeignDataWrapper.java @@ -0,0 +1,39 @@ +package org.postgresql.pljava.fdw; + +import java.util.Collections; +import java.util.Map; + +/** + * The Foreign Data Wrapper. + * + * This is the highest-level abstraction, e.g., for information + * contained in S3 files. + * + * It could also capture an abstract concept, e.g., one FDW + * to capture multiple authentication implementations. + * + * There may be multiple instances of a single FOREIGN DATA WRAPPER. + */ +public interface FDWForeignDataWrapper { + + /** + * The instances unique ID. It should be used to maintain a cache. + * @return + */ + default Long getId() { return null; } + + /** + * Return a copy of the options provided to `CREATE FOREIGN DATA WRAPPER...` + * @return + */ + default Map getOptions() { return Collections.emptyMap(); }; + + /** + * Validate a set of options against an existing instance. There should be + * a similar static method before creating a new instance. + * + * @param options + * @return + */ + default boolean validateOptions(Map options) { return true; }; +} diff --git a/pljava-api/src/main/java/org/postgresql/pljava/fdw/FDWForeignTable.java b/pljava-api/src/main/java/org/postgresql/pljava/fdw/FDWForeignTable.java new file mode 100644 index 000000000..9cdfe3d5c --- /dev/null +++ b/pljava-api/src/main/java/org/postgresql/pljava/fdw/FDWForeignTable.java @@ -0,0 +1,117 @@ +package org.postgresql.pljava.fdw; + +import java.sql.ResultSetMetaData; +import java.util.Collections; +import java.util.Map; + +/** + * The Foreign Table + * + * This is the lowest-level abstraction, e.g., a specific + * S3 file. + * + * There may be multiple instances of a Foreign Table + * for a single Foreign Server. + */ +public interface FDWForeignTable { + + /** + * The instances unique ID. It should be used to maintain a cache. + */ + default Long getId() { return null; } + + /** + * Return a copy of the options provided to `CREATE FOREIGN TABLE...` + * @return + */ + default Map getOptions() { return Collections.emptyMap(); }; + + /** + * Validate a set of options against an existing instance. There should be + * a similar static method before creating a new instance. + * + * @param options + * @return + */ + default boolean validateOptions(Map options) { return true; }; + + /** + * Create an object used for query planning. + * + * @param user + * @return + */ + FDWPlanState newPlanState(FDWUser user); + + /** + * Create an object used for SELECT statements. + * @param user + * @return + */ + FDWScanState newScanState(FDWUser user, boolean explainOnly); + + // values from PlannerInfo *root, RelOptInfo *baserel + // BUT NOT foreigntableoid + void getRelSize(); + +/* + static void blackholeGetForeignPaths(PlannerInfo *root, + RelOptInfo *baserel, + Oid foreigntableid); +*/ + + // values from PlannerInfo *root, RelOptInfo *baserel + // BUT NOT foreigntableoid + void getForeignPaths(); + + /** + * Is this table updatable by this user? + * + * @param user + * @return + */ + default boolean isUupdatable(FDWUser user) { return false; } + + /** + * Does this table support concurrent access? + * @return + */ + default boolean supportsConcurrency() { return false; } + + /** + * Does this table support asynchronous queries? + * @return + */ + default boolean supportsAsyncOperations() { return false; } + + /** + * Collect statistics used by the query optimizer. + * This can be supported for read-only tables. + * + * Details TBD + */ + default void analyze() { } + + /** + * Compact the data, if appropriate. + * This should be a noop for read-only tables. + * + * Details TBD. + */ + default void vacuum() { } + + /** + * Get the table's schema. This information + * will be used when executing `IMPORT FOREIGN SCHEMA...` + * + * @return + */ + default ResultSetMetaData getMetaData() { return null; } + + /** + * Estimate the number of rows. + * + * @return + */ + default long getRows() { return 0; } +} diff --git a/pljava-api/src/main/java/org/postgresql/pljava/fdw/FDWPlanState.java b/pljava-api/src/main/java/org/postgresql/pljava/fdw/FDWPlanState.java new file mode 100644 index 000000000..6ffbee457 --- /dev/null +++ b/pljava-api/src/main/java/org/postgresql/pljava/fdw/FDWPlanState.java @@ -0,0 +1,14 @@ +package org.postgresql.pljava.fdw; + +public interface FDWPlanState { + + // values from PlannerInfo *root, RelOptInfo *baserel. + // the PlannerInfo is only used in advanced queries. + void open(); + + void close(); + + default long getRows() { return 0; } + + default FDWUser getUser() { return null; } +} diff --git a/pljava-api/src/main/java/org/postgresql/pljava/fdw/FDWScanState.java b/pljava-api/src/main/java/org/postgresql/pljava/fdw/FDWScanState.java new file mode 100644 index 000000000..cd0dfaffd --- /dev/null +++ b/pljava-api/src/main/java/org/postgresql/pljava/fdw/FDWScanState.java @@ -0,0 +1,55 @@ +package org.postgresql.pljava.fdw; + +import java.util.Map; + +public interface FDWScanState { + /** + * The database has already performed an initial check for + * the user's permission to execute SELECT - but our java + * code may impose additional requirements. + * + * The minimal implementation just adds the ability to check + * whether * the user is authorized. (The current FDWUser is + * transparently * passed to the appropriate method.) + * + * A more advanced implementation would allow us to + * add row and column filtering beyond what will already + * be done by the database. + */ + default boolean isAuthorizedUser() { return true; } + + /** + * Verify that we have a valid configuration. + * + * No external resources should be accessed if + * the `explainOnly` flag is true. (It's okay to + * check a file exists and is readable but it should + * not be opened. A REST service can have its hostname + * verified but it should not be called. + * + * If the `explainOnly` flag is false than external + * resources can be accessed in order to verify + * that it's a valid URL and we have valid credentials. + * However all external resources should be released + * before this method exits. + */ + void open(boolean explainOnly); + + // values from TableTupleType. It is an element + // of the ForeignScanState mentioned above. + Map next(); + + /** + * Reset scan to initial state. + */ + void reset(); + + /** + * Release resources + */ + void close(); + + default FDWExplainState explain() { return null; } + + default FDWUser getUser() { return null; } +} diff --git a/pljava-api/src/main/java/org/postgresql/pljava/fdw/FDWServer.java b/pljava-api/src/main/java/org/postgresql/pljava/fdw/FDWServer.java new file mode 100644 index 000000000..70aa4b754 --- /dev/null +++ b/pljava-api/src/main/java/org/postgresql/pljava/fdw/FDWServer.java @@ -0,0 +1,50 @@ +package org.postgresql.pljava.fdw; + +import java.sql.DatabaseMetaData; +import java.util.Collections; +import java.util.Map; + +/** + * The Foreign Server + * + * This is the middle-level abstraction, e.g., a specific + * AWS account with access to the required resources. + * + * There may be multiple instances of a Foreign Server + * for a single Foreign Data Wrapper. + */ +public interface FDWServer { + + /** + * The instances unique ID. It should be used to maintain a cache. + */ + default Long getId() { return null; } + + /** + * Return a copy of the options provided to `CREATE FOREIGN SERVER...` + * @return + */ + default Map getOptions() { return Collections.emptyMap(); }; + + /** + * Validate a set of options against an existing instance. There should be + * a similar static method before creating a new instance. + * + * @param options + * @return + */ + default boolean validateOptions(Map options) { return true; }; + + /** + * Get the server's entire schema. This can be useful + * information even if the backend only gets the + * schema for individual tables. + * + * (It's not clear since the backend struct supports + * foreign keys but I don't think the individual + * ResultSetMetadata includes that information.) + * + * @return + */ + default DatabaseMetaData getMetaData() { return null; } +} diff --git a/pljava-api/src/main/java/org/postgresql/pljava/fdw/FDWUser.java b/pljava-api/src/main/java/org/postgresql/pljava/fdw/FDWUser.java new file mode 100644 index 000000000..a8c453a8e --- /dev/null +++ b/pljava-api/src/main/java/org/postgresql/pljava/fdw/FDWUser.java @@ -0,0 +1,118 @@ +package org.postgresql.pljava.fdw; + +import java.security.cert.Certificate; + +/** + * Placeholder for user information - it should use the + * existing pl/java class. + * + * The effective and real usernames can be retrieved from + * the OIDs. + * + * For sensitive material the java classes can implement + * additional AuthN and AuthZ functionality on their own. + * In this case it's also common for the AuthN and AuthZ + * to consider both how the user was authenticated and + * their apparent physical location. + * + * Some of this information is available if you have a + * JDBC Connection... but the whole point of this module + * is to hide the fact that this code is used by a database. + * It will be much harder to break the database if the java + * classes have absolutely no access to the connection, or + * even any awareness of its existence. + * + * Kerberos notes: + * + * - The java code should have access to the user's + * Kerberos principal. I know the principal -> username + * mapping is in pg_ident.conf but the java code should + * not rely on it. + * + * - Reminder that Kerberos provides secure authentication + * but it does not provide secure transport. You must explicitly + * add TLS for an encrypted connection. This is often + * overlooked by people unfamiliar with Kerberos and is + * why we need an explicit check for a secure connection. + */ +public interface FDWUser { // also implement Principal ?? + + // is there an actual default user?... + String DEFAULT_USER = "unknown_user"; + + enum AuthenticationMechanism { + TRUST, + REJECT, + MD5, + PASSWORD, + SCRAM_SHA_256, + GSS, // Kerberos + SSPI, + IDENT, + PEER, + PAM, + LDAP, + RADIUS, + CERT, + UNKNOWN + } + + /** + * The user's unique ID. It can be used to maintain a cache. + */ + default Long getOid() { return null; } + + /** + * The real user's unique ID. It can be used to maintain a cache. + */ + default Long getRealOid() { return null; } + + /** + * Get the effective database username. + * @return + */ + default String getUsername() { + return getRealUsername(); + }; + + /** + * Get the real database username. + * @return + */ + default String getRealUsername() { + return DEFAULT_USER; + } + + /** + * Is the connection secure? + * + * This may be superfluous since this information is + * already available via the `DatabaseMetaData` + * connection information. However I can't rule out + * the possibility of a desire to have more details + * about the connection, e.g., the algorithm used, + * the keysize, etc. + */ + default boolean isConnectionSecure() + { + return false; + } + + /** + * Get the authentication mechanism used. + */ + default AuthenticationMechanism getAuthenticationMechanism() + { + return AuthenticationMechanism.UNKNOWN; + } + + /** + * Get the user's location. (named socket, IP address + port) + */ + default Object getLocation() { return null; } + + /** + * Get the user's Certificate, if `cert` authentication was used. + */ + default Certificate getCertificate() { return null; } +} diff --git a/pljava-api/src/main/java/org/postgresql/pljava/fdw/FDWValidator.java b/pljava-api/src/main/java/org/postgresql/pljava/fdw/FDWValidator.java new file mode 100644 index 000000000..0aea55966 --- /dev/null +++ b/pljava-api/src/main/java/org/postgresql/pljava/fdw/FDWValidator.java @@ -0,0 +1,59 @@ +package org.postgresql.pljava.fdw; + +import java.util.Collections; +import java.util.Map; + +/** + * The Validator + * + * This class is used to validate the options provided to + * the FDWForeignDataWrapper, FDWForeignServer, and FDWForeignTable + * constructors. + * + * Note: the Foreign Data Wrapper or Foreign Server may already + * exist. If so they will be provided a copy of the appropriate + * options. + */ +public interface FDWValidator { + enum Scope { + FOREIGN_DATA_WRAPPER, + FOREIGN_SERVER, + FOREIGN_TABLE + // plus two others... + }; + + /** + * Add an option + * + * @param scope + * @param key + * @param value + */ + default void addOption(Scope scope, String key, String value) { } + + /** + * Get options + */ + default Map getOptions(Scope scope) + { + return Collections.emptyMap(); + } + + /** + * Validate all options. + * + * This method should create any missing objects and add + * them to an internal cache. + * + * @TODO - should the return value indicate where the validation + * failed? FDW, SERVER, TABLE, bad property or unable to create + * object? + * + * @param fdwId if for existing ForeignDataWrapper, or null + * @param srvId if for existing Server, or null + * @param ftId if for existing Foreign Table, or null + * + * @return true if successfully validated + */ + default boolean validate(long fdwId, Long srvId, Long ftId) { return true; } +} diff --git a/pljava-examples/pom.xml b/pljava-examples/pom.xml index bc245281f..cd338df30 100644 --- a/pljava-examples/pom.xml +++ b/pljava-examples/pom.xml @@ -42,7 +42,13 @@ pljava-api ${project.version} - + + commons-collections + commons-collections + 3.2.2 + compile + + diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/fdw/BlackholeForeignDataWrapper.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/fdw/BlackholeForeignDataWrapper.java new file mode 100644 index 000000000..f1e8bf44a --- /dev/null +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/fdw/BlackholeForeignDataWrapper.java @@ -0,0 +1,32 @@ +package org.postgresql.pljava.example.fdw; + +import org.postgresql.pljava.fdw.FDWForeignDataWrapper; +import org.postgresql.pljava.fdw.FDWServer; + +import java.util.Collections; +import java.util.Map; +import java.util.logging.Logger; + +/** + * A ForeignDataWrapper. (Persistent) + * + * Note: a single ForeignDataWrapper may contain multiple servers + * so there should be caching somewhere. + */ +public class BlackholeForeignDataWrapper implements FDWForeignDataWrapper { + private static final Logger LOG = Logger.getLogger(BlackholeForeignDataWrapper.class.getName()); + + public BlackholeForeignDataWrapper() { + this(Collections.emptyMap()); + } + + public BlackholeForeignDataWrapper(Map options) { + LOG.info("constructor"); + } + + @Override + public FDWServer getServer() { + LOG.info("getServer()"); + return new BlackholeServer(); + } +} diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/fdw/BlackholeForeignTable.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/fdw/BlackholeForeignTable.java new file mode 100644 index 000000000..da8cc5fc2 --- /dev/null +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/fdw/BlackholeForeignTable.java @@ -0,0 +1,75 @@ +package org.postgresql.pljava.example.fdw; + +import org.postgresql.pljava.fdw.FDWForeignDataWrapper; +import org.postgresql.pljava.fdw.FDWPlanState; +import org.postgresql.pljava.fdw.FDWScanState; +import org.postgresql.pljava.fdw.FDWForeignTable; +import org.postgresql.pljava.fdw.FDWUser; + +import java.sql.ResultSetMetaData; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.logging.Logger; + +/** + * A Foreign Table (persistent) + * + * A single ForeignTable may have multiple PlanStates and ScanStates + * however they are transient and unlikely to be reused. + */ +public class BlackholeForeignTable implements FDWForeignTable { + private static final Logger LOG = Logger.getLogger(BlackholeForeignTable.class.getName()); + + private final List> dummyTable = new ArrayList<>(); + + public BlackholeForeignTable() { + this(Collections.emptyMap()); + } + + public BlackholeForeignTable(Map options) { + LOG.info("constructor"); + } + + @Override + public void getRelSize() { + } + + /* + public void blackholeGetForeignPaths(PlannerInfo *root, + RelOptInfo *baserel, + Oid foreigntableid) { + } + */ + + @Override + public FDWPlanState newPlanState(FDWUser user) { + return null; + } + + @Override + public FDWScanState newScanState(FDWUser user, boolean explainOnly) { + return null; + } + + /* + @Override + public boolean isUpdatable(FDWUser user); + + @Override + public boolean supportsConcurrency(); + + @Override + public boolean supportsAsyncOperation(); + + @Override + public void analyze(); + + @Override + public void vacuum(); + + @Override + public ResultSetMetaData getMetaData(); + */ +} diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/fdw/BlackholePlanState.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/fdw/BlackholePlanState.java new file mode 100644 index 000000000..865912759 --- /dev/null +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/fdw/BlackholePlanState.java @@ -0,0 +1,29 @@ +package org.postgresql.pljava.example.fdw; + +import org.postgresql.pljava.fdw.FDWPlanState; + +import java.util.logging.Logger; + +/** + * A ForeignTable plan state. (Temporary) + */ +public class BlackholePlanState implements FDWPlanState { + private static final Logger LOG = Logger.getLogger(BlackholePlanState.class.getName()); + + private final BlackholeForeignTable table; + + public BlackholePlanState(BlackholeForeignTable table) { + LOG.info("constructor()"); + this.table = table; + } + + @Override + public void open() { + LOG.info("open()"); + } + + @Override + public void close() { + LOG.info("close()"); + } +} diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/fdw/BlackholeScanState.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/fdw/BlackholeScanState.java new file mode 100644 index 000000000..3a35677cf --- /dev/null +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/fdw/BlackholeScanState.java @@ -0,0 +1,90 @@ +package org.postgresql.pljava.example.fdw; + +import org.postgresql.pljava.fdw.FDWScanState; +import org.postgresql.pljava.fdw.FDWUser; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.logging.Logger; + +/** + * A ForeignTable scan state. (Temporary) + */ +public class BlackholeScanState implements FDWScanState { + private static final Logger LOG = Logger.getLogger(BlackholeScanState.class.getName()); + + private final FDWUser user; + private final List> data; + + private Iterator> iter; + private boolean isOpen = false; + + public BlackholeScanState(FDWUser user, List> data) { + LOG.info("constructor()"); + + this.user = user; + + // create copy + this.data = new ArrayList> (data); + this.iter = null; + } + + @Override + public void open(boolean explainOnly) { + LOG.info("open()"); + if (this.isOpen) { + LOG.info("unexpected state!"); + } + else if (!isAuthorizedUser()) + { + LOG.info("unauthorized user"); + } + else if (!explainOnly) + { + // open file, read it, ... + this.iter = this.data.iterator(); + this.isOpen = true; + } + } + + @Override + public Map next() { + LOG.info("next()"); + if (this.isOpen) { + if (iter.hasNext()) { + return iter.next(); + } else { + return Collections.emptyMap(); + } + } else { + // what about 'explain only?' + LOG.info("unexpected state!"); + } + } + + @Override + public void reset() { + LOG.info("reset()"); + if (this.isOpen) { + this.iter = this.data.iterator(); + } else { + // what about 'explain only?' + LOG.info("unexpected state!"); + } + } + + @Override + public void close() { + LOG.info("close()"); + if (!this.isOpen) { + // don't do anything else... + LOG.info("unexpected state!"); + } + + this.iter = null; + this.isOpen = false; + } +} diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/fdw/BlackholeServer.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/fdw/BlackholeServer.java new file mode 100644 index 000000000..0abb486f1 --- /dev/null +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/fdw/BlackholeServer.java @@ -0,0 +1,39 @@ +package org.postgresql.pljava.example.fdw; + +import org.postgresql.pljava.fdw.FDWForeignTable; +import org.postgresql.pljava.fdw.FDWServer; + +import java.sql.ResultSetMetaData; +import java.util.Collections; +import java.util.Map; +import java.util.logging.Logger; + +/** + * A Foreign Server (persistent) + * + * Note: a single Server may contain multiple ForeignTables + * so there should be caching somewhere. + */ +public class BlackholeServer implements FDWServer { + private static final Logger LOG = Logger.getLogger(BlackholeServer.class.getName()); + + public BlackholeServer() { + this(Collections.emptyMap()); + } + + public BlackholeServer(Map options) { + LOG.info("constructor()"); + } + + @Override + public FDWForeignTable getForeignTable() { + LOG.info("getForeignTable()"); + return new BlackholeForeignTable() {}; + } + + @Override + public ResultSetMetaData getMetaData() { + LOG.info("getMetaData()"); + return null; + } +} diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/fdw/BlackholeValidator.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/fdw/BlackholeValidator.java new file mode 100644 index 000000000..863987ffb --- /dev/null +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/fdw/BlackholeValidator.java @@ -0,0 +1,47 @@ +package org.postgresql.pljava.example.fdw; + +import org.postgresql.pljava.fdw.FDWForeignDataWrapper; +import org.postgresql.pljava.fdw.FDWValidator; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Logger; + +public class BlackholeValidator implements FDWValidator { + private static final Logger LOG = Logger.getLogger(BlackholeValidator.class.getName()); + + // note: we know there's only five possible integer values. + private final Map> options = new HashMap<>(); + + public BlackholeValidator() { + this(Collections.emptyMap()); + } + + public BlackholeValidator(Map options) { + LOG.info("constructor"); + } + + @Override + public void addOption(int relid, String key, String value) { + LOG.info(String.format("addOption(%d, %s, %s)", relid, key, value)); + + if (!options.containsKey(relid)) { + options.put(relid, new HashMap<>()); + } + + options.get(relid).put(key, value); + } + + @Override + public boolean validate() { + LOG.info("validate()"); + return true; + } + + @Override + public FDWForeignDataWrapper getForeignDataWrapper() { + LOG.info("getForeignDataWrapper()"); + return new BlackholeForeignDataWrapper(); + } +} diff --git a/pljava-so/Dockerfile b/pljava-so/Dockerfile new file mode 100644 index 000000000..788590971 --- /dev/null +++ b/pljava-so/Dockerfile @@ -0,0 +1,27 @@ +# TODO: pull various VERS from the environment... + +FROM postgres:16-bookworm +LABEL authors="Bear Giles " + +ENV TARGET=target +ENV RESOURCES=src/main/resources + +# can/should be set as build property... +ENV PG_VERS=16 +ENV LIBDIR=/usr/lib/postgresql/${PG_VERS}/lib +ENV EXTDIR=/usr/share/postgresql/${PG_VERS}/extension + +ENV SO_NAME=pljava + +ENV FDW_NAME=blackhole_fdw +ENV FDW_VERS=1.6.9 + +# this will install the standard version. It can be updated once the docker image is running in a test environment. +RUN apt-get update && apt-get install -y postgresql-${PG_VERS}-pljava postgresql-${PG_VERS}-pljava-dbgsym + +COPY ${TARGET}/pljava-pgxs/lib${SO_NAME}-so-2-SNAPSHOT.so ${LIBDIR}/lib${SO_NAME}-so-${FDW_VERS}.so + +COPY ${RESOURCES}/fdw/${FDW_NAME}.control ${EXTDIR}/ +COPY ${RESOURCES}/fdw/sql/${FDW_NAME}*.sql ${EXTDIR}/ + +# ENTRYPOINT ["top", "-b"] diff --git a/pljava-so/docker-compose.yml b/pljava-so/docker-compose.yml new file mode 100644 index 000000000..d432a00d8 --- /dev/null +++ b/pljava-so/docker-compose.yml @@ -0,0 +1,17 @@ +services: + postgresql: + image: blackhole:latest + container_name: blackhole + ports: + - '5432:5432' + environment: + POSTGRES_PASSWORD: password + networks: + - frontend + +networks: + frontend: + # Specify driver options + driver: bridge + driver_opts: + com.docker.network.bridge.host_binding_ipv4: "127.0.1.1" diff --git a/pljava-so/src/main/c/BlackholeFDW.c b/pljava-so/src/main/c/BlackholeFDW.c new file mode 100644 index 000000000..531040462 --- /dev/null +++ b/pljava-so/src/main/c/BlackholeFDW.c @@ -0,0 +1,708 @@ +/** + * Actual implementation of minimal FDW based on the 'blackhole_fdw' + * project. For simplicity all of the comments and unused functions + * have been removed. + * + * The purpose of this file is to demonstrate the ability of C-based + * FDW implementation to successfully interact with a java object + * that implements the FDW interfaces. + * + * The first milestone is simply sending a NOTICE from the java: + * method. + */ +#include "postgres.h" +#include "postgres_ext.h" + +#include "access/reloptions.h" +#include "commands/explain.h" +#include "foreign/fdwapi.h" +#include "foreign/foreign.h" +#include "optimizer/pathnode.h" +#include "optimizer/planmain.h" +#include "optimizer/restrictinfo.h" + +#include "../include/pljava/FDW.h" + +// PG_MODULE_MAGIC; + +#if (PG_VERSION_NUM < 90500) +// fail... +#endif + +/** + * Sidenotes: + * + * PlanState also provides: + * - Instrumentation *instrument; // Optional runtime stats for this node + * - WorkerInstrumentation *worker_instrument; // per-worker instrumentation + */ + +// this needs to be known BEFORE we execute 'CREATE FOREIGN DATA WRAPPER...' +// static const char* FDW_validator_classname = "org/postgresql/pljava/fdw/BlackholeValidator"; + +/* + * Notes: copied from fdwapi.h + * + * extern Oid GetForeignServerIdByRelId(Oid relid); + * extern bool IsImportableForeignTable(const char *tablename, + * ImportForeignSchemaStmt *stmt); + * extern Path *GetExistingLocalJoinPath(RelOptInfo *joinrel); + * + * extern FdwRoutine *GetFdwRoutine(Oid fdwhandler); + * extern FdwRoutine *GetFdwRoutineByServerId(Oid serverid); + * extern FdwRoutine *GetFdwRoutineByRelId(Oid relid); + * extern FdwRoutine *GetFdwRoutineForRelation(Relation relation, bool makecopy); + * + * And from foreign.h + * + * extern ForeignServer *GetForeignServer(Oid serverid); + * extern ForeignServer *GetForeignServerExtended(Oid serverid, + * bits16 flags); + * extern ForeignServer *GetForeignServerByName(const char *srvname, + * bool missing_ok); + * extern UserMapping *GetUserMapping(Oid userid, Oid serverid); + * extern ForeignDataWrapper *GetForeignDataWrapper(Oid fdwid); + * extern ForeignDataWrapper *GetForeignDataWrapperExtended(Oid fdwid, + * bits16 flags); + * extern ForeignDataWrapper *GetForeignDataWrapperByName(const char *fdwname, + * bool missing_ok); + * extern ForeignTable *GetForeignTable(Oid relid); + * + * extern List *GetForeignColumnOptions(Oid relid, AttrNumber attnum); + * + * extern Oid get_foreign_data_wrapper_oid(const char *fdwname, bool missing_ok); + * extern Oid get_foreign_server_oid(const char *servername, bool missing_ok); + */ +extern Datum blackhole_fdw_handler(PG_FUNCTION_ARGS); +extern Datum blackhole_fdw_validator(PG_FUNCTION_ARGS); + +PG_FUNCTION_INFO_V1(blackhole_fdw_handler); +PG_FUNCTION_INFO_V1(blackhole_fdw_validator); + +/* callback functions */ +static void blackholeGetForeignRelSize(PlannerInfo *root, + RelOptInfo *baserel, + Oid foreigntableid); + +static void blackholeGetForeignPaths(PlannerInfo *root, + RelOptInfo *baserel, + Oid foreigntableid); + +static ForeignScan *blackholeGetForeignPlan(PlannerInfo *root, + RelOptInfo *rel, + Oid foreigntableid, + ForeignPath *best_path, + List *tlist, + List *restrictinfo_list, + Plan *outer_plan); + +static void blackholeBeginForeignScan(ForeignScanState *node, + int eflags); + +static TupleTableSlot *blackholeIterateForeignScan(ForeignScanState *node); + +static void blackholeReScanForeignScan(ForeignScanState *node); + +static void blackholeEndForeignScan(ForeignScanState *node); + +/* everything below here is optional */ +static int blackholeIsForeignRelUpdatable(Relation rel); +static void blackholeExplainForeignScan(ForeignScanState *node, ExplainState *es); +static List *blackholeImportForeignSchema(ImportForeignSchemaStmt *stmt, Oid serverOid); +static bool blackholeIsForeignScanParallelSafe(PlannerInfo *root, RelOptInfo *rel, RangeTblEntry *rte); +static bool blackholeIsForeignPathAsyncCapable(ForeignPath *path); + + +/* ------------------------------------------------------------ + * The POSTGRESQL Functions + * -----------------------------------------------------------*/ + +Datum +blackhole_fdw_handler(PG_FUNCTION_ARGS) +{ + FdwRoutine *fdwroutine = makeNode(FdwRoutine); + + elog(DEBUG1, "entering function %s", __func__); + + /* + * assign the handlers for the FDW + * + * This function might be called a number of times. In particular, it is + * likely to be called for each INSERT statement. For an explanation, see + * core postgres file src/optimizer/plan/createplan.c where it calls + * GetFdwRoutineByRelId((). + */ + + /* Required by notations: S=SELECT I=INSERT U=UPDATE D=DELETE */ + + /* these are required */ + fdwroutine->GetForeignRelSize = blackholeGetForeignRelSize; /* S U D */ + fdwroutine->GetForeignPaths = blackholeGetForeignPaths; /* S U D */ + fdwroutine->GetForeignPlan = blackholeGetForeignPlan; /* S U D */ + fdwroutine->BeginForeignScan = blackholeBeginForeignScan; /* S U D */ + fdwroutine->IterateForeignScan = blackholeIterateForeignScan; /* S */ + fdwroutine->ReScanForeignScan = blackholeReScanForeignScan; /* S */ + fdwroutine->EndForeignScan = blackholeEndForeignScan; /* S U D */ + + /* remainder are optional - use NULL if not required */ + /* support for insert / update / delete */ + fdwroutine->IsForeignRelUpdatable = blackholeIsForeignRelUpdatable; + + /* Support for scanning foreign joins */ + fdwroutine->GetForeignJoinPaths = NULL; + + /* Functions for remote upper-relation (post scan/join) planning */ + fdwroutine->GetForeignUpperPaths = NULL; + + /* Functions for modifying foreign tables */ + fdwroutine->AddForeignUpdateTargets = NULL; /* U D */ + fdwroutine->PlanForeignModify = NULL; /* I U D */ + fdwroutine->BeginForeignModify = NULL; /* I U D */ + fdwroutine->ExecForeignInsert = NULL; /* I */ + fdwroutine->ExecForeignUpdate = NULL; /* U */ + fdwroutine->ExecForeignDelete = NULL; /* D */ + fdwroutine->EndForeignModify = NULL; /* I U D */ + + /* Next-Generation functions for modifying foreign tables? */ + fdwroutine->PlanDirectModify = NULL; + fdwroutine->BeginDirectModify = NULL; + fdwroutine->IterateDirectModify = NULL; + fdwroutine->EndDirectModify = NULL; + + /* Support for SELECT FOR UPODATE/SHARE row locking */ + fdwroutine->GetForeignRowMarkType = NULL; + fdwroutine->RefetchForeignRow = NULL; + fdwroutine->RecheckForeignScan = NULL; + + /* support for EXPLAIN */ + fdwroutine->ExplainForeignScan = blackholeExplainForeignScan; /* EXPLAIN S U D */ + fdwroutine->ExplainForeignModify = NULL; /* EXPLAIN I U D */ + fdwroutine->ExplainDirectModify = NULL; + + /* Support functions for ANALYZE */ + fdwroutine->AnalyzeForeignTable = NULL; /* ANALYZE only */ + + /* Support functions for IMPORT FOREIGN SCHEMA */ + fdwroutine->ImportForeignSchema = blackholeImportForeignSchema; + + /* Support functions for TRUNCATE */ +#if (PG_VERSION_NUM >= 14000) + fdwroutine->ExecForeignTruncate = NULL; +#endif + + /* Support functions for parallelism under Gather node */ + fdwroutine->IsForeignScanParallelSafe = blackholeIsForeignScanParallelSafe; + fdwroutine->EstimateDSMForeignScan = NULL; + fdwroutine->InitializeDSMForeignScan = NULL; + fdwroutine->ReInitializeDSMForeignScan = NULL; + fdwroutine->InitializeWorkerForeignScan = NULL; + fdwroutine->ShutdownForeignScan = NULL; + + /* Support functions for path reparameterization. */ + fdwroutine->ReparameterizeForeignPathByChild = NULL; + +#if (PG_VERSION_NUM >= 14000) + /* Support functions for asynchronous execution */ + fdwroutine->IsForeignPathAsyncCapable = blackholeIsForeignPathAsyncCapable; + fdwroutine->ForeignAsyncRequest = NULL; + fdwroutine->ForeignAsyncConfigureWait = NULL; + fdwroutine->ForeignAsyncNotify = NULL; +#endif + + PG_RETURN_POINTER(fdwroutine); +} + +Datum +blackhole_fdw_validator(PG_FUNCTION_ARGS) +{ + List *options_list = untransformRelOptions(PG_GETARG_DATUM(0)); + + elog(DEBUG1, "entering function %s", __func__); + + /* collect options */ + + /* validate them */ +#ifdef USE_JAVA + JNI_FDW_Validator newValidator = call constructor (static method) +#endif + + if (list_length(options_list) > 0) + ereport(ERROR, + (errcode(ERRCODE_FDW_INVALID_OPTION_NAME), + errmsg("invalid options"), + errhint("Simple FDW does not support any options"))); + + PG_RETURN_VOID(); +} + +static void +blackholeShowInfo(Oid foreigntableid, RelOptInfo *rel) +{ + // rel->serverid; + // rel->userid; // may be InvalidOid = current user) + // rel->useriscurrent + // rel->fdwroutine + // rel->fdw_private + + // rel->rows + // rel->relid (only base rel, not joins) + // rel->min_attr (often <0) + // rel->max_attr; + + // ForeignTable *table = GetForeignTableExtended(foreigntableid, flags); // expects relid? + // ForeignServer *server = GetForeignServerExtended(table->serverid, flags); + + // ForeignTable *table = GetForeignTable(rel->relid); // expects relid? + // ForeignServer *server = GetForeignServer(table->serverid); + // ForeignDataWrapper *fdw = GetForeignDataWrapper(server->fdwid); + + // List *ftOptions = table->options; + // List *srvOptions = server->options; + // List *fdwOptions = fdw->options; + // List *userOptions = um->options; + // List *columnOptions = GetForeignColumnOptions(rel->relid, (AttrNumber) 0); + + // macro + // char *username = MappingUserName(userid) + + // Oid serverId = svr->serverid; + // Oid fdwid = svr->fdwid; + // Oid srvOwnerId = svr->ownerid; + // char *servername = svr->servername; + // char *serverType = svr->servertype; // optional + // char *serverVersion = svr->serverversion; // optional + + // Oid fwdid = fdw->fdwid; + // Oid owner = fdw->owner; + // char *fdwname = fdw->fdwname; + // Oid fdwhandler = fdw->fdwhandler; + // Oid fdwvalidator = fdw->fdwvalidator; + + // Oid usermappingoid = um->umid; + // Oid userId = um->userId; + // Oid umServerId = um->serverId; + + // wrapper->options; + // wrapper->owner; +} + +static void +blackholeGetForeignRelSize(PlannerInfo *root, + RelOptInfo *baserel, + Oid foreigntableid) { + /*1 + * Obtain relation size estimates for a foreign table. This is called at + * the beginning of planning for a query that scans a foreign table. root + * is the planner's global information about the query; baserel is the + * planner's information about this table; and foreigntableid is the + * pg_class OID of the foreign table. (foreigntableid could be obtained + * from the planner data structures, but it's passed explicitly to save + * effort.) + * + * This function should update baserel->rows to be the expected number of + * rows returned by the table scan, after accounting for the filtering + * done by the restriction quals. The initial value of baserel->rows is + * just a constant default estimate, which should be replaced if at all + * possible. The function may also choose to update baserel->width if it + * can compute a better estimate of the average result row width. + */ + + elog(NOTICE, "entering function %s", __func__); + +#ifdef USE_JAVA + // JAVA CONSTRUCTOR based on foreigntable id + baserel->fdw_private = JNI_getForeignRelSize(user, root, baserel); + + // JAVA METHOD + /* initialize required state in plan_state */ + (JNI_FDW_PlanState *baserel->plan_state)->open(plan_state); +#endif + // baserel->rows = plan_state->rows; +} + +static void +blackholeGetForeignPaths(PlannerInfo *root, + RelOptInfo *rel, + Oid foreigntableid) { + /* + * Create possible access paths for a scan on a foreign table. This is + * called during query planning. The parameters are the same as for + * GetForeignRelSize, which has already been called. + * + * This function must generate at least one access path (ForeignPath node) + * for a scan on the foreign table and must call add_path to add each such + * path to rel->pathlist. It's recommended to use + * create_foreignscan_path to build the ForeignPath nodes. The function + * can generate multiple access paths, e.g., a path which has valid + * pathkeys to represent a pre-sorted result. Each access path must + * contain cost estimates, and can contain any FDW-private information + * that is needed to identify the specific scan method intended. + */ + + PathTarget *target = NULL; + List *pathkeys = NIL; + Relids required_outer = NULL; + Path *fdw_outerpath = NULL; // extra plan + List *fdw_restrictinfo = NIL; + List *options = NIL; + + Cost startup_cost = 0; + Cost total_cost = startup_cost + rel->rows; + + // JNI_FDW_PlanState *plan_state = NULL; + + // elog(NOTICE, "server: %s (%s) type: %s (%d)", server->servername, server->serverversion, server->servertype, server->fdwid); + // elog(NOTICE, "server: %s (%d)", server->servername, server->fdwid); + + // elog(NOTICE, "table:, rel: %d, serverid: %d", table->relid, table->serverid); + // table->options; + + // List *to_list = table->options; + +#ifdef USE_JAVA + // NOTE: the fact that we see the `foreigntableid` parameter means that + // this is probably a null value and we need to call a constructor. + // I remember the documentation referred to a state being available + // but the `fdw_private` ptr was still null. + plan_state = ... + + // now update the plan_state. +#endif + + /* Create a ForeignPath node and add it as only possible path */ + add_path(rel, (Path *) create_foreignscan_path( + root, + rel, + target, + rel->rows, // planState->rows +#if (PG_VERSION_NUM >= 180000) + 0, /* no disabled nodes */ +#endif + startup_cost, // planState->startup_cost + total_cost, // planState->total_cost + pathkeys, + required_outer, + fdw_outerpath, +#if (PG_VERSION_NUM >= 170000) + fdw_restrictinfo, +#endif + options)); +} + +static ForeignScan * +blackholeGetForeignPlan(PlannerInfo *root, + RelOptInfo *rel, + Oid foreigntableid, + ForeignPath *best_path, + List *tlist, + List *restrictinfo_list, + Plan *outer_plan) +{ + /* + * Create a ForeignScan plan node from the selected foreign access path. + * This is called at the end of query planning. The parameters are as for + * GetForeignRelSize, plus the selected ForeignPath (previously produced + * by GetForeignPaths), the target list to be emitted by the plan node, + * and the restriction clauses to be enforced by the plan node. + * + * This function must create and return a ForeignScan plan node; it's + * recommended to use make_foreignscan to build the ForeignScan node. + */ + + Index scan_relid = rel->relid; + + /* + * We have no native ability to evaluate restriction clauses, so we just + * put all the scan_clauses into the plan node's qual list for the + * executor to check. So all we have to do here is strip RestrictInfo + * nodes from the clauses and ignore pseudoconstants (which will be + * handled elsewhere). + */ + + bool pseudocontent = false; + + /* Create the ForeignScan node */ + List *fdw_exprs = NIL; // expressions to evaluate + List *fdw_private = NIL; // private state + List *fdw_scan_tlist = NIL; // custom tlist + List *fdw_recheck_quals = NIL; // remote quals + List *restrictions = extract_actual_clauses(restrictinfo_list, pseudocontent); + ForeignScan *scan = NULL; + + // JNI_FDW_ScanState *scan_state; + + elog(NOTICE, "entering function %s", __func__); + +#ifdef USE_JAVA + // the presence of the `foreigntableid` says that we should be calling a constructor. + + // update the variables mentioned above. +#endif + + scan = make_foreignscan(tlist, + restrictions, + scan_relid, + fdw_exprs, + fdw_private, + fdw_scan_tlist, + fdw_recheck_quals, + outer_plan); + +#ifdef USE_JAVA + // I'm not sure we do this here... see next method + // scan->fdw_private = scanState; +#endif + + return scan; +} + +static void +blackholeBeginForeignScan(ForeignScanState *node, + int eflags) { + /* + * Begin executing a foreign scan. This is called during executor startup. + * It should perform any initialization needed before the scan can start, + * but not start executing the actual scan (that should be done upon the + * first call to IterateForeignScan). The ForeignScanState node has + * already been created, but its fdw_state field is still NULL. + * Information about the table to scan is accessible through the + * ForeignScanState node (in particular, from the underlying ForeignScan + * plan node, which contains any FDW-private information provided by + * GetForeignPlan). eflags contains flag bits describing the executor's + * operating mode for this plan node. + * + * Note that when (eflags & EXEC_FLAG_EXPLAIN_ONLY) is true, this function + * should not perform any externally-visible actions; it should only do + * the minimum required to make the node state valid for + * ExplainForeignScan and EndForeignScan. + * + */ + + // ScanState scanState = (ForeignScan *) node->ss + // PlanState planState = (ForeignScan *) node->ss.ps; + +// Plan *plan = (ForeignScan *) node->ss.ps.plan; +// EState *state = (ForeignScan *) node->ss.ps.state; +// List *options; + bool explainOnly = eflags & EXEC_FLAG_EXPLAIN_ONLY; + + // this is initially set to NULL as a marker for 'explainOnly' + JNI_FDW_ScanState *scan_state = NULL; + + elog(NOTICE, "entering function %s", __func__); + + /* Fetch options of foreign table */ +// fileGetOptions(RelationGetRelid(node->ss.ss_currentRelation), +// &filename, &is_program, &options); + + /* Add any options from the plan (currently only convert_selectively) */ +/* + if (plan_state != NULL) + { + options = list_concat(options, plan_state); + } +*/ + + /* + * Do nothing in EXPLAIN (no ANALYZE) case. node->fdw_state stays NULL. + */ + if (explainOnly) + { + // this function should not perform any externally-visible actions; + // it should only do the minimum required to make the node state valid for + // ExplainForeignScan and EndForeignScan. + + return; + } + + /* + * From FileFDW + * Create CopyState from FDW options. We always acquire all columns, so + * as to match the expected ScanTupleSlot signature. + */ + /* + cstate = BeginCopyFrom(NULL, + node->ss.ss_currentRelation, + NULL, + filename, + is_program, + NULL, + NIL, + options); + */ + + // plan_state = (JNI_FDW_PlanState *) plan->fdw_private; +#ifdef USE_JAVA + // 'explain only' is always false here... + scan_state = jni_create_blackhole_fdw_scan_state(node, explainOnly); + + if (scan_state != NULL) { + scan_state->open(scan_state); + } +#endif + + node->fdw_state = scan_state; +} + + +static TupleTableSlot * +blackholeIterateForeignScan(ForeignScanState *node) { + /* + * Fetch one row from the foreign source, returning it in a tuple table + * slot (the node's ScanTupleSlot should be used for this purpose). Return + * NULL if no more rows are available. The tuple table slot infrastructure + * allows either a physical or virtual tuple to be returned; in most cases + * the latter choice is preferable from a performance standpoint. Note + * that this is called in a short-lived memory context that will be reset + * between invocations. Create a memory context in BeginForeignScan if you + * need longer-lived storage, or use the es_query_cxt of the node's + * EState. + * + * The rows returned must match the column signature of the foreign table + * being scanned. If you choose to optimize away fetching columns that are + * not needed, you should insert nulls in those column positions. + * + * Note that PostgreSQL's executor doesn't care whether the rows returned + * violate any NOT NULL constraints that were defined on the foreign table + * columns — but the planner does care, and may optimize queries + * incorrectly if NULL values are present in a column declared not to + * contain them. If a NULL value is encountered when the user has declared + * that none should be present, it may be appropriate to raise an error + * (just as you would need to do in the case of a data type mismatch). + */ + + JNI_FDW_ScanState *scan_state = (JNI_FDW_ScanState *) node->fdw_state; + TupleTableSlot *slot = node->ss.ss_ScanTupleSlot; + + /* get the current slot and clear it */ + ExecClearTuple(slot); + + /* + * Do nothing in EXPLAIN (no ANALYZE) case. node->fdw_state stays NULL. + */ + if (node->fdw_state == NULL) { + return NULL; + } + + elog(NOTICE, "entering function %s", __func__); + + if (scan_state != NULL) + { +#ifdef USE_JAVA + /* get the next record, if any, and fill in the slot */ + scan_state->next(scan_state); + populate slot +#endif + } + + /* then return the slot */ + return slot; +} + + +static void +blackholeReScanForeignScan(ForeignScanState *node) { + /* + * Restart the scan from the beginning. Note that any parameters the scan + * depends on may have changed value, so the new scan does not necessarily + * return exactly the same rows. + */ + + JNI_FDW_ScanState *scan_state = (JNI_FDW_ScanState *) node->fdw_state; + + elog(NOTICE, "entering function %s", __func__); + + if (scan_state != NULL) + { +#ifdef USE_JAVA + // note: should this include 'user' parameter? + scan_state->reset(scan_state); +#endif + } +} + + +static void +blackholeEndForeignScan(ForeignScanState *node) { + /* + * End the scan and release resources. It is normally not important to + * release palloc'd memory, but for example open files and connections to + * remote servers should be cleaned up. + */ + + JNI_FDW_ScanState *scan_state = (JNI_FDW_ScanState *) node->fdw_state; + + /* + * Do nothing in EXPLAIN (no ANALYZE) case. node->fdw_state stays NULL. + */ + if (node->fdw_state == NULL) { + return; + } + + elog(NOTICE, "entering function %s", __func__); + + if (scan_state != NULL) + { +#ifdef USE_JAVA + scan_state->close(scan_state); +#endif + pfree(scan_state); + } +} + +static int +blackholeIsForeignRelUpdatable(Relation rel) +{ + // TODO: check FDW_user... + return 0; +} + +/* + * fileExplainForeignScan + * Produce extra output for EXPLAIN + */ +static void +blackholeExplainForeignScan(ForeignScanState *node, ExplainState *es) +{ + // List *options; + +#ifdef NEVER + // this comes from FileFdw. + + /* Fetch options --- we only need filename and is_program at this point */ + fileGetOptions(RelationGetRelid(node->ss.ss_currentRelation), + &filename, &is_program, &options); + + if (is_program) + ExplainPropertyText("Foreign Program", filename, es); + else + ExplainPropertyText("Foreign File", filename, es); + + /* Suppress file size if we're not showing cost details */ + if (es->costs) + { + struct stat stat_buf; + + if (!is_program && + stat(filename, &stat_buf) == 0) + ExplainPropertyInteger("Foreign File Size", "b", + (int64) stat_buf.st_size, es); + } +#endif +} + +static List *blackholeImportForeignSchema(ImportForeignSchemaStmt *stmt, Oid serverOid) +{ + return NIL; +} + +static bool blackholeIsForeignScanParallelSafe(PlannerInfo *root, + RelOptInfo *rel, + RangeTblEntry *rte) +{ + return false; +} + +static bool blackholeIsForeignPathAsyncCapable(ForeignPath *path) { + return false; +} diff --git a/pljava-so/src/main/c/BlackholeFDWJNI.c b/pljava-so/src/main/c/BlackholeFDWJNI.c new file mode 100644 index 000000000..cd32cfe81 --- /dev/null +++ b/pljava-so/src/main/c/BlackholeFDWJNI.c @@ -0,0 +1,253 @@ +/** + * Actual implementation of minimal FDW based on the 'blackhole_fdw' + * project. For simplicity all of the comments and unused functions + * have been removed. + * + * The purpose of this file is to demonstrate the ability of C-based + * FDW implementation to successfully interact with a java object + * that implements the FDW interfaces. + * + * The first milestone is simply sending a NOTICE from the java: + * method. + */ +#include "postgres.h" + +#include "access/reloptions.h" +#include "commands/explain.h" +#include "foreign/fdwapi.h" +#include "foreign/foreign.h" +#include "optimizer/pathnode.h" +#include "optimizer/planmain.h" +#include "optimizer/restrictinfo.h" + +#include "pljava/pljava.h" +#include "pljava/FDW.h" + +// PG_MODULE_MAGIC; + +#if (PG_VERSION_NUM < 90500) +// fail... +#endif + +/* ------------------------------------------------------------ */ + +// had been 'JNI_FDW.h' +#ifndef NEVER + +/** + * Wrapper functions + */ + +/* + * Persistent + */ +struct { + // void (*addOption)(JNI_FDW_Validator *, int, const char *, const char *) = validator_add_option; + // bool (*validate)(JNI_FDW_Validator *); + + const JNIEnv *env; + const jclass validatorClass; + const jobject instance; +} JNI_FDW_Validator_; + +/* + * Persistent + */ +struct { + // JNI_FDW_validate_options_for_reuse = fdw_validate_options_for_reuse; + + JNIEnv *env; + jclass wrapperClass; + jobject *instance; +} JNI_FDW_Wrapper_; + +/* + * Persistent + */ +struct { + // JNI_FDW_validate_options_for_reuse = srv_validate_options_for_reuse; + // void* (*getMetadata)(JNI_FDW_Server *server); + + JNIEnv *env; + jclass serverClass; + jobject *instance; +} JNI_FDW_Server_; + +/* + * Persistent + */ +struct { + // JNI_FDW_validate_options_for_reuse = srv_validate_options_for_reuse; + + JNIEnv *env; + jclass serverClass; + jobject *instance; +} JNI_FDW_User_; + +/* + * Persistent + */ +struct { + // JNI_FDW_validate_options_for_reuse = ft_validate_options_for_reuse; + + // void* (*newPlanState)(JNI_FDW_Table *table, JNI_FDW_User *user); + // void *(*newScanState)(JNI_FDW_Table *table, JNI_FDW_User *user); + + // void *(getMetaData)(JNI_FDW_Table *table JNI_FDW_User *user); + + // bool (*updateable)(JNI_FDW_Table *table, JNI_FDW_User *user); + // bool (*supportsConcurrency)(JNI_FDW_Table *table); + // bool (*supportsAsyncOperations)(JNI_FDW_Table *table); + + // void (*analyze)(JNI_FDW_Table *table); + // void (*vacuum)(JNI_FDW_Table *table); + + JNIEnv *env; + jclass tableClass; + jobject instance; +} JNI_FDW_Table_; + +/* + * Temporary + */ +struct { + // void (*open)(JNI_FDW_PlanState *planState, PlannerInfo *root, RelOptInfo *baserel, Oid foregntableid); + // void (*open)(JNI_FDW_PlanState *planState); + // void (*close)(JNI_FDW_PlanState *planState); + + JNIEnv *env; + // jclass planStateClass; ? + jobject *instance; + jlong rows; + + // cached values? + jdouble cost; + jdouble startup_cost; + jdouble totalcost; +} JNI_FDW_PlanState_; + +/* + * Temporary + */ +struct { + // void (*open)(JNI_FDW_ScanState *scanState, ForeignScanState *node, int eflags) = open_scan; + // void (*next)(JNI_FDW_ScanState *scanState, TableTupleSlot *slot); + // void (*reset)(JNI_FDW_ScanState *scanState); + // void (*close)(JNI_FDW_ScanState *scanState); + // void (*explain)(JNI_FDW_ScanState *scanState); + + JNIEnv *env; + jobject *instance; +} JNI_FDW_ScanState_; +#endif + +/* ------------------------------------------------------------ */ + +/* + +static JNI_FDW_Wrapper *validator_get_wrapper(JNI_FDW_Validator *validator); +static JNI_FDW_Server *wrapper_get_server(JNI_FDW_Wrapper *wrapper); +static JNI_FDW_Table *server_get_table(JNI_FDW_Server *server); +static JNI_FDW_PlanState *table_new_plan(JNI_FDW_Table *table); +static JNI_FDW_ScanState *table_new_scan(JNI_FDW_Table *table); + +// not all functions... + +static void validator_add_option(JNI_FDW_Validator *, int relid, String key, String value); +static bool validator_validate(JNI_FDW_Validator *); + +static JNI_FDW_PlanState *table_new_planstate(JNI_FDW_Table *table); +static void plan_open(JNI_FDW_Table *table, PlannerInfo *root, RelOptInfo *baserel, Oid foreigntableid); +static void plan_close(JNI_FDW_PlanState *plan_state); + +static void scan_open(JNI_FDW_ScanState *scan_state, ForeignScanState *node, int eflag); +static void scan_next(JNI_FDW_ScanState *scan_state, Slot *slot); +static void scan_reset(JNI_FDW_ScanState *scan_state); +static void scan_close(JNI_FDW_ScanState *scan_state); + +static JNI_FDW_ScanState *table_new_scanPlan(JNI_FDW_Table *table); + */ + +// Note: this does not do memory management yet! + +/* +static +jmethodID getMethodID(JNIEnv *env, jclass class, ...) +{ + return env->GetMethodID(class, va_arg); +} +*/ + +#ifdef USE_JAVA +JNI_FDW_Validator newValidator(JNIEnv *env, const char *validator_classname) { + JNI_FDW_Validator *validator = (JNI_FDW_Validator *) palloc0(sizeof JNI_FDW_Validator); + + validator->env = env; + validator->validatorClass = env->FindClass(validator_classname); + validator->instance = env->AllocObject(fdw->validatorClass); + + return validator; +} + +static +JNI_FDW_PlanState *table_new_planstate(JNI_FDW_Table *table) { + const JNIEnv *env = table->env; + + jmethodID openPlanMethodId = env->GetMethodID(table->tableClass, "newPlanState", + "(V)[org.postgresql.pljava.fdw.PlanState;"); + + const JNI_FDW_PlanState *planState = (JNI_FDW_PlanState *) palloc0(sizeof JNI_FDW_PlanState); + planState->env = env; + // table->planStateClass = env->FindClass(planstate_classname); + planState->instance = env->CallObjectMethod(table->instance, newPlanStateMethodId); + planState->table = table; +} + +static +void *plan_open(JNI_FDW_PlanState *planState, PlannerInfo *root, RelOptInfo *baserel, Oid foreigntableid) { + const ForeignTable foreignTable = GetForeignTable(foreigntableid); + + const JNIEnv *env = table->env; + + // FIXME: for now we don't pass anything through. However we could after a bit of conversions... + const jmethodId openPlanMethodId = env->GetMethodID(planState->planStateClass, "open", "(V)V"); + env->CallObjectMethod(planState->instance, openPlanStateMethodId); +} +#endif + +#ifdef USE_JAVA +JNIEXPORT +JNI_FDW_ScanState * JNICALL table_new_scan_state(JNI_FDW_Table *table, ForeignScanState *node, int eflag) { + const JNIEnv *env = table->env; + + // for now we ignore the extra parameters. + const jmethodID newScanStateMethodId = env->GetMethodID(table->tableClass, "newScanState", + "(V)[org.postgresql.pljava.fdw.ScanState;"); + + const JNI_FDW_ScanState *scanState = (JNI_FDW_ScanState *) palloc0(sizeof JNI_FDW_ScanState); + scanState->env = env; + + // for now we ignore the extra parameters. + scanState->instance = env->CallObjectMethod(table->instance, newScanStateMethodId); + scanState->table = table; + + return planState; +} +#endif + +/* ------------------------------------------------------------ + * Rest of JNI Implementation - does not use memory management yet! + * ------------------------------------------------------------*/ +#ifdef USE_JAVA +static void validator_add_option(JNI_FDW_Validator *validator, int relid, String key, String value) +{ + const JNIEnv *env = validator->env; + const jmethodID addOptionMethodId = env->GetMethodID(validator->validatorClass, "addOption", "(int, String, String)V"); + + const jint jrelid = NULL; + const jstring jkey = NULL; + const jstring jvalue = NULL; + + env->CallObjectMethod(validator->instance, addOptionMethodId, jrelid, jkey, jvalue); +} +#endif \ No newline at end of file diff --git a/pljava-so/src/main/include/pljava/FDW.h b/pljava-so/src/main/include/pljava/FDW.h new file mode 100644 index 000000000..c0ecfdee1 --- /dev/null +++ b/pljava-so/src/main/include/pljava/FDW.h @@ -0,0 +1,38 @@ +/** + * Actual implementation of minimal FDW based on the 'blackhole_fdw' + * project. For simplicity all of the comments and unused functions + * have been removed. + * + * The purpose of this file is to demonstrate the ability of C-based + * FDW implementation to successfully interact with a java object + * that implements the FDW interfaces. + * + * The first milestone is simply sending a NOTICE from the java + * method. + */ +#ifndef PLJAVA_SO_FDW_H +#define PLJAVA_SO_FDW_H + +// temporary name... +extern Datum blackhole_fdw_handler(PG_FUNCTION_ARGS); +extern Datum blackhole_fdw_validator(PG_FUNCTION_ARGS); + +struct JNI_FDW_Wrapper_; +struct JNI_FDW_Server_; +struct JNI_FDW_User_; // or 'UserMapping' ? +struct JNI_FDW_Table_; + +struct JNI_FDW_PlanState_; +struct JNI_FDW_ScanState_; + +// permanent objects (with OID) +typedef struct JNI_FDW_Wrapper_ JNI_FDW_Wrapper; +typedef struct JNI_FDW_Server_ JNI_FDW_Server; +typedef struct JNI_FDW_User_ JNI_FDW_User; // or 'UserMapping' ? +typedef struct JNI_FDW_Table_ JNI_FDW_Table; + +// temporary objects (no OID) +typedef struct JNI_FDW_PlanState_ JNI_FDW_PlanState; +typedef struct JNI_FDW_ScanState_ JNI_FDW_ScanState; + +#endif //PLJAVA_SO_FDW_H diff --git a/pljava-so/src/main/resources/fdw/blackhole_fdw.control b/pljava-so/src/main/resources/fdw/blackhole_fdw.control new file mode 100644 index 000000000..913cede60 --- /dev/null +++ b/pljava-so/src/main/resources/fdw/blackhole_fdw.control @@ -0,0 +1,5 @@ +# blackhole FDW +comment = 'Blackhole Foreign Data Wrapper' +default_version = '1.9.6' +module_pathname = '$libdir/libpljava.so' +relocatable = true diff --git a/pljava-so/src/main/resources/fdw/sql/blackhole_fdw.sql b/pljava-so/src/main/resources/fdw/sql/blackhole_fdw.sql new file mode 100644 index 000000000..82356afde --- /dev/null +++ b/pljava-so/src/main/resources/fdw/sql/blackhole_fdw.sql @@ -0,0 +1,31 @@ +* +* foreign-data wrapper blackhole + * + * Copyright (c) 2013, PostgreSQL Global Development Group + * + * This software is released under the PostgreSQL Licence + * + * Author: Andrew Dunstan + * + * IDENTIFICATION + * blackhole_fdw/=sql/blackhole_fdw.sql + * + *------------------------------------------------------------------------- + */ + +CREATE FUNCTION blackhole_fdw_handler() + RETURNS fdw_handler +AS '$libdir/blackhole_fdw' +LANGUAGE C STRICT; + +CREATE FUNCTION blackhole_fdw_validator(text[], oid) + RETURNS void +AS '$libdir/blackhole_fdw' +LANGUAGE C STRICT; + +CREATE +FOREIGN DATA WRAPPER blackhole_fdw + HANDLER blackhole_fdw_handler + VALIDATOR blackhole_fdw_validator; + +-- CREATE EXTENSION blackhole_fdw;