Current section
Files
Jump to
Current section
Files
src/xqldb_string_table2.erl
%% -------------------------------------------------------------------
%%
%% xqerl - XQuery processor
%%
%% Copyright (c) 2018-2020 Zachary N. Dean All Rights Reserved.
%%
%% This file is provided to you under the Apache License,
%% Version 2.0 (the "License"); you may not use this file
%% except in compliance with the License. You may obtain
%% a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing,
%% software distributed under the License is distributed on an
%% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
%% KIND, either express or implied. See the License for the
%% specific language governing permissions and limitations
%% under the License.
%%
%% -------------------------------------------------------------------
%% A string table is a lookup for each distinct string and a 32 bit integer.
%% The ID for the string is made up of a hash followed by a pigeonhole
%% offset value in case of collision.
%% When a collision occurs, the offset is increased and appended to the hash.
%% When there is no collision, 0 is appended.
%% Looking up a value by string means getting its hash value, appending
%% min and max (all 1 bits) tails, and then selecting each possible ID until it
%% is found. This means that there is a maximum nr. of collisions possible,
%% so the table should not get too large. (95% max size?)
%% Linear Probing
%% Every value is written to file. This can slow down insert operations.
%% Note: For all intents and purposes, a 'string' is a UTF8 binary value.
-module(xqldb_string_table2).
-behaviour(gen_server).
-include("xqerl_db.hrl").
-export([
init/1,
handle_call/3,
handle_cast/2,
handle_info/2,
terminate/2,
code_change/3
]).
%% ====================================================================
%% API functions
%% ====================================================================
-export([
start_link/3,
stop/1,
insert/2,
lookup/2
]).
-type state() :: #{indx_file => file:io_device()}.
%% Open or create a new string table server.
-spec start_link(
open | new,
DBDirectory :: file:name_all(),
TableName :: string()
) -> {ok, Pid :: pid()}.
start_link(new, DBDirectory, TableName) ->
gen_server:start_link(?MODULE, [new, DBDirectory, TableName], []);
start_link(open, DBDirectory, TableName) ->
gen_server:start_link(?MODULE, [open, DBDirectory, TableName], []).
%% Shutdown this server.
-spec stop(db()) -> ok | {ok, _}.
stop(#{texts := Pid}) when is_pid(Pid) ->
gen_server:stop(Pid).
%% Returns the binary value for the given ID, or error if no such ID exists.
-spec lookup(db(), Id :: binary()) -> Value :: binary() | error.
lookup(#{texts := Pid}, Id) when is_pid(Pid), byte_size(Id) =< 63 ->
Id;
lookup(#{texts := Pid}, Id) when is_pid(Pid), byte_size(Id) =:= 64 ->
gen_server:call(Pid, {lookup, Id});
lookup(_, _) ->
error.
%% Returns the ID for a binary value in the table.
%% If no such value exists, creates a new value in the table and returns its ID.
-spec insert(db(), Value :: binary()) -> Id :: binary().
insert(#{texts := Pid}, Value) when is_pid(Pid), byte_size(Value) =< 63 ->
Value;
insert(#{texts := Pid}, Value) when is_pid(Pid) ->
gen_server:call(Pid, {insert, Value}).
%% ====================================================================
%% Internal functions
%% ====================================================================
%% Creates a new string table in the DBDirectory in the DB DBName,
%% with the name TableName. Deletes any existing DB file with the same name.
-spec new(
DBDirectory :: file:name_all(),
TableName :: string()
) -> state().
new(DBDirectory, TableName) when is_binary(DBDirectory) ->
new(binary_to_list(DBDirectory), TableName);
new(DBDirectory, TableName) ->
init_state(DBDirectory, TableName).
%% Opens an existing string table in the DBDirectory in the DB DBName,
%% with the name TableName.
-spec open(
DBDirectory :: file:name_all(),
TableName :: string()
) -> state().
open(DBDirectory, TableName) when is_binary(DBDirectory) ->
open(binary_to_list(DBDirectory), TableName);
open(DBDirectory, TableName) ->
init_state(DBDirectory, TableName).
init_state(DBDirectory, TableName) ->
AbsTabName = filename:join(DBDirectory, TableName),
ok = filelib:ensure_dir(AbsTabName),
IndxName = AbsTabName ++ ".heap",
{ok, IndxFile} = dets:open_file(IndxName, []),
#{indx_file => IndxFile}.
lookup_string_from_id(Id, IndxFile, ReplyTo) ->
Reply =
case dets:lookup(IndxFile, Id) of
[] ->
error;
[{_, String}] ->
String
end,
gen_server:reply(ReplyTo, Reply).
upsert_string_value(String, #{indx_file := IndxFile} = State) ->
Hash = hash(String),
case dets:lookup(IndxFile, Hash) of
[] ->
dets:insert(IndxFile, {Hash, String}),
{Hash, State};
[_] ->
{Hash, State}
end.
%% ====================================================================
%% Callbacks
%% ====================================================================
init([new, DBDirectory, TableName]) ->
State = new(DBDirectory, TableName),
{ok, State};
init([open, DBDirectory, TableName]) ->
State = open(DBDirectory, TableName),
{ok, State}.
terminate(_Reason, #{indx_file := IndxFile}) ->
dets:close(IndxFile).
handle_cast(_Request, State) -> {noreply, State}.
handle_call({insert, Value}, _From, State) ->
{Reply, State1} = upsert_string_value(Value, State),
{reply, Reply, State1};
handle_call({lookup, Id}, From, #{indx_file := IndxFile} = State) ->
Fun = fun() ->
lookup_string_from_id(Id, IndxFile, From)
end,
_ = erlang:spawn_link(Fun),
{noreply, State}.
handle_info(_Request, State) -> {noreply, State}.
code_change(_, State, _) -> {ok, State}.
hash(Value) ->
crypto:hash(sha512, Value).