Switch to sqlite (#22)

* Updated project to use sqlite

* Edited migrations directly because I fear no man

* Updated search functions and tests to work with sqlite

* Updated build tools to remove postgres
This commit is contained in:
Kieran 2024-02-16 17:11:41 -08:00 committed by GitHub
parent 53bd8681c3
commit b81c8d64b3
29 changed files with 121 additions and 186 deletions

View file

@ -1,3 +0,0 @@
POSTGRES_HOST=postgres
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres

View file

@ -24,9 +24,6 @@ jobs:
- name: Create and populate ENV file - name: Create and populate ENV file
run: | run: |
echo MIX_ENV=test >> .env echo MIX_ENV=test >> .env
echo POSTGRES_HOST=postgres >> .env
echo POSTGRES_USER=postgres >> .env
echo POSTGRES_PASSWORD=postgres >> .env
- name: Pull prebuilt images - name: Pull prebuilt images
run: docker compose pull run: docker compose pull

4
.gitignore vendored
View file

@ -41,3 +41,7 @@ npm-debug.log
.DS_Store .DS_Store
scratchpad.md scratchpad.md
/scratchpad/ /scratchpad/
# Database files
*.db
*.db-*

View file

@ -3,7 +3,7 @@ FROM elixir:latest
# Install debian packages # Install debian packages
RUN apt-get update -qq RUN apt-get update -qq
RUN apt-get install -y inotify-tools postgresql-client ffmpeg \ RUN apt-get install -y inotify-tools ffmpeg \
python3 python3-pip python3-setuptools python3-wheel python3-dev python3 python3-pip python3-setuptools python3-wheel python3-dev
# Install nodejs # Install nodejs

View file

@ -29,6 +29,7 @@ config :pinchflat, PinchflatWeb.Endpoint,
live_view: [signing_salt: "/t5878kO"] live_view: [signing_salt: "/t5878kO"]
config :pinchflat, Oban, config :pinchflat, Oban,
engine: Oban.Engines.Lite,
repo: Pinchflat.Repo, repo: Pinchflat.Repo,
# Keep old jobs for 30 days for display in the UI # Keep old jobs for 30 days for display in the UI
plugins: [{Oban.Plugins.Pruner, max_age: 30 * 24 * 60 * 60}], plugins: [{Oban.Plugins.Pruner, max_age: 30 * 24 * 60 * 60}],

View file

@ -6,13 +6,9 @@ config :pinchflat,
# Configure your database # Configure your database
config :pinchflat, Pinchflat.Repo, config :pinchflat, Pinchflat.Repo,
username: System.get_env("POSTGRES_USER"), database: Path.expand("../priv/repo/pinchflat_dev.db", Path.dirname(__ENV__.file)),
password: System.get_env("POSTGRES_PASSWORD"),
hostname: System.get_env("POSTGRES_HOST"),
database: "pinchflat_dev",
stacktrace: true,
show_sensitive_data_on_connection_error: true, show_sensitive_data_on_connection_error: true,
pool_size: 10 pool_size: 5
# For development, we disable any cache and enable # For development, we disable any cache and enable
# debugging and code reloading. # debugging and code reloading.

View file

@ -21,20 +21,16 @@ if System.get_env("PHX_SERVER") do
end end
if config_env() == :prod do if config_env() == :prod do
database_url = database_path =
System.get_env("DATABASE_URL") || System.get_env("DATABASE_PATH") ||
raise """ raise """
environment variable DATABASE_URL is missing. environment variable DATABASE_PATH is missing.
For example: ecto://USER:PASS@HOST/DATABASE For example: /etc/pinchflat/pinchflat.db
""" """
maybe_ipv6 = if System.get_env("ECTO_IPV6") in ~w(true 1), do: [:inet6], else: []
config :pinchflat, Pinchflat.Repo, config :pinchflat, Pinchflat.Repo,
# ssl: true, database: database_path,
url: database_url, pool_size: String.to_integer(System.get_env("POOL_SIZE") || "5")
pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"),
socket_options: maybe_ipv6
# The secret key base is used to sign/encrypt cookies and other secrets. # The secret key base is used to sign/encrypt cookies and other secrets.
# A default value is used in config/dev.exs and config/test.exs but you # A default value is used in config/dev.exs and config/test.exs but you

View file

@ -14,12 +14,9 @@ config :pinchflat, Oban, testing: :manual
# to provide built-in test partitioning in CI environment. # to provide built-in test partitioning in CI environment.
# Run `mix help test` for more information. # Run `mix help test` for more information.
config :pinchflat, Pinchflat.Repo, config :pinchflat, Pinchflat.Repo,
username: System.get_env("POSTGRES_USER"), database: Path.expand("../priv/repo/pinchflat_test.db", Path.dirname(__ENV__.file)),
password: System.get_env("POSTGRES_PASSWORD"), pool_size: 5,
hostname: System.get_env("POSTGRES_HOST"), pool: Ecto.Adapters.SQL.Sandbox
database: "pinchflat_test#{System.get_env("MIX_TEST_PARTITION")}",
pool: Ecto.Adapters.SQL.Sandbox,
pool_size: 10
# We don't run a server during test. If one is required, # We don't run a server during test. If one is required,
# you can enable the server option below. # you can enable the server option below.

View file

@ -1,11 +1,5 @@
version: '3' version: '3'
services: services:
postgres:
image: 'postgres:16-alpine'
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
phx: phx:
build: . build: .
volumes: volumes:
@ -13,4 +7,3 @@ services:
ports: ports:
- '4008:4008' - '4008:4008'
command: tail -F /dev/null command: tail -F /dev/null
env_file: .env

View file

@ -1,22 +1,12 @@
version: '3' version: '3'
services: services:
postgres:
image: 'postgres:16-alpine'
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
phx: phx:
build: . build: .
volumes: volumes:
- '.:/app' - '.:/app'
ports: ports:
- '4008:4008' - '4008:4008'
depends_on:
- postgres
command: command:
- ./docker-run.sh - ./docker-run.sh
stdin_open: true stdin_open: true
tty: true tty: true
env_file:
- .env

View file

@ -10,15 +10,6 @@ echo "\nInstalling JS..."
cd assets && yarn install cd assets && yarn install
cd .. cd ..
# Wait for Postgres to become available.
export PGPASSWORD=$(echo $POSTGRES_PASSWORD)
until psql -h postgres -U $POSTGRES_USER -c '\q' 2>/dev/null; do
echo >&2 "Postgres is unavailable - sleeping"
sleep 1
done
echo "\nPostgres is available: continuing with database setup..."
# Potentially Set up the database # Potentially Set up the database
mix ecto.create mix ecto.create
mix ecto.migrate mix ecto.migrate

View file

@ -51,31 +51,29 @@ defmodule Pinchflat.Media do
Returns a list of media_items that match the search term. Adds a `matching_search_term` Returns a list of media_items that match the search term. Adds a `matching_search_term`
virtual field to the result set. virtual field to the result set.
Has explit handling for blank search terms because SQLite doesn't like empty MATCH clauses.
Returns [%MediaItem{}, ...]. Returns [%MediaItem{}, ...].
""" """
def search(search_term, opts \\ []) do def search(_search_term, _opts \\ [])
def search("", _opts), do: []
def search(nil, _opts), do: []
def search(search_term, opts) do
limit = Keyword.get(opts, :limit, 50) limit = Keyword.get(opts, :limit, 50)
from(mi in MediaItem, from(mi in MediaItem,
where: fragment("searchable @@ websearch_to_tsquery(?)", ^search_term), join: mi_search_index in assoc(mi, :media_items_search_index),
where: fragment("media_items_search_index MATCH ?", ^search_term),
select_merge: %{ select_merge: %{
matching_search_term: matching_search_term:
fragment( fragment("""
""" snippet(media_items_search_index, 0, '[PF_HIGHLIGHT]', '[/PF_HIGHLIGHT]', '...', 20) ||
ts_headline( ' ' ||
'english', snippet(media_items_search_index, 1, '[PF_HIGHLIGHT]', '[/PF_HIGHLIGHT]', '...', 20)
CONCAT(title, ' ', description), """)
websearch_to_tsquery(?),
'StartSel=[PF_HIGHLIGHT],StopSel=[/PF_HIGHLIGHT]'
)
""",
^search_term
)
},
order_by: {
:desc,
fragment("ts_rank_cd(searchable, websearch_to_tsquery(?), 0)", ^search_term)
}, },
order_by: [desc: fragment("rank")],
limit: ^limit limit: ^limit
) )
|> Repo.all() |> Repo.all()
@ -174,7 +172,10 @@ defmodule Pinchflat.Media do
Enum.reduce(mapped_struct, dynamic(true), fn attr, dynamic -> Enum.reduce(mapped_struct, dynamic(true), fn attr, dynamic ->
case {attr, media_profile} do case {attr, media_profile} do
{{:shorts_behaviour, :only}, %{livestream_behaviour: :only}} -> {{:shorts_behaviour, :only}, %{livestream_behaviour: :only}} ->
dynamic([mi], ^dynamic and (mi.livestream == true or fragment("? ILIKE ?", mi.original_url, "%/shorts/%"))) dynamic(
[mi],
^dynamic and (mi.livestream == true or fragment("LOWER(?) LIKE LOWER(?)", mi.original_url, "%/shorts/%"))
)
# Technically redundant, but makes the other clauses easier to parse # Technically redundant, but makes the other clauses easier to parse
# (redundant because this condition is the same as the condition above, just flipped) # (redundant because this condition is the same as the condition above, just flipped)
@ -183,7 +184,7 @@ defmodule Pinchflat.Media do
{{:shorts_behaviour, :only}, _} -> {{:shorts_behaviour, :only}, _} ->
# return records with /shorts/ in the original_url # return records with /shorts/ in the original_url
dynamic([mi], ^dynamic and fragment("? ILIKE ?", mi.original_url, "%/shorts/%")) dynamic([mi], ^dynamic and fragment("LOWER(?) LIKE LOWER(?)", mi.original_url, "%/shorts/%"))
{{:livestream_behaviour, :only}, _} -> {{:livestream_behaviour, :only}, _} ->
# return records with livestream: true # return records with livestream: true
@ -191,7 +192,7 @@ defmodule Pinchflat.Media do
{{:shorts_behaviour, :exclude}, %{livestream_behaviour: lb}} when lb != :only -> {{:shorts_behaviour, :exclude}, %{livestream_behaviour: lb}} when lb != :only ->
# return records without /shorts/ in the original_url # return records without /shorts/ in the original_url
dynamic([mi], ^dynamic and fragment("? NOT ILIKE ?", mi.original_url, "%/shorts/%")) dynamic([mi], ^dynamic and fragment("LOWER(?) NOT LIKE LOWER(?)", mi.original_url, "%/shorts/%"))
{{:livestream_behaviour, :exclude}, %{shorts_behaviour: sb}} when sb != :only -> {{:livestream_behaviour, :exclude}, %{shorts_behaviour: sb}} when sb != :only ->
# return records with livestream: false # return records with livestream: false

View file

@ -9,6 +9,7 @@ defmodule Pinchflat.Media.MediaItem do
alias Pinchflat.Tasks.Task alias Pinchflat.Tasks.Task
alias Pinchflat.MediaSource.Source alias Pinchflat.MediaSource.Source
alias Pinchflat.Media.MediaMetadata alias Pinchflat.Media.MediaMetadata
alias Pinchflat.Media.MediaItemSearchIndex
@allowed_fields ~w( @allowed_fields ~w(
title title
@ -46,6 +47,7 @@ defmodule Pinchflat.Media.MediaItem do
belongs_to :source, Source belongs_to :source, Source
has_one :metadata, MediaMetadata, on_replace: :update has_one :metadata, MediaMetadata, on_replace: :update
has_one :media_items_search_index, MediaItemSearchIndex, foreign_key: :id
has_many :tasks, Task has_many :tasks, Task

View file

@ -0,0 +1,16 @@
defmodule Pinchflat.Media.MediaItemSearchIndex do
@moduledoc """
The MediaItem fts5 search index. Not made to be directly interacted with,
but I figured it'd be better to have it in-app so it's not a mystery.
"""
use Ecto.Schema
@primary_key {:id, :id, autogenerate: true, source: :rowid}
schema "media_items_search_index" do
field :title, :string
field :description, :string
field :rank, :float, virtual: true
end
end

View file

@ -1,7 +1,7 @@
defmodule Pinchflat.Repo do defmodule Pinchflat.Repo do
use Ecto.Repo, use Ecto.Repo,
otp_app: :pinchflat, otp_app: :pinchflat,
adapter: Ecto.Adapters.Postgres adapter: Ecto.Adapters.SQLite3
@doc """ @doc """
It's not immediately obvious if an Oban job qualifies as unique, so this method It's not immediately obvious if an Oban job qualifies as unique, so this method

View file

@ -35,7 +35,7 @@ defmodule Pinchflat.MixProject do
{:phoenix, "~> 1.7.10"}, {:phoenix, "~> 1.7.10"},
{:phoenix_ecto, "~> 4.4"}, {:phoenix_ecto, "~> 4.4"},
{:ecto_sql, "~> 3.10"}, {:ecto_sql, "~> 3.10"},
{:postgrex, ">= 0.0.0"}, {:ecto_sqlite3, ">= 0.0.0"},
{:phoenix_html, "~> 3.3"}, {:phoenix_html, "~> 3.3"},
{:phoenix_live_reload, "~> 1.2", only: :dev}, {:phoenix_live_reload, "~> 1.2", only: :dev},
{:phoenix_live_view, "~> 0.20.1"}, {:phoenix_live_view, "~> 0.20.1"},

View file

@ -1,6 +1,7 @@
%{ %{
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
"castore": {:hex, :castore, "1.0.5", "9eeebb394cc9a0f3ae56b813459f990abb0a3dedee1be6b27fdb50301930502f", [:mix], [], "hexpm", "8d7c597c3e4a64c395980882d4bca3cebb8d74197c590dc272cfd3b6a6310578"}, "castore": {:hex, :castore, "1.0.5", "9eeebb394cc9a0f3ae56b813459f990abb0a3dedee1be6b27fdb50301930502f", [:mix], [], "hexpm", "8d7c597c3e4a64c395980882d4bca3cebb8d74197c590dc272cfd3b6a6310578"},
"cc_precompiler": {:hex, :cc_precompiler, "0.1.9", "e8d3364f310da6ce6463c3dd20cf90ae7bbecbf6c5203b98bf9b48035592649b", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "9dcab3d0f3038621f1601f13539e7a9ee99843862e66ad62827b0c42b2f58a54"},
"cowboy": {:hex, :cowboy, "2.10.0", "ff9ffeff91dae4ae270dd975642997afe2a1179d94b1887863e43f681a203e26", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "3afdccb7183cc6f143cb14d3cf51fa00e53db9ec80cdcd525482f5e99bc41d6b"}, "cowboy": {:hex, :cowboy, "2.10.0", "ff9ffeff91dae4ae270dd975642997afe2a1179d94b1887863e43f681a203e26", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "3afdccb7183cc6f143cb14d3cf51fa00e53db9ec80cdcd525482f5e99bc41d6b"},
"cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
"cowlib": {:hex, :cowlib, "2.12.1", "a9fa9a625f1d2025fe6b462cb865881329b5caff8f1854d1cbc9f9533f00e1e1", [:make, :rebar3], [], "hexpm", "163b73f6367a7341b33c794c4e88e7dbfe6498ac42dcd69ef44c5bc5507c8db0"}, "cowlib": {:hex, :cowlib, "2.12.1", "a9fa9a625f1d2025fe6b462cb865881329b5caff8f1854d1cbc9f9533f00e1e1", [:make, :rebar3], [], "hexpm", "163b73f6367a7341b33c794c4e88e7dbfe6498ac42dcd69ef44c5bc5507c8db0"},
@ -10,8 +11,11 @@
"dns_cluster": {:hex, :dns_cluster, "0.1.2", "3eb5be824c7888dadf9781018e1a5f1d3d1113b333c50bce90fb1b83df1015f2", [:mix], [], "hexpm", "7494272040f847637bbdb01bcdf4b871e82daf09b813e7d3cb3b84f112c6f2f8"}, "dns_cluster": {:hex, :dns_cluster, "0.1.2", "3eb5be824c7888dadf9781018e1a5f1d3d1113b333c50bce90fb1b83df1015f2", [:mix], [], "hexpm", "7494272040f847637bbdb01bcdf4b871e82daf09b813e7d3cb3b84f112c6f2f8"},
"ecto": {:hex, :ecto, "3.11.1", "4b4972b717e7ca83d30121b12998f5fcdc62ba0ed4f20fd390f16f3270d85c3e", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ebd3d3772cd0dfcd8d772659e41ed527c28b2a8bde4b00fe03e0463da0f1983b"}, "ecto": {:hex, :ecto, "3.11.1", "4b4972b717e7ca83d30121b12998f5fcdc62ba0ed4f20fd390f16f3270d85c3e", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ebd3d3772cd0dfcd8d772659e41ed527c28b2a8bde4b00fe03e0463da0f1983b"},
"ecto_sql": {:hex, :ecto_sql, "3.11.1", "e9abf28ae27ef3916b43545f9578b4750956ccea444853606472089e7d169470", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ce14063ab3514424276e7e360108ad6c2308f6d88164a076aac8a387e1fea634"}, "ecto_sql": {:hex, :ecto_sql, "3.11.1", "e9abf28ae27ef3916b43545f9578b4750956ccea444853606472089e7d169470", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ce14063ab3514424276e7e360108ad6c2308f6d88164a076aac8a387e1fea634"},
"ecto_sqlite3": {:hex, :ecto_sqlite3, "0.15.1", "40f2fbd9e246455f8c42e7e0a77009ef806caa1b3ce6f717b2a0a80e8432fcfd", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.11", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:exqlite, "~> 0.19", [hex: :exqlite, repo: "hexpm", optional: false]}], "hexpm", "28b16e177123c688948357176662bf9ff9084daddf950ef5b6baf3ee93707064"},
"elixir_make": {:hex, :elixir_make, "0.7.8", "505026f266552ee5aabca0b9f9c229cbb496c689537c9f922f3eb5431157efc7", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.0", [hex: :certifi, repo: "hexpm", optional: true]}], "hexpm", "7a71945b913d37ea89b06966e1342c85cfe549b15e6d6d081e8081c493062c07"},
"esbuild": {:hex, :esbuild, "0.8.1", "0cbf919f0eccb136d2eeef0df49c4acf55336de864e63594adcea3814f3edf41", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "25fc876a67c13cb0a776e7b5d7974851556baeda2085296c14ab48555ea7560f"}, "esbuild": {:hex, :esbuild, "0.8.1", "0cbf919f0eccb136d2eeef0df49c4acf55336de864e63594adcea3814f3edf41", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "25fc876a67c13cb0a776e7b5d7974851556baeda2085296c14ab48555ea7560f"},
"expo": {:hex, :expo, "0.5.1", "249e826a897cac48f591deba863b26c16682b43711dd15ee86b92f25eafd96d9", [:mix], [], "hexpm", "68a4233b0658a3d12ee00d27d37d856b1ba48607e7ce20fd376958d0ba6ce92b"}, "expo": {:hex, :expo, "0.5.1", "249e826a897cac48f591deba863b26c16682b43711dd15ee86b92f25eafd96d9", [:mix], [], "hexpm", "68a4233b0658a3d12ee00d27d37d856b1ba48607e7ce20fd376958d0ba6ce92b"},
"exqlite": {:hex, :exqlite, "0.19.0", "0f3ee29e35bed38552dd0ed59600aa81c78f867f5b5ff0e17d330148e0465483", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "55a8fbb0443f03d4a256e3458bd1203eff5037a6624b76460eaaa9080f462b06"},
"faker": {:hex, :faker, "0.17.0", "671019d0652f63aefd8723b72167ecdb284baf7d47ad3a82a15e9b8a6df5d1fa", [:mix], [], "hexpm", "a7d4ad84a93fd25c5f5303510753789fc2433ff241bf3b4144d3f6f291658a6a"}, "faker": {:hex, :faker, "0.17.0", "671019d0652f63aefd8723b72167ecdb284baf7d47ad3a82a15e9b8a6df5d1fa", [:mix], [], "hexpm", "a7d4ad84a93fd25c5f5303510753789fc2433ff241bf3b4144d3f6f291658a6a"},
"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
"finch": {:hex, :finch, "0.17.0", "17d06e1d44d891d20dbd437335eebe844e2426a0cd7e3a3e220b461127c73f70", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8d014a661bb6a437263d4b5abf0bcbd3cf0deb26b1e8596f2a271d22e48934c7"}, "finch": {:hex, :finch, "0.17.0", "17d06e1d44d891d20dbd437335eebe844e2426a0cd7e3a3e220b461127c73f70", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8d014a661bb6a437263d4b5abf0bcbd3cf0deb26b1e8596f2a271d22e48934c7"},

View file

@ -1,17 +1,18 @@
defmodule Pinchflat.Repo.Migrations.CreateChannels do defmodule Pinchflat.Repo.Migrations.CreateSources do
use Ecto.Migration use Ecto.Migration
def change do def change do
create table(:channels) do create table(:sources) do
add :name, :string, null: false add :name, :string, null: false
add :channel_id, :string, null: false add :collection_id, :string, null: false
add :collection_type, :string, null: false
add :original_url, :string, null: false add :original_url, :string, null: false
add :media_profile_id, references(:media_profiles, on_delete: :restrict), null: false add :media_profile_id, references(:media_profiles, on_delete: :restrict), null: false
timestamps(type: :utc_datetime) timestamps(type: :utc_datetime)
end end
create index(:channels, [:media_profile_id]) create index(:sources, [:media_profile_id])
create unique_index(:channels, [:channel_id, :media_profile_id]) create unique_index(:sources, [:collection_id, :media_profile_id])
end end
end end

View file

@ -6,12 +6,12 @@ defmodule Pinchflat.Repo.Migrations.CreateMediaItems do
add :media_id, :string, null: false add :media_id, :string, null: false
add :title, :string add :title, :string
add :video_filepath, :string add :video_filepath, :string
add :channel_id, references(:channels, on_delete: :restrict), null: false add :source_id, references(:sources, on_delete: :restrict), null: false
timestamps(type: :utc_datetime) timestamps(type: :utc_datetime)
end end
create index(:media_items, [:channel_id]) create index(:media_items, [:source_id])
create unique_index(:media_items, [:media_id, :channel_id]) create unique_index(:media_items, [:media_id, :source_id])
end end
end end

View file

@ -1,8 +1,8 @@
defmodule Pinchflat.Repo.Migrations.AddIndexFrequencyToChannels do defmodule Pinchflat.Repo.Migrations.AddIndexFrequencyToSources do
use Ecto.Migration use Ecto.Migration
def change do def change do
alter table(:channels) do alter table(:sources) do
add :index_frequency_minutes, :integer, default: 60 * 24, null: false add :index_frequency_minutes, :integer, default: 60 * 24, null: false
end end
end end

View file

@ -4,13 +4,13 @@ defmodule Pinchflat.Repo.Migrations.CreateTasks do
def change do def change do
create table(:tasks) do create table(:tasks) do
add :job_id, references(:oban_jobs, on_delete: :delete_all), null: false add :job_id, references(:oban_jobs, on_delete: :delete_all), null: false
# `restrict` because we need to be sure to delete pending tasks when a channel is deleted # `restrict` because we need to be sure to delete pending tasks when a source is deleted
add :channel_id, references(:channels, on_delete: :restrict), null: true add :source_id, references(:sources, on_delete: :restrict), null: true
timestamps(type: :utc_datetime) timestamps(type: :utc_datetime)
end end
create index(:tasks, [:job_id]) create index(:tasks, [:job_id])
create index(:tasks, [:channel_id]) create index(:tasks, [:source_id])
end end
end end

View file

@ -3,13 +3,12 @@ defmodule Pinchflat.Repo.Migrations.CreateMediaMetadata do
def change do def change do
create table(:media_metadata) do create table(:media_metadata) do
add :client_response, :jsonb, null: false add :client_response, :json, null: false
add :media_item_id, references(:media_items, on_delete: :delete_all), null: false add :media_item_id, references(:media_items, on_delete: :delete_all), null: false
timestamps(type: :utc_datetime) timestamps(type: :utc_datetime)
end end
create unique_index(:media_metadata, [:media_item_id]) create unique_index(:media_metadata, [:media_item_id])
create index(:media_metadata, [:client_response], using: :gin)
end end
end end

View file

@ -3,7 +3,7 @@ defmodule Pinchflat.Repo.Migrations.AddMediaItemToTasks do
def change do def change do
alter table(:tasks) do alter table(:tasks) do
# `restrict` because we need to be sure to delete pending tasks when a channel is deleted # `restrict` because we need to be sure to delete pending tasks when a media item is deleted
add :media_item_id, references(:media_items, on_delete: :restrict), null: true add :media_item_id, references(:media_items, on_delete: :restrict), null: true
end end

View file

@ -1,45 +0,0 @@
defmodule Pinchflat.Repo.Migrations.RenameChannelAndRelatedFields do
use Ecto.Migration
def change do
# Creation
create table(:sources) do
add :name, :string, null: false
add :collection_type, :string, null: false
add :collection_id, :string, null: false
add :original_url, :string, null: false
add :media_profile_id, references(:media_profiles, on_delete: :restrict), null: false
add :index_frequency_minutes, :integer, default: 60 * 24, null: false
timestamps(type: :utc_datetime)
end
alter table(:media_items) do
add :source_id, references(:sources, on_delete: :restrict), null: false
end
alter table(:tasks) do
# `restrict` because we need to be sure to delete pending tasks when a source is deleted
add :source_id, references(:sources, on_delete: :restrict), null: true
end
create index(:sources, [:media_profile_id])
create unique_index(:sources, [:collection_id, :media_profile_id])
create index(:media_items, [:source_id])
create unique_index(:media_items, [:media_id, :source_id])
# Deletion
drop index(:media_items, [:channel_id])
drop unique_index(:media_items, [:media_id, :channel_id])
alter table(:tasks) do
# `restrict` because we need to be sure to delete pending tasks when a source is deleted
remove :channel_id, references(:channels, on_delete: :restrict), null: true
end
alter table(:media_items) do
remove :channel_id, references(:channels, on_delete: :restrict), null: true
end
end
end

View file

@ -1,21 +0,0 @@
defmodule Pinchflat.Repo.Migrations.DropChannelsTable do
use Ecto.Migration
def up do
drop table(:channels)
end
def down do
create table(:channels) do
add :name, :string, null: false
add :channel_id, :string, null: false
add :original_url, :string, null: false
add :media_profile_id, references(:media_profiles, on_delete: :restrict), null: false
timestamps(type: :utc_datetime)
end
create index(:channels, [:media_profile_id])
create unique_index(:channels, [:channel_id, :media_profile_id])
end
end

View file

@ -2,27 +2,50 @@ defmodule Pinchflat.Repo.Migrations.AddSearchFieldToMediaItems do
use Ecto.Migration use Ecto.Migration
def up do def up do
# These all need to run as part of separate `execute` blocks. Do NOT ask me why.
execute """ execute """
ALTER TABLE media_items CREATE VIRTUAL TABLE media_items_search_index USING fts5(
ADD COLUMN searchable tsvector title,
GENERATED ALWAYS AS ( description,
setweight(to_tsvector('english', coalesce(title, '')), 'A') || tokenize=porter
setweight(to_tsvector('english', coalesce(description, '')), 'B') );
) STORED;
""" """
execute """ execute """
CREATE INDEX media_items_searchable_idx ON media_items USING gin(searchable); CREATE TRIGGER media_items_search_index_insert AFTER INSERT ON media_items BEGIN
INSERT INTO media_items_search_index(
rowid,
title,
description
)
VALUES(
new.id,
new.title,
new.description
);
END;
"""
execute """
CREATE TRIGGER media_items_search_index_update AFTER UPDATE ON media_items BEGIN
UPDATE media_items_search_index SET
title = new.title,
description = new.description
WHERE
rowid = old.id;
END;
"""
execute """
CREATE TRIGGER media_items_search_index_delete AFTER DELETE ON media_items BEGIN
DELETE FROM media_items_search_index WHERE rowid = old.id;
END;
""" """
end end
def down do def down do
execute """ execute """
DROP INDEX media_items_searchable_idx; DROP TABLE media_items_search_index;
""" """
alter table(:media_items) do
remove :searchable
end
end end
end end

View file

@ -212,6 +212,14 @@ defmodule Pinchflat.MediaTest do
assert [_] = Media.search("dog", limit: 1) assert [_] = Media.search("dog", limit: 1)
end end
test "returns an empty list when the search term is blank" do
assert [] = Media.search("")
end
test "returns an empty list when the search term is nil" do
assert [] = Media.search(nil)
end
end end
describe "get_media_item!/1" do describe "get_media_item!/1" do

View file

@ -34,7 +34,6 @@ defmodule PinchflatWeb.ConnCase do
setup tags do setup tags do
Pinchflat.DataCase.setup_sandbox(tags) Pinchflat.DataCase.setup_sandbox(tags)
Pinchflat.DataCase.setup_temp_filepaths()
{:ok, conn: Phoenix.ConnTest.build_conn()} {:ok, conn: Phoenix.ConnTest.build_conn()}
end end
end end

View file

@ -32,20 +32,6 @@ defmodule Pinchflat.DataCase do
setup tags do setup tags do
Pinchflat.DataCase.setup_sandbox(tags) Pinchflat.DataCase.setup_sandbox(tags)
Pinchflat.DataCase.setup_temp_filepaths()
:ok
end
@doc """
Sets up the temp filepaths for the media and metadata directories.
"""
def setup_temp_filepaths do
File.rm_rf!(Application.get_env(:pinchflat, :media_directory))
File.rm_rf!(Application.get_env(:pinchflat, :metadata_directory))
File.mkdir_p!(Application.get_env(:pinchflat, :media_directory))
File.mkdir_p!(Application.get_env(:pinchflat, :metadata_directory))
:ok :ok
end end