You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@calcite.apache.org by jh...@apache.org on 2019/05/13 05:37:43 UTC

[calcite] branch master updated: [CALCITE-3017] Re-organize how we represent built-in operators that are not in the standard operator table

This is an automated email from the ASF dual-hosted git repository.

jhyde pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/calcite.git


The following commit(s) were added to refs/heads/master by this push:
     new 9a4eab5  [CALCITE-3017] Re-organize how we represent built-in operators that are not in the standard operator table
9a4eab5 is described below

commit 9a4eab5240d96379431d14a1ac33bfebaf6fbb28
Author: yuzhao.cyz <yu...@alibaba-inc.com>
AuthorDate: Thu Apr 25 12:00:28 2019 +0800

    [CALCITE-3017] Re-organize how we represent built-in operators that are not in the standard operator table
    
    Add enum SqlLibrary, with values STANDARD, SPATIAL, ORACLE, MYSQL etc.,
    and SqlLibraryOperatorTableFactory, that can create operator tables for
    any combination of libraries.
    
    Further changes by Julian Hyde:
     * Adopt the term "library" (earlier revisions had "dialect" and "flavor");
     * Change SqlDialectOperatorTableFactory into a cache (previously it
       pre-built one table per library);
     * Make it responsible for standard and spatial libraries in addition to
       database-specific ones;
     * Move operators from SqlLibraryOperatorTableFactory into a dedicated
       holder class, SqlLibraryOperators;
     * Deprecate OracleSqlOperatorTable, and move its operator definitions.
    
    Close apache/calcite#1203
---
 core/src/main/codegen/templates/Parser.jj          |   6 +-
 .../calcite/adapter/enumerable/RexImpTable.java    |  12 +-
 .../adapter/enumerable/RexToLixTranslator.java     |   2 +-
 .../config/CalciteConnectionConfigImpl.java        |  42 ++----
 .../calcite/config/CalciteConnectionProperty.java  |   4 +-
 .../calcite/rex/RexSqlStandardConvertletTable.java |   4 +-
 .../calcite/sql/dialect/OracleSqlDialect.java      |   4 +-
 .../apache/calcite/sql/fun/LibraryOperator.java    |  45 ++++++
 .../calcite/sql/fun/OracleSqlOperatorTable.java    | 100 +++----------
 .../org/apache/calcite/sql/fun/SqlLibrary.java     |  96 ++++++++++++
 .../sql/fun/SqlLibraryOperatorTableFactory.java    | 166 +++++++++++++++++++++
 ...OperatorTable.java => SqlLibraryOperators.java} |  72 +++++----
 .../calcite/sql/fun/SqlStdOperatorTable.java       |   4 +-
 .../calcite/sql2rel/StandardConvertletTable.java   |  24 +--
 .../apache/calcite/sql/test/DocumentationTest.java |  71 ++++++---
 .../calcite/sql/test/SqlOperatorBaseTest.java      |  31 ++--
 .../org/apache/calcite/test/SqlValidatorTest.java  |   8 +-
 site/_docs/reference.md                            |   5 +-
 18 files changed, 481 insertions(+), 215 deletions(-)

diff --git a/core/src/main/codegen/templates/Parser.jj b/core/src/main/codegen/templates/Parser.jj
index 37e5bc5..ab706f1 100644
--- a/core/src/main/codegen/templates/Parser.jj
+++ b/core/src/main/codegen/templates/Parser.jj
@@ -96,7 +96,7 @@ import org.apache.calcite.sql.SqlWindow;
 import org.apache.calcite.sql.SqlWith;
 import org.apache.calcite.sql.SqlWithItem;
 import org.apache.calcite.sql.fun.SqlCase;
-import org.apache.calcite.sql.fun.OracleSqlOperatorTable;
+import org.apache.calcite.sql.fun.SqlLibraryOperators;
 import org.apache.calcite.sql.fun.SqlStdOperatorTable;
 import org.apache.calcite.sql.fun.SqlTrimFunction;
 import org.apache.calcite.sql.parser.Span;
@@ -4792,8 +4792,8 @@ SqlNode BuiltinFunctionCall() :
                 }
             )*
             <RPAREN> {
-                return OracleSqlOperatorTable.TRANSLATE3.createCall(
-                    s.end(this), args);
+                return SqlLibraryOperators.TRANSLATE3.createCall(s.end(this),
+                    args);
             }
         )
     |
diff --git a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java
index 73fee32..4f29b2c 100644
--- a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java
+++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java
@@ -91,7 +91,12 @@ import static org.apache.calcite.linq4j.tree.ExpressionType.NotEqual;
 import static org.apache.calcite.linq4j.tree.ExpressionType.OrElse;
 import static org.apache.calcite.linq4j.tree.ExpressionType.Subtract;
 import static org.apache.calcite.linq4j.tree.ExpressionType.UnaryPlus;
-import static org.apache.calcite.sql.fun.OracleSqlOperatorTable.TRANSLATE3;
+import static org.apache.calcite.sql.fun.SqlLibraryOperators.JSON_DEPTH;
+import static org.apache.calcite.sql.fun.SqlLibraryOperators.JSON_KEYS;
+import static org.apache.calcite.sql.fun.SqlLibraryOperators.JSON_LENGTH;
+import static org.apache.calcite.sql.fun.SqlLibraryOperators.JSON_PRETTY;
+import static org.apache.calcite.sql.fun.SqlLibraryOperators.JSON_TYPE;
+import static org.apache.calcite.sql.fun.SqlLibraryOperators.TRANSLATE3;
 import static org.apache.calcite.sql.fun.SqlStdOperatorTable.ABS;
 import static org.apache.calcite.sql.fun.SqlStdOperatorTable.ACOS;
 import static org.apache.calcite.sql.fun.SqlStdOperatorTable.AND;
@@ -164,15 +169,10 @@ import static org.apache.calcite.sql.fun.SqlStdOperatorTable.ITEM;
 import static org.apache.calcite.sql.fun.SqlStdOperatorTable.JSON_API_COMMON_SYNTAX;
 import static org.apache.calcite.sql.fun.SqlStdOperatorTable.JSON_ARRAY;
 import static org.apache.calcite.sql.fun.SqlStdOperatorTable.JSON_ARRAYAGG;
-import static org.apache.calcite.sql.fun.SqlStdOperatorTable.JSON_DEPTH;
 import static org.apache.calcite.sql.fun.SqlStdOperatorTable.JSON_EXISTS;
-import static org.apache.calcite.sql.fun.SqlStdOperatorTable.JSON_KEYS;
-import static org.apache.calcite.sql.fun.SqlStdOperatorTable.JSON_LENGTH;
 import static org.apache.calcite.sql.fun.SqlStdOperatorTable.JSON_OBJECT;
 import static org.apache.calcite.sql.fun.SqlStdOperatorTable.JSON_OBJECTAGG;
-import static org.apache.calcite.sql.fun.SqlStdOperatorTable.JSON_PRETTY;
 import static org.apache.calcite.sql.fun.SqlStdOperatorTable.JSON_QUERY;
-import static org.apache.calcite.sql.fun.SqlStdOperatorTable.JSON_TYPE;
 import static org.apache.calcite.sql.fun.SqlStdOperatorTable.JSON_VALUE_ANY;
 import static org.apache.calcite.sql.fun.SqlStdOperatorTable.JSON_VALUE_EXPRESSION;
 import static org.apache.calcite.sql.fun.SqlStdOperatorTable.LAG;
diff --git a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexToLixTranslator.java b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexToLixTranslator.java
index 1a63068..a828a34 100644
--- a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexToLixTranslator.java
+++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexToLixTranslator.java
@@ -67,7 +67,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 
-import static org.apache.calcite.sql.fun.OracleSqlOperatorTable.TRANSLATE3;
+import static org.apache.calcite.sql.fun.SqlLibraryOperators.TRANSLATE3;
 import static org.apache.calcite.sql.fun.SqlStdOperatorTable.CHARACTER_LENGTH;
 import static org.apache.calcite.sql.fun.SqlStdOperatorTable.CHAR_LENGTH;
 import static org.apache.calcite.sql.fun.SqlStdOperatorTable.SUBSTRING;
diff --git a/core/src/main/java/org/apache/calcite/config/CalciteConnectionConfigImpl.java b/core/src/main/java/org/apache/calcite/config/CalciteConnectionConfigImpl.java
index 8a97d4d..5b6397a 100644
--- a/core/src/main/java/org/apache/calcite/config/CalciteConnectionConfigImpl.java
+++ b/core/src/main/java/org/apache/calcite/config/CalciteConnectionConfigImpl.java
@@ -20,17 +20,14 @@ import org.apache.calcite.avatica.ConnectionConfigImpl;
 import org.apache.calcite.avatica.util.Casing;
 import org.apache.calcite.avatica.util.Quoting;
 import org.apache.calcite.model.JsonSchema;
-import org.apache.calcite.prepare.CalciteCatalogReader;
-import org.apache.calcite.runtime.GeoFunctions;
+import org.apache.calcite.runtime.ConsList;
 import org.apache.calcite.sql.SqlOperatorTable;
-import org.apache.calcite.sql.fun.OracleSqlOperatorTable;
-import org.apache.calcite.sql.fun.SqlStdOperatorTable;
-import org.apache.calcite.sql.util.ChainedSqlOperatorTable;
+import org.apache.calcite.sql.fun.SqlLibrary;
+import org.apache.calcite.sql.fun.SqlLibraryOperatorTableFactory;
 import org.apache.calcite.sql.validate.SqlConformance;
 import org.apache.calcite.sql.validate.SqlConformanceEnum;
 
-import java.util.Collection;
-import java.util.LinkedHashSet;
+import java.util.List;
 import java.util.Properties;
 
 /** Implementation of {@link CalciteConnectionConfig}. */
@@ -92,32 +89,11 @@ public class CalciteConnectionConfigImpl extends ConnectionConfigImpl
     if (fun == null || fun.equals("") || fun.equals("standard")) {
       return defaultOperatorTable;
     }
-    final Collection<SqlOperatorTable> tables = new LinkedHashSet<>();
-    for (String s : fun.split(",")) {
-      operatorTable(s, tables);
-    }
-    tables.add(SqlStdOperatorTable.instance());
-    return operatorTableClass.cast(
-        ChainedSqlOperatorTable.of(
-            tables.toArray(new SqlOperatorTable[0])));
-  }
-
-  private static void operatorTable(String s,
-        Collection<SqlOperatorTable> tables) {
-    switch (s) {
-    case "standard":
-      tables.add(SqlStdOperatorTable.instance());
-      return;
-    case "oracle":
-      tables.add(OracleSqlOperatorTable.instance());
-      return;
-    case "spatial":
-      tables.add(
-          CalciteCatalogReader.operatorTable(GeoFunctions.class.getName()));
-      return;
-    default:
-      throw new IllegalArgumentException("Unknown operator table: " + s);
-    }
+    final List<SqlLibrary> libraryList = SqlLibrary.parse(fun);
+    final SqlOperatorTable operatorTable =
+            SqlLibraryOperatorTableFactory.INSTANCE.getOperatorTable(
+                ConsList.of(SqlLibrary.STANDARD, libraryList));
+    return operatorTableClass.cast(operatorTable);
   }
 
   public String model() {
diff --git a/core/src/main/java/org/apache/calcite/config/CalciteConnectionProperty.java b/core/src/main/java/org/apache/calcite/config/CalciteConnectionProperty.java
index bd17fc7..3fe048e 100644
--- a/core/src/main/java/org/apache/calcite/config/CalciteConnectionProperty.java
+++ b/core/src/main/java/org/apache/calcite/config/CalciteConnectionProperty.java
@@ -79,8 +79,8 @@ public enum CalciteConnectionProperty implements ConnectionProperty {
   LEX("lex", Type.ENUM, Lex.ORACLE, false),
 
   /** Collection of built-in functions and operators. Valid values include
-   * "standard", "oracle" and "spatial", and also comma-separated lists, for
-   * example "oracle,spatial". */
+   * "standard", "mysql", "oracle", "postgresql" and "spatial", and also
+   * comma-separated lists, for example "oracle,spatial". */
   FUN("fun", Type.STRING, "standard", true),
 
   /** How identifiers are quoted.
diff --git a/core/src/main/java/org/apache/calcite/rex/RexSqlStandardConvertletTable.java b/core/src/main/java/org/apache/calcite/rex/RexSqlStandardConvertletTable.java
index 9bad2f3..310a6b4 100644
--- a/core/src/main/java/org/apache/calcite/rex/RexSqlStandardConvertletTable.java
+++ b/core/src/main/java/org/apache/calcite/rex/RexSqlStandardConvertletTable.java
@@ -22,8 +22,8 @@ import org.apache.calcite.sql.SqlDataTypeSpec;
 import org.apache.calcite.sql.SqlNode;
 import org.apache.calcite.sql.SqlNodeList;
 import org.apache.calcite.sql.SqlOperator;
-import org.apache.calcite.sql.fun.OracleSqlOperatorTable;
 import org.apache.calcite.sql.fun.SqlCaseOperator;
+import org.apache.calcite.sql.fun.SqlLibraryOperators;
 import org.apache.calcite.sql.fun.SqlStdOperatorTable;
 import org.apache.calcite.sql.parser.SqlParserPos;
 import org.apache.calcite.sql.type.SqlTypeUtil;
@@ -100,7 +100,7 @@ public class RexSqlStandardConvertletTable
     registerEquivOp(SqlStdOperatorTable.TRANSLATE);
     registerEquivOp(SqlStdOperatorTable.OVERLAY);
     registerEquivOp(SqlStdOperatorTable.TRIM);
-    registerEquivOp(OracleSqlOperatorTable.TRANSLATE3);
+    registerEquivOp(SqlLibraryOperators.TRANSLATE3);
     registerEquivOp(SqlStdOperatorTable.POSITION);
     registerEquivOp(SqlStdOperatorTable.CHAR_LENGTH);
     registerEquivOp(SqlStdOperatorTable.CHARACTER_LENGTH);
diff --git a/core/src/main/java/org/apache/calcite/sql/dialect/OracleSqlDialect.java b/core/src/main/java/org/apache/calcite/sql/dialect/OracleSqlDialect.java
index aa16ddb..ad58319 100644
--- a/core/src/main/java/org/apache/calcite/sql/dialect/OracleSqlDialect.java
+++ b/core/src/main/java/org/apache/calcite/sql/dialect/OracleSqlDialect.java
@@ -22,8 +22,8 @@ import org.apache.calcite.sql.SqlDialect;
 import org.apache.calcite.sql.SqlLiteral;
 import org.apache.calcite.sql.SqlUtil;
 import org.apache.calcite.sql.SqlWriter;
-import org.apache.calcite.sql.fun.OracleSqlOperatorTable;
 import org.apache.calcite.sql.fun.SqlFloorFunction;
+import org.apache.calcite.sql.fun.SqlLibraryOperators;
 import org.apache.calcite.sql.fun.SqlStdOperatorTable;
 
 /**
@@ -55,7 +55,7 @@ public class OracleSqlDialect extends SqlDialect {
   @Override public void unparseCall(SqlWriter writer, SqlCall call,
       int leftPrec, int rightPrec) {
     if (call.getOperator() == SqlStdOperatorTable.SUBSTRING) {
-      SqlUtil.unparseFunctionSyntax(OracleSqlOperatorTable.SUBSTR, writer, call);
+      SqlUtil.unparseFunctionSyntax(SqlLibraryOperators.SUBSTR, writer, call);
     } else {
       switch (call.getKind()) {
       case FLOOR:
diff --git a/core/src/main/java/org/apache/calcite/sql/fun/LibraryOperator.java b/core/src/main/java/org/apache/calcite/sql/fun/LibraryOperator.java
new file mode 100644
index 0000000..da21aa3
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/sql/fun/LibraryOperator.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you 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.
+ */
+package org.apache.calcite.sql.fun;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * An annotation that is read by {@link SqlLibraryOperatorTableFactory} to
+ * add functions and operators to a library.
+ *
+ * <p>Typically, such collections are associated with a particular dialect or
+ * database. For example, {@link SqlLibrary#ORACLE} is a collection of functions
+ * that are in the Oracle database but not the SQL standard.
+ *
+ * <p>In {@link SqlLibraryOperatorTableFactory} this annotation is applied to
+ * function definitions to include them in a particular library. It allows
+ * an operator to belong to more than one library.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.FIELD)
+public @interface LibraryOperator {
+
+  /** The set of libraries that this function or operator belongs to.
+   * Must not be null or empty. */
+  SqlLibrary[] libraries();
+}
+
+// End LibraryOperator.java
diff --git a/core/src/main/java/org/apache/calcite/sql/fun/OracleSqlOperatorTable.java b/core/src/main/java/org/apache/calcite/sql/fun/OracleSqlOperatorTable.java
index f88d95d..0dcf58a 100644
--- a/core/src/main/java/org/apache/calcite/sql/fun/OracleSqlOperatorTable.java
+++ b/core/src/main/java/org/apache/calcite/sql/fun/OracleSqlOperatorTable.java
@@ -16,23 +16,17 @@
  */
 package org.apache.calcite.sql.fun;
 
-import org.apache.calcite.rel.type.RelDataType;
-import org.apache.calcite.rel.type.RelDataTypeFactory;
 import org.apache.calcite.sql.SqlFunction;
-import org.apache.calcite.sql.SqlFunctionCategory;
-import org.apache.calcite.sql.SqlKind;
-import org.apache.calcite.sql.type.OperandTypes;
-import org.apache.calcite.sql.type.ReturnTypes;
-import org.apache.calcite.sql.type.SqlReturnTypeInference;
-import org.apache.calcite.sql.type.SqlTypeTransforms;
 import org.apache.calcite.sql.util.ReflectiveSqlOperatorTable;
 
-import java.util.ArrayList;
-import java.util.List;
-
 /**
  * Operator table that contains only Oracle-specific functions and operators.
+ *
+ * @deprecated Use
+ * {@link SqlLibraryOperatorTableFactory#getOperatorTable(SqlLibrary...)}
+ * instead, passing {@link SqlLibrary#ORACLE} as argument.
  */
+@Deprecated // to be removed before 2.0
 public class OracleSqlOperatorTable extends ReflectiveSqlOperatorTable {
   //~ Static fields/initializers ---------------------------------------------
 
@@ -41,81 +35,29 @@ public class OracleSqlOperatorTable extends ReflectiveSqlOperatorTable {
    */
   private static OracleSqlOperatorTable instance;
 
-  /** Return type inference for {@code DECODE}. */
-  protected static final SqlReturnTypeInference DECODE_RETURN_TYPE =
-      opBinding -> {
-        final List<RelDataType> list = new ArrayList<>();
-        for (int i = 1, n = opBinding.getOperandCount(); i < n; i++) {
-          if (i < n - 1) {
-            ++i;
-          }
-          list.add(opBinding.getOperandType(i));
-        }
-        final RelDataTypeFactory typeFactory = opBinding.getTypeFactory();
-        RelDataType type = typeFactory.leastRestrictive(list);
-        if (opBinding.getOperandCount() % 2 == 1) {
-          type = typeFactory.createTypeWithNullability(type, true);
-        }
-        return type;
-      };
-
-  /** The "DECODE(v, v1, result1, [v2, result2, ...], resultN)" function. */
-  public static final SqlFunction DECODE =
-      new SqlFunction("DECODE", SqlKind.DECODE, DECODE_RETURN_TYPE, null,
-          OperandTypes.VARIADIC, SqlFunctionCategory.SYSTEM);
+  @Deprecated // to be removed before 2.0
+  public static final SqlFunction DECODE = SqlLibraryOperators.DECODE;
 
-  /** The "NVL(value, value)" function. */
-  public static final SqlFunction NVL =
-      new SqlFunction("NVL", SqlKind.NVL,
-          ReturnTypes.cascade(ReturnTypes.LEAST_RESTRICTIVE,
-              SqlTypeTransforms.TO_NULLABLE_ALL),
-          null, OperandTypes.SAME_SAME, SqlFunctionCategory.SYSTEM);
+  @Deprecated // to be removed before 2.0
+  public static final SqlFunction NVL = SqlLibraryOperators.NVL;
 
-  /** The "LTRIM(string)" function. */
-  public static final SqlFunction LTRIM =
-      new SqlFunction("LTRIM", SqlKind.LTRIM,
-          ReturnTypes.cascade(ReturnTypes.ARG0, SqlTypeTransforms.TO_NULLABLE,
-              SqlTypeTransforms.TO_VARYING), null,
-          OperandTypes.STRING, SqlFunctionCategory.STRING);
+  @Deprecated // to be removed before 2.0
+  public static final SqlFunction LTRIM = SqlLibraryOperators.LTRIM;
 
-  /** The "RTRIM(string)" function. */
-  public static final SqlFunction RTRIM =
-      new SqlFunction("RTRIM", SqlKind.RTRIM,
-          ReturnTypes.cascade(ReturnTypes.ARG0, SqlTypeTransforms.TO_NULLABLE,
-              SqlTypeTransforms.TO_VARYING), null,
-          OperandTypes.STRING, SqlFunctionCategory.STRING);
+  @Deprecated // to be removed before 2.0
+  public static final SqlFunction RTRIM = SqlLibraryOperators.RTRIM;
 
-  /** Oracle's "SUBSTR(string, position [, substringLength ])" function.
-   *
-   * <p>It has similar semantics to standard SQL's
-   * {@link SqlStdOperatorTable#SUBSTRING} function but different syntax. */
-  public static final SqlFunction SUBSTR =
-      new SqlFunction("SUBSTR", SqlKind.OTHER_FUNCTION,
-          ReturnTypes.ARG0_NULLABLE_VARYING, null, null,
-          SqlFunctionCategory.STRING);
+  @Deprecated // to be removed before 2.0
+  public static final SqlFunction SUBSTR = SqlLibraryOperators.SUBSTR;
 
-  /** The "GREATEST(value, value)" function. */
-  public static final SqlFunction GREATEST =
-      new SqlFunction("GREATEST", SqlKind.GREATEST,
-          ReturnTypes.cascade(ReturnTypes.LEAST_RESTRICTIVE,
-              SqlTypeTransforms.TO_NULLABLE), null,
-          OperandTypes.SAME_VARIADIC, SqlFunctionCategory.SYSTEM);
+  @Deprecated // to be removed before 2.0
+  public static final SqlFunction GREATEST = SqlLibraryOperators.GREATEST;
 
-  /** The "LEAST(value, value)" function. */
-  public static final SqlFunction LEAST =
-      new SqlFunction("LEAST", SqlKind.LEAST,
-          ReturnTypes.cascade(ReturnTypes.LEAST_RESTRICTIVE,
-              SqlTypeTransforms.TO_NULLABLE), null,
-          OperandTypes.SAME_VARIADIC, SqlFunctionCategory.SYSTEM);
+  @Deprecated // to be removed before 2.0
+  public static final SqlFunction LEAST = SqlLibraryOperators.LEAST;
 
-  /**
-   * The <code>TRANSLATE(<i>string_expr</i>, <i>search_chars</i>, <i>replacement_chars</i>)</code>
-   * function returns <i>string_expr</i> with all occurrences of each character in
-   * <i>search_chars</i> replaced by its corresponding character in <i>replacement_chars</i>.
-   *
-   * <p>It is not defined in the SQL standard, but occurs in Oracle and PostgreSQL.
-   */
-  public static final SqlFunction TRANSLATE3 = new SqlTranslate3Function();
+  @Deprecated // to be removed before 2.0
+  public static final SqlFunction TRANSLATE3 = SqlLibraryOperators.TRANSLATE3;
 
   /**
    * Returns the Oracle operator table, creating it if necessary.
diff --git a/core/src/main/java/org/apache/calcite/sql/fun/SqlLibrary.java b/core/src/main/java/org/apache/calcite/sql/fun/SqlLibrary.java
new file mode 100644
index 0000000..d0b9ee8
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlLibrary.java
@@ -0,0 +1,96 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you 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.
+ */
+package org.apache.calcite.sql.fun;
+
+import org.apache.calcite.config.CalciteConnectionProperty;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * A library is a collection of SQL functions and operators.
+ *
+ * <p>Typically, such collections are associated with a particular dialect or
+ * database. For example, {@link SqlLibrary#ORACLE} is a collection of functions
+ * that are in the Oracle database but not the SQL standard.
+ *
+ * <p>In {@link SqlLibraryOperatorTableFactory} this annotation is applied to
+ * function definitions to include them in a particular library. It allows
+ * an operator to belong to more than one library.
+ *
+ * @see LibraryOperator
+ */
+public enum SqlLibrary {
+  /** The standard operators. */
+  STANDARD(""),
+  /** Geospatial operators. */
+  SPATIAL("s"),
+  /** A collection of operators that are in MySQL but not in standard SQL. */
+  MYSQL("m"),
+  /** A collection of operators that are in Oracle but not in standard SQL. */
+  ORACLE("o"),
+  /** A collection of operators that are in PostgreSQL but not in standard
+   * SQL. */
+  POSTGRESQL("p");
+
+  /** Abbreviation for the library used in SQL reference. */
+  public final String abbrev;
+
+  /** Name of this library when it appears in the connect string;
+   * see {@link CalciteConnectionProperty#FUN}. */
+  public final String fun;
+
+  SqlLibrary(String abbrev) {
+    this.abbrev = Objects.requireNonNull(abbrev);
+    this.fun = name().toLowerCase(Locale.ROOT);
+  }
+
+  /** Looks up a value.
+   * Returns null if not found.
+   * You can use upper- or lower-case name. */
+  public static SqlLibrary of(String name) {
+    return MAP.get(name);
+  }
+
+  /** Parses a comma-separated string such as "standard,oracle". */
+  public static List<SqlLibrary> parse(String libraryNameList) {
+    final ImmutableList.Builder<SqlLibrary> list = ImmutableList.builder();
+    for (String libraryName : libraryNameList.split(",")) {
+      list.add(SqlLibrary.of(libraryName));
+    }
+    return list.build();
+  }
+
+  public static final Map<String, SqlLibrary> MAP;
+
+  static {
+    final ImmutableMap.Builder<String, SqlLibrary> builder =
+        ImmutableMap.builder();
+    for (SqlLibrary value : values()) {
+      builder.put(value.name(), value);
+      builder.put(value.fun, value);
+    }
+    MAP = builder.build();
+  }
+}
+
+// End SqlLibrary.java
diff --git a/core/src/main/java/org/apache/calcite/sql/fun/SqlLibraryOperatorTableFactory.java b/core/src/main/java/org/apache/calcite/sql/fun/SqlLibraryOperatorTableFactory.java
new file mode 100644
index 0000000..4a65685
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlLibraryOperatorTableFactory.java
@@ -0,0 +1,166 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you 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.
+ */
+package org.apache.calcite.sql.fun;
+
+import org.apache.calcite.prepare.CalciteCatalogReader;
+import org.apache.calcite.runtime.GeoFunctions;
+import org.apache.calcite.sql.SqlOperator;
+import org.apache.calcite.sql.SqlOperatorTable;
+import org.apache.calcite.sql.util.ChainedSqlOperatorTable;
+import org.apache.calcite.sql.util.ListSqlOperatorTable;
+import org.apache.calcite.util.Util;
+
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+
+import java.lang.reflect.Field;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+
+/**
+ * Factory that creates operator tables that consist of functions and operators
+ * for particular named libraries. For example, the following code will return
+ * an operator table that contains operators for both Oracle and MySQL:
+ *
+ * <blockquote>
+ *   <pre>SqlOperatorTable opTab =
+ *     SqlDialectOperatorTableFactory.INSTANCE.getOperatorTable(
+ *         EnumSet.of(SqlLibrary.ORACLE, SqlLibrary.MYSQL))</pre>
+ * </blockquote>
+ *
+ * <p>To define a new library, add a value to enum {@link SqlLibrary}.
+ *
+ * <p>To add functions to a library, add the {@link LibraryOperator} annotation
+ * to fields that of type {@link SqlOperator}, and the library name to its
+ * {@link LibraryOperator#libraries()} field.
+ */
+public class SqlLibraryOperatorTableFactory {
+
+  /** List of classes to scan for operators. */
+  private final ImmutableList<Class> classes;
+
+  /** The singleton instance. */
+  public static final SqlLibraryOperatorTableFactory INSTANCE =
+      new SqlLibraryOperatorTableFactory(SqlLibraryOperators.class);
+
+  private SqlLibraryOperatorTableFactory(Class... classes) {
+    this.classes = ImmutableList.copyOf(classes);
+  }
+
+  //~ Instance fields --------------------------------------------------------
+
+  /** A cache that returns an operator table for a given library (or set of
+   * libraries). */
+  private final LoadingCache<ImmutableSet<SqlLibrary>, SqlOperatorTable> cache =
+      CacheBuilder.newBuilder().build(CacheLoader.from(this::create));
+
+  //~ Methods ----------------------------------------------------------------
+
+  /** Creates an operator table that contains operators in the given set of
+   * libraries. */
+  private SqlOperatorTable create(ImmutableSet<SqlLibrary> librarySet) {
+    final ImmutableList.Builder<SqlOperator> list = ImmutableList.builder();
+    boolean custom = false;
+    boolean standard = false;
+    for (SqlLibrary library : librarySet) {
+      switch (library) {
+      case STANDARD:
+        standard = true;
+        break;
+      case SPATIAL:
+        list.addAll(
+            CalciteCatalogReader.operatorTable(GeoFunctions.class.getName())
+                .getOperatorList());
+        break;
+      default:
+        custom = true;
+      }
+    }
+
+    // Use reflection to register the expressions stored in public fields.
+    // Skip if the only libraries asked for are "standard" or "spatial".
+    if (custom) {
+      for (Class aClass : classes) {
+        for (Field field : aClass.getFields()) {
+          try {
+            if (SqlOperator.class.isAssignableFrom(field.getType())) {
+              final SqlOperator op = (SqlOperator) field.get(this);
+              if (operatorIsInLibrary(op.getName(), field, librarySet)) {
+                list.add(op);
+              }
+            }
+          } catch (IllegalArgumentException | IllegalAccessException e) {
+            Util.throwIfUnchecked(e.getCause());
+            throw new RuntimeException(e.getCause());
+          }
+        }
+      }
+    }
+    SqlOperatorTable operatorTable = new ListSqlOperatorTable(list.build());
+    if (standard) {
+      operatorTable =
+          ChainedSqlOperatorTable.of(SqlStdOperatorTable.instance(),
+              operatorTable);
+    }
+    return operatorTable;
+  }
+
+  /** Returns whether an operator is in one or more of the given libraries. */
+  private boolean operatorIsInLibrary(String operatorName, Field field,
+      Set<SqlLibrary> seekLibrarySet) {
+    LibraryOperator libraryOperator =
+        field.getAnnotation(LibraryOperator.class);
+    if (libraryOperator == null) {
+      throw new AssertionError("Operator must have annotation: "
+          + operatorName);
+    }
+    SqlLibrary[] librarySet = libraryOperator.libraries();
+    if (librarySet.length <= 0) {
+      throw new AssertionError("Operator must belong to at least one library: "
+          + operatorName);
+    }
+    for (SqlLibrary library : librarySet) {
+      if (seekLibrarySet.contains(library)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /** Returns a SQL operator table that contains operators in the given library
+   * or libraries. */
+  public SqlOperatorTable getOperatorTable(SqlLibrary... libraries) {
+    return getOperatorTable(ImmutableSet.copyOf(libraries));
+  }
+
+  /** Returns a SQL operator table that contains operators in the given set of
+   * libraries. */
+  public SqlOperatorTable getOperatorTable(Iterable<SqlLibrary> librarySet) {
+    try {
+      return cache.get(ImmutableSet.copyOf(librarySet));
+    } catch (ExecutionException e) {
+      Util.throwIfUnchecked(e.getCause());
+      throw new RuntimeException("populating SqlOperatorTable for library "
+          + librarySet, e);
+    }
+  }
+}
+
+// End SqlLibraryOperatorTableFactory.java
diff --git a/core/src/main/java/org/apache/calcite/sql/fun/OracleSqlOperatorTable.java b/core/src/main/java/org/apache/calcite/sql/fun/SqlLibraryOperators.java
similarity index 70%
copy from core/src/main/java/org/apache/calcite/sql/fun/OracleSqlOperatorTable.java
copy to core/src/main/java/org/apache/calcite/sql/fun/SqlLibraryOperators.java
index f88d95d..9a536e3 100644
--- a/core/src/main/java/org/apache/calcite/sql/fun/OracleSqlOperatorTable.java
+++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlLibraryOperators.java
@@ -21,28 +21,33 @@ import org.apache.calcite.rel.type.RelDataTypeFactory;
 import org.apache.calcite.sql.SqlFunction;
 import org.apache.calcite.sql.SqlFunctionCategory;
 import org.apache.calcite.sql.SqlKind;
+import org.apache.calcite.sql.SqlOperatorTable;
 import org.apache.calcite.sql.type.OperandTypes;
 import org.apache.calcite.sql.type.ReturnTypes;
 import org.apache.calcite.sql.type.SqlReturnTypeInference;
 import org.apache.calcite.sql.type.SqlTypeTransforms;
-import org.apache.calcite.sql.util.ReflectiveSqlOperatorTable;
 
 import java.util.ArrayList;
 import java.util.List;
 
+import static org.apache.calcite.sql.fun.SqlLibrary.MYSQL;
+import static org.apache.calcite.sql.fun.SqlLibrary.ORACLE;
+import static org.apache.calcite.sql.fun.SqlLibrary.POSTGRESQL;
+
 /**
- * Operator table that contains only Oracle-specific functions and operators.
+ * Defines functions and operators that are not part of standard SQL but
+ * belong to one or more other dialects of SQL.
+ *
+ * <p>They are read by {@link SqlLibraryOperatorTableFactory} into instances
+ * of {@link SqlOperatorTable} that contain functions and operators for
+ * particular libraries.
  */
-public class OracleSqlOperatorTable extends ReflectiveSqlOperatorTable {
-  //~ Static fields/initializers ---------------------------------------------
-
-  /**
-   * The table of contains Oracle-specific operators.
-   */
-  private static OracleSqlOperatorTable instance;
+public abstract class SqlLibraryOperators {
+  private SqlLibraryOperators() {
+  }
 
   /** Return type inference for {@code DECODE}. */
-  protected static final SqlReturnTypeInference DECODE_RETURN_TYPE =
+  private static final SqlReturnTypeInference DECODE_RETURN_TYPE =
       opBinding -> {
         final List<RelDataType> list = new ArrayList<>();
         for (int i = 1, n = opBinding.getOperandCount(); i < n; i++) {
@@ -60,11 +65,13 @@ public class OracleSqlOperatorTable extends ReflectiveSqlOperatorTable {
       };
 
   /** The "DECODE(v, v1, result1, [v2, result2, ...], resultN)" function. */
+  @LibraryOperator(libraries = {ORACLE})
   public static final SqlFunction DECODE =
       new SqlFunction("DECODE", SqlKind.DECODE, DECODE_RETURN_TYPE, null,
           OperandTypes.VARIADIC, SqlFunctionCategory.SYSTEM);
 
   /** The "NVL(value, value)" function. */
+  @LibraryOperator(libraries = {ORACLE})
   public static final SqlFunction NVL =
       new SqlFunction("NVL", SqlKind.NVL,
           ReturnTypes.cascade(ReturnTypes.LEAST_RESTRICTIVE,
@@ -72,6 +79,7 @@ public class OracleSqlOperatorTable extends ReflectiveSqlOperatorTable {
           null, OperandTypes.SAME_SAME, SqlFunctionCategory.SYSTEM);
 
   /** The "LTRIM(string)" function. */
+  @LibraryOperator(libraries = {ORACLE})
   public static final SqlFunction LTRIM =
       new SqlFunction("LTRIM", SqlKind.LTRIM,
           ReturnTypes.cascade(ReturnTypes.ARG0, SqlTypeTransforms.TO_NULLABLE,
@@ -79,6 +87,7 @@ public class OracleSqlOperatorTable extends ReflectiveSqlOperatorTable {
           OperandTypes.STRING, SqlFunctionCategory.STRING);
 
   /** The "RTRIM(string)" function. */
+  @LibraryOperator(libraries = {ORACLE})
   public static final SqlFunction RTRIM =
       new SqlFunction("RTRIM", SqlKind.RTRIM,
           ReturnTypes.cascade(ReturnTypes.ARG0, SqlTypeTransforms.TO_NULLABLE,
@@ -89,12 +98,14 @@ public class OracleSqlOperatorTable extends ReflectiveSqlOperatorTable {
    *
    * <p>It has similar semantics to standard SQL's
    * {@link SqlStdOperatorTable#SUBSTRING} function but different syntax. */
+  @LibraryOperator(libraries = {ORACLE})
   public static final SqlFunction SUBSTR =
       new SqlFunction("SUBSTR", SqlKind.OTHER_FUNCTION,
           ReturnTypes.ARG0_NULLABLE_VARYING, null, null,
           SqlFunctionCategory.STRING);
 
   /** The "GREATEST(value, value)" function. */
+  @LibraryOperator(libraries = {ORACLE})
   public static final SqlFunction GREATEST =
       new SqlFunction("GREATEST", SqlKind.GREATEST,
           ReturnTypes.cascade(ReturnTypes.LEAST_RESTRICTIVE,
@@ -102,6 +113,7 @@ public class OracleSqlOperatorTable extends ReflectiveSqlOperatorTable {
           OperandTypes.SAME_VARIADIC, SqlFunctionCategory.SYSTEM);
 
   /** The "LEAST(value, value)" function. */
+  @LibraryOperator(libraries = {ORACLE})
   public static final SqlFunction LEAST =
       new SqlFunction("LEAST", SqlKind.LEAST,
           ReturnTypes.cascade(ReturnTypes.LEAST_RESTRICTIVE,
@@ -109,27 +121,31 @@ public class OracleSqlOperatorTable extends ReflectiveSqlOperatorTable {
           OperandTypes.SAME_VARIADIC, SqlFunctionCategory.SYSTEM);
 
   /**
-   * The <code>TRANSLATE(<i>string_expr</i>, <i>search_chars</i>, <i>replacement_chars</i>)</code>
-   * function returns <i>string_expr</i> with all occurrences of each character in
-   * <i>search_chars</i> replaced by its corresponding character in <i>replacement_chars</i>.
+   * The <code>TRANSLATE(<i>string_expr</i>, <i>search_chars</i>,
+   * <i>replacement_chars</i>)</code> function returns <i>string_expr</i> with
+   * all occurrences of each character in <i>search_chars</i> replaced by its
+   * corresponding character in <i>replacement_chars</i>.
    *
-   * <p>It is not defined in the SQL standard, but occurs in Oracle and PostgreSQL.
+   * <p>It is not defined in the SQL standard, but occurs in Oracle and
+   * PostgreSQL.
    */
+  @LibraryOperator(libraries = {ORACLE, POSTGRESQL})
   public static final SqlFunction TRANSLATE3 = new SqlTranslate3Function();
 
-  /**
-   * Returns the Oracle operator table, creating it if necessary.
-   */
-  public static synchronized OracleSqlOperatorTable instance() {
-    if (instance == null) {
-      // Creates and initializes the standard operator table.
-      // Uses two-phase construction, because we can't initialize the
-      // table until the constructor of the sub-class has completed.
-      instance = new OracleSqlOperatorTable();
-      instance.init();
-    }
-    return instance;
-  }
+  @LibraryOperator(libraries = {MYSQL})
+  public static final SqlFunction JSON_TYPE = SqlStdOperatorTable.JSON_TYPE;
+
+  @LibraryOperator(libraries = {MYSQL})
+  public static final SqlFunction JSON_DEPTH = SqlStdOperatorTable.JSON_DEPTH;
+
+  @LibraryOperator(libraries = {MYSQL})
+  public static final SqlFunction JSON_LENGTH = SqlStdOperatorTable.JSON_LENGTH;
+
+  @LibraryOperator(libraries = {MYSQL})
+  public static final SqlFunction JSON_KEYS = SqlStdOperatorTable.JSON_KEYS;
+
+  @LibraryOperator(libraries = {MYSQL})
+  public static final SqlFunction JSON_PRETTY = SqlStdOperatorTable.JSON_PRETTY;
 }
 
-// End OracleSqlOperatorTable.java
+// End SqlLibraryOperators.java
diff --git a/core/src/main/java/org/apache/calcite/sql/fun/SqlStdOperatorTable.java b/core/src/main/java/org/apache/calcite/sql/fun/SqlStdOperatorTable.java
index 1281077..f4b61b6 100644
--- a/core/src/main/java/org/apache/calcite/sql/fun/SqlStdOperatorTable.java
+++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlStdOperatorTable.java
@@ -1434,8 +1434,8 @@ public class SqlStdOperatorTable extends ReflectiveSqlOperatorTable {
    * The <code>TRANSLATE(<i>char_value</i> USING <i>translation_name</i>)</code> function
    * alters the character set of a string value from one base character set to another.
    *
-   * <p>It is defined in the SQL standard. See also non-standard
-   * {@link OracleSqlOperatorTable#TRANSLATE3}.
+   * <p>It is defined in the SQL standard. See also the non-standard
+   * {@link SqlLibraryOperators#TRANSLATE3}, which has a different purpose.
    */
   public static final SqlFunction TRANSLATE =
       new SqlConvertFunction("TRANSLATE");
diff --git a/core/src/main/java/org/apache/calcite/sql2rel/StandardConvertletTable.java b/core/src/main/java/org/apache/calcite/sql2rel/StandardConvertletTable.java
index f79e247..23359fc 100644
--- a/core/src/main/java/org/apache/calcite/sql2rel/StandardConvertletTable.java
+++ b/core/src/main/java/org/apache/calcite/sql2rel/StandardConvertletTable.java
@@ -47,12 +47,12 @@ import org.apache.calcite.sql.SqlNumericLiteral;
 import org.apache.calcite.sql.SqlOperator;
 import org.apache.calcite.sql.SqlPostfixOperator;
 import org.apache.calcite.sql.SqlUtil;
-import org.apache.calcite.sql.fun.OracleSqlOperatorTable;
 import org.apache.calcite.sql.fun.SqlArrayValueConstructor;
 import org.apache.calcite.sql.fun.SqlBetweenOperator;
 import org.apache.calcite.sql.fun.SqlCase;
 import org.apache.calcite.sql.fun.SqlDatetimeSubtractionOperator;
 import org.apache.calcite.sql.fun.SqlExtractFunction;
+import org.apache.calcite.sql.fun.SqlLibraryOperators;
 import org.apache.calcite.sql.fun.SqlLiteralChainOperator;
 import org.apache.calcite.sql.fun.SqlMapValueConstructor;
 import org.apache.calcite.sql.fun.SqlMultisetQueryConstructor;
@@ -133,15 +133,15 @@ public class StandardConvertletTable extends ReflectiveConvertletTable {
           }
         });
 
-    registerOp(OracleSqlOperatorTable.LTRIM,
+    registerOp(SqlLibraryOperators.LTRIM,
         new TrimConvertlet(SqlTrimFunction.Flag.LEADING));
-    registerOp(OracleSqlOperatorTable.RTRIM,
+    registerOp(SqlLibraryOperators.RTRIM,
         new TrimConvertlet(SqlTrimFunction.Flag.TRAILING));
 
-    registerOp(OracleSqlOperatorTable.GREATEST, new GreatestConvertlet());
-    registerOp(OracleSqlOperatorTable.LEAST, new GreatestConvertlet());
+    registerOp(SqlLibraryOperators.GREATEST, new GreatestConvertlet());
+    registerOp(SqlLibraryOperators.LEAST, new GreatestConvertlet());
 
-    registerOp(OracleSqlOperatorTable.NVL,
+    registerOp(SqlLibraryOperators.NVL,
         (cx, call) -> {
           final RexBuilder rexBuilder = cx.getRexBuilder();
           final RexNode operand0 =
@@ -158,7 +158,7 @@ public class StandardConvertletTable extends ReflectiveConvertletTable {
                   rexBuilder.makeCast(type, operand1)));
         });
 
-    registerOp(OracleSqlOperatorTable.DECODE,
+    registerOp(SqlLibraryOperators.DECODE,
         (cx, call) -> {
           final RexBuilder rexBuilder = cx.getRexBuilder();
           final List<RexNode> operands = convertExpressionList(cx,
@@ -215,15 +215,15 @@ public class StandardConvertletTable extends ReflectiveConvertletTable {
 
     registerOp(SqlStdOperatorTable.JSON_API_COMMON_SYNTAX,
         new JsonOperatorValueExprConvertlet(0));
-    registerOp(SqlStdOperatorTable.JSON_PRETTY,
+    registerOp(SqlLibraryOperators.JSON_PRETTY,
         new JsonOperatorValueExprConvertlet(0));
-    registerOp(SqlStdOperatorTable.JSON_TYPE,
+    registerOp(SqlLibraryOperators.JSON_TYPE,
         new JsonOperatorValueExprConvertlet(0));
-    registerOp(SqlStdOperatorTable.JSON_DEPTH,
+    registerOp(SqlLibraryOperators.JSON_DEPTH,
         new JsonOperatorValueExprConvertlet(0));
-    registerOp(SqlStdOperatorTable.JSON_LENGTH,
+    registerOp(SqlLibraryOperators.JSON_LENGTH,
         new JsonOperatorValueExprConvertlet(0));
-    registerOp(SqlStdOperatorTable.JSON_KEYS,
+    registerOp(SqlLibraryOperators.JSON_KEYS,
         new JsonOperatorValueExprConvertlet(0));
 
     // Convert json_value('{"foo":"bar"}', 'lax $.foo', returning varchar(2000))
diff --git a/core/src/test/java/org/apache/calcite/sql/test/DocumentationTest.java b/core/src/test/java/org/apache/calcite/sql/test/DocumentationTest.java
index 2edc8cf..a4503a8 100644
--- a/core/src/test/java/org/apache/calcite/sql/test/DocumentationTest.java
+++ b/core/src/test/java/org/apache/calcite/sql/test/DocumentationTest.java
@@ -19,7 +19,8 @@ package org.apache.calcite.sql.test;
 import org.apache.calcite.sql.SqlFunction;
 import org.apache.calcite.sql.SqlOperator;
 import org.apache.calcite.sql.SqlSpecialOperator;
-import org.apache.calcite.sql.fun.OracleSqlOperatorTable;
+import org.apache.calcite.sql.fun.SqlLibrary;
+import org.apache.calcite.sql.fun.SqlLibraryOperatorTableFactory;
 import org.apache.calcite.sql.fun.SqlOverlapsOperator;
 import org.apache.calcite.sql.fun.SqlStdOperatorTable;
 import org.apache.calcite.sql.parser.SqlAbstractParserImpl;
@@ -37,6 +38,7 @@ import java.io.IOException;
 import java.io.LineNumberReader;
 import java.io.PrintWriter;
 import java.nio.file.Paths;
+import java.util.EnumSet;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
@@ -100,53 +102,74 @@ public class DocumentationTest {
    * reference.md. */
   @Test public void testAllFunctionsAreDocumented() throws IOException {
     final FileFixture f = new FileFixture();
-    final Map<String, Pattern> map = new TreeMap<>();
-    final Set<String> functionsSeen = new HashSet<>();
+    final Map<String, PatternOp> map = new TreeMap<>();
     addOperators(map, "", SqlStdOperatorTable.instance().getOperatorList());
-    addOperators(map, "\\| o ",
-        OracleSqlOperatorTable.instance().getOperatorList());
+    for (SqlLibrary library : SqlLibrary.values()) {
+      switch (library) {
+      case STANDARD:
+      case SPATIAL:
+        continue;
+      }
+      addOperators(map, "\\| [^|]*" + library.abbrev + "[^|]* ",
+          SqlLibraryOperatorTableFactory.INSTANCE
+              .getOperatorTable(EnumSet.of(library)).getOperatorList());
+    }
+    final Set<String> regexSeen = new HashSet<>();
     try (LineNumberReader r = new LineNumberReader(Util.reader(f.inFile))) {
       for (;;) {
         final String line = r.readLine();
         if (line == null) {
           break;
         }
-        for (Map.Entry<String, Pattern> entry : map.entrySet()) {
-          if (entry.getValue().matcher(line).matches()) {
-            functionsSeen.add(entry.getKey()); // function is documented
+        for (Map.Entry<String, PatternOp> entry : map.entrySet()) {
+          if (entry.getValue().pattern.matcher(line).matches()) {
+            regexSeen.add(entry.getKey()); // function is documented
           }
         }
       }
     }
-    final Set<String> functionsNotSeen = new TreeSet<>(map.keySet());
-    functionsNotSeen.removeAll(functionsSeen);
-    assertThat("some functions are documented: " + map.entrySet().stream()
-            .filter(e -> functionsNotSeen.contains(e.getKey()))
-            .map(Object::toString)
+    final Set<String> regexNotSeen = new TreeSet<>(map.keySet());
+    regexNotSeen.removeAll(regexSeen);
+    assertThat("some functions are not documented: " + map.entrySet().stream()
+            .filter(e -> regexNotSeen.contains(e.getKey()))
+            .map(e -> e.getValue().opName + "(" + e.getKey() + ")")
             .collect(Collectors.joining(", ")),
-        functionsNotSeen.isEmpty(), is(true));
+        regexNotSeen.isEmpty(), is(true));
   }
 
-  private void addOperators(Map<String, Pattern> map, String prefix,
+  private void addOperators(Map<String, PatternOp> map, String prefix,
       List<SqlOperator> operatorList) {
     for (SqlOperator op : operatorList) {
-      if (op.getName().startsWith("$")
-          || op.getName().isEmpty()
-          || op instanceof SqlSpecialOperator
-          || !op.getName().matches("^[a-zA-Z_]*$")) {
+      final String name = op.getName().equals("TRANSLATE3") ? "TRANSLATE"
+          : op.getName();
+      if (op instanceof SqlSpecialOperator
+          || !name.matches("^[a-zA-Z][a-zA-Z0-9_]*$")) {
         continue;
       }
       final String regex;
       if (op instanceof SqlOverlapsOperator) {
-        regex = "[ ]*<td>period1 " + op.getName() + " period2</td>";
+        regex = "[ ]*<td>period1 " + name + " period2</td>";
       } else if (op instanceof SqlFunction
           && (op.getOperandTypeChecker() == null
-          || op.getOperandTypeChecker().getOperandCountRange().getMin() != 0)) {
-        regex = prefix + "\\| .*" + op.getName() + "\\(.*";
+              || op.getOperandTypeChecker().getOperandCountRange().getMin()
+                  != 0)) {
+        regex = prefix + "\\| .*" + name + "\\(.*";
       } else {
-        regex = prefix + "\\| .*" + op.getName() + ".*";
+        regex = prefix + "\\| .*" + name + ".*";
       }
-      map.put(op.getName(), Pattern.compile(regex));
+      map.put(regex, new PatternOp(Pattern.compile(regex), name));
+    }
+  }
+
+  /** A compiled regex and an operator name. An item to be found in the
+   * documentation. */
+  private static class PatternOp {
+    final Pattern pattern;
+    final String opName;
+
+    private PatternOp(Pattern pattern, String opName) {
+      this.pattern = pattern;
+      this.opName = opName;
     }
   }
 
diff --git a/core/src/test/java/org/apache/calcite/sql/test/SqlOperatorBaseTest.java b/core/src/test/java/org/apache/calcite/sql/test/SqlOperatorBaseTest.java
index c7847f6..ef4ba27 100644
--- a/core/src/test/java/org/apache/calcite/sql/test/SqlOperatorBaseTest.java
+++ b/core/src/test/java/org/apache/calcite/sql/test/SqlOperatorBaseTest.java
@@ -37,14 +37,15 @@ import org.apache.calcite.sql.SqlOperandCountRange;
 import org.apache.calcite.sql.SqlOperator;
 import org.apache.calcite.sql.dialect.AnsiSqlDialect;
 import org.apache.calcite.sql.dialect.CalciteSqlDialect;
-import org.apache.calcite.sql.fun.OracleSqlOperatorTable;
+import org.apache.calcite.sql.fun.SqlLibrary;
+import org.apache.calcite.sql.fun.SqlLibraryOperatorTableFactory;
+import org.apache.calcite.sql.fun.SqlLibraryOperators;
 import org.apache.calcite.sql.fun.SqlStdOperatorTable;
 import org.apache.calcite.sql.parser.SqlParserPos;
 import org.apache.calcite.sql.pretty.SqlPrettyWriter;
 import org.apache.calcite.sql.type.BasicSqlType;
 import org.apache.calcite.sql.type.SqlOperandTypeChecker;
 import org.apache.calcite.sql.type.SqlTypeName;
-import org.apache.calcite.sql.util.ChainedSqlOperatorTable;
 import org.apache.calcite.sql.util.SqlString;
 import org.apache.calcite.sql.validate.SqlConformance;
 import org.apache.calcite.sql.validate.SqlConformanceEnum;
@@ -308,8 +309,8 @@ public abstract class SqlOperatorBaseTest {
 
   protected SqlTester oracleTester() {
     return tester.withOperatorTable(
-        ChainedSqlOperatorTable.of(OracleSqlOperatorTable.instance(),
-            SqlStdOperatorTable.instance()))
+            SqlLibraryOperatorTableFactory.INSTANCE
+                .getOperatorTable(SqlLibrary.STANDARD, SqlLibrary.ORACLE))
         .withConnectionFactory(
             CalciteAssert.EMPTY_CONNECTION_FACTORY
                 .with(new CalciteAssert
@@ -324,8 +325,8 @@ public abstract class SqlOperatorBaseTest {
     return tester
         .withConformance(conformance)
         .withOperatorTable(
-            ChainedSqlOperatorTable.of(OracleSqlOperatorTable.instance(),
-                SqlStdOperatorTable.instance()))
+            SqlLibraryOperatorTableFactory.INSTANCE
+                .getOperatorTable(SqlLibrary.STANDARD, SqlLibrary.ORACLE))
         .withConnectionFactory(
             CalciteAssert.EMPTY_CONNECTION_FACTORY
                 .with(new CalciteAssert
@@ -4096,7 +4097,7 @@ public abstract class SqlOperatorBaseTest {
 
   @Test public void testTranslate3Func() {
     final SqlTester tester1 = oracleTester();
-    tester1.setFor(OracleSqlOperatorTable.TRANSLATE3);
+    tester1.setFor(SqlLibraryOperators.TRANSLATE3);
     tester1.checkString(
         "translate('aabbcc', 'ab', '+-')",
         "++--cc",
@@ -4472,7 +4473,7 @@ public abstract class SqlOperatorBaseTest {
   }
 
   @Test public void testJsonType() {
-    tester.setFor(SqlStdOperatorTable.JSON_TYPE);
+    tester.setFor(SqlLibraryOperators.JSON_TYPE);
     tester.checkString("json_type('\"1\"')",
             "STRING", "VARCHAR(20) NOT NULL");
     tester.checkString("json_type('1')",
@@ -4496,7 +4497,7 @@ public abstract class SqlOperatorBaseTest {
   }
 
   @Test public void testJsonDepth() {
-    tester.setFor(SqlStdOperatorTable.JSON_DEPTH);
+    tester.setFor(SqlLibraryOperators.JSON_DEPTH);
     tester.checkString("json_depth('1')",
             "1", "INTEGER");
     tester.checkString("json_depth('11.45')",
@@ -5888,21 +5889,21 @@ public abstract class SqlOperatorBaseTest {
   }
 
   @Test public void testRtrimFunc() {
-    tester.setFor(OracleSqlOperatorTable.RTRIM);
+    tester.setFor(SqlLibraryOperators.RTRIM);
     final SqlTester tester1 = oracleTester();
     tester1.checkString("rtrim(' aAa  ')", " aAa", "VARCHAR(6) NOT NULL");
     tester1.checkNull("rtrim(CAST(NULL AS VARCHAR(6)))");
   }
 
   @Test public void testLtrimFunc() {
-    tester.setFor(OracleSqlOperatorTable.LTRIM);
+    tester.setFor(SqlLibraryOperators.LTRIM);
     final SqlTester tester1 = oracleTester();
     tester1.checkString("ltrim(' aAa  ')", "aAa  ", "VARCHAR(6) NOT NULL");
     tester1.checkNull("ltrim(CAST(NULL AS VARCHAR(6)))");
   }
 
   @Test public void testGreatestFunc() {
-    tester.setFor(OracleSqlOperatorTable.GREATEST);
+    tester.setFor(SqlLibraryOperators.GREATEST);
     final SqlTester tester1 = oracleTester();
     tester1.checkString("greatest('on', 'earth')", "on   ", "CHAR(5) NOT NULL");
     tester1.checkString("greatest('show', 'on', 'earth')", "show ",
@@ -5917,7 +5918,7 @@ public abstract class SqlOperatorBaseTest {
   }
 
   @Test public void testLeastFunc() {
-    tester.setFor(OracleSqlOperatorTable.LEAST);
+    tester.setFor(SqlLibraryOperators.LEAST);
     final SqlTester tester1 = oracleTester();
     tester1.checkString("least('on', 'earth')", "earth", "CHAR(5) NOT NULL");
     tester1.checkString("least('show', 'on', 'earth')", "earth",
@@ -5932,7 +5933,7 @@ public abstract class SqlOperatorBaseTest {
   }
 
   @Test public void testNvlFunc() {
-    tester.setFor(OracleSqlOperatorTable.NVL);
+    tester.setFor(SqlLibraryOperators.NVL);
     final SqlTester tester1 = oracleTester();
     tester1.checkScalar("nvl(1, 2)", "1", "INTEGER NOT NULL");
     tester1.checkFails("^nvl(1, true)^", "Parameters must be of the same type",
@@ -5960,7 +5961,7 @@ public abstract class SqlOperatorBaseTest {
   }
 
   @Test public void testDecodeFunc() {
-    tester.setFor(OracleSqlOperatorTable.DECODE);
+    tester.setFor(SqlLibraryOperators.DECODE);
     final SqlTester tester1 = oracleTester();
     tester1.checkScalar("decode(0, 0, 'a', 1, 'b', 2, 'c')", "a", "CHAR(1)");
     tester1.checkScalar("decode(1, 0, 'a', 1, 'b', 2, 'c')", "b", "CHAR(1)");
diff --git a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
index 37de177..739f1dd 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
@@ -27,7 +27,8 @@ import org.apache.calcite.sql.SqlCollation;
 import org.apache.calcite.sql.SqlNode;
 import org.apache.calcite.sql.SqlOperator;
 import org.apache.calcite.sql.SqlSpecialOperator;
-import org.apache.calcite.sql.fun.OracleSqlOperatorTable;
+import org.apache.calcite.sql.fun.SqlLibrary;
+import org.apache.calcite.sql.fun.SqlLibraryOperatorTableFactory;
 import org.apache.calcite.sql.fun.SqlStdOperatorTable;
 import org.apache.calcite.sql.parser.SqlParser;
 import org.apache.calcite.sql.test.SqlTester;
@@ -35,7 +36,6 @@ import org.apache.calcite.sql.type.ArraySqlType;
 import org.apache.calcite.sql.type.SqlTypeFactoryImpl;
 import org.apache.calcite.sql.type.SqlTypeName;
 import org.apache.calcite.sql.type.SqlTypeUtil;
-import org.apache.calcite.sql.util.ChainedSqlOperatorTable;
 import org.apache.calcite.sql.validate.SqlAbstractConformance;
 import org.apache.calcite.sql.validate.SqlConformance;
 import org.apache.calcite.sql.validate.SqlConformanceEnum;
@@ -803,8 +803,8 @@ public class SqlValidatorTest extends SqlValidatorTestCase {
     checkWholeExpFails("translate('aabbcc', 'ab', '+-')",
         "No match found for function signature TRANSLATE3\\(<CHARACTER>, <CHARACTER>, <CHARACTER>\\)");
     tester = tester.withOperatorTable(
-        ChainedSqlOperatorTable.of(OracleSqlOperatorTable.instance(),
-            SqlStdOperatorTable.instance()));
+        SqlLibraryOperatorTableFactory.INSTANCE
+            .getOperatorTable(SqlLibrary.STANDARD, SqlLibrary.ORACLE));
     checkExpType("translate('aabbcc', 'ab', '+-')",
         "VARCHAR(6) NOT NULL");
     checkWholeExpFails("translate('abc', 'ab')",
diff --git a/site/_docs/reference.md b/site/_docs/reference.md
index 2506e84..f8414e2 100644
--- a/site/_docs/reference.md
+++ b/site/_docs/reference.md
@@ -2087,7 +2087,8 @@ connect string parameter.
 
 The 'C' (compatibility) column contains value
 'm' for MySQL ('fun=mysql' in the connect string),
-'o' for Oracle ('fun=oracle' in the connect string).
+'o' for Oracle ('fun=oracle' in the connect string),
+'p' for PostgreSQL ('fun=postgresql' in the connect string).
 
 One operator name may correspond to multiple SQL dialects, but with different
 semantics.
@@ -2106,7 +2107,7 @@ semantics.
 | o | SUBSTR(string, position [, substring_length ]) | Returns a portion of *string*, beginning at character *position*, *substring_length* characters long. SUBSTR calculates lengths using characters as defined by the input character set
 | o | GREATEST(expr [, expr ]*)                      | Returns the greatest of the expressions
 | o | LEAST(expr [, expr ]* )                        | Returns the least of the expressions
-| o | TRANSLATE(expr, fromString, toString)          | Returns *expr* with all occurrences of each character in *fromString* replaced by its corresponding character in *toString*. Characters in *expr* that are not in *fromString* are not replaced
+| o p | TRANSLATE(expr, fromString, toString)          | Returns *expr* with all occurrences of each character in *fromString* replaced by its corresponding character in *toString*. Characters in *expr* that are not in *fromString* are not replaced
 
 Note: