diff --git a/.github/workflows/parparvm-tests.yml b/.github/workflows/parparvm-tests.yml index 060b8adbd0d..58e65b9a609 100644 --- a/.github/workflows/parparvm-tests.yml +++ b/.github/workflows/parparvm-tests.yml @@ -33,6 +33,7 @@ on: - 'CodenameOne/src/com/codename1/impl/SharedWithBackend.java' - 'CodenameOne/src/com/codename1/impl/orm/**' - 'CodenameOne/src/com/codename1/orm/session/**' + - 'CodenameOne/src/com/codename1/orm/repository/**' - 'CodenameOne/src/com/codename1/annotations/db/**' - 'CodenameOne/src/com/codename1/annotations/Entity.java' - 'CodenameOne/src/com/codename1/annotations/Id.java' @@ -85,6 +86,7 @@ on: - 'CodenameOne/src/com/codename1/impl/SharedWithBackend.java' - 'CodenameOne/src/com/codename1/impl/orm/**' - 'CodenameOne/src/com/codename1/orm/session/**' + - 'CodenameOne/src/com/codename1/orm/repository/**' - 'CodenameOne/src/com/codename1/annotations/db/**' - 'CodenameOne/src/com/codename1/annotations/Entity.java' - 'CodenameOne/src/com/codename1/annotations/Id.java' diff --git a/CodenameOne/src/com/codename1/impl/orm/GeneratedRepository.java b/CodenameOne/src/com/codename1/impl/orm/GeneratedRepository.java new file mode 100644 index 00000000000..c9028855b8a --- /dev/null +++ b/CodenameOne/src/com/codename1/impl/orm/GeneratedRepository.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.impl.orm; + +/// Marker for processor-owned repositories. Not application API. +/// @hidden +@com.codename1.impl.SharedWithBackend +@java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) +@java.lang.annotation.Target(java.lang.annotation.ElementType.TYPE) +public @interface GeneratedRepository { +} diff --git a/CodenameOne/src/com/codename1/impl/orm/JpqlQueryImpl.java b/CodenameOne/src/com/codename1/impl/orm/JpqlQueryImpl.java index 43be24d5057..2c270879895 100644 --- a/CodenameOne/src/com/codename1/impl/orm/JpqlQueryImpl.java +++ b/CodenameOne/src/com/codename1/impl/orm/JpqlQueryImpl.java @@ -331,6 +331,7 @@ private static final class Expr { String field; Parameter parameter; final List children = new ArrayList(); + List resultBranches; boolean literal; boolean aggregate; boolean numericOperands; @@ -845,7 +846,82 @@ private Expr multiply() { } return left; } + private Expr caseExpression() { + Expr operand = peek("WHEN") ? null : expression(); + List conditions = new ArrayList(); + List branches = new ArrayList(); + List predicates = new ArrayList(); + List tests = new ArrayList(); + do { + expect("WHEN"); + tests.add(expression()); + expect("THEN"); + branches.add(expression()); + } while (peek("WHEN")); + branches.add(take("ELSE") ? expression() : literal(-1, null)); + expect("END"); + if (operand != null) { + List comparisons = new ArrayList(tests); + comparisons.add(operand); + Expr comparisonMapping = matchingMapping(comparisons, "CASE"); + if (comparisonMapping != null && comparisonMapping.projectionQuery != null) { + for (Expr comparison : comparisons) { + bindType(comparisonMapping, comparison); + } + } + } + for (Expr test : tests) { + if (operand != null) { + compatible(operand, test); + bindType(operand, test); + bindType(test, operand); + String left = operand.sql; + String right = test.sql; + if (operand.kind == Attribute.BOOLEAN || test.kind == Attribute.BOOLEAN) { + left = condition(operand); right = condition(test); + } else if (numericKind(operand.kind) && numericKind(test.kind) + && (operand.kind == Attribute.REAL || test.kind == Attribute.REAL)) { + left = session.numericOperand(left, Attribute.REAL); + right = session.numericOperand(right, Attribute.REAL); + } + test = node("(" + left + " = " + right + ")", Attribute.BOOLEAN, operand, test); + } + predicates.add(condition(test)); + conditions.add(test); + } + int kind = commonKind(branches); + if (kind < 0) { + throw error("CASE needs at least one typed result branch"); + } + Expr mapping = coalesceMapping(branches); + StringBuilder sql = new StringBuilder("CASE"); + for (int i = 0; i < branches.size(); i++) { + Expr branch = branches.get(i); + requireKind(branch, kind); + if (branch.entity != null || branch.association || branch.predicate) { + throw error("CASE result branches must be scalar values, not entities or predicates"); + } + if (mapping != null && mapping.projectionQuery != null) { + bindType(mapping, branch); + } + sql.append(i < predicates.size() ? " WHEN " + predicates.get(i) + " THEN " : " ELSE "); + // Explicit numeric casts give PostgreSQL the same branch promotion as SQLite. + sql.append(numericKind(kind) ? session.numericOperand(branch.sql, kind) : branch.sql); + } + Expr result = new Expr(sql.append(" END").toString(), kind); + result.resultBranches = branches; + result.children.addAll(conditions); + result.children.addAll(branches); + if (mapping != null && mapping.projectionQuery != null && kind == mapping.kind) { + result.query = result.projectionQuery = mapping.projectionQuery; + result.field = result.projectionField = mapping.projectionField; + } + return result; + } private Expr primary() { + if (take("CASE")) { + return caseExpression(); + } if (take("-")) { String magnitude = peek(); if (magnitude.length() > 0 && Character.isDigit(magnitude.charAt(0)) @@ -1137,6 +1213,7 @@ private Expr wrap(String sql, Expr value) { result.nullPreserving = true; result.numericOperands = true; result.predicate = value.predicate; + result.resultBranches = value.resultBranches; result.association = value.association; result.projectionQuery = value.projectionQuery; result.projectionField = value.projectionField; @@ -1183,6 +1260,14 @@ private boolean nonNullExpression(Expr value, boolean allowParameters, boolean e if (value.nonNull) { return true; } + if (value.resultBranches != null) { + for (Expr branch : value.resultBranches) { + if (!nonNullExpression(branch, allowParameters, enforce)) { + return false; + } + } + return true; + } if (value.coalesce) { // Prefer an unconditional fallback so earlier nullable parameters // remain usable when a later operand already guarantees a value. @@ -1230,6 +1315,9 @@ private boolean selected(Expr term, List selections) { return false; } private Expr coalesceMapping(List args) { + return matchingMapping(args, "COALESCE"); + } + private Expr matchingMapping(List args, String operator) { Expr selected = null; String mapping = null; for (Expr arg : args) { @@ -1239,7 +1327,7 @@ private Expr coalesceMapping(List args) { String current = arg.projectionQuery == null ? "" : arg.projectionQuery.mapping(arg.projectionField); if (mapping != null && !mapping.equals(current)) { - throw error("COALESCE operands require matching domain and converter mappings"); + throw error(operator + " operands require matching domain and converter mappings"); } if (selected == null) { selected = arg; @@ -1255,8 +1343,8 @@ private void requireIntegralAssignment(Expr value) { if (value.parameter != null) { value.parameter.integralAssignment = true; } - if (value.kind < 0 || value.numericOperands) { - for (Expr child : value.children) { + if (value.kind < 0 || value.numericOperands || value.resultBranches != null) { + for (Expr child : value.resultBranches == null ? value.children : value.resultBranches) { requireIntegralAssignment(child); } } @@ -1269,7 +1357,7 @@ private void validateAssignment(String mapping, Expr value) { && !mapping.equals(value.projectionQuery.mapping(value.projectionField))) { throw error("Bulk assignment requires matching domain and converter mappings"); } - for (Expr child : value.children) { + for (Expr child : value.resultBranches == null ? value.children : value.resultBranches) { validateAssignment(mapping, child); } } @@ -1285,7 +1373,7 @@ private void bindType(Expr attribute, Expr value) { value.parameter.query = attribute.query; value.parameter.field = attribute.field; } - for (Expr child : value.children) { + for (Expr child : value.resultBranches == null ? value.children : value.resultBranches) { bindType(attribute, child); } } diff --git a/CodenameOne/src/com/codename1/impl/orm/NativeQueryImpl.java b/CodenameOne/src/com/codename1/impl/orm/NativeQueryImpl.java new file mode 100644 index 00000000000..08ba6a963db --- /dev/null +++ b/CodenameOne/src/com/codename1/impl/orm/NativeQueryImpl.java @@ -0,0 +1,202 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.impl.orm; + +import com.codename1.orm.session.JpqlQuery; +import com.codename1.orm.session.PersistenceException; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/// Scalar native SQL using the session's connection and failure handling. +/// @hidden +@com.codename1.impl.SharedWithBackend +public final class NativeQueryImpl implements JpqlQuery { + private final SessionImpl session; + private final String sql; + private final boolean mutation; + private final int kind; + private final Class type; + private final List names = new ArrayList(); + private final Map parameters = new LinkedHashMap(); + private int maximum = -1; + private int skip; + + NativeQueryImpl(SessionImpl session, String statement, Class type) { + if (statement == null || type == null) { + throw new IllegalArgumentException("statement/resultType is null"); + } + this.session = session; + this.type = type; + String start = statement.trim(); + mutation = keyword(start, "UPDATE") || keyword(start, "DELETE"); + if (!mutation && !keyword(start, "SELECT")) { + throw new IllegalArgumentException("Native query must start with SELECT, UPDATE or DELETE followed by whitespace"); + } + kind = type == String.class ? Attribute.TEXT : type == Long.class || type == Object.class && mutation ? Attribute.BIGINT + : type == Integer.class ? Attribute.INTEGER : type == Double.class ? Attribute.REAL + : type == Boolean.class ? Attribute.BOOLEAN : type == byte[].class ? Attribute.BLOB : -1; + if (kind < 0) { + throw new IllegalArgumentException("Native selects support String, Long, Integer, Double, Boolean or byte[] scalars"); + } + StringBuilder output = new StringBuilder(); + char quote = 0; + for (int i = 0; i < statement.length(); i++) { + char c = statement.charAt(i); + if (c == '\\') { + throw new IllegalArgumentException("Bind backslashes as parameter values instead of SQL literals"); + } + if (quote != 0) { + output.append(c); + if (c == quote) { + if (i + 1 < statement.length() && statement.charAt(i + 1) == quote) { + output.append(statement.charAt(++i)); + } else { + quote = 0; + } + } + } else if (c == '\'' || c == '"') { + quote = c; output.append(c); + } else if (c == ':' && i + 1 < statement.length() && statement.charAt(i + 1) == ':') { + output.append("::"); i++; + } else if (c == ':') { + int begin = ++i; + if (i >= statement.length() || !Character.isJavaIdentifierStart(statement.charAt(i))) { + throw new IllegalArgumentException("Expected native parameter name after colon"); + } + while (i < statement.length() && Character.isJavaIdentifierPart(statement.charAt(i))) { + i++; + } + names.add(statement.substring(begin, i--)); output.append('?'); + } else { + if (c == ';' || c == '?' || c == '$' || c == '`' + || c == '-' && i + 1 < statement.length() && statement.charAt(i + 1) == '-' + || c == '/' && i + 1 < statement.length() && statement.charAt(i + 1) == '*') { + throw new IllegalArgumentException("Native queries require one statement, named parameters, standard quotes and no comments"); + } + output.append(c); + } + } + if (quote != 0) { + throw new IllegalArgumentException("Unterminated native SQL quote"); + } + sql = output.toString(); + } + private static boolean keyword(String sql, String word) { + return sql.length() > word.length() && sql.substring(0, word.length()).equalsIgnoreCase(word) + && Character.isWhitespace(sql.charAt(word.length())); + } + @Override + public JpqlQuery setParameter(String name, Object value) { + if (!names.contains(name)) { + throw new IllegalArgumentException("Unknown named parameter: " + name); + } + if (value instanceof Iterable || value != null && value.getClass().isArray() && !(value instanceof byte[])) { + throw new IllegalArgumentException("Native parameters must be scalars or byte[] BLOBs"); + } + parameters.put(name, value); return this; + } + @Override + public JpqlQuery limit(int value) { + if (value < 0) { + throw new IllegalArgumentException("Negative limit"); + } + maximum = value; return this; + } + @Override + public JpqlQuery offset(int value) { + if (value < 0) { + throw new IllegalArgumentException("Negative offset"); + } + skip = value; return this; + } + private Object[] arguments() { + SessionImpl.checkParameterCount(names.size()); + Object[] result = new Object[names.size()]; + for (int i = 0; i < result.length; i++) { + String name = names.get(i); + if (!parameters.containsKey(name)) { + throw new IllegalArgumentException("Unbound named parameter: " + name); + } + result[i] = Values.storage(parameters.get(name)); + } + return result; + } + @Override + public List list() { + return list(maximum); + } + @Override + public T first() { + List rows = list(maximum == 0 ? 0 : 1); + return rows.isEmpty() ? null : rows.get(0); + } + private List list(int max) { + if (mutation) { + throw new IllegalStateException("Use executeUpdate for bulk mutations"); + } + Object[] args = arguments(); + session.autoFlush(); + List result = new ArrayList(); + if (max == 0) { + return result; + } + for (Object[] row : session.read(sql + session.limit(max, skip), args, new int[]{SqlAccess.NATIVE_SCALAR})) { + Object value = row[0]; + try { + if (kind == Attribute.REAL && value instanceof Number) { + value = Double.valueOf(((Number) value).doubleValue()); + } else if (kind == Attribute.BOOLEAN) { + value = Values.asBooleanObject(value); + } else if (kind == Attribute.INTEGER) { + value = Values.asIntObject(value); + } else if (kind == Attribute.BIGINT) { + value = Values.asLongObject(value); + } + } catch (java.io.IOException error) { + throw new PersistenceException("Invalid native scalar projection", error); + } + if (value != null && !type.isInstance(value)) { + throw new PersistenceException("Native scalar does not match " + type.getName()); + } + result.add((T) value); + } + return result; + } + @Override + public int executeUpdate() { + if (!mutation) { + throw new IllegalStateException("Not a bulk mutation"); + } + if (maximum >= 0 || skip > 0) { + throw new IllegalArgumentException("Bulk mutation pagination is not supported"); + } + Object[] args = arguments(); + session.requireTransaction(); + session.flush(); + int changed = session.write(sql, args); + session.clear(); + return changed; + } +} diff --git a/CodenameOne/src/com/codename1/impl/orm/RepositoryFactories.java b/CodenameOne/src/com/codename1/impl/orm/RepositoryFactories.java new file mode 100644 index 00000000000..e3be19f938b --- /dev/null +++ b/CodenameOne/src/com/codename1/impl/orm/RepositoryFactories.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.impl.orm; + +import com.codename1.orm.session.Session; +import com.codename1.orm.session.PersistenceException; +import java.util.HashMap; +import java.util.Map; + +/// Direct factories installed by the generated DAO bootstrap. +/// @hidden +@com.codename1.impl.SharedWithBackend +public final class RepositoryFactories { + private static final Map, Factory> FACTORIES = new HashMap, Factory>(); + + private RepositoryFactories() { + } + + public interface Factory { + T create(Session session); + } + + public static synchronized void register(Class type, Factory factory) { + FACTORIES.put(type, factory); + } + + @SuppressWarnings("unchecked") + public static synchronized T create(Class type, Session session) { + Factory factory = (Factory) FACTORIES.get(type); + if (factory == null) { + throw new PersistenceException("No generated repository registered for " + type.getName() + + "; run cn1:process-annotations and initialize the generated DAO bootstrap"); + } + return factory.create(session); + } +} diff --git a/CodenameOne/src/com/codename1/impl/orm/SessionImpl.java b/CodenameOne/src/com/codename1/impl/orm/SessionImpl.java index bdb135a42a0..93951e15bc7 100644 --- a/CodenameOne/src/com/codename1/impl/orm/SessionImpl.java +++ b/CodenameOne/src/com/codename1/impl/orm/SessionImpl.java @@ -182,6 +182,12 @@ EntityModel model(String name) { } return result; } + @Override + public com.codename1.orm.session.JpqlQuery createNativeQuery(String statement, Class resultType) { + check(); + return new NativeQueryImpl(this, statement, resultType); + } + @Override public JpqlQueryImpl createQuery(String statement, Class resultType) { check(); diff --git a/CodenameOne/src/com/codename1/impl/orm/SessionSqlAccess.java b/CodenameOne/src/com/codename1/impl/orm/SessionSqlAccess.java index 0a4786f4c09..37db204f62a 100644 --- a/CodenameOne/src/com/codename1/impl/orm/SessionSqlAccess.java +++ b/CodenameOne/src/com/codename1/impl/orm/SessionSqlAccess.java @@ -136,17 +136,57 @@ private void validateParameters(Object[] args) throws IOException { } } } + private static boolean containsAlias(String sql, String alias) { + for (int i = 0; i <= sql.length() - alias.length(); i++) { + if (sql.regionMatches(true, i, alias, 0, alias.length())) { + return true; + } + } + return false; + } @Override public List query(String sql, Object[] args, int[] kinds) throws IOException { validateParameters(args); synchronized (db) { checkOwner(); + boolean nativeScalar = kinds.length == 1 && kinds[0] == SqlAccess.NATIVE_SCALAR; + if (nativeScalar) { + // SQLite's typed Row getters coerce values. Read typeof alongside the + // value, and let the CTE's column list enforce width even for empty results. + StringBuilder alias = new StringBuilder("cn1_native_scalar"); + while (containsAlias(sql, alias.toString())) { + alias.append('_'); + } + // OFFSET prevents SQLite flattening the CTE and evaluating a volatile + // expression separately for its type and value. + sql = "WITH " + alias + "(value) AS (SELECT * FROM (" + sql + + ") LIMIT -1 OFFSET 0) SELECT typeof(value), value FROM " + alias; + } Cursor cursor = db.executeQuery(sql, args); try { + if (cursor.getColumnCount() != (nativeScalar ? 2 : kinds.length)) { + throw new IOException("Unexpected SQL projection width"); + } List result = new ArrayList(); while (cursor.next()) { Row row = cursor.getRow(); Object[] values = new Object[kinds.length]; + if (nativeScalar) { + String storage = row.getString(0); + if ("integer".equals(storage)) { + values[0] = Long.valueOf(row.getLong(1)); + } else if ("real".equals(storage)) { + values[0] = Double.valueOf(row.getDouble(1)); + } else if ("text".equals(storage)) { + values[0] = row.getString(1); + } else if ("blob".equals(storage)) { + values[0] = row.getBlob(1); + } else if (!"null".equals(storage)) { + throw new IOException("Unknown SQLite storage type: " + storage); + } + result.add(values); + continue; + } for (int i = 0; i < values.length; i++) { switch (kinds[i]) { case Attribute.TEXT: diff --git a/CodenameOne/src/com/codename1/impl/orm/SqlAccess.java b/CodenameOne/src/com/codename1/impl/orm/SqlAccess.java index cd20c61f791..2e1c02f7050 100644 --- a/CodenameOne/src/com/codename1/impl/orm/SqlAccess.java +++ b/CodenameOne/src/com/codename1/impl/orm/SqlAccess.java @@ -33,6 +33,8 @@ /// @hidden @com.codename1.impl.SharedWithBackend public interface SqlAccess { + /// A single native scalar read using its physical storage type, without requested-type coercion. + int NATIVE_SCALAR = -2; /// Returns the dialect identifier used for schema and query choices. String dialect(); /// Rows contain column name, declared SQL type, not-null flag, primary-key flag. diff --git a/CodenameOne/src/com/codename1/orm/repository/CrudRepository.java b/CodenameOne/src/com/codename1/orm/repository/CrudRepository.java new file mode 100644 index 00000000000..6a1f97d43c4 --- /dev/null +++ b/CodenameOne/src/com/codename1/orm/repository/CrudRepository.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.orm.repository; + +/// Base contract for generated repositories. The caller owns the Session and transaction. +/// save returns the managed merge result; insert schedules a new instance for persistence. +@com.codename1.impl.SharedWithBackend +public interface CrudRepository { + E findById(K id); + E save(E entity); + void insert(E entity); + void deleteById(K id); + java.util.List findAll(); + long count(); + boolean existsById(K id); +} diff --git a/CodenameOne/src/com/codename1/orm/repository/Lock.java b/CodenameOne/src/com/codename1/orm/repository/Lock.java new file mode 100644 index 00000000000..76b17ed1667 --- /dev/null +++ b/CodenameOne/src/com/codename1/orm/repository/Lock.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.orm.repository; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/// Applies a lock to a findById lookup in the caller-owned transaction. +@com.codename1.impl.SharedWithBackend +@Retention(RetentionPolicy.CLASS) +@Target(ElementType.METHOD) +public @interface Lock { + com.codename1.orm.session.LockMode value(); +} diff --git a/CodenameOne/src/com/codename1/orm/repository/Modifying.java b/CodenameOne/src/com/codename1/orm/repository/Modifying.java new file mode 100644 index 00000000000..24aa9efa4aa --- /dev/null +++ b/CodenameOne/src/com/codename1/orm/repository/Modifying.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.orm.repository; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/// Marks a bulk update or delete. Always flushes before and clears after execution. +@com.codename1.impl.SharedWithBackend +@Retention(RetentionPolicy.CLASS) +@Target(ElementType.METHOD) +public @interface Modifying { + +} diff --git a/CodenameOne/src/com/codename1/orm/repository/PageRequest.java b/CodenameOne/src/com/codename1/orm/repository/PageRequest.java new file mode 100644 index 00000000000..8f44ebbe27f --- /dev/null +++ b/CodenameOne/src/com/codename1/orm/repository/PageRequest.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.orm.repository; + +/// An immutable offset and positive maximum row count. Supply a deterministic OrderBy +/// in the repository method name or declared query when paging. +@com.codename1.impl.SharedWithBackend +public final class PageRequest { + private final int offset; + private final int size; + public PageRequest(int offset, int size) { + if (offset < 0 || size <= 0) { + throw new IllegalArgumentException("Page offset must be nonnegative and size positive"); + } + this.offset = offset; + this.size = size; + } + public int getOffset() { + return offset; + } + public int getSize() { + return size; + } +} diff --git a/CodenameOne/src/com/codename1/orm/repository/Param.java b/CodenameOne/src/com/codename1/orm/repository/Param.java new file mode 100644 index 00000000000..794bb9c9608 --- /dev/null +++ b/CodenameOne/src/com/codename1/orm/repository/Param.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.orm.repository; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/// Names a declared query parameter without its colon. +@com.codename1.impl.SharedWithBackend +@Retention(RetentionPolicy.CLASS) +@Target(ElementType.PARAMETER) +public @interface Param { + String value(); +} diff --git a/CodenameOne/src/com/codename1/orm/repository/Query.java b/CodenameOne/src/com/codename1/orm/repository/Query.java new file mode 100644 index 00000000000..b74ed32c933 --- /dev/null +++ b/CodenameOne/src/com/codename1/orm/repository/Query.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.orm.repository; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/// Declares JPQL, or scalar native SQL when nativeSql is true. +@com.codename1.impl.SharedWithBackend +@Retention(RetentionPolicy.CLASS) +@Target(ElementType.METHOD) +public @interface Query { + String value(); + boolean nativeSql() default false; +} diff --git a/CodenameOne/src/com/codename1/orm/repository/Repositories.java b/CodenameOne/src/com/codename1/orm/repository/Repositories.java new file mode 100644 index 00000000000..1c34c9f03d8 --- /dev/null +++ b/CodenameOne/src/com/codename1/orm/repository/Repositories.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.orm.repository; + +import com.codename1.orm.session.Session; + +/// Creates session-bound repositories through factories installed by the generated DAO bootstrap. +/// Callers reference their repository interface, so they compile before implementations are generated. +@com.codename1.impl.SharedWithBackend +public final class Repositories { + private Repositories() { + } + + /// Creates an implementation of the annotated repository interface bound to the supplied session. + public static T create(Class repositoryType, Session session) { + if (repositoryType == null || session == null) { + throw new IllegalArgumentException("Repository type and session are required"); + } + return com.codename1.impl.orm.RepositoryFactories.create(repositoryType, session); + } +} diff --git a/CodenameOne/src/com/codename1/orm/repository/Repository.java b/CodenameOne/src/com/codename1/orm/repository/Repository.java new file mode 100644 index 00000000000..be5fd23906b --- /dev/null +++ b/CodenameOne/src/com/codename1/orm/repository/Repository.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.orm.repository; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/// Generates an implementation named InterfaceNameCn1Repository at build time. +@com.codename1.impl.SharedWithBackend +@Retention(RetentionPolicy.CLASS) +@Target(ElementType.TYPE) +public @interface Repository { + Class entity(); + Class key(); +} diff --git a/CodenameOne/src/com/codename1/orm/repository/package-info.java b/CodenameOne/src/com/codename1/orm/repository/package-info.java new file mode 100644 index 00000000000..f01c634d35c --- /dev/null +++ b/CodenameOne/src/com/codename1/orm/repository/package-info.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +/// Build-time repository interfaces over managed ORM sessions. +/// Implementations are generated by the ORM processor. Applications supply the +/// Session and own its transaction boundaries; bulk methods flush and clear it. +package com.codename1.orm.repository; diff --git a/CodenameOne/src/com/codename1/orm/session/JpqlQuery.java b/CodenameOne/src/com/codename1/orm/session/JpqlQuery.java index b73b3f545ad..b19003d8ec3 100644 --- a/CodenameOne/src/com/codename1/orm/session/JpqlQuery.java +++ b/CodenameOne/src/com/codename1/orm/session/JpqlQuery.java @@ -24,7 +24,10 @@ import java.util.List; -/// A mutable query obtained from {@link Session#createQuery(String, Class)}. +/// A mutable query obtained from {@link Session#createQuery(String, Class)} or +/// {@link Session#createNativeQuery(String, Class)}. Native queries use the same +/// binding, pagination and bulk-execution contract but accept native SQL instead +/// of the JPQL features below. /// Supports entity and scalar selections, relationship joins, aggregates, grouping, /// ordering, predicate subqueries, and transactional bulk update/delete statements. /// This is a JPQL subset, not a complete JPA implementation; unsupported syntax is diff --git a/CodenameOne/src/com/codename1/orm/session/Session.java b/CodenameOne/src/com/codename1/orm/session/Session.java index 6c7fbbaf803..8466048686f 100644 --- a/CodenameOne/src/com/codename1/orm/session/Session.java +++ b/CodenameOne/src/com/codename1/orm/session/Session.java @@ -53,6 +53,18 @@ public interface Session { /// @throws IllegalArgumentException if syntax or a mapped name is unsupported JpqlQuery createQuery(String statement); + /// Creates a native SQL query with named parameters. Selects return a single + /// scalar column of String, Long, Integer, Double, Boolean, or byte[]. Entity + /// hydration and tuples are not supported. SQL identifiers and functions are + /// database-specific. Bulk UPDATE/DELETE requires a transaction, flushes first, + /// and clears the persistence context after success, just like JPQL. + /// @param scalar result type + /// @param statement one SELECT, UPDATE or DELETE statement + /// @param resultType scalar class, or Object.class for mutations + /// @return a query belonging to this session + /// @throws IllegalArgumentException for unsupported query syntax or result types + JpqlQuery createNativeQuery(String statement, Class resultType); + /// Starts a transaction for this session. /// @throws PersistenceException if the session is closed, a transaction is already /// active, or the database cannot begin the transaction diff --git a/docs/demos/common/src/main/java/com/codenameone/developerguide/snippets/repository/Records.java b/docs/demos/common/src/main/java/com/codenameone/developerguide/snippets/repository/Records.java new file mode 100644 index 00000000000..6eb25578081 --- /dev/null +++ b/docs/demos/common/src/main/java/com/codenameone/developerguide/snippets/repository/Records.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codenameone.developerguide.snippets.repository; + +import com.codename1.orm.repository.*; +import com.codename1.orm.session.LockMode; +import java.util.List; + +/** Build-time repository example; no handwritten implementation. */ +// tag::generated-repository[] +@Repository(entity=RepositoryRecord.class, key=Long.class) +public interface Records extends CrudRepository { + List findByNameAndScoreGreaterThanOrderByScoreDescIdAsc(String name, long score, PageRequest page); + boolean existsByName(String name); + long countByName(String name); + int deleteByName(String name); + @Lock(LockMode.PESSIMISTIC_WRITE) + RepositoryRecord findById(Long id); + @Query("select sum(case when r.score>=:floor then 1 else 0 end) from com.codenameone.developerguide.snippets.repository.RepositoryRecord r") + Long qualified(@Param("floor") long floor); + @Query(value="SELECT COUNT(*) FROM repository_records WHERE name=:name OR name=:name", nativeSql=true) + Long nativeCount(@Param("name") String name); + @Modifying + @Query("update com.codenameone.developerguide.snippets.repository.RepositoryRecord r set r.score=:score where r.name=:name") + int revise(@Param("name") String name, @Param("score") long score); +} +// end::generated-repository[] diff --git a/docs/demos/common/src/main/java/com/codenameone/developerguide/snippets/repository/RepositoryRecord.java b/docs/demos/common/src/main/java/com/codenameone/developerguide/snippets/repository/RepositoryRecord.java new file mode 100644 index 00000000000..a02367c2b86 --- /dev/null +++ b/docs/demos/common/src/main/java/com/codenameone/developerguide/snippets/repository/RepositoryRecord.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codenameone.developerguide.snippets.repository; + +import com.codename1.annotations.Entity; +import com.codename1.annotations.Id; + +/** Entity used by the generated repository guide example. */ +@Entity(table="repository_records") +public class RepositoryRecord { + @Id public long id; + public String name; + public long score; +} diff --git a/docs/demos/common/src/main/java/com/codenameone/developerguide/snippets/repository/RepositorySnippets.java b/docs/demos/common/src/main/java/com/codenameone/developerguide/snippets/repository/RepositorySnippets.java new file mode 100644 index 00000000000..f818352a71b --- /dev/null +++ b/docs/demos/common/src/main/java/com/codenameone/developerguide/snippets/repository/RepositorySnippets.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codenameone.developerguide.snippets.repository; + +import com.codename1.orm.session.Session; +import com.codename1.orm.EntityManager; +import com.codename1.orm.repository.Repositories; + +public class RepositorySnippets { + // tag::repository-unit-of-work[] + public void insertRecord(EntityManager entityManager) { + Session session = entityManager.openSession(); + Records records = Repositories.create(Records.class, session); + session.beginTransaction(); + try { + RepositoryRecord record = new RepositoryRecord(); + record.name = "open"; + records.insert(record); + session.commitTransaction(); + } catch (RuntimeException error) { + session.rollbackTransaction(); + throw error; + } finally { + session.close(); + } + } + // end::repository-unit-of-work[] +} diff --git a/docs/developer-guide/Annotation-SQLite-ORM.asciidoc b/docs/developer-guide/Annotation-SQLite-ORM.asciidoc index d1204dbad0a..096e5ac59de 100644 --- a/docs/developer-guide/Annotation-SQLite-ORM.asciidoc +++ b/docs/developer-guide/Annotation-SQLite-ORM.asciidoc @@ -157,6 +157,22 @@ parameters can't be compared directly. `IS NULL` and `IS NOT NULL` also require known operand type; a standalone untyped parameter is rejected. A scalar nested query requires an aggregate without `GROUP BY`, so it returns at most one row. `IN` and `EXISTS` may use queries that return multiple rows. +Searched `CASE WHEN predicate THEN value ... ELSE value END` and simple +`CASE expression WHEN value THEN value ... ELSE value END` work in scalar +expressions, including `SUM(CASE WHEN ... THEN ... ELSE ... END)`. The first +matching branch wins. An omitted `ELSE` yields SQL NULL; a NULL condition is +not true, and simple CASE uses SQL equality (NULL doesn't match NULL). Aggregates +ignore NULL inputs; SUM over no non-null inputs yields NULL, while COUNT yields +zero. Use a boxed scalar return type when NULL is possible. + +Result branches must have compatible scalar storage types. Integral branches +promote to Long when combined with Long literals; any real branch promotes the +result to Double. At least one branch must have a known type; CASE with only +untyped parameters or NULL branches is rejected. Entity and predicate result +branches are unsupported; Boolean values and literals are supported. Converter +mappings must agree across mapped result branches. Parameters in WHEN conditions +and result branches remain bound parameters. + A `DISTINCT` query can order only by selected expressions. Text range comparisons and `LIKE` patterns are case-sensitive across supported databases. An explicit `LIKE ... ESCAPE` uses a literal or named parameter for both the pattern and the @@ -409,3 +425,105 @@ The runtime registry is keyed on `Class#getName()`; obfuscation renames the call sites and the registered keys together within a single execution. The keys are never persisted across builds, so the renaming has no observable effect on behavior. + + +[[generated-orm-repositories]] +=== Generated repositories + +A public top-level interface annotated with +`com.codename1.orm.repository.Repository` and extending +`CrudRepository` directly gets an `InterfaceNameCn1Repository` +implementation during `cn1:process-annotations`. Both class literals are explicit; +the key must match the mapped identifier's boxed type (the embedded key class or +`Identifier` for composite keys). Use `Repositories.create(Records.class, session)` to obtain an implementation. +This factory references the interface, so application sources compile on a clean +build before `process-classes` generates the implementation. The generated DAO +bootstrap registers direct factories alongside the entity mappings. +The implementation makes direct session calls. There is no runtime classpath scan, +proxy generation, or Hibernate dependency. + +[source,java] +---- +include::../demos/common/src/main/java/com/codenameone/developerguide/snippets/repository/Records.java[tag=generated-repository] +---- + +The example's `RepositoryRecord` entity has `@Id public long id`, +`public String name`, and `public long score`. This operation opens a session, +creates its repository, commits the insert, and closes the session: + +[source,java] +---- +include::../demos/common/src/main/java/com/codenameone/developerguide/snippets/repository/RepositorySnippets.java[tag=repository-unit-of-work] +---- + +The inherited CRUD methods are `insert`, `save`, `findById`, `findAll`, `count`, +`existsById`, and `deleteById`. `insert` calls `persist`; `save` calls `merge` and +returns the managed instance, which may differ from the argument. `findById` +returns null for a missing row. `deleteById` removes a managed entity and observes +its cascades and callbacks; deleting a missing key is a no-op in an active +transaction. + +Derived methods support the following bounded grammar: + +* `findBy`, `findFirstBy`, `findTopBy`, `findFirstNBy`, and `findTopNBy` return the + entity or `List`. A single result means the first row, not a uniqueness + assertion. +* `countBy` returns `long`; `existsBy` returns `boolean`; `deleteBy` returns `int` + (affected rows) or `void`. +* Direct scalar property names combine with `And` and `Or`; AND has higher precedence than OR. + Comparison suffixes are `Not`, `GreaterThan`, `GreaterThanEqual`, `LessThan`, + `LessThanEqual`, `Like`, and `In`. No suffix means equality. `IsNull`, + `IsNotNull`, `True`, and `False` consume no parameter. `In` takes an Iterable or + object array whose concrete element type matches the property. Raw and wildcard + collections are rejected at build time. `Like` requires String properties with + text storage. `True` and `False` require Boolean + properties with Boolean storage. Use `IsNull` to match NULL; equality retains SQL NULL semantics. +* Find methods can end with `OrderByScoreDescIdAsc`; every field needs an explicit + `Asc` or `Desc`. Relationship paths and ambiguous property names containing + grammar keywords should use `@Query` instead. +* A list select can take one final `PageRequest(offset, size)`. Offset must be + nonnegative and size positive. It returns the requested list without a separate + total-count query. Use explicit ordering with a unique tie-breaker for stable + pages. Top/First and PageRequest aren't combined. + +`@Query` declares the existing JPQL subset. Every value argument requires a unique +`@Param("name")` matching the query exactly. SQL values are bound, never +interpolated. `@Modifying` is required for declared UPDATE/DELETE and forbidden +for SELECT. Such methods return `int` or `void`. Native SQL uses +`@Query(value="...", nativeSql=true)` with the same named parameters, including +repeated occurrences. Native parameters accept primitive and boxed scalar types, +String, Date, BigInteger, BigDecimal, and `byte[]` BLOBs. Date values bind as epoch +milliseconds and Character values as numeric code units on both client and backend. +Collections and other array types are rejected during generation. Native repository +selects return one scalar column of +String, Long, Integer, Double, or Boolean, or a List of that type. They don't hydrate +entities. Scalar conversion rejects fractional values requested as integers and +integer values requested as String; it preserves SQL NULL. Native SQL is +database-specific: use portable SQL or validate each engine. Only SELECT, UPDATE, and DELETE are accepted; comments, multiple +statements, positional parameters, dollar quoting, and backtick quoting are +unsupported. Use standard quotes and bind values containing backslashes. + +Unsupported signatures, unmapped derived fields, and missing or surplus declared +parameter names produce build errors naming the repository and method. JPQL syntax +and mapped names are validated when the method creates its query, before SQL +execution. Native SQL syntax beyond the supported envelope is checked by the +database. Generic methods, additional parent interfaces, default methods, dynamic +sort arguments, and arbitrary DTO projections aren't supported. + +Repositories never open, commit, roll back, or close transactions or sessions. +The caller owns those boundaries, including when the supplied Session is injected +into a backend `@Transactional` service. Writes require an active usable +transaction. Reads follow Session's automatic flush rules. Redeclare only +`findById(Key)` with `@Lock` to request a lock; pessimistic locks need an active +transaction and an engine supporting row locks. SQLite rejects them explicitly. + +Derived deletes and declared JPQL/native modifications flush pending changes, +execute bulk SQL, then clear the entire persistence context. They bypass entity +callbacks, cascades, and automatic optimistic-version updates. Previously held +objects are detached and retain their old Java values; reload them before further +work. There is no option to disable flush or clear. Validation failures remain +`IllegalArgumentException`; database failures remain `PersistenceException`, and +optimistic conflicts remain `OptimisticLockException`. Repository code neither +swallows nor retries them. Roll back after a failed database write; Session marks +the transaction rollback-only. Rollback detaches entities without reverting Java +field values. diff --git a/docs/developer-guide/Backend-Data.asciidoc b/docs/developer-guide/Backend-Data.asciidoc index 8f894984c3e..5d056ac7097 100644 --- a/docs/developer-guide/Backend-Data.asciidoc +++ b/docs/developer-guide/Backend-Data.asciidoc @@ -8,6 +8,10 @@ make several statements one unit of work. The last of those gets the most room, because declarative transactions are where a Spring developer's intuitions are most likely to be almost right. +For generated CRUD interfaces, derived finders, paging, declared queries, and +conditional aggregates over managed sessions, see +<>. + === Talking to a database The server opens a connection pool from `cn1.datasource.url` -- a SQLite path or diff --git a/maven/backend/spotbugs-exclude.xml b/maven/backend/spotbugs-exclude.xml index 308858aa531..375269ad05c 100644 --- a/maven/backend/spotbugs-exclude.xml +++ b/maven/backend/spotbugs-exclude.xml @@ -33,6 +33,17 @@ + + + + + +