diff --git a/driver/src/main/java/oracle/nosql/driver/ops/PreparedStatement.java b/driver/src/main/java/oracle/nosql/driver/ops/PreparedStatement.java index 285bc504..c3af5788 100644 --- a/driver/src/main/java/oracle/nosql/driver/ops/PreparedStatement.java +++ b/driver/src/main/java/oracle/nosql/driver/ops/PreparedStatement.java @@ -7,6 +7,7 @@ package oracle.nosql.driver.ops; +import java.util.ArrayList; import java.util.HashMap; import java.util.Map; @@ -35,11 +36,14 @@ public class PreparedStatement { private final String querySchema; /* - * The serialized PreparedStatement created at the backend store. It is - * opaque for the driver. It is received from the proxy and sent back to - * the proxy every time a new batch of results is needed. + * The serialized PreparedStatements created at the backend store. There is + * one of them for each union branch (or a single one if the query has no + * UNION). They are opaque for the driver. They are received from the proxy + * and one of them (the one corresponding to the current UNION branch that + * is being executed) is sent back to the proxy every time a new batch of + * results is needed. */ - private final byte[] proxyStatement; + private final ArrayList proxyStatements; /* * The part of the query plan that must be executed at the driver. It is @@ -79,15 +83,17 @@ public class PreparedStatement { /* - * The namespace returned from a prepared query result, if any. + * The namespaces returned from a prepared query result. One for each + * UNION branch. */ - private final String namespace; + private final ArrayList namespaces; /* - * The table name returned from a prepared query result, if any. + * The top-table names returned from a prepared query result. One for + * each UNION branch. */ - private final String tableName; + private final ArrayList topTableNames; /* * the operation code for the query. @@ -128,32 +134,31 @@ public PreparedStatement( String sqlText, String queryPlan, String querySchema, - byte[] proxyStatement, + ArrayList proxyStatements, PlanIter driverPlan, int numIterators, int numRegisters, Map externalVars, - String namespace, - String tableName, + ArrayList namespaces, + ArrayList tableNames, byte operation, int maxParallelism) { - /* 10 is arbitrary. TODO: put magic number in it for validation? */ - if (proxyStatement == null || proxyStatement.length < 10) { + if (proxyStatements.isEmpty()) { throw new IllegalArgumentException( - "Invalid prepared query, cannot be null"); + "Invalid prepared query: no proxy-side query"); } this.sqlText = sqlText; this.queryPlan = queryPlan; this.querySchema = querySchema; - this.proxyStatement = proxyStatement; + this.proxyStatements = proxyStatements; this.driverQueryPlan = driverPlan; this.numIterators = numIterators; this.numRegisters = numRegisters; this.variables = externalVars; - this.namespace = namespace; - this.tableName = tableName; + this.namespaces = namespaces; + this.topTableNames = tableNames; this.operation = operation; this.maxParallelism = maxParallelism; } @@ -170,13 +175,13 @@ public PreparedStatement copyStatement() { return new PreparedStatement(sqlText, queryPlan, querySchema, - proxyStatement, + proxyStatements, driverQueryPlan, numIterators, numRegisters, variables, - namespace, - tableName, + namespaces, + topTableNames, operation, maxParallelism); } @@ -313,8 +318,8 @@ public PreparedStatement setVariable(int pos, FieldValue value) { * @return the serialized query * @hidden */ - public final byte[] getStatement() { - return proxyStatement; + public final byte[] getProxyStatement(int branch) { + return proxyStatements.get(branch); } /** @@ -391,8 +396,8 @@ public int numIterators() { * @return namespace from prepared statement, if any * @hidden */ - public String getNamespace() { - return namespace; + public String getNamespace(int branch) { + return namespaces.get(branch); } /** @@ -400,8 +405,8 @@ public String getNamespace() { * @return table name from prepared statement, if any * @hidden */ - public String getTableName() { - return tableName; + public String getTopTableName(int branch) { + return topTableNames.get(branch); } /** diff --git a/driver/src/main/java/oracle/nosql/driver/ops/QueryRequest.java b/driver/src/main/java/oracle/nosql/driver/ops/QueryRequest.java index afbfc705..37f59b11 100644 --- a/driver/src/main/java/oracle/nosql/driver/ops/QueryRequest.java +++ b/driver/src/main/java/oracle/nosql/driver/ops/QueryRequest.java @@ -1007,6 +1007,34 @@ public QueryRequest setNamespace(String namespace) { return this; } + @Override + public String getNamespace() { + if (namespace != null) { + return namespace; + } + if (preparedStatement == null) { + return null; + } + if (driver == null) { + return preparedStatement.getNamespace(0); + } + return preparedStatement.getNamespace(driver.getUnionBranch()); + } + + /** + * @hidden + */ + @Override + public String getTableName() { + if (preparedStatement == null) { + return null; + } + if (driver == null) { + return preparedStatement.getTopTableName(0); + } + return preparedStatement.getTopTableName(driver.getUnionBranch()); + } + /** * Returns the timeout to use for the operation, in milliseconds. A value * of 0 indicates that the timeout has not been set. @@ -1156,17 +1184,6 @@ public void validate() { } } - /** - * @hidden - */ - @Override - public String getTableName() { - if (preparedStatement == null) { - return null; - } - return preparedStatement.getTableName(); - } - /** * @hidden */ diff --git a/driver/src/main/java/oracle/nosql/driver/ops/serde/PrepareRequestSerializer.java b/driver/src/main/java/oracle/nosql/driver/ops/serde/PrepareRequestSerializer.java index 97b4ca71..27b533a0 100644 --- a/driver/src/main/java/oracle/nosql/driver/ops/serde/PrepareRequestSerializer.java +++ b/driver/src/main/java/oracle/nosql/driver/ops/serde/PrepareRequestSerializer.java @@ -10,6 +10,7 @@ import static oracle.nosql.driver.http.Client.trace; import java.io.IOException; +import java.util.ArrayList; import java.util.HashMap; import java.util.Map; @@ -118,12 +119,15 @@ static PreparedStatement deserializeInternal( */ int savedOffset = in.getOffset(); in.skip(37); // 4 + 32 + 1 - String namespace = readString(in); - String tableName = readString(in); + ArrayList namespaces = new ArrayList<>(); + namespaces.add(readString(in)); + ArrayList tableNames = new ArrayList<>(); + tableNames.add(readString(in)); byte operation = in.readByte(); in.setOffset(savedOffset); - byte[] proxyStatement = readByteArrayWithInt(in); + ArrayList proxyStatements = new ArrayList<>(); + proxyStatements.add(readByteArrayWithInt(in)); int numIterators = 0; int numRegisters = 0; @@ -160,13 +164,13 @@ static PreparedStatement deserializeInternal( new PreparedStatement(sqlText, queryPlan, null, // query schema - proxyStatement, + proxyStatements, driverPlan, numIterators, numRegisters, externalVars, - namespace, - tableName, + namespaces, + tableNames, operation, 0); /* no parallelism available */ diff --git a/driver/src/main/java/oracle/nosql/driver/ops/serde/QueryRequestSerializer.java b/driver/src/main/java/oracle/nosql/driver/ops/serde/QueryRequestSerializer.java index 4f976d1b..f08b2343 100644 --- a/driver/src/main/java/oracle/nosql/driver/ops/serde/QueryRequestSerializer.java +++ b/driver/src/main/java/oracle/nosql/driver/ops/serde/QueryRequestSerializer.java @@ -79,7 +79,7 @@ public void serialize(Request request, if (queryRq.isPrepared()) { PreparedStatement ps = queryRq.getPreparedStatement(); - writeByteArrayWithInt(out, ps.getStatement()); + writeByteArrayWithInt(out, ps.getProxyStatement(0)); if (ps.getVariables() != null) { diff --git a/driver/src/main/java/oracle/nosql/driver/ops/serde/nson/NsonProtocol.java b/driver/src/main/java/oracle/nosql/driver/ops/serde/nson/NsonProtocol.java index 414a2f14..35d0f35d 100644 --- a/driver/src/main/java/oracle/nosql/driver/ops/serde/nson/NsonProtocol.java +++ b/driver/src/main/java/oracle/nosql/driver/ops/serde/nson/NsonProtocol.java @@ -75,6 +75,7 @@ public class NsonProtocol { public static String PREPARED_QUERY = "pq"; public static String PREPARED_STATEMENT = "ps"; public static String QUERY = "q"; + public static String QUERY_BRANCHES = "qb"; public static String QUERY_NAME = "qn"; public static String QUERY_OPERATION_NUM = "on"; public static String QUERY_VERSION = "qv"; @@ -251,6 +252,7 @@ public class NsonProtocol { {PREPARED_QUERY,"PREPARED_QUERY"}, {PREPARED_STATEMENT,"PREPARED_STATEMENT"}, {QUERY,"QUERY"}, + {QUERY_BRANCHES,"QUERY_BRANCHES"}, {QUERY_NAME,"QUERY_NAME"}, {QUERY_OPERATION_NUM,"QUERY_OPERATION_NUM"}, {QUERY_VERSION,"QUERY_VERSION"}, diff --git a/driver/src/main/java/oracle/nosql/driver/ops/serde/nson/NsonSerializerFactory.java b/driver/src/main/java/oracle/nosql/driver/ops/serde/nson/NsonSerializerFactory.java index afce73c7..7d262ba1 100644 --- a/driver/src/main/java/oracle/nosql/driver/ops/serde/nson/NsonSerializerFactory.java +++ b/driver/src/main/java/oracle/nosql/driver/ops/serde/nson/NsonSerializerFactory.java @@ -872,10 +872,13 @@ public void serialize(Request request, writeMapField(ns, QUERY_VERSION, (int)queryVersion); boolean isPrepared = rq.isPrepared(); if (isPrepared) { + QueryDriver driver = rq.getDriver(); + int unionBranch = (driver != null ? driver.getUnionBranch() : 0); + byte[] proxyPlan = rq.getPreparedStatement(). + getProxyStatement(unionBranch); writeMapField(ns, IS_PREPARED, isPrepared); writeMapField(ns, IS_SIMPLE_QUERY, rq.isSimpleQuery()); - writeMapField(ns, PREPARED_QUERY, - rq.getPreparedStatement().getStatement()); + writeMapField(ns, PREPARED_QUERY, proxyPlan); /* * validation of parallel ops is handled in * QueryRequest.validate @@ -1008,13 +1011,12 @@ private static void deserializePrepareOrQuery( } boolean isPreparedRequest = (prep != null); - byte[] proxyPreparedQuery = null; + ArrayList namespaces = new ArrayList<>(); + ArrayList tableNames = new ArrayList<>(); + ArrayList proxyPreparedQueries = new ArrayList<>(); DriverPlanInfo dpi = null; - String queryPlan = null; - String tableName = null; - String namespace = null; String querySchema = null; byte operation = 0; int proxyTopoSeqNum = -1; /* QUERY_V3 and earlier */ @@ -1046,20 +1048,44 @@ private static void deserializePrepareOrQuery( readPhase1Results(arr, qres); } else if (name.equals(PREPARED_QUERY)) { - proxyPreparedQuery = Nson.readNsonBinary(in); + proxyPreparedQueries.add(Nson.readNsonBinary(in)); + + } else if (name.equals(QUERY_BRANCHES)) { + readType(in, Nson.TYPE_ARRAY); + in.readInt(); /* length of array in bytes */ + int numBranches = in.readInt(); /* number of array elements */ + + for (int i = 0; i < numBranches; ++i) { + MapWalker walker2 = getMapWalker(in); + while (walker2.hasNext()) { + walker2.next(); + String name2 = walker2.getCurrentName(); + if (name2.equals(NAMESPACE)) { + namespaces.add(Nson.readNsonString(in)); + } else if (name2.equals(TABLE_NAME)) { + tableNames.add(Nson.readNsonString(in)); + } else if (name2.equals(PREPARED_QUERY)) { + proxyPreparedQueries.add(Nson.readNsonBinary(in)); + } else { + throw new IOException( + "Unexpected field in query branch: " + + name2); + } + } + } } else if (name.equals(DRIVER_QUERY_PLAN)) { dpi = getDriverPlanInfo(Nson.readNsonBinary(in), - serialVersion); + queryVersion); } else if (name.equals(REACHED_LIMIT) && qres != null) { qres.setReachedLimit(Nson.readNsonBoolean(in)); } else if (name.equals(TABLE_NAME)) { - tableName = Nson.readNsonString(in); + tableNames.add(Nson.readNsonString(in)); } else if (name.equals(NAMESPACE)) { - namespace = Nson.readNsonString(in); + namespaces.add(Nson.readNsonString(in)); } else if (name.equals(QUERY_PLAN_STRING)) { queryPlan = Nson.readNsonString(in); @@ -1136,13 +1162,13 @@ private static void deserializePrepareOrQuery( prep = new PreparedStatement(statement, queryPlan, querySchema, - proxyPreparedQuery, + proxyPreparedQueries, (dpi!=null)?dpi.driverQueryPlan:null, (dpi!=null)?dpi.numIterators:0, (dpi!=null)?dpi.numRegisters:0, (dpi!=null)?dpi.externalVars:null, - namespace, - tableName, + namespaces, + tableNames, operation, maxParallelism); if (pres != null) { @@ -1241,7 +1267,7 @@ private static void readPhase1Results(byte[] arr, QueryResult result) } private static DriverPlanInfo getDriverPlanInfo(byte[] arr, - short serialVersion) + short queryVersion) throws IOException { if (arr == null || arr.length == 0) { return null; @@ -1249,7 +1275,7 @@ private static DriverPlanInfo getDriverPlanInfo(byte[] arr, ByteBuf buf = Unpooled.wrappedBuffer(arr); ByteInputStream bis = new NettyByteInputStream(buf); DriverPlanInfo dpi = new DriverPlanInfo(); - dpi.driverQueryPlan = PlanIter.deserializeIter(bis, serialVersion); + dpi.driverQueryPlan = PlanIter.deserializeIter(bis, queryVersion); if (dpi.driverQueryPlan == null) { return null; } diff --git a/driver/src/main/java/oracle/nosql/driver/query/AggrIterState.java b/driver/src/main/java/oracle/nosql/driver/query/AggrIterState.java index 69898396..c0d7ea1c 100644 --- a/driver/src/main/java/oracle/nosql/driver/query/AggrIterState.java +++ b/driver/src/main/java/oracle/nosql/driver/query/AggrIterState.java @@ -25,10 +25,10 @@ class AggrIterState extends PlanIterState { Type theSumType = Type.LONG; - boolean theNullInputOnly = true; - FieldValue theMinMax = NullValue.getInstance(); + boolean theGotNumericInput; + @Override public void reset(PlanIter iter) { super.reset(iter); @@ -37,7 +37,7 @@ public void reset(PlanIter iter) { theDoubleSum = 0; theNumberSum = null; theSumType = Type.LONG; - theNullInputOnly = true; + theGotNumericInput = false; theMinMax = NullValue.getInstance(); } } diff --git a/driver/src/main/java/oracle/nosql/driver/query/AndOrIter.java b/driver/src/main/java/oracle/nosql/driver/query/AndOrIter.java new file mode 100644 index 00000000..8a9de684 --- /dev/null +++ b/driver/src/main/java/oracle/nosql/driver/query/AndOrIter.java @@ -0,0 +1,156 @@ +/*- + * Copyright (c) 2011, 2025 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Universal Permissive License v 1.0 as shown at + * https://oss.oracle.com/licenses/upl/ + */ + +package oracle.nosql.driver.query; + +import java.io.IOException; + +import oracle.nosql.driver.util.ByteInputStream; +import oracle.nosql.driver.util.SerializationUtil; +import oracle.nosql.driver.values.FieldValue; +import oracle.nosql.driver.values.BooleanValue; +import oracle.nosql.driver.values.NullValue; + +public class AndOrIter extends PlanIter { + + private final FuncCode theCode; + + private final PlanIter[] theArgs; + + public AndOrIter(ByteInputStream in, short queryVersion) + throws IOException { + + super(in, queryVersion); + short ordinal = in.readShort(); + theCode = FuncCode.valueOf(ordinal); + theArgs = deserializeIters(in, queryVersion); + } + + @Override + public PlanIterKind getKind() { + return PlanIterKind.AND_OR; + } + + @Override + FuncCode getFuncCode() { + return theCode; + } + + @Override + public void open(RuntimeControlBlock rcb) { + rcb.setState(theStatePos, new PlanIterState()); + for (PlanIter arg : theArgs) { + arg.open(rcb); + } + } + + @Override + public void reset(RuntimeControlBlock rcb) { + + for (PlanIter arg : theArgs) { + arg.reset(rcb); + } + PlanIterState state = rcb.getState(theStatePos); + state.reset(this); + } + + @Override + public void close(RuntimeControlBlock rcb) { + + PlanIterState state = rcb.getState(theStatePos); + if (state == null) { + return; + } + + for (PlanIter arg : theArgs) { + arg.close(rcb); + } + + state.close(); + } + + @Override + public boolean next(RuntimeControlBlock rcb) { + + PlanIterState state = rcb.getState(theStatePos); + + if (state.isDone()) { + return false; + } + + /* + * If AND, start true, and exit as soon as there is a false result. + * If OR, start false, and exit as soon as there is a true result. + */ + assert(theCode == FuncCode.OP_AND || theCode == FuncCode.OP_OR); + boolean result = (theCode == FuncCode.OP_AND ? true : false); + boolean haveNull = false; + FieldValue res; + + for (PlanIter arg : theArgs) { + + boolean more = arg.next(rcb); + + boolean argResult; + + if (!more) { + argResult = false; + } else { + FieldValue argVal = rcb.getRegVal(arg.getResultReg()); + + if (argVal.isNull()) { + haveNull = true; + continue; + } + + argResult = argVal.getBoolean(); + } + + if (theCode == FuncCode.OP_AND) { + result &= argResult; + if (!result) { + haveNull = false; + break; + } + } else { + result |= argResult; + if (result) { + haveNull = false; + break; + } + } + } + + if (haveNull) { + res = NullValue.getInstance(); + } else { + res = BooleanValue.getInstance(result); + } + + rcb.setRegVal(theResultReg, res); + state.done(); + return true; + } + + + @Override + protected void displayContent( + StringBuilder sb, + QueryFormatter formatter) { + + displayInputIters(sb, formatter, theArgs); + } + + @Override + void displayName(StringBuilder sb) { + if (theCode == FuncCode.OP_AND) { + sb.append("AND"); + } else { + sb.append("OR"); + } + } +} diff --git a/driver/src/main/java/oracle/nosql/driver/query/ArithOpIter.java b/driver/src/main/java/oracle/nosql/driver/query/ArithOpIter.java index 4c74ba40..920b7051 100644 --- a/driver/src/main/java/oracle/nosql/driver/query/ArithOpIter.java +++ b/driver/src/main/java/oracle/nosql/driver/query/ArithOpIter.java @@ -66,13 +66,13 @@ public class ArithOpIter extends PlanIter { public ArithOpIter( ByteInputStream in, - short serialVersion) throws IOException { + short queryVersion) throws IOException { - super(in, serialVersion); + super(in, queryVersion); short ordinal = in.readShort(); theCode = FuncCode.valueOf(ordinal); - theArgs = deserializeIters(in, serialVersion); + theArgs = deserializeIters(in, queryVersion); theOps = SerializationUtil.readString(in); theInitResult = (theCode == FuncCode.OP_ADD_SUB ? 0 : 1); diff --git a/driver/src/main/java/oracle/nosql/driver/query/ArrayConstrIter.java b/driver/src/main/java/oracle/nosql/driver/query/ArrayConstrIter.java new file mode 100644 index 00000000..48ad7f94 --- /dev/null +++ b/driver/src/main/java/oracle/nosql/driver/query/ArrayConstrIter.java @@ -0,0 +1,151 @@ +/*- + * Copyright (c) 2011, 2025 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Universal Permissive License v 1.0 as shown at + * https://oss.oracle.com/licenses/upl/ + */ + +package oracle.nosql.driver.query; + +import java.io.IOException; + +import oracle.nosql.driver.util.ByteInputStream; +import oracle.nosql.driver.util.SerializationUtil; +import oracle.nosql.driver.values.FieldValue; +import oracle.nosql.driver.values.ArrayValue; + +public class ArrayConstrIter extends PlanIter { + + private final PlanIter[] theArgs; + + private final boolean theIsConditional; + + public ArrayConstrIter(ByteInputStream in, short queryVersion) + throws IOException { + + super(in, queryVersion); + theIsConditional = in.readBoolean(); + theArgs = deserializeIters(in, queryVersion); + } + + @Override + public PlanIterKind getKind() { + return PlanIterKind.ARRAY_CONSTRUCTOR; + } + + @Override + public void open(RuntimeControlBlock rcb) { + rcb.setState(theStatePos, new PlanIterState()); + for (PlanIter arg : theArgs) { + arg.open(rcb); + } + } + + @Override + public void reset(RuntimeControlBlock rcb) { + + for (PlanIter arg : theArgs) { + arg.reset(rcb); + } + PlanIterState state = rcb.getState(theStatePos); + state.reset(this); + } + + @Override + public void close(RuntimeControlBlock rcb) { + + PlanIterState state = rcb.getState(theStatePos); + if (state == null) { + return; + } + + for (PlanIter arg : theArgs) { + arg.close(rcb); + } + + state.close(); + } + + @Override + public boolean next(RuntimeControlBlock rcb) { + + PlanIterState state = rcb.getState(theStatePos); + + if (state.isDone()) { + return false; + } + + ArrayValue array; + + if (theIsConditional) { + + boolean more = theArgs[0].next(rcb); + + if (!more) { + state.done(); + return false; + } + + FieldValue val = rcb.getRegVal(theArgs[0].getResultReg()); + + more = theArgs[0].next(rcb); + + if (!more) { + rcb.setRegVal(theResultReg, val); + state.done(); + return true; + } + + array = new ArrayValue(); + + if (!val.isNull()) { + array.add(val); + } + + val = rcb.getRegVal(theArgs[0].getResultReg()); + + if (!val.isNull()) { + array.add(val); + } + + } else { + array = new ArrayValue(); + } + + for (int currArg = 0; currArg < theArgs.length; ++currArg) { + + while (true) { + boolean more = theArgs[currArg].next(rcb); + + if (!more) { + break; + } + + FieldValue val = + rcb.getRegVal(theArgs[currArg].getResultReg()); + + if (val.isNull()) { + continue; + } + + array.add(val); + } + } + + rcb.setRegVal(theResultReg, array); + state.done(); + return true; + } + + + @Override + protected void displayContent( + StringBuilder sb, + QueryFormatter formatter) { + + formatter.indent(sb); + sb.append("\"conditional\" : ").append(theIsConditional); + sb.append(",\n"); + displayInputIters(sb, formatter, theArgs); + } +} diff --git a/driver/src/main/java/oracle/nosql/driver/query/CaseIter.java b/driver/src/main/java/oracle/nosql/driver/query/CaseIter.java new file mode 100644 index 00000000..c9246e96 --- /dev/null +++ b/driver/src/main/java/oracle/nosql/driver/query/CaseIter.java @@ -0,0 +1,200 @@ +/*- + * Copyright (c) 2011, 2025 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Universal Permissive License v 1.0 as shown at + * https://oss.oracle.com/licenses/upl/ + */ + +package oracle.nosql.driver.query; + +import java.io.IOException; + +import oracle.nosql.driver.util.ByteInputStream; +import oracle.nosql.driver.util.SerializationUtil; +import oracle.nosql.driver.values.FieldValue; + +public class CaseIter extends PlanIter { + + static private class CaseIterState extends PlanIterState { + + /* theActiveIter is set to the iterator whose associated condition + * evaluated to true. */ + PlanIter theActiveIter; + } + + private final PlanIter[] theCondIters; + + private final PlanIter[] theThenIters; + + private final PlanIter theElseIter; + + public CaseIter(ByteInputStream in, short queryVersion) + throws IOException { + + super(in, queryVersion); + theCondIters = deserializeIters(in, queryVersion); + theThenIters = deserializeIters(in, queryVersion); + theElseIter = deserializeIter(in, queryVersion); + } + + @Override + public PlanIterKind getKind() { + return PlanIterKind.CASE; + } + + @Override + public void open(RuntimeControlBlock rcb) { + + rcb.setState(theStatePos, new CaseIterState()); + + for (PlanIter iter : theCondIters) { + iter.open(rcb); + } + for (PlanIter iter : theThenIters) { + iter.open(rcb); + } + if (theElseIter != null) { + theElseIter.open(rcb); + } + } + + @Override + public void reset(RuntimeControlBlock rcb) { + + for (PlanIter iter : theCondIters) { + iter.reset(rcb); + } + for (PlanIter iter : theThenIters) { + iter.reset(rcb); + } + if (theElseIter != null) { + theElseIter.reset(rcb); + } + + CaseIterState state = (CaseIterState)rcb.getState(theStatePos); + state.reset(this); + } + @Override + public void close(RuntimeControlBlock rcb) { + + CaseIterState state = (CaseIterState)rcb.getState(theStatePos); + if (state == null) { + return; + } + + for (PlanIter iter : theCondIters) { + iter.close(rcb); + } + for (PlanIter iter : theThenIters) { + iter.close(rcb); + } + if (theElseIter != null) { + theElseIter.close(rcb); + } + state.close(); + } + + @Override + public boolean next(RuntimeControlBlock rcb) { + + CaseIterState state = (CaseIterState)rcb.getState(theStatePos); + + if (state.isDone()) { + return false; + } + + if (state.isOpen()) { + + int i; + for (i = 0; i < theCondIters.length; ++i) { + + boolean more = theCondIters[i].next(rcb); + + if (!more) { + continue; + } + + FieldValue val = + rcb.getRegVal(theCondIters[i].getResultReg()); + + if (val.isNull() || !val.getBoolean()) { + continue; + } + + state.theActiveIter = theThenIters[i]; + break; + } + + if (i == theCondIters.length) { + if (theElseIter == null) { + state.done(); + return false; + } + state.theActiveIter = theElseIter; + } + + state.setState(PlanIterState.StateEnum.RUNNING); + } + + if (!state.theActiveIter.next(rcb)) { + state.done(); + return false; + } + + FieldValue retValue = rcb.getRegVal(state.theActiveIter.getResultReg()); + rcb.setRegVal(theResultReg, retValue); + return true; + } + + @Override + protected void displayContent( + StringBuilder sb, + QueryFormatter formatter) { + + formatter.indent(sb); + sb.append("\"clauses\" : [\n"); + + formatter.incIndent(); + + for (int i = 0; i < theCondIters.length; ++i) { + + formatter.indent(sb); + sb.append("{\n"); + formatter.incIndent(); + formatter.indent(sb); + sb.append("\"when iterator\" :\n"); + theCondIters[i].display(sb, formatter); + sb.append(",\n"); + formatter.indent(sb); + sb.append("\"then iterator\" :\n"); + theThenIters[i].display(sb, formatter); + sb.append("\n"); + formatter.decIndent(); + formatter.indent(sb); + sb.append("}"); + + if (i < theCondIters.length - 1) { + sb.append(",\n"); + } + } + + if (theElseIter != null) { + sb.append(",\n"); + formatter.indent(sb); + sb.append("{\n"); + formatter.incIndent(); + formatter.indent(sb); + sb.append("\"else iterator\" :\n"); + theElseIter.display(sb, formatter); + sb.append("\n"); + formatter.decIndent(); + formatter.indent(sb); + sb.append("}"); + } + + formatter.decIndent(); + sb.append("\n"); + formatter.indent(sb); + sb.append("]"); + } +} diff --git a/driver/src/main/java/oracle/nosql/driver/query/CompareOpIter.java b/driver/src/main/java/oracle/nosql/driver/query/CompareOpIter.java new file mode 100644 index 00000000..fb90be8e --- /dev/null +++ b/driver/src/main/java/oracle/nosql/driver/query/CompareOpIter.java @@ -0,0 +1,638 @@ +/*- + * Copyright (c) 2011, 2025 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Universal Permissive License v 1.0 as shown at + * https://oss.oracle.com/licenses/upl/ + */ + +package oracle.nosql.driver.query; + +import java.io.IOException; +import java.util.Map; + +import oracle.nosql.driver.query.QueryException.Location; +import oracle.nosql.driver.util.ByteInputStream; +import oracle.nosql.driver.util.SerializationUtil; +import oracle.nosql.driver.values.ArrayValue; +import oracle.nosql.driver.values.FieldValue; +import oracle.nosql.driver.values.FieldValue.Type; +import oracle.nosql.driver.values.BooleanValue; +import oracle.nosql.driver.values.MapValue; +import oracle.nosql.driver.values.NullValue; + +public class CompareOpIter extends PlanIter { + + static public class CompResult { + + public int comp; + public boolean incompatible; + public boolean haveNull; + + void clear() { + comp = 0; + incompatible = false; + haveNull = false; + } + + @Override + public String toString() { + return ("(comp, incompatible, haveNull) = (" + + comp + ", " + incompatible + ", " + haveNull + ")"); + } + } + + static private class CompIterState extends PlanIterState { + + final CompResult theResult = new CompResult(); + + @Override + public void reset(PlanIter iter) { + super.reset(iter); + theResult.clear(); + } + } + + private final FuncCode theCode; + + private final PlanIter theLeftOp; + + private final PlanIter theRightOp; + + public CompareOpIter(ByteInputStream in, short queryVersion) + throws IOException { + + super(in, queryVersion); + short ordinal = in.readShort(); + theCode = FuncCode.valueOf(ordinal); + theLeftOp = deserializeIter(in, queryVersion); + theRightOp = deserializeIter(in, queryVersion); + } + + @Override + public PlanIterKind getKind() { + return PlanIterKind.VALUE_COMPARE; + } + + @Override + FuncCode getFuncCode() { + return theCode; + } + + @Override + public void open(RuntimeControlBlock rcb) { + rcb.setState(theStatePos, new CompIterState()); + theLeftOp.open(rcb); + theRightOp.open(rcb); + } + + @Override + public void reset(RuntimeControlBlock rcb) { + theLeftOp.reset(rcb); + theRightOp.reset(rcb); + PlanIterState state = rcb.getState(theStatePos); + state.reset(this); + } + + @Override + public void close(RuntimeControlBlock rcb) { + + PlanIterState state = rcb.getState(theStatePos); + if (state == null) { + return; + } + + theLeftOp.close(rcb); + theRightOp.close(rcb); + state.close(); + } + + @Override + public boolean next(RuntimeControlBlock rcb) { + + CompIterState state = (CompIterState)rcb.getState(theStatePos); + + if (state.isDone()) { + return false; + } + + boolean leftOpNext = theLeftOp.next(rcb); + + if (leftOpNext && theLeftOp.next(rcb)) { + throw new QueryException( + "The left operand of comparison operator " + theCode + + " is a sequence with more than one items. Comparison " + + "operators cannot operate on sequences of more than one items.", + theLocation); + } + + boolean rightOpNext = theRightOp.next(rcb); + + if (rightOpNext && theRightOp.next(rcb)) { + throw new QueryException( + "The right operand of comparison operator " + theCode + + " is a sequence with more than one items. Comparison " + + "operators cannot operate on sequences of more than one items.", + theLocation); + } + + if (!rightOpNext && !leftOpNext) { + /* both sides are empty */ + state.theResult.comp = 0; + + } else if (!rightOpNext || !leftOpNext) { + /* only one of the sides is empty */ + if (theCode != FuncCode.OP_NEQ) { + /* this will be converted to false */ + state.theResult.incompatible = true; + } else { + /* this will be converted to true */ + state.theResult.comp = 1; + } + + } else { + FieldValue lvalue = rcb.getRegVal(theLeftOp.getResultReg()); + FieldValue rvalue = rcb.getRegVal(theRightOp.getResultReg()); + + assert(lvalue != null && rvalue != null); + + compare(rcb, + lvalue, + rvalue, + theCode, + state.theResult, + getLocation()); + } + + if (state.theResult.haveNull) { + rcb.setRegVal(theResultReg, NullValue.getInstance()); + state.done(); + return true; + } + + if (state.theResult.incompatible) { + rcb.setRegVal(theResultReg, BooleanValue.getInstance(false)); + state.done(); + return true; + } + + int comp = state.theResult.comp; + boolean result; + + switch (theCode) { + case OP_EQ: + result = (comp == 0); + break; + case OP_NEQ: + result = (comp != 0); + break; + case OP_GT: + result = (comp > 0); + break; + case OP_GE: + result = (comp >= 0); + break; + case OP_LT: + result = (comp < 0); + break; + case OP_LE: + result = (comp <= 0); + break; + default: + throw new QueryStateException( + "Invalid operation code: " + theCode); + } + + FieldValue res = BooleanValue.getInstance(result); + rcb.setRegVal(theResultReg, res); + + state.done(); + return true; + } + + /* + * Compare 2 values for the order-relation specified by the given opCode. + * If the values are complex, the method will, in general, call itself + * recursivelly on the contained values. + * + * The method retuns 3 pieces of info (inside the "res" out param): + * + * a. Whether either v0 or v1 is NULL. + * b. Whether the values are not cmparable + * c1. If both a and b are false and the operator is = or !=, an integer which is + * equal to 0 if v0 == v1, and non-0 if v0 != v1. + * c2. If both a nd b are false and the operator is >, >=, <, or <=, an integer + * which is equal to 0 if v0 == v1, greater than 0 if v0 > v1, and + * less than zero if v0 < v1. + */ + public static void compare( + RuntimeControlBlock rcb, + FieldValue v0, + FieldValue v1, + FuncCode opCode, + CompResult res, + Location location) { + + if (rcb != null && rcb.getTraceLevel() >= 4) { + rcb.trace("Comparing values: \n" + v0 + "\n" + v1); + } + + res.clear(); + + if (v0.isNull() || v1.isNull()) { + res.haveNull = true; + return; + } + + if (v0.isJsonNull()) { + + if (v1.isJsonNull()) { + res.comp = 0; + return; + } + + if (opCode != FuncCode.OP_NEQ) { + /* this will be converted to false */ + res.incompatible = true; + return; + } + + /* this will be converted to true */ + res.comp = 1; + return; + } + + if (v1.isJsonNull()) { + + if (opCode != FuncCode.OP_NEQ) { + /* this will be converted to false */ + res.incompatible = true; + return; + } + + /* this will be converted to true */ + res.comp = 1; + return; + } + + Type tc0 = v0.getType(); + Type tc1 = v1.getType(); + + switch (tc0) { + + case EMPTY: + if (tc1 == Type.EMPTY) { + if (opCode == FuncCode.OP_EQ || + opCode == FuncCode.OP_GE || + opCode == FuncCode.OP_LE) { + res.comp = 0; + } else { + res.incompatible = true; + } + + return; + } + + if (opCode == FuncCode.OP_NEQ) { + res.comp = 1; + } else { + res.incompatible = true; + } + + return; + + case INTEGER: { + switch (tc1) { + case INTEGER: + res.comp = v0.compareTo(v1); + return; + case LONG: + res.comp = -v1.compareTo(v0); + return; + case DOUBLE: + res.comp = Double.compare(v0.getInt(), v1.getDouble()); + return; + case NUMBER: + res.comp = -v1.compareTo(v0); + return; + case EMPTY: + if (opCode == FuncCode.OP_NEQ) { + res.comp = 1; + } else { + res.incompatible = true; + } + return; + default: + res.incompatible = true; + return; + } + } + case LONG: { + switch (tc1) { + case INTEGER: + res.comp = v0.compareTo(v1); + return; + case LONG: + res.comp = v0.compareTo(v1); + return; + case DOUBLE: + res.comp = Double.compare(v0.getLong(), v1.getDouble()); + return; + case NUMBER: + res.comp = -v1.compareTo(v0); + return; + case EMPTY: + if (opCode == FuncCode.OP_NEQ) { + res.comp = 1; + } else { + res.incompatible = true; + } + return; + default: + res.incompatible = true; + return; + } + } + case DOUBLE: { + switch (tc1) { + case INTEGER: + res.comp = Double.compare(v0.getDouble(), v1.getInt()); + return; + case LONG: + res.comp = Double.compare(v0.getDouble(), v1.getLong()); + return; + case DOUBLE: + res.comp = Double.compare(v0.getDouble(), v1.getDouble()); + return; + case NUMBER: + res.comp = -v1.compareTo(v0); + return; + case EMPTY: + if (opCode == FuncCode.OP_NEQ) { + res.comp = 1; + } else { + res.incompatible = true; + } + return; + default: + res.incompatible = true; + return; + } + } + case NUMBER: { + switch (tc1) { + case NUMBER: + case DOUBLE: + case INTEGER: + case LONG: + res.comp = v0.compareTo(v1); + return; + case EMPTY: + if (opCode == FuncCode.OP_NEQ) { + res.comp = 1; + } else { + res.incompatible = true; + } + return; + default: + res.incompatible = true; + return; + } + } + case STRING: { + switch (tc1) { + case STRING: + case TIMESTAMP: + res.comp = v0.getString().compareTo(v1.getString()); + + if (rcb != null && rcb.getTraceLevel() >= 3) { + rcb.trace("Comparing STRING " + v0 + " with " + v1); + rcb.trace("res.comp = " + res.comp); + } + + return; + case EMPTY: + if (opCode == FuncCode.OP_NEQ) { + res.comp = 1; + } else { + res.incompatible = true; + } + return; + default: + res.incompatible = true; + return; + } + } + case BOOLEAN: { + switch (tc1) { + case BOOLEAN: + res.comp = v0.compareTo(v1); + return; + case EMPTY: + if (opCode == FuncCode.OP_NEQ) { + res.comp = 1; + } else { + res.incompatible = true; + } + return; + default: + res.incompatible = true; + return; + } + } + case BINARY: { + switch (tc1) { + case BINARY: + if (opCode != FuncCode.OP_EQ && opCode != FuncCode.OP_NEQ) { + res.incompatible = true; + return; + } + res.comp = v0.compareTo(v1); + return; + case EMPTY: + if (opCode == FuncCode.OP_NEQ) { + res.comp = 1; + } else { + res.incompatible = true; + } + return; + default: + res.incompatible = true; + return; + } + } + case TIMESTAMP: { + switch (tc1) { + case TIMESTAMP: + case STRING: + res.comp = v0.getString().compareTo(v1.getString()); + + if (rcb != null && rcb.getTraceLevel() >= 3) { + rcb.trace("Comparing TIMESTAMP " + v0 + " with " + v1); + rcb.trace("res.comp = " + res.comp); + } + + return; + case EMPTY: + if (opCode == FuncCode.OP_NEQ) { + res.comp = 1; + } else { + res.incompatible = true; + } + return; + default: + res.incompatible = true; + return; + } + } + case MAP: { + switch (tc1) { + case MAP: + if (opCode != FuncCode.OP_EQ && opCode != FuncCode.OP_NEQ) { + res.incompatible = true; + return; + } + + MapValue m0 = (MapValue)v0; + MapValue m1 = (MapValue)v1; + compareMaps(rcb, m0, m1, opCode, res, location); + return; + case EMPTY: + if (opCode == FuncCode.OP_NEQ) { + res.comp = 1; + } else { + res.incompatible = true; + } + return; + default: + res.incompatible = true; + return; + } + } + case ARRAY: { + switch (tc1) { + case ARRAY: + ArrayValue a0 = (ArrayValue)v0; + ArrayValue a1 = (ArrayValue)v1; + + if (opCode == FuncCode.OP_EQ || opCode == FuncCode.OP_NEQ) { + if (a0.size() != a1.size()) { + res.comp = 1; + return; + } + } + + int minSize = Math.min(a0.size(), a1.size()); + + for (int i = 0; i < minSize; ++i) { + + FieldValue elem0 = a0.get(i); + FieldValue elem1 = a1.get(i); + assert(elem0 != null); + assert(elem1 != null); + + compare(rcb, elem0, elem1, opCode, res, location); + + if (res.comp != 0 || res.haveNull || res.incompatible) { + return; + } + } + + if (a0.size() != minSize) { + res.comp = 1; + return; + } else if (a1.size() != minSize) { + res.comp = -1; + return; + } else { + res.comp = 0; + return; + } + case EMPTY: + if (opCode == FuncCode.OP_NEQ) { + res.comp = 1; + } else { + res.incompatible = true; + } + return; + default: + res.incompatible = true; + return; + } + } + default: + throw new QueryStateException( + "Unexpected operand type in comparison operator: " + tc0); + } + } + + static void compareMaps( + RuntimeControlBlock rcb, + MapValue v0, + MapValue v1, + FuncCode opCode, + CompResult res, + Location location) { + + if (v0.size() != v1.size()) { + res.comp = 1; + return; + } + + for (Map.Entry e0 : v0.getMap().entrySet()) { + + String k0 = e0.getKey(); + FieldValue fv0 = e0.getValue(); + FieldValue fv1 = v1.get(k0); + + if (fv1 == null) { + res.comp = 1; + return; + } + + compare(rcb, fv0, fv1, opCode, res, location); + + if (res.comp != 0 || res.haveNull || res.incompatible) { + return; + } + } + + res.comp = 0; + return; + } + + @Override + protected void displayContent( + StringBuilder sb, + QueryFormatter formatter) { + + displayInputIter(sb, formatter, theLeftOp); + displayInputIter(sb, formatter, theRightOp); + } + + @Override + void displayName(StringBuilder sb) { + + switch (theCode) { + case OP_GT: + sb.append("GREATER_THAN"); + break; + case OP_GE: + sb.append("GREATER_OR_EQUAL"); + break; + case OP_LT: + sb.append("LESS_THAN"); + break; + case OP_LE: + sb.append("LESS_OR_EQUAL"); + break; + case OP_EQ: + sb.append("EQUAL"); + break; + case OP_NEQ: + sb.append("NOT_EQUAL"); + break; + default: + break; + } + } +} diff --git a/driver/src/main/java/oracle/nosql/driver/query/ConstIter.java b/driver/src/main/java/oracle/nosql/driver/query/ConstIter.java index 4f1bbcc4..fd75f81f 100644 --- a/driver/src/main/java/oracle/nosql/driver/query/ConstIter.java +++ b/driver/src/main/java/oracle/nosql/driver/query/ConstIter.java @@ -22,9 +22,9 @@ public class ConstIter extends PlanIter { final FieldValue theValue; - ConstIter(ByteInputStream in, short serialVersion) throws IOException { + ConstIter(ByteInputStream in, short queryVersion) throws IOException { - super(in, serialVersion); + super(in, queryVersion); theValue = BinaryProtocol.readFieldValue(in); } diff --git a/driver/src/main/java/oracle/nosql/driver/query/ExternalVarRefIter.java b/driver/src/main/java/oracle/nosql/driver/query/ExternalVarRefIter.java index dcc4a798..e42e8a86 100644 --- a/driver/src/main/java/oracle/nosql/driver/query/ExternalVarRefIter.java +++ b/driver/src/main/java/oracle/nosql/driver/query/ExternalVarRefIter.java @@ -37,9 +37,9 @@ public class ExternalVarRefIter extends PlanIter { ExternalVarRefIter( ByteInputStream in, - short serialVersion) throws IOException { + short queryVersion) throws IOException { - super(in, serialVersion); + super(in, queryVersion); theName = SerializationUtil.readString(in); theId = readPositiveInt(in); } diff --git a/driver/src/main/java/oracle/nosql/driver/query/FieldStepIter.java b/driver/src/main/java/oracle/nosql/driver/query/FieldStepIter.java index b4af9409..02f41615 100644 --- a/driver/src/main/java/oracle/nosql/driver/query/FieldStepIter.java +++ b/driver/src/main/java/oracle/nosql/driver/query/FieldStepIter.java @@ -28,10 +28,10 @@ public class FieldStepIter extends PlanIter { private final String theFieldName; - FieldStepIter(ByteInputStream in, short serialVersion) throws IOException { + FieldStepIter(ByteInputStream in, short queryVersion) throws IOException { - super(in, serialVersion); - theInputIter = deserializeIter(in, serialVersion); + super(in, queryVersion); + theInputIter = deserializeIter(in, queryVersion); theFieldName = SerializationUtil.readString(in); } diff --git a/driver/src/main/java/oracle/nosql/driver/query/FuncCollectIter.java b/driver/src/main/java/oracle/nosql/driver/query/FuncCollectIter.java index 10144902..25a83bff 100644 --- a/driver/src/main/java/oracle/nosql/driver/query/FuncCollectIter.java +++ b/driver/src/main/java/oracle/nosql/driver/query/FuncCollectIter.java @@ -105,10 +105,10 @@ public void close() { private final PlanIter theInput; - FuncCollectIter(ByteInputStream in, short serialVersion) throws IOException { - super(in, serialVersion); + FuncCollectIter(ByteInputStream in, short queryVersion) throws IOException { + super(in, queryVersion); theIsDistinct = in.readBoolean(); - theInput = deserializeIter(in, serialVersion); + theInput = deserializeIter(in, queryVersion); } @Override diff --git a/driver/src/main/java/oracle/nosql/driver/query/FuncMinMaxIter.java b/driver/src/main/java/oracle/nosql/driver/query/FuncMinMaxIter.java index 59516ab4..c9d661e6 100644 --- a/driver/src/main/java/oracle/nosql/driver/query/FuncMinMaxIter.java +++ b/driver/src/main/java/oracle/nosql/driver/query/FuncMinMaxIter.java @@ -26,11 +26,11 @@ public class FuncMinMaxIter extends PlanIter { private final PlanIter theInput; - FuncMinMaxIter(ByteInputStream in, short serialVersion) throws IOException { - super(in, serialVersion); + FuncMinMaxIter(ByteInputStream in, short queryVersion) throws IOException { + super(in, queryVersion); short ordinal = in.readShort(); theFuncCode = FuncCode.valueOf(ordinal); - theInput = deserializeIter(in, serialVersion); + theInput = deserializeIter(in, queryVersion); } @Override @@ -99,7 +99,7 @@ public boolean next(RuntimeControlBlock rcb) { } } - private static void minmaxNewVal( + static void minmaxNewVal( RuntimeControlBlock rcb, AggrIterState state, FuncCode fncode, diff --git a/driver/src/main/java/oracle/nosql/driver/query/FuncSeqAggrIter.java b/driver/src/main/java/oracle/nosql/driver/query/FuncSeqAggrIter.java new file mode 100644 index 00000000..813b8100 --- /dev/null +++ b/driver/src/main/java/oracle/nosql/driver/query/FuncSeqAggrIter.java @@ -0,0 +1,258 @@ +/*- + * Copyright (c) 2011, 2025 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Universal Permissive License v 1.0 as shown at + * https://oss.oracle.com/licenses/upl/ + */ + +package oracle.nosql.driver.query; + +import java.io.IOException; +import java.math.BigDecimal; + +import oracle.nosql.driver.util.ByteInputStream; +import oracle.nosql.driver.util.SerializationUtil; +import oracle.nosql.driver.values.FieldValue; +import oracle.nosql.driver.values.DoubleValue; +import oracle.nosql.driver.values.LongValue; +import oracle.nosql.driver.values.NullValue; +import oracle.nosql.driver.values.NumberValue; + +public class FuncSeqAggrIter extends PlanIter { + + private final FuncCode theCode; + + private final PlanIter theInput; + + public FuncSeqAggrIter(ByteInputStream in, short queryVersion) + throws IOException { + + super(in, queryVersion); + short ordinal = in.readShort(); + theCode = FuncCode.valueOf(ordinal); + theInput = deserializeIter(in, queryVersion); + } + + @Override + public PlanIterKind getKind() { + return PlanIterKind.SEQ_AGGR; + } + + @Override + FuncCode getFuncCode() { + return theCode; + } + + @Override + public void open(RuntimeControlBlock rcb) { + rcb.setState(theStatePos, new AggrIterState()); + theInput.open(rcb); + } + + @Override + public void reset(RuntimeControlBlock rcb) { + theInput.reset(rcb); + PlanIterState state = rcb.getState(theStatePos); + state.reset(this); + } + + @Override + public void close(RuntimeControlBlock rcb) { + + PlanIterState state = rcb.getState(theStatePos); + if (state == null) { + return; + } + + theInput.close(rcb); + + state.close(); + } + + @Override + public boolean next(RuntimeControlBlock rcb) { + + AggrIterState state = (AggrIterState)rcb.getState(theStatePos); + + if (state.isDone()) { + return false; + } + + boolean more = theInput.next(rcb); + + if (!more) { + state.done(); + + if (theCode == FuncCode.FN_SEQ_COUNT || + theCode == FuncCode.FN_SEQ_COUNT_I) { + rcb.setRegVal(theResultReg, new LongValue(0)); + + return true; + } + + return false; + } + + switch (theCode) { + case FN_SEQ_COUNT: + case FN_SEQ_COUNT_I: + nextCount(rcb, state); + break; + case FN_SEQ_COUNT_NUMBERS_I: + nextCountNumbers(rcb, state); + break; + case FN_SEQ_SUM: + case FN_SEQ_AVG: + nextSumAvg(rcb, state); + break; + case FN_SEQ_MIN: + case FN_SEQ_MAX: + case FN_SEQ_MIN_I: + case FN_SEQ_MAX_I: + nextMinMax(rcb, state); + break; + default: + throw new QueryStateException("Unexpected function: " + theCode); + } + + state.done(); + return true; + } + + private void nextCount(RuntimeControlBlock rcb, AggrIterState state) { + + boolean more = true; + + while (more) { + + FieldValue val = rcb.getRegVal(theInput.getResultReg()); + + if (val.isNull()) { + if (theCode == FuncCode.FN_SEQ_COUNT) { + rcb.setRegVal(theResultReg, NullValue.getInstance()); + return; + } + more = theInput.next(rcb); + continue; + } + + ++state.theCount; + more = theInput.next(rcb); + } + + rcb.setRegVal(theResultReg, new LongValue(state.theCount)); + } + + private void nextCountNumbers(RuntimeControlBlock rcb, AggrIterState state) { + + boolean more = true; + + while (more) { + + FieldValue val = rcb.getRegVal(theInput.getResultReg()); + + if (val.isNumeric()) { + ++state.theCount; + } + + more = theInput.next(rcb); + } + + rcb.setRegVal(theResultReg, new LongValue(state.theCount)); + } + + private void nextSumAvg(RuntimeControlBlock rcb, AggrIterState state) { + + boolean more = true; + + while (more) { + + FieldValue val = rcb.getRegVal(theInput.getResultReg()); + + FuncSumIter.sumNewValue(state, val); + + more = theInput.next(rcb); + } + + if (!state.theGotNumericInput) { + rcb.setRegVal(theResultReg, NullValue.getInstance()); + return; + } + + FieldValue res = null; + + if (theCode == FuncCode.FN_SEQ_SUM) { + + switch (state.theSumType) { + case LONG: + res = new LongValue(state.theLongSum); + break; + case DOUBLE: + res = new DoubleValue(state.theDoubleSum); + break; + case NUMBER: + res = new NumberValue(state.theNumberSum); + break; + default: + throw new QueryStateException( + "Unexpected result type for SUM function: " + + state.theSumType); + } + } else { + double avg; + + switch (state.theSumType) { + case LONG: + avg = state.theLongSum / (double)state.theCount; + res = new DoubleValue(avg); + break; + case DOUBLE: + avg = state.theDoubleSum / state.theCount; + res = new DoubleValue(avg); + break; + case NUMBER: + BigDecimal bcount = new BigDecimal(state.theCount); + BigDecimal bavg = state.theNumberSum. + divide(bcount, rcb.getMathContext()); + res = new NumberValue(bavg); + break; + default: + throw new QueryStateException( + "Unexpected result type for SUM function: " + + state.theSumType); + } + } + + rcb.setRegVal(theResultReg, res); + } + + private void nextMinMax(RuntimeControlBlock rcb, AggrIterState state) { + + boolean more = true; + + while (more) { + + FieldValue val = rcb.getRegVal(theInput.getResultReg()); + + if (val.isNull() && + (theCode == FuncCode.FN_SEQ_MIN || + theCode == FuncCode.FN_SEQ_MAX)) { + rcb.setRegVal(theResultReg, val); + return; + } + + FuncMinMaxIter.minmaxNewVal(rcb, state, theCode, val); + + more = theInput.next(rcb); + } + + rcb.setRegVal(theResultReg, state.theMinMax); + } + + @Override + protected void displayContent( + StringBuilder sb, + QueryFormatter formatter) { + displayInputIter(sb, formatter, theInput); + } +} diff --git a/driver/src/main/java/oracle/nosql/driver/query/FuncSizeIter.java b/driver/src/main/java/oracle/nosql/driver/query/FuncSizeIter.java index 7f522e41..f173ba0a 100644 --- a/driver/src/main/java/oracle/nosql/driver/query/FuncSizeIter.java +++ b/driver/src/main/java/oracle/nosql/driver/query/FuncSizeIter.java @@ -20,9 +20,9 @@ public class FuncSizeIter extends PlanIter { private final PlanIter theInput; - FuncSizeIter(ByteInputStream in, short serialVersion) throws IOException { - super(in, serialVersion); - theInput = deserializeIter(in, serialVersion); + FuncSizeIter(ByteInputStream in, short queryVersion) throws IOException { + super(in, queryVersion); + theInput = deserializeIter(in, queryVersion); } @Override diff --git a/driver/src/main/java/oracle/nosql/driver/query/FuncSumIter.java b/driver/src/main/java/oracle/nosql/driver/query/FuncSumIter.java index 1440c375..dfd47422 100644 --- a/driver/src/main/java/oracle/nosql/driver/query/FuncSumIter.java +++ b/driver/src/main/java/oracle/nosql/driver/query/FuncSumIter.java @@ -35,9 +35,9 @@ public class FuncSumIter extends PlanIter { private final PlanIter theInput; - FuncSumIter(ByteInputStream in, short serialVersion) throws IOException { - super(in, serialVersion); - theInput = deserializeIter(in, serialVersion); + FuncSumIter(ByteInputStream in, short queryVersion) throws IOException { + super(in, queryVersion); + theInput = deserializeIter(in, queryVersion); } @Override @@ -101,12 +101,6 @@ public boolean next(RuntimeControlBlock rcb) { rcb.trace("Summing up value " + val); } - if (val.isNull()) { - continue; - } - - state.theNullInputOnly = false; - sumNewValue(state, val); } } @@ -117,6 +111,7 @@ static void sumNewValue(AggrIterState state, FieldValue val) { switch (val.getType()) { case INTEGER: { + state.theGotNumericInput = true; ++state.theCount; switch (state.theSumType) { case LONG: @@ -135,6 +130,7 @@ static void sumNewValue(AggrIterState state, FieldValue val) { break; } case LONG: { + state.theGotNumericInput = true; ++state.theCount; switch (state.theSumType) { case LONG: @@ -153,6 +149,7 @@ static void sumNewValue(AggrIterState state, FieldValue val) { break; } case DOUBLE: { + state.theGotNumericInput = true; ++state.theCount; switch (state.theSumType) { case LONG: @@ -173,6 +170,7 @@ static void sumNewValue(AggrIterState state, FieldValue val) { break; } case NUMBER: { + state.theGotNumericInput = true; ++state.theCount; if (state.theNumberSum == null) { state.theNumberSum = new BigDecimal(0); @@ -180,13 +178,13 @@ static void sumNewValue(AggrIterState state, FieldValue val) { switch (state.theSumType) { case LONG: - state.theNumberSum = new BigDecimal(state.theLongSum); + state.theNumberSum = new BigDecimal(state.theLongSum); state.theNumberSum = state.theNumberSum.add(((NumberValue)val).getValue()); state.theSumType = Type.NUMBER; break; case DOUBLE: - state.theNumberSum = new BigDecimal(state.theDoubleSum); + state.theNumberSum = new BigDecimal(state.theDoubleSum); state.theNumberSum = state.theNumberSum.add(((NumberValue)val).getValue()); state.theSumType = Type.NUMBER; @@ -220,7 +218,7 @@ FieldValue getAggrValue(RuntimeControlBlock rcb, boolean reset) { AggrIterState state = (AggrIterState)rcb.getState(theStatePos); FieldValue res = null; - if (state.theNullInputOnly) { + if (!state.theGotNumericInput) { return NullValue.getInstance(); } diff --git a/driver/src/main/java/oracle/nosql/driver/query/GroupIter.java b/driver/src/main/java/oracle/nosql/driver/query/GroupIter.java index 26469b75..efd461a5 100644 --- a/driver/src/main/java/oracle/nosql/driver/query/GroupIter.java +++ b/driver/src/main/java/oracle/nosql/driver/query/GroupIter.java @@ -82,7 +82,11 @@ private static class AggrValue { boolean theGotNumericInput; - AggrValue(FuncCode aggrIterKind) { + boolean theIsRegrouping; + + AggrValue(FuncCode aggrIterKind, boolean isRegrouping) { + + theIsRegrouping = isRegrouping; switch (aggrIterKind) { case FN_COUNT: @@ -149,12 +153,20 @@ void collect( } } else { ArrayValue collectArray = (ArrayValue)theValue; - ArrayValue arrayVal = (ArrayValue)val; - collectArray.addAll(arrayVal.iterator()); - if (countMemory) { - rcb.incMemoryConsumption(val.sizeof() + - SizeOf.OBJECT_REF_OVERHEAD * - arrayVal.size()); + if (theIsRegrouping) { + ArrayValue arrayVal = (ArrayValue)val; + collectArray.addAll(arrayVal.iterator()); + if (countMemory) { + rcb.incMemoryConsumption(val.sizeof() + + SizeOf.OBJECT_REF_OVERHEAD * + arrayVal.size()); + } + } else { + collectArray.add(val); + if (countMemory) { + rcb.incMemoryConsumption(val.sizeof() + + SizeOf.OBJECT_REF_OVERHEAD); + } } } } @@ -364,11 +376,13 @@ public void close() { private final boolean theCountMemory; - public GroupIter(ByteInputStream in, short serialVersion) throws IOException { + private final boolean theIsRegrouping; - super(in, serialVersion); + public GroupIter(ByteInputStream in, short queryVersion) throws IOException { - theInput = deserializeIter(in, serialVersion); + super(in, queryVersion); + + theInput = deserializeIter(in, queryVersion); theNumGBColumns = in.readInt(); theColumnNames = SerializationUtil.readStringArray(in); @@ -384,6 +398,11 @@ public GroupIter(ByteInputStream in, short serialVersion) throws IOException { theIsDistinct = in.readBoolean(); theRemoveProducedResult = in.readBoolean(); theCountMemory = in.readBoolean(); + if (queryVersion >= QueryDriver.QUERY_V6) { + theIsRegrouping = in.readBoolean(); + } else { + theIsRegrouping = false; + } } @Override @@ -511,7 +530,7 @@ public boolean next(RuntimeControlBlock rcb) { long aggrTupleSize = 0; for (i = 0; i < numAggrColumns; ++i) { - aggrTuple[i] = new AggrValue(theAggrFuncs[i]); + aggrTuple[i] = new AggrValue(theAggrFuncs[i], theIsRegrouping); if (theCountMemory) { aggrTupleSize += aggrTuple[i].sizeof(); } diff --git a/driver/src/main/java/oracle/nosql/driver/query/IsNullIter.java b/driver/src/main/java/oracle/nosql/driver/query/IsNullIter.java new file mode 100644 index 00000000..0bc9844f --- /dev/null +++ b/driver/src/main/java/oracle/nosql/driver/query/IsNullIter.java @@ -0,0 +1,117 @@ +/*- + * Copyright (c) 2011, 2025 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Universal Permissive License v 1.0 as shown at + * https://oss.oracle.com/licenses/upl/ + */ + +package oracle.nosql.driver.query; + +import java.io.IOException; + +import oracle.nosql.driver.query.QueryException.Location; +import oracle.nosql.driver.util.ByteInputStream; +import oracle.nosql.driver.util.SerializationUtil; +import oracle.nosql.driver.values.FieldValue; +import oracle.nosql.driver.values.BooleanValue; + +public class IsNullIter extends PlanIter { + + private final FuncCode theCode; + + private final PlanIter theArg; + + public IsNullIter(ByteInputStream in, short queryVersion) + throws IOException { + + super(in, queryVersion); + short ordinal = in.readShort(); + theCode = FuncCode.valueOf(ordinal); + theArg = deserializeIter(in, queryVersion); + } + + @Override + public PlanIterKind getKind() { + return PlanIterKind.IS_NULL; + } + + @Override + FuncCode getFuncCode() { + return theCode; + } + + @Override + public void open(RuntimeControlBlock rcb) { + rcb.setState(theStatePos, new PlanIterState()); + theArg.open(rcb); + } + + @Override + public void reset(RuntimeControlBlock rcb) { + theArg.reset(rcb); + PlanIterState state = rcb.getState(theStatePos); + state.reset(this); + } + + @Override + public void close(RuntimeControlBlock rcb) { + + PlanIterState state = rcb.getState(theStatePos); + if (state == null) { + return; + } + + theArg.close(rcb); + + state.close(); + } + + @Override + public boolean next(RuntimeControlBlock rcb) { + + PlanIterState state = rcb.getState(theStatePos); + + if (state.isDone()) { + return false; + } + + boolean more = theArg.next(rcb); + + if (!more) { + if (theCode == FuncCode.OP_IS_NULL) { + rcb.setRegVal(theResultReg, BooleanValue.getInstance(false)); + } else { + rcb.setRegVal(theResultReg, BooleanValue.getInstance(true)); + } + + state.done(); + return true; + } + + FieldValue val = rcb.getRegVal(theArg.getResultReg()); + + if (theCode == FuncCode.OP_IS_NULL) { + if (val.isNull()) { + rcb.setRegVal(theResultReg, BooleanValue.getInstance(true)); + } else { + rcb.setRegVal(theResultReg, BooleanValue.getInstance(false)); + } + } else { + if (val.isNull()) { + rcb.setRegVal(theResultReg, BooleanValue.getInstance(false)); + } else { + rcb.setRegVal(theResultReg, BooleanValue.getInstance(true)); + } + } + + state.done(); + return true; + } + + @Override + protected void displayContent( + StringBuilder sb, + QueryFormatter formatter) { + displayInputIter(sb, formatter, theArg); + } +} diff --git a/driver/src/main/java/oracle/nosql/driver/query/PlanIter.java b/driver/src/main/java/oracle/nosql/driver/query/PlanIter.java index 0ae74115..e74a15c7 100644 --- a/driver/src/main/java/oracle/nosql/driver/query/PlanIter.java +++ b/driver/src/main/java/oracle/nosql/driver/query/PlanIter.java @@ -102,25 +102,27 @@ public abstract class PlanIter { */ public static enum PlanIterKind { - RECV(17), - SFW(14), - SORT(47), - CONST(0), VAR_REF(1), EXTERNAL_VAR_REF(2), - - FIELD_STEP(11), - + ARRAY_CONSTRUCTOR(3), + VALUE_COMPARE(5), + AND_OR(7), ARITH_OP(8), - + FIELD_STEP(11), + SFW(14), FN_SIZE(15), + RECV(17), + CASE(19), + IS_NULL(26), FN_SUM(39), FN_MIN_MAX(41), - + SEQ_AGGR(48), + SORT(47), GROUP(65), SORT2(66), - FN_COLLECT(78); + FN_COLLECT(78), + UNION(90); private static final PlanIterKind[] VALUES = values(); @@ -151,15 +153,36 @@ static PlanIterKind valueOf(int kvcode) { */ static enum FuncCode { + OP_AND(0), + OP_OR(1), + OP_EQ(2), + OP_NEQ(3), + OP_GT(4), + OP_GE(5), + OP_LT(6), + OP_LE(7), + OP_ADD_SUB(14), OP_MULT_DIV(15), + OP_IS_NULL(22), + OP_IS_NOT_NULL(23), + FN_COUNT_STAR(42), FN_COUNT(43), FN_COUNT_NUMBERS(44), FN_SUM(45), FN_MIN(47), FN_MAX(48), + FN_SEQ_COUNT(49), + FN_SEQ_SUM(50), + FN_SEQ_AVG(51), + FN_SEQ_MIN(52), + FN_SEQ_MAX(53), + FN_SEQ_COUNT_I(76), + FN_SEQ_COUNT_NUMBERS_I(77), + FN_SEQ_MIN_I(78), + FN_SEQ_MAX_I(79), FN_ARRAY_COLLECT(91), FN_ARRAY_COLLECT_DISTINCT(92); @@ -193,7 +216,7 @@ public static FuncCode valueOf(int kvcode) { protected PlanIter( ByteInputStream in, - short serialVersion) throws IOException { + short queryVersion) throws IOException { theResultReg = readPositiveInt(in, true); theStatePos = readPositiveInt(in); @@ -306,9 +329,39 @@ protected abstract void displayContent( StringBuilder sb, QueryFormatter formatter); + void displayInputIter( + StringBuilder sb, + QueryFormatter formatter, + PlanIter iter) { + + formatter.indent(sb); + sb.append("\"input iterator\" :\n"); + iter.display(sb, formatter); + } + + void displayInputIters( + StringBuilder sb, + QueryFormatter formatter, + PlanIter[] iters) { + + formatter.indent(sb); + sb.append("\"input iterators\" : [\n"); + formatter.incIndent(); + for (int i = 0; i < iters.length; ++i) { + iters[i].display(sb, formatter); + if (i < iters.length - 1) { + sb.append(",\n"); + } + } + sb.append("\n"); + formatter.decIndent(); + formatter.indent(sb); + sb.append("]"); + } + public static PlanIter deserializeIter( ByteInputStream in, - short serialVersion) throws IOException { + short queryVersion) throws IOException { int ord = in.readByte(); @@ -327,43 +380,64 @@ public static PlanIter deserializeIter( switch (kind) { case SORT: case SORT2: - iter = new SortIter(in, kind, serialVersion); + iter = new SortIter(in, kind, queryVersion); break; case GROUP: - iter = new GroupIter(in, serialVersion); + iter = new GroupIter(in, queryVersion); break; case SFW: - iter = new SFWIter(in, serialVersion); + iter = new SFWIter(in, queryVersion); break; case RECV: - iter = new ReceiveIter(in, serialVersion); + iter = new ReceiveIter(in, queryVersion); break; case CONST: - iter = new ConstIter(in, serialVersion); + iter = new ConstIter(in, queryVersion); break; case VAR_REF: - iter = new VarRefIter(in, serialVersion); + iter = new VarRefIter(in, queryVersion); break; case EXTERNAL_VAR_REF: - iter = new ExternalVarRefIter(in, serialVersion); + iter = new ExternalVarRefIter(in, queryVersion); break; case FIELD_STEP: - iter = new FieldStepIter(in, serialVersion); + iter = new FieldStepIter(in, queryVersion); break; case ARITH_OP: - iter = new ArithOpIter(in, serialVersion); + iter = new ArithOpIter(in, queryVersion); break; case FN_SIZE: - iter = new FuncSizeIter(in, serialVersion); + iter = new FuncSizeIter(in, queryVersion); break; case FN_SUM: - iter = new FuncSumIter(in, serialVersion); + iter = new FuncSumIter(in, queryVersion); break; case FN_MIN_MAX: - iter = new FuncMinMaxIter(in, serialVersion); + iter = new FuncMinMaxIter(in, queryVersion); break; case FN_COLLECT: - iter = new FuncCollectIter(in, serialVersion); + iter = new FuncCollectIter(in, queryVersion); + break; + case UNION: + iter = new UnionIter(in, queryVersion); + break; + case AND_OR: + iter = new AndOrIter(in, queryVersion); + break; + case VALUE_COMPARE: + iter = new CompareOpIter(in, queryVersion); + break; + case IS_NULL: + iter = new IsNullIter(in, queryVersion); + break; + case SEQ_AGGR: + iter = new FuncSeqAggrIter(in, queryVersion); + break; + case ARRAY_CONSTRUCTOR: + iter = new ArrayConstrIter(in, queryVersion); + break; + case CASE: + iter = new CaseIter(in, queryVersion); break; default: throw new QueryStateException( @@ -379,12 +453,12 @@ public static PlanIter deserializeIter( static PlanIter[] deserializeIters( ByteInputStream in, - short serialVersion) throws IOException { + short queryVersion) throws IOException { final int numArgs = SerializationUtil.readSequenceLength(in); final PlanIter[] iters = new PlanIter[numArgs]; for (int i = 0; i < numArgs; i++) { - iters[i] = deserializeIter(in, serialVersion); + iters[i] = deserializeIter(in, queryVersion); } return iters; } diff --git a/driver/src/main/java/oracle/nosql/driver/query/QueryDriver.java b/driver/src/main/java/oracle/nosql/driver/query/QueryDriver.java index efa45b16..86097c83 100644 --- a/driver/src/main/java/oracle/nosql/driver/query/QueryDriver.java +++ b/driver/src/main/java/oracle/nosql/driver/query/QueryDriver.java @@ -38,7 +38,10 @@ public class QueryDriver { /* Changed VirtualScan info exchanged between sdk and proxy */ public static final short QUERY_V5 = 5; - public static final short QUERY_VERSION = QUERY_V5; + /* Added UNION */ + public static final short QUERY_V6 = 6; + + public static final short QUERY_VERSION = QUERY_V6; private static final int BATCH_SIZE = 100; @@ -90,6 +93,10 @@ public void setPrepCost(int cost) { thePrepCost = cost; } + public int getUnionBranch() { + return theRCB.getUnionBranch(); + } + /** * Computes a batch of results and fills-in the given QueryResult. */ diff --git a/driver/src/main/java/oracle/nosql/driver/query/ReceiveIter.java b/driver/src/main/java/oracle/nosql/driver/query/ReceiveIter.java index 0cc85be1..f101cf76 100644 --- a/driver/src/main/java/oracle/nosql/driver/query/ReceiveIter.java +++ b/driver/src/main/java/oracle/nosql/driver/query/ReceiveIter.java @@ -190,9 +190,9 @@ void clear() { public ReceiveIter( ByteInputStream in, - short serialVersion) throws IOException { + short queryVersion) throws IOException { - super(in, serialVersion); + super(in, queryVersion); short ordinal = in.readShort(); theDistributionKind = DistributionKind.values()[ordinal]; @@ -736,10 +736,16 @@ void fetch() { reqCopy.setLimit((int)numResults); } + if (!theRCB.reachedLimit()) { + reqCopy.setMaxReadKB(origRequest.getMaxReadKB() - + theRCB.getReadKB()); + } + if (theRCB.getTraceLevel() >= 1) { theRCB.trace("RemoteScanner : executing remote batch " + - origRequest.getBatchCounter() + ". spid = " + - theShardOrPartId); + origRequest.getBatchCounter() + + " with max read KB " + reqCopy.getMaxReadKB() + + ". spid = " + theShardOrPartId); if (theVirtualScan != null) { theRCB.trace("RemoteScanner : request is for virtual scan:\n" + theVirtualScan); diff --git a/driver/src/main/java/oracle/nosql/driver/query/RuntimeControlBlock.java b/driver/src/main/java/oracle/nosql/driver/query/RuntimeControlBlock.java index 2a8b41af..51ad2212 100644 --- a/driver/src/main/java/oracle/nosql/driver/query/RuntimeControlBlock.java +++ b/driver/src/main/java/oracle/nosql/driver/query/RuntimeControlBlock.java @@ -80,6 +80,9 @@ public class RuntimeControlBlock { */ long theMemoryConsumption; + /* The current UNION branch */ + private int theUnionBranch; + private final StringBuilder theTraceBuilder = new StringBuilder(); public RuntimeControlBlock( @@ -192,6 +195,18 @@ PlanIter getRootIter() { return theRootIter; } + int getUnionBranch() { + return theUnionBranch; + } + + void setUnionBranch(int b) { + theUnionBranch = b; + } + + void incUnionBranch() { + ++theUnionBranch; + } + public void setState(int pos, PlanIterState state) { theIteratorStates[pos] = state; } diff --git a/driver/src/main/java/oracle/nosql/driver/query/SFWIter.java b/driver/src/main/java/oracle/nosql/driver/query/SFWIter.java index f9478af1..55f54f99 100644 --- a/driver/src/main/java/oracle/nosql/driver/query/SFWIter.java +++ b/driver/src/main/java/oracle/nosql/driver/query/SFWIter.java @@ -67,17 +67,17 @@ public void reset(PlanIter iter) { private final PlanIter theLimitIter; - SFWIter(ByteInputStream in, short serialVersion) throws IOException { + SFWIter(ByteInputStream in, short queryVersion) throws IOException { - super(in, serialVersion); + super(in, queryVersion); theColumnNames = SerializationUtil.readStringArray(in); theNumGBColumns = in.readInt(); theFromVarName = SerializationUtil.readString(in); theIsSelectStar = in.readBoolean(); - theColumnIters = deserializeIters(in, serialVersion); - theFromIter = deserializeIter(in, serialVersion); - theOffsetIter = deserializeIter(in, serialVersion); - theLimitIter = deserializeIter(in, serialVersion); + theColumnIters = deserializeIters(in, queryVersion); + theFromIter = deserializeIter(in, queryVersion); + theOffsetIter = deserializeIter(in, queryVersion); + theLimitIter = deserializeIter(in, queryVersion); } @Override diff --git a/driver/src/main/java/oracle/nosql/driver/query/SortIter.java b/driver/src/main/java/oracle/nosql/driver/query/SortIter.java index d148666d..938b3ea6 100644 --- a/driver/src/main/java/oracle/nosql/driver/query/SortIter.java +++ b/driver/src/main/java/oracle/nosql/driver/query/SortIter.java @@ -64,11 +64,11 @@ public void close() { private final boolean theCountMemory; - public SortIter(ByteInputStream in, PlanIterKind kind, short serialVersion) + public SortIter(ByteInputStream in, PlanIterKind kind, short queryVersion) throws IOException { - super(in, serialVersion); - theInput = deserializeIter(in, serialVersion); + super(in, queryVersion); + theInput = deserializeIter(in, queryVersion); theSortFields = SerializationUtil.readStringArray(in); theSortSpecs = readSortSpecs(in); diff --git a/driver/src/main/java/oracle/nosql/driver/query/UnionIter.java b/driver/src/main/java/oracle/nosql/driver/query/UnionIter.java new file mode 100644 index 00000000..d176adf9 --- /dev/null +++ b/driver/src/main/java/oracle/nosql/driver/query/UnionIter.java @@ -0,0 +1,343 @@ +/*- + * Copyright (c) 2011, 2025 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Universal Permissive License v 1.0 as shown at + * https://oss.oracle.com/licenses/upl/ + */ + +package oracle.nosql.driver.query; + +import java.io.IOException; +import java.util.TreeSet; + +import oracle.nosql.driver.util.ByteInputStream; +import oracle.nosql.driver.util.SerializationUtil; +import oracle.nosql.driver.values.FieldValue; +import oracle.nosql.driver.values.MapValue; + +public class UnionIter extends PlanIter { + + class SortedBranch implements Comparable { + + final RuntimeControlBlock theRCB; + + final int theBranchNo; + + final PlanIter theBranch; + + MapValue theCurrentResult; + + boolean theIsDone; + + SortedBranch(RuntimeControlBlock rcb, PlanIter branch, int branchNo) { + theRCB = rcb; + theBranchNo = branchNo; + theBranch = branch; + } + + @Override + public int compareTo(SortedBranch other) { + + if (theCurrentResult == null) { + //theRCB.trace("branch " + theBranchNo + " has no result"); + return -1; + } + + if (other.theCurrentResult == null) { + //theRCB.trace("other branch " + other.theBranchNo + " has no result"); + return 1; + } + + int cmp = Compare.sortResults(theRCB, + theCurrentResult, + other.theCurrentResult, + theSortFields, + theSortSpecs); + //theRCB.trace("branch " + theBranchNo + " has result " + + // theCurrentResult + "\nbranch " + other.theBranchNo + + // " has result " + other.theCurrentResult + + // "\ncmp = " + cmp); + if (cmp == 0) { + return (theBranchNo < other.theBranchNo ? -1 : 1); + } + return cmp; + } + + MapValue next() { + + MapValue res = theCurrentResult; + + boolean more = theBranch.next(theRCB); + if (more) { + setCurrentResult(); + } else { + if (!theRCB.reachedLimit()) { + theIsDone = true; + } + theCurrentResult = null; + } + + return res; + } + + void setCurrentResult() { + FieldValue val = theRCB.getRegVal(theBranch.getResultReg()); + if (val.isMap()) { + theCurrentResult = (MapValue)val; + } else { + throw new QueryStateException( + "Union branch should produce a map value"); + } + + if (theRCB.getTraceLevel() >= 3) { + theRCB.trace("UNION: branch " + theBranchNo + " received result\n" + + theCurrentResult); + } + } + + boolean isDone() { + return theIsDone; + } + } + + public static class UnionState extends PlanIterState { + + private int theCurrentBranch; + + private final TreeSet theSortedBranches; + + UnionState() { + theSortedBranches = new TreeSet(); + } + + @Override + public void reset(PlanIter iter) { + super.reset(iter); + theCurrentBranch = 0; + theSortedBranches.clear(); + } + } + + private final PlanIter[] theBranches; + + private final String[] theSortFields; + + private final SortSpec[] theSortSpecs; + + public UnionIter(ByteInputStream in, short queryVersion) + throws IOException { + + super(in, queryVersion); + theBranches = deserializeIters(in, queryVersion); + theSortFields = SerializationUtil.readStringArray(in); + theSortSpecs = readSortSpecs(in); + } + + @Override + public PlanIterKind getKind() { + return PlanIterKind.UNION; + } + + @Override + public void open(RuntimeControlBlock rcb) { + + UnionState state = new UnionState(); + rcb.setState(theStatePos, state); + + if (theSortFields == null) { + PlanIter branch = theBranches[state.theCurrentBranch]; + branch.open(rcb); + } else { + for (int i = 0; i < theBranches.length; ++i) { + PlanIter branch = theBranches[i]; + branch.open(rcb); + SortedBranch sb = new SortedBranch(rcb, branch, i); + state.theSortedBranches.add(sb); + } + } + } + + @Override + public void reset(RuntimeControlBlock rcb) { + throw new QueryStateException("Unexpected call"); + } + + @Override + public void close(RuntimeControlBlock rcb) { + + UnionState state = (UnionState)rcb.getState(theStatePos); + if (state == null) { + return; + } + + for (PlanIter branch : theBranches) { + branch.close(rcb); + } + + state.close(); + } + + @Override + public boolean next(RuntimeControlBlock rcb) { + + if (theSortFields == null) { + return simpleNext(rcb); + } + return sortingNext(rcb); + } + + private boolean simpleNext(RuntimeControlBlock rcb) { + + UnionState state = (UnionState)rcb.getState(theStatePos); + + if (state.isDone()) { + return false; + } + + while (state.theCurrentBranch < theBranches.length) { + + PlanIter branch = theBranches[state.theCurrentBranch]; + + boolean more = branch.next(rcb); + + if (more) { + FieldValue res = rcb.getRegVal(branch.getResultReg()); + rcb.setRegVal(theResultReg, res); + if (rcb.getTraceLevel() >= 3) { + rcb.trace("UNION: got result from branch " + + state.theCurrentBranch + "\n" + res); + } + return true; + } + + if (rcb.reachedLimit()) { + return false; + } + + ++state.theCurrentBranch; + rcb.incUnionBranch(); + + if (rcb.getTraceLevel() >= 3) { + rcb.trace("UNION: moved to branch " + + state.theCurrentBranch); + } + + if (state.theCurrentBranch < theBranches.length) { + branch = theBranches[state.theCurrentBranch]; + branch.open(rcb); + } + + if (rcb.getTraceLevel() >= 3) { + rcb.trace("UNION: moved to branch " + state.theCurrentBranch); + } + } + + if (rcb.getTraceLevel() >= 3) { + rcb.trace("UNION: is done " + state.theCurrentBranch); + } + state.done(); + return false; + } + + private boolean sortingNext(RuntimeControlBlock rcb) { + + UnionState state = (UnionState)rcb.getState(theStatePos); + + if (state.isDone()) { + return false; + } + + while (true) { + SortedBranch sb = state.theSortedBranches.pollFirst(); + + if (sb == null) { + state.done(); + return false; + } + + rcb.setUnionBranch(sb.theBranchNo); + + if (rcb.getTraceLevel() >= 3) { + rcb.trace("UNION: requesting result from branch " + + sb.theBranchNo); + } + + MapValue res = sb.next(); + + if (res != null) { + + rcb.setRegVal(theResultReg, res); + if (rcb.getTraceLevel() >= 3) { + rcb.trace("UNION: got result from branch " + + sb.theBranchNo + "\n" + res); + } + + if (!sb.isDone()) { + state.theSortedBranches.add(sb); + } else { + if (rcb.getTraceLevel() >= 1) { + rcb.trace("UNION : done with branch " + sb.theBranchNo); + } + } + + return true; + } + + if (!sb.isDone()) { + state.theSortedBranches.add(sb); + } + + if (rcb.reachedLimit()) { + return false; + } + } + } + + @Override + protected void displayContent(StringBuilder sb, QueryFormatter formatter) { + + formatter.indent(sb); + sb.append("\"branches\" : [\n"); + formatter.incIndent(); + for (int i = 0; i < theBranches.length; ++i) { + theBranches[i].display(sb, formatter); + if (i < theBranches.length - 1) { + sb.append(",\n"); + } else { + sb.append("\n"); + } + } + formatter.decIndent(); + formatter.indent(sb); + sb.append("]"); + + if (theSortFields != null) { + + sb.append(",\n"); + formatter.indent(sb); + sb.append("\"order by fields\" : [ "); + for (int i = 0; i < theSortFields.length; ++i) { + sb.append(theSortFields[i]); + if (i < theSortFields.length - 1) { + sb.append(", "); + } + } + sb.append(" ]"); + + sb.append(",\n"); + formatter.indent(sb); + sb.append("\"sort specs\" : [ "); + for (int i = 0; i < theSortSpecs.length; ++i) { + sb.append("{ \"desc\" : ").append(theSortSpecs[i].theIsDesc); + sb.append(", \"nulls_first\" : ").append(theSortSpecs[i].theNullsFirst); + sb.append(" }"); + if (i < theSortSpecs.length - 1) { + sb.append(", "); + } + } + sb.append(" ]"); + } else { + sb.append("\n"); + } + } +} diff --git a/driver/src/main/java/oracle/nosql/driver/query/VarRefIter.java b/driver/src/main/java/oracle/nosql/driver/query/VarRefIter.java index c2064a8a..b5e05cc3 100644 --- a/driver/src/main/java/oracle/nosql/driver/query/VarRefIter.java +++ b/driver/src/main/java/oracle/nosql/driver/query/VarRefIter.java @@ -32,9 +32,9 @@ public class VarRefIter extends PlanIter { private final String theName; - VarRefIter(ByteInputStream in, short serialVersion) throws IOException { + VarRefIter(ByteInputStream in, short queryVersion) throws IOException { - super(in, serialVersion); + super(in, queryVersion); theName = SerializationUtil.readString(in); }