diff --git a/build.gradle.kts b/build.gradle.kts
index f102af0b..24187ff1 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -132,6 +132,7 @@ dependencies {
     testImplementation(libs.bundles.test.common)
     testImplementation(libs.mockito.junit.jupiter)
     testRuntimeOnly("org.junit.platform:junit-platform-launcher")
+    testCompileOnly(libs.checker.qual)
 
     integrationTestImplementation(libs.bundles.test.common)
     @Suppress("UnstableApiUsage")
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index d6093fb9..2e091a53 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -23,6 +23,7 @@ mongo-java-driver-sync = "5.3.1"
 slf4j-api = "2.0.16"
 logback-classic = "1.5.16"
 mockito = "5.16.0"
+checker-qual = "3.49.1"
 
 plugin-spotless = "7.0.2"
 plugin-errorprone = "4.1.0"
@@ -42,6 +43,7 @@ mongo-java-driver-sync = { module = "org.mongodb:mongodb-driver-sync", version.r
 sl4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j-api" }
 logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "logback-classic" }
 mockito-junit-jupiter = { module = "org.mockito:mockito-junit-jupiter", version.ref = "mockito" }
+checker-qual = { module = "org.checkerframework:checker-qual", version.ref = "checker-qual" }
 
 [bundles]
 test-common = ["junit-jupiter", "assertj", "logback-classic"]
diff --git a/src/integrationTest/java/com/mongodb/hibernate/BasicCrudIntegrationTests.java b/src/integrationTest/java/com/mongodb/hibernate/BasicCrudIntegrationTests.java
index f61d4a01..78c08eff 100644
--- a/src/integrationTest/java/com/mongodb/hibernate/BasicCrudIntegrationTests.java
+++ b/src/integrationTest/java/com/mongodb/hibernate/BasicCrudIntegrationTests.java
@@ -198,6 +198,45 @@ void testDynamicUpdate() {
         }
     }
 
+    @Nested
+    class SelectTests {
+
+        @Test
+        void testGetByPrimaryKeyWithoutNullValueField() {
+            var book = new Book();
+            book.id = 1;
+            book.author = "Marcel Proust";
+            book.title = "In Search of Lost Time";
+            book.publishYear = 1913;
+
+            sessionFactoryScope.inTransaction(session -> session.persist(book));
+
+            var loadedBook = sessionFactoryScope.fromTransaction(session -> session.get(Book.class, 1));
+            assertThat(loadedBook)
+                    .isNotNull()
+                    .usingRecursiveComparison()
+                    .withStrictTypeChecking()
+                    .isEqualTo(book);
+        }
+
+        @Test
+        void testGetByPrimaryKeyWithNullValueField() {
+            var book = new Book();
+            book.id = 1;
+            book.title = "Brave New World";
+            book.publishYear = 1932;
+
+            sessionFactoryScope.inTransaction(session -> session.persist(book));
+
+            var loadedBook = sessionFactoryScope.fromTransaction(session -> session.get(Book.class, 1));
+            assertThat(loadedBook)
+                    .isNotNull()
+                    .usingRecursiveComparison()
+                    .withStrictTypeChecking()
+                    .isEqualTo(book);
+        }
+    }
+
     private static void assertCollectionContainsExactly(BsonDocument expectedDoc) {
         assertThat(mongoCollection.find()).containsExactly(expectedDoc);
     }
diff --git a/src/integrationTest/resources/logback-test.xml b/src/integrationTest/resources/logback-test.xml
index a70fad20..c6ffeff3 100644
--- a/src/integrationTest/resources/logback-test.xml
+++ b/src/integrationTest/resources/logback-test.xml
@@ -10,7 +10,9 @@
     <logger name="org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator" level="debug" additivity="true"/>
     <logger name="org.hibernate.SQL" level="debug" additivity="true"/>
     <logger name="org.hibernate.orm.jdbc.bind" level="trace" additivity="true"/>
+    <logger name="org.hibernate.orm.jdbc.extract" level="trace" additivity="true"/>
     <logger name="org.hibernate.persister.entity" level="debug" additivity="true"/>
+    <logger name="org.hibernate.orm.sql.ast.tree" level="debug" additivity="true"/>
     <logger name="org.mongodb.driver" level="warn" additivity="true"/>
     <logger name="com.mongodb.hibernate" level="debug" additivity="true"/>
     <root level="info">
diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java
index 5fe51746..b3fcff3d 100644
--- a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java
+++ b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java
@@ -19,8 +19,13 @@
 import static com.mongodb.hibernate.internal.MongoAssertions.assertNotNull;
 import static com.mongodb.hibernate.internal.MongoAssertions.assertTrue;
 import static com.mongodb.hibernate.internal.MongoConstants.ID_FIELD_NAME;
+import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.COLLECTION_AGGREGATE;
 import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.COLLECTION_MUTATION;
+import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.COLLECTION_NAME;
+import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.FIELD_PATH;
 import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.FIELD_VALUE;
+import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.FILTER;
+import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.PROJECT_STAGE_SPECIFICATIONS;
 import static com.mongodb.hibernate.internal.translate.mongoast.filter.AstComparisonFilterOperator.EQ;
 import static java.lang.String.format;
 
@@ -31,16 +36,24 @@
 import com.mongodb.hibernate.internal.translate.mongoast.AstFieldUpdate;
 import com.mongodb.hibernate.internal.translate.mongoast.AstNode;
 import com.mongodb.hibernate.internal.translate.mongoast.AstParameterMarker;
+import com.mongodb.hibernate.internal.translate.mongoast.command.AstCommand;
 import com.mongodb.hibernate.internal.translate.mongoast.command.AstDeleteCommand;
 import com.mongodb.hibernate.internal.translate.mongoast.command.AstInsertCommand;
 import com.mongodb.hibernate.internal.translate.mongoast.command.AstUpdateCommand;
+import com.mongodb.hibernate.internal.translate.mongoast.command.aggregate.AstAggregateCommand;
+import com.mongodb.hibernate.internal.translate.mongoast.command.aggregate.AstMatchStage;
+import com.mongodb.hibernate.internal.translate.mongoast.command.aggregate.AstProjectStage;
+import com.mongodb.hibernate.internal.translate.mongoast.command.aggregate.AstProjectStageIncludeSpecification;
+import com.mongodb.hibernate.internal.translate.mongoast.command.aggregate.AstProjectStageSpecification;
 import com.mongodb.hibernate.internal.translate.mongoast.filter.AstComparisonFilterOperation;
+import com.mongodb.hibernate.internal.translate.mongoast.filter.AstComparisonFilterOperator;
 import com.mongodb.hibernate.internal.translate.mongoast.filter.AstFieldOperationFilter;
 import com.mongodb.hibernate.internal.translate.mongoast.filter.AstFilter;
 import com.mongodb.hibernate.internal.translate.mongoast.filter.AstFilterFieldPath;
 import java.io.IOException;
 import java.io.StringWriter;
 import java.util.ArrayList;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
 import org.bson.json.JsonMode;
@@ -48,7 +61,10 @@
 import org.bson.json.JsonWriterSettings;
 import org.hibernate.engine.spi.SessionFactoryImplementor;
 import org.hibernate.internal.util.collections.Stack;
+import org.hibernate.persister.entity.EntityPersister;
 import org.hibernate.persister.internal.SqlFragmentPredicate;
+import org.hibernate.query.spi.QueryOptions;
+import org.hibernate.query.sqm.ComparisonOperator;
 import org.hibernate.query.sqm.tree.expression.Conversion;
 import org.hibernate.sql.ast.Clause;
 import org.hibernate.sql.ast.SqlAstNodeRenderingMode;
@@ -143,6 +159,8 @@ abstract class AbstractMqlTranslator<T extends JdbcOperation> implements SqlAstT
 
     private final List<JdbcParameterBinder> parameterBinders = new ArrayList<>();
 
+    private final Set<String> affectedTableNames = new HashSet<>();
+
     AbstractMqlTranslator(SessionFactoryImplementor sessionFactory) {
         this.sessionFactory = sessionFactory;
         assertNotNull(sessionFactory
@@ -178,7 +196,7 @@ public Stack<Clause> getCurrentClauseStack() {
 
     @Override
     public Set<String> getAffectedTableNames() {
-        throw new FeatureNotSupportedException("TODO-HIBERNATE-22 https://jira.mongodb.org/browse/HIBERNATE-22");
+        return affectedTableNames;
     }
 
     List<JdbcParameterBinder> getParameterBinders() {
@@ -197,12 +215,12 @@ static String renderMongoAstNode(AstNode rootAstNode) {
     }
 
     @SuppressWarnings("overloads")
-    <R extends AstNode> R acceptAndYield(Statement statement, AstVisitorValueDescriptor<R> resultDescriptor) {
+    <R extends AstCommand> R acceptAndYield(Statement statement, AstVisitorValueDescriptor<R> resultDescriptor) {
         return astVisitorValueHolder.execute(resultDescriptor, () -> statement.accept(this));
     }
 
     @SuppressWarnings("overloads")
-    <R extends AstNode> R acceptAndYield(SqlAstNode node, AstVisitorValueDescriptor<R> resultDescriptor) {
+    <R> R acceptAndYield(SqlAstNode node, AstVisitorValueDescriptor<R> resultDescriptor) {
         return astVisitorValueHolder.execute(resultDescriptor, () -> node.accept(this));
     }
 
@@ -217,17 +235,16 @@ public void visitStandardTableInsert(TableInsertStandard tableInsert) {
         var astElements = new ArrayList<AstElement>(tableInsert.getNumberOfValueBindings());
         for (var columnValueBinding : tableInsert.getValueBindings()) {
             var fieldName = columnValueBinding.getColumnReference().getColumnExpression();
-
             var valueExpression = columnValueBinding.getValueExpression();
             if (valueExpression == null) {
                 throw new FeatureNotSupportedException();
             }
             var fieldValue = acceptAndYield(valueExpression, FIELD_VALUE);
-
             astElements.add(new AstElement(fieldName, fieldValue));
         }
         astVisitorValueHolder.yield(
-                COLLECTION_MUTATION, new AstInsertCommand(tableInsert.getTableName(), new AstDocument(astElements)));
+                COLLECTION_MUTATION,
+                new AstInsertCommand(tableInsert.getMutatingTable().getTableName(), new AstDocument(astElements)));
     }
 
     @Override
@@ -301,7 +318,111 @@ public void visitParameter(JdbcParameter jdbcParameter) {
 
     @Override
     public void visitSelectStatement(SelectStatement selectStatement) {
-        throw new FeatureNotSupportedException("TODO-HIBERNATE-22 https://jira.mongodb.org/browse/HIBERNATE-22");
+        if (!selectStatement.getQueryPart().isRoot()) {
+            throw new FeatureNotSupportedException("Subquery not supported");
+        }
+        if (!selectStatement.getCteStatements().isEmpty()
+                || !selectStatement.getCteObjects().isEmpty()) {
+            throw new FeatureNotSupportedException("CTE not supported");
+        }
+        selectStatement.getQueryPart().accept(this);
+    }
+
+    @Override
+    public void visitQuerySpec(QuerySpec querySpec) {
+        if (!querySpec.getGroupByClauseExpressions().isEmpty()) {
+            throw new FeatureNotSupportedException("GroupBy not supported");
+        }
+        if (querySpec.hasSortSpecifications()) {
+            throw new FeatureNotSupportedException("Sorting not supported");
+        }
+        if (querySpec.hasOffsetOrFetchClause()) {
+            throw new FeatureNotSupportedException("TO-DO-HIBERNATE-70 https://jira.mongodb.org/browse/HIBERNATE-70");
+        }
+
+        var collection = acceptAndYield(querySpec.getFromClause(), COLLECTION_NAME);
+
+        var whereClauseRestrictions = querySpec.getWhereClauseRestrictions();
+        var filter = whereClauseRestrictions == null || whereClauseRestrictions.isEmpty()
+                ? null
+                : acceptAndYield(whereClauseRestrictions, FILTER);
+
+        var projectStageSpecifications = acceptAndYield(querySpec.getSelectClause(), PROJECT_STAGE_SPECIFICATIONS);
+
+        var stages = filter == null
+                ? List.of(new AstProjectStage(projectStageSpecifications))
+                : List.of(new AstMatchStage(filter), new AstProjectStage(projectStageSpecifications));
+        astVisitorValueHolder.yield(COLLECTION_AGGREGATE, new AstAggregateCommand(collection, stages));
+    }
+
+    @Override
+    public void visitFromClause(FromClause fromClause) {
+        if (fromClause.getRoots().size() != 1) {
+            throw new FeatureNotSupportedException();
+        }
+        var tableGroup = fromClause.getRoots().get(0);
+
+        if (!(tableGroup.getModelPart() instanceof EntityPersister entityPersister)
+                || entityPersister.getQuerySpaces().length != 1) {
+            throw new FeatureNotSupportedException();
+        }
+
+        affectedTableNames.add(((String[]) entityPersister.getQuerySpaces())[0]);
+        tableGroup.getPrimaryTableReference().accept(this);
+    }
+
+    @Override
+    public void visitNamedTableReference(NamedTableReference namedTableReference) {
+        astVisitorValueHolder.yield(COLLECTION_NAME, namedTableReference.getTableExpression());
+    }
+
+    @Override
+    public void visitRelationalPredicate(ComparisonPredicate comparisonPredicate) {
+        var astComparisonFilterOperator = getAstComparisonFilterOperator(comparisonPredicate.getOperator());
+
+        var fieldPath = acceptAndYield(comparisonPredicate.getLeftHandExpression(), FIELD_PATH);
+        var fieldValue = acceptAndYield(comparisonPredicate.getRightHandExpression(), FIELD_VALUE);
+
+        var filter = new AstFieldOperationFilter(
+                new AstFilterFieldPath(fieldPath),
+                new AstComparisonFilterOperation(astComparisonFilterOperator, fieldValue));
+        astVisitorValueHolder.yield(FILTER, filter);
+    }
+
+    private static AstComparisonFilterOperator getAstComparisonFilterOperator(ComparisonOperator operator) {
+        return switch (operator) {
+            case EQUAL -> EQ;
+            default -> throw new FeatureNotSupportedException("Unsupported operator: " + operator.name());
+        };
+    }
+
+    @Override
+    public void visitSelectClause(SelectClause selectClause) {
+        if (selectClause.isDistinct()) {
+            throw new FeatureNotSupportedException();
+        }
+        var projectStageSpecifications = new ArrayList<AstProjectStageSpecification>(
+                selectClause.getSqlSelections().size());
+
+        for (SqlSelection sqlSelection : selectClause.getSqlSelections()) {
+            if (sqlSelection.isVirtual()) {
+                continue;
+            }
+            if (!(sqlSelection.getExpression() instanceof ColumnReference columnReference)) {
+                throw new FeatureNotSupportedException();
+            }
+            var field = acceptAndYield(columnReference, FIELD_PATH);
+            projectStageSpecifications.add(new AstProjectStageIncludeSpecification(field));
+        }
+        astVisitorValueHolder.yield(PROJECT_STAGE_SPECIFICATIONS, projectStageSpecifications);
+    }
+
+    @Override
+    public void visitColumnReference(ColumnReference columnReference) {
+        if (columnReference.isColumnExpressionFormula()) {
+            throw new FeatureNotSupportedException();
+        }
+        astVisitorValueHolder.yield(FIELD_PATH, columnReference.getColumnExpression());
     }
 
     @Override
@@ -329,11 +450,6 @@ public void visitQueryGroup(QueryGroup queryGroup) {
         throw new FeatureNotSupportedException();
     }
 
-    @Override
-    public void visitQuerySpec(QuerySpec querySpec) {
-        throw new FeatureNotSupportedException();
-    }
-
     @Override
     public void visitSortSpecification(SortSpecification sortSpecification) {
         throw new FeatureNotSupportedException();
@@ -344,21 +460,11 @@ public void visitOffsetFetchClause(QueryPart queryPart) {
         throw new FeatureNotSupportedException();
     }
 
-    @Override
-    public void visitSelectClause(SelectClause selectClause) {
-        throw new FeatureNotSupportedException();
-    }
-
     @Override
     public void visitSqlSelection(SqlSelection sqlSelection) {
         throw new FeatureNotSupportedException();
     }
 
-    @Override
-    public void visitFromClause(FromClause fromClause) {
-        throw new FeatureNotSupportedException();
-    }
-
     @Override
     public void visitTableGroup(TableGroup tableGroup) {
         throw new FeatureNotSupportedException();
@@ -369,11 +475,6 @@ public void visitTableGroupJoin(TableGroupJoin tableGroupJoin) {
         throw new FeatureNotSupportedException();
     }
 
-    @Override
-    public void visitNamedTableReference(NamedTableReference namedTableReference) {
-        throw new FeatureNotSupportedException();
-    }
-
     @Override
     public void visitValuesTableReference(ValuesTableReference valuesTableReference) {
         throw new FeatureNotSupportedException();
@@ -394,11 +495,6 @@ public void visitTableReferenceJoin(TableReferenceJoin tableReferenceJoin) {
         throw new FeatureNotSupportedException();
     }
 
-    @Override
-    public void visitColumnReference(ColumnReference columnReference) {
-        throw new FeatureNotSupportedException();
-    }
-
     @Override
     public void visitNestedColumnReference(NestedColumnReference nestedColumnReference) {
         throw new FeatureNotSupportedException();
@@ -609,11 +705,6 @@ public void visitThruthnessPredicate(ThruthnessPredicate thruthnessPredicate) {
         throw new FeatureNotSupportedException();
     }
 
-    @Override
-    public void visitRelationalPredicate(ComparisonPredicate comparisonPredicate) {
-        throw new FeatureNotSupportedException();
-    }
-
     @Override
     public void visitSelfRenderingPredicate(SelfRenderingPredicate selfRenderingPredicate) {
         throw new FeatureNotSupportedException();
@@ -653,4 +744,56 @@ public void visitOptionalTableUpdate(OptionalTableUpdate optionalTableUpdate) {
     public void visitCustomTableUpdate(TableUpdateCustomSql tableUpdateCustomSql) {
         throw new FeatureNotSupportedException();
     }
+
+    void checkQueryOptionsSupportability(QueryOptions queryOptions) {
+        if (queryOptions.getTimeout() != null) {
+            throw new FeatureNotSupportedException("'timeout' inQueryOptions not supported");
+        }
+        if (queryOptions.getFlushMode() != null) {
+            throw new FeatureNotSupportedException("'flushMode' in QueryOptions not supported");
+        }
+        if (Boolean.TRUE.equals(queryOptions.isReadOnly())) {
+            throw new FeatureNotSupportedException("'readOnly' in QueryOptions not supported");
+        }
+        if (queryOptions.getAppliedGraph() != null) {
+            throw new FeatureNotSupportedException("'appliedGraph' in QueryOptions not supported");
+        }
+        if (queryOptions.getTupleTransformer() != null) {
+            throw new FeatureNotSupportedException("'tupleTransformer' in QueryOptions not supported");
+        }
+        if (queryOptions.getResultListTransformer() != null) {
+            throw new FeatureNotSupportedException("'resultListTransformer' in QueryOptions not supported");
+        }
+        if (Boolean.TRUE.equals(queryOptions.isResultCachingEnabled())) {
+            throw new FeatureNotSupportedException("'resultCaching' in QueryOptions not supported");
+        }
+        if (queryOptions.getDisabledFetchProfiles() != null
+                && !queryOptions.getDisabledFetchProfiles().isEmpty()) {
+            throw new FeatureNotSupportedException("'disabledFetchProfiles' in QueryOptions not supported");
+        }
+        if (queryOptions.getEnabledFetchProfiles() != null
+                && !queryOptions.getEnabledFetchProfiles().isEmpty()) {
+            throw new FeatureNotSupportedException("'enabledFetchProfiles' in QueryOptions not supported");
+        }
+        if (Boolean.TRUE.equals(queryOptions.getQueryPlanCachingEnabled())) {
+            throw new FeatureNotSupportedException("'queryPlanCaching' in QueryOptions not supported");
+        }
+        if (queryOptions.getLockOptions() != null
+                && !queryOptions.getLockOptions().isEmpty()) {
+            throw new FeatureNotSupportedException("'lockOptions' in QueryOptions not supported");
+        }
+        if (queryOptions.getComment() != null) {
+            throw new FeatureNotSupportedException("TO-DO-HIBERNATE-53 https://jira.mongodb.org/browse/HIBERNATE-53");
+        }
+        if (queryOptions.getDatabaseHints() != null
+                && !queryOptions.getDatabaseHints().isEmpty()) {
+            throw new FeatureNotSupportedException("'databaseHints' in QueryOptions not supported");
+        }
+        if (queryOptions.getFetchSize() != null) {
+            throw new FeatureNotSupportedException("TO-DO-HIBERNATE-54 https://jira.mongodb.org/browse/HIBERNATE-54");
+        }
+        if (queryOptions.getLimit() != null && !queryOptions.getLimit().isEmpty()) {
+            throw new FeatureNotSupportedException("TO-DO-HIBERNATE-70 https://jira.mongodb.org/browse/HIBERNATE-70");
+        }
+    }
 }
diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/AstVisitorValueDescriptor.java b/src/main/java/com/mongodb/hibernate/internal/translate/AstVisitorValueDescriptor.java
index f261657d..399a4931 100644
--- a/src/main/java/com/mongodb/hibernate/internal/translate/AstVisitorValueDescriptor.java
+++ b/src/main/java/com/mongodb/hibernate/internal/translate/AstVisitorValueDescriptor.java
@@ -19,19 +19,31 @@
 import static com.mongodb.hibernate.internal.MongoAssertions.assertNotNull;
 import static com.mongodb.hibernate.internal.MongoAssertions.fail;
 
-import com.mongodb.hibernate.internal.translate.mongoast.AstNode;
 import com.mongodb.hibernate.internal.translate.mongoast.AstValue;
+import com.mongodb.hibernate.internal.translate.mongoast.command.AstCommand;
+import com.mongodb.hibernate.internal.translate.mongoast.command.aggregate.AstProjectStageSpecification;
+import com.mongodb.hibernate.internal.translate.mongoast.filter.AstFilter;
 import java.lang.reflect.Modifier;
 import java.util.Collections;
 import java.util.IdentityHashMap;
+import java.util.List;
 import java.util.Map;
 
 @SuppressWarnings("UnusedTypeParameter")
 final class AstVisitorValueDescriptor<T> {
 
-    static final AstVisitorValueDescriptor<AstNode> COLLECTION_MUTATION = new AstVisitorValueDescriptor<>();
+    static final AstVisitorValueDescriptor<AstCommand> COLLECTION_MUTATION = new AstVisitorValueDescriptor<>();
+    static final AstVisitorValueDescriptor<AstCommand> COLLECTION_AGGREGATE = new AstVisitorValueDescriptor<>();
+
+    static final AstVisitorValueDescriptor<String> COLLECTION_NAME = new AstVisitorValueDescriptor<>();
+
+    static final AstVisitorValueDescriptor<String> FIELD_PATH = new AstVisitorValueDescriptor<>();
     static final AstVisitorValueDescriptor<AstValue> FIELD_VALUE = new AstVisitorValueDescriptor<>();
 
+    static final AstVisitorValueDescriptor<List<AstProjectStageSpecification>> PROJECT_STAGE_SPECIFICATIONS =
+            new AstVisitorValueDescriptor<>();
+    static final AstVisitorValueDescriptor<AstFilter> FILTER = new AstVisitorValueDescriptor<>();
+
     private static final Map<AstVisitorValueDescriptor<?>, String> CONSTANT_TOSTRING_CONTENT_MAP;
 
     static {
diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/TableMutationMqlTranslator.java b/src/main/java/com/mongodb/hibernate/internal/translate/ModelMutationMqlTranslator.java
similarity index 81%
rename from src/main/java/com/mongodb/hibernate/internal/translate/TableMutationMqlTranslator.java
rename to src/main/java/com/mongodb/hibernate/internal/translate/ModelMutationMqlTranslator.java
index 7b9ecc27..071c8fdf 100644
--- a/src/main/java/com/mongodb/hibernate/internal/translate/TableMutationMqlTranslator.java
+++ b/src/main/java/com/mongodb/hibernate/internal/translate/ModelMutationMqlTranslator.java
@@ -26,11 +26,11 @@
 import org.hibernate.sql.model.jdbc.JdbcMutationOperation;
 import org.jspecify.annotations.Nullable;
 
-final class TableMutationMqlTranslator<O extends JdbcMutationOperation> extends AbstractMqlTranslator<O> {
+final class ModelMutationMqlTranslator<O extends JdbcMutationOperation> extends AbstractMqlTranslator<O> {
 
     private final TableMutation<O> tableMutation;
 
-    TableMutationMqlTranslator(TableMutation<O> tableMutation, SessionFactoryImplementor sessionFactory) {
+    ModelMutationMqlTranslator(TableMutation<O> tableMutation, SessionFactoryImplementor sessionFactory) {
         super(sessionFactory);
         this.tableMutation = tableMutation;
     }
@@ -38,9 +38,9 @@ final class TableMutationMqlTranslator<O extends JdbcMutationOperation> extends
     @Override
     public O translate(@Nullable JdbcParameterBindings jdbcParameterBindings, QueryOptions queryOptions) {
         assertNull(jdbcParameterBindings);
-        // QueryOptions class is not applicable to table mutation so a dummy value is always passed in
+        checkQueryOptionsSupportability(queryOptions);
 
-        var rootAstNode = acceptAndYield(tableMutation, COLLECTION_MUTATION);
-        return tableMutation.createMutationOperation(renderMongoAstNode(rootAstNode), getParameterBinders());
+        var mutationCommand = acceptAndYield(tableMutation, COLLECTION_MUTATION);
+        return tableMutation.createMutationOperation(renderMongoAstNode(mutationCommand), getParameterBinders());
     }
 }
diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/MongoTranslatorFactory.java b/src/main/java/com/mongodb/hibernate/internal/translate/MongoTranslatorFactory.java
index b2a9c05f..9a1a87fe 100644
--- a/src/main/java/com/mongodb/hibernate/internal/translate/MongoTranslatorFactory.java
+++ b/src/main/java/com/mongodb/hibernate/internal/translate/MongoTranslatorFactory.java
@@ -30,8 +30,7 @@ public final class MongoTranslatorFactory implements SqlAstTranslatorFactory {
     @Override
     public SqlAstTranslator<JdbcOperationQuerySelect> buildSelectTranslator(
             SessionFactoryImplementor sessionFactoryImplementor, SelectStatement selectStatement) {
-        // TODO-HIBERNATE-22 https://jira.mongodb.org/browse/HIBERNATE-22
-        return new NoopSqlAstTranslator<>();
+        return new SelectMqlTranslator(sessionFactoryImplementor, selectStatement);
     }
 
     @Override
@@ -44,6 +43,6 @@ public SqlAstTranslator<? extends JdbcOperationQueryMutation> buildMutationTrans
     @Override
     public <O extends JdbcMutationOperation> SqlAstTranslator<O> buildModelMutationTranslator(
             TableMutation<O> tableMutation, SessionFactoryImplementor sessionFactoryImplementor) {
-        return new TableMutationMqlTranslator<>(tableMutation, sessionFactoryImplementor);
+        return new ModelMutationMqlTranslator<>(tableMutation, sessionFactoryImplementor);
     }
 }
diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/SelectMqlTranslator.java b/src/main/java/com/mongodb/hibernate/internal/translate/SelectMqlTranslator.java
new file mode 100644
index 00000000..dfa6e131
--- /dev/null
+++ b/src/main/java/com/mongodb/hibernate/internal/translate/SelectMqlTranslator.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2025-present MongoDB, Inc.
+ *
+ * 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.
+ */
+
+package com.mongodb.hibernate.internal.translate;
+
+import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.COLLECTION_AGGREGATE;
+import static org.hibernate.sql.ast.SqlTreePrinter.logSqlAst;
+
+import com.mongodb.hibernate.internal.FeatureNotSupportedException;
+import org.hibernate.engine.spi.SessionFactoryImplementor;
+import org.hibernate.query.spi.QueryOptions;
+import org.hibernate.sql.ast.tree.Statement;
+import org.hibernate.sql.ast.tree.select.SelectStatement;
+import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect;
+import org.hibernate.sql.exec.spi.JdbcParameterBindings;
+import org.hibernate.sql.results.jdbc.spi.JdbcValuesMappingProducerProvider;
+import org.jspecify.annotations.Nullable;
+
+final class SelectMqlTranslator extends AbstractMqlTranslator<JdbcOperationQuerySelect> {
+
+    private final SelectStatement selectStatement;
+    private final JdbcValuesMappingProducerProvider jdbcValuesMappingProducerProvider;
+
+    SelectMqlTranslator(SessionFactoryImplementor sessionFactory, SelectStatement selectStatement) {
+        super(sessionFactory);
+        this.selectStatement = selectStatement;
+        jdbcValuesMappingProducerProvider =
+                sessionFactory.getServiceRegistry().requireService(JdbcValuesMappingProducerProvider.class);
+    }
+
+    @Override
+    public JdbcOperationQuerySelect translate(
+            @Nullable JdbcParameterBindings jdbcParameterBindings, QueryOptions queryOptions) {
+
+        logSqlAst(selectStatement);
+
+        if (jdbcParameterBindings != null) {
+            throw new FeatureNotSupportedException();
+        }
+        checkQueryOptionsSupportability(queryOptions);
+
+        var aggregateCommand = acceptAndYield((Statement) selectStatement, COLLECTION_AGGREGATE);
+        var jdbcValuesMappingProducer =
+                jdbcValuesMappingProducerProvider.buildMappingProducer(selectStatement, getSessionFactory());
+
+        return new JdbcOperationQuerySelect(
+                renderMongoAstNode(aggregateCommand),
+                getParameterBinders(),
+                jdbcValuesMappingProducer,
+                getAffectedTableNames());
+    }
+}
diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/command/AstCommand.java b/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/command/AstCommand.java
new file mode 100644
index 00000000..c76c9083
--- /dev/null
+++ b/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/command/AstCommand.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2025-present MongoDB, Inc.
+ *
+ * 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.
+ */
+
+package com.mongodb.hibernate.internal.translate.mongoast.command;
+
+import com.mongodb.hibernate.internal.translate.mongoast.AstNode;
+
+public interface AstCommand extends AstNode {}
diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/command/AstDeleteCommand.java b/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/command/AstDeleteCommand.java
index 75a83e5c..6c2e10f6 100644
--- a/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/command/AstDeleteCommand.java
+++ b/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/command/AstDeleteCommand.java
@@ -16,11 +16,10 @@
 
 package com.mongodb.hibernate.internal.translate.mongoast.command;
 
-import com.mongodb.hibernate.internal.translate.mongoast.AstNode;
 import com.mongodb.hibernate.internal.translate.mongoast.filter.AstFilter;
 import org.bson.BsonWriter;
 
-public record AstDeleteCommand(String collection, AstFilter filter) implements AstNode {
+public record AstDeleteCommand(String collection, AstFilter filter) implements AstCommand {
     @Override
     public void render(BsonWriter writer) {
         writer.writeStartDocument();
diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/command/AstInsertCommand.java b/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/command/AstInsertCommand.java
index 586b7e5e..a8358991 100644
--- a/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/command/AstInsertCommand.java
+++ b/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/command/AstInsertCommand.java
@@ -17,10 +17,9 @@
 package com.mongodb.hibernate.internal.translate.mongoast.command;
 
 import com.mongodb.hibernate.internal.translate.mongoast.AstDocument;
-import com.mongodb.hibernate.internal.translate.mongoast.AstNode;
 import org.bson.BsonWriter;
 
-public record AstInsertCommand(String collection, AstDocument document) implements AstNode {
+public record AstInsertCommand(String collection, AstDocument document) implements AstCommand {
     @Override
     public void render(BsonWriter writer) {
         writer.writeStartDocument();
diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/command/AstUpdateCommand.java b/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/command/AstUpdateCommand.java
index 0e93a639..8a6d58eb 100644
--- a/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/command/AstUpdateCommand.java
+++ b/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/command/AstUpdateCommand.java
@@ -17,13 +17,12 @@
 package com.mongodb.hibernate.internal.translate.mongoast.command;
 
 import com.mongodb.hibernate.internal.translate.mongoast.AstFieldUpdate;
-import com.mongodb.hibernate.internal.translate.mongoast.AstNode;
 import com.mongodb.hibernate.internal.translate.mongoast.filter.AstFilter;
 import java.util.List;
 import org.bson.BsonWriter;
 
 public record AstUpdateCommand(String collection, AstFilter filter, List<? extends AstFieldUpdate> updates)
-        implements AstNode {
+        implements AstCommand {
     @Override
     public void render(BsonWriter writer) {
         writer.writeStartDocument();
diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstAggregateCommand.java b/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstAggregateCommand.java
new file mode 100644
index 00000000..5e287332
--- /dev/null
+++ b/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstAggregateCommand.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2025-present MongoDB, Inc.
+ *
+ * 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.
+ */
+
+package com.mongodb.hibernate.internal.translate.mongoast.command.aggregate;
+
+import com.mongodb.hibernate.internal.translate.mongoast.command.AstCommand;
+import java.util.List;
+import org.bson.BsonWriter;
+
+public record AstAggregateCommand(String collection, List<? extends AstStage> stages) implements AstCommand {
+
+    @Override
+    public void render(BsonWriter writer) {
+        writer.writeStartDocument();
+        {
+            writer.writeString("aggregate", collection);
+            writer.writeName("pipeline");
+            writer.writeStartArray();
+            {
+                stages.forEach(stage -> stage.render(writer));
+            }
+            writer.writeEndArray();
+        }
+        writer.writeEndDocument();
+    }
+}
diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstMatchStage.java b/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstMatchStage.java
new file mode 100644
index 00000000..d219aaf4
--- /dev/null
+++ b/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstMatchStage.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2025-present MongoDB, Inc.
+ *
+ * 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.
+ */
+
+package com.mongodb.hibernate.internal.translate.mongoast.command.aggregate;
+
+import com.mongodb.hibernate.internal.translate.mongoast.filter.AstFilter;
+import org.bson.BsonWriter;
+
+public record AstMatchStage(AstFilter filter) implements AstStage {
+    @Override
+    public void render(BsonWriter writer) {
+        writer.writeStartDocument();
+        {
+            writer.writeName("$match");
+            filter.render(writer);
+        }
+        writer.writeEndDocument();
+    }
+}
diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstProjectStage.java b/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstProjectStage.java
new file mode 100644
index 00000000..86806794
--- /dev/null
+++ b/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstProjectStage.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2025-present MongoDB, Inc.
+ *
+ * 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.
+ */
+
+package com.mongodb.hibernate.internal.translate.mongoast.command.aggregate;
+
+import java.util.List;
+import org.bson.BsonWriter;
+
+public record AstProjectStage(List<? extends AstProjectStageSpecification> specifications) implements AstStage {
+    @Override
+    public void render(BsonWriter writer) {
+        writer.writeStartDocument();
+        {
+            writer.writeName("$project");
+            writer.writeStartDocument();
+            {
+                specifications.forEach(specification -> specification.render(writer));
+            }
+            writer.writeEndDocument();
+        }
+        writer.writeEndDocument();
+    }
+}
diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstProjectStageIncludeSpecification.java b/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstProjectStageIncludeSpecification.java
new file mode 100644
index 00000000..b5acdcce
--- /dev/null
+++ b/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstProjectStageIncludeSpecification.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2025-present MongoDB, Inc.
+ *
+ * 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.
+ */
+
+package com.mongodb.hibernate.internal.translate.mongoast.command.aggregate;
+
+import org.bson.BsonWriter;
+
+public record AstProjectStageIncludeSpecification(String field) implements AstProjectStageSpecification {
+    @Override
+    public void render(BsonWriter writer) {
+        writer.writeBoolean(field, true);
+    }
+}
diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstProjectStageSpecification.java b/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstProjectStageSpecification.java
new file mode 100644
index 00000000..50959471
--- /dev/null
+++ b/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstProjectStageSpecification.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2025-present MongoDB, Inc.
+ *
+ * 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.
+ */
+
+package com.mongodb.hibernate.internal.translate.mongoast.command.aggregate;
+
+import com.mongodb.hibernate.internal.translate.mongoast.AstNode;
+
+public interface AstProjectStageSpecification extends AstNode {}
diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstStage.java b/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstStage.java
new file mode 100644
index 00000000..84751d5b
--- /dev/null
+++ b/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstStage.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2025-present MongoDB, Inc.
+ *
+ * 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.
+ */
+
+package com.mongodb.hibernate.internal.translate.mongoast.command.aggregate;
+
+import com.mongodb.hibernate.internal.translate.mongoast.AstNode;
+
+public interface AstStage extends AstNode {}
diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/package-info.java b/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/package-info.java
new file mode 100644
index 00000000..266c34c5
--- /dev/null
+++ b/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2024-present MongoDB, Inc.
+ *
+ * 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.
+ */
+
+/** The program elements within this package are not part of the public API and may be removed or changed at any time */
+@NullMarked
+package com.mongodb.hibernate.internal.translate.mongoast.command.aggregate;
+
+import org.jspecify.annotations.NullMarked;
diff --git a/src/test/java/com/mongodb/hibernate/internal/translate/SelectMqlTranslatorTests.java b/src/test/java/com/mongodb/hibernate/internal/translate/SelectMqlTranslatorTests.java
new file mode 100644
index 00000000..c7201eb1
--- /dev/null
+++ b/src/test/java/com/mongodb/hibernate/internal/translate/SelectMqlTranslatorTests.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2025-present MongoDB, Inc.
+ *
+ * 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.
+ */
+
+package com.mongodb.hibernate.internal.translate;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+
+import com.mongodb.hibernate.internal.extension.service.StandardServiceRegistryScopedState;
+import org.hibernate.engine.spi.SessionFactoryImplementor;
+import org.hibernate.persister.entity.EntityPersister;
+import org.hibernate.query.spi.QueryOptions;
+import org.hibernate.service.spi.ServiceRegistryImplementor;
+import org.hibernate.spi.NavigablePath;
+import org.hibernate.sql.ast.spi.SqlAliasBaseImpl;
+import org.hibernate.sql.ast.tree.from.NamedTableReference;
+import org.hibernate.sql.ast.tree.from.StandardTableGroup;
+import org.hibernate.sql.ast.tree.select.QuerySpec;
+import org.hibernate.sql.ast.tree.select.SelectStatement;
+import org.hibernate.sql.results.jdbc.spi.JdbcValuesMappingProducerProvider;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.MockMakers;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+class SelectMqlTranslatorTests {
+
+    @Test
+    void testAffectedTableNames(
+            @Mock EntityPersister entityPersister,
+            @Mock(mockMaker = MockMakers.PROXY) SessionFactoryImplementor sessionFactory,
+            @Mock JdbcValuesMappingProducerProvider jdbcValuesMappingProducerProvider,
+            @Mock(mockMaker = MockMakers.PROXY) ServiceRegistryImplementor serviceRegistry,
+            @Mock StandardServiceRegistryScopedState standardServiceRegistryScopedState) {
+
+        var tableName = "books";
+        SelectStatement selectFromTableName;
+        { // prepare `selectFromTableName`
+            doReturn(new String[] {tableName}).when(entityPersister).getQuerySpaces();
+
+            var namedTableReference = new NamedTableReference(tableName, "b1_0");
+
+            var querySpec = new QuerySpec(true);
+            var tableGroup = new StandardTableGroup(
+                    false,
+                    new NavigablePath("Book"),
+                    entityPersister,
+                    null,
+                    namedTableReference,
+                    new SqlAliasBaseImpl("b1"),
+                    sessionFactory);
+            querySpec.getFromClause().addRoot(tableGroup);
+            selectFromTableName = new SelectStatement(querySpec);
+        }
+        { // prepare `sessionFactory`
+            doReturn(serviceRegistry).when(sessionFactory).getServiceRegistry();
+            doReturn(jdbcValuesMappingProducerProvider)
+                    .when(serviceRegistry)
+                    .requireService(eq(JdbcValuesMappingProducerProvider.class));
+            doReturn(standardServiceRegistryScopedState)
+                    .when(serviceRegistry)
+                    .requireService(eq(StandardServiceRegistryScopedState.class));
+        }
+
+        var translator = new SelectMqlTranslator(sessionFactory, selectFromTableName);
+
+        translator.translate(null, QueryOptions.NONE);
+
+        assertThat(translator.getAffectedTableNames()).containsExactly(tableName);
+    }
+}
diff --git a/src/test/java/com/mongodb/hibernate/internal/translate/mongoast/AstNodeAssertions.java b/src/test/java/com/mongodb/hibernate/internal/translate/mongoast/AstNodeAssertions.java
index eab60d59..85dfc3a2 100644
--- a/src/test/java/com/mongodb/hibernate/internal/translate/mongoast/AstNodeAssertions.java
+++ b/src/test/java/com/mongodb/hibernate/internal/translate/mongoast/AstNodeAssertions.java
@@ -27,9 +27,23 @@ public final class AstNodeAssertions {
     private AstNodeAssertions() {}
 
     public static void assertRender(String expectedJson, AstNode node) {
+        doAssertRender(expectedJson, node, false);
+    }
+
+    public static void assertElementRender(String expectedJson, AstNode node) {
+        doAssertRender(expectedJson, node, true);
+    }
+
+    private static void doAssertRender(String expectedJson, AstNode node, boolean isElement) {
         try (var stringWriter = new StringWriter();
                 var jsonWriter = new JsonWriter(stringWriter)) {
+            if (isElement) {
+                jsonWriter.writeStartDocument();
+            }
             node.render(jsonWriter);
+            if (isElement) {
+                jsonWriter.writeEndDocument();
+            }
             jsonWriter.flush();
             var actualJson = stringWriter.toString();
             assertEquals(expectedJson, actualJson);
diff --git a/src/test/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstAggregateCommandTests.java b/src/test/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstAggregateCommandTests.java
new file mode 100644
index 00000000..66c9b80a
--- /dev/null
+++ b/src/test/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstAggregateCommandTests.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2025-present MongoDB, Inc.
+ *
+ * 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.
+ */
+
+package com.mongodb.hibernate.internal.translate.mongoast.command.aggregate;
+
+import static com.mongodb.hibernate.internal.translate.mongoast.AstNodeAssertions.assertRender;
+
+import java.util.List;
+import org.junit.jupiter.api.Test;
+
+class AstAggregateCommandTests {
+
+    @Test
+    void testRendering() {
+        var aggregateCommand = new AstAggregateCommand(
+                "books", List.of(new AstProjectStage(List.of()), new AstProjectStage(List.of())));
+        var expectedJson =
+                """
+                {"aggregate": "books", "pipeline": [{"$project": {}}, {"$project": {}}]}\
+                """;
+        assertRender(expectedJson, aggregateCommand);
+    }
+}
diff --git a/src/test/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstMatchStageTests.java b/src/test/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstMatchStageTests.java
new file mode 100644
index 00000000..9d85a422
--- /dev/null
+++ b/src/test/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstMatchStageTests.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2025-present MongoDB, Inc.
+ *
+ * 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.
+ */
+
+package com.mongodb.hibernate.internal.translate.mongoast.command.aggregate;
+
+import static com.mongodb.hibernate.internal.translate.mongoast.AstNodeAssertions.assertRender;
+import static com.mongodb.hibernate.internal.translate.mongoast.filter.AstComparisonFilterOperator.EQ;
+
+import com.mongodb.hibernate.internal.translate.mongoast.AstLiteralValue;
+import com.mongodb.hibernate.internal.translate.mongoast.filter.AstComparisonFilterOperation;
+import com.mongodb.hibernate.internal.translate.mongoast.filter.AstFieldOperationFilter;
+import com.mongodb.hibernate.internal.translate.mongoast.filter.AstFilterFieldPath;
+import org.bson.BsonString;
+import org.junit.jupiter.api.Test;
+
+class AstMatchStageTests {
+
+    @Test
+    void testRendering() {
+        var astFilter = new AstFieldOperationFilter(
+                new AstFilterFieldPath("title"),
+                new AstComparisonFilterOperation(EQ, new AstLiteralValue(new BsonString("Jane Eyre"))));
+        var astMatchStage = new AstMatchStage(astFilter);
+
+        var expectedJson = """
+                {"$match": {"title": {"$eq": "Jane Eyre"}}}\
+                """;
+        assertRender(expectedJson, astMatchStage);
+    }
+}
diff --git a/src/test/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstProjectStageIncludeSpecificationTests.java b/src/test/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstProjectStageIncludeSpecificationTests.java
new file mode 100644
index 00000000..cb3b7f45
--- /dev/null
+++ b/src/test/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstProjectStageIncludeSpecificationTests.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2025-present MongoDB, Inc.
+ *
+ * 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.
+ */
+
+package com.mongodb.hibernate.internal.translate.mongoast.command.aggregate;
+
+import static com.mongodb.hibernate.internal.translate.mongoast.AstNodeAssertions.assertElementRender;
+
+import org.junit.jupiter.api.Test;
+
+class AstProjectStageIncludeSpecificationTests {
+
+    @Test
+    void testRendering() {
+        var projectStageIncludeSpecification = new AstProjectStageIncludeSpecification("name");
+        var expectedJson = """
+                           {"name": true}\
+                           """;
+        assertElementRender(expectedJson, projectStageIncludeSpecification);
+    }
+}
diff --git a/src/test/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstProjectStageTests.java b/src/test/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstProjectStageTests.java
new file mode 100644
index 00000000..fcdde16a
--- /dev/null
+++ b/src/test/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstProjectStageTests.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2025-present MongoDB, Inc.
+ *
+ * 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.
+ */
+
+package com.mongodb.hibernate.internal.translate.mongoast.command.aggregate;
+
+import static com.mongodb.hibernate.internal.translate.mongoast.AstNodeAssertions.assertRender;
+
+import java.util.Collections;
+import org.junit.jupiter.api.Test;
+
+class AstProjectStageTests {
+
+    @Test
+    void testRendering() {
+        var astProjectStage = new AstProjectStage(Collections.emptyList());
+        var expectedJson = """
+                           {"$project": {}}\
+                           """;
+        assertRender(expectedJson, astProjectStage);
+    }
+}