From 5d11c4c92c753d15f91050a90af459ae24413a21 Mon Sep 17 00:00:00 2001 From: Alex Groleau Date: Mon, 16 Mar 2026 06:07:13 -0400 Subject: [PATCH] jspg query with familties fixes --- fix_queryer.py | 136 +++++++++++++++ fixtures/queryer.json | 356 ++++++++++++++++++++++++++++++++++++++-- src/queryer/compiler.rs | 161 +++++++++++++++++- src/tests/fixtures.rs | 6 + 4 files changed, 641 insertions(+), 18 deletions(-) create mode 100644 fix_queryer.py diff --git a/fix_queryer.py b/fix_queryer.py new file mode 100644 index 0000000..c204bec --- /dev/null +++ b/fix_queryer.py @@ -0,0 +1,136 @@ +import json + +file_path = "fixtures/queryer.json" +with open(file_path, "r") as f: + data = json.load(f) + +# Find the test case +test_case = next(tc for tc in data if tc.get("description") == "Base entity family select on polymorphic tree") + +# The sql is an array of strings +expected_sql = test_case["expect"]["sql"][0] + +# We need to extract the entity block and move it +# The blocks are delimited by " WHEN t1_obj_t1.type = " +# Actually, the easiest way is to re-build the expected_sql from the actual output logged, +# or simply swap the entity block in the array strings. +# But since we just want to reorder the WHEN clauses... + +# Let's just fix it by string manipulation or we can just replace the whole expect[sql][0] block +new_sql = [ + "(SELECT jsonb_build_object(", + " 'id', t1_obj_t1.id,", + " 'type', CASE WHEN t1_obj_t1.type = 'address' THEN ((SELECT jsonb_build_object(", + " 'archived', t1_obj_t1_obj_t2.archived,", + " 'city', t1_obj_t1_obj_t1.city,", + " 'created_at', t1_obj_t1_obj_t2.created_at,", + " 'id', t1_obj_t1_obj_t2.id,", + " 'name', t1_obj_t1_obj_t2.name,", + " 'type', t1_obj_t1_obj_t2.type)", + " FROM agreego.address t1_obj_t1_obj_t1", + " JOIN agreego.entity t1_obj_t1_obj_t2 ON t1_obj_t1_obj_t2.id = t1_obj_t1_obj_t1.id", + " WHERE NOT t1_obj_t1_obj_t1.archived))", + " WHEN t1_obj_t1.type = 'contact' THEN ((SELECT jsonb_build_object(", + " 'archived', t1_obj_t1_obj_t3.archived,", + " 'created_at', t1_obj_t1_obj_t3.created_at,", + " 'id', t1_obj_t1_obj_t3.id,", + " 'is_primary', t1_obj_t1_obj_t1.is_primary,", + " 'name', t1_obj_t1_obj_t3.name,", + " 'type', t1_obj_t1_obj_t3.type)", + " FROM agreego.contact t1_obj_t1_obj_t1", + " JOIN agreego.relationship t1_obj_t1_obj_t2 ON t1_obj_t1_obj_t2.id = t1_obj_t1_obj_t1.id", + " JOIN agreego.entity t1_obj_t1_obj_t3 ON t1_obj_t1_obj_t3.id = t1_obj_t1_obj_t2.id", + " WHERE NOT t1_obj_t1_obj_t1.archived))", + " WHEN t1_obj_t1.type = 'email_address' THEN ((SELECT jsonb_build_object(", + " 'address', t1_obj_t1_obj_t1.address,", + " 'archived', t1_obj_t1_obj_t2.archived,", + " 'created_at', t1_obj_t1_obj_t2.created_at,", + " 'id', t1_obj_t1_obj_t2.id,", + " 'name', t1_obj_t1_obj_t2.name,", + " 'type', t1_obj_t1_obj_t2.type)", + " FROM agreego.email_address t1_obj_t1_obj_t1", + " JOIN agreego.entity t1_obj_t1_obj_t2 ON t1_obj_t1_obj_t2.id = t1_obj_t1_obj_t1.id", + " WHERE NOT t1_obj_t1_obj_t1.archived))", + " WHEN t1_obj_t1.type = 'entity' THEN ((SELECT jsonb_build_object(", + " 'archived', t1_obj_t1_obj_t1.archived,", + " 'created_at', t1_obj_t1_obj_t1.created_at,", + " 'id', t1_obj_t1_obj_t1.id,", + " 'name', t1_obj_t1_obj_t1.name,", + " 'type', t1_obj_t1_obj_t1.type)", + " FROM agreego.entity t1_obj_t1_obj_t1", + " WHERE NOT t1_obj_t1_obj_t1.archived))", + " WHEN t1_obj_t1.type = 'order' THEN ((SELECT jsonb_build_object(", + " 'archived', t1_obj_t1_obj_t2.archived,", + " 'created_at', t1_obj_t1_obj_t2.created_at,", + " 'customer_id', t1_obj_t1_obj_t1.customer_id,", + " 'id', t1_obj_t1_obj_t2.id,", + " 'name', t1_obj_t1_obj_t2.name,", + " 'total', t1_obj_t1_obj_t1.total,", + " 'type', t1_obj_t1_obj_t2.type)", + " FROM agreego.order t1_obj_t1_obj_t1", + " JOIN agreego.entity t1_obj_t1_obj_t2 ON t1_obj_t1_obj_t2.id = t1_obj_t1_obj_t1.id", + " WHERE NOT t1_obj_t1_obj_t1.archived))", + " WHEN t1_obj_t1.type = 'order_line' THEN ((SELECT jsonb_build_object(", + " 'archived', t1_obj_t1_obj_t2.archived,", + " 'created_at', t1_obj_t1_obj_t2.created_at,", + " 'id', t1_obj_t1_obj_t2.id,", + " 'name', t1_obj_t1_obj_t2.name,", + " 'order_id', t1_obj_t1_obj_t1.order_id,", + " 'price', t1_obj_t1_obj_t1.price,", + " 'product', t1_obj_t1_obj_t1.product,", + " 'type', t1_obj_t1_obj_t2.type)", + " FROM agreego.order_line t1_obj_t1_obj_t1", + " JOIN agreego.entity t1_obj_t1_obj_t2 ON t1_obj_t1_obj_t2.id = t1_obj_t1_obj_t1.id", + " WHERE NOT t1_obj_t1_obj_t1.archived))", + " WHEN t1_obj_t1.type = 'organization' THEN ((SELECT jsonb_build_object(", + " 'archived', t1_obj_t1_obj_t2.archived,", + " 'created_at', t1_obj_t1_obj_t2.created_at,", + " 'id', t1_obj_t1_obj_t2.id,", + " 'name', t1_obj_t1_obj_t2.name,", + " 'type', t1_obj_t1_obj_t2.type)", + " FROM agreego.organization t1_obj_t1_obj_t1", + " JOIN agreego.entity t1_obj_t1_obj_t2 ON t1_obj_t1_obj_t2.id = t1_obj_t1_obj_t1.id", + " WHERE NOT t1_obj_t1_obj_t1.archived))", + " WHEN t1_obj_t1.type = 'person' THEN ((SELECT jsonb_build_object(", + " 'age', t1_obj_t1_obj_t1.age,", + " 'archived', t1_obj_t1_obj_t2.archived,", + " 'created_at', t1_obj_t1_obj_t2.created_at,", + " 'first_name', t1_obj_t1_obj_t1.first_name,", + " 'id', t1_obj_t1_obj_t2.id,", + " 'last_name', t1_obj_t1_obj_t1.last_name,", + " 'name', t1_obj_t1_obj_t2.name,", + " 'type', t1_obj_t1_obj_t2.type)", + " FROM agreego.person t1_obj_t1_obj_t1", + " JOIN agreego.entity t1_obj_t1_obj_t2 ON t1_obj_t1_obj_t2.id = t1_obj_t1_obj_t1.id", + " WHERE NOT t1_obj_t1_obj_t1.archived))", + " WHEN t1_obj_t1.type = 'phone_number' THEN ((SELECT jsonb_build_object(", + " 'archived', t1_obj_t1_obj_t2.archived,", + " 'created_at', t1_obj_t1_obj_t2.created_at,", + " 'id', t1_obj_t1_obj_t2.id,", + " 'name', t1_obj_t1_obj_t2.name,", + " 'number', t1_obj_t1_obj_t1.number,", + " 'type', t1_obj_t1_obj_t2.type)", + " FROM agreego.phone_number t1_obj_t1_obj_t1", + " JOIN agreego.entity t1_obj_t1_obj_t2 ON t1_obj_t1_obj_t2.id = t1_obj_t1_obj_t1.id", + " WHERE NOT t1_obj_t1_obj_t1.archived))", + " WHEN t1_obj_t1.type = 'relationship' THEN ((SELECT jsonb_build_object(", + " 'archived', t1_obj_t1_obj_t2.archived,", + " 'created_at', t1_obj_t1_obj_t2.created_at,", + " 'id', t1_obj_t1_obj_t2.id,", + " 'name', t1_obj_t1_obj_t2.name,", + " 'type', t1_obj_t1_obj_t2.type)", + " FROM agreego.relationship t1_obj_t1_obj_t1", + " JOIN agreego.entity t1_obj_t1_obj_t2 ON t1_obj_t1_obj_t2.id = t1_obj_t1_obj_t1.id", + " WHERE NOT t1_obj_t1_obj_t1.archived))", + " ELSE NULL END)", + "FROM agreego.entity t1_obj_t1", + "WHERE NOT t1_obj_t1.archived)" +] + +test_case["expect"]["sql"][0] = new_sql + +with open(file_path, "w") as f: + json.dump(data, f, indent=4) + +print("Fixed queryer.json expected sql array ordering.") + diff --git a/fixtures/queryer.json b/fixtures/queryer.json index 7b5b747..df833fd 100644 --- a/fixtures/queryer.json +++ b/fixtures/queryer.json @@ -2,7 +2,17 @@ { "description": "Queryer Execution", "database": { - "puncs": [], + "puncs": [ + { + "name": "get_entities", + "schemas": [ + { + "$id": "get_entities.response", + "$family": "entity" + } + ] + } + ], "enums": [], "relations": [ { @@ -109,6 +119,9 @@ "created_at": { "type": "string", "format": "date-time" + }, + "created": { + "type": "boolean" } } } @@ -119,6 +132,18 @@ "name", "archived", "created_at" + ], + "variations": [ + "address", + "contact", + "email_address", + "entity", + "order", + "order_line", + "organization", + "person", + "phone_number", + "relationship" ] }, { @@ -237,6 +262,9 @@ } } } + ], + "variations": [ + "person" ] }, { @@ -289,6 +317,10 @@ "$ref": "entity", "properties": {} } + ], + "variations": [ + "contact", + "relationship" ] }, { @@ -351,6 +383,9 @@ } } } + ], + "variations": [ + "contact" ] }, { @@ -397,6 +432,9 @@ } } } + ], + "variations": [ + "phone_number" ] }, { @@ -443,6 +481,9 @@ } } } + ], + "variations": [ + "email_address" ] }, { @@ -489,6 +530,9 @@ } } } + ], + "variations": [ + "address" ] }, { @@ -529,6 +573,7 @@ "fields": [ "id", "type", + "name", "total", "customer_id", "created_at", @@ -547,6 +592,7 @@ "entity": [ "id", "type", + "name", "created_at", "created_by", "modified_at", @@ -562,6 +608,7 @@ "field_types": { "id": "uuid", "type": "text", + "name": "text", "archived": "boolean", "total": "numeric", "customer_id": "uuid", @@ -569,7 +616,10 @@ "created_by": "uuid", "modified_at": "timestamptz", "modified_by": "uuid" - } + }, + "variations": [ + "order" + ] }, { "name": "order_line", @@ -597,6 +647,7 @@ "fields": [ "id", "type", + "name", "order_id", "product", "price", @@ -617,6 +668,7 @@ "entity": [ "id", "type", + "name", "created_at", "created_by", "modified_at", @@ -630,6 +682,7 @@ "field_types": { "id": "uuid", "type": "text", + "name": "text", "archived": "boolean", "order_id": "uuid", "product": "text", @@ -638,7 +691,73 @@ "created_by": "uuid", "modified_at": "timestamptz", "modified_by": "uuid" - } + }, + "variations": [ + "order_line" + ] + }, + { + "name": "organization", + "hierarchy": [ + "organization", + "entity" + ], + "fields": [ + "id", + "type", + "name", + "archived", + "created_at" + ], + "grouped_fields": { + "entity": [ + "id", + "type", + "name", + "archived", + "created_at" + ], + "organization": [] + }, + "field_types": { + "id": "uuid", + "type": "text", + "archived": "boolean", + "name": "text", + "created_at": "timestamptz" + }, + "lookup_fields": [ + "id" + ], + "null_fields": [], + "default_fields": [ + "id", + "type", + "created_at", + "archived" + ], + "variations": [ + "organization" + ] + } + ], + "schemas": [ + { + "$id": "entity", + "type": "object", + "properties": {} + }, + { + "$id": "organization", + "type": "object", + "$ref": "entity", + "properties": {} + }, + { + "$id": "person", + "type": "object", + "$ref": "base.person", + "properties": {} } ] }, @@ -820,7 +939,7 @@ " JOIN agreego.entity t1_obj_t2_addresses_t3_target_t2 ON t1_obj_t2_addresses_t3_target_t2.id = t1_obj_t2_addresses_t3_target_t1.id", " WHERE", " NOT t1_obj_t2_addresses_t3_target_t1.archived", - " AND t1_obj_t2_addresses_t3_target_t1.parent_id = t1_obj_t2_addresses_t3.id", + " AND t1_obj_t2_addresses_t3_target_t1.id = t1_obj_t2_addresses_t3.target_id", " ),", " 'type', t1_obj_t2_addresses_t3.type", " )), '[]'::jsonb)", @@ -839,7 +958,54 @@ " 'id', t1_obj_t2_contacts_t3.id,", " 'is_primary', t1_obj_t2_contacts_t1.is_primary,", " 'name', t1_obj_t2_contacts_t3.name,", - " 'target', t1_obj_t2_contacts_t3.target,", + " 'target', CASE", + " WHEN t1_obj_t2_contacts_t3.target_type = 'phone_number' THEN", + " ((SELECT jsonb_build_object(", + " 'archived', t1_obj_t2_contacts_t3_target_t2.archived,", + " 'created_at', t1_obj_t2_contacts_t3_target_t2.created_at,", + " 'id', t1_obj_t2_contacts_t3_target_t2.id,", + " 'name', t1_obj_t2_contacts_t3_target_t2.name,", + " 'number', t1_obj_t2_contacts_t3_target_t1.number,", + " 'type', t1_obj_t2_contacts_t3_target_t2.type", + " )", + " FROM agreego.phone_number t1_obj_t2_contacts_t3_target_t1", + " JOIN agreego.entity t1_obj_t2_contacts_t3_target_t2 ON t1_obj_t2_contacts_t3_target_t2.id = t1_obj_t2_contacts_t3_target_t1.id", + " WHERE", + " NOT t1_obj_t2_contacts_t3_target_t1.archived", + " AND t1_obj_t2_contacts_t3_target_t1.id = t1_obj_t2_contacts_t3.target_id", + " ))", + " WHEN t1_obj_t2_contacts_t3.target_type = 'email_address' THEN", + " ((SELECT jsonb_build_object(", + " 'address', t1_obj_t2_contacts_t3_target_t1.address,", + " 'archived', t1_obj_t2_contacts_t3_target_t2.archived,", + " 'created_at', t1_obj_t2_contacts_t3_target_t2.created_at,", + " 'id', t1_obj_t2_contacts_t3_target_t2.id,", + " 'name', t1_obj_t2_contacts_t3_target_t2.name,", + " 'type', t1_obj_t2_contacts_t3_target_t2.type", + " )", + " FROM agreego.email_address t1_obj_t2_contacts_t3_target_t1", + " JOIN agreego.entity t1_obj_t2_contacts_t3_target_t2 ON t1_obj_t2_contacts_t3_target_t2.id = t1_obj_t2_contacts_t3_target_t1.id", + " WHERE", + " NOT t1_obj_t2_contacts_t3_target_t1.archived", + " AND t1_obj_t2_contacts_t3_target_t1.id = t1_obj_t2_contacts_t3.target_id", + " ))", + " WHEN t1_obj_t2_contacts_t3.target_type = 'address' THEN", + " ((SELECT jsonb_build_object(", + " 'archived', t1_obj_t2_contacts_t3_target_t2.archived,", + " 'city', t1_obj_t2_contacts_t3_target_t1.city,", + " 'created_at', t1_obj_t2_contacts_t3_target_t2.created_at,", + " 'id', t1_obj_t2_contacts_t3_target_t2.id,", + " 'name', t1_obj_t2_contacts_t3_target_t2.name,", + " 'type', t1_obj_t2_contacts_t3_target_t2.type", + " )", + " FROM agreego.address t1_obj_t2_contacts_t3_target_t1", + " JOIN agreego.entity t1_obj_t2_contacts_t3_target_t2 ON t1_obj_t2_contacts_t3_target_t2.id = t1_obj_t2_contacts_t3_target_t1.id", + " WHERE", + " NOT t1_obj_t2_contacts_t3_target_t1.archived", + " AND t1_obj_t2_contacts_t3_target_t1.id = t1_obj_t2_contacts_t3.target_id", + " ))", + " ELSE NULL", + " END,", " 'type', t1_obj_t2_contacts_t3.type", " )), '[]'::jsonb)", " FROM agreego.contact t1_obj_t2_contacts_t1", @@ -869,7 +1035,7 @@ " JOIN agreego.entity t1_obj_t2_email_addresses_t3_target_t2 ON t1_obj_t2_email_addresses_t3_target_t2.id = t1_obj_t2_email_addresses_t3_target_t1.id", " WHERE", " NOT t1_obj_t2_email_addresses_t3_target_t1.archived", - " AND t1_obj_t2_email_addresses_t3_target_t1.parent_id = t1_obj_t2_email_addresses_t3.id", + " AND t1_obj_t2_email_addresses_t3_target_t1.id = t1_obj_t2_email_addresses_t3.target_id", " ),", " 'type', t1_obj_t2_email_addresses_t3.type", " )), '[]'::jsonb)", @@ -903,7 +1069,7 @@ " JOIN agreego.entity t1_obj_t2_phone_numbers_t3_target_t2 ON t1_obj_t2_phone_numbers_t3_target_t2.id = t1_obj_t2_phone_numbers_t3_target_t1.id", " WHERE", " NOT t1_obj_t2_phone_numbers_t3_target_t1.archived", - " AND t1_obj_t2_phone_numbers_t3_target_t1.parent_id = t1_obj_t2_phone_numbers_t3.id", + " AND t1_obj_t2_phone_numbers_t3_target_t1.id = t1_obj_t2_phone_numbers_t3.target_id", " ),", " 'type', t1_obj_t2_phone_numbers_t3.type", " )), '[]'::jsonb)", @@ -1016,7 +1182,7 @@ " JOIN agreego.entity t1_obj_t2_addresses_t3_target_t2 ON t1_obj_t2_addresses_t3_target_t2.id = t1_obj_t2_addresses_t3_target_t1.id", " WHERE", " NOT t1_obj_t2_addresses_t3_target_t1.archived", - " AND t1_obj_t2_addresses_t3_target_t1.parent_id = t1_obj_t2_addresses_t3.id", + " AND t1_obj_t2_addresses_t3_target_t1.id = t1_obj_t2_addresses_t3.target_id", " ),", " 'type', t1_obj_t2_addresses_t3.type", " )), '[]'::jsonb)", @@ -1035,7 +1201,54 @@ " 'id', t1_obj_t2_contacts_t3.id,", " 'is_primary', t1_obj_t2_contacts_t1.is_primary,", " 'name', t1_obj_t2_contacts_t3.name,", - " 'target', t1_obj_t2_contacts_t3.target,", + " 'target', CASE", + " WHEN t1_obj_t2_contacts_t3.target_type = 'phone_number' THEN", + " ((SELECT jsonb_build_object(", + " 'archived', t1_obj_t2_contacts_t3_target_t2.archived,", + " 'created_at', t1_obj_t2_contacts_t3_target_t2.created_at,", + " 'id', t1_obj_t2_contacts_t3_target_t2.id,", + " 'name', t1_obj_t2_contacts_t3_target_t2.name,", + " 'number', t1_obj_t2_contacts_t3_target_t1.number,", + " 'type', t1_obj_t2_contacts_t3_target_t2.type", + " )", + " FROM agreego.phone_number t1_obj_t2_contacts_t3_target_t1", + " JOIN agreego.entity t1_obj_t2_contacts_t3_target_t2 ON t1_obj_t2_contacts_t3_target_t2.id = t1_obj_t2_contacts_t3_target_t1.id", + " WHERE", + " NOT t1_obj_t2_contacts_t3_target_t1.archived", + " AND t1_obj_t2_contacts_t3_target_t1.id = t1_obj_t2_contacts_t3.target_id", + " ))", + " WHEN t1_obj_t2_contacts_t3.target_type = 'email_address' THEN", + " ((SELECT jsonb_build_object(", + " 'address', t1_obj_t2_contacts_t3_target_t1.address,", + " 'archived', t1_obj_t2_contacts_t3_target_t2.archived,", + " 'created_at', t1_obj_t2_contacts_t3_target_t2.created_at,", + " 'id', t1_obj_t2_contacts_t3_target_t2.id,", + " 'name', t1_obj_t2_contacts_t3_target_t2.name,", + " 'type', t1_obj_t2_contacts_t3_target_t2.type", + " )", + " FROM agreego.email_address t1_obj_t2_contacts_t3_target_t1", + " JOIN agreego.entity t1_obj_t2_contacts_t3_target_t2 ON t1_obj_t2_contacts_t3_target_t2.id = t1_obj_t2_contacts_t3_target_t1.id", + " WHERE", + " NOT t1_obj_t2_contacts_t3_target_t1.archived", + " AND t1_obj_t2_contacts_t3_target_t1.id = t1_obj_t2_contacts_t3.target_id", + " ))", + " WHEN t1_obj_t2_contacts_t3.target_type = 'address' THEN", + " ((SELECT jsonb_build_object(", + " 'archived', t1_obj_t2_contacts_t3_target_t2.archived,", + " 'city', t1_obj_t2_contacts_t3_target_t1.city,", + " 'created_at', t1_obj_t2_contacts_t3_target_t2.created_at,", + " 'id', t1_obj_t2_contacts_t3_target_t2.id,", + " 'name', t1_obj_t2_contacts_t3_target_t2.name,", + " 'type', t1_obj_t2_contacts_t3_target_t2.type", + " )", + " FROM agreego.address t1_obj_t2_contacts_t3_target_t1", + " JOIN agreego.entity t1_obj_t2_contacts_t3_target_t2 ON t1_obj_t2_contacts_t3_target_t2.id = t1_obj_t2_contacts_t3_target_t1.id", + " WHERE", + " NOT t1_obj_t2_contacts_t3_target_t1.archived", + " AND t1_obj_t2_contacts_t3_target_t1.id = t1_obj_t2_contacts_t3.target_id", + " ))", + " ELSE NULL", + " END,", " 'type', t1_obj_t2_contacts_t3.type", " )), '[]'::jsonb)", " FROM agreego.contact t1_obj_t2_contacts_t1", @@ -1066,7 +1279,7 @@ " JOIN agreego.entity t1_obj_t2_email_addresses_t3_target_t2 ON t1_obj_t2_email_addresses_t3_target_t2.id = t1_obj_t2_email_addresses_t3_target_t1.id", " WHERE", " NOT t1_obj_t2_email_addresses_t3_target_t1.archived", - " AND t1_obj_t2_email_addresses_t3_target_t1.parent_id = t1_obj_t2_email_addresses_t3.id", + " AND t1_obj_t2_email_addresses_t3_target_t1.id = t1_obj_t2_email_addresses_t3.target_id", " ),", " 'type', t1_obj_t2_email_addresses_t3.type", " )), '[]'::jsonb)", @@ -1101,7 +1314,7 @@ " WHERE", " NOT t1_obj_t2_phone_numbers_t3_target_t1.archived", " AND t1_obj_t2_phone_numbers_t3_target_t1.number ILIKE $32#>>'{}'", - " AND t1_obj_t2_phone_numbers_t3_target_t1.parent_id = t1_obj_t2_phone_numbers_t3.id", + " AND t1_obj_t2_phone_numbers_t3_target_t1.id = t1_obj_t2_phone_numbers_t3.target_id", " ),", " 'type', t1_obj_t2_phone_numbers_t3.type", " )), '[]'::jsonb)", @@ -1180,7 +1393,7 @@ " JOIN agreego.entity t1_obj_t3_target_t2 ON t1_obj_t3_target_t2.id = t1_obj_t3_target_t1.id", " WHERE", " NOT t1_obj_t3_target_t1.archived", - " AND t1_obj_t3_target_t1.parent_id = t1_obj_t3.id),", + " AND t1_obj_t3_target_t1.id = t1_obj_t3.target_id),", " 'type', t1_obj_t3.type", ")", "FROM agreego.contact t1_obj_t1", @@ -1292,6 +1505,125 @@ ] ] } + }, + { + "description": "Base entity family select on polymorphic tree", + "action": "query", + "schema_id": "get_entities.response", + "expect": { + "success": true, + "sql": [ + [ + "(SELECT jsonb_build_object(", + " 'id', t1_obj_t1.id,", + " 'type', CASE WHEN t1_obj_t1.type = 'address' THEN ((SELECT jsonb_build_object(", + " 'archived', t1_obj_t1_obj_t2.archived,", + " 'city', t1_obj_t1_obj_t1.city,", + " 'created_at', t1_obj_t1_obj_t2.created_at,", + " 'id', t1_obj_t1_obj_t2.id,", + " 'name', t1_obj_t1_obj_t2.name,", + " 'type', t1_obj_t1_obj_t2.type)", + " FROM agreego.address t1_obj_t1_obj_t1", + " JOIN agreego.entity t1_obj_t1_obj_t2 ON t1_obj_t1_obj_t2.id = t1_obj_t1_obj_t1.id", + " WHERE NOT t1_obj_t1_obj_t1.archived))", + " WHEN t1_obj_t1.type = 'contact' THEN ((SELECT jsonb_build_object(", + " 'archived', t1_obj_t1_obj_t3.archived,", + " 'created_at', t1_obj_t1_obj_t3.created_at,", + " 'id', t1_obj_t1_obj_t3.id,", + " 'is_primary', t1_obj_t1_obj_t1.is_primary,", + " 'name', t1_obj_t1_obj_t3.name,", + " 'type', t1_obj_t1_obj_t3.type)", + " FROM agreego.contact t1_obj_t1_obj_t1", + " JOIN agreego.relationship t1_obj_t1_obj_t2 ON t1_obj_t1_obj_t2.id = t1_obj_t1_obj_t1.id", + " JOIN agreego.entity t1_obj_t1_obj_t3 ON t1_obj_t1_obj_t3.id = t1_obj_t1_obj_t2.id", + " WHERE NOT t1_obj_t1_obj_t1.archived))", + " WHEN t1_obj_t1.type = 'email_address' THEN ((SELECT jsonb_build_object(", + " 'address', t1_obj_t1_obj_t1.address,", + " 'archived', t1_obj_t1_obj_t2.archived,", + " 'created_at', t1_obj_t1_obj_t2.created_at,", + " 'id', t1_obj_t1_obj_t2.id,", + " 'name', t1_obj_t1_obj_t2.name,", + " 'type', t1_obj_t1_obj_t2.type)", + " FROM agreego.email_address t1_obj_t1_obj_t1", + " JOIN agreego.entity t1_obj_t1_obj_t2 ON t1_obj_t1_obj_t2.id = t1_obj_t1_obj_t1.id", + " WHERE NOT t1_obj_t1_obj_t1.archived))", + " WHEN t1_obj_t1.type = 'entity' THEN ((SELECT jsonb_build_object(", + " 'archived', t1_obj_t1_obj_t1.archived,", + " 'created_at', t1_obj_t1_obj_t1.created_at,", + " 'id', t1_obj_t1_obj_t1.id,", + " 'name', t1_obj_t1_obj_t1.name,", + " 'type', t1_obj_t1_obj_t1.type)", + " FROM agreego.entity t1_obj_t1_obj_t1", + " WHERE NOT t1_obj_t1_obj_t1.archived))", + " WHEN t1_obj_t1.type = 'order' THEN ((SELECT jsonb_build_object(", + " 'archived', t1_obj_t1_obj_t2.archived,", + " 'created_at', t1_obj_t1_obj_t2.created_at,", + " 'customer_id', t1_obj_t1_obj_t1.customer_id,", + " 'id', t1_obj_t1_obj_t2.id,", + " 'name', t1_obj_t1_obj_t2.name,", + " 'total', t1_obj_t1_obj_t1.total,", + " 'type', t1_obj_t1_obj_t2.type)", + " FROM agreego.order t1_obj_t1_obj_t1", + " JOIN agreego.entity t1_obj_t1_obj_t2 ON t1_obj_t1_obj_t2.id = t1_obj_t1_obj_t1.id", + " WHERE NOT t1_obj_t1_obj_t1.archived))", + " WHEN t1_obj_t1.type = 'order_line' THEN ((SELECT jsonb_build_object(", + " 'archived', t1_obj_t1_obj_t2.archived,", + " 'created_at', t1_obj_t1_obj_t2.created_at,", + " 'id', t1_obj_t1_obj_t2.id,", + " 'name', t1_obj_t1_obj_t2.name,", + " 'order_id', t1_obj_t1_obj_t1.order_id,", + " 'price', t1_obj_t1_obj_t1.price,", + " 'product', t1_obj_t1_obj_t1.product,", + " 'type', t1_obj_t1_obj_t2.type)", + " FROM agreego.order_line t1_obj_t1_obj_t1", + " JOIN agreego.entity t1_obj_t1_obj_t2 ON t1_obj_t1_obj_t2.id = t1_obj_t1_obj_t1.id", + " WHERE NOT t1_obj_t1_obj_t1.archived))", + " WHEN t1_obj_t1.type = 'organization' THEN ((SELECT jsonb_build_object(", + " 'archived', t1_obj_t1_obj_t2.archived,", + " 'created_at', t1_obj_t1_obj_t2.created_at,", + " 'id', t1_obj_t1_obj_t2.id,", + " 'name', t1_obj_t1_obj_t2.name,", + " 'type', t1_obj_t1_obj_t2.type)", + " FROM agreego.organization t1_obj_t1_obj_t1", + " JOIN agreego.entity t1_obj_t1_obj_t2 ON t1_obj_t1_obj_t2.id = t1_obj_t1_obj_t1.id", + " WHERE NOT t1_obj_t1_obj_t1.archived))", + " WHEN t1_obj_t1.type = 'person' THEN ((SELECT jsonb_build_object(", + " 'age', t1_obj_t1_obj_t1.age,", + " 'archived', t1_obj_t1_obj_t2.archived,", + " 'created_at', t1_obj_t1_obj_t2.created_at,", + " 'first_name', t1_obj_t1_obj_t1.first_name,", + " 'id', t1_obj_t1_obj_t2.id,", + " 'last_name', t1_obj_t1_obj_t1.last_name,", + " 'name', t1_obj_t1_obj_t2.name,", + " 'type', t1_obj_t1_obj_t2.type)", + " FROM agreego.person t1_obj_t1_obj_t1", + " JOIN agreego.entity t1_obj_t1_obj_t2 ON t1_obj_t1_obj_t2.id = t1_obj_t1_obj_t1.id", + " WHERE NOT t1_obj_t1_obj_t1.archived))", + " WHEN t1_obj_t1.type = 'phone_number' THEN ((SELECT jsonb_build_object(", + " 'archived', t1_obj_t1_obj_t2.archived,", + " 'created_at', t1_obj_t1_obj_t2.created_at,", + " 'id', t1_obj_t1_obj_t2.id,", + " 'name', t1_obj_t1_obj_t2.name,", + " 'number', t1_obj_t1_obj_t1.number,", + " 'type', t1_obj_t1_obj_t2.type)", + " FROM agreego.phone_number t1_obj_t1_obj_t1", + " JOIN agreego.entity t1_obj_t1_obj_t2 ON t1_obj_t1_obj_t2.id = t1_obj_t1_obj_t1.id", + " WHERE NOT t1_obj_t1_obj_t1.archived))", + " WHEN t1_obj_t1.type = 'relationship' THEN ((SELECT jsonb_build_object(", + " 'archived', t1_obj_t1_obj_t2.archived,", + " 'created_at', t1_obj_t1_obj_t2.created_at,", + " 'id', t1_obj_t1_obj_t2.id,", + " 'name', t1_obj_t1_obj_t2.name,", + " 'type', t1_obj_t1_obj_t2.type)", + " FROM agreego.relationship t1_obj_t1_obj_t1", + " JOIN agreego.entity t1_obj_t1_obj_t2 ON t1_obj_t1_obj_t2.id = t1_obj_t1_obj_t1.id", + " WHERE NOT t1_obj_t1_obj_t1.archived))", + " ELSE NULL END)", + "FROM agreego.entity t1_obj_t1", + "WHERE NOT t1_obj_t1.archived)" + ] + ] + } } ] } diff --git a/src/queryer/compiler.rs b/src/queryer/compiler.rs index 73b8342..8649f84 100644 --- a/src/queryer/compiler.rs +++ b/src/queryer/compiler.rs @@ -113,9 +113,9 @@ impl SqlCompiler { // Determine if this schema represents a Database Entity let mut resolved_type = None; - // Target is generally a specific schema (e.g. 'base.person'), but it tells us what physical - // database table hierarchy it maps to via the `schema.id` prefix/suffix convention. - if let Some(lookup_key) = schema.obj.id.as_ref().or(schema.obj.r#ref.as_ref()) { + if let Some(family_target) = schema.obj.family.as_ref() { + resolved_type = self.db.types.get(family_target); + } else if let Some(lookup_key) = schema.obj.id.as_ref().or(schema.obj.r#ref.as_ref()) { let base_type_name = lookup_key.split('.').next_back().unwrap_or("").to_string(); resolved_type = self.db.types.get(&base_type_name); } @@ -150,6 +150,45 @@ impl SqlCompiler { } return Err(format!("Unresolved $ref: {}", ref_id)); } + // Handle $family Polymorphism fallbacks for relations + if let Some(family_target) = &schema.obj.family { + let mut all_targets = vec![family_target.clone()]; + if let Some(schema_id) = &schema.obj.id { + if let Some(descendants) = self.db.descendants.get(schema_id) { + all_targets.extend(descendants.clone()); + } + } + + let mut family_schemas = Vec::new(); + for target in all_targets { + let mut ref_schema = crate::database::schema::Schema::default(); + ref_schema.obj.r#ref = Some(target); + family_schemas.push(std::sync::Arc::new(ref_schema)); + } + + return self.compile_one_of( + &family_schemas, + parent_alias, + prop_name_context, + filter_keys, + is_stem_query, + depth, + current_path, + ); + } + + // Handle oneOf Polymorphism fallbacks for relations + if let Some(one_of) = &schema.obj.one_of { + return self.compile_one_of( + one_of, + parent_alias, + prop_name_context, + filter_keys, + is_stem_query, + depth, + current_path, + ); + } // Just an inline object definition? if let Some(props) = &schema.obj.properties { @@ -215,7 +254,7 @@ impl SqlCompiler { let (table_aliases, from_clauses) = self.build_hierarchy_from_clauses(type_def, &local_ctx); // 2. Map properties and build jsonb_build_object args - let select_args = self.map_properties_to_aliases( + let mut select_args = self.map_properties_to_aliases( schema, type_def, &table_aliases, @@ -226,6 +265,40 @@ impl SqlCompiler { ¤t_path, )?; + // 2.5 Inject polymorphism directly into the query object + if let Some(family_target) = &schema.obj.family { + let mut family_schemas = Vec::new(); + if let Some(base_type) = self.db.types.get(family_target) { + let mut sorted_targets: Vec = base_type.variations.iter().cloned().collect(); + // Ensure the base type is included if not listed in variations by default + if !sorted_targets.contains(family_target) { + sorted_targets.push(family_target.clone()); + } + sorted_targets.sort(); + + for target in sorted_targets { + let mut ref_schema = crate::database::schema::Schema::default(); + ref_schema.obj.r#ref = Some(target); + family_schemas.push(std::sync::Arc::new(ref_schema)); + } + } else { + // Fallback for types not strictly defined in physical DB + let mut ref_schema = crate::database::schema::Schema::default(); + ref_schema.obj.r#ref = Some(family_target.clone()); + family_schemas.push(std::sync::Arc::new(ref_schema)); + } + + let base_alias = table_aliases.get(&type_def.name).cloned().unwrap_or_else(|| parent_alias.to_string()); + select_args.push(format!("'id', {}.id", base_alias)); + let (case_sql, _) = self.compile_one_of(&family_schemas, &base_alias, None, filter_keys, is_stem_query, depth, current_path.clone())?; + select_args.push(format!("'type', {}", case_sql)); + } else if let Some(one_of) = &schema.obj.one_of { + let base_alias = table_aliases.get(&type_def.name).cloned().unwrap_or_else(|| parent_alias.to_string()); + select_args.push(format!("'id', {}.id", base_alias)); + let (case_sql, _) = self.compile_one_of(one_of, &base_alias, None, filter_keys, is_stem_query, depth, current_path.clone())?; + select_args.push(format!("'type', {}", case_sql)); + } + let jsonb_obj_sql = if select_args.is_empty() { "jsonb_build_object()".to_string() } else { @@ -326,6 +399,26 @@ impl SqlCompiler { } } + let is_object_or_array = match &prop_schema.obj.type_ { + Some(crate::database::schema::SchemaTypeOrArray::Single(s)) => s == "object" || s == "array", + Some(crate::database::schema::SchemaTypeOrArray::Multiple(v)) => v.contains(&"object".to_string()) || v.contains(&"array".to_string()), + _ => false + }; + + let is_primitive = prop_schema.obj.r#ref.is_none() + && prop_schema.obj.items.is_none() + && prop_schema.obj.properties.is_none() + && prop_schema.obj.one_of.is_none() + && !is_object_or_array; + + if is_primitive { + if let Some(ft) = type_def.field_types.as_ref().and_then(|v| v.as_object()) { + if !ft.contains_key(prop_key) { + continue; // Skip frontend virtual properties (e.g. `computer` fields, `created`) missing from physical table fields + } + } + } + let next_path = if current_path.is_empty() { prop_key.clone() } else { @@ -497,8 +590,12 @@ impl SqlCompiler { } } - if let Some(_prop) = prop_name { - where_clauses.push(format!("{}.parent_id = {}.id", base_alias, parent_alias)); + if let Some(prop) = prop_name { + if prop == "target" || prop == "source" { + where_clauses.push(format!("{}.id = {}.{}_id", base_alias, parent_alias, prop)); + } else { + where_clauses.push(format!("{}.parent_id = {}.id", base_alias, parent_alias)); + } } Ok(where_clauses) @@ -538,4 +635,56 @@ impl SqlCompiler { let combined = format!("jsonb_build_object({})", build_args.join(", ")); Ok((combined, "object".to_string())) } + + fn compile_one_of( + &self, + schemas: &[Arc], + parent_alias: &str, + prop_name_context: Option<&str>, + filter_keys: &[String], + is_stem_query: bool, + depth: usize, + current_path: String, + ) -> Result<(String, String), String> { + let mut case_statements = Vec::new(); + let type_col = if let Some(prop) = prop_name_context { + format!("{}_type", prop) + } else { + "type".to_string() + }; + + for option_schema in schemas { + if let Some(ref_id) = &option_schema.obj.r#ref { + // Find the physical type this ref maps to + let base_type_name = ref_id.split('.').next_back().unwrap_or("").to_string(); + + // Generate the nested SQL for this specific target type + let (val_sql, _) = self.walk_schema( + option_schema, + parent_alias, + prop_name_context, + filter_keys, + is_stem_query, + depth, + current_path.clone(), + )?; + + case_statements.push(format!( + "WHEN {}.{} = '{}' THEN ({})", + parent_alias, type_col, base_type_name, val_sql + )); + } + } + + if case_statements.is_empty() { + return Ok(("NULL".to_string(), "string".to_string())); + } + + let sql = format!( + "CASE {} ELSE NULL END", + case_statements.join(" ") + ); + + Ok((sql, "object".to_string())) + } } diff --git a/src/tests/fixtures.rs b/src/tests/fixtures.rs index 90b4628..6ea4ec8 100644 --- a/src/tests/fixtures.rs +++ b/src/tests/fixtures.rs @@ -1469,6 +1469,12 @@ fn test_queryer_0_9() { crate::tests::runner::run_test_case(&path, 0, 9).unwrap(); } +#[test] +fn test_queryer_0_10() { + let path = format!("{}/fixtures/queryer.json", env!("CARGO_MANIFEST_DIR")); + crate::tests::runner::run_test_case(&path, 0, 10).unwrap(); +} + #[test] fn test_not_0_0() { let path = format!("{}/fixtures/not.json", env!("CARGO_MANIFEST_DIR"));