From 9c712a05e25aa034545414261ad1ebb9787826d2 Mon Sep 17 00:00:00 2001 From: Benjamin Schultzer Date: Sun, 13 Apr 2025 13:54:14 -0400 Subject: [PATCH] Add conformance tests --- .github/workflows/ci.yml | 2 +- .github/workflows/conformance.yml | 33 ++++++++++++++ lib/mix/tasks/sql.gen.test.ex | 71 +++++++++++++++++++++++++++++++ mix.exs | 3 +- mix.lock | 1 + 5 files changed, 108 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/conformance.yml create mode 100644 lib/mix/tasks/sql.gen.test.ex diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d381598..17e1e92 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,7 +1,7 @@ on: push jobs: test: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 name: OTP 27 / Elixir 1.18 services: postgres: diff --git a/.github/workflows/conformance.yml b/.github/workflows/conformance.yml new file mode 100644 index 0000000..080ce99 --- /dev/null +++ b/.github/workflows/conformance.yml @@ -0,0 +1,33 @@ +on: push +jobs: + test: + runs-on: ubuntu-22.04 + name: OTP 27 / Elixir 1.18 + services: + postgres: + image: postgres:latest + env: + POSTGRES_PASSWORD: postgres + ports: ["5432:5432"] + options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 + steps: + - name: Checkout ${{github.repository}} + uses: actions/checkout@v4 + - name: Checkout sqltest + uses: actions/checkout@v4 + with: + path: sqltest + repository: elliotchance/sqltest + - uses: erlef/setup-beam@v1 + with: + otp-version: 27 + elixir-version: 1.18 + - uses: actions/cache@v4 + with: + path: | + deps + _build + key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} + restore-keys: | + ${{ runner.os }}-mix- + - run: mix deps.get && mix sql.gen.test sqltest/standards/2016 && mix test diff --git a/lib/mix/tasks/sql.gen.test.ex b/lib/mix/tasks/sql.gen.test.ex new file mode 100644 index 0000000..afe7bff --- /dev/null +++ b/lib/mix/tasks/sql.gen.test.ex @@ -0,0 +1,71 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: 2025 DBVisor + +defmodule Mix.Tasks.Sql.Gen.Test do + use Mix.Task + import Mix.Generator + + @shortdoc "Generates test from the BNF rules" + @shortdoc since: "0.2.0" + + def run([base]) do + create_file("test/conformance/e_test.exs", test_template([mod: SQL.Conformance.ETest, dir: Path.join(base, "E")])) + create_file("test/conformance/f_test.exs", test_template([mod: SQL.Conformance.FTest, dir: Path.join(base, "F")])) + create_file("test/conformance/s_test.exs", test_template([mod: SQL.Conformance.STest, dir: Path.join(base, "S")])) + create_file("test/conformance/t_test.exs", test_template([mod: SQL.Conformance.TTest, dir: Path.join(base, "T")])) + end + + def generate_test(dir) do + for path <- File.ls!(dir), path =~ ".tests.yml", [{~c"feature", feature}, {~c"id", id}, {~c"sql", sql}] <- :yamerl.decode_file(to_charlist(Path.join(dir, path))) do + statements = if is_list(hd(sql)), do: sql, else: [sql] + statements = Enum.map(statements, &String.replace(to_string(&1), ~r{(VARING)}, "VARYING")) + {"#{feature} #{id}", Enum.map(statements, &{trim(&1), &1})} + end + end + + def trim(value) do + value + |> String.replace(~r{\(\s+\b}, &String.replace(&1, " ", "")) + |> String.replace(~r{\(\s+'}, &String.replace(&1, " ", "")) + |> String.replace(~r{\(\s+"}, &String.replace(&1, " ", "")) + |> String.replace(~r{\(\s+\*}, &String.replace(&1, " ", "")) + |> String.replace(~r{[[:alpha:]]+\s+\(}, &String.replace(&1, " ", "")) + |> String.replace(~r{\b\s+\,}, &String.replace(&1, " ", "")) + |> String.replace(~r{\)\s+\,}, &String.replace(&1, " ", "")) + |> String.replace(~r{\'\s+\,}, &String.replace(&1, " ", "")) + |> String.replace(~r{\b\s+\)}, &String.replace(&1, " ", "")) + |> String.replace(~r{'\s+\)}, &String.replace(&1, " ", "")) + |> String.replace(~r{\*\s+\)}, &String.replace(&1, " ", "")) + |> String.replace(~r{\)\s+\)}, &String.replace(&1, " ", "")) + |> String.replace(~r{\W(SELECT|REFERENCES|INSERT|UPDATE|IN|MYTEMP)\(}, &Enum.join(Regex.split(~r{\(}, &1, include_captures: true, trim: true), " ")) + |> String.replace(~r{^(SELECT)\(}, &Enum.join(Regex.split(~r{\(}, &1, include_captures: true, trim: true), " ")) + |> String.replace(~r{\s+\.\s+}, &String.replace(&1, " ", "")) + |> String.replace(~r{\d\s(\+|\-)\d}, &Enum.join(Enum.map(Regex.split(~r{\+|\-}, &1, include_captures: true, trim: true), fn x -> String.trim(x) end), " ")) + |> String.trim() + end + + embed_template(:test, """ + # SPDX-License-Identifier: Apache-2.0 + # SPDX-FileCopyrightText: 2025 DBVisor + + defmodule <%= inspect @mod %>.Adapter do + use SQL.Token + + def token_to_string(value, mod \\\\ __MODULE__) + def token_to_string(value, _mod) when is_atom(value), do: String.upcase(Atom.to_string(value)) + def token_to_string(token, mod), do: SQL.Adapters.ANSI.token_to_string(token, mod) + end + defmodule <%= inspect @mod %> do + use ExUnit.Case, async: true + use SQL, adapter: <%= inspect @mod %>.Adapter + + <%= for {name, statements} <- generate_test(@dir) do %> + test <%= inspect name %> do + <%= for {left, right} <- statements do %> + assert ~s{<%= left %>} == to_string(~SQL[<%= right %>]) + <% end %> + end + <% end %> + end + """) +end diff --git a/mix.exs b/mix.exs index 8415dc6..1d811a1 100644 --- a/mix.exs +++ b/mix.exs @@ -16,7 +16,7 @@ defmodule SQL.MixProject do name: "SQL", docs: docs(), package: package(), - aliases: [bench: "run bench.exs"] + aliases: ["sql.bench": "run bench.exs"] ] end @@ -44,6 +44,7 @@ defmodule SQL.MixProject do {:ecto_sql, "~> 3.12", only: [:dev, :test]}, {:ex_doc, "~> 0.37", only: :dev}, {:postgrex, ">= 0.0.0", only: [:dev, :test]}, + {:yamerl, ">= 0.0.0", only: [:dev, :test]}, ] end end diff --git a/mix.lock b/mix.lock index e41dee6..abc81ad 100644 --- a/mix.lock +++ b/mix.lock @@ -14,4 +14,5 @@ "postgrex": {:hex, :postgrex, "0.20.0", "363ed03ab4757f6bc47942eff7720640795eb557e1935951c1626f0d303a3aed", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "d36ef8b36f323d29505314f704e21a1a038e2dc387c6409ee0cd24144e187c0f"}, "statistex": {:hex, :statistex, "1.0.0", "f3dc93f3c0c6c92e5f291704cf62b99b553253d7969e9a5fa713e5481cd858a5", [:mix], [], "hexpm", "ff9d8bee7035028ab4742ff52fc80a2aa35cece833cf5319009b52f1b5a86c27"}, "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, + "yamerl": {:hex, :yamerl, "0.10.0", "4ff81fee2f1f6a46f1700c0d880b24d193ddb74bd14ef42cb0bcf46e81ef2f8e", [:rebar3], [], "hexpm", "346adb2963f1051dc837a2364e4acf6eb7d80097c0f53cbdc3046ec8ec4b4e6e"}, }