update
This commit is contained in:
104
phoenix/deps/exqlite/lib/exqlite.ex
Normal file
104
phoenix/deps/exqlite/lib/exqlite.ex
Normal file
@@ -0,0 +1,104 @@
|
||||
defmodule Exqlite do
|
||||
@moduledoc """
|
||||
SQLite3 driver for Elixir.
|
||||
"""
|
||||
|
||||
alias Exqlite.Connection
|
||||
alias Exqlite.Error
|
||||
alias Exqlite.Query
|
||||
alias Exqlite.Result
|
||||
|
||||
@doc "See `Exqlite.Connection.connect/1`"
|
||||
@spec start_link([Connection.connection_opt()]) :: {:ok, pid()} | {:error, Error.t()}
|
||||
def start_link(opts) do
|
||||
DBConnection.start_link(Connection, opts)
|
||||
end
|
||||
|
||||
@spec query(DBConnection.conn(), iodata(), list(), list()) ::
|
||||
{:ok, Result.t()} | {:error, Exception.t()}
|
||||
def query(conn, statement, params \\ [], opts \\ []) do
|
||||
query = %Query{name: "", statement: IO.iodata_to_binary(statement)}
|
||||
|
||||
case DBConnection.prepare_execute(conn, query, params, opts) do
|
||||
{:ok, _query, result} ->
|
||||
{:ok, result}
|
||||
|
||||
otherwise ->
|
||||
otherwise
|
||||
end
|
||||
end
|
||||
|
||||
@spec query!(DBConnection.conn(), iodata(), list(), list()) :: Result.t()
|
||||
def query!(conn, statement, params \\ [], opts \\ []) do
|
||||
case query(conn, statement, params, opts) do
|
||||
{:ok, result} -> result
|
||||
{:error, err} -> raise err
|
||||
end
|
||||
end
|
||||
|
||||
@spec prepare(DBConnection.conn(), iodata(), list()) ::
|
||||
{:ok, Query.t()} | {:error, Exception.t()}
|
||||
def prepare(conn, name, statement, opts \\ []) do
|
||||
query = %Query{name: name, statement: statement}
|
||||
DBConnection.prepare(conn, query, opts)
|
||||
end
|
||||
|
||||
@spec prepare!(DBConnection.conn(), iodata(), iodata(), list()) :: Query.t()
|
||||
def prepare!(conn, name, statement, opts \\ []) do
|
||||
query = %Query{name: name, statement: statement}
|
||||
DBConnection.prepare!(conn, query, opts)
|
||||
end
|
||||
|
||||
@spec prepare_execute(DBConnection.conn(), iodata(), iodata(), list(), list()) ::
|
||||
{:ok, Query.t(), Result.t()} | {:error, Error.t()}
|
||||
def prepare_execute(conn, name, statement, params, opts \\ []) do
|
||||
query = %Query{name: name, statement: statement}
|
||||
DBConnection.prepare_execute(conn, query, params, opts)
|
||||
end
|
||||
|
||||
@spec prepare_execute!(DBConnection.conn(), iodata(), iodata(), list(), list()) ::
|
||||
{Query.t(), Result.t()}
|
||||
def prepare_execute!(conn, name, statement, params, opts \\ []) do
|
||||
query = %Query{name: name, statement: statement}
|
||||
DBConnection.prepare_execute!(conn, query, params, opts)
|
||||
end
|
||||
|
||||
@spec execute(DBConnection.conn(), Query.t(), list(), list()) ::
|
||||
{:ok, Result.t()} | {:error, Error.t()}
|
||||
def execute(conn, query, params, opts \\ []) do
|
||||
DBConnection.execute(conn, query, params, opts)
|
||||
end
|
||||
|
||||
@spec execute!(DBConnection.conn(), Query.t(), list(), list()) :: Result.t()
|
||||
def execute!(conn, query, params, opts \\ []) do
|
||||
DBConnection.execute!(conn, query, params, opts)
|
||||
end
|
||||
|
||||
@spec close(DBConnection.conn(), Query.t(), list()) :: :ok | {:error, Exception.t()}
|
||||
def close(conn, query, opts \\ []) do
|
||||
with {:ok, _} <- DBConnection.close(conn, query, opts) do
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
@spec close!(DBConnection.conn(), Query.t(), list()) :: :ok
|
||||
def close!(conn, query, opts \\ []) do
|
||||
DBConnection.close!(conn, query, opts)
|
||||
:ok
|
||||
end
|
||||
|
||||
@spec transaction(DBConnection.conn(), (DBConnection.t() -> result), list()) ::
|
||||
{:ok, result} | {:error, any}
|
||||
when result: var
|
||||
def transaction(conn, fun, opts \\ []) do
|
||||
DBConnection.transaction(conn, fun, opts)
|
||||
end
|
||||
|
||||
@spec rollback(DBConnection.t(), term()) :: no_return()
|
||||
def rollback(conn, reason), do: DBConnection.rollback(conn, reason)
|
||||
|
||||
@spec child_spec([Connection.connection_opt()]) :: :supervisor.child_spec()
|
||||
def child_spec(opts) do
|
||||
DBConnection.child_spec(Connection, opts)
|
||||
end
|
||||
end
|
||||
48
phoenix/deps/exqlite/lib/exqlite/basic.ex
Normal file
48
phoenix/deps/exqlite/lib/exqlite/basic.ex
Normal file
@@ -0,0 +1,48 @@
|
||||
defmodule Exqlite.Basic do
|
||||
@moduledoc """
|
||||
A very basic API for simple use cases.
|
||||
"""
|
||||
|
||||
alias Exqlite.Connection
|
||||
alias Exqlite.Query
|
||||
alias Exqlite.Sqlite3
|
||||
alias Exqlite.Error
|
||||
alias Exqlite.Result
|
||||
|
||||
def open(path) do
|
||||
Connection.connect(database: path)
|
||||
end
|
||||
|
||||
def close(%Connection{} = conn) do
|
||||
case Sqlite3.close(conn.db) do
|
||||
:ok -> :ok
|
||||
{:error, reason} -> {:error, %Error{message: to_string(reason)}}
|
||||
end
|
||||
end
|
||||
|
||||
def exec(%Connection{} = conn, stmt, args \\ []) do
|
||||
%Query{statement: stmt} |> Connection.handle_execute(args, [], conn)
|
||||
end
|
||||
|
||||
def rows(exec_result) do
|
||||
case exec_result do
|
||||
{:ok, %Query{}, %Result{rows: rows, columns: columns}, %Connection{}} ->
|
||||
{:ok, rows, columns}
|
||||
|
||||
{:error, %Error{message: message}, %Connection{}} ->
|
||||
{:error, to_string(message)}
|
||||
end
|
||||
end
|
||||
|
||||
def load_extension(conn, path) do
|
||||
exec(conn, "select load_extension(?)", [path])
|
||||
end
|
||||
|
||||
def enable_load_extension(conn) do
|
||||
Sqlite3.enable_load_extension(conn.db, true)
|
||||
end
|
||||
|
||||
def disable_load_extension(conn) do
|
||||
Sqlite3.enable_load_extension(conn.db, false)
|
||||
end
|
||||
end
|
||||
741
phoenix/deps/exqlite/lib/exqlite/connection.ex
Normal file
741
phoenix/deps/exqlite/lib/exqlite/connection.ex
Normal file
@@ -0,0 +1,741 @@
|
||||
defmodule Exqlite.Connection do
|
||||
@moduledoc """
|
||||
This module implements connection details as defined in DBProtocol.
|
||||
|
||||
## Attributes
|
||||
|
||||
- `db` - The sqlite3 database reference.
|
||||
- `path` - The path that was used to open.
|
||||
- `transaction_status` - The status of the connection. Can be `:idle` or `:transaction`.
|
||||
|
||||
## Unknowns
|
||||
|
||||
- How are pooled connections going to work? Since sqlite3 doesn't allow for
|
||||
simultaneous access. We would need to check if the write ahead log is
|
||||
enabled on the database. We can't assume and set the WAL pragma because the
|
||||
database may be stored on a network volume which would cause potential
|
||||
issues.
|
||||
|
||||
Notes:
|
||||
- we try to closely follow structure and naming convention of myxql.
|
||||
- sqlite thrives when there are many small conventions, so we may not implement
|
||||
some strategies employed by other adapters. See https://sqlite.org/np1queryprob.html
|
||||
"""
|
||||
|
||||
use DBConnection
|
||||
alias Exqlite.Error
|
||||
alias Exqlite.Pragma
|
||||
alias Exqlite.Query
|
||||
alias Exqlite.Result
|
||||
alias Exqlite.Sqlite3
|
||||
require Logger
|
||||
|
||||
defstruct [
|
||||
:db,
|
||||
:default_transaction_mode,
|
||||
:directory,
|
||||
:path,
|
||||
:transaction_status,
|
||||
:status,
|
||||
:chunk_size,
|
||||
:before_disconnect
|
||||
]
|
||||
|
||||
@type t() :: %__MODULE__{
|
||||
db: Sqlite3.db(),
|
||||
directory: String.t() | nil,
|
||||
path: String.t(),
|
||||
transaction_status: :idle | :transaction,
|
||||
status: :idle | :busy,
|
||||
chunk_size: integer(),
|
||||
before_disconnect: (t -> any) | {module, atom, [any]} | nil
|
||||
}
|
||||
|
||||
@type journal_mode() :: :delete | :truncate | :persist | :memory | :wal | :off
|
||||
@type temp_store() :: :default | :file | :memory
|
||||
@type synchronous() :: :extra | :full | :normal | :off
|
||||
@type auto_vacuum() :: :none | :full | :incremental
|
||||
@type locking_mode() :: :normal | :exclusive
|
||||
@type transaction_mode() :: :deferred | :immediate | :exclusive
|
||||
|
||||
@type connection_opt() ::
|
||||
{:database, String.t()}
|
||||
| {:default_transaction_mode, transaction_mode()}
|
||||
| {:mode, Sqlite3.open_opt()}
|
||||
| {:journal_mode, journal_mode()}
|
||||
| {:temp_store, temp_store()}
|
||||
| {:synchronous, synchronous()}
|
||||
| {:foreign_keys, :on | :off}
|
||||
| {:cache_size, integer()}
|
||||
| {:cache_spill, :on | :off}
|
||||
| {:case_sensitive_like, boolean()}
|
||||
| {:auto_vacuum, auto_vacuum()}
|
||||
| {:locking_mode, locking_mode()}
|
||||
| {:secure_delete, :on | :off}
|
||||
| {:wal_auto_check_point, integer()}
|
||||
| {:busy_timeout, integer()}
|
||||
| {:chunk_size, integer()}
|
||||
| {:journal_size_limit, integer()}
|
||||
| {:soft_heap_limit, integer()}
|
||||
| {:hard_heap_limit, integer()}
|
||||
| {:key, String.t()}
|
||||
| {:custom_pragmas, [{keyword(), integer() | boolean() | String.t()}]}
|
||||
| {:before_disconnect, (t -> any) | {module, atom, [any]} | nil}
|
||||
|
||||
@impl true
|
||||
@doc """
|
||||
Initializes the Ecto Exqlite adapter.
|
||||
|
||||
For connection configurations we use the defaults that come with SQLite3, but
|
||||
we recommend which options to choose. We do not default to the recommended
|
||||
because we don't know what your environment is like.
|
||||
|
||||
Allowed options:
|
||||
|
||||
* `:database` - The path to the database. In memory is allowed. You can use
|
||||
`:memory` or `":memory:"` to designate that.
|
||||
* `:default_transaction_mode` - one of `deferred` (default), `immediate`,
|
||||
or `exclusive`. If a mode is not specified in a call to `Repo.transaction/2`,
|
||||
this will be the default transaction mode.
|
||||
* `:mode` - use `:readwrite` to open the database for reading and writing
|
||||
, `:readonly` to open it in read-only mode or `[:readonly | :readwrite, :nomutex]`
|
||||
to open it with no mutex mode. `:readwrite` will also create
|
||||
the database if it doesn't already exist. Defaults to `:readwrite`.
|
||||
Note: [:readwrite, :nomutex] is not recommended.
|
||||
* `:journal_mode` - Sets the journal mode for the sqlite connection. Can be
|
||||
one of the following `:delete`, `:truncate`, `:persist`, `:memory`,
|
||||
`:wal`, or `:off`. Defaults to `:delete`. It is recommended that you use
|
||||
`:wal` due to support for concurrent reads. Note: `:wal` does not mean
|
||||
concurrent writes.
|
||||
* `:temp_store` - Sets the storage used for temporary tables. Default is
|
||||
`:default`. Allowed values are `:default`, `:file`, `:memory`. It is
|
||||
recommended that you use `:memory` for storage.
|
||||
* `:synchronous` - Can be `:extra`, `:full`, `:normal`, or `:off`. Defaults
|
||||
to `:normal`.
|
||||
* `:foreign_keys` - Sets if foreign key checks should be enforced or not.
|
||||
Can be `:on` or `:off`. Default is `:on`.
|
||||
* `:cache_size` - Sets the cache size to be used for the connection. This is
|
||||
an odd setting as a positive value is the number of pages in memory to use
|
||||
and a negative value is the size in kilobytes to use. Default is `-2000`.
|
||||
It is recommended that you use `-64000`.
|
||||
* `:cache_spill` - The cache_spill pragma enables or disables the ability of
|
||||
the pager to spill dirty cache pages to the database file in the middle of
|
||||
a transaction. By default it is `:on`, and for most applications, it
|
||||
should remain so.
|
||||
* `:case_sensitive_like`
|
||||
* `:auto_vacuum` - Defaults to `:none`. Can be `:none`, `:full` or
|
||||
`:incremental`. Depending on the database size, `:incremental` may be
|
||||
beneficial.
|
||||
* `:locking_mode` - Defaults to `:normal`. Allowed values are `:normal` or
|
||||
`:exclusive`. See [sqlite documentation][1] for more information.
|
||||
* `:secure_delete` - Defaults to `:off`. If enabled, it will cause SQLite3
|
||||
to overwrite records that were deleted with zeros.
|
||||
* `:wal_auto_check_point` - Sets the write-ahead log auto-checkpoint
|
||||
interval. Default is `1000`. Setting the auto-checkpoint size to zero or a
|
||||
negative value turns auto-checkpointing off.
|
||||
* `:busy_timeout` - Sets the busy timeout in milliseconds for a connection.
|
||||
Default is `2000`.
|
||||
* `:chunk_size` - The chunk size for bulk fetching. Defaults to `50`.
|
||||
* `:key` - Optional key to set during database initialization. This PRAGMA
|
||||
is often used to set up database level encryption.
|
||||
* `:journal_size_limit` - The size limit in bytes of the journal.
|
||||
* `:soft_heap_limit` - The size limit in bytes for the heap limit.
|
||||
* `:hard_heap_limit` - The size limit in bytes for the heap.
|
||||
* `:custom_pragmas` - A list of custom pragmas to set on the connection, for example to configure extensions.
|
||||
* `:serialized` - A SQLite database which was previously serialized, to load into the database after connection.
|
||||
* `:load_extensions` - A list of paths identifying extensions to load. Defaults to `[]`.
|
||||
The provided list will be merged with the global extensions list, set on `:exqlite, :load_extensions`.
|
||||
Be aware that the path should handle pointing to a library compiled for the current architecture.
|
||||
Example configuration:
|
||||
|
||||
```
|
||||
arch_dir =
|
||||
System.cmd("uname", ["-sm"])
|
||||
|> elem(0)
|
||||
|> String.trim()
|
||||
|> String.replace(" ", "-")
|
||||
|> String.downcase() # => "darwin-arm64"
|
||||
|
||||
config :myapp, arch_dir: arch_dir
|
||||
|
||||
# global
|
||||
config :exqlite, load_extensions: [ "./priv/sqlite/\#{arch_dir}/rotate" ]
|
||||
|
||||
# per connection in a Phoenix app
|
||||
config :myapp, Myapp.Repo,
|
||||
database: "path/to/db",
|
||||
load_extensions: [
|
||||
"./priv/sqlite/\#{arch_dir}/vector0",
|
||||
"./priv/sqlite/\#{arch_dir}/vss0"
|
||||
]
|
||||
```
|
||||
* `:before_disconnect` - A function to run before disconnect, either a
|
||||
2-arity fun or `{module, function, args}` with the close reason and
|
||||
`t:Exqlite.Connection.t/0` prepended to `args` or `nil` (default: `nil`)
|
||||
|
||||
For more information about the options above, see [sqlite documentation][1]
|
||||
|
||||
[1]: https://www.sqlite.org/pragma.html
|
||||
"""
|
||||
@spec connect([connection_opt()]) :: {:ok, t()} | {:error, Exception.t()}
|
||||
def connect(options) do
|
||||
database = Keyword.get(options, :database)
|
||||
|
||||
options =
|
||||
Keyword.put_new(
|
||||
options,
|
||||
:chunk_size,
|
||||
Application.get_env(:exqlite, :default_chunk_size, 50)
|
||||
)
|
||||
|
||||
case database do
|
||||
nil ->
|
||||
{:error,
|
||||
%Error{
|
||||
message: """
|
||||
You must provide a :database to the database. \
|
||||
Example: connect(database: "./") or connect(database: :memory)\
|
||||
"""
|
||||
}}
|
||||
|
||||
:memory ->
|
||||
do_connect(":memory:", options)
|
||||
|
||||
_ ->
|
||||
do_connect(database, options)
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def disconnect(err, %__MODULE__{db: db} = state) do
|
||||
if state.before_disconnect != nil do
|
||||
apply(state.before_disconnect, [err, state])
|
||||
end
|
||||
|
||||
case Sqlite3.close(db) do
|
||||
:ok -> :ok
|
||||
{:error, reason} -> {:error, %Error{message: to_string(reason)}}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def checkout(%__MODULE__{status: :idle} = state) do
|
||||
{:ok, %{state | status: :busy}}
|
||||
end
|
||||
|
||||
def checkout(%__MODULE__{status: :busy} = state) do
|
||||
{:disconnect, %Error{message: "Database is busy"}, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def ping(state), do: {:ok, state}
|
||||
|
||||
##
|
||||
## Handlers
|
||||
##
|
||||
|
||||
@impl true
|
||||
def handle_prepare(%Query{} = query, options, state) do
|
||||
with {:ok, query} <- prepare(query, options, state) do
|
||||
{:ok, query, state}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_execute(%Query{} = query, params, options, state) do
|
||||
with {:ok, query} <- prepare(query, options, state) do
|
||||
execute(:execute, query, params, state)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Begin a transaction.
|
||||
|
||||
For full info refer to sqlite docs: https://sqlite.org/lang_transaction.html
|
||||
|
||||
Note: default transaction mode is DEFERRED.
|
||||
"""
|
||||
@impl true
|
||||
def handle_begin(options, %{transaction_status: transaction_status} = state) do
|
||||
# This doesn't handle more than 2 levels of transactions.
|
||||
#
|
||||
# One possible solution would be to just track the number of open
|
||||
# transactions and use that for driving the transaction status being idle or
|
||||
# in a transaction.
|
||||
#
|
||||
# I do not know why the other official adapters do not track this and just
|
||||
# append level on the savepoint. Instead the rollbacks would just completely
|
||||
# revert the issues when it may be desirable to fix something while in the
|
||||
# transaction and then commit.
|
||||
|
||||
mode = Keyword.get(options, :mode, state.default_transaction_mode)
|
||||
|
||||
case mode do
|
||||
:deferred when transaction_status == :idle ->
|
||||
handle_transaction(:begin, "BEGIN TRANSACTION", state)
|
||||
|
||||
:transaction when transaction_status == :idle ->
|
||||
handle_transaction(:begin, "BEGIN TRANSACTION", state)
|
||||
|
||||
:immediate when transaction_status == :idle ->
|
||||
handle_transaction(:begin, "BEGIN IMMEDIATE TRANSACTION", state)
|
||||
|
||||
:exclusive when transaction_status == :idle ->
|
||||
handle_transaction(:begin, "BEGIN EXCLUSIVE TRANSACTION", state)
|
||||
|
||||
mode
|
||||
when mode in [:deferred, :immediate, :exclusive, :savepoint] and
|
||||
transaction_status == :transaction ->
|
||||
handle_transaction(:begin, "SAVEPOINT exqlite_savepoint", state)
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_commit(options, %{transaction_status: transaction_status} = state) do
|
||||
case Keyword.get(options, :mode, :deferred) do
|
||||
:savepoint when transaction_status == :transaction ->
|
||||
handle_transaction(
|
||||
:commit_savepoint,
|
||||
"RELEASE SAVEPOINT exqlite_savepoint",
|
||||
state
|
||||
)
|
||||
|
||||
mode
|
||||
when mode in [:deferred, :immediate, :exclusive, :transaction] and
|
||||
transaction_status == :transaction ->
|
||||
handle_transaction(:commit, "COMMIT", state)
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_rollback(options, %{transaction_status: transaction_status} = state) do
|
||||
case Keyword.get(options, :mode, :deferred) do
|
||||
:savepoint when transaction_status == :transaction ->
|
||||
with {:ok, _result, state} <-
|
||||
handle_transaction(
|
||||
:rollback_savepoint,
|
||||
"ROLLBACK TO SAVEPOINT exqlite_savepoint",
|
||||
state
|
||||
) do
|
||||
handle_transaction(
|
||||
:rollback_savepoint,
|
||||
"RELEASE SAVEPOINT exqlite_savepoint",
|
||||
state
|
||||
)
|
||||
end
|
||||
|
||||
mode
|
||||
when mode in [:deferred, :immediate, :exclusive, :transaction] ->
|
||||
handle_transaction(:rollback, "ROLLBACK TRANSACTION", state)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Close a query prepared by `handle_prepare/3` with the database. Return
|
||||
`{:ok, result, state}` on success and to continue,
|
||||
`{:error, exception, state}` to return an error and continue, or
|
||||
`{:disconnect, exception, state}` to return an error and disconnect.
|
||||
|
||||
This callback is called in the client process.
|
||||
"""
|
||||
@impl true
|
||||
def handle_close(query, _opts, state) do
|
||||
Sqlite3.release(state.db, query.ref)
|
||||
{:ok, nil, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_declare(%Query{} = query, params, opts, state) do
|
||||
# We emulate cursor functionality by just using a prepared statement and
|
||||
# step through it. Thus we just return the query ref as the cursor.
|
||||
with {:ok, query} <- prepare_no_cache(query, opts, state),
|
||||
{:ok, query} <- bind_params(query, params, state) do
|
||||
{:ok, query, query.ref, state}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_deallocate(%Query{} = query, _cursor, _opts, state) do
|
||||
Sqlite3.release(state.db, query.ref)
|
||||
{:ok, nil, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_fetch(%Query{statement: statement}, cursor, opts, state) do
|
||||
chunk_size = opts[:chunk_size] || opts[:max_rows] || state.chunk_size
|
||||
|
||||
case Sqlite3.multi_step(state.db, cursor, chunk_size) do
|
||||
{:done, rows} ->
|
||||
{:halt, %Result{rows: rows, command: :fetch, num_rows: length(rows)}, state}
|
||||
|
||||
{:rows, rows} ->
|
||||
{:cont, %Result{rows: rows, command: :fetch, num_rows: chunk_size}, state}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, %Error{message: to_string(reason), statement: statement}, state}
|
||||
|
||||
:busy ->
|
||||
{:error, %Error{message: "Database is busy", statement: statement}, state}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_status(_opts, state) do
|
||||
{state.transaction_status, state}
|
||||
end
|
||||
|
||||
### ----------------------------------
|
||||
# Internal functions and helpers
|
||||
### ----------------------------------
|
||||
|
||||
defp set_pragma(db, pragma_name, value) do
|
||||
Sqlite3.execute(db, "PRAGMA #{pragma_name} = #{value}")
|
||||
end
|
||||
|
||||
defp get_pragma(db, pragma_name) do
|
||||
{:ok, statement} = Sqlite3.prepare(db, "PRAGMA #{pragma_name}")
|
||||
|
||||
case Sqlite3.fetch_all(db, statement) do
|
||||
{:ok, [[value]]} -> {:ok, value}
|
||||
_ -> :error
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_set_pragma(db, pragma_name, value) do
|
||||
case get_pragma(db, pragma_name) do
|
||||
{:ok, current} ->
|
||||
if current == value do
|
||||
:ok
|
||||
else
|
||||
set_pragma(db, pragma_name, value)
|
||||
end
|
||||
|
||||
_ ->
|
||||
set_pragma(db, pragma_name, value)
|
||||
end
|
||||
end
|
||||
|
||||
defp set_key(db, options) do
|
||||
# we can't use maybe_set_pragma here since
|
||||
# the only thing that will work on an encrypted
|
||||
# database without error is setting the key.
|
||||
case Keyword.fetch(options, :key) do
|
||||
{:ok, key} -> set_pragma(db, "key", key)
|
||||
_ -> :ok
|
||||
end
|
||||
end
|
||||
|
||||
defp set_custom_pragmas(db, options) do
|
||||
# we can't use maybe_set_pragma because some pragmas
|
||||
# are required to be set before the database is e.g. decrypted.
|
||||
case Keyword.fetch(options, :custom_pragmas) do
|
||||
{:ok, list} -> do_set_custom_pragmas(db, list)
|
||||
_ -> :ok
|
||||
end
|
||||
end
|
||||
|
||||
defp do_set_custom_pragmas(db, list) do
|
||||
list
|
||||
|> Enum.reduce_while(:ok, fn {key, value}, :ok ->
|
||||
case set_pragma(db, key, value) do
|
||||
:ok -> {:cont, :ok}
|
||||
{:error, _reason} -> {:halt, :error}
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp set_pragma_if_present(_db, _pragma, nil), do: :ok
|
||||
defp set_pragma_if_present(db, pragma, value), do: set_pragma(db, pragma, value)
|
||||
|
||||
defp set_journal_size_limit(db, options) do
|
||||
set_pragma_if_present(
|
||||
db,
|
||||
"journal_size_limit",
|
||||
Keyword.get(options, :journal_size_limit)
|
||||
)
|
||||
end
|
||||
|
||||
defp set_soft_heap_limit(db, options) do
|
||||
set_pragma_if_present(db, "soft_heap_limit", Keyword.get(options, :soft_heap_limit))
|
||||
end
|
||||
|
||||
defp set_hard_heap_limit(db, options) do
|
||||
set_pragma_if_present(db, "hard_heap_limit", Keyword.get(options, :hard_heap_limit))
|
||||
end
|
||||
|
||||
defp set_journal_mode(db, options) do
|
||||
set_pragma_if_present(db, "journal_mode", Keyword.get(options, :journal_mode))
|
||||
end
|
||||
|
||||
defp set_temp_store(db, options) do
|
||||
set_pragma(db, "temp_store", Pragma.temp_store(options))
|
||||
end
|
||||
|
||||
defp set_synchronous(db, options) do
|
||||
set_pragma(db, "synchronous", Pragma.synchronous(options))
|
||||
end
|
||||
|
||||
defp set_foreign_keys(db, options) do
|
||||
set_pragma(db, "foreign_keys", Pragma.foreign_keys(options))
|
||||
end
|
||||
|
||||
defp set_cache_size(db, options) do
|
||||
maybe_set_pragma(db, "cache_size", Pragma.cache_size(options))
|
||||
end
|
||||
|
||||
defp set_cache_spill(db, options) do
|
||||
set_pragma(db, "cache_spill", Pragma.cache_spill(options))
|
||||
end
|
||||
|
||||
defp set_case_sensitive_like(db, options) do
|
||||
set_pragma(db, "case_sensitive_like", Pragma.case_sensitive_like(options))
|
||||
end
|
||||
|
||||
defp set_auto_vacuum(db, options) do
|
||||
set_pragma(db, "auto_vacuum", Pragma.auto_vacuum(options))
|
||||
end
|
||||
|
||||
defp set_locking_mode(db, options) do
|
||||
set_pragma(db, "locking_mode", Pragma.locking_mode(options))
|
||||
end
|
||||
|
||||
defp set_secure_delete(db, options) do
|
||||
set_pragma(db, "secure_delete", Pragma.secure_delete(options))
|
||||
end
|
||||
|
||||
defp set_wal_auto_check_point(db, options) do
|
||||
set_pragma(db, "wal_autocheckpoint", Pragma.wal_auto_check_point(options))
|
||||
end
|
||||
|
||||
defp set_busy_timeout(db, options) do
|
||||
set_pragma(db, "busy_timeout", Pragma.busy_timeout(options))
|
||||
end
|
||||
|
||||
defp deserialize(db, options) do
|
||||
case Keyword.get(options, :serialized, nil) do
|
||||
nil -> :ok
|
||||
serialized -> Sqlite3.deserialize(db, serialized)
|
||||
end
|
||||
end
|
||||
|
||||
defp load_extensions(db, options) do
|
||||
global_extensions = Application.get_env(:exqlite, :load_extensions, [])
|
||||
|
||||
extensions =
|
||||
Keyword.get(options, :load_extensions, [])
|
||||
|> Enum.concat(global_extensions)
|
||||
|> Enum.uniq()
|
||||
|
||||
do_load_extensions(db, extensions)
|
||||
end
|
||||
|
||||
defp do_load_extensions(_db, []), do: :ok
|
||||
|
||||
defp do_load_extensions(db, extensions) do
|
||||
Sqlite3.enable_load_extension(db, true)
|
||||
|
||||
Enum.each(extensions, fn extension ->
|
||||
Logger.debug(fn -> "Exqlite: loading extension `#{extension}`" end)
|
||||
Sqlite3.execute(db, "SELECT load_extension('#{extension}')")
|
||||
end)
|
||||
|
||||
Sqlite3.enable_load_extension(db, false)
|
||||
end
|
||||
|
||||
defp do_connect(database, options) do
|
||||
with {:ok, directory} <- resolve_directory(database),
|
||||
:ok <- mkdir_p(directory),
|
||||
{:ok, db} <- Sqlite3.open(database, options),
|
||||
:ok <- set_key(db, options),
|
||||
:ok <- set_custom_pragmas(db, options),
|
||||
:ok <- set_journal_mode(db, options),
|
||||
:ok <- set_temp_store(db, options),
|
||||
:ok <- set_synchronous(db, options),
|
||||
:ok <- set_foreign_keys(db, options),
|
||||
:ok <- set_cache_size(db, options),
|
||||
:ok <- set_cache_spill(db, options),
|
||||
:ok <- set_auto_vacuum(db, options),
|
||||
:ok <- set_locking_mode(db, options),
|
||||
:ok <- set_secure_delete(db, options),
|
||||
:ok <- set_wal_auto_check_point(db, options),
|
||||
:ok <- set_case_sensitive_like(db, options),
|
||||
:ok <- set_busy_timeout(db, options),
|
||||
:ok <- set_journal_size_limit(db, options),
|
||||
:ok <- set_soft_heap_limit(db, options),
|
||||
:ok <- set_hard_heap_limit(db, options),
|
||||
:ok <- load_extensions(db, options),
|
||||
:ok <- deserialize(db, options) do
|
||||
state = %__MODULE__{
|
||||
db: db,
|
||||
default_transaction_mode:
|
||||
Keyword.get(options, :default_transaction_mode, :deferred),
|
||||
directory: directory,
|
||||
path: database,
|
||||
transaction_status: :idle,
|
||||
status: :idle,
|
||||
chunk_size: Keyword.get(options, :chunk_size),
|
||||
before_disconnect: Keyword.get(options, :before_disconnect, nil)
|
||||
}
|
||||
|
||||
{:ok, state}
|
||||
else
|
||||
{:error, reason} ->
|
||||
{:error, %Exqlite.Error{message: to_string(reason)}}
|
||||
end
|
||||
end
|
||||
|
||||
def maybe_put_command(query, options) do
|
||||
case Keyword.get(options, :command) do
|
||||
nil -> query
|
||||
command -> %{query | command: command}
|
||||
end
|
||||
end
|
||||
|
||||
# Attempt to retrieve the cached query, if it doesn't exist, we'll prepare one
|
||||
# and cache it for later.
|
||||
defp prepare(%Query{statement: statement} = query, options, state) do
|
||||
query = maybe_put_command(query, options)
|
||||
|
||||
with {:ok, ref} <- Sqlite3.prepare(state.db, IO.iodata_to_binary(statement)),
|
||||
query <- %{query | ref: ref} do
|
||||
{:ok, query}
|
||||
else
|
||||
{:error, reason} ->
|
||||
{:error, %Error{message: to_string(reason), statement: statement}, state}
|
||||
end
|
||||
end
|
||||
|
||||
# Prepare a query and do not cache it.
|
||||
defp prepare_no_cache(%Query{statement: statement} = query, options, state) do
|
||||
query = maybe_put_command(query, options)
|
||||
|
||||
case Sqlite3.prepare(state.db, statement) do
|
||||
{:ok, ref} ->
|
||||
{:ok, %{query | ref: ref}}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, %Error{message: to_string(reason), statement: statement}, state}
|
||||
end
|
||||
end
|
||||
|
||||
@spec maybe_changes(Sqlite3.db(), Query.t()) :: integer() | nil
|
||||
defp maybe_changes(db, %Query{command: command})
|
||||
when command in [:update, :insert, :delete] do
|
||||
case Sqlite3.changes(db) do
|
||||
{:ok, total} -> total
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_changes(_, _), do: nil
|
||||
|
||||
# when we have an empty list of columns, that signifies that
|
||||
# there was no possible return tuple (e.g., update statement without RETURNING)
|
||||
# and in that case, we return nil to signify no possible result.
|
||||
defp maybe_rows([], []), do: nil
|
||||
defp maybe_rows(rows, _cols), do: rows
|
||||
|
||||
defp execute(call, %Query{} = query, params, state) do
|
||||
with {:ok, query} <- bind_params(query, params, state),
|
||||
{:ok, columns} <- get_columns(query, state),
|
||||
{:ok, rows} <- get_rows(query, state),
|
||||
{:ok, transaction_status} <- Sqlite3.transaction_status(state.db),
|
||||
changes <- maybe_changes(state.db, query) do
|
||||
case query.command do
|
||||
command when command in [:delete, :insert, :update] ->
|
||||
{
|
||||
:ok,
|
||||
query,
|
||||
Result.new(
|
||||
command: call,
|
||||
num_rows: changes,
|
||||
rows: maybe_rows(rows, columns)
|
||||
),
|
||||
%{state | transaction_status: transaction_status}
|
||||
}
|
||||
|
||||
_ ->
|
||||
{
|
||||
:ok,
|
||||
query,
|
||||
Result.new(
|
||||
command: call,
|
||||
columns: columns,
|
||||
rows: rows,
|
||||
num_rows: Enum.count(rows)
|
||||
),
|
||||
%{state | transaction_status: transaction_status}
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp bind_params(%Query{ref: ref, statement: statement} = query, params, state)
|
||||
when ref != nil do
|
||||
try do
|
||||
Sqlite3.bind(ref, params)
|
||||
rescue
|
||||
e -> {:error, %Error{message: Exception.message(e), statement: statement}, state}
|
||||
else
|
||||
:ok -> {:ok, query}
|
||||
end
|
||||
end
|
||||
|
||||
defp get_columns(%Query{ref: ref, statement: statement}, state) do
|
||||
case Sqlite3.columns(state.db, ref) do
|
||||
{:ok, columns} ->
|
||||
{:ok, columns}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, %Error{message: to_string(reason), statement: statement}, state}
|
||||
end
|
||||
end
|
||||
|
||||
defp get_rows(%Query{ref: ref, statement: statement}, state) do
|
||||
case Sqlite3.fetch_all(state.db, ref, state.chunk_size) do
|
||||
{:ok, rows} ->
|
||||
{:ok, rows}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, %Error{message: to_string(reason), statement: statement}, state}
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_transaction(call, statement, state) do
|
||||
with :ok <- Sqlite3.execute(state.db, statement),
|
||||
{:ok, transaction_status} <- Sqlite3.transaction_status(state.db) do
|
||||
result = %Result{
|
||||
command: call,
|
||||
rows: [],
|
||||
columns: [],
|
||||
num_rows: 0
|
||||
}
|
||||
|
||||
{:ok, result, %{state | transaction_status: transaction_status}}
|
||||
else
|
||||
{:error, reason} ->
|
||||
{:disconnect, %Error{message: to_string(reason), statement: statement}, state}
|
||||
end
|
||||
end
|
||||
|
||||
defp resolve_directory(":memory:"), do: {:ok, nil}
|
||||
|
||||
defp resolve_directory("file:" <> _ = uri) do
|
||||
case URI.parse(uri) do
|
||||
%{path: path} when is_binary(path) ->
|
||||
{:ok, Path.dirname(path)}
|
||||
|
||||
_ ->
|
||||
{:error, "No path in #{inspect(uri)}"}
|
||||
end
|
||||
end
|
||||
|
||||
defp resolve_directory(path), do: {:ok, Path.dirname(path)}
|
||||
|
||||
# SQLITE_OPEN_CREATE will create the DB file if not existing, but
|
||||
# will not create intermediary directories if they are missing.
|
||||
# So let's preemptively create the intermediate directories here
|
||||
# before trying to open the DB file.
|
||||
defp mkdir_p(nil), do: :ok
|
||||
defp mkdir_p(directory), do: File.mkdir_p(directory)
|
||||
end
|
||||
18
phoenix/deps/exqlite/lib/exqlite/error.ex
Normal file
18
phoenix/deps/exqlite/lib/exqlite/error.ex
Normal file
@@ -0,0 +1,18 @@
|
||||
defmodule Exqlite.Error do
|
||||
@moduledoc """
|
||||
The error emitted from SQLite or a general error with the library.
|
||||
"""
|
||||
|
||||
defexception [:message, :statement]
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
message: String.t(),
|
||||
statement: String.t()
|
||||
}
|
||||
|
||||
@impl true
|
||||
def message(%__MODULE__{message: message, statement: nil}), do: message
|
||||
|
||||
def message(%__MODULE__{message: message, statement: statement}),
|
||||
do: "#{message}\n#{statement}"
|
||||
end
|
||||
35
phoenix/deps/exqlite/lib/exqlite/flags.ex
Normal file
35
phoenix/deps/exqlite/lib/exqlite/flags.ex
Normal file
@@ -0,0 +1,35 @@
|
||||
defmodule Exqlite.Flags do
|
||||
@moduledoc false
|
||||
|
||||
import Bitwise
|
||||
|
||||
# https://www.sqlite.org/c3ref/c_open_autoproxy.html
|
||||
@file_open_flags [
|
||||
sqlite_open_readonly: 0x00000001,
|
||||
sqlite_open_readwrite: 0x00000002,
|
||||
sqlite_open_create: 0x00000004,
|
||||
sqlite_open_deleteonclos: 0x00000008,
|
||||
sqlite_open_exclusive: 0x00000010,
|
||||
sqlite_open_autoproxy: 0x00000020,
|
||||
sqlite_open_uri: 0x00000040,
|
||||
sqlite_open_memory: 0x00000080,
|
||||
sqlite_open_main_db: 0x00000100,
|
||||
sqlite_open_temp_db: 0x00000200,
|
||||
sqlite_open_transient_db: 0x00000400,
|
||||
sqlite_open_main_journal: 0x00000800,
|
||||
sqlite_open_temp_journal: 0x00001000,
|
||||
sqlite_open_subjournal: 0x00002000,
|
||||
sqlite_open_super_journal: 0x00004000,
|
||||
sqlite_open_nomutex: 0x00008000,
|
||||
sqlite_open_fullmutex: 0x00010000,
|
||||
sqlite_open_sharedcache: 0x00020000,
|
||||
sqlite_open_privatecache: 0x00040000,
|
||||
sqlite_open_wal: 0x00080000,
|
||||
sqlite_open_nofollow: 0x01000000,
|
||||
sqlite_open_exrescode: 0x02000000
|
||||
]
|
||||
|
||||
def put_file_open_flags(current_flags \\ 0, flags) do
|
||||
Enum.reduce(flags, current_flags, &(&2 ||| Keyword.fetch!(@file_open_flags, &1)))
|
||||
end
|
||||
end
|
||||
121
phoenix/deps/exqlite/lib/exqlite/pragma.ex
Normal file
121
phoenix/deps/exqlite/lib/exqlite/pragma.ex
Normal file
@@ -0,0 +1,121 @@
|
||||
defmodule Exqlite.Pragma do
|
||||
@moduledoc """
|
||||
Handles parsing extra options for the SQLite connection
|
||||
"""
|
||||
|
||||
def busy_timeout(nil), do: busy_timeout([])
|
||||
|
||||
def busy_timeout(options) do
|
||||
Keyword.get(options, :busy_timeout, 2000)
|
||||
end
|
||||
|
||||
def journal_mode(nil), do: journal_mode([])
|
||||
|
||||
def journal_mode(options) do
|
||||
case Keyword.get(options, :journal_mode, :delete) do
|
||||
:delete -> "delete"
|
||||
:memory -> "memory"
|
||||
:off -> "off"
|
||||
:persist -> "persist"
|
||||
:truncate -> "truncate"
|
||||
:wal -> "wal"
|
||||
_ -> raise ArgumentError, "invalid :journal_mode"
|
||||
end
|
||||
end
|
||||
|
||||
def temp_store(nil), do: temp_store([])
|
||||
|
||||
def temp_store(options) do
|
||||
case Keyword.get(options, :temp_store, :default) do
|
||||
:file -> 1
|
||||
:memory -> 2
|
||||
:default -> 0
|
||||
_ -> raise ArgumentError, "invalid :temp_store"
|
||||
end
|
||||
end
|
||||
|
||||
def synchronous(nil), do: synchronous([])
|
||||
|
||||
def synchronous(options) do
|
||||
case Keyword.get(options, :synchronous, :normal) do
|
||||
:extra -> 3
|
||||
:full -> 2
|
||||
:normal -> 1
|
||||
:off -> 0
|
||||
_ -> raise ArgumentError, "invalid :synchronous"
|
||||
end
|
||||
end
|
||||
|
||||
def foreign_keys(nil), do: foreign_keys([])
|
||||
|
||||
def foreign_keys(options) do
|
||||
case Keyword.get(options, :foreign_keys, :on) do
|
||||
:off -> 0
|
||||
:on -> 1
|
||||
_ -> raise ArgumentError, "invalid :foreign_keys"
|
||||
end
|
||||
end
|
||||
|
||||
def cache_size(nil), do: cache_size([])
|
||||
|
||||
def cache_size(options) do
|
||||
Keyword.get(options, :cache_size, -2000)
|
||||
end
|
||||
|
||||
def cache_spill(nil), do: cache_spill([])
|
||||
|
||||
def cache_spill(options) do
|
||||
case Keyword.get(options, :cache_spill, :on) do
|
||||
:off -> 0
|
||||
:on -> 1
|
||||
_ -> raise ArgumentError, "invalid :cache_spill"
|
||||
end
|
||||
end
|
||||
|
||||
def case_sensitive_like(nil), do: case_sensitive_like([])
|
||||
|
||||
def case_sensitive_like(options) do
|
||||
case Keyword.get(options, :case_sensitive_like, :off) do
|
||||
:off -> 0
|
||||
:on -> 1
|
||||
_ -> raise ArgumentError, "invalid :case_sensitive_like"
|
||||
end
|
||||
end
|
||||
|
||||
def auto_vacuum(nil), do: auto_vacuum([])
|
||||
|
||||
def auto_vacuum(options) do
|
||||
case Keyword.get(options, :auto_vacuum, :none) do
|
||||
:none -> 0
|
||||
:full -> 1
|
||||
:incremental -> 2
|
||||
_ -> raise ArgumentError, "invalid :auto_vacuum"
|
||||
end
|
||||
end
|
||||
|
||||
def locking_mode(nil), do: locking_mode([])
|
||||
|
||||
def locking_mode(options) do
|
||||
case Keyword.get(options, :locking_mode, :normal) do
|
||||
:normal -> "NORMAL"
|
||||
:exclusive -> "EXCLUSIVE"
|
||||
_ -> raise ArgumentError, "invalid :locking_mode"
|
||||
end
|
||||
end
|
||||
|
||||
def secure_delete(nil), do: secure_delete([])
|
||||
|
||||
def secure_delete(options) do
|
||||
case Keyword.get(options, :secure_delete, :off) do
|
||||
:off -> 0
|
||||
:on -> 1
|
||||
_ -> raise ArgumentError, "invalid :secure_delete"
|
||||
end
|
||||
end
|
||||
|
||||
def wal_auto_check_point(nil), do: wal_auto_check_point([])
|
||||
|
||||
def wal_auto_check_point(options) do
|
||||
Keyword.get(options, :wal_auto_check_point, 1000)
|
||||
end
|
||||
end
|
||||
70
phoenix/deps/exqlite/lib/exqlite/query.ex
Normal file
70
phoenix/deps/exqlite/lib/exqlite/query.ex
Normal file
@@ -0,0 +1,70 @@
|
||||
defmodule Exqlite.Query do
|
||||
@moduledoc """
|
||||
Query struct returned from a successfully prepared query.
|
||||
"""
|
||||
@type t :: %__MODULE__{
|
||||
statement: iodata(),
|
||||
name: atom() | String.t(),
|
||||
ref: reference() | nil,
|
||||
command: :insert | :delete | :update | nil
|
||||
}
|
||||
|
||||
defstruct statement: nil,
|
||||
name: nil,
|
||||
ref: nil,
|
||||
command: nil
|
||||
|
||||
def build(options) do
|
||||
statement = Keyword.get(options, :statement)
|
||||
name = Keyword.get(options, :name)
|
||||
ref = Keyword.get(options, :ref)
|
||||
|
||||
command =
|
||||
case Keyword.get(options, :command) do
|
||||
nil -> extract_command(statement)
|
||||
value -> value
|
||||
end
|
||||
|
||||
%__MODULE__{
|
||||
statement: statement,
|
||||
name: name,
|
||||
ref: ref,
|
||||
command: command
|
||||
}
|
||||
end
|
||||
|
||||
defp extract_command(nil), do: nil
|
||||
|
||||
defp extract_command(statement) do
|
||||
cond do
|
||||
String.contains?(statement, "INSERT") -> :insert
|
||||
String.contains?(statement, "DELETE") -> :delete
|
||||
String.contains?(statement, "UPDATE") -> :update
|
||||
true -> nil
|
||||
end
|
||||
end
|
||||
|
||||
defimpl DBConnection.Query do
|
||||
def parse(query, _opts) do
|
||||
query
|
||||
end
|
||||
|
||||
def describe(query, _opts) do
|
||||
query
|
||||
end
|
||||
|
||||
def encode(_query, params, _opts) do
|
||||
params
|
||||
end
|
||||
|
||||
def decode(_query, result, _opts) do
|
||||
result
|
||||
end
|
||||
end
|
||||
|
||||
defimpl String.Chars do
|
||||
def to_string(%{statement: statement}) do
|
||||
IO.iodata_to_binary(statement)
|
||||
end
|
||||
end
|
||||
end
|
||||
35
phoenix/deps/exqlite/lib/exqlite/result.ex
Normal file
35
phoenix/deps/exqlite/lib/exqlite/result.ex
Normal file
@@ -0,0 +1,35 @@
|
||||
defmodule Exqlite.Result do
|
||||
@moduledoc """
|
||||
The database results.
|
||||
"""
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
command: atom,
|
||||
columns: [String.t()] | nil,
|
||||
rows: [[term] | term] | nil,
|
||||
num_rows: integer()
|
||||
}
|
||||
|
||||
defstruct command: nil, columns: [], rows: [], num_rows: 0
|
||||
|
||||
def new(options) do
|
||||
%__MODULE__{
|
||||
command: Keyword.get(options, :command),
|
||||
columns: Keyword.get(options, :columns, []),
|
||||
rows: Keyword.get(options, :rows, []),
|
||||
num_rows: Keyword.get(options, :num_rows, 0)
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
if Code.ensure_loaded?(Table.Reader) do
|
||||
defimpl Table.Reader, for: Exqlite.Result do
|
||||
def init(%{columns: columns}) when columns in [nil, []] do
|
||||
{:rows, %{columns: []}, []}
|
||||
end
|
||||
|
||||
def init(result) do
|
||||
{:rows, %{columns: result.columns}, result.rows}
|
||||
end
|
||||
end
|
||||
end
|
||||
587
phoenix/deps/exqlite/lib/exqlite/sqlite3.ex
Normal file
587
phoenix/deps/exqlite/lib/exqlite/sqlite3.ex
Normal file
@@ -0,0 +1,587 @@
|
||||
defmodule Exqlite.Sqlite3 do
|
||||
@moduledoc """
|
||||
The interface to the NIF implementation.
|
||||
"""
|
||||
|
||||
# If the database reference is closed, any prepared statements should be
|
||||
# dereferenced as well. It is entirely possible that an application does
|
||||
# not properly remove a stale reference.
|
||||
#
|
||||
# Will need to add a test for this and think of possible solution.
|
||||
|
||||
# Need to figure out if we can just stream results where we use this
|
||||
# module as a sink.
|
||||
|
||||
alias Exqlite.Flags
|
||||
alias Exqlite.Sqlite3NIF
|
||||
|
||||
@type db() :: reference()
|
||||
@type statement() :: reference()
|
||||
@type reason() :: atom() | String.t()
|
||||
@type row() :: list()
|
||||
@type open_mode :: :readwrite | :readonly | :nomutex
|
||||
@type open_opt :: {:mode, :readwrite | :readonly | [open_mode()]}
|
||||
|
||||
@doc """
|
||||
Opens a new sqlite database at the Path provided.
|
||||
|
||||
`path` can be `":memory"` to keep the sqlite database in memory.
|
||||
|
||||
## Options
|
||||
|
||||
* `:mode` - use `:readwrite` to open the database for reading and writing
|
||||
, `:readonly` to open it in read-only mode or `[:readonly | :readwrite, :nomutex]`
|
||||
to open it with no mutex mode. `:readwrite` will also create
|
||||
the database if it doesn't already exist. Defaults to `:readwrite`.
|
||||
Note: [:readwrite, :nomutex] is not recommended.
|
||||
"""
|
||||
@spec open(String.t(), [open_opt()]) :: {:ok, db()} | {:error, reason()}
|
||||
def open(path, opts \\ []) do
|
||||
mode = Keyword.get(opts, :mode, :readwrite)
|
||||
Sqlite3NIF.open(path, flags_from_mode(mode))
|
||||
end
|
||||
|
||||
defp flags_from_mode(:nomutex) do
|
||||
raise ArgumentError,
|
||||
"expected mode to be `:readwrite` or `:readonly`, can't use a single :nomutex mode"
|
||||
end
|
||||
|
||||
defp flags_from_mode(:readwrite),
|
||||
do: do_flags_from_mode([:readwrite], [])
|
||||
|
||||
defp flags_from_mode(:readonly),
|
||||
do: do_flags_from_mode([:readonly], [])
|
||||
|
||||
defp flags_from_mode([_ | _] = modes),
|
||||
do: do_flags_from_mode(modes, [])
|
||||
|
||||
defp flags_from_mode(mode) do
|
||||
raise ArgumentError,
|
||||
"expected mode to be `:readwrite`, `:readonly` or list of modes, but received #{inspect(mode)}"
|
||||
end
|
||||
|
||||
defp do_flags_from_mode([:readwrite | tail], acc),
|
||||
do: do_flags_from_mode(tail, [:sqlite_open_readwrite, :sqlite_open_create | acc])
|
||||
|
||||
defp do_flags_from_mode([:readonly | tail], acc),
|
||||
do: do_flags_from_mode(tail, [:sqlite_open_readonly | acc])
|
||||
|
||||
defp do_flags_from_mode([:nomutex | tail], acc),
|
||||
do: do_flags_from_mode(tail, [:sqlite_open_nomutex | acc])
|
||||
|
||||
defp do_flags_from_mode([mode | _tail], _acc) do
|
||||
raise ArgumentError,
|
||||
"expected mode to be `:readwrite`, `:readonly` or `:nomutex`, but received #{inspect(mode)}"
|
||||
end
|
||||
|
||||
defp do_flags_from_mode([], acc),
|
||||
do: Flags.put_file_open_flags(acc)
|
||||
|
||||
@doc """
|
||||
Closes the database and releases any underlying resources.
|
||||
"""
|
||||
@spec close(db() | nil) :: :ok | {:error, reason()}
|
||||
def close(nil), do: :ok
|
||||
def close(conn), do: Sqlite3NIF.close(conn)
|
||||
|
||||
@doc """
|
||||
Interrupt a long-running query.
|
||||
|
||||
> #### Warning {: .warning}
|
||||
> If you are going to interrupt a long running process, it is unsafe to call
|
||||
> `close/1` immediately after. You run the risk of undefined behavior. This
|
||||
> is a limitation of the sqlite library itself. Please see the documentation
|
||||
> https://www.sqlite.org/c3ref/interrupt.html for more information.
|
||||
>
|
||||
> If close must be called after, it is best to put a short sleep in order to
|
||||
> let sqlite finish doing its book keeping.
|
||||
"""
|
||||
@spec interrupt(db() | nil) :: :ok | {:error, reason()}
|
||||
def interrupt(nil), do: :ok
|
||||
def interrupt(conn), do: Sqlite3NIF.interrupt(conn)
|
||||
|
||||
@doc """
|
||||
Executes an sql script. Multiple stanzas can be passed at once.
|
||||
"""
|
||||
@spec execute(db(), String.t()) :: :ok | {:error, reason()}
|
||||
def execute(conn, sql), do: Sqlite3NIF.execute(conn, sql)
|
||||
|
||||
@doc """
|
||||
Get the number of changes recently.
|
||||
|
||||
**Note**: If triggers are used, the count may be larger than expected.
|
||||
|
||||
See: https://sqlite.org/c3ref/changes.html
|
||||
"""
|
||||
@spec changes(db()) :: {:ok, integer()} | {:error, reason()}
|
||||
def changes(conn), do: Sqlite3NIF.changes(conn)
|
||||
|
||||
@spec prepare(db(), String.t()) :: {:ok, statement()} | {:error, reason()}
|
||||
def prepare(conn, sql), do: Sqlite3NIF.prepare(conn, sql)
|
||||
|
||||
@doc """
|
||||
Resets a prepared statement.
|
||||
|
||||
See: https://sqlite.org/c3ref/reset.html
|
||||
"""
|
||||
@spec reset(statement) :: :ok | {:error, atom()}
|
||||
def reset(stmt), do: Sqlite3NIF.reset(stmt)
|
||||
|
||||
@doc """
|
||||
Returns number of SQL parameters in a prepared statement.
|
||||
|
||||
iex> {:ok, conn} = Sqlite3.open(":memory:", [:readonly])
|
||||
iex> {:ok, stmt} = Sqlite3.prepare(conn, "SELECT ?, ?")
|
||||
iex> Sqlite3.bind_parameter_count(stmt)
|
||||
2
|
||||
|
||||
"""
|
||||
@spec bind_parameter_count(statement) :: non_neg_integer | {:error, atom()}
|
||||
def bind_parameter_count(stmt), do: Sqlite3NIF.bind_parameter_count(stmt)
|
||||
|
||||
@type bind_value ::
|
||||
NaiveDateTime.t()
|
||||
| DateTime.t()
|
||||
| Date.t()
|
||||
| Time.t()
|
||||
| number
|
||||
| iodata
|
||||
| {:blob, iodata}
|
||||
| atom
|
||||
|
||||
@doc """
|
||||
Resets a prepared statement and binds values to it.
|
||||
|
||||
iex> {:ok, conn} = Sqlite3.open(":memory:", [:readonly])
|
||||
iex> {:ok, stmt} = Sqlite3.prepare(conn, "SELECT ?, ?, ?, ?, ?")
|
||||
iex> Sqlite3.bind(stmt, [42, 3.14, "Alice", {:blob, <<0, 0, 0>>}, nil])
|
||||
iex> Sqlite3.step(conn, stmt)
|
||||
{:row, [42, 3.14, "Alice", <<0, 0, 0>>, nil]}
|
||||
|
||||
iex> {:ok, conn} = Sqlite3.open(":memory:", [:readonly])
|
||||
iex> {:ok, stmt} = Sqlite3.prepare(conn, "SELECT :42, @pi, $name, @blob, :null")
|
||||
iex> Sqlite3.bind(stmt, %{":42" => 42, "@pi" => 3.14, "$name" => "Alice", :"@blob" => {:blob, <<0, 0, 0>>}, ~c":null" => nil})
|
||||
iex> Sqlite3.step(conn, stmt)
|
||||
{:row, [42, 3.14, "Alice", <<0, 0, 0>>, nil]}
|
||||
|
||||
iex> {:ok, conn} = Sqlite3.open(":memory:", [:readonly])
|
||||
iex> {:ok, stmt} = Sqlite3.prepare(conn, "SELECT ?")
|
||||
iex> Sqlite3.bind(stmt, [42, 3.14, "Alice"])
|
||||
** (ArgumentError) expected 1 arguments, got 3
|
||||
|
||||
iex> {:ok, conn} = Sqlite3.open(":memory:", [:readonly])
|
||||
iex> {:ok, stmt} = Sqlite3.prepare(conn, "SELECT ?, ?")
|
||||
iex> Sqlite3.bind(stmt, [42])
|
||||
** (ArgumentError) expected 2 arguments, got 1
|
||||
|
||||
iex> {:ok, conn} = Sqlite3.open(":memory:", [:readonly])
|
||||
iex> {:ok, stmt} = Sqlite3.prepare(conn, "SELECT ?")
|
||||
iex> Sqlite3.bind(stmt, [:erlang.list_to_pid(~c"<0.0.0>")])
|
||||
** (ArgumentError) unsupported type: #PID<0.0.0>
|
||||
|
||||
"""
|
||||
@spec bind(
|
||||
statement,
|
||||
[bind_value] | %{optional(String.t()) => bind_value} | nil
|
||||
) :: :ok
|
||||
def bind(stmt, nil), do: bind(stmt, [])
|
||||
|
||||
def bind(stmt, args) when is_list(args) do
|
||||
case bind_parameter_count(stmt) do
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
|
||||
params_count ->
|
||||
args_count = length(args)
|
||||
|
||||
if args_count == params_count do
|
||||
bind_all(args, stmt, 1)
|
||||
else
|
||||
raise ArgumentError, "expected #{params_count} arguments, got #{args_count}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def bind(stmt, args) when is_map(args) do
|
||||
case bind_parameter_count(stmt) do
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
|
||||
params_count ->
|
||||
args_count = map_size(args)
|
||||
|
||||
if args_count == params_count do
|
||||
bind_all_named(Map.to_list(args), stmt)
|
||||
else
|
||||
raise ArgumentError,
|
||||
"expected #{params_count} named arguments, got #{args_count}: #{inspect(Map.keys(args))}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp bind_all([param | params], stmt, idx) do
|
||||
do_bind(stmt, idx, param)
|
||||
bind_all(params, stmt, idx + 1)
|
||||
end
|
||||
|
||||
defp bind_all([], _stmt, _idx), do: :ok
|
||||
|
||||
defp bind_all_named([{name, param} | named_params], stmt) do
|
||||
idx = Sqlite3NIF.bind_parameter_index(stmt, to_string(name))
|
||||
|
||||
if idx == 0 do
|
||||
raise ArgumentError, "unknown named parameter: #{inspect(name)}"
|
||||
end
|
||||
|
||||
do_bind(stmt, idx, param)
|
||||
bind_all_named(named_params, stmt)
|
||||
end
|
||||
|
||||
defp bind_all_named([], _stmt), do: :ok
|
||||
|
||||
# credo:disable-for-next-line Credo.Check.Refactor.CyclomaticComplexity
|
||||
defp do_bind(stmt, idx, param) do
|
||||
case convert(param) do
|
||||
i when is_integer(i) -> bind_integer(stmt, idx, i)
|
||||
f when is_float(f) -> bind_float(stmt, idx, f)
|
||||
b when is_binary(b) -> bind_text(stmt, idx, b)
|
||||
b when is_list(b) -> bind_text(stmt, idx, IO.iodata_to_binary(b))
|
||||
nil -> bind_null(stmt, idx)
|
||||
:undefined -> bind_null(stmt, idx)
|
||||
a when is_atom(a) -> bind_text(stmt, idx, Atom.to_string(a))
|
||||
{:blob, b} when is_binary(b) -> bind_blob(stmt, idx, b)
|
||||
{:blob, b} when is_list(b) -> bind_blob(stmt, idx, IO.iodata_to_binary(b))
|
||||
_other -> raise ArgumentError, "unsupported type: #{inspect(param)}"
|
||||
end
|
||||
end
|
||||
|
||||
@spec columns(db(), statement()) :: {:ok, [binary()]} | {:error, reason()}
|
||||
def columns(conn, statement), do: Sqlite3NIF.columns(conn, statement)
|
||||
|
||||
@spec step(db(), statement()) :: :done | :busy | {:row, row()} | {:error, reason()}
|
||||
def step(conn, statement), do: Sqlite3NIF.step(conn, statement)
|
||||
|
||||
@spec multi_step(db(), statement()) ::
|
||||
:busy | {:rows, [row()]} | {:done, [row()]} | {:error, reason()}
|
||||
def multi_step(conn, statement) do
|
||||
chunk_size = Application.get_env(:exqlite, :default_chunk_size, 50)
|
||||
multi_step(conn, statement, chunk_size)
|
||||
end
|
||||
|
||||
@spec multi_step(db(), statement(), integer()) ::
|
||||
:busy | {:rows, [row()]} | {:done, [row()]} | {:error, reason()}
|
||||
def multi_step(conn, statement, chunk_size) do
|
||||
case Sqlite3NIF.multi_step(conn, statement, chunk_size) do
|
||||
:busy ->
|
||||
:busy
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
|
||||
{:rows, rows} ->
|
||||
{:rows, Enum.reverse(rows)}
|
||||
|
||||
{:done, rows} ->
|
||||
{:done, Enum.reverse(rows)}
|
||||
end
|
||||
end
|
||||
|
||||
@spec last_insert_rowid(db()) :: {:ok, integer()}
|
||||
def last_insert_rowid(conn), do: Sqlite3NIF.last_insert_rowid(conn)
|
||||
|
||||
@spec transaction_status(db()) :: {:ok, :idle | :transaction}
|
||||
def transaction_status(conn), do: Sqlite3NIF.transaction_status(conn)
|
||||
|
||||
@doc """
|
||||
Causes the database connection to free as much memory as it can. This is
|
||||
useful if you are on a memory restricted system.
|
||||
"""
|
||||
@spec shrink_memory(db()) :: :ok | {:error, reason()}
|
||||
def shrink_memory(conn) do
|
||||
Sqlite3NIF.execute(conn, "PRAGMA shrink_memory")
|
||||
end
|
||||
|
||||
@spec fetch_all(db(), statement(), integer()) :: {:ok, [row()]} | {:error, reason()}
|
||||
def fetch_all(conn, statement, chunk_size) do
|
||||
{:ok, try_fetch_all(conn, statement, chunk_size)}
|
||||
catch
|
||||
:throw, {:error, _reason} = error -> error
|
||||
end
|
||||
|
||||
defp try_fetch_all(conn, statement, chunk_size) do
|
||||
case multi_step(conn, statement, chunk_size) do
|
||||
{:done, rows} -> rows
|
||||
{:rows, rows} -> rows ++ try_fetch_all(conn, statement, chunk_size)
|
||||
{:error, _reason} = error -> throw(error)
|
||||
:busy -> throw({:error, "Database busy"})
|
||||
end
|
||||
end
|
||||
|
||||
@spec fetch_all(db(), statement()) :: {:ok, [row()]} | {:error, reason()}
|
||||
def fetch_all(conn, statement) do
|
||||
# Should this be done in the NIF? It can be _much_ faster to build a list
|
||||
# there, but at the expense that it could block other dirty nifs from
|
||||
# getting work done.
|
||||
#
|
||||
# For now this just works
|
||||
chunk_size = Application.get_env(:exqlite, :default_chunk_size, 50)
|
||||
fetch_all(conn, statement, chunk_size)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Serialize the contents of the database to a binary.
|
||||
"""
|
||||
@spec serialize(db(), String.t()) :: {:ok, binary()} | {:error, reason()}
|
||||
def serialize(conn, database \\ "main") do
|
||||
Sqlite3NIF.serialize(conn, database)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Disconnect from database and then reopen as an in-memory database based on
|
||||
the serialized binary.
|
||||
"""
|
||||
@spec deserialize(db(), String.t(), binary()) :: :ok | {:error, reason()}
|
||||
def deserialize(conn, database \\ "main", serialized) do
|
||||
Sqlite3NIF.deserialize(conn, database, serialized)
|
||||
end
|
||||
|
||||
def release(_conn, nil), do: :ok
|
||||
|
||||
@doc """
|
||||
Once finished with the prepared statement, call this to release the underlying
|
||||
resources.
|
||||
|
||||
This should be called whenever you are done operating with the prepared statement. If
|
||||
the system has a high load the garbage collector may not clean up the prepared
|
||||
statements in a timely manner and causing higher than normal levels of memory
|
||||
pressure.
|
||||
|
||||
If you are operating on limited memory capacity systems, definitely call this.
|
||||
"""
|
||||
@spec release(db(), statement()) :: :ok | {:error, reason()}
|
||||
def release(conn, statement) do
|
||||
Sqlite3NIF.release(conn, statement)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Allow loading native extensions.
|
||||
"""
|
||||
@spec enable_load_extension(db(), boolean()) :: :ok | {:error, reason()}
|
||||
def enable_load_extension(conn, flag) do
|
||||
if flag do
|
||||
Sqlite3NIF.enable_load_extension(conn, 1)
|
||||
else
|
||||
Sqlite3NIF.enable_load_extension(conn, 0)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Send data change notifications to a process.
|
||||
|
||||
Each time an insert, update, or delete is performed on the connection provided
|
||||
as the first argument, a message will be sent to the pid provided as the second argument.
|
||||
|
||||
The message is of the form: `{action, db_name, table, row_id}`, where:
|
||||
|
||||
* `action` is one of `:insert`, `:update` or `:delete`
|
||||
* `db_name` is a string representing the database name where the change took place
|
||||
* `table` is a string representing the table name where the change took place
|
||||
* `row_id` is an integer representing the unique row id assigned by SQLite
|
||||
|
||||
## Restrictions
|
||||
|
||||
* There are some conditions where the update hook will not be invoked by SQLite.
|
||||
See the documentation for [more details](https://www.sqlite.org/c3ref/update_hook.html)
|
||||
* Only one pid can listen to the changes on a given database connection at a time.
|
||||
If this function is called multiple times for the same connection, only the last pid will
|
||||
receive the notifications
|
||||
* Updates only happen for the connection that is opened. For example, there
|
||||
are two connections A and B. When an update happens on connection B, the
|
||||
hook set for connection A will not receive the update, but the hook for
|
||||
connection B will receive the update.
|
||||
"""
|
||||
@spec set_update_hook(db(), pid()) :: :ok | {:error, reason()}
|
||||
def set_update_hook(conn, pid) do
|
||||
Sqlite3NIF.set_update_hook(conn, pid)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Set an authorizer that denies specific SQL operations.
|
||||
|
||||
Accepts a list of action atoms to deny. Any SQL statement that triggers a
|
||||
denied action will fail with a "not authorized" error during preparation.
|
||||
|
||||
Pass an empty list to clear the authorizer.
|
||||
|
||||
## Action atoms
|
||||
|
||||
`:attach`, `:detach`, `:pragma`, `:insert`, `:update`, `:delete`,
|
||||
`:create_table`, `:drop_table`, `:create_index`, `:drop_index`,
|
||||
`:create_trigger`, `:drop_trigger`, `:create_view`, `:drop_view`,
|
||||
`:alter_table`, `:reindex`, `:analyze`, `:function`, `:savepoint`,
|
||||
`:transaction`, `:read`, `:select`, `:recursive`,
|
||||
`:create_temp_table`, `:create_temp_index`, `:create_temp_trigger`,
|
||||
`:create_temp_view`, `:drop_temp_table`, `:drop_temp_index`,
|
||||
`:drop_temp_trigger`, `:drop_temp_view`, `:create_vtable`, `:drop_vtable`
|
||||
|
||||
## Examples
|
||||
|
||||
# Block ATTACH and DETACH (prevent cross-database reads)
|
||||
:ok = Sqlite3.set_authorizer(conn, [:attach, :detach])
|
||||
|
||||
# Clear the authorizer
|
||||
:ok = Sqlite3.set_authorizer(conn, [])
|
||||
"""
|
||||
@spec set_authorizer(db(), [atom()]) :: :ok | {:error, reason()}
|
||||
def set_authorizer(conn, deny_list) when is_list(deny_list) do
|
||||
Sqlite3NIF.set_authorizer(conn, deny_list)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Send log messages to a process.
|
||||
|
||||
Each time a message is logged in SQLite a message will be sent to the pid provided as the argument.
|
||||
|
||||
The message is of the form: `{:log, rc, message}`, where:
|
||||
|
||||
* `rc` is an integer [result code](https://www.sqlite.org/rescode.html) or an [extended result code](https://www.sqlite.org/rescode.html#extrc)
|
||||
* `message` is a string representing the log message
|
||||
|
||||
See [`SQLITE_CONFIG_LOG`](https://www.sqlite.org/c3ref/c_config_covering_index_scan.html) and
|
||||
["The Error And Warning Log"](https://www.sqlite.org/errlog.html) for more details.
|
||||
|
||||
## Restrictions
|
||||
|
||||
* Only one pid can listen to the log messages at a time.
|
||||
If this function is called multiple times, only the last pid will
|
||||
receive the notifications
|
||||
"""
|
||||
@spec set_log_hook(pid()) :: :ok | {:error, reason()}
|
||||
def set_log_hook(pid) do
|
||||
Sqlite3NIF.set_log_hook(pid)
|
||||
end
|
||||
|
||||
@sqlite_ok 0
|
||||
|
||||
@doc """
|
||||
Binds a text value to a prepared statement.
|
||||
|
||||
iex> {:ok, conn} = Sqlite3.open(":memory:", [:readonly])
|
||||
iex> {:ok, stmt} = Sqlite3.prepare(conn, "SELECT ?")
|
||||
iex> Sqlite3.bind_text(stmt, 1, "Alice")
|
||||
:ok
|
||||
|
||||
"""
|
||||
@spec bind_text(statement, non_neg_integer, String.t()) :: :ok
|
||||
def bind_text(stmt, index, text) do
|
||||
case Sqlite3NIF.bind_text(stmt, index, text) do
|
||||
@sqlite_ok -> :ok
|
||||
rc -> raise Exqlite.Error, message: errmsg(stmt) || errstr(rc)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Binds a blob value to a prepared statement.
|
||||
|
||||
iex> {:ok, conn} = Sqlite3.open(":memory:", [:readonly])
|
||||
iex> {:ok, stmt} = Sqlite3.prepare(conn, "SELECT ?")
|
||||
iex> Sqlite3.bind_blob(stmt, 1, <<0, 0, 0>>)
|
||||
:ok
|
||||
|
||||
"""
|
||||
@spec bind_blob(statement, non_neg_integer, binary) :: :ok
|
||||
def bind_blob(stmt, index, blob) do
|
||||
case Sqlite3NIF.bind_blob(stmt, index, blob) do
|
||||
@sqlite_ok -> :ok
|
||||
rc -> raise Exqlite.Error, message: errmsg(stmt) || errstr(rc)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Binds an integer value to a prepared statement.
|
||||
|
||||
iex> {:ok, conn} = Sqlite3.open(":memory:", [:readonly])
|
||||
iex> {:ok, stmt} = Sqlite3.prepare(conn, "SELECT ?")
|
||||
iex> Sqlite3.bind_integer(stmt, 1, 42)
|
||||
:ok
|
||||
|
||||
"""
|
||||
@spec bind_integer(statement, non_neg_integer, integer) :: :ok
|
||||
def bind_integer(stmt, index, integer) do
|
||||
case Sqlite3NIF.bind_integer(stmt, index, integer) do
|
||||
@sqlite_ok -> :ok
|
||||
rc -> raise Exqlite.Error, message: errmsg(stmt) || errstr(rc)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Binds a float value to a prepared statement.
|
||||
|
||||
iex> {:ok, conn} = Sqlite3.open(":memory:", [:readonly])
|
||||
iex> {:ok, stmt} = Sqlite3.prepare(conn, "SELECT ?")
|
||||
iex> Sqlite3.bind_float(stmt, 1, 3.14)
|
||||
:ok
|
||||
|
||||
"""
|
||||
@spec bind_float(statement, non_neg_integer, float) :: :ok
|
||||
def bind_float(stmt, index, float) do
|
||||
case Sqlite3NIF.bind_float(stmt, index, float) do
|
||||
@sqlite_ok -> :ok
|
||||
rc -> raise Exqlite.Error, message: errmsg(stmt) || errstr(rc)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Binds a null value to a prepared statement.
|
||||
|
||||
iex> {:ok, conn} = Sqlite3.open(":memory:", [:readonly])
|
||||
iex> {:ok, stmt} = Sqlite3.prepare(conn, "SELECT ?")
|
||||
iex> Sqlite3.bind_null(stmt, 1)
|
||||
:ok
|
||||
|
||||
"""
|
||||
@spec bind_null(statement, non_neg_integer) :: :ok
|
||||
def bind_null(stmt, index) do
|
||||
case Sqlite3NIF.bind_null(stmt, index) do
|
||||
@sqlite_ok -> :ok
|
||||
rc -> raise Exqlite.Error, message: errmsg(stmt) || errstr(rc)
|
||||
end
|
||||
end
|
||||
|
||||
defp errmsg(stmt), do: Sqlite3NIF.errmsg(stmt)
|
||||
defp errstr(rc), do: Sqlite3NIF.errstr(rc)
|
||||
|
||||
defp convert(%Date{} = val), do: Date.to_iso8601(val)
|
||||
defp convert(%Time{} = val), do: Time.to_iso8601(val)
|
||||
defp convert(%NaiveDateTime{} = val), do: NaiveDateTime.to_iso8601(val)
|
||||
defp convert(%DateTime{time_zone: "Etc/UTC"} = val), do: NaiveDateTime.to_iso8601(val)
|
||||
|
||||
defp convert(%DateTime{} = datetime) do
|
||||
raise ArgumentError, "#{inspect(datetime)} is not in UTC"
|
||||
end
|
||||
|
||||
defp convert(val) do
|
||||
convert_with_type_extensions(type_extensions(), val)
|
||||
end
|
||||
|
||||
defp convert_with_type_extensions(nil, val), do: val
|
||||
defp convert_with_type_extensions([], val), do: val
|
||||
|
||||
defp convert_with_type_extensions([extension | other_extensions], val) do
|
||||
case extension.convert(val) do
|
||||
nil ->
|
||||
convert_with_type_extensions(other_extensions, val)
|
||||
|
||||
{:ok, converted} ->
|
||||
converted
|
||||
|
||||
{:error, reason} ->
|
||||
raise ArgumentError,
|
||||
"Failed conversion by TypeExtension #{extension}: #{inspect(val)}. Reason: #{inspect(reason)}."
|
||||
end
|
||||
end
|
||||
|
||||
defp type_extensions do
|
||||
Application.get_env(:exqlite, :type_extensions)
|
||||
end
|
||||
end
|
||||
106
phoenix/deps/exqlite/lib/exqlite/sqlite3_nif.ex
Normal file
106
phoenix/deps/exqlite/lib/exqlite/sqlite3_nif.ex
Normal file
@@ -0,0 +1,106 @@
|
||||
defmodule Exqlite.Sqlite3NIF do
|
||||
@moduledoc """
|
||||
This is the module where all of the NIF entry points reside. Calling this directly
|
||||
should be avoided unless you are aware of what you are doing.
|
||||
"""
|
||||
|
||||
@compile {:autoload, false}
|
||||
@on_load {:load_nif, 0}
|
||||
|
||||
@type db() :: reference()
|
||||
@type statement() :: reference()
|
||||
@type reason() :: :atom | String.Chars.t()
|
||||
@type row() :: list()
|
||||
|
||||
def load_nif() do
|
||||
path = :filename.join(:code.priv_dir(:exqlite), ~c"sqlite3_nif")
|
||||
:erlang.load_nif(path, 0)
|
||||
end
|
||||
|
||||
@spec open(String.t(), integer()) :: {:ok, db()} | {:error, reason()}
|
||||
def open(_path, _flags), do: :erlang.nif_error(:not_loaded)
|
||||
|
||||
@spec close(db()) :: :ok | {:error, reason()}
|
||||
def close(_conn), do: :erlang.nif_error(:not_loaded)
|
||||
|
||||
@spec interrupt(db()) :: :ok | {:error, reason()}
|
||||
def interrupt(_conn), do: :erlang.nif_error(:not_loaded)
|
||||
|
||||
@spec execute(db(), String.t()) :: :ok | {:error, reason()}
|
||||
def execute(_conn, _sql), do: :erlang.nif_error(:not_loaded)
|
||||
|
||||
@spec changes(db()) :: {:ok, integer()} | {:error, reason()}
|
||||
def changes(_conn), do: :erlang.nif_error(:not_loaded)
|
||||
|
||||
@spec prepare(db(), String.t()) :: {:ok, statement()} | {:error, reason()}
|
||||
def prepare(_conn, _sql), do: :erlang.nif_error(:not_loaded)
|
||||
|
||||
@spec step(db(), statement()) :: :done | :busy | {:row, row()} | {:error, reason()}
|
||||
def step(_conn, _statement), do: :erlang.nif_error(:not_loaded)
|
||||
|
||||
@spec multi_step(db(), statement(), integer()) ::
|
||||
:busy | {:rows, [row()]} | {:done, [row()]} | {:error, reason()}
|
||||
def multi_step(_conn, _statement, _chunk_size), do: :erlang.nif_error(:not_loaded)
|
||||
|
||||
@spec columns(db(), statement()) :: {:ok, list(binary())} | {:error, reason()}
|
||||
def columns(_conn, _statement), do: :erlang.nif_error(:not_loaded)
|
||||
|
||||
@spec last_insert_rowid(db()) :: {:ok, integer()}
|
||||
def last_insert_rowid(_conn), do: :erlang.nif_error(:not_loaded)
|
||||
|
||||
@spec transaction_status(db()) :: {:ok, :idle | :transaction}
|
||||
def transaction_status(_conn), do: :erlang.nif_error(:not_loaded)
|
||||
|
||||
@spec serialize(db(), String.t()) :: {:ok, binary()} | {:error, reason()}
|
||||
def serialize(_conn, _database), do: :erlang.nif_error(:not_loaded)
|
||||
|
||||
@spec deserialize(db(), String.t(), binary()) :: :ok | {:error, reason()}
|
||||
def deserialize(_conn, _database, _serialized), do: :erlang.nif_error(:not_loaded)
|
||||
|
||||
@spec release(db(), statement()) :: :ok | {:error, reason()}
|
||||
def release(_conn, _statement), do: :erlang.nif_error(:not_loaded)
|
||||
|
||||
@spec enable_load_extension(db(), integer()) :: :ok | {:error, reason()}
|
||||
def enable_load_extension(_conn, _flag), do: :erlang.nif_error(:not_loaded)
|
||||
|
||||
@spec set_update_hook(db(), pid()) :: :ok | {:error, reason()}
|
||||
def set_update_hook(_conn, _pid), do: :erlang.nif_error(:not_loaded)
|
||||
|
||||
@spec set_authorizer(db(), [atom()]) :: :ok | {:error, reason()}
|
||||
def set_authorizer(_conn, _deny_list), do: :erlang.nif_error(:not_loaded)
|
||||
|
||||
@spec set_log_hook(pid()) :: :ok | {:error, reason()}
|
||||
def set_log_hook(_pid), do: :erlang.nif_error(:not_loaded)
|
||||
|
||||
@spec bind_parameter_count(statement) :: integer
|
||||
def bind_parameter_count(_stmt), do: :erlang.nif_error(:not_loaded)
|
||||
|
||||
@spec bind_parameter_index(statement, String.t()) :: integer
|
||||
def bind_parameter_index(_stmt, _name), do: :erlang.nif_error(:not_loaded)
|
||||
|
||||
@spec bind_text(statement, non_neg_integer, String.t()) :: integer()
|
||||
def bind_text(_stmt, _index, _text), do: :erlang.nif_error(:not_loaded)
|
||||
|
||||
@spec bind_blob(statement, non_neg_integer, binary) :: integer()
|
||||
def bind_blob(_stmt, _index, _blob), do: :erlang.nif_error(:not_loaded)
|
||||
|
||||
@spec bind_integer(statement, non_neg_integer, integer) :: integer()
|
||||
def bind_integer(_stmt, _index, _integer), do: :erlang.nif_error(:not_loaded)
|
||||
|
||||
@spec bind_float(statement, non_neg_integer, float) :: integer()
|
||||
def bind_float(_stmt, _index, _float), do: :erlang.nif_error(:not_loaded)
|
||||
|
||||
@spec bind_null(statement, non_neg_integer) :: integer()
|
||||
def bind_null(_stmt, _index), do: :erlang.nif_error(:not_loaded)
|
||||
|
||||
@spec reset(statement) :: :ok
|
||||
def reset(_stmt), do: :erlang.nif_error(:not_loaded)
|
||||
|
||||
@spec errmsg(db | statement) :: String.t() | nil
|
||||
def errmsg(_db_or_stmt), do: :erlang.nif_error(:not_loaded)
|
||||
|
||||
@spec errstr(integer) :: String.t()
|
||||
def errstr(_rc), do: :erlang.nif_error(:not_loaded)
|
||||
|
||||
# add statement inspection tooling https://sqlite.org/c3ref/expanded_sql.html
|
||||
end
|
||||
42
phoenix/deps/exqlite/lib/exqlite/stream.ex
Normal file
42
phoenix/deps/exqlite/lib/exqlite/stream.ex
Normal file
@@ -0,0 +1,42 @@
|
||||
defmodule Exqlite.Stream do
|
||||
@moduledoc false
|
||||
defstruct [:conn, :query, :params, :options]
|
||||
@type t :: %Exqlite.Stream{}
|
||||
|
||||
defimpl Enumerable do
|
||||
def reduce(%Exqlite.Stream{query: %Exqlite.Query{} = query} = stream, acc, fun) do
|
||||
# Possibly need to pass a chunk size option along so that we can let
|
||||
# the NIF chunk it.
|
||||
%Exqlite.Stream{conn: conn, params: params, options: opts} = stream
|
||||
|
||||
stream = %DBConnection.Stream{
|
||||
conn: conn,
|
||||
query: query,
|
||||
params: params,
|
||||
opts: opts
|
||||
}
|
||||
|
||||
DBConnection.reduce(stream, acc, fun)
|
||||
end
|
||||
|
||||
def reduce(%Exqlite.Stream{query: statement} = stream, acc, fun) do
|
||||
%Exqlite.Stream{conn: conn, params: params, options: opts} = stream
|
||||
query = %Exqlite.Query{name: "", statement: statement}
|
||||
|
||||
stream = %DBConnection.PrepareStream{
|
||||
conn: conn,
|
||||
query: query,
|
||||
params: params,
|
||||
opts: opts
|
||||
}
|
||||
|
||||
DBConnection.reduce(stream, acc, fun)
|
||||
end
|
||||
|
||||
def member?(_, _), do: {:error, __MODULE__}
|
||||
|
||||
def count(_), do: {:error, __MODULE__}
|
||||
|
||||
def slice(_), do: {:error, __MODULE__}
|
||||
end
|
||||
end
|
||||
14
phoenix/deps/exqlite/lib/exqlite/type_extension.ex
Normal file
14
phoenix/deps/exqlite/lib/exqlite/type_extension.ex
Normal file
@@ -0,0 +1,14 @@
|
||||
defmodule Exqlite.TypeExtension do
|
||||
@moduledoc """
|
||||
A behaviour that defines the API for extensions providing custom data loaders and dumpers
|
||||
for Ecto schemas.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Takes a value and convers it to data suitable for storage in the database.
|
||||
|
||||
Returns a tagged :ok/:error tuple. If the value is not convertable by this
|
||||
extension, returns nil.
|
||||
"""
|
||||
@callback convert(value :: term) :: {:ok, term} | {:error, reason :: term} | nil
|
||||
end
|
||||
Reference in New Issue
Block a user