Packages

A comprehensive Elixir library for reading, writing, and manipulating xBase database files

Current section

Files

Jump to
xbase lib xbase memo_handler.ex
Raw

lib/xbase/memo_handler.ex

defmodule Xbase.MemoHandler do
@moduledoc """
Handles coordinated operations between DBF and DBT files for memo field support.
Provides high-level API for seamless memo content handling in record operations,
automatic file discovery, and transaction safety across both file types.
"""
alias Xbase.Parser
alias Xbase.DbtParser
alias Xbase.DbtWriter
alias Xbase.Types.DbtFile
defstruct [:dbf, :dbt, :dbf_path, :dbt_path, :memo_mode]
@type t :: %__MODULE__{
dbf: map(),
dbt: DbtFile.t() | nil,
dbf_path: String.t(),
dbt_path: String.t() | nil,
memo_mode: :auto | :required | :disabled
}
@doc """
Opens a DBF file with automatic memo file discovery and coordination.
## Parameters
- `dbf_path` - Path to the DBF file
- `modes` - File access modes (default: [:read])
- `opts` - Options for memo handling
- `:memo` - Memo mode (:auto, :required, :disabled)
- `:dbt_path` - Explicit DBT file path (overrides auto-discovery)
## Returns
- `{:ok, MemoHandler.t()}` - Successfully opened coordinated files
- `{:error, reason}` - Error opening files
## Examples
# Automatic memo file discovery
{:ok, handler} = MemoHandler.open_dbf_with_memo("data.dbf")
# Read-write mode with required memo support
{:ok, handler} = MemoHandler.open_dbf_with_memo("data.dbf", [:read, :write], memo: :required)
# Explicit DBT file path
{:ok, handler} = MemoHandler.open_dbf_with_memo("data.dbf", [], dbt_path: "memos.dbt")
"""
def open_dbf_with_memo(dbf_path, modes \\ [:read], opts \\ []) do
memo_mode = Keyword.get(opts, :memo, :auto)
explicit_dbt_path = Keyword.get(opts, :dbt_path)
# Open DBF file first
case Parser.open_dbf(dbf_path, modes) do
{:ok, dbf} ->
# Check if DBF supports memo fields
if has_memo_support?(dbf) do
dbt_path = explicit_dbt_path || discover_dbt_path(dbf_path)
open_memo_file(dbf, dbf_path, dbt_path, modes, memo_mode)
else
case memo_mode do
:required ->
Parser.close_dbf(dbf)
{:error, :dbf_no_memo_support}
_ ->
# DBF without memo support, return handler without DBT
{:ok, %__MODULE__{
dbf: dbf,
dbt: nil,
dbf_path: dbf_path,
dbt_path: nil,
memo_mode: :disabled
}}
end
end
{:error, reason} ->
{:error, reason}
end
end
@doc """
Creates a new DBF file with memo support and coordinated DBT file.
## Parameters
- `dbf_path` - Path for the new DBF file
- `fields` - Field definitions for the DBF file
- `opts` - Options for file creation
- `:version` - DBF version (automatically set to memo-capable if memo fields present)
- `:dbt_path` - Explicit DBT file path (defaults to replacing .dbf with .dbt)
- `:block_size` - DBT block size (default: 512)
## Returns
- `{:ok, MemoHandler.t()}` - Successfully created coordinated files
- `{:error, reason}` - Error creating files
"""
def create_dbf_with_memo(dbf_path, fields, opts \\ []) do
has_memo_fields = Enum.any?(fields, fn field -> field.type == "M" end)
if has_memo_fields do
# Force memo-capable version
version = Keyword.get(opts, :version, 0x8B) # dBase IV with memo
dbt_path = Keyword.get(opts, :dbt_path, replace_extension(dbf_path, ".dbt"))
block_size = Keyword.get(opts, :block_size, 512)
# Create DBF file first
case Parser.create_dbf(dbf_path, fields, Keyword.put(opts, :version, version)) do
{:ok, dbf} ->
# Create coordinated DBT file
case DbtWriter.create_dbt(dbt_path, version: :dbase_iii, block_size: block_size) do
{:ok, dbt} ->
{:ok, %__MODULE__{
dbf: dbf,
dbt: dbt,
dbf_path: dbf_path,
dbt_path: dbt_path,
memo_mode: :auto
}}
{:error, reason} ->
Parser.close_dbf(dbf)
File.rm(dbf_path) # Clean up DBF file
{:error, {:dbt_creation_failed, reason}}
end
{:error, reason} ->
{:error, reason}
end
else
# No memo fields, create regular DBF file
case Parser.create_dbf(dbf_path, fields, opts) do
{:ok, dbf} ->
{:ok, %__MODULE__{
dbf: dbf,
dbt: nil,
dbf_path: dbf_path,
dbt_path: nil,
memo_mode: :disabled
}}
{:error, reason} ->
{:error, reason}
end
end
end
@doc """
Appends a record with automatic memo content handling.
## Parameters
- `handler` - MemoHandler structure
- `record_data` - Map of field names to values, where memo fields contain content strings
## Returns
- `{:ok, updated_handler}` - Successfully appended record with memo content
- `{:error, reason}` - Error appending record
## Example
record_data = %{
"NAME" => "John Doe",
"NOTES" => "This memo content will be automatically stored in the DBT file"
}
{:ok, updated_handler} = MemoHandler.append_record_with_memo(handler, record_data)
"""
def append_record_with_memo(%__MODULE__{memo_mode: :disabled} = handler, record_data) do
# No memo support, use regular record append
case Parser.append_record(handler.dbf, record_data) do
{:ok, updated_dbf} ->
{:ok, %{handler | dbf: updated_dbf}}
{:error, reason} ->
{:error, reason}
end
end
def append_record_with_memo(%__MODULE__{} = handler, record_data) do
# Process memo fields and convert content to references
case process_memo_fields(handler, record_data) do
{:ok, {processed_data, updated_handler}} ->
# Append record with memo references
case Parser.append_record(updated_handler.dbf, processed_data) do
{:ok, updated_dbf} ->
{:ok, %{updated_handler | dbf: updated_dbf}}
{:error, reason} ->
{:error, reason}
end
{:error, reason} ->
{:error, reason}
end
end
@doc """
Updates a record with automatic memo content handling.
## Parameters
- `handler` - MemoHandler structure
- `record_index` - Zero-based record index to update
- `record_data` - Map of field names to values for update
## Returns
- `{:ok, updated_handler}` - Successfully updated record with memo content
- `{:error, reason}` - Error updating record
"""
def update_record_with_memo(%__MODULE__{memo_mode: :disabled} = handler, record_index, record_data) do
# No memo support, use regular record update
case Parser.update_record(handler.dbf, record_index, record_data) do
{:ok, updated_dbf} ->
{:ok, %{handler | dbf: updated_dbf}}
{:error, reason} ->
{:error, reason}
end
end
def update_record_with_memo(%__MODULE__{} = handler, record_index, record_data) do
# For updates, we need to handle memo field updates carefully
# Read existing record to preserve non-updated memo references
case Parser.read_record(handler.dbf, record_index) do
{:ok, existing_record} ->
# Extract data from Record struct
existing_data = existing_record.data
# Merge update data with existing record
_merged_data = Map.merge(existing_data, record_data)
# Process memo fields in the merged data
case process_memo_fields_for_update(handler, existing_data, record_data) do
{:ok, {processed_data, updated_handler}} ->
# Update record with processed memo references
case Parser.update_record(updated_handler.dbf, record_index, processed_data) do
{:ok, updated_dbf} ->
{:ok, %{updated_handler | dbf: updated_dbf}}
{:error, reason} ->
{:error, reason}
end
{:error, reason} ->
{:error, reason}
end
{:error, reason} ->
{:error, reason}
end
end
@doc """
Reads a record with automatic memo content resolution.
## Parameters
- `handler` - MemoHandler structure
- `record_index` - Zero-based record index to read
## Returns
- `{:ok, record_data}` - Record data with memo content resolved
- `{:error, reason}` - Error reading record
"""
def read_record_with_memo(%__MODULE__{} = handler, record_index) do
case Parser.read_record(handler.dbf, record_index) do
{:ok, record_struct} ->
# Extract data from Record struct and resolve memo content
resolve_memo_content(handler, record_struct.data)
{:error, reason} ->
{:error, reason}
end
end
@doc """
Closes both DBF and DBT files properly.
## Parameters
- `handler` - MemoHandler structure
## Returns
- `:ok` - Files closed successfully
"""
def close_memo_files(%__MODULE__{} = handler) do
# Close DBF file
Parser.close_dbf(handler.dbf)
# Close DBT file if present
if handler.dbt do
DbtWriter.close_dbt(handler.dbt)
end
:ok
end
@doc """
Executes a function with transaction safety across both DBF and DBT files.
## Parameters
- `handler` - MemoHandler structure
- `transaction_fn` - Function to execute with the handler
## Returns
- `{:ok, {result, updated_handler}}` - Transaction successful
- `{:error, reason}` - Transaction failed, changes rolled back
"""
def memo_transaction(%__MODULE__{} = handler, transaction_fn) do
# Create backups of both files
dbf_backup = handler.dbf_path <> ".bak"
dbt_backup = if handler.dbt_path, do: handler.dbt_path <> ".bak", else: nil
try do
# Backup DBF file
File.cp!(handler.dbf_path, dbf_backup)
# Backup DBT file if it exists
if dbt_backup && File.exists?(handler.dbt_path) do
File.cp!(handler.dbt_path, dbt_backup)
end
# Execute transaction
case transaction_fn.(handler) do
{:ok, result, updated_handler} ->
# Transaction succeeded, clean up backups
File.rm(dbf_backup)
if dbt_backup, do: File.rm(dbt_backup)
{:ok, {result, updated_handler}}
{:error, reason} ->
# Transaction failed, restore from backups
restore_from_backup(handler, dbf_backup, dbt_backup)
{:error, reason}
end
rescue
e ->
# Exception occurred, restore from backups
restore_from_backup(handler, dbf_backup, dbt_backup)
{:error, {:exception, e}}
end
end
# Private helper functions
defp has_memo_support?(%{header: header}) do
# Check if DBF version supports memo fields
header.version in [0x83, 0x8B, 0xF5]
end
defp discover_dbt_path(dbf_path) do
replace_extension(dbf_path, ".dbt")
end
defp replace_extension(path, new_ext) do
Path.rootname(path) <> new_ext
end
defp open_memo_file(dbf, dbf_path, dbt_path, modes, memo_mode) do
write_mode = :write in modes
case File.exists?(dbt_path) do
true ->
# DBT file exists, open it
open_function = if write_mode, do: &DbtWriter.open_dbt_for_writing/1, else: &DbtParser.open_dbt/1
case open_function.(dbt_path) do
{:ok, dbt} ->
{:ok, %__MODULE__{
dbf: dbf,
dbt: dbt,
dbf_path: dbf_path,
dbt_path: dbt_path,
memo_mode: memo_mode
}}
{:error, reason} ->
Parser.close_dbf(dbf)
{:error, {:dbt_open_failed, reason}}
end
false ->
case memo_mode do
:required ->
Parser.close_dbf(dbf)
{:error, :dbt_file_required}
:auto when write_mode ->
# Create new DBT file for writing
case DbtWriter.create_dbt(dbt_path) do
{:ok, dbt} ->
{:ok, %__MODULE__{
dbf: dbf,
dbt: dbt,
dbf_path: dbf_path,
dbt_path: dbt_path,
memo_mode: memo_mode
}}
{:error, reason} ->
Parser.close_dbf(dbf)
{:error, {:dbt_creation_failed, reason}}
end
_ ->
# Auto mode without write access, or disabled - no DBT file
{:ok, %__MODULE__{
dbf: dbf,
dbt: nil,
dbf_path: dbf_path,
dbt_path: nil,
memo_mode: memo_mode
}}
end
end
end
defp process_memo_fields(%__MODULE__{dbt: nil}, record_data) do
# No DBT file available, ensure no memo content is provided
memo_fields = get_memo_fields_with_content(record_data)
if Enum.empty?(memo_fields) do
{:ok, {record_data, nil}} # No handler update needed
else
{:error, {:memo_content_without_dbt, memo_fields}}
end
end
defp process_memo_fields(%__MODULE__{} = handler, record_data) do
# Process each memo field that contains string content
memo_fields = get_memo_fields(handler.dbf)
process_memo_fields_recursive(handler, record_data, memo_fields, %{})
end
defp process_memo_fields_recursive(handler, record_data, [], processed_data) do
# All fields processed
final_data = Map.merge(record_data, processed_data)
{:ok, {final_data, handler}}
end
defp process_memo_fields_recursive(handler, record_data, [field_name | rest], processed_data) do
case Map.get(record_data, field_name) do
content when is_binary(content) ->
# Write memo content and get block reference
case DbtWriter.write_memo(handler.dbt, content) do
{:ok, {block_number, updated_dbt}} ->
updated_handler = %{handler | dbt: updated_dbt}
updated_processed = Map.put(processed_data, field_name, {:memo_ref, block_number})
process_memo_fields_recursive(updated_handler, record_data, rest, updated_processed)
{:error, reason} ->
{:error, {:memo_write_failed, field_name, reason}}
end
{:memo_ref, _block_number} = memo_ref ->
# Already a memo reference, keep as-is
updated_processed = Map.put(processed_data, field_name, memo_ref)
process_memo_fields_recursive(handler, record_data, rest, updated_processed)
nil ->
# No value for this memo field, continue
process_memo_fields_recursive(handler, record_data, rest, processed_data)
other ->
# Invalid value for memo field
{:error, {:invalid_memo_value, field_name, other}}
end
end
defp process_memo_fields_for_update(handler, existing_record, update_data) do
# Only process memo fields that are being updated
memo_fields = get_memo_fields(handler.dbf)
updated_memo_fields = Enum.filter(memo_fields, fn field -> Map.has_key?(update_data, field) end)
process_memo_update_fields_recursive(handler, existing_record, update_data, updated_memo_fields, %{})
end
defp process_memo_update_fields_recursive(handler, _existing_record, update_data, [], processed_data) do
# All updated memo fields processed
final_data = Map.merge(update_data, processed_data)
{:ok, {final_data, handler}}
end
defp process_memo_update_fields_recursive(handler, existing_record, update_data, [field_name | rest], processed_data) do
case Map.get(update_data, field_name) do
content when is_binary(content) ->
# Check if we can reuse existing block
existing_ref = Map.get(existing_record, field_name)
case reuse_or_create_memo_block(handler, existing_ref, content) do
{:ok, {memo_ref, updated_handler}} ->
updated_processed = Map.put(processed_data, field_name, memo_ref)
process_memo_update_fields_recursive(updated_handler, existing_record, update_data, rest, updated_processed)
{:error, reason} ->
{:error, {:memo_update_failed, field_name, reason}}
end
{:memo_ref, _block_number} = memo_ref ->
# New memo reference provided
updated_processed = Map.put(processed_data, field_name, memo_ref)
process_memo_update_fields_recursive(handler, existing_record, update_data, rest, updated_processed)
other ->
{:error, {:invalid_memo_update_value, field_name, other}}
end
end
defp reuse_or_create_memo_block(handler, {:memo_ref, block_number}, content) do
# Try to update existing block
case DbtWriter.update_memo(handler.dbt, block_number, content) do
{:ok, updated_dbt} ->
{:ok, {{:memo_ref, block_number}, %{handler | dbt: updated_dbt}}}
{:error, _reason} ->
# Update failed, create new block
create_new_memo_block(handler, content)
end
end
defp reuse_or_create_memo_block(handler, _other, content) do
# No existing reference, create new block
create_new_memo_block(handler, content)
end
defp create_new_memo_block(handler, content) do
case DbtWriter.write_memo(handler.dbt, content) do
{:ok, {block_number, updated_dbt}} ->
{:ok, {{:memo_ref, block_number}, %{handler | dbt: updated_dbt}}}
{:error, reason} ->
{:error, reason}
end
end
defp resolve_memo_content(%__MODULE__{dbt: nil}, record_data) do
# No DBT file, return record as-is
{:ok, record_data}
end
defp resolve_memo_content(%__MODULE__{} = handler, record_data) do
memo_fields = get_memo_fields(handler.dbf)
resolve_memo_content_recursive(handler, record_data, memo_fields, %{})
end
defp resolve_memo_content_recursive(_handler, record_data, [], resolved_data) do
# All memo fields resolved
final_data = Map.merge(record_data, resolved_data)
{:ok, final_data}
end
defp resolve_memo_content_recursive(handler, record_data, [field_name | rest], resolved_data) do
case Map.get(record_data, field_name) do
{:memo_ref, block_number} when block_number > 0 ->
# Resolve memo reference to content
case DbtParser.read_memo(handler.dbt, block_number) do
{:ok, content} ->
updated_resolved = Map.put(resolved_data, field_name, content)
resolve_memo_content_recursive(handler, record_data, rest, updated_resolved)
{:error, reason} ->
{:error, {:memo_read_failed, field_name, block_number, reason}}
end
{:memo_ref, 0} ->
# Empty memo reference
updated_resolved = Map.put(resolved_data, field_name, "")
resolve_memo_content_recursive(handler, record_data, rest, updated_resolved)
_other ->
# Not a memo reference or already resolved
resolve_memo_content_recursive(handler, record_data, rest, resolved_data)
end
end
defp get_memo_fields(%{fields: fields}) do
fields
|> Enum.filter(fn field -> field.type == "M" end)
|> Enum.map(fn field -> field.name end)
end
defp get_memo_fields_with_content(record_data) do
record_data
|> Enum.filter(fn {_key, value} -> is_binary(value) end)
|> Enum.map(fn {key, _value} -> key end)
end
defp restore_from_backup(handler, dbf_backup, dbt_backup) do
# Restore DBF file
if File.exists?(dbf_backup) do
File.cp!(dbf_backup, handler.dbf_path)
File.rm(dbf_backup)
end
# Restore DBT file
if dbt_backup && File.exists?(dbt_backup) do
File.cp!(dbt_backup, handler.dbt_path)
File.rm(dbt_backup)
end
end
end