1use fraiseql_core::{db::dialect::SqlDialect, schema::TypeDefinition};
9
10use super::proto_gen::graphql_to_row_view_type;
11
12pub fn generate_row_view_sql(dialect: &dyn SqlDialect, type_def: &TypeDefinition) -> String {
27 let source_table = format!("tb_{}", type_def.sql_source);
28 let view_name = format!("vr_{}", type_def.sql_source);
29
30 let columns: Vec<(String, String)> = type_def
31 .fields
32 .iter()
33 .filter(|f| f.field_type.is_scalar())
34 .map(|f| {
35 let col_type = graphql_to_row_view_type(&f.field_type.to_graphql_string());
36 let expr =
37 dialect.row_view_column_expr(&type_def.jsonb_column, f.name.as_ref(), &col_type);
38 (f.name.to_string(), expr)
39 })
40 .collect();
41
42 dialect.create_row_view_ddl(&view_name, &source_table, &columns)
43}
44
45pub fn generate_all_row_views(
57 dialect: &dyn SqlDialect,
58 types: &[TypeDefinition],
59 include_types: &[String],
60 exclude_types: &[String],
61) -> String {
62 let mut ddl_parts = Vec::new();
63
64 for td in types {
65 let name: &str = td.name.as_ref();
66 if !include_types.is_empty() && !include_types.iter().any(|t| t == name) {
67 continue;
68 }
69 if exclude_types.iter().any(|t| t == name) {
70 continue;
71 }
72
73 let has_scalars = td.fields.iter().any(|f| f.field_type.is_scalar());
74 if !has_scalars {
75 continue;
76 }
77
78 ddl_parts.push(generate_row_view_sql(dialect, td));
79 }
80
81 ddl_parts.join("\n\n")
82}
83
84#[cfg(test)]
85mod tests {
86 use fraiseql_core::{
87 db::dialect::{MySqlDialect, PostgresDialect, SqlServerDialect, SqliteDialect},
88 schema::{FieldDefinition, FieldDenyPolicy, FieldType},
89 };
90
91 use super::*;
92
93 fn make_user_type() -> TypeDefinition {
94 TypeDefinition {
95 name: "user".into(),
96 sql_source: "user".into(),
97 jsonb_column: "data".to_string(),
98 fields: vec![
99 make_field("id", FieldType::Id, false),
100 make_field("name", FieldType::String, false),
101 make_field("email", FieldType::String, true),
102 make_field("created_at", FieldType::DateTime, false),
103 ],
104 description: None,
105 sql_projection_hint: None,
106 implements: vec![],
107 requires_role: None,
108 is_error: false,
109 relay: false,
110 relationships: Vec::new(),
111 }
112 }
113
114 fn make_field(name: &str, ft: FieldType, nullable: bool) -> FieldDefinition {
115 FieldDefinition {
116 name: name.into(),
117 field_type: ft,
118 nullable,
119 description: None,
120 default_value: None,
121 vector_config: None,
122 alias: None,
123 deprecation: None,
124 requires_scope: None,
125 on_deny: FieldDenyPolicy::default(),
126 encryption: None,
127 }
128 }
129
130 #[test]
133 fn test_postgres_row_view_ddl() {
134 let td = make_user_type();
135 let ddl = generate_row_view_sql(&PostgresDialect, &td);
136
137 assert!(ddl.contains("CREATE OR REPLACE VIEW \"vr_user\""));
138 assert!(ddl.contains("FROM \"tb_user\""));
139 assert!(ddl.contains("(data->>'id')::uuid AS \"id\""));
140 assert!(ddl.contains("(data->>'name')::text AS \"name\""));
141 assert!(ddl.contains("(data->>'email')::text AS \"email\""));
142 assert!(ddl.contains("(data->>'created_at')::timestamptz AS \"created_at\""));
143 }
144
145 #[test]
148 fn test_mysql_row_view_ddl() {
149 let td = make_user_type();
150 let ddl = generate_row_view_sql(&MySqlDialect, &td);
151
152 assert!(ddl.contains("CREATE OR REPLACE VIEW `vr_user`"));
153 assert!(ddl.contains("FROM `tb_user`"));
154 assert!(ddl.contains("CAST(JSON_UNQUOTE(JSON_EXTRACT(data, '$.id')) AS CHAR) AS `id`"));
155 assert!(ddl.contains("CAST(JSON_UNQUOTE(JSON_EXTRACT(data, '$.name')) AS CHAR) AS `name`"));
156 }
157
158 #[test]
161 fn test_sqlite_row_view_ddl() {
162 let td = make_user_type();
163 let ddl = generate_row_view_sql(&SqliteDialect, &td);
164
165 assert!(ddl.contains("DROP VIEW IF EXISTS \"vr_user\""));
166 assert!(ddl.contains("CREATE VIEW \"vr_user\""));
167 assert!(ddl.contains("FROM \"tb_user\""));
168 assert!(ddl.contains("CAST(json_extract(data, '$.id') AS TEXT) AS \"id\""));
169 assert!(ddl.contains("CAST(json_extract(data, '$.name') AS TEXT) AS \"name\""));
170 }
171
172 #[test]
175 fn test_sqlserver_row_view_ddl() {
176 let td = make_user_type();
177 let ddl = generate_row_view_sql(&SqlServerDialect, &td);
178
179 assert!(ddl.contains("CREATE OR ALTER VIEW [vr_user]"));
180 assert!(ddl.contains("FROM [tb_user]"));
181 assert!(ddl.contains("CAST(JSON_VALUE(data, '$.id') AS UNIQUEIDENTIFIER) AS [id]"));
182 assert!(ddl.contains("CAST(JSON_VALUE(data, '$.name') AS NVARCHAR(MAX)) AS [name]"));
183 }
184
185 #[test]
188 fn test_non_scalar_fields_excluded() {
189 let td = TypeDefinition {
190 name: "post".into(),
191 sql_source: "post".into(),
192 jsonb_column: "data".to_string(),
193 fields: vec![
194 make_field("id", FieldType::Id, false),
195 make_field("title", FieldType::String, false),
196 make_field("author", FieldType::Object("User".to_string()), false),
198 make_field("tags", FieldType::List(Box::new(FieldType::String)), false),
200 ],
201 description: None,
202 sql_projection_hint: None,
203 implements: vec![],
204 requires_role: None,
205 is_error: false,
206 relay: false,
207 relationships: Vec::new(),
208 };
209
210 let ddl = generate_row_view_sql(&PostgresDialect, &td);
211
212 assert!(ddl.contains("\"id\""));
214 assert!(ddl.contains("\"title\""));
215 assert!(!ddl.contains("\"author\""));
217 assert!(!ddl.contains("\"tags\""));
218 }
219
220 #[test]
223 fn test_custom_jsonb_column() {
224 let mut td = make_user_type();
225 td.jsonb_column = "payload".to_string();
226
227 let ddl = generate_row_view_sql(&PostgresDialect, &td);
228
229 assert!(ddl.contains("(payload->>'id')::uuid"));
230 assert!(!ddl.contains("data"));
231 }
232
233 #[test]
236 fn test_generate_all_with_exclude() {
237 let types = vec![
238 make_user_type(),
239 TypeDefinition {
240 name: "secret".into(),
241 sql_source: "secret".into(),
242 jsonb_column: "data".to_string(),
243 fields: vec![make_field("id", FieldType::Id, false)],
244 description: None,
245 sql_projection_hint: None,
246 implements: vec![],
247 requires_role: None,
248 is_error: false,
249 relay: false,
250 relationships: Vec::new(),
251 },
252 ];
253
254 let ddl = generate_all_row_views(&PostgresDialect, &types, &[], &["secret".to_string()]);
255
256 assert!(ddl.contains("vr_user"));
257 assert!(!ddl.contains("vr_secret"));
258 }
259
260 #[test]
263 fn test_source_table_is_command_side() {
264 let td = make_user_type();
265 let ddl = generate_row_view_sql(&PostgresDialect, &td);
266
267 assert!(ddl.contains("tb_user"));
269 assert!(!ddl.contains("v_user"));
270 }
271}