Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
9476e41
Add PPL outputlookup command (synchronous terminal write sink)
noCharger Jul 14, 2026
92cfd5f
Harden outputlookup failure cases (key_field validation, dest guard, …
noCharger Jul 14, 2026
e1d19cd
Merge remote-tracking branch 'origin/main' into feature/ppl-outputloo…
noCharger Jul 15, 2026
4c9fb21
Surface outputlookup write params in explain + add user doc
noCharger Jul 15, 2026
d39455c
Merge remote-tracking branch 'origin/main' into feature/ppl-outputloo…
noCharger Jul 15, 2026
b1c4ccb
outputlookup: migration-aware overwrite for data-importer (#11303) lo…
noCharger Jul 15, 2026
7faebf1
outputlookup C2: plain-index substrate + lookup-marker guard + indice…
noCharger Jul 15, 2026
a986700
Apply spotless formatting
noCharger Jul 15, 2026
7963644
Add PPL outputlookup command on the shared .lookups substrate
noCharger Jul 20, 2026
5bf7c7c
Merge origin/main into feature/ppl-outputlookup-clean
noCharger Jul 20, 2026
29b7e8b
Merge origin/main into feature/ppl-outputlookup-clean
noCharger Jul 20, 2026
e682917
Make outputlookup max_rows exceed error actionable
noCharger Jul 20, 2026
dade0d5
Add PPL outputlookup command
noCharger Jul 21, 2026
b84bad6
Optimize outputlookup slice write with build-time index settings
noCharger Jul 21, 2026
5dc4c9a
Address review comments: close input enumerator, restore settings on …
noCharger Jul 23, 2026
306bf6e
Create the outputlookup backing index only on paths that write to it
noCharger Jul 30, 2026
9c2adda
Match the merged data importer: drop write-time settings tuning and r…
noCharger Aug 11, 2026
9d9a533
Merge remote-tracking branch 'origin/main' into feature/ppl-outputloo…
noCharger Aug 12, 2026
f9d9260
Document the outputlookup concurrency contract for same-name writes
noCharger Aug 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ public enum Key {
PPL_SYNTAX_LEGACY_PREFERRED("plugins.ppl.syntax.legacy.preferred"),
PPL_SUBSEARCH_MAXOUT("plugins.ppl.subsearch.maxout"),
PPL_JOIN_SUBSEARCH_MAXOUT("plugins.ppl.join.subsearch_maxout"),
OUTPUTLOOKUP_MAX_ROWS("plugins.ppl.outputlookup.max_rows"),
PPL_REST_ALLOWED_ENDPOINTS("plugins.ppl.rest.allowed_endpoints"),

/** Enable Calcite as execution engine */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
import org.opensearch.sql.ast.tree.MvCombine;
import org.opensearch.sql.ast.tree.MvExpand;
import org.opensearch.sql.ast.tree.NoMv;
import org.opensearch.sql.ast.tree.OutputLookup;
import org.opensearch.sql.ast.tree.Paginate;
import org.opensearch.sql.ast.tree.Parse;
import org.opensearch.sql.ast.tree.Patterns;
Expand Down Expand Up @@ -564,6 +565,11 @@ public LogicalPlan visitNoMv(NoMv node, AnalysisContext context) {
throw getOnlyForCalciteException("nomv");
}

@Override
public LogicalPlan visitOutputLookup(OutputLookup node, AnalysisContext context) {
throw getOnlyForCalciteException("outputlookup");
}

@Override
public LogicalPlan visitMakeResults(MakeResults node, AnalysisContext context) {
throw getOnlyForCalciteException("makeresults");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
import org.opensearch.sql.ast.tree.MvCombine;
import org.opensearch.sql.ast.tree.MvExpand;
import org.opensearch.sql.ast.tree.NoMv;
import org.opensearch.sql.ast.tree.OutputLookup;
import org.opensearch.sql.ast.tree.Paginate;
import org.opensearch.sql.ast.tree.Parse;
import org.opensearch.sql.ast.tree.Patterns;
Expand Down Expand Up @@ -306,6 +307,10 @@ public T visitReverse(Reverse node, C context) {
return visitChildren(node, context);
}

public T visitOutputLookup(OutputLookup node, C context) {
return visitChildren(node, context);
}

public T visitTranspose(Transpose node, C context) {
return visitChildren(node, context);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

package org.opensearch.sql.ast.tree;

import com.google.common.collect.ImmutableList;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import org.opensearch.sql.ast.AbstractNodeVisitor;

/** AST node for the {@code outputlookup} command: a terminal write sink, overwrite by default. */
@Getter
@Setter
@ToString
@EqualsAndHashCode(callSuper = false)
@RequiredArgsConstructor
@AllArgsConstructor
public class OutputLookup extends UnresolvedPlan {

private UnresolvedPlan child;

/** The lookup name: the filtered alias published over the backing index. */
private final String indexName;

/** false (default) overwrites the destination; true appends to it. */
private boolean append = false;

/** true (default) clears the destination on empty results; false keeps it. */
private boolean overrideIfEmpty = true;

/**
* Fields whose values form the document {@code _id} for upsert; empty means auto-generated id.
*/
private List<String> keyFields = List.of();

/** Cap on the number of rows written; null means unbounded. */
private Integer max;

@Override
public OutputLookup attach(UnresolvedPlan child) {
this.child = child;
return this;
}

@Override
public List<UnresolvedPlan> getChild() {
return this.child == null ? ImmutableList.of() : ImmutableList.of(this.child);
}

@Override
public <T, C> T accept(AbstractNodeVisitor<T, C> nodeVisitor, C context) {
return nodeVisitor.visitOutputLookup(this, context);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
import org.apache.calcite.adapter.enumerable.RexToLixTranslator;
import org.apache.calcite.plan.RelOptTable;
import org.apache.calcite.plan.ViewExpanders;
import org.apache.calcite.prepare.Prepare;
import org.apache.calcite.rel.RelCollation;
import org.apache.calcite.rel.RelFieldCollation;
import org.apache.calcite.rel.RelHomogeneousShuttle;
Expand Down Expand Up @@ -147,6 +148,7 @@
import org.opensearch.sql.ast.tree.MvCombine;
import org.opensearch.sql.ast.tree.MvExpand;
import org.opensearch.sql.ast.tree.NoMv;
import org.opensearch.sql.ast.tree.OutputLookup;
import org.opensearch.sql.ast.tree.Paginate;
import org.opensearch.sql.ast.tree.Parse;
import org.opensearch.sql.ast.tree.Patterns;
Expand Down Expand Up @@ -962,6 +964,58 @@ public RelNode visitReverse(
return context.relBuilder.peek();
}

@Override
public RelNode visitOutputLookup(OutputLookup node, CalcitePlanContext context) {
visitChildren(node, context);
String name = node.getIndexName();
if (name.startsWith(".")) {
throw new IllegalArgumentException(
"outputlookup lookup name [" + name + "] must not be dot-prefixed");
}
RelNode child = context.relBuilder.build();
// Validate key_field names against the result schema: a missing/misspelled key field would
// otherwise encode identically for every row and collapse them into a single document.
if (!node.getKeyFields().isEmpty()) {
List<String> resultFields = child.getRowType().getFieldNames();
for (String keyField : node.getKeyFields()) {
if (!resultFields.contains(keyField)) {
throw new IllegalArgumentException(
"outputlookup key_field ["
+ keyField
+ "] is not a field of the result; available fields: "
+ resultFields);
}
}
}
org.apache.calcite.plan.RelOptSchema relOptSchema = context.relBuilder.getRelOptSchema();
if (!(relOptSchema instanceof Prepare.CatalogReader catalogReader)) {
throw new IllegalStateException("outputlookup could not obtain a Calcite catalog reader");
}
// Resolve a table from the schema (not a scan) so sourceless pipelines reach the client; any
// OpenSearch name yields the same client, and the supplied row type avoids a mapping fetch.
OpenSearchSchema openSearchSchema =
context.config.getDefaultSchema().unwrap(OpenSearchSchema.class);
org.apache.calcite.schema.Table clientTable = openSearchSchema.getTable(name);
RelOptTable destTable =
org.apache.calcite.prepare.RelOptTableImpl.create(
relOptSchema,
child.getRowType(),
clientTable,
ImmutableList.of(OpenSearchSchema.OPEN_SEARCH_SCHEMA_NAME, name));
RelNode sink =
OutputLookupTableModify.create(
child,
destTable,
catalogReader,
name,
node.isAppend(),
node.isOverrideIfEmpty(),
node.getKeyFields(),
node.getMax());
context.relBuilder.push(sink);
return context.relBuilder.peek();
}

@Override
public RelNode visitTranspose(
org.opensearch.sql.ast.tree.Transpose node, CalcitePlanContext context) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

package org.opensearch.sql.calcite;

import java.util.List;
import lombok.Getter;
import org.apache.calcite.plan.Convention;
import org.apache.calcite.plan.RelOptCluster;
import org.apache.calcite.plan.RelOptTable;
import org.apache.calcite.plan.RelTraitSet;
import org.apache.calcite.prepare.Prepare.CatalogReader;
import org.apache.calcite.rel.RelNode;
import org.apache.calcite.rel.core.TableModify;
import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.rel.type.RelDataTypeFactory;
import org.apache.calcite.sql.type.SqlTypeName;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.opensearch.sql.calcite.plan.AbstractOpenSearchTable;

/**
* Terminal write node for outputlookup, a Calcite {@link TableModify} INSERT so the optimizer
* treats it as a mandatory side effect (never dropped or reordered). The referenced table is the
* destination backing index, used only so the lowering rule can reach the in-cluster client; the
* lookup is created and written at execution time. Subclasses {@link TableModify} rather than
* LogicalTableModify so the built-in EnumerableTableModifyRule (which needs a ModifiableTable) does
* not fire.
*/
@Getter
public class OutputLookupTableModify extends TableModify {
Comment thread
noCharger marked this conversation as resolved.

private final String indexName;
private final boolean append;
private final boolean overrideIfEmpty;
private final List<String> keyFields;
private final @Nullable Integer max;

public OutputLookupTableModify(
RelOptCluster cluster,
RelTraitSet traits,
RelOptTable table,
CatalogReader catalogReader,
RelNode input,
String indexName,
boolean append,
boolean overrideIfEmpty,
java.util.List<String> keyFields,
@Nullable Integer max) {
super(cluster, traits, table, catalogReader, input, Operation.INSERT, null, null, false);
this.indexName = indexName;
this.append = append;
this.overrideIfEmpty = overrideIfEmpty;
this.keyFields = keyFields;
this.max = max;
}

public static OutputLookupTableModify create(
RelNode input,
RelOptTable table,
CatalogReader catalogReader,
String indexName,
boolean append,
boolean overrideIfEmpty,
java.util.List<String> keyFields,
@Nullable Integer max) {
RelOptCluster cluster = input.getCluster();
RelTraitSet traits = cluster.traitSetOf(Convention.NONE);
return new OutputLookupTableModify(
cluster,
traits,
table,
catalogReader,
input,
indexName,
append,
overrideIfEmpty,
keyFields,
max);
}

@Override
public RelDataType deriveRowType() {
RelDataTypeFactory typeFactory = getCluster().getTypeFactory();
return typeFactory
.builder()
.add("rows_written", typeFactory.createSqlType(SqlTypeName.BIGINT))
.build();
}

/** Registers the write-lowering rule when no OpenSearch scan in the pipeline does. */
@Override
public void register(org.apache.calcite.plan.RelOptPlanner planner) {
AbstractOpenSearchTable osTable = table.unwrap(AbstractOpenSearchTable.class);
if (osTable != null) {
osTable.getWriteConversionRules().forEach(planner::addRule);
}
}

@Override
public RelNode copy(RelTraitSet traitSet, List<RelNode> inputs) {
return new OutputLookupTableModify(
getCluster(),
traitSet,
getTable(),
getCatalogReader(),
inputs.get(0),
indexName,
append,
overrideIfEmpty,
keyFields,
max);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

package org.opensearch.sql.calcite.plan;

import java.util.List;
import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.rel.type.RelDataTypeFactory;
import org.apache.calcite.schema.TranslatableTable;
Expand All @@ -22,4 +23,11 @@ public abstract class AbstractOpenSearchTable extends AbstractTable
public RelDataType getRowType(RelDataTypeFactory relDataTypeFactory) {
return OpenSearchTypeFactory.convertSchema(this);
}

/**
* Write-lowering rules a write node registers itself when the pipeline has no OpenSearch scan.
*/
public java.util.List<org.apache.calcite.plan.RelOptRule> getWriteConversionRules() {
return List.of();
}
}
1 change: 1 addition & 0 deletions docs/category.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"user/ppl/cmd/head.md",
"user/ppl/cmd/join.md",
"user/ppl/cmd/lookup.md",
"user/ppl/cmd/outputlookup.md",
"user/ppl/cmd/mvcombine.md",
"user/ppl/cmd/nomv.md",
"user/ppl/cmd/mvexpand.md",
Expand Down
Loading
Loading