text:
//
// Copyright 2019 ZetaSQL 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.
//

#include "zetasql/public/analyzer.h"

#include <type_traits>

#include "zetasql/base/logging.h"
#include "zetasql/base/path.h"
#include "google/protobuf/compiler/importer.h"
#include "google/protobuf/descriptor.h"
#include "zetasql/common/status_payload_utils.h"
#include "zetasql/base/testing/status_matchers.h"
#include "zetasql/parser/parser.h"
#include "zetasql/public/function.h"
#include "zetasql/public/function.pb.h"
#include "zetasql/public/parse_resume_location.h"
#include "zetasql/public/simple_catalog.h"
#include "zetasql/public/sql_formatter.h"
#include "zetasql/public/type.h"
#include "zetasql/public/type.pb.h"
#include "zetasql/resolved_ast/resolved_ast.h"
#include "zetasql/resolved_ast/resolved_column.h"
#include "zetasql/resolved_ast/resolved_node_kind.pb.h"
#include "zetasql/resolved_ast/sql_builder.h"
#include "zetasql/testdata/sample_catalog.h"
#include "zetasql/testdata/test_schema.pb.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "absl/container/node_hash_set.h"
#include "absl/memory/memory.h"
#include "zetasql/base/case.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/str_join.h"
#include "zetasql/base/map_util.h"
#include "zetasql/base/status.h"

namespace zetasql {

using testing::_;
using testing::HasSubstr;
using testing::IsNull;
using zetasql_base::testing::StatusIs;

class AnalyzerOptionsTest : public ::testing::Test {
 public:
  AnalyzerOptionsTest() {
    options_.mutable_language()->SetSupportsAllStatementKinds();
    sample_catalog_ = absl::make_unique<SampleCatalog>(options_.language());
  }
  AnalyzerOptionsTest(const AnalyzerOptionsTest&) = delete;
  AnalyzerOptionsTest& operator=(const AnalyzerOptionsTest&) = delete;
  ~AnalyzerOptionsTest() override {}

  SimpleCatalog* catalog() {
    return sample_catalog_->catalog();
  }

  AnalyzerOptions options_;
  TypeFactory type_factory_;

  void ValidateQueryParam(const std::string& name, const Type* type) {
    const std::string& key = absl::AsciiStrToLower(name);
    const QueryParametersMap& query_parameters = options_.query_parameters();
    EXPECT_TRUE(zetasql_base::ContainsKey(query_parameters, key));
    EXPECT_EQ(query_parameters.find(key)->second, type);
  }

 private:
  std::unique_ptr<SampleCatalog> sample_catalog_;
};

TEST_F(AnalyzerOptionsTest, AddSystemVariable) {
  // Simple cases
  EXPECT_EQ(options_.system_variables().size(), 0);
  ZETASQL_EXPECT_OK(options_.AddSystemVariable({"bytes"}, type_factory_.get_bytes()));
  ZETASQL_EXPECT_OK(options_.AddSystemVariable({"bool"}, type_factory_.get_bool()));
  ZETASQL_EXPECT_OK(
      options_.AddSystemVariable({"foo", "bar"}, type_factory_.get_string()));
  ZETASQL_EXPECT_OK(options_.AddSystemVariable({"foo.bar", "baz"},
                                       type_factory_.get_string()));

  // Null type
  EXPECT_THAT(
      options_.AddSystemVariable({"zzz"}, nullptr),
      StatusIs(
          _, HasSubstr("Type associated with system variable cannot be NULL")));

  // Unsupported type
  AnalyzerOptions external_mode(options_);
  external_mode.mutable_language_options()->set_product_mode(
      ProductMode::PRODUCT_EXTERNAL);
  EXPECT_THAT(
      external_mode.AddSystemVariable({"zzz"}, type_factory_.get_int32()),
      StatusIs(_,
               HasSubstr("System variable zzz has unsupported type: INT32")));

  // Duplicate variable
  EXPECT_THAT(
      options_.AddSystemVariable({"foo", "bar"}, type_factory_.get_int64()),
      StatusIs(_, HasSubstr("Duplicate system variable foo.bar")));

  // Duplicate variable (case insensitive)
  EXPECT_THAT(
      options_.AddSystemVariable({"FOO", "BaR"}, type_factory_.get_int64()),
      StatusIs(_, HasSubstr("Duplicate system variable FOO.BaR")));

  // Empty name path
  EXPECT_THAT(
      options_.AddSystemVariable({}, type_factory_.get_int64()),
      StatusIs(_, HasSubstr("System variable cannot have empty name path")));

  // name path with empty element
  EXPECT_THAT(
      options_.AddSystemVariable({""}, type_factory_.get_int64()),
      StatusIs(
          _,
          HasSubstr("System variable cannot have empty string as path part")));
}

TEST_F(AnalyzerOptionsTest, AddQueryParameter) {
  EXPECT_EQ(options_.query_parameters().size(), 0);
  ZETASQL_EXPECT_OK(
      options_.AddQueryParameter("bytes", type_factory_.get_bytes()));
  ZETASQL_EXPECT_OK(options_.AddQueryParameter("bool", type_factory_.get_bool()));
  ZETASQL_EXPECT_OK(options_.AddQueryParameter("int32", type_factory_.get_int32()));
  ZETASQL_EXPECT_OK(options_.AddQueryParameter("string", type_factory_.get_string()));
  ZETASQL_EXPECT_OK(
      options_.AddQueryParameter("MiXeDcAsE", type_factory_.get_string()));

  const QueryParametersMap& query_parameters = options_.query_parameters();
  EXPECT_EQ(query_parameters.size(), 5);
  ValidateQueryParam("bool", type_factory_.get_bool());
  ValidateQueryParam("bytes", type_factory_.get_bytes());
  ValidateQueryParam("int32", type_factory_.get_int32());
  ValidateQueryParam("string", type_factory_.get_string());
  // Verifies case insensitivity of parameter names.
  ValidateQueryParam("mixedcase", type_factory_.get_string());
  ValidateQueryParam("MIXEDCASE", type_factory_.get_string());
  ValidateQueryParam("mixedCASE", type_factory_.get_string());

  // Invalid cases.
  EXPECT_THAT(
      options_.AddQueryParameter("random", nullptr),
      StatusIs(
          _, HasSubstr("Type associated with query parameter cannot be NULL")));
  EXPECT_THAT(
      options_.AddPositionalQueryParameter(nullptr),
      StatusIs(
          _, HasSubstr("Type associated with query parameter cannot be NULL")));
  EXPECT_THAT(options_.AddQueryParameter("", type_factory_.get_bool()),
              StatusIs(_, HasSubstr("Query parameter cannot have empty name")));
  EXPECT_THAT(options_.AddQueryParameter("string", type_factory_.get_bool()),
              StatusIs(_, HasSubstr("Duplicate parameter name")));
}

TEST_F(AnalyzerOptionsTest, AddQueryParameterCivilTimeTypes) {
  EXPECT_EQ(options_.query_parameters().size(), 0);
  const StructType* time_struct;
  ZETASQL_CHECK_OK(type_factory_.MakeStructType(
      {{"time_value", type_factory_.get_time()}}, &time_struct));
  const ArrayType* time_array;
  ZETASQL_CHECK_OK(type_factory_.MakeArrayType(type_factory_.get_time(), &time_array));
  const ProtoType* proto_with_time;
  ZETASQL_CHECK_OK(type_factory_.MakeProtoType(
      zetasql_test::CivilTimeTypesSinkPB::descriptor(), &proto_with_time));

  options_.mutable_language()->DisableAllLanguageFeatures();
  EXPECT_FALSE(
      options_.AddQueryParameter("time_1", type_factory_.get_time()).ok());
  EXPECT_FALSE(
      options_.AddPositionalQueryParameter(type_factory_.get_time()).ok());
  EXPECT_FALSE(options_.AddQueryParameter("time_struct_1", time_struct).ok());
  EXPECT_FALSE(options_.AddQueryParameter("time_array_1", time_array).ok());
  // Adding a proto as parameter should be fine, even if there are some field in
  // it can be recognized as time or datetime.
  ZETASQL_EXPECT_OK(options_.AddQueryParameter("proto_with_time_1", proto_with_time));

  options_.mutable_language()->EnableLanguageFeature(FEATURE_V_1_2_CIVIL_TIME);
  ZETASQL_EXPECT_OK(options_.AddQueryParameter("time_2", type_factory_.get_time()));
  ZETASQL_EXPECT_OK(options_.AddQueryParameter("time_struct_2", time_struct));
  ZETASQL_EXPECT_OK(options_.AddQueryParameter("time_array_2", time_array));
  ZETASQL_EXPECT_OK(options_.AddQueryParameter("proto_with_time_2", proto_with_time));
}

TEST_F(AnalyzerOptionsTest, ParserASTOwnershipTests) {
  const std::string sql("SELECT 1;");

  std::unique_ptr<ParserOutput> parser_output;
  ParserOptions parser_options = options_.GetParserOptions();
  ZETASQL_ASSERT_OK(ParseStatement(sql, parser_options, &parser_output));

  // Analyze statement from ParserOutput without taking ownership of
  // <parser_output>.
  std::unique_ptr<const AnalyzerOutput> analyzer_output_unowned_parser_output;
  ZETASQL_EXPECT_OK(AnalyzeStatementFromParserOutputUnowned(
      &parser_output, options_, sql, catalog(), &type_factory_,
      &analyzer_output_unowned_parser_output));
  // <parser_output> retains ownership.
  EXPECT_NE(nullptr, parser_output);

  // Analyze statement from ParserOutput and take ownership of <parser_output>.
  std::unique_ptr<const AnalyzerOutput> analyzer_output_owned_parser_output;
  ZETASQL_EXPECT_OK(AnalyzeStatementFromParserOutputOwnedOnSuccess(
      &parser_output, options_, sql, catalog(), &type_factory_,
      &analyzer_output_owned_parser_output));

  // Both analyses result in the same resolved AST.
  EXPECT_EQ(analyzer_output_unowned_parser_output->resolved_statement()
              ->DebugString(),
            analyzer_output_owned_parser_output->resolved_statement()
              ->DebugString());

  // <parser_output> ownership has been released.
  EXPECT_EQ(nullptr, parser_output);

  // Test that for failed resolution, ownership of the <parser_output>
  // is retained.
  const std::string bad_sql("SELECT 1 + 'a';");

  // Re-use same ParserOutput.
  ZETASQL_ASSERT_OK(ParseStatement(bad_sql, parser_options, &parser_output));

  // Re-use same AnalyzerOutput.
  EXPECT_FALSE(AnalyzeStatementFromParserOutputOwnedOnSuccess(
      &parser_output, options_, bad_sql, catalog(), &type_factory_,
      &analyzer_output_owned_parser_output).ok());

  // <parser_output> retains ownership.
  EXPECT_NE(nullptr, parser_output);
}

TEST_F(AnalyzerOptionsTest, QueryParameterModes) {
  std::unique_ptr<const AnalyzerOutput> output;
  {
    // Named mode with positional parameter.
    AnalyzerOptions options = options_;
    options.set_parameter_mode(PARAMETER_NAMED);
    ZETASQL_EXPECT_OK(options.AddPositionalQueryParameter(types::Int64Type()));

    EXPECT_THAT(AnalyzeStatement("SELECT 1;", options, nullptr, &type_factory_,
                                 &output),
                StatusIs(_, HasSubstr("Positional parameters cannot be "
                                      "provided in named parameter mode")));
  }

  {
    // Positional mode with named parameter.
    AnalyzerOptions options = options_;
    options.set_parameter_mode(PARAMETER_POSITIONAL);
    ZETASQL_EXPECT_OK(options.AddQueryParameter("test_param", types::Int64Type()));

    EXPECT_THAT(AnalyzeStatement("SELECT 1;", options, nullptr, &type_factory_,
                                 &output),
                StatusIs(_, HasSubstr("Named parameters cannot be provided "
                                      "in positional parameter mode")));
  }

  {
    // Adding a positional parameter after setting allow_undeclared_parameters.
    AnalyzerOptions options = options_;
    options.set_allow_undeclared_parameters(true);
    options.set_parameter_mode(PARAMETER_POSITIONAL);
    EXPECT_THAT(options.AddPositionalQueryParameter(types::Int64Type()),
                StatusIs(_, HasSubstr("Positional query parameters cannot "
                                      "be provided when undeclared "
                                      "parameters are allowed")));
  }

  {
    // Adding a positional parameter prior to setting
    // allow_undeclared_parameters.
    AnalyzerOptions options = options_;
    options.set_parameter_mode(PARAMETER_POSITIONAL);
    ZETASQL_EXPECT_OK(options.AddPositionalQueryParameter(types::Int64Type()));
    options.set_allow_undeclared_parameters(true);

    EXPECT_THAT(
        AnalyzeStatement("SELECT 1;", options, nullptr, &type_factory_,
                         &output),
        StatusIs(
            _,
            HasSubstr(
                "When undeclared parameters are allowed, no positional query "
                "parameters can be provided")));
  }
}

TEST_F(AnalyzerOptionsTest, AddExpressionColumn) {
  EXPECT_EQ(0, options_.expression_columns().size());
  ZETASQL_EXPECT_OK(
      options_.AddExpressionColumn("bytes", type_factory_.get_bytes()));
  ZETASQL_EXPECT_OK(options_.AddExpressionColumn("bool", type_factory_.get_bool()));
  ZETASQL_EXPECT_OK(
      options_.AddExpressionColumn("MiXeDcAsE", type_factory_.get_string()));

  const QueryParametersMap& expression_columns = options_.expression_columns();
  EXPECT_EQ(3, expression_columns.size());
  EXPECT_EQ("", options_.in_scope_expression_column_name());
  EXPECT_EQ(nullptr, options_.in_scope_expression_column_type());

  // Invalid cases.
  EXPECT_THAT(
      options_.AddExpressionColumn("random", nullptr),
      StatusIs(
          _,
          HasSubstr("Type associated with expression column cannot be NULL")));
  EXPECT_THAT(
      options_.AddExpressionColumn("", type_factory_.get_bool()),
      StatusIs(_, HasSubstr("Expression column cannot have empty name")));
  EXPECT_THAT(
      options_.AddExpressionColumn("MIXEDcase", type_factory_.get_bool()),
      StatusIs(_, HasSubstr("Duplicate expression column name")));

  EXPECT_THAT(options_.SetInScopeExpressionColumn("MIXEDcase",
                                                  type_factory_.get_bool()),
              StatusIs(_, HasSubstr("Duplicate expression column name")));
  EXPECT_EQ("", options_.in_scope_expression_column_name());
  EXPECT_EQ(nullptr, options_.in_scope_expression_column_type());

  ZETASQL_EXPECT_OK(options_.SetInScopeExpressionColumn("InScopeName",
                                                type_factory_.get_bool()));
  EXPECT_EQ(4, expression_columns.size());
  EXPECT_EQ("inscopename", options_.in_scope_expression_column_name());
  EXPECT_EQ("BOOL", options_.in_scope_expression_column_type()->DebugString());
}

TEST_F(AnalyzerOptionsTest, SetInScopeExpressionColumn_Unnamed) {
  EXPECT_TRUE(options_.expression_columns().empty());
  EXPECT_EQ("", options_.in_scope_expression_column_name());
  EXPECT_EQ(nullptr, options_.in_scope_expression_column_type());

  EXPECT_THAT(options_.SetInScopeExpressionColumn("col3", nullptr),
              StatusIs(_, HasSubstr("cannot be NULL")));

  ZETASQL_EXPECT_OK(options_.SetInScopeExpressionColumn("", types::Int32Type()));

  EXPECT_EQ(1, options_.expression_columns().size());
  EXPECT_EQ("", options_.in_scope_expression_column_name());
  EXPECT_EQ("INT32", options_.in_scope_expression_column_type()->DebugString());

  EXPECT_THAT(
      options_.SetInScopeExpressionColumn("xxx", types::BoolType()),
      StatusIs(_, HasSubstr("Cannot call SetInScopeExpressionColumn twice")));

  ZETASQL_EXPECT_OK(options_.AddExpressionColumn("col1", types::BoolType()));
  EXPECT_EQ(2, options_.expression_columns().size());
  EXPECT_EQ("", options_.in_scope_expression_column_name());
  EXPECT_EQ("INT32", options_.in_scope_expression_column_type()->DebugString());
}

TEST_F(AnalyzerOptionsTest, SetInScopeExpressionColumn_Named) {
  ZETASQL_EXPECT_OK(options_.SetInScopeExpressionColumn("value", types::Int32Type()));

  EXPECT_EQ(1, options_.expression_columns().size());
  EXPECT_EQ("value", options_.in_scope_expression_column_name());
  EXPECT_EQ("INT32", options_.in_scope_expression_column_type()->DebugString());

  // When we insert a named in-scope expression column, it also shows up in
  // expression_columns().
  EXPECT_EQ("value", options_.expression_columns().begin()->first);
  EXPECT_EQ("INT32",
            options_.expression_columns().begin()->second->DebugString());

  ZETASQL_EXPECT_OK(options_.AddExpressionColumn("col1", types::BoolType()));
  EXPECT_EQ(2, options_.expression_columns().size());
}

TEST_F(AnalyzerOptionsTest, SetDdlPseudoColumns) {
  EXPECT_EQ(nullptr, options_.ddl_pseudo_columns_callback());

  std::string table_name;
  std::vector<std::string> option_names;
  options_.SetDdlPseudoColumnsCallback(
      [this, &table_name, &option_names](
          const std::vector<std::string>& table_name_parts,
          const std::vector<const ResolvedOption*>& options,
          std::vector<std::pair<std::string, const Type*>>* pseudo_columns) {
        // Just record the table name and options.
        table_name = absl::StrJoin(table_name_parts, ".");
        option_names.clear();
        option_names.reserve(options.size());
        for (const ResolvedOption* option : options) {
          option_names.push_back(option->name());
          // Each option name can be referenced as a pseudo-column.
          pseudo_columns->push_back(
              std::make_pair(option->name(), type_factory_.get_int64()));
        }
        return absl::OkStatus();
      });
  EXPECT_NE(nullptr, options_.ddl_pseudo_columns_callback());

  std::unique_ptr<const AnalyzerOutput> output;
  LanguageOptions language_options;
  language_options.SetSupportsAllStatementKinds();
  language_options.EnableLanguageFeature(FEATURE_CREATE_TABLE_PARTITION_BY);
  options_.set_language_options(language_options);

  // Each test case contains the statement to analyze and the expected table and
  // options that the DDL pseudo-column callback will receive.
  struct TestCase {
    std::string statement;
    std::string expected_table;
    std::vector<std::string> expected_options;
  };
  const std::vector<TestCase> kTestCases = {
      // No PARTITION BY, hence the callback is not invoked.
      {"CREATE TABLE T (x INT64);", "", {}},
      // The PARTITION BY callback receives table name "T".
      {"CREATE TABLE T (x INT64) PARTITION BY x;", "T", {}},
      // Different table name.
      {"CREATE TABLE T.X.Y (x INT64) PARTITION BY x;", "T.X.Y", {}},
      // OPTIONS are foo and bar.
      {"CREATE TABLE T (x INT64) PARTITION BY x OPTIONS (foo=true, bar=1);",
       "T",
       {"foo", "bar"}},
      // The callback sets the pseudo-columns to the names of the options.
      {"CREATE TABLE T (x INT64) PARTITION BY foo, bar "
       "OPTIONS (foo=true, bar=1);",
       "T",
       {"foo", "bar"}},
  };
  for (const TestCase& test_case : kTestCases) {
    SCOPED_TRACE(test_case.statement);
    table_name.clear();
    option_names.clear();
    ZETASQL_ASSERT_OK(AnalyzeStatement(test_case.statement, options_, catalog(),
                               &type_factory_, &output));
    EXPECT_EQ(test_case.expected_table, table_name);
    EXPECT_THAT(option_names,
                ::testing::ElementsAreArray(test_case.expected_options));
  }

  options_.SetDdlPseudoColumns(
      {{"_partition_timestamp", type_factory_.get_timestamp()},
       {"_partition_date", type_factory_.get_date()}});
  std::vector<std::pair<std::string, const Type*>> pseudo_columns;
  ZETASQL_ASSERT_OK(options_.ddl_pseudo_columns_callback()({}, {}, &pseudo_columns));
  EXPECT_EQ(2, pseudo_columns.size());
  ZETASQL_ASSERT_OK(
      AnalyzeStatement("CREATE TABLE T (x INT64, y STRING) "
                       "PARTITION BY _partition_date, _partition_timestamp;",
                       options_, catalog(), &type_factory_, &output));
}

TEST_F(AnalyzerOptionsTest, ErrorMessageFormat) {
  std::unique_ptr<const AnalyzerOutput> output;

  const std::string query = "select *\nfrom BadTable";
  const std::string expr = "1 +\n2 + BadCol +\n3";

  EXPECT_EQ(ErrorMessageMode::ERROR_MESSAGE_ONE_LINE,
            options_.error_message_mode());

  EXPECT_EQ("generic::invalid_argument: Table not found: BadTable [at 2:6]",
            zetasql::internal::StatusToString(AnalyzeStatement(
                query, options_, catalog(), &type_factory_, &output)));
  EXPECT_EQ("generic::invalid_argument: Unrecognized name: BadCol [at 2:5]",
            zetasql::internal::StatusToString(AnalyzeExpression(
                expr, options_, catalog(), &type_factory_, &output)));

  options_.set_error_message_mode(ErrorMessageMode::ERROR_MESSAGE_WITH_PAYLOAD);
  EXPECT_EQ(
      "generic::invalid_argument: Table not found: BadTable "
      "[zetasql.ErrorLocation] { line: 2 column: 6 }",
      zetasql::internal::StatusToString(AnalyzeStatement(
          query, options_, catalog(), &type_factory_, &output)));
  EXPECT_EQ(
      "generic::invalid_argument: Unrecognized name: BadCol "
      "[zetasql.ErrorLocation] { line: 2 column: 5 }",
      zetasql::internal::StatusToString(AnalyzeExpression(
          expr, options_, catalog(), &type_factory_, &output)));

  options_.set_error_message_mode(
      ErrorMessageMode::ERROR_MESSAGE_MULTI_LINE_WITH_CARET);
  EXPECT_EQ(
      "generic::invalid_argument: Table not found: BadTable [at 2:6]\n"
      "from BadTable\n"
      "     ^",
      zetasql::internal::StatusToString(AnalyzeStatement(
          query, options_, catalog(), &type_factory_, &output)));
  EXPECT_EQ(
      "generic::invalid_argument: Unrecognized name: BadCol [at 2:5]\n"
      "2 + BadCol +\n"
      "    ^",
      zetasql::internal::StatusToString(AnalyzeExpression(
          expr, options_, catalog(), &type_factory_, &output)));
}

// Some of these were previously dchecking because of bug 20010119.
TEST_F(AnalyzerOptionsTest, EofErrorMessageTrailingNewlinesAndWhitespace) {
  std::unique_ptr<const AnalyzerOutput> output;

  options_.set_error_message_mode(
      ErrorMessageMode::ERROR_MESSAGE_MULTI_LINE_WITH_CARET);

  // Trailing newlines are ignored for unexpected end of statement errors.
  for (const std::string& newline : {"\n", "\r\n", "\n\r", "\r"}) {
    EXPECT_EQ(
        "generic::invalid_argument: Syntax error: Unexpected end of "
        "statement [at 1:1]\n"
        "\n"
        "^",
        zetasql::internal::StatusToString(AnalyzeStatement(
            newline, options_, catalog(), &type_factory_, &output)));
  }

  // All of these instances of trailing whitespace are ignored for unexpected
  // end of statement errors. We're not testing all possible whitespace, but
  // one multibyte whitespace character is included to verify that we're using
  // the generic whitespace rule for this, and not some ASCII-only hack.
  for (const std::string& whitespace :
       {" ", "   ", "\342\200\200" /* EN QUAD */}) {
    for (const std::string& newline : {"\n", "\r\n", "\n\r", "\r"}) {
      for (const std::string& more_whitespace :
           {"", " ", "   ", "\342\200\200" /* EN QUAD */}) {
        EXPECT_EQ(
            absl::StrCat(
                "generic::invalid_argument: Syntax error: Unexpected end of "
                "statement [at 1:7]\n"
                "SELECT",
                whitespace,
                "\n"
                "      ^"),
            zetasql::internal::StatusToString(AnalyzeStatement(
                absl::StrCat("SELECT", whitespace, newline, more_whitespace),
                options_, catalog(), &type_factory_, &output)));
      }
    }
  }
}

// Basic functionality of literal replacement.
TEST_F(AnalyzerOptionsTest, LiteralReplacement) {
  std::unique_ptr<const AnalyzerOutput> output;
  options_.set_record_parse_locations(true);

  std::string sql = "   \tSELECT 1, 'Yes', 'a'='a'AND'b'='b'";
  ZETASQL_EXPECT_OK(AnalyzeStatement(sql, options_, catalog(),
                             &type_factory_, &output));

  std::string new_sql;
  LiteralReplacementMap literal_map;
  GeneratedParameterMap generated_parameters;
  absl::node_hash_set<std::string> option_names_to_ignore;
  ZETASQL_ASSERT_OK(ReplaceLiteralsByParameters(
      sql, option_names_to_ignore, options_, output.get(),
      &literal_map, &generated_parameters, &new_sql));
  EXPECT_EQ(
      "   \tSELECT @_p0_INT64, @_p1_STRING, @_p2_STRING=@_p3_STRING "
      "AND@_p4_STRING=@_p5_STRING",
      new_sql);
  ASSERT_EQ(6, literal_map.size());
  for (const auto& pair : literal_map) {
    if (pair.second == "_p0_INT64") {
      EXPECT_EQ(pair.first->value(), Value::Int64(1));
    } else if (pair.second == "_p1_STRING") {
      EXPECT_EQ(pair.first->value(), Value::String("Yes"));
    } else {
      EXPECT_THAT(pair.second,
                  testing::AnyOf("_p0_INT64", "_p1_STRING", "_p2_STRING",
                                 "_p3_STRING", "_p4_STRING", "_p5_STRING"));
    }
  }
  ASSERT_EQ(6, generated_parameters.size());
  EXPECT_EQ(generated_parameters["_p0_INT64"], Value::Int64(1));
  EXPECT_EQ(generated_parameters["_p1_STRING"], Value::String("Yes"));
}

// Test 'option_names_to_ignore' which is a set of option names to ignore during
// literal removal.
TEST_F(AnalyzerOptionsTest, LiteralReplacementIgnoreWhitelistedOptions) {
  std::unique_ptr<const AnalyzerOutput> output;
  options_.set_record_parse_locations(true);

  std::string sql = "   \t@{ hint1=1, hint2=true, hint3='foo' }SELECT 1, 'Yes'";
  ZETASQL_EXPECT_OK(AnalyzeStatement(sql, options_, catalog(),
                             &type_factory_, &output));

  std::string new_sql;
  LiteralReplacementMap literal_map;
  GeneratedParameterMap generated_parameters;
  ZETASQL_ASSERT_OK(ReplaceLiteralsByParameters(
      sql, /* ignore hint1 and hint3 but remove hint2 */ {"hint1", "hint3"},
      options_, output.get(), &literal_map, &generated_parameters, &new_sql));
  EXPECT_EQ(
      "   \t@{ hint1=1, hint2=@_p0_BOOL, hint3='foo' }SELECT @_p1_INT64, "
      "@_p2_STRING",
      new_sql);
  ASSERT_EQ(3, literal_map.size());
  for (const auto& pair : literal_map) {
    if (pair.second == "_p0_BOOL") {
      EXPECT_EQ(pair.first->value(), Value::Bool(true));
    } else if (pair.second == "_p1_INT64") {
      EXPECT_EQ(pair.first->value(), Value::Int64(1));
    } else if (pair.second == "_p2_STRING") {
      EXPECT_EQ(pair.first->value(), Value::String("Yes"));
    }
  }
  ASSERT_EQ(3, generated_parameters.size());
  EXPECT_EQ(generated_parameters["_p0_BOOL"], Value::Bool(true));
  EXPECT_EQ(generated_parameters["_p1_INT64"], Value::Int64(1));
  EXPECT_EQ(generated_parameters["_p2_STRING"], Value::String("Yes"));
}

TEST_F(AnalyzerOptionsTest, DeprecationWarnings) {
  // Create some deprecated functions as a stable way to generate deprecation
  // errors.  (Real deprecated features in the language won't stick around.)
  FunctionOptions function_options;
  function_options.set_is_deprecated(true);

  SimpleCatalog catalog("catalog");
  catalog.AddZetaSQLFunctions();
  catalog.AddOwnedFunction(new Function(
      "depr1", "test_group", Function::SCALAR,
      {{ARG_TYPE_ANY_1, {ARG_TYPE_ANY_1}, 1}}, function_options));
  catalog.AddOwnedFunction(new Function(
      "depr2", "test_group", Function::SCALAR,
      {{ARG_TYPE_ANY_1, {ARG_TYPE_ANY_1}, 2}}, function_options));

  // Use the same base expression in a query or in an expression, with a
  // leading newline so the error locations come out the same.
  const std::string base_expr = "DEPR1(5) + DEPR1(6) + DEPR2(7)";
  const std::string expr = absl::StrCat("\n", base_expr);
  const std::string sql = absl::StrCat("SELECT\n", base_expr);
  const std::string multi_sql = absl::StrCat("SELECT 1; SELECT\n", base_expr);

  std::unique_ptr<const AnalyzerOutput> output;

  EXPECT_FALSE(options_.language().error_on_deprecated_syntax());

  // Test with multiple input types (0=Statement, 1=NextStatement, 2=Expression)
  // and with error_on_deprecated_syntax on or off.
  for (int input_type = 0; input_type <= 2; ++input_type) {
    for (int with_errors = 0; with_errors <= 1; ++with_errors) {
      options_.mutable_language()->set_error_on_deprecated_syntax(
          with_errors == 1);

      absl::Status status;
      if (input_type == 0) {
        status = AnalyzeStatement(sql, options_, &catalog, &type_factory_,
                                  &output);
      } else if (input_type == 1) {
        // Parse two queries from one string.  The first query has no warnings.
        bool at_end;
        ParseResumeLocation location =
            ParseResumeLocation::FromStringView(multi_sql);
        ZETASQL_EXPECT_OK(AnalyzeNextStatement(
            &location, options_, &catalog, &type_factory_, &output, &at_end));
        ASSERT_FALSE(at_end);
        EXPECT_EQ(0, output->deprecation_warnings().size());

        status = AnalyzeNextStatement(
            &location, options_, &catalog, &type_factory_, &output, &at_end);
        EXPECT_TRUE(at_end);
      } else if (input_type == 2) {
        status = AnalyzeExpression(expr, options_, &catalog, &type_factory_,
                                   &output);
      }

      if (with_errors == 1) {
        EXPECT_EQ(
            "generic::invalid_argument: Function TEST_GROUP:DEPR1 is "
            "deprecated [at 2:1] [zetasql.DeprecationWarning] "
            "{ kind: DEPRECATED_FUNCTION }",
            zetasql::internal::StatusToString(status));
      } else {
        ZETASQL_EXPECT_OK(status);
        ASSERT_EQ(2, output->deprecation_warnings().size());
        EXPECT_EQ(
            "generic::invalid_argument: Function TEST_GROUP:DEPR1 is "
            "deprecated [at 2:1] [zetasql.DeprecationWarning] "
            "{ kind: DEPRECATED_FUNCTION }",
            zetasql::internal::StatusToString(
                output->deprecation_warnings()[0]));
        EXPECT_EQ(
            "generic::invalid_argument: Function TEST_GROUP:DEPR2 is "
            "deprecated [at 2:23] [zetasql.DeprecationWarning] "
            "{ kind: DEPRECATED_FUNCTION }",
            zetasql::internal::StatusToString(
                output->deprecation_warnings()[1]));
      }
    }
  }
}

void InitSourceTree(google::protobuf::compiler::DiskSourceTree* source_tree) {
  // Support both sides of --incompatible_generated_protos_in_virtual_imports.
  source_tree->MapPath(
      "", zetasql_base::JoinPath(getenv("TEST_SRCDIR"), "com_google_protobuf",
            "_virtual_imports", "descriptor_proto"));
  source_tree->MapPath(
      "", zetasql_base::JoinPath(getenv("TEST_SRCDIR"), "com_google_protobuf"));
  source_tree->MapPath(
      "", zetasql_base::JoinPath(getenv("TEST_SRCDIR"), "com_google_zetasql"));
}

// Need to implement this to catch importing errors.
class MultiFileErrorCollector
    : public google::protobuf::compiler::MultiFileErrorCollector {
 public:
  MultiFileErrorCollector() {}
  MultiFileErrorCollector(const MultiFileErrorCollector&) = delete;
  MultiFileErrorCollector& operator=(const MultiFileErrorCollector&) = delete;
  void AddError(const std::string& filename, int line, int column,
                const std::string& message) override {
    absl::StrAppend(&error_, "Line ", line, " Column ", column, " :", message,
                    "\n");
  }
  const std::string& GetError() const { return error_; }

 private:
  std::string error_;
};

TEST_F(AnalyzerOptionsTest, Deserialize) {
  TypeFactory factory;

  const std::vector<std::string> test_files{
      // Order matters for these imports.
      "google/protobuf/descriptor.proto",
      "zetasql/public/proto/type_annotation.proto",
      "zetasql/testdata/test_schema.proto",
      "zetasql/testdata/external_extension.proto",
  };
  google::protobuf::compiler::DiskSourceTree source_tree;
  InitSourceTree(&source_tree);
  MultiFileErrorCollector error_collector;

  std::unique_ptr<google::protobuf::compiler::Importer> proto_importer(
      new google::protobuf::compiler::Importer(&source_tree, &error_collector));

  for (const std::string& test_file : test_files) {
    CHECK(proto_importer->Import(test_file) != nullptr)
        << "Error importing " << test_file << ": "
        << error_collector.GetError();
  }
  std::unique_ptr<google::protobuf::DescriptorPool> external_pool(
      new google::protobuf::DescriptorPool(proto_importer->pool()));

  const EnumType* generated_enum_type;
  ZETASQL_CHECK_OK(factory.MakeEnumType(TypeKind_descriptor(), &generated_enum_type));

  const ProtoType* generated_proto_type;
  ZETASQL_CHECK_OK(factory.MakeProtoType(
      TypeProto::descriptor(), &generated_proto_type));

  const EnumType* external_enum_type;
  ZETASQL_CHECK_OK(factory.MakeEnumType(
      external_pool->FindEnumTypeByName("zetasql_test.TestEnum"),
      &external_enum_type));

  const ProtoType* external_proto_type;
  ZETASQL_CHECK_OK(factory.MakeProtoType(
      external_pool->FindMessageTypeByName("zetasql_test.KitchenSinkPB"),
      &external_proto_type));

  std::vector<const google::protobuf::DescriptorPool*> pools = {
    external_pool.get(), google::protobuf::DescriptorPool::generated_pool()
  };

  AnalyzerOptionsProto proto;
  proto.set_error_message_mode(ERROR_MESSAGE_WITH_PAYLOAD);
  proto.set_prune_unused_columns(true);
  proto.set_record_parse_locations(true);
  proto.set_allow_undeclared_parameters(true);
  proto.set_parameter_mode(PARAMETER_POSITIONAL);
  proto.mutable_language_options()->set_product_mode(PRODUCT_INTERNAL);
  proto.set_default_timezone("Asia/Shanghai");
  proto.set_preserve_column_aliases(false);

  auto* param = proto.add_query_parameters();
  param->set_name("q1");
  ZETASQL_CHECK_OK(types::Int64Type()->SerializeToSelfContainedProto(
      param->mutable_type()));

  param = proto.add_query_parameters();
  param->set_name("q2");
  ZETASQL_CHECK_OK(external_enum_type->SerializeToProtoAndFileDescriptors(
      param->mutable_type()));
  param->mutable_type()->mutable_enum_type()->set_file_descriptor_set_index(0);

  param = proto.add_query_parameters();
  param->set_name("q3");
  ZETASQL_CHECK_OK(external_proto_type->SerializeToProtoAndFileDescriptors(
      param->mutable_type()));
  param->mutable_type()->mutable_proto_type()->set_file_descriptor_set_index(0);

  param = proto.add_query_parameters();
  param->set_name("q4");
  ZETASQL_CHECK_OK(generated_enum_type->SerializeToProtoAndFileDescriptors(
      param->mutable_type()));
  param->mutable_type()->mutable_enum_type()->set_file_descriptor_set_index(1);

  param = proto.add_query_parameters();
  param->set_name("q5");
  ZETASQL_CHECK_OK(generated_proto_type->SerializeToProtoAndFileDescriptors(
      param->mutable_type()));
  param->mutable_type()->mutable_proto_type()->set_file_descriptor_set_index(1);

  ZETASQL_ASSERT_OK(types::Int64Type()->SerializeToSelfContainedProto(
      proto.add_positional_query_parameters()));

  ZETASQL_ASSERT_OK(generated_proto_type->SerializeToSelfContainedProto(
      proto.add_positional_query_parameters()));
  proto.mutable_positional_query_parameters(1)
      ->mutable_proto_type()
      ->set_file_descriptor_set_index(1);

  auto* column = proto.add_expression_columns();
  column->set_name("c1");
  ZETASQL_CHECK_OK(external_enum_type->SerializeToProtoAndFileDescriptors(
      column->mutable_type()));
  column->mutable_type()->mutable_enum_type()->set_file_descriptor_set_index(0);

  column = proto.add_expression_columns();
  column->set_name("c2");
  ZETASQL_CHECK_OK(types::Int64Type()->SerializeToSelfContainedProto(
      column->mutable_type()));

  column = proto.add_expression_columns();
  column->set_name("c3");
  ZETASQL_CHECK_OK(external_proto_type->SerializeToProtoAndFileDescriptors(
      column->mutable_type()));
  column->mutable_type()->mutable_proto_type()
      ->set_file_descriptor_set_index(0);

  column = proto.add_expression_columns();
  column->set_name("c4");
  ZETASQL_CHECK_OK(generated_enum_type->SerializeToProtoAndFileDescriptors(
      column->mutable_type()));
  column->mutable_type()->mutable_enum_type()->set_file_descriptor_set_index(1);

  column = proto.add_expression_columns();
  column->set_name("c5");
  ZETASQL_CHECK_OK(generated_proto_type->SerializeToProtoAndFileDescriptors(
      column->mutable_type()));
  column->mutable_type()->mutable_proto_type()
      ->set_file_descriptor_set_index(1);

  auto* in_scope_column = proto.mutable_in_scope_expression_column();
  in_scope_column->set_name("isc");
  ZETASQL_CHECK_OK(generated_proto_type->SerializeToProtoAndFileDescriptors(
      in_scope_column->mutable_type()));
  in_scope_column->mutable_type()->mutable_proto_type()
      ->set_file_descriptor_set_index(1);

  auto* allowed = proto.mutable_allowed_hints_and_options();
  allowed->add_disallow_unknown_hints_with_qualifier("qual");
  allowed->add_disallow_unknown_hints_with_qualifier("");
  allowed->set_disallow_unknown_options(true);
  auto* hint1 = allowed->add_hint();
  hint1->set_qualifier("test_qual");
  hint1->set_name("hint1");
  ZETASQL_CHECK_OK(generated_proto_type->SerializeToProtoAndFileDescriptors(
      hint1->mutable_type()));
  hint1->mutable_type()->mutable_proto_type()->set_file_descriptor_set_index(1);
  hint1->set_allow_unqualified(true);
  auto* hint2 = allowed->add_hint();
  hint2->set_qualifier("test_qual");
  hint2->set_name("untyped_hint");
  hint2->set_allow_unqualified(true);
  auto* option1 = allowed->add_option();
  option1->set_name("untyped_option");
  auto* option2 = allowed->add_option();
  option2->set_name("option2");
  ZETASQL_CHECK_OK(generated_enum_type->SerializeToProtoAndFileDescriptors(
      option2->mutable_type()));
  option2->mutable_type()->mutable_enum_type()
      ->set_file_descriptor_set_index(1);

  AnalyzerOptions options;
  ZETASQL_CHECK_OK(AnalyzerOptions::Deserialize(proto, pools, &factory, &options));

  ASSERT_TRUE(options.prune_unused_columns());
  ASSERT_TRUE(options.record_parse_locations());
  ASSERT_TRUE(options.allow_undeclared_parameters());
  ASSERT_EQ(ERROR_MESSAGE_WITH_PAYLOAD, options.error_message_mode());
  ASSERT_EQ(PRODUCT_INTERNAL, options.language().product_mode());
  ASSERT_EQ(PARAMETER_POSITIONAL, options.parameter_mode());
  ASSERT_FALSE(options.preserve_column_aliases());

  ASSERT_EQ(5, options.query_parameters().size());
  ASSERT_TRUE(types::Int64Type()->Equals(options.query_parameters().at("q1")));
  ASSERT_TRUE(external_enum_type->Equals(options.query_parameters().at("q2")));
  ASSERT_TRUE(external_proto_type->Equals(options.query_parameters().at("q3")));
  ASSERT_TRUE(generated_enum_type->Equals(options.query_parameters().at("q4")));
  ASSERT_TRUE(generated_proto_type->Equals(
      options.query_parameters().at("q5")));

  ASSERT_EQ(2, options.positional_query_parameters().size());
  ASSERT_TRUE(
      types::Int64Type()->Equals(options.positional_query_parameters()[0]));
  ASSERT_TRUE(
      generated_proto_type->Equals(options.positional_query_parameters()[1]));

  ASSERT_EQ(6, options.expression_columns().size());
  ASSERT_TRUE(external_enum_type->Equals(
      options.expression_columns().at("c1")));
  ASSERT_TRUE(types::Int64Type()->Equals(
      options.expression_columns().at("c2")));
  ASSERT_TRUE(external_proto_type->Equals(
      options.expression_columns().at("c3")));
  ASSERT_TRUE(generated_enum_type->Equals(
      options.expression_columns().at("c4")));
  ASSERT_TRUE(generated_proto_type->Equals(
      options.expression_columns().at("c5")));
  ASSERT_TRUE(generated_proto_type->Equals(
      options.expression_columns().at("isc")));

  ASSERT_TRUE(options.has_in_scope_expression_column());
  ASSERT_EQ("isc", options.in_scope_expression_column_name());
  ASSERT_TRUE(generated_proto_type->Equals(
      options.in_scope_expression_column_type()));
  ASSERT_EQ("Asia/Shanghai", options.default_time_zone().name());

  ASSERT_TRUE(options.allowed_hints_and_options()
              .disallow_unknown_hints_with_qualifiers.find("qual") !=
                  options.allowed_hints_and_options()
                  .disallow_unknown_hints_with_qualifiers.end());
  ASSERT_TRUE(options.allowed_hints_and_options()
              .disallow_unknown_hints_with_qualifiers.find("") !=
                  options.allowed_hints_and_options()
                  .disallow_unknown_hints_with_qualifiers.end());
  ASSERT_TRUE(options.allowed_hints_and_options().disallow_unknown_options);
  ASSERT_TRUE(generated_proto_type->Equals(
      options.allowed_hints_and_options()
          .hints_lower.find(std::make_pair("test_qual", "hint1"))
          ->second));
  ASSERT_TRUE(generated_proto_type->Equals(
      options.allowed_hints_and_options()
          .hints_lower.find(std::make_pair("", "hint1"))
          ->second));
  ASSERT_THAT(options.allowed_hints_and_options()
                  .hints_lower.find(std::make_pair("test_qual", "untyped_hint"))
                  ->second,
              IsNull());
  ASSERT_TRUE(generated_enum_type->Equals(options.allowed_hints_and_options()
              .options_lower.find("option2")->second));
  ASSERT_THAT(options.allowed_hints_and_options()
              .options_lower.find("untyped_option")->second, IsNull());
}

TEST_F(AnalyzerOptionsTest, ClassAndProtoSize) {
  EXPECT_EQ(224, sizeof(AnalyzerOptions) - sizeof(LanguageOptions) -
                     sizeof(AllowedHintsAndOptions) -
                     sizeof(Catalog::FindOptions) - sizeof(SystemVariablesMap) -
                     2 * sizeof(QueryParametersMap) - 1 * sizeof(std::string))
      << "The size of AnalyzerOptions class has changed, please also update "
      << "the proto and serialization code if you added/removed fields in it.";
  EXPECT_EQ(18, AnalyzerOptionsProto::descriptor()->field_count())
      << "The number of fields in AnalyzerOptionsProto has changed, please "
      << "also update the serialization code accordingly.";
}

TEST_F(AnalyzerOptionsTest, AllowedHintsAndOptionsSerializeAndDeserialize) {
  TypeFactory factory;

  const std::vector<std::string> test_files{
      // Order matters for these imports.
      "google/protobuf/descriptor.proto",
      "zetasql/public/proto/type_annotation.proto",
      "zetasql/testdata/test_schema.proto",
      "zetasql/testdata/external_extension.proto",
  };
  google::protobuf::compiler::DiskSourceTree source_tree;
  InitSourceTree(&source_tree);
  MultiFileErrorCollector error_collector;

  std::unique_ptr<google::protobuf::compiler::Importer> proto_importer(
      new google::protobuf::compiler::Importer(&source_tree, &error_collector));

  for (const std::string& test_file : test_files) {
    CHECK(proto_importer->Import(test_file) != nullptr)
        << "Error importing " << test_file << ": "
        << error_collector.GetError();
  }
  std::unique_ptr<google::protobuf::DescriptorPool> external_pool(
      new google::protobuf::DescriptorPool(proto_importer->pool()));

  const EnumType* generated_enum_type;
  ZETASQL_CHECK_OK(factory.MakeEnumType(TypeKind_descriptor(), &generated_enum_type));

  const ProtoType* generated_proto_type;
  ZETASQL_CHECK_OK(factory.MakeProtoType(
      TypeProto::descriptor(), &generated_proto_type));

  std::vector<const google::protobuf::DescriptorPool*> pools = {
      google::protobuf::DescriptorPool::generated_pool(), external_pool.get(),
  };

  std::map<const google::protobuf::DescriptorPool*,
  std::unique_ptr<Type::FileDescriptorEntry> > file_descriptor_set_map;
  AllowedHintsAndOptionsProto proto;
  AllowedHintsAndOptions allowed("Qual");
  allowed.AddHint("test_qual", "hint1", generated_proto_type, true);
  allowed.AddHint("test_qual", "hint2", generated_proto_type, false);
  allowed.AddHint("", "hint2", generated_proto_type, true);
  allowed.AddHint("test_qual", "hint3", nullptr, false);
  allowed.AddHint("", "hint3", nullptr, true);
  allowed.AddHint("test_qual", "hint4", generated_enum_type, false);
  allowed.AddHint("", "hint4", nullptr, true);
  allowed.AddHint("test_qual", "hint5", nullptr, false);
  allowed.AddHint("", "hint5", generated_enum_type, true);
  allowed.AddHint("test_qual", "hint6", generated_proto_type, false);
  allowed.AddHint("", "untyped_qual", nullptr, true);
  allowed.AddOption("option1", generated_enum_type);
  allowed.AddOption("untyped_option", nullptr);
  ZETASQL_CHECK_OK(allowed.Serialize(&file_descriptor_set_map, &proto));
  EXPECT_EQ(2, proto.disallow_unknown_hints_with_qualifier_size());
  EXPECT_TRUE(proto.disallow_unknown_options());
  EXPECT_EQ(9, proto.hint_size());
  EXPECT_EQ(2, proto.option_size());
  AllowedHintsAndOptions generated_allowed;
  ZETASQL_CHECK_OK(AllowedHintsAndOptions::Deserialize(
      proto, pools, &factory, &generated_allowed));
  EXPECT_EQ(2, generated_allowed.disallow_unknown_hints_with_qualifiers.size());
  EXPECT_TRUE(generated_allowed.disallow_unknown_options);
  EXPECT_EQ(12, generated_allowed.hints_lower.size());
  EXPECT_EQ(2, generated_allowed.options_lower.size());
}

TEST(AllowedHintsAndOptionsTest, ClassAndProtoSize) {
  EXPECT_EQ(104, sizeof(AllowedHintsAndOptions) - sizeof(std::set<std::string>))
      << "The size of AllowedHintsAndOptions class has changed, please also "
      << "update the proto and serialization code if you added/removed fields "
      << "in it.";
  EXPECT_EQ(4, AllowedHintsAndOptionsProto::descriptor()->field_count())
      << "The number of fields in AllowedHintsAndOptionsProto has changed, "
      << "please also update the serialization code accordingly.";
}

// Defines a triple for testing the SupportedStatement feature in ZetaSQL.
struct SupportedStatementTestInput {
  SupportedStatementTestInput(const std::string& stmt, bool success,
                              const std::set<ResolvedNodeKind>& statement_kinds)
      : statement(stmt),
        expect_success(success),
        supported_statement_kinds(statement_kinds) {}
  // The statement to attempt to parse and analyze.
  const std::string statement;
  // The expected outcome of the parse and analyze.
  const bool expect_success;
  // The ResolvedNodeKinds to whitelist. The empty set is all accepting.
  const std::set<ResolvedNodeKind> supported_statement_kinds;
};

// Test ZetaSQL's ability to reject statements not in the whitelist
// contained in the AnalyzerOptions.
TEST(AnalyzerSupportedStatementsTest, SupportedStatementTest) {
  TypeFactory type_factory;
  std::unique_ptr<const AnalyzerOutput> output;
  const std::string query = "SELECT 1;";
  const std::string explain_query = "EXPLAIN SELECT 1;";
  // The statement is resolved first before looking at the hint.  If we have
  // an invalid hint on an unsupported statement, we'll get the unsupported
  // statement error.
  const std::string hinted_query = "@{key=value} SELECT 1;";
  const std::string bad_hinted_query = "@{key=@xxx} SELECT 1;";

  std::vector<SupportedStatementTestInput> test_input = {
      SupportedStatementTestInput(query, true, {}),
      SupportedStatementTestInput(query, true, {RESOLVED_QUERY_STMT}),
      SupportedStatementTestInput(query, false, {RESOLVED_EXPLAIN_STMT}),
      SupportedStatementTestInput(query, true,
                                  {RESOLVED_QUERY_STMT, RESOLVED_EXPLAIN_STMT}),
      SupportedStatementTestInput(explain_query, true, {}),
      SupportedStatementTestInput(explain_query, false, {RESOLVED_QUERY_STMT}),
      SupportedStatementTestInput(explain_query, false,
                                  {RESOLVED_EXPLAIN_STMT}),
      SupportedStatementTestInput(explain_query, true,
                                  {RESOLVED_QUERY_STMT,
                                   RESOLVED_EXPLAIN_STMT}),
      SupportedStatementTestInput(hinted_query, true, {RESOLVED_QUERY_STMT}),
      SupportedStatementTestInput(hinted_query, false, {RESOLVED_EXPLAIN_STMT}),
      SupportedStatementTestInput(bad_hinted_query, false,
                                  {RESOLVED_EXPLAIN_STMT}),
  };

  for (const SupportedStatementTestInput& input : test_input) {
    AnalyzerOptions options;
    options.mutable_language()->SetSupportedStatementKinds(
        input.supported_statement_kinds);
    SampleCatalog catalog(options.language());
    const absl::Status status = AnalyzeStatement(
        input.statement, options, catalog.catalog(), &type_factory, &output);
    if (input.expect_success) {
      ZETASQL_EXPECT_OK(status);
    } else {
      EXPECT_THAT(status, StatusIs(_, HasSubstr("Statement not supported:")));
    }
  }
}

// Verifies that an unsupported ALTER TABLE statement reports an informative
// error message rather than a syntax error (b/31389932).
TEST(AnalyzerSupportedStatementsTest, AlterTableNotSupported) {
  AnalyzerOptions options;
  options.mutable_language()->SetSupportedStatementKinds({RESOLVED_QUERY_STMT});
  SampleCatalog catalog(options.language());
  TypeFactory type_factory;
  std::unique_ptr<const AnalyzerOutput> output;
  const std::string sql = "ALTER TABLE t ADD COLUMN c INT64";
  auto status = AnalyzeStatement(
      sql, options, catalog.catalog(), &type_factory, &output);
  EXPECT_THAT(status, StatusIs(_, HasSubstr("Statement not supported")));
}

// Defines a test for the LanguageFeatures option in ZetaSQL.  The
// <expected_error_string> is only compared if it is non-empty and
// <expect_success> is false.
struct SupportedFeatureTestInput {
  SupportedFeatureTestInput(const std::string& stmt,
                            const std::set<LanguageFeature>& features,
                            bool expect_success_in,
                            const std::string& error_string = "")
      : statement(stmt),
        supported_features(features),
        expect_success(expect_success_in),
        expected_error_string(error_string) {}

  // The statement to parse and analyze.
  const std::string statement;
  // The LanguageFeatures to whitelist.
  const std::set<LanguageFeature> supported_features;
  // The expected outcome of the parse and analyze.
  const bool expect_success;
  // The expected error string, only relevant if <expect_success> is false.
  const std::string expected_error_string;

  std::string FeaturesToString() const {
    std::string out;
    for (LanguageFeature feature : supported_features) {
      if (!out.empty()) {
        absl::StrAppend(&out, ", ");
      }
      absl::StrAppend(&out, LanguageFeature_Name(feature));
    }
    return out;
  }
};

// Test ZetaSQL's ability to reject features not in the whitelist
// contained in the AnalyzerOptions.
TEST(AnalyzerSupportedFeaturesTest, SupportedFeaturesTest) {
  // Simple getter/setter tests.
  AnalyzerOptions options;
  EXPECT_FALSE(
      options.language().LanguageFeatureEnabled(FEATURE_ANALYTIC_FUNCTIONS));
  options.mutable_language()->EnableLanguageFeature(FEATURE_ANALYTIC_FUNCTIONS);
  EXPECT_TRUE(
      options.language().LanguageFeatureEnabled(FEATURE_ANALYTIC_FUNCTIONS));
  options.mutable_language()->DisableAllLanguageFeatures();
  EXPECT_FALSE(
      options.language().LanguageFeatureEnabled(FEATURE_ANALYTIC_FUNCTIONS));

  // Setting an already set feature is not an error.
  options.mutable_language()->EnableLanguageFeature(FEATURE_ANALYTIC_FUNCTIONS);
  EXPECT_TRUE(
      options.language().LanguageFeatureEnabled(FEATURE_ANALYTIC_FUNCTIONS));
  options.mutable_language()->EnableMaximumLanguageFeatures();
  EXPECT_TRUE(
      options.language().LanguageFeatureEnabled(FEATURE_ANALYTIC_FUNCTIONS));

  // Disable all and enable all.
  options.mutable_language()->DisableAllLanguageFeatures();
  EXPECT_FALSE(
      options.language().LanguageFeatureEnabled(FEATURE_ANALYTIC_FUNCTIONS));
  options.mutable_language()->EnableMaximumLanguageFeatures();
  EXPECT_TRUE(
      options.language().LanguageFeatureEnabled(FEATURE_ANALYTIC_FUNCTIONS));

  // Query tests.
  TypeFactory type_factory;
  std::unique_ptr<const AnalyzerOutput> output;
  const std::string aggregate_query = "SELECT sum(key) from KeyValue;";
  const std::string analytic_query = "SELECT sum(key) over () from KeyValue;";

  std::vector<SupportedFeatureTestInput> test_input = {
    SupportedFeatureTestInput(aggregate_query, {}, true /* expect_success */),
    SupportedFeatureTestInput(
        aggregate_query, {FEATURE_ANALYTIC_FUNCTIONS}, true),
    // This tests the whitelist feature capability prior to analysis/resolution
    // being implemented for analytic functions.
    SupportedFeatureTestInput(
        analytic_query, {}, false,
        "Analytic functions not supported"),
    SupportedFeatureTestInput(
        analytic_query, {FEATURE_ANALYTIC_FUNCTIONS},
        true /* expect_success */),
  };

  for (const SupportedFeatureTestInput& input : test_input) {
    AnalyzerOptions options;
    options.mutable_language()->SetEnabledLanguageFeatures(
        input.supported_features);
    LOG(INFO) << "Supported features: " << input.FeaturesToString();
    SampleCatalog catalog(options.language());
    const absl::Status status = AnalyzeStatement(
        input.statement, options, catalog.catalog(), &type_factory, &output);
    if (input.expect_success) {
      ZETASQL_EXPECT_OK(status)
          << "Query: " << input.statement
          << "\nSupported features: " << input.FeaturesToString();
    } else {
      EXPECT_THAT(status, StatusIs(_, HasSubstr(input.expected_error_string)))
          << "Query: " << input.statement
          << "\nSupported features: " << input.FeaturesToString();
    }
  }
}

TEST(AnalyzerSupportedFeaturesTest, EnableFeaturesForVersion) {
  // Tests below show that SetLanguageVersion sets the features to exactly
  // the features for that version, and clears any others (like
  // FEATURE_ANALYTIC_FUNCTIONS).
  AnalyzerOptions options;
  options.mutable_language()->EnableLanguageFeature(FEATURE_ANALYTIC_FUNCTIONS);
  options.mutable_language()->EnableLanguageFeature(
      FEATURE_V_1_1_ORDER_BY_COLLATE);

  options.mutable_language()->SetLanguageVersion(VERSION_1_0);
  EXPECT_FALSE(
      options.language().LanguageFeatureEnabled(FEATURE_ANALYTIC_FUNCTIONS));
  EXPECT_FALSE(options.language().LanguageFeatureEnabled(
      FEATURE_V_1_1_ORDER_BY_COLLATE));

  options.mutable_language()->EnableLanguageFeature(FEATURE_ANALYTIC_FUNCTIONS);
  options.mutable_language()->SetLanguageVersion(VERSION_1_1);
  EXPECT_FALSE(
      options.language().LanguageFeatureEnabled(FEATURE_ANALYTIC_FUNCTIONS));
  EXPECT_TRUE(options.language().LanguageFeatureEnabled(
      FEATURE_V_1_1_ORDER_BY_COLLATE));

  options.mutable_language()->EnableLanguageFeature(FEATURE_ANALYTIC_FUNCTIONS);
  EXPECT_TRUE(
      options.language().LanguageFeatureEnabled(FEATURE_ANALYTIC_FUNCTIONS));
  EXPECT_TRUE(options.language().LanguageFeatureEnabled(
      FEATURE_V_1_1_ORDER_BY_COLLATE));

  options.mutable_language()->DisableAllLanguageFeatures();
  EXPECT_FALSE(
      options.language().LanguageFeatureEnabled(FEATURE_ANALYTIC_FUNCTIONS));
  EXPECT_FALSE(options.language().LanguageFeatureEnabled(
      FEATURE_V_1_1_ORDER_BY_COLLATE));

  // VERSION_CURRENT clears the features and adds back in the maximal set
  // of versioned features, but not the cross-version features.
  options.mutable_language()->EnableLanguageFeature(FEATURE_ANALYTIC_FUNCTIONS);
  options.mutable_language()->SetLanguageVersion(VERSION_CURRENT);
  EXPECT_FALSE(
      options.language().LanguageFeatureEnabled(FEATURE_ANALYTIC_FUNCTIONS));
  EXPECT_TRUE(options.language().LanguageFeatureEnabled(
      FEATURE_V_1_1_ORDER_BY_COLLATE));
}

// Test resolving a proto extension where the extension comes from
// a different DescriptorPool.  We have to load up a second DescriptorPool
// manually to test this.
TEST(AnalyzerTest, ExternalExtension) {
  const std::vector<std::string> test_files{
      // Order matters for these imports.
      "google/protobuf/descriptor.proto",
      "zetasql/public/proto/type_annotation.proto",
      "zetasql/testdata/test_schema.proto",
      "zetasql/testdata/external_extension.proto",
  };
  google::protobuf::compiler::DiskSourceTree source_tree;
  InitSourceTree(&source_tree);

  MultiFileErrorCollector error_collector;
  std::unique_ptr<google::protobuf::compiler::Importer> proto_importer(
      new google::protobuf::compiler::Importer(&source_tree, &error_collector));

  for (const std::string& test_file : test_files) {
    CHECK(proto_importer->Import(test_file) != nullptr)
        << "Error importing " << test_file << ": "
        << error_collector.GetError();
  }
  std::unique_ptr<google::protobuf::DescriptorPool> external_pool(
      new google::protobuf::DescriptorPool(proto_importer->pool()));
  const std::string external_extension_name =
      "zetasql_test.ExternalExtension";
  const google::protobuf::Descriptor* external_extension =
      external_pool->FindMessageTypeByName(external_extension_name);
  ASSERT_NE(external_extension, nullptr);

  const std::string query1 =
      "SELECT "
      "t.KitchenSink.test_extra_pb.(zetasql_test.ExternalExtension.field) "
      "as ext_field FROM TestTable t";

  TypeFactory type_factory;
  AnalyzerOptions options;
  SampleCatalog sample_catalog(options.language());
  SimpleCatalog* catalog = sample_catalog.catalog();

  std::unique_ptr<const AnalyzerOutput> output1;

  // Check that the extension type name does not exist in the default catalog.
  const Type* found_type;
  ZETASQL_EXPECT_OK(catalog->FindType({"zetasql_test.KitchenSinkPB"}, &found_type));
  EXPECT_FALSE(catalog->FindType({external_extension_name}, &found_type).ok());

  // Analyze the queries and we'll get an error that the extension is not found.
  EXPECT_THAT(
      AnalyzeStatement(query1, options, catalog, &type_factory, &output1),
      StatusIs(
          _,
          HasSubstr(
              "Extension zetasql_test.ExternalExtension.field not found")));

  // Now add the extension type into the Catalog.
  // (Modifying the SampleCatalog is a hack to save code.)
  const ProtoType* extension_type;
  ZETASQL_ASSERT_OK(type_factory.MakeProtoType(external_extension, &extension_type));
  static_cast<SimpleCatalog*>(catalog)->AddType(
      external_extension_name, extension_type);

  ZETASQL_EXPECT_OK(catalog->FindType({external_extension_name}, &found_type));

  // Re-analyze the queries and now it will succeed.
  // This checks that we can create and validate a ResolvedGetProtoField that
  // points at a FieldDescriptor from a different DescriptorPool.
  ZETASQL_ASSERT_OK(AnalyzeStatement(query1, options, catalog, &type_factory,
                             &output1));
}

// Test that the language_options setters and getters on AnalyzerOptions work
// correctly and don't overwrite the options outside LanguageOptions.
TEST(AnalyzerTest, LanguageOptions) {
  AnalyzerOptions analyzer_options;
  EXPECT_FALSE(
      analyzer_options.language().SupportsStatementKind(RESOLVED_DELETE_STMT));
  EXPECT_EQ(ErrorMessageMode::ERROR_MESSAGE_ONE_LINE,
            analyzer_options.error_message_mode());
  analyzer_options.set_error_message_mode(
      ErrorMessageMode::ERROR_MESSAGE_WITH_PAYLOAD);
  EXPECT_EQ(ErrorMessageMode::ERROR_MESSAGE_WITH_PAYLOAD,
            analyzer_options.error_message_mode());

  LanguageOptions language_options;
  EXPECT_FALSE(language_options.SupportsStatementKind(RESOLVED_DELETE_STMT));
  language_options.SetSupportsAllStatementKinds();
  EXPECT_TRUE(language_options.SupportsStatementKind(RESOLVED_DELETE_STMT));

  analyzer_options.set_language_options(language_options);
  EXPECT_TRUE(
      analyzer_options.language().SupportsStatementKind(RESOLVED_DELETE_STMT));
  EXPECT_TRUE(
      analyzer_options.language().SupportsStatementKind(RESOLVED_DELETE_STMT));
  EXPECT_EQ(ErrorMessageMode::ERROR_MESSAGE_WITH_PAYLOAD,
            analyzer_options.error_message_mode());

  EXPECT_FALSE(analyzer_options.language().LanguageFeatureEnabled(
      FEATURE_V_1_1_ORDER_BY_COLLATE));
  analyzer_options.mutable_language()->EnableLanguageFeature(
      FEATURE_V_1_1_ORDER_BY_COLLATE);
  EXPECT_TRUE(analyzer_options.language().LanguageFeatureEnabled(
      FEATURE_V_1_1_ORDER_BY_COLLATE));

  const AnalyzerOptions analyzer_options2(language_options);
  EXPECT_TRUE(
      analyzer_options2.language().SupportsStatementKind(RESOLVED_DELETE_STMT));
  EXPECT_EQ(ErrorMessageMode::ERROR_MESSAGE_ONE_LINE,
            analyzer_options2.error_message_mode());
}

TEST(SQLBuilderTest, Int32ParameterForLimit) {
  auto cast_limit = MakeResolvedCast(types::Int64Type(),
                                     MakeResolvedLiteral(values::Int32(2)),
                                     /*return_null_on_error=*/false);
  auto cast_offset = MakeResolvedCast(types::Int64Type(),
                                      MakeResolvedLiteral(values::Int32(1)),
                                      /*return_null_on_error=*/false);

  auto limit_offset_scan = MakeResolvedLimitOffsetScan(
      /*column_list=*/{}, MakeResolvedSingleRowScan(), std::move(cast_limit),
      std::move(cast_offset));
  SQLBuilder sql_builder;
  ZETASQL_ASSERT_OK(sql_builder.Process(*limit_offset_scan));
  std::string formatted_sql;
  ZETASQL_ASSERT_OK(FormatSql(sql_builder.sql(), &formatted_sql));
  EXPECT_EQ("SELECT\n  1\nLIMIT CAST(2 AS INT32) OFFSET CAST(1 AS INT32);",
            formatted_sql);
}

// Adding specific unit test to input provided by Random Query Generator tree.
// From a SQL String (like in golden file sql_builder.test), we get a different
// tree (FilterScan is under other ResolvedScans and this scenario isn't tested.
TEST(SQLBuilderTest, WithScanWithFilterScan) {
  const std::string with_query_name = "WithTable";
  const std::string table_name = "T1";
  const std::string col_name = "C";

  TypeFactory type_factory;
  auto table = absl::make_unique<SimpleTable>(table_name);

  // With entry list.
  const ResolvedColumn scan_column(10, table_name, col_name,
                                   type_factory.get_int32());
  auto table_scan = MakeResolvedTableScan({scan_column}, table.get(),
                                          /*for_system_time_expr=*/nullptr);
  auto filter_scan = MakeResolvedFilterScan(
      {scan_column}, std::move(table_scan),
      MakeResolvedLiteral(type_factory.get_bool(), Value::Bool(true)));
  std::vector<std::unique_ptr<const ResolvedWithEntry>> with_entry_list;
  with_entry_list.emplace_back(
      MakeResolvedWithEntry(with_query_name, std::move(filter_scan)));

  // With scan query.
  const ResolvedColumn query_column(20, with_query_name, col_name,
                                    type_factory.get_int32());
  auto with_ref_scan = MakeResolvedWithRefScan({query_column}, with_query_name);
  auto query = MakeResolvedProjectScan({query_column}, /*expr_list=*/{},
                                       std::move(with_ref_scan));
  auto with_scan = MakeResolvedWithScan(
      {scan_column}, std::move(with_entry_list), std::move(query));

  // Test that this scan is correctly supported in WITH clause.
  SQLBuilder sql_builder;
  ZETASQL_ASSERT_OK(sql_builder.Process(*with_scan));
  std::string formatted_sql;
  ZETASQL_ASSERT_OK(FormatSql(sql_builder.sql(), &formatted_sql));
  EXPECT_EQ(
      "WITH\n  WithTable AS (\n"
      "    SELECT\n      T1.C AS a_1\n    FROM\n      T1\n"
      "    WHERE\n      true\n  )\n"
      "SELECT\n  withrefscan_2.a_1 AS a_1\nFROM\n  WithTable AS withrefscan_2;",
      formatted_sql);
}

// Adding specific unit test to input provided by Random Query Generator tree.
// From a SQL String (like in golden file sql_builder.test), we get a different
// tree (JoinScan is under other ResolvedScans and this scenario isn't tested.
TEST(SQLBuilderTest, WithScanWithJoinScan) {
  const std::string with_query_name = "WithTable";
  const std::string table_name1 = "T1";
  const std::string table_name2 = "T2";
  const std::string col_name = "C";

  TypeFactory type_factory;
  std::unique_ptr<SimpleTable> table1 =
      absl::make_unique<SimpleTable>(table_name1);
  std::unique_ptr<SimpleTable> table2 =
      absl::make_unique<SimpleTable>(table_name2);

  // With entry list.
  const ResolvedColumn scan_column(10, table_name1, col_name,
                                   type_factory.get_int32());
  auto table_scan1 = MakeResolvedTableScan({scan_column}, table1.get(),
                                           /*for_system_time_expr=*/nullptr);
  auto table_scan2 = MakeResolvedTableScan(/*column_list=*/{}, table2.get(),
                                           /*for_system_time_expr=*/nullptr);
  auto join_scan =
      MakeResolvedJoinScan({scan_column}, ResolvedJoinScan::INNER,
                           std::move(table_scan1), std::move(table_scan2),
                           /*join_expr=*/nullptr);
  std::vector<std::unique_ptr<const ResolvedWithEntry>> with_entry_list;
  with_entry_list.emplace_back(
      MakeResolvedWithEntry(with_query_name, std::move(join_scan)));

  // With scan query.
  const ResolvedColumn query_column(20, with_query_name, col_name,
                                    type_factory.get_int32());
  std::unique_ptr<ResolvedWithRefScan> with_ref_scan =
      MakeResolvedWithRefScan({query_column}, with_query_name);

  auto query = MakeResolvedProjectScan({query_column}, /*expr_list=*/{},
                                       std::move(with_ref_scan));
  auto with_scan = MakeResolvedWithScan(
      {scan_column}, std::move(with_entry_list), std::move(query));

  // Test that this scan is correctly supported in WITH clause.
  SQLBuilder sql_builder;
  ZETASQL_ASSERT_OK(sql_builder.Process(*with_scan));
  std::string formatted_sql;
  ZETASQL_ASSERT_OK(FormatSql(sql_builder.sql(), &formatted_sql));
  EXPECT_EQ(
      "WITH\n  WithTable AS (\n"
      "    SELECT\n      t1_2.a_1 AS a_1\n    FROM\n"
      "      (\n        SELECT\n          T1.C AS a_1\n        FROM\n"
      "          T1\n      ) AS t1_2\n"
      "      CROSS JOIN\n"
      "      (\n        SELECT\n          NULL\n        FROM\n"
      "          T2\n      ) AS t2_3\n"
      "  )\n"
      "SELECT\n  withrefscan_4.a_1 AS a_1\nFROM\n  WithTable AS withrefscan_4;",
      formatted_sql);
}

// Adding specific unit test to input provided by Random Query Generator tree.
// From a SQL String (like in golden file sql_builder.test), we get a different
// tree (ArrayScan is under other ResolvedScans and this scenario isn't tested.
TEST(SQLBuilderTest, WithScanWithArrayScan) {
  const std::string with_query_name = "WithTable";
  const std::string table_name = "T1";
  const std::string col_name = "C";

  TypeFactory type_factory;
  std::unique_ptr<SimpleTable> table =
      absl::make_unique<SimpleTable>(table_name);

  // With entry list.
  const ResolvedColumn scan_column(10, table_name, col_name,
                                   type_factory.get_int32());
  auto table_scan = MakeResolvedTableScan({scan_column}, table.get(),
                                          /*for_system_time_expr=*/nullptr);
  const ResolvedColumn array_column(15, "ArrayName", "ArrayCol",
                                    type_factory.get_int32());
  auto array_expr = MakeResolvedSubqueryExpr(
      type_factory.get_bool(), ResolvedSubqueryExpr::ARRAY,
      /*parameter_list=*/{}, /*in_expr=*/nullptr,
      /*subquery=*/MakeResolvedSingleRowScan());
  auto array_scan = MakeResolvedArrayScan(
      {scan_column}, std::move(table_scan), std::move(array_expr), array_column,
      /*array_offset_column=*/nullptr,
      /*join_expr=*/nullptr, /*is_outer=*/true);
  std::vector<std::unique_ptr<const ResolvedWithEntry>> with_entry_list;
  with_entry_list.emplace_back(
      MakeResolvedWithEntry(with_query_name, std::move(array_scan)));

  // With scan query.
  const ResolvedColumn query_column(20, with_query_name, col_name,
                                    type_factory.get_int32());
  std::unique_ptr<ResolvedWithRefScan> with_ref_scan =
      MakeResolvedWithRefScan({query_column}, with_query_name);
  auto query = MakeResolvedProjectScan({query_column}, /*expr_list=*/{},
                                       std::move(with_ref_scan));
  auto with_scan = MakeResolvedWithScan(
      {scan_column}, std::move(with_entry_list), std::move(query));

  // Test that this scan is correctly supported in WITH clause.
  SQLBuilder sql_builder;
  ZETASQL_ASSERT_OK(sql_builder.Process(*with_scan));
  std::string formatted_sql;
  ZETASQL_ASSERT_OK(FormatSql(sql_builder.sql(), &formatted_sql));
  EXPECT_EQ(
      "WITH\n  WithTable AS (\n"
      "    SELECT\n      t1_2.a_1 AS a_1\n    FROM\n"
      "      (\n        SELECT\n          T1.C AS a_1\n        FROM\n"
      "          T1\n      ) AS t1_2\n"
      "      LEFT JOIN\n"
      "      UNNEST(ARRAY(\n"
      "        SELECT\n          1\n      )) AS a_3\n"
      "  )\n"
      "SELECT\n  withrefscan_4.a_1 AS a_1\nFROM\n  WithTable AS withrefscan_4;",
      formatted_sql);
}

}  // namespace zetasql


text:
layout: post
title: Comment installer Jekyll
categories: howto
tags: [Jekyll]
scripts: [installJekyll.sh]
comments: true
share: true
---

[Jekyll](http://jekyllrb.com/) est le moteur de site de [Github](http://github.com). C'est un générateur de pages
HTML _statiques_, il n'y a donc pas besoin de base de donnée, un simple serveur HTTP fera l'affaire. Cependant,
avant de mettre en ligne le site, il est important de vérifier _en local_ que le rendu est correct.

Jekyll dépends de [Ruby](http://www.ruby-lang.org) en version _1.9.3_ ou _2.0.0_. Sur
[Debian Wheezy](http://www.debian.org/), la version _1.9.3_ est disponible sur le repo.

{% highlight bash %}
sudo aptitude install ruby1.9.3
{% endhighlight %}

Pour faciliter l'installation, il est intéressant d'utiliser bundler. Pour cela :

{% highlight bash %}
sudo gem install bundler
{% endhighlight %}

Dans le dossier du site, créer un fichier nommé `Gemfile` et mettre :

{% highlight ruby %}
source 'https://rubygems.org'
gem 'jekyll'
{% endhighlight %}

Puis lancer la commande :

{% highlight bash %}
bundle install
{% endhighlight %}

Pour générer les pages et lancer un serveur HTTP sur le port 4000 :

{% highlight bash %}
jekyll serve --watch --drafts
{% endhighlight %}

Aller ensuite sur [http://localhost:4000](http://localhost:4000)

redpajama_set_name: RedPajamaGithub


text:
ES5Harness.registerTest( {
id: "14.1-7",

path: "TestCases/chapter14/14.1/14.1-7.js",

description: "'use strict' directive - not recognized if upper case",

test: function testcase() {

  function foo()
  {
    'Use Strict';
     if(this !== undefined)
       return true;
  }

  return foo.call(undefined);
 },

precondition: function prereq() {
  return fnSupportsStrict();
 }
});


text:
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Karbon.Cms.Core;
using Karbon.Cms.Core.Models;
using Karbon.Cms.Core.Stores;

namespace Karbon.Cms.Web.Parsers.KarbonText
{
    [KarbonTextTag("link")]
    public class LinkTag : AbstractKarbonTextTag
    {
        ///
        /// Gets the markup for the tag based upon the passed in parameters.
        ///
        /// <param name="currentPage">The current page.</param>
        /// <param name="parameters">The parameters.</param>
        /// <returns></returns>
        public override string GetMarkup(IContent currentPage, IDictionary<string, string> parameters)
        {
            var url = parameters["link"];
            var text = url;

            if (!url.StartsWith("http") && !url.StartsWith("/"))
            {
                // Try to find matching content
                var content = StoreManager.ContentStore.GetByUrl("~/" + url);
                if (content != null)
                {
                    url = content.Url();
                    text = content.Name;
                }
                else
                {
                    // Try to find matching file
                    var file = currentPage.AllFiles.SingleOrDefault(x => x.Slug == url);
                    if (file != null)
                    {
                        url = file.Url();
                        text = file.Name;
                    }
                }
            }

            var sb = new StringBuilder();
            sb.AppendFormat("");
            sb.Append(parameters.ContainsKey("text")
                ? parameters["text"]
                : text);

            sb.Append("");

            return sb.ToString();
        }
    }

    [KarbonTextTag("a")]
    public class ATag : LinkTag
    {
        ///
        /// Gets the markup for the tag based upon the passed in parameters.
        ///
        /// <param name="currentPage">The current page.</param>
        /// <param name="parameters">The parameters.</param>
        /// <returns></returns>
        public override string GetMarkup(IContent currentPage, IDictionary<string, string> parameters)
        {
            parameters.AddOrUpdate("link", parameters["a"]);

            return base.GetMarkup(currentPage, parameters);
        }
    }
}


text:
<?php

namespace Qless;

require_once __DIR__ . '/Client.php';

class Resource
{
    /**
     * @var Client
     */
    private $client;

    /**
     * @var string
     */
    private $name;

    /**
     * @var int
     */
    private $max;

    public function __construct(Client $client, $name) {
        $this->client = $client;
        $this->name   = $name;
    }

    /**
     * Returns true if this resource exists
     *
     * @return bool
     */
    public function exists() {
        return $this->client->lua->run('resource.exists', [$this->name]) === 1;
    }

    /**
     * Gets the current lock count for this resource
     *
     * @return int
     */
    public function getLockCount() {
        return $this->client->lua->run('resource.lock_count', [$this->name]);
    }

    /**
     * Get a list of job identifiers that have active locks for this resource
     *
     * @return string[]
     */
    public function getLocks() {
        $data = $this->client->lua->run('resource.locks', [$this->name]);

        return json_decode($data);
    }

    /**
     * Gets the current lock count for this resource
     *
     * @return int
     */
    public function getPendingCount() {
        return $this->client->lua->run('resource.pending_count', [$this->name]);
    }

    /**
     * Gets a list of job identifiers that are waiting on this resource
     *
     * @return string[]
     */
    public function getPending() {
        $data = $this->client->lua->run('resource.pending', [$this->name]);

        return json_decode($data);
    }

    /**
     * Deletes this resource
     *
     * @return bool true if resource was deleted
     */
    public function delete() {
        return $this->client->lua->run('resource.unset', [$this->name]) === 1;
    }

    /**
     * @param int $max Update the maximum number of units available for this resource
     */
    public function setMax($max) {
        $this->client->lua->run('resource.set', [$this->name, $max]);
        unset($this->max);
    }

    /**
     * Return the maximum number of units available
     *
     * @return int
     */
    public function getMax() {
        if (!isset($this->max)) {
            $this->max = $this->client->lua->run('resource.get', [$this->name]);
        }
        return $this->max;
    }
}


text:
import { createContext, useContext, useRef } from 'react';
import type { Callbacks } from '../../types';

export const CallbacksContext = createContext<Callbacks>({});

/**
 * @returns the callbacks object of the current Editor.
 *
 * this object is memoized, alltough its better to use `usecallback` instead if you want to use a single callback
 */
export const useCallbackOptions = () => useContext(CallbacksContext);

/**
 * get a single (memoized) callback
 * @param key the callback key
 * @returns the callback value
 */
export const useCallbackOption = <K extends keyof Callbacks>(key: K) => {
  const callbacks = useCallbackOptions();
  const callback = callbacks[key];
  const lastcallback = useRef(callback);
  if (lastcallback.current !== callback) {
    lastcallback.current = callback;
  }
  return lastcallback.current;
};


text:
require 'spec_helper'

module WIP::Checklist::Commands
  describe Console do
    subject(:command) { Console.new(io) }

    after do
      WIP::Checklist::Runner.register(Console)
      WIP::Checklist::Runner.deregister(Console::Quit)
    end

    describe '#initialize' do
      it 'deregisters the "console" Command' do
        Console.new(io)
        expect(WIP::Checklist::Runner.commands)
          .to_not include(Console)
      end

      it 'registers the "quit" Command (once)' do
        Console.new(io)
        expect(WIP::Checklist::Runner.commands)
          .to include(Console::Quit)
      end
    end

    describe '#run' do
      it 'starts the REPL' do
        expect(command).to receive(:start)
        command.run
      end
    end

    describe '(private method) #process' do
      let(:args) {  }

      it 'delegates to the Runner' do
        expect(command.runner).to receive(:run)
          .with(['command', '--option'])
        command.send(:process, 'command --option')
      end

      context 'given arguments as "help"' do
        it 'shows help' do
          expect { command.send(:process, 'help') }.to show <<-HELP
            Usage: wip-checklist COMMAND [options]

            Commands:
                quit                             Quits this console session
                help                             Prints context-relevant help
                create                           Creates issues
                version                          Prints version information
                edit                             Edits checklists
                list                             Prints an index of checklists
                save                             Saves checklists
                show                             Edits checklists

            Options:
                -h, --help                       Prints help messages
          HELP
        end
      end
    end

    describe '(private method) #read' do
      let(:answer) { 'command --option' }
      let(:prompt) { '-->' }

      it 'prompts for input and returns the answer' do
        expect(io).to receive(:ask)
          .with(prompt)
          .and_return(answer)
        expect(command.send(:read, prompt)).to eq(answer)
      end
    end
  end
end


text:
<?php require_once("../includes/sessions.php"); ?>
<?php require_once("../includes/functions.php"); ?>
<?php require_once("../includes/validation-functions.php"); ?>
<?php require_once("../includes/db-connection.php"); ?>

<?php

  // Temporary hack to simulate logged in user
  $user_id = 1;

  if(isset($_POST['submit-new-mc-question'])) {

    // The New Multiple Choice Form was submitted

    // Validate New User Form inputs
    $fields_required = array("question_text", "answer1", "answer2",
                              "answer3", "answer4", "weight");
    foreach($fields_required as $field) {
      $value = trim($_POST[$field]);
      if(!has_presence($value)) {
        $error_messages[$field] = ucfirst($field) . " is required.";
      }
    }

    // If there were errors, redirect back to the form.
    if(!empty($error_messages)) {
      $_SESSION["errors"] = $error_messages;

      $form_values = array("question_text" => $_POST['question_text'],
                           "answer1"  => $_POST['answer1'],
                           "answer2"  => $_POST['answer2'],
                           "answer3"  => $_POST['answer3'],
                           "answer4"  => $_POST['answer4'],
                           "weight"   => $_POST['weight']);
      $_SESSION["form_history"] = $form_values;

      redirect_to("new-question.php");
    }

    // If inputs were valid begin insertion.
    $_POST = array_map('mysql_real_escape_string',$_POST);

    $question_text    = $_POST['question_text'];
    $answer1          = $_POST['answer1'];
    $answer2          = $_POST['answer2'];
    $answer3          = $_POST['answer3'];
    $answer4          = $_POST['answer4'];
    $weight           = $_POST['weight'];
    $type             = "Multiple Choice";
    $owner            = $user_id;

    // Insert new question into question table
    $query  = "INSERT INTO question (";
    $query .= "  question_text, type, owner, weight";
    $query .= ") VALUES (";
    $query .= "  '{$question_text}', '{$type}', '{$owner}',";
    $query .= "  '{$weight}'";
    $query .= ")";

    $result = mysqli_query($db, $query);

    if ( false === $result ) {
      // Query failed. Print out information.
      printf("error: %s\n", mysqli_error($db));
      $_SESSION["error_message"] = "Database insertion failure";
      redirect_to("new-question.php");

    }

    $inserted_question_id = mysqli_insert_id($db);

    // Insert new question's answers into answer table
    $ischecked = false;

    // Answer 1
    if($_POST['ischecked'] == '1') { $ischecked = true; }

    $query1  = "INSERT INTO answer (";
    $query1 .= "  answer_text, question_id, is_correct";
    $query1 .= ") VALUES (";
    $query1 .= "  '{$answer1}', '{$inserted_question_id}', '{$ischecked}'";
    $query1 .= ")";

    $result1 = mysqli_query($db, $query1);

    if ( false === $result1 ) {
      // Query failed. Print out information.
      printf("error: %s\n", mysqli_error($db));
      $_SESSION["error_message"] = "Database insertion failure";
      redirect_to("new-question.php");
    }

    $ischecked = false;

    // Answer 2
    if($_POST['ischecked'] == '2') { $ischecked = true; }

    $query2  = "INSERT INTO answer (";
    $query2 .= "  answer_text, question_id, is_correct";
    $query2 .= ") VALUES (";
    $query2 .= "  '{$answer2}', '{$inserted_question_id}', '{$ischecked}'";
    $query2 .= ")";

    $result2 = mysqli_query($db, $query2);

    if ( false === $result2 ) {
      // Query failed. Print out information.
      printf("error: %s\n", mysqli_error($db));
      $_SESSION["error_message"] = "Database insertion failure";
      redirect_to("new-question.php");
    }

    $ischecked = false;

    // Answer 3
    if($_POST['ischecked'] == '3') { $ischecked = true; }

    $query3  = "INSERT INTO answer (";
    $query3 .= "  answer_text, question_id, is_correct";
    $query3 .= ") VALUES (";
    $query3 .= "  '{$answer3}', '{$inserted_question_id}', '{$ischecked}'";
    $query3 .= ")";

    $result3 = mysqli_query($db, $query3);

    if ( false === $result3 ) {
      // Query failed. Print out information.
      printf("error: %s\n", mysqli_error($db));
      $_SESSION["error_message"] = "Database insertion failure";
      redirect_to("new-question.php");
    }

    $ischecked = false;

    // Answer 4
    if($_POST['ischecked'] == '4') { $ischecked = true; }

    $query4  = "INSERT INTO answer (";
    $query4 .= "  answer_text, question_id, is_correct";
    $query4 .= ") VALUES (";
    $query4 .= "  '{$answer4}', '{$inserted_question_id}', '{$ischecked}'";
    $query4 .= ")";

    $result4 = mysqli_query($db, $query4);

    if ( false === $result4 ) {
      // Query failed. Print out information.
      printf("error: %s\n", mysqli_error($db));
      $_SESSION["error_message"] = "Database insertion failure";
      redirect_to("new-question.php");
    }

    // Success
    $_SESSION["message"] = "Successfully created new multiple choice question!";
    redirect_to("manage-questions.php");

  } else {
    // This is probably a get request
    $_SESSION["message"] = "Please fill in the form to create a new question.";
    redirect_to("new-question.php");

  }
?>

<?php require_once("../includes/db-connection-close.php"); ?>


text:
package org.wisdom.test.parents;

import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceReference;
import org.ow2.chameleon.testing.helpers.OSGiHelper;
import org.wisdom.api.Controller;
import org.wisdom.api.router.Router;
import org.wisdom.api.templates.Template;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

/**
 * Injects all @Inject fields found in the given object's class.
 * Support Bundle Context injection, Template injection and Service injection.
 *
 * Be aware that this class is heavily relying on reflection and type matching as we are in a different classloader.
 */
public final class DependencyInjector {

    private static final String INJECTION_ERROR = "Cannot inject a template in ";
    private static final String VALUE = "value";

    private DependencyInjector(){
        //Hide implicit constructor
    }

    /**
     * Injects values in the annotated fields.
     * @param object the test object
     * @param helper the OSGi Helper letting us accessing services
     */
    public static void inject(Object object, OSGiHelper helper) {
        Field[] fields = object.getClass().getFields();
        for (Field field : fields) {
            if (field.getAnnotation(javax.inject.Inject.class) != null) {
                inject(object, field, helper);
            }
        }
        fields = object.getClass().getDeclaredFields();
        for (Field field : fields) {
            if (field.getAnnotation(javax.inject.Inject.class) != null) {
                inject(object, field, helper);
            }
        }
    }

    /**
     * Injects a value in the field 'field' from the object 'object'.
     * @param object the test object
     * @param field the field to inject.
     * @param helper the OSGi Helper letting us accessing services
     */
    public static void inject(Object object, Field field, OSGiHelper helper) {
        if (field.getType().getName().equals(BundleContext.class.getName())) {
            set(object, field, helper.getContext());
        } else if (field.getType().getName().equals(Template.class.getName())) {
            String name = readNameAnnotation(field);
            String filter = readFilterAnnotation(field);
            if (name == null  && filter == null) {
                throw new ExceptionInInitializerError(INJECTION_ERROR + field.getName() + ", " +
                        "the @Name annotation or @Filter annotation are required to indicate the template to inject");
            }
            if (name != null  && filter != null) {
                throw new ExceptionInInitializerError(INJECTION_ERROR + field.getName() + ", " +
                        "both @Name annotation and @Filter annotations are used to indicate the template to " +
                        "inject, please use only one");
            }
            if (name != null) {
                Object template = waitForService(helper, Template.class, "(name=" + name + ")");
                if (template == null) {
                    throw new ExceptionInInitializerError(INJECTION_ERROR+ field.getName() + ", " +
                            "cannot find a template with name=" + name);
                }
                set(object, field, template);
            }
            if (filter != null) {
                Object template = waitForService(helper, Template.class, filter);
                if (template == null) {
                    throw new ExceptionInInitializerError(INJECTION_ERROR + field.getName() + ", " +
                            "cannot find a template matching the given filter: " + filter);
                }
                set(object, field, template);
            }
        } else if (field.getType().getName().equals(Router.class.getName())) {
            Object router = waitForService(helper, Router.class, null);
            set(object, field, router);
        } else if (isController(field.getType())) {
            // Controller are identified by their classname (matching the factory.name).
            String filter = String.format("(factory.name=%s)", field.getType().getName());
            Object controller = waitForService(helper, Controller.class, filter);
            if (controller == null) {
                throw new ExceptionInInitializerError("Cannot inject a controller in '" + field.getName() + "' - " +
                        "cannot find a controller matching the given filter: " + filter);
            }
            set(object, field, controller);
        } else {
            // Service
            String filter = readFilterAnnotation(field);
            Object service = waitForService(helper, field.getType(), filter);
            if (service == null) {
                try {

                    // No service, try with a getAllServiceReferences to inject services
                    // not accessible because of class loading constraints
                    // As the wait time has already been reached, we can do an immediate
                    // lookup.
                    //TODO Configure (enable/disable this behavior).
                    ServiceReference[] refs =
                            helper.getContext()
                                    .getAllServiceReferences(field.getType().getName(),
                            filter);
                    if (refs != null) {
                        set(object, field, helper.getServiceObject(refs[0]));
                        return;
                    }
                } catch (Exception e) {
                    // ignore
                }

                throw new ExceptionInInitializerError("Cannot inject a service in " + field.getName() + ", " +
                        "cannot find a service publishing " + field.getType().getName() + " matching the filter " +
                        filter);
            }
            set(object, field, service);
        }
    }

    private static boolean isController(Class<?> type) {
        List<String> classes = traverseHierarchy(type);
        return classes.contains(Controller.class.getName());
    }

    private static List<String> traverseHierarchy(Class<?> type) {
        List<String> list = new ArrayList<>();
        list.add(type.getName());
        for (Class<?> clazz : type.getInterfaces()) {
            list.addAll(traverseHierarchy(clazz));
        }
        if (type.getSuperclass() != null) {
            list.addAll(traverseHierarchy(type.getSuperclass()));
        }
        return list;
    }

    private static String readNameAnnotation(Field field) {
        // We can't access the annotation directly because of the classloading.
        // We retrieve the value by reflection.
        for (Annotation annotation : field.getAnnotations()) {
            if (annotation.annotationType().getName().endsWith(Name.class.getName())) {
                try {
                    Method method = annotation.getClass().getMethod(VALUE);
                    return (String) method.invoke(annotation);
                } catch (Exception e) { //NOSONAR
                    throw new ExceptionInInitializerError("Cannot retrieve the value of the @Name annotation");
                }
            }
        }
        return null;
    }

    private static String readFilterAnnotation(Field field) {
        // We can't access the annotation directly because of the classloading.
        // We retrieve the value by reflection.
        for (Annotation annotation : field.getAnnotations()) {
            if (annotation.annotationType().getName().endsWith(Filter.class.getName())) {
                try {
                    Method method = annotation.getClass().getMethod(VALUE);
                    return (String) method.invoke(annotation);
                } catch (Exception e) { //NOSONAR
                    throw new ExceptionInInitializerError("Cannot retrieve the value of the @Filter annotation");
                }
            }
        }
        return null;
    }

    private static void set(Object object, Field field, Object value) {
        field.setAccessible(true);
        try {
            field.set(object, value);
        } catch (IllegalAccessException e) {
            throw new RuntimeException("Error when injecting " + value + " in " + field, e);
        }
    }

    /**
     * This method is used until the new OSGi helper are released.
     * @param helper the helper
     * @param clazz the service interface
     * @param filter the filter (optional)
     * @return the service object, {@literal null} if not found
     */
    private static Object waitForService(OSGiHelper helper, Class<?> clazz, String filter) {
        ServiceReference<?> ref = helper.waitForService(clazz.getName(), filter, 10000, false);
        if (ref == null) {
            return null;
        } else {
            return helper.getServiceObject(ref);
        }
    }

}


text:
ActiveAdmin.register_page "Dashboard" do

  menu priority: 1, label: proc{ I18n.t("active_admin.dashboard") }

  content title: proc{ I18n.t("active_admin.dashboard") } do
        section "Recent Pages", :priority => 1 do
            table_for Page.order("id desc").limit(20) do
                column :id
                column "Page Title", :title do |page|
                    link_to page.title, [:admin, page]
                end
                column :section, :sortable => :section
                column :created_at
            end
        end

        section "Recent Sections", :priority => 1 do
            table_for Section.order("id desc").limit(20) do
                column :id
                column "Section Name", :title do |section|
                    link_to section.name, [:admin, section]
                end
                column :created_at
            end
        end

    # Here is an example of a simple dashboard with columns and panels.
    #
    # columns do
    #   column do
    #     panel "Recent Posts" do
    #       ul do
    #         Post.recent(5).map do |post|
    #           li link_to(post.title, admin_post_path(post))
    #         end
    #       end
    #     end
    #   end

    #   column do
    #     panel "Info" do
    #       para "Welcome to ActiveAdmin."
    #     end
    #   end
    # end
  end # content
end


text:
package com.bluelinelabs.conductor.rxlifecycle;

public enum ControllerEvent {

    CREATE,
    CREATE_VIEW,
    ATTACH,
    DETACH,
    DESTROY_VIEW,
    DESTROY

}


text:
package com.primeton.appframe.controller;

import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import com.primeton.appframe.common.annotation.Authority;
import com.primeton.appframe.common.annotation.ControllerLog;
import com.primeton.appframe.common.pojo.AjaxResult;
import com.primeton.appframe.common.pojo.PageAjax;
import com.primeton.appframe.common.utils.DateUtil;
import com.primeton.appframe.model.IArticle;
import com.primeton.appframe.model.IFile;
import com.primeton.appframe.service.TechService;

/**
 *
 * ClassName: TechController

 * Function: TODO ADD FUNCTION.

 * Reason: TODO ADD REASON(可选).

 * date: 2017年10月17日 下午1:28:13

 *
 * @author Jin.He (mailto:hejin@primeton.com)
 * @version
 */
@Controller
@RequestMapping("/admin/tech/")
public class TechController extends BaseController{

	@Autowired
	private TechService techService;

	@Authority(opCode = "0201", opName = "文件管理界面")
	@RequestMapping("filePage")
	public String filePage() {
		return "tech/file";
	}

	@ControllerLog("查询文件列表")
	@RequestMapping("queryFilePage")
	@ResponseBody
	@Authority(opCode = "0201", opName = "查询文件列表")
	public PageAjax<IFile> queryFilePage(PageAjax<IFile> page, IFile file) {
		return techService.queryFilePage(page, file);
	}

	@Authority(opCode = "020101", opName = "添加文件页面")
	@RequestMapping("addFilePage")
	public String addFilePage(Map<String, Object> map) {
		return "tech/file_add";
	}

	@ControllerLog("添加文件")
	@RequestMapping("addFile")
	@ResponseBody
	@Authority(opCode = "020101", opName = "添加文件")
	public AjaxResult addFile(IFile file) {
		return techService.addFile(file);
	}

	@Authority(opCode = "020102", opName = "更新文件页面")
	@RequestMapping("updateFilePage/{id}")
	public String updateFilePage(@PathVariable("id") int id, Map<String, Object> map) {
		IFile file = techService.queryFileByID(id);
		map.put("file", file);
		return "tech/file_update";
	}

	@ControllerLog("修改文件")
	@RequestMapping("updateFile")
	@ResponseBody
	@Authority(opCode = "020102", opName = "修改文件")
	public AjaxResult updateFile(IFile file) {
		return techService.updateFile(file);
	}

	@ControllerLog("删除文件")
	@RequestMapping("delFile/{id}")
	@ResponseBody
	@Authority(opCode = "020103", opName = "删除文件")
	public AjaxResult delFile(@PathVariable("id") int id) {
		return techService.delFile(id);
	}

	//==================================================文章管理==================================================
	@Authority(opCode = "0202", opName = "文章管理界面")
	@RequestMapping("articlePage")
	public String articlePage(Map<String, Object> map) {
		return "tech/article";
	}

	@ControllerLog("查询文章列表")
	@RequestMapping("queryArticlePage")
	@ResponseBody
	@Authority(opCode = "0202", opName = "查询文章列表")
	public PageAjax<IArticle> queryArticlePage(PageAjax<IArticle> page, IArticle article) {
		return techService.queryPage(page, article);
	}

	@Authority(opCode = "020201", opName = "添加文章页面")
	@RequestMapping("addArticlePage")
	public String addArticlePage(Map<String, Object> map) {
		return "tech/article_add";
	}

	@ControllerLog("添加文章")
	@RequestMapping("addArticle")
	@ResponseBody
	@Authority(opCode = "020201", opName = "添加文章")
	public AjaxResult addArticle(IArticle article) {
		article.setPosttime(DateUtil.getCurDateTime());
		return techService.save(article);
	}

	@Authority(opCode = "020202", opName = "更新文章页面")
	@RequestMapping("updateArticlePage/{id}")
	public String updateArticlePage(@PathVariable("id") int id, Map<String, Object> map) {
		IArticle article = techService.queryByID(id);
		map.put("article", article);
		return "tech/article_update";
	}

	@ControllerLog("修改文章")
	@RequestMapping("updateArticle")
	@ResponseBody
	@Authority(opCode = "020202", opName = "修改文章")
	public AjaxResult updateArticle(IArticle article) {
		return techService.update(article);
	}

	@ControllerLog("删除文章")
	@RequestMapping("delArticle/{id}")
	@ResponseBody
	@Authority(opCode = "020203", opName = "删除文章")
	public AjaxResult delArticle(@PathVariable("id") int id) {
		return techService.deleteByID(id);
	}

	//==================================================文章管理==================================================
	@Authority(opCode = "0203", opName = "测试界面")
	@RequestMapping("testPage")
	public String testPage(Map<String, Object> map) {
		return "tech/article";
	}
}