diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ff436b5..0bdada6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -71,6 +71,8 @@ jobs: - '8' scala-project: - ++2.13.8 zio-quickstart-encode-decode-json + - ++2.13.13 zio-quickstart-sql + - ++2.13.13 zio-quickstart-prelude steps: - name: Install libuv run: sudo apt-get update && sudo apt-get install -y libuv1-dev diff --git a/README.md b/README.md index 6613f76..c7b2538 100644 --- a/README.md +++ b/README.md @@ -36,4 +36,5 @@ $ sbt run - [ZIO RESTful webservice with docker](zio-quickstart-restful-webservice-dockerize) - [ZIO RESTful webservice with metrics](zio-quickstart-restful-webservice-metrics) - [ZIO STM](zio-quickstart-stm) - many thanks to [@jorge-vasquez-2301](https://github.com/jorge-vasquez-2301) and his [article](https://scalac.io/blog/how-to-write-a-completely-lock-free-concurrent-lru-cache-with-zio-stm/) for this example +- [ZIO SQL](zio-quickstart-sql) - [ZIO Streams](zio-quickstart-streams) diff --git a/build.sbt b/build.sbt index 6bb1bbc..509d01a 100644 --- a/build.sbt +++ b/build.sbt @@ -10,7 +10,9 @@ inThisBuild( ciCheckWebsiteBuildProcess := Seq.empty, scalaVersion := "2.13.8", ciTargetScalaVersions := makeTargetScalaMap( - `zio-quickstart-encode-decode-json` + `zio-quickstart-encode-decode-json`, + `zio-quickstart-sql`, + `zio-quickstart-prelude` ).value, ciDefaultTargetJavaVersions := Seq("8"), semanticdbEnabled := true, @@ -35,7 +37,11 @@ lazy val root = `zio-quickstart-encode-decode-json`, `zio-quickstart-cache`, `zio-quickstart-prelude`, - `zio-quickstart-stm` + `zio-quickstart-stm`, + `zio-quickstart-sql` + ) + .settings( + testFrameworks += new TestFramework("zio.test.sbt.ZTestFramework") ) lazy val `zio-quickstart-hello-world` = project @@ -54,3 +60,4 @@ lazy val `zio-quickstart-reloadable-services` = project lazy val `zio-quickstart-cache` = project lazy val `zio-quickstart-prelude` = project lazy val `zio-quickstart-stm` = project +lazy val `zio-quickstart-sql` = project diff --git a/zio-quickstart-prelude/build.sbt b/zio-quickstart-prelude/build.sbt index 3c2a8b8..c8ab310 100644 --- a/zio-quickstart-prelude/build.sbt +++ b/zio-quickstart-prelude/build.sbt @@ -1,4 +1,5 @@ scalaVersion := "2.13.13" +Test / fork := true libraryDependencies ++= Seq( "dev.zio" %% "zio-prelude" % "1.0.0-RC23", diff --git a/zio-quickstart-sql/build.sbt b/zio-quickstart-sql/build.sbt new file mode 100644 index 0000000..d48f0ce --- /dev/null +++ b/zio-quickstart-sql/build.sbt @@ -0,0 +1,17 @@ +scalaVersion := "2.13.13" +Test / fork := true + +libraryDependencies ++= Seq( + "org.slf4j" % "slf4j-simple" % "2.0.13", + "dev.zio" %% "zio-sql" % "0.1.2", + "dev.zio" %% "zio-sql-postgres" % "0.1.2", + "org.postgresql" % "postgresql" % "42.7.3" % Compile, + "dev.zio" %% "zio-test" % "2.0.22" % Test, + "dev.zio" %% "zio-test-sbt" % "2.0.22" % Test, + "dev.zio" %% "zio-test-junit" % "2.0.22" % Test, + "org.testcontainers" % "testcontainers" % "1.19.7" % Test, + "org.testcontainers" % "database-commons" % "1.19.7" % Test, + "org.testcontainers" % "postgresql" % "1.19.7" % Test, + "org.testcontainers" % "jdbc" % "1.19.7" % Test, + "com.dimafeng" %% "testcontainers-scala-postgresql" % "0.41.3" % Test +) diff --git a/zio-quickstart-sql/src/test/resources/db_schema.sql b/zio-quickstart-sql/src/test/resources/db_schema.sql new file mode 100644 index 0000000..dfb9309 --- /dev/null +++ b/zio-quickstart-sql/src/test/resources/db_schema.sql @@ -0,0 +1,193 @@ +create table "customers" +( + "id" uuid not null primary key, + "first_name" varchar not null, + "last_name" varchar not null, + "verified" boolean not null, + "dob" date not null, + "created_timestamp" timestamp with time zone default now() +); + +create table orders +( + id uuid not null primary key, + customer_id uuid not null, + order_date date not null +); + +create table products +( + id uuid not null primary key, + name varchar not null, + description varchar not null, + image_url varchar +); + +create table product_prices +( + product_id uuid not null, + effective date not null, + price money not null +); + +create table order_details +( + order_id uuid not null, + product_id uuid not null, + quantity integer not null, + unit_price numeric(12, 2) not null +); + +insert into "customers" +("id", "first_name", "last_name", "verified", "dob", "created_timestamp") +values ('60b01fc9-c902-4468-8d49-3c0f989def37', 'Ronald', 'Russell', true, '1983-01-05', '2020-11-21 19:10:25+00'), + ('f76c9ace-be07-4bf3-bd4c-4a9c62882e64', 'Terrence', 'Noel', true, '1999-11-02', '2020-11-21 15:10:25-04'), + ('784426a5-b90a-4759-afbb-571b7a0ba35e', 'Mila', 'Paterso', true, '1990-11-16', '2020-11-22 02:10:25+07'), + ('df8215a2-d5fd-4c6c-9984-801a1b3a2a0b', 'Alana', 'Murray', true, '1995-11-12', '2020-11-21 12:10:25-07'), + ('636ae137-5b1a-4c8c-b11f-c47c624d9cdc', 'Jose', 'Wiggins', false, '1987-03-23', '2020-11-21 19:10:25+00'); + +insert into products + (id, name, description, image_url) +values ('7368ABF4-AED2-421F-B426-1725DE756895', 'Thermometer', 'Make sure you don''t have a fever (could be covid!)', + 'Thermometer picture'), + ('4C770002-4C8F-455A-96FF-36A8186D5290', 'Slippers', 'Keep your feet warm this winter', + 'Slippers picture'), + ('05182725-F5C8-4FD6-9C43-6671E179BF55', 'Mouse Pad', 'Who uses these anyway?', + 'Mouse Pad picture'), + ('105A2701-EF93-4E25-81AB-8952CC7D9DAA', 'Pants', 'Avoid a lawsuit, wear pants to work today!', + 'Pants picture'), + ('F35B0053-855B-4145-ABE1-DC62BC1FDB96', 'Nail File', 'Keep those nails looking good', + 'Nail File picture'), + ('D5137D3A-894A-4109-9986-E982541B434F', 'Teddy Bear', 'Because sometimes you just need something to hug', + 'Teddy Bear picture'); + +insert into product_prices + (product_id, effective, price) +values ('7368ABF4-AED2-421F-B426-1725DE756895', '2018-01-01', 10.00), + ('7368ABF4-AED2-421F-B426-1725DE756895', '2019-01-01', 11.00), + ('7368ABF4-AED2-421F-B426-1725DE756895', '2020-01-01', 12.00), + ('4C770002-4C8F-455A-96FF-36A8186D5290', '2018-01-01', 20.00), + ('4C770002-4C8F-455A-96FF-36A8186D5290', '2019-01-01', 22.00), + ('4C770002-4C8F-455A-96FF-36A8186D5290', '2020-01-01', 22.00), + ('05182725-F5C8-4FD6-9C43-6671E179BF55', '2018-01-01', 2.00), + ('105A2701-EF93-4E25-81AB-8952CC7D9DAA', '2018-01-01', 70.00), + ('105A2701-EF93-4E25-81AB-8952CC7D9DAA', '2019-01-01', 74.00), + ('105A2701-EF93-4E25-81AB-8952CC7D9DAA', '2020-01-01', 80.00), + ('F35B0053-855B-4145-ABE1-DC62BC1FDB96', '2018-01-01', 5.00), + ('F35B0053-855B-4145-ABE1-DC62BC1FDB96', '2019-01-01', 6.00), + ('D5137D3A-894A-4109-9986-E982541B434F', '2018-01-01', 50.00), + ('D5137D3A-894A-4109-9986-E982541B434F', '2020-01-01', 55.00); + +insert into orders + (id, customer_id, order_date) +values ('04912093-cc2e-46ac-b64c-1bd7bb7758c3', '60b01fc9-c902-4468-8d49-3c0f989def37', '2019-03-25'), + ('a243fa42-817a-44ec-8b67-22193d212d82', '60b01fc9-c902-4468-8d49-3c0f989def37', '2018-06-04'), + ('9022dd0d-06d6-4a43-9121-2993fc7712a1', 'df8215a2-d5fd-4c6c-9984-801a1b3a2a0b', '2019-08-19'), + ('38d66d44-3cfa-488a-ac77-30277751418f', '636ae137-5b1a-4c8c-b11f-c47c624d9cdc', '2019-08-30'), + ('7b2627d5-0150-44df-9171-3462e20797ee', '636ae137-5b1a-4c8c-b11f-c47c624d9cdc', '2019-03-07'), + ('62cd4109-3e5d-40cc-8188-3899fc1f8bdf', '60b01fc9-c902-4468-8d49-3c0f989def37', '2020-03-19'), + ('9473a0bc-396a-4936-96b0-3eea922af36b', 'df8215a2-d5fd-4c6c-9984-801a1b3a2a0b', '2020-05-11'), + ('b8bac18d-769f-48ed-809d-4b6c0e4d1795', 'df8215a2-d5fd-4c6c-9984-801a1b3a2a0b', '2019-02-21'), + ('852e2dc9-4ec3-4225-a6f7-4f42f8ff728e', '60b01fc9-c902-4468-8d49-3c0f989def37', '2018-05-06'), + ('bebbfe4d-4ec3-4389-bdc2-50e9eac2b15b', '784426a5-b90a-4759-afbb-571b7a0ba35e', '2019-02-11'), + ('742d45a0-e81a-41ce-95ad-55b4cabba258', 'f76c9ace-be07-4bf3-bd4c-4a9c62882e64', '2019-10-12'), + ('618aa21f-700b-4ca7-933c-67066cf4cd97', '60b01fc9-c902-4468-8d49-3c0f989def37', '2019-01-29'), + ('606da090-dd33-4a77-8746-6ed0e8443ab2', 'f76c9ace-be07-4bf3-bd4c-4a9c62882e64', '2019-02-10'), + ('4914028d-2e28-4033-a5f2-8f4fcdee8206', '60b01fc9-c902-4468-8d49-3c0f989def37', '2019-09-27'), + ('d4e77298-d829-4e36-a6a0-902403f4b7d3', 'df8215a2-d5fd-4c6c-9984-801a1b3a2a0b', '2018-11-13'), + ('fd0fa8d4-e1a0-4369-be07-945450db5d36', '636ae137-5b1a-4c8c-b11f-c47c624d9cdc', '2020-01-15'), + ('d6d8dddc-4b0b-4d74-8edc-a54e9b7f35f7', 'f76c9ace-be07-4bf3-bd4c-4a9c62882e64', '2018-07-10'), + ('876b6034-b33c-4497-81ee-b4e8742164c2', '784426a5-b90a-4759-afbb-571b7a0ba35e', '2019-08-01'), + ('91caa28a-a5fe-40d7-979c-bd6a128d0418', 'df8215a2-d5fd-4c6c-9984-801a1b3a2a0b', '2019-12-08'), + ('401c7ab1-41cf-4756-8af5-be25cf2ae67b', '784426a5-b90a-4759-afbb-571b7a0ba35e', '2019-11-04'), + ('2c3fc180-d0df-4d7b-a271-e6ccd2440393', '784426a5-b90a-4759-afbb-571b7a0ba35e', '2018-10-14'), + ('763a7c39-833f-4ee8-9939-e80dfdbfc0fc', 'f76c9ace-be07-4bf3-bd4c-4a9c62882e64', '2020-04-05'), + ('5011d206-8eff-42c4-868e-f1a625e1f186', '636ae137-5b1a-4c8c-b11f-c47c624d9cdc', '2019-01-23'), + ('0a48ffb0-ec61-4147-af56-fc4dbca8de0a', 'f76c9ace-be07-4bf3-bd4c-4a9c62882e64', '2019-05-14'), + ('5883cb62-d792-4ee3-acbc-fe85b6baa998', '784426a5-b90a-4759-afbb-571b7a0ba35e', '2020-04-30'); + +insert into order_details + (order_id, product_id, quantity, unit_price) +values ('9022DD0D-06D6-4A43-9121-2993FC7712A1', '7368ABF4-AED2-421F-B426-1725DE756895', 4, 11.00), + ('38D66D44-3CFA-488A-AC77-30277751418F', '7368ABF4-AED2-421F-B426-1725DE756895', 1, 11.00), + ('7B2627D5-0150-44DF-9171-3462E20797EE', '7368ABF4-AED2-421F-B426-1725DE756895', 1, 11.00), + ('62CD4109-3E5D-40CC-8188-3899FC1F8BDF', '7368ABF4-AED2-421F-B426-1725DE756895', 2, 10.90), + ('9473A0BC-396A-4936-96B0-3EEA922AF36B', '7368ABF4-AED2-421F-B426-1725DE756895', 1, 12.00), + ('852E2DC9-4EC3-4225-A6F7-4F42F8FF728E', '7368ABF4-AED2-421F-B426-1725DE756895', 1, 9.09), + ('742D45A0-E81A-41CE-95AD-55B4CABBA258', '7368ABF4-AED2-421F-B426-1725DE756895', 2, 10.00), + ('618AA21F-700B-4CA7-933C-67066CF4CD97', '7368ABF4-AED2-421F-B426-1725DE756895', 2, 11.00), + ('606DA090-DD33-4A77-8746-6ED0E8443AB2', '7368ABF4-AED2-421F-B426-1725DE756895', 2, 10.00), + ('4914028D-2E28-4033-A5F2-8F4FCDEE8206', '7368ABF4-AED2-421F-B426-1725DE756895', 1, 10.00), + ('D4E77298-D829-4E36-A6A0-902403F4B7D3', '7368ABF4-AED2-421F-B426-1725DE756895', 2, 10.00), + ('FD0FA8D4-E1A0-4369-BE07-945450DB5D36', '7368ABF4-AED2-421F-B426-1725DE756895', 1, 12.00), + ('D6D8DDDC-4B0B-4D74-8EDC-A54E9B7F35F7', '7368ABF4-AED2-421F-B426-1725DE756895', 2, 10.00), + ('876B6034-B33C-4497-81EE-B4E8742164C2', '7368ABF4-AED2-421F-B426-1725DE756895', 1, 11.00), + ('91CAA28A-A5FE-40D7-979C-BD6A128D0418', '7368ABF4-AED2-421F-B426-1725DE756895', 3, 11.00), + ('2C3FC180-D0DF-4D7B-A271-E6CCD2440393', '7368ABF4-AED2-421F-B426-1725DE756895', 2, 10.00), + ('763A7C39-833F-4EE8-9939-E80DFDBFC0FC', '7368ABF4-AED2-421F-B426-1725DE756895', 4, 12.00), + ('5011D206-8EFF-42C4-868E-F1A625E1F186', '7368ABF4-AED2-421F-B426-1725DE756895', 4, 11.00), + ('0A48FFB0-EC61-4147-AF56-FC4DBCA8DE0A', '7368ABF4-AED2-421F-B426-1725DE756895', 1, 11.00), + ('5883CB62-D792-4EE3-ACBC-FE85B6BAA998', '7368ABF4-AED2-421F-B426-1725DE756895', 3, 12.00), + ('04912093-CC2E-46AC-B64C-1BD7BB7758C3', '4C770002-4C8F-455A-96FF-36A8186D5290', 2, 22.00), + ('A243FA42-817A-44EC-8B67-22193D212D82', '4C770002-4C8F-455A-96FF-36A8186D5290', 5, 18.18), + ('9022DD0D-06D6-4A43-9121-2993FC7712A1', '4C770002-4C8F-455A-96FF-36A8186D5290', 2, 22.00), + ('38D66D44-3CFA-488A-AC77-30277751418F', '4C770002-4C8F-455A-96FF-36A8186D5290', 1, 22.00), + ('62CD4109-3E5D-40CC-8188-3899FC1F8BDF', '4C770002-4C8F-455A-96FF-36A8186D5290', 1, 20.00), + ('852E2DC9-4EC3-4225-A6F7-4F42F8FF728E', '4C770002-4C8F-455A-96FF-36A8186D5290', 1, 18.18), + ('BEBBFE4D-4EC3-4389-BDC2-50E9EAC2B15B', '4C770002-4C8F-455A-96FF-36A8186D5290', 1, 20.00), + ('618AA21F-700B-4CA7-933C-67066CF4CD97', '4C770002-4C8F-455A-96FF-36A8186D5290', 1, 22.00), + ('606DA090-DD33-4A77-8746-6ED0E8443AB2', '4C770002-4C8F-455A-96FF-36A8186D5290', 3, 20.00), + ('4914028D-2E28-4033-A5F2-8F4FCDEE8206', '4C770002-4C8F-455A-96FF-36A8186D5290', 1, 20.00), + ('FD0FA8D4-E1A0-4369-BE07-945450DB5D36', '4C770002-4C8F-455A-96FF-36A8186D5290', 1, 22.00), + ('D6D8DDDC-4B0B-4D74-8EDC-A54E9B7F35F7', '4C770002-4C8F-455A-96FF-36A8186D5290', 1, 20.00), + ('876B6034-B33C-4497-81EE-B4E8742164C2', '4C770002-4C8F-455A-96FF-36A8186D5290', 2, 22.00), + ('91CAA28A-A5FE-40D7-979C-BD6A128D0418', '4C770002-4C8F-455A-96FF-36A8186D5290', 1, 22.00), + ('2C3FC180-D0DF-4D7B-A271-E6CCD2440393', '4C770002-4C8F-455A-96FF-36A8186D5290', 1, 20.00), + ('763A7C39-833F-4EE8-9939-E80DFDBFC0FC', '4C770002-4C8F-455A-96FF-36A8186D5290', 1, 22.00), + ('5011D206-8EFF-42C4-868E-F1A625E1F186', '4C770002-4C8F-455A-96FF-36A8186D5290', 1, 22.00), + ('0A48FFB0-EC61-4147-AF56-FC4DBCA8DE0A', '4C770002-4C8F-455A-96FF-36A8186D5290', 3, 22.00), + ('5883CB62-D792-4EE3-ACBC-FE85B6BAA998', '4C770002-4C8F-455A-96FF-36A8186D5290', 3, 22.00), + ('A243FA42-817A-44EC-8B67-22193D212D82', '05182725-F5C8-4FD6-9C43-6671E179BF55', 1, 1.81), + ('852E2DC9-4EC3-4225-A6F7-4F42F8FF728E', '05182725-F5C8-4FD6-9C43-6671E179BF55', 1, 1.81), + ('D4E77298-D829-4E36-A6A0-902403F4B7D3', '05182725-F5C8-4FD6-9C43-6671E179BF55', 1, 2.00), + ('D6D8DDDC-4B0B-4D74-8EDC-A54E9B7F35F7', '05182725-F5C8-4FD6-9C43-6671E179BF55', 3, 2.00), + ('04912093-CC2E-46AC-B64C-1BD7BB7758C3', '105A2701-EF93-4E25-81AB-8952CC7D9DAA', 1, 74.00), + ('9022DD0D-06D6-4A43-9121-2993FC7712A1', '105A2701-EF93-4E25-81AB-8952CC7D9DAA', 4, 74.00), + ('38D66D44-3CFA-488A-AC77-30277751418F', '105A2701-EF93-4E25-81AB-8952CC7D9DAA', 2, 74.00), + ('7B2627D5-0150-44DF-9171-3462E20797EE', '105A2701-EF93-4E25-81AB-8952CC7D9DAA', 1, 74.00), + ('62CD4109-3E5D-40CC-8188-3899FC1F8BDF', '105A2701-EF93-4E25-81AB-8952CC7D9DAA', 1, 72.72), + ('9473A0BC-396A-4936-96B0-3EEA922AF36B', '105A2701-EF93-4E25-81AB-8952CC7D9DAA', 2, 80.00), + ('B8BAC18D-769F-48ED-809D-4B6C0E4D1795', '105A2701-EF93-4E25-81AB-8952CC7D9DAA', 3, 67.27), + ('BEBBFE4D-4EC3-4389-BDC2-50E9EAC2B15B', '105A2701-EF93-4E25-81AB-8952CC7D9DAA', 2, 67.27), + ('742D45A0-E81A-41CE-95AD-55B4CABBA258', '105A2701-EF93-4E25-81AB-8952CC7D9DAA', 1, 67.27), + ('618AA21F-700B-4CA7-933C-67066CF4CD97', '105A2701-EF93-4E25-81AB-8952CC7D9DAA', 2, 74.00), + ('606DA090-DD33-4A77-8746-6ED0E8443AB2', '105A2701-EF93-4E25-81AB-8952CC7D9DAA', 1, 67.27), + ('FD0FA8D4-E1A0-4369-BE07-945450DB5D36', '105A2701-EF93-4E25-81AB-8952CC7D9DAA', 1, 80.00), + ('876B6034-B33C-4497-81EE-B4E8742164C2', '105A2701-EF93-4E25-81AB-8952CC7D9DAA', 2, 74.00), + ('91CAA28A-A5FE-40D7-979C-BD6A128D0418', '105A2701-EF93-4E25-81AB-8952CC7D9DAA', 5, 74.00), + ('2C3FC180-D0DF-4D7B-A271-E6CCD2440393', '105A2701-EF93-4E25-81AB-8952CC7D9DAA', 1, 70.00), + ('763A7C39-833F-4EE8-9939-E80DFDBFC0FC', '105A2701-EF93-4E25-81AB-8952CC7D9DAA', 3, 80.00), + ('5011D206-8EFF-42C4-868E-F1A625E1F186', '105A2701-EF93-4E25-81AB-8952CC7D9DAA', 6, 74.00), + ('0A48FFB0-EC61-4147-AF56-FC4DBCA8DE0A', '105A2701-EF93-4E25-81AB-8952CC7D9DAA', 2, 74.00), + ('04912093-CC2E-46AC-B64C-1BD7BB7758C3', 'F35B0053-855B-4145-ABE1-DC62BC1FDB96', 1, 6.00), + ('A243FA42-817A-44EC-8B67-22193D212D82', 'F35B0053-855B-4145-ABE1-DC62BC1FDB96', 4, 4.54), + ('9022DD0D-06D6-4A43-9121-2993FC7712A1', 'F35B0053-855B-4145-ABE1-DC62BC1FDB96', 1, 6.00), + ('38D66D44-3CFA-488A-AC77-30277751418F', 'F35B0053-855B-4145-ABE1-DC62BC1FDB96', 1, 6.00), + ('7B2627D5-0150-44DF-9171-3462E20797EE', 'F35B0053-855B-4145-ABE1-DC62BC1FDB96', 2, 6.00), + ('B8BAC18D-769F-48ED-809D-4B6C0E4D1795', 'F35B0053-855B-4145-ABE1-DC62BC1FDB96', 1, 5.45), + ('BEBBFE4D-4EC3-4389-BDC2-50E9EAC2B15B', 'F35B0053-855B-4145-ABE1-DC62BC1FDB96', 1, 5.45), + ('742D45A0-E81A-41CE-95AD-55B4CABBA258', 'F35B0053-855B-4145-ABE1-DC62BC1FDB96', 1, 5.45), + ('606DA090-DD33-4A77-8746-6ED0E8443AB2', 'F35B0053-855B-4145-ABE1-DC62BC1FDB96', 1, 5.45), + ('4914028D-2E28-4033-A5F2-8F4FCDEE8206', 'F35B0053-855B-4145-ABE1-DC62BC1FDB96', 1, 5.45), + ('D6D8DDDC-4B0B-4D74-8EDC-A54E9B7F35F7', 'F35B0053-855B-4145-ABE1-DC62BC1FDB96', 1, 5.00), + ('91CAA28A-A5FE-40D7-979C-BD6A128D0418', 'F35B0053-855B-4145-ABE1-DC62BC1FDB96', 1, 6.00), + ('401C7AB1-41CF-4756-8AF5-BE25CF2AE67B', 'F35B0053-855B-4145-ABE1-DC62BC1FDB96', 2, 5.45), + ('5011D206-8EFF-42C4-868E-F1A625E1F186', 'F35B0053-855B-4145-ABE1-DC62BC1FDB96', 1, 6.00), + ('0A48FFB0-EC61-4147-AF56-FC4DBCA8DE0A', 'F35B0053-855B-4145-ABE1-DC62BC1FDB96', 5, 6.00), + ('A243FA42-817A-44EC-8B67-22193D212D82', 'D5137D3A-894A-4109-9986-E982541B434F', 1, 45.45), + ('62CD4109-3E5D-40CC-8188-3899FC1F8BDF', 'D5137D3A-894A-4109-9986-E982541B434F', 4, 50.00), + ('9473A0BC-396A-4936-96B0-3EEA922AF36B', 'D5137D3A-894A-4109-9986-E982541B434F', 2, 55.00), + ('852E2DC9-4EC3-4225-A6F7-4F42F8FF728E', 'D5137D3A-894A-4109-9986-E982541B434F', 1, 45.45), + ('D6D8DDDC-4B0B-4D74-8EDC-A54E9B7F35F7', 'D5137D3A-894A-4109-9986-E982541B434F', 2, 50.00), + ('2C3FC180-D0DF-4D7B-A271-E6CCD2440393', 'D5137D3A-894A-4109-9986-E982541B434F', 2, 50.00), + ('5883CB62-D792-4EE3-ACBC-FE85B6BAA998', 'D5137D3A-894A-4109-9986-E982541B434F', 1, 55.00); diff --git a/zio-quickstart-sql/src/test/scala/JdbcRunnableSpec.scala b/zio-quickstart-sql/src/test/scala/JdbcRunnableSpec.scala new file mode 100644 index 0000000..486b08c --- /dev/null +++ b/zio-quickstart-sql/src/test/scala/JdbcRunnableSpec.scala @@ -0,0 +1,65 @@ +import com.dimafeng.testcontainers.{JdbcDatabaseContainer, SingleContainer} +import zio.sql.postgresql.PostgresJdbcModule +import zio.sql.{ConnectionPool, ConnectionPoolConfig} +import zio.test.{Spec, TestEnvironment, ZIOSpecDefault} +import zio.{Scope, ZIO, ZLayer} + +import java.util.Properties + +trait JdbcRunnableSpec extends ZIOSpecDefault with PostgresJdbcModule { + + type JdbcEnvironment = TestEnvironment with SqlDriver + + def specLayered: Spec[JdbcEnvironment, Object] + + protected def getContainer: SingleContainer[_] with JdbcDatabaseContainer + + protected val autoCommit = false + + override def spec: Spec[TestEnvironment, Any] = + specLayered.provideCustomShared(jdbcLayer) + + private[this] def connProperties( + user: String, + password: String + ): Properties = { + val props = new Properties + props.setProperty("user", user) + props.setProperty("password", password) + props + } + + private[this] val poolConfigLayer + : ZLayer[Any, Throwable, ConnectionPoolConfig] = + ZLayer.scoped { + testContainer + .map(a => + ConnectionPoolConfig( + url = a.jdbcUrl, + properties = connProperties(a.username, a.password), + autoCommit = autoCommit + ) + ) + } + + val connectionPool: ZLayer[Any, Throwable, ConnectionPool] = + poolConfigLayer >>> ConnectionPool.live + + private[this] final lazy val jdbcLayer: ZLayer[Any, Any, SqlDriver] = + ZLayer.make[SqlDriver]( + connectionPool.orDie, + SqlDriver.live + ) + + val testContainer + : ZIO[Scope, Throwable, SingleContainer[_] with JdbcDatabaseContainer] = + ZIO.acquireRelease { + ZIO.attemptBlocking { + val c = getContainer + c.start() + c + } + } { container => + ZIO.attemptBlocking(container.stop()).orDie + } +} diff --git a/zio-quickstart-sql/src/test/scala/PostgresSqlSpec.scala b/zio-quickstart-sql/src/test/scala/PostgresSqlSpec.scala new file mode 100644 index 0000000..f510fa6 --- /dev/null +++ b/zio-quickstart-sql/src/test/scala/PostgresSqlSpec.scala @@ -0,0 +1,284 @@ +import com.dimafeng.testcontainers.{ + JdbcDatabaseContainer, + PostgreSQLContainer, + SingleContainer +} +import org.testcontainers.utility.DockerImageName +import zio.{Cause, Chunk} +import zio.schema.DeriveSchema +import zio.test.Assertion._ +import zio.test._ + +import java.math.{BigDecimal, RoundingMode} +import java.time.{LocalDate, ZonedDateTime} +import java.util.UUID + +object PostgresSqlSpec extends JdbcRunnableSpec { + override protected def getContainer + : SingleContainer[_] with JdbcDatabaseContainer = + new PostgreSQLContainer( + dockerImageNameOverride = + Option("postgres:alpine").map(DockerImageName.parse) + ).configure { a => + a.withInitScript("db_schema.sql") // initialize tables with rows + () + } + + // define schemes + object ProductSchema { + final case class Product( + id: UUID, + name: String, + description: String, + imageUrl: Option[String] // also work with optional fields + ) + implicit val productSchema = + DeriveSchema.gen[Product] // derive schema for model + val products = defineTableSmart[Product] // derive table instance for model + val (productId, name, description, imageUrl) = + products.columns // you can extract all columns from table instance + } + + object CustomerSchema { + final case class Customer( + id: UUID, + firstName: String, + lastName: String, + verified: Boolean, + dob: LocalDate, + createdTimestamp: ZonedDateTime = ZonedDateTime.now() + ) + implicit val customerSchema = DeriveSchema.gen[Customer] + val customers = defineTableSmart[Customer] + val (customerId, firstName, lastName, verified, dob, createdTimestamp) = + customers.columns + + val ALL = + customerId ++ firstName ++ lastName ++ verified ++ dob ++ createdTimestamp + } + + object OrderSchema { + final case class Orders(id: UUID, customerId: UUID, orderDate: LocalDate) + implicit val orderSchema = DeriveSchema.gen[Orders] + val orders = defineTable[Orders] + val (orderId, fkCustomerId, orderDate) = orders.columns + } + + object ProductPriceSchema { + case class ProductPrices( + productId: UUID, + effective: LocalDate, + price: BigDecimal + ) + implicit val productPricesSchema = DeriveSchema.gen[ProductPrices] + val productPrices = defineTableSmart[ProductPrices] + val (productPricesOrderId, effectiveDate, productPrice) = + productPrices.columns + } + + object OrderDetailsSchema { + case class OrderDetails( + orderId: UUID, + productId: UUID, + quantity: Int, + unitPrice: BigDecimal + ) + implicit val orderDetailsSchema = DeriveSchema.gen[OrderDetails] + val orderDetails = defineTableSmart[OrderDetails] + val (orderDetailsOrderId, orderDetailsProductId, quantity, unitPrice) = + orderDetails.columns + } + + override def specLayered: Spec[PostgresSqlSpec.JdbcEnvironment, Object] = + suite("Query")( + test("run simple select") { + import ProductSchema._ + + val selectQuery = select(name) + .from(products) + .where( + productId === UUID.fromString( + "4C770002-4C8F-455A-96FF-36A8186D5290" + ) + ) // type safe building sql query + val expectedName = "Slippers" + + for { + result <- execute( + selectQuery + ).runHead // return option of one row + } yield assert(result)( + isSome(equalTo(expectedName)) + ) // save assert on option + }, + test("run simple select with mapping into model") { + import OrderSchema._ + + val selectQuery = select(orderId, fkCustomerId, orderDate) + .from(orders) + .orderBy(Ordering.Desc(orderDate)) // sorting results by field + .limit(2) // set limit + .offset(1) // set offset + + val expected = Chunk( + Orders( + id = UUID.fromString("5883cb62-d792-4ee3-acbc-fe85b6baa998"), + customerId = + UUID.fromString("784426a5-b90a-4759-afbb-571b7a0ba35e"), + orderDate = LocalDate.of(2020, 4, 30) + ), + Orders( + id = UUID.fromString("763a7c39-833f-4ee8-9939-e80dfdbfc0fc"), + customerId = + UUID.fromString("f76c9ace-be07-4bf3-bd4c-4a9c62882e64"), + orderDate = LocalDate.of(2020, 4, 5) + ) + ) + + for { + result <- execute(selectQuery) + .map(Orders tupled _) + .runCollect // return chunk of founded rows + } yield assert(result)(hasSameElementsDistinct(expected)) + }, + test("run select with join") { + import ProductSchema._ + import OrderDetailsSchema._ + + val orderId = UUID.fromString("D4E77298-D829-4E36-A6A0-902403F4B7D3") + val joinSelectQuery = select(name) + .from( + products + .join(orderDetails) + .on(productId === orderDetailsProductId) + ) + .where(orderDetailsOrderId === orderId) + + val expected = Chunk("Mouse Pad", "Thermometer") + + for { + result <- execute(joinSelectQuery).runCollect + } yield assert(result)(hasSameElementsDistinct(expected)) + }, + test("run select with aggregation functions") { + import AggregationDef._ // to use aggregations + import OrderDetailsSchema._ + + val query = select( + SumDec(unitPrice) as "totalAmount", + SumInt(quantity) as "soldQuantity" + ) // use aliases + .from(orderDetails) + .where( + orderDetailsProductId === UUID.fromString( + "7368ABF4-AED2-421F-B426-1725DE756895" + ) + ) + + for { + result <- execute(query).runHead + roundedResult = result.map { case (totalAmount, soldQuantity) => + (totalAmount.setScale(2, RoundingMode.HALF_EVEN), soldQuantity) + } + } yield assert(roundedResult)( + isSome( + equalTo( + (new BigDecimal(215.99).setScale(2, RoundingMode.HALF_EVEN), 40) + ) + ) + ) + }, + test("run custom function") { + import PostgresFunctionDef._ // to use custom functions + + assertZIO(execute(select(GenRandomUuid)).runHead.some)(!isNull) + }, + test("run batch insert") { + import CustomerSchema._ + + val id1 = UUID.randomUUID() + val id2 = UUID.randomUUID() + val id3 = UUID.randomUUID() + val id4 = UUID.randomUUID() + val c1 = Customer( + id1, + "fnameCustomer1", + "lnameCustomer1", + verified = true, + LocalDate.now() + ) + val c2 = Customer( + id2, + "fnameCustomer2", + "lnameCustomer2", + true, + LocalDate.now() + ) + val c3 = Customer( + id3, + "fnameCustomer3", + "lnameCustomer3", + verified = true, + LocalDate.now() + ) + val c4 = Customer( + id4, + "fnameCustomer4", + "lnameCustomer4", + verified = false, + LocalDate.now() + ) + + val allCustomer = List(c1, c2, c3, c4) + val data = allCustomer.map(Customer.unapply(_).get) + val query = insertInto(customers)(ALL).values(data) + + val insertAssertion = for { + result <- execute(query) + } yield assert(result)(equalTo(4)) + insertAssertion.mapErrorCause(cause => Cause.stackless(cause.untraced)) + }, + test("render query") { + import OrderDetailsSchema._ + import ProductSchema._ + + val selectQuery = select(quantity, name) + .from( + products.join(orderDetails).on(orderDetailsProductId === productId) + ) + .limit(5) + .offset(10) + val selectQueryRender = + "SELECT \"order_details\".\"quantity\", \"products\".\"name\" FROM \"products\" INNER JOIN \"order_details\" ON \"order_details\".\"product_id\" = \"products\".\"id\" LIMIT 5 OFFSET 10" + + val uuid = UUID.randomUUID() + val insertQuery = + insertInto(products)(productId, name, description).values( + ( + uuid, + "Zionomicon", + "Good book to start your journey in zio" + ) + ) + val insertQueryRender = + s"INSERT INTO \"products\" (\"id\", \"name\", \"description\") VALUES ('${uuid.toString}', 'Zionomicon', 'Good book to start your journey in zio');" + + val updateQuery = update(products) + .set(name, "foo") + .where(productId === uuid) + val updateQueryRender = + s"UPDATE \"products\" SET \"name\" = 'foo' WHERE \"products\".\"id\" = '${uuid.toString}'" + + val deleteQuery = deleteFrom(products).where(productId === uuid) + val deleteQueryRender = + s"DELETE FROM \"products\" WHERE \"products\".\"id\" = '${uuid.toString}'" + + assertTrue( + renderRead(selectQuery) == selectQueryRender && + renderInsert(insertQuery) == insertQueryRender && + renderUpdate(updateQuery) == updateQueryRender && + renderDelete(deleteQuery) == deleteQueryRender + ) + } + ) +}