Skip to content

Commit d4725d1

Browse files
committed
Support modifiers and drop mode
1 parent af4dd33 commit d4725d1

3 files changed

Lines changed: 118 additions & 97 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ project adheres to [Semantic Versioning][semver].
77

88
## Unreleased
99

10+
- changed: Raise on incorrect use of drop mode.
11+
- added: Support rendering `:modifiers` in `CREATE TABLE` commands.
1012
- added: Support check constraints via [`Ecto.Migration.constraint/3`](https://hexdocs.pm/ecto_sql/Ecto.Migration.html#constraint/3).
1113
- changed: Reimplement querying prefix names.
1214
- added: Raise on cross-database foreign keys.

‎lib/ecto/adapters/sqlite3/connection.ex‎

Lines changed: 76 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -390,38 +390,30 @@ defmodule Ecto.Adapters.SQLite3.Connection do
390390
## DDL
391391
##
392392

393+
@creates [:create, :create_if_not_exists]
394+
@drops [:drop, :drop_if_exists]
395+
393396
@impl true
394397
def execute_ddl({_command, %Table{options: options}, _}) when is_list(options) do
395398
raise ArgumentError, "SQLite3 adapter does not support keyword lists in :options"
396399
end
397400

398-
def execute_ddl({:create, %Table{} = table, columns}) do
401+
def execute_ddl({command, %Table{} = table, columns}) when command in @creates do
399402
{table, composite_pk_def} = composite_pk_definition(table, columns)
400403
composite_fk_defs = composite_fk_definitions(table, columns)
401404

402-
[
403-
[
404-
"CREATE TABLE ",
405-
quote_name(table.prefix, table.name),
406-
?\s,
407-
?(,
408-
column_definitions(table, columns),
409-
composite_pk_def,
410-
composite_fk_defs,
411-
?),
412-
options_expr(table.options)
413-
]
414-
]
415-
end
405+
table_name = quote_name(table.prefix, table.name)
416406

417-
def execute_ddl({:create_if_not_exists, %Table{} = table, columns}) do
418-
{table, composite_pk_def} = composite_pk_definition(table, columns)
419-
composite_fk_defs = composite_fk_definitions(table, columns)
407+
modifiers = modifiers_expr(table.modifiers)
408+
assert_unqual_name(modifiers, table.prefix)
420409

421410
[
422411
[
423-
"CREATE TABLE IF NOT EXISTS ",
424-
quote_name(table.prefix, table.name),
412+
"CREATE ",
413+
modifiers,
414+
"TABLE ",
415+
if_do(command == :create_if_not_exists, "IF NOT EXISTS "),
416+
table_name,
425417
?\s,
426418
?(,
427419
column_definitions(table, columns),
@@ -433,32 +425,17 @@ defmodule Ecto.Adapters.SQLite3.Connection do
433425
]
434426
end
435427

436-
def execute_ddl({:drop, %Table{} = table}) do
428+
def execute_ddl({command, %Table{} = table, mode}) when command in @drops do
437429
[
438430
[
439431
"DROP TABLE ",
440-
quote_name(table.prefix, table.name)
441-
]
442-
]
443-
end
444-
445-
def execute_ddl({:drop, %Table{} = table, _mode}) do
446-
execute_ddl({:drop, table})
447-
end
448-
449-
def execute_ddl({:drop_if_exists, %Table{} = table}) do
450-
[
451-
[
452-
"DROP TABLE IF EXISTS ",
453-
quote_name(table.prefix, table.name)
432+
if_do(command == :drop_if_exists, "IF EXISTS "),
433+
quote_name(table.prefix, table.name),
434+
drop_mode(mode)
454435
]
455436
]
456437
end
457438

458-
def execute_ddl({:drop_if_exists, %Table{} = table, _mode}) do
459-
execute_ddl({:drop_if_exists, table})
460-
end
461-
462439
def execute_ddl({:alter, %Table{} = table, changes}) do
463440
Enum.map(changes, fn change ->
464441
[
@@ -490,14 +467,15 @@ defmodule Ecto.Adapters.SQLite3.Connection do
490467
raise ArgumentError, "`nulls_distinct` is not supported with SQLite3"
491468
end
492469

493-
def execute_ddl({:create, %Index{} = index}) do
470+
def execute_ddl({command, %Index{} = index}) when command in @creates do
494471
fields = Enum.map_intersperse(index.columns, ", ", &index_expr/1)
495472

496473
[
497474
[
498475
"CREATE ",
499476
if_do(index.unique, "UNIQUE "),
500477
"INDEX ",
478+
if_do(command == :create_if_not_exists, "IF NOT EXISTS "),
501479
quote_name(index.prefix, index.name),
502480
" ON ",
503481
quote_name(index.table),
@@ -509,51 +487,17 @@ defmodule Ecto.Adapters.SQLite3.Connection do
509487
]
510488
end
511489

512-
def execute_ddl({:create_if_not_exists, %Index{} = index}) do
513-
fields = Enum.map_intersperse(index.columns, ", ", &index_expr/1)
514-
515-
[
516-
[
517-
"CREATE ",
518-
if_do(index.unique, "UNIQUE "),
519-
"INDEX IF NOT EXISTS ",
520-
quote_name(index.prefix, index.name),
521-
" ON ",
522-
quote_name(index.table),
523-
" (",
524-
fields,
525-
?),
526-
if_do(index.where, [" WHERE ", to_string(index.where)])
527-
]
528-
]
529-
end
530-
531-
def execute_ddl({:drop, %Index{} = index}) do
490+
def execute_ddl({command, %Index{} = index, mode}) when command in @drops do
532491
[
533492
[
534493
"DROP INDEX ",
535-
quote_name(index.prefix, index.name)
536-
]
537-
]
538-
end
539-
540-
def execute_ddl({:drop, %Index{} = index, _mode}) do
541-
execute_ddl({:drop, index})
542-
end
543-
544-
def execute_ddl({:drop_if_exists, %Index{} = index}) do
545-
[
546-
[
547-
"DROP INDEX IF EXISTS ",
548-
quote_name(index.prefix, index.name)
494+
if_do(command == :drop_if_exists, "IF EXISTS "),
495+
quote_name(index.prefix, index.name),
496+
drop_mode(mode)
549497
]
550498
]
551499
end
552500

553-
def execute_ddl({:drop_if_exists, %Index{} = index, _mode}) do
554-
execute_ddl({:drop_if_exists, index})
555-
end
556-
557501
def execute_ddl({:rename, %Table{} = current_table, %Table{} = new_table}) do
558502
[
559503
[
@@ -600,18 +544,14 @@ defmodule Ecto.Adapters.SQLite3.Connection do
600544
raise ArgumentError, "SQLite3 does not support exclusion constraints"
601545
end
602546

603-
def execute_ddl({:drop, %Constraint{}, :cascade}) do
604-
raise ArgumentError,
605-
"SQLite3 does not support `CASCADE` in `DROP CONSTRAINT` commands"
606-
end
607-
608-
def execute_ddl({:drop, %Constraint{} = constraint, _mode}) do
547+
def execute_ddl({:drop, %Constraint{} = constraint, mode}) do
609548
[
610549
[
611550
"ALTER TABLE ",
612551
quote_name(constraint.prefix, constraint.table),
613552
" DROP CONSTRAINT ",
614-
quote_name(constraint.name)
553+
quote_name(constraint.name),
554+
drop_mode(mode)
615555
]
616556
]
617557
end
@@ -622,7 +562,7 @@ defmodule Ecto.Adapters.SQLite3.Connection do
622562

623563
def execute_ddl({:rename, %Index{} = index, new_index}) do
624564
[
625-
execute_ddl({:drop, index}),
565+
execute_ddl({:drop, index, :restrict}),
626566
execute_ddl({:create, %Index{index | name: new_index}})
627567
]
628568
end
@@ -635,6 +575,11 @@ defmodule Ecto.Adapters.SQLite3.Connection do
635575
{"SELECT name FROM sqlite_master WHERE type='table' AND name=? LIMIT 1", [table]}
636576
end
637577

578+
defp drop_mode(:cascade) do
579+
raise ArgumentError, "SQLite3 does not support `CASCADE` in this command"
580+
end
581+
defp drop_mode(:restrict), do: []
582+
638583
##
639584
## Query generation
640585
##
@@ -1736,6 +1681,22 @@ defmodule Ecto.Adapters.SQLite3.Connection do
17361681
defp pk_expr(true, _), do: " PRIMARY KEY"
17371682
defp pk_expr(_, _), do: []
17381683

1684+
defp modifiers_expr(nil), do: []
1685+
1686+
defp modifiers_expr(modifiers) when is_binary(modifiers) do
1687+
if temporary?(modifiers) do
1688+
[modifiers, ?\s]
1689+
else
1690+
raise ArgumentError,
1691+
~s|SQLite3 adapter expects :modifiers to be one of [nil, "TEMP", "TEMPORARY"], got #{inspect(modifiers)}|
1692+
end
1693+
end
1694+
1695+
defp modifiers_expr(other) do
1696+
raise ArgumentError,
1697+
"SQLite3 adapter expects :modifiers to be a string or nil, got #{inspect(other)}"
1698+
end
1699+
17391700
defp options_expr(nil), do: []
17401701

17411702
defp options_expr(options) when is_list(options) do
@@ -1748,7 +1709,7 @@ defmodule Ecto.Adapters.SQLite3.Connection do
17481709
defp reference_expr(%Reference{with: [_]}, _table, _name), do: []
17491710

17501711
defp reference_expr(%Reference{} = ref, table, name) do
1751-
assert_same_database(table.prefix, ref.prefix)
1712+
assert_same_database(table, ref)
17521713

17531714
[
17541715
" CONSTRAINT ",
@@ -1859,7 +1820,7 @@ defmodule Ecto.Adapters.SQLite3.Connection do
18591820
end
18601821

18611822
defp composite_fk_definition(table, {_op, name, ref, _opts}) do
1862-
assert_same_database(table.prefix, ref.prefix)
1823+
assert_same_database(table, ref)
18631824

18641825
{current_columns, reference_columns} = Enum.unzip([{name, ref.column} | ref.with])
18651826

@@ -1950,8 +1911,6 @@ defmodule Ecto.Adapters.SQLite3.Connection do
19501911
# We know this holds since exqlite does not export sqlite3_db_config from Sqlite3NIF,
19511912
# thus nobody can call sqlite3_db_config(db, SQLITE_DBCONFIG_MAINDBNAME, ...)
19521913
defp normalize_database_name(nil) do
1953-
# TODO: handle modifier-selected temp database somehow?
1954-
# src/parse.y: `temp(A) ::= TEMP. {A = pParse->db->init.busy==0;}`
19551914
"main"
19561915
end
19571916

@@ -1963,10 +1922,34 @@ defmodule Ecto.Adapters.SQLite3.Connection do
19631922
String.downcase(name, :ascii)
19641923
end
19651924

1966-
defp assert_same_database(table_prefix, ref_prefix) do
1967-
if normalize_database_name(ref_prefix || table_prefix) !=
1968-
normalize_database_name(table_prefix) do
1925+
defp temporary?(nil), do: false
1926+
1927+
defp temporary?(modifiers) when is_binary(modifiers),
1928+
do:
1929+
(modifiers
1930+
|> String.trim()
1931+
|> String.upcase(:ascii)) in ["TEMP", "TEMPORARY"]
1932+
1933+
defp temporary?(_), do: false
1934+
1935+
defp table_database(%Table{} = table) do
1936+
if temporary?(table.modifiers),
1937+
do: "temp",
1938+
else: normalize_database_name(table.prefix)
1939+
end
1940+
1941+
defp assert_same_database(%Table{} = table, %Reference{} = ref) do
1942+
table_db = table_database(table)
1943+
ref_db = if ref.prefix, do: normalize_database_name(ref.prefix), else: table_db
1944+
1945+
if ref_db != table_db do
19691946
raise ArgumentError, "SQLite3 does not support cross-database foreign keys"
19701947
end
19711948
end
1949+
1950+
defp assert_unqual_name(modifiers, prefix) do
1951+
if modifiers != [] and prefix != nil do
1952+
raise ArgumentError, "SQLite3 does not support a prefix on a temporary table"
1953+
end
1954+
end
19721955
end

‎test/ecto/adapters/sqlite3/connection/migration_test.exs‎

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,42 @@ defmodule Ecto.Adapters.SQLite3.Connection.MigrationTest do
130130
]
131131
end
132132

133+
test "create table with modifiers" do
134+
create =
135+
{:create, table(:posts, modifiers: "TEMPORARY"),
136+
[
137+
{:add, :id, :serial, [primary_key: true]},
138+
{:add, :created_at, :naive_datetime, []}
139+
]}
140+
141+
assert execute_ddl(create) ==
142+
[
143+
~s|CREATE TEMPORARY TABLE "posts" ("id" INTEGER PRIMARY KEY AUTOINCREMENT, "created_at" TEXT)|
144+
]
145+
146+
create =
147+
{:create, table(:posts, modifiers: "TEMPORARY"),
148+
[
149+
{:add, :id, :serial, [primary_key: true]},
150+
{:add, :category_0, %Reference{table: :categories, prefix: :foo}, []}
151+
]}
152+
153+
assert_raise ArgumentError,
154+
"SQLite3 does not support cross-database foreign keys",
155+
fn -> execute_ddl(create) end
156+
157+
create =
158+
{:create, table(:posts, modifiers: "UNLOGGED"),
159+
[
160+
{:add, :id, :serial, [primary_key: true]},
161+
{:add, :created_at, :naive_datetime, []}
162+
]}
163+
164+
assert_raise ArgumentError,
165+
~s|SQLite3 adapter expects :modifiers to be one of [nil, "TEMP", "TEMPORARY"], got "UNLOGGED"|,
166+
fn -> execute_ddl(create) end
167+
end
168+
133169
test "create table with composite key" do
134170
create =
135171
{:create, table(:posts),
@@ -310,12 +346,12 @@ defmodule Ecto.Adapters.SQLite3.Connection.MigrationTest do
310346
end
311347

312348
test "drop table" do
313-
drop = {:drop, table(:posts)}
349+
drop = {:drop, table(:posts), :restrict}
314350
assert execute_ddl(drop) == [~s|DROP TABLE "posts"|]
315351
end
316352

317353
test "drop table with prefix" do
318-
drop = {:drop, table(:posts, prefix: :foo)}
354+
drop = {:drop, table(:posts, prefix: :foo), :restrict}
319355

320356
assert execute_ddl(drop) == [~s|DROP TABLE "foo"."posts"|]
321357
end
@@ -496,7 +532,7 @@ defmodule Ecto.Adapters.SQLite3.Connection.MigrationTest do
496532
end
497533

498534
test "drop index" do
499-
drop = {:drop, index(:posts, [:id], name: "posts$main")}
535+
drop = {:drop, index(:posts, [:id], name: "posts$main"), :restrict}
500536
assert execute_ddl(drop) == [~s|DROP INDEX "posts$main"|]
501537
end
502538

@@ -589,7 +625,7 @@ defmodule Ecto.Adapters.SQLite3.Connection.MigrationTest do
589625
drop = {:drop, constraint(:products, "price_must_be_positive"), :cascade}
590626

591627
assert_raise ArgumentError,
592-
"SQLite3 does not support `CASCADE` in `DROP CONSTRAINT` commands",
628+
"SQLite3 does not support `CASCADE` in this command",
593629
fn -> execute_ddl(drop) end
594630

595631
drop =

0 commit comments

Comments
 (0)