Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
49390a6
feat: Grant driver RBAC permissions to manage executor scratch PVCs
johngrimes Jun 26, 2026
d794187
feat: Add shared SQL on FHIR Library parser and dependency graph model
johngrimes Jun 26, 2026
acd5238
feat: Resolve and execute SQLView dependency graphs
johngrimes Jun 26, 2026
822cf23
test: Cover nested SQLView graphs, diamonds, cycles, and depth
johngrimes Jun 26, 2026
ee3a099
feat: Run and export a SQLView as a top-level resource
johngrimes Jun 26, 2026
80e8207
feat: Require metadata READ to resolve stored views and queries
johngrimes Jun 26, 2026
d58f75f
docs: Document SQLView composition, depth limit, and authorisation
johngrimes Jun 26, 2026
ce0a55d
refactor: Prefer imports over inline fully-qualified names
johngrimes Jun 27, 2026
743a034
test: Add SQLView Library fixtures for the SQL query UI
johngrimes Jun 27, 2026
48e23c2
feat: List stored SQLViews through a shared Library list core
johngrimes Jun 27, 2026
606036d
feat: Surface stored SQLViews in the Select query picker
johngrimes Jun 27, 2026
c7aa599
feat: Hide runtime parameter values when no parameters apply
johngrimes Jun 27, 2026
4c900d1
feat: Reference a SQLView when authoring SQL inline
johngrimes Jun 27, 2026
62fbb54
test: Update SQL query export e2e for the renamed source picker
johngrimes Jun 27, 2026
8f1a246
test: Point inline e2e selectors at the renamed Views editor
johngrimes Jun 27, 2026
94d7531
docs: Note SQLView listing in the SQL query client header
johngrimes Jun 27, 2026
c02ce06
feat: Retain ViewDefinition url and version through encoding
johngrimes Jun 27, 2026
e68d3c0
feat: Resolve SQL on FHIR dependencies by canonical URL
johngrimes Jun 27, 2026
462ff56
feat: Author SQL on FHIR view references by canonical URL in the admi…
johngrimes Jun 27, 2026
58165af
docs: Describe canonical-URL resolution for SQL on FHIR dependencies
johngrimes Jun 27, 2026
52ad991
fix: Use a spaced hyphen in the URL-less source explanation
johngrimes Jun 27, 2026
2b06300
fix: Open sun.util.calendar for local server runs
johngrimes Jun 27, 2026
2d807f2
fix: Show submitted SQL for stored SQL on FHIR queries
johngrimes Jun 28, 2026
e26fb95
fix: Bound the height of the submitted SQL in query results
johngrimes Jun 28, 2026
d4f7704
fix: Truncate long view references in the SQL query summary
johngrimes Jun 28, 2026
776bb51
fix: Validate relation references inside CTE definition bodies
johngrimes Jun 28, 2026
9d4e083
fix: Allow string concatenation in $sqlquery-run
johngrimes Jun 28, 2026
9c1d596
test: Cover ViewDefinition url and version accessors
johngrimes Jun 28, 2026
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
5 changes: 5 additions & 0 deletions deployment/helm/pathling/templates/role.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ rules:
- apiGroups: [""]
resources: ["pods", "services", "configmaps"]
verbs: ["create", "get", "list", "watch", "delete"]
# Allow the driver to provision and remove the dynamically created scratch
# volumes used for executor local storage (claimName OnDemand).
- apiGroups: [""]
resources: ["persistentvolumeclaims"]
verbs: ["create", "get", "list", "watch", "delete"]
- apiGroups: [""]
resources: ["pods/log"]
verbs: ["get", "list", "watch"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,14 @@ public class ViewDefinitionResource extends DomainResource {

@Serial private static final long serialVersionUID = 1909997123685548098L;

@Nullable
@Child(name = "url")
private UriType url;

@Nullable
@Child(name = "version")
private StringType version;

@Nullable
@Getter
@Child(name = "name")
Expand Down Expand Up @@ -100,6 +108,50 @@ public class ViewDefinitionResource extends DomainResource {
@Child(name = "constant", max = Child.MAX_UNLIMITED)
private List<ConstantComponent> constant;

@Nullable
public String getUrl() {
return url == null ? null : url.getValue();
}

@Nullable
public UriType getUrlElement() {
return url;
}

public boolean hasUrlElement() {
return url != null && !url.isEmpty();
}

public void setUrlElement(final UriType url) {
this.url = url;
}

public void setUrl(final String url) {
this.url = url == null ? null : new UriType(url);
}

@Nullable
public String getVersion() {
return version == null ? null : version.getValue();
}

@Nullable
public StringType getVersionElement() {
return version;
}

public boolean hasVersionElement() {
return version != null && !version.isEmpty();
}

public void setVersionElement(final StringType version) {
this.version = version;
}

public void setVersion(final String version) {
this.version = version == null ? null : new StringType(version);
}

@Nullable
public StringType getNameElement() {
return name;
Expand Down Expand Up @@ -187,6 +239,8 @@ public boolean hasConstant() {
public DomainResource copy() {
final ViewDefinitionResource copy = new ViewDefinitionResource();
copyValues(copy);
copy.url = url != null ? url.copy() : null;
copy.version = version != null ? version.copy() : null;
copy.name = name != null ? name.copy() : null;
if (fhirVersion != null) {
copy.fhirVersion = new ArrayList<>();
Expand Down Expand Up @@ -233,6 +287,8 @@ public String fhirType() {
@Override
public boolean isEmpty() {
return super.isEmpty()
&& (url == null || url.isEmpty())
&& (version == null || version.isEmpty())
&& (name == null || name.isEmpty())
&& (fhirVersion == null || fhirVersion.isEmpty())
&& (resource == null || resource.isEmpty())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
import org.hl7.fhir.r4.model.BooleanType;
import org.hl7.fhir.r4.model.CodeType;
import org.hl7.fhir.r4.model.IntegerType;
import org.hl7.fhir.r4.model.UriType;
import org.json.JSONException;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
Expand Down Expand Up @@ -127,6 +128,8 @@ void testSchemaHasBasicFields() {

// Verify top-level fields exist.
assertTrue(schema.getFieldIndex("id").isDefined());
assertTrue(schema.getFieldIndex("url").isDefined());
assertTrue(schema.getFieldIndex("version").isDefined());
assertTrue(schema.getFieldIndex("name").isDefined());
assertTrue(schema.getFieldIndex("resource").isDefined());
assertTrue(schema.getFieldIndex("status").isDefined());
Expand All @@ -135,6 +138,49 @@ void testSchemaHasBasicFields() {
assertTrue(schema.getFieldIndex("constant").isDefined());
}

@Test
void testUrlAndVersionSurviveRoundTrip() {
// The url and version are required to match dependency references by canonical URL, so they
// must be retained through an encode/decode round-trip.
final ViewDefinitionResource original = createSimpleViewDefinition();
original.setUrlElement(new UriType("https://example.org/ViewDefinition/patients"));
original.setVersionElement(new org.hl7.fhir.r4.model.StringType("2.0"));

final ExpressionEncoder<ViewDefinitionResource> encoder =
fhirEncodersL0.of(ViewDefinitionResource.class);
final ExpressionEncoder<ViewDefinitionResource> resolvedEncoder =
EncoderUtils.defaultResolveAndBind(encoder);

final InternalRow serializedRow = resolvedEncoder.createSerializer().apply(original);
final ViewDefinitionResource decoded =
resolvedEncoder.createDeserializer().apply(serializedRow);

assertTrue(original.equalsDeep(decoded));
assertEquals("https://example.org/ViewDefinition/patients", decoded.getUrl());
assertEquals("2.0", decoded.getVersion());
}

@Test
void testDecodeWithoutUrlAndVersionYieldsEmptyValues() {
// A ViewDefinition stored without a url or version (the pre-existing case) must decode cleanly
// with absent url and version, leaving it unmatchable by canonical URL.
final ViewDefinitionResource original = createSimpleViewDefinition();

final ExpressionEncoder<ViewDefinitionResource> encoder =
fhirEncodersL0.of(ViewDefinitionResource.class);
final ExpressionEncoder<ViewDefinitionResource> resolvedEncoder =
EncoderUtils.defaultResolveAndBind(encoder);

final InternalRow serializedRow = resolvedEncoder.createSerializer().apply(original);
final ViewDefinitionResource decoded =
resolvedEncoder.createDeserializer().apply(serializedRow);

assertFalse(decoded.hasUrlElement());
assertFalse(decoded.hasVersionElement());
assertNull(decoded.getUrl());
assertNull(decoded.getVersion());
}

@Test
void testSchemaHandlesRecursiveSelectComponent() {
// Level 0: should NOT have nested select.select or unionAll.
Expand Down Expand Up @@ -846,4 +892,84 @@ void testFhirType() {
final ViewDefinitionResource view = new ViewDefinitionResource();
assertEquals("ViewDefinition", view.fhirType());
}

// ========== URL AND VERSION ACCESSOR TESTS ==========

@Test
void testSetUrlWithStringValue() {
// setUrl(String) wraps the value in a UriType and getUrl() unwraps it.
final ViewDefinitionResource view = new ViewDefinitionResource();
view.setUrl("https://example.org/ViewDefinition/patients");

assertTrue(view.hasUrlElement());
assertInstanceOf(UriType.class, view.getUrlElement());
assertEquals("https://example.org/ViewDefinition/patients", view.getUrl());
}

@Test
void testSetUrlWithNullValue() {
// setUrl(null) clears the element, leaving the field null so getUrl() returns null.
final ViewDefinitionResource view = new ViewDefinitionResource();
view.setUrl("https://example.org/ViewDefinition/patients");
view.setUrl(null);

assertNull(view.getUrlElement());
assertNull(view.getUrl());
assertFalse(view.hasUrlElement());
}

@Test
void testSetVersionWithStringValue() {
// setVersion(String) wraps the value in a StringType and getVersion() unwraps it.
final ViewDefinitionResource view = new ViewDefinitionResource();
view.setVersion("2.0");

assertTrue(view.hasVersionElement());
assertInstanceOf(org.hl7.fhir.r4.model.StringType.class, view.getVersionElement());
assertEquals("2.0", view.getVersion());
}

@Test
void testSetVersionWithNullValue() {
// setVersion(null) clears the element, leaving the field null so getVersion() returns null.
final ViewDefinitionResource view = new ViewDefinitionResource();
view.setVersion("2.0");
view.setVersion(null);

assertNull(view.getVersionElement());
assertNull(view.getVersion());
assertFalse(view.hasVersionElement());
}

@Test
void testCopyRetainsUrlAndVersion() {
// copy() must duplicate the url and version elements when they are present.
final ViewDefinitionResource original = createSimpleViewDefinition();
original.setUrl("https://example.org/ViewDefinition/patients");
original.setVersion("2.0");

final ViewDefinitionResource copy = (ViewDefinitionResource) original.copy();

assertTrue(original.equalsDeep(copy));
assertEquals("https://example.org/ViewDefinition/patients", copy.getUrl());
assertEquals("2.0", copy.getVersion());
}

@Test
void testIsEmptyConsidersUrlAndVersion() {
// A populated url or version makes the resource non-empty.
final ViewDefinitionResource withUrl = new ViewDefinitionResource();
withUrl.setUrl("https://example.org/ViewDefinition/patients");
assertFalse(withUrl.isEmpty());

final ViewDefinitionResource withVersion = new ViewDefinitionResource();
withVersion.setVersion("1.0");
assertFalse(withVersion.isEmpty());

// Non-null but empty url and version elements leave the resource empty.
final ViewDefinitionResource emptyElements = new ViewDefinitionResource();
emptyElements.setUrlElement(new UriType());
emptyElements.setVersionElement(new org.hl7.fhir.r4.model.StringType());
assertTrue(emptyElements.isEmpty());
}
}
51 changes: 17 additions & 34 deletions server/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.majorVersion>1</project.majorVersion>
<pathling.version>9.7.1</pathling.version>
<pathling.version>9.8.0</pathling.version>
<pathling.hapiFhirVersion>8.10.0</pathling.hapiFhirVersion>
<pathling.springBootVersion>3.5.14</pathling.springBootVersion>
<pathling.sparkVersion>4.0.2</pathling.sparkVersion>
Expand All @@ -56,6 +56,17 @@
<pathling.awsSdkVersion>2.40.3</pathling.awsSdkVersion>
<pathling.bulkExportVersion>1.0.4</pathling.bulkExportVersion>
<pathling.fhirAuthVersion>1.0.0</pathling.fhirAuthVersion>

<!-- The JVM module options required to run Pathling on Spark with Java 21.
These open the JDK internals that Spark accesses reflectively, most
notably sun.util.calendar, which Spark's date conversion reaches via a
method handle. They are referenced by the spring-boot:run goal and must
be kept in sync with the production entrypoint, which cannot reference
this property at runtime (see src/main/jib/usr/bin/entrypoint.sh). -->
<pathling.runtime.jvmModuleOpts>--add-exports=java.base/sun.nio.ch=ALL-UNNAMED --add-opens=java.base/java.net=ALL-UNNAMED --add-opens=java.base/sun.util.calendar=ALL-UNNAMED</pathling.runtime.jvmModuleOpts>
<!-- The test harness needs every runtime option plus additional opens for
the mocking and serialisation frameworks exercised during testing. -->
<pathling.test.jvmModuleOpts>${pathling.runtime.jvmModuleOpts} --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.invoke=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=java.base/java.nio=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.util.concurrent=ALL-UNNAMED --add-opens=java.base/java.util.concurrent.atomic=ALL-UNNAMED --add-opens=java.base/sun.nio.ch=ALL-UNNAMED --add-opens=java.base/sun.nio.cs=ALL-UNNAMED --add-opens=java.base/sun.security.action=ALL-UNNAMED</pathling.test.jvmModuleOpts>
</properties>

<repositories>
Expand Down Expand Up @@ -585,7 +596,9 @@
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${pathling.springBootVersion}</version>
<configuration>
<jvmArguments>--add-opens=java.base/sun.nio.ch=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED</jvmArguments>
<!-- Run with the shared runtime module options so that a local
spring-boot:run faithfully reproduces the production runtime. -->
<jvmArguments>${pathling.runtime.jvmModuleOpts}</jvmArguments>
</configuration>
</plugin>
<plugin>
Expand All @@ -601,22 +614,7 @@
<!-- Each integration test class runs in its own JVM to prevent URLStreamHandlerFactory
conflicts between Spark/Hadoop and Tomcat. See SPARK-25694 and Spring Boot #10617. -->
<reuseForks>false</reuseForks>
<argLine>@{argLine}
--add-opens java.base/java.lang=ALL-UNNAMED
--add-opens java.base/java.lang.invoke=ALL-UNNAMED
--add-opens java.base/java.lang.reflect=ALL-UNNAMED
--add-opens java.base/java.io=ALL-UNNAMED
--add-opens java.base/java.net=ALL-UNNAMED
--add-opens java.base/java.nio=ALL-UNNAMED
--add-opens java.base/java.util=ALL-UNNAMED
--add-opens java.base/java.util.concurrent=ALL-UNNAMED
--add-opens java.base/java.util.concurrent.atomic=ALL-UNNAMED
--add-opens java.base/sun.nio.ch=ALL-UNNAMED
--add-opens java.base/sun.nio.cs=ALL-UNNAMED
--add-opens java.base/sun.security.action=ALL-UNNAMED
--add-opens java.base/sun.util.calendar=ALL-UNNAMED
--add-exports java.base/sun.nio.ch=ALL-UNNAMED
</argLine>
<argLine>@{argLine} ${pathling.test.jvmModuleOpts}</argLine>
</configuration>
<executions>
<execution>
Expand All @@ -639,22 +637,7 @@
junit.jupiter.execution.parallel.enabled=true
</configurationParameters>
</properties>
<argLine>@{argLine}
--add-opens java.base/java.lang=ALL-UNNAMED
--add-opens java.base/java.lang.invoke=ALL-UNNAMED
--add-opens java.base/java.lang.reflect=ALL-UNNAMED
--add-opens java.base/java.io=ALL-UNNAMED
--add-opens java.base/java.net=ALL-UNNAMED
--add-opens java.base/java.nio=ALL-UNNAMED
--add-opens java.base/java.util=ALL-UNNAMED
--add-opens java.base/java.util.concurrent=ALL-UNNAMED
--add-opens java.base/java.util.concurrent.atomic=ALL-UNNAMED
--add-opens java.base/sun.nio.ch=ALL-UNNAMED
--add-opens java.base/sun.nio.cs=ALL-UNNAMED
--add-opens java.base/sun.security.action=ALL-UNNAMED
--add-opens java.base/sun.util.calendar=ALL-UNNAMED
--add-exports java.base/sun.nio.ch=ALL-UNNAMED
</argLine>
<argLine>@{argLine} ${pathling.test.jvmModuleOpts}</argLine>
</configuration>
</plugin>
<plugin>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,14 @@ public class SqlQueryConfiguration {
*/
@Min(1)
private long timeoutSeconds = 60L;

/**
* The maximum nesting depth of the dependency graph resolved for a single query. The top-level
* query's direct {@code relatedArtifact} dependencies sit at depth one; each further level of
* nested {@code SQLView} dependency increments the depth. A graph that nests deeper than this
* limit is rejected before any Spark work, guarding against accidental fan-out and runaway
* resolution. Real view graphs are shallow, so the default is generous while still bounded.
*/
@Min(1)
private int maxDependencyDepth = 10;
}
Loading
Loading