[ASTERIXDB-3542][SQL++] Add CRS support and st_transform function

  - New metadata entity: CoordinateReferenceSystem (SRID, name, WKT)
    backed by a dedicated metadata index and tuple translator
  - DDL: CREATE CRS, DROP CRS
  - SQL++ parser (SQLPP.jj): grammar rules for CREATE/DROP CRS
  - ST_Transform(geom, fromSRID, toSRID): compile-time WKT lookup via
    metadata, runtime coordinate transformation via Apache SIS
  - ST_Distance_Spheroid(geom1, geom2): geodesic distance on WGS-84
    ellipsoid using SIS GeodeticCalculator
  - Metadata lock support: acquireCRSReadLock / acquireCRSWriteLock
  - Error codes 1244-1249 covering CRS DDL and function failure modes
  - Test suite: 13 test cases covering DDL lifecycle, ST_Transform,
    ST_Distance_Spheroid, and negative cases
  - Documentation: DDL reference and geo-functions markdown updated
  - Apache SIS + GeoAPI dependencies added (asterix-geo, asterix-app)

Some parts of this commit were Generated-by: Claude code

Change-Id: Ia6e37080a581292744ddc9020b214926412c16ac
Reviewed-on: https://asterix-gerrit.ics.uci.edu/c/asterixdb/+/20968
Integration-Tests: Jenkins <jenkins@fulliautomatix.ics.uci.edu>
Reviewed-by: Ian Maxon <imaxon@apache.org>
Tested-by: Jenkins <jenkins@fulliautomatix.ics.uci.edu>
diff --git a/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/compiler/provider/DefaultRuleSetFactory.java b/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/compiler/provider/DefaultRuleSetFactory.java
index b74363d..bf9d4fb 100644
--- a/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/compiler/provider/DefaultRuleSetFactory.java
+++ b/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/compiler/provider/DefaultRuleSetFactory.java
@@ -98,6 +98,7 @@
         defaultLogicalRewrites
                 .add(new Pair<>(seqCtrlFullDfs, RuleCollections.buildLoadFieldsRuleCollection(appCtx, true)));
         defaultLogicalRewrites.add(new Pair<>(seqOnceCtrl, RuleCollections.buildFulltextContainsRuleCollection()));
+        defaultLogicalRewrites.add(new Pair<>(seqOnceCtrl, RuleCollections.buildSTTransformRuleCollection()));
         defaultLogicalRewrites.add(new Pair<>(seqOnceCtrl, RuleCollections.buildDataExchangeRuleCollection()));
         defaultLogicalRewrites.add(new Pair<>(seqOnceCtrl, RuleCollections.buildCBORuleCollection()));
         defaultLogicalRewrites.add(new Pair<>(seqCtrlNoDfs, RuleCollections.buildConsolidationRuleCollection()));
diff --git a/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/base/RuleCollections.java b/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/base/RuleCollections.java
index 5defe14..241bda1 100644
--- a/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/base/RuleCollections.java
+++ b/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/base/RuleCollections.java
@@ -93,6 +93,7 @@
 import org.apache.asterix.optimizer.rules.RemoveUnknownCheckForKnownTypeExpressionRule;
 import org.apache.asterix.optimizer.rules.RemoveUnusedOneToOneEquiJoinRule;
 import org.apache.asterix.optimizer.rules.RewriteDistinctAggregateRule;
+import org.apache.asterix.optimizer.rules.STTransformResolveCRSRule;
 import org.apache.asterix.optimizer.rules.SetAsterixMemoryRequirementsRule;
 import org.apache.asterix.optimizer.rules.SetAsterixPhysicalOperatorsRule;
 import org.apache.asterix.optimizer.rules.SetClosedRecordConstructorsRule;
@@ -196,6 +197,10 @@
         return Collections.singletonList(new FullTextContainsParameterCheckAndSetRule());
     }
 
+    public static List<IAlgebraicRewriteRule> buildSTTransformRuleCollection() {
+        return Collections.singletonList(new STTransformResolveCRSRule());
+    }
+
     public static List<IAlgebraicRewriteRule> buildNormalizationRuleCollection(ICcApplicationContext appCtx) {
         List<IAlgebraicRewriteRule> normalization = new LinkedList<>();
         normalization.add(new CheckInsertUpsertReturningRule());
diff --git a/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/STTransformResolveCRSRule.java b/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/STTransformResolveCRSRule.java
new file mode 100644
index 0000000..fdd646b
--- /dev/null
+++ b/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/STTransformResolveCRSRule.java
@@ -0,0 +1,154 @@
+/*
+ * 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.asterix.optimizer.rules;
+
+import org.apache.asterix.common.exceptions.CompilationException;
+import org.apache.asterix.common.exceptions.ErrorCode;
+import org.apache.asterix.common.metadata.DataverseName;
+import org.apache.asterix.common.metadata.Namespace;
+import org.apache.asterix.metadata.MetadataManager;
+import org.apache.asterix.metadata.MetadataTransactionContext;
+import org.apache.asterix.metadata.declared.MetadataProvider;
+import org.apache.asterix.metadata.entities.CoordinateReferenceSystem;
+import org.apache.asterix.om.functions.BuiltinFunctions;
+import org.apache.asterix.om.utils.ConstantExpressionUtil;
+import org.apache.commons.lang3.mutable.Mutable;
+import org.apache.hyracks.algebricks.common.exceptions.AlgebricksException;
+import org.apache.hyracks.algebricks.core.algebra.base.ILogicalExpression;
+import org.apache.hyracks.algebricks.core.algebra.base.ILogicalOperator;
+import org.apache.hyracks.algebricks.core.algebra.base.IOptimizationContext;
+import org.apache.hyracks.algebricks.core.algebra.base.LogicalExpressionTag;
+import org.apache.hyracks.algebricks.core.algebra.expressions.AbstractFunctionCallExpression;
+import org.apache.hyracks.algebricks.core.algebra.functions.FunctionIdentifier;
+import org.apache.hyracks.algebricks.core.algebra.operators.logical.AbstractLogicalOperator;
+import org.apache.hyracks.algebricks.core.algebra.util.OperatorPropertiesUtil;
+import org.apache.hyracks.algebricks.core.algebra.visitors.ILogicalExpressionReferenceTransform;
+import org.apache.hyracks.algebricks.core.rewriter.base.IAlgebraicRewriteRule;
+
+/**
+ * Resolves the two CRS WKT definitions referenced by an ST_Transform call at compile time
+ * from metadata and stores them as opaque parameters on the function expression. The
+ * runtime type-inferer then simply forwards these WKT strings to the descriptor. This
+ * mirrors {@link FullTextContainsParameterCheckAndSetRule}.
+ */
+public class STTransformResolveCRSRule implements IAlgebraicRewriteRule {
+
+    private final STTransformExpressionVisitor visitor = new STTransformExpressionVisitor();
+
+    @Override
+    public boolean rewritePre(Mutable<ILogicalOperator> opRef, IOptimizationContext context)
+            throws AlgebricksException {
+        if (context.checkIfInDontApplySet(this, opRef.getValue())) {
+            return false;
+        }
+        AbstractLogicalOperator op = (AbstractLogicalOperator) opRef.getValue();
+        visitor.setContext(context);
+        boolean modified = op.acceptExpressionTransform(visitor);
+        if (modified) {
+            context.addToDontApplySet(this, op);
+            OperatorPropertiesUtil.typeOpRec(opRef, context);
+            return true;
+        }
+        return false;
+    }
+
+    private static final class STTransformExpressionVisitor implements ILogicalExpressionReferenceTransform {
+
+        private IOptimizationContext context;
+
+        void setContext(IOptimizationContext context) {
+            this.context = context;
+        }
+
+        @Override
+        public boolean transform(Mutable<ILogicalExpression> exprRef) throws AlgebricksException {
+            ILogicalExpression e = exprRef.getValue();
+            if (e.getExpressionTag() != LogicalExpressionTag.FUNCTION_CALL) {
+                return false;
+            }
+            return transformFunctionCall((AbstractFunctionCallExpression) e);
+        }
+
+        private boolean transformFunctionCall(AbstractFunctionCallExpression fce) throws AlgebricksException {
+            FunctionIdentifier fi = fce.getFunctionIdentifier();
+            boolean modified = false;
+            if (fi == BuiltinFunctions.ST_TRANSFORM) {
+                if (fce.getOpaqueParameters() == null) {
+                    resolveCRS(fce);
+                    modified = true;
+                }
+            }
+            for (Mutable<ILogicalExpression> arg : fce.getArguments()) {
+                if (transform(arg)) {
+                    modified = true;
+                }
+            }
+            return modified;
+        }
+
+        private void resolveCRS(AbstractFunctionCallExpression fce) throws AlgebricksException {
+            Long fromSRIDLong = getIntegerLikeConstant(fce.getArguments().get(1).getValue());
+            Long toSRIDLong = getIntegerLikeConstant(fce.getArguments().get(2).getValue());
+
+            if (fromSRIDLong == null || toSRIDLong == null) {
+                throw new CompilationException(ErrorCode.COMPILATION_ERROR, fce.getSourceLocation(),
+                        "ST_Transform requires constant SRID arguments");
+            }
+            if (fromSRIDLong < 1 || fromSRIDLong > Integer.MAX_VALUE) {
+                throw new CompilationException(ErrorCode.COMPILATION_ERROR, fce.getSourceLocation(),
+                        "SRID must be a positive integer (1 to " + Integer.MAX_VALUE + "), got: " + fromSRIDLong);
+            }
+            if (toSRIDLong < 1 || toSRIDLong > Integer.MAX_VALUE) {
+                throw new CompilationException(ErrorCode.COMPILATION_ERROR, fce.getSourceLocation(),
+                        "SRID must be a positive integer (1 to " + Integer.MAX_VALUE + "), got: " + toSRIDLong);
+            }
+
+            int fromSRID = fromSRIDLong.intValue();
+            int toSRID = toSRIDLong.intValue();
+
+            MetadataProvider metadataProvider = (MetadataProvider) context.getMetadataProvider();
+            Namespace defaultNamespace = metadataProvider.getDefaultNamespace();
+            String database = defaultNamespace.getDatabaseName();
+            DataverseName dataverseName = defaultNamespace.getDataverseName();
+            MetadataTransactionContext mdTxnCtx = metadataProvider.getMetadataTxnContext();
+
+            CoordinateReferenceSystem fromCrs =
+                    MetadataManager.INSTANCE.getCRS(mdTxnCtx, database, dataverseName, fromSRID);
+            if (fromCrs == null) {
+                throw new CompilationException(ErrorCode.CRS_NOT_FOUND, fce.getSourceLocation(), fromSRID);
+            }
+            CoordinateReferenceSystem toCrs =
+                    MetadataManager.INSTANCE.getCRS(mdTxnCtx, database, dataverseName, toSRID);
+            if (toCrs == null) {
+                throw new CompilationException(ErrorCode.CRS_NOT_FOUND, fce.getSourceLocation(), toSRID);
+            }
+
+            fce.setOpaqueParameters(new Object[] { fromCrs.getCrsWkt(), toCrs.getCrsWkt() });
+        }
+
+        private static Long getIntegerLikeConstant(ILogicalExpression expr) {
+            Long longConstant = ConstantExpressionUtil.getLongConstant(expr);
+            if (longConstant != null) {
+                return longConstant;
+            }
+            Integer intConstant = ConstantExpressionUtil.getIntConstant(expr);
+            return intConstant != null ? intConstant.longValue() : null;
+        }
+    }
+}
diff --git a/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/util/SpatialJoinUtils.java b/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/util/SpatialJoinUtils.java
index ca997e0..6279a45 100644
--- a/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/util/SpatialJoinUtils.java
+++ b/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/util/SpatialJoinUtils.java
@@ -86,7 +86,7 @@
     protected static boolean trySpatialJoinAssignment(AbstractBinaryJoinOperator op, IOptimizationContext context,
             ILogicalExpression joinCondition, int left, int right) throws AlgebricksException {
         AbstractFunctionCallExpression funcExpr = (AbstractFunctionCallExpression) joinCondition;
-        // Check if the join condition contains spatial join
+        // Check if the join condition contains spatial joinbut
         AbstractFunctionCallExpression spatialJoinFuncExpr = null;
         // Maintain conditions which is not spatial_intersect in the join condition
         List<Mutable<ILogicalExpression>> conditionExprs = new ArrayList<>();
diff --git a/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/translator/AbstractLangTranslator.java b/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/translator/AbstractLangTranslator.java
index 7fc758b..2b6dbbc 100644
--- a/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/translator/AbstractLangTranslator.java
+++ b/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/translator/AbstractLangTranslator.java
@@ -68,6 +68,8 @@
 import org.apache.asterix.lang.common.statement.TypeDropStatement;
 import org.apache.asterix.lang.common.statement.UpdateStatement;
 import org.apache.asterix.lang.common.statement.UpsertStatement;
+import org.apache.asterix.lang.common.statement.crs.CRSCreateStatement;
+import org.apache.asterix.lang.common.statement.crs.CRSDropStatement;
 import org.apache.asterix.metadata.dataset.hints.DatasetHints;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.hyracks.algebricks.common.exceptions.AlgebricksException;
@@ -408,6 +410,22 @@
                     message = formatObjectDdlMessage("analyze drop", dataset(), namespace, usingDb);
                 }
                 break;
+
+            case CRS_CREATE:
+                namespace = getStatementNamespace(((CRSCreateStatement) stmt).getNamespace(), activeNamespace);
+                invalidOperation = isSystemNamespace(namespace);
+                if (invalidOperation) {
+                    message = formatObjectDdlMessage("create", "coordinate reference system", namespace, usingDb);
+                }
+                break;
+
+            case CRS_DROP:
+                namespace = getStatementNamespace(((CRSDropStatement) stmt).getNamespace(), activeNamespace);
+                invalidOperation = isSystemNamespace(namespace);
+                if (invalidOperation) {
+                    message = formatObjectDdlMessage("drop", "coordinate reference system", namespace, usingDb);
+                }
+                break;
         }
 
         if (invalidOperation) {
diff --git a/asterixdb/asterix-app/pom.xml b/asterixdb/asterix-app/pom.xml
index 059c8f6..30eb1a9 100644
--- a/asterixdb/asterix-app/pom.xml
+++ b/asterixdb/asterix-app/pom.xml
@@ -730,6 +730,10 @@
       <version>${project.version}</version>
     </dependency>
     <dependency>
+      <groupId>org.apache.sis.core</groupId>
+      <artifactId>sis-referencing</artifactId>
+    </dependency>
+    <dependency>
       <groupId>org.apache.asterix</groupId>
       <artifactId>asterix-common</artifactId>
       <version>${project.version}</version>
diff --git a/asterixdb/asterix-app/src/main/java/org/apache/asterix/app/translator/QueryTranslator.java b/asterixdb/asterix-app/src/main/java/org/apache/asterix/app/translator/QueryTranslator.java
index ea42a3d..df52f9c 100644
--- a/asterixdb/asterix-app/src/main/java/org/apache/asterix/app/translator/QueryTranslator.java
+++ b/asterixdb/asterix-app/src/main/java/org/apache/asterix/app/translator/QueryTranslator.java
@@ -68,6 +68,7 @@
 import org.apache.asterix.app.result.ResultReader;
 import org.apache.asterix.app.result.fields.ResultHandlePrinter;
 import org.apache.asterix.app.result.fields.ResultsPrinter;
+import org.apache.asterix.app.translator.handlers.CRSStatementHandler;
 import org.apache.asterix.app.translator.handlers.IcebergCatalogStatementHandler;
 import org.apache.asterix.app.translator.helpers.IcebergStatementValidationHelper;
 import org.apache.asterix.column.validation.ColumnPropertiesValidationUtil;
@@ -183,6 +184,7 @@
 import org.apache.asterix.lang.common.statement.UpsertStatement;
 import org.apache.asterix.lang.common.statement.ViewDecl;
 import org.apache.asterix.lang.common.statement.ViewDropStatement;
+import org.apache.asterix.lang.common.statement.crs.CRSStatement;
 import org.apache.asterix.lang.common.struct.Identifier;
 import org.apache.asterix.lang.common.struct.VarIdentifier;
 import org.apache.asterix.lang.common.util.FunctionUtil;
@@ -598,6 +600,10 @@
                     case CATALOG_DROP:
                         handleCatalogStatement(kind, metadataProvider, stmt, hcc, requestParameters);
                         break;
+                    case CRS_CREATE:
+                    case CRS_DROP:
+                        handleCRSStatement(kind, metadataProvider, stmt);
+                        break;
                     default:
                         throw new CompilationException(ErrorCode.COMPILATION_ILLEGAL_STATE, stmt.getSourceLocation(),
                                 "Unexpected statement: " + kind);
@@ -5909,6 +5915,14 @@
         statement.handle();
     }
 
+    protected void handleCRSStatement(Statement.Kind kind, MetadataProvider metadataProvider, Statement stmt)
+            throws Exception {
+        Namespace resolvedNamespace = getActiveNamespace(((CRSStatement) stmt).getNamespace());
+        CRSStatementHandler handler = new CRSStatementHandler(kind, metadataProvider, stmt, sessionConfig, lockUtil,
+                lockManager, resolvedNamespace);
+        handler.handle();
+    }
+
     @Override
     public Namespace getActiveNamespace(Namespace namespace) {
         return namespace != null ? namespace : activeNamespace;
diff --git a/asterixdb/asterix-app/src/main/java/org/apache/asterix/app/translator/handlers/CRSStatementHandler.java b/asterixdb/asterix-app/src/main/java/org/apache/asterix/app/translator/handlers/CRSStatementHandler.java
new file mode 100644
index 0000000..4a822bf
--- /dev/null
+++ b/asterixdb/asterix-app/src/main/java/org/apache/asterix/app/translator/handlers/CRSStatementHandler.java
@@ -0,0 +1,172 @@
+/*
+ * 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.asterix.app.translator.handlers;
+
+import static org.apache.asterix.app.translator.QueryTranslator.abort;
+
+import org.apache.asterix.common.api.IMetadataLockManager;
+import org.apache.asterix.common.exceptions.CompilationException;
+import org.apache.asterix.common.exceptions.ErrorCode;
+import org.apache.asterix.common.metadata.DataverseName;
+import org.apache.asterix.common.metadata.IMetadataLockUtil;
+import org.apache.asterix.common.metadata.MetadataUtil;
+import org.apache.asterix.common.metadata.Namespace;
+import org.apache.asterix.lang.common.base.Statement;
+import org.apache.asterix.lang.common.statement.crs.CRSCreateStatement;
+import org.apache.asterix.lang.common.statement.crs.CRSDropStatement;
+import org.apache.asterix.metadata.MetadataManager;
+import org.apache.asterix.metadata.MetadataTransactionContext;
+import org.apache.asterix.metadata.declared.MetadataProvider;
+import org.apache.asterix.metadata.entities.CoordinateReferenceSystem;
+import org.apache.asterix.translator.SessionConfig;
+import org.apache.sis.referencing.CRS;
+import org.opengis.util.FactoryException;
+
+public class CRSStatementHandler {
+
+    private final Statement.Kind kind;
+    private final MetadataProvider metadataProvider;
+    private final Statement statement;
+    private final SessionConfig sessionConfig;
+    private final IMetadataLockUtil lockUtil;
+    private final IMetadataLockManager lockManager;
+    private final Namespace activeNamespace;
+
+    public CRSStatementHandler(Statement.Kind kind, MetadataProvider metadataProvider, Statement statement,
+            SessionConfig sessionConfig, IMetadataLockUtil lockUtil, IMetadataLockManager lockManager,
+            Namespace activeNamespace) {
+        this.kind = kind;
+        this.metadataProvider = metadataProvider;
+        this.statement = statement;
+        this.sessionConfig = sessionConfig;
+        this.lockUtil = lockUtil;
+        this.lockManager = lockManager;
+        this.activeNamespace = activeNamespace;
+    }
+
+    public void handle() throws Exception {
+        switch (kind) {
+            case CRS_CREATE:
+                handleCreate();
+                return;
+            case CRS_DROP:
+                handleDrop();
+                return;
+            default:
+                throw new IllegalStateException("CRS statement handler handling non-CRS statement: " + kind);
+        }
+    }
+
+    private void handleCreate() throws Exception {
+        if (isCompileOnly()) {
+            return;
+        }
+        CRSCreateStatement stmt = (CRSCreateStatement) statement;
+        String databaseName = activeNamespace.getDatabaseName();
+        DataverseName dataverseName = activeNamespace.getDataverseName();
+        lockUtil.createCRSBegin(lockManager, metadataProvider.getLocks(), databaseName, dataverseName, stmt.getSrid());
+        try {
+            doHandleCreate(stmt, databaseName, dataverseName);
+        } finally {
+            metadataProvider.getLocks().unlock();
+        }
+    }
+
+    private void doHandleCreate(CRSCreateStatement stmt, String databaseName, DataverseName dataverseName)
+            throws Exception {
+        MetadataTransactionContext mdTxnCtx = MetadataManager.INSTANCE.beginTransaction();
+        metadataProvider.setMetadataTxnContext(mdTxnCtx);
+        try {
+            if (MetadataManager.INSTANCE.getDataverse(mdTxnCtx, databaseName, dataverseName) == null) {
+                throw new CompilationException(ErrorCode.UNKNOWN_DATAVERSE, stmt.getSourceLocation(),
+                        MetadataUtil.dataverseName(databaseName, dataverseName, metadataProvider.isUsingDatabase()));
+            }
+            CoordinateReferenceSystem existing =
+                    MetadataManager.INSTANCE.getCRS(mdTxnCtx, databaseName, dataverseName, stmt.getSrid());
+            if (existing != null) {
+                if (stmt.getIfNotExists()) {
+                    MetadataManager.INSTANCE.commitTransaction(mdTxnCtx);
+                    return;
+                } else {
+                    throw new CompilationException(ErrorCode.CRS_ALREADY_EXISTS, stmt.getSourceLocation(),
+                            stmt.getSrid());
+                }
+            }
+            try {
+                CRS.fromWKT(stmt.getCrsWkt());
+            } catch (FactoryException e) {
+                throw new CompilationException(ErrorCode.INVALID_CRS_WKT, stmt.getSourceLocation(), stmt.getSrid(),
+                        e.getMessage());
+            }
+            CoordinateReferenceSystem crs = new CoordinateReferenceSystem(databaseName, dataverseName, stmt.getSrid(),
+                    stmt.getCrsName(), stmt.getCrsWkt());
+            MetadataManager.INSTANCE.addCRS(mdTxnCtx, crs);
+            MetadataManager.INSTANCE.commitTransaction(mdTxnCtx);
+        } catch (Exception e) {
+            abort(e, e, mdTxnCtx);
+            throw e;
+        }
+    }
+
+    private void handleDrop() throws Exception {
+        if (isCompileOnly()) {
+            return;
+        }
+        CRSDropStatement stmt = (CRSDropStatement) statement;
+        String databaseName = activeNamespace.getDatabaseName();
+        DataverseName dataverseName = activeNamespace.getDataverseName();
+        lockUtil.dropCRSBegin(lockManager, metadataProvider.getLocks(), databaseName, dataverseName, stmt.getSrid());
+        try {
+            doHandleDrop(stmt, databaseName, dataverseName);
+        } finally {
+            metadataProvider.getLocks().unlock();
+        }
+    }
+
+    private void doHandleDrop(CRSDropStatement stmt, String databaseName, DataverseName dataverseName)
+            throws Exception {
+        MetadataTransactionContext mdTxnCtx = MetadataManager.INSTANCE.beginTransaction();
+        metadataProvider.setMetadataTxnContext(mdTxnCtx);
+        try {
+            if (MetadataManager.INSTANCE.getDataverse(mdTxnCtx, databaseName, dataverseName) == null) {
+                throw new CompilationException(ErrorCode.UNKNOWN_DATAVERSE, stmt.getSourceLocation(),
+                        MetadataUtil.dataverseName(databaseName, dataverseName, metadataProvider.isUsingDatabase()));
+            }
+            CoordinateReferenceSystem existing =
+                    MetadataManager.INSTANCE.getCRS(mdTxnCtx, databaseName, dataverseName, stmt.getSrid());
+            if (existing == null) {
+                if (stmt.getIfExists()) {
+                    MetadataManager.INSTANCE.commitTransaction(mdTxnCtx);
+                    return;
+                } else {
+                    throw new CompilationException(ErrorCode.CRS_NOT_FOUND, stmt.getSourceLocation(), stmt.getSrid());
+                }
+            }
+            MetadataManager.INSTANCE.dropCRS(mdTxnCtx, databaseName, dataverseName, stmt.getSrid());
+            MetadataManager.INSTANCE.commitTransaction(mdTxnCtx);
+        } catch (Exception e) {
+            abort(e, e, mdTxnCtx);
+            throw e;
+        }
+    }
+
+    private boolean isCompileOnly() {
+        return !sessionConfig.isExecuteQuery();
+    }
+}
diff --git a/asterixdb/asterix-app/src/test/resources/metadata/results/basic/metadata_dataset/metadata_dataset.1.adm b/asterixdb/asterix-app/src/test/resources/metadata/results/basic/metadata_dataset/metadata_dataset.1.adm
index 876b010..891f53b 100644
--- a/asterixdb/asterix-app/src/test/resources/metadata/results/basic/metadata_dataset/metadata_dataset.1.adm
+++ b/asterixdb/asterix-app/src/test/resources/metadata/results/basic/metadata_dataset/metadata_dataset.1.adm
@@ -1,5 +1,6 @@
 { "DataverseName": "Metadata", "DatasetName": "Catalog", "DatatypeDataverseName": "Metadata", "DatatypeName": "CatalogRecordType", "DatasetType": "INTERNAL", "GroupName": "MetadataGroup", "InternalDetails": { "FileStructure": "BTREE", "PartitioningStrategy": "HASH", "PartitioningKey": [ [ "CatalogName" ] ], "PrimaryKey": [ [ "CatalogName" ] ], "Autogenerated": false }, "Hints": {{  }}, "Timestamp": "Fri Aug 22 12:45:45 UTC 2025", "DatasetId": 19, "PendingOp": 0, "DatasetFormat": { "Format": "ROW" } }
 { "DataverseName": "Metadata", "DatasetName": "CompactionPolicy", "DatatypeDataverseName": "Metadata", "DatatypeName": "CompactionPolicyRecordType", "DatasetType": "INTERNAL", "GroupName": "MetadataGroup", "InternalDetails": { "FileStructure": "BTREE", "PartitioningStrategy": "HASH", "PartitioningKey": [ [ "DataverseName" ], [ "CompactionPolicy" ] ], "PrimaryKey": [ [ "DataverseName" ], [ "CompactionPolicy" ] ], "Autogenerated": false }, "Hints": {{  }}, "Timestamp": "Fri Oct 21 10:29:21 PDT 2016", "DatasetId": 13, "PendingOp": 0, "DatasetFormat": { "Format": "ROW" } }
+{ "DataverseName": "Metadata", "DatasetName": "CoordinateReferenceSystem", "DatatypeDataverseName": "Metadata", "DatatypeName": "CoordinateReferenceSystemRecordType", "DatasetType": "INTERNAL", "GroupName": "MetadataGroup", "InternalDetails": { "FileStructure": "BTREE", "PartitioningStrategy": "HASH", "PartitioningKey": [ [ "DataverseName" ], [ "SRID" ] ], "PrimaryKey": [ [ "DataverseName" ], [ "SRID" ] ], "Autogenerated": false }, "Hints": {{  }}, "Timestamp": "Fri Oct 21 10:29:21 PDT 2016", "DatasetId": 20, "PendingOp": 0, "DatasetFormat": { "Format": "ROW" } }
 { "DataverseName": "Metadata", "DatasetName": "Dataset", "DatatypeDataverseName": "Metadata", "DatatypeName": "DatasetRecordType", "DatasetType": "INTERNAL", "GroupName": "MetadataGroup", "InternalDetails": { "FileStructure": "BTREE", "PartitioningStrategy": "HASH", "PartitioningKey": [ [ "DataverseName" ], [ "DatasetName" ] ], "PrimaryKey": [ [ "DataverseName" ], [ "DatasetName" ] ], "Autogenerated": false }, "Hints": {{  }}, "Timestamp": "Fri Oct 21 10:29:21 PDT 2016", "DatasetId": 2, "PendingOp": 0, "DatasetFormat": { "Format": "ROW" } }
 { "DataverseName": "Metadata", "DatasetName": "DatasourceAdapter", "DatatypeDataverseName": "Metadata", "DatatypeName": "DatasourceAdapterRecordType", "DatasetType": "INTERNAL", "GroupName": "MetadataGroup", "InternalDetails": { "FileStructure": "BTREE", "PartitioningStrategy": "HASH", "PartitioningKey": [ [ "DataverseName" ], [ "Name" ] ], "PrimaryKey": [ [ "DataverseName" ], [ "Name" ] ], "Autogenerated": false }, "Hints": {{  }}, "Timestamp": "Fri Oct 21 10:29:21 PDT 2016", "DatasetId": 8, "PendingOp": 0, "DatasetFormat": { "Format": "ROW" } }
 { "DataverseName": "Metadata", "DatasetName": "Datatype", "DatatypeDataverseName": "Metadata", "DatatypeName": "DatatypeRecordType", "DatasetType": "INTERNAL", "GroupName": "MetadataGroup", "InternalDetails": { "FileStructure": "BTREE", "PartitioningStrategy": "HASH", "PartitioningKey": [ [ "DataverseName" ], [ "DatatypeName" ] ], "PrimaryKey": [ [ "DataverseName" ], [ "DatatypeName" ] ], "Autogenerated": false }, "Hints": {{  }}, "Timestamp": "Fri Oct 21 10:29:21 PDT 2016", "DatasetId": 3, "PendingOp": 0, "DatasetFormat": { "Format": "ROW" } }
diff --git a/asterixdb/asterix-app/src/test/resources/metadata/results/basic/metadata_datatype/metadata_datatype.1.adm b/asterixdb/asterix-app/src/test/resources/metadata/results/basic/metadata_datatype/metadata_datatype.1.adm
index 1e3618a..f8105eb 100644
--- a/asterixdb/asterix-app/src/test/resources/metadata/results/basic/metadata_datatype/metadata_datatype.1.adm
+++ b/asterixdb/asterix-app/src/test/resources/metadata/results/basic/metadata_datatype/metadata_datatype.1.adm
@@ -3,6 +3,7 @@
 { "DataverseName": "Metadata", "DatatypeName": "CatalogRecordType_CatalogDetails", "Derived": { "Tag": "RECORD", "IsAnonymous": true, "Record": { "IsOpen": true, "Fields": [ { "FieldName": "DatasourceAdapter", "FieldType": "string", "IsNullable": false, "IsMissable": false }, { "FieldName": "Properties", "FieldType": "CatalogRecordType_CatalogDetails_Properties", "IsNullable": false, "IsMissable": false } ] } }, "Timestamp": "Tue Sep 30 09:21:44 PDT 2025" }
 { "DataverseName": "Metadata", "DatatypeName": "CatalogRecordType_CatalogDetails_Properties", "Derived": { "Tag": "ORDEREDLIST", "IsAnonymous": true, "OrderedList": "DatasetRecordType_ExternalDetails_Properties_Item" }, "Timestamp": "Tue Sep 30 09:21:44 PDT 2025" }
 { "DataverseName": "Metadata", "DatatypeName": "CompactionPolicyRecordType", "Derived": { "Tag": "RECORD", "IsAnonymous": false, "Record": { "IsOpen": true, "Fields": [ { "FieldName": "DataverseName", "FieldType": "string", "IsNullable": false, "IsMissable": false }, { "FieldName": "CompactionPolicy", "FieldType": "string", "IsNullable": false, "IsMissable": false }, { "FieldName": "Classname", "FieldType": "string", "IsNullable": false, "IsMissable": false } ] } }, "Timestamp": "Tue Sep 30 09:21:44 PDT 2025" }
+{ "DataverseName": "Metadata", "DatatypeName": "CoordinateReferenceSystemRecordType", "Derived": { "Tag": "RECORD", "IsAnonymous": false, "Record": { "IsOpen": true, "Fields": [ { "FieldName": "DataverseName", "FieldType": "string", "IsNullable": false, "IsMissable": false }, { "FieldName": "SRID", "FieldType": "int32", "IsNullable": false, "IsMissable": false }, { "FieldName": "CRSName", "FieldType": "string", "IsNullable": false, "IsMissable": false }, { "FieldName": "CrsWKT", "FieldType": "string", "IsNullable": false, "IsMissable": false } ] } }, "Timestamp": "Tue Sep 30 09:21:44 PDT 2025" }
 { "DataverseName": "Metadata", "DatatypeName": "DatasetRecordType", "Derived": { "Tag": "RECORD", "IsAnonymous": false, "Record": { "IsOpen": true, "Fields": [ { "FieldName": "DataverseName", "FieldType": "string", "IsNullable": false, "IsMissable": false }, { "FieldName": "DatasetName", "FieldType": "string", "IsNullable": false, "IsMissable": false }, { "FieldName": "DatatypeDataverseName", "FieldType": "string", "IsNullable": false, "IsMissable": false }, { "FieldName": "DatatypeName", "FieldType": "string", "IsNullable": false, "IsMissable": false }, { "FieldName": "DatasetType", "FieldType": "string", "IsNullable": false, "IsMissable": false }, { "FieldName": "GroupName", "FieldType": "string", "IsNullable": false, "IsMissable": false }, { "FieldName": "CompactionPolicy", "FieldType": "string", "IsNullable": false, "IsMissable": false }, { "FieldName": "CompactionPolicyProperties", "FieldType": "DatasetRecordType_CompactionPolicyProperties", "IsNullable": false, "IsMissable": false }, { "FieldName": "InternalDetails", "FieldType": "DatasetRecordType_InternalDetails", "IsNullable": true, "IsMissable": true }, { "FieldName": "ExternalDetails", "FieldType": "DatasetRecordType_ExternalDetails", "IsNullable": true, "IsMissable": true }, { "FieldName": "Hints", "FieldType": "DatasetRecordType_Hints", "IsNullable": false, "IsMissable": false }, { "FieldName": "Timestamp", "FieldType": "string", "IsNullable": false, "IsMissable": false }, { "FieldName": "DatasetId", "FieldType": "int32", "IsNullable": false, "IsMissable": false }, { "FieldName": "PendingOp", "FieldType": "int32", "IsNullable": false, "IsMissable": false } ] } }, "Timestamp": "Tue Sep 30 09:21:44 PDT 2025" }
 { "DataverseName": "Metadata", "DatatypeName": "DatasetRecordType_CompactionPolicyProperties", "Derived": { "Tag": "ORDEREDLIST", "IsAnonymous": true, "OrderedList": "DatasetRecordType_CompactionPolicyProperties_Item" }, "Timestamp": "Tue Sep 30 09:21:44 PDT 2025" }
 { "DataverseName": "Metadata", "DatatypeName": "DatasetRecordType_CompactionPolicyProperties_Item", "Derived": { "Tag": "RECORD", "IsAnonymous": true, "Record": { "IsOpen": true, "Fields": [ { "FieldName": "Name", "FieldType": "string", "IsNullable": false, "IsMissable": false }, { "FieldName": "Value", "FieldType": "string", "IsNullable": false, "IsMissable": false } ] } }, "Timestamp": "Tue Sep 30 09:21:44 PDT 2025" }
diff --git a/asterixdb/asterix-app/src/test/resources/metadata/results/basic/metadata_index/metadata_index.1.adm b/asterixdb/asterix-app/src/test/resources/metadata/results/basic/metadata_index/metadata_index.1.adm
index 5973911..10c6254 100644
--- a/asterixdb/asterix-app/src/test/resources/metadata/results/basic/metadata_index/metadata_index.1.adm
+++ b/asterixdb/asterix-app/src/test/resources/metadata/results/basic/metadata_index/metadata_index.1.adm
@@ -1,5 +1,6 @@
 { "DataverseName": "Metadata", "DatasetName": "Catalog", "IndexName": "Catalog", "IndexStructure": "BTREE", "SearchKey": [ [ "CatalogName" ] ], "IsPrimary": true, "Timestamp": "Sat Aug 23 05:28:47 AST 2025", "PendingOp": 0 }
 { "DataverseName": "Metadata", "DatasetName": "CompactionPolicy", "IndexName": "CompactionPolicy", "IndexStructure": "BTREE", "SearchKey": [ [ "DataverseName" ], [ "CompactionPolicy" ] ], "IsPrimary": true, "Timestamp": "Fri Oct 21 10:29:21 PDT 2016", "PendingOp": 0 }
+{ "DataverseName": "Metadata", "DatasetName": "CoordinateReferenceSystem", "IndexName": "CoordinateReferenceSystem", "IndexStructure": "BTREE", "SearchKey": [ [ "DataverseName" ], [ "SRID" ] ], "IsPrimary": true, "Timestamp": "Fri Oct 21 10:29:21 PDT 2016", "PendingOp": 0 }
 { "DataverseName": "Metadata", "DatasetName": "Dataset", "IndexName": "Dataset", "IndexStructure": "BTREE", "SearchKey": [ [ "DataverseName" ], [ "DatasetName" ] ], "IsPrimary": true, "Timestamp": "Fri Oct 21 10:29:21 PDT 2016", "PendingOp": 0 }
 { "DataverseName": "Metadata", "DatasetName": "DatasourceAdapter", "IndexName": "DatasourceAdapter", "IndexStructure": "BTREE", "SearchKey": [ [ "DataverseName" ], [ "Name" ] ], "IsPrimary": true, "Timestamp": "Fri Oct 21 10:29:21 PDT 2016", "PendingOp": 0 }
 { "DataverseName": "Metadata", "DatasetName": "Datatype", "IndexName": "Datatype", "IndexStructure": "BTREE", "SearchKey": [ [ "DataverseName" ], [ "DatatypeName" ] ], "IsPrimary": true, "Timestamp": "Fri Oct 21 10:29:21 PDT 2016", "PendingOp": 0 }
diff --git a/asterixdb/asterix-app/src/test/resources/metadata/results/basic/metadata_selfjoin/metadata_selfjoin.1.adm b/asterixdb/asterix-app/src/test/resources/metadata/results/basic/metadata_selfjoin/metadata_selfjoin.1.adm
index ef5b440..2390ceb 100644
--- a/asterixdb/asterix-app/src/test/resources/metadata/results/basic/metadata_selfjoin/metadata_selfjoin.1.adm
+++ b/asterixdb/asterix-app/src/test/resources/metadata/results/basic/metadata_selfjoin/metadata_selfjoin.1.adm
@@ -15,4 +15,5 @@
 { "dv1": "Metadata", "dv2": "Metadata" }
 { "dv1": "Metadata", "dv2": "Metadata" }
 { "dv1": "Metadata", "dv2": "Metadata" }
+{ "dv1": "Metadata", "dv2": "Metadata" }
 { "dv1": "Metadata", "dv2": "Metadata" }
\ No newline at end of file
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/GeoJSONQueries.xml b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/GeoJSONQueries.xml
index ce37cfc..6847c6b 100644
--- a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/GeoJSONQueries.xml
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/GeoJSONQueries.xml
@@ -47,4 +47,61 @@
             <output-dir compare="Text">joins</output-dir>
         </compilation-unit>
     </test-case>
+    <test-case FilePath="geojson/crs">
+        <compilation-unit name="functions">
+            <output-dir compare="Text">functions</output-dir>
+        </compilation-unit>
+    </test-case>
+    <test-case FilePath="geojson/crs">
+        <compilation-unit name="negative/crs-invalid-wkt">
+            <output-dir compare="Text">negative</output-dir>
+            <expected-error>Invalid CRS WKT for SRID 9999</expected-error>
+        </compilation-unit>
+    </test-case>
+    <test-case FilePath="geojson/crs">
+        <compilation-unit name="negative/crs-duplicate">
+            <output-dir compare="Text">negative</output-dir>
+            <expected-error>CRS definition already exists for SRID 32632</expected-error>
+        </compilation-unit>
+    </test-case>
+    <test-case FilePath="geojson/crs">
+        <compilation-unit name="negative/crs-drop-not-found">
+            <output-dir compare="Text">negative</output-dir>
+            <expected-error>CRS definition not found for SRID 99998</expected-error>
+        </compilation-unit>
+    </test-case>
+    <test-case FilePath="geojson/crs">
+        <compilation-unit name="negative/crs-transform-not-found">
+            <output-dir compare="Text">negative</output-dir>
+            <expected-error>CRS definition not found for SRID 99999</expected-error>
+        </compilation-unit>
+    </test-case>
+    <test-case FilePath="geojson/crs">
+        <compilation-unit name="negative/crs-transform-srid-zero">
+            <output-dir compare="Text">negative</output-dir>
+            <expected-error>Compilation error: SRID must be a positive integer (1 to 2147483647), got: 0</expected-error>
+        </compilation-unit>
+    </test-case>
+    <test-case FilePath="geojson/crs">
+        <compilation-unit name="negative/crs-transform-srid-too-large">
+            <output-dir compare="Text">negative</output-dir>
+            <expected-error>Compilation error: SRID must be a positive integer (1 to 2147483647), got: 2147483648</expected-error>
+        </compilation-unit>
+    </test-case>
+    <test-case FilePath="geojson/crs">
+        <compilation-unit name="negative/crs-distance-spheroid-srid">
+            <output-dir compare="Text">negative</output-dir>
+        </compilation-unit>
+    </test-case>
+    <test-case FilePath="geojson/crs">
+        <compilation-unit name="dataverse-scoping">
+            <output-dir compare="Text">dataverse-scoping</output-dir>
+        </compilation-unit>
+    </test-case>
+    <test-case FilePath="geojson/crs">
+        <compilation-unit name="negative/crs-create-metadata-namespace">
+            <output-dir compare="Text">negative</output-dir>
+            <expected-error>Compilation error: Invalid operation - Cannot create a coordinate reference system belonging to the dataverse: Metadata</expected-error>
+        </compilation-unit>
+    </test-case>
 </test-group>
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/dataverse-scoping/dataverse-scoping.01.ddl.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/dataverse-scoping/dataverse-scoping.01.ddl.sqlpp
new file mode 100644
index 0000000..be2f55e
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/dataverse-scoping/dataverse-scoping.01.ddl.sqlpp
@@ -0,0 +1,38 @@
+/*
+ * 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.
+ */
+
+DROP DATAVERSE CRSTestDV1 IF EXISTS;
+DROP DATAVERSE CRSTestDV2 IF EXISTS;
+
+CREATE DATAVERSE CRSTestDV1;
+CREATE DATAVERSE CRSTestDV2;
+
+USE CRSTestDV1;
+CREATE COORDINATE REFERENCE SYSTEM 4326
+  NAME 'WGS 84 (DV1)'
+  AS 'GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["degree",0.0174532925199433]]';
+
+USE CRSTestDV2;
+CREATE COORDINATE REFERENCE SYSTEM 4326
+  NAME 'WGS 84 (DV2)'
+  AS 'GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["degree",0.0174532925199433]]';
+
+CREATE COORDINATE REFERENCE SYSTEM 3857
+  NAME 'WGS 84 / Pseudo-Mercator'
+  AS 'PROJCS["WGS 84 / Pseudo-Mercator",GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["degree",0.0174532925199433]],PROJECTION["Mercator_1SP"],PARAMETER["central_meridian",0],PARAMETER["scale_factor",1],PARAMETER["false_easting",0],PARAMETER["false_northing",0],UNIT["metre",1],AXIS["X",EAST],AXIS["Y",NORTH]]';
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/dataverse-scoping/dataverse-scoping.02.ddl.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/dataverse-scoping/dataverse-scoping.02.ddl.sqlpp
new file mode 100644
index 0000000..7f3841f
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/dataverse-scoping/dataverse-scoping.02.ddl.sqlpp
@@ -0,0 +1,22 @@
+/*
+ * 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.
+ */
+
+CREATE COORDINATE REFERENCE SYSTEM 32633
+  NAME 'WGS 84 / UTM zone 33N (Default)'
+  AS 'PROJCS["WGS 84 / UTM zone 33N",GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["degree",0.0174532925199433]],PROJECTION["Transverse_Mercator"],PARAMETER["latitude_of_origin",0],PARAMETER["central_meridian",9],PARAMETER["scale_factor",0.9996],PARAMETER["false_easting",500000],PARAMETER["false_northing",0],UNIT["metre",1]]';
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/dataverse-scoping/dataverse-scoping.03.query.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/dataverse-scoping/dataverse-scoping.03.query.sqlpp
new file mode 100644
index 0000000..f36a75a
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/dataverse-scoping/dataverse-scoping.03.query.sqlpp
@@ -0,0 +1,24 @@
+/*
+ * 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.
+ */
+
+SELECT c.DataverseName, c.SRID, c.CRSName
+FROM Metadata.`CoordinateReferenceSystem` c
+WHERE c.DataverseName IN ["CRSTestDV1", "CRSTestDV2"]
+   OR (c.DataverseName = "Default" AND c.SRID = 32633)
+ORDER BY c.DataverseName, c.SRID;
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/dataverse-scoping/dataverse-scoping.04.ddl.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/dataverse-scoping/dataverse-scoping.04.ddl.sqlpp
new file mode 100644
index 0000000..de236ee
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/dataverse-scoping/dataverse-scoping.04.ddl.sqlpp
@@ -0,0 +1,20 @@
+/*
+ * 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.
+ */
+
+DROP DATAVERSE CRSTestDV1;
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/dataverse-scoping/dataverse-scoping.05.query.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/dataverse-scoping/dataverse-scoping.05.query.sqlpp
new file mode 100644
index 0000000..f36a75a
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/dataverse-scoping/dataverse-scoping.05.query.sqlpp
@@ -0,0 +1,24 @@
+/*
+ * 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.
+ */
+
+SELECT c.DataverseName, c.SRID, c.CRSName
+FROM Metadata.`CoordinateReferenceSystem` c
+WHERE c.DataverseName IN ["CRSTestDV1", "CRSTestDV2"]
+   OR (c.DataverseName = "Default" AND c.SRID = 32633)
+ORDER BY c.DataverseName, c.SRID;
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/dataverse-scoping/dataverse-scoping.06.ddl.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/dataverse-scoping/dataverse-scoping.06.ddl.sqlpp
new file mode 100644
index 0000000..b01c4be
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/dataverse-scoping/dataverse-scoping.06.ddl.sqlpp
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+
+DROP DATAVERSE CRSTestDV2 IF EXISTS;
+DROP COORDINATE REFERENCE SYSTEM 32633 IF EXISTS;
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/functions/crs.01.ddl.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/functions/crs.01.ddl.sqlpp
new file mode 100644
index 0000000..9fc0c5b
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/functions/crs.01.ddl.sqlpp
@@ -0,0 +1,30 @@
+/*
+ * 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.
+ */
+
+drop dataverse GeoJSONCRS if exists;
+create dataverse GeoJSONCRS;
+
+use GeoJSONCRS;
+
+CREATE TYPE CRSGeometryType AS {
+  id : int,
+  myGeometry : geometry
+};
+
+CREATE DATASET CRSGeometries(CRSGeometryType) PRIMARY KEY id;
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/functions/crs.02.ddl.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/functions/crs.02.ddl.sqlpp
new file mode 100644
index 0000000..3f590eb
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/functions/crs.02.ddl.sqlpp
@@ -0,0 +1,28 @@
+/*
+ * 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.
+ */
+
+USE GeoJSONCRS;
+
+CREATE COORDINATE REFERENCE SYSTEM 4326 IF NOT EXISTS
+  NAME 'WGS 84'
+  AS 'GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["degree",0.0174532925199433]]';
+
+CREATE COORDINATE REFERENCE SYSTEM 3857 IF NOT EXISTS
+  NAME 'WGS 84 / Pseudo-Mercator'
+  AS 'PROJCS["WGS 84 / Pseudo-Mercator",GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["degree",0.0174532925199433]],PROJECTION["Mercator_1SP"],PARAMETER["central_meridian",0],PARAMETER["scale_factor",1],PARAMETER["false_easting",0],PARAMETER["false_northing",0],UNIT["metre",1],AXIS["X",EAST],AXIS["Y",NORTH]]';
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/functions/crs.03.update.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/functions/crs.03.update.sqlpp
new file mode 100644
index 0000000..8335a93
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/functions/crs.03.update.sqlpp
@@ -0,0 +1,25 @@
+/*
+ * 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.
+ */
+use GeoJSONCRS;
+
+INSERT INTO CRSGeometries ([
+  {"id":1, "myGeometry": st_geom_from_text("POINT(-73.985428 40.748817)")},
+  {"id":2, "myGeometry": st_geom_from_text("POINT(-0.118092 51.509865)")},
+  {"id":3, "myGeometry": st_geom_from_text("POINT(2.3522 48.8566)")}
+]);
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/functions/crs.04.query.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/functions/crs.04.query.sqlpp
new file mode 100644
index 0000000..2df0518
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/functions/crs.04.query.sqlpp
@@ -0,0 +1,25 @@
+/*
+ * 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.
+ */
+use GeoJSONCRS;
+
+SELECT g.id,
+       round(st_x(st_transform(g.myGeometry, 4326, 3857))) as x_3857,
+       round(st_y(st_transform(g.myGeometry, 4326, 3857))) as y_3857
+FROM CRSGeometries g
+ORDER BY g.id;
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/functions/crs.05.query.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/functions/crs.05.query.sqlpp
new file mode 100644
index 0000000..a13f66d
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/functions/crs.05.query.sqlpp
@@ -0,0 +1,23 @@
+/*
+ * 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.
+ */
+use GeoJSONCRS;
+
+SELECT round(st_distance_spheroid(a.myGeometry, b.myGeometry) / 1000) as dist_km
+FROM CRSGeometries a, CRSGeometries b
+WHERE a.id = 1 AND b.id = 2;
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/functions/crs.06.query.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/functions/crs.06.query.sqlpp
new file mode 100644
index 0000000..be6618d
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/functions/crs.06.query.sqlpp
@@ -0,0 +1,33 @@
+/*
+ * 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.
+ */
+
+-- Exercise the warn-and-null path of ST_Transform's arg-0 type-mismatch
+-- branch. Row 1 feeds a string into arg 0: the evaluator emits a
+-- TYPE_MISMATCH_FUNCTION warning and returns null. Row 2 feeds a valid
+-- geometry: it transforms normally. Both rows must appear in the result
+-- (i.e. the evaluator must not abort the query on the bad row).
+
+USE GeoJSONCRS;
+
+FROM [
+  {"id": 1, "val": "not-a-geometry"},
+  {"id": 2, "val": st_geom_from_text('POINT(0 0)', 4326)}
+] AS r
+SELECT r.id, st_transform(r.val, 4326, 3857) IS NULL AS transformed_is_null
+ORDER BY r.id;
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/functions/crs.08.query.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/functions/crs.08.query.sqlpp
new file mode 100644
index 0000000..87ee630
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/functions/crs.08.query.sqlpp
@@ -0,0 +1,34 @@
+/*
+ * 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.
+ */
+use GeoJSONCRS;
+
+SELECT VALUE {
+  "nyc_london_km": round(st_distance_spheroid(
+    st_geom_from_text('POINT(-73.985428 40.748817)', 4326),
+    st_geom_from_text('POINT(-0.118092 51.509865)', 4326)
+  ) / 1000),
+  "nyc_paris_km": round(st_distance_spheroid(
+    st_geom_from_text('POINT(-73.985428 40.748817)', 4326),
+    st_geom_from_text('POINT(2.3522 48.8566)', 4326)
+  ) / 1000),
+  "same_point": st_distance_spheroid(
+    st_geom_from_text('POINT(-73.985428 40.748817)', 4326),
+    st_geom_from_text('POINT(-73.985428 40.748817)', 4326)
+  )
+};
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/functions/crs.09.query.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/functions/crs.09.query.sqlpp
new file mode 100644
index 0000000..35fb4ed
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/functions/crs.09.query.sqlpp
@@ -0,0 +1,26 @@
+/*
+ * 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.
+ */
+use GeoJSONCRS;
+
+SELECT VALUE {
+  "origin_x": round(st_x(st_transform(st_geom_from_text('POINT(0 0)', 4326), 4326, 3857))),
+  "origin_y": round(st_y(st_transform(st_geom_from_text('POINT(0 0)', 4326), 4326, 3857))),
+  "london_x": round(st_x(st_transform(st_geom_from_text('POINT(-0.118092 51.509865)', 4326), 4326, 3857))),
+  "london_y": round(st_y(st_transform(st_geom_from_text('POINT(-0.118092 51.509865)', 4326), 4326, 3857)))
+};
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/functions/crs.10.query.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/functions/crs.10.query.sqlpp
new file mode 100644
index 0000000..e74b41f
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/functions/crs.10.query.sqlpp
@@ -0,0 +1,28 @@
+/*
+ * 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.
+ */
+use GeoJSONCRS;
+
+SELECT VALUE {
+  "roundtrip_lon": round(st_x(st_transform(st_transform(
+    st_geom_from_text('POINT(-0.118092 51.509865)', 4326), 4326, 3857), 3857, 4326)) * 1000000) / 1000000,
+  "roundtrip_lat": round(st_y(st_transform(st_transform(
+    st_geom_from_text('POINT(-0.118092 51.509865)', 4326), 4326, 3857), 3857, 4326)) * 1000000) / 1000000,
+  "original_lon": -0.118092,
+  "original_lat": 51.509865
+};
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/functions/crs.11.ddl.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/functions/crs.11.ddl.sqlpp
new file mode 100644
index 0000000..5e8355a
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/functions/crs.11.ddl.sqlpp
@@ -0,0 +1,23 @@
+/*
+ * 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.
+ */
+
+USE GeoJSONCRS;
+
+DROP COORDINATE REFERENCE SYSTEM 4326 IF EXISTS;
+DROP COORDINATE REFERENCE SYSTEM 3857 IF EXISTS;
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/functions/crs.99.ddl.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/functions/crs.99.ddl.sqlpp
new file mode 100644
index 0000000..8001062
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/functions/crs.99.ddl.sqlpp
@@ -0,0 +1,20 @@
+/*
+ * 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.
+ */
+
+drop dataverse GeoJSONCRS if exists;
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/negative/crs-create-metadata-namespace/crs-create-metadata-namespace.01.ddl.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/negative/crs-create-metadata-namespace/crs-create-metadata-namespace.01.ddl.sqlpp
new file mode 100644
index 0000000..61ed6af
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/negative/crs-create-metadata-namespace/crs-create-metadata-namespace.01.ddl.sqlpp
@@ -0,0 +1,24 @@
+/*
+ * 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.
+ */
+
+USE Metadata;
+
+CREATE COORDINATE REFERENCE SYSTEM 4326
+  NAME 'WGS 84'
+  AS 'GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["degree",0.0174532925199433]]';
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/negative/crs-distance-spheroid-srid/crs-distance-spheroid-srid.01.query.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/negative/crs-distance-spheroid-srid/crs-distance-spheroid-srid.01.query.sqlpp
new file mode 100644
index 0000000..a771af6
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/negative/crs-distance-spheroid-srid/crs-distance-spheroid-srid.01.query.sqlpp
@@ -0,0 +1,24 @@
+/*
+ * 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.
+ */
+
+-- Warning test: ST_DISTANCE_SPHEROID warns on non-4326 SRID but still returns a distance
+SELECT round(st_distance_spheroid(
+  st_geom_from_text('POINT(0 0)', 3857),
+  st_geom_from_text('POINT(1 0)', 4326)
+));
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/negative/crs-drop-not-found/crs-drop-not-found.01.ddl.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/negative/crs-drop-not-found/crs-drop-not-found.01.ddl.sqlpp
new file mode 100644
index 0000000..ee8f07c
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/negative/crs-drop-not-found/crs-drop-not-found.01.ddl.sqlpp
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+
+-- Negative test: dropping non-existent CRS without IF EXISTS should fail
+DROP COORDINATE REFERENCE SYSTEM 99998;
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/negative/crs-duplicate/crs-duplicate.01.ddl.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/negative/crs-duplicate/crs-duplicate.01.ddl.sqlpp
new file mode 100644
index 0000000..846a4e7
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/negative/crs-duplicate/crs-duplicate.01.ddl.sqlpp
@@ -0,0 +1,29 @@
+/*
+ * 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.
+ */
+
+-- Negative test: creating duplicate CRS without IF NOT EXISTS should fail
+DROP COORDINATE REFERENCE SYSTEM 32632 IF EXISTS;
+
+CREATE COORDINATE REFERENCE SYSTEM 32632
+  NAME 'WGS 84 / UTM zone 32N'
+  AS 'PROJCS["WGS 84 / UTM zone 32N",GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["degree",0.0174532925199433]],PROJECTION["Transverse_Mercator"],PARAMETER["latitude_of_origin",0],PARAMETER["central_meridian",9],PARAMETER["scale_factor",0.9996],PARAMETER["false_easting",500000],PARAMETER["false_northing",0],UNIT["metre",1],AXIS["Easting",EAST],AXIS["Northing",NORTH]]';
+
+CREATE COORDINATE REFERENCE SYSTEM 32632
+  NAME 'WGS 84 / UTM zone 32N duplicate'
+  AS 'PROJCS["WGS 84 / UTM zone 32N",GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["degree",0.0174532925199433]],PROJECTION["Transverse_Mercator"],PARAMETER["latitude_of_origin",0],PARAMETER["central_meridian",9],PARAMETER["scale_factor",0.9996],PARAMETER["false_easting",500000],PARAMETER["false_northing",0],UNIT["metre",1],AXIS["Easting",EAST],AXIS["Northing",NORTH]]';
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/negative/crs-invalid-wkt/crs-invalid-wkt.01.ddl.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/negative/crs-invalid-wkt/crs-invalid-wkt.01.ddl.sqlpp
new file mode 100644
index 0000000..25b7359
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/negative/crs-invalid-wkt/crs-invalid-wkt.01.ddl.sqlpp
@@ -0,0 +1,23 @@
+/*
+ * 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.
+ */
+
+-- Negative test: CREATE CRS with invalid WKT should fail with INVALID_CRS_WKT
+CREATE COORDINATE REFERENCE SYSTEM 9999
+  NAME 'Invalid CRS'
+  AS 'not valid wkt at all';
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/negative/crs-transform-not-found/crs-transform-not-found.00.ddl.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/negative/crs-transform-not-found/crs-transform-not-found.00.ddl.sqlpp
new file mode 100644
index 0000000..49f3650
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/negative/crs-transform-not-found/crs-transform-not-found.00.ddl.sqlpp
@@ -0,0 +1,24 @@
+/*
+ * 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.
+ */
+
+-- Setup: create CRS 4326 so the fromSRID lookup succeeds
+-- The error should trigger on the toSRID (99999) which does not exist
+CREATE COORDINATE REFERENCE SYSTEM 4326 IF NOT EXISTS
+  NAME 'WGS 84'
+  AS 'GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4326"]]';
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/negative/crs-transform-not-found/crs-transform-not-found.01.query.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/negative/crs-transform-not-found/crs-transform-not-found.01.query.sqlpp
new file mode 100644
index 0000000..2f508c9
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/negative/crs-transform-not-found/crs-transform-not-found.01.query.sqlpp
@@ -0,0 +1,23 @@
+/*
+ * 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.
+ */
+
+-- Negative test: st_transform with non-existent target CRS should fail at compile time
+-- CRS 4326 exists (created in step 00), but SRID 99999 is not registered
+-- Type inference should throw CRS_NOT_FOUND for SRID 99999
+SELECT st_transform(st_geom_from_text('POINT(0 0)', 4326), 4326, 99999);
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/negative/crs-transform-not-found/crs-transform-not-found.99.ddl.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/negative/crs-transform-not-found/crs-transform-not-found.99.ddl.sqlpp
new file mode 100644
index 0000000..2ec699e
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/negative/crs-transform-not-found/crs-transform-not-found.99.ddl.sqlpp
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+
+-- Cleanup
+DROP COORDINATE REFERENCE SYSTEM 4326 IF EXISTS;
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/negative/crs-transform-srid-too-large/crs-transform-srid-too-large.01.query.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/negative/crs-transform-srid-too-large/crs-transform-srid-too-large.01.query.sqlpp
new file mode 100644
index 0000000..39d0395
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/negative/crs-transform-srid-too-large/crs-transform-srid-too-large.01.query.sqlpp
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+
+-- Negative test: ST_Transform rejects SRID values above Integer.MAX_VALUE
+SELECT st_transform(st_geom_from_text('POINT(0 0)', 4326), 4326, 2147483648);
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/negative/crs-transform-srid-zero/crs-transform-srid-zero.01.query.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/negative/crs-transform-srid-zero/crs-transform-srid-zero.01.query.sqlpp
new file mode 100644
index 0000000..a979b87
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/geojson/crs/negative/crs-transform-srid-zero/crs-transform-srid-zero.01.query.sqlpp
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+
+-- Negative test: ST_Transform rejects SRID 0
+SELECT st_transform(st_geom_from_text('POINT(0 0)', 4326), 0, 3857);
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/results/geojson/crs/dataverse-scoping/dataverse-scoping.03.adm b/asterixdb/asterix-app/src/test/resources/runtimets/results/geojson/crs/dataverse-scoping/dataverse-scoping.03.adm
new file mode 100644
index 0000000..b25227e
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/results/geojson/crs/dataverse-scoping/dataverse-scoping.03.adm
@@ -0,0 +1,4 @@
+{ "DataverseName": "CRSTestDV1", "SRID": 4326, "CRSName": "WGS 84 (DV1)" }
+{ "DataverseName": "CRSTestDV2", "SRID": 3857, "CRSName": "WGS 84 / Pseudo-Mercator" }
+{ "DataverseName": "CRSTestDV2", "SRID": 4326, "CRSName": "WGS 84 (DV2)" }
+{ "DataverseName": "Default", "SRID": 32633, "CRSName": "WGS 84 / UTM zone 33N (Default)" }
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/results/geojson/crs/dataverse-scoping/dataverse-scoping.05.adm b/asterixdb/asterix-app/src/test/resources/runtimets/results/geojson/crs/dataverse-scoping/dataverse-scoping.05.adm
new file mode 100644
index 0000000..c48626c
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/results/geojson/crs/dataverse-scoping/dataverse-scoping.05.adm
@@ -0,0 +1,3 @@
+{ "DataverseName": "CRSTestDV2", "SRID": 3857, "CRSName": "WGS 84 / Pseudo-Mercator" }
+{ "DataverseName": "CRSTestDV2", "SRID": 4326, "CRSName": "WGS 84 (DV2)" }
+{ "DataverseName": "Default", "SRID": 32633, "CRSName": "WGS 84 / UTM zone 33N (Default)" }
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/results/geojson/crs/functions/result.04.adm b/asterixdb/asterix-app/src/test/resources/runtimets/results/geojson/crs/functions/result.04.adm
new file mode 100644
index 0000000..2bf6910
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/results/geojson/crs/functions/result.04.adm
@@ -0,0 +1,3 @@
+{ "id": 1, "x_3857": -8236020.0, "y_3857": 4947465.0 }
+{ "id": 2, "x_3857": -13146.0, "y_3857": 6678517.0 }
+{ "id": 3, "x_3857": 261846.0, "y_3857": 6218369.0 }
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/results/geojson/crs/functions/result.05.adm b/asterixdb/asterix-app/src/test/resources/runtimets/results/geojson/crs/functions/result.05.adm
new file mode 100644
index 0000000..3b96634
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/results/geojson/crs/functions/result.05.adm
@@ -0,0 +1 @@
+{ "dist_km": 5582.0 }
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/results/geojson/crs/functions/result.06.adm b/asterixdb/asterix-app/src/test/resources/runtimets/results/geojson/crs/functions/result.06.adm
new file mode 100644
index 0000000..869f6b1
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/results/geojson/crs/functions/result.06.adm
@@ -0,0 +1,2 @@
+{ "transformed_is_null": true, "id": 1 }
+{ "transformed_is_null": false, "id": 2 }
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/results/geojson/crs/functions/result.08.adm b/asterixdb/asterix-app/src/test/resources/runtimets/results/geojson/crs/functions/result.08.adm
new file mode 100644
index 0000000..60cb021
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/results/geojson/crs/functions/result.08.adm
@@ -0,0 +1 @@
+{ "nyc_london_km": 5582.0, "nyc_paris_km": 5849.0, "same_point": 0.0 }
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/results/geojson/crs/functions/result.09.adm b/asterixdb/asterix-app/src/test/resources/runtimets/results/geojson/crs/functions/result.09.adm
new file mode 100644
index 0000000..40f40b0
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/results/geojson/crs/functions/result.09.adm
@@ -0,0 +1 @@
+{ "origin_x": 0.0, "origin_y": 0.0, "london_x": -13146.0, "london_y": 6678517.0 }
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/results/geojson/crs/functions/result.10.adm b/asterixdb/asterix-app/src/test/resources/runtimets/results/geojson/crs/functions/result.10.adm
new file mode 100644
index 0000000..a5c04ef
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/results/geojson/crs/functions/result.10.adm
@@ -0,0 +1 @@
+{ "roundtrip_lon": -0.118092, "roundtrip_lat": 51.509865, "original_lon": -0.118092, "original_lat": 51.509865 }
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/results/geojson/crs/negative-fractional/crs-fractional-srid.01.adm b/asterixdb/asterix-app/src/test/resources/runtimets/results/geojson/crs/negative-fractional/crs-fractional-srid.01.adm
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/results/geojson/crs/negative-fractional/crs-fractional-srid.01.adm
@@ -0,0 +1 @@
+
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/results/geojson/crs/negative-srid-fromtext/crs-negative-srid-fromtext.01.adm b/asterixdb/asterix-app/src/test/resources/runtimets/results/geojson/crs/negative-srid-fromtext/crs-negative-srid-fromtext.01.adm
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/results/geojson/crs/negative-srid-fromtext/crs-negative-srid-fromtext.01.adm
@@ -0,0 +1 @@
+
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/results/geojson/crs/negative-srid/crs-negative-srid.01.adm b/asterixdb/asterix-app/src/test/resources/runtimets/results/geojson/crs/negative-srid/crs-negative-srid.01.adm
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/results/geojson/crs/negative-srid/crs-negative-srid.01.adm
@@ -0,0 +1 @@
+
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/results/geojson/crs/negative/crs-distance-spheroid-srid.01.adm b/asterixdb/asterix-app/src/test/resources/runtimets/results/geojson/crs/negative/crs-distance-spheroid-srid.01.adm
new file mode 100644
index 0000000..a33fd13
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/results/geojson/crs/negative/crs-distance-spheroid-srid.01.adm
@@ -0,0 +1 @@
+{ "$1": 111319.0 }
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/results/user-defined-functions/udf23/udf23.1.adm b/asterixdb/asterix-app/src/test/resources/runtimets/results/user-defined-functions/udf23/udf23.1.adm
index 8798c6d..58ee222 100644
--- a/asterixdb/asterix-app/src/test/resources/runtimets/results/user-defined-functions/udf23/udf23.1.adm
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/results/user-defined-functions/udf23/udf23.1.adm
@@ -1,7 +1,7 @@
 { "DataverseName": "Metadata", "DatasetName": "Catalog", "DatatypeDataverseName": "Metadata", "DatatypeName": "CatalogRecordType", "DatasetType": "INTERNAL", "GroupName": "MetadataGroup", "InternalDetails": { "FileStructure": "BTREE", "PartitioningStrategy": "HASH", "PartitioningKey": [ [ "CatalogName" ] ], "PrimaryKey": [ [ "CatalogName" ] ], "Autogenerated": false }, "Hints": {{  }}, "Timestamp": "Sat Aug 23 05:37:12 AST 2025", "DatasetId": 19, "PendingOp": 0, "DatasetFormat": { "Format": "ROW" } }
 { "DataverseName": "Metadata", "DatasetName": "CompactionPolicy", "DatatypeDataverseName": "Metadata", "DatatypeName": "CompactionPolicyRecordType", "DatasetType": "INTERNAL", "GroupName": "MetadataGroup", "InternalDetails": { "FileStructure": "BTREE", "PartitioningStrategy": "HASH", "PartitioningKey": [ [ "DataverseName" ], [ "CompactionPolicy" ] ], "PrimaryKey": [ [ "DataverseName" ], [ "CompactionPolicy" ] ], "Autogenerated": false }, "Hints": {{  }}, "Timestamp": "Fri May 19 12:41:05 PDT 2023", "DatasetId": 13, "PendingOp": 0, "DatasetFormat": { "Format": "ROW" } }
+{ "DataverseName": "Metadata", "DatasetName": "CoordinateReferenceSystem", "DatatypeDataverseName": "Metadata", "DatatypeName": "CoordinateReferenceSystemRecordType", "DatasetType": "INTERNAL", "GroupName": "MetadataGroup", "InternalDetails": { "FileStructure": "BTREE", "PartitioningStrategy": "HASH", "PartitioningKey": [ [ "DataverseName" ], [ "SRID" ] ], "PrimaryKey": [ [ "DataverseName" ], [ "SRID" ] ], "Autogenerated": false }, "Hints": {{  }}, "Timestamp": "Fri Mar 06 21:38:36 UTC 2026", "DatasetId": 20, "PendingOp": 0, "DatasetFormat": { "Format": "ROW" } }
 { "DataverseName": "Metadata", "DatasetName": "Dataset", "DatatypeDataverseName": "Metadata", "DatatypeName": "DatasetRecordType", "DatasetType": "INTERNAL", "GroupName": "MetadataGroup", "InternalDetails": { "FileStructure": "BTREE", "PartitioningStrategy": "HASH", "PartitioningKey": [ [ "DataverseName" ], [ "DatasetName" ] ], "PrimaryKey": [ [ "DataverseName" ], [ "DatasetName" ] ], "Autogenerated": false }, "Hints": {{  }}, "Timestamp": "Fri May 19 12:41:05 PDT 2023", "DatasetId": 2, "PendingOp": 0, "DatasetFormat": { "Format": "ROW" } }
 { "DataverseName": "Metadata", "DatasetName": "DatasourceAdapter", "DatatypeDataverseName": "Metadata", "DatatypeName": "DatasourceAdapterRecordType", "DatasetType": "INTERNAL", "GroupName": "MetadataGroup", "InternalDetails": { "FileStructure": "BTREE", "PartitioningStrategy": "HASH", "PartitioningKey": [ [ "DataverseName" ], [ "Name" ] ], "PrimaryKey": [ [ "DataverseName" ], [ "Name" ] ], "Autogenerated": false }, "Hints": {{  }}, "Timestamp": "Fri May 19 12:41:05 PDT 2023", "DatasetId": 8, "PendingOp": 0, "DatasetFormat": { "Format": "ROW" } }
 { "DataverseName": "Metadata", "DatasetName": "Datatype", "DatatypeDataverseName": "Metadata", "DatatypeName": "DatatypeRecordType", "DatasetType": "INTERNAL", "GroupName": "MetadataGroup", "InternalDetails": { "FileStructure": "BTREE", "PartitioningStrategy": "HASH", "PartitioningKey": [ [ "DataverseName" ], [ "DatatypeName" ] ], "PrimaryKey": [ [ "DataverseName" ], [ "DatatypeName" ] ], "Autogenerated": false }, "Hints": {{  }}, "Timestamp": "Fri May 19 12:41:05 PDT 2023", "DatasetId": 3, "PendingOp": 0, "DatasetFormat": { "Format": "ROW" } }
 { "DataverseName": "Metadata", "DatasetName": "Dataverse", "DatatypeDataverseName": "Metadata", "DatatypeName": "DataverseRecordType", "DatasetType": "INTERNAL", "GroupName": "MetadataGroup", "InternalDetails": { "FileStructure": "BTREE", "PartitioningStrategy": "HASH", "PartitioningKey": [ [ "DataverseName" ] ], "PrimaryKey": [ [ "DataverseName" ] ], "Autogenerated": false }, "Hints": {{  }}, "Timestamp": "Fri May 19 12:41:05 PDT 2023", "DatasetId": 1, "PendingOp": 0, "DatasetFormat": { "Format": "ROW" } }
-{ "DataverseName": "Metadata", "DatasetName": "ExternalFile", "DatatypeDataverseName": "Metadata", "DatatypeName": "ExternalFileRecordType", "DatasetType": "INTERNAL", "GroupName": "MetadataGroup", "InternalDetails": { "FileStructure": "BTREE", "PartitioningStrategy": "HASH", "PartitioningKey": [ [ "DataverseName" ], [ "DatasetName" ], [ "FileNumber" ] ], "PrimaryKey": [ [ "DataverseName" ], [ "DatasetName" ], [ "FileNumber" ] ], "Autogenerated": false }, "Hints": {{  }}, "Timestamp": "Fri May 19 12:41:05 PDT 2023", "DatasetId": 14, "PendingOp": 0, "DatasetFormat": { "Format": "ROW" } }
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/results_cloud/user-defined-functions/udf23/udf23.1.adm b/asterixdb/asterix-app/src/test/resources/runtimets/results_cloud/user-defined-functions/udf23/udf23.1.adm
index d2f3fcb..d430940 100644
--- a/asterixdb/asterix-app/src/test/resources/runtimets/results_cloud/user-defined-functions/udf23/udf23.1.adm
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/results_cloud/user-defined-functions/udf23/udf23.1.adm
@@ -1,7 +1,7 @@
 { "DatabaseName": "System", "DataverseName": "Metadata", "DatasetName": "Catalog", "DatatypeDataverseName": "Metadata", "DatatypeName": "CatalogRecordType", "DatasetType": "INTERNAL", "GroupName": "MetadataGroup", "InternalDetails": { "FileStructure": "BTREE", "PartitioningStrategy": "HASH", "PartitioningKey": [ [ "CatalogName" ] ], "PrimaryKey": [ [ "CatalogName" ] ], "Autogenerated": false }, "Hints": {{  }}, "Timestamp": "Sat Aug 23 05:47:34 UTC 2025", "DatasetId": 19, "PendingOp": 0, "DatatypeDatabaseName": "System", "DatasetFormat": { "Format": "ROW" } }
 { "DatabaseName": "System", "DataverseName": "Metadata", "DatasetName": "CompactionPolicy", "DatatypeDataverseName": "Metadata", "DatatypeName": "CompactionPolicyRecordType", "DatasetType": "INTERNAL", "GroupName": "MetadataGroup", "InternalDetails": { "FileStructure": "BTREE", "PartitioningStrategy": "HASH", "PartitioningKey": [ [ "DatabaseName" ], [ "DataverseName" ], [ "CompactionPolicy" ] ], "PrimaryKey": [ [ "DatabaseName" ], [ "DataverseName" ], [ "CompactionPolicy" ] ], "Autogenerated": false }, "Hints": {{  }}, "Timestamp": "Tue Oct 03 12:04:25 PDT 2023", "DatasetId": 13, "PendingOp": 0, "DatatypeDatabaseName": "System", "DatasetFormat": { "Format": "ROW" } }
+{ "DatabaseName": "System", "DataverseName": "Metadata", "DatasetName": "CoordinateReferenceSystem", "DatatypeDataverseName": "Metadata", "DatatypeName": "CoordinateReferenceSystemRecordType", "DatasetType": "INTERNAL", "GroupName": "MetadataGroup", "InternalDetails": { "FileStructure": "BTREE", "PartitioningStrategy": "HASH", "PartitioningKey": [ [ "DatabaseName" ], [ "DataverseName" ], [ "SRID" ] ], "PrimaryKey": [ [ "DatabaseName" ], [ "DataverseName" ], [ "SRID" ] ], "Autogenerated": false }, "Hints": {{  }}, "Timestamp": "Tue Oct 03 12:04:25 PDT 2023", "DatasetId": 20, "PendingOp": 0, "DatatypeDatabaseName": "System", "DatasetFormat": { "Format": "ROW" } }
 { "DatabaseName": "System", "DataverseName": "Metadata", "DatasetName": "Database", "DatatypeDataverseName": "Metadata", "DatatypeName": "DatabaseRecordType", "DatasetType": "INTERNAL", "GroupName": "MetadataGroup", "InternalDetails": { "FileStructure": "BTREE", "PartitioningStrategy": "HASH", "PartitioningKey": [ [ "DatabaseName" ] ], "PrimaryKey": [ [ "DatabaseName" ] ], "Autogenerated": false }, "Hints": {{  }}, "Timestamp": "Tue Oct 03 12:04:25 PDT 2023", "DatasetId": 18, "PendingOp": 0, "DatatypeDatabaseName": "System", "DatasetFormat": { "Format": "ROW" } }
 { "DatabaseName": "System", "DataverseName": "Metadata", "DatasetName": "Dataset", "DatatypeDataverseName": "Metadata", "DatatypeName": "DatasetRecordType", "DatasetType": "INTERNAL", "GroupName": "MetadataGroup", "InternalDetails": { "FileStructure": "BTREE", "PartitioningStrategy": "HASH", "PartitioningKey": [ [ "DatabaseName" ], [ "DataverseName" ], [ "DatasetName" ] ], "PrimaryKey": [ [ "DatabaseName" ], [ "DataverseName" ], [ "DatasetName" ] ], "Autogenerated": false }, "Hints": {{  }}, "Timestamp": "Tue Oct 03 12:04:25 PDT 2023", "DatasetId": 2, "PendingOp": 0, "DatatypeDatabaseName": "System", "DatasetFormat": { "Format": "ROW" } }
 { "DatabaseName": "System", "DataverseName": "Metadata", "DatasetName": "DatasourceAdapter", "DatatypeDataverseName": "Metadata", "DatatypeName": "DatasourceAdapterRecordType", "DatasetType": "INTERNAL", "GroupName": "MetadataGroup", "InternalDetails": { "FileStructure": "BTREE", "PartitioningStrategy": "HASH", "PartitioningKey": [ [ "DatabaseName" ], [ "DataverseName" ], [ "Name" ] ], "PrimaryKey": [ [ "DatabaseName" ], [ "DataverseName" ], [ "Name" ] ], "Autogenerated": false }, "Hints": {{  }}, "Timestamp": "Tue Oct 03 12:04:25 PDT 2023", "DatasetId": 8, "PendingOp": 0, "DatatypeDatabaseName": "System", "DatasetFormat": { "Format": "ROW" } }
 { "DatabaseName": "System", "DataverseName": "Metadata", "DatasetName": "Datatype", "DatatypeDataverseName": "Metadata", "DatatypeName": "DatatypeRecordType", "DatasetType": "INTERNAL", "GroupName": "MetadataGroup", "InternalDetails": { "FileStructure": "BTREE", "PartitioningStrategy": "HASH", "PartitioningKey": [ [ "DatabaseName" ], [ "DataverseName" ], [ "DatatypeName" ] ], "PrimaryKey": [ [ "DatabaseName" ], [ "DataverseName" ], [ "DatatypeName" ] ], "Autogenerated": false }, "Hints": {{  }}, "Timestamp": "Tue Oct 03 12:04:25 PDT 2023", "DatasetId": 3, "PendingOp": 0, "DatatypeDatabaseName": "System", "DatasetFormat": { "Format": "ROW" } }
-{ "DatabaseName": "System", "DataverseName": "Metadata", "DatasetName": "Dataverse", "DatatypeDataverseName": "Metadata", "DatatypeName": "DataverseRecordType", "DatasetType": "INTERNAL", "GroupName": "MetadataGroup", "InternalDetails": { "FileStructure": "BTREE", "PartitioningStrategy": "HASH", "PartitioningKey": [ [ "DatabaseName" ], [ "DataverseName" ] ], "PrimaryKey": [ [ "DatabaseName" ], [ "DataverseName" ] ], "Autogenerated": false }, "Hints": {{  }}, "Timestamp": "Tue Oct 03 12:04:25 PDT 2023", "DatasetId": 1, "PendingOp": 0, "DatatypeDatabaseName": "System", "DatasetFormat": { "Format": "ROW" } }
diff --git a/asterixdb/asterix-common/src/main/java/org/apache/asterix/common/api/IMetadataLockManager.java b/asterixdb/asterix-common/src/main/java/org/apache/asterix/common/api/IMetadataLockManager.java
index 8b09580..4b2d77c 100644
--- a/asterixdb/asterix-common/src/main/java/org/apache/asterix/common/api/IMetadataLockManager.java
+++ b/asterixdb/asterix-common/src/main/java/org/apache/asterix/common/api/IMetadataLockManager.java
@@ -573,4 +573,7 @@
     void acquireCatalogReadLock(LockList locks, String catalogName) throws AlgebricksException;
 
     void acquireCatalogWriteLock(LockList locks, String catalogName) throws AlgebricksException;
+
+    void acquireCRSWriteLock(LockList locks, String database, DataverseName dataverseName, int srid)
+            throws AlgebricksException;
 }
diff --git a/asterixdb/asterix-common/src/main/java/org/apache/asterix/common/exceptions/ErrorCode.java b/asterixdb/asterix-common/src/main/java/org/apache/asterix/common/exceptions/ErrorCode.java
index 9555fd3..5942bfe 100644
--- a/asterixdb/asterix-common/src/main/java/org/apache/asterix/common/exceptions/ErrorCode.java
+++ b/asterixdb/asterix-common/src/main/java/org/apache/asterix/common/exceptions/ErrorCode.java
@@ -351,6 +351,12 @@
     NOT_ICEBERG_CATALOG(1243),
     UPDATE_PRIMARY_KEY(1244),
     PARQUET_WRITER_ERROR(1245),
+    SRID_MISMATCH(1246),
+    CRS_NOT_FOUND(1247),
+    CRS_ALREADY_EXISTS(1248),
+    CRS_TRANSFORM_FAILED(1249),
+    INVALID_CRS_WKT(1250),
+    ST_DISTANCE_SPHEROID_REQUIRES_4326(1251),
 
     // Feed errors
     DATAFLOW_ILLEGAL_STATE(3001),
diff --git a/asterixdb/asterix-common/src/main/java/org/apache/asterix/common/metadata/IMetadataLockUtil.java b/asterixdb/asterix-common/src/main/java/org/apache/asterix/common/metadata/IMetadataLockUtil.java
index 07a1442..2064e78 100644
--- a/asterixdb/asterix-common/src/main/java/org/apache/asterix/common/metadata/IMetadataLockUtil.java
+++ b/asterixdb/asterix-common/src/main/java/org/apache/asterix/common/metadata/IMetadataLockUtil.java
@@ -177,4 +177,11 @@
 
     void dropCatalogBegin(IMetadataLockManager lockManager, LockList locks, String catalogName)
             throws AlgebricksException;
+
+    // CRS helpers
+    void createCRSBegin(IMetadataLockManager lockManager, LockList locks, String database, DataverseName dataverseName,
+            int srid) throws AlgebricksException;
+
+    void dropCRSBegin(IMetadataLockManager lockManager, LockList locks, String database, DataverseName dataverseName,
+            int srid) throws AlgebricksException;
 }
diff --git a/asterixdb/asterix-common/src/main/java/org/apache/asterix/common/metadata/MetadataConstants.java b/asterixdb/asterix-common/src/main/java/org/apache/asterix/common/metadata/MetadataConstants.java
index 9079d88..041d224 100644
--- a/asterixdb/asterix-common/src/main/java/org/apache/asterix/common/metadata/MetadataConstants.java
+++ b/asterixdb/asterix-common/src/main/java/org/apache/asterix/common/metadata/MetadataConstants.java
@@ -73,6 +73,7 @@
     public static final String FULL_TEXT_CONFIG_DATASET_NAME = "FullTextConfig";
     public static final String FULL_TEXT_FILTER_DATASET_NAME = "FullTextFilter";
     public static final String CATALOG_DATASET_NAME = "Catalog";
+    public static final String CRS_DATASET_NAME = "CoordinateReferenceSystem";
 
     public static final String PRIMARY_INDEX_PREFIX = "primary_idx_";
     public static final String SAMPLE_INDEX_PREFIX = "sample_idx_";
diff --git a/asterixdb/asterix-common/src/main/resources/asx_errormsg/en.properties b/asterixdb/asterix-common/src/main/resources/asx_errormsg/en.properties
index d7dcff3..1034864 100644
--- a/asterixdb/asterix-common/src/main/resources/asx_errormsg/en.properties
+++ b/asterixdb/asterix-common/src/main/resources/asx_errormsg/en.properties
@@ -353,6 +353,12 @@
 1243 = Catalog '%1$s' is not an Iceberg catalog
 1244 = Cannot update primary key '%1$s'
 1245 = Parquet writer error: %1$s
+1246 = SRID mismatch: %1$s vs %2$s in spatial operation %3$s
+1247 = CRS definition not found for SRID %1$s
+1248 = CRS definition already exists for SRID %1$s
+1249 = Failed to transform geometry from SRID %1$s to %2$s: %3$s
+1250 = Invalid CRS WKT for SRID %1$s: %2$s
+1251 = ST_DISTANCE_SPHEROID requires SRID 4326 (WGS84). Found SRID %1$s and %2$s
 
 # Feed Errors
 3001 = Illegal state.
diff --git a/asterixdb/asterix-doc/src/main/markdown/sqlpp/7_ddl_dml.md b/asterixdb/asterix-doc/src/main/markdown/sqlpp/7_ddl_dml.md
index 71e6af5..4f7174c 100644
--- a/asterixdb/asterix-doc/src/main/markdown/sqlpp/7_ddl_dml.md
+++ b/asterixdb/asterix-doc/src/main/markdown/sqlpp/7_ddl_dml.md
@@ -623,6 +623,21 @@
      LOAD DATASET customers USING localfs
         (("path"="127.0.0.1:///Users/bignosqlfan/commercenew/gbu.adm"),("format"="adm"));
 
+### <a id="CRS_statements">Coordinate Reference System (CRS) Statements</a>
+
+CRS statements manage CRS definitions used by geospatial functions such as `st_transform`.
+CRS metadata is stored in `Metadata.\`CoordinateReferenceSystem\`` with the fields `SRID`, `CRSName`, and `CrsWKT`.
+
+Create a CRS definition:
+
+    CREATE COORDINATE REFERENCE SYSTEM 4326 IF NOT EXISTS
+      NAME 'WGS 84'
+      AS 'GEOGCS["WGS 84", ... ]';
+
+Drop a CRS definition:
+
+    DROP COORDINATE REFERENCE SYSTEM 4326 IF EXISTS;
+
 ## <a id="Modification_statements">Modification statements</a>
 
 ### <a id="Inserts">Insert Statement</a>
diff --git a/asterixdb/asterix-doc/src/site/markdown/geo/functions.md b/asterixdb/asterix-doc/src/site/markdown/geo/functions.md
index e91568f..f6d605c 100644
--- a/asterixdb/asterix-doc/src/site/markdown/geo/functions.md
+++ b/asterixdb/asterix-doc/src/site/markdown/geo/functions.md
@@ -344,6 +344,34 @@
 
         111195.0662708989
 
+### st_distance_spheroid ###
+* Return the minimum geodesic distance in meters between two geometries using the WGS84 ellipsoid.
+* If an input geometry has a known SRID that is not `4326`, the function emits a warning and still computes distance using WGS84.
+
+* Example:
+  * Command:
+
+        st_distance_spheroid(st_geom_from_text('POINT(-73.985428 40.748817)', 4326), st_geom_from_text('POINT(-0.118092 51.509865)', 4326));
+  * Result:
+
+        5581421.87552557
+
+### st_transform ###
+* Transform geometry coordinates from a source SRID to a target SRID.
+* Syntax: `st_transform(geometry, source_srid, target_srid)`
+* `source_srid` and `target_srid` must be integer constants.
+* CRS definitions are resolved from metadata. Register them with:
+  * `CREATE COORDINATE REFERENCE SYSTEM ...`
+  * `LOAD COORDINATE REFERENCE SYSTEM FROM PATH ...`
+
+* Example:
+  * Command:
+
+        round(st_x(st_transform(st_geom_from_text('POINT(-0.118092 51.509865)', 4326), 4326, 3857)));
+  * Result:
+
+        -13146
+
 ## <a id="predicate">Spatial Predicate</a>
 Spatial predicate functions test for a relationship between two geometries and return a Boolean value (true/false).
 
@@ -742,4 +770,4 @@
         st_union((SELECT VALUE gbu FROM [st_make_point(1.0,1.0),st_make_point(1.0,2.0)] as gbu));
   * Result:
   
-        {"type":"MultiPoint","coordinates":[[1,1],[1,2]],"crs":{"type":"name","properties":{"name":"EPSG:4326"}}}
\ No newline at end of file
+        {"type":"MultiPoint","coordinates":[[1,1],[1,2]],"crs":{"type":"name","properties":{"name":"EPSG:4326"}}}
diff --git a/asterixdb/asterix-geo/pom.xml b/asterixdb/asterix-geo/pom.xml
index f2784ce..bd9ee24 100644
--- a/asterixdb/asterix-geo/pom.xml
+++ b/asterixdb/asterix-geo/pom.xml
@@ -131,6 +131,15 @@
       <groupId>org.locationtech.jts</groupId>
       <artifactId>jts-core</artifactId>
     </dependency>
+    <dependency>
+      <groupId>org.apache.sis.core</groupId>
+      <artifactId>sis-referencing</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.asterix</groupId>
+      <artifactId>asterix-metadata</artifactId>
+      <version>${project.version}</version>
+    </dependency>
   </dependencies>
 
 </project>
diff --git a/asterixdb/asterix-geo/src/main/java/org/apache/asterix/geo/evaluators/GeoFunctionRegistrant.java b/asterixdb/asterix-geo/src/main/java/org/apache/asterix/geo/evaluators/GeoFunctionRegistrant.java
index ba20fa4..9c370ef 100644
--- a/asterixdb/asterix-geo/src/main/java/org/apache/asterix/geo/evaluators/GeoFunctionRegistrant.java
+++ b/asterixdb/asterix-geo/src/main/java/org/apache/asterix/geo/evaluators/GeoFunctionRegistrant.java
@@ -39,6 +39,7 @@
 import org.apache.asterix.geo.evaluators.functions.STDisjointDescriptor;
 import org.apache.asterix.geo.evaluators.functions.STDistanceDescriptor;
 import org.apache.asterix.geo.evaluators.functions.STDistanceSphereDescriptor;
+import org.apache.asterix.geo.evaluators.functions.STDistanceSpheroidDescriptor;
 import org.apache.asterix.geo.evaluators.functions.STEndPointDescriptor;
 import org.apache.asterix.geo.evaluators.functions.STEnvelopeDescriptor;
 import org.apache.asterix.geo.evaluators.functions.STEqualsDescriptor;
@@ -80,6 +81,7 @@
 import org.apache.asterix.geo.evaluators.functions.STStartPointDescriptor;
 import org.apache.asterix.geo.evaluators.functions.STSymDifferenceDescriptor;
 import org.apache.asterix.geo.evaluators.functions.STTouchesDescriptor;
+import org.apache.asterix.geo.evaluators.functions.STTransformDescriptor;
 import org.apache.asterix.geo.evaluators.functions.STUnionDescriptor;
 import org.apache.asterix.geo.evaluators.functions.STWithinDescriptor;
 import org.apache.asterix.geo.evaluators.functions.STXDescriptor;
@@ -176,5 +178,7 @@
         fc.add(STDistanceSphereDescriptor.FACTORY);
         fc.add(STDWithinDescriptor.FACTORY);
         fc.add(STBufferDescriptor.FACTORY);
+        fc.add(STTransformDescriptor.FACTORY);
+        fc.add(STDistanceSpheroidDescriptor.FACTORY);
     }
 }
diff --git a/asterixdb/asterix-geo/src/main/java/org/apache/asterix/geo/evaluators/GeoFunctionTypeInferers.java b/asterixdb/asterix-geo/src/main/java/org/apache/asterix/geo/evaluators/GeoFunctionTypeInferers.java
index db6615c..0568117 100644
--- a/asterixdb/asterix-geo/src/main/java/org/apache/asterix/geo/evaluators/GeoFunctionTypeInferers.java
+++ b/asterixdb/asterix-geo/src/main/java/org/apache/asterix/geo/evaluators/GeoFunctionTypeInferers.java
@@ -19,6 +19,8 @@
 package org.apache.asterix.geo.evaluators;
 
 import org.apache.asterix.common.config.CompilerProperties;
+import org.apache.asterix.common.exceptions.CompilationException;
+import org.apache.asterix.common.exceptions.ErrorCode;
 import org.apache.asterix.om.functions.IFunctionDescriptor;
 import org.apache.asterix.om.functions.IFunctionTypeInferer;
 import org.apache.asterix.om.types.ATypeTag;
@@ -52,4 +54,18 @@
         }
     }
 
+    public static final class STTransformTypeInferer implements IFunctionTypeInferer {
+        @Override
+        public void infer(ILogicalExpression expr, IFunctionDescriptor fd, IVariableTypeEnvironment context,
+                CompilerProperties compilerProps, IMetadataProvider<?, ?> mp) throws AlgebricksException {
+            AbstractFunctionCallExpression fce = (AbstractFunctionCallExpression) expr;
+            Object[] opaque = fce.getOpaqueParameters();
+            if (opaque == null || opaque.length < 2) {
+                throw new CompilationException(ErrorCode.COMPILATION_ERROR, expr.getSourceLocation(),
+                        "ST_Transform CRS resolution rule did not run");
+            }
+            fd.setImmutableStates(opaque[0], opaque[1]);
+        }
+    }
+
 }
diff --git a/asterixdb/asterix-geo/src/main/java/org/apache/asterix/geo/evaluators/functions/AbstractSTDoubleGeometryDescriptor.java b/asterixdb/asterix-geo/src/main/java/org/apache/asterix/geo/evaluators/functions/AbstractSTDoubleGeometryDescriptor.java
index 8221c31..7f3b89f 100644
--- a/asterixdb/asterix-geo/src/main/java/org/apache/asterix/geo/evaluators/functions/AbstractSTDoubleGeometryDescriptor.java
+++ b/asterixdb/asterix-geo/src/main/java/org/apache/asterix/geo/evaluators/functions/AbstractSTDoubleGeometryDescriptor.java
@@ -47,6 +47,11 @@
 
     abstract protected Object evaluateOGCGeometry(Geometry geometry0, Geometry geometry1) throws HyracksDataException;
 
+    protected Object evaluateOGCGeometry(Geometry geometry0, Geometry geometry1, IEvaluatorContext ctx)
+            throws HyracksDataException {
+        return evaluateOGCGeometry(geometry0, geometry1);
+    }
+
     @Override
     public IScalarEvaluatorFactory createEvaluatorFactory(final IScalarEvaluatorFactory[] args) {
         return new IScalarEvaluatorFactory() {
@@ -67,6 +72,7 @@
         private final IPointable argPtr1;
         private final IScalarEvaluator eval0;
         private final IScalarEvaluator eval1;
+        private final IEvaluatorContext ctx;
 
         public AbstractSTDoubleGeometryEvaluator(IScalarEvaluatorFactory[] args, IEvaluatorContext ctx)
                 throws HyracksDataException {
@@ -76,6 +82,7 @@
             argPtr1 = new VoidPointable();
             eval0 = args[0].createScalarEvaluator(ctx);
             eval1 = args[1].createScalarEvaluator(ctx);
+            this.ctx = ctx;
         }
 
         @Override
@@ -112,7 +119,7 @@
                 Geometry geometry0 = AGeometrySerializerDeserializer.INSTANCE.deserialize(dataIn0).getGeometry();
                 DataInputStream dataIn1 = new DataInputStream(new ByteArrayInputStream(bytes1, offset1 + 1, len1 - 1));
                 Geometry geometry1 = AGeometrySerializerDeserializer.INSTANCE.deserialize(dataIn1).getGeometry();
-                Object finalResult = evaluateOGCGeometry(geometry0, geometry1);
+                Object finalResult = evaluateOGCGeometry(geometry0, geometry1, ctx);
                 if (finalResult instanceof Geometry) {
                     out.writeByte(ATypeTag.SERIALIZED_GEOMETRY_TYPE_TAG);
                     AGeometrySerializerDeserializer.INSTANCE.serialize((Geometry) finalResult, out);
diff --git a/asterixdb/asterix-geo/src/main/java/org/apache/asterix/geo/evaluators/functions/STDistanceSpheroidDescriptor.java b/asterixdb/asterix-geo/src/main/java/org/apache/asterix/geo/evaluators/functions/STDistanceSpheroidDescriptor.java
new file mode 100644
index 0000000..50e7b7e
--- /dev/null
+++ b/asterixdb/asterix-geo/src/main/java/org/apache/asterix/geo/evaluators/functions/STDistanceSpheroidDescriptor.java
@@ -0,0 +1,92 @@
+/*
+ * 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.asterix.geo.evaluators.functions;
+
+import java.io.Serial;
+
+import org.apache.asterix.common.exceptions.ErrorCode;
+import org.apache.asterix.om.functions.BuiltinFunctions;
+import org.apache.asterix.om.functions.IFunctionDescriptorFactory;
+import org.apache.hyracks.algebricks.core.algebra.functions.FunctionIdentifier;
+import org.apache.hyracks.api.context.IEvaluatorContext;
+import org.apache.hyracks.api.exceptions.HyracksDataException;
+import org.apache.hyracks.api.exceptions.IWarningCollector;
+import org.apache.hyracks.api.exceptions.Warning;
+import org.apache.sis.geometry.DirectPosition2D;
+import org.apache.sis.referencing.CommonCRS;
+import org.apache.sis.referencing.GeodeticCalculator;
+import org.apache.sis.referencing.crs.AbstractCRS;
+import org.apache.sis.referencing.cs.AxesConvention;
+import org.locationtech.jts.geom.Coordinate;
+import org.locationtech.jts.geom.Geometry;
+import org.locationtech.jts.operation.distance.DistanceOp;
+import org.opengis.referencing.crs.GeographicCRS;
+
+/**
+ * ST_DISTANCE_SPHEROID(geometry, geometry) - computes the geodetic (ellipsoidal) distance
+ * in meters between two geometries using the WGS84 ellipsoid via Apache SIS GeodeticCalculator.
+ * Inputs are assumed to be in lon/lat degrees (EPSG:4326). If an input geometry
+ * has a known SRID that is not 4326, a warning is emitted and the function still
+ * computes distance using WGS84.
+ * For non-Point geometries the centroid is used.
+ */
+public class STDistanceSpheroidDescriptor extends AbstractSTDoubleGeometryDescriptor {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+    public static final IFunctionDescriptorFactory FACTORY = STDistanceSpheroidDescriptor::new;
+
+    private static final GeographicCRS WGS84 = (GeographicCRS) AbstractCRS.castOrCopy(CommonCRS.WGS84.geographic())
+            .forConvention(AxesConvention.RIGHT_HANDED);
+
+    @Override
+    public FunctionIdentifier getIdentifier() {
+        return BuiltinFunctions.ST_DISTANCE_SPHEROID;
+    }
+
+    @Override
+    protected Object evaluateOGCGeometry(Geometry geom0, Geometry geom1) throws HyracksDataException {
+        Coordinate[] nearest = DistanceOp.nearestPoints(geom0, geom1);
+        Coordinate c0 = nearest[0];
+        Coordinate c1 = nearest[1];
+        GeodeticCalculator calc = GeodeticCalculator.create(WGS84);
+        calc.setStartPoint(new DirectPosition2D(WGS84, c0.x, c0.y));
+        calc.setEndPoint(new DirectPosition2D(WGS84, c1.x, c1.y));
+        return calc.getGeodesicDistance();
+    }
+
+    @Override
+    protected Object evaluateOGCGeometry(Geometry geom0, Geometry geom1, IEvaluatorContext ctx)
+            throws HyracksDataException {
+        int srid0 = geom0.getSRID();
+        int srid1 = geom1.getSRID();
+        // NOTE: This warning is currently unreachable. AGeometrySerializerDeserializer uses
+        // JTS WKBWriter, which does not persist SRID in the binary representation.
+        // geom.getSRID() always returns 0 after deserialization, so the condition
+        // (srid != 0 && srid != 4326) is always false. This block will become active
+        // when WKB SRID persistence is implemented.
+        if ((srid0 != 0 && srid0 != 4326) || (srid1 != 0 && srid1 != 4326)) {
+            IWarningCollector wc = ctx.getWarningCollector();
+            if (wc.shouldWarn()) {
+                wc.warn(Warning.of(sourceLoc, ErrorCode.ST_DISTANCE_SPHEROID_REQUIRES_4326, srid0, srid1));
+            }
+        }
+        return evaluateOGCGeometry(geom0, geom1);
+    }
+}
diff --git a/asterixdb/asterix-geo/src/main/java/org/apache/asterix/geo/evaluators/functions/STFlipCoordinatesDescriptor.java b/asterixdb/asterix-geo/src/main/java/org/apache/asterix/geo/evaluators/functions/STFlipCoordinatesDescriptor.java
index 7ad3277..1c2b93a 100644
--- a/asterixdb/asterix-geo/src/main/java/org/apache/asterix/geo/evaluators/functions/STFlipCoordinatesDescriptor.java
+++ b/asterixdb/asterix-geo/src/main/java/org/apache/asterix/geo/evaluators/functions/STFlipCoordinatesDescriptor.java
@@ -18,6 +18,8 @@
  */
 package org.apache.asterix.geo.evaluators.functions;
 
+import java.io.Serial;
+
 import org.apache.asterix.om.functions.BuiltinFunctions;
 import org.apache.asterix.om.functions.IFunctionDescriptorFactory;
 import org.apache.hyracks.algebricks.core.algebra.functions.FunctionIdentifier;
@@ -28,14 +30,14 @@
 
 public class STFlipCoordinatesDescriptor extends AbstractSTSingleGeometryDescriptor {
 
+    @Serial
     private static final long serialVersionUID = 1L;
     public static final IFunctionDescriptorFactory FACTORY = STFlipCoordinatesDescriptor::new;
 
     @Override
     protected Object evaluateOGCGeometry(Geometry geometry) throws HyracksDataException {
-        Geometry flipped = geometry.copy();
-        flipped.apply(FlipCoordinatesFilter.INSTANCE);
-        return flipped;
+        geometry.apply(FlipCoordinatesFilter.INSTANCE);
+        return geometry;
     }
 
     @Override
diff --git a/asterixdb/asterix-geo/src/main/java/org/apache/asterix/geo/evaluators/functions/STTransformDescriptor.java b/asterixdb/asterix-geo/src/main/java/org/apache/asterix/geo/evaluators/functions/STTransformDescriptor.java
new file mode 100644
index 0000000..a57ca7a
--- /dev/null
+++ b/asterixdb/asterix-geo/src/main/java/org/apache/asterix/geo/evaluators/functions/STTransformDescriptor.java
@@ -0,0 +1,295 @@
+/*
+ * 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.asterix.geo.evaluators.functions;
+
+import java.io.ByteArrayInputStream;
+import java.io.DataInputStream;
+import java.io.DataOutput;
+import java.io.IOException;
+import java.io.Serial;
+
+import org.apache.asterix.common.exceptions.ErrorCode;
+import org.apache.asterix.dataflow.data.nontagged.serde.AGeometrySerializerDeserializer;
+import org.apache.asterix.dataflow.data.nontagged.serde.AInt32SerializerDeserializer;
+import org.apache.asterix.dataflow.data.nontagged.serde.AInt64SerializerDeserializer;
+import org.apache.asterix.geo.evaluators.GeoFunctionTypeInferers;
+import org.apache.asterix.om.exceptions.ExceptionUtil;
+import org.apache.asterix.om.functions.BuiltinFunctions;
+import org.apache.asterix.om.functions.IFunctionDescriptor;
+import org.apache.asterix.om.functions.IFunctionDescriptorFactory;
+import org.apache.asterix.om.functions.IFunctionTypeInferer;
+import org.apache.asterix.om.types.ATypeTag;
+import org.apache.asterix.om.types.EnumDeserializer;
+import org.apache.asterix.runtime.evaluators.base.AbstractScalarFunctionDynamicDescriptor;
+import org.apache.asterix.runtime.evaluators.functions.PointableHelper;
+import org.apache.asterix.runtime.exceptions.InvalidDataFormatException;
+import org.apache.asterix.runtime.exceptions.TypeMismatchException;
+import org.apache.hyracks.algebricks.core.algebra.functions.FunctionIdentifier;
+import org.apache.hyracks.algebricks.runtime.base.IScalarEvaluator;
+import org.apache.hyracks.algebricks.runtime.base.IScalarEvaluatorFactory;
+import org.apache.hyracks.api.context.IEvaluatorContext;
+import org.apache.hyracks.api.exceptions.HyracksDataException;
+import org.apache.hyracks.api.exceptions.IWarningCollector;
+import org.apache.hyracks.api.exceptions.Warning;
+import org.apache.hyracks.data.std.api.IPointable;
+import org.apache.hyracks.data.std.primitive.VoidPointable;
+import org.apache.hyracks.data.std.util.ArrayBackedValueStorage;
+import org.apache.hyracks.dataflow.common.data.accessors.IFrameTupleReference;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.apache.sis.referencing.CRS;
+import org.apache.sis.referencing.crs.AbstractCRS;
+import org.apache.sis.referencing.cs.AxesConvention;
+import org.locationtech.jts.geom.CoordinateSequence;
+import org.locationtech.jts.geom.CoordinateSequenceFilter;
+import org.locationtech.jts.geom.Geometry;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.opengis.referencing.operation.MathTransform;
+import org.opengis.referencing.operation.TransformException;
+import org.opengis.util.FactoryException;
+
+/**
+ * ST_Transform(geometry, fromSRID, toSRID) - transforms geometry coordinates
+ * from one CRS to another using the 3-argument form where both source and
+ * target SRIDs are explicitly specified.
+ * CRS WKT definitions are resolved at compile time from metadata and injected
+ * via {@link #setImmutableStates(Object...)}.
+ */
+public class STTransformDescriptor extends AbstractScalarFunctionDynamicDescriptor {
+
+    private static final Logger LOGGER = LogManager.getLogger();
+
+    public static final IFunctionDescriptorFactory FACTORY = new IFunctionDescriptorFactory() {
+        @Override
+        public IFunctionDescriptor createFunctionDescriptor() {
+            return new STTransformDescriptor();
+        }
+
+        @Override
+        public IFunctionTypeInferer createFunctionTypeInferer() {
+            return new GeoFunctionTypeInferers.STTransformTypeInferer();
+        }
+    };
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    private String fromWkt;
+    private String toWkt;
+
+    @Override
+    public void setImmutableStates(Object... states) {
+        this.fromWkt = (String) states[0];
+        this.toWkt = (String) states[1];
+    }
+
+    @Override
+    public FunctionIdentifier getIdentifier() {
+        return BuiltinFunctions.ST_TRANSFORM;
+    }
+
+    @Override
+    public IScalarEvaluatorFactory createEvaluatorFactory(final IScalarEvaluatorFactory[] args) {
+        final String capturedFromWkt = fromWkt;
+        final String capturedToWkt = toWkt;
+        return new IScalarEvaluatorFactory() {
+            @Serial
+            private static final long serialVersionUID = 1L;
+
+            @Override
+            public IScalarEvaluator createScalarEvaluator(IEvaluatorContext ctx) throws HyracksDataException {
+                return new STTransformEvaluator(args, ctx, capturedFromWkt, capturedToWkt);
+            }
+        };
+    }
+
+    private class STTransformEvaluator implements IScalarEvaluator {
+        private final ArrayBackedValueStorage resultStorage;
+        private final DataOutput out;
+        private final IPointable inputArg0;
+        private final IScalarEvaluator eval0;
+        private final IPointable inputArg1;
+        private final IScalarEvaluator eval1;
+        private final IPointable inputArg2;
+        private final IScalarEvaluator eval2;
+        private final IEvaluatorContext ctx;
+        private final MathTransform mathTransform;
+        private final TransformFilter transformFilter = new TransformFilter();
+
+        public STTransformEvaluator(IScalarEvaluatorFactory[] args, IEvaluatorContext ctx, String fromWkt, String toWkt)
+                throws HyracksDataException {
+            resultStorage = new ArrayBackedValueStorage();
+            out = resultStorage.getDataOutput();
+            inputArg0 = new VoidPointable();
+            eval0 = args[0].createScalarEvaluator(ctx);
+            inputArg1 = new VoidPointable();
+            eval1 = args[1].createScalarEvaluator(ctx);
+            inputArg2 = new VoidPointable();
+            eval2 = args[2].createScalarEvaluator(ctx);
+            this.ctx = ctx;
+            // Precompute MathTransform once at evaluator construction time.
+            // CRS WKTs were resolved from metadata at compile time on the CC and
+            // serialized into this evaluator factory as immutable state.
+            try {
+                // TODO: expose AxesConvention (e.g. for POSITIVE_RANGE or NORMALIZED)
+                //  if a user needs non-default axis handling.
+                CoordinateReferenceSystem fromCrs =
+                        AbstractCRS.castOrCopy(CRS.fromWKT(fromWkt)).forConvention(AxesConvention.RIGHT_HANDED);
+                CoordinateReferenceSystem toCrs =
+                        AbstractCRS.castOrCopy(CRS.fromWKT(toWkt)).forConvention(AxesConvention.RIGHT_HANDED);
+                this.mathTransform = CRS.findOperation(fromCrs, toCrs, null).getMathTransform();
+            } catch (FactoryException e) {
+                throw HyracksDataException.create(e);
+            }
+            transformFilter.setTransform(this.mathTransform);
+            LOGGER.debug("[NC runtime] STTransformEvaluator: precomputed MathTransform from WKTs");
+        }
+
+        @Override
+        public void evaluate(IFrameTupleReference tuple, IPointable result) throws HyracksDataException {
+            resultStorage.reset();
+
+            eval0.evaluate(tuple, inputArg0);
+            eval1.evaluate(tuple, inputArg1);
+            eval2.evaluate(tuple, inputArg2);
+
+            if (PointableHelper.checkAndSetMissingOrNull(result, inputArg0, inputArg1, inputArg2)) {
+                return;
+            }
+
+            byte[] data0 = inputArg0.getByteArray();
+            int offset0 = inputArg0.getStartOffset();
+            int len0 = inputArg0.getLength();
+
+            byte[] data1 = inputArg1.getByteArray();
+            int offset1 = inputArg1.getStartOffset();
+
+            byte[] data2 = inputArg2.getByteArray();
+            int offset2 = inputArg2.getStartOffset();
+
+            ATypeTag tag0 = EnumDeserializer.ATYPETAGDESERIALIZER.deserialize(data0[offset0]);
+            if (tag0 != ATypeTag.GEOMETRY) {
+                ExceptionUtil.warnTypeMismatch(ctx, sourceLoc, getIdentifier(), data0[offset0], 0, ATypeTag.GEOMETRY);
+                PointableHelper.setNull(result);
+                return;
+            }
+
+            try {
+                DataInputStream dataIn0 = new DataInputStream(new ByteArrayInputStream(data0, offset0 + 1, len0 - 1));
+                Geometry geometry = AGeometrySerializerDeserializer.INSTANCE.deserialize(dataIn0).getGeometry();
+
+                int fromSRID = readSrid(data1, offset1, 1);
+                int toSRID = readSrid(data2, offset2, 2);
+
+                if (geometry.getSRID() != 0 && geometry.getSRID() != fromSRID) {
+                    IWarningCollector wc = ctx.getWarningCollector();
+                    if (wc.shouldWarn()) {
+                        wc.warn(Warning.of(sourceLoc, ErrorCode.SRID_MISMATCH, geometry.getSRID(), fromSRID,
+                                getIdentifier().getName()));
+                    }
+                }
+
+                try {
+                    transformGeometry(geometry, transformFilter);
+                } catch (HyracksDataException e) {
+                    IWarningCollector wc = ctx.getWarningCollector();
+                    if (wc.shouldWarn()) {
+                        wc.warn(Warning.of(sourceLoc, ErrorCode.CRS_TRANSFORM_FAILED, fromSRID, toSRID,
+                                e.getMessage()));
+                    }
+                    PointableHelper.setNull(result);
+                    return;
+                }
+                geometry.setSRID(toSRID);
+
+                out.writeByte(ATypeTag.SERIALIZED_GEOMETRY_TYPE_TAG);
+                AGeometrySerializerDeserializer.INSTANCE.serialize(geometry, out);
+                result.set(resultStorage);
+            } catch (IOException e) {
+                IWarningCollector wc = ctx.getWarningCollector();
+                if (wc.shouldWarn()) {
+                    wc.warn(Warning.of(sourceLoc, ErrorCode.INVALID_FORMAT, getIdentifier().getName(),
+                            ATypeTag.GEOMETRY));
+                }
+                PointableHelper.setNull(result);
+            }
+        }
+    }
+
+    private int readSrid(byte[] data, int offset, int argIndex) throws HyracksDataException {
+        byte serializedTypeTag = data[offset];
+        if (serializedTypeTag == ATypeTag.SERIALIZED_INT64_TYPE_TAG) {
+            long sridValue = AInt64SerializerDeserializer.getLong(data, offset + 1);
+            if (sridValue < Integer.MIN_VALUE || sridValue > Integer.MAX_VALUE) {
+                throw new InvalidDataFormatException(sourceLoc, getIdentifier(),
+                        "SRID value " + sridValue + " is out of range for int");
+            }
+            return (int) sridValue;
+        }
+        if (serializedTypeTag == ATypeTag.SERIALIZED_INT32_TYPE_TAG) {
+            return AInt32SerializerDeserializer.getInt(data, offset + 1);
+        }
+        throw new TypeMismatchException(sourceLoc, getIdentifier(), argIndex, serializedTypeTag,
+                ATypeTag.SERIALIZED_INT64_TYPE_TAG, ATypeTag.SERIALIZED_INT32_TYPE_TAG);
+    }
+
+    static void transformGeometry(Geometry geometry, TransformFilter filter) throws HyracksDataException {
+        try {
+            geometry.apply(filter);
+        } catch (RuntimeException e) {
+            if (e.getCause() instanceof TransformException) {
+                throw HyracksDataException.create(e.getCause());
+            }
+            throw HyracksDataException.create(e);
+        }
+    }
+
+    private static final class TransformFilter implements CoordinateSequenceFilter {
+        private final double[] srcPt = new double[2];
+        private final double[] dstPt = new double[2];
+        private MathTransform mt;
+
+        void setTransform(MathTransform mt) {
+            this.mt = mt;
+        }
+
+        @Override
+        public void filter(CoordinateSequence seq, int i) {
+            try {
+                srcPt[0] = seq.getX(i);
+                srcPt[1] = seq.getY(i);
+                mt.transform(srcPt, 0, dstPt, 0, 1);
+                seq.setOrdinate(i, CoordinateSequence.X, dstPt[0]);
+                seq.setOrdinate(i, CoordinateSequence.Y, dstPt[1]);
+            } catch (TransformException e) {
+                throw new RuntimeException(e);
+            }
+        }
+
+        @Override
+        public boolean isDone() {
+            return false;
+        }
+
+        @Override
+        public boolean isGeometryChanged() {
+            return true;
+        }
+    }
+}
diff --git a/asterixdb/asterix-lang-common/src/main/java/org/apache/asterix/lang/common/base/Statement.java b/asterixdb/asterix-lang-common/src/main/java/org/apache/asterix/lang/common/base/Statement.java
index 6acaa07..7a5aa17 100644
--- a/asterixdb/asterix-lang-common/src/main/java/org/apache/asterix/lang/common/base/Statement.java
+++ b/asterixdb/asterix-lang-common/src/main/java/org/apache/asterix/lang/common/base/Statement.java
@@ -125,6 +125,8 @@
         COPY_FROM,
         COPY_TO,
         CATALOG_CREATE,
-        CATALOG_DROP
+        CATALOG_DROP,
+        CRS_CREATE,
+        CRS_DROP
     }
 }
diff --git a/asterixdb/asterix-lang-common/src/main/java/org/apache/asterix/lang/common/statement/crs/CRSCreateStatement.java b/asterixdb/asterix-lang-common/src/main/java/org/apache/asterix/lang/common/statement/crs/CRSCreateStatement.java
new file mode 100644
index 0000000..2c23845
--- /dev/null
+++ b/asterixdb/asterix-lang-common/src/main/java/org/apache/asterix/lang/common/statement/crs/CRSCreateStatement.java
@@ -0,0 +1,59 @@
+/*
+ * 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.asterix.lang.common.statement.crs;
+
+import org.apache.asterix.common.exceptions.CompilationException;
+import org.apache.asterix.common.metadata.Namespace;
+import org.apache.asterix.lang.common.visitor.base.ILangVisitor;
+
+public class CRSCreateStatement extends CRSStatement {
+
+    private final String crsName;
+    private final String crsWkt;
+    private final boolean ifNotExists;
+
+    public CRSCreateStatement(Namespace namespace, int srid, String crsName, String crsWkt, boolean ifNotExists) {
+        super(namespace, srid);
+        this.crsName = crsName;
+        this.crsWkt = crsWkt;
+        this.ifNotExists = ifNotExists;
+    }
+
+    public String getCrsName() {
+        return crsName;
+    }
+
+    public String getCrsWkt() {
+        return crsWkt;
+    }
+
+    public boolean getIfNotExists() {
+        return ifNotExists;
+    }
+
+    @Override
+    public Kind getKind() {
+        return Kind.CRS_CREATE;
+    }
+
+    @Override
+    public <R, T> R accept(ILangVisitor<R, T> visitor, T arg) throws CompilationException {
+        return null;
+    }
+}
diff --git a/asterixdb/asterix-lang-common/src/main/java/org/apache/asterix/lang/common/statement/crs/CRSDropStatement.java b/asterixdb/asterix-lang-common/src/main/java/org/apache/asterix/lang/common/statement/crs/CRSDropStatement.java
new file mode 100644
index 0000000..3707d8d
--- /dev/null
+++ b/asterixdb/asterix-lang-common/src/main/java/org/apache/asterix/lang/common/statement/crs/CRSDropStatement.java
@@ -0,0 +1,47 @@
+/*
+ * 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.asterix.lang.common.statement.crs;
+
+import org.apache.asterix.common.exceptions.CompilationException;
+import org.apache.asterix.common.metadata.Namespace;
+import org.apache.asterix.lang.common.visitor.base.ILangVisitor;
+
+public class CRSDropStatement extends CRSStatement {
+
+    private final boolean ifExists;
+
+    public CRSDropStatement(Namespace namespace, int srid, boolean ifExists) {
+        super(namespace, srid);
+        this.ifExists = ifExists;
+    }
+
+    public boolean getIfExists() {
+        return ifExists;
+    }
+
+    @Override
+    public Kind getKind() {
+        return Kind.CRS_DROP;
+    }
+
+    @Override
+    public <R, T> R accept(ILangVisitor<R, T> visitor, T arg) throws CompilationException {
+        return null;
+    }
+}
diff --git a/asterixdb/asterix-lang-common/src/main/java/org/apache/asterix/lang/common/statement/crs/CRSStatement.java b/asterixdb/asterix-lang-common/src/main/java/org/apache/asterix/lang/common/statement/crs/CRSStatement.java
new file mode 100644
index 0000000..d3539dd
--- /dev/null
+++ b/asterixdb/asterix-lang-common/src/main/java/org/apache/asterix/lang/common/statement/crs/CRSStatement.java
@@ -0,0 +1,46 @@
+/*
+ * 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.asterix.lang.common.statement.crs;
+
+import org.apache.asterix.common.metadata.Namespace;
+import org.apache.asterix.lang.common.base.AbstractStatement;
+
+public abstract class CRSStatement extends AbstractStatement {
+
+    private final Namespace namespace;
+    private final int srid;
+
+    protected CRSStatement(Namespace namespace, int srid) {
+        this.namespace = namespace;
+        this.srid = srid;
+    }
+
+    public Namespace getNamespace() {
+        return namespace;
+    }
+
+    public int getSrid() {
+        return srid;
+    }
+
+    @Override
+    public byte getCategory() {
+        return Category.DDL;
+    }
+}
diff --git a/asterixdb/asterix-lang-sqlpp/src/main/javacc/SQLPP.jj b/asterixdb/asterix-lang-sqlpp/src/main/javacc/SQLPP.jj
index b08750c..cbe0e7e 100644
--- a/asterixdb/asterix-lang-sqlpp/src/main/javacc/SQLPP.jj
+++ b/asterixdb/asterix-lang-sqlpp/src/main/javacc/SQLPP.jj
@@ -186,6 +186,8 @@
 import org.apache.asterix.lang.common.statement.catalog.CatalogDropStatement;
 import org.apache.asterix.lang.common.statement.catalog.IcebergCatalogCreateStatement;
 import org.apache.asterix.lang.common.statement.catalog.IcebergCatalogDetailsDecl;
+import org.apache.asterix.lang.common.statement.crs.CRSCreateStatement;
+import org.apache.asterix.lang.common.statement.crs.CRSDropStatement;
 import org.apache.asterix.lang.common.struct.Identifier;
 import org.apache.asterix.lang.common.struct.OperatorType;
 import org.apache.asterix.lang.common.struct.QuantifiedPair;
@@ -288,6 +290,10 @@
     protected static final String CATALOG = "CATALOG";
     private static final String SOURCE = "SOURCE";
     private static final String CASCADE = "CASCADE";
+    private static final String COORDINATE = "COORDINATE";
+    private static final String REFERENCE = "REFERENCE";
+    private static final String SYSTEM = "SYSTEM";
+    private static final String NAME = "NAME";
 
 
     private static final String INT_TYPE_NAME = "int";
@@ -1114,6 +1120,7 @@
     | stmt = CreateFeedPolicyStatement(startToken)
     | stmt = CreateFullTextStatement(startToken)
     | LOOKAHEAD({laIdentifier(CATALOG)}) stmt = CreateCatalogStatement(startToken)
+    | LOOKAHEAD({laIdentifier(COORDINATE)}) stmt = CreateCRSStatement(startToken)
     | stmt = CreateViewStatement(startToken, false)
   )
   {
@@ -1294,6 +1301,46 @@
   }
 }
 
+CRSCreateStatement CreateCRSStatement(Token startStmtToken) throws ParseException:
+{
+  int srid;
+  boolean ifNotExists = false;
+  String crsName = null;
+  String crsWkt = null;
+}
+{
+  // consume "COORDINATE"
+  <IDENTIFIER> { expectToken(COORDINATE); }
+  // consume "REFERENCE"
+  <IDENTIFIER> { expectToken(REFERENCE); }
+  // consume "SYSTEM"
+  <IDENTIFIER> { expectToken(SYSTEM); }
+  <INTEGER_LITERAL>
+  {
+    long sridLong;
+    try {
+      sridLong = Long.parseLong(token.image);
+    } catch (NumberFormatException e) {
+      throw new SqlppParseException(getSourceLocation(token), "Invalid SRID value: " + token.image);
+    }
+    if (sridLong < 1 || sridLong > Integer.MAX_VALUE) {
+      throw new SqlppParseException(getSourceLocation(token),
+        "SRID must be a positive integer (1 to " + Integer.MAX_VALUE + "), got: " + sridLong);
+    }
+    srid = (int) sridLong;
+  }
+  ifNotExists = IfNotExists()
+  // consume "NAME"
+  <IDENTIFIER> { expectToken(NAME); }
+  crsName = StringLiteral()
+  <AS>
+  crsWkt = StringLiteral()
+  {
+    CRSCreateStatement stmt = new CRSCreateStatement(defaultNamespace, srid, crsName, crsWkt, ifNotExists);
+    return addSourceLocation(stmt, startStmtToken);
+  }
+}
+
 DatasetDecl CreateDatasetStatement(Token startStmtToken) throws ParseException:
 {
   DatasetDecl stmt = null;
@@ -2621,6 +2668,7 @@
     | stmt = DropSynonymStatement(startToken)
     | stmt = DropFullTextStatement(startToken)
     | LOOKAHEAD({laIdentifier(CATALOG)}) stmt = DropCatalogStatement(startToken)
+    | LOOKAHEAD({laIdentifier(COORDINATE)}) stmt = DropCRSStatement(startToken)
     | stmt = DropViewStatement(startToken)
   )
   {
@@ -2657,6 +2705,39 @@
   }
 }
 
+CRSDropStatement DropCRSStatement(Token startStmtToken) throws ParseException:
+{
+  int srid;
+  boolean ifExists = false;
+}
+{
+  // consume "COORDINATE"
+  <IDENTIFIER> { expectToken(COORDINATE); }
+  // consume "REFERENCE"
+  <IDENTIFIER> { expectToken(REFERENCE); }
+  // consume "SYSTEM"
+  <IDENTIFIER> { expectToken(SYSTEM); }
+  <INTEGER_LITERAL>
+  {
+    long sridLong;
+    try {
+      sridLong = Long.parseLong(token.image);
+    } catch (NumberFormatException e) {
+      throw new SqlppParseException(getSourceLocation(token), "Invalid SRID value: " + token.image);
+    }
+    if (sridLong < 1 || sridLong > Integer.MAX_VALUE) {
+      throw new SqlppParseException(getSourceLocation(token),
+        "SRID must be a positive integer (1 to " + Integer.MAX_VALUE + "), got: " + sridLong);
+    }
+    srid = (int) sridLong;
+  }
+  ifExists = IfExists()
+  {
+    CRSDropStatement stmt = new CRSDropStatement(defaultNamespace, srid, ifExists);
+    return addSourceLocation(stmt, startStmtToken);
+  }
+}
+
 ViewDropStatement DropViewStatement(Token startStmtToken) throws ParseException:
 {
   ViewDropStatement stmt = null;
diff --git a/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/MetadataCache.java b/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/MetadataCache.java
index 638e4f3..81eb535 100644
--- a/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/MetadataCache.java
+++ b/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/MetadataCache.java
@@ -31,6 +31,7 @@
 import org.apache.asterix.metadata.api.IMetadataEntity;
 import org.apache.asterix.metadata.entities.Catalog;
 import org.apache.asterix.metadata.entities.CompactionPolicy;
+import org.apache.asterix.metadata.entities.CoordinateReferenceSystem;
 import org.apache.asterix.metadata.entities.Database;
 import org.apache.asterix.metadata.entities.Dataset;
 import org.apache.asterix.metadata.entities.DatasourceAdapter;
@@ -93,6 +94,8 @@
             new HashMap<>();
     // Key is catalog name
     protected final Map<String, Catalog> catalogs = new HashMap<>();
+    // Key is database name, then dataverse name, then SRID
+    protected final Map<String, Map<DataverseName, Map<Integer, CoordinateReferenceSystem>>> crs = new HashMap<>();
 
     // Atomically executes all metadata operations in ctx's log.
     public void commit(MetadataTransactionContext ctx) {
@@ -134,20 +137,23 @@
                                                 synchronized (libraries) {
                                                     synchronized (compactionPolicies) {
                                                         synchronized (synonyms) {
-                                                            databases.clear();
-                                                            dataverses.clear();
-                                                            nodeGroups.clear();
-                                                            datasets.clear();
-                                                            indexes.clear();
-                                                            datatypes.clear();
-                                                            functions.clear();
-                                                            fullTextConfigs.clear();
-                                                            fullTextFilters.clear();
-                                                            adapters.clear();
-                                                            libraries.clear();
-                                                            compactionPolicies.clear();
-                                                            synonyms.clear();
-                                                            catalogs.clear();
+                                                            synchronized (crs) {
+                                                                databases.clear();
+                                                                dataverses.clear();
+                                                                nodeGroups.clear();
+                                                                datasets.clear();
+                                                                indexes.clear();
+                                                                datatypes.clear();
+                                                                functions.clear();
+                                                                fullTextConfigs.clear();
+                                                                fullTextFilters.clear();
+                                                                adapters.clear();
+                                                                libraries.clear();
+                                                                compactionPolicies.clear();
+                                                                synonyms.clear();
+                                                                catalogs.clear();
+                                                                crs.clear();
+                                                            }
                                                         }
                                                     }
                                                 }
@@ -652,6 +658,59 @@
         }
     }
 
+    public CoordinateReferenceSystem addOrUpdateCrs(CoordinateReferenceSystem crsEntity) {
+        synchronized (crs) {
+            Map<DataverseName, Map<Integer, CoordinateReferenceSystem>> databaseDataverses =
+                    crs.computeIfAbsent(crsEntity.getDatabaseName(), k -> new HashMap<>());
+            Map<Integer, CoordinateReferenceSystem> crsInDataverse =
+                    databaseDataverses.computeIfAbsent(crsEntity.getDataverseName(), k -> new HashMap<>());
+            return crsInDataverse.put(crsEntity.getSrid(), crsEntity);
+        }
+    }
+
+    public CoordinateReferenceSystem dropCrs(CoordinateReferenceSystem crsEntity) {
+        synchronized (crs) {
+            Map<DataverseName, Map<Integer, CoordinateReferenceSystem>> databaseDataverses =
+                    crs.get(crsEntity.getDatabaseName());
+            if (databaseDataverses == null) {
+                return null;
+            }
+            Map<Integer, CoordinateReferenceSystem> crsInDataverse =
+                    databaseDataverses.get(crsEntity.getDataverseName());
+            if (crsInDataverse != null) {
+                return crsInDataverse.remove(crsEntity.getSrid());
+            }
+            return null;
+        }
+    }
+
+    public CoordinateReferenceSystem getCrs(String database, DataverseName dataverseName, int srid) {
+        synchronized (crs) {
+            Map<DataverseName, Map<Integer, CoordinateReferenceSystem>> databaseDataverses = crs.get(database);
+            if (databaseDataverses == null) {
+                return null;
+            }
+            Map<Integer, CoordinateReferenceSystem> crsInDataverse = databaseDataverses.get(dataverseName);
+            if (crsInDataverse != null) {
+                return crsInDataverse.get(srid);
+            }
+            return null;
+        }
+    }
+
+    public CoordinateReferenceSystem addCrsIfNotExists(CoordinateReferenceSystem crsEntity) {
+        synchronized (crs) {
+            Map<DataverseName, Map<Integer, CoordinateReferenceSystem>> databaseDataverses =
+                    crs.computeIfAbsent(crsEntity.getDatabaseName(), k -> new HashMap<>());
+            Map<Integer, CoordinateReferenceSystem> crsInDataverse =
+                    databaseDataverses.computeIfAbsent(crsEntity.getDataverseName(), k -> new HashMap<>());
+            if (!crsInDataverse.containsKey(crsEntity.getSrid())) {
+                return crsInDataverse.put(crsEntity.getSrid(), crsEntity);
+            }
+            return null;
+        }
+    }
+
     public Function addFunctionIfNotExists(Function function) {
         synchronized (functions) {
             FunctionSignature signature = new FunctionSignature(function.getDatabaseName(), function.getDataverseName(),
diff --git a/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/MetadataManager.java b/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/MetadataManager.java
index f7d0378..53b4dcb 100644
--- a/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/MetadataManager.java
+++ b/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/MetadataManager.java
@@ -45,6 +45,7 @@
 import org.apache.asterix.metadata.api.IMetadataNode;
 import org.apache.asterix.metadata.entities.Catalog;
 import org.apache.asterix.metadata.entities.CompactionPolicy;
+import org.apache.asterix.metadata.entities.CoordinateReferenceSystem;
 import org.apache.asterix.metadata.entities.Database;
 import org.apache.asterix.metadata.entities.Dataset;
 import org.apache.asterix.metadata.entities.DatasourceAdapter;
@@ -1422,6 +1423,54 @@
         ctx.dropCatalog(catalogName);
     }
 
+    @Override
+    public void addCRS(MetadataTransactionContext ctx, CoordinateReferenceSystem crs) throws AlgebricksException {
+        try {
+            metadataNode.addCRS(ctx.getTxnId(), crs);
+        } catch (RemoteException e) {
+            throw new MetadataException(ErrorCode.REMOTE_EXCEPTION_WHEN_CALLING_METADATA_NODE, e);
+        }
+        ctx.addCRS(crs);
+    }
+
+    @Override
+    public CoordinateReferenceSystem getCRS(MetadataTransactionContext ctx, String database,
+            DataverseName dataverseName, int srid) throws AlgebricksException {
+        Objects.requireNonNull(database);
+        CoordinateReferenceSystem crs = ctx.getCrs(database, dataverseName, srid);
+        if (crs != null) {
+            return crs;
+        }
+        if (ctx.crsIsDropped(database, dataverseName, srid)) {
+            return null;
+        }
+        crs = cache.getCrs(database, dataverseName, srid);
+        if (crs != null) {
+            return crs;
+        }
+        try {
+            crs = metadataNode.getCRS(ctx.getTxnId(), database, dataverseName, srid);
+        } catch (RemoteException e) {
+            throw new MetadataException(ErrorCode.REMOTE_EXCEPTION_WHEN_CALLING_METADATA_NODE, e);
+        }
+        if (crs != null) {
+            ctx.addCRS(crs);
+        }
+        return crs;
+    }
+
+    @Override
+    public void dropCRS(MetadataTransactionContext ctx, String database, DataverseName dataverseName, int srid)
+            throws AlgebricksException {
+        try {
+            Objects.requireNonNull(database);
+            metadataNode.dropCRS(ctx.getTxnId(), database, dataverseName, srid);
+        } catch (RemoteException e) {
+            throw new MetadataException(ErrorCode.REMOTE_EXCEPTION_WHEN_CALLING_METADATA_NODE, e);
+        }
+        ctx.dropCRS(database, dataverseName, srid);
+    }
+
     private static class CCMetadataManagerImpl extends MetadataManager {
         private final MetadataProperties metadataProperties;
         private final ICcApplicationContext appCtx;
diff --git a/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/MetadataNode.java b/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/MetadataNode.java
index fa7db2f..076a6a7 100644
--- a/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/MetadataNode.java
+++ b/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/MetadataNode.java
@@ -26,6 +26,8 @@
 import static org.apache.asterix.common.exceptions.ErrorCode.CANNOT_DROP_OBJECT_DEPENDENT_EXISTS;
 import static org.apache.asterix.common.exceptions.ErrorCode.CATALOG_EXISTS;
 import static org.apache.asterix.common.exceptions.ErrorCode.COMPACTION_POLICY_EXISTS;
+import static org.apache.asterix.common.exceptions.ErrorCode.CRS_ALREADY_EXISTS;
+import static org.apache.asterix.common.exceptions.ErrorCode.CRS_NOT_FOUND;
 import static org.apache.asterix.common.exceptions.ErrorCode.DATABASE_EXISTS;
 import static org.apache.asterix.common.exceptions.ErrorCode.DATASET_EXISTS;
 import static org.apache.asterix.common.exceptions.ErrorCode.DATAVERSE_EXISTS;
@@ -114,6 +116,7 @@
 import org.apache.asterix.metadata.bootstrap.MetadataIndexesProvider;
 import org.apache.asterix.metadata.entities.Catalog;
 import org.apache.asterix.metadata.entities.CompactionPolicy;
+import org.apache.asterix.metadata.entities.CoordinateReferenceSystem;
 import org.apache.asterix.metadata.entities.Database;
 import org.apache.asterix.metadata.entities.Dataset;
 import org.apache.asterix.metadata.entities.DatasourceAdapter;
@@ -134,6 +137,7 @@
 import org.apache.asterix.metadata.entities.NodeGroup;
 import org.apache.asterix.metadata.entities.Synonym;
 import org.apache.asterix.metadata.entities.ViewDetails;
+import org.apache.asterix.metadata.entitytupletranslators.CRSTupleTranslator;
 import org.apache.asterix.metadata.entitytupletranslators.CatalogTupleTranslator;
 import org.apache.asterix.metadata.entitytupletranslators.CompactionPolicyTupleTranslator;
 import org.apache.asterix.metadata.entitytupletranslators.DatabaseTupleTranslator;
@@ -159,6 +163,7 @@
 import org.apache.asterix.metadata.valueextractors.MetadataEntityValueExtractor;
 import org.apache.asterix.metadata.valueextractors.TupleCopyValueExtractor;
 import org.apache.asterix.om.base.AInt32;
+import org.apache.asterix.om.base.AMutableInt32;
 import org.apache.asterix.om.base.AMutableString;
 import org.apache.asterix.om.base.AString;
 import org.apache.asterix.om.typecomputer.impl.TypeComputeUtils;
@@ -810,6 +815,12 @@
                 dropSynonym(txnId, databaseName, synonym.getDataverseName(), synonym.getSynonymName(), true);
             }
 
+            // Drop all CRS definitions in this database.
+            List<CoordinateReferenceSystem> databaseCRSList = getDatabaseCRSList(txnId, databaseName);
+            for (CoordinateReferenceSystem crs : databaseCRSList) {
+                dropCRS(txnId, databaseName, crs.getDataverseName(), crs.getSrid());
+            }
+
             // Drop all datasets and indexes in this database.
             // Datasets depend on datatypes
             List<Dataset> databaseDatasets = getDatabaseDatasets(txnId, databaseName);
@@ -930,6 +941,12 @@
                 dropSynonym(txnId, database, dataverseName, synonym.getSynonymName(), true);
             }
 
+            // Drop all CRS definitions in this dataverse.
+            List<CoordinateReferenceSystem> dataverseCRSList = getDataverseCRSList(txnId, database, dataverseName);
+            for (CoordinateReferenceSystem crs : dataverseCRSList) {
+                dropCRS(txnId, database, dataverseName, crs.getSrid());
+            }
+
             // Drop all datasets and indexes in this dataverse.
             // Datasets depend on datatypes
             List<Dataset> dataverseDatasets = getDataverseDatasets(txnId, database, dataverseName);
@@ -996,6 +1013,7 @@
                 || !getDataverseFeedPolicies(txnId, database, dataverseName).isEmpty()
                 || !getDataverseFeeds(txnId, database, dataverseName).isEmpty()
                 || !getDataverseSynonyms(txnId, database, dataverseName).isEmpty()
+                || !getDataverseCRSList(txnId, database, dataverseName).isEmpty()
                 || !getDataverseFullTextConfigs(txnId, database, dataverseName).isEmpty()
                 || !getDataverseFullTextFilters(txnId, database, dataverseName).isEmpty();
     }
@@ -3234,4 +3252,111 @@
                     String.join(".", database, dataverseName, collectionName));
         }
     }
+
+    @Override
+    public void addCRS(TxnId txnId, CoordinateReferenceSystem crs) throws AlgebricksException {
+        try {
+            CRSTupleTranslator tupleReaderWriter = tupleTranslatorProvider.getCRSTupleTranslator(true);
+            ITupleReference tuple = tupleReaderWriter.getTupleFromMetadataEntity(crs);
+            insertTupleIntoIndex(txnId, mdIndexesProvider.getCRSEntity().getIndex(), tuple);
+        } catch (HyracksDataException e) {
+            if (e.matches(ErrorCode.DUPLICATE_KEY)) {
+                throw new AsterixException(CRS_ALREADY_EXISTS, e, crs.getSrid());
+            } else {
+                throw new AsterixException(METADATA_ERROR, e, e.getMessage());
+            }
+        }
+    }
+
+    @Override
+    public CoordinateReferenceSystem getCRS(TxnId txnId, String database, DataverseName dataverseName, int srid)
+            throws AlgebricksException {
+        try {
+            ITupleReference searchKey = createCRSTuple(database, dataverseName, srid);
+            CRSTupleTranslator tupleReaderWriter = tupleTranslatorProvider.getCRSTupleTranslator(false);
+            IValueExtractor<CoordinateReferenceSystem> valueExtractor =
+                    new MetadataEntityValueExtractor<>(tupleReaderWriter);
+            List<CoordinateReferenceSystem> results = new ArrayList<>();
+            searchIndex(txnId, mdIndexesProvider.getCRSEntity().getIndex(), searchKey, valueExtractor, results);
+            if (results.isEmpty()) {
+                return null;
+            }
+            return results.get(0);
+        } catch (HyracksDataException e) {
+            throw new AsterixException(METADATA_ERROR, e, e.getMessage());
+        }
+    }
+
+    @Override
+    public void dropCRS(TxnId txnId, String database, DataverseName dataverseName, int srid)
+            throws AlgebricksException {
+        try {
+            CoordinateReferenceSystem crs = getCRS(txnId, database, dataverseName, srid);
+            if (crs == null) {
+                throw new AsterixException(CRS_NOT_FOUND, srid);
+            }
+            ITupleReference searchKey = createCRSTuple(database, dataverseName, srid);
+            MetadataIndex crsIndex = mdIndexesProvider.getCRSEntity().getIndex();
+            ITupleReference tuple = getTupleToBeDeleted(txnId, crsIndex, searchKey);
+            deleteTupleFromIndex(txnId, crsIndex, tuple);
+        } catch (HyracksDataException e) {
+            if (e.matches(ErrorCode.UPDATE_OR_DELETE_NON_EXISTENT_KEY)) {
+                throw new AsterixException(CRS_NOT_FOUND, e, srid);
+            } else {
+                throw new AsterixException(METADATA_ERROR, e, e.getMessage());
+            }
+        }
+    }
+
+    @Override
+    public List<CoordinateReferenceSystem> getDataverseCRSList(TxnId txnId, String database,
+            DataverseName dataverseName) throws AlgebricksException {
+        return getCRSListImpl(txnId, createTuple(database, dataverseName));
+    }
+
+    private List<CoordinateReferenceSystem> getDatabaseCRSList(TxnId txnId, String database)
+            throws AlgebricksException {
+        return getCRSListImpl(txnId, createTuple(database));
+    }
+
+    private List<CoordinateReferenceSystem> getCRSListImpl(TxnId txnId, ITupleReference searchKey)
+            throws AlgebricksException {
+        try {
+            CRSTupleTranslator tupleReaderWriter = tupleTranslatorProvider.getCRSTupleTranslator(false);
+            IValueExtractor<CoordinateReferenceSystem> valueExtractor =
+                    new MetadataEntityValueExtractor<>(tupleReaderWriter);
+            List<CoordinateReferenceSystem> results = new ArrayList<>();
+            searchIndex(txnId, mdIndexesProvider.getCRSEntity().getIndex(), searchKey, valueExtractor, results);
+            return results;
+        } catch (HyracksDataException e) {
+            throw new AsterixException(METADATA_ERROR, e, e.getMessage());
+        }
+    }
+
+    @SuppressWarnings({ "unchecked", "deprecation" })
+    private ITupleReference createCRSTuple(String database, DataverseName dataverseName, int srid) {
+        try {
+            ISerializerDeserializer<AString> strSerde =
+                    SerializerDeserializerProvider.INSTANCE.getSerializerDeserializer(BuiltinType.ASTRING);
+            ISerializerDeserializer<AInt32> intSerde =
+                    SerializerDeserializerProvider.INSTANCE.getSerializerDeserializer(BuiltinType.AINT32);
+            AMutableString aStr = new AMutableString("");
+            AMutableInt32 aInt = new AMutableInt32(srid);
+            boolean usingDb = mdIndexesProvider.isUsingDatabase();
+            int numFields = usingDb ? 3 : 2;
+            ArrayTupleBuilder tb = new ArrayTupleBuilder(numFields);
+            if (usingDb) {
+                aStr.setValue(database);
+                tb.addField(strSerde, aStr);
+            }
+            aStr.setValue(dataverseName.getCanonicalForm());
+            tb.addField(strSerde, aStr);
+            tb.addField(intSerde, aInt);
+            ArrayTupleReference t = new ArrayTupleReference();
+            t.reset(tb.getFieldEndOffsets(), tb.getByteArray());
+            return t;
+        } catch (HyracksDataException e) {
+            throw new IllegalStateException("Failed to create CRS search tuple", e);
+        }
+    }
 }
diff --git a/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/MetadataTransactionContext.java b/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/MetadataTransactionContext.java
index 483c4f2..b7d64f4 100644
--- a/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/MetadataTransactionContext.java
+++ b/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/MetadataTransactionContext.java
@@ -29,6 +29,7 @@
 import org.apache.asterix.external.dataset.adapter.AdapterIdentifier;
 import org.apache.asterix.metadata.entities.Catalog;
 import org.apache.asterix.metadata.entities.CompactionPolicy;
+import org.apache.asterix.metadata.entities.CoordinateReferenceSystem;
 import org.apache.asterix.metadata.entities.Database;
 import org.apache.asterix.metadata.entities.Dataset;
 import org.apache.asterix.metadata.entities.DatasourceAdapter;
@@ -169,6 +170,11 @@
         logAndApply(new MetadataLogicalOperation(catalog, true));
     }
 
+    public void addCRS(CoordinateReferenceSystem crs) {
+        droppedCache.dropCrs(crs);
+        logAndApply(new MetadataLogicalOperation(crs, true));
+    }
+
     public void dropDataset(String database, DataverseName dataverseName, String datasetName) {
         Dataset dataset = new Dataset(database, dataverseName, datasetName, null, null, null, null, null, null, null,
                 null, null, -1, MetadataUtil.PENDING_NO_OP, null);
@@ -273,6 +279,12 @@
         logAndApply(new MetadataLogicalOperation(catalog, false));
     }
 
+    public void dropCRS(String database, DataverseName dataverseName, int srid) {
+        CoordinateReferenceSystem crs = new CoordinateReferenceSystem(database, dataverseName, srid, null, null);
+        droppedCache.addCrsIfNotExists(crs);
+        logAndApply(new MetadataLogicalOperation(crs, false));
+    }
+
     public void logAndApply(MetadataLogicalOperation op) {
         opLog.add(op);
         doOperation(op);
@@ -346,6 +358,10 @@
         return droppedCache.getCatalog(catalogName) != null;
     }
 
+    public boolean crsIsDropped(String database, DataverseName dataverseName, int srid) {
+        return droppedCache.getCrs(database, dataverseName, srid) != null;
+    }
+
     public List<MetadataLogicalOperation> getOpLog() {
         return opLog;
     }
diff --git a/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/api/IMetadataManager.java b/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/api/IMetadataManager.java
index acd3fe8..7578b0b 100644
--- a/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/api/IMetadataManager.java
+++ b/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/api/IMetadataManager.java
@@ -31,6 +31,7 @@
 import org.apache.asterix.metadata.MetadataTransactionContext;
 import org.apache.asterix.metadata.entities.Catalog;
 import org.apache.asterix.metadata.entities.CompactionPolicy;
+import org.apache.asterix.metadata.entities.CoordinateReferenceSystem;
 import org.apache.asterix.metadata.entities.Database;
 import org.apache.asterix.metadata.entities.Dataset;
 import org.apache.asterix.metadata.entities.DatasourceAdapter;
@@ -933,4 +934,12 @@
     Catalog getCatalog(MetadataTransactionContext ctx, String catalogName) throws AlgebricksException;
 
     void dropCatalog(MetadataTransactionContext ctx, String catalogName) throws AlgebricksException;
+
+    void addCRS(MetadataTransactionContext ctx, CoordinateReferenceSystem crs) throws AlgebricksException;
+
+    CoordinateReferenceSystem getCRS(MetadataTransactionContext ctx, String database, DataverseName dataverseName,
+            int srid) throws AlgebricksException;
+
+    void dropCRS(MetadataTransactionContext ctx, String database, DataverseName dataverseName, int srid)
+            throws AlgebricksException;
 }
diff --git a/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/api/IMetadataNode.java b/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/api/IMetadataNode.java
index 4c1426a..45d91f2 100644
--- a/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/api/IMetadataNode.java
+++ b/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/api/IMetadataNode.java
@@ -30,6 +30,7 @@
 import org.apache.asterix.external.indexing.ExternalFile;
 import org.apache.asterix.metadata.entities.Catalog;
 import org.apache.asterix.metadata.entities.CompactionPolicy;
+import org.apache.asterix.metadata.entities.CoordinateReferenceSystem;
 import org.apache.asterix.metadata.entities.Database;
 import org.apache.asterix.metadata.entities.Dataset;
 import org.apache.asterix.metadata.entities.DatasourceAdapter;
@@ -1042,4 +1043,30 @@
     Catalog getCatalog(TxnId txnId, String catalogName) throws AlgebricksException, RemoteException;
 
     void dropCatalog(TxnId txnId, String catalogName) throws AlgebricksException, RemoteException;
+
+    void addCRS(TxnId txnId, CoordinateReferenceSystem crs) throws AlgebricksException, RemoteException;
+
+    CoordinateReferenceSystem getCRS(TxnId txnId, String database, DataverseName dataverseName, int srid)
+            throws AlgebricksException, RemoteException;
+
+    void dropCRS(TxnId txnId, String database, DataverseName dataverseName, int srid)
+            throws AlgebricksException, RemoteException;
+
+    /**
+     * Retrieves all CRS definitions belonging to the given dataverse, acquiring local
+     * locks on behalf of the given transaction id.
+     *
+     * @param txnId
+     *            A globally unique id for an active metadata transaction.
+     * @param database
+     *            Name of the database.
+     * @param dataverseName
+     *            Name of the dataverse of which to find all CRS definitions.
+     * @return A list of CRS entity instances.
+     * @throws AlgebricksException
+     *             For example, if the dataverse does not exist.
+     * @throws RemoteException remote exception
+     */
+    List<CoordinateReferenceSystem> getDataverseCRSList(TxnId txnId, String database, DataverseName dataverseName)
+            throws AlgebricksException, RemoteException;
 }
diff --git a/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/bootstrap/CRSEntity.java b/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/bootstrap/CRSEntity.java
new file mode 100644
index 0000000..8b8413e
--- /dev/null
+++ b/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/bootstrap/CRSEntity.java
@@ -0,0 +1,122 @@
+/*
+ * 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.asterix.metadata.bootstrap;
+
+import static org.apache.asterix.metadata.bootstrap.MetadataPrimaryIndexes.PROPERTIES_CRS;
+import static org.apache.asterix.metadata.bootstrap.MetadataRecordTypes.FIELD_NAME_CRS_NAME;
+import static org.apache.asterix.metadata.bootstrap.MetadataRecordTypes.FIELD_NAME_CRS_SRID;
+import static org.apache.asterix.metadata.bootstrap.MetadataRecordTypes.FIELD_NAME_CRS_WKT;
+import static org.apache.asterix.metadata.bootstrap.MetadataRecordTypes.FIELD_NAME_DATABASE_NAME;
+import static org.apache.asterix.metadata.bootstrap.MetadataRecordTypes.FIELD_NAME_DATAVERSE_NAME;
+import static org.apache.asterix.metadata.bootstrap.MetadataRecordTypes.RECORD_NAME_CRS;
+
+import java.util.List;
+
+import org.apache.asterix.om.types.ARecordType;
+import org.apache.asterix.om.types.BuiltinType;
+import org.apache.asterix.om.types.IAType;
+
+public final class CRSEntity {
+
+    private static final CRSEntity CRS =
+            new CRSEntity(new MetadataIndex(PROPERTIES_CRS, 3, new IAType[] { BuiltinType.ASTRING, BuiltinType.AINT32 },
+                    List.of(List.of(FIELD_NAME_DATAVERSE_NAME), List.of(FIELD_NAME_CRS_SRID)), 0, crsType(), true,
+                    new int[] { 0, 1 }), 2, -1);
+
+    private static final CRSEntity DB_CRS =
+            new CRSEntity(
+                    new MetadataIndex(PROPERTIES_CRS, 4,
+                            new IAType[] { BuiltinType.ASTRING, BuiltinType.ASTRING, BuiltinType.AINT32 },
+                            List.of(List.of(FIELD_NAME_DATABASE_NAME), List.of(FIELD_NAME_DATAVERSE_NAME),
+                                    List.of(FIELD_NAME_CRS_SRID)),
+                            0, databaseCrsType(), true, new int[] { 0, 1, 2 }),
+                    3, 0);
+
+    private final int payloadPosition;
+    private final MetadataIndex index;
+    private final int databaseNameIndex;
+    private final int dataverseNameIndex;
+    private final int sridIndex;
+    private final int crsNameIndex;
+    private final int crsWktIndex;
+
+    private CRSEntity(MetadataIndex index, int payloadPosition, int startIndex) {
+        this.index = index;
+        this.payloadPosition = payloadPosition;
+        this.databaseNameIndex = startIndex++;
+        this.dataverseNameIndex = startIndex++;
+        this.sridIndex = startIndex++;
+        this.crsNameIndex = startIndex++;
+        this.crsWktIndex = startIndex++;
+    }
+
+    public static CRSEntity of(boolean usingDatabase) {
+        return usingDatabase ? DB_CRS : CRS;
+    }
+
+    public MetadataIndex getIndex() {
+        return index;
+    }
+
+    public ARecordType getRecordType() {
+        return index.getPayloadRecordType();
+    }
+
+    public int payloadPosition() {
+        return payloadPosition;
+    }
+
+    public int databaseNameIndex() {
+        return databaseNameIndex;
+    }
+
+    public int dataverseNameIndex() {
+        return dataverseNameIndex;
+    }
+
+    public int getSridIndex() {
+        return sridIndex;
+    }
+
+    public int getCrsNameIndex() {
+        return crsNameIndex;
+    }
+
+    public int getCrsWktIndex() {
+        return crsWktIndex;
+    }
+
+    private static ARecordType crsType() {
+        return MetadataRecordTypes.createRecordType(RECORD_NAME_CRS,
+                new String[] { FIELD_NAME_DATAVERSE_NAME, FIELD_NAME_CRS_SRID, FIELD_NAME_CRS_NAME,
+                        FIELD_NAME_CRS_WKT },
+                new IAType[] { BuiltinType.ASTRING, BuiltinType.AINT32, BuiltinType.ASTRING, BuiltinType.ASTRING },
+                true);
+    }
+
+    private static ARecordType databaseCrsType() {
+        return MetadataRecordTypes.createRecordType(RECORD_NAME_CRS,
+                new String[] { FIELD_NAME_DATABASE_NAME, FIELD_NAME_DATAVERSE_NAME, FIELD_NAME_CRS_SRID,
+                        FIELD_NAME_CRS_NAME, FIELD_NAME_CRS_WKT },
+                new IAType[] { BuiltinType.ASTRING, BuiltinType.ASTRING, BuiltinType.AINT32, BuiltinType.ASTRING,
+                        BuiltinType.ASTRING },
+                true);
+    }
+}
diff --git a/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/bootstrap/MetadataBootstrap.java b/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/bootstrap/MetadataBootstrap.java
index 1f8c4dc..c2f75b3 100644
--- a/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/bootstrap/MetadataBootstrap.java
+++ b/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/bootstrap/MetadataBootstrap.java
@@ -177,6 +177,7 @@
                 insertSynonymEntitiesIfNotExist(mdTxnCtx, mdIndexesProvider);
                 insertFullTextConfigAndFilterIfNotExist(mdTxnCtx, mdIndexesProvider);
                 insertCatalogIfNotExist(mdTxnCtx, mdIndexesProvider);
+                insertCRSIfNotExist(mdTxnCtx, mdIndexesProvider);
             }
             // #. initialize datasetIdFactory
             MetadataManager.INSTANCE.initializeDatasetIdFactory(mdTxnCtx);
@@ -390,6 +391,24 @@
         }
     }
 
+    // For backward-compatibility: for old datasets created by an older version of AsterixDB, they
+    // may not have a CRS dataset in the metadata catalog.
+    private static void insertCRSIfNotExist(MetadataTransactionContext mdTxnCtx,
+            MetadataIndexesProvider metadataIndexesProvider) throws AlgebricksException {
+        // We need to insert data types first because datasets depend on data types
+        IAType crsRecordType = metadataIndexesProvider.getCRSEntity().getRecordType();
+        if (MetadataManager.INSTANCE.getDatatype(mdTxnCtx, MetadataConstants.SYSTEM_DATABASE,
+                MetadataConstants.METADATA_DATAVERSE_NAME, crsRecordType.getTypeName()) == null) {
+            MetadataManager.INSTANCE.addDatatype(mdTxnCtx, new Datatype(MetadataConstants.SYSTEM_DATABASE,
+                    MetadataConstants.METADATA_DATAVERSE_NAME, crsRecordType.getTypeName(), crsRecordType, false));
+        }
+        if (MetadataManager.INSTANCE.getDataset(mdTxnCtx, MetadataConstants.SYSTEM_DATABASE,
+                MetadataConstants.METADATA_DATAVERSE_NAME, MetadataConstants.CRS_DATASET_NAME) == null) {
+            insertMetadataDatasets(mdTxnCtx,
+                    new IMetadataIndex[] { metadataIndexesProvider.getCRSEntity().getIndex() });
+        }
+    }
+
     private static DatasourceAdapter getAdapter(String adapterFactoryClassName) throws AlgebricksException {
         try {
             String adapterName =
@@ -627,10 +646,12 @@
                 // Backward-compatibility:
                 // - FULLTEXT_ENTITY_DATASET entity
                 // - Catalog entity
+                // - CRS entity
                 // is added to AsterixDB recently and may not exist in an older dataverse
                 && index != mdIndexesProvider.getFullTextConfigEntity().getIndex()
                 && index != mdIndexesProvider.getFullTextFilterEntity().getIndex()
-                && index != mdIndexesProvider.getCatalogEntity().getIndex()) {
+                && index != mdIndexesProvider.getCatalogEntity().getIndex()
+                && index != mdIndexesProvider.getCRSEntity().getIndex()) {
             throw new IllegalStateException(
                     "attempt to create metadata index " + index.getIndexName() + ". Index should already exist");
         }
diff --git a/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/bootstrap/MetadataIndexesProvider.java b/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/bootstrap/MetadataIndexesProvider.java
index 3b686f4..5519769 100644
--- a/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/bootstrap/MetadataIndexesProvider.java
+++ b/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/bootstrap/MetadataIndexesProvider.java
@@ -104,6 +104,10 @@
         return CatalogEntity.of(usingDatabase);
     }
 
+    public CRSEntity getCRSEntity() {
+        return CRSEntity.of(usingDatabase);
+    }
+
     public IMetadataIndex[] getMetadataIndexes() {
         if (isUsingDatabase()) {
             return new IMetadataIndex[] { getDatabaseEntity().getIndex(), getDataverseEntity().getIndex(),
@@ -113,7 +117,7 @@
                     getFeedPolicyEntity().getIndex(), getLibraryEntity().getIndex(),
                     getCompactionPolicyEntity().getIndex(), getExternalFileEntity().getIndex(),
                     getFeedConnectionEntity().getIndex(), getFullTextConfigEntity().getIndex(),
-                    getFullTextFilterEntity().getIndex(), getCatalogEntity().getIndex() };
+                    getFullTextFilterEntity().getIndex(), getCatalogEntity().getIndex(), getCRSEntity().getIndex() };
         } else {
             return new IMetadataIndex[] { getDataverseEntity().getIndex(), getDatasetEntity().getIndex(),
                     getDatatypeEntity().getIndex(), getIndexEntity().getIndex(), getSynonymEntity().getIndex(),
@@ -122,7 +126,7 @@
                     getFeedPolicyEntity().getIndex(), getLibraryEntity().getIndex(),
                     getCompactionPolicyEntity().getIndex(), getExternalFileEntity().getIndex(),
                     getFeedConnectionEntity().getIndex(), getFullTextConfigEntity().getIndex(),
-                    getFullTextFilterEntity().getIndex(), getCatalogEntity().getIndex() };
+                    getFullTextFilterEntity().getIndex(), getCatalogEntity().getIndex(), getCRSEntity().getIndex() };
         }
     }
 
diff --git a/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/bootstrap/MetadataPrimaryIndexes.java b/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/bootstrap/MetadataPrimaryIndexes.java
index ba0cc2c..7d9f3ed 100644
--- a/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/bootstrap/MetadataPrimaryIndexes.java
+++ b/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/bootstrap/MetadataPrimaryIndexes.java
@@ -64,6 +64,8 @@
             new MetadataIndexImmutableProperties(MetadataConstants.DATABASE_DATASET_NAME, 18, 18);
     public static final MetadataIndexImmutableProperties PROPERTIES_CATALOG =
             new MetadataIndexImmutableProperties(MetadataConstants.CATALOG_DATASET_NAME, 19, 19);
+    public static final MetadataIndexImmutableProperties PROPERTIES_CRS =
+            new MetadataIndexImmutableProperties(MetadataConstants.CRS_DATASET_NAME, 20, 20);
 
     private MetadataPrimaryIndexes() {
     }
diff --git a/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/bootstrap/MetadataRecordTypes.java b/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/bootstrap/MetadataRecordTypes.java
index 820cbb9..179aff1 100644
--- a/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/bootstrap/MetadataRecordTypes.java
+++ b/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/bootstrap/MetadataRecordTypes.java
@@ -134,6 +134,11 @@
     public static final String FIELD_NAME_CATALOG_TYPE = "CatalogType";
     public static final String FIELD_NAME_CATALOG_DETAILS = "CatalogDetails";
 
+    //-------------------------------------- CRS ---------------------------------------//
+    public static final String FIELD_NAME_CRS_SRID = "SRID";
+    public static final String FIELD_NAME_CRS_NAME = "CRSName";
+    public static final String FIELD_NAME_CRS_WKT = "CrsWKT";
+
     //open field
     public static final String FIELD_NAME_CREATOR_NAME = "Name";
     public static final String FIELD_NAME_CREATOR_UUID = "Uuid";
@@ -305,6 +310,9 @@
 
     //-------------------------------------- Catalog ---------------------------------------//
     public static final String RECORD_NAME_CATALOG = "CatalogRecordType";
+
+    //-------------------------------------- CRS ---------------------------------------//
+    public static final String RECORD_NAME_CRS = "CoordinateReferenceSystemRecordType";
     public static final int CATALOG_DETAILS_ARECORD_DATASOURCE_ADAPTER_FIELD_INDEX = 0;
     public static final int CATALOG_DETAILS_ARECORD_PROPERTIES_FIELD_INDEX = 1;
     public static final ARecordType CATALOG_DETAILS_RECORDTYPE =
diff --git a/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/entities/CoordinateReferenceSystem.java b/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/entities/CoordinateReferenceSystem.java
new file mode 100644
index 0000000..283cb6b
--- /dev/null
+++ b/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/entities/CoordinateReferenceSystem.java
@@ -0,0 +1,94 @@
+/*
+ * 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.asterix.metadata.entities;
+
+import java.io.Serial;
+import java.util.Objects;
+
+import org.apache.asterix.common.metadata.DataverseName;
+import org.apache.asterix.metadata.MetadataCache;
+import org.apache.asterix.metadata.api.IMetadataEntity;
+
+public class CoordinateReferenceSystem implements IMetadataEntity<CoordinateReferenceSystem> {
+
+    @Serial
+    private static final long serialVersionUID = 2L;
+
+    private final String databaseName;
+    private final DataverseName dataverseName;
+    private final int srid;
+    private final String crsName;
+    private final String crsWkt;
+
+    public CoordinateReferenceSystem(String databaseName, DataverseName dataverseName, int srid, String crsName,
+            String crsWkt) {
+        this.databaseName = databaseName;
+        this.dataverseName = dataverseName;
+        this.srid = srid;
+        this.crsName = crsName;
+        this.crsWkt = crsWkt;
+    }
+
+    public String getDatabaseName() {
+        return databaseName;
+    }
+
+    public DataverseName getDataverseName() {
+        return dataverseName;
+    }
+
+    public int getSrid() {
+        return srid;
+    }
+
+    public String getCrsName() {
+        return crsName;
+    }
+
+    public String getCrsWkt() {
+        return crsWkt;
+    }
+
+    @Override
+    public CoordinateReferenceSystem addToCache(MetadataCache cache) {
+        return cache.addOrUpdateCrs(this);
+    }
+
+    @Override
+    public CoordinateReferenceSystem dropFromCache(MetadataCache cache) {
+        return cache.dropCrs(this);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (!(o instanceof CoordinateReferenceSystem that)) {
+            return false;
+        }
+        return srid == that.srid && Objects.equals(databaseName, that.databaseName)
+                && Objects.equals(dataverseName, that.dataverseName);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(databaseName, dataverseName, srid);
+    }
+}
diff --git a/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/entitytupletranslators/CRSTupleTranslator.java b/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/entitytupletranslators/CRSTupleTranslator.java
new file mode 100644
index 0000000..373f374
--- /dev/null
+++ b/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/entitytupletranslators/CRSTupleTranslator.java
@@ -0,0 +1,128 @@
+/*
+ * 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.asterix.metadata.entitytupletranslators;
+
+import org.apache.asterix.common.metadata.DataverseName;
+import org.apache.asterix.common.metadata.MetadataUtil;
+import org.apache.asterix.metadata.bootstrap.CRSEntity;
+import org.apache.asterix.metadata.entities.CoordinateReferenceSystem;
+import org.apache.asterix.om.base.AInt32;
+import org.apache.asterix.om.base.AMutableInt32;
+import org.apache.asterix.om.base.ARecord;
+import org.apache.asterix.om.base.AString;
+import org.apache.hyracks.algebricks.common.exceptions.AlgebricksException;
+import org.apache.hyracks.api.exceptions.HyracksDataException;
+import org.apache.hyracks.dataflow.common.data.accessors.ITupleReference;
+
+/**
+ * Translates a CoordinateReferenceSystem metadata entity to an ITupleReference and vice versa.
+ */
+public final class CRSTupleTranslator extends AbstractTupleTranslator<CoordinateReferenceSystem> {
+
+    private final CRSEntity crsEntity;
+    private AMutableInt32 aInt32;
+
+    CRSTupleTranslator(boolean getTuple, CRSEntity crsEntity) {
+        super(getTuple, crsEntity.getIndex(), crsEntity.payloadPosition());
+        this.crsEntity = crsEntity;
+        if (getTuple) {
+            aInt32 = new AMutableInt32(-1);
+        }
+    }
+
+    @Override
+    protected CoordinateReferenceSystem createMetadataEntityFromARecord(ARecord crsRecord) throws AlgebricksException {
+        String dataverseCanonicalName =
+                ((AString) crsRecord.getValueByPos(crsEntity.dataverseNameIndex())).getStringValue();
+        DataverseName dataverseName = DataverseName.createFromCanonicalForm(dataverseCanonicalName);
+        int databaseNameIndex = crsEntity.databaseNameIndex();
+        String databaseName;
+        if (databaseNameIndex >= 0) {
+            databaseName = ((AString) crsRecord.getValueByPos(databaseNameIndex)).getStringValue();
+        } else {
+            databaseName = MetadataUtil.databaseFor(dataverseName);
+        }
+        int srid = ((AInt32) crsRecord.getValueByPos(crsEntity.getSridIndex())).getIntegerValue();
+        String crsName = ((AString) crsRecord.getValueByPos(crsEntity.getCrsNameIndex())).getStringValue();
+        String crsWkt = ((AString) crsRecord.getValueByPos(crsEntity.getCrsWktIndex())).getStringValue();
+        return new CoordinateReferenceSystem(databaseName, dataverseName, srid, crsName, crsWkt);
+    }
+
+    @Override
+    public ITupleReference getTupleFromMetadataEntity(CoordinateReferenceSystem crs) throws HyracksDataException {
+        String dataverseCanonicalName = crs.getDataverseName().getCanonicalForm();
+
+        tupleBuilder.reset();
+
+        // write key fields
+        if (crsEntity.databaseNameIndex() >= 0) {
+            aString.setValue(crs.getDatabaseName());
+            stringSerde.serialize(aString, tupleBuilder.getDataOutput());
+            tupleBuilder.addFieldEndOffset();
+        }
+        aString.setValue(dataverseCanonicalName);
+        stringSerde.serialize(aString, tupleBuilder.getDataOutput());
+        tupleBuilder.addFieldEndOffset();
+        aInt32.setValue(crs.getSrid());
+        int32Serde.serialize(aInt32, tupleBuilder.getDataOutput());
+        tupleBuilder.addFieldEndOffset();
+
+        // write the payload record
+        recordBuilder.reset(crsEntity.getRecordType());
+
+        if (crsEntity.databaseNameIndex() >= 0) {
+            fieldValue.reset();
+            aString.setValue(crs.getDatabaseName());
+            stringSerde.serialize(aString, fieldValue.getDataOutput());
+            recordBuilder.addField(crsEntity.databaseNameIndex(), fieldValue);
+        }
+
+        // write DataverseName
+        fieldValue.reset();
+        aString.setValue(dataverseCanonicalName);
+        stringSerde.serialize(aString, fieldValue.getDataOutput());
+        recordBuilder.addField(crsEntity.dataverseNameIndex(), fieldValue);
+
+        // write SRID
+        fieldValue.reset();
+        aInt32.setValue(crs.getSrid());
+        int32Serde.serialize(aInt32, fieldValue.getDataOutput());
+        recordBuilder.addField(crsEntity.getSridIndex(), fieldValue);
+
+        // write CRSName
+        fieldValue.reset();
+        aString.setValue(crs.getCrsName());
+        stringSerde.serialize(aString, fieldValue.getDataOutput());
+        recordBuilder.addField(crsEntity.getCrsNameIndex(), fieldValue);
+
+        // write CrsWKT
+        fieldValue.reset();
+        aString.setValue(crs.getCrsWkt());
+        stringSerde.serialize(aString, fieldValue.getDataOutput());
+        recordBuilder.addField(crsEntity.getCrsWktIndex(), fieldValue);
+
+        // write the payload record to the tuple
+        recordBuilder.write(tupleBuilder.getDataOutput(), true);
+        tupleBuilder.addFieldEndOffset();
+
+        tuple.reset(tupleBuilder.getFieldEndOffsets(), tupleBuilder.getByteArray());
+        return tuple;
+    }
+}
diff --git a/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/entitytupletranslators/MetadataTupleTranslatorProvider.java b/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/entitytupletranslators/MetadataTupleTranslatorProvider.java
index 74a4940..0fcb6d6 100644
--- a/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/entitytupletranslators/MetadataTupleTranslatorProvider.java
+++ b/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/entitytupletranslators/MetadataTupleTranslatorProvider.java
@@ -107,4 +107,8 @@
     public CatalogTupleTranslator getCatalogTupleTranslator(boolean getTuple) {
         return new CatalogTupleTranslator(getTuple, mdIndexesProvider.getCatalogEntity());
     }
+
+    public CRSTupleTranslator getCRSTupleTranslator(boolean getTuple) {
+        return new CRSTupleTranslator(getTuple, mdIndexesProvider.getCRSEntity());
+    }
 }
diff --git a/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/lock/MetadataLockKey.java b/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/lock/MetadataLockKey.java
index 27a1b84..d42dea5 100644
--- a/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/lock/MetadataLockKey.java
+++ b/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/lock/MetadataLockKey.java
@@ -42,7 +42,8 @@
         MERGE_POLICY,
         NODE_GROUP,
         SYNONYM,
-        CATALOG
+        CATALOG,
+        CRS
     }
 
     private final EntityKind entityKind;
@@ -167,4 +168,8 @@
     static MetadataLockKey createCatalogLockKey(String catalogName) {
         return new MetadataLockKey(EntityKind.CATALOG, null, null, null, catalogName);
     }
+
+    static MetadataLockKey createCRSLockKey(String database, DataverseName dataverseName, int srid) {
+        return new MetadataLockKey(EntityKind.CRS, null, database, dataverseName, String.valueOf(srid));
+    }
 }
diff --git a/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/lock/MetadataLockManager.java b/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/lock/MetadataLockManager.java
index 40c3cd1..0976530 100644
--- a/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/lock/MetadataLockManager.java
+++ b/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/lock/MetadataLockManager.java
@@ -328,4 +328,12 @@
         IMetadataLock lock = mdlocks.computeIfAbsent(key, LOCK_FUNCTION);
         locks.add(IMetadataLock.Mode.WRITE, lock);
     }
+
+    @Override
+    public void acquireCRSWriteLock(LockList locks, String database, DataverseName dataverseName, int srid)
+            throws AlgebricksException {
+        MetadataLockKey key = MetadataLockKey.createCRSLockKey(database, dataverseName, srid);
+        IMetadataLock lock = mdlocks.computeIfAbsent(key, LOCK_FUNCTION);
+        locks.add(IMetadataLock.Mode.WRITE, lock);
+    }
 }
diff --git a/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/utils/MetadataLockUtil.java b/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/utils/MetadataLockUtil.java
index 07f62c9..8887064 100644
--- a/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/utils/MetadataLockUtil.java
+++ b/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/utils/MetadataLockUtil.java
@@ -413,6 +413,22 @@
         lockMgr.acquireCatalogWriteLock(locks, catalogName);
     }
 
+    @Override
+    public void createCRSBegin(IMetadataLockManager lockMgr, LockList locks, String database,
+            DataverseName dataverseName, int srid) throws AlgebricksException {
+        lockMgr.acquireDatabaseReadLock(locks, database);
+        lockMgr.acquireDataverseReadLock(locks, database, dataverseName);
+        lockMgr.acquireCRSWriteLock(locks, database, dataverseName, srid);
+    }
+
+    @Override
+    public void dropCRSBegin(IMetadataLockManager lockMgr, LockList locks, String database, DataverseName dataverseName,
+            int srid) throws AlgebricksException {
+        lockMgr.acquireDatabaseReadLock(locks, database);
+        lockMgr.acquireDataverseReadLock(locks, database, dataverseName);
+        lockMgr.acquireCRSWriteLock(locks, database, dataverseName, srid);
+    }
+
     private static void lockIfDifferentNamespace(IMetadataLockManager lockMgr, LockList locks, String lockedDatabase,
             DataverseName lockedDataverse, String toBeLockedDatabase, DataverseName toBeLockedDataverse)
             throws AlgebricksException {
diff --git a/asterixdb/asterix-om/src/main/java/org/apache/asterix/om/functions/BuiltinFunctions.java b/asterixdb/asterix-om/src/main/java/org/apache/asterix/om/functions/BuiltinFunctions.java
index 144a389..4ca36d3 100644
--- a/asterixdb/asterix-om/src/main/java/org/apache/asterix/om/functions/BuiltinFunctions.java
+++ b/asterixdb/asterix-om/src/main/java/org/apache/asterix/om/functions/BuiltinFunctions.java
@@ -1125,6 +1125,10 @@
     public static final FunctionIdentifier ST_SYM_DIFFERENCE = FunctionConstants.newAsterix("st-sym-difference", 2);
     public static final FunctionIdentifier ST_POLYGONIZE = FunctionConstants.newAsterix("st-polygonize", 1);
 
+    public static final FunctionIdentifier ST_TRANSFORM = FunctionConstants.newAsterix("st-transform", 3);
+    public static final FunctionIdentifier ST_DISTANCE_SPHEROID =
+            FunctionConstants.newAsterix("st-distance-spheroid", 2);
+
     public static final FunctionIdentifier ST_MBR = FunctionConstants.newAsterix("st-mbr", 1);
     public static final FunctionIdentifier ST_MBR_ENLARGE = FunctionConstants.newAsterix("st-mbr-enlarge", 2);
 
@@ -2015,6 +2019,8 @@
         addPrivateFunction(ST_UNION_AGG, AGeometryTypeComputer.INSTANCE, true);
         addPrivateFunction(ST_UNION_SQL_AGG, AGeometryTypeComputer.INSTANCE, true);
         addFunction(ST_POLYGONIZE, AGeometryTypeComputer.INSTANCE, true);
+        addFunction(ST_TRANSFORM, AGeometryTypeComputer.INSTANCE, true);
+        addFunction(ST_DISTANCE_SPHEROID, ADoubleTypeComputer.INSTANCE, true);
 
         addPrivateFunction(ST_MBR, ARectangleTypeComputer.INSTANCE, true);
         addPrivateFunction(ST_MBR_ENLARGE, ARectangleTypeComputer.INSTANCE, true);
diff --git a/asterixdb/asterix-server/pom.xml b/asterixdb/asterix-server/pom.xml
index c7b1e39..2aa04c8 100644
--- a/asterixdb/asterix-server/pom.xml
+++ b/asterixdb/asterix-server/pom.xml
@@ -670,6 +670,11 @@
               <contentFile>xmlenc_0.52_LICENSE.txt</contentFile>
             </license>
             <license>
+              <displayName>a BSD 3-clause license</displayName>
+              <url>https://raw.githubusercontent.com/unitsofmeasurement/unit-api/refs/heads/master/LICENSE</url>
+              <contentFile>javax.measure--unit-api--2.1.3_LICENSE.txt</contentFile>
+            </license>
+            <license>
               <displayName>The Apache Software License, Version 2.0</displayName>
               <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
               <aliasUrls>
@@ -721,6 +726,7 @@
                 <aliasUrl>https://raw.githubusercontent.com/RoaringBitmap/RoaringBitmap/0.9.39/LICENSE</aliasUrl>
                 <aliasUrl>https://raw.githubusercontent.com/JetBrains/java-annotations/master/LICENSE.txt</aliasUrl>
                 <aliasUrl>https://raw.githubusercontent.com/awslabs/aws-crt-java/v0.27.1/LICENSE</aliasUrl>
+                <aliasUrl>https://raw.githubusercontent.com/opengeospatial/geoapi/master/LICENSE.txt</aliasUrl>
               </aliasUrls>
               <metric>1</metric>
             </license>
diff --git a/asterixdb/pom.xml b/asterixdb/pom.xml
index 9369e5c..fd7d670 100644
--- a/asterixdb/pom.xml
+++ b/asterixdb/pom.xml
@@ -125,6 +125,7 @@
     <implementation.url>https://asterixdb.apache.org/</implementation.url>
     <implementation.version>${project.version}</implementation.version>
     <implementation.vendor>${project.organization.name}</implementation.vendor>
+    <sis.version>1.6</sis.version>
   </properties>
 
   <build>
@@ -2025,6 +2026,16 @@
         <artifactId>nessie-client</artifactId>
         <version>${nessieproject.version}</version>
       </dependency>
+      <dependency>
+        <groupId>org.apache.iceberg</groupId>
+        <artifactId>iceberg-bundled-guava</artifactId>
+        <version>${icebergjavasdk.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.sis.core</groupId>
+        <artifactId>sis-referencing</artifactId>
+        <version>${sis.version}</version>
+      </dependency>
     </dependencies>
   </dependencyManagement>
 
diff --git a/asterixdb/src/main/licenses/content/javax.measure--unit-api--2.1.3_LICENSE.txt b/asterixdb/src/main/licenses/content/javax.measure--unit-api--2.1.3_LICENSE.txt
new file mode 100644
index 0000000..4a357aa
--- /dev/null
+++ b/asterixdb/src/main/licenses/content/javax.measure--unit-api--2.1.3_LICENSE.txt
@@ -0,0 +1,23 @@
+====
+    Units of Measurement API
+    Copyright (c) 2014-2020, Jean-Marie Dautelle, Werner Keil, Otavio Santana.
+    All rights reserved.
+    Redistribution and use in source and binary forms, with or without modification,
+    are permitted provided that the following conditions are met:
+    1. Redistributions of source code must retain the above copyright notice,
+       this list of conditions and the following disclaimer.
+    2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions
+       and the following disclaimer in the documentation and/or other materials provided with the distribution.
+    3. Neither the name of JSR-385 nor the names of its contributors may be used to endorse or promote products
+       derived from this software without specific prior written permission.
+    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+    AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+    THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+    ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+    FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+    (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+    LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
+    AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+    (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
+    EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+====
\ No newline at end of file
diff --git a/asterixdb/src/main/licenses/content/raw.githubusercontent.com_opengeospatial_geoapi_master_LICENSE.txt b/asterixdb/src/main/licenses/content/raw.githubusercontent.com_opengeospatial_geoapi_master_LICENSE.txt
new file mode 100644
index 0000000..65fcd5a
--- /dev/null
+++ b/asterixdb/src/main/licenses/content/raw.githubusercontent.com_opengeospatial_geoapi_master_LICENSE.txt
@@ -0,0 +1,171 @@
+
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+   1. Definitions.
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+   END OF TERMS AND CONDITIONS
+   APPENDIX: How to apply the Apache License to your work.
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+   Copyright [yyyy] [name of copyright owner]
+   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.
\ No newline at end of file