Skip to content

Commit 9c43dd3

Browse files
authored
Merge pull request #28 from Virtual-Repetitions/computed-type-support
Add @computed type support to Postgres subsystem
2 parents d0c0650 + 77f03af commit 9c43dd3

22 files changed

Lines changed: 288 additions & 169 deletions

File tree

Cargo.lock

Lines changed: 73 additions & 73 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[workspace.package]
2-
version = "0.29.3"
2+
version = "0.29.4"
33
edition = "2024"
44

55
# See https://github.com/mozilla/application-services/blob/main/Cargo.toml for the reasons why we use this structure

crates/cli/src/util/watcher.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,10 @@ where
156156
let mut command = tokio::process::Command::new(&server_binary);
157157

158158
command.kill_on_drop(true);
159+
command
160+
.stdin(Stdio::inherit())
161+
.stdout(Stdio::inherit())
162+
.stderr(Stdio::inherit());
159163

160164
if let Some(port) = server_port {
161165
command.env(EXO_SERVER_PORT, port.to_string());

crates/common/src/test_support.rs

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -84,12 +84,12 @@ fn relative_path(
8484
folder_path.join(path)
8585
}
8686

87-
pub fn read_relative_file(test_path: &PathBuf, path: &str) -> Result<String, std::io::Error> {
87+
pub fn read_relative_file(test_path: &Path, path: &str) -> Result<String, std::io::Error> {
8888
std::fs::read_to_string(test_path.join(path))
8989
}
9090

9191
pub fn assert_file_content(
92-
test_path: &PathBuf,
92+
test_path: &Path,
9393
path: &str,
9494
actual_content: &str,
9595
test_name: &str,
@@ -102,7 +102,7 @@ pub fn assert_file_content(
102102
path.replace(".expected.", ".actual.")
103103
} else {
104104
// Drop the extension and add ".actual" followed by the extension
105-
let extension = path.split('.').last().unwrap();
105+
let extension = path.split('.').next_back().unwrap();
106106
let file_name = path.split('.').next().unwrap();
107107
format!("{}.actual.{}", file_name, extension)
108108
}
@@ -111,13 +111,11 @@ pub fn assert_file_content(
111111

112112
let actual_content = actual_content.trim();
113113

114-
if !compare_strings_ignoring_whitespace(actual_content, &expected_content) {
114+
if !compare_strings_ignoring_whitespace(actual_content, expected_content) {
115115
std::fs::write(actual_file, actual_content).unwrap();
116116
return Err(format!("{}: {}", "File content mismatch".red(), test_name));
117-
} else {
118-
if actual_file.exists() {
119-
std::fs::remove_file(actual_file).unwrap();
120-
}
117+
} else if actual_file.exists() {
118+
std::fs::remove_file(actual_file).unwrap();
121119
}
122120

123121
Ok(())

crates/postgres-subsystem/postgres-builder/src/plugin.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,8 @@ impl SubsystemBuilder for PostgresSubsystemBuilder {
105105
(
106106
"computed",
107107
AnnotationSpec {
108-
targets: &[AnnotationTarget::Field],
109-
no_params: false,
108+
targets: &[AnnotationTarget::Field, AnnotationTarget::Type],
109+
no_params: true,
110110
single_params: false,
111111
mapped_params: Some(&[
112112
MappedAnnotationParamSpec {

crates/postgres-subsystem/postgres-core-builder/src/aggregate_type_builder.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ use core_model_builder::error::ModelBuildingError;
1313
use postgres_core_model::aggregate::{
1414
AggregateField, AggregateFieldType, AggregateType, ScalarAggregateFieldKind,
1515
};
16-
use postgres_core_model::types::EntityRepresentation;
1716

1817
use crate::resolved_type::ResolvedType;
1918
use crate::resolved_type::ResolvedTypeEnv;
@@ -28,7 +27,7 @@ pub fn aggregate_type_name(type_name: &str) -> String {
2827

2928
fn needs_aggregate(resolved_type: &ResolvedType) -> bool {
3029
if let ResolvedType::Composite(c) = resolved_type
31-
&& c.representation == EntityRepresentation::Json
30+
&& c.representation.is_json_like()
3231
{
3332
return false;
3433
}

crates/postgres-subsystem/postgres-core-builder/src/database_builder.rs

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ fn expand_database_info(
109109
resolved_env: &ResolvedTypeEnv,
110110
building: &mut DatabaseBuilding,
111111
) -> Result<(), ModelBuildingError> {
112-
if resolved_type.representation == EntityRepresentation::Json {
112+
if resolved_type.representation.is_json_like() {
113113
return Ok(());
114114
}
115115

@@ -200,7 +200,7 @@ fn expand_type_relations(
200200
resolved_env: &ResolvedTypeEnv,
201201
building: &mut DatabaseBuilding,
202202
) -> Result<(), ModelBuildingError> {
203-
if resolved_type.representation == EntityRepresentation::Json {
203+
if resolved_type.representation.is_json_like() {
204204
return Ok(());
205205
}
206206

@@ -227,14 +227,14 @@ fn expand_type_relations(
227227
.ok_or_else(|| {
228228
ModelBuildingError::Generic(format!(
229229
"The field `{}.{}` references type `{}` which is not registered in the Postgres subsystem. \
230-
Ensure this type is defined, imported, or marked with `@json` if it represents computed data.",
230+
Ensure this type is defined, imported, or marked with `@json` or `@computed` if it represents computed data.",
231231
resolved_type.name,
232232
field.name,
233233
field.typ.innermost().type_name
234234
))
235235
})?;
236236
if let ResolvedType::Composite(ct) = field_type
237-
&& ct.representation == EntityRepresentation::Json
237+
&& ct.representation.is_json_like()
238238
{
239239
return Ok(());
240240
}
@@ -492,9 +492,7 @@ fn create_columns(
492492
PhysicalColumn {
493493
table_id,
494494
name: column_name.to_string(),
495-
typ: if composite.representation
496-
== EntityRepresentation::Json
497-
{
495+
typ: if composite.representation.is_json_like() {
498496
Box::new(JsonColumnType)
499497
} else {
500498
// A placeholder value. Will be resolved in the next phase (see expand_type_relations)

crates/postgres-subsystem/postgres-core-builder/src/resolved_builder.rs

Lines changed: 109 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1000,8 +1000,42 @@ fn resolve_composite_type(
10001000
(None, None) => true,
10011001
};
10021002

1003-
let representation = if ct.annotations.contains("json") {
1003+
let is_json = ct.annotations.contains("json");
1004+
let is_computed = ct.annotations.contains("computed");
1005+
1006+
if is_json && is_computed {
1007+
errors.push(Diagnostic {
1008+
level: Level::Error,
1009+
message: format!("Type '{}' cannot use both @json and @computed", ct.name),
1010+
code: Some("C000".to_string()),
1011+
spans: vec![SpanLabel {
1012+
span: ct.span,
1013+
style: SpanStyle::Primary,
1014+
label: None,
1015+
}],
1016+
});
1017+
}
1018+
1019+
if is_computed && table_annotation.is_some() {
1020+
errors.push(Diagnostic {
1021+
level: Level::Error,
1022+
message: format!(
1023+
"Type '{}' cannot use @table when marked as @computed",
1024+
ct.name
1025+
),
1026+
code: Some("C000".to_string()),
1027+
spans: vec![SpanLabel {
1028+
span: ct.span,
1029+
style: SpanStyle::Primary,
1030+
label: None,
1031+
}],
1032+
});
1033+
}
1034+
1035+
let representation = if is_json {
10041036
EntityRepresentation::Json
1037+
} else if is_computed {
1038+
EntityRepresentation::Computed
10051039
} else if table_managed {
10061040
EntityRepresentation::Managed
10071041
} else {
@@ -1010,13 +1044,13 @@ fn resolve_composite_type(
10101044

10111045
let access_annotation = ct.annotations.get("access");
10121046

1013-
let is_json = representation == EntityRepresentation::Json;
1047+
let is_json_like = representation.is_json_like();
10141048

1015-
if is_json && access_annotation.is_some() {
1049+
if is_json_like && access_annotation.is_some() {
10161050
errors.push(Diagnostic {
10171051
level: Level::Error,
10181052
message: format!(
1019-
"Cannot use @access for type {}. Json types behave like a primitive (and thus have always-allowed access)",
1053+
"Cannot use @access for type {}. Json and computed types behave like a primitive (and thus have always-allowed access)",
10201054
ct.name
10211055
),
10221056
code: Some("C000".to_string()),
@@ -1040,7 +1074,7 @@ fn resolve_composite_type(
10401074
let join_table_config = join_table_annotation
10411075
.and_then(|annotation| parse_join_table_annotation(ct, annotation, errors));
10421076

1043-
let mut access = if is_json {
1077+
let mut access = if is_json_like {
10441078
// As if the user has annotated with `access(true)`
10451079
ResolvedAccess {
10461080
default: Some(AstExpr::BooleanLiteral(true, default_span())),
@@ -1054,7 +1088,7 @@ fn resolve_composite_type(
10541088

10551089
let (resolved_fields, ownership_field_found) = resolve_composite_type_fields(
10561090
ct,
1057-
is_json,
1091+
is_json_like,
10581092
table_managed,
10591093
module_base_path,
10601094
typechecked_system,
@@ -1120,7 +1154,7 @@ fn resolve_composite_type(
11201154

11211155
fn resolve_composite_type_fields(
11221156
ct: &AstModel<Typed>,
1123-
is_json: bool,
1157+
is_json_like: bool,
11241158
table_managed: bool,
11251159
module_base_path: &Path,
11261160
typechecked_system: &TypecheckedSystem,
@@ -1138,13 +1172,13 @@ fn resolve_composite_type_fields(
11381172

11391173
let access_annotation = field.annotations.get("access");
11401174

1141-
if is_json && access_annotation.is_some() {
1175+
if is_json_like && access_annotation.is_some() {
11421176
errors.push(Diagnostic {
1143-
level: Level::Error,
1144-
message: format!(
1145-
"Cannot use @access for field '{}' in a type with a '@json' annotation",
1146-
field.name
1147-
),
1177+
level: Level::Error,
1178+
message: format!(
1179+
"Cannot use @access for field '{}' in a type with a '@json' or '@computed' annotation",
1180+
field.name
1181+
),
11481182
code: Some("C000".to_string()),
11491183
spans: vec![SpanLabel {
11501184
span: field.span,
@@ -1944,6 +1978,8 @@ fn compute_column_info(
19441978
_ => field_name.to_snake_case(),
19451979
};
19461980
let enclosing_is_json = enclosing_type.annotations.contains("json");
1981+
let enclosing_is_computed = enclosing_type.annotations.contains("computed");
1982+
let enclosing_is_json_like = enclosing_is_json || enclosing_is_computed;
19471983

19481984
let id_column_names = |field: &AstField<Typed>| -> Result<Vec<String>, Diagnostic> {
19491985
let user_supplied_column_mapping = column_annotation_mapping(field);
@@ -2013,7 +2049,9 @@ fn compute_column_info(
20132049
AstFieldType::Plain(..) => {
20142050
match field_base_type.to_typ(types).deref(types) {
20152051
Type::Composite(field_type) => {
2016-
if field_type.annotations.contains("json") {
2052+
if field_type.annotations.contains("json")
2053+
|| (enclosing_is_json_like && enclosing_is_computed)
2054+
{
20172055
return Ok(ColumnInfo {
20182056
names: vec![compute_column_name(&field.name)],
20192057
self_column: true,
@@ -2132,9 +2170,9 @@ fn compute_column_info(
21322170
}
21332171
}
21342172
Type::Set(typ) => {
2135-
if enclosing_is_json
2136-
&& let Type::Composite(field_type) = typ.deref(types)
2137-
&& field_type.annotations.contains("json")
2173+
if let Type::Composite(field_type) = typ.deref(types)
2174+
&& (field_type.annotations.contains("json")
2175+
|| (enclosing_is_json_like && enclosing_is_computed))
21382176
{
21392177
return Ok(ColumnInfo {
21402178
names: vec![compute_column_name(&field.name)],
@@ -2227,17 +2265,29 @@ fn compute_column_info(
22272265
indices,
22282266
cardinality: None,
22292267
})
2230-
} else if enclosing_is_json
2231-
&& let Type::Composite(field_type) = &underlying_resolved
2232-
&& field_type.annotations.contains("json")
2233-
{
2234-
Ok(ColumnInfo {
2235-
names: vec![compute_column_name(&field.name)],
2236-
self_column: true,
2237-
unique_constraints,
2238-
indices,
2239-
cardinality: None,
2240-
})
2268+
} else if let Type::Composite(field_type) = &underlying_resolved {
2269+
if field_type.annotations.contains("json")
2270+
|| (enclosing_is_json_like && enclosing_is_computed)
2271+
{
2272+
Ok(ColumnInfo {
2273+
names: vec![compute_column_name(&field.name)],
2274+
self_column: true,
2275+
unique_constraints,
2276+
indices,
2277+
cardinality: None,
2278+
})
2279+
} else {
2280+
Err(Diagnostic {
2281+
level: Level::Error,
2282+
message: "Arrays of non-primitives are not supported".to_string(),
2283+
code: Some("C000".to_string()),
2284+
spans: vec![SpanLabel {
2285+
span: field.span,
2286+
style: SpanStyle::Primary,
2287+
label: None,
2288+
}],
2289+
})
2290+
}
22412291
} else {
22422292
Err(Diagnostic {
22432293
level: Level::Error,
@@ -2674,6 +2724,37 @@ mod tests {
26742724
);
26752725
}
26762726

2727+
#[multiplatform_test]
2728+
fn computed_collection_of_computed_types() {
2729+
let resolved = create_resolved_system_from_src(
2730+
r#"
2731+
@postgres
2732+
module ContentModule {
2733+
@computed
2734+
type TeamAvailablePlaybook {
2735+
tags: Array<TeamAvailablePlaybookTag>
2736+
lessons: Set<TeamAvailablePlaybookLesson>
2737+
}
2738+
2739+
@computed
2740+
type TeamAvailablePlaybookTag {
2741+
label: String
2742+
}
2743+
2744+
@computed
2745+
type TeamAvailablePlaybookLesson {
2746+
id: Int
2747+
}
2748+
}
2749+
"#,
2750+
);
2751+
2752+
assert!(
2753+
resolved.is_ok(),
2754+
"Expected @computed collection fields to resolve without errors"
2755+
);
2756+
}
2757+
26772758
#[test]
26782759
fn with_access() {
26792760
File::create("logger.js").unwrap();

0 commit comments

Comments
 (0)