Skip to content

Add Exqlite.TypeExtensions to allow more types to be stored in the database #333

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jun 20, 2025

Conversation

aseigo
Copy link
Contributor

@aseigo aseigo commented Jun 20, 2025

This PR introduces a runtime extension to convert data types into something that can be serialized into the database.

See elixir-sqlite/ecto_sqlite3#167 and elixir-sqlite/ecto_sqlite3#166

My primary goal is to be able to use Spatialite from Elixir. With this PR and the one against ecto_sqlite3, it is 95% of the way there. With these two PRs applied, one can now do things like:

defmodule Location do
  use Ecto.Schema

  schema "locations" do
    field(:name, :string)
    field(:geom, GeoSQL.Geometry)
  end
end

MyApp.Repo.all(from(location in Location, select: MM2.distance(location.geom, ^geometry)))

.. and it just works, regardless of whether MyApp.Repo is point at a PostgreSQL database with PostGIS enabled, or an SQLite3 database with Spatialite loaded.

The one remaining annoyance is getting structs back out of the database without going through ecto (which does work with the ecto_sqlite3 PR!). So, I still have to figure out a nice way for the user to get back Geo structs from raw queries such as from(location in Location, limit: 1, select: MM2.transform(location.geom, 3452)), but otherwise everything Just Works transparently with this change.

Runtime extensions to convert data types into something that can be
serialized into the database.
@doc """
Takes a value and convers it to data suitable for storage in the database.
"""
@callback convert(value :: term) :: term
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this common for these to simply be term that only return nil when it can't be converted? Or is a {:ok, term()} | :error more useful?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It could do a tagged tuple, yes. See 7b5d2ee

This also has the nice effect of making it more like the Ecto usage.

Comment on lines 484 to 488
defp convert(val) do
convert_with_type_extensions(type_extensions(), val)
end

defp convert_with_type_extensions([], val), do: val
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm going back and fourth on allowing type_extensions to be nil.

On one hand, letting it default to nil, will make the pattern match be faster than matching with an empty list.

defp convert_with_type_extensions(nil, val), do: val
defp convert_with_type_extensions([], val), do: val

And then changing Application.get_env(:exqlite, :type_extensions, []) to be Application.get_env(:exqlite, :type_extensions).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went back and forth as well .. I'm not sure the speed gain is really going to be noticeable, but it's easy enough to do.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See 704c9bd

Comment on lines 9 to 13

Returns a tagged :ok/:error tuple. If the value is not convertable by this
extension, returns nil.
"""
@callback convert(value :: term) :: term
@callback convert(value :: term) :: {:ok, term} | {:error, reason :: term} | nil
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know it's just an atom, but perhaps :unsupported is more clear than nil?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason I didn't use a more "expressive" atom is one ends up having to remember all the various names for "this is not something that is happening" return values. nil is convenient in that it is the least surprising (at least for me) and widely used to mean "nothing here".

If you prefer, I can change it to :unsupported, but as a library user I lean towards "common and therefore predictable" in these cases.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No that's a fine justification.

Copy link
Member

@warmwaffles warmwaffles left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a great start. I think we can make the type extensions be configurable per ecto adapter. The idea that I have is that we store the configuration with the state of the connection later and allow each ecto repo to optionally add a type extension it wants to use so that others aren't "poisoned" by it.

However, I'm not gonna implement that right now. We'll let this one play out and adjust later.

@warmwaffles warmwaffles merged commit 6fe9100 into elixir-sqlite:main Jun 20, 2025
12 checks passed
@aseigo
Copy link
Contributor Author

aseigo commented Jun 20, 2025

This is a great start. I think we can make the type extensions be configurable per ecto adapter. The idea that I have is that we store the configuration with the state of the connection later and allow each ecto repo to optionally add a type extension it wants to use so that others aren't "poisoned" by it.

That sounds great. It's a bit more complicated internally (obviously), but would be a nice improvement. Cheers!

@warmwaffles
Copy link
Member

I'll get this cut out in the next version soon. I need to retire an old version of this adapter and update the previous release.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants