Datatype mismatch error using SQLite

First of all, I am a total noob with Elixir. So this may be an obvious error on my part.

I am using SQLite as the database for a test project so that I can learn Elixir. I have an accounts table, and have created a test case to insert data to it. Here is my code:

defmodule Corebank.Accounts.Account do
  @moduledoc """
  Represents an account in the banking system.
  This module defines the Ecto schema and related functions for account management.
  """

  use Ecto.Schema
  import Ecto.Changeset

  @primary_key {:id, :binary_id, autogenerate: true}
  schema "accounts" do
    field :name, :string
    field :account_type, :string
    field :account_number, :string
    field :currency, :string
    field :balance, :decimal, default: 0.0

    timestamps()
  end

def changeset(account, attrs) do
    account
    |> cast(attrs, [:name, :account_number, :currency, :balance, :account_type])
    |> validate_required([:name, :account_number, :currency, :account_type])
    |> validate_length(:account_number, min: 6, max: 12)
    |> validate_number(:balance, greater_than_or_equal_to: 0)
    |> unique_constraint(:account_number)
  end

This is my migration that I used to create the table:

defmodule Corebank.Repo.Migrations.CreateAccountsTable do
  use Ecto.Migration

  def change do
    create table(:accounts) do
      add :name, :string, null: false
      add :account_type, :string, null: false
      add :account_number, :string, null: false
      add :currency, :string, null: false
      add :balance, :decimal, null: false, default: 0.0

      #timestamps() # Adds `inserted_at` and `updated_at` fields
    end

    create unique_index(:accounts, [:account_number]) # Ensures unique account numbers
  end
end

This is my AccountManager module that creates a GenServer to do the account management:

defmodule Corebank.Accounts.AccountManageange do
  use GenServer

  alias Corebank.Accounts.Account
  alias Corebank.Repo

  ## Client API

  def start_link(_) do
    GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
  end

  def create_account(attrs) do
    IO.inspect(attrs)
    GenServer.call(__MODULE__, {:create_account, attrs})
  end

  def get_balance(account_id) do
    GenServer.call(__MODULE__, {:get_balance, account_id})
  end

  def deposit(account_id, amount) do
    GenServer.call(__MODULE__, {:deposit, account_id, amount})
  end

  def withdraw(account_id, amount) do
    GenServer.call(__MODULE__, {:withdraw, account_id, amount})
  end

  def transfer(from_account_id, to_account_id, amount) do
    GenServer.call(__MODULE__, {:transfer, from_account_id, to_account_id, amount})
  end

  ## Server Callbacks

  def init(state) do
    {:ok, state}
  end

def handle_call({:create_account, attrs}, _from, state) do
    changeset = Account.changeset(%Account{}, attrs)
    IO.inspect(changeset)

    case Repo.insert(changeset) do
      {:ok, account} ->
        {:reply, {:ok, account}, state}
      {:error, changeset} ->
        IO.warn("Account insert error")
        {:reply, {:error, changeset}, state}
    end
  end

  def handle_call({:get_balance, account_id}, _from, state) do
    case Repo.get(Account, account_id) do
      nil ->
        {:reply, {:error, :account_not_found}, state}
      account ->
        {:reply, {:ok, account.balance}, state}
    end
  end

  def handle_call({:deposit, account_id, amount}, _from, state) do
    case Repo.get(Account, account_id) do
      nil ->
        {:reply, {:error, :account_not_found}, state}

      account ->
        new_balance = Decimal.add(account.balance, Decimal.new(amount))
        changeset = Ecto.Changeset.change(account, balance: new_balance)

        case Repo.update(changeset) do
          {:ok, updated_account} ->
            send_notification(:deposit, updated_account, amount)
            {:reply, {:ok, updated_account}, state}

          {:error, _} ->
            {:reply, {:error, :update_failed}, state}
        end
    end
  end

  def handle_call({:withdraw, account_id, amount}, _from, state) do
    case Repo.get(Account, account_id) do
      nil ->
        {:reply, {:error, :account_not_found}, state}

      account when account.balance < amount ->
        {:reply, {:error, :insufficient_funds}, state}

      account ->
        new_balance = Decimal.sub(account.balance, Decimal.new(amount))
        changeset = Ecto.Changeset.change(account, balance: new_balance)

        case Repo.update(changeset) do
          {:ok, updated_account} ->
            send_notification(:withdrawal, updated_account, amount)
            {:reply, {:ok, updated_account}, state}

          {:error, _} ->
            {:reply, {:error, :update_failed}, state}
        end
    end
  end

  def handle_call({:transfer, from_account_id, to_account_id, amount}, _from, state) do
    Repo.transaction(fn ->
      case withdraw(from_account_id, amount) do
        {:ok, _from_account} ->
          case deposit(to_account_id, amount) do
            {:ok, _to_account} ->
              {:ok, :transfer_completed}

            {:error, reason} ->
              Repo.rollback(reason)
          end

        {:error, reason} ->
          Repo.rollback(reason)
      end
    end)
    |> case do
      {:ok, :transfer_completed} ->
        {:reply, {:ok, :transfer_successful}, state}

      {:error, reason} ->
        {:reply, {:error, reason}, state}
    end
  end

  ## Helper Functions

  defp send_notification(:deposit, account, amount) do
    # Emit deposit notification to PubSub or an external service.
    IO.puts("Deposit of #{amount} to account #{account.id}")
  end

  defp send_notification(:withdrawal, account, amount) do
    # Emit withdrawal notification to PubSub or an external service.
    IO.puts("Withdrawal of #{amount} from account #{account.id}")
  end
end

and finally my test:

defmodule Corebank.Accounts.AccountManagerTest do
  use Corebank.DataCase

  alias Corebank.Accounts.AccountManager

  test "creates an account and retrieves its balance" do
    attrs = %{
      name: "Test Savings",
      account_type: "SAVINGS",
      account_number: "1234567",
      currency: "USD",
      balance: Decimal.new("1000.0"),
    }

    {:ok, _pid} = AccountManager.start_link(nil)

    result = AccountManager.create_account(attrs)
    IO.inspect(result)
    case result do
      {:ok, account} ->
        IO.inspect(result)
        {:ok, balance} = AccountManager.get_balance(account.id)
        assert balance == Decimal.new("1000.00")
      {:error, changeset} ->
        IO.puts("Create account error")
        IO.inspect(changeset)
        flunk "Failed to create account: #{inspect(changeset)}"
    end
  end
end

The issue I am having is that when the test code attempts to insert into the accounts table, an exception is raised:

Running ExUnit with seed: 191072, max_cases: 16

....%{
  name: "Test Savings",
  balance: Decimal.new("1000.0"),
  currency: "USD",
  account_type: "SAVINGS",
  account_number: "1234567"
}
#Ecto.Changeset<
  action: nil,
  changes: %{
    name: "Test Savings",
    balance: Decimal.new("1000.0"),
    currency: "USD",
    account_type: "SAVINGS",
    account_number: "1234567"
  },
  errors: [],
  data: #Corebank.Accounts.Account<>,
  valid?: true,
  ...
>
16:27:33.354 [error] GenServer Corebank.Accounts.AccountManager terminating
** (Exqlite.Error) table accounts has no column named inserted_at
INSERT INTO "accounts" ("name","balance","currency","account_type","account_number","inserted_at","updated_at","id") VALUES (?1,?2,?3,?4,?5,?6,?7,?8)
    (ecto_sql 3.12.1) lib/ecto/adapters/sql.ex:1096: Ecto.Adapters.SQL.raise_sql_call_error/1
    (ecto 3.12.5) lib/ecto/repo/schema.ex:837: Ecto.Repo.Schema.apply/4
    (ecto 3.12.5) lib/ecto/repo/schema.ex:416: anonymous fn/15 in Ecto.Repo.Schema.do_insert/4
    (corebank 0.1.0) lib/corebank/Accounts/AccountManager.ex:44: Corebank.Accounts.AccountManager.handle_call/3
    (stdlib 6.1.2) gen_server.erl:2381: :gen_server.try_handle_call/4
    (stdlib 6.1.2) gen_server.erl:2410: :gen_server.handle_msg/6
    (stdlib 6.1.2) proc_lib.erl:329: :proc_lib.init_p_do_apply/3
Last message (from #PID<0.346.0>): {:create_account, %{name: "Test Savings", balance: Decimal.new("1000.0"), currency: "USD", account_type: "SAVINGS", account_number: "1234567"}}


  1) test creates an account and retrieves its balance (Corebank.Accounts.AccountManagerTest)
     test/corebank/accounts/account_manager_test.exs:6
     ** (EXIT from #PID<0.346.0>) an exception was raised:
         ** (Exqlite.Error) table accounts has no column named inserted_at
     INSERT INTO "accounts" ("name","balance","currency","account_type","account_number","inserted_at","updated_at","id") VALUES (?1,?2,?3,?4,?5,?6,?7,?8)      
             (ecto_sql 3.12.1) lib/ecto/adapters/sql.ex:1096: Ecto.Adapters.SQL.raise_sql_call_error/1
             (ecto 3.12.5) lib/ecto/repo/schema.ex:837: Ecto.Repo.Schema.apply/4
             (ecto 3.12.5) lib/ecto/repo/schema.ex:416: anonymous fn/15 in Ecto.Repo.Schema.do_insert/4
             (corebank 0.1.0) lib/corebank/Accounts/AccountManager.ex:44: Corebank.Accounts.AccountManager.handle_call/3
             (stdlib 6.1.2) gen_server.erl:2381: :gen_server.try_handle_call/4
             (stdlib 6.1.2) gen_server.erl:2410: :gen_server.handle_msg/6
             (stdlib 6.1.2) proc_lib.erl:329: :proc_lib.init_p_do_apply/3

.
Finished in 0.2 seconds (0.1s async, 0.1s sync)
6 tests, 1 failure

For the life of me, I can’t figure out what the data type mismatch might be. I have been able to manually insert into the accounts table, using the exact same values, using the sqlite3 cli. But I can’t see what the issue is with my code that would prevent the insert from succeeding.

Any help is much appreciated.

thanks

The migration in your post has timestamps() commented out, which would produce exactly the symptom you’re observing (no inserted_at column).

The final part of your post mentions a “data type mismatch”, which isn’t otherwise mentioned. Maybe a different error? My first suspect for something like that would be the :decimal column, which doesn’t have a corresponding SQLite3 data type, but I’m not sure what Ecto will do in that situation.

The commented out timestamps() call was a cut/paste error on my part. I was playing around with things and thought maybe the inserted_at was causing the datatype mismatch so I removed it from the accounts table and tried inserting without it.

Your mention of the balance column being :decimal got me thinking that maybe that could be the problem. I changed the column to be integer but am still getting the ‘datatype mismatch’ error.

Could you post the original error you get when you don’t comment out timestamps()?

Looking at the code of ecto_sqlite3, it looks like Decimal would be encoded as string/text, so that could be the source of the datatype mismatch?

1 Like

This is the error I get when I include timestamps()

 Running ExUnit with seed: 967028, max_cases: 16

....%{
  name: "TestSavings",
  balance: 1000,
  currency: "USD",
  account_type: "SAVINGS",
  account_number: "1234567"
}
#Ecto.Changeset<
  action: nil,
  changes: %{
    name: "TestSavings",
    balance: 1000,
    currency: "USD",
    account_type: "SAVINGS",
    account_number: "1234567"
  },
  errors: [],
  data: #Corebank.Accounts.Account<>,
  valid?: true,
  ...
>
11:35:13.124 [error] GenServer Corebank.Accounts.AccountManager terminating
** (Exqlite.Error) datatype mismatch
INSERT INTO "accounts" ("name","balance","currency","account_type","account_number","id") VALUES (?1,?2,?3,?4,?5,?6)
    (ecto_sql 3.12.1) lib/ecto/adapters/sql.ex:1096: Ecto.Adapters.SQL.raise_sql_call_error/1
    (ecto 3.12.5) lib/ecto/repo/schema.ex:837: Ecto.Repo.Schema.apply/4
    (ecto 3.12.5) lib/ecto/repo/schema.ex:416: anonymous fn/15 in Ecto.Repo.Schema.do_insert/4
    (corebank 0.1.0) lib/corebank/Accounts/AccountManager.ex:44: Corebank.Accounts.AccountManager.handle_call/3
    (stdlib 6.1.2) gen_server.erl:2381: :gen_server.try_handle_call/4
    (stdlib 6.1.2) gen_server.erl:2410: :gen_server.handle_msg/6
    (stdlib 6.1.2) proc_lib.erl:329: :proc_lib.init_p_do_apply/3
Last message (from #PID<0.346.0>): {:create_account, %{name: "TestSavings", balance: 1000, currency: "USD", account_type: "SAVINGS", account_number: "1234567"}}


  1) test creates an account and retrieves its balance (Corebank.Accounts.AccountManagerTest)
     test/corebank/accounts/account_manager_test.exs:6
     ** (EXIT from #PID<0.346.0>) an exception was raised:
         ** (Exqlite.Error) datatype mismatch
     INSERT INTO "accounts" ("name","balance","currency","account_type","account_number","id") VALUES (?1,?2,?3,?4,?5,?6)
             (ecto_sql 3.12.1) lib/ecto/adapters/sql.ex:1096: Ecto.Adapters.SQL.raise_sql_call_error/1
             (ecto 3.12.5) lib/ecto/repo/schema.ex:837: Ecto.Repo.Schema.apply/4
             (ecto 3.12.5) lib/ecto/repo/schema.ex:416: anonymous fn/15 in Ecto.Repo.Schema.do_insert/4
             (corebank 0.1.0) lib/corebank/Accounts/AccountManager.ex:44: Corebank.Accounts.AccountManager.handle_call/3
             (stdlib 6.1.2) gen_server.erl:2381: :gen_server.try_handle_call/4
             (stdlib 6.1.2) gen_server.erl:2410: :gen_server.handle_msg/6
             (stdlib 6.1.2) proc_lib.erl:329: :proc_lib.init_p_do_apply/3

.
Finished in 0.2 seconds (0.1s async, 0.1s sync)
6 tests, 1 failure

I have some telemetry in there so that I can see what the contents of changeset are.

Here is my accounts table configuration:

sqlite> .schema accounts 
CREATE TABLE IF NOT EXISTS "accounts" ("id" INTEGER PRIMARY KEY AUTOINCREMENT, "name" TEXT NOT NULL, "account_type" TEXT NOT NULL, "account_number" TEXT NOT NULL, "currency" TEXT NOT NULL, "balance" INTEGER DEFAULT 0 NOT NULL, "inserted_at" TEXT NOT NULL, "updated_at" TEXT NOT NULL);
CREATE UNIQUE INDEX "accounts_account_number_index" ON "accounts" ("account_number");
sqlite> 

I have since changed the data type of the balance column to be integer, but I am still getting the ‘datatype mismatch’ error.

You have inserted_at and created_at set as NOT NULL in your migrations, but they are clearly missing from your insert query, have you forgot to add/uncomment timestamps() in your ecto schema?

schema "accounts" do
  ..
  timestamps()
end

I think I have this right?

defmodule Corebank.Accounts.Account do
  @moduledoc """
  Represents an account in the banking system.
  This module defines the Ecto schema and related functions for account management.
  """

  use Ecto.Schema
  import Ecto.Changeset

  @primary_key {:id, :binary_id, autogenerate: true}
  schema "accounts" do
    field :name, :string
    field :account_type, :string
    field :account_number, :string
    field :currency, :string
    field :balance, :integer, default: 0

    timestamps()
  end

Very strange, can you please confirm the version of exqlite you are currently using? It should be located in mix.lock.

"ecto_sql": {:hex, :ecto_sql, "3.12.1", "c0d0d60e85d9ff4631f12bafa454bc392ce8b9ec83531a412c12a0d415a3a4d0", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 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", "aff5b958a899762c5f09028c847569f7dfb9cc9d63bdb8133bff8a5546de6bf5"},
  "ecto_sqlite3": {:hex, :ecto_sqlite3, "0.17.5", "fbee5c17ff6afd8e9ded519b0abb363926c65d30b27577232bb066b2a79957b8", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.12", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:exqlite, "~> 0.22", [hex: :exqlite, repo: "hexpm", optional: false]}], "hexpm", "3b54734d998cbd032ac59403c36acf4e019670e8b6ceef9c6c33d8986c4e9704"},

"exqlite": {:hex, :exqlite, "0.27.1", "73fc0b3dc3b058a77a2b3771f82a6af2ddcf370b069906968a34083d2ffd2884", [: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.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "79ef5756451cfb022e8013e1ed00d0f8f7d1333c19502c394dc16b15cfb4e9b4"},
 
1 Like

:wave:

id in the table is INTEGER, and on the schema it’s binary_id which is BLOB or TEXT. SQLite doesn’t do any type checking by default, but here it simply might be failing to pack an arbitraty binary into an INTEGER primary key. Also note that it might make sense to double-check that Exqlite and sqlite3 CLI (the one showing .schema result) are using the same database file.

1 Like

Yep, I’ve just replicated the setup and I can confirm that that was the problem, completely makes sense as it can be seen clearly after the migration that the default id field is integer.

I think the strange thing is that the query from OP’s post is inconsistent, I guess a lot of changes were done to debug. If everything was configured right, the query for the test should look like this (without having set the @primary_key option):

INSERT INTO “accounts” (“name”,“balance”,“currency”,“account_type”,“account_number”,“inserted_at”,“updated_at”) VALUES (?1,?2,?3,?4,?5,?6,?7) RETURNING “id” [“Test Savings”, Decimal.new(“1000.0”), “USD”, “SAVINGS”, “1234567”, ~N[2025-01-07 20:20:51], ~N[2025-01-07 20:20:51]]

While the one with the custom id set on ecto schema will be:

INSERT INTO “accounts” (“name”,“balance”,“currency”,“account_type”,“account_number”,“inserted_at”,“updated_at”,“id”) VALUES (?1,?2,?3,?4,?5,?6,?7,?8) [“Test Savings”, Decimal.new(“1000”), “USD”, “SAVINGS”, “1234567”, ~N[2025-01-07 20:19:24], ~N[2025-01-07 20:19:24], “a2391f39-431a-48a6-9519-0929c67a437c”]

1 Like

My config/test.exs setup has the database path pointing to the same file that I am using in the sqlite3 cli command:

config :corebank, Corebank.Repo,
  log: :debug,
  database: Path.expand("../corebank_test.db", __DIR__),
  pool_size: 5,
  pool: Ecto.Adapters.SQL.Sandbox

So I think I have that configured correctly.

Are you saying that the datatype for id should be something else?

@primary_key {:id, :binary_id, autogenerate: true}
  schema "accounts" do
    field :name, :string
    field :account_type, :string
    field :account_number, :string
    field :currency, :string
    field :balance, :integer, default: 0

    timestamps()
  end

Changing the :id type from :binary_id to :id in the account schema seems to have worked:

 @primary_key {:id, :id, autogenerate: true}
  schema "accounts" do
    field :name, :string
    field :account_type, :string
    field :account_number, :string
    field :currency, :string
    field :balance, :integer, default: 0

    timestamps()
  end

my test is now passing:

C:\Users\mike\wb\corebank>mix test
Running ExUnit with seed: 85059, max_cases: 16

....%{
  name: "TestSavings",
  balance: 1000,
  currency: "USD",
  account_type: "SAVINGS",
  account_number: "1234567"
}
#Ecto.Changeset<
  action: nil,
  changes: %{
    name: "TestSavings",
    balance: 1000,
    currency: "USD",
    account_type: "SAVINGS",
    account_number: "1234567"
  },
  errors: [],
  data: #Corebank.Accounts.Account<>,
  valid?: true,
  ...
>
{:ok,
 %Corebank.Accounts.Account{
   __meta__: #Ecto.Schema.Metadata<:loaded, "accounts">,
   id: 1,
   name: "TestSavings",
   account_type: "SAVINGS",
   account_number: "1234567",
   currency: "USD",
   balance: 1000,
   inserted_at: ~N[2025-01-07 22:01:57],
   updated_at: ~N[2025-01-07 22:01:57]
 }}
%Corebank.Accounts.Account{
  __meta__: #Ecto.Schema.Metadata<:loaded, "accounts">,
  id: 1,
  name: "TestSavings",
  account_type: "SAVINGS",
  account_number: "1234567",
  currency: "USD",
  balance: 1000,
  inserted_at: ~N[2025-01-07 22:01:57],
  updated_at: ~N[2025-01-07 22:01:57]
}
..
Finished in 0.2 seconds (0.1s async, 0.1s sync)
6 tests, 0 failures

thanks to all that helped me with this.

1 Like