diff --git a/snapshot-tests/anorm-sql/CompositeIdsTest/query2.sql b/snapshot-tests/anorm-sql/CompositeIdsTest/query2.sql new file mode 100644 index 000000000..96641b183 --- /dev/null +++ b/snapshot-tests/anorm-sql/CompositeIdsTest/query2.sql @@ -0,0 +1,5 @@ +with +emailaddress0 as ( + (select emailaddress0 from person.emailaddress emailaddress0 where ((emailaddress0).businessentityid, (emailaddress0).emailaddressid) in (select unnest(?::int4[]), unnest(?::int4[]))) +) +select (emailaddress0)."businessentityid",(emailaddress0)."emailaddressid",(emailaddress0)."emailaddress",(emailaddress0)."rowguid",(emailaddress0)."modifieddate"::text from emailaddress0 \ No newline at end of file diff --git a/snapshot-tests/anorm-sql/DSLTest/doubled.sql b/snapshot-tests/anorm-sql/DSLTest/doubled.sql new file mode 100644 index 000000000..17dff5eb9 --- /dev/null +++ b/snapshot-tests/anorm-sql/DSLTest/doubled.sql @@ -0,0 +1,115 @@ +with +salesperson0 as ( + (select salesperson0 from sales.salesperson salesperson0 where ((salesperson0).rowguid = ?::uuid)) +), +employee0 as ( + (select employee0 from humanresources.employee employee0 ) +), +join_cte5 as ( + select salesperson0, employee0 + from salesperson0 + join employee0 + on ((salesperson0).businessentityid = (employee0).businessentityid) + +), +person0 as ( + (select person0 from person.person person0 ) +), +join_cte4 as ( + select salesperson0, employee0, person0 + from join_cte5 + join person0 + on ((employee0).businessentityid = (person0).businessentityid) + +), +businessentity0 as ( + (select businessentity0 from person.businessentity businessentity0 ) +), +join_cte3 as ( + select salesperson0, employee0, person0, businessentity0 + from join_cte4 + join businessentity0 + on ((person0).businessentityid = (businessentity0).businessentityid) + +), +emailaddress0 as ( + (select emailaddress0 from person.emailaddress emailaddress0 order by (emailaddress0).rowguid ASC ) +), +join_cte2 as ( + select salesperson0, employee0, person0, businessentity0, emailaddress0 + from join_cte3 + join emailaddress0 + on ((emailaddress0).businessentityid = (businessentity0).businessentityid) + +), +salesperson1 as ( + (select salesperson1 from sales.salesperson salesperson1 ) +), +join_cte1 as ( + select salesperson0, employee0, person0, businessentity0, emailaddress0, salesperson1 + from join_cte2 + join salesperson1 + on ((emailaddress0).businessentityid = (salesperson1).businessentityid) + +), +salesperson2 as ( + (select salesperson2 from sales.salesperson salesperson2 where ((salesperson2).rowguid = ?::uuid)) +), +employee1 as ( + (select employee1 from humanresources.employee employee1 ) +), +join_cte10 as ( + select salesperson2, employee1 + from salesperson2 + join employee1 + on ((salesperson2).businessentityid = (employee1).businessentityid) + +), +person1 as ( + (select person1 from person.person person1 ) +), +join_cte9 as ( + select salesperson2, employee1, person1 + from join_cte10 + join person1 + on ((employee1).businessentityid = (person1).businessentityid) + +), +businessentity1 as ( + (select businessentity1 from person.businessentity businessentity1 ) +), +join_cte8 as ( + select salesperson2, employee1, person1, businessentity1 + from join_cte9 + join businessentity1 + on ((person1).businessentityid = (businessentity1).businessentityid) + +), +emailaddress1 as ( + (select emailaddress1 from person.emailaddress emailaddress1 order by (emailaddress1).rowguid ASC ) +), +join_cte7 as ( + select salesperson2, employee1, person1, businessentity1, emailaddress1 + from join_cte8 + join emailaddress1 + on ((emailaddress1).businessentityid = (businessentity1).businessentityid) + +), +salesperson3 as ( + (select salesperson3 from sales.salesperson salesperson3 ) +), +join_cte6 as ( + select salesperson2, employee1, person1, businessentity1, emailaddress1, salesperson3 + from join_cte7 + join salesperson3 + on ((emailaddress1).businessentityid = (salesperson3).businessentityid) + +), +join_cte0 as ( + select salesperson0, employee0, person0, businessentity0, emailaddress0, salesperson1, salesperson2, employee1, person1, businessentity1, emailaddress1, salesperson3 + from join_cte1 + join join_cte6 + on ((emailaddress0).businessentityid = (emailaddress1).businessentityid) + +) +select (salesperson0)."businessentityid",(salesperson0)."territoryid",(salesperson0)."salesquota",(salesperson0)."bonus",(salesperson0)."commissionpct",(salesperson0)."salesytd",(salesperson0)."saleslastyear",(salesperson0)."rowguid",(salesperson0)."modifieddate"::text,(employee0)."businessentityid",(employee0)."nationalidnumber",(employee0)."loginid",(employee0)."jobtitle",(employee0)."birthdate"::text,(employee0)."maritalstatus",(employee0)."gender",(employee0)."hiredate"::text,(employee0)."salariedflag",(employee0)."vacationhours",(employee0)."sickleavehours",(employee0)."currentflag",(employee0)."rowguid",(employee0)."modifieddate"::text,(employee0)."organizationnode",(person0)."businessentityid",(person0)."persontype",(person0)."namestyle",(person0)."title",(person0)."firstname",(person0)."middlename",(person0)."lastname",(person0)."suffix",(person0)."emailpromotion",(person0)."additionalcontactinfo",(person0)."demographics",(person0)."rowguid",(person0)."modifieddate"::text,(businessentity0)."businessentityid",(businessentity0)."rowguid",(businessentity0)."modifieddate"::text,(emailaddress0)."businessentityid",(emailaddress0)."emailaddressid",(emailaddress0)."emailaddress",(emailaddress0)."rowguid",(emailaddress0)."modifieddate"::text,(salesperson1)."businessentityid",(salesperson1)."territoryid",(salesperson1)."salesquota",(salesperson1)."bonus",(salesperson1)."commissionpct",(salesperson1)."salesytd",(salesperson1)."saleslastyear",(salesperson1)."rowguid",(salesperson1)."modifieddate"::text,(salesperson2)."businessentityid",(salesperson2)."territoryid",(salesperson2)."salesquota",(salesperson2)."bonus",(salesperson2)."commissionpct",(salesperson2)."salesytd",(salesperson2)."saleslastyear",(salesperson2)."rowguid",(salesperson2)."modifieddate"::text,(employee1)."businessentityid",(employee1)."nationalidnumber",(employee1)."loginid",(employee1)."jobtitle",(employee1)."birthdate"::text,(employee1)."maritalstatus",(employee1)."gender",(employee1)."hiredate"::text,(employee1)."salariedflag",(employee1)."vacationhours",(employee1)."sickleavehours",(employee1)."currentflag",(employee1)."rowguid",(employee1)."modifieddate"::text,(employee1)."organizationnode",(person1)."businessentityid",(person1)."persontype",(person1)."namestyle",(person1)."title",(person1)."firstname",(person1)."middlename",(person1)."lastname",(person1)."suffix",(person1)."emailpromotion",(person1)."additionalcontactinfo",(person1)."demographics",(person1)."rowguid",(person1)."modifieddate"::text,(businessentity1)."businessentityid",(businessentity1)."rowguid",(businessentity1)."modifieddate"::text,(emailaddress1)."businessentityid",(emailaddress1)."emailaddressid",(emailaddress1)."emailaddress",(emailaddress1)."rowguid",(emailaddress1)."modifieddate"::text,(salesperson3)."businessentityid",(salesperson3)."territoryid",(salesperson3)."salesquota",(salesperson3)."bonus",(salesperson3)."commissionpct",(salesperson3)."salesytd",(salesperson3)."saleslastyear",(salesperson3)."rowguid",(salesperson3)."modifieddate"::text from join_cte0 \ No newline at end of file diff --git a/snapshot-tests/anorm-sql/ProductTest/delete.sql b/snapshot-tests/anorm-sql/ProductTest/delete.sql new file mode 100644 index 000000000..f06950e1f --- /dev/null +++ b/snapshot-tests/anorm-sql/ProductTest/delete.sql @@ -0,0 +1 @@ +delete from production.product where coalesce((productid = ?::INTEGER), ?::BOOLEAN) \ No newline at end of file diff --git a/snapshot-tests/anorm-sql/ProductTest/leftJoined.sql b/snapshot-tests/anorm-sql/ProductTest/leftJoined.sql new file mode 100644 index 000000000..52682cd64 --- /dev/null +++ b/snapshot-tests/anorm-sql/ProductTest/leftJoined.sql @@ -0,0 +1,15 @@ +with +product0 as ( + (select product0 from production.product product0 ) +), +productmodel0 as ( + (select productmodel0 from production.productmodel productmodel0 ) +), +left_join_cte0 as ( + select product0, productmodel0 + from product0 + left join productmodel0 + on ((product0).productmodelid = (productmodel0).productmodelid) + +) +select (product0)."productid",(product0)."name",(product0)."productnumber",(product0)."makeflag",(product0)."finishedgoodsflag",(product0)."color",(product0)."safetystocklevel",(product0)."reorderpoint",(product0)."standardcost",(product0)."listprice",(product0)."size",(product0)."sizeunitmeasurecode",(product0)."weightunitmeasurecode",(product0)."weight",(product0)."daystomanufacture",(product0)."productline",(product0)."class",(product0)."style",(product0)."productsubcategoryid",(product0)."productmodelid",(product0)."sellstartdate"::text,(product0)."sellenddate"::text,(product0)."discontinueddate"::text,(product0)."rowguid",(product0)."modifieddate"::text,(productmodel0)."productmodelid",(productmodel0)."name",(productmodel0)."catalogdescription",(productmodel0)."instructions",(productmodel0)."rowguid",(productmodel0)."modifieddate"::text from left_join_cte0 \ No newline at end of file diff --git a/snapshot-tests/anorm-sql/ProductTest/q.sql b/snapshot-tests/anorm-sql/ProductTest/q.sql new file mode 100644 index 000000000..96ff7a6a9 --- /dev/null +++ b/snapshot-tests/anorm-sql/ProductTest/q.sql @@ -0,0 +1,15 @@ +with +product0 as ( + (select product0 from production.product product0 where (((NOT ((product0).name LIKE ?::VARCHAR) AND NOT (((product0).name || (product0).color) LIKE ?::VARCHAR)) AND ((product0).daystomanufacture > ?::INTEGER)) AND ((product0).modifieddate < ?::timestamp))) +), +productmodel0 as ( + (select productmodel0 from production.productmodel productmodel0 where ((productmodel0).modifieddate < ?::timestamp)) +), +join_cte0 as ( + select product0, productmodel0 + from product0 + join productmodel0 + on ((product0).productmodelid = (productmodel0).productmodelid) + where NOT (productmodel0).instructions IS NULL +) +select (product0)."productid",(product0)."name",(product0)."productnumber",(product0)."makeflag",(product0)."finishedgoodsflag",(product0)."color",(product0)."safetystocklevel",(product0)."reorderpoint",(product0)."standardcost",(product0)."listprice",(product0)."size",(product0)."sizeunitmeasurecode",(product0)."weightunitmeasurecode",(product0)."weight",(product0)."daystomanufacture",(product0)."productline",(product0)."class",(product0)."style",(product0)."productsubcategoryid",(product0)."productmodelid",(product0)."sellstartdate"::text,(product0)."sellenddate"::text,(product0)."discontinueddate"::text,(product0)."rowguid",(product0)."modifieddate"::text,(productmodel0)."productmodelid",(productmodel0)."name",(productmodel0)."catalogdescription",(productmodel0)."instructions",(productmodel0)."rowguid",(productmodel0)."modifieddate"::text from join_cte0 \ No newline at end of file diff --git a/snapshot-tests/anorm-sql/ProductTest/q2.sql b/snapshot-tests/anorm-sql/ProductTest/q2.sql new file mode 100644 index 000000000..6951d3d2f --- /dev/null +++ b/snapshot-tests/anorm-sql/ProductTest/q2.sql @@ -0,0 +1,25 @@ +with +product0 as ( + (select product0 from production.product product0 where (((((product0).productid = ANY(?) AND (length((product0).name) > ?::INTEGER)) AND NOT (((product0).name || (product0).color) LIKE ?::VARCHAR)) AND (coalesce((product0).color, ?::VARCHAR) != ?::VARCHAR)) AND ((product0).modifieddate < ?::timestamp))) +), +productmodel0 as ( + (select productmodel0 from production.productmodel productmodel0 where (length((productmodel0).name) > ?::INTEGER)) +), +join_cte0 as ( + select product0, productmodel0 + from product0 + join productmodel0 + on ((product0).productmodelid = (productmodel0).productmodelid) + where ((productmodel0).name != ?::VARCHAR) +), +productmodel1 as ( + (select productmodel1 from production.productmodel productmodel1 where (length((productmodel1).name) > ?::INTEGER)) +), +left_join_cte0 as ( + select product0, productmodel0, productmodel1 + from join_cte0 + left join productmodel1 + on (((product0).productmodelid = (productmodel1).productmodelid) AND ?::BOOLEAN) + order by (productmodel1).name ASC , (product0).color DESC NULLS FIRST +) +select (product0)."productid",(product0)."name",(product0)."productnumber",(product0)."makeflag",(product0)."finishedgoodsflag",(product0)."color",(product0)."safetystocklevel",(product0)."reorderpoint",(product0)."standardcost",(product0)."listprice",(product0)."size",(product0)."sizeunitmeasurecode",(product0)."weightunitmeasurecode",(product0)."weight",(product0)."daystomanufacture",(product0)."productline",(product0)."class",(product0)."style",(product0)."productsubcategoryid",(product0)."productmodelid",(product0)."sellstartdate"::text,(product0)."sellenddate"::text,(product0)."discontinueddate"::text,(product0)."rowguid",(product0)."modifieddate"::text,(productmodel0)."productmodelid",(productmodel0)."name",(productmodel0)."catalogdescription",(productmodel0)."instructions",(productmodel0)."rowguid",(productmodel0)."modifieddate"::text,(productmodel1)."productmodelid",(productmodel1)."name",(productmodel1)."catalogdescription",(productmodel1)."instructions",(productmodel1)."rowguid",(productmodel1)."modifieddate"::text from left_join_cte0 \ No newline at end of file diff --git a/snapshot-tests/anorm-sql/ProductTest/query.sql b/snapshot-tests/anorm-sql/ProductTest/query.sql new file mode 100644 index 000000000..558f8350b --- /dev/null +++ b/snapshot-tests/anorm-sql/ProductTest/query.sql @@ -0,0 +1,25 @@ +with +product0 as ( + (select product0 from production.product product0 where ((((product0).class = ?::VARCHAR) AND (((product0).daystomanufacture > ?::INTEGER) OR ((product0).daystomanufacture <= ?::INTEGER))) AND ((product0).productline = ?::VARCHAR))) +), +unitmeasure0 as ( + (select unitmeasure0 from production.unitmeasure unitmeasure0 where ((unitmeasure0).name LIKE ?::VARCHAR)) +), +join_cte0 as ( + select product0, unitmeasure0 + from product0 + join unitmeasure0 + on ((product0).sizeunitmeasurecode = (unitmeasure0).unitmeasurecode) + +), +productmodel0 as ( + (select productmodel0 from production.productmodel productmodel0 ) +), +left_join_cte0 as ( + select product0, unitmeasure0, productmodel0 + from join_cte0 + left join productmodel0 + on ((product0).productmodelid = (productmodel0).productmodelid) + where ((product0).productmodelid = (productmodel0).productmodelid) order by (product0).productmodelid ASC , (productmodel0).name DESC NULLS FIRST +) +select (product0)."productid",(product0)."name",(product0)."productnumber",(product0)."makeflag",(product0)."finishedgoodsflag",(product0)."color",(product0)."safetystocklevel",(product0)."reorderpoint",(product0)."standardcost",(product0)."listprice",(product0)."size",(product0)."sizeunitmeasurecode",(product0)."weightunitmeasurecode",(product0)."weight",(product0)."daystomanufacture",(product0)."productline",(product0)."class",(product0)."style",(product0)."productsubcategoryid",(product0)."productmodelid",(product0)."sellstartdate"::text,(product0)."sellenddate"::text,(product0)."discontinueddate"::text,(product0)."rowguid",(product0)."modifieddate"::text,(unitmeasure0)."unitmeasurecode",(unitmeasure0)."name",(unitmeasure0)."modifieddate"::text,(productmodel0)."productmodelid",(productmodel0)."name",(productmodel0)."catalogdescription",(productmodel0)."instructions",(productmodel0)."rowguid",(productmodel0)."modifieddate"::text from left_join_cte0 \ No newline at end of file diff --git a/snapshot-tests/anorm-sql/ProductTest/query0.sql b/snapshot-tests/anorm-sql/ProductTest/query0.sql new file mode 100644 index 000000000..4a3bf1a66 --- /dev/null +++ b/snapshot-tests/anorm-sql/ProductTest/query0.sql @@ -0,0 +1,35 @@ +with +product0 as ( + (select product0 from production.product product0 ) +), +productmodel0 as ( + (select productmodel0 from production.productmodel productmodel0 ) +), +join_cte2 as ( + select product0, productmodel0 + from product0 + join productmodel0 + on ((product0).productmodelid = (productmodel0).productmodelid) + +), +productsubcategory0 as ( + (select productsubcategory0 from production.productsubcategory productsubcategory0 ) +), +join_cte1 as ( + select product0, productmodel0, productsubcategory0 + from join_cte2 + join productsubcategory0 + on ((product0).productsubcategoryid = (productsubcategory0).productsubcategoryid) + +), +productcategory0 as ( + (select productcategory0 from production.productcategory productcategory0 ) +), +join_cte0 as ( + select product0, productmodel0, productsubcategory0, productcategory0 + from join_cte1 + join productcategory0 + on ((productsubcategory0).productcategoryid = (productcategory0).productcategoryid) + +) +select (product0)."productid",(product0)."name",(product0)."productnumber",(product0)."makeflag",(product0)."finishedgoodsflag",(product0)."color",(product0)."safetystocklevel",(product0)."reorderpoint",(product0)."standardcost",(product0)."listprice",(product0)."size",(product0)."sizeunitmeasurecode",(product0)."weightunitmeasurecode",(product0)."weight",(product0)."daystomanufacture",(product0)."productline",(product0)."class",(product0)."style",(product0)."productsubcategoryid",(product0)."productmodelid",(product0)."sellstartdate"::text,(product0)."sellenddate"::text,(product0)."discontinueddate"::text,(product0)."rowguid",(product0)."modifieddate"::text,(productmodel0)."productmodelid",(productmodel0)."name",(productmodel0)."catalogdescription",(productmodel0)."instructions",(productmodel0)."rowguid",(productmodel0)."modifieddate"::text,(productsubcategory0)."productsubcategoryid",(productsubcategory0)."productcategoryid",(productsubcategory0)."name",(productsubcategory0)."rowguid",(productsubcategory0)."modifieddate"::text,(productcategory0)."productcategoryid",(productcategory0)."name",(productcategory0)."rowguid",(productcategory0)."modifieddate"::text from join_cte0 \ No newline at end of file diff --git a/snapshot-tests/anorm-sql/ProductTest/updateReturning.sql b/snapshot-tests/anorm-sql/ProductTest/updateReturning.sql new file mode 100644 index 000000000..5265ee020 --- /dev/null +++ b/snapshot-tests/anorm-sql/ProductTest/updateReturning.sql @@ -0,0 +1 @@ +update production.product set name = substring((upper(reverse(name)) || ?::"public"."Name"), ?::INTEGER, ?::INTEGER)::varchar, listprice = ?::DECIMAL::numeric, reorderpoint = (reorderpoint + ?::int2)::int2, sizeunitmeasurecode = ?::VARCHAR::bpchar, sellstartdate = ?::timestamp::timestamp where coalesce((productid = ?::INTEGER), ?::BOOLEAN) returning "productid","name","productnumber","makeflag","finishedgoodsflag","color","safetystocklevel","reorderpoint","standardcost","listprice","size","sizeunitmeasurecode","weightunitmeasurecode","weight","daystomanufacture","productline","class","style","productsubcategoryid","productmodelid","sellstartdate"::text,"sellenddate"::text,"discontinueddate"::text,"rowguid","modifieddate"::text \ No newline at end of file diff --git a/snapshot-tests/anorm-sql/SeekTest/complex.sql b/snapshot-tests/anorm-sql/SeekTest/complex.sql new file mode 100644 index 000000000..58454573a --- /dev/null +++ b/snapshot-tests/anorm-sql/SeekTest/complex.sql @@ -0,0 +1,5 @@ +with +product0 as ( + (select product0 from production.product product0 where ((((product0).name > ?::"public"."Name") OR (((product0).name = ?::"public"."Name") AND ((product0).weight < ?::DECIMAL))) OR ((((product0).name = ?::"public"."Name") AND ((product0).weight = ?::DECIMAL)) AND ((product0).listprice < ?::DECIMAL))) order by (product0).name ASC , (product0).weight DESC , (product0).listprice DESC ) +) +select (product0)."productid",(product0)."name",(product0)."productnumber",(product0)."makeflag",(product0)."finishedgoodsflag",(product0)."color",(product0)."safetystocklevel",(product0)."reorderpoint",(product0)."standardcost",(product0)."listprice",(product0)."size",(product0)."sizeunitmeasurecode",(product0)."weightunitmeasurecode",(product0)."weight",(product0)."daystomanufacture",(product0)."productline",(product0)."class",(product0)."style",(product0)."productsubcategoryid",(product0)."productmodelid",(product0)."sellstartdate"::text,(product0)."sellenddate"::text,(product0)."discontinueddate"::text,(product0)."rowguid",(product0)."modifieddate"::text from product0 \ No newline at end of file diff --git a/snapshot-tests/anorm-sql/SeekTest/uniform-ascending.sql b/snapshot-tests/anorm-sql/SeekTest/uniform-ascending.sql new file mode 100644 index 000000000..ba1886c42 --- /dev/null +++ b/snapshot-tests/anorm-sql/SeekTest/uniform-ascending.sql @@ -0,0 +1,5 @@ +with +product0 as ( + (select product0 from production.product product0 where (((product0).name,(product0).weight,(product0).listprice) > (?::"public"."Name",?::DECIMAL,?::DECIMAL)) order by (product0).name ASC , (product0).weight ASC , (product0).listprice ASC ) +) +select (product0)."productid",(product0)."name",(product0)."productnumber",(product0)."makeflag",(product0)."finishedgoodsflag",(product0)."color",(product0)."safetystocklevel",(product0)."reorderpoint",(product0)."standardcost",(product0)."listprice",(product0)."size",(product0)."sizeunitmeasurecode",(product0)."weightunitmeasurecode",(product0)."weight",(product0)."daystomanufacture",(product0)."productline",(product0)."class",(product0)."style",(product0)."productsubcategoryid",(product0)."productmodelid",(product0)."sellstartdate"::text,(product0)."sellenddate"::text,(product0)."discontinueddate"::text,(product0)."rowguid",(product0)."modifieddate"::text from product0 \ No newline at end of file diff --git a/snapshot-tests/anorm-sql/SeekTest/uniform-descending.sql b/snapshot-tests/anorm-sql/SeekTest/uniform-descending.sql new file mode 100644 index 000000000..0cfadff5a --- /dev/null +++ b/snapshot-tests/anorm-sql/SeekTest/uniform-descending.sql @@ -0,0 +1,5 @@ +with +product0 as ( + (select product0 from production.product product0 where (((product0).name,(product0).weight,(product0).listprice) < (?::"public"."Name",?::DECIMAL,?::DECIMAL)) order by (product0).name DESC , (product0).weight DESC , (product0).listprice DESC ) +) +select (product0)."productid",(product0)."name",(product0)."productnumber",(product0)."makeflag",(product0)."finishedgoodsflag",(product0)."color",(product0)."safetystocklevel",(product0)."reorderpoint",(product0)."standardcost",(product0)."listprice",(product0)."size",(product0)."sizeunitmeasurecode",(product0)."weightunitmeasurecode",(product0)."weight",(product0)."daystomanufacture",(product0)."productline",(product0)."class",(product0)."style",(product0)."productsubcategoryid",(product0)."productmodelid",(product0)."sellstartdate"::text,(product0)."sellenddate"::text,(product0)."discontinueddate"::text,(product0)."rowguid",(product0)."modifieddate"::text from product0 \ No newline at end of file diff --git a/snapshot-tests/dooble-sql/CompositeIdsTest/query2.sql b/snapshot-tests/dooble-sql/CompositeIdsTest/query2.sql new file mode 100644 index 000000000..3a2dd13e2 --- /dev/null +++ b/snapshot-tests/dooble-sql/CompositeIdsTest/query2.sql @@ -0,0 +1,5 @@ +with +emailaddress0 as ( + (select emailaddress0 from person.emailaddress emailaddress0 WHERE ((emailaddress0).businessentityid , (emailaddress0).emailaddressid ) in (select unnest(?::_int4 ), unnest(?::_int4 ))) +) +select (emailaddress0)."businessentityid",(emailaddress0)."emailaddressid",(emailaddress0)."emailaddress",(emailaddress0)."rowguid",(emailaddress0)."modifieddate"::text from emailaddress0 \ No newline at end of file diff --git a/snapshot-tests/dooble-sql/DSLTest/doubled.sql b/snapshot-tests/dooble-sql/DSLTest/doubled.sql new file mode 100644 index 000000000..2b87b4697 --- /dev/null +++ b/snapshot-tests/dooble-sql/DSLTest/doubled.sql @@ -0,0 +1,115 @@ +with +salesperson0 as ( + (select salesperson0 from sales.salesperson salesperson0 WHERE ((salesperson0).rowguid = ?::uuid ) ) +) , +employee0 as ( + (select employee0 from humanresources.employee employee0 ) +) , +join_cte5 as ( + select salesperson0, employee0 + from salesperson0 + join employee0 + on ((salesperson0).businessentityid = (employee0).businessentityid) + +) , +person0 as ( + (select person0 from person.person person0 ) +) , +join_cte4 as ( + select salesperson0, employee0, person0 + from join_cte5 + join person0 + on ((employee0).businessentityid = (person0).businessentityid) + +) , +businessentity0 as ( + (select businessentity0 from person.businessentity businessentity0 ) +) , +join_cte3 as ( + select salesperson0, employee0, person0, businessentity0 + from join_cte4 + join businessentity0 + on ((person0).businessentityid = (businessentity0).businessentityid) + +) , +emailaddress0 as ( + (select emailaddress0 from person.emailaddress emailaddress0 ORDER BY (emailaddress0).rowguid ASC ) +) , +join_cte2 as ( + select salesperson0, employee0, person0, businessentity0, emailaddress0 + from join_cte3 + join emailaddress0 + on ((emailaddress0).businessentityid = (businessentity0).businessentityid) + +) , +salesperson1 as ( + (select salesperson1 from sales.salesperson salesperson1 ) +) , +join_cte1 as ( + select salesperson0, employee0, person0, businessentity0, emailaddress0, salesperson1 + from join_cte2 + join salesperson1 + on ((emailaddress0).businessentityid = (salesperson1).businessentityid) + +) , +salesperson2 as ( + (select salesperson2 from sales.salesperson salesperson2 WHERE ((salesperson2).rowguid = ?::uuid ) ) +) , +employee1 as ( + (select employee1 from humanresources.employee employee1 ) +) , +join_cte10 as ( + select salesperson2, employee1 + from salesperson2 + join employee1 + on ((salesperson2).businessentityid = (employee1).businessentityid) + +) , +person1 as ( + (select person1 from person.person person1 ) +) , +join_cte9 as ( + select salesperson2, employee1, person1 + from join_cte10 + join person1 + on ((employee1).businessentityid = (person1).businessentityid) + +) , +businessentity1 as ( + (select businessentity1 from person.businessentity businessentity1 ) +) , +join_cte8 as ( + select salesperson2, employee1, person1, businessentity1 + from join_cte9 + join businessentity1 + on ((person1).businessentityid = (businessentity1).businessentityid) + +) , +emailaddress1 as ( + (select emailaddress1 from person.emailaddress emailaddress1 ORDER BY (emailaddress1).rowguid ASC ) +) , +join_cte7 as ( + select salesperson2, employee1, person1, businessentity1, emailaddress1 + from join_cte8 + join emailaddress1 + on ((emailaddress1).businessentityid = (businessentity1).businessentityid) + +) , +salesperson3 as ( + (select salesperson3 from sales.salesperson salesperson3 ) +) , +join_cte6 as ( + select salesperson2, employee1, person1, businessentity1, emailaddress1, salesperson3 + from join_cte7 + join salesperson3 + on ((emailaddress1).businessentityid = (salesperson3).businessentityid) + +) , +join_cte0 as ( + select salesperson0, employee0, person0, businessentity0, emailaddress0, salesperson1, salesperson2, employee1, person1, businessentity1, emailaddress1, salesperson3 + from join_cte1 + join join_cte6 + on ((emailaddress0).businessentityid = (emailaddress1).businessentityid) + +) +select (salesperson0)."businessentityid",(salesperson0)."territoryid",(salesperson0)."salesquota",(salesperson0)."bonus",(salesperson0)."commissionpct",(salesperson0)."salesytd",(salesperson0)."saleslastyear",(salesperson0)."rowguid",(salesperson0)."modifieddate"::text,(employee0)."businessentityid",(employee0)."nationalidnumber",(employee0)."loginid",(employee0)."jobtitle",(employee0)."birthdate"::text,(employee0)."maritalstatus",(employee0)."gender",(employee0)."hiredate"::text,(employee0)."salariedflag",(employee0)."vacationhours",(employee0)."sickleavehours",(employee0)."currentflag",(employee0)."rowguid",(employee0)."modifieddate"::text,(employee0)."organizationnode",(person0)."businessentityid",(person0)."persontype",(person0)."namestyle",(person0)."title",(person0)."firstname",(person0)."middlename",(person0)."lastname",(person0)."suffix",(person0)."emailpromotion",(person0)."additionalcontactinfo",(person0)."demographics",(person0)."rowguid",(person0)."modifieddate"::text,(businessentity0)."businessentityid",(businessentity0)."rowguid",(businessentity0)."modifieddate"::text,(emailaddress0)."businessentityid",(emailaddress0)."emailaddressid",(emailaddress0)."emailaddress",(emailaddress0)."rowguid",(emailaddress0)."modifieddate"::text,(salesperson1)."businessentityid",(salesperson1)."territoryid",(salesperson1)."salesquota",(salesperson1)."bonus",(salesperson1)."commissionpct",(salesperson1)."salesytd",(salesperson1)."saleslastyear",(salesperson1)."rowguid",(salesperson1)."modifieddate"::text,(salesperson2)."businessentityid",(salesperson2)."territoryid",(salesperson2)."salesquota",(salesperson2)."bonus",(salesperson2)."commissionpct",(salesperson2)."salesytd",(salesperson2)."saleslastyear",(salesperson2)."rowguid",(salesperson2)."modifieddate"::text,(employee1)."businessentityid",(employee1)."nationalidnumber",(employee1)."loginid",(employee1)."jobtitle",(employee1)."birthdate"::text,(employee1)."maritalstatus",(employee1)."gender",(employee1)."hiredate"::text,(employee1)."salariedflag",(employee1)."vacationhours",(employee1)."sickleavehours",(employee1)."currentflag",(employee1)."rowguid",(employee1)."modifieddate"::text,(employee1)."organizationnode",(person1)."businessentityid",(person1)."persontype",(person1)."namestyle",(person1)."title",(person1)."firstname",(person1)."middlename",(person1)."lastname",(person1)."suffix",(person1)."emailpromotion",(person1)."additionalcontactinfo",(person1)."demographics",(person1)."rowguid",(person1)."modifieddate"::text,(businessentity1)."businessentityid",(businessentity1)."rowguid",(businessentity1)."modifieddate"::text,(emailaddress1)."businessentityid",(emailaddress1)."emailaddressid",(emailaddress1)."emailaddress",(emailaddress1)."rowguid",(emailaddress1)."modifieddate"::text,(salesperson3)."businessentityid",(salesperson3)."territoryid",(salesperson3)."salesquota",(salesperson3)."bonus",(salesperson3)."commissionpct",(salesperson3)."salesytd",(salesperson3)."saleslastyear",(salesperson3)."rowguid",(salesperson3)."modifieddate"::text from join_cte0 \ No newline at end of file diff --git a/snapshot-tests/dooble-sql/ProductTest/delete.sql b/snapshot-tests/dooble-sql/ProductTest/delete.sql new file mode 100644 index 000000000..0f2b7b948 --- /dev/null +++ b/snapshot-tests/dooble-sql/ProductTest/delete.sql @@ -0,0 +1 @@ +delete from production.product where coalesce((productid = ? ) , ? ) \ No newline at end of file diff --git a/snapshot-tests/dooble-sql/ProductTest/leftJoined.sql b/snapshot-tests/dooble-sql/ProductTest/leftJoined.sql new file mode 100644 index 000000000..90eb93cb9 --- /dev/null +++ b/snapshot-tests/dooble-sql/ProductTest/leftJoined.sql @@ -0,0 +1,15 @@ +with +product0 as ( + (select product0 from production.product product0 ) +) , +productmodel0 as ( + (select productmodel0 from production.productmodel productmodel0 ) +) , +left_join_cte0 as ( + select product0, productmodel0 + from product0 + left join productmodel0 + on ((product0).productmodelid = (productmodel0).productmodelid) + +) +select (product0)."productid",(product0)."name",(product0)."productnumber",(product0)."makeflag",(product0)."finishedgoodsflag",(product0)."color",(product0)."safetystocklevel",(product0)."reorderpoint",(product0)."standardcost",(product0)."listprice",(product0)."size",(product0)."sizeunitmeasurecode",(product0)."weightunitmeasurecode",(product0)."weight",(product0)."daystomanufacture",(product0)."productline",(product0)."class",(product0)."style",(product0)."productsubcategoryid",(product0)."productmodelid",(product0)."sellstartdate"::text,(product0)."sellenddate"::text,(product0)."discontinueddate"::text,(product0)."rowguid",(product0)."modifieddate"::text,(productmodel0)."productmodelid",(productmodel0)."name",(productmodel0)."catalogdescription",(productmodel0)."instructions",(productmodel0)."rowguid",(productmodel0)."modifieddate"::text from left_join_cte0 \ No newline at end of file diff --git a/snapshot-tests/dooble-sql/ProductTest/q.sql b/snapshot-tests/dooble-sql/ProductTest/q.sql new file mode 100644 index 000000000..1f6b30adf --- /dev/null +++ b/snapshot-tests/dooble-sql/ProductTest/q.sql @@ -0,0 +1,15 @@ +with +product0 as ( + (select product0 from production.product product0 WHERE NOT ((product0).name LIKE ? ) AND NOT (((product0).name || (product0).color) LIKE ? ) AND ((product0).daystomanufacture > ? ) AND ((product0).modifieddate < ?::timestamp ) ) +) , +productmodel0 as ( + (select productmodel0 from production.productmodel productmodel0 WHERE ((productmodel0).modifieddate < ?::timestamp ) ) +) , +join_cte0 as ( + select product0, productmodel0 + from product0 + join productmodel0 + on ((product0).productmodelid = (productmodel0).productmodelid) + WHERE NOT (productmodel0).instructions IS NULL +) +select (product0)."productid",(product0)."name",(product0)."productnumber",(product0)."makeflag",(product0)."finishedgoodsflag",(product0)."color",(product0)."safetystocklevel",(product0)."reorderpoint",(product0)."standardcost",(product0)."listprice",(product0)."size",(product0)."sizeunitmeasurecode",(product0)."weightunitmeasurecode",(product0)."weight",(product0)."daystomanufacture",(product0)."productline",(product0)."class",(product0)."style",(product0)."productsubcategoryid",(product0)."productmodelid",(product0)."sellstartdate"::text,(product0)."sellenddate"::text,(product0)."discontinueddate"::text,(product0)."rowguid",(product0)."modifieddate"::text,(productmodel0)."productmodelid",(productmodel0)."name",(productmodel0)."catalogdescription",(productmodel0)."instructions",(productmodel0)."rowguid",(productmodel0)."modifieddate"::text from join_cte0 \ No newline at end of file diff --git a/snapshot-tests/dooble-sql/ProductTest/q2.sql b/snapshot-tests/dooble-sql/ProductTest/q2.sql new file mode 100644 index 000000000..4ef423071 --- /dev/null +++ b/snapshot-tests/dooble-sql/ProductTest/q2.sql @@ -0,0 +1,25 @@ +with +product0 as ( + (select product0 from production.product product0 WHERE (product0).productid = ANY(?) AND (length((product0).name) > ? ) AND NOT (((product0).name || (product0).color) LIKE ? ) AND (coalesce(length((product0).color) , ? ) > ? ) AND ((product0).modifieddate < ?::timestamp ) ) +) , +productmodel0 as ( + (select productmodel0 from production.productmodel productmodel0 WHERE NOT ((productmodel0).name LIKE ? ) ) +) , +join_cte0 as ( + select product0, productmodel0 + from product0 + join productmodel0 + on ((product0).productmodelid = (productmodel0).productmodelid) + WHERE ((productmodel0).productmodelid > ? ) +) , +productmodel1 as ( + (select productmodel1 from production.productmodel productmodel1 WHERE NOT ((productmodel1).name LIKE ? ) ) +) , +left_join_cte0 as ( + select product0, productmodel0, productmodel1 + from join_cte0 + left join productmodel1 + on (((product0).productmodelid = (productmodel1).productmodelid) AND ? ) + ORDER BY (product0).name ASC , (productmodel0).rowguid DESC NULLS FIRST , (productmodel1).rowguid ASC +) +select (product0)."productid",(product0)."name",(product0)."productnumber",(product0)."makeflag",(product0)."finishedgoodsflag",(product0)."color",(product0)."safetystocklevel",(product0)."reorderpoint",(product0)."standardcost",(product0)."listprice",(product0)."size",(product0)."sizeunitmeasurecode",(product0)."weightunitmeasurecode",(product0)."weight",(product0)."daystomanufacture",(product0)."productline",(product0)."class",(product0)."style",(product0)."productsubcategoryid",(product0)."productmodelid",(product0)."sellstartdate"::text,(product0)."sellenddate"::text,(product0)."discontinueddate"::text,(product0)."rowguid",(product0)."modifieddate"::text,(productmodel0)."productmodelid",(productmodel0)."name",(productmodel0)."catalogdescription",(productmodel0)."instructions",(productmodel0)."rowguid",(productmodel0)."modifieddate"::text,(productmodel1)."productmodelid",(productmodel1)."name",(productmodel1)."catalogdescription",(productmodel1)."instructions",(productmodel1)."rowguid",(productmodel1)."modifieddate"::text from left_join_cte0 \ No newline at end of file diff --git a/snapshot-tests/dooble-sql/ProductTest/query.sql b/snapshot-tests/dooble-sql/ProductTest/query.sql new file mode 100644 index 000000000..4a7dc865a --- /dev/null +++ b/snapshot-tests/dooble-sql/ProductTest/query.sql @@ -0,0 +1,25 @@ +with +product0 as ( + (select product0 from production.product product0 WHERE ((product0).class = ? ) AND (((product0).daystomanufacture > ? ) OR ((product0).daystomanufacture <= ? ) ) AND ((product0).productline = ? ) ) +) , +unitmeasure0 as ( + (select unitmeasure0 from production.unitmeasure unitmeasure0 WHERE ((unitmeasure0).name LIKE ? ) ) +) , +join_cte0 as ( + select product0, unitmeasure0 + from product0 + join unitmeasure0 + on ((product0).sizeunitmeasurecode = (unitmeasure0).unitmeasurecode) + +) , +productmodel0 as ( + (select productmodel0 from production.productmodel productmodel0 ) +) , +left_join_cte0 as ( + select product0, unitmeasure0, productmodel0 + from join_cte0 + left join productmodel0 + on ((product0).productmodelid = (productmodel0).productmodelid) + WHERE ((product0).productmodelid = (productmodel0).productmodelid) ORDER BY (product0).productmodelid ASC , (productmodel0).name DESC NULLS FIRST +) +select (product0)."productid",(product0)."name",(product0)."productnumber",(product0)."makeflag",(product0)."finishedgoodsflag",(product0)."color",(product0)."safetystocklevel",(product0)."reorderpoint",(product0)."standardcost",(product0)."listprice",(product0)."size",(product0)."sizeunitmeasurecode",(product0)."weightunitmeasurecode",(product0)."weight",(product0)."daystomanufacture",(product0)."productline",(product0)."class",(product0)."style",(product0)."productsubcategoryid",(product0)."productmodelid",(product0)."sellstartdate"::text,(product0)."sellenddate"::text,(product0)."discontinueddate"::text,(product0)."rowguid",(product0)."modifieddate"::text,(unitmeasure0)."unitmeasurecode",(unitmeasure0)."name",(unitmeasure0)."modifieddate"::text,(productmodel0)."productmodelid",(productmodel0)."name",(productmodel0)."catalogdescription",(productmodel0)."instructions",(productmodel0)."rowguid",(productmodel0)."modifieddate"::text from left_join_cte0 \ No newline at end of file diff --git a/snapshot-tests/dooble-sql/ProductTest/query0.sql b/snapshot-tests/dooble-sql/ProductTest/query0.sql new file mode 100644 index 000000000..3e26c46fb --- /dev/null +++ b/snapshot-tests/dooble-sql/ProductTest/query0.sql @@ -0,0 +1,35 @@ +with +product0 as ( + (select product0 from production.product product0 ) +) , +productmodel0 as ( + (select productmodel0 from production.productmodel productmodel0 ) +) , +join_cte2 as ( + select product0, productmodel0 + from product0 + join productmodel0 + on ((product0).productmodelid = (productmodel0).productmodelid) + +) , +productsubcategory0 as ( + (select productsubcategory0 from production.productsubcategory productsubcategory0 ) +) , +join_cte1 as ( + select product0, productmodel0, productsubcategory0 + from join_cte2 + join productsubcategory0 + on ((product0).productsubcategoryid = (productsubcategory0).productsubcategoryid) + +) , +productcategory0 as ( + (select productcategory0 from production.productcategory productcategory0 ) +) , +join_cte0 as ( + select product0, productmodel0, productsubcategory0, productcategory0 + from join_cte1 + join productcategory0 + on ((productsubcategory0).productcategoryid = (productcategory0).productcategoryid) + +) +select (product0)."productid",(product0)."name",(product0)."productnumber",(product0)."makeflag",(product0)."finishedgoodsflag",(product0)."color",(product0)."safetystocklevel",(product0)."reorderpoint",(product0)."standardcost",(product0)."listprice",(product0)."size",(product0)."sizeunitmeasurecode",(product0)."weightunitmeasurecode",(product0)."weight",(product0)."daystomanufacture",(product0)."productline",(product0)."class",(product0)."style",(product0)."productsubcategoryid",(product0)."productmodelid",(product0)."sellstartdate"::text,(product0)."sellenddate"::text,(product0)."discontinueddate"::text,(product0)."rowguid",(product0)."modifieddate"::text,(productmodel0)."productmodelid",(productmodel0)."name",(productmodel0)."catalogdescription",(productmodel0)."instructions",(productmodel0)."rowguid",(productmodel0)."modifieddate"::text,(productsubcategory0)."productsubcategoryid",(productsubcategory0)."productcategoryid",(productsubcategory0)."name",(productsubcategory0)."rowguid",(productsubcategory0)."modifieddate"::text,(productcategory0)."productcategoryid",(productcategory0)."name",(productcategory0)."rowguid",(productcategory0)."modifieddate"::text from join_cte0 \ No newline at end of file diff --git a/snapshot-tests/dooble-sql/ProductTest/updateReturning.sql b/snapshot-tests/dooble-sql/ProductTest/updateReturning.sql new file mode 100644 index 000000000..aafaf3e98 --- /dev/null +++ b/snapshot-tests/dooble-sql/ProductTest/updateReturning.sql @@ -0,0 +1 @@ +update production.product SET name = substring((upper(reverse(name) ) || ? ) , ? , ? ) ::varchar , listprice = ? ::numeric , reorderpoint = (reorderpoint + ?::int2 ) ::int2 WHERE coalesce((productid = ? ) , ? ) returning "productid", "name", "productnumber", "makeflag", "finishedgoodsflag", "color", "safetystocklevel", "reorderpoint", "standardcost", "listprice", "size", "sizeunitmeasurecode", "weightunitmeasurecode", "weight", "daystomanufacture", "productline", "class", "style", "productsubcategoryid", "productmodelid", "sellstartdate"::text, "sellenddate"::text, "discontinueddate"::text, "rowguid", "modifieddate"::text \ No newline at end of file diff --git a/snapshot-tests/dooble-sql/SeekTest/complex.sql b/snapshot-tests/dooble-sql/SeekTest/complex.sql new file mode 100644 index 000000000..2411321a2 --- /dev/null +++ b/snapshot-tests/dooble-sql/SeekTest/complex.sql @@ -0,0 +1,5 @@ +with +product0 as ( + (select product0 from production.product product0 WHERE ((((product0).name > ? ) OR (((product0).name = ? ) AND ((product0).weight < ? ) ) ) OR ((((product0).name = ? ) AND ((product0).weight = ? ) ) AND ((product0).listprice < ? ) ) ) ORDER BY (product0).name ASC , (product0).weight DESC , (product0).listprice DESC ) +) +select (product0)."productid",(product0)."name",(product0)."productnumber",(product0)."makeflag",(product0)."finishedgoodsflag",(product0)."color",(product0)."safetystocklevel",(product0)."reorderpoint",(product0)."standardcost",(product0)."listprice",(product0)."size",(product0)."sizeunitmeasurecode",(product0)."weightunitmeasurecode",(product0)."weight",(product0)."daystomanufacture",(product0)."productline",(product0)."class",(product0)."style",(product0)."productsubcategoryid",(product0)."productmodelid",(product0)."sellstartdate"::text,(product0)."sellenddate"::text,(product0)."discontinueddate"::text,(product0)."rowguid",(product0)."modifieddate"::text from product0 \ No newline at end of file diff --git a/snapshot-tests/dooble-sql/SeekTest/uniform-ascending.sql b/snapshot-tests/dooble-sql/SeekTest/uniform-ascending.sql new file mode 100644 index 000000000..bb28fbe38 --- /dev/null +++ b/snapshot-tests/dooble-sql/SeekTest/uniform-ascending.sql @@ -0,0 +1,5 @@ +with +product0 as ( + (select product0 from production.product product0 WHERE (((product0).name, (product0).weight, (product0).listprice) > (? , ? , ? ) ) ORDER BY (product0).name ASC , (product0).weight ASC , (product0).listprice ASC ) +) +select (product0)."productid",(product0)."name",(product0)."productnumber",(product0)."makeflag",(product0)."finishedgoodsflag",(product0)."color",(product0)."safetystocklevel",(product0)."reorderpoint",(product0)."standardcost",(product0)."listprice",(product0)."size",(product0)."sizeunitmeasurecode",(product0)."weightunitmeasurecode",(product0)."weight",(product0)."daystomanufacture",(product0)."productline",(product0)."class",(product0)."style",(product0)."productsubcategoryid",(product0)."productmodelid",(product0)."sellstartdate"::text,(product0)."sellenddate"::text,(product0)."discontinueddate"::text,(product0)."rowguid",(product0)."modifieddate"::text from product0 \ No newline at end of file diff --git a/snapshot-tests/dooble-sql/SeekTest/uniform-descending.sql b/snapshot-tests/dooble-sql/SeekTest/uniform-descending.sql new file mode 100644 index 000000000..b5c8f4310 --- /dev/null +++ b/snapshot-tests/dooble-sql/SeekTest/uniform-descending.sql @@ -0,0 +1,5 @@ +with +product0 as ( + (select product0 from production.product product0 WHERE (((product0).name, (product0).weight, (product0).listprice) < (? , ? , ? ) ) ORDER BY (product0).name DESC , (product0).weight DESC , (product0).listprice DESC ) +) +select (product0)."productid",(product0)."name",(product0)."productnumber",(product0)."makeflag",(product0)."finishedgoodsflag",(product0)."color",(product0)."safetystocklevel",(product0)."reorderpoint",(product0)."standardcost",(product0)."listprice",(product0)."size",(product0)."sizeunitmeasurecode",(product0)."weightunitmeasurecode",(product0)."weight",(product0)."daystomanufacture",(product0)."productline",(product0)."class",(product0)."style",(product0)."productsubcategoryid",(product0)."productmodelid",(product0)."sellstartdate"::text,(product0)."sellenddate"::text,(product0)."discontinueddate"::text,(product0)."rowguid",(product0)."modifieddate"::text from product0 \ No newline at end of file diff --git a/snapshot-tests/zio-jdbc-sql/CompositeIdsTest/query2.sql b/snapshot-tests/zio-jdbc-sql/CompositeIdsTest/query2.sql new file mode 100644 index 000000000..25ee3b147 --- /dev/null +++ b/snapshot-tests/zio-jdbc-sql/CompositeIdsTest/query2.sql @@ -0,0 +1,5 @@ +with +emailaddress0 as ( + (select emailaddress0 from person.emailaddress emailaddress0 WHERE ((emailaddress0).businessentityid, (emailaddress0).emailaddressid) in (select unnest(?::int4[]), unnest(?::int4[]))) +) +select (emailaddress0)."businessentityid",(emailaddress0)."emailaddressid",(emailaddress0)."emailaddress",(emailaddress0)."rowguid",(emailaddress0)."modifieddate"::text from emailaddress0 \ No newline at end of file diff --git a/snapshot-tests/zio-jdbc-sql/DSLTest/doubled.sql b/snapshot-tests/zio-jdbc-sql/DSLTest/doubled.sql new file mode 100644 index 000000000..4a8455750 --- /dev/null +++ b/snapshot-tests/zio-jdbc-sql/DSLTest/doubled.sql @@ -0,0 +1,115 @@ +with +salesperson0 as ( + (select salesperson0 from sales.salesperson salesperson0 WHERE ((salesperson0).rowguid = ?::uuid)) +), +employee0 as ( + (select employee0 from humanresources.employee employee0 ) +), +join_cte5 as ( + select salesperson0, employee0 + from salesperson0 + join employee0 + on ((salesperson0).businessentityid = (employee0).businessentityid) + +), +person0 as ( + (select person0 from person.person person0 ) +), +join_cte4 as ( + select salesperson0, employee0, person0 + from join_cte5 + join person0 + on ((employee0).businessentityid = (person0).businessentityid) + +), +businessentity0 as ( + (select businessentity0 from person.businessentity businessentity0 ) +), +join_cte3 as ( + select salesperson0, employee0, person0, businessentity0 + from join_cte4 + join businessentity0 + on ((person0).businessentityid = (businessentity0).businessentityid) + +), +emailaddress0 as ( + (select emailaddress0 from person.emailaddress emailaddress0 ORDER BY (emailaddress0).rowguid ASC ) +), +join_cte2 as ( + select salesperson0, employee0, person0, businessentity0, emailaddress0 + from join_cte3 + join emailaddress0 + on ((emailaddress0).businessentityid = (businessentity0).businessentityid) + +), +salesperson1 as ( + (select salesperson1 from sales.salesperson salesperson1 ) +), +join_cte1 as ( + select salesperson0, employee0, person0, businessentity0, emailaddress0, salesperson1 + from join_cte2 + join salesperson1 + on ((emailaddress0).businessentityid = (salesperson1).businessentityid) + +), +salesperson2 as ( + (select salesperson2 from sales.salesperson salesperson2 WHERE ((salesperson2).rowguid = ?::uuid)) +), +employee1 as ( + (select employee1 from humanresources.employee employee1 ) +), +join_cte10 as ( + select salesperson2, employee1 + from salesperson2 + join employee1 + on ((salesperson2).businessentityid = (employee1).businessentityid) + +), +person1 as ( + (select person1 from person.person person1 ) +), +join_cte9 as ( + select salesperson2, employee1, person1 + from join_cte10 + join person1 + on ((employee1).businessentityid = (person1).businessentityid) + +), +businessentity1 as ( + (select businessentity1 from person.businessentity businessentity1 ) +), +join_cte8 as ( + select salesperson2, employee1, person1, businessentity1 + from join_cte9 + join businessentity1 + on ((person1).businessentityid = (businessentity1).businessentityid) + +), +emailaddress1 as ( + (select emailaddress1 from person.emailaddress emailaddress1 ORDER BY (emailaddress1).rowguid ASC ) +), +join_cte7 as ( + select salesperson2, employee1, person1, businessentity1, emailaddress1 + from join_cte8 + join emailaddress1 + on ((emailaddress1).businessentityid = (businessentity1).businessentityid) + +), +salesperson3 as ( + (select salesperson3 from sales.salesperson salesperson3 ) +), +join_cte6 as ( + select salesperson2, employee1, person1, businessentity1, emailaddress1, salesperson3 + from join_cte7 + join salesperson3 + on ((emailaddress1).businessentityid = (salesperson3).businessentityid) + +), +join_cte0 as ( + select salesperson0, employee0, person0, businessentity0, emailaddress0, salesperson1, salesperson2, employee1, person1, businessentity1, emailaddress1, salesperson3 + from join_cte1 + join join_cte6 + on ((emailaddress0).businessentityid = (emailaddress1).businessentityid) + +) +select (salesperson0)."businessentityid",(salesperson0)."territoryid",(salesperson0)."salesquota",(salesperson0)."bonus",(salesperson0)."commissionpct",(salesperson0)."salesytd",(salesperson0)."saleslastyear",(salesperson0)."rowguid",(salesperson0)."modifieddate"::text,(employee0)."businessentityid",(employee0)."nationalidnumber",(employee0)."loginid",(employee0)."jobtitle",(employee0)."birthdate"::text,(employee0)."maritalstatus",(employee0)."gender",(employee0)."hiredate"::text,(employee0)."salariedflag",(employee0)."vacationhours",(employee0)."sickleavehours",(employee0)."currentflag",(employee0)."rowguid",(employee0)."modifieddate"::text,(employee0)."organizationnode",(person0)."businessentityid",(person0)."persontype",(person0)."namestyle",(person0)."title",(person0)."firstname",(person0)."middlename",(person0)."lastname",(person0)."suffix",(person0)."emailpromotion",(person0)."additionalcontactinfo",(person0)."demographics",(person0)."rowguid",(person0)."modifieddate"::text,(businessentity0)."businessentityid",(businessentity0)."rowguid",(businessentity0)."modifieddate"::text,(emailaddress0)."businessentityid",(emailaddress0)."emailaddressid",(emailaddress0)."emailaddress",(emailaddress0)."rowguid",(emailaddress0)."modifieddate"::text,(salesperson1)."businessentityid",(salesperson1)."territoryid",(salesperson1)."salesquota",(salesperson1)."bonus",(salesperson1)."commissionpct",(salesperson1)."salesytd",(salesperson1)."saleslastyear",(salesperson1)."rowguid",(salesperson1)."modifieddate"::text,(salesperson2)."businessentityid",(salesperson2)."territoryid",(salesperson2)."salesquota",(salesperson2)."bonus",(salesperson2)."commissionpct",(salesperson2)."salesytd",(salesperson2)."saleslastyear",(salesperson2)."rowguid",(salesperson2)."modifieddate"::text,(employee1)."businessentityid",(employee1)."nationalidnumber",(employee1)."loginid",(employee1)."jobtitle",(employee1)."birthdate"::text,(employee1)."maritalstatus",(employee1)."gender",(employee1)."hiredate"::text,(employee1)."salariedflag",(employee1)."vacationhours",(employee1)."sickleavehours",(employee1)."currentflag",(employee1)."rowguid",(employee1)."modifieddate"::text,(employee1)."organizationnode",(person1)."businessentityid",(person1)."persontype",(person1)."namestyle",(person1)."title",(person1)."firstname",(person1)."middlename",(person1)."lastname",(person1)."suffix",(person1)."emailpromotion",(person1)."additionalcontactinfo",(person1)."demographics",(person1)."rowguid",(person1)."modifieddate"::text,(businessentity1)."businessentityid",(businessentity1)."rowguid",(businessentity1)."modifieddate"::text,(emailaddress1)."businessentityid",(emailaddress1)."emailaddressid",(emailaddress1)."emailaddress",(emailaddress1)."rowguid",(emailaddress1)."modifieddate"::text,(salesperson3)."businessentityid",(salesperson3)."territoryid",(salesperson3)."salesquota",(salesperson3)."bonus",(salesperson3)."commissionpct",(salesperson3)."salesytd",(salesperson3)."saleslastyear",(salesperson3)."rowguid",(salesperson3)."modifieddate"::text from join_cte0 \ No newline at end of file diff --git a/snapshot-tests/zio-jdbc-sql/ProductTest/delete.sql b/snapshot-tests/zio-jdbc-sql/ProductTest/delete.sql new file mode 100644 index 000000000..91b007ef3 --- /dev/null +++ b/snapshot-tests/zio-jdbc-sql/ProductTest/delete.sql @@ -0,0 +1 @@ +DELETE FROM production.product where coalesce((productid = ?::int4), ?::boolean) \ No newline at end of file diff --git a/snapshot-tests/zio-jdbc-sql/ProductTest/leftJoined.sql b/snapshot-tests/zio-jdbc-sql/ProductTest/leftJoined.sql new file mode 100644 index 000000000..049467cbe --- /dev/null +++ b/snapshot-tests/zio-jdbc-sql/ProductTest/leftJoined.sql @@ -0,0 +1,15 @@ +with +product0 as ( + (select product0 from production.product product0 ) +), +productmodel0 as ( + (select productmodel0 from production.productmodel productmodel0 ) +), +left_join_cte0 as ( + select product0, productmodel0 + from product0 + left join productmodel0 + on ((product0).productmodelid = (productmodel0).productmodelid) + +) +select (product0)."productid",(product0)."name",(product0)."productnumber",(product0)."makeflag",(product0)."finishedgoodsflag",(product0)."color",(product0)."safetystocklevel",(product0)."reorderpoint",(product0)."standardcost",(product0)."listprice",(product0)."size",(product0)."sizeunitmeasurecode",(product0)."weightunitmeasurecode",(product0)."weight",(product0)."daystomanufacture",(product0)."productline",(product0)."class",(product0)."style",(product0)."productsubcategoryid",(product0)."productmodelid",(product0)."sellstartdate"::text,(product0)."sellenddate"::text,(product0)."discontinueddate"::text,(product0)."rowguid",(product0)."modifieddate"::text,(productmodel0)."productmodelid",(productmodel0)."name",(productmodel0)."catalogdescription",(productmodel0)."instructions",(productmodel0)."rowguid",(productmodel0)."modifieddate"::text from left_join_cte0 \ No newline at end of file diff --git a/snapshot-tests/zio-jdbc-sql/ProductTest/q.sql b/snapshot-tests/zio-jdbc-sql/ProductTest/q.sql new file mode 100644 index 000000000..850bfc53b --- /dev/null +++ b/snapshot-tests/zio-jdbc-sql/ProductTest/q.sql @@ -0,0 +1,15 @@ +with +product0 as ( + (select product0 from production.product product0 WHERE NOT ((product0).name LIKE ?::text) AND NOT (((product0).name || (product0).color) LIKE ?::text) AND ((product0).daystomanufacture > ?::int4) AND ((product0).modifieddate < ?::timestamp)) +), +productmodel0 as ( + (select productmodel0 from production.productmodel productmodel0 WHERE ((productmodel0).modifieddate < ?::timestamp)) +), +join_cte0 as ( + select product0, productmodel0 + from product0 + join productmodel0 + on ((product0).productmodelid = (productmodel0).productmodelid) + WHERE NOT (productmodel0).instructions IS NULL +) +select (product0)."productid",(product0)."name",(product0)."productnumber",(product0)."makeflag",(product0)."finishedgoodsflag",(product0)."color",(product0)."safetystocklevel",(product0)."reorderpoint",(product0)."standardcost",(product0)."listprice",(product0)."size",(product0)."sizeunitmeasurecode",(product0)."weightunitmeasurecode",(product0)."weight",(product0)."daystomanufacture",(product0)."productline",(product0)."class",(product0)."style",(product0)."productsubcategoryid",(product0)."productmodelid",(product0)."sellstartdate"::text,(product0)."sellenddate"::text,(product0)."discontinueddate"::text,(product0)."rowguid",(product0)."modifieddate"::text,(productmodel0)."productmodelid",(productmodel0)."name",(productmodel0)."catalogdescription",(productmodel0)."instructions",(productmodel0)."rowguid",(productmodel0)."modifieddate"::text from join_cte0 \ No newline at end of file diff --git a/snapshot-tests/zio-jdbc-sql/ProductTest/q2.sql b/snapshot-tests/zio-jdbc-sql/ProductTest/q2.sql new file mode 100644 index 000000000..dd2375c14 --- /dev/null +++ b/snapshot-tests/zio-jdbc-sql/ProductTest/q2.sql @@ -0,0 +1,25 @@ +with +product0 as ( + (select product0 from production.product product0 WHERE (product0).productid = ANY(?) AND (length((product0).name) > ?::int4) AND NOT (((product0).name || (product0).color) LIKE ?::text) AND (coalesce(length((product0).color), ?::int4) > ?::int4) AND ((product0).modifieddate < ?::timestamp)) +), +productmodel0 as ( + (select productmodel0 from production.productmodel productmodel0 WHERE NOT ((productmodel0).name LIKE ?::text)) +), +join_cte0 as ( + select product0, productmodel0 + from product0 + join productmodel0 + on ((product0).productmodelid = (productmodel0).productmodelid) + WHERE ((productmodel0).productmodelid > ?::int4) +), +productmodel1 as ( + (select productmodel1 from production.productmodel productmodel1 WHERE NOT ((productmodel1).name LIKE ?::text)) +), +left_join_cte0 as ( + select product0, productmodel0, productmodel1 + from join_cte0 + left join productmodel1 + on (((product0).productmodelid = (productmodel1).productmodelid) AND ?::boolean) + ORDER BY (product0).name ASC , (productmodel0).rowguid DESC NULLS FIRST, (productmodel1).rowguid ASC +) +select (product0)."productid",(product0)."name",(product0)."productnumber",(product0)."makeflag",(product0)."finishedgoodsflag",(product0)."color",(product0)."safetystocklevel",(product0)."reorderpoint",(product0)."standardcost",(product0)."listprice",(product0)."size",(product0)."sizeunitmeasurecode",(product0)."weightunitmeasurecode",(product0)."weight",(product0)."daystomanufacture",(product0)."productline",(product0)."class",(product0)."style",(product0)."productsubcategoryid",(product0)."productmodelid",(product0)."sellstartdate"::text,(product0)."sellenddate"::text,(product0)."discontinueddate"::text,(product0)."rowguid",(product0)."modifieddate"::text,(productmodel0)."productmodelid",(productmodel0)."name",(productmodel0)."catalogdescription",(productmodel0)."instructions",(productmodel0)."rowguid",(productmodel0)."modifieddate"::text,(productmodel1)."productmodelid",(productmodel1)."name",(productmodel1)."catalogdescription",(productmodel1)."instructions",(productmodel1)."rowguid",(productmodel1)."modifieddate"::text from left_join_cte0 \ No newline at end of file diff --git a/snapshot-tests/zio-jdbc-sql/ProductTest/query.sql b/snapshot-tests/zio-jdbc-sql/ProductTest/query.sql new file mode 100644 index 000000000..5ce5f3eef --- /dev/null +++ b/snapshot-tests/zio-jdbc-sql/ProductTest/query.sql @@ -0,0 +1,25 @@ +with +product0 as ( + (select product0 from production.product product0 WHERE ((product0).class = ?::text) AND (((product0).daystomanufacture > ?::int4) OR ((product0).daystomanufacture <= ?::int4)) AND ((product0).productline = ?::text)) +), +unitmeasure0 as ( + (select unitmeasure0 from production.unitmeasure unitmeasure0 WHERE ((unitmeasure0).name LIKE ?::text)) +), +join_cte0 as ( + select product0, unitmeasure0 + from product0 + join unitmeasure0 + on ((product0).sizeunitmeasurecode = (unitmeasure0).unitmeasurecode) + +), +productmodel0 as ( + (select productmodel0 from production.productmodel productmodel0 ) +), +left_join_cte0 as ( + select product0, unitmeasure0, productmodel0 + from join_cte0 + left join productmodel0 + on ((product0).productmodelid = (productmodel0).productmodelid) + WHERE ((product0).productmodelid = (productmodel0).productmodelid) ORDER BY (product0).productmodelid ASC , (productmodel0).name DESC NULLS FIRST +) +select (product0)."productid",(product0)."name",(product0)."productnumber",(product0)."makeflag",(product0)."finishedgoodsflag",(product0)."color",(product0)."safetystocklevel",(product0)."reorderpoint",(product0)."standardcost",(product0)."listprice",(product0)."size",(product0)."sizeunitmeasurecode",(product0)."weightunitmeasurecode",(product0)."weight",(product0)."daystomanufacture",(product0)."productline",(product0)."class",(product0)."style",(product0)."productsubcategoryid",(product0)."productmodelid",(product0)."sellstartdate"::text,(product0)."sellenddate"::text,(product0)."discontinueddate"::text,(product0)."rowguid",(product0)."modifieddate"::text,(unitmeasure0)."unitmeasurecode",(unitmeasure0)."name",(unitmeasure0)."modifieddate"::text,(productmodel0)."productmodelid",(productmodel0)."name",(productmodel0)."catalogdescription",(productmodel0)."instructions",(productmodel0)."rowguid",(productmodel0)."modifieddate"::text from left_join_cte0 \ No newline at end of file diff --git a/snapshot-tests/zio-jdbc-sql/ProductTest/query0.sql b/snapshot-tests/zio-jdbc-sql/ProductTest/query0.sql new file mode 100644 index 000000000..29ea0f6ec --- /dev/null +++ b/snapshot-tests/zio-jdbc-sql/ProductTest/query0.sql @@ -0,0 +1,35 @@ +with +product0 as ( + (select product0 from production.product product0 ) +), +productmodel0 as ( + (select productmodel0 from production.productmodel productmodel0 ) +), +join_cte2 as ( + select product0, productmodel0 + from product0 + join productmodel0 + on ((product0).productmodelid = (productmodel0).productmodelid) + +), +productsubcategory0 as ( + (select productsubcategory0 from production.productsubcategory productsubcategory0 ) +), +join_cte1 as ( + select product0, productmodel0, productsubcategory0 + from join_cte2 + join productsubcategory0 + on ((product0).productsubcategoryid = (productsubcategory0).productsubcategoryid) + +), +productcategory0 as ( + (select productcategory0 from production.productcategory productcategory0 ) +), +join_cte0 as ( + select product0, productmodel0, productsubcategory0, productcategory0 + from join_cte1 + join productcategory0 + on ((productsubcategory0).productcategoryid = (productcategory0).productcategoryid) + +) +select (product0)."productid",(product0)."name",(product0)."productnumber",(product0)."makeflag",(product0)."finishedgoodsflag",(product0)."color",(product0)."safetystocklevel",(product0)."reorderpoint",(product0)."standardcost",(product0)."listprice",(product0)."size",(product0)."sizeunitmeasurecode",(product0)."weightunitmeasurecode",(product0)."weight",(product0)."daystomanufacture",(product0)."productline",(product0)."class",(product0)."style",(product0)."productsubcategoryid",(product0)."productmodelid",(product0)."sellstartdate"::text,(product0)."sellenddate"::text,(product0)."discontinueddate"::text,(product0)."rowguid",(product0)."modifieddate"::text,(productmodel0)."productmodelid",(productmodel0)."name",(productmodel0)."catalogdescription",(productmodel0)."instructions",(productmodel0)."rowguid",(productmodel0)."modifieddate"::text,(productsubcategory0)."productsubcategoryid",(productsubcategory0)."productcategoryid",(productsubcategory0)."name",(productsubcategory0)."rowguid",(productsubcategory0)."modifieddate"::text,(productcategory0)."productcategoryid",(productcategory0)."name",(productcategory0)."rowguid",(productcategory0)."modifieddate"::text from join_cte0 \ No newline at end of file diff --git a/snapshot-tests/zio-jdbc-sql/ProductTest/updateReturning.sql b/snapshot-tests/zio-jdbc-sql/ProductTest/updateReturning.sql new file mode 100644 index 000000000..9a221aa50 --- /dev/null +++ b/snapshot-tests/zio-jdbc-sql/ProductTest/updateReturning.sql @@ -0,0 +1 @@ +UPDATE production.product SET name = substring((upper(reverse(name)) || ?::"public"."Name"), ?::int4, ?::int4)::varchar, listprice = ?::numeric::numeric, reorderpoint = (reorderpoint + ?::int2)::int2 WHERE coalesce((productid = ?::int4), ?::boolean) returning "productid", "name", "productnumber", "makeflag", "finishedgoodsflag", "color", "safetystocklevel", "reorderpoint", "standardcost", "listprice", "size", "sizeunitmeasurecode", "weightunitmeasurecode", "weight", "daystomanufacture", "productline", "class", "style", "productsubcategoryid", "productmodelid", "sellstartdate"::text, "sellenddate"::text, "discontinueddate"::text, "rowguid", "modifieddate"::text \ No newline at end of file diff --git a/snapshot-tests/zio-jdbc-sql/SeekTest/complex.sql b/snapshot-tests/zio-jdbc-sql/SeekTest/complex.sql new file mode 100644 index 000000000..63e1db8a9 --- /dev/null +++ b/snapshot-tests/zio-jdbc-sql/SeekTest/complex.sql @@ -0,0 +1,5 @@ +with +product0 as ( + (select product0 from production.product product0 WHERE ((((product0).name > ?::"public"."Name") OR (((product0).name = ?::"public"."Name") AND ((product0).weight < ?::numeric))) OR ((((product0).name = ?::"public"."Name") AND ((product0).weight = ?::numeric)) AND ((product0).listprice < ?::numeric))) ORDER BY (product0).name ASC , (product0).weight DESC , (product0).listprice DESC ) +) +select (product0)."productid",(product0)."name",(product0)."productnumber",(product0)."makeflag",(product0)."finishedgoodsflag",(product0)."color",(product0)."safetystocklevel",(product0)."reorderpoint",(product0)."standardcost",(product0)."listprice",(product0)."size",(product0)."sizeunitmeasurecode",(product0)."weightunitmeasurecode",(product0)."weight",(product0)."daystomanufacture",(product0)."productline",(product0)."class",(product0)."style",(product0)."productsubcategoryid",(product0)."productmodelid",(product0)."sellstartdate"::text,(product0)."sellenddate"::text,(product0)."discontinueddate"::text,(product0)."rowguid",(product0)."modifieddate"::text from product0 \ No newline at end of file diff --git a/snapshot-tests/zio-jdbc-sql/SeekTest/uniform-ascending.sql b/snapshot-tests/zio-jdbc-sql/SeekTest/uniform-ascending.sql new file mode 100644 index 000000000..265dd2e3b --- /dev/null +++ b/snapshot-tests/zio-jdbc-sql/SeekTest/uniform-ascending.sql @@ -0,0 +1,5 @@ +with +product0 as ( + (select product0 from production.product product0 WHERE (((product0).name,(product0).weight,(product0).listprice) > (?::"public"."Name",?::numeric,?::numeric)) ORDER BY (product0).name ASC , (product0).weight ASC , (product0).listprice ASC ) +) +select (product0)."productid",(product0)."name",(product0)."productnumber",(product0)."makeflag",(product0)."finishedgoodsflag",(product0)."color",(product0)."safetystocklevel",(product0)."reorderpoint",(product0)."standardcost",(product0)."listprice",(product0)."size",(product0)."sizeunitmeasurecode",(product0)."weightunitmeasurecode",(product0)."weight",(product0)."daystomanufacture",(product0)."productline",(product0)."class",(product0)."style",(product0)."productsubcategoryid",(product0)."productmodelid",(product0)."sellstartdate"::text,(product0)."sellenddate"::text,(product0)."discontinueddate"::text,(product0)."rowguid",(product0)."modifieddate"::text from product0 \ No newline at end of file diff --git a/snapshot-tests/zio-jdbc-sql/SeekTest/uniform-descending.sql b/snapshot-tests/zio-jdbc-sql/SeekTest/uniform-descending.sql new file mode 100644 index 000000000..627122413 --- /dev/null +++ b/snapshot-tests/zio-jdbc-sql/SeekTest/uniform-descending.sql @@ -0,0 +1,5 @@ +with +product0 as ( + (select product0 from production.product product0 WHERE (((product0).name,(product0).weight,(product0).listprice) < (?::"public"."Name",?::numeric,?::numeric)) ORDER BY (product0).name DESC , (product0).weight DESC , (product0).listprice DESC ) +) +select (product0)."productid",(product0)."name",(product0)."productnumber",(product0)."makeflag",(product0)."finishedgoodsflag",(product0)."color",(product0)."safetystocklevel",(product0)."reorderpoint",(product0)."standardcost",(product0)."listprice",(product0)."size",(product0)."sizeunitmeasurecode",(product0)."weightunitmeasurecode",(product0)."weight",(product0)."daystomanufacture",(product0)."productline",(product0)."class",(product0)."style",(product0)."productsubcategoryid",(product0)."productmodelid",(product0)."sellstartdate"::text,(product0)."sellenddate"::text,(product0)."discontinueddate"::text,(product0)."rowguid",(product0)."modifieddate"::text from product0 \ No newline at end of file diff --git a/typo-dsl-anorm/src/scala/typo/dsl/SelectBuilder.scala b/typo-dsl-anorm/src/scala/typo/dsl/SelectBuilder.scala index a19f3121d..4387b0a26 100644 --- a/typo-dsl-anorm/src/scala/typo/dsl/SelectBuilder.scala +++ b/typo-dsl-anorm/src/scala/typo/dsl/SelectBuilder.scala @@ -48,11 +48,11 @@ trait SelectBuilder[Fields, Row] { withParams(params.orderBy(v)) final def seek[T, N[_]](v: Fields => SortOrder[T, N])(value: SqlExpr.Const[T, N]): SelectBuilder[Fields, Row] = - withParams(params.seek(SelectParams.Seek[Fields, T, N](v, value))) + withParams(params.seek(v, value)) - final def maybeSeek[T, N[_]](v: Fields => SortOrder[T, N])(maybeValue: Option[SqlExpr.Const[T, N]]): SelectBuilder[Fields, Row] = + final def maybeSeek[T, N[_]](v: Fields => SortOrder[T, N])(maybeValue: Option[N[T]])(implicit asConst: SqlExpr.Const.As[T, N]): SelectBuilder[Fields, Row] = maybeValue match { - case Some(value) => seek(v)(value) + case Some(value) => seek(v)(asConst(value)) case None => orderBy(v) } @@ -64,6 +64,8 @@ trait SelectBuilder[Fields, Row] { /** Execute the query and return the results as a list */ def toList(implicit c: Connection): List[Row] + def count(implicit c: Connection): Int + /** Return sql for debugging. [[None]] if backed by a mock repository */ def sql: Option[Fragment] diff --git a/typo-dsl-anorm/src/scala/typo/dsl/SelectBuilderMock.scala b/typo-dsl-anorm/src/scala/typo/dsl/SelectBuilderMock.scala index 053a69506..4518d1d16 100644 --- a/typo-dsl-anorm/src/scala/typo/dsl/SelectBuilderMock.scala +++ b/typo-dsl-anorm/src/scala/typo/dsl/SelectBuilderMock.scala @@ -1,7 +1,6 @@ package typo.dsl import java.sql.Connection -import typo.dsl.internal.seeks import typo.dsl.internal.mocks.RowOrdering final case class SelectBuilderMock[Fields, Row]( @@ -20,6 +19,9 @@ final case class SelectBuilderMock[Fields, Row]( override def toList(implicit c: Connection): List[Row] = SelectBuilderMock.applyParams(structure, all(), params) + override def count(implicit c: Connection): Int = + toList(c).length + override def joinOn[Fields2, N[_]: Nullability, Row2](other: SelectBuilder[Fields2, Row2])(pred: (Fields ~ Fields2) => SqlExpr[Boolean, N]): SelectBuilderMock[Fields ~ Fields2, Row ~ Row2] = { val otherMock: SelectBuilderMock[Fields2, Row2] = other match { case x: SelectBuilderMock[Fields2, Row2] => x.withPath(Path.RightInJoin) @@ -66,7 +68,7 @@ final case class SelectBuilderMock[Fields, Row]( object SelectBuilderMock { def applyParams[Fields, R](structure: Structure[Fields, R], rows: List[R], params: SelectParams[Fields, R]): List[R] = { - val (filters, orderBys) = seeks.expand(structure.fields, params) + val (filters, orderBys) = OrderByOrSeek.expand(structure.fields, params) implicit val rowOrdering: Ordering[R] = new RowOrdering(structure, orderBys) rows .filter(row => filters.forall(expr => structure.untypedEval(expr, row).getOrElse(false))) diff --git a/typo-dsl-anorm/src/scala/typo/dsl/SelectBuilderSql.scala b/typo-dsl-anorm/src/scala/typo/dsl/SelectBuilderSql.scala index cf1330701..f4423261c 100644 --- a/typo-dsl-anorm/src/scala/typo/dsl/SelectBuilderSql.scala +++ b/typo-dsl-anorm/src/scala/typo/dsl/SelectBuilderSql.scala @@ -1,6 +1,6 @@ package typo.dsl -import anorm.{AnormException, RowParser, SQL, SimpleSql} +import anorm.{AnormException, RowParser, SQL, SimpleSql, SqlParser} import typo.dsl.Fragment.FragmentStringInterpolator import java.sql.Connection @@ -9,11 +9,33 @@ import java.util.concurrent.atomic.AtomicInteger sealed trait SelectBuilderSql[Fields, Row] extends SelectBuilder[Fields, Row] { def withPath(path: Path): SelectBuilderSql[Fields, Row] def instantiate(ctx: RenderCtx, counter: AtomicInteger): SelectBuilderSql.Instantiated[Fields, Row] - def sqlFor(ctx: RenderCtx, counter: AtomicInteger): (Fragment, RowParser[Row]) override lazy val renderCtx: RenderCtx = RenderCtx.from(this) - override lazy val sql: Option[Fragment] = Some(sqlFor(renderCtx, new AtomicInteger(0))._1) + lazy val sqlAndRowParser: (Fragment, RowParser[Row]) = { + val instance = this.instantiate(renderCtx, new AtomicInteger(0)) + + val cols: List[String] = + instance.columns.map { case (alias, col) => + col.sqlReadCast.foldLeft(s"($alias).\"${col.name}\"") { case (acc, cast) => s"$acc::$cast" } + } + + val ctes = instance.asCTEs + val formattedCTEs = ctes.map { cte => + frag"""|${Fragment(cte.name)} as ( + | ${cte.sql} + |)""".stripMargin + } + + val frag = + frag"""|with + |${formattedCTEs.mkFragment(",\n")} + |select ${Fragment(cols.mkString(","))} from ${Fragment(ctes.last.name)}""".stripMargin + + (frag, instance.rowParser(1)) + } + + override lazy val sql: Option[Fragment] = Some(sqlAndRowParser._1) final override def joinOn[Fields2, N[_]: Nullability, Row2](other: SelectBuilder[Fields2, Row2])(pred: Fields ~ Fields2 => SqlExpr[Boolean, N]): SelectBuilder[Fields ~ Fields2, Row ~ Row2] = other match { case otherSql: SelectBuilderSql[Fields2, Row2] => @@ -31,9 +53,14 @@ sealed trait SelectBuilderSql[Fields, Row] extends SelectBuilder[Fields, Row] { } final override def toList(implicit c: Connection): List[Row] = { - val (frag, rowParser) = sqlFor(renderCtx, new AtomicInteger(0)) + val (frag, rowParser) = sqlAndRowParser SimpleSql(SQL(frag.sql), frag.params.map(_.tupled).toMap, RowParser.successful).as(rowParser.*)(c) } + + final override def count(implicit c: Connection): Int = { + val (frag, _) = sqlAndRowParser + SimpleSql(SQL(s"select count(*) from (${frag.sql}) rows"), frag.params.map(_.tupled).toMap, RowParser.successful).as(SqlParser.int(1).single)(c) + } } object SelectBuilderSql { @@ -56,27 +83,19 @@ object SelectBuilderSql { override def withParams(sqlParams: SelectParams[Fields, Row]): SelectBuilder[Fields, Row] = copy(params = sqlParams) - private def sql(ctx: RenderCtx, counter: AtomicInteger): Fragment = { - val cols = structure.columns - .map(x => x.sqlReadCast.foldLeft(s"\"${x.name}\"") { case (acc, cast) => s"$acc::$cast" }) - .mkString(",") + override def instantiate(ctx: RenderCtx, counter: AtomicInteger): SelectBuilderSql.Instantiated[Fields, Row] = { val alias = ctx.alias(structure._path) - val baseSql = frag"select ${Fragment(cols)} from ${Fragment(name)} ${Fragment(alias)}" - SelectParams.render(structure.fields, baseSql, ctx, counter, params) - } - - override def sqlFor(ctx: RenderCtx, counter: AtomicInteger): (Fragment, RowParser[Row]) = - (sql(ctx, counter), rowParser(1)) + val sql = frag"(select ${Fragment(alias)} from ${Fragment(name)} ${Fragment(alias)} ${SelectParams.render(structure.fields, ctx, counter, params).getOrElse(Fragment.empty)})" - override def instantiate(ctx: RenderCtx, counter: AtomicInteger): SelectBuilderSql.Instantiated[Fields, Row] = { - val part = SelectBuilderSql.InstantiatedPart( - alias = ctx.alias(structure._path), - columns = structure.columns, - sqlFrag = sql(ctx, counter), - joinFrag = Fragment.empty, - joinType = JoinType.Inner + SelectBuilderSql.Instantiated( + alias = alias, + isJoin = false, + columns = structure.columns.map(c => (alias, c)), + sqlFrag = sql, + upstreamCTEs = Nil, + structure = structure, + rowParser = rowParser ) - SelectBuilderSql.Instantiated(structure, List(part), rowParser = rowParser) } } @@ -96,56 +115,31 @@ object SelectBuilderSql { copy(params = sqlParams) override def instantiate(ctx: RenderCtx, counter: AtomicInteger): Instantiated[Fields1 ~ Fields2, Row1 ~ Row2] = { - - val leftInstantiated: Instantiated[Fields1, Row1] = left.instantiate(ctx, counter) - val rightInstantiated: Instantiated[Fields2, Row2] = right.instantiate(ctx, counter) - - val newStructure = leftInstantiated.structure.join(rightInstantiated.structure) - val newRightInstantiatedParts = rightInstantiated.parts - .mapLast( - _.copy( - joinFrag = pred(newStructure.fields).render(ctx, counter), - joinType = JoinType.Inner - ) - ) - + val alias = ctx.alias(structure._path) + val leftInstance = left.instantiate(ctx, counter) + val rightInstance = right.instantiate(ctx, counter) + val newStructure = leftInstance.structure.join(rightInstance.structure) + val ctes = leftInstance.asCTEs ++ rightInstance.asCTEs + val sql = + frag"""|select ${ctes.filterNot(_.isJoin).map(cte => Fragment(cte.name)).mkFragment(", ")} + | from ${Fragment(leftInstance.alias)} + | join ${Fragment(rightInstance.alias)} + | on ${pred(newStructure.fields).render(ctx, counter)} + | ${SelectParams.render(newStructure.fields, ctx, counter, params).getOrElse(Fragment.empty)}""".stripMargin SelectBuilderSql.Instantiated( + alias = alias, + isJoin = true, + columns = leftInstance.columns ++ rightInstance.columns, + sqlFrag = sql, + upstreamCTEs = ctes, structure = newStructure, - parts = leftInstantiated.parts ++ newRightInstantiatedParts, rowParser = (i: Int) => for { - r1 <- leftInstantiated.rowParser(i) - r2 <- rightInstantiated.rowParser(i + leftInstantiated.columns.size) + r1 <- leftInstance.rowParser(i) + r2 <- rightInstance.rowParser(i + leftInstance.columns.size) } yield (r1, r2) ) } - - override def sqlFor(ctx: RenderCtx, counter: AtomicInteger): (Fragment, RowParser[Row1 ~ Row2]) = { - val instance = instantiate(ctx, counter) - val combinedFrag = instance.parts match { - case Nil => sys.error("unreachable (tm)") - case List(one) => one.sqlFrag - case first :: rest => - val prelude = - frag"""|select ${instance.columns.map(c => c.render(ctx, counter)).mkFragment(", ")} - |from ( - |${first.sqlFrag.indent(2)} - |) ${Fragment(first.alias)} - |""".stripMargin - - val joins = rest.map { case SelectBuilderSql.InstantiatedPart(alias, _, sqlFrag, joinFrag, joinType) => - frag"""|${joinType.frag} ( - |${sqlFrag.indent(2)} - |) ${Fragment(alias)} on $joinFrag - |""".stripMargin - } - - prelude ++ joins.mkFragment("") - } - val newCombinedFrag = SelectParams.render[Fields1 ~ Fields2, Row1 ~ Row2](instance.structure.fields, combinedFrag, ctx, counter, params) - - (newCombinedFrag, instance.rowParser(1)) - } } final case class TableLeftJoin[Fields1, Fields2, N[_]: Nullability, Row1, Row2]( @@ -164,27 +158,33 @@ object SelectBuilderSql { copy(params = sqlParams) override def instantiate(ctx: RenderCtx, counter: AtomicInteger): Instantiated[Fields1 ~ OuterJoined[Fields2], Row1 ~ Option[Row2]] = { - val leftInstantiated = left.instantiate(ctx, counter) - val rightInstantiated = right.instantiate(ctx, counter) - - val newStructure = leftInstantiated.structure.leftJoin(rightInstantiated.structure) - val newRightInstantiatedParts = rightInstantiated.parts - .mapLast( - _.copy( - joinFrag = pred(leftInstantiated.structure.join(rightInstantiated.structure).fields).render(ctx, counter), - joinType = JoinType.LeftJoin - ) - ) + val alias = ctx.alias(structure._path) + val leftInstance = left.instantiate(ctx, counter) + val rightInstance = right.instantiate(ctx, counter) + + val joinedStructure = leftInstance.structure.join(rightInstance.structure) + val newStructure = leftInstance.structure.leftJoin(rightInstance.structure) + val ctes = leftInstance.asCTEs ++ rightInstance.asCTEs + val sql = + frag"""|select ${ctes.filterNot(_.isJoin).map(cte => Fragment(cte.name)).mkFragment(", ")} + | from ${Fragment(leftInstance.alias)} + | left join ${Fragment(rightInstance.alias)} + | on ${pred(joinedStructure.fields).render(ctx, counter)} + | ${SelectParams.render(newStructure.fields, ctx, counter, params).getOrElse(Fragment.empty)}""".stripMargin SelectBuilderSql.Instantiated( - newStructure, - leftInstantiated.parts ++ newRightInstantiatedParts, + alias = alias, + isJoin = true, + columns = leftInstance.columns ++ rightInstance.columns, + sqlFrag = sql, + upstreamCTEs = ctes, + structure = newStructure, rowParser = (i: Int) => for { - r1 <- leftInstantiated.rowParser(i) + r1 <- leftInstance.rowParser(i) /** note, `RowParser` has a `?` combinator, but it doesn't work. fails with exception instead of [[anorm.Error]] */ r2 <- RowParser[Option[Row2]] { row => - try rightInstantiated.rowParser(i + leftInstantiated.columns.size)(row).map(Some.apply) + try rightInstance.rowParser(i + leftInstance.columns.size)(row).map(Some.apply) catch { case x: AnormException if x.message.contains("not found, available columns") => anorm.Success(None) } @@ -192,65 +192,21 @@ object SelectBuilderSql { } yield (r1, r2) ) } - - override def sqlFor(ctx: RenderCtx, counter: AtomicInteger): (Fragment, RowParser[Row1 ~ Option[Row2]]) = { - val instance = instantiate(ctx, counter) - val combinedFrag = instance.parts match { - case Nil => sys.error("unreachable (tm)") - case List(one) => one.sqlFrag - case first :: rest => - val prelude = - frag"""|select ${instance.columns.map(c => c.render(ctx, counter)).mkFragment(", ")} - |from ( - |${first.sqlFrag.indent(2)} - |) ${Fragment(first.alias)} - |""".stripMargin - - val joins = rest.map { case SelectBuilderSql.InstantiatedPart(alias, _, sqlFrag, joinFrag, joinType) => - frag"""|${joinType.frag} ( - |${sqlFrag.indent(2)} - |) ${Fragment(alias)} on $joinFrag - |""".stripMargin - } - prelude ++ joins.mkFragment("") - } - val newCombinedFrag = - SelectParams.render[Fields1 ~ OuterJoined[Fields2], Row1 ~ Option[Row2]](instance.structure.fields, combinedFrag, ctx, counter, params) - - (newCombinedFrag, instance.rowParser(1)) - } - } - - implicit class ListMapLastOps[T](private val ts: List[T]) extends AnyVal { - def mapLast(f: T => T): List[T] = if (ts.isEmpty) ts else ts.updated(ts.length - 1, f(ts.last)) } /** Need this intermediate data structure to generate aliases for tables (and prefixes for column selections) when we have a tree of joined tables. Need to start from the root after the user has * constructed the tree */ final case class Instantiated[Fields, Row]( + alias: String, + isJoin: Boolean, + columns: List[(String, SqlExpr.FieldLikeNoHkt[?, ?])], + sqlFrag: Fragment, + upstreamCTEs: List[CTE], structure: Structure[Fields, Row], - parts: List[SelectBuilderSql.InstantiatedPart], rowParser: Int => RowParser[Row] ) { - val columns: List[SqlExpr.FieldLikeNoHkt[?, ?]] = parts.flatMap(_.columns) - } - sealed abstract class JoinType(_frag: String) { - val frag = Fragment(_frag) + def asCTEs: List[CTE] = upstreamCTEs :+ CTE(alias, sqlFrag, isJoin) } - - object JoinType { - case object Inner extends JoinType("join") - case object LeftJoin extends JoinType("left join") - case object RightJoin extends JoinType("right join") - } - - /** This is needlessly awkward because the we start with a tree, but we need to make it linear to render it */ - final case class InstantiatedPart( - alias: String, - columns: List[SqlExpr.FieldLikeNoHkt[?, ?]], - sqlFrag: Fragment, - joinFrag: Fragment, - joinType: JoinType - ) + case class CTE(name: String, sql: Fragment, isJoin: Boolean) } diff --git a/typo-dsl-anorm/src/scala/typo/dsl/SelectParams.scala b/typo-dsl-anorm/src/scala/typo/dsl/SelectParams.scala index 374cec20c..158a01227 100644 --- a/typo-dsl-anorm/src/scala/typo/dsl/SelectParams.scala +++ b/typo-dsl-anorm/src/scala/typo/dsl/SelectParams.scala @@ -1,56 +1,39 @@ package typo.dsl import typo.dsl.Fragment.FragmentStringInterpolator -import typo.dsl.internal.seeks import java.util.concurrent.atomic.AtomicInteger final case class SelectParams[Fields, Row]( where: List[Fields => SqlExpr[Boolean, Option]], - orderBy: List[Fields => SortOrderNoHkt[?]], - seeks: List[SelectParams.SeekNoHkt[Fields, ?]], + orderBy: List[OrderByOrSeek[Fields, ?]], offset: Option[Int], limit: Option[Int] ) { - def where(v: Fields => SqlExpr[Boolean, Option]): SelectParams[Fields, Row] = copy(where = where :+ v) - def orderBy(v: Fields => SortOrderNoHkt[?]): SelectParams[Fields, Row] = copy(orderBy = orderBy :+ v) - def seek(v: SelectParams.SeekNoHkt[Fields, ?]): SelectParams[Fields, Row] = copy(seeks = seeks :+ v) + def where(f: Fields => SqlExpr[Boolean, Option]): SelectParams[Fields, Row] = copy(where = where :+ f) + def orderBy[T, N[_]](f: Fields => SortOrder[T, N]): SelectParams[Fields, Row] = copy(orderBy = orderBy :+ OrderByOrSeek.OrderBy(f)) + def seek[T, N[_]](f: Fields => SortOrder[T, N], value: SqlExpr.Const[T, N]): SelectParams[Fields, Row] = copy(orderBy = orderBy :+ OrderByOrSeek.Seek(f, value)) def offset(v: Int): SelectParams[Fields, Row] = copy(offset = Some(v)) def limit(v: Int): SelectParams[Fields, Row] = copy(limit = Some(v)) } object SelectParams { def empty[Fields, R]: SelectParams[Fields, R] = - SelectParams[Fields, R](List.empty, List.empty, List.empty, None, None) - - sealed trait SeekNoHkt[Fields, NT] { - val f: Fields => SortOrderNoHkt[NT] - } - - case class Seek[Fields, T, N[_]](f: Fields => SortOrder[T, N], value: SqlExpr.Const[T, N]) extends SeekNoHkt[Fields, N[T]] - - def render[Fields, R](fields: Fields, baseSql: Fragment, ctx: RenderCtx, counter: AtomicInteger, params: SelectParams[Fields, R]): Fragment = { - val (filters, orderBys) = seeks.expand(fields, params) - - val maybeEnd: Option[Fragment] = - List[Option[Fragment]]( - filters.reduceLeftOption(_.and(_)).map { where => - Fragment(" where ") ++ where.render(ctx, counter) - }, - orderBys match { - case Nil => None - case nonEmpty => Some(frag" order by ${nonEmpty.map(x => x.render(ctx, counter)).mkFragment(", ")}") - }, - params.offset.map(value => Fragment(" offset " + value)), - params.limit.map { value => Fragment(" limit " + value) } - ).flatten.reduceOption(_ ++ _) - - val completeSql = maybeEnd match { - case Some(end) => frag"""|$baseSql - |$end - |""".stripMargin - case None => baseSql - } - completeSql + SelectParams[Fields, R](List.empty, List.empty, None, None) + + def render[Fields, R](fields: Fields, ctx: RenderCtx, counter: AtomicInteger, params: SelectParams[Fields, R]): Option[Fragment] = { + val (filters, orderBys) = OrderByOrSeek.expand(fields, params) + + List[Option[Fragment]]( + filters.reduceLeftOption(_.and(_)).map { where => + Fragment("where ") ++ where.render(ctx, counter) + }, + orderBys match { + case Nil => None + case nonEmpty => Some(frag"order by ${nonEmpty.map(x => x.render(ctx, counter)).mkFragment(", ")}") + }, + params.offset.map(value => Fragment("offset " + value)), + params.limit.map { value => Fragment("limit " + value) } + ).flatten.reduceOption(_ ++ Fragment(" ") ++ _) } } diff --git a/typo-dsl-anorm/src/scala/typo/dsl/SqlExpr.scala b/typo-dsl-anorm/src/scala/typo/dsl/SqlExpr.scala index f37dd4406..b52e5975f 100644 --- a/typo-dsl-anorm/src/scala/typo/dsl/SqlExpr.scala +++ b/typo-dsl-anorm/src/scala/typo/dsl/SqlExpr.scala @@ -107,7 +107,7 @@ object SqlExpr { val sqlReadCast: Option[String] val sqlWriteCast: Option[String] - final def value(ctx: RenderCtx): String = ctx.alias.get(path).fold("")(_ + ".") + name + final def value(ctx: RenderCtx): String = ctx.alias.get(path).fold("")(alias => s"($alias).") + name final def render(ctx: RenderCtx, counter: AtomicInteger): Fragment = Fragment(value(ctx)) } diff --git a/typo-dsl-doobie/src/scala/typo/dsl/SelectBuilder.scala b/typo-dsl-doobie/src/scala/typo/dsl/SelectBuilder.scala index f993fbed0..d3a0de462 100644 --- a/typo-dsl-doobie/src/scala/typo/dsl/SelectBuilder.scala +++ b/typo-dsl-doobie/src/scala/typo/dsl/SelectBuilder.scala @@ -49,11 +49,11 @@ trait SelectBuilder[Fields, Row] { withParams(params.orderBy(v)) final def seek[T, N[_]](v: Fields => SortOrder[T, N])(value: SqlExpr.Const[T, N]): SelectBuilder[Fields, Row] = - withParams(params.seek(SelectParams.Seek[Fields, T, N](v, value))) + withParams(params.seek(v, value)) - final def maybeSeek[T, N[_]](v: Fields => SortOrder[T, N])(maybeValue: Option[SqlExpr.Const[T, N]]): SelectBuilder[Fields, Row] = + final def maybeSeek[T, N[_]](v: Fields => SortOrder[T, N])(maybeValue: Option[N[T]])(implicit asConst: SqlExpr.Const.As[T, N]): SelectBuilder[Fields, Row] = maybeValue match { - case Some(value) => seek(v)(value) + case Some(value) => seek(v)(asConst(value)) case None => orderBy(v) } @@ -65,6 +65,8 @@ trait SelectBuilder[Fields, Row] { /** Execute the query and return the results as a list */ def toList: ConnectionIO[List[Row]] + def count: ConnectionIO[Int] + /** Return sql for debugging. [[None]] if backed by a mock repository */ def sql: Option[Fragment] diff --git a/typo-dsl-doobie/src/scala/typo/dsl/SelectBuilderMock.scala b/typo-dsl-doobie/src/scala/typo/dsl/SelectBuilderMock.scala index 90b7ed614..ac452bda8 100644 --- a/typo-dsl-doobie/src/scala/typo/dsl/SelectBuilderMock.scala +++ b/typo-dsl-doobie/src/scala/typo/dsl/SelectBuilderMock.scala @@ -3,7 +3,6 @@ package typo.dsl import doobie.free.connection.ConnectionIO import doobie.util.fragment.Fragment import typo.dsl.internal.mocks.RowOrdering -import typo.dsl.internal.seeks final case class SelectBuilderMock[Fields, Row]( structure: Structure[Fields, Row], @@ -21,6 +20,9 @@ final case class SelectBuilderMock[Fields, Row]( override def toList: ConnectionIO[List[Row]] = all.map(all => SelectBuilderMock.applyParams(structure, all, params)) + override def count: ConnectionIO[Int] = + all.map(all => SelectBuilderMock.applyParams(structure, all, params)).map(_.length) + override def joinOn[Fields2, N[_]: Nullability, Row2](other: SelectBuilder[Fields2, Row2])(pred: Fields ~ Fields2 => SqlExpr[Boolean, N]): SelectBuilderMock[Fields ~ Fields2, Row ~ Row2] = { val otherMock: SelectBuilderMock[Fields2, Row2] = other match { case x: SelectBuilderMock[Fields2, Row2] => x.withPath(Path.RightInJoin) @@ -74,7 +76,7 @@ final case class SelectBuilderMock[Fields, Row]( object SelectBuilderMock { def applyParams[Fields, R](structure: Structure[Fields, R], rows: List[R], params: SelectParams[Fields, R]): List[R] = { - val (filters, orderBys) = seeks.expand(structure.fields, params) + val (filters, orderBys) = OrderByOrSeek.expand(structure.fields, params) implicit val rowOrdering: Ordering[R] = new RowOrdering(structure, orderBys) rows .filter(row => filters.forall(expr => structure.untypedEval(expr, row).getOrElse(false))) diff --git a/typo-dsl-doobie/src/scala/typo/dsl/SelectBuilderSql.scala b/typo-dsl-doobie/src/scala/typo/dsl/SelectBuilderSql.scala index 05e8943b9..fd2507e55 100644 --- a/typo-dsl-doobie/src/scala/typo/dsl/SelectBuilderSql.scala +++ b/typo-dsl-doobie/src/scala/typo/dsl/SelectBuilderSql.scala @@ -1,23 +1,43 @@ package typo.dsl -import cats.data.NonEmptyList import cats.syntax.apply.* import cats.syntax.foldable.* import doobie.free.connection.ConnectionIO import doobie.implicits.toSqlInterpolator +import doobie.util.Read import doobie.util.fragment.Fragment -import doobie.util.query.Query0 -import doobie.util.{Read, fragments} +import typo.dsl.internal.mkFragment.* import scala.util.Try sealed trait SelectBuilderSql[Fields, Row] extends SelectBuilder[Fields, Row] { def withPath(path: Path): SelectBuilderSql[Fields, Row] def instantiate(ctx: RenderCtx): SelectBuilderSql.Instantiated[Fields, Row] - def sqlFor(ctx: RenderCtx): Query0[Row] override lazy val renderCtx: RenderCtx = RenderCtx.from(this) - override lazy val sql: Option[Fragment] = Some(sqlFor(renderCtx).toFragment) + + lazy val sqlAndRowParser: (Fragment, Read[Row]) = { + val instance = this.instantiate(renderCtx) + val cols = instance.columns.map { case (alias, col) => + col.sqlReadCast.foldLeft(s"($alias).\"${col.name}\"") { case (acc, cast) => s"$acc::$cast" } + } + + val ctes = instance.asCTEs + val formattedCTEs = ctes.map { cte => + fr"""|${Fragment.const0(cte.name)} as ( + | ${cte.sql} + |)""".stripMargin + } + + val frag = + fr"""|with + |${formattedCTEs.mkFragment(Fragment.const0(",\n"))} + |select ${Fragment.const0(cols.mkString(","))} from ${Fragment.const0(ctes.last.name)}""".stripMargin + + (frag, instance.read) + } + + override lazy val sql: Option[Fragment] = Some(sqlAndRowParser._1) final override def joinOn[Fields2, N[_]: Nullability, Row2](other: SelectBuilder[Fields2, Row2])(pred: Fields ~ Fields2 => SqlExpr[Boolean, N]): SelectBuilder[Fields ~ Fields2, Row ~ Row2] = other match { @@ -35,17 +55,23 @@ sealed trait SelectBuilderSql[Fields, Row] extends SelectBuilder[Fields, Row] { case _ => sys.error("you cannot mix mock and sql repos") } - final override def toList: ConnectionIO[List[Row]] = - sqlFor(renderCtx).to[List] + final override def toList: ConnectionIO[List[Row]] = { + val (frag, read) = sqlAndRowParser + frag.query(using read).to[List] + } + final override def count: ConnectionIO[Int] = { + val (frag, _) = sqlAndRowParser + fr"select count(*) from ($frag) rows".query[Int].unique + } } object SelectBuilderSql { def apply[Fields, Row]( name: String, structure: Structure.Relation[Fields, Row], - read: Read[Row] + rowParser: Read[Row] ): SelectBuilderSql[Fields, Row] = - Relation(name, structure, read, SelectParams.empty) + Relation(name, structure, rowParser, SelectParams.empty) final case class Relation[Fields, Row]( name: String, @@ -59,27 +85,19 @@ object SelectBuilderSql { override def withParams(sqlParams: SelectParams[Fields, Row]): SelectBuilder[Fields, Row] = copy(params = sqlParams) - private def sql(ctx: RenderCtx): Fragment = { - val cols = structure.columns - .map(x => Fragment.const0(x.sqlReadCast.foldLeft("\"" + x.name + "\"") { case (acc, cast) => s"$acc::$cast" })) - .intercalate(Fragment.const0(", ")) + override def instantiate(ctx: RenderCtx): SelectBuilderSql.Instantiated[Fields, Row] = { val alias = ctx.alias(structure._path) - val baseSql = fr"select $cols from ${Fragment.const0(name)} ${Fragment.const0(alias)}" - SelectParams.render(structure.fields, baseSql, ctx, params) - } + val sql = fr"(select ${Fragment.const0(alias)} from ${Fragment.const0(name)} ${Fragment.const0(alias)} ${SelectParams.render(structure.fields, ctx, params).getOrElse(Fragment.empty)})" - override def sqlFor(ctx: RenderCtx): Query0[Row] = - sql(ctx).query(using read) - - override def instantiate(ctx: RenderCtx): SelectBuilderSql.Instantiated[Fields, Row] = { - val part = SelectBuilderSql.InstantiatedPart( - alias = ctx.alias(structure._path), - columns = NonEmptyList.fromListUnsafe(structure.columns), - sqlFrag = sql(ctx), - joinFrag = Fragment.empty, - joinType = JoinType.Inner + SelectBuilderSql.Instantiated( + alias = alias, + isJoin = false, + columns = structure.columns.map(c => (alias, c)), + sqlFrag = sql, + upstreamCTEs = Nil, + structure = structure, + read = read ) - SelectBuilderSql.Instantiated(structure, NonEmptyList.of(part), read) } } @@ -99,45 +117,27 @@ object SelectBuilderSql { copy(params = sqlParams) override def instantiate(ctx: RenderCtx): Instantiated[Fields1 ~ Fields2, Row1 ~ Row2] = { - val leftInstantiated: Instantiated[Fields1, Row1] = left.instantiate(ctx) - val rightInstantiated: Instantiated[Fields2, Row2] = right.instantiate(ctx) - - val newStructure = leftInstantiated.structure.join(rightInstantiated.structure) - val newRightInstantiatedParts = rightInstantiated.parts - .mapLast(_.copy(joinFrag = pred(newStructure.fields).render(ctx), joinType = JoinType.Inner)) - + val alias = ctx.alias(structure._path) + val leftInstance = left.instantiate(ctx) + val rightInstance = right.instantiate(ctx) + val newStructure = leftInstance.structure.join(rightInstance.structure) + val ctes = leftInstance.asCTEs ++ rightInstance.asCTEs + val sql = + fr"""|select ${ctes.filterNot(_.isJoin).map(cte => Fragment.const0(cte.name)).mkFragment(Fragment.const0(", "))} + | from ${Fragment.const0(leftInstance.alias)} + | join ${Fragment.const0(rightInstance.alias)} + | on ${pred(newStructure.fields).render(ctx)} + | ${SelectParams.render(newStructure.fields, ctx, params).getOrElse(Fragment.empty)}""".stripMargin SelectBuilderSql.Instantiated( + alias = alias, + isJoin = true, + columns = leftInstance.columns ++ rightInstance.columns, + sqlFrag = sql, + upstreamCTEs = ctes, structure = newStructure, - parts = leftInstantiated.parts ++ newRightInstantiatedParts.toList, - read = (leftInstantiated.read, rightInstantiated.read).tupled + read = (leftInstance.read, rightInstance.read).tupled ) } - override def sqlFor(ctx: RenderCtx): Query0[Row1 ~ Row2] = { - val instance = instantiate(ctx) - val combinedFrag = instance.parts match { - case NonEmptyList(one, Nil) => one.sqlFrag - case NonEmptyList(first, rest) => - val prelude = - fr"""|select ${instance.columns.map(c => Fragment.const0(c.value(ctx))).intercalate(Fragment.const0(", "))} - |from ( - |${first.sqlFrag} - |) ${Fragment.const0(first.alias)} - |""".stripMargin - - val joins = rest.map { case SelectBuilderSql.InstantiatedPart(alias, _, sqlFrag, joinFrag, joinType) => - fr"""|${joinType.frag} ( - |$sqlFrag - |) ${Fragment.const0(alias)} on $joinFrag - |""".stripMargin - } - - prelude ++ joins.reduce(_ ++ _) - } - - val newCombinedFrag = SelectParams.render[Fields1 ~ Fields2, Row1 ~ Row2](instance.structure.fields, combinedFrag, ctx, params) - - newCombinedFrag.query(using instance.read) - } } final case class TableLeftJoin[Fields1, Fields2, N[_]: Nullability, Row1, Row2]( @@ -152,9 +152,7 @@ object SelectBuilderSql { override def withPath(path: Path): SelectBuilderSql[Fields1 ~ OuterJoined[Fields2], Row1 ~ Option[Row2]] = copy(left = left.withPath(path), right = right.withPath(path)) - override def withParams( - sqlParams: SelectParams[Fields1 ~ OuterJoined[Fields2], Row1 ~ Option[Row2]] - ): SelectBuilder[Fields1 ~ OuterJoined[Fields2], Row1 ~ Option[Row2]] = + override def withParams(sqlParams: SelectParams[Fields1 ~ OuterJoined[Fields2], Row1 ~ Option[Row2]]): SelectBuilder[Fields1 ~ OuterJoined[Fields2], Row1 ~ Option[Row2]] = copy(params = sqlParams) def opt[A](read: Read[A]): Read[Option[A]] = { @@ -165,82 +163,45 @@ object SelectBuilderSql { } override def instantiate(ctx: RenderCtx): Instantiated[Fields1 ~ OuterJoined[Fields2], Row1 ~ Option[Row2]] = { - val leftInstantiated = left.instantiate(ctx) - val rightInstantiated = right.instantiate(ctx) - - val newStructure = leftInstantiated.structure.leftJoin(rightInstantiated.structure) - val newRightInstantiatedParts = rightInstantiated.parts - .mapLast( - _.copy( - joinFrag = pred(leftInstantiated.structure.join(rightInstantiated.structure).fields).render(ctx), - joinType = JoinType.LeftJoin - ) - ) + val alias = ctx.alias(structure._path) + val leftInstance = left.instantiate(ctx) + val rightInstance = right.instantiate(ctx) + + val joinedStructure = leftInstance.structure.join(rightInstance.structure) + val newStructure = leftInstance.structure.leftJoin(rightInstance.structure) + val ctes = leftInstance.asCTEs ++ rightInstance.asCTEs + val sql = + fr"""|select ${ctes.filterNot(_.isJoin).map(cte => Fragment.const0(cte.name)).mkFragment(Fragment.const0(", "))} + | from ${Fragment.const0(leftInstance.alias)} + | left join ${Fragment.const0(rightInstance.alias)} + | on ${pred(joinedStructure.fields).render(ctx)} + | ${SelectParams.render(newStructure.fields, ctx, params).getOrElse(Fragment.empty)}""".stripMargin SelectBuilderSql.Instantiated( - newStructure, - leftInstantiated.parts ++ newRightInstantiatedParts.toList, - read = (leftInstantiated.read, opt(rightInstantiated.read)).tupled + alias = alias, + isJoin = true, + columns = leftInstance.columns ++ rightInstance.columns, + sqlFrag = sql, + upstreamCTEs = ctes, + structure = newStructure, + read = (leftInstance.read, opt(rightInstance.read)).tupled ) } - - override def sqlFor(ctx: RenderCtx): Query0[Row1 ~ Option[Row2]] = { - val instance = instantiate(ctx) - val combinedFrag = instance.parts match { - case NonEmptyList(one, Nil) => one.sqlFrag - case NonEmptyList(first, rest) => - val prelude = - fr"""|select ${fragments.comma(instance.columns.map(c => Fragment.const0(c.value(ctx))))} - |from ( - | ${first.sqlFrag} - |) ${Fragment.const0(first.alias)} - |""".stripMargin - - val joins = rest.map { case SelectBuilderSql.InstantiatedPart(alias, _, sqlFrag, joinFrag, joinType) => - fr"""|${joinType.frag} ( - |${sqlFrag} - |) ${Fragment.const0(alias)} on $joinFrag - |""".stripMargin - } - prelude ++ joins.reduce(_ ++ _) - } - val newCombinedFrag = - SelectParams.render[Fields1 ~ OuterJoined[Fields2], Row1 ~ Option[Row2]](instance.structure.fields, combinedFrag, ctx, params) - - newCombinedFrag.query(using instance.read) - } - } - - implicit class ListMapLastOps[T](private val ts: NonEmptyList[T]) extends AnyVal { - def mapLast(f: T => T): NonEmptyList[T] = NonEmptyList.fromListUnsafe(ts.toList.updated(ts.length - 1, f(ts.last))) } /** Need this intermediate data structure to generate aliases for tables (and prefixes for column selections) when we have a tree of joined tables. Need to start from the root after the user has * constructed the tree */ final case class Instantiated[Fields, Row]( + alias: String, + isJoin: Boolean, + columns: List[(String, SqlExpr.FieldLikeNoHkt[?, ?])], + sqlFrag: Fragment, + upstreamCTEs: List[CTE], structure: Structure[Fields, Row], - parts: NonEmptyList[SelectBuilderSql.InstantiatedPart], read: Read[Row] ) { - val columns: NonEmptyList[SqlExpr.FieldLikeNoHkt[?, ?]] = parts.flatMap(_.columns) - } - - sealed abstract class JoinType(_frag: String) { - val frag = Fragment.const0(_frag) + def asCTEs: List[CTE] = upstreamCTEs :+ CTE(alias, sqlFrag, isJoin) } - object JoinType { - case object Inner extends JoinType("join") - case object LeftJoin extends JoinType("left join") - case object RightJoin extends JoinType("right join") - } - - /** This is needlessly awkward because the we start with a tree, but we need to make it linear to render it */ - final case class InstantiatedPart( - alias: String, - columns: NonEmptyList[SqlExpr.FieldLikeNoHkt[?, ?]], - sqlFrag: Fragment, - joinFrag: Fragment, - joinType: JoinType - ) + case class CTE(name: String, sql: Fragment, isJoin: Boolean) } diff --git a/typo-dsl-doobie/src/scala/typo/dsl/SelectParams.scala b/typo-dsl-doobie/src/scala/typo/dsl/SelectParams.scala index 390b33353..4877d6f54 100644 --- a/typo-dsl-doobie/src/scala/typo/dsl/SelectParams.scala +++ b/typo-dsl-doobie/src/scala/typo/dsl/SelectParams.scala @@ -4,41 +4,32 @@ import cats.data.NonEmptyList import doobie.implicits.toSqlInterpolator import doobie.util.fragment.Fragment import doobie.util.fragments -import typo.dsl.internal.seeks final case class SelectParams[Fields, Row]( where: List[Fields => SqlExpr[Boolean, Option]], - orderBy: List[Fields => SortOrderNoHkt[?]], - seeks: List[SelectParams.SeekNoHkt[Fields, ?]], + orderBy: List[OrderByOrSeek[Fields, ?]], offset: Option[Int], limit: Option[Int] ) { def where(v: Fields => SqlExpr[Boolean, Option]): SelectParams[Fields, Row] = copy(where = where :+ v) - def orderBy(v: Fields => SortOrderNoHkt[?]): SelectParams[Fields, Row] = copy(orderBy = orderBy :+ v) - def seek(v: SelectParams.SeekNoHkt[Fields, ?]): SelectParams[Fields, Row] = copy(seeks = seeks :+ v) + def orderBy[T, N[_]](f: Fields => SortOrder[T, N]): SelectParams[Fields, Row] = copy(orderBy = orderBy :+ OrderByOrSeek.OrderBy(f)) + def seek[T, N[_]](f: Fields => SortOrder[T, N], value: SqlExpr.Const[T, N]): SelectParams[Fields, Row] = copy(orderBy = orderBy :+ OrderByOrSeek.Seek(f, value)) def offset(v: Int): SelectParams[Fields, Row] = copy(offset = Some(v)) def limit(v: Int): SelectParams[Fields, Row] = copy(limit = Some(v)) } object SelectParams { def empty[Fields, R]: SelectParams[Fields, R] = - SelectParams[Fields, R](List.empty, List.empty, List.empty, None, None) + SelectParams[Fields, R](List.empty, List.empty, None, None) - sealed trait SeekNoHkt[Fields, NT] { - val f: Fields => SortOrderNoHkt[NT] - } - - case class Seek[Fields, T, N[_]](f: Fields => SortOrder[T, N], value: SqlExpr.Const[T, N]) extends SeekNoHkt[Fields, N[T]] - - def render[Fields, R](fields: Fields, baseSql: Fragment, ctx: RenderCtx, params: SelectParams[Fields, R]): Fragment = { - val (filters, orderBys) = seeks.expand(fields, params) + def render[Fields, R](fields: Fields, ctx: RenderCtx, params: SelectParams[Fields, R]): Option[Fragment] = { + val (filters, orderBys) = OrderByOrSeek.expand(fields, params) List[Option[Fragment]]( - Some(baseSql), NonEmptyList.fromFoldable(filters.map(f => f.render(ctx))).map(fragments.whereAnd(_)), NonEmptyList.fromFoldable(orderBys.map(f => f.render(ctx))).map(fragments.orderBy(_)), params.offset.map(value => fr"offset $value"), params.limit.map(value => fr"limit $value") - ).flatten.reduce(_ ++ _) + ).flatten.reduceOption(_ ++ _) } } diff --git a/typo-dsl-doobie/src/scala/typo/dsl/SqlExpr.scala b/typo-dsl-doobie/src/scala/typo/dsl/SqlExpr.scala index 2345bf5da..95d1bc8b8 100644 --- a/typo-dsl-doobie/src/scala/typo/dsl/SqlExpr.scala +++ b/typo-dsl-doobie/src/scala/typo/dsl/SqlExpr.scala @@ -108,7 +108,7 @@ object SqlExpr { val set: (R, NT) => R val sqlReadCast: Option[String] val sqlWriteCast: Option[String] - final def value(ctx: RenderCtx): String = ctx.alias.get(path).fold("")(_ + ".") + name + final def value(ctx: RenderCtx): String = ctx.alias.get(path).fold("")(alias => s"($alias).") + name final def render(ctx: RenderCtx): Fragment = Fragment.const0(value(ctx)) } diff --git a/typo-dsl-shared/typo/dsl/internal/seeks.scala b/typo-dsl-shared/typo/dsl/OrderByOrSeek.scala similarity index 69% rename from typo-dsl-shared/typo/dsl/internal/seeks.scala rename to typo-dsl-shared/typo/dsl/OrderByOrSeek.scala index e411ff435..669cdd903 100644 --- a/typo-dsl-shared/typo/dsl/internal/seeks.scala +++ b/typo-dsl-shared/typo/dsl/OrderByOrSeek.scala @@ -1,18 +1,25 @@ -package typo.dsl.internal +package typo.dsl -import typo.dsl.SelectParams.Seek -import typo.dsl.* +sealed trait OrderByOrSeek[Fields, NT] { + val f: Fields => SortOrderNoHkt[NT] +} + +object OrderByOrSeek { + case class OrderBy[Fields, NT](f: Fields => SortOrderNoHkt[NT]) extends OrderByOrSeek[Fields, NT] + sealed trait SeekNoHkt[Fields, NT] extends OrderByOrSeek[Fields, NT] + case class Seek[Fields, T, N[_]](f: Fields => SortOrder[T, N], value: SqlExpr.Const[T, N]) extends SeekNoHkt[Fields, N[T]] -object seeks { - def expand[Fields, Row](fields: Fields, params: SelectParams[Fields, Row]): (List[SqlExpr[Boolean, Option]], List[SortOrderNoHkt[?]]) = - params.seeks match { - case Nil => (params.where.map(_.apply(fields)), params.orderBy.map(_.apply(fields))) - case nonEmpty => - require(params.orderBy.isEmpty, "Cannot have both seeks and orderBy") - val seekOrderBys: List[SortOrderNoHkt[?]] = - nonEmpty.map { case seek: Seek[Fields, _, _] @unchecked /* for 2.13*/ => seek.f(fields) } + def expand[Fields, Row](fields: Fields, params: SelectParams[Fields, Row]): (List[SqlExpr[Boolean, Option]], List[SortOrderNoHkt[?]]) = { + val seeks: List[SeekNoHkt[Fields, ?]] = + params.orderBy.collect { case x: OrderByOrSeek.SeekNoHkt[Fields, nt] => x } + + val maybeSeekPredicate: Option[SqlExpr[Boolean, Option]] = + seeks match { + case Nil => None + case nonEmpty => + val seekOrderBys: List[SortOrderNoHkt[?]] = + nonEmpty.map { case seek: Seek[Fields, _, _] @unchecked /* for 2.13*/ => seek.f(fields) } - val seekWhere: SqlExpr[Boolean, Option] = seekOrderBys.map(_.ascending).distinct match { case List(uniformIsAscending) => val dbTuple = SqlExpr.RowExpr(seekOrderBys.map(so => so.expr)) @@ -29,7 +36,7 @@ object seeks { .find(_ != 0) .getOrElse(0) - if (uniformIsAscending) dbTuple > valueTuple else dbTuple < valueTuple + Some(if (uniformIsAscending) dbTuple > valueTuple else dbTuple < valueTuple) case _ => val orConditions: Seq[SqlExpr[Boolean, Option]] = nonEmpty.indices.map { i => @@ -52,9 +59,9 @@ object seeks { } } - orConditions.reduce(_ or _) + Some(orConditions.reduce(_ or _)) } - - (params.where.map(_.apply(fields)) :+ seekWhere, seekOrderBys) - } + } + (params.where.map(_.apply(fields)) ++ maybeSeekPredicate, params.orderBy.map(_.f(fields))) + } } diff --git a/typo-dsl-shared/typo/dsl/RenderCtx.scala b/typo-dsl-shared/typo/dsl/RenderCtx.scala index c410b5ffb..27c0ec83a 100644 --- a/typo-dsl-shared/typo/dsl/RenderCtx.scala +++ b/typo-dsl-shared/typo/dsl/RenderCtx.scala @@ -11,9 +11,9 @@ object RenderCtx { def from[F, R](s: SelectBuilderSql[F, R]): RenderCtx = { def findPathsAndTableNames(s: SelectBuilderSql[?, ?]): List[(List[Path], String)] = s match { - case SelectBuilderSql.Relation(name, structure, _, _) => List(structure._path -> name) - case SelectBuilderSql.TableJoin(left, right, _, _) => findPathsAndTableNames(left) ++ findPathsAndTableNames(right) - case SelectBuilderSql.TableLeftJoin(left, right, _, _) => findPathsAndTableNames(left) ++ findPathsAndTableNames(right) + case SelectBuilderSql.Relation(name, structure, _, _) => List(structure._path -> name) + case x @ SelectBuilderSql.TableJoin(left, right, _, _) => List(x.structure._path -> "join_cte") ++ findPathsAndTableNames(left) ++ findPathsAndTableNames(right) + case x @ SelectBuilderSql.TableLeftJoin(left, right, _, _) => List(x.structure._path -> "left_join_cte") ++ findPathsAndTableNames(left) ++ findPathsAndTableNames(right) } val nameForPathsMap: Map[List[Path], String] = diff --git a/typo-dsl-shared/typo/dsl/Structure.scala b/typo-dsl-shared/typo/dsl/Structure.scala index dc79937c9..b9201633c 100644 --- a/typo-dsl-shared/typo/dsl/Structure.scala +++ b/typo-dsl-shared/typo/dsl/Structure.scala @@ -7,7 +7,7 @@ package typo.dsl trait Structure[Fields, Row] { def fields: Fields def columns: List[SqlExpr.FieldLikeNoHkt[?, ?]] - + def _path: List[Path] def withPath(path: Path): Structure[Fields, Row] // It's up to you to ensure that the `Row` in `field` is the same type as `row` @@ -64,21 +64,23 @@ trait Structure[Fields, Row] { } final def join[Fields2, Row2](other: Structure[Fields2, Row2]): Structure[Fields ~ Fields2, Row ~ Row2] = - new Structure.Tupled(left = this, right = other) + new Structure.Tupled(Structure.sharedPrefix(this._path, other._path), left = this, right = other) final def leftJoin[Fields2, Row2](other: Structure[Fields2, Row2]): Structure[Fields ~ OuterJoined[Fields2], Row ~ Option[Row2]] = - new Structure.LeftTupled(left = this, right = other) + new Structure.LeftTupled(Structure.sharedPrefix(this._path, other._path), left = this, right = other) } object Structure { + def sharedPrefix[T](left: List[T], right: List[T]): List[T] = { + val prefix = left.zip(right).takeWhile { case (a, b) => a == b } + prefix.map(_._1) + } // some of the row types are discarded, exchanging some type-safety for a cleaner API implicit class FieldOps[T, N[_], R0](private val field: SqlExpr.FieldLike[T, N, R0]) extends AnyVal { def castRow[R1]: SqlExpr.FieldLike[T, N, R1] = field.asInstanceOf[SqlExpr.FieldLike[T, N, R1]] } trait Relation[Fields, Row] extends Structure[Fields, Row] { outer => - val _path: List[Path] - def copy(path: List[Path]): Relation[Fields, Row] override def withPath(newPath: Path): Relation[Fields, Row] = @@ -88,7 +90,7 @@ object Structure { field.castRow[Row].get(row) } - private class Tupled[Fields1, Fields2, Row1, Row2](val left: Structure[Fields1, Row1], val right: Structure[Fields2, Row2]) extends Structure[Fields1 ~ Fields2, Row1 ~ Row2] { + private class Tupled[Fields1, Fields2, Row1, Row2](val _path: List[Path], val left: Structure[Fields1, Row1], val right: Structure[Fields2, Row2]) extends Structure[Fields1 ~ Fields2, Row1 ~ Row2] { override val fields: Fields1 ~ Fields2 = (left.fields, right.fields) @@ -99,11 +101,11 @@ object Structure { if (left.columns.contains(field)) left.untypedGet(field.castRow, row._1) else right.untypedGet(field.castRow, row._2) - override def withPath(path: Path): Tupled[Fields1, Fields2, Row1, Row2] = - new Tupled(left.withPath(path), right.withPath(path)) + override def withPath(newPath: Path): Tupled[Fields1, Fields2, Row1, Row2] = + new Tupled(newPath :: _path, left.withPath(newPath), right.withPath(newPath)) } - private class LeftTupled[Fields1, Fields2, Row1, Row2](val left: Structure[Fields1, Row1], val right: Structure[Fields2, Row2]) + private class LeftTupled[Fields1, Fields2, Row1, Row2](val _path: List[Path], val left: Structure[Fields1, Row1], val right: Structure[Fields2, Row2]) extends Structure[Fields1 ~ OuterJoined[Fields2], Row1 ~ Option[Row2]] { override val fields: Fields1 ~ OuterJoined[Fields2] = @@ -125,7 +127,7 @@ object Structure { flattened.asInstanceOf[N[T]] } - override def withPath(path: Path): LeftTupled[Fields1, Fields2, Row1, Row2] = - new LeftTupled(left.withPath(path), right.withPath(path)) + override def withPath(newPath: Path): LeftTupled[Fields1, Fields2, Row1, Row2] = + new LeftTupled(newPath :: _path, left.withPath(newPath), right.withPath(newPath)) } } diff --git a/typo-dsl-zio-jdbc/src/scala/typo/dsl/SelectBuilder.scala b/typo-dsl-zio-jdbc/src/scala/typo/dsl/SelectBuilder.scala index bc744011f..5470b8217 100644 --- a/typo-dsl-zio-jdbc/src/scala/typo/dsl/SelectBuilder.scala +++ b/typo-dsl-zio-jdbc/src/scala/typo/dsl/SelectBuilder.scala @@ -49,11 +49,11 @@ trait SelectBuilder[Fields, Row] { withParams(params.orderBy(v)) final def seek[T, N[_]](v: Fields => SortOrder[T, N])(value: SqlExpr.Const[T, N]): SelectBuilder[Fields, Row] = - withParams(params.seek(SelectParams.Seek[Fields, T, N](v, value))) + withParams(params.seek(v, value)) - final def maybeSeek[T, N[_]](v: Fields => SortOrder[T, N])(maybeValue: Option[SqlExpr.Const[T, N]]): SelectBuilder[Fields, Row] = + final def maybeSeek[T, N[_]](v: Fields => SortOrder[T, N])(maybeValue: Option[N[T]])(implicit asConst: SqlExpr.Const.As[T, N]): SelectBuilder[Fields, Row] = maybeValue match { - case Some(value) => seek(v)(value) + case Some(value) => seek(v)(asConst(value)) case None => orderBy(v) } @@ -65,6 +65,8 @@ trait SelectBuilder[Fields, Row] { /** Execute the query and return the results as a list */ def toChunk: ZIO[ZConnection, Throwable, Chunk[Row]] + def count: ZIO[ZConnection, Throwable, Int] + /** Return sql for debugging. [[None]] if backed by a mock repository */ def sql: Option[SqlFragment] diff --git a/typo-dsl-zio-jdbc/src/scala/typo/dsl/SelectBuilderMock.scala b/typo-dsl-zio-jdbc/src/scala/typo/dsl/SelectBuilderMock.scala index 47841ba35..7d140bc65 100644 --- a/typo-dsl-zio-jdbc/src/scala/typo/dsl/SelectBuilderMock.scala +++ b/typo-dsl-zio-jdbc/src/scala/typo/dsl/SelectBuilderMock.scala @@ -1,6 +1,5 @@ package typo.dsl -import typo.dsl.internal.seeks import typo.dsl.internal.mocks.RowOrdering import zio.{Chunk, ZIO} import zio.jdbc.* @@ -21,6 +20,9 @@ final case class SelectBuilderMock[Fields, Row]( override def toChunk: ZIO[ZConnection, Throwable, Chunk[Row]] = all.map(all => SelectBuilderMock.applyParams(structure, all, params)) + override def count: ZIO[ZConnection, Throwable, Int] = + all.map(all => SelectBuilderMock.applyParams(structure, all, params).length) + override def joinOn[Fields2, N[_]: Nullability, Row2](other: SelectBuilder[Fields2, Row2])(pred: Fields ~ Fields2 => SqlExpr[Boolean, N]): SelectBuilderMock[Fields ~ Fields2, Row ~ Row2] = { val otherMock: SelectBuilderMock[Fields2, Row2] = other match { case x: SelectBuilderMock[Fields2, Row2] => x.withPath(Path.RightInJoin) @@ -75,7 +77,7 @@ final case class SelectBuilderMock[Fields, Row]( object SelectBuilderMock { def applyParams[Fields, R](structure: Structure[Fields, R], rows: Chunk[R], params: SelectParams[Fields, R]): Chunk[R] = { - val (filters, orderBys) = seeks.expand(structure.fields, params) + val (filters, orderBys) = OrderByOrSeek.expand(structure.fields, params) implicit val rowOrdering: Ordering[R] = new RowOrdering(structure, orderBys) rows .filter(row => filters.forall(expr => structure.untypedEval(expr, row).getOrElse(false))) diff --git a/typo-dsl-zio-jdbc/src/scala/typo/dsl/SelectBuilderSql.scala b/typo-dsl-zio-jdbc/src/scala/typo/dsl/SelectBuilderSql.scala index 390e1cd43..42c0d3eaa 100644 --- a/typo-dsl-zio-jdbc/src/scala/typo/dsl/SelectBuilderSql.scala +++ b/typo-dsl-zio-jdbc/src/scala/typo/dsl/SelectBuilderSql.scala @@ -1,15 +1,36 @@ package typo.dsl import zio.jdbc.* -import zio.{Chunk, NonEmptyChunk, ZIO} +import zio.{Chunk, ZIO} sealed trait SelectBuilderSql[Fields, Row] extends SelectBuilder[Fields, Row] { def withPath(path: Path): SelectBuilderSql[Fields, Row] def instantiate(ctx: RenderCtx): SelectBuilderSql.Instantiated[Fields, Row] - def sqlFor(ctx: RenderCtx): Query[Row] override lazy val renderCtx: RenderCtx = RenderCtx.from(this) - override def sql: Option[SqlFragment] = Some(sqlFor(renderCtx).sql) + + lazy val sqlAndRowParser: (SqlFragment, JdbcDecoder[Row]) = { + val instance = this.instantiate(renderCtx) + val cols = instance.columns.map { case (alias, x) => + x.sqlReadCast.foldLeft(s"($alias).\"${x.name}\"") { case (acc, cast) => s"$acc::$cast" } + } + + val ctes = instance.asCTEs + val formattedCTEs = ctes.map { cte => + sql"""${SqlFragment(cte.name)} as ( + ${cte.sql} +)""" + } + + val frag = + sql"""with +${formattedCTEs.mkFragment(SqlFragment(",\n"))} +select ${SqlFragment(cols.mkString(","))} from ${SqlFragment(ctes.last.name)}""" + + (frag, instance.read) + } + + override lazy val sql: Option[SqlFragment] = Some(sqlAndRowParser._1) final override def joinOn[Fields2, N[_]: Nullability, Row2](other: SelectBuilder[Fields2, Row2])(pred: Fields ~ Fields2 => SqlExpr[Boolean, N]): SelectBuilder[Fields ~ Fields2, Row ~ Row2] = other match { @@ -27,17 +48,23 @@ sealed trait SelectBuilderSql[Fields, Row] extends SelectBuilder[Fields, Row] { case _ => sys.error("you cannot mix mock and sql repos") } - final override def toChunk: ZIO[ZConnection, Throwable, Chunk[Row]] = - sqlFor(renderCtx).selectAll + final override def toChunk: ZIO[ZConnection, Throwable, Chunk[Row]] = { + val (frag, read) = sqlAndRowParser + frag.query(using read).selectAll + } + final override def count: ZIO[ZConnection, Throwable, Int] = { + val (frag, _) = sqlAndRowParser + sql"select count(*) from ($frag) rows".query[Int].selectOne.map(_.get) + } } object SelectBuilderSql { def apply[Fields, Row]( name: String, structure: Structure.Relation[Fields, Row], - read: JdbcDecoder[Row] + rowParser: JdbcDecoder[Row] ): SelectBuilderSql[Fields, Row] = - Relation(name, structure, read, SelectParams.empty) + Relation(name, structure, rowParser, SelectParams.empty) final case class Relation[Fields, Row]( name: String, @@ -51,27 +78,19 @@ object SelectBuilderSql { override def withParams(sqlParams: SelectParams[Fields, Row]): SelectBuilder[Fields, Row] = copy(params = sqlParams) - private def sql(ctx: RenderCtx): SqlFragment = { - val cols = structure.columns - .map(x => SqlFragment(x.sqlReadCast.foldLeft("\"" + x.name + "\"") { case (acc, cast) => s"$acc::$cast" })) - .mkFragment(", ") + override def instantiate(ctx: RenderCtx): SelectBuilderSql.Instantiated[Fields, Row] = { val alias = ctx.alias(structure._path) - val baseSql = sql"select $cols from ${SqlFragment(name)} ${SqlFragment(alias)}" - SelectParams.render(structure.fields, baseSql, ctx, params) - } - - override def sqlFor(ctx: RenderCtx): Query[Row] = - sql(ctx).query[Row](using read) + val sql = sql"(select ${SqlFragment(alias)} from ${SqlFragment(name)} ${SqlFragment(alias)} ${SelectParams.render(structure.fields, ctx, params).getOrElse[SqlFragment](SqlFragment.empty)})" - override def instantiate(ctx: RenderCtx): SelectBuilderSql.Instantiated[Fields, Row] = { - val part = SelectBuilderSql.InstantiatedPart( - alias = ctx.alias(structure._path), - columns = NonEmptyChunk.fromIterableOption(structure.columns).get, - sqlFrag = sql(ctx), - joinFrag = SqlFragment.empty, - joinType = JoinType.Inner + SelectBuilderSql.Instantiated( + alias = alias, + isJoin = false, + columns = structure.columns.map(c => (alias, c)), + sqlFrag = sql, + upstreamCTEs = Nil, + structure = structure, + read = read ) - SelectBuilderSql.Instantiated(structure, NonEmptyChunk.single(part), read) } } @@ -91,54 +110,29 @@ object SelectBuilderSql { copy(params = sqlParams) override def instantiate(ctx: RenderCtx): Instantiated[Fields1 ~ Fields2, Row1 ~ Row2] = { - val leftInstantiated: Instantiated[Fields1, Row1] = left.instantiate(ctx) - val rightInstantiated: Instantiated[Fields2, Row2] = right.instantiate(ctx) - - val newStructure = leftInstantiated.structure.join(rightInstantiated.structure) - val newRightInstantiatedParts = rightInstantiated.parts - .mapLast( - _.copy( - joinFrag = pred(newStructure.fields).render(ctx), - joinType = JoinType.Inner - ) - ) + val alias = ctx.alias(structure._path) + val leftInstance = left.instantiate(ctx) + val rightInstance = right.instantiate(ctx) + val newStructure = leftInstance.structure.join(rightInstance.structure) + val ctes = leftInstance.asCTEs ++ rightInstance.asCTEs + val renderedCtes = ctes.filterNot(_.isJoin).map(cte => SqlFragment(cte.name)).mkFragment(SqlFragment(", ")) + val sql = + sql"""select $renderedCtes + from ${SqlFragment(leftInstance.alias)} + join ${SqlFragment(rightInstance.alias)} + on ${pred(newStructure.fields).render(ctx)} + ${SelectParams.render(newStructure.fields, ctx, params).getOrElse[SqlFragment](SqlFragment.empty)}""" SelectBuilderSql.Instantiated( + alias = alias, + isJoin = true, + columns = leftInstance.columns ++ rightInstance.columns, + sqlFrag = sql, + upstreamCTEs = ctes, structure = newStructure, - parts = leftInstantiated.parts ++ newRightInstantiatedParts, - decoder = JdbcDecoder.tuple2Decoder(leftInstantiated.decoder, rightInstantiated.decoder) + read = JdbcDecoder.tuple2Decoder(leftInstance.read, rightInstance.read) ) } - override def sqlFor(ctx: RenderCtx): Query[Row1 ~ Row2] = { - val instance = instantiate(ctx) - val combinedFrag = { - val size = instance.parts.size - if (size == 1) instance.parts.head.sqlFrag - else { - val first = instance.parts.head - val rest = instance.parts.tail - - val prelude = - sql"""select ${instance.columns.map(c => c.render(ctx)).mkFragment(", ")} - from ( - ${first.sqlFrag} - ) ${SqlFragment(first.alias)} - """ - - val joins = rest.map { case SelectBuilderSql.InstantiatedPart(alias, _, sqlFrag, joinFrag, joinType) => - sql"""${joinType.frag} ( - $sqlFrag - ) ${SqlFragment(alias)} on $joinFrag - """ - } - - prelude ++ joins.reduce(_ ++ _) - } - } - val newCombinedFrag = SelectParams.render[Fields1 ~ Fields2, Row1 ~ Row2](instance.structure.fields, combinedFrag, ctx, params) - - newCombinedFrag.query(using instance.decoder) - } } final case class TableLeftJoin[Fields1, Fields2, N[_]: Nullability, Row1, Row2]( @@ -153,92 +147,49 @@ object SelectBuilderSql { override def withPath(path: Path): SelectBuilderSql[Fields1 ~ OuterJoined[Fields2], Row1 ~ Option[Row2]] = copy(left = left.withPath(path), right = right.withPath(path)) - override def withParams( - sqlParams: SelectParams[Fields1 ~ OuterJoined[Fields2], Row1 ~ Option[Row2]] - ): SelectBuilder[Fields1 ~ OuterJoined[Fields2], Row1 ~ Option[Row2]] = + override def withParams(sqlParams: SelectParams[Fields1 ~ OuterJoined[Fields2], Row1 ~ Option[Row2]]): SelectBuilder[Fields1 ~ OuterJoined[Fields2], Row1 ~ Option[Row2]] = copy(params = sqlParams) override def instantiate(ctx: RenderCtx): Instantiated[Fields1 ~ OuterJoined[Fields2], Row1 ~ Option[Row2]] = { - val leftInstantiated = left.instantiate(ctx) - val rightInstantiated = right.instantiate(ctx) - - val newStructure = leftInstantiated.structure.leftJoin(rightInstantiated.structure) - val newRightInstantiatedParts = rightInstantiated.parts - .mapLast( - _.copy( - joinFrag = pred(leftInstantiated.structure.join(rightInstantiated.structure).fields).render(ctx), - joinType = JoinType.LeftJoin - ) - ) + val alias = ctx.alias(structure._path) + val leftInstance = left.instantiate(ctx) + val rightInstance = right.instantiate(ctx) + + val joinedStructure = leftInstance.structure.join(rightInstance.structure) + val newStructure = leftInstance.structure.leftJoin(rightInstance.structure) + val ctes = leftInstance.asCTEs ++ rightInstance.asCTEs + val sql = + sql"""select ${ctes.filterNot(_.isJoin).map(cte => SqlFragment(cte.name)).mkFragment(SqlFragment(", "))} + from ${SqlFragment(leftInstance.alias)} + left join ${SqlFragment(rightInstance.alias)} + on ${pred(joinedStructure.fields).render(ctx)} + ${SelectParams.render(newStructure.fields, ctx, params).getOrElse[SqlFragment](SqlFragment.empty)}""" SelectBuilderSql.Instantiated( + alias = alias, + isJoin = true, + columns = leftInstance.columns ++ rightInstance.columns, + sqlFrag = sql, + upstreamCTEs = ctes, structure = newStructure, - parts = leftInstantiated.parts ++ newRightInstantiatedParts, - decoder = JdbcDecoder.tuple2Decoder(leftInstantiated.decoder, JdbcDecoder.optionDecoder(rightInstantiated.decoder)) + read = JdbcDecoder.tuple2Decoder(leftInstance.read, JdbcDecoder.optionDecoder(rightInstance.read)) ) } - - override def sqlFor(ctx: RenderCtx): Query[Row1 ~ Option[Row2]] = { - val instance = instantiate(ctx) - val combinedFrag = { - val size = instance.parts.size - if (size == 1) instance.parts.head.sqlFrag - else { - val first = instance.parts.head - val rest = instance.parts.tail - - val prelude = - sql"""select ${instance.columns.map(c => c.render(ctx)).mkFragment(", ")} - from ( - ${first.sqlFrag} - ) ${SqlFragment(first.alias)} - """ - - val joins = rest.map { case SelectBuilderSql.InstantiatedPart(alias, _, sqlFrag, joinFrag, joinType) => - sql"""${joinType.frag} ( - ${sqlFrag} - ) ${SqlFragment(alias)} on $joinFrag - """ - } - prelude ++ joins.reduce(_ ++ _) - } - } - val newCombinedFrag = - SelectParams.render[Fields1 ~ OuterJoined[Fields2], Row1 ~ Option[Row2]](instance.structure.fields, combinedFrag, ctx, params) - - newCombinedFrag.query(using instance.decoder) - } - } - - implicit class ListMapLastOps[T](private val ts: NonEmptyChunk[T]) extends AnyVal { - def mapLast(f: T => T): NonEmptyChunk[T] = NonEmptyChunk.fromChunk(ts.updated(ts.length - 1, f(ts.last))).get // unsafe } /** Need this intermediate data structure to generate aliases for tables (and prefixes for column selections) when we have a tree of joined tables. Need to start from the root after the user has * constructed the tree */ final case class Instantiated[Fields, Row]( + alias: String, + isJoin: Boolean, + columns: List[(String, SqlExpr.FieldLikeNoHkt[?, ?])], + sqlFrag: SqlFragment, + upstreamCTEs: List[CTE], structure: Structure[Fields, Row], - parts: NonEmptyChunk[SelectBuilderSql.InstantiatedPart], - decoder: JdbcDecoder[Row] + read: JdbcDecoder[Row] ) { - val columns: NonEmptyChunk[SqlExpr.FieldLikeNoHkt[?, ?]] = parts.flatMap(_.columns) - } - sealed abstract class JoinType(_frag: String) { - val frag = SqlFragment(_frag) - } - object JoinType { - case object Inner extends JoinType("join") - case object LeftJoin extends JoinType("left join") - case object RightJoin extends JoinType("right join") + def asCTEs: List[CTE] = upstreamCTEs :+ CTE(alias, sqlFrag, isJoin) } - - /** This is needlessly awkward because the we start with a tree, but we need to make it linear to render it */ - final case class InstantiatedPart( - alias: String, - columns: NonEmptyChunk[SqlExpr.FieldLikeNoHkt[?, ?]], - sqlFrag: SqlFragment, - joinFrag: SqlFragment, - joinType: JoinType - ) + case class CTE(name: String, sql: SqlFragment, isJoin: Boolean) } diff --git a/typo-dsl-zio-jdbc/src/scala/typo/dsl/SelectParams.scala b/typo-dsl-zio-jdbc/src/scala/typo/dsl/SelectParams.scala index 577d7fc87..00143903d 100644 --- a/typo-dsl-zio-jdbc/src/scala/typo/dsl/SelectParams.scala +++ b/typo-dsl-zio-jdbc/src/scala/typo/dsl/SelectParams.scala @@ -1,41 +1,32 @@ package typo.dsl -import typo.dsl.internal.seeks import zio.NonEmptyChunk import zio.jdbc.* final case class SelectParams[Fields, Row]( where: List[Fields => SqlExpr[Boolean, Option]], - orderBy: List[Fields => SortOrderNoHkt[?]], - seeks: List[SelectParams.SeekNoHkt[Fields, ?]], + orderBy: List[OrderByOrSeek[Fields, ?]], offset: Option[Int], limit: Option[Int] ) { def where(v: Fields => SqlExpr[Boolean, Option]): SelectParams[Fields, Row] = copy(where = where :+ v) - def orderBy(v: Fields => SortOrderNoHkt[?]): SelectParams[Fields, Row] = copy(orderBy = orderBy :+ v) - def seek(v: SelectParams.SeekNoHkt[Fields, ?]): SelectParams[Fields, Row] = copy(seeks = seeks :+ v) + def orderBy[T, N[_]](f: Fields => SortOrder[T, N]): SelectParams[Fields, Row] = copy(orderBy = orderBy :+ OrderByOrSeek.OrderBy(f)) + def seek[T, N[_]](f: Fields => SortOrder[T, N], value: SqlExpr.Const[T, N]): SelectParams[Fields, Row] = copy(orderBy = orderBy :+ OrderByOrSeek.Seek(f, value)) def offset(v: Int): SelectParams[Fields, Row] = copy(offset = Some(v)) def limit(v: Int): SelectParams[Fields, Row] = copy(limit = Some(v)) } object SelectParams { def empty[Fields, R]: SelectParams[Fields, R] = - SelectParams[Fields, R](List.empty, List.empty, List.empty, None, None) + SelectParams[Fields, R](List.empty, List.empty, None, None) - sealed trait SeekNoHkt[Fields, NT] { - val f: Fields => SortOrderNoHkt[NT] - } - - case class Seek[Fields, T, N[_]](f: Fields => SortOrder[T, N], value: SqlExpr.Const[T, N]) extends SeekNoHkt[Fields, N[T]] - - def render[Fields, R](fields: Fields, baseSql: SqlFragment, ctx: RenderCtx, params: SelectParams[Fields, R]): SqlFragment = { - val (filters, orderBys) = seeks.expand(fields, params) + def render[Fields, R](fields: Fields, ctx: RenderCtx, params: SelectParams[Fields, R]): Option[SqlFragment] = { + val (filters, orderBys) = OrderByOrSeek.expand(fields, params) List[Option[SqlFragment]]( - Some(baseSql), NonEmptyChunk.fromIterableOption(filters.map(f => f.render(ctx))).map(fs => fs.mkFragment(" WHERE ", " AND ", "")), NonEmptyChunk.fromIterableOption(orderBys.map(f => f.render(ctx))).map(fs => fs.mkFragment(" ORDER BY ", ", ", "")), params.offset.map(value => sql" offset $value"), params.limit.map(value => sql" limit $value") - ).flatten.reduce(_ ++ _) + ).flatten.reduceOption(_ ++ _) } } diff --git a/typo-dsl-zio-jdbc/src/scala/typo/dsl/SqlExpr.scala b/typo-dsl-zio-jdbc/src/scala/typo/dsl/SqlExpr.scala index 4893faf4c..94bc5b291 100644 --- a/typo-dsl-zio-jdbc/src/scala/typo/dsl/SqlExpr.scala +++ b/typo-dsl-zio-jdbc/src/scala/typo/dsl/SqlExpr.scala @@ -104,7 +104,7 @@ object SqlExpr { val set: (R, NT) => R val sqlReadCast: Option[String] val sqlWriteCast: Option[String] - final def value(ctx: RenderCtx): String = ctx.alias.get(path).fold("")(_ + ".") + name + final def value(ctx: RenderCtx): String = ctx.alias.get(path).fold("")(alias => s"($alias).") + name final def render(ctx: RenderCtx): SqlFragment = SqlFragment(value(ctx)) } diff --git a/typo-tester-anorm/src/scala/adventureworks/DSLTest.scala b/typo-tester-anorm/src/scala/adventureworks/DSLTest.scala new file mode 100644 index 000000000..5b9689223 --- /dev/null +++ b/typo-tester-anorm/src/scala/adventureworks/DSLTest.scala @@ -0,0 +1,50 @@ +package adventureworks + +import adventureworks.customtypes.* +import adventureworks.humanresources.employee.EmployeeRepoImpl +import adventureworks.person.businessentity.BusinessentityRepoImpl +import adventureworks.person.emailaddress.EmailaddressRepoImpl +import adventureworks.person.person.PersonRepoImpl +import adventureworks.sales.salesperson.SalespersonRepoImpl +import adventureworks.userdefined.FirstName + +import scala.annotation.nowarn +import scala.util.Random + +class DSLTest extends SnapshotTest { + val businessentityRepoImpl = new BusinessentityRepoImpl + val personRepoImpl = new PersonRepoImpl + val employeeRepoImpl = new EmployeeRepoImpl() + val salespersonRepoImpl = new SalespersonRepoImpl + val emailaddressRepoImpl = new EmailaddressRepoImpl + + test("works") { + withConnection { implicit c => + val testInsert = new TestInsert(new Random(0), DomainInsert) + val businessentityRow = testInsert.personBusinessentity() + val personRow = testInsert.personPerson(businessentityRow.businessentityid, persontype = "EM", FirstName("a")) + testInsert.personEmailaddress(personRow.businessentityid, Some("a@b.c")): @nowarn + val employeeRow = + testInsert.humanresourcesEmployee(personRow.businessentityid, gender = "M", maritalstatus = "M", birthdate = TypoLocalDate("1998-01-01"), hiredate = TypoLocalDate("1997-01-01")) + val salespersonRow = testInsert.salesSalesperson(employeeRow.businessentityid) + + def q = salespersonRepoImpl.select + .where(_.rowguid === salespersonRow.rowguid) + .joinFk(_.fkHumanresourcesEmployee)(employeeRepoImpl.select) + .joinFk { case (_, e) => e.fkPersonPerson }(personRepoImpl.select) + .joinFk { case ((_, _), p) => p.fkBusinessentity }(businessentityRepoImpl.select) + .join(emailaddressRepoImpl.select.orderBy(_.rowguid.asc)) + .on { case ((_, b), email) => email.businessentityid === b.businessentityid } + .joinOn(salespersonRepoImpl.select) { case ((_, e), s2) => + e.businessentityid.underlying === s2.businessentityid.underlying + } + + val doubled = q.join(q).on { case (((_, e1), _), ((_, e2), _)) => e1.businessentityid === e2.businessentityid } + + doubled.toList.foreach(println) + assert(doubled.count == 1): @nowarn + + compareFragment("doubled")(doubled.sql) + } + } +} diff --git a/typo-tester-anorm/src/scala/adventureworks/PaginationTest.scala b/typo-tester-anorm/src/scala/adventureworks/PaginationTest.scala index 79d178ea6..314a98239 100644 --- a/typo-tester-anorm/src/scala/adventureworks/PaginationTest.scala +++ b/typo-tester-anorm/src/scala/adventureworks/PaginationTest.scala @@ -40,7 +40,7 @@ class PaginationTest extends AnyFunSuite with TypeCheckedTripleEquals { def patch(c: ClientCursor[JsValue]): ClientCursor[JsValue] = businessentityRepo match { case _: BusinessentityRepoMock => - c.copy(parts = c.parts.map { case (k, v) => (SortOrderRepr(k.expr.replace("businessentity0.", "")), v) }) + c.copy(parts = c.parts.map { case (k, v) => (SortOrderRepr(k.expr.replace("(businessentity0).", "")), v) }) case _ => c } @@ -74,8 +74,8 @@ class PaginationTest extends AnyFunSuite with TypeCheckedTripleEquals { patch( ClientCursor( Map( - SortOrderRepr("businessentity0.modifieddate") -> JsString("2020-12-29T00:00:00"), - SortOrderRepr("(businessentity0.businessentityid - {param1}::INTEGER):2") -> JsNumber(BigDecimal(1)) + SortOrderRepr("(businessentity0).modifieddate") -> JsString("2020-12-29T00:00:00"), + SortOrderRepr("((businessentity0).businessentityid - {param1}::INTEGER):2") -> JsNumber(BigDecimal(1)) ) ) ) @@ -90,8 +90,8 @@ class PaginationTest extends AnyFunSuite with TypeCheckedTripleEquals { patch( ClientCursor( Map( - SortOrderRepr("businessentity0.modifieddate") -> JsString("2020-12-25T00:00:00"), - SortOrderRepr("(businessentity0.businessentityid - {param1}::INTEGER):2") -> JsNumber(BigDecimal(15)) + SortOrderRepr("(businessentity0).modifieddate") -> JsString("2020-12-25T00:00:00"), + SortOrderRepr("((businessentity0).businessentityid - {param1}::INTEGER):2") -> JsNumber(BigDecimal(15)) ) ) ) diff --git a/typo-tester-anorm/src/scala/adventureworks/SnapshotTest.scala b/typo-tester-anorm/src/scala/adventureworks/SnapshotTest.scala new file mode 100644 index 000000000..aa552a07f --- /dev/null +++ b/typo-tester-anorm/src/scala/adventureworks/SnapshotTest.scala @@ -0,0 +1,64 @@ +package adventureworks + +import org.scalactic.TypeCheckedTripleEquals +import org.scalatest.Assertion +import org.scalatest.funsuite.AnyFunSuite +import typo.dsl.Fragment + +import java.nio.file.{Files, Path} +import scala.annotation.nowarn + +trait SnapshotTest extends AnyFunSuite with TypeCheckedTripleEquals { + val outFolder: Path = + SnapshotTest.projectRoot.resolve("snapshot-tests/anorm-sql") + + def compareFragment(fragmentname: String)(ot: Option[Fragment]): Unit = { + ot.foreach(sql => { + val str = sql.sql.replaceAll("\\{param[\\d]+\\}", "?") + writeAndCompare(outFolder.resolve(s"${getClass.getSimpleName}/$fragmentname.sql"), str) + }) + } + + def writeAndCompare(in: Path, contents: String): Assertion = { + if (SnapshotTest.isCi) { + if (Files.exists(in)) { + val existing = Files.readString(in) + assert(existing == contents) + } else { + fail(s"Expected $in to exist") + } + } else { + Files.createDirectories(in.getParent) + Files.writeString(in, contents) + // burde sikkert ta en fil-lås, men dette dekker tester i samme JVM + SnapshotTest.GitLock.synchronized { + import scala.sys.process.* + List("git", "add", in.toString).!! : @nowarn + () + } + succeed + } + } +} + +object SnapshotTest { + val `.git`: Path = Path.of(".git") + val cwd: Path = Path.of(sys.props("user.dir")) + + /** intellij and sbt uses different working directories. This will place us somewhere predictable + */ + val projectRoot: Path = { + def lookUpwards(p: Path): Option[Path] = + if (Files.list(p).anyMatch(p => p.getFileName == `.git`)) Some(p) + else Option(p.getParent).flatMap(lookUpwards) + + lookUpwards(cwd).getOrElse { + sys.error(s"Cannot find root of repo (uses ${`.git`} to determine where)") + } + } + + val isCi: Boolean = + sys.env.contains("BUILD_NUMBER") || sys.env.contains("CI") // from sbt + + private object GitLock +} diff --git a/typo-tester-anorm/src/scala/adventureworks/production/product/CompositeIdsTest.scala b/typo-tester-anorm/src/scala/adventureworks/production/product/CompositeIdsTest.scala index 14bbc4976..3c9ba7247 100644 --- a/typo-tester-anorm/src/scala/adventureworks/production/product/CompositeIdsTest.scala +++ b/typo-tester-anorm/src/scala/adventureworks/production/product/CompositeIdsTest.scala @@ -1,23 +1,21 @@ package adventureworks.production.product import adventureworks.customtypes.{TypoLocalDateTime, TypoShort, TypoUUID, TypoXml} -import adventureworks.person.businessentity.{BusinessentityId, BusinessentityRepo, BusinessentityRepoImpl, BusinessentityRepoMock, BusinessentityRow} +import adventureworks.person.businessentity.* import adventureworks.person.emailaddress.{EmailaddressRepo, EmailaddressRepoImpl, EmailaddressRepoMock, EmailaddressRow} import adventureworks.person.person.{PersonRepo, PersonRepoImpl, PersonRepoMock, PersonRow} import adventureworks.production.productcosthistory.* import adventureworks.production.unitmeasure.UnitmeasureId import adventureworks.public.{Name, NameStyle} import adventureworks.userdefined.FirstName -import adventureworks.{DomainInsert, TestInsert, withConnection} -import org.scalactic.TypeCheckedTripleEquals +import adventureworks.{DomainInsert, SnapshotTest, TestInsert, withConnection} import org.scalatest.Assertion -import org.scalatest.funsuite.AnyFunSuite import java.time.LocalDateTime import scala.annotation.nowarn import scala.util.Random -class CompositeIdsTest extends AnyFunSuite with TypeCheckedTripleEquals { +class CompositeIdsTest extends SnapshotTest { implicit class Foo(x: TypoLocalDateTime) { def map(f: LocalDateTime => LocalDateTime): TypoLocalDateTime = TypoLocalDateTime(f(x.value)) } @@ -103,7 +101,7 @@ class CompositeIdsTest extends AnyFunSuite with TypeCheckedTripleEquals { ) ) val res2 = query2.toList - query2.sql.foreach(x => println(x)) + compareFragment("query2")(query2.sql) assert(res2 === List(emailaddress1_2, emailaddress1_3)) } } diff --git a/typo-tester-anorm/src/scala/adventureworks/production/product/ProductTest.scala b/typo-tester-anorm/src/scala/adventureworks/production/product/ProductTest.scala index 7786ab71d..251414997 100644 --- a/typo-tester-anorm/src/scala/adventureworks/production/product/ProductTest.scala +++ b/typo-tester-anorm/src/scala/adventureworks/production/product/ProductTest.scala @@ -7,16 +7,14 @@ import adventureworks.production.productmodel.* import adventureworks.production.productsubcategory.* import adventureworks.production.unitmeasure.* import adventureworks.public.{Flag, Name} -import adventureworks.{DomainInsert, TestInsert, withConnection} -import org.scalactic.TypeCheckedTripleEquals +import adventureworks.{DomainInsert, SnapshotTest, TestInsert, withConnection} import org.scalatest.Assertion -import org.scalatest.funsuite.AnyFunSuite import java.time.LocalDateTime import scala.annotation.nowarn import scala.util.Random -class ProductTest extends AnyFunSuite with TypeCheckedTripleEquals { +class ProductTest extends SnapshotTest { val personDynamicSqlRepo = new PersonDynamicSqlRepoImpl test("flaf") { @@ -129,9 +127,8 @@ class ProductTest extends AnyFunSuite with TypeCheckedTripleEquals { .joinFk(_.fkProductmodel)(projectModelRepo.select) .joinFk(_._1.fkProductsubcategory)(productsubcategoryRepo.select) .joinFk(_._2.fkProductcategory)(productcategoryRepo.select) + compareFragment("query0")(query0.sql) query0.toList.foreach(println) - query0.sql.foreach(println) - println("foo") val query = productRepo.select @@ -146,15 +143,14 @@ class ProductTest extends AnyFunSuite with TypeCheckedTripleEquals { .orderBy { case ((product, _), _) => product.productmodelid.asc } .orderBy { case ((_, _), productModel) => productModel(_.name).desc.withNullsFirst } - query.sql.foreach(f => println(f.sql)) - + compareFragment("query")(query.sql) println(query.toList) val leftJoined = productRepo.select .join(projectModelRepo.select) .leftOn { case (p, pm) => p.productmodelid === pm.productmodelid } - leftJoined.sql.foreach(f => println(f.sql)) + compareFragment("leftJoined")(leftJoined.sql) leftJoined.toList.foreach(println) val sellStartDate = TypoLocalDateTime.now @@ -166,7 +162,7 @@ class ProductTest extends AnyFunSuite with TypeCheckedTripleEquals { .setComputedValue(_.sellstartdate)(_ => sellStartDate) .where(_.productid === saved1.productid) - update.sql(returning = true).foreach(f => println(f.sql)) + compareFragment("updateReturning")(update.sql(returning = true)) val List(updated) = update.executeReturnChanged() assert(updated.name === Name("MANf")): @nowarn assert(updated.listprice === BigDecimal(2)): @nowarn @@ -182,7 +178,7 @@ class ProductTest extends AnyFunSuite with TypeCheckedTripleEquals { .on { case (p, pm) => p.productmodelid === pm.productmodelid } .where { case (_, pm) => !pm.instructions.isNull } - q.sql.foreach(f => println(f.sql)) + compareFragment("q")(q.sql) q.toList.foreach(println) val q2 = productRepo.select @@ -210,7 +206,7 @@ class ProductTest extends AnyFunSuite with TypeCheckedTripleEquals { .orderBy { case (_, pm2) => pm2(_.name).asc } .orderBy { case ((p, _), _) => p.color.desc.withNullsFirst } - q2.sql.foreach(println) + compareFragment("q2")(q2.sql) q2.toList.foreach { case ((p, pm1), pm2) => println(p) println(pm1) @@ -218,7 +214,10 @@ class ProductTest extends AnyFunSuite with TypeCheckedTripleEquals { } // delete - productRepo.delete.where(_.productid === saved1.productid).execute(): @nowarn + val delete = productRepo.delete.where(_.productid === saved1.productid) + compareFragment("delete")(delete.sql) + + delete.execute(): @nowarn val List() = productRepo.selectAll: @unchecked diff --git a/typo-tester-anorm/src/scala/adventureworks/production/product/SeekTest.scala b/typo-tester-anorm/src/scala/adventureworks/production/product/SeekTest.scala index 2b7502afe..bd8ee769c 100644 --- a/typo-tester-anorm/src/scala/adventureworks/production/product/SeekTest.scala +++ b/typo-tester-anorm/src/scala/adventureworks/production/product/SeekTest.scala @@ -1,10 +1,9 @@ package adventureworks.production.product +import adventureworks.SnapshotTest import adventureworks.public.Name -import org.scalactic.TypeCheckedTripleEquals -import org.scalatest.funsuite.AnyFunSuite -class SeekTest extends AnyFunSuite with TypeCheckedTripleEquals { +class SeekTest extends SnapshotTest { val productRepo = new ProductRepoImpl test("uniform ascending") { @@ -12,11 +11,7 @@ class SeekTest extends AnyFunSuite with TypeCheckedTripleEquals { .seek(_.name.asc)(Name("foo")) .seek(_.weight.asc)(Some(BigDecimal(22.2))) .seek(_.listprice.asc)(BigDecimal(33.3)) - assertResult(query.sql.get.toString)( - s"""Fragment(List(NamedParameter(param1,ParameterValue(Name(foo))), NamedParameter(param2,ParameterValue(Some(22.2))), NamedParameter(param3,ParameterValue(33.3))),select "productid","name","productnumber","makeflag","finishedgoodsflag","color","safetystocklevel","reorderpoint","standardcost","listprice","size","sizeunitmeasurecode","weightunitmeasurecode","weight","daystomanufacture","productline","class","style","productsubcategoryid","productmodelid","sellstartdate"::text,"sellenddate"::text,"discontinueddate"::text,"rowguid","modifieddate"::text from production.product product0 - | where ((product0.name,product0.weight,product0.listprice) > ({param1}::"public"."Name",{param2}::DECIMAL,{param3}::DECIMAL)) order by product0.name ASC , product0.weight ASC , product0.listprice ASC${" "} - |)""".stripMargin - ) + compareFragment("uniform-ascending")(query.sql) } test("uniform descending") { @@ -24,11 +19,7 @@ class SeekTest extends AnyFunSuite with TypeCheckedTripleEquals { .seek(_.name.desc)(Name("foo")) .seek(_.weight.desc)(Some(BigDecimal(22.2))) .seek(_.listprice.desc)(BigDecimal(33.3)) - assertResult(query.sql.get.toString)( - s"""Fragment(List(NamedParameter(param1,ParameterValue(Name(foo))), NamedParameter(param2,ParameterValue(Some(22.2))), NamedParameter(param3,ParameterValue(33.3))),select "productid","name","productnumber","makeflag","finishedgoodsflag","color","safetystocklevel","reorderpoint","standardcost","listprice","size","sizeunitmeasurecode","weightunitmeasurecode","weight","daystomanufacture","productline","class","style","productsubcategoryid","productmodelid","sellstartdate"::text,"sellenddate"::text,"discontinueddate"::text,"rowguid","modifieddate"::text from production.product product0 - | where ((product0.name,product0.weight,product0.listprice) < ({param1}::"public"."Name",{param2}::DECIMAL,{param3}::DECIMAL)) order by product0.name DESC , product0.weight DESC , product0.listprice DESC${" "} - |)""".stripMargin - ) + compareFragment("uniform-descending")(query.sql) } test("complex") { @@ -36,10 +27,6 @@ class SeekTest extends AnyFunSuite with TypeCheckedTripleEquals { .seek(_.name.asc)(Name("foo")) .seek(_.weight.desc)(Some(BigDecimal(22.2))) .seek(_.listprice.desc)(BigDecimal(33.3)) - assertResult(query.sql.get.toString)( - s"""Fragment(List(NamedParameter(param1,ParameterValue(Name(foo))), NamedParameter(param2,ParameterValue(Name(foo))), NamedParameter(param3,ParameterValue(Some(22.2))), NamedParameter(param4,ParameterValue(Name(foo))), NamedParameter(param5,ParameterValue(Some(22.2))), NamedParameter(param6,ParameterValue(33.3))),select "productid","name","productnumber","makeflag","finishedgoodsflag","color","safetystocklevel","reorderpoint","standardcost","listprice","size","sizeunitmeasurecode","weightunitmeasurecode","weight","daystomanufacture","productline","class","style","productsubcategoryid","productmodelid","sellstartdate"::text,"sellenddate"::text,"discontinueddate"::text,"rowguid","modifieddate"::text from production.product product0 - | where (((product0.name > {param1}::"public"."Name") OR ((product0.name = {param2}::"public"."Name") AND (product0.weight < {param3}::DECIMAL))) OR (((product0.name = {param4}::"public"."Name") AND (product0.weight = {param5}::DECIMAL)) AND (product0.listprice < {param6}::DECIMAL))) order by product0.name ASC , product0.weight DESC , product0.listprice DESC${' '} - |)""".stripMargin - ) + compareFragment("complex")(query.sql) } } diff --git a/typo-tester-doobie/src/scala/adventureworks/DSLTest.scala b/typo-tester-doobie/src/scala/adventureworks/DSLTest.scala new file mode 100644 index 000000000..5dff07fb5 --- /dev/null +++ b/typo-tester-doobie/src/scala/adventureworks/DSLTest.scala @@ -0,0 +1,48 @@ +package adventureworks + +import adventureworks.customtypes.* +import adventureworks.humanresources.employee.EmployeeRepoImpl +import adventureworks.person.businessentity.BusinessentityRepoImpl +import adventureworks.person.emailaddress.EmailaddressRepoImpl +import adventureworks.person.person.PersonRepoImpl +import adventureworks.sales.salesperson.SalespersonRepoImpl +import adventureworks.userdefined.FirstName +import doobie.free.connection.delay + +import scala.util.Random + +class DSLTest extends SnapshotTest { + val businessentityRepoImpl = new BusinessentityRepoImpl + val personRepoImpl = new PersonRepoImpl + val employeeRepoImpl = new EmployeeRepoImpl() + val salespersonRepoImpl = new SalespersonRepoImpl + val emailaddressRepoImpl = new EmailaddressRepoImpl + + test("works") { + withConnection { + val testInsert = new TestInsert(new Random(0), DomainInsert) + for { + businessentityRow <- testInsert.personBusinessentity() + personRow <- testInsert.personPerson(businessentityRow.businessentityid, persontype = "EM", FirstName("a")) + _ <- testInsert.personEmailaddress(personRow.businessentityid, Some("a@b.c")) + employeeRow <- testInsert.humanresourcesEmployee(personRow.businessentityid, gender = "M", maritalstatus = "M", birthdate = TypoLocalDate("1998-01-01"), hiredate = TypoLocalDate("1997-01-01")) + salespersonRow <- testInsert.salesSalesperson(employeeRow.businessentityid) + q = salespersonRepoImpl.select + .where(_.rowguid === salespersonRow.rowguid) + .joinFk(_.fkHumanresourcesEmployee)(employeeRepoImpl.select) + .joinFk { case (_, e) => e.fkPersonPerson }(personRepoImpl.select) + .joinFk { case ((_, _), p) => p.fkBusinessentity }(businessentityRepoImpl.select) + .join(emailaddressRepoImpl.select.orderBy(_.rowguid.asc)) + .on { case ((_, b), email) => email.businessentityid === b.businessentityid } + .joinOn(salespersonRepoImpl.select) { case ((_, e), s2) => + e.businessentityid.underlying === s2.businessentityid.underlying + } + doubled = q.join(q).on { case (((_, e1), _), ((_, e2), _)) => e1.businessentityid === e2.businessentityid } + doubledRes <- doubled.toList + _ <- delay(doubledRes.foreach(println)) + _ <- doubled.count.map(v => assert(v == 1)) + _ <- delay(compareFragment("doubled")(doubled.sql)) + } yield () + } + } +} diff --git a/typo-tester-doobie/src/scala/adventureworks/PaginationTest.scala b/typo-tester-doobie/src/scala/adventureworks/PaginationTest.scala index 61ce692ab..13165e759 100644 --- a/typo-tester-doobie/src/scala/adventureworks/PaginationTest.scala +++ b/typo-tester-doobie/src/scala/adventureworks/PaginationTest.scala @@ -40,7 +40,7 @@ class PaginationTest extends AnyFunSuite with TypeCheckedTripleEquals { def patch(c: ClientCursor[Json]): ClientCursor[Json] = businessentityRepo match { case _: BusinessentityRepoMock => - c.copy(parts = c.parts.map { case (k, v) => (SortOrderRepr(k.expr.replace("businessentity0.", "")), v) }) + c.copy(parts = c.parts.map { case (k, v) => (SortOrderRepr(k.expr.replace("(businessentity0).", "")), v) }) case _ => c } @@ -77,8 +77,8 @@ class PaginationTest extends AnyFunSuite with TypeCheckedTripleEquals { patch( ClientCursor( Map( - SortOrderRepr("businessentity0.modifieddate") -> Json.fromString("2020-12-29T00:00:00"), - SortOrderRepr("(businessentity0.businessentityid - ? ) :2") -> Json.fromInt(1) + SortOrderRepr("(businessentity0).modifieddate") -> Json.fromString("2020-12-29T00:00:00"), + SortOrderRepr("((businessentity0).businessentityid - ? ) :2") -> Json.fromInt(1) ) ) ) @@ -96,8 +96,8 @@ class PaginationTest extends AnyFunSuite with TypeCheckedTripleEquals { patch( ClientCursor( Map( - SortOrderRepr("businessentity0.modifieddate") -> Json.fromString("2020-12-25T00:00:00"), - SortOrderRepr("(businessentity0.businessentityid - ? ) :2") -> Json.fromInt(15) + SortOrderRepr("(businessentity0).modifieddate") -> Json.fromString("2020-12-25T00:00:00"), + SortOrderRepr("((businessentity0).businessentityid - ? ) :2") -> Json.fromInt(15) ) ) ) diff --git a/typo-tester-doobie/src/scala/adventureworks/SnapshotTest.scala b/typo-tester-doobie/src/scala/adventureworks/SnapshotTest.scala new file mode 100644 index 000000000..27e587f47 --- /dev/null +++ b/typo-tester-doobie/src/scala/adventureworks/SnapshotTest.scala @@ -0,0 +1,63 @@ +package adventureworks + +import doobie.util.fragment.Fragment +import org.scalactic.TypeCheckedTripleEquals +import org.scalatest.Assertion +import org.scalatest.funsuite.AnyFunSuite + +import java.nio.file.{Files, Path} +import scala.annotation.nowarn + +trait SnapshotTest extends AnyFunSuite with TypeCheckedTripleEquals { + val outFolder: Path = + SnapshotTest.projectRoot.resolve("snapshot-tests/dooble-sql") + + def compareFragment(fragmentname: String)(ot: Option[Fragment]): Unit = { + ot.foreach(sql => { + writeAndCompare(outFolder.resolve(s"${getClass.getSimpleName}/$fragmentname.sql"), sql.internals.sql) + }) + } + + def writeAndCompare(in: Path, contents: String): Assertion = { + if (SnapshotTest.isCi) { + if (Files.exists(in)) { + val existing = Files.readString(in) + assert(existing == contents) + } else { + fail(s"Expected $in to exist") + } + } else { + Files.createDirectories(in.getParent) + Files.writeString(in, contents) + // burde sikkert ta en fil-lås, men dette dekker tester i samme JVM + SnapshotTest.GitLock.synchronized { + import scala.sys.process.* + List("git", "add", in.toString).!! : @nowarn + () + } + succeed + } + } +} + +object SnapshotTest { + val `.git`: Path = Path.of(".git") + val cwd: Path = Path.of(sys.props("user.dir")) + + /** intellij and sbt uses different working directories. This will place us somewhere predictable + */ + val projectRoot: Path = { + def lookUpwards(p: Path): Option[Path] = + if (Files.list(p).anyMatch(p => p.getFileName == `.git`)) Some(p) + else Option(p.getParent).flatMap(lookUpwards) + + lookUpwards(cwd).getOrElse { + sys.error(s"Cannot find root of repo (uses ${`.git`} to determine where)") + } + } + + val isCi: Boolean = + sys.env.contains("BUILD_NUMBER") || sys.env.contains("CI") // from sbt + + private object GitLock +} diff --git a/typo-tester-doobie/src/scala/adventureworks/production/product/CompositeIdsTest.scala b/typo-tester-doobie/src/scala/adventureworks/production/product/CompositeIdsTest.scala index 90e1deac5..846e76375 100644 --- a/typo-tester-doobie/src/scala/adventureworks/production/product/CompositeIdsTest.scala +++ b/typo-tester-doobie/src/scala/adventureworks/production/product/CompositeIdsTest.scala @@ -1,23 +1,21 @@ package adventureworks.production.product import adventureworks.customtypes.{TypoLocalDateTime, TypoShort, TypoUUID, TypoXml} -import adventureworks.person.businessentity.{BusinessentityId, BusinessentityRepo, BusinessentityRepoImpl, BusinessentityRepoMock, BusinessentityRow} +import adventureworks.person.businessentity.* import adventureworks.person.emailaddress.{EmailaddressRepo, EmailaddressRepoImpl, EmailaddressRepoMock, EmailaddressRow} import adventureworks.person.person.{PersonRepo, PersonRepoImpl, PersonRepoMock, PersonRow} import adventureworks.production.productcosthistory.* import adventureworks.production.unitmeasure.UnitmeasureId import adventureworks.public.{Name, NameStyle} -import adventureworks.{DomainInsert, TestInsert, withConnection} import adventureworks.userdefined.FirstName -import org.scalactic.TypeCheckedTripleEquals -import org.scalatest.funsuite.AnyFunSuite +import adventureworks.{DomainInsert, SnapshotTest, TestInsert, withConnection} import doobie.free.connection.delay import org.scalatest.Assertion import java.time.LocalDateTime import scala.util.Random -class CompositeIdsTest extends AnyFunSuite with TypeCheckedTripleEquals { +class CompositeIdsTest extends SnapshotTest { implicit class Foo(x: TypoLocalDateTime) { def map(f: LocalDateTime => LocalDateTime): TypoLocalDateTime = TypoLocalDateTime(f(x.value)) } @@ -107,7 +105,7 @@ class CompositeIdsTest extends AnyFunSuite with TypeCheckedTripleEquals { ) ) res2 <- query2.toList - _ <- delay(query2.sql.foreach(x => println(x))) + _ <- delay(compareFragment("query2")(query2.sql)) } yield assert(res2 === List(emailaddress1_2, emailaddress1_3)) } } diff --git a/typo-tester-doobie/src/scala/adventureworks/production/product/ProductTest.scala b/typo-tester-doobie/src/scala/adventureworks/production/product/ProductTest.scala index 4f50d4a41..4b2866ca3 100644 --- a/typo-tester-doobie/src/scala/adventureworks/production/product/ProductTest.scala +++ b/typo-tester-doobie/src/scala/adventureworks/production/product/ProductTest.scala @@ -6,15 +6,13 @@ import adventureworks.production.productmodel.* import adventureworks.production.productsubcategory.* import adventureworks.production.unitmeasure.* import adventureworks.public.{Flag, Name} -import adventureworks.withConnection +import adventureworks.{SnapshotTest, withConnection} import doobie.free.connection.delay -import org.scalactic.TypeCheckedTripleEquals import org.scalatest.Assertion -import org.scalatest.funsuite.AnyFunSuite import java.time.LocalDateTime -class ProductTest extends AnyFunSuite with TypeCheckedTripleEquals { +class ProductTest extends SnapshotTest { def runTest( productRepo: ProductRepo, projectModelRepo: ProductmodelRepo, @@ -95,8 +93,7 @@ class ProductTest extends AnyFunSuite with TypeCheckedTripleEquals { .joinFk(_._1.fkProductsubcategory)(productsubcategoryRepo.select) .joinFk(_._2.fkProductcategory)(productcategoryRepo.select) _ <- query0.toList.map(println(_)) - _ <- delay(query0.sql.foreach(f => println(f))) - _ <- delay(println("foo")) + _ <- delay(compareFragment("query0")(query0.sql)) query = productRepo.select .where(_.`class` === "H ") @@ -110,10 +107,10 @@ class ProductTest extends AnyFunSuite with TypeCheckedTripleEquals { .orderBy { case ((product, _), _) => product.productmodelid.asc } .orderBy { case ((_, _), productModel) => productModel(_.name).desc.withNullsFirst } - _ <- delay(query.sql.foreach(f => println(f))) + _ <- delay(compareFragment("query")(query.sql)) _ <- query.toList.map(println(_)) leftJoined = productRepo.select.join(projectModelRepo.select).leftOn { case (p, pm) => p.productmodelid === pm.productmodelid } - _ <- delay(leftJoined.sql.foreach(println)) + _ <- delay(compareFragment("leftJoined")(leftJoined.sql)) _ <- leftJoined.toList.map(println) update = productRepo.update @@ -123,7 +120,7 @@ class ProductTest extends AnyFunSuite with TypeCheckedTripleEquals { // .setComputedValue(_.sizeunitmeasurecode)(_ => Some(unitmeasure.unitmeasurecode)) .where(_.productid === saved1.productid) - _ <- delay(update.sql(returning = true).foreach(println(_))) + _ <- delay(compareFragment("updateReturning")(update.sql(returning = true))) foo <- update.executeReturnChanged List(updated) = foo _ <- delay(assert(updated.name === Name("MANf"))) @@ -139,11 +136,11 @@ class ProductTest extends AnyFunSuite with TypeCheckedTripleEquals { .on { case (p, pm) => p.productmodelid === pm.productmodelid } .where { case (_, pm) => !pm.instructions.isNull } - q.sql.foreach(f => println(f)) + compareFragment("q")(q.sql) q.toList.map(list => list.foreach(println)) } _ <- { - val q = productRepo.select + val q2 = productRepo.select // select from id, arrays work .where(p => p.productid.in(Array(saved1.productid, new ProductId(22)))) // call `length` function and compare result @@ -168,8 +165,8 @@ class ProductTest extends AnyFunSuite with TypeCheckedTripleEquals { .orderBy { case ((_, pm), _) => pm.rowguid.desc.withNullsFirst } .orderBy { case ((_, _), pm2) => pm2(_.rowguid).asc } -// q.sql.foreach(f => println(f.sql)) - q.toList.map { + compareFragment("q2")(q2.sql) + q2.toList.map { _.map { case ((p, pm1), pm2) => println(p) println(pm1) @@ -178,7 +175,9 @@ class ProductTest extends AnyFunSuite with TypeCheckedTripleEquals { } } // delete - _ <- productRepo.deleteById(saved1.productid) + delete = productRepo.delete.where(_.productid === saved1.productid) + _ <- delay(compareFragment("delete")(delete.sql)) + _ <- delete.execute _ <- productRepo.selectAll.compile.toList.map { case Nil => () case other => throw new MatchError(other) diff --git a/typo-tester-doobie/src/scala/adventureworks/production/product/SeekTest.scala b/typo-tester-doobie/src/scala/adventureworks/production/product/SeekTest.scala index dcdbbb67b..36a79ee49 100644 --- a/typo-tester-doobie/src/scala/adventureworks/production/product/SeekTest.scala +++ b/typo-tester-doobie/src/scala/adventureworks/production/product/SeekTest.scala @@ -1,11 +1,10 @@ package adventureworks.production.product +import adventureworks.SnapshotTest import adventureworks.public.Name -import org.scalactic.TypeCheckedTripleEquals -import org.scalatest.funsuite.AnyFunSuite import typo.dsl.SqlExpr -class SeekTest extends AnyFunSuite with TypeCheckedTripleEquals { +class SeekTest extends SnapshotTest { val productRepo = new ProductRepoImpl test("uniform ascending") { @@ -13,9 +12,7 @@ class SeekTest extends AnyFunSuite with TypeCheckedTripleEquals { .seek(_.name.asc)(Name("foo")) .seek(_.weight.asc)(SqlExpr.asConstOpt(Some(BigDecimal(22.2)))) .seek(_.listprice.asc)(BigDecimal(33.3)) - assertResult(query.sql.get.toString)( - s"""Fragment("select "productid", "name", "productnumber", "makeflag", "finishedgoodsflag", "color", "safetystocklevel", "reorderpoint", "standardcost", "listprice", "size", "sizeunitmeasurecode", "weightunitmeasurecode", "weight", "daystomanufacture", "productline", "class", "style", "productsubcategoryid", "productmodelid", "sellstartdate"::text, "sellenddate"::text, "discontinueddate"::text, "rowguid", "modifieddate"::text from production.product product0 WHERE ((product0.name, product0.weight, product0.listprice) > (? , ? , ? ) ) ORDER BY product0.name ASC , product0.weight ASC , product0.listprice ASC ")""" - ) + compareFragment("uniform-ascending")(query.sql) } test("uniform descending") { @@ -23,9 +20,7 @@ class SeekTest extends AnyFunSuite with TypeCheckedTripleEquals { .seek(_.name.desc)(Name("foo")) .seek(_.weight.desc)(Some(BigDecimal(22.2))) .seek(_.listprice.desc)(BigDecimal(33.3)) - assertResult(query.sql.get.toString)( - s"""Fragment("select "productid", "name", "productnumber", "makeflag", "finishedgoodsflag", "color", "safetystocklevel", "reorderpoint", "standardcost", "listprice", "size", "sizeunitmeasurecode", "weightunitmeasurecode", "weight", "daystomanufacture", "productline", "class", "style", "productsubcategoryid", "productmodelid", "sellstartdate"::text, "sellenddate"::text, "discontinueddate"::text, "rowguid", "modifieddate"::text from production.product product0 WHERE ((product0.name, product0.weight, product0.listprice) < (? , ? , ? ) ) ORDER BY product0.name DESC , product0.weight DESC , product0.listprice DESC ")""" - ) + compareFragment("uniform-descending")(query.sql) } test("complex") { @@ -33,8 +28,6 @@ class SeekTest extends AnyFunSuite with TypeCheckedTripleEquals { .seek(_.name.asc)(Name("foo")) .seek(_.weight.desc)(Some(BigDecimal(22.2))) .seek(_.listprice.desc)(BigDecimal(33.3)) - assertResult(query.sql.get.toString)( - s"""Fragment("select "productid", "name", "productnumber", "makeflag", "finishedgoodsflag", "color", "safetystocklevel", "reorderpoint", "standardcost", "listprice", "size", "sizeunitmeasurecode", "weightunitmeasurecode", "weight", "daystomanufacture", "productline", "class", "style", "productsubcategoryid", "productmodelid", "sellstartdate"::text, "sellenddate"::text, "discontinueddate"::text, "rowguid", "modifieddate"::text from production.product product0 WHERE (((product0.name > ? ) OR ((product0.name = ? ) AND (product0.weight < ? ) ) ) OR (((product0.name = ? ) AND (product0.weight = ? ) ) AND (product0.listprice < ? ) ) ) ORDER BY product0.name ASC , product0.weight DESC , product0.listprice DESC ")""" - ) + compareFragment("complex")(query.sql) } } diff --git a/typo-tester-zio-jdbc/src/scala/adventureworks/DSLTest.scala b/typo-tester-zio-jdbc/src/scala/adventureworks/DSLTest.scala new file mode 100644 index 000000000..48f6a9f2b --- /dev/null +++ b/typo-tester-zio-jdbc/src/scala/adventureworks/DSLTest.scala @@ -0,0 +1,48 @@ +package adventureworks + +import adventureworks.customtypes.* +import adventureworks.humanresources.employee.EmployeeRepoImpl +import adventureworks.person.businessentity.BusinessentityRepoImpl +import adventureworks.person.emailaddress.EmailaddressRepoImpl +import adventureworks.person.person.PersonRepoImpl +import adventureworks.sales.salesperson.SalespersonRepoImpl +import adventureworks.userdefined.FirstName +import zio.ZIO + +import scala.util.Random + +class DSLTest extends SnapshotTest { + val businessentityRepoImpl = new BusinessentityRepoImpl + val personRepoImpl = new PersonRepoImpl + val employeeRepoImpl = new EmployeeRepoImpl() + val salespersonRepoImpl = new SalespersonRepoImpl + val emailaddressRepoImpl = new EmailaddressRepoImpl + + test("works") { + withConnection { + val testInsert = new TestInsert(new Random(0), DomainInsert) + for { + businessentityRow <- testInsert.personBusinessentity() + personRow <- testInsert.personPerson(businessentityRow.businessentityid, persontype = "EM", FirstName("a")) + _ <- testInsert.personEmailaddress(personRow.businessentityid, Some("a@b.c")) + employeeRow <- testInsert.humanresourcesEmployee(personRow.businessentityid, gender = "M", maritalstatus = "M", birthdate = TypoLocalDate("1998-01-01"), hiredate = TypoLocalDate("1997-01-01")) + salespersonRow <- testInsert.salesSalesperson(employeeRow.businessentityid) + q = salespersonRepoImpl.select + .where(_.rowguid === salespersonRow.rowguid) + .joinFk(_.fkHumanresourcesEmployee)(employeeRepoImpl.select) + .joinFk { case (_, e) => e.fkPersonPerson }(personRepoImpl.select) + .joinFk { case ((_, _), p) => p.fkBusinessentity }(businessentityRepoImpl.select) + .join(emailaddressRepoImpl.select.orderBy(_.rowguid.asc)) + .on { case ((_, b), email) => email.businessentityid === b.businessentityid } + .joinOn(salespersonRepoImpl.select) { case ((_, e), s2) => + e.businessentityid.underlying === s2.businessentityid.underlying + } + doubled = q.join(q).on { case (((_, e1), _), ((_, e2), _)) => e1.businessentityid === e2.businessentityid } + doubledRes <- doubled.toChunk + _ <- ZIO.succeed(doubledRes.foreach(println)) + _ <- doubled.count.map(v => assert(v == 1)) + _ <- ZIO.succeed(compareFragment("doubled")(doubled.sql)) + } yield () + } + } +} diff --git a/typo-tester-zio-jdbc/src/scala/adventureworks/PaginationTest.scala b/typo-tester-zio-jdbc/src/scala/adventureworks/PaginationTest.scala index 2d8cff605..bf8e12c7d 100644 --- a/typo-tester-zio-jdbc/src/scala/adventureworks/PaginationTest.scala +++ b/typo-tester-zio-jdbc/src/scala/adventureworks/PaginationTest.scala @@ -28,7 +28,7 @@ class PaginationTest extends AnyFunSuite with TypeCheckedTripleEquals { def patch(c: ClientCursor[Json]): ClientCursor[Json] = businessentityRepo match { case _: BusinessentityRepoMock => - c.copy(parts = c.parts.map { case (k, v) => (SortOrderRepr(k.expr.replace("businessentity0.", "")), v) }) + c.copy(parts = c.parts.map { case (k, v) => (SortOrderRepr(k.expr.replace("(businessentity0).", "")), v) }) case _ => c } @@ -65,8 +65,8 @@ class PaginationTest extends AnyFunSuite with TypeCheckedTripleEquals { patch( ClientCursor( Map( - SortOrderRepr("businessentity0.modifieddate") -> new Json.Str("2020-12-29T00:00:00"), - SortOrderRepr("(businessentity0.businessentityid - 2::int4)") -> new Json.Num(java.math.BigDecimal.valueOf(1)) + SortOrderRepr("(businessentity0).modifieddate") -> new Json.Str("2020-12-29T00:00:00"), + SortOrderRepr("((businessentity0).businessentityid - 2::int4)") -> new Json.Num(java.math.BigDecimal.valueOf(1)) ) ) ) @@ -83,8 +83,8 @@ class PaginationTest extends AnyFunSuite with TypeCheckedTripleEquals { patch( ClientCursor( Map( - SortOrderRepr("businessentity0.modifieddate") -> new Json.Str("2020-12-25T00:00:00"), - SortOrderRepr("(businessentity0.businessentityid - 2::int4)") -> new Json.Num(java.math.BigDecimal.valueOf(15)) + SortOrderRepr("(businessentity0).modifieddate") -> new Json.Str("2020-12-25T00:00:00"), + SortOrderRepr("((businessentity0).businessentityid - 2::int4)") -> new Json.Num(java.math.BigDecimal.valueOf(15)) ) ) ) diff --git a/typo-tester-zio-jdbc/src/scala/adventureworks/SnapshotTest.scala b/typo-tester-zio-jdbc/src/scala/adventureworks/SnapshotTest.scala new file mode 100644 index 000000000..059391ef6 --- /dev/null +++ b/typo-tester-zio-jdbc/src/scala/adventureworks/SnapshotTest.scala @@ -0,0 +1,76 @@ +package adventureworks + +import org.scalactic.TypeCheckedTripleEquals +import org.scalatest.Assertion +import org.scalatest.funsuite.AnyFunSuite +import zio.jdbc.SqlFragment +import zio.jdbc.SqlFragment.Segment + +import java.nio.file.{Files, Path} +import scala.annotation.nowarn + +trait SnapshotTest extends AnyFunSuite with TypeCheckedTripleEquals { + val outFolder: Path = + SnapshotTest.projectRoot.resolve("snapshot-tests/zio-jdbc-sql") + + def asString(frag: SqlFragment): String = { + val sql = new StringBuilder() + + def go(frag: SqlFragment): Unit = + frag.segments.foreach { + case Segment.Empty => () + case syntax: Segment.Syntax => sql.append(syntax.value) + case _: Segment.Param => sql.append('?') + case nested: Segment.Nested => go(nested.sql) + } + + go(frag) + sql.result() + } + + def compareFragment(fragmentname: String)(ot: Option[SqlFragment]): Unit = + ot.foreach(sql => writeAndCompare(outFolder.resolve(s"${getClass.getSimpleName}/$fragmentname.sql"), asString(sql))) + + def writeAndCompare(in: Path, contents: String): Assertion = { + if (SnapshotTest.isCi) { + if (Files.exists(in)) { + val existing = Files.readString(in) + assert(existing == contents) + } else { + fail(s"Expected $in to exist") + } + } else { + Files.createDirectories(in.getParent) + Files.writeString(in, contents) + // burde sikkert ta en fil-lås, men dette dekker tester i samme JVM + SnapshotTest.GitLock.synchronized { + import scala.sys.process.* + List("git", "add", in.toString).!! : @nowarn + () + } + succeed + } + } +} + +object SnapshotTest { + val `.git`: Path = Path.of(".git") + val cwd: Path = Path.of(sys.props("user.dir")) + + /** intellij and sbt uses different working directories. This will place us somewhere predictable + */ + val projectRoot: Path = { + def lookUpwards(p: Path): Option[Path] = + if (Files.list(p).anyMatch(p => p.getFileName == `.git`)) Some(p) + else Option(p.getParent).flatMap(lookUpwards) + + lookUpwards(cwd).getOrElse { + sys.error(s"Cannot find root of repo (uses ${`.git`} to determine where)") + } + } + + val isCi: Boolean = + sys.env.contains("BUILD_NUMBER") || sys.env.contains("CI") // from sbt + + private object GitLock +} diff --git a/typo-tester-zio-jdbc/src/scala/adventureworks/production/product/CompositeIdsTest.scala b/typo-tester-zio-jdbc/src/scala/adventureworks/production/product/CompositeIdsTest.scala index 0a593d1b0..187dd18a8 100644 --- a/typo-tester-zio-jdbc/src/scala/adventureworks/production/product/CompositeIdsTest.scala +++ b/typo-tester-zio-jdbc/src/scala/adventureworks/production/product/CompositeIdsTest.scala @@ -8,16 +8,14 @@ import adventureworks.production.productcosthistory.ProductcosthistoryRepoImpl import adventureworks.production.unitmeasure.UnitmeasureId import adventureworks.public.{Name, NameStyle} import adventureworks.userdefined.FirstName -import adventureworks.{DomainInsert, TestInsert, withConnection} -import org.scalactic.TypeCheckedTripleEquals +import adventureworks.{DomainInsert, SnapshotTest, TestInsert, withConnection} import org.scalatest.Assertion -import org.scalatest.funsuite.AnyFunSuite import zio.{Chunk, ZIO} import java.time.LocalDateTime import scala.util.Random -class CompositeIdsTest extends AnyFunSuite with TypeCheckedTripleEquals { +class CompositeIdsTest extends SnapshotTest { implicit class Foo(x: TypoLocalDateTime) { def map(f: LocalDateTime => LocalDateTime): TypoLocalDateTime = TypoLocalDateTime(f(x.value)) } @@ -107,7 +105,7 @@ class CompositeIdsTest extends AnyFunSuite with TypeCheckedTripleEquals { ) ) res2 <- query2.toChunk - _ <- ZIO.attempt(query2.sql.foreach(x => println(x))) + _ <- ZIO.attempt(compareFragment("query2")(query2.sql)) } yield assert(res2.toList === List(emailaddress1_2, emailaddress1_3)) } } diff --git a/typo-tester-zio-jdbc/src/scala/adventureworks/production/product/ProductTest.scala b/typo-tester-zio-jdbc/src/scala/adventureworks/production/product/ProductTest.scala index 40206d21a..54fe271fd 100644 --- a/typo-tester-zio-jdbc/src/scala/adventureworks/production/product/ProductTest.scala +++ b/typo-tester-zio-jdbc/src/scala/adventureworks/production/product/ProductTest.scala @@ -6,15 +6,13 @@ import adventureworks.production.productmodel.* import adventureworks.production.productsubcategory.* import adventureworks.production.unitmeasure.* import adventureworks.public.{Flag, Name} -import adventureworks.withConnection -import org.scalactic.TypeCheckedTripleEquals +import adventureworks.{SnapshotTest, withConnection} import org.scalatest.Assertion -import org.scalatest.funsuite.AnyFunSuite import zio.{Chunk, ZIO} import java.time.LocalDateTime -class ProductTest extends AnyFunSuite with TypeCheckedTripleEquals { +class ProductTest extends SnapshotTest { def runTest( productRepo: ProductRepo, projectModelRepo: ProductmodelRepo, @@ -95,8 +93,7 @@ class ProductTest extends AnyFunSuite with TypeCheckedTripleEquals { .joinFk(_._1.fkProductsubcategory)(productsubcategoryRepo.select) .joinFk(_._2.fkProductcategory)(productcategoryRepo.select) _ <- query0.toChunk.map(println(_)) - _ <- ZIO.succeed(query0.sql.foreach(f => println(f))) - _ <- ZIO.succeed(println("foo")) + _ <- ZIO.succeed(compareFragment("query0")(query0.sql)) query = productRepo.select .where(_.`class` === "H ") .where(x => (x.daystomanufacture > 25).or(x.daystomanufacture <= 0)) @@ -109,10 +106,10 @@ class ProductTest extends AnyFunSuite with TypeCheckedTripleEquals { .orderBy { case ((product, _), _) => product.productmodelid.asc } .orderBy { case ((_, _), productModel) => productModel(_.name).desc.withNullsFirst } - _ <- ZIO.succeed(query.sql.foreach(f => println(f))) + _ <- ZIO.succeed(compareFragment("query")(query.sql)) _ <- query.toChunk.map(println(_)) leftJoined = productRepo.select.join(projectModelRepo.select).leftOn { case (p, pm) => p.productmodelid === pm.productmodelid } - _ <- ZIO.succeed(leftJoined.sql.foreach(println)) + _ <- ZIO.succeed(compareFragment("leftJoined")(leftJoined.sql)) _ <- leftJoined.toChunk.map(println) update = productRepo.update @@ -122,7 +119,7 @@ class ProductTest extends AnyFunSuite with TypeCheckedTripleEquals { // .setComputedValue(_.sizeunitmeasurecode)(_ => Some(unitmeasure.unitmeasurecode)) .where(_.productid === saved1.productid) - _ <- ZIO.succeed(update.sql(returning = true).foreach(println(_))) + _ <- ZIO.succeed(compareFragment("updateReturning")(update.sql(returning = true))) foo <- update.executeReturnChanged Chunk(updated) = foo _ <- ZIO.succeed(assert(updated.name === Name("MANf"))) @@ -138,11 +135,11 @@ class ProductTest extends AnyFunSuite with TypeCheckedTripleEquals { .on { case (p, pm) => p.productmodelid === pm.productmodelid } .where { case (_, pm) => !pm.instructions.isNull } - q.sql.foreach(f => println(f)) + compareFragment("q")(q.sql) q.toChunk.map(list => list.foreach(println)) } _ <- { - val q = productRepo.select + val q2 = productRepo.select // select from id, arrays work .where(p => p.productid.in(Array(saved1.productid, new ProductId(22)))) // call `length` function and compare result @@ -167,8 +164,8 @@ class ProductTest extends AnyFunSuite with TypeCheckedTripleEquals { .orderBy { case ((_, pm), _) => pm.rowguid.desc.withNullsFirst } .orderBy { case ((_, _), pm2) => pm2(_.rowguid).asc } -// q.sql.foreach(f => println(f.sql)) - q.toChunk.map { + compareFragment("q2")(q2.sql) + q2.toChunk.map { _.map { case ((p, pm1), pm2) => println(p) println(pm1) @@ -177,7 +174,9 @@ class ProductTest extends AnyFunSuite with TypeCheckedTripleEquals { } } // delete - _ <- productRepo.deleteById(saved1.productid) + delete = productRepo.delete.where(_.productid === saved1.productid) + _ <- ZIO.succeed(compareFragment("delete")(delete.sql)) + _ <- delete.execute _ <- productRepo.selectAll.runCollect.map(_.toList).map { case Nil => () case other => throw new MatchError(other) diff --git a/typo-tester-zio-jdbc/src/scala/adventureworks/production/product/SeekTest.scala b/typo-tester-zio-jdbc/src/scala/adventureworks/production/product/SeekTest.scala index 6b84d0a3b..36a79ee49 100644 --- a/typo-tester-zio-jdbc/src/scala/adventureworks/production/product/SeekTest.scala +++ b/typo-tester-zio-jdbc/src/scala/adventureworks/production/product/SeekTest.scala @@ -1,20 +1,18 @@ package adventureworks.production.product +import adventureworks.SnapshotTest import adventureworks.public.Name -import org.scalactic.TypeCheckedTripleEquals -import org.scalatest.funsuite.AnyFunSuite +import typo.dsl.SqlExpr -class SeekTest extends AnyFunSuite with TypeCheckedTripleEquals { +class SeekTest extends SnapshotTest { val productRepo = new ProductRepoImpl test("uniform ascending") { val query = productRepo.select .seek(_.name.asc)(Name("foo")) - .seek(_.weight.asc)(Some(BigDecimal(22.2))) + .seek(_.weight.asc)(SqlExpr.asConstOpt(Some(BigDecimal(22.2)))) .seek(_.listprice.asc)(BigDecimal(33.3)) - assertResult(query.sql.get.toString)( - s"""Sql(select "productid", "name", "productnumber", "makeflag", "finishedgoodsflag", "color", "safetystocklevel", "reorderpoint", "standardcost", "listprice", "size", "sizeunitmeasurecode", "weightunitmeasurecode", "weight", "daystomanufacture", "productline", "class", "style", "productsubcategoryid", "productmodelid", "sellstartdate"::text, "sellenddate"::text, "discontinueddate"::text, "rowguid", "modifieddate"::text from production.product product0 WHERE ((product0.name,product0.weight,product0.listprice) > (?::"public"."Name",?::numeric,?::numeric)) ORDER BY product0.name ASC , product0.weight ASC , product0.listprice ASC , foo, Some(22.2), 33.3)""" - ) + compareFragment("uniform-ascending")(query.sql) } test("uniform descending") { @@ -22,9 +20,7 @@ class SeekTest extends AnyFunSuite with TypeCheckedTripleEquals { .seek(_.name.desc)(Name("foo")) .seek(_.weight.desc)(Some(BigDecimal(22.2))) .seek(_.listprice.desc)(BigDecimal(33.3)) - assertResult(query.sql.get.toString)( - s"""Sql(select "productid", "name", "productnumber", "makeflag", "finishedgoodsflag", "color", "safetystocklevel", "reorderpoint", "standardcost", "listprice", "size", "sizeunitmeasurecode", "weightunitmeasurecode", "weight", "daystomanufacture", "productline", "class", "style", "productsubcategoryid", "productmodelid", "sellstartdate"::text, "sellenddate"::text, "discontinueddate"::text, "rowguid", "modifieddate"::text from production.product product0 WHERE ((product0.name,product0.weight,product0.listprice) < (?::"public"."Name",?::numeric,?::numeric)) ORDER BY product0.name DESC , product0.weight DESC , product0.listprice DESC , foo, Some(22.2), 33.3)""" - ) + compareFragment("uniform-descending")(query.sql) } test("complex") { @@ -32,8 +28,6 @@ class SeekTest extends AnyFunSuite with TypeCheckedTripleEquals { .seek(_.name.asc)(Name("foo")) .seek(_.weight.desc)(Some(BigDecimal(22.2))) .seek(_.listprice.desc)(BigDecimal(33.3)) - assertResult(query.sql.get.toString)( - s"""Sql(select "productid", "name", "productnumber", "makeflag", "finishedgoodsflag", "color", "safetystocklevel", "reorderpoint", "standardcost", "listprice", "size", "sizeunitmeasurecode", "weightunitmeasurecode", "weight", "daystomanufacture", "productline", "class", "style", "productsubcategoryid", "productmodelid", "sellstartdate"::text, "sellenddate"::text, "discontinueddate"::text, "rowguid", "modifieddate"::text from production.product product0 WHERE (((product0.name > ?::"public"."Name") OR ((product0.name = ?::"public"."Name") AND (product0.weight < ?::numeric))) OR (((product0.name = ?::"public"."Name") AND (product0.weight = ?::numeric)) AND (product0.listprice < ?::numeric))) ORDER BY product0.name ASC , product0.weight DESC , product0.listprice DESC , foo, foo, Some(22.2), foo, Some(22.2), 33.3)""" - ) + compareFragment("complex")(query.sql) } }