diff options
13 files changed, 442 insertions, 0 deletions
diff --git a/buildscripts/resmokeconfig/suites/replica_sets_multi_stmt_txn_kill_primary_jscore_passthrough.yml b/buildscripts/resmokeconfig/suites/replica_sets_multi_stmt_txn_kill_primary_jscore_passthrough.yml index 25013723167..fb0535caf22 100644 --- a/buildscripts/resmokeconfig/suites/replica_sets_multi_stmt_txn_kill_primary_jscore_passthrough.yml +++ b/buildscripts/resmokeconfig/suites/replica_sets_multi_stmt_txn_kill_primary_jscore_passthrough.yml @@ -330,6 +330,9 @@ selector: # operation and cluster times aren't shared between shells. # "Cowardly refusing to run test with network retries enabled when it uses startParallelShell()" - uses_parallel_shell + # Transaction-continuing commands cannot specify API parameters, so tests that use API parameters + # cannot be run with transactions. + - uses_api_parameters executor: archive: diff --git a/buildscripts/resmokeconfig/suites/replica_sets_multi_stmt_txn_stepdown_jscore_passthrough.yml b/buildscripts/resmokeconfig/suites/replica_sets_multi_stmt_txn_stepdown_jscore_passthrough.yml index 446dc151877..34f856bfe8d 100644 --- a/buildscripts/resmokeconfig/suites/replica_sets_multi_stmt_txn_stepdown_jscore_passthrough.yml +++ b/buildscripts/resmokeconfig/suites/replica_sets_multi_stmt_txn_stepdown_jscore_passthrough.yml @@ -312,6 +312,9 @@ selector: # operation and cluster times aren't shared between shells. # "Cowardly refusing to run test with network retries enabled when it uses startParallelShell()" - uses_parallel_shell + # Transaction-continuing commands cannot specify API parameters, so tests that use API parameters + # cannot be run with transactions. + - uses_api_parameters executor: archive: diff --git a/buildscripts/resmokeconfig/suites/replica_sets_multi_stmt_txn_terminate_primary_jscore_passthrough.yml b/buildscripts/resmokeconfig/suites/replica_sets_multi_stmt_txn_terminate_primary_jscore_passthrough.yml index 2312c1a1260..815a7ccd853 100644 --- a/buildscripts/resmokeconfig/suites/replica_sets_multi_stmt_txn_terminate_primary_jscore_passthrough.yml +++ b/buildscripts/resmokeconfig/suites/replica_sets_multi_stmt_txn_terminate_primary_jscore_passthrough.yml @@ -319,6 +319,10 @@ selector: # operation and cluster times aren't shared between shells. # "Cowardly refusing to run test with network retries enabled when it uses startParallelShell()" - uses_parallel_shell + # Transaction-continuing commands cannot specify API parameters, so tests that use API parameters + # cannot be run with transactions. + - uses_api_parameters + executor: archive: diff --git a/jstests/core/api_version_test_expression.js b/jstests/core/api_version_test_expression.js new file mode 100644 index 00000000000..ca3d2b6b231 --- /dev/null +++ b/jstests/core/api_version_test_expression.js @@ -0,0 +1,115 @@ +/** + * Checks that the $_testApiVersion expression used for API versioning testing + * throws errors as expected. + * + * Tests which create views aren't expected to work when collections are implicitly sharded. + * @tags: [requires_fcv_47, uses_api_parameters, assumes_unsharded_collection, sbe_incompatible] + */ + +(function() { +"use strict"; + +const collName = "api_version_test_expression"; +const coll = db[collName]; +coll.drop(); + +for (let i = 0; i < 5; i++) { + assert.commandWorked(coll.insert({num: i})); +} + +// Assert error thrown when command specifies {apiStrict: true} and expression specifies {unstable: +// true}. +let pipeline = [{$project: {v: {$_testApiVersion: {unstable: true}}}}]; +assert.commandFailedWithCode( + db.runCommand( + {aggregate: collName, pipeline: pipeline, cursor: {}, apiStrict: true, apiVersion: "1"}), + ErrorCodes.APIStrictError); + +// Assert error thrown when command specifies {apiDeprecationErrors: true} and expression specifies +// {deprecated: true} +pipeline = [{$project: {v: {$_testApiVersion: {deprecated: true}}}}]; +assert.commandFailedWithCode(db.runCommand({ + aggregate: collName, + pipeline: pipeline, + cursor: {}, + apiDeprecationErrors: true, + apiVersion: "1" +}), + ErrorCodes.APIDeprecationError); + +// Test that command successfully runs to completion without any API parameters. +pipeline = [{$project: {v: {$_testApiVersion: {unstable: true}}}}]; +assert.commandWorked(db.runCommand({aggregate: collName, pipeline: pipeline, cursor: {}})); + +// Create a view with {apiStrict: true}. +db.view.drop(); +assert.commandWorked(db.runCommand( + {create: "view", viewOn: collName, pipeline: [], apiStrict: true, apiVersion: "1"})); +// This command will work because API parameters are not inherited from views. +assert.commandWorked(db.runCommand({aggregate: "view", pipeline: pipeline, cursor: {}})); +assert.commandFailedWithCode( + db.runCommand( + {aggregate: "view", pipeline: pipeline, cursor: {}, apiVersion: "1", apiStrict: true}), + ErrorCodes.APIStrictError); + +// Create a view with {unstable: true}. +db.unstableView.drop(); +assert.commandWorked(db.runCommand({ + create: "unstableView", + viewOn: collName, + pipeline: pipeline, + apiStrict: true, + apiVersion: "1" +})); +assert.commandWorked(db.runCommand({aggregate: "unstableView", pipeline: [], cursor: {}})); +// This commmand will fail even with the empty pipeline because of the view. +assert.commandFailedWithCode( + db.runCommand( + {aggregate: "unstableView", pipeline: [], cursor: {}, apiVersion: "1", apiStrict: true}), + ErrorCodes.APIStrictError); + +// Create a validator containing the unstable test expression. +let validator = {$expr: {$_testApiVersion: {unstable: true}}}; +let validatedCollName = collName + "_validated"; + +// Create the collection with the unstable validator, setting apiStrict: true does not have an +// effect. +db[validatedCollName].drop(); +assert.commandWorked(db.runCommand( + {create: validatedCollName, validator: validator, apiVersion: "1", apiStrict: true})); + +// Run an insert command without any API version and verify that it is successful. +assert.commandWorked( + db[validatedCollName].runCommand({insert: validatedCollName, documents: [{num: 1}]})); + +// TODO SERVER-53218: Specifying apiStrict: true results in an error. +assert.commandWorked(db[validatedCollName].runCommand( + {insert: validatedCollName, documents: [{num: 1}], apiVersion: "1", apiStrict: true})); + +// Recreate the validator containing a deprecated test expression. +db[validatedCollName].drop(); +validator = { + $expr: {$_testApiVersion: {deprecated: true}} +}; + +// Create the collection with the unstable validator, setting apiDeprecationErrors : true does not +// have an effect. +assert.commandWorked(db.runCommand({ + create: validatedCollName, + validator: validator, + apiVersion: "1", + apiDeprecationErrors: true, +})); + +// Run an insert command without any API version and verify that it is successful. +assert.commandWorked( + db[validatedCollName].runCommand({insert: validatedCollName, documents: [{num: 1}]})); + +// TODO SERVER-53218: Specifying apiDeprecationErrors: true results in an error. +assert.commandWorked(db[validatedCollName].runCommand({ + insert: validatedCollName, + documents: [{num: 1}], + apiVersion: "1", + apiDeprecationErrors: true +})); +})(); diff --git a/src/mongo/db/pipeline/SConscript b/src/mongo/db/pipeline/SConscript index 41c30aecb02..9a5adb2ce1f 100644 --- a/src/mongo/db/pipeline/SConscript +++ b/src/mongo/db/pipeline/SConscript @@ -70,12 +70,14 @@ env.Library( 'expression_context.cpp', 'expression_function.cpp', 'expression_js_emit.cpp', + 'expression_test_api_version.cpp', 'expression_trigonometric.cpp', 'javascript_execution.cpp', 'make_js_function.cpp', 'variables.cpp', ], LIBDEPS=[ + '$BUILD_DIR/mongo/db/commands/test_commands_enabled', '$BUILD_DIR/mongo/db/exec/document_value/document_value', '$BUILD_DIR/mongo/db/query/collation/collator_factory_interface', '$BUILD_DIR/mongo/db/query/datetime/date_time_support', @@ -389,6 +391,7 @@ env.CppUnitTest( 'expression_or_test.cpp', 'expression_replace_test.cpp', 'expression_test.cpp', + 'expression_test_api_version_test.cpp', 'expression_trigonometric_test.cpp', 'expression_trim_test.cpp', 'expression_walker_test.cpp', diff --git a/src/mongo/db/pipeline/expression.h b/src/mongo/db/pipeline/expression.h index cbcf74dd618..647f7a24527 100644 --- a/src/mongo/db/pipeline/expression.h +++ b/src/mongo/db/pipeline/expression.h @@ -41,6 +41,7 @@ #include <vector> #include "mongo/base/init.h" +#include "mongo/db/commands/test_commands_enabled.h" #include "mongo/db/exec/document_value/document.h" #include "mongo/db/exec/document_value/value.h" #include "mongo/db/pipeline/dependencies.h" @@ -91,6 +92,20 @@ class DocumentSource; return Status::OK(); \ } +/** + * Registers a Parser only if test commands are enabled. Use this if your expression is only used + * for testing purposes. + */ +#define REGISTER_TEST_EXPRESSION(key, parser) \ + MONGO_INITIALIZER_GENERAL( \ + addToExpressionParserMap_##key, ("EndStartupOptionHandling"), ("expressionParserMap")) \ + (InitializerContext*) { \ + if (getTestCommandsEnabled()) { \ + Expression::registerExpression("$" #key, (parser), boost::none); \ + } \ + return Status::OK(); \ + } + class Expression : public RefCountable { public: using Parser = std::function<boost::intrusive_ptr<Expression>( diff --git a/src/mongo/db/pipeline/expression_context.cpp b/src/mongo/db/pipeline/expression_context.cpp index a27d8ca64b1..0ca09f7a27c 100644 --- a/src/mongo/db/pipeline/expression_context.cpp +++ b/src/mongo/db/pipeline/expression_context.cpp @@ -125,6 +125,8 @@ ExpressionContext::ExpressionContext( } if (letParameters) variables.seedVariablesWithLetParameters(this, *letParameters); + if (opCtx) + apiParameters = APIParameters::get(opCtx); } ExpressionContext::ExpressionContext( diff --git a/src/mongo/db/pipeline/expression_context.h b/src/mongo/db/pipeline/expression_context.h index c6a20a28ab9..721cf6ffe01 100644 --- a/src/mongo/db/pipeline/expression_context.h +++ b/src/mongo/db/pipeline/expression_context.h @@ -37,6 +37,7 @@ #include "mongo/base/string_data.h" #include "mongo/bson/bsonobj.h" +#include "mongo/db/api_parameters.h" #include "mongo/db/exec/document_value/document_comparator.h" #include "mongo/db/exec/document_value/value_comparator.h" #include "mongo/db/namespace_string.h" @@ -363,6 +364,11 @@ public: // construction. const bool mayDbProfile = true; + // API Parameters pulled from OperationContext upon object creation. + // This may become stale if OperationContext changes after object creation. + // Expressions should reach APIParameters with this variable instead of using the decorator. + APIParameters apiParameters; + protected: static const int kInterruptCheckPeriod = 128; diff --git a/src/mongo/db/pipeline/expression_test_api_version.cpp b/src/mongo/db/pipeline/expression_test_api_version.cpp new file mode 100644 index 00000000000..c501d8b9292 --- /dev/null +++ b/src/mongo/db/pipeline/expression_test_api_version.cpp @@ -0,0 +1,97 @@ +/** + * Copyright (C) 2020-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the Server Side Public License in all respects for + * all of the code used other than as permitted herein. If you modify file(s) + * with this exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do so, + * delete this exception statement from your version. If you delete this + * exception statement from all source files in the program, then also delete + * it in the license file. + */ +#include "mongo/platform/basic.h" + +#include "mongo/db/api_parameters.h" +#include "mongo/db/pipeline/expression_test_api_version.h" + +namespace mongo { + +REGISTER_TEST_EXPRESSION(_testApiVersion, ExpressionTestApiVersion::parse); + +ExpressionTestApiVersion::ExpressionTestApiVersion(ExpressionContext* const expCtx, + bool unstable, + bool deprecated) + : Expression(expCtx), _unstable(unstable), _deprecated(deprecated) {} + +boost::intrusive_ptr<Expression> ExpressionTestApiVersion::parse(ExpressionContext* const expCtx, + BSONElement expr, + const VariablesParseState& vps) { + uassert(5161700, + "$_testApiVersion only supports an object as its argument", + expr.type() == BSONType::Object); + + const BSONObj params = expr.embeddedObject(); + uassert(5161701, + "$_testApiVersion only accepts an object with a single field.", + params.nFields() == 1); + + bool unstableField = false; + bool deprecatedField = false; + + auto field = params.firstElementFieldNameStringData(); + if (field == kUnstableField) { + uassert(5161702, "unstable must be a boolean", params.firstElement().isBoolean()); + unstableField = params.firstElement().boolean(); + } else if (field == kDeprecatedField) { + uassert(5161703, "deprecated must be a boolean", params.firstElement().isBoolean()); + deprecatedField = params.firstElement().boolean(); + } else { + uasserted(5161704, + str::stream() << field << " is not a valid argument for $_testApiVersion"); + } + + return new ExpressionTestApiVersion(expCtx, unstableField, deprecatedField); +} + +Value ExpressionTestApiVersion::serialize(bool explain) const { + return Value(Document{{"$_testApiVersion", + Document{{"unstable", _unstable ? Value(_unstable) : Value()}, + {"deprecated", _deprecated ? Value(_deprecated) : Value()}}}}); +} + +Value ExpressionTestApiVersion::evaluate(const Document& root, Variables* variables) const { + APIParameters apiParams = getExpressionContext()->apiParameters; + + if (apiParams.getAPIStrict() && _unstable) { + uasserted(ErrorCodes::APIStrictError, + "Provided apiStrict is true with an unstable command."); + } + + if (apiParams.getAPIDeprecationErrors() && _deprecated) { + uasserted(ErrorCodes::APIDeprecationError, + "Provided apiDeprecatedErrors is true with a deprecated command."); + } + + return Value(1); +} + +void ExpressionTestApiVersion::_doAddDependencies(DepsTracker* deps) const {} + +} // namespace mongo
\ No newline at end of file diff --git a/src/mongo/db/pipeline/expression_test_api_version.h b/src/mongo/db/pipeline/expression_test_api_version.h new file mode 100644 index 00000000000..ede9a8378d4 --- /dev/null +++ b/src/mongo/db/pipeline/expression_test_api_version.h @@ -0,0 +1,64 @@ +/** + * Copyright (C) 2020-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the Server Side Public License in all respects for + * all of the code used other than as permitted herein. If you modify file(s) + * with this exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do so, + * delete this exception statement from your version. If you delete this + * exception statement from all source files in the program, then also delete + * it in the license file. + */ + +#pragma once + +#include "mongo/db/pipeline/expression.h" + +namespace mongo { +/** + * This expression will be used to validate that versioning code is working as expected. + * $_testApiVersion should only take one parameter, either {unstable: true} or {deprecated: true}. + * If no error is thrown, this expression will return an integer value. + */ +class ExpressionTestApiVersion final : public Expression { +public: + static constexpr auto kUnstableField = "unstable"; + static constexpr auto kDeprecatedField = "deprecated"; + + static boost::intrusive_ptr<Expression> parse(ExpressionContext* const expCtx, + BSONElement expr, + const VariablesParseState& vps); + + Value evaluate(const Document& root, Variables* variables) const final; + + Value serialize(bool explain) const final; + + void acceptVisitor(ExpressionVisitor* visitor) final { + return visitor->visit(this); + } + +private: + ExpressionTestApiVersion(ExpressionContext* const expCtx, bool unstable, bool deprecated); + void _doAddDependencies(DepsTracker* deps) const final override; + + bool _unstable; + bool _deprecated; +}; +} // namespace mongo diff --git a/src/mongo/db/pipeline/expression_test_api_version_test.cpp b/src/mongo/db/pipeline/expression_test_api_version_test.cpp new file mode 100644 index 00000000000..5dc4b93493e --- /dev/null +++ b/src/mongo/db/pipeline/expression_test_api_version_test.cpp @@ -0,0 +1,123 @@ +/** + * Copyright (C) 2020-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the Server Side Public License in all respects for + * all of the code used other than as permitted herein. If you modify file(s) + * with this exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do so, + * delete this exception statement from your version. If you delete this + * exception statement from all source files in the program, then also delete + * it in the license file. + */ + +#include "mongo/db/exec/document_value/document_value_test_util.h" +#include "mongo/db/pipeline/aggregation_context_fixture.h" +#include "mongo/db/pipeline/expression_context_for_test.h" +#include "mongo/platform/basic.h" + +#include "mongo/db/pipeline/expression_test_api_version.h" +#include "mongo/unittest/unittest.h" + +namespace mongo { +namespace { + +using TestApiVersion = AggregationContextFixture; + +TEST_F(TestApiVersion, UnstableAcceptsBooleanValue) { + auto expCtx = getExpCtx(); + boost::intrusive_ptr<Expression> expression = ExpressionTestApiVersion::parse( + expCtx.get(), + BSON("$_testApiVersion" << BSON("unstable" << true)).firstElement(), + expCtx->variablesParseState); + + ASSERT_VALUE_EQ(Value(DOC("$_testApiVersion" << DOC("unstable" << true))), + expression->serialize(false)); +} + +TEST_F(TestApiVersion, UnstableDoesNotAcceptNumericValue) { + auto expCtx = getExpCtx(); + + ASSERT_THROWS_CODE(ExpressionTestApiVersion::parse( + expCtx.get(), + BSON("$_testApiVersion" << BSON("unstable" << 1)).firstElement(), + expCtx->variablesParseState), + AssertionException, + 5161702); +} + +TEST_F(TestApiVersion, DeprecatedAcceptsBooleanValue) { + auto expCtx = getExpCtx(); + boost::intrusive_ptr<Expression> expression = ExpressionTestApiVersion::parse( + expCtx.get(), + BSON("$_testApiVersion" << BSON("deprecated" << true)).firstElement(), + expCtx->variablesParseState); + + ASSERT_VALUE_EQ(Value(DOC("$_testApiVersion" << DOC("deprecated" << true))), + expression->serialize(false)); +} + +TEST_F(TestApiVersion, DeprecatedDoesNotAcceptNumericValue) { + auto expCtx = getExpCtx(); + + ASSERT_THROWS_CODE(ExpressionTestApiVersion::parse( + expCtx.get(), + BSON("$_testApiVersion" << BSON("deprecated" << 1)).firstElement(), + expCtx->variablesParseState), + AssertionException, + 5161703); +} + +TEST_F(TestApiVersion, DoesNotAcceptInvalidParameter) { + auto expCtx = getExpCtx(); + + ASSERT_THROWS_CODE( + ExpressionTestApiVersion::parse( + expCtx.get(), + BSON("$_testApiVersion" << BSON("invalidParameter" << true)).firstElement(), + expCtx->variablesParseState), + AssertionException, + 5161704); +} + +TEST_F(TestApiVersion, OnlyTakesOneParameter) { + auto expCtx = getExpCtx(); + + ASSERT_THROWS_CODE( + ExpressionTestApiVersion::parse( + expCtx.get(), + BSON("$_testApiVersion" << BSON("deprecated" << true << "unstable" << true)) + .firstElement(), + expCtx->variablesParseState), + AssertionException, + 5161701); +} + +TEST_F(TestApiVersion, DoesNotAcceptEmptyDocument) { + auto expCtx = getExpCtx(); + + ASSERT_THROWS_CODE( + ExpressionTestApiVersion::parse(expCtx.get(), + BSON("$_testApiVersion" << BSONObj()).firstElement(), + expCtx->variablesParseState), + AssertionException, + 5161701); +} +} // namespace +} // namespace mongo
\ No newline at end of file diff --git a/src/mongo/db/pipeline/expression_visitor.h b/src/mongo/db/pipeline/expression_visitor.h index de4b32908e3..51225289b26 100644 --- a/src/mongo/db/pipeline/expression_visitor.h +++ b/src/mongo/db/pipeline/expression_visitor.h @@ -117,6 +117,7 @@ class ExpressionBinarySize; class ExpressionStrLenCP; class ExpressionSubtract; class ExpressionSwitch; +class ExpressionTestApiVersion; class ExpressionToLower; class ExpressionToUpper; class ExpressionTrim; @@ -185,6 +186,7 @@ public: virtual void visit(ExpressionAllElementsTrue*) = 0; virtual void visit(ExpressionAnd*) = 0; virtual void visit(ExpressionAnyElementTrue*) = 0; + virtual void visit(ExpressionTestApiVersion*) = 0; virtual void visit(ExpressionArray*) = 0; virtual void visit(ExpressionArrayElemAt*) = 0; virtual void visit(ExpressionFirst*) = 0; diff --git a/src/mongo/db/query/sbe_stage_builder_expression.cpp b/src/mongo/db/query/sbe_stage_builder_expression.cpp index c46454c1935..cd653deb016 100644 --- a/src/mongo/db/query/sbe_stage_builder_expression.cpp +++ b/src/mongo/db/query/sbe_stage_builder_expression.cpp @@ -427,6 +427,7 @@ public: void visit(ExpressionSwitch* expr) final { _context->evalStack.emplaceFrame(EvalStage{}); } + void visit(ExpressionTestApiVersion* expr) final {} void visit(ExpressionToLower* expr) final {} void visit(ExpressionToUpper* expr) final {} void visit(ExpressionTrim* expr) final {} @@ -622,6 +623,7 @@ public: void visit(ExpressionSwitch* expr) final { _context->evalStack.emplaceFrame(EvalStage{}); } + void visit(ExpressionTestApiVersion* expr) final {} void visit(ExpressionToLower* expr) final {} void visit(ExpressionToUpper* expr) final {} void visit(ExpressionTrim* expr) final {} @@ -2324,6 +2326,9 @@ public: void visit(ExpressionSwitch* expr) final { visitConditionalExpression(expr); } + void visit(ExpressionTestApiVersion* expr) final { + unsupportedExpression("$_testApiVersion"); + } void visit(ExpressionToLower* expr) final { generateStringCaseConversionExpression(_context, "toLower"); } |