defmodule Fsdb do @moduledoc """ Fsdb module. ```elixir #run with: mix sample_direct db = Path.expand("_build/dev/.fsdb") :ok = Fsdb.init(db) #drop to start over :ok = Fsdb.drop(db, "table1") :ok = Fsdb.create(db, "table1") #insert generates and returns an autoincrementing id {:ok, 1} = Fsdb.insert(db, "table1", "row1") {:ok, "row1"} = Fsdb.fetch(db, "table1", 1) #operation on nonexisting ids return not found :nf = Fsdb.delete(db, "table1", 5) :nf = Fsdb.update(db, "table1", 5, "row5+") #save overrides and updates id generator :ok = Fsdb.save(db, "table1", 5, "row5") :ok = Fsdb.save(db, "table1", 3, "row3") #continue +1 of largest id used/generated before {:ok, 6} = Fsdb.insert(db, "table1", "row6") #operations that get back previous value {:ok, "row5"} = Fsdb.update(db, "table1", 5, "row5+") {:ok, "row5+"} = Fsdb.save(db, "table1", 5, "row5++") {:ok, "row5++"} = Fsdb.delete(db, "table1", 5) #tuples may not be id ordered [{1, "row1"}, {3, "row3"}, {6, "row6"}] = Fsdb.list(db, "table1") ``` """ @rowre ~r/row\.[0-9]+/ @rowid "row.id" def init(path) do File.mkdir_p!(path) end def create(path, table) do folder = Path.join(path, table) if !File.exists?(folder) do File.mkdir_p!(folder) file = Path.join(folder, @rowid) :ok = dump(file, 0) end :ok end def drop(path, table) do folder = Path.join(path, table) File.rm_rf!(folder) :ok #rm returns list of deleted files end def list(path, table) do folder = Path.join(path, table) for file <- File.ls!(folder), Regex.match?(@rowre, file) do parse(Path.join(folder, file)) end end defp parse(file) do txt = File.read!(file) |> :erlang.binary_to_list {:ok, tokens, _} = :erl_scan.string(txt) {:ok, row} = :erl_parse.parse_term(tokens) row end def insert(path, table, row) do folder = Path.join(path, table) id = nextid(folder) file = Path.join(folder, rowid(id)) :ok = dump(file, {id, row}) {:ok, id} end def save(path, table, id, row) do folder = Path.join(path, table) file = Path.join(folder, rowid(id)) if File.exists?(file) do {^id, crow} = parse(file) :ok = dump(file, {id, row}) maxid(folder, id) {:ok, crow} else :ok = dump(file, {id, row}) maxid(folder, id) :ok end end defp nextid(folder) do file = Path.join(folder, @rowid) id = parse(file) + 1 :ok = dump(file, id) id end defp maxid(folder, id) do file = Path.join(folder, @rowid) cid = parse(file) mid = if id > cid, do: id, else: cid :ok = dump(file, mid) mid end defp dump(file, row) do File.write! file, :io_lib.format("~p.", [row]) end def fetch(path, table, id) do folder = Path.join(path, table) file = Path.join(folder, rowid(id)) if File.exists?(file) do {^id, row} = parse(file) {:ok, row} else :nf end end defp rowid(id) do fid = Integer.to_string(id) |> String.pad_leading(6, "0") "row.#{fid}" end def update(path, table, id, row) do folder = Path.join(path, table) file = Path.join(folder, rowid(id)) if File.exists?(file) do {^id, crow} = parse(file) :ok = dump(file, {id, row}) {:ok, crow} else :nf end end def delete(path, table, id) do folder = Path.join(path, table) file = Path.join(folder, rowid(id)) if File.exists?(file) do {^id, row} = parse(file) File.rm!(file) {:ok, row} else :nf end end end