diff --git a/pom.xml b/pom.xml
index 9f81523..1fb2acc 100644
--- a/pom.xml
+++ b/pom.xml
@@ -19,7 +19,7 @@
org.eclipse.daanseorg.eclipse.daanse.pom.parent
- 0.0.6
+ 0.0.7org.eclipse.daanse.sql${revision}
@@ -81,5 +81,6 @@
guarddeparser
+ statement
diff --git a/statement/api/pom.xml b/statement/api/pom.xml
new file mode 100644
index 0000000..c54c330
--- /dev/null
+++ b/statement/api/pom.xml
@@ -0,0 +1,37 @@
+
+
+
+ 4.0.0
+
+ org.eclipse.daanse
+ org.eclipse.daanse.sql.statement
+ ${revision}
+
+ org.eclipse.daanse.sql.statement.api
+ Daanse SQL Query Builder API
+ Immutable query model, fluent assembler and renderer contract for
+ the
+ dialect-agnostic SQL query builder. Depends on the dialect API only for the
+ shared
+ value types (Datatype, BestFitColumnType); contains no dialect behaviour.
+
+
+
+ org.eclipse.daanse
+ org.eclipse.daanse.jdbc.db.dialect.api
+ 0.0.1-SNAPSHOT
+
+
+
diff --git a/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/DeleteStatementBuilder.java b/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/DeleteStatementBuilder.java
new file mode 100644
index 0000000..0ee40b0
--- /dev/null
+++ b/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/DeleteStatementBuilder.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (c) 2026 Contributors to the Eclipse Foundation.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * SmartCity Jena - initial
+ * Stefan Bischof (bipolis.org) - initial
+ */
+package org.eclipse.daanse.sql.statement.api;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+import org.eclipse.daanse.jdbc.db.api.schema.TableReference;
+import org.eclipse.daanse.sql.statement.api.expression.Predicate;
+import org.eclipse.daanse.sql.statement.api.model.DeleteStatement;
+
+/** Mutable, dialect-free builder for a {@link DeleteStatement}. */
+public final class DeleteStatementBuilder {
+
+ private TableReference table;
+ private final List filters = new ArrayList<>();
+ private String footerComment;
+
+ private DeleteStatementBuilder() {
+ }
+
+ public static DeleteStatementBuilder create() {
+ return new DeleteStatementBuilder();
+ }
+
+ public DeleteStatementBuilder from(String table) {
+ this.table = new TableReference(table);
+ return this;
+ }
+
+ public DeleteStatementBuilder from(String schema, String table) {
+ this.table = From.tableRef(schema, table);
+ return this;
+ }
+
+ public DeleteStatementBuilder from(TableReference table) {
+ this.table = table;
+ return this;
+ }
+
+ public DeleteStatementBuilder where(Predicate predicate) {
+ filters.add(predicate);
+ return this;
+ }
+
+ /**
+ * Sets an optional trailing explanatory comment, appended at the very end
+ * (emitted only when comments are on).
+ */
+ public DeleteStatementBuilder footerComment(String comment) {
+ this.footerComment = comment;
+ return this;
+ }
+
+ public DeleteStatement build() {
+ return new DeleteStatement(Objects.requireNonNull(table, "table"), List.copyOf(filters),
+ java.util.Optional.ofNullable(footerComment));
+ }
+}
diff --git a/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/Expressions.java b/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/Expressions.java
new file mode 100644
index 0000000..be72e03
--- /dev/null
+++ b/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/Expressions.java
@@ -0,0 +1,384 @@
+/*
+ * Copyright (c) 2026 Contributors to the Eclipse Foundation.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * SmartCity Jena - initial
+ * Stefan Bischof (bipolis.org) - initial
+ */
+package org.eclipse.daanse.sql.statement.api;
+
+import java.util.List;
+import java.util.Optional;
+
+import org.eclipse.daanse.jdbc.db.dialect.api.generator.KnownFunction;
+import org.eclipse.daanse.jdbc.db.dialect.api.type.Datatype;
+import org.eclipse.daanse.sql.statement.api.expression.ArithmeticOperator;
+import org.eclipse.daanse.sql.statement.api.expression.Predicate;
+import org.eclipse.daanse.sql.statement.api.expression.SqlExpression;
+import org.eclipse.daanse.sql.statement.api.model.SelectStatement;
+import org.eclipse.daanse.sql.statement.api.model.TableAlias;
+
+/**
+ * Factory methods for {@link SqlExpression}s (house style: static helpers over
+ * values).
+ */
+public final class Expressions {
+
+ private Expressions() {
+ }
+
+ /** An unqualified column reference. */
+ public static SqlExpression column(String name) {
+ return new SqlExpression.Column(Optional.empty(), name);
+ }
+
+ /** A column reference qualified by a table alias. */
+ public static SqlExpression column(TableAlias table, String name) {
+ return new SqlExpression.Column(Optional.of(table.name()), name);
+ }
+
+ /**
+ * A column from a jdbc.db
+ * {@link org.eclipse.daanse.jdbc.db.api.schema.ColumnReference}, qualified by
+ * the given query alias. Only the column's {@code name()} is used; the
+ * reference's own table is metadata and is not the in-query qualifier.
+ */
+ public static SqlExpression column(TableAlias table, org.eclipse.daanse.jdbc.db.api.schema.ColumnReference column) {
+ return new SqlExpression.Column(Optional.of(table.name()), column.name());
+ }
+
+ /**
+ * An unqualified column from a jdbc.db
+ * {@link org.eclipse.daanse.jdbc.db.api.schema.ColumnReference} (uses
+ * {@code name()} only).
+ */
+ public static SqlExpression column(org.eclipse.daanse.jdbc.db.api.schema.ColumnReference column) {
+ return new SqlExpression.Column(Optional.empty(), column.name());
+ }
+
+ /** A typed literal value. */
+ public static SqlExpression literal(Object value, Datatype datatype) {
+ return new SqlExpression.Literal(value, datatype);
+ }
+
+ /** A verbatim SQL fragment. */
+ public static SqlExpression raw(String sql) {
+ return new SqlExpression.Raw(sql);
+ }
+
+ /**
+ * A per-dialect SQL fragment chosen at render time — the multi-variant sibling
+ * of {@link #raw(String)} for computed columns / measure expressions whose SQL
+ * differs per engine. The renderer resolves the {@code dialect-name -> SQL} map
+ * (live dialect else {@code "generic"}).
+ */
+ public static SqlExpression rawVariant(java.util.Map byDialectName) {
+ return new SqlExpression.RawVariant(byDialectName);
+ }
+
+ /**
+ * A bind parameter carrying an immediate value (rendered as a placeholder,
+ * bound by the executor).
+ */
+ public static SqlExpression param(Object value, Datatype datatype) {
+ return new SqlExpression.Param(value, true, datatype);
+ }
+
+ /**
+ * A positional parameter marker whose value is supplied at execute time (e.g.
+ * per batch row).
+ */
+ public static SqlExpression paramMarker(Datatype datatype) {
+ return new SqlExpression.Param(null, false, datatype);
+ }
+
+ /** A function call, e.g. {@code function("UPPER", column("name"))}. */
+ public static SqlExpression function(String name, SqlExpression... arguments) {
+ return new SqlExpression.Function(name, List.of(arguments));
+ }
+
+ /**
+ * The whole-row projection {@code *} (never given a column alias by the
+ * renderer).
+ */
+ public static SqlExpression star() {
+ return new SqlExpression.Star(Optional.empty());
+ }
+
+ /**
+ * The qualified whole-row projection {@code t.*} (never given a column alias by
+ * the renderer).
+ */
+ public static SqlExpression star(TableAlias table) {
+ return new SqlExpression.Star(Optional.of(table.name()));
+ }
+
+ /**
+ * A scalar subquery {@code (select ...)} usable as a value — in the
+ * {@code SELECT} list or as one side of a comparison. Rendered compact; nested
+ * bind parameters keep placeholder order.
+ */
+ public static SqlExpression scalarSubquery(SelectStatement query) {
+ return new SqlExpression.ScalarSubquery(query);
+ }
+
+ /**
+ * A 1-based ordinal position reference, e.g. {@code ordinal(1)} for
+ * {@code ORDER BY 1}.
+ */
+ public static SqlExpression ordinal(int position) {
+ return new SqlExpression.Ordinal(position);
+ }
+
+ /** The {@code COUNT(*)} aggregate. */
+ public static SqlExpression countStar() {
+ return new SqlExpression.Function("COUNT", List.of(new SqlExpression.Star(Optional.empty())));
+ }
+
+ /**
+ * A non-distinct aggregate call, e.g.
+ * {@code aggregate("SUM", column("amount"))}.
+ */
+ public static SqlExpression aggregate(String name, SqlExpression... arguments) {
+ requireArgs(arguments, "aggregate");
+ return new SqlExpression.Aggregate(name, false, List.of(arguments));
+ }
+
+ /**
+ * A {@code DISTINCT} aggregate call, e.g. {@code SUM(DISTINCT amount)} or
+ * {@code COUNT(DISTINCT a, b)} (the multi-argument form is compound
+ * count-distinct, which not every dialect supports — gate at the call site on
+ * {@code allowsCompoundCountDistinct}).
+ */
+ public static SqlExpression aggregateDistinct(String name, SqlExpression... arguments) {
+ requireArgs(arguments, "aggregateDistinct");
+ return new SqlExpression.Aggregate(name, true, List.of(arguments));
+ }
+
+ /** {@code COUNT(DISTINCT arg0, arg1, ...)}. */
+ public static SqlExpression countDistinct(SqlExpression... arguments) {
+ requireArgs(arguments, "countDistinct");
+ return new SqlExpression.Aggregate("COUNT", true, List.of(arguments));
+ }
+
+ private static void requireArgs(SqlExpression[] arguments, String method) {
+ if (arguments == null || arguments.length == 0) {
+ throw new IllegalArgumentException(method + " requires at least one argument");
+ }
+ }
+
+ /** {@code UPPER(expression)}. */
+ public static SqlExpression upper(SqlExpression expression) {
+ return new SqlExpression.Function("UPPER", List.of(expression));
+ }
+
+ /** {@code LOWER(expression)}. */
+ public static SqlExpression lower(SqlExpression expression) {
+ return new SqlExpression.Function("LOWER", List.of(expression));
+ }
+
+ /** {@code COALESCE(arg0, arg1, ...)}. */
+ public static SqlExpression coalesce(SqlExpression... arguments) {
+ return new SqlExpression.Function("COALESCE", List.of(arguments));
+ }
+
+ /** A binary arithmetic expression {@code left right}. */
+ public static SqlExpression arithmetic(SqlExpression left, ArithmeticOperator operator, SqlExpression right) {
+ return new SqlExpression.Binary(left, operator, right);
+ }
+
+ /**
+ * A binary arithmetic expression that renders WITHOUT enclosing parentheses —
+ * for infix fragments already inside an enclosing context (e.g. the
+ * {@code a / b} inside {@code sum(a / b)}), where the standalone
+ * {@link #arithmetic} form's outer parens would be incorrect.
+ */
+ public static SqlExpression infix(SqlExpression left, ArithmeticOperator operator, SqlExpression right) {
+ return new SqlExpression.Binary(left, operator, right, false);
+ }
+
+ public static SqlExpression add(SqlExpression left, SqlExpression right) {
+ return arithmetic(left, ArithmeticOperator.ADD, right);
+ }
+
+ public static SqlExpression subtract(SqlExpression left, SqlExpression right) {
+ return arithmetic(left, ArithmeticOperator.SUBTRACT, right);
+ }
+
+ public static SqlExpression multiply(SqlExpression left, SqlExpression right) {
+ return arithmetic(left, ArithmeticOperator.MULTIPLY, right);
+ }
+
+ public static SqlExpression divide(SqlExpression left, SqlExpression right) {
+ return arithmetic(left, ArithmeticOperator.DIVIDE, right);
+ }
+
+ public static SqlExpression modulo(SqlExpression left, SqlExpression right) {
+ return arithmetic(left, ArithmeticOperator.MODULO, right);
+ }
+
+ // ---- portable well-known functions (KnownCall: the dialect spells them at
+ // render time) ----
+
+ /**
+ * Substring from a 1-based start position to the end:
+ * {@code SUBSTRING(e, start)}.
+ */
+ public static SqlExpression substring(SqlExpression expression, SqlExpression start) {
+ return new SqlExpression.KnownCall(KnownFunction.SUBSTRING, List.of(expression, start));
+ }
+
+ /**
+ * Substring of {@code length} characters from a 1-based start position:
+ * {@code SUBSTRING(e, start, length)}.
+ */
+ public static SqlExpression substring(SqlExpression expression, SqlExpression start, SqlExpression length) {
+ return new SqlExpression.KnownCall(KnownFunction.SUBSTRING, List.of(expression, start, length));
+ }
+
+ /** Character length of a string. */
+ public static SqlExpression length(SqlExpression expression) {
+ return new SqlExpression.KnownCall(KnownFunction.LENGTH, List.of(expression));
+ }
+
+ /** String concatenation of two or more expressions. */
+ public static SqlExpression concat(SqlExpression... arguments) {
+ if (arguments == null || arguments.length < 2) {
+ throw new IllegalArgumentException("concat requires at least two arguments");
+ }
+ return new SqlExpression.KnownCall(KnownFunction.CONCAT, List.of(arguments));
+ }
+
+ /**
+ * 1-based position of {@code needle} inside {@code haystack} — needle
+ * first, matching the OData {@code indexof} builtin; dialects whose native
+ * function takes the haystack first (Oracle {@code INSTR}) swap the arguments
+ * at render time.
+ */
+ public static SqlExpression indexOf(SqlExpression needle, SqlExpression haystack) {
+ return new SqlExpression.KnownCall(KnownFunction.INDEX_OF, List.of(needle, haystack));
+ }
+
+ /** Strip leading and trailing whitespace. */
+ public static SqlExpression trim(SqlExpression expression) {
+ return new SqlExpression.KnownCall(KnownFunction.TRIM, List.of(expression));
+ }
+
+ /** Strip leading whitespace. */
+ public static SqlExpression ltrim(SqlExpression expression) {
+ return new SqlExpression.KnownCall(KnownFunction.LTRIM, List.of(expression));
+ }
+
+ /** Strip trailing whitespace. */
+ public static SqlExpression rtrim(SqlExpression expression) {
+ return new SqlExpression.KnownCall(KnownFunction.RTRIM, List.of(expression));
+ }
+
+ /** Year part of a date/time value. */
+ public static SqlExpression year(SqlExpression expression) {
+ return new SqlExpression.KnownCall(KnownFunction.YEAR, List.of(expression));
+ }
+
+ /** Month part of a date/time value. */
+ public static SqlExpression month(SqlExpression expression) {
+ return new SqlExpression.KnownCall(KnownFunction.MONTH, List.of(expression));
+ }
+
+ /** Day-of-month part of a date/time value. */
+ public static SqlExpression day(SqlExpression expression) {
+ return new SqlExpression.KnownCall(KnownFunction.DAY, List.of(expression));
+ }
+
+ /** Hour part of a time value. */
+ public static SqlExpression hour(SqlExpression expression) {
+ return new SqlExpression.KnownCall(KnownFunction.HOUR, List.of(expression));
+ }
+
+ /** Minute part of a time value. */
+ public static SqlExpression minute(SqlExpression expression) {
+ return new SqlExpression.KnownCall(KnownFunction.MINUTE, List.of(expression));
+ }
+
+ /** Second part of a time value. */
+ public static SqlExpression second(SqlExpression expression) {
+ return new SqlExpression.KnownCall(KnownFunction.SECOND, List.of(expression));
+ }
+
+ /** Date part of a date/time value. */
+ public static SqlExpression date(SqlExpression expression) {
+ return new SqlExpression.KnownCall(KnownFunction.DATE, List.of(expression));
+ }
+
+ /** Time part of a date/time value. */
+ public static SqlExpression time(SqlExpression expression) {
+ return new SqlExpression.KnownCall(KnownFunction.TIME, List.of(expression));
+ }
+
+ /** Numeric rounding to an integer: {@code ROUND(e)}. */
+ public static SqlExpression round(SqlExpression expression) {
+ return new SqlExpression.KnownCall(KnownFunction.ROUND, List.of(expression));
+ }
+
+ /**
+ * Numeric rounding to {@code digits} decimal places: {@code ROUND(e, digits)}.
+ */
+ public static SqlExpression round(SqlExpression expression, SqlExpression digits) {
+ return new SqlExpression.KnownCall(KnownFunction.ROUND, List.of(expression, digits));
+ }
+
+ /** Largest integer not greater than the value. */
+ public static SqlExpression floor(SqlExpression expression) {
+ return new SqlExpression.KnownCall(KnownFunction.FLOOR, List.of(expression));
+ }
+
+ /** Smallest integer not less than the value. */
+ public static SqlExpression ceiling(SqlExpression expression) {
+ return new SqlExpression.KnownCall(KnownFunction.CEILING, List.of(expression));
+ }
+
+ /** Absolute value. */
+ public static SqlExpression abs(SqlExpression expression) {
+ return new SqlExpression.KnownCall(KnownFunction.ABS, List.of(expression));
+ }
+
+ /** Remainder of an integer division: {@code MOD(dividend, divisor)}. */
+ public static SqlExpression mod(SqlExpression dividend, SqlExpression divisor) {
+ return new SqlExpression.KnownCall(KnownFunction.MOD, List.of(dividend, divisor));
+ }
+
+ /** Exponentiation: {@code POWER(base, exponent)}. */
+ public static SqlExpression power(SqlExpression base, SqlExpression exponent) {
+ return new SqlExpression.KnownCall(KnownFunction.POWER, List.of(base, exponent));
+ }
+
+ /** Square root. */
+ public static SqlExpression sqrt(SqlExpression expression) {
+ return new SqlExpression.KnownCall(KnownFunction.SQRT, List.of(expression));
+ }
+
+ /** Current date and time. */
+ public static SqlExpression now() {
+ return new SqlExpression.KnownCall(KnownFunction.NOW, List.of());
+ }
+
+ /** A single {@code WHEN condition THEN result} branch for {@link #caseExpr}. */
+ public static SqlExpression.Case.WhenClause when(Predicate condition, SqlExpression result) {
+ return new SqlExpression.Case.WhenClause(condition, result);
+ }
+
+ /** A {@code CASE WHEN ... THEN ... END} with no ELSE. */
+ public static SqlExpression caseExpr(List whens) {
+ return new SqlExpression.Case(List.copyOf(whens), Optional.empty());
+ }
+
+ /** A {@code CASE WHEN ... THEN ... ELSE elseResult END}. */
+ public static SqlExpression caseExpr(List whens, SqlExpression elseResult) {
+ return new SqlExpression.Case(List.copyOf(whens), Optional.ofNullable(elseResult));
+ }
+}
diff --git a/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/From.java b/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/From.java
new file mode 100644
index 0000000..71d2530
--- /dev/null
+++ b/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/From.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (c) 2026 Contributors to the Eclipse Foundation.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * SmartCity Jena - initial
+ * Stefan Bischof (bipolis.org) - initial
+ */
+package org.eclipse.daanse.sql.statement.api;
+
+import java.util.Map;
+import java.util.Optional;
+
+import org.eclipse.daanse.jdbc.db.api.schema.SchemaReference;
+import org.eclipse.daanse.jdbc.db.api.schema.TableReference;
+import org.eclipse.daanse.sql.statement.api.expression.Predicate;
+import org.eclipse.daanse.sql.statement.api.model.FromClause;
+import org.eclipse.daanse.sql.statement.api.model.SelectStatement;
+import org.eclipse.daanse.sql.statement.api.model.SetOperation;
+import org.eclipse.daanse.sql.statement.api.model.TableAlias;
+
+/** Factory methods for {@link FromClause} nodes. */
+public final class From {
+
+ private From() {
+ }
+
+ /** Builds a (optionally schema-qualified) {@link TableReference}. */
+ public static TableReference tableRef(String schema, String table) {
+ return schema == null ? new TableReference(table)
+ : new TableReference(Optional.of(new SchemaReference(schema)), table);
+ }
+
+ public static FromClause.FromTable table(String table, TableAlias alias) {
+ return table(new TableReference(table), alias);
+ }
+
+ public static FromClause.FromTable table(String schema, String table, TableAlias alias) {
+ return table(tableRef(schema, table), alias);
+ }
+
+ public static FromClause.FromTable table(TableReference table, TableAlias alias) {
+ return new FromClause.FromTable(table, alias, Optional.empty(), Map.of());
+ }
+
+ public static FromClause.FromTable table(String schema, String table, TableAlias alias, Predicate filter,
+ Map hints) {
+ return table(tableRef(schema, table), alias, filter, hints);
+ }
+
+ public static FromClause.FromTable table(TableReference table, TableAlias alias, Predicate filter,
+ Map hints) {
+ return new FromClause.FromTable(table, alias, Optional.ofNullable(filter), Map.copyOf(hints));
+ }
+
+ public static FromClause.FromSubquery subquery(SelectStatement query, TableAlias alias) {
+ return new FromClause.FromSubquery(query, alias);
+ }
+
+ /**
+ * A parenthesized set operation (e.g. {@code UNION}) as a derived table (see
+ * {@link FromClause.FromSet}).
+ */
+ public static FromClause.FromSet set(SetOperation set, TableAlias alias) {
+ return new FromClause.FromSet(set, alias);
+ }
+
+ /**
+ * A derived table from a pre-rendered SQL fragment (view / inline
+ * {@code VALUES}).
+ */
+ public static FromClause.FromRaw raw(String sql, TableAlias alias) {
+ return new FromClause.FromRaw(sql, alias);
+ }
+
+ /**
+ * A derived table from an inline {@code VALUES} table — the structured data is
+ * carried as-is and the dialect-specific SQL is generated at render time (see
+ * {@link FromClause.FromInline}).
+ */
+ public static FromClause.FromInline inline(java.util.List columnNames, java.util.List columnTypes,
+ java.util.List rows, TableAlias alias) {
+ return new FromClause.FromInline(java.util.List.copyOf(columnNames), java.util.List.copyOf(columnTypes),
+ java.util.List.copyOf(rows), alias);
+ }
+
+ /**
+ * The alias of the BASE table of a FROM tree — the leftmost aliased leaf, i.e.
+ * the item the renderer emits first (a join tree's leftmost input; a product's
+ * first item) — or {@code null} when the tree has no aliased base
+ * (empty/unsupported shapes).
+ */
+ public static TableAlias baseAlias(FromClause from) {
+ if (from instanceof FromClause.Aliased a) {
+ return a.alias();
+ }
+ if (from instanceof FromClause.FromJoin j) {
+ return baseAlias(j.left());
+ }
+ if (from instanceof FromClause.FromProduct p) {
+ return baseAlias(p.items().get(0));
+ }
+ return null;
+ }
+
+ /**
+ * A copy of {@code from} whose BASE table (the leftmost
+ * {@link FromClause.FromTable}) carries the given provenance comment (see
+ * {@link FromClause.FromTable#comment()}). Joined tables keep their provenance
+ * on the {@link FromClause.FromJoin} comment; this reaches the one item no join
+ * comment can. No-op (returns {@code from} unchanged) when the base is not a
+ * plain table or the comment is {@code null}/blank.
+ */
+ public static FromClause commentBase(FromClause from, String comment) {
+ if (comment == null || comment.isBlank()) {
+ return from;
+ }
+ if (from instanceof FromClause.FromTable t) {
+ return t.withComment(comment);
+ }
+ if (from instanceof FromClause.FromJoin j) {
+ FromClause left = commentBase(j.left(), comment);
+ return left == j.left() ? from : new FromClause.FromJoin(left, j.kind(), j.right(), j.on(), j.comment());
+ }
+ if (from instanceof FromClause.FromProduct p) {
+ FromClause first = commentBase(p.items().get(0), comment);
+ if (first == p.items().get(0)) {
+ return from;
+ }
+ java.util.List items = new java.util.ArrayList<>(p.items());
+ items.set(0, first);
+ return new FromClause.FromProduct(items);
+ }
+ return from;
+ }
+
+ /**
+ * A comma product of two-or-more from-clauses with no join predicate of its own
+ * — the caller adds the join conditions to {@code WHERE} (see
+ * {@link FromClause.FromProduct}).
+ */
+ public static FromClause.FromProduct product(FromClause first, FromClause second, FromClause... rest) {
+ java.util.List items = new java.util.ArrayList<>();
+ items.add(first);
+ items.add(second);
+ java.util.Collections.addAll(items, rest);
+ return new FromClause.FromProduct(items);
+ }
+}
diff --git a/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/InsertStatementBuilder.java b/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/InsertStatementBuilder.java
new file mode 100644
index 0000000..77716e4
--- /dev/null
+++ b/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/InsertStatementBuilder.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (c) 2026 Contributors to the Eclipse Foundation.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * SmartCity Jena - initial
+ * Stefan Bischof (bipolis.org) - initial
+ */
+package org.eclipse.daanse.sql.statement.api;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+
+import org.eclipse.daanse.jdbc.db.api.schema.TableReference;
+import org.eclipse.daanse.sql.statement.api.expression.SqlExpression;
+import org.eclipse.daanse.sql.statement.api.model.InsertStatement;
+import org.eclipse.daanse.sql.statement.api.model.Statement;
+
+/**
+ * Mutable, dialect-free builder for an {@link InsertStatement} — either
+ * {@code INSERT ...
+ * VALUES} (via {@link #addRow}) or {@code INSERT ... SELECT} (via
+ * {@link #fromSelect}).
+ */
+public final class InsertStatementBuilder {
+
+ private TableReference table;
+ private final List columns = new ArrayList<>();
+ private final List> rows = new ArrayList<>();
+ private Statement source;
+ private String footerComment;
+
+ private InsertStatementBuilder() {
+ }
+
+ public static InsertStatementBuilder create() {
+ return new InsertStatementBuilder();
+ }
+
+ public InsertStatementBuilder into(String table) {
+ this.table = new TableReference(table);
+ return this;
+ }
+
+ public InsertStatementBuilder into(String schema, String table) {
+ this.table = From.tableRef(schema, table);
+ return this;
+ }
+
+ public InsertStatementBuilder into(TableReference table) {
+ this.table = table;
+ return this;
+ }
+
+ public InsertStatementBuilder columns(String... names) {
+ columns.addAll(List.of(names));
+ return this;
+ }
+
+ /** Adds one VALUES row; its length should match {@link #columns(String...)}. */
+ public InsertStatementBuilder addRow(SqlExpression... values) {
+ rows.add(List.of(values));
+ return this;
+ }
+
+ /** Sources the rows from a sub-query ({@code INSERT ... SELECT}). */
+ public InsertStatementBuilder fromSelect(Statement source) {
+ this.source = source;
+ return this;
+ }
+
+ /**
+ * Sets an optional trailing explanatory comment, appended at the very end
+ * (emitted only when comments are on).
+ */
+ public InsertStatementBuilder footerComment(String comment) {
+ this.footerComment = comment;
+ return this;
+ }
+
+ public InsertStatement build() {
+ if (source != null && !rows.isEmpty()) {
+ throw new IllegalStateException("insert has both VALUES rows and a SELECT source");
+ }
+ return new InsertStatement(Objects.requireNonNull(table, "table"), List.copyOf(columns), List.copyOf(rows),
+ Optional.ofNullable(source), Optional.ofNullable(footerComment));
+ }
+}
diff --git a/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/Predicates.java b/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/Predicates.java
new file mode 100644
index 0000000..1082e3f
--- /dev/null
+++ b/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/Predicates.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (c) 2026 Contributors to the Eclipse Foundation.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * SmartCity Jena - initial
+ * Stefan Bischof (bipolis.org) - initial
+ */
+package org.eclipse.daanse.sql.statement.api;
+
+import java.util.List;
+import java.util.Optional;
+
+import org.eclipse.daanse.sql.statement.api.expression.ComparisonOperator;
+import org.eclipse.daanse.sql.statement.api.expression.Predicate;
+import org.eclipse.daanse.sql.statement.api.expression.SqlExpression;
+import org.eclipse.daanse.sql.statement.api.model.SelectStatement;
+
+/** Factory methods for {@link Predicate}s. */
+public final class Predicates {
+
+ private static final Predicate ALWAYS_TRUE = new Predicate.Constant(true);
+ private static final Predicate ALWAYS_FALSE = new Predicate.Constant(false);
+
+ private Predicates() {
+ }
+
+ public static Predicate comparison(SqlExpression left, ComparisonOperator operator, SqlExpression right) {
+ return new Predicate.Comparison(left, operator, right);
+ }
+
+ public static Predicate eq(SqlExpression left, SqlExpression right) {
+ return comparison(left, ComparisonOperator.EQ, right);
+ }
+
+ public static Predicate gt(SqlExpression left, SqlExpression right) {
+ return comparison(left, ComparisonOperator.GT, right);
+ }
+
+ public static Predicate in(SqlExpression expression, List values) {
+ return new Predicate.In(expression, List.copyOf(values));
+ }
+
+ public static Predicate in(SqlExpression expression, SqlExpression... values) {
+ return new Predicate.In(expression, List.of(values));
+ }
+
+ /**
+ * Row-value / tuple {@code IN}:
+ * {@code (c1, c2, ...) IN ((v11, ...), (v21, ...), ...)}. Every row must match
+ * {@code columns} in arity. Gate at the call site on
+ * {@code dialect.supportsMultiValueInExpr()}.
+ */
+ public static Predicate inTuple(List columns, List> rows) {
+ List> copy = new java.util.ArrayList<>(rows.size());
+ for (List row : rows) {
+ if (row.size() != columns.size()) {
+ throw new IllegalArgumentException(
+ "inTuple row arity " + row.size() + " != column count " + columns.size());
+ }
+ copy.add(List.copyOf(row));
+ }
+ return new Predicate.InTuple(List.copyOf(columns), List.copyOf(copy));
+ }
+
+ public static Predicate isNull(SqlExpression expression) {
+ return new Predicate.IsNull(expression, false);
+ }
+
+ public static Predicate isNotNull(SqlExpression expression) {
+ return new Predicate.IsNull(expression, true);
+ }
+
+ public static Predicate like(SqlExpression expression, SqlExpression pattern) {
+ return new Predicate.Like(expression, pattern, false, Optional.empty());
+ }
+
+ public static Predicate like(SqlExpression expression, SqlExpression pattern, char escape) {
+ return new Predicate.Like(expression, pattern, false, Optional.of(escape));
+ }
+
+ public static Predicate notLike(SqlExpression expression, SqlExpression pattern) {
+ return new Predicate.Like(expression, pattern, true, Optional.empty());
+ }
+
+ public static Predicate between(SqlExpression expression, SqlExpression low, SqlExpression high) {
+ return new Predicate.Between(expression, low, high, false);
+ }
+
+ public static Predicate notBetween(SqlExpression expression, SqlExpression low, SqlExpression high) {
+ return new Predicate.Between(expression, low, high, true);
+ }
+
+ public static Predicate not(Predicate operand) {
+ return new Predicate.Not(operand);
+ }
+
+ public static Predicate and(List operands) {
+ return new Predicate.And(List.copyOf(operands));
+ }
+
+ public static Predicate and(Predicate... operands) {
+ return new Predicate.And(List.of(operands));
+ }
+
+ public static Predicate or(List operands) {
+ return new Predicate.Or(List.copyOf(operands));
+ }
+
+ public static Predicate or(Predicate... operands) {
+ return new Predicate.Or(List.of(operands));
+ }
+
+ /**
+ * {@code EXISTS (query)}. The subquery may be correlated (reference outer
+ * aliases).
+ */
+ public static Predicate exists(SelectStatement query) {
+ return new Predicate.Exists(query, false);
+ }
+
+ /**
+ * {@code NOT EXISTS (query)}. The subquery may be correlated (reference outer
+ * aliases).
+ */
+ public static Predicate notExists(SelectStatement query) {
+ return new Predicate.Exists(query, true);
+ }
+
+ /**
+ * The always-true constant predicate, rendered {@code 1 = 1} (no parentheses).
+ */
+ public static Predicate alwaysTrue() {
+ return ALWAYS_TRUE;
+ }
+
+ /**
+ * The always-false constant predicate, rendered {@code 1 = 0} (no parentheses).
+ */
+ public static Predicate alwaysFalse() {
+ return ALWAYS_FALSE;
+ }
+
+ public static Predicate raw(String sql) {
+ return new Predicate.Raw(sql);
+ }
+}
diff --git a/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/SelectStatementBuilder.java b/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/SelectStatementBuilder.java
new file mode 100644
index 0000000..d2dfee2
--- /dev/null
+++ b/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/SelectStatementBuilder.java
@@ -0,0 +1,276 @@
+/*
+ * Copyright (c) 2026 Contributors to the Eclipse Foundation.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * SmartCity Jena - initial
+ * Stefan Bischof (bipolis.org) - initial
+ */
+package org.eclipse.daanse.sql.statement.api;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+
+import org.eclipse.daanse.jdbc.db.dialect.api.generator.StatementHint;
+import org.eclipse.daanse.jdbc.db.dialect.api.type.BestFitColumnType;
+import org.eclipse.daanse.sql.statement.api.expression.Predicate;
+import org.eclipse.daanse.sql.statement.api.expression.SqlExpression;
+import org.eclipse.daanse.sql.statement.api.model.ColumnAlias;
+import org.eclipse.daanse.sql.statement.api.model.FromClause;
+import org.eclipse.daanse.sql.statement.api.model.GroupBy;
+import org.eclipse.daanse.sql.statement.api.model.JoinKind;
+import org.eclipse.daanse.sql.statement.api.model.OrderKey;
+import org.eclipse.daanse.sql.statement.api.model.Projection;
+import org.eclipse.daanse.sql.statement.api.model.ProjectionRef;
+import org.eclipse.daanse.sql.statement.api.model.RowLimit;
+import org.eclipse.daanse.sql.statement.api.model.SelectStatement;
+import org.eclipse.daanse.sql.statement.api.model.SortSpec;
+import org.eclipse.daanse.sql.statement.api.model.TableAlias;
+
+/**
+ * Mutable, dialect-free builder for a {@link SelectStatement}.
+ *
+ * Consumers accumulate clauses with intent methods ({@code project},
+ * {@code where}, {@code groupOn}, {@code orderOn}, {@code from}/{@code join},
+ * {@code rowLimit}) and call {@link #build()} to obtain the immutable model.
+ * There is deliberately no {@code Dialect} here: alias generation and
+ * alias-vs-expression decisions are made later by the renderer, which is the
+ * only dialect-aware component.
+ *
+ * {@link #project(SqlExpression, BestFitColumnType)} returns a
+ * {@link ProjectionRef} handle; pass it to {@link #groupOn(ProjectionRef)} /
+ * {@link #orderOn(ProjectionRef, SortSpec)} instead of capturing and threading
+ * an alias string.
+ *
+ * Not thread-safe; use one instance per query being built.
+ */
+public final class SelectStatementBuilder {
+
+ private boolean distinct;
+ private final List projections = new ArrayList<>();
+ private FromClause from;
+ private final List filters = new ArrayList<>();
+ private final List groupKeys = new ArrayList<>();
+ private final List groupingSets = new ArrayList<>();
+ private final List groupingFunctions = new ArrayList<>();
+ private final List having = new ArrayList<>();
+ private final List orderKeys = new ArrayList<>();
+ private RowLimit rowLimit;
+ private String headerComment;
+ private String footerComment;
+ private final List statementHints = new ArrayList<>();
+ /**
+ * Identity-keyed so structurally-equal predicates (and the pushed-down join
+ * ONs) don't collide.
+ */
+ private final java.util.Map filterComments = new java.util.IdentityHashMap<>();
+
+ private SelectStatementBuilder() {
+ }
+
+ /**
+ * Sets an optional statement-level explanatory comment (emitted only when
+ * comments are on).
+ */
+ public SelectStatementBuilder header(String comment) {
+ this.headerComment = comment;
+ return this;
+ }
+
+ /**
+ * Sets an optional trailing explanatory comment, appended at the very end
+ * (emitted only when comments are on).
+ */
+ public SelectStatementBuilder footerComment(String comment) {
+ this.footerComment = comment;
+ return this;
+ }
+
+ /**
+ * Adds a statement-level optimizer hint, spelled (or silently ignored) by the
+ * dialect at render time. Unlike comments, hints are semantic for the DBMS and
+ * are always emitted.
+ */
+ public SelectStatementBuilder statementHint(StatementHint hint) {
+ this.statementHints.add(Objects.requireNonNull(hint, "hint"));
+ return this;
+ }
+
+ /** Creates an empty assembler. */
+ public static SelectStatementBuilder create() {
+ return new SelectStatementBuilder();
+ }
+
+ public SelectStatementBuilder distinct(boolean value) {
+ this.distinct = value;
+ return this;
+ }
+
+ /** Adds an expression to the {@code SELECT} list and returns a handle to it. */
+ public ProjectionRef project(SqlExpression expression, BestFitColumnType columnType) {
+ return project(expression, columnType, null);
+ }
+
+ /** Adds an expression to the {@code SELECT} list with an explicit alias. */
+ public ProjectionRef project(SqlExpression expression, BestFitColumnType columnType, ColumnAlias alias) {
+ return project(expression, columnType, alias, null);
+ }
+
+ /**
+ * Adds an expression to the {@code SELECT} list with an explicit alias and an
+ * explanatory comment.
+ */
+ public ProjectionRef project(SqlExpression expression, BestFitColumnType columnType, ColumnAlias alias,
+ String comment) {
+ int ordinal = projections.size();
+ Optional a = Optional.ofNullable(alias);
+ projections.add(new Projection(expression, columnType, a, Optional.ofNullable(comment)));
+ return new ProjectionRef(ordinal, a);
+ }
+
+ /** Convenience: project an expression and group by it. */
+ public ProjectionRef projectAndGroup(SqlExpression expression, BestFitColumnType columnType) {
+ ProjectionRef ref = project(expression, columnType);
+ groupOn(ref);
+ return ref;
+ }
+
+ public SelectStatementBuilder groupOn(ProjectionRef ref) {
+ groupKeys.add(new GroupBy.GroupKey.Ref(ref));
+ return this;
+ }
+
+ public SelectStatementBuilder groupOn(SqlExpression expression) {
+ groupKeys.add(new GroupBy.GroupKey.Expr(expression));
+ return this;
+ }
+
+ public SelectStatementBuilder addGroupingSet(List keys) {
+ groupingSets.add(new GroupBy.GroupingSet(List.copyOf(keys)));
+ return this;
+ }
+
+ public SelectStatementBuilder addGroupingFunction(SqlExpression argument) {
+ groupingFunctions.add(new GroupBy.GroupingFunction(argument));
+ return this;
+ }
+
+ public SelectStatementBuilder where(Predicate predicate) {
+ filters.add(predicate);
+ return this;
+ }
+
+ /**
+ * Adds a {@code WHERE} predicate carrying an explanatory comment (emitted only
+ * when comments are on).
+ */
+ public SelectStatementBuilder where(Predicate predicate, String comment) {
+ filters.add(predicate);
+ if (comment != null) {
+ filterComments.put(predicate, comment);
+ }
+ return this;
+ }
+
+ public SelectStatementBuilder having(Predicate predicate) {
+ having.add(predicate);
+ return this;
+ }
+
+ /**
+ * Adds a {@code HAVING} predicate carrying an explanatory comment (emitted only
+ * when comments are on).
+ */
+ public SelectStatementBuilder having(Predicate predicate, String comment) {
+ having.add(predicate);
+ if (comment != null) {
+ filterComments.put(predicate, comment);
+ }
+ return this;
+ }
+
+ public SelectStatementBuilder orderOn(SqlExpression expression, SortSpec spec) {
+ addOrder(new OrderKey(expression, Optional.empty(), spec), spec);
+ return this;
+ }
+
+ public SelectStatementBuilder orderOn(ProjectionRef ref, SortSpec spec) {
+ SqlExpression expression = projections.get(ref.ordinal()).expression();
+ addOrder(new OrderKey(expression, Optional.of(ref), spec), spec);
+ return this;
+ }
+
+ private void addOrder(OrderKey key, SortSpec spec) {
+ if (spec.prepend()) {
+ orderKeys.add(0, key);
+ } else {
+ orderKeys.add(key);
+ }
+ }
+
+ /** Sets the {@code FROM} clause (table, sub-query, or join tree). */
+ public SelectStatementBuilder from(FromClause clause) {
+ this.from = clause;
+ return this;
+ }
+
+ /** Sets the {@code FROM} to a sub-query built by another assembler. */
+ public SelectStatementBuilder fromSubquery(SelectStatementBuilder inner, TableAlias alias) {
+ this.from = From.subquery(inner.build(), alias);
+ return this;
+ }
+
+ /** Joins the current {@code FROM} with {@code right} on {@code on}. */
+ public SelectStatementBuilder join(JoinKind kind, FromClause right, Predicate on) {
+ return join(kind, right, on, null);
+ }
+
+ /**
+ * Joins the current {@code FROM} with {@code right} on {@code on}, carrying an
+ * explanatory comment.
+ */
+ public SelectStatementBuilder join(JoinKind kind, FromClause right, Predicate on, String comment) {
+ Objects.requireNonNull(from, "set a FROM before joining");
+ this.from = new FromClause.FromJoin(from, kind, right, on, Optional.ofNullable(comment));
+ return this;
+ }
+
+ public SelectStatementBuilder innerJoin(FromClause right, Predicate on) {
+ return join(JoinKind.INNER, right, on);
+ }
+
+ public SelectStatementBuilder leftJoin(FromClause right, Predicate on) {
+ return join(JoinKind.LEFT, right, on);
+ }
+
+ public SelectStatementBuilder rowLimit(long maxRows) {
+ this.rowLimit = RowLimit.of(maxRows);
+ return this;
+ }
+
+ public SelectStatementBuilder rowLimit(long maxRows, long offset) {
+ this.rowLimit = RowLimit.of(maxRows, offset);
+ return this;
+ }
+
+ public int projectionCount() {
+ return projections.size();
+ }
+
+ /** Produces the immutable {@link SelectStatement}. */
+ public SelectStatement build() {
+ GroupBy groupBy = new GroupBy(List.copyOf(groupKeys), List.copyOf(groupingSets),
+ List.copyOf(groupingFunctions));
+ return new SelectStatement(distinct, List.copyOf(projections), Optional.ofNullable(from), List.copyOf(filters),
+ groupBy, List.copyOf(having), List.copyOf(orderKeys), Optional.ofNullable(rowLimit),
+ Optional.ofNullable(headerComment), new java.util.IdentityHashMap<>(filterComments),
+ Optional.ofNullable(footerComment), List.copyOf(statementHints));
+ }
+}
diff --git a/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/UpdateStatementBuilder.java b/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/UpdateStatementBuilder.java
new file mode 100644
index 0000000..d7300ad
--- /dev/null
+++ b/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/UpdateStatementBuilder.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (c) 2026 Contributors to the Eclipse Foundation.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * SmartCity Jena - initial
+ * Stefan Bischof (bipolis.org) - initial
+ */
+package org.eclipse.daanse.sql.statement.api;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+import org.eclipse.daanse.jdbc.db.api.schema.TableReference;
+import org.eclipse.daanse.sql.statement.api.expression.Predicate;
+import org.eclipse.daanse.sql.statement.api.expression.SqlExpression;
+import org.eclipse.daanse.sql.statement.api.model.UpdateStatement;
+import org.eclipse.daanse.sql.statement.api.model.UpdateStatement.Assignment;
+
+/** Mutable, dialect-free builder for an {@link UpdateStatement}. */
+public final class UpdateStatementBuilder {
+
+ private TableReference table;
+ private final List assignments = new ArrayList<>();
+ private final List filters = new ArrayList<>();
+ private String footerComment;
+
+ private UpdateStatementBuilder() {
+ }
+
+ public static UpdateStatementBuilder create() {
+ return new UpdateStatementBuilder();
+ }
+
+ public UpdateStatementBuilder table(String table) {
+ this.table = new TableReference(table);
+ return this;
+ }
+
+ public UpdateStatementBuilder table(String schema, String table) {
+ this.table = From.tableRef(schema, table);
+ return this;
+ }
+
+ public UpdateStatementBuilder table(TableReference table) {
+ this.table = table;
+ return this;
+ }
+
+ /** Adds a {@code SET column = value} assignment. */
+ public UpdateStatementBuilder set(String column, SqlExpression value) {
+ assignments.add(new Assignment(column, value));
+ return this;
+ }
+
+ public UpdateStatementBuilder where(Predicate predicate) {
+ filters.add(predicate);
+ return this;
+ }
+
+ /**
+ * Sets an optional trailing explanatory comment, appended at the very end
+ * (emitted only when comments are on).
+ */
+ public UpdateStatementBuilder footerComment(String comment) {
+ this.footerComment = comment;
+ return this;
+ }
+
+ public UpdateStatement build() {
+ return new UpdateStatement(Objects.requireNonNull(table, "table"), List.copyOf(assignments),
+ List.copyOf(filters), java.util.Optional.ofNullable(footerComment));
+ }
+}
diff --git a/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/WithStatementBuilder.java b/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/WithStatementBuilder.java
new file mode 100644
index 0000000..11dd903
--- /dev/null
+++ b/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/WithStatementBuilder.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (c) 2026 Contributors to the Eclipse Foundation.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * SmartCity Jena - initial
+ * Stefan Bischof (bipolis.org) - initial
+ */
+package org.eclipse.daanse.sql.statement.api;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+import org.eclipse.daanse.sql.statement.api.model.CommonTableExpression;
+import org.eclipse.daanse.sql.statement.api.model.Statement;
+import org.eclipse.daanse.sql.statement.api.model.WithStatement;
+
+/**
+ * Mutable, dialect-free builder for a {@link WithStatement}
+ * ({@code WITH ... }).
+ */
+public final class WithStatementBuilder {
+
+ private final List ctes = new ArrayList<>();
+ private boolean recursive;
+ private Statement body;
+ private String footerComment;
+
+ private WithStatementBuilder() {
+ }
+
+ public static WithStatementBuilder create() {
+ return new WithStatementBuilder();
+ }
+
+ /**
+ * Adds a common-table expression. Reference it from the body via
+ * {@code From.table(name, alias)}.
+ */
+ public WithStatementBuilder cte(String name, Statement query) {
+ ctes.add(new CommonTableExpression(name, List.of(), query));
+ return this;
+ }
+
+ /**
+ * Adds a CTE with an explicit column list (recommended/required for recursive
+ * CTEs).
+ */
+ public WithStatementBuilder cte(String name, List columns, Statement query) {
+ ctes.add(new CommonTableExpression(name, List.copyOf(columns), query));
+ return this;
+ }
+
+ /**
+ * Marks the CTEs as recursive (emitted as {@code WITH RECURSIVE} where the
+ * dialect supports it).
+ */
+ public WithStatementBuilder recursive(boolean value) {
+ this.recursive = value;
+ return this;
+ }
+
+ /** Sets the main statement that may reference the CTEs. */
+ public WithStatementBuilder body(Statement body) {
+ this.body = body;
+ return this;
+ }
+
+ /**
+ * Sets an optional trailing explanatory comment, appended at the very end
+ * (emitted only when comments are on).
+ */
+ public WithStatementBuilder footerComment(String comment) {
+ this.footerComment = comment;
+ return this;
+ }
+
+ public WithStatement build() {
+ if (ctes.isEmpty()) {
+ throw new IllegalStateException("WITH requires at least one common-table expression");
+ }
+ return new WithStatement(List.copyOf(ctes), recursive, Objects.requireNonNull(body, "body"),
+ java.util.Optional.ofNullable(footerComment));
+ }
+}
diff --git a/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/exec/StatementExecutionException.java b/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/exec/StatementExecutionException.java
new file mode 100644
index 0000000..aa84208
--- /dev/null
+++ b/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/exec/StatementExecutionException.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2026 Contributors to the Eclipse Foundation.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * SmartCity Jena - initial
+ * Stefan Bischof (bipolis.org) - initial
+ */
+package org.eclipse.daanse.sql.statement.api.exec;
+
+/**
+ * Unchecked wrapper for {@link java.sql.SQLException}s raised during statement
+ * execution.
+ */
+public class StatementExecutionException extends RuntimeException {
+
+ private static final long serialVersionUID = 1L;
+
+ public StatementExecutionException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/exec/StatementExecutor.java b/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/exec/StatementExecutor.java
new file mode 100644
index 0000000..137f956
--- /dev/null
+++ b/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/exec/StatementExecutor.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2026 Contributors to the Eclipse Foundation.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * SmartCity Jena - initial
+ * Stefan Bischof (bipolis.org) - initial
+ */
+package org.eclipse.daanse.sql.statement.api.exec;
+
+import java.util.List;
+
+import org.eclipse.daanse.sql.statement.api.render.RenderedSql;
+import org.eclipse.daanse.sql.statement.api.result.RowMapper;
+
+/**
+ * Executes already-rendered SQL and maps result rows. This is the adaptable
+ * seam: the JDBC implementation is {@link JdbcStatementExecutor}, but other
+ * backends (a test double, a remote service, a cached source) can implement the
+ * same contract.
+ */
+public interface StatementExecutor {
+
+ /**
+ * Runs a query and maps each row.
+ *
+ * @param sql the rendered query (carries the SQL text and column types)
+ * @param mapper maps each {@link org.eclipse.daanse.sql.statement.api.result.Row}
+ * to a {@code T}
+ * @return the mapped rows, in result order
+ */
+ List query(RenderedSql sql, RowMapper mapper);
+
+ /**
+ * Runs a write statement (INSERT/UPDATE/DELETE/DDL).
+ *
+ * @param sql the rendered statement (its bound parameters, if any, are applied)
+ * @return the affected-row count
+ */
+ int update(RenderedSql sql);
+
+ /**
+ * Runs a parameterized write statement once per value-row as a JDBC batch. The
+ * statement should be rendered with parameter markers (see
+ * {@code Expressions.paramMarker}); each {@code Object[]} supplies the values
+ * for one row, in placeholder order.
+ *
+ * @param sql the rendered statement (its placeholder count must match each
+ * row's length)
+ * @param rows the per-row parameter values
+ * @return the per-row affected-row counts (as from
+ * {@link java.sql.PreparedStatement#executeBatch()})
+ */
+ int[] batch(RenderedSql sql, List