Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 122 additions & 0 deletions examples/exact-decimal/GeneratedExactDecimalProbe.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package com.example.exactdecimalservice;

import com.example.exactdecimalservice.decimalentry.DecimalEntry;
import com.example.exactdecimalservice.platform.Platform;
import io.teaql.core.meta.SimpleEntityMetaFactory;
import io.teaql.core.sql.portable.IdSpaceIdGenerator;
import io.teaql.core.sqlite.SqliteDataServiceExecutor;
import io.teaql.provider.jdbc.JdbcSqlExecutor;
import io.teaql.runtime.TeaQLRuntime;
import java.math.BigDecimal;
import java.nio.file.Files;
import java.nio.file.Path;
import org.sqlite.SQLiteDataSource;

/** Application-owned exact Decimal check against a freshly generated domain library. */
public final class GeneratedExactDecimalProbe {
private GeneratedExactDecimalProbe() {}

private static TeaQLRuntime runtime(SQLiteDataSource dataSource, JdbcSqlExecutor sql) {
return TeaQLRuntime.builder()
.metadata(new SimpleEntityMetaFactory())
.dataService("default", new SqliteDataServiceExecutor("default", sql, dataSource))
.idGenerationService(new IdSpaceIdGenerator(new WorkspaceTeaQLDatabase(sql, dataSource)))
.executionLogging(false)
.build()
.install(GeneratedRuntimeModule.module());
}

public static void main(String[] args) throws Exception {
Path directory = Files.createTempDirectory("teaql-java-generated-exact-decimal-");
SQLiteDataSource sourceData = new SQLiteDataSource();
sourceData.setUrl("jdbc:sqlite:" + directory.resolve("source.sqlite"));
JdbcSqlExecutor sourceSql = new JdbcSqlExecutor(sourceData);
var source = new CustomUserContext(runtime(sourceData, sourceSql));
source.ensureSchema();
Platform platform = Q.platforms().withIdIs(1L)
.comment("Locate the exact Decimal platform")
.purpose("Attach ledger entries to their generated root")
.executeForOne(source);
if (platform == null) throw new AssertionError("seeded platform is missing");

save(source, platform, "first", new BigDecimal("0.1"));
save(source, platform, "second", new BigDecimal("0.2"));
var loaded = Q.decimalEntriesWithMinimalFields()
.selectLabel().selectAmount()
.withAmountIs(new BigDecimal("0.1"))
.orderByIdAscending().limit(10)
.comment("Read a generated exact Decimal row")
.purpose("Verify Q filtering and E hydration")
.executeForList(source);
if (loaded.size() != 1 || !"first".equals(E.decimalEntry(loaded.get(0)).getLabel().eval())
|| E.decimalEntry(loaded.get(0)).getAmount().eval().compareTo(new BigDecimal("0.1")) != 0) {
throw new AssertionError("generated Decimal Q/E round-trip failed");
}
if (sum(source).compareTo(new BigDecimal("0.3")) != 0) {
throw new AssertionError("generated Decimal SUM is not exact");
}
DecimalEntry preciseEntry = save(source, platform, "precise", new BigDecimal("3.50"));
BigDecimal scale = reloadAmount(source, preciseEntry.getId());
if (!new BigDecimal("3.50").equals(scale)) {
throw new AssertionError("generated Decimal scale was not preserved: " + scale);
}
BigDecimal preciseInput = new BigDecimal("123456789012.3456789");
preciseEntry.updateAmount(preciseInput);
preciseEntry.auditAs("Update exact Decimal precision for generated integration gate").save(source);
BigDecimal precision = reloadAmount(source, preciseEntry.getId());
if (!preciseInput.equals(precision)) {
throw new AssertionError("generated Decimal precision was not preserved: " + precision);
}

for (String exponent : new String[] {"1E-7", "1E+8"}) {
BigDecimal input = new BigDecimal(exponent);
DecimalEntry entry = save(source, platform, "exponent " + exponent, input);
BigDecimal reloaded = reloadAmount(source, entry.getId());
var filtered = Q.decimalEntriesWithMinimalFields().selectAmount()
.withIdIs(entry.getId()).withAmountIs(input).limit(1)
.comment("Find generated exact Decimal exponent value")
.purpose("Verify canonical SQLite binding in generated Q and mutation APIs")
.executeForOne(source);
if (input.compareTo(reloaded) != 0 || filtered == null
|| input.compareTo(E.decimalEntry(filtered).getAmount().eval()) != 0) {
throw new AssertionError("generated Decimal exponent round-trip failed: " + exponent);
}
}

System.out.println("GENERATED_EXACT_DECIMAL_PASS q=1 e=1 save=5 update=1 sum=0.3 scale=3.50 precision=19 exponent=2");
}

private static DecimalEntry save(CustomUserContext context, Platform platform,
String label, BigDecimal amount) {
DecimalEntry entry = Q.decimalEntries()
.comment("Initialize generated Decimal entry")
.purpose("Record an auditable ledger value")
.newEntity(context);
entry.updatePlatform(platform);
entry.updateLabel(label);
entry.updateAmount(amount);
entry.auditAs("Create exact Decimal entry for generated integration gate").save(context);
return entry;
}

private static BigDecimal sum(CustomUserContext context) {
var rows = Q.decimalEntriesWithMinimalFields().sumAmountAs("total")
.comment("Sum generated exact Decimal entries")
.purpose("Verify financial aggregation without binary floating point")
.aggregation(context).toList();
if (rows.size() != 1 || !(rows.get(0).get("total") instanceof BigDecimal total)) {
throw new AssertionError("generated Decimal aggregate has the wrong shape");
}
return total;
}

private static BigDecimal reloadAmount(CustomUserContext context, Long id) {
var entry = Q.decimalEntriesWithMinimalFields().selectAmount()
.withIdIs(id).limit(1)
.comment("Reload generated exact Decimal value")
.purpose("Verify scale and precision after audited save")
.executeForOne(context);
if (entry == null) throw new AssertionError("generated Decimal entry is missing: " + id);
return E.decimalEntry(entry).getAmount().eval();
}
}
18 changes: 18 additions & 0 deletions examples/exact-decimal/GeneratedWorkspaceStartupProbe.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.example.exactdecimalservice;

import io.teaql.provider.jdbc.JdbcSqlExecutor;
import org.springframework.boot.SpringApplication;

/** Closes the real generated Spring workspace after its ensure-schema runner. */
public final class GeneratedWorkspaceStartupProbe {
private GeneratedWorkspaceStartupProbe() {}

public static void main(String[] args) {
try (var context = SpringApplication.run(App.class, args)) {
if (context.getBeansOfType(JdbcSqlExecutor.class).size() != 1) {
throw new AssertionError("generated SQLite workspace must own one JDBC executor");
}
}
System.out.println("GENERATED_EXACT_DECIMAL_WORKSPACE_PASS");
}
}
29 changes: 29 additions & 0 deletions examples/exact-decimal/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Generated Java/SQLite exact Decimal example

This is a small, generation-dependent runtime example. It uses two entities,
one modeled `number()` field, a local generator checkout and this local Java
runtime checkout. No generated domain source is edited.

```bash
TEAQL_CODEGEN_DIR=/path/to/teaql-code-gen bash examples/exact-decimal/verify.sh
```

The verifier creates a fresh generated Java library and console workspace,
installs only the local runtime into Maven's local cache, then runs twice:

- Generated Q equality, strong `BigDecimal` hydration and E evaluation.
- Audited saves of `0.1` and `0.2`, followed by an exact modeled Q `SUM=0.3`.
- A `3.50` scale round-trip and audited update to
`123456789012.3456789` without losing numeric precision.
- Exponent-form `BigDecimal` inputs (`1E-7`, `1E+8`) through generated
audited mutation, typed Q filtering and E hydration.
- The actual generated Spring console startup, including repeated
`ensureSchema` against one workspace database.

It also verifies that the generated library hash does not change while the
application code is compiled and executed. Every run retains its logs under
the printed temporary evidence directory. This does not qualify migration of
pre-existing SQLite `NUMERIC` columns or participation in an application-owned
Spring `@Transactional` boundary; TeaQL's own graph save is transactional.
Committed mutation replication is tested separately on its P1 branch and is
not required for this isolated Decimal fix.
18 changes: 18 additions & 0 deletions examples/exact-decimal/model.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<root name="exact-decimal-service"
cfg_mask_china_mobile="false"
data_service="sqlite"
org="example"
_module_key="root">
<platform _name="Platform"
_module="Organization"
_module_key="organization"
name="Exact Decimal Platform"/>

<decimal_entry _name="Decimal Entry"
_module="Ledger"
_module_key="ledger"
platform="platform()"
label="string()"
amount="number()"/>
</root>
65 changes: 65 additions & 0 deletions examples/exact-decimal/verify.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
#!/usr/bin/env bash
set -euo pipefail

fixture_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
runtime_root="$(cd "$fixture_dir/../.." && pwd)"
generator_root="${TEAQL_CODEGEN_DIR:?Set TEAQL_CODEGEN_DIR to the local teaql-code-gen checkout}"
generator_root="$(cd "$generator_root" && pwd)"
run_dir="$(mktemp -d -t teaql-java-exact-decimal.XXXXXX)"
model_file="$fixture_dir/model.xml"

command -v mvn >/dev/null
command -v rg >/dev/null
mvn -q -f "$generator_root/pom.xml" -pl model -am -DskipTests install \
>"$run_dir/model-install.log" 2>&1
mvn -q -f "$generator_root/pom.xml" -pl generator -am -DskipTests compile \
>"$run_dir/generator-compile.log" 2>&1
mvn -q -f "$generator_root/pom.xml" -pl generator exec:java \
-Dexec.mainClass=com.skynet.codegenerator.LocalGenerator \
-Dexec.args="java-app-console $run_dir/java-app-console $model_file" \
>"$run_dir/generation.log" 2>&1
mvn -q -f "$runtime_root/pom.xml" -pl teaql-sqlite -am \
-DskipTests install >"$run_dir/runtime-install.log" 2>&1

app_dir="$run_dir/java-app-console"
app_src="$app_dir/src/main/java/com/example/exactdecimalservice"
cp "$fixture_dir/GeneratedExactDecimalProbe.java" "$app_src/GeneratedExactDecimalProbe.java"
cp "$fixture_dir/GeneratedWorkspaceStartupProbe.java" "$app_src/GeneratedWorkspaceStartupProbe.java"
library_hash_before="$(cd "$app_dir/lib" && rg --files src | LC_ALL=C sort \
| xargs sha256sum | sha256sum | cut -d' ' -f1)"
mvn -q -f "$app_dir/lib/pom.xml" -DskipTests install \
>"$run_dir/library-install.log" 2>&1

for round in 1 2; do
if ! mvn -q -f "$app_dir/pom.xml" -DskipTests compile exec:java \
-Dexec.mainClass=com.example.exactdecimalservice.GeneratedExactDecimalProbe \
>"$run_dir/round-$round.log" 2>&1; then
echo "Generated exact Decimal probe failed: $run_dir/round-$round.log" >&2
exit 1
fi
if ! rg -q '^GENERATED_EXACT_DECIMAL_PASS q=1 e=1 save=5 update=1 sum=0.3 scale=3.50 precision=19 exponent=2$' \
"$run_dir/round-$round.log"; then
echo "Generated exact Decimal probe lacks its complete marker: $run_dir/round-$round.log" >&2
exit 1
fi
if ! timeout 35s mvn -q -f "$app_dir/pom.xml" -DskipTests compile exec:java \
-Dexec.mainClass=com.example.exactdecimalservice.GeneratedWorkspaceStartupProbe \
-Dexec.args="--spring.datasource.url=jdbc:sqlite:$run_dir/workspace.sqlite" \
>"$run_dir/workspace-$round.log" 2>&1; then
echo "Generated SQLite workspace failed: $run_dir/workspace-$round.log" >&2
exit 1
fi
if ! rg -q '^GENERATED_EXACT_DECIMAL_WORKSPACE_PASS$' "$run_dir/workspace-$round.log"; then
echo "Generated SQLite workspace lacks its complete marker: $run_dir/workspace-$round.log" >&2
exit 1
fi
done
library_hash_after="$(cd "$app_dir/lib" && rg --files src | LC_ALL=C sort \
| xargs sha256sum | sha256sum | cut -d' ' -f1)"
if [[ "$library_hash_before" != "$library_hash_after" ]]; then
echo "Generated library changed during the application probe" >&2
exit 1
fi
printf 'GENERATED_EXACT_DECIMAL_GATE_PASS rounds=2 workspace=2 library_unchanged=1 generator=%s runtime=%s evidence=%s\n' \
"$(git -C "$generator_root" rev-parse --short HEAD)" \
"$(git -C "$runtime_root" rev-parse --short HEAD)" "$run_dir"
4 changes: 3 additions & 1 deletion examples/verify-runtime-examples.sh
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,9 @@ done
bash "$repo_dir/examples/trace-chain/verify.sh"
if [[ -n "${TEAQL_CODEGEN_DIR:-}" ]]; then
bash "$repo_dir/examples/shared-load-state/verify.sh" --generate
bash "$repo_dir/examples/exact-decimal/verify.sh"
printf 'PASS Java runtime examples: 5/5 (including generated exact Decimal)\n'
else
bash "$repo_dir/examples/shared-load-state/verify.sh"
printf 'PASS Java runtime examples: 4/4; generated exact Decimal requires TEAQL_CODEGEN_DIR\n'
fi
printf 'PASS Java runtime examples: 4/4\n'
4 changes: 2 additions & 2 deletions scripts/verify-examples.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ set -euo pipefail
repo="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
verification_dir="$(mktemp -d)"
trap 'rm -rf -- "$verification_dir"' EXIT
expected=(business-id-runtime conformance http-resilience object-storage order-management round-trip-reference-runtime school-management security-foundations shared-load-state standard-order trace-chain)
expected=(business-id-runtime conformance exact-decimal http-resilience object-storage order-management round-trip-reference-runtime school-management security-foundations shared-load-state standard-order trace-chain)
mapfile -t actual < <(find "$repo/examples" -mindepth 1 -maxdepth 1 -type d -printf '%f\n' | sort)
if [[ "${actual[*]}" != "${expected[*]}" ]]; then
echo "example inventory changed; update scripts/verify-examples.sh: ${actual[*]}" >&2
Expand Down Expand Up @@ -36,4 +36,4 @@ mvn -q -f examples/order-management/pom.xml install -DskipTests
bash examples/trace-chain/verify.sh
bash examples/trace-chain/verify-standard-telemetry.sh
bash examples/standard-order/verify.sh
echo "PASS: standard Java examples; shared-load-state has its own generator-backed verify.sh gate"
echo "PASS: standard Java examples; shared-load-state and exact-decimal have separate generator-backed gates"
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@

public class JdbcSqlExecutor implements SqlExecutionAdapter {

// Cap SQLite plain-text materialization before expanding exponent-form
// BigDecimal inputs. Model precision/scale checks remain separate.
private static final long MAX_SQLITE_DECIMAL_TEXT_LENGTH = 1_000_000L;

private final DataSource dataSource;
// A held view retains only this token, not the executor, connection or DataSource.
private final Object storageIdentity = new Object();
Expand Down Expand Up @@ -281,6 +285,9 @@ private static <V> V convertColumn(Class<V> type, Object value) {
return (V) java.time.LocalTime.parse(separator < 0 ? text : text.substring(separator + 1));
}
if (type == String.class) return (V) String.valueOf(value);
if (type == java.math.BigDecimal.class && value instanceof CharSequence text) {
return (V) new java.math.BigDecimal(text.toString());
}
throw new IllegalArgumentException("Cannot convert JDBC value " + value.getClass().getName()
+ " to " + type.getName());
}
Expand Down Expand Up @@ -421,7 +428,22 @@ static void bind(PreparedStatement statement, int index, Object value) throws SQ
} else if (value instanceof Byte number) {
statement.setByte(index, number);
} else if (value instanceof java.math.BigDecimal number) {
statement.setBigDecimal(index, number);
// SQLite's exact Decimal columns require canonical plain text. The
// driver binds exponent-form BigDecimal values (for example 1E-7)
// as exponent text, which the column CHECK correctly rejects.
// Keep native numeric binding for the other JDBC providers.
if (isSqlite(statement)) {
long precision = number.precision();
long scale = number.scale();
long plainLength = scale <= 0
? precision - scale
: Math.max(precision, scale + 1) + 1;
if (number.signum() < 0) plainLength++;
if (plainLength > MAX_SQLITE_DECIMAL_TEXT_LENGTH) {
throw new SQLException("SQLite Decimal plain-text representation exceeds safe length");
}
statement.setString(index, number.toPlainString());
} else statement.setBigDecimal(index, number);
} else if (value instanceof Double number) {
statement.setDouble(index, number);
} else if (value instanceof Float number) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import java.util.logging.Logger;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;

public class JdbcSqlExecutorTest {
Expand Down Expand Up @@ -162,6 +163,22 @@ public void testSqliteLocalDateParametersUseIsoTextOrdering() throws Exception {
}
}

@Test
public void sqliteDecimalBindingRejectsExponentExpansionBeforeMaterialization() throws Exception {
try (Connection connection = DriverManager.getConnection("jdbc:sqlite::memory:");
java.sql.PreparedStatement query = connection.prepareStatement("SELECT ?")) {
assertThrows(SQLException.class, () -> JdbcSqlExecutor.bind(
query, 1, new java.math.BigDecimal("1E-1000000000")));
assertThrows(SQLException.class, () -> JdbcSqlExecutor.bind(
query, 1, new java.math.BigDecimal("1E+1000000000")));
JdbcSqlExecutor.bind(query, 1, new java.math.BigDecimal("1E-7"));
try (ResultSet result = query.executeQuery()) {
assertTrue(result.next());
assertEquals("0.0000001", result.getString(1));
}
}
}

@Test
public void testBatchUpdate() throws Exception {
List<Object[]> batch = new ArrayList<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,20 @@
import java.util.Map;

import io.teaql.core.SearchRequest;
import io.teaql.core.AggrFunction;
import io.teaql.core.Expression;
import io.teaql.core.UserContext;
import io.teaql.core.utils.CollUtil;
import io.teaql.core.sql.expression.SQLExpressionParser;

public interface SQLColumnResolver {

/** Optional provider override for typed aggregation; null uses portable SQL. */
default String aggregateSql(UserContext context, AggrFunction function,
Expression operand, String columnSql) {
return null;
}

default io.teaql.core.SqlParameterLogPolicy parameterLogPolicy(String property) {
return io.teaql.core.SqlParameterLogPolicy.UNKNOWN;
}
Expand Down
Loading
Loading