diff --git a/WSFPlugin/src/main/java/org/apidb/apicomplexa/wsfplugin/spanlogic/SpanCompositionPlugin.java b/WSFPlugin/src/main/java/org/apidb/apicomplexa/wsfplugin/spanlogic/SpanCompositionPlugin.java index 51869de5..972ea7e3 100644 --- a/WSFPlugin/src/main/java/org/apidb/apicomplexa/wsfplugin/spanlogic/SpanCompositionPlugin.java +++ b/WSFPlugin/src/main/java/org/apidb/apicomplexa/wsfplugin/spanlogic/SpanCompositionPlugin.java @@ -97,8 +97,50 @@ public String getRegion() { } - private static class Flag { - private boolean hasSnp = false; + static class Flag { + /** Set when either input is a point feature with no meaningful strand. */ + boolean strandless = false; + } + + /** + * Where a record type's genomic coordinates come from. One implementation per record + * class that may be an input to colocation. + * + * Implementations MUST alias their location table "fl" -- getStartStop() hardcodes that + * prefix (String table = "fl.") when building the region[] expressions that every + * implementation interpolates into its SQL. + */ + interface SpanSource { + + /** Full CREATE TABLE statement producing the per-record span temp table. */ + String createTableSql(String tableName, String[] region, String cacheSql); + + /** + * True for a point feature with no meaningful strand. Suppresses the same-strand / + * opposite-strand filter for the whole comparison; without it, "same strand" would + * silently match only forward-strand records. + */ + default boolean isStrandless() { + return false; + } + } + + private static final Map SPAN_SOURCES = Map.of( + "TranscriptRecordClasses.TranscriptRecordClass", new TranscriptSpanSource(), + "DynSpanRecordClasses.DynSpanRecordClass", new DynSpanSource(), + "VariantRecordClasses.VariantRecordClass", new VariantSpanSource()); + + static SpanSource spanSourceFor(String recordClassName) throws WdkModelException { + if (recordClassName == null) { + throw new WdkModelException("Genomic colocation is not configured for record class " + + "null. Register a SpanSource for it in SpanCompositionPlugin."); + } + SpanSource source = SPAN_SOURCES.get(recordClassName); + if (source == null) { + throw new WdkModelException("Genomic colocation is not configured for record class " + + recordClassName + ". Register a SpanSource for it in SpanCompositionPlugin."); + } + return source; } public static final String COLUMN_SOURCE_ID = "source_id"; @@ -281,12 +323,20 @@ public int execute(PluginRequest request, PluginResponse response) throws Plugin // execute the final sql, and fetch the result for the output. prepareResult(wdkModel, response, sql, request.getOrderedColumns(), output); - // drop the cache tables + // Drop the cache tables UNQUALIFIED, to match the unqualified CREATE TABLE in + // getSpanSql. Do not reach for getDefaultSchema() here: it means different things + // per platform. Oracle returns the login user's schema -- which is exactly where an + // unqualified CREATE lands, so the two agreed. PostgreSQL hardcodes "public" + // (PostgreSQL.getDefaultSchema), while an unqualified CREATE follows search_path, + // which is "$user". The tables were therefore created in the login schema and the + // drop looked in public, failing with 'table "spanlogic" does not exist' AFTER + // the results had been computed -- so a working colocation surfaced as an error and + // leaked a table per run. Passing null makes dropTable emit a bare table name, + // which resolves the same way the CREATE did on either platform. DBPlatform platform = wdkModel.getAppDb().getPlatform(); DataSource dataSource = wdkModel.getAppDb().getDataSource(); - String schema = wdkModel.getAppDb().getDefaultSchema(); - platform.dropTable(dataSource, schema, tempA, true); - platform.dropTable(dataSource, schema, tempB, true); + platform.dropTable(dataSource, null, tempA, true); + platform.dropTable(dataSource, null, tempB, true); return 0; } @@ -355,7 +405,10 @@ private String[] getStartStop(Map params, String suffix) { return new String[] { start, stop }; } - private String composeSql(String operation, String tempTableA, String tempTableB, + // package-private so SpanSourceTest can assert the FROM clause names the temp tables + // bare. Wrapping a table name in parentheses is legal Oracle and a syntax error in + // PostgreSQL, which broke every colocation regardless of record type. + String composeSql(String operation, String tempTableA, String tempTableB, String strand, String output, Flag flag) { StringBuilder builder = new StringBuilder(); @@ -372,7 +425,9 @@ private String composeSql(String operation, String tempTableA, String tempTableB builder.append(" fb.wdk_weight AS wdk_weight_b, "); builder.append(" fb.begin AS begin_b, fb.end AS end_b, "); builder.append(" fb.is_reversed AS is_reversed_b "); - builder.append("FROM (" + tempTableA + ") fa, (" + tempTableB + ") fb "); + // Bare table names, NOT "(name)". Oracle accepts a parenthesized table name; + // PostgreSQL raises 'syntax error at or near ")"'. + builder.append("FROM " + tempTableA + " fa, " + tempTableB + " fb "); // make sure the regions come from sequence source. builder.append("WHERE fa.sequence_source_id = fb.sequence_source_id "); @@ -382,7 +437,7 @@ private String composeSql(String operation, String tempTableA, String tempTableB builder.append(" AND fb.begin <= fb.end "); // check the strand choice - if (!flag.hasSnp) { + if (!flag.strandless) { if (strand.equalsIgnoreCase(PARAM_VALUE_SAME_STRAND)) { builder.append(" AND fa.is_reversed = fb.is_reversed "); } @@ -425,29 +480,11 @@ private String getSpanSql(WdkModel wdkModel, User requestingUser, Map