diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/visitors/QueryVisitor.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/visitors/QueryVisitor.java index 5ec8a60dcf..a3e2960ad3 100644 --- a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/visitors/QueryVisitor.java +++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/visitors/QueryVisitor.java @@ -31,6 +31,7 @@ import com.apple.foundationdb.record.query.plan.cascades.predicates.CompatibleTypeEvolutionPredicate; import com.apple.foundationdb.record.query.plan.cascades.typing.Type; import com.apple.foundationdb.record.query.plan.cascades.values.FieldValue; +import com.apple.foundationdb.record.query.plan.cascades.values.LiteralValue; import com.apple.foundationdb.record.query.plan.cascades.values.Value; import com.apple.foundationdb.record.util.pair.NonnullPair; import com.apple.foundationdb.relational.api.exceptions.ErrorCode; @@ -222,11 +223,20 @@ public LogicalOperator handleRecursiveNamedQuery(@Nonnull final RelationalParser @Nonnull @Override public LogicalOperator visitSimpleTable(@Nonnull RelationalParser.SimpleTableContext simpleTableContext) { - Assert.notNullUnchecked(simpleTableContext.fromClause(), ErrorCode.UNSUPPORTED_QUERY, "query is not supported"); getDelegate().pushPlanFragment(); - simpleTableContext.fromClause().accept(this); + if (simpleTableContext.fromClause() != null) { + simpleTableContext.fromClause().accept(this); + } else { + // No FROM clause: synthesize a single-row source so constant expressions + // can be projected over exactly one output row (standard SQL semantics). + final var dummyElement = Expression.fromUnderlying(LiteralValue.ofScalar(true)); + final var arrayOfOne = getDelegate().resolveFunction("__internal_array", false, dummyElement); + final var explodeExpr = new ExplodeExpression(arrayOfOne.getUnderlying()); + final var syntheticQuantifier = Quantifier.forEach(Reference.initialOf(explodeExpr)); + getDelegate().getCurrentPlanFragment().setOperator(LogicalOperator.newUnnamedOperator(Expressions.empty(), syntheticQuantifier)); + } - var where = Optional.ofNullable(simpleTableContext.fromClause().whereExpr() == null ? + var where = Optional.ofNullable(simpleTableContext.fromClause() == null || simpleTableContext.fromClause().whereExpr() == null ? null : visitWhereExpr(simpleTableContext.fromClause().whereExpr())); diff --git a/yaml-tests/src/test/java/YamlIntegrationTests.java b/yaml-tests/src/test/java/YamlIntegrationTests.java index f42b76bf15..4b1a302ae9 100644 --- a/yaml-tests/src/test/java/YamlIntegrationTests.java +++ b/yaml-tests/src/test/java/YamlIntegrationTests.java @@ -493,4 +493,9 @@ public void recordTypeKeyTest(YamlTest.Runner runner) throws Exception { public void filterIndexTest(YamlTest.Runner runner) throws Exception { runner.runYamsql("filter-index.yamsql"); } + + @TestTemplate + public void selectWithoutFrom(YamlTest.Runner runner) throws Exception { + runner.runYamsql("select-without-from.yamsql"); + } } diff --git a/yaml-tests/src/test/resources/select-without-from.yamsql b/yaml-tests/src/test/resources/select-without-from.yamsql new file mode 100644 index 0000000000..2cb4d6f851 --- /dev/null +++ b/yaml-tests/src/test/resources/select-without-from.yamsql @@ -0,0 +1,59 @@ +# +# select-without-from.yamsql +# +# This source file is part of the FoundationDB open source project +# +# Copyright 2021-2025 Apple Inc. and the FoundationDB project authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +--- +options: + supported_version: !current_version +--- +schema_template: + create table T(id bigint, primary key(id)) +--- +test_block: + preset: single_repetition_ordered + tests: + - + - query: SELECT 'bob' AS greeting + - resultMetadata: [{GREETING: STRING}] + - result: [{'bob'}] + - + - query: SELECT 42 AS answer + - resultMetadata: [{ANSWER: INTEGER}] + - result: [{42}] + - + - query: SELECT 1 + 1 AS result + - resultMetadata: [{RESULT: INTEGER}] + - result: [{2}] + - + - query: SELECT 'hello' AS a, 'world' AS b + - resultMetadata: [{A: STRING}, {B: STRING}] + - result: [{'hello', 'world'}] + - + - query: SELECT true AS flag + - resultMetadata: [{FLAG: BOOLEAN}] + - result: [{true}] + - + - query: SELECT 41 + - resultMetadata: [{_0: INTEGER}] + - result: [{41}] + - + # WHERE without FROM is a syntax error: WHERE is part of fromClause in the grammar + # and cannot appear without a preceding FROM. + - query: SELECT 'bob' WHERE true + - error: "42601" +...