From e0b84073248cd31a86c069d8668066b75c9f9cf1 Mon Sep 17 00:00:00 2001 From: Zephyr Lykos Date: Wed, 30 Sep 2026 14:26:23 +0800 Subject: [PATCH] use :prefix to query attached databases Revert "raise on table prefixes" This reverts commit 71ddd0a431b2b1783e687305e5e0590e86c43a65. As a side effect, the quoting changes also Fixes: eca58dd64053 ("Add support for :check constraint at column level (#29)") --- CHANGELOG.md | 5 + integration_test/prefix_test.exs | 21 ++++ integration_test/test_helper.exs | 5 +- lib/ecto/adapters/sqlite3/connection.ex | 101 ++++++++++++------ .../sqlite3/connection/delete_all_test.exs | 12 +-- .../sqlite3/connection/delete_test.exs | 6 ++ .../sqlite3/connection/insert_test.exs | 5 +- .../adapters/sqlite3/connection/join_test.exs | 24 +++-- .../sqlite3/connection/migration_test.exs | 81 +++++++++----- .../sqlite3/connection/update_all_test.exs | 17 ++- 10 files changed, 196 insertions(+), 81 deletions(-) create mode 100644 integration_test/prefix_test.exs diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c9b50b..9acb0cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ project adheres to [Semantic Versioning][semver]. ## Unreleased +- changed: Reimplement querying prefix names. +- added: Raise on cross-database foreign keys. +- changed: Made `quote_name/1` private. +- changed: quote_name no longer accepts names with double quote. + ## v0.25.0 - fixed: Precedence issues with SQL generation. See [#182](https://github.com/elixir-sqlite/ecto_sqlite3/pull/182) diff --git a/integration_test/prefix_test.exs b/integration_test/prefix_test.exs new file mode 100644 index 0000000..02de614 --- /dev/null +++ b/integration_test/prefix_test.exs @@ -0,0 +1,21 @@ +defmodule Ecto.Integration.PrefixTest do + use Ecto.Integration.Case, async: true + + import Ecto.Query, only: [from: 2] + + alias Ecto.Integration.Post + alias Ecto.Integration.TestRepo + + test "queries an attached database through :prefix" do + TestRepo.query!(~s|ATTACH DATABASE ":memory:" AS "foo"|) + TestRepo.query!(~s|CREATE TABLE "foo"."posts" AS SELECT * FROM "main"."posts"|) + + TestRepo.insert!(%Post{id: 1}, prefix: "foo") + + results = + from(Post, prefix: "foo") + |> TestRepo.all() + + assert [%Post{id: 1}] = results + end +end diff --git a/integration_test/test_helper.exs b/integration_test/test_helper.exs index 1b32ca9..f26545b 100644 --- a/integration_test/test_helper.exs +++ b/integration_test/test_helper.exs @@ -90,8 +90,11 @@ excludes = [ # which is not true for SQLite :lock_for_migrations, - # Migration we don't support + # sadly we can not run prefix tests since ecto_sql's integration test expects + # attached db to persist across connections :prefix, + + # Migration we don't support :add_column_if_not_exists, :remove_column_if_exists, :alter_primary_key, diff --git a/lib/ecto/adapters/sqlite3/connection.ex b/lib/ecto/adapters/sqlite3/connection.ex index 9bbfc55..784332d 100644 --- a/lib/ecto/adapters/sqlite3/connection.ex +++ b/lib/ecto/adapters/sqlite3/connection.ex @@ -271,7 +271,7 @@ defmodule Ecto.Adapters.SQLite3.Connection do def insert(prefix, table, [], [[]], on_conflict, returning, [], _opts) do [ "INSERT INTO ", - quote_table(prefix, table), + quote_name(prefix, table), insert_as(on_conflict), " DEFAULT VALUES", returning(returning) @@ -290,7 +290,7 @@ defmodule Ecto.Adapters.SQLite3.Connection do [ "INSERT INTO ", - quote_table(prefix, table), + quote_name(prefix, table), insert_as(on_conflict), values, on_conflict(on_conflict, header), @@ -313,7 +313,7 @@ defmodule Ecto.Adapters.SQLite3.Connection do [ "UPDATE ", - quote_table(prefix, table), + quote_name(prefix, table), " SET ", fields, " WHERE ", @@ -335,7 +335,7 @@ defmodule Ecto.Adapters.SQLite3.Connection do [ "DELETE FROM ", - quote_table(prefix, table), + quote_name(prefix, table), " WHERE ", filters, returning(returning) @@ -402,7 +402,7 @@ defmodule Ecto.Adapters.SQLite3.Connection do [ [ "CREATE TABLE ", - quote_table(table.prefix, table.name), + quote_name(table.prefix, table.name), ?\s, ?(, column_definitions(table, columns), @@ -421,7 +421,7 @@ defmodule Ecto.Adapters.SQLite3.Connection do [ [ "CREATE TABLE IF NOT EXISTS ", - quote_table(table.prefix, table.name), + quote_name(table.prefix, table.name), ?\s, ?(, column_definitions(table, columns), @@ -437,7 +437,7 @@ defmodule Ecto.Adapters.SQLite3.Connection do [ [ "DROP TABLE ", - quote_table(table.prefix, table.name) + quote_name(table.prefix, table.name) ] ] end @@ -450,7 +450,7 @@ defmodule Ecto.Adapters.SQLite3.Connection do [ [ "DROP TABLE IF EXISTS ", - quote_table(table.prefix, table.name) + quote_name(table.prefix, table.name) ] ] end @@ -463,7 +463,7 @@ defmodule Ecto.Adapters.SQLite3.Connection do Enum.map(changes, fn change -> [ "ALTER TABLE ", - quote_table(table.prefix, table.name), + quote_name(table.prefix, table.name), ?\s, column_change(table, change) ] @@ -498,9 +498,9 @@ defmodule Ecto.Adapters.SQLite3.Connection do "CREATE ", if_do(index.unique, "UNIQUE "), "INDEX ", - quote_name(index.name), + quote_name(index.prefix, index.name), " ON ", - quote_table(index.prefix, index.table), + quote_name(index.table), " (", fields, ?), @@ -517,9 +517,9 @@ defmodule Ecto.Adapters.SQLite3.Connection do "CREATE ", if_do(index.unique, "UNIQUE "), "INDEX IF NOT EXISTS ", - quote_name(index.name), + quote_name(index.prefix, index.name), " ON ", - quote_table(index.prefix, index.table), + quote_name(index.table), " (", fields, ?), @@ -532,7 +532,7 @@ defmodule Ecto.Adapters.SQLite3.Connection do [ [ "DROP INDEX ", - quote_table(index.prefix, index.name) + quote_name(index.prefix, index.name) ] ] end @@ -545,7 +545,7 @@ defmodule Ecto.Adapters.SQLite3.Connection do [ [ "DROP INDEX IF EXISTS ", - quote_table(index.prefix, index.name) + quote_name(index.prefix, index.name) ] ] end @@ -558,9 +558,9 @@ defmodule Ecto.Adapters.SQLite3.Connection do [ [ "ALTER TABLE ", - quote_table(current_table.prefix, current_table.name), + quote_name(current_table.prefix, current_table.name), " RENAME TO ", - quote_table(nil, new_table.name) + quote_name(nil, new_table.name) ] ] end @@ -569,7 +569,7 @@ defmodule Ecto.Adapters.SQLite3.Connection do [ [ "ALTER TABLE ", - quote_table(table.prefix, table.name), + quote_name(table.prefix, table.name), " RENAME COLUMN ", quote_name(current_column), " TO ", @@ -1539,7 +1539,7 @@ defmodule Ecto.Adapters.SQLite3.Connection do {table, schema, prefix} -> name = as_prefix ++ [create_alias(table) | Integer.to_string(pos)] - {quote_table(prefix, table), name, schema} + {quote_name(prefix, table), name, schema} %Ecto.SubQuery{} -> {nil, as_prefix ++ [?s | Integer.to_string(pos)], nil} @@ -1658,7 +1658,7 @@ defmodule Ecto.Adapters.SQLite3.Connection do defp check_expr(nil), do: [] defp check_expr(%{name: name, expr: expr}), - do: [" CONSTRAINT ", name, " CHECK (", expr, ")"] + do: [" CONSTRAINT ", quote_name(name), " CHECK (", expr, ")"] defp collate_expr(nil), do: [] @@ -1717,11 +1717,13 @@ defmodule Ecto.Adapters.SQLite3.Connection do defp reference_expr(%Reference{with: [_]}, _table, _name), do: [] defp reference_expr(%Reference{} = ref, table, name) do + assert_same_database(table.prefix, ref.prefix) + [ " CONSTRAINT ", reference_name(ref, table, name), " REFERENCES ", - quote_table(ref.prefix || table.prefix, ref.table), + quote_name(ref.table), ?(, quote_name(ref.column), ?), @@ -1826,13 +1828,15 @@ defmodule Ecto.Adapters.SQLite3.Connection do end defp composite_fk_definition(table, {_op, name, ref, _opts}) do + assert_same_database(table.prefix, ref.prefix) + {current_columns, reference_columns} = Enum.unzip([{name, ref.column} | ref.with]) [ ", FOREIGN KEY (", quote_names(current_columns), ") REFERENCES ", - quote_table(ref.prefix || table.prefix, ref.table), + quote_name(ref.table), ?(, quote_names(reference_columns), ?), @@ -1855,24 +1859,32 @@ defmodule Ecto.Adapters.SQLite3.Connection do defp quote_names(names), do: Enum.map_intersperse(names, ?,, "e_name/1) - def quote_name(name), do: quote_entity(name) + defp quote_name(nil, name), do: quote_name(name) - def quote_table(table), do: quote_entity(table) + defp quote_name(prefix, name), do: [quote_name(prefix), ?., quote_name(name)] - defp quote_table(nil, name), do: quote_entity(name) - - defp quote_table(prefix, _name) when is_atom(prefix) or is_binary(prefix) do - raise ArgumentError, "SQLite3 does not support table prefixes" + defp quote_name(val) when is_atom(val) do + quote_name(Atom.to_string(val)) end - defp quote_table(_, name), do: quote_entity(name) + defp quote_name(val) when is_binary(val) do + # Don't introduce unnecessary complexity and align with Ecto.Adapters.Postgres.Connection. + # + # Although SQLite and Postgres both allow syntax like: + # ```sql + # CREATE TABLE "lookma""quotes"(id INTEGER); + # SELECT name FROM sqlite_schema WHERE name = 'lookma"quotes'; + # SELECT relname FROM pg_class WHERE relname = 'lookma"quotes'; + # ``` + # there isn't much practical use case for it. + if String.contains?(val, "\"") do + raise ArgumentError, + "bad literal/field/index/table name #{inspect(val)} (\" is not permitted)" + end - defp quote_entity(val) when is_atom(val) do - quote_entity(Atom.to_string(val)) + [[?", val, ?"]] end - defp quote_entity(val), do: [[?", val, ?"]] - defp intersperse_reduce(list, separator, user_acc, reducer, acc \\ []) defp intersperse_reduce([], _separator, user_acc, _reducer, acc), @@ -1903,4 +1915,27 @@ defmodule Ecto.Adapters.SQLite3.Connection do |> escape_string() |> :binary.replace("\"", "\\\"", [:global]) end + + # We know this holds since exqlite does not export sqlite3_db_config from Sqlite3NIF, + # thus nobody can call sqlite3_db_config(db, SQLITE_DBCONFIG_MAINDBNAME, ...) + defp normalize_database_name(nil) do + # TODO: handle modifier-selected temp database somehow? + # src/parse.y: `temp(A) ::= TEMP. {A = pParse->db->init.busy==0;}` + "main" + end + + defp normalize_database_name(name) when is_atom(name) do + normalize_database_name(Atom.to_string(name)) + end + + defp normalize_database_name(name) when is_binary(name) do + String.downcase(name, :ascii) + end + + defp assert_same_database(table_prefix, ref_prefix) do + if normalize_database_name(ref_prefix || table_prefix) != + normalize_database_name(table_prefix) do + raise ArgumentError, "SQLite3 does not support cross-database foreign keys" + end + end end diff --git a/test/ecto/adapters/sqlite3/connection/delete_all_test.exs b/test/ecto/adapters/sqlite3/connection/delete_all_test.exs index d73ea7f..e82cd56 100644 --- a/test/ecto/adapters/sqlite3/connection/delete_all_test.exs +++ b/test/ecto/adapters/sqlite3/connection/delete_all_test.exs @@ -65,20 +65,20 @@ defmodule Ecto.Adapters.SQLite3.Connection.DeleteAllTest do end test "delete all with prefix" do - assert_raise ArgumentError, "SQLite3 does not support table prefixes", fn -> + query = Schema |> Ecto.Queryable.to_query() |> Map.put(:prefix, "prefix") |> plan() - |> delete_all() - end - assert_raise ArgumentError, "SQLite3 does not support table prefixes", fn -> + assert delete_all(query) == ~s{DELETE FROM "prefix"."schema" AS s0} + + query = Schema |> from(prefix: "first") |> Map.put(:prefix, "prefix") |> plan() - |> delete_all() - end + + assert delete_all(query) == ~s{DELETE FROM "first"."schema" AS s0} end end diff --git a/test/ecto/adapters/sqlite3/connection/delete_test.exs b/test/ecto/adapters/sqlite3/connection/delete_test.exs index 892847d..d725833 100644 --- a/test/ecto/adapters/sqlite3/connection/delete_test.exs +++ b/test/ecto/adapters/sqlite3/connection/delete_test.exs @@ -6,5 +6,11 @@ defmodule Ecto.Adapters.SQLite3.DeleteTest do test "delete" do query = delete(nil, "schema", [x: 1, y: 2], []) assert query == ~s{DELETE FROM "schema" WHERE "x" = ? AND "y" = ?} + + query = delete("prefix", "schema", [x: 1, y: 2], []) + assert query == ~s{DELETE FROM "prefix"."schema" WHERE "x" = ? AND "y" = ?} + + query = delete(nil, "schema", [x: nil, y: 2], []) + assert query == ~s{DELETE FROM "schema" WHERE "x" IS NULL AND "y" = ?} end end diff --git a/test/ecto/adapters/sqlite3/connection/insert_test.exs b/test/ecto/adapters/sqlite3/connection/insert_test.exs index 5e954eb..878e75c 100644 --- a/test/ecto/adapters/sqlite3/connection/insert_test.exs +++ b/test/ecto/adapters/sqlite3/connection/insert_test.exs @@ -25,9 +25,8 @@ defmodule Ecto.Adapters.SQLite3.Connection.InsertTest do query = insert(nil, "schema", [], [[]], {:raise, [], []}, []) assert query == ~s{INSERT INTO "schema" DEFAULT VALUES} - assert_raise ArgumentError, "SQLite3 does not support table prefixes", fn -> - insert("prefix", "schema", [], [[]], {:raise, [], []}, []) - end + assert insert("prefix", "schema", [], [[]], {:raise, [], []}, []) == + ~s{INSERT INTO "prefix"."schema" DEFAULT VALUES} query = insert(nil, "schema", [:x, :y], [[:x, :y]], {:raise, [], []}, [:id]) assert query == ~s{INSERT INTO "schema" ("x","y") VALUES (?1,?2) RETURNING "id"} diff --git a/test/ecto/adapters/sqlite3/connection/join_test.exs b/test/ecto/adapters/sqlite3/connection/join_test.exs index 7868272..dbec98c 100644 --- a/test/ecto/adapters/sqlite3/connection/join_test.exs +++ b/test/ecto/adapters/sqlite3/connection/join_test.exs @@ -114,25 +114,35 @@ defmodule Ecto.Adapters.SQLite3.Connection.JoinTest do ~s{) AS s1 ON 1} == all(query) end - test "join with prefix is not supported" do - assert_raise ArgumentError, "SQLite3 does not support table prefixes", fn -> + test "join with prefix" do + query = Schema |> join(:inner, [p], q in Schema2, on: p.x == q.z) |> select([], true) |> Map.put(:prefix, "prefix") |> plan() - |> all() - end - assert_raise ArgumentError, "SQLite3 does not support table prefixes", fn -> + assert all(query) == + """ + SELECT 1 \ + FROM "prefix"."schema" AS s0 \ + INNER JOIN "prefix"."schema2" AS s1 ON s0."x" = s1."z"\ + """ + + query = Schema |> from(prefix: "first") |> join(:inner, [p], q in Schema2, on: p.x == q.z, prefix: "second") |> select([], true) |> Map.put(:prefix, "prefix") |> plan() - |> all() - end + + assert all(query) == + """ + SELECT 1 \ + FROM "first"."schema" AS s0 \ + INNER JOIN "second"."schema2" AS s1 ON s0."x" = s1."z"\ + """ end test "join with values" do diff --git a/test/ecto/adapters/sqlite3/connection/migration_test.exs b/test/ecto/adapters/sqlite3/connection/migration_test.exs index 73826d8..69f2be0 100644 --- a/test/ecto/adapters/sqlite3/connection/migration_test.exs +++ b/test/ecto/adapters/sqlite3/connection/migration_test.exs @@ -43,9 +43,13 @@ defmodule Ecto.Adapters.SQLite3.Connection.MigrationTest do {:create, table(:posts, prefix: :foo), [{:add, :category_0, %Reference{table: :categories}, []}]} - assert_raise ArgumentError, "SQLite3 does not support table prefixes", fn -> - execute_ddl(create) - end + assert execute_ddl(create) == [ + """ + CREATE TABLE "foo"."posts" (\ + "category_0" INTEGER CONSTRAINT "posts_category_0_fkey" REFERENCES "categories"("id")\ + )\ + """ + ] end test "create table with references" do @@ -60,7 +64,8 @@ defmodule Ecto.Adapters.SQLite3.Connection.MigrationTest do [null: false]}, {:add, :category_4, %Reference{table: :categories, on_delete: :nilify_all}, []}, - {:add, :category_5, %Reference{table: :categories, on_update: :nothing}, []}, + {:add, :category_5, + %Reference{table: :categories, prefix: :main, on_update: :nothing}, []}, {:add, :category_6, %Reference{table: :categories, on_update: :update_all}, [null: false]}, {:add, :category_7, %Reference{table: :categories, on_update: :nilify_all}, @@ -93,6 +98,20 @@ defmodule Ecto.Adapters.SQLite3.Connection.MigrationTest do )\ """ ] + + create = + {:create, table(:posts), + [ + {:add, :id, :serial, [primary_key: true]}, + {:add, :cross_db_fk, + %Reference{table: :categories, prefix: :foo, on_update: :nothing}, []} + ]} + + assert_raise ArgumentError, + "SQLite3 does not support cross-database foreign keys", + fn -> + execute_ddl(create) + end end test "create table with options" do @@ -296,9 +315,7 @@ defmodule Ecto.Adapters.SQLite3.Connection.MigrationTest do test "drop table with prefix" do drop = {:drop, table(:posts, prefix: :foo)} - assert_raise ArgumentError, "SQLite3 does not support table prefixes", fn -> - execute_ddl(drop) - end + assert execute_ddl(drop) == [~s|DROP TABLE "foo"."posts"|] end test "alter table" do @@ -308,6 +325,8 @@ defmodule Ecto.Adapters.SQLite3.Connection.MigrationTest do {:add, :title, :string, [default: "Untitled", size: 100, null: false]}, {:add, :author_id, %Reference{table: :author}, []}, {:add, :category_id, %Reference{table: :categories, validate: false}, []}, + {:add, :email, :string, + check: %{name: "test_constraint", expr: "email != 'test@example.com'"}}, {:remove, :summary}, {:remove, :body, :text, []}, {:remove, :space_id, %Reference{table: :author}, []} @@ -317,6 +336,7 @@ defmodule Ecto.Adapters.SQLite3.Connection.MigrationTest do ~s|ALTER TABLE "posts" ADD COLUMN "title" TEXT DEFAULT 'Untitled' NOT NULL|, ~s|ALTER TABLE "posts" ADD COLUMN "author_id" INTEGER CONSTRAINT "posts_author_id_fkey" REFERENCES "author"("id")|, ~s|ALTER TABLE "posts" ADD COLUMN "category_id" INTEGER CONSTRAINT "posts_category_id_fkey" REFERENCES "categories"("id")|, + ~s|ALTER TABLE "posts" ADD COLUMN "email" TEXT CONSTRAINT "test_constraint" CHECK (email != 'test@example.com')|, ~s|ALTER TABLE "posts" DROP COLUMN "summary"|, ~s|ALTER TABLE "posts" DROP COLUMN "body"|, ~s|ALTER TABLE "posts" DROP COLUMN "space_id"| @@ -328,9 +348,13 @@ defmodule Ecto.Adapters.SQLite3.Connection.MigrationTest do {:alter, table(:posts, prefix: :foo), [{:add, :author_id, %Reference{table: :author}, []}]} - assert_raise ArgumentError, "SQLite3 does not support table prefixes", fn -> - execute_ddl(alter) - end + assert execute_ddl(alter) == [ + """ + ALTER TABLE "foo"."posts" \ + ADD COLUMN "author_id" INTEGER \ + CONSTRAINT "posts_author_id_fkey" REFERENCES "author"("id")\ + """ + ] end test "alter table with serial primary key" do @@ -374,20 +398,26 @@ defmodule Ecto.Adapters.SQLite3.Connection.MigrationTest do test "create index with prefix" do create = {:create, index(:posts, [:category_id, :permalink], prefix: :foo)} - assert_raise ArgumentError, "SQLite3 does not support table prefixes", fn -> - execute_ddl(create) - end + assert execute_ddl(create) == [ + """ + CREATE INDEX "foo"."posts_category_id_permalink_index" \ + ON "posts" ("category_id", "permalink")\ + """ + ] create = {:create, index(:posts, ["lower(permalink)"], name: "posts$main", prefix: :foo)} - assert_raise ArgumentError, "SQLite3 does not support table prefixes", fn -> - execute_ddl(create) - end + assert execute_ddl(create) == [ + """ + CREATE INDEX "foo"."posts$main" ON "posts" (lower(permalink))\ + """ + ] end test "create index with comment" do - create = {:create, index(:posts, [:category_id, :permalink], comment: "comment")} + create = + {:create, index(:posts, [:category_id, :permalink], comment: "comment")} assert execute_ddl(create) == [ """ @@ -470,10 +500,7 @@ defmodule Ecto.Adapters.SQLite3.Connection.MigrationTest do test "drop index with prefix" do drop = {:drop, index(:posts, [:id], name: "posts$main", prefix: :foo), :restrict} - - assert_raise ArgumentError, "SQLite3 does not support table prefixes", fn -> - execute_ddl(drop) - end + assert execute_ddl(drop) == [~s|DROP INDEX "foo"."posts$main"|] end test "drop index concurrently not supported" do @@ -517,9 +544,9 @@ defmodule Ecto.Adapters.SQLite3.Connection.MigrationTest do test "rename table with prefix" do rename = {:rename, table(:posts, prefix: :foo), table(:new_posts, prefix: :foo)} - assert_raise ArgumentError, "SQLite3 does not support table prefixes", fn -> - execute_ddl(rename) - end + assert execute_ddl(rename) == [ + ~s|ALTER TABLE "foo"."posts" RENAME TO "new_posts"| + ] end test "rename column" do @@ -533,9 +560,9 @@ defmodule Ecto.Adapters.SQLite3.Connection.MigrationTest do test "rename column in prefixed table" do rename = {:rename, table(:posts, prefix: :foo), :given_name, :first_name} - assert_raise ArgumentError, "SQLite3 does not support table prefixes", fn -> - execute_ddl(rename) - end + assert execute_ddl(rename) == [ + ~s|ALTER TABLE "foo"."posts" RENAME COLUMN "given_name" TO "first_name"| + ] end test "autoincrement support" do diff --git a/test/ecto/adapters/sqlite3/connection/update_all_test.exs b/test/ecto/adapters/sqlite3/connection/update_all_test.exs index 60b23cd..1c3c31e 100644 --- a/test/ecto/adapters/sqlite3/connection/update_all_test.exs +++ b/test/ecto/adapters/sqlite3/connection/update_all_test.exs @@ -131,12 +131,21 @@ defmodule Ecto.Adapters.SQLite3.Connection.UpdateAllTest do end test "update all with prefix" do - assert_raise ArgumentError, "SQLite3 does not support table prefixes", fn -> - from(m in Schema, update: [set: [x: 0]]) + query = + (m in Schema) + |> from(update: [set: [x: 0]]) |> Map.put(:prefix, "prefix") |> plan(:update_all) - |> update_all() - end + + assert update_all(query) == ~s{UPDATE "prefix"."schema" AS s0 SET "x" = 0} + + query = + (m in Schema) + |> from(prefix: "first", update: [set: [x: 0]]) + |> Map.put(:prefix, "prefix") + |> plan(:update_all) + + assert update_all(query) == ~s{UPDATE "first"."schema" AS s0 SET "x" = 0} end test "update all with left join" do