diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 00000000..d4a292f1
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1 @@
+test-vectors/*.tsv whitespace=-blank-at-eol
diff --git a/examples/security-foundations/pom.xml b/examples/security-foundations/pom.xml
index da1cfae2..84045a01 100644
--- a/examples/security-foundations/pom.xml
+++ b/examples/security-foundations/pom.xml
@@ -12,6 +12,11 @@
teaql-security-foundations-example
TeaQL security foundations focused example
+
+ io.teaql
+ teaql-sqlite
+ ${project.version}
+
io.teaql
teaql-runtime
diff --git a/examples/security-foundations/src/test/java/io/teaql/examples/security/SecurityFoundationsExampleTest.java b/examples/security-foundations/src/test/java/io/teaql/examples/security/SecurityFoundationsExampleTest.java
index 49e5628a..a4c465d2 100644
--- a/examples/security-foundations/src/test/java/io/teaql/examples/security/SecurityFoundationsExampleTest.java
+++ b/examples/security-foundations/src/test/java/io/teaql/examples/security/SecurityFoundationsExampleTest.java
@@ -20,6 +20,10 @@
import org.junit.Test;
public class SecurityFoundationsExampleTest {
+ @Test
+ public void sqliteMaskingLifecycle() throws Exception {
+ SqlMaskingLifecycleExample.verify();
+ }
private static final class OrderItem extends BaseEntity {
OrderItem(long id, long version) {
__internalSet("id", id);
diff --git a/examples/security-foundations/src/test/java/io/teaql/examples/security/SqlMaskingLifecycleExample.java b/examples/security-foundations/src/test/java/io/teaql/examples/security/SqlMaskingLifecycleExample.java
new file mode 100644
index 00000000..ef7c3004
--- /dev/null
+++ b/examples/security-foundations/src/test/java/io/teaql/examples/security/SqlMaskingLifecycleExample.java
@@ -0,0 +1,149 @@
+package io.teaql.examples.security;
+
+import io.teaql.core.*;
+import io.teaql.core.criteria.Operator;
+import io.teaql.core.meta.SimpleEntityMetaFactory;
+import io.teaql.core.sql.SQLEntityDescriptor;
+import io.teaql.core.sqlite.SqliteDataServiceExecutor;
+import io.teaql.provider.jdbc.JdbcSqlExecutor;
+import io.teaql.runtime.*;
+import java.nio.file.Files;
+import java.util.*;
+import java.util.concurrent.atomic.AtomicLong;
+import org.sqlite.SQLiteDataSource;
+import static org.junit.Assert.*;
+
+/** Runtime-owned fixture, not a hand-edited generated library. */
+final class SqlMaskingLifecycleExample {
+ static void verify() throws Exception {
+ var metadata = new SimpleEntityMetaFactory();
+ var descriptor = new SQLEntityDescriptor();
+ descriptor.setType("MaskFixture"); descriptor.setTargetType(MaskFixture.class);
+ descriptor.setEntitySupplier(MaskFixture::new); descriptor.setDataService("sqlite");
+ descriptor.addSimpleProperty("id", Long.class);
+ descriptor.addSimpleProperty("version", Long.class);
+ descriptor.addSimpleProperty("name", String.class);
+ descriptor.setAuditMaskFields(List.of("name"));
+ descriptor.with("table_name", "mask_fixture_data"); metadata.register(descriptor);
+ var ds = new SQLiteDataSource();
+ ds.setUrl("jdbc:sqlite:" + Files.createTempFile("teaql-java-mask-example-", ".db"));
+ var provider = new SqliteDataServiceExecutor("sqlite", new JdbcSqlExecutor(ds), ds);
+ var logs = new ArrayList();
+ var sequence = new AtomicLong(10);
+ var context = new DefaultUserContext(TeaQLRuntime.builder().metadata(metadata)
+ .dataService("sqlite", provider).idGenerationService((caller, entity) -> sequence.getAndIncrement())
+ .logSink((caller, entry) -> logs.add(entry)).build());
+
+ assertThrows(RuntimeException.class, () -> query().executeForStream(context));
+ assertEquals(1, logs.size());
+ assertEquals("failure", logs.get(0).getExecutionOutcome());
+ assertNull(logs.get(0).getResultCount());
+ context.ensureSchema();
+ for (int i = 0; i < 3; i++) {
+ logs.clear();
+ new MaskFixture().updateName("Riverside").auditAs("seed masking example Riverside").save(context);
+ var readback = logs.stream().filter(e -> e.getOperation() == DataServiceOperation.QUERY
+ && String.valueOf(e.getParameterizedQuery()).contains("SELECT *")).findFirst().orElseThrow();
+ assertEquals("seed masking example [REDACTED]", readback.getAuditReason());
+ assertFalse(readback.getDebugQuery().contains("Riverside"));
+ assertEquals(Integer.valueOf(1), readback.getResultCount());
+ }
+ MaskFixture changed;
+ try (var rows = query().executeForStream(context)) { changed = rows.findFirst().orElseThrow(); }
+ for (String name : List.of("Lakeside", "Riverside")) {
+ logs.clear();
+ changed.updateName(name).auditAs("update masked fixture " + name).save(context);
+ var readback = logs.stream().filter(e -> e.getOperation() == DataServiceOperation.QUERY
+ && String.valueOf(e.getParameterizedQuery()).contains("SELECT *")).findFirst().orElseThrow();
+ assertEquals("update masked fixture [REDACTED]", readback.getAuditReason());
+ assertNull(readback.getIntentRedactions());
+ }
+ // Test-only fault injection, not application persistence SQL.
+ try (var conn = ds.getConnection(); var statement = conn.createStatement()) {
+ statement.execute("CREATE TRIGGER disappear_mask_fixture AFTER INSERT ON mask_fixture_data WHEN NEW.id >= 13 BEGIN DELETE FROM mask_fixture_data WHERE id = NEW.id; END");
+ }
+ try {
+ logs.clear();
+ assertThrows(RuntimeException.class, () -> new MaskFixture().updateName("READBACK-CANARY")
+ .auditAs("verify missing READBACK-CANARY snapshot").save(context));
+ var readback = logs.stream().filter(e -> e.getOperation() == DataServiceOperation.QUERY
+ && String.valueOf(e.getParameterizedQuery()).contains("SELECT *")).findFirst().orElseThrow();
+ assertEquals("success", readback.getExecutionOutcome());
+ assertEquals(Integer.valueOf(0), readback.getResultCount());
+ assertEquals("verify missing [REDACTED] snapshot", readback.getAuditReason());
+ assertTrue(logs.stream().anyMatch(e -> e.getOperation() == DataServiceOperation.MUTATION
+ && "success".equals(e.getExecutionOutcome()) && Long.valueOf(1).equals(e.getAffectedRows())));
+ } finally {
+ try (var conn = ds.getConnection(); var statement = conn.createStatement()) {
+ statement.execute("DROP TRIGGER disappear_mask_fixture");
+ }
+ }
+ System.out.println("PASS Java readback inherits sensitive intent for create/update and empty SQLite snapshot");
+ logs.clear();
+ var duplicateContext = new DefaultUserContext(TeaQLRuntime.builder().metadata(metadata)
+ .dataService("sqlite", provider).idGenerationService((caller, entity) -> 10L)
+ .logSink((caller, entry) -> logs.add(entry)).build());
+ assertThrows(RuntimeException.class, () -> new MaskFixture().updateName("MASKED-DUPLICATE-CANARY")
+ .auditAs("verify failed duplicate insert").save(duplicateContext));
+ assertTrue(logs.stream().anyMatch(entry -> entry.getOperation() == DataServiceOperation.MUTATION));
+ assertTrue(logs.stream().noneMatch(entry -> String.valueOf(entry.getDebugQuery()).contains("MASKED-DUPLICATE-CANARY")));
+ logs.clear();
+ try (var rows = query().executeForStream(context)) {
+ assertEquals(1, rows.limit(1).count());
+ }
+ assertEquals(1, logs.size());
+ assertEquals("cancelled", logs.get(0).getExecutionOutcome());
+ assertEquals(Integer.valueOf(1), logs.get(0).getResultCount());
+ assertEquals("verify cursor diagnostics", logs.get(0).getPurpose());
+ assertTrue(logs.get(0).getDebugQuery().contains("Ri*****de"));
+ assertFalse(logs.get(0).getDebugQuery().contains("Riverside"));
+ logs.clear();
+ try (var rows = query().executeForStream(context)) {
+ assertEquals(List.of("Riverside", "Riverside", "Riverside"), rows.map(MaskFixture::getName).toList());
+ }
+ assertEquals(1, logs.size());
+ assertEquals("success", logs.get(0).getExecutionOutcome());
+ assertEquals(Integer.valueOf(3), logs.get(0).getResultCount());
+ // Re-emitting a stored safe record must never upgrade it to debug plaintext.
+ var retained = logs.get(0);
+ var replay = LogPrivacy.sql(retained, true);
+ assertEquals(retained.getDebugQuery(), replay.getDebugQuery());
+ var output = Files.createTempFile("teaql-java-masked-replay-", ".log");
+ try (var out = new java.io.PrintStream(Files.newOutputStream(output))) {
+ new DefaultTextRuntimeLogSink(out).writeExecutionLog(context, replay);
+ }
+ String text = Files.readString(output);
+ assertTrue(text.contains("Ri*****de"));
+ assertFalse(text.contains("Riverside"));
+ assertFalse(text.contains("DEBUG PLAINTEXT"));
+ assertTrue(text.contains("verify cursor diagnostics"));
+ System.out.println("PASS Java retained SQL projection: no plaintext upgrade, masked file output, intent preserved");
+ System.out.println("PASS Java SQLite masking lifecycle: failed open/duplicate insert, early close, completion, intent and original data");
+ }
+
+ private static ExecutableRequest query() {
+ var request = new FixtureRequest();
+ request.setSize(3);
+ request.appendSearchCriteria(request.createBasicSearchCriteria("name", Operator.EQUAL, "Riverside"));
+ return request.comment("read masked fixture").purpose("verify cursor diagnostics");
+ }
+
+ public static final class FixtureRequest extends BaseRequest {
+ FixtureRequest() { super(MaskFixture.class); }
+ @Override public String getTypeName() { return "MaskFixture"; }
+ public FixtureRequest comment(String text) { internalComment(text); return this; }
+ }
+
+ public static final class MaskFixture extends BaseEntity {
+ private String name;
+ @Override public String typeName() { return "MaskFixture"; }
+ public String getName() { return name; }
+ public MaskFixture updateName(String value) { handleUpdate("name", name, value); name = value; return this; }
+ @Override public void __internalSet(String property, Object value) {
+ if (property.equals("name")) name = (String) value; else super.__internalSet(property, value);
+ }
+ @Override public Object __internalGet(String property) {
+ return property.equals("name") ? name : super.__internalGet(property);
+ }
+ }
+}
diff --git a/examples/security-foundations/src/test/java/io/teaql/examples/security/SqlRelationMaskingTest.java b/examples/security-foundations/src/test/java/io/teaql/examples/security/SqlRelationMaskingTest.java
new file mode 100644
index 00000000..d240fdde
--- /dev/null
+++ b/examples/security-foundations/src/test/java/io/teaql/examples/security/SqlRelationMaskingTest.java
@@ -0,0 +1,194 @@
+package io.teaql.examples.security;
+
+import io.teaql.core.*;
+import io.teaql.core.criteria.Operator;
+import io.teaql.core.meta.SimpleEntityMetaFactory;
+import io.teaql.core.sql.*;
+import io.teaql.core.sqlite.SqliteDataServiceExecutor;
+import io.teaql.provider.jdbc.JdbcSqlExecutor;
+import io.teaql.runtime.*;
+import java.nio.file.Files;
+import java.util.*;
+import java.util.concurrent.atomic.AtomicLong;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.sqlite.SQLiteDataSource;
+import static org.junit.Assert.*;
+
+/** Real SQLite graph loading; handwritten runtime fixture, not generated source. */
+@RunWith(Parameterized.class)
+public class SqlRelationMaskingTest {
+ @Parameterized.Parameters(name = "{0}-failure={1}-transaction={2}-sensitiveSink={3}")
+ public static Collection
+
+ io.teaql
+ teaql-runtime
+ test
+
junit
junit
test
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+ --add-reads io.teaql.hana=io.teaql.runtime
+
+
+
+
diff --git a/teaql-hana/src/test/java/io/teaql/core/hana/HanaDialectTest.java b/teaql-hana/src/test/java/io/teaql/core/hana/HanaDialectTest.java
index 45ecd243..e1a8228b 100644
--- a/teaql-hana/src/test/java/io/teaql/core/hana/HanaDialectTest.java
+++ b/teaql-hana/src/test/java/io/teaql/core/hana/HanaDialectTest.java
@@ -1,10 +1,15 @@
package io.teaql.core.hana;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import io.teaql.core.BaseEntity;
import io.teaql.core.BaseRequest;
+import io.teaql.core.ExecutionMetadata;
+import io.teaql.core.SqlParameterLogPolicy;
+import io.teaql.runtime.LogPrivacy;
+import java.util.List;
import org.junit.Test;
public class HanaDialectTest {
@@ -20,4 +25,49 @@ public void usesHanaPaginationWindowAndLargeText() {
assertTrue(dialect.getPartitionSQL().contains("row_number() over(partition by"));
assertEquals("NCLOB", dialect.mapColumnType("LARGE_TEXT"));
}
+
+ @Test
+ public void hanaSqlDiagnosticsUseSafeProjectionWithoutChangingExecutionParameters() {
+ ExecutionMetadata source = new ExecutionMetadata();
+ source.setBackend("hana");
+ source.setGeneratedSql(true);
+ source.setParameterizedQuery(
+ "SELECT \"NAME?\" FROM CUSTOMER WHERE NAME = ? AND ADDRESS = ? AND PASSWORD = ?");
+ source.setParameters(List.of("Riverside", "1 Runtime Road", "PASSWORD-CANARY"));
+ source.setParameterLogPolicies(List.of(
+ SqlParameterLogPolicy.MASKED,
+ SqlParameterLogPolicy.PLAIN,
+ SqlParameterLogPolicy.CREDENTIAL));
+
+ ExecutionMetadata projected = LogPrivacy.sql(source, false);
+ String diagnostic = projected.getDebugQuery();
+ assertTrue(diagnostic, diagnostic.contains("Ri*****de"));
+ assertTrue(diagnostic, diagnostic.contains("1 Runtime Road"));
+ assertTrue(diagnostic, diagnostic.contains("/* masked */"));
+ assertTrue(diagnostic, diagnostic.contains("NOT REPLAYABLE"));
+ assertTrue(diagnostic, diagnostic.contains("\"NAME?\""));
+ assertTrue(diagnostic, diagnostic.contains("[REDACTED]"));
+ assertFalse(diagnostic, diagnostic.contains("Riverside"));
+ assertFalse(diagnostic, diagnostic.contains("PASSWORD-CANARY"));
+ assertEquals(List.of("Riverside", "1 Runtime Road", "PASSWORD-CANARY"), source.getParameters());
+
+ String explicitDebug = LogPrivacy.sql(source, true).getDebugQuery();
+ assertTrue(explicitDebug, explicitDebug.contains("Riverside"));
+ assertTrue(explicitDebug, explicitDebug.contains("1 Runtime Road"));
+ assertFalse(explicitDebug, explicitDebug.contains("PASSWORD-CANARY"));
+ }
+
+ @Test
+ public void oldHanaDescriptorsWithoutFieldPolicyFailClosed() {
+ ExecutionMetadata source = new ExecutionMetadata();
+ source.setBackend("hana");
+ source.setGeneratedSql(true);
+ source.setParameterizedQuery("SELECT id FROM CUSTOMER WHERE NAME = ?");
+ source.setParameters(List.of("LEGACY-HANA-CANARY"));
+
+ ExecutionMetadata projected = LogPrivacy.sql(source, false);
+ assertTrue(projected.getDebugQuery(), projected.getDebugQuery().contains("[REDACTED]"));
+ assertFalse(projected.getDebugQuery(), projected.getDebugQuery().contains("LEGACY-HANA-CANARY"));
+ assertEquals(List.of("LEGACY-HANA-CANARY"), source.getParameters());
+ }
}
diff --git a/teaql-mysql/src/test/java/io/teaql/mysql/MysqlIntegrationTest.java b/teaql-mysql/src/test/java/io/teaql/mysql/MysqlIntegrationTest.java
index a404f13d..bc7d899f 100644
--- a/teaql-mysql/src/test/java/io/teaql/mysql/MysqlIntegrationTest.java
+++ b/teaql-mysql/src/test/java/io/teaql/mysql/MysqlIntegrationTest.java
@@ -18,6 +18,9 @@
import javax.sql.DataSource;
import java.io.PrintWriter;
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import java.nio.charset.StandardCharsets;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
@@ -392,6 +395,53 @@ private static SQLEntityDescriptor relationEntity(
return descriptor;
}
+ @Test
+ public void testLiveSqlMaskingThroughQueryAndMutation() throws Exception {
+ SimpleEntityMetaFactory metadata = new SimpleEntityMetaFactory();
+ SQLEntityDescriptor descriptor = new SQLEntityDescriptor();
+ descriptor.setType("Task");
+ descriptor.setTargetType(Task.class);
+ descriptor.setEntitySupplier(Task::new);
+ descriptor.setDataService("mysql");
+ descriptor.setAuditMaskFields(List.of("title"));
+ ((io.teaql.core.sql.GenericSQLProperty) descriptor.addSimpleProperty("id", Long.class)).setColumnType("BIGINT");
+ ((io.teaql.core.sql.GenericSQLProperty) descriptor.addSimpleProperty("version", Long.class)).setColumnType("BIGINT");
+ ((io.teaql.core.sql.GenericSQLProperty) descriptor.addSimpleProperty("title", String.class)).setColumnType("VARCHAR(200)");
+ ((io.teaql.core.sql.GenericSQLProperty) descriptor.addSimpleProperty("status", String.class)).setColumnType("VARCHAR(50)");
+ metadata.register(descriptor);
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+ try (PrintStream output = new PrintStream(bytes, true, StandardCharsets.UTF_8)) {
+ TeaQLRuntime maskingRuntime = TeaQLRuntime.builder()
+ .metadata(metadata)
+ .dataService("mysql", new MysqlDataServiceExecutor("mysql", new JdbcSqlExecutor(dataSource), dataSource))
+ .idGenerationService(new IdSpaceIdGenerator(new JdbcTeaQLDatabase(dataSource)))
+ .logSink(new io.teaql.runtime.DefaultTextRuntimeLogSink(output))
+ .build();
+ UserContext maskingContext = new DefaultUserContext(maskingRuntime);
+ maskingContext.ensureSchema();
+ String status = "1 Runtime Road " + UUID.randomUUID().toString().substring(0, 8);
+ Task task = new Task();
+ task.updateTitle("Riverside").updateStatus(status);
+ task.auditAs("what: create masked task").save(maskingContext);
+ SmartList rows = new TaskRequest().filterByTitle("Riverside")
+ .filterByStatus(status)
+ .comment("what: read masked task")
+ .purpose("why: verify live-provider SQL masking")
+ .executeForList(maskingContext);
+ assertEquals(1, rows.size());
+ assertEquals("Riverside", rows.get(0).getTitle());
+ assertEquals(status, rows.get(0).getStatus());
+ }
+ String logged = bytes.toString(StandardCharsets.UTF_8);
+ assertTrue(logged, logged.contains("INSERT"));
+ assertTrue(logged, logged.contains("SELECT"));
+ assertTrue(logged, logged.contains("Ri*****de"));
+ assertTrue(logged, logged.contains("1 Runtime Road"));
+ assertTrue(logged, logged.contains("what: read masked task"));
+ assertTrue(logged, logged.contains("why: verify live-provider SQL masking"));
+ assertFalse(logged, logged.contains("Riverside"));
+ }
+
@Test
public void testMysqlCrud() {
// 1. Create and Save Tasks
diff --git a/teaql-mysql/src/test/java/io/teaql/mysql/MysqlTemporalSqlTest.java b/teaql-mysql/src/test/java/io/teaql/mysql/MysqlTemporalSqlTest.java
index 8db49d01..a9c0169b 100644
--- a/teaql-mysql/src/test/java/io/teaql/mysql/MysqlTemporalSqlTest.java
+++ b/teaql-mysql/src/test/java/io/teaql/mysql/MysqlTemporalSqlTest.java
@@ -12,8 +12,13 @@ public class MysqlTemporalSqlTest {
@Test
public void preparedAndDiagnosticSqlAreEquivalent() throws Exception {
String url = System.getenv("TEAQL_TEST_MYSQL_URL");
- Assume.assumeTrue(url != null && !url.isBlank());
- try (Connection connection = DriverManager.getConnection(url)) {
+ String user = System.getenv("TEAQL_TEST_MYSQL_USER");
+ String password = System.getenv("TEAQL_TEST_MYSQL_PASSWORD");
+ boolean configured = url != null && !url.isBlank() && user != null && password != null;
+ if (!configured && Boolean.parseBoolean(System.getenv("TEAQL_REQUIRE_LIVE_DB")))
+ fail("MySQL temporal live gate requires URL, USER, and PASSWORD environment variables");
+ Assume.assumeTrue(configured);
+ try (Connection connection = DriverManager.getConnection(url, user, password)) {
run(connection, "DROP TABLE IF EXISTS teaql_temporal_java_fixture");
run(connection, "CREATE TABLE teaql_temporal_java_fixture(id INTEGER, d DATE, local_time DATETIME(3))");
String sql = "INSERT INTO teaql_temporal_java_fixture VALUES (?, ?, ?) /* ignored ? */";
diff --git a/teaql-opentelemetry/src/test/java/io/teaql/opentelemetry/OpenTelemetryRuntimeTelemetryTest.java b/teaql-opentelemetry/src/test/java/io/teaql/opentelemetry/OpenTelemetryRuntimeTelemetryTest.java
index 76006313..1814a92e 100644
--- a/teaql-opentelemetry/src/test/java/io/teaql/opentelemetry/OpenTelemetryRuntimeTelemetryTest.java
+++ b/teaql-opentelemetry/src/test/java/io/teaql/opentelemetry/OpenTelemetryRuntimeTelemetryTest.java
@@ -101,6 +101,46 @@ public void exportsSafeSpanAndMetricsThroughOfficialSdk() {
loggerProvider.close();
}
+ @Test
+ public void failureTelemetryDoesNotExportDriverErrorMessages() {
+ InMemorySpanExporter spans = InMemorySpanExporter.create();
+ SdkTracerProvider tracerProvider = SdkTracerProvider.builder()
+ .addSpanProcessor(SimpleSpanProcessor.create(spans)).build();
+ SdkMeterProvider meterProvider = SdkMeterProvider.builder().build();
+ InMemoryLogRecordExporter logs = InMemoryLogRecordExporter.create();
+ SdkLoggerProvider loggerProvider = SdkLoggerProvider.builder()
+ .addLogRecordProcessor(SimpleLogRecordProcessor.create(logs)).build();
+ OpenTelemetryRuntimeTelemetry telemetry = new OpenTelemetryRuntimeTelemetry(
+ tracerProvider.get("io.teaql.runtime"),
+ meterProvider.get("io.teaql.runtime"),
+ loggerProvider.get("io.teaql.runtime"));
+
+ RuntimeTelemetry.Scope scope = RuntimeTelemetry.startSafely(telemetry,
+ new RuntimeTelemetry.Operation("provider", "sqlite.query", Map.of()));
+ scope.failure(new IllegalStateException(
+ "SQL failed for password=OTEL-FAILURE-CANARY"));
+
+ assertEquals(1, spans.getFinishedSpanItems().size());
+ assertEquals(1, logs.getFinishedLogRecordItems().size());
+ var span = spans.getFinishedSpanItems().get(0);
+ var log = logs.getFinishedLogRecordItems().get(0);
+ assertEquals("IllegalStateException", span.getAttributes().get(
+ AttributeKey.stringKey("teaql.error.type")));
+ assertEquals("internal", span.getAttributes().get(
+ AttributeKey.stringKey("teaql.error.category")));
+ assertEquals("failure", log.getAttributes().get(
+ AttributeKey.stringKey("teaql.operation.outcome")));
+ assertFalse(span.getAttributes().toString().contains("OTEL-FAILURE-CANARY"));
+ assertFalse(span.getStatus().toString().contains("OTEL-FAILURE-CANARY"));
+ assertFalse(span.getEvents().toString().contains("OTEL-FAILURE-CANARY"));
+ assertFalse(log.getAttributes().toString().contains("OTEL-FAILURE-CANARY"));
+ assertFalse(log.getBodyValue().asString().contains("OTEL-FAILURE-CANARY"));
+
+ tracerProvider.close();
+ meterProvider.close();
+ loggerProvider.close();
+ }
+
@Test
public void extractsCaseInsensitiveW3cCarrierAsServerParent() {
diff --git a/teaql-postgres/src/test/java/io/teaql/postgres/PostgresIntegrationTest.java b/teaql-postgres/src/test/java/io/teaql/postgres/PostgresIntegrationTest.java
index bab08058..db82ad80 100644
--- a/teaql-postgres/src/test/java/io/teaql/postgres/PostgresIntegrationTest.java
+++ b/teaql-postgres/src/test/java/io/teaql/postgres/PostgresIntegrationTest.java
@@ -18,6 +18,9 @@
import javax.sql.DataSource;
import java.io.PrintWriter;
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import java.nio.charset.StandardCharsets;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
@@ -25,6 +28,7 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Collections;
+import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@@ -350,6 +354,53 @@ private static SQLEntityDescriptor relationEntity(
return descriptor;
}
+ @Test
+ public void testLiveSqlMaskingThroughQueryAndMutation() throws Exception {
+ SimpleEntityMetaFactory metadata = new SimpleEntityMetaFactory();
+ SQLEntityDescriptor descriptor = new SQLEntityDescriptor();
+ descriptor.setType("Task");
+ descriptor.setTargetType(Task.class);
+ descriptor.setEntitySupplier(Task::new);
+ descriptor.setDataService("postgres");
+ descriptor.setAuditMaskFields(List.of("title"));
+ ((io.teaql.core.sql.GenericSQLProperty) descriptor.addSimpleProperty("id", Long.class)).setColumnType("BIGINT");
+ ((io.teaql.core.sql.GenericSQLProperty) descriptor.addSimpleProperty("version", Long.class)).setColumnType("BIGINT");
+ ((io.teaql.core.sql.GenericSQLProperty) descriptor.addSimpleProperty("title", String.class)).setColumnType("VARCHAR(200)");
+ ((io.teaql.core.sql.GenericSQLProperty) descriptor.addSimpleProperty("status", String.class)).setColumnType("VARCHAR(50)");
+ metadata.register(descriptor);
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+ try (PrintStream output = new PrintStream(bytes, true, StandardCharsets.UTF_8)) {
+ TeaQLRuntime maskingRuntime = TeaQLRuntime.builder()
+ .metadata(metadata)
+ .dataService("postgres", new PostgresDataServiceExecutor("postgres", new JdbcSqlExecutor(dataSource)))
+ .idGenerationService(new IdSpaceIdGenerator(new JdbcTeaQLDatabase(dataSource)))
+ .logSink(new io.teaql.runtime.DefaultTextRuntimeLogSink(output))
+ .build();
+ UserContext maskingContext = new DefaultUserContext(maskingRuntime);
+ maskingContext.ensureSchema();
+ String status = "1 Runtime Road " + UUID.randomUUID().toString().substring(0, 8);
+ Task task = new Task();
+ task.updateTitle("Riverside").updateStatus(status);
+ task.auditAs("what: create masked task").save(maskingContext);
+ SmartList rows = new TaskRequest().filterByTitle("Riverside")
+ .filterByStatus(status)
+ .comment("what: read masked task")
+ .purpose("why: verify live-provider SQL masking")
+ .executeForList(maskingContext);
+ assertEquals(1, rows.size());
+ assertEquals("Riverside", rows.get(0).getTitle());
+ assertEquals(status, rows.get(0).getStatus());
+ }
+ String logged = bytes.toString(StandardCharsets.UTF_8);
+ assertTrue(logged, logged.contains("INSERT"));
+ assertTrue(logged, logged.contains("SELECT"));
+ assertTrue(logged, logged.contains("Ri*****de"));
+ assertTrue(logged, logged.contains("1 Runtime Road"));
+ assertTrue(logged, logged.contains("what: read masked task"));
+ assertTrue(logged, logged.contains("why: verify live-provider SQL masking"));
+ assertFalse(logged, logged.contains("Riverside"));
+ }
+
@Test
public void testPostgresCrud() {
// 1. Create and Save Tasks
diff --git a/teaql-postgres/src/test/java/io/teaql/postgres/PostgresTemporalSqlTest.java b/teaql-postgres/src/test/java/io/teaql/postgres/PostgresTemporalSqlTest.java
index 448be6dc..bad5cf69 100644
--- a/teaql-postgres/src/test/java/io/teaql/postgres/PostgresTemporalSqlTest.java
+++ b/teaql-postgres/src/test/java/io/teaql/postgres/PostgresTemporalSqlTest.java
@@ -12,8 +12,13 @@ public class PostgresTemporalSqlTest {
@Test
public void preparedAndDiagnosticSqlAreEquivalent() throws Exception {
String url = System.getenv("TEAQL_TEST_POSTGRES_URL");
- Assume.assumeTrue(url != null && !url.isBlank());
- try (Connection connection = DriverManager.getConnection(url)) {
+ String user = System.getenv("TEAQL_TEST_POSTGRES_USER");
+ String password = System.getenv("TEAQL_TEST_POSTGRES_PASSWORD");
+ boolean configured = url != null && !url.isBlank() && user != null && password != null;
+ if (!configured && Boolean.parseBoolean(System.getenv("TEAQL_REQUIRE_LIVE_DB")))
+ fail("PostgreSQL temporal live gate requires URL, USER, and PASSWORD environment variables");
+ Assume.assumeTrue(configured);
+ try (Connection connection = DriverManager.getConnection(url, user, password)) {
run(connection, "DROP TABLE IF EXISTS teaql_temporal_java_fixture");
run(connection, "CREATE TABLE teaql_temporal_java_fixture(id INTEGER, d DATE, local_time TIMESTAMP(3))");
String sql = "INSERT INTO teaql_temporal_java_fixture VALUES (?, ?, ?) /* ignored ? */";
diff --git a/teaql-provider-jdbc/src/main/java/io/teaql/provider/jdbc/JdbcSqlExecutor.java b/teaql-provider-jdbc/src/main/java/io/teaql/provider/jdbc/JdbcSqlExecutor.java
index 7000a96f..97b3ac3b 100644
--- a/teaql-provider-jdbc/src/main/java/io/teaql/provider/jdbc/JdbcSqlExecutor.java
+++ b/teaql-provider-jdbc/src/main/java/io/teaql/provider/jdbc/JdbcSqlExecutor.java
@@ -54,16 +54,17 @@ public Stream queryForStream(String sql, Map params, SqlR
@Override
public Stream