diff --git a/README.md b/README.md index 6e28dcf..1b07b77 100644 --- a/README.md +++ b/README.md @@ -30,11 +30,12 @@ You are able to change the location of the model file output in the configuratio * module_name -> This is the name of the module that the models will be placed under * destination -> The output location for the generated models. If this is not provided then the models will go in the directory that you ran plsm in - * server -> this is the name of the server that you are connecting to. It can be a DNS name or an IP Address. This needs to be filled in as there are no defaults - * port -> The port that the database server is listening on. This needs to be provided as there may not be a default for your server - * database_name -> the name of the database that you are connecting to. This is required. + * server -> this is the name of the server that you are connecting to. It can be a DNS name or an IP Address. This needs to be filled in for PostgreSQL and MySQL as there are no defaults + * port -> The port that the database server is listening on. This needs to be provided except for SQLite as there may not be a default for your server + * database_name -> the name of the database that you are connecting to. This is required except for SQLite * username -> The username that is used to connect. Make sure that there is sufficient privileges to be able to connect, query tables as well as query information schemas on the database. The schema information is used to find the index/keys on each table * password -> This is necessary as there is no default nor is there any handling of a blank password currently. + * filename -> Database file full path. This is required for SQLite * type -> This dictates which database vendor you are using. We currently support PostgreSQL and MySQL. If no value is entered then it will default to MySQL. Accepted values: `:mysql` or `:postgres`. **Do note that this is an atom and not a string** @@ -44,6 +45,7 @@ You are able to change the location of the model file output in the configuratio * MySQL * PostgreSQL + * SQLite We may add support to other databases based on demand. Please reach out and if you want a specific database supported. Please feel free to contribute commits that add different database vendor support! diff --git a/lib/common/configs.ex b/lib/common/configs.ex index adfbc47..83e7186 100644 --- a/lib/common/configs.ex +++ b/lib/common/configs.ex @@ -10,6 +10,7 @@ defmodule Plsm.Common.Configs do database_name: Application.get_env(:plsm, :database_name, ""), username: Application.get_env(:plsm, :username, ""), password: Application.get_env(:plsm, :password, ""), + filename: Application.get_env(:plsm, :filename, ""), type: Application.get_env(:plsm, :type, :mysql) } diff --git a/lib/common/structs.ex b/lib/common/structs.ex index f5f3cce..78893a4 100644 --- a/lib/common/structs.ex +++ b/lib/common/structs.ex @@ -3,7 +3,7 @@ defmodule Plsm.Configs do end defmodule Plsm.Configs.Database do - defstruct server: "", port: "", database_name: "", username: "", password: "", type: :mysql + defstruct server: "", port: "", database_name: "", username: "", password: "", filename: "", type: :mysql end defmodule Plsm.Configs.Project do diff --git a/lib/config/config.ex b/lib/config/config.ex index 1708a2d..8467ac8 100644 --- a/lib/config/config.ex +++ b/lib/config/config.ex @@ -38,6 +38,8 @@ defmodule Plsm.Config.Config do |> append_next_item() |> append_config_item_string("password", "password") |> append_next_item() + |> append_config_item_string("filename", "filename") + |> append_next_item() |> append_config_item_atom("type", "mysql") end @@ -65,12 +67,13 @@ defmodule Plsm.Config.Config do # Plsm configs are used to drive the extraction process. Below are what each field means: # * module_name -> This is the name of the module that the models will be placed under # * destination -> The output location for the generated models - # * server -> this is the name of the server that you are connecting to. It can be a DNS name or an IP Address. This needs to be filled in as there are no defaults - # * port -> The port that the database server is listening on. This needs to be provided as there may not be a default for your server + # * server -> this is the name of the server that you are connecting to. It can be a DNS name or an IP Address. This needs to be filled in for PostgreSQL and MySQL as there are no defaults + # * port -> The port that the database server is listening on. This needs to be provided for PostgreSQL and MySQL as there may not be a default for your server # * database_name -> the name of the database that you are connecting to. This is required. # * username -> The username that is used to connect. Make sure that there is sufficient privileges to be able to connect, query tables as well as query information schemas on the database. The schema information is used to find the index/keys on each table - # * password -> This is necessary as there is no default nor is there any handling of a blank password currently. - # * type -> This dictates which database vendor you are using. We currently support PostgreSQL and MySQL. If no value is entered then it will default to MySQL. Do note that this is an atom and not a string + # * password -> This is necessary for PostgreSQL and MySQL as there is no default nor is there any handling of a blank password currently. + # * filename -> Database file full path. This is required for SQLite + # * type -> This dictates which database vendor you are using. We currently support PostgreSQL, MySQL and SQLite. If no value is entered then it will default to MySQL. Do note that this is an atom and not a string """ end diff --git a/lib/database/common.ex b/lib/database/common.ex index b65adac..62f2828 100644 --- a/lib/database/common.ex +++ b/lib/database/common.ex @@ -13,6 +13,10 @@ defmodule Plsm.Database.Common do IO.puts("Using PostgreSQL...") Plsm.Database.create(%Plsm.Database.PostgreSQL{}, configs) + :sqlite -> + IO.puts("Using SQLite...") + Plsm.Database.create(%Plsm.Database.SQLite{}, configs) + _ -> IO.puts("Using default database MySql...") Plsm.Database.create(%Plsm.Database.MySql{}, configs) diff --git a/lib/database/sqlite.ex b/lib/database/sqlite.ex new file mode 100644 index 0000000..45f93f3 --- /dev/null +++ b/lib/database/sqlite.ex @@ -0,0 +1,87 @@ +defmodule Plsm.Database.SQLite do + defstruct filename: "filename.sqlite", + connection: nil +end + +defimpl Plsm.Database, for: Plsm.Database.SQLite do + @doc """ + Create a SQLite database struct. We pass in the configs in order to + properly open the SQLite database + """ + @spec create(Plsm.Database.SQLite, Plsm.Configs) :: Plsm.Database.SQLite + def create(_db, configs) do + %Plsm.Database.SQLite{ + filename: configs.database.filename, + } + end + + @spec connect(Plsm.Database.SQLite) :: Plsm.Database.SQLite + def connect(db) do + {_, conn} = Exqlite.Sqlite3.open(db.filename) + + %Plsm.Database.SQLite{ + connection: conn, + filename: db.filename, + } + end + + @spec get_tables(Plsm.Database.SQLite) :: [Plsm.Database.TableHeader] + def get_tables(db) do + {_, statement} = Exqlite.Sqlite3.prepare(db.connection, " + SELECT + name + FROM + sqlite_schema + WHERE + type = 'table' + AND name NOT LIKE 'sqlite_%'" + ) + + {_, result} = Exqlite.Sqlite3.fetch_all(db.connection, statement) + + result + |> List.flatten() + |> Enum.map(fn x -> %Plsm.Database.TableHeader{database: db, name: x} end) + end + + @spec get_columns(Plsm.Database.SQLite, Plsm.Database.Table) :: [Plsm.Database.Column] + def get_columns(db, table) do + {_, statement} = Exqlite.Sqlite3.prepare(db.connection, "PRAGMA table_info(#{table.name})") + {_, result} = Exqlite.Sqlite3.fetch_all(db.connection, statement) + + result + |> Enum.map(&to_column/1) + end + + defp to_column(row) do + {_, name} = Enum.fetch(row, 1) + type = Enum.fetch(row, 2) |> get_type + {_, pk} = Enum.fetch(row, 5) + primary_key? = pk == 1 + %Plsm.Database.Column{name: name, type: type, primary_key: primary_key?} + end + + defp get_type(start_type) do + {_, type} = start_type + downcase = String.downcase(type) + + cond do + # Determines types using the rules under paragraph 3.1 in SQLite docs: + # https://www.sqlite.org/datatype3.html + String.contains?(downcase, "int") -> :integer + String.contains?(downcase, "char") -> :string + String.contains?(downcase, "clob") -> :string + String.contains?(downcase, "text") -> :string + String.contains?(downcase, "real") -> :float + String.contains?(downcase, "floa") -> :float + String.contains?(downcase, "doub") -> :float + + # Useful use cases + String.starts_with?(downcase, "boolean") -> :boolean + String.starts_with?(downcase, "text_datetime") -> :date + String.starts_with?(downcase, "int_datetime") -> :date + + true -> :none + end + end +end diff --git a/mix.exs b/mix.exs index 13e5c73..ffee6f9 100644 --- a/mix.exs +++ b/mix.exs @@ -15,7 +15,7 @@ defmodule Plsm.Mixfile do end def application do - [applications: [:postgrex, :myxql]] + [applications: [:postgrex, :myxql, :exqlite]] end defp deps do @@ -23,6 +23,7 @@ defmodule Plsm.Mixfile do {:ex_doc, "~> 0.23.0", only: :dev, runtime: false}, {:myxql, "~> 0.4.4"}, {:postgrex, "~> 0.15"}, + {:exqlite, "~> 0.8.4"}, {:ecto_sql, "~> 3.5.3", only: :test}, {:mock, "~> 0.2.0", only: :test} ] diff --git a/mix.lock b/mix.lock index b76071c..a8a435e 100644 --- a/mix.lock +++ b/mix.lock @@ -5,7 +5,9 @@ "earmark_parser": {:hex, :earmark_parser, "1.4.12", "b245e875ec0a311a342320da0551da407d9d2b65d98f7a9597ae078615af3449", [:mix], [], "hexpm", "711e2cc4d64abb7d566d43f54b78f7dc129308a63bc103fbd88550d2174b3160"}, "ecto": {:hex, :ecto, "3.5.5", "48219a991bb86daba6e38a1e64f8cea540cded58950ff38fbc8163e062281a07", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "98dd0e5e1de7f45beca6130d13116eae675db59adfa055fb79612406acf6f6f1"}, "ecto_sql": {:hex, :ecto_sql, "3.5.3", "1964df0305538364b97cc4661a2bd2b6c89d803e66e5655e4e55ff1571943efd", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.5.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.3.0 or ~> 0.4.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d2f53592432ce17d3978feb8f43e8dc0705e288b0890caf06d449785f018061c"}, + "elixir_make": {:hex, :elixir_make, "0.6.3", "bc07d53221216838d79e03a8019d0839786703129599e9619f4ab74c8c096eac", [:mix], [], "hexpm", "f5cbd651c5678bcaabdbb7857658ee106b12509cd976c2c2fca99688e1daf716"}, "ex_doc": {:hex, :ex_doc, "0.23.0", "a069bc9b0bf8efe323ecde8c0d62afc13d308b1fa3d228b65bca5cf8703a529d", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "f5e2c4702468b2fd11b10d39416ddadd2fcdd173ba2a0285ebd92c39827a5a16"}, + "exqlite": {:hex, :exqlite, "0.8.4", "6cbfcd15d1307727615c8ce33589a0498e34c1020a3401bba4dd9ae723e99cc6", [:make, :mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "791fee489e0529b24218d49f39e3a9ed64fd2d26a2110a3fab3e26d8c8524b44"}, "makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"}, "makeup_elixir": {:hex, :makeup_elixir, "0.15.0", "98312c9f0d3730fde4049985a1105da5155bfe5c11e47bdc7406d88e01e4219b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "75ffa34ab1056b7e24844c90bfc62aaf6f3a37a15faa76b07bc5eba27e4a8b4a"}, "meck": {:hex, :meck, "0.8.13", "ffedb39f99b0b99703b8601c6f17c7f76313ee12de6b646e671e3188401f7866", [:rebar3], [], "hexpm", "d34f013c156db51ad57cc556891b9720e6a1c1df5fe2e15af999c84d6cebeb1a"}, diff --git a/test/plsm_test.exs b/test/plsm_test.exs index 7d53a68..f7c2015 100644 --- a/test/plsm_test.exs +++ b/test/plsm_test.exs @@ -24,7 +24,31 @@ defmodule PlsmTest do test "schema files are generated and can compile" do Mix.Tasks.Plsm.run([]) - assert :ok == IEx.Helpers.recompile() + assert :ok == IEx.Helpers.recompile([force: true]) + end + end + + describe "plsm task using sqlite" do + setup do + Application.put_env(:plsm, :filename, File.cwd! <> "/test/support/schemas/sqlite/test.sqlite") + Application.put_env(:plsm, :type, :sqlite) + Application.put_env(:plsm, :module_name, "PlsmTest") + Application.put_env(:plsm, :destination, @schema_dir) + + :ok + end + + test "schema files are generated and can compile" do + Mix.Tasks.Plsm.run([]) + + assert :ok == IEx.Helpers.recompile([force: true]) + + assert File.exists?(@schema_dir <> "test_entity_one.ex") + assert File.exists?(@schema_dir <> "test_entity_two.ex") + + File.ls!("#{@schema_dir}") + |> Enum.filter(fn file -> !String.starts_with?(file, ".") end) + |> Enum.each(fn file -> File.rm!(@schema_dir <> file) end) end end end diff --git a/test/support/schemas/sqlite/test.sqlite b/test/support/schemas/sqlite/test.sqlite new file mode 100644 index 0000000..7ddc83d Binary files /dev/null and b/test/support/schemas/sqlite/test.sqlite differ