mirror of
https://github.com/gnh1201/welsonjs.git
synced 2025-07-11 03:03:10 +00:00
428 lines
16 KiB
C#
428 lines
16 KiB
C#
// DataStore.cs (WelsonJS.Esent)
|
|
// SPDX-License-Identifier: MIT
|
|
// SPDX-FileCopyrightText: 2025 Namhyeon Go <gnh1201@catswords.re.kr>, Catswords OSS and WelsonJS Contributors
|
|
// https://github.com/gnh1201/welsonjs
|
|
//
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using Microsoft.Isam.Esent.Interop;
|
|
|
|
namespace WelsonJS.Esent
|
|
{
|
|
public class EsentDatabase : IDisposable
|
|
{
|
|
private const string _primaryKeyindexName = "primary";
|
|
private const string _indexNamePrefix = "idx_";
|
|
private const string _databaseName = "metadata.edb";
|
|
|
|
private readonly ICompatibleLogger _logger;
|
|
private static readonly object _lock = new object();
|
|
private static bool _initialized = false;
|
|
private static Instance _instance;
|
|
private static string _workingDirectory;
|
|
private static string _filePath;
|
|
|
|
private readonly Session _session;
|
|
private readonly JET_DBID _dbid;
|
|
private readonly Schema _schema;
|
|
private readonly Column _primaryKey;
|
|
private readonly Dictionary<string, JET_COLUMNID> _columnIds;
|
|
|
|
public EsentDatabase(Schema schema, string workingDirectory, ICompatibleLogger logger = null)
|
|
{
|
|
_logger = logger ?? new TraceLogger();
|
|
|
|
if (schema == null)
|
|
throw new ArgumentNullException(nameof(schema));
|
|
|
|
_primaryKey = schema.PrimaryKey;
|
|
|
|
if (_primaryKey == null)
|
|
throw new ArgumentNullException();
|
|
|
|
if (!schema.Columns.Exists(c => c == _primaryKey))
|
|
throw new ArgumentException($"Primary key '{_primaryKey.Name}' is not in schema.");
|
|
|
|
_workingDirectory = workingDirectory;
|
|
_schema = schema;
|
|
_columnIds = new Dictionary<string, JET_COLUMNID>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
InitializeInstance();
|
|
|
|
_session = new Session(_instance);
|
|
|
|
if (!File.Exists(_filePath))
|
|
{
|
|
Api.JetCreateDatabase(_session, _filePath, null, out _dbid, CreateDatabaseGrbit.None);
|
|
CreateTable(_schema);
|
|
}
|
|
else
|
|
{
|
|
Api.JetAttachDatabase(_session, _filePath, AttachDatabaseGrbit.None);
|
|
Api.JetOpenDatabase(_session, _filePath, null, out _dbid, OpenDatabaseGrbit.None);
|
|
}
|
|
|
|
CacheColumns();
|
|
}
|
|
|
|
private static void InitializeInstance()
|
|
{
|
|
if (_initialized) return;
|
|
|
|
lock (_lock)
|
|
{
|
|
if (_initialized) return;
|
|
|
|
// set the file path
|
|
_filePath = Path.Combine(_workingDirectory, _databaseName);
|
|
|
|
// config the instance
|
|
_instance = new Instance(typeof(EsentDatabase).Namespace);
|
|
_instance.Parameters.SystemDirectory = _workingDirectory;
|
|
_instance.Parameters.LogFileDirectory = _workingDirectory;
|
|
_instance.Parameters.TempDirectory = _workingDirectory;
|
|
|
|
// initialize the instance
|
|
_instance.Init();
|
|
_initialized = true;
|
|
}
|
|
}
|
|
|
|
private void CreateTable(Schema schema)
|
|
{
|
|
Api.JetBeginTransaction(_session);
|
|
JET_TABLEID tableid;
|
|
Api.JetCreateTable(_session, _dbid, schema.TableName, 0, 100, out tableid);
|
|
|
|
foreach (var col in schema.Columns)
|
|
{
|
|
var coldef = new JET_COLUMNDEF
|
|
{
|
|
coltyp = col.Type,
|
|
cbMax = col.MaxSize,
|
|
cp = col.CodePage
|
|
};
|
|
Api.JetAddColumn(_session, tableid, col.Name, coldef, null, 0, out _);
|
|
}
|
|
|
|
CreateIndex(tableid, new[] { _primaryKey }, CreateIndexGrbit.IndexPrimary | CreateIndexGrbit.IndexUnique);
|
|
|
|
Api.JetCloseTable(_session, tableid);
|
|
Api.JetCommitTransaction(_session, CommitTransactionGrbit.None);
|
|
}
|
|
|
|
public void CreateIndex(JET_TABLEID tableid, IEnumerable<Column> columns, CreateIndexGrbit grbit)
|
|
{
|
|
if (columns == null)
|
|
throw new ArgumentNullException(nameof(columns));
|
|
|
|
var columnList = columns.ToList();
|
|
if (columnList.Count == 0)
|
|
throw new ArgumentException("At least one column is required to create an index.", nameof(columns));
|
|
|
|
if (tableid == JET_TABLEID.Nil)
|
|
throw new ArgumentException("Invalid table ID.", nameof(tableid));
|
|
|
|
bool isPrimaryKeyIndex = (columnList.Count == 1 && columnList[0].IsPrimaryKey);
|
|
|
|
if (isPrimaryKeyIndex && (grbit & CreateIndexGrbit.IndexPrimary) == 0)
|
|
throw new ArgumentException("Primary key index must have the CreateIndexGrbit.IndexPrimary flag set.", nameof(grbit));
|
|
|
|
string indexName = isPrimaryKeyIndex
|
|
? _primaryKeyindexName
|
|
: _indexNamePrefix + string.Join("_", columnList.Select(c => c.Name));
|
|
|
|
string key = string.Concat(columnList.Select(c => "+" + c.Name));
|
|
string keyDescription = key + "\0\0"; // double null-terminated
|
|
int keyDescriptionLength = keyDescription.Length;
|
|
|
|
Api.JetCreateIndex(
|
|
_session,
|
|
tableid,
|
|
indexName,
|
|
grbit,
|
|
keyDescription,
|
|
keyDescriptionLength,
|
|
100
|
|
);
|
|
}
|
|
|
|
private void CacheColumns()
|
|
{
|
|
using (var table = new Table(_session, _dbid, _schema.TableName, OpenTableGrbit.ReadOnly))
|
|
{
|
|
foreach (var col in _schema.Columns)
|
|
{
|
|
try
|
|
{
|
|
JET_COLUMNID colid = Api.GetTableColumnid(_session, table, col.Name);
|
|
_columnIds[col.Name] = colid;
|
|
}
|
|
catch (EsentColumnNotFoundException)
|
|
{
|
|
_logger.Warn($"Column '{col.Name}' not found.");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public bool Insert(Dictionary<string, object> values, out object key)
|
|
{
|
|
return TrySaveRecord(values, JET_prep.Insert, expectSeek: false, out key);
|
|
}
|
|
|
|
public bool Update(Dictionary<string, object> values)
|
|
{
|
|
return TrySaveRecord(values, JET_prep.Replace, expectSeek: true, out _);
|
|
}
|
|
|
|
private bool TrySaveRecord(
|
|
Dictionary<string, object> values,
|
|
JET_prep prepType,
|
|
bool expectSeek,
|
|
out object primaryKeyValue)
|
|
{
|
|
primaryKeyValue = null;
|
|
|
|
if (!TryGetPrimaryKeyValue(values, out var keyValue))
|
|
return false;
|
|
|
|
var keyType = _primaryKey.Type;
|
|
|
|
using (var table = new Table(_session, _dbid, _schema.TableName, OpenTableGrbit.Updatable))
|
|
{
|
|
try
|
|
{
|
|
Api.JetBeginTransaction(_session);
|
|
|
|
Api.JetSetCurrentIndex(_session, table, _primaryKeyindexName);
|
|
MakeKeyByType(keyValue, keyType, _session, table);
|
|
bool found = Api.TrySeek(_session, table, SeekGrbit.SeekEQ);
|
|
|
|
if (expectSeek != found)
|
|
{
|
|
_logger.Warn($"[ESENT] Operation skipped. Seek result = {found}, expected = {expectSeek}");
|
|
Api.JetRollback(_session, RollbackTransactionGrbit.None);
|
|
return false;
|
|
}
|
|
|
|
Api.JetPrepareUpdate(_session, table, prepType);
|
|
SetAllColumns(values, table);
|
|
|
|
Api.JetUpdate(_session, table);
|
|
Api.JetCommitTransaction(_session, CommitTransactionGrbit.None);
|
|
|
|
if (prepType == JET_prep.Insert)
|
|
primaryKeyValue = keyValue;
|
|
|
|
return true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Api.JetRollback(_session, RollbackTransactionGrbit.None);
|
|
throw new InvalidOperationException($"[ESENT] Operation failed: {ex.Message}");
|
|
}
|
|
}
|
|
}
|
|
|
|
public Dictionary<string, object> FindById(object keyValue)
|
|
{
|
|
var result = new Dictionary<string, object>();
|
|
var keyType = _primaryKey.Type;
|
|
|
|
using (var table = new Table(_session, _dbid, _schema.TableName, OpenTableGrbit.ReadOnly))
|
|
{
|
|
Api.JetSetCurrentIndex(_session, table, _primaryKeyindexName);
|
|
MakeKeyByType(keyValue, keyType, _session, table);
|
|
if (!Api.TrySeek(_session, table, SeekGrbit.SeekEQ))
|
|
return null;
|
|
|
|
foreach (var col in _schema.Columns)
|
|
{
|
|
if (!_columnIds.TryGetValue(col.Name, out var colid))
|
|
continue;
|
|
|
|
var value = RetrieveColumnByType(_session, table, colid, col.Type);
|
|
result[col.Name] = value;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
public List<Dictionary<string, object>> FindAll()
|
|
{
|
|
var results = new List<Dictionary<string, object>>();
|
|
|
|
using (var table = new Table(_session, _dbid, _schema.TableName, OpenTableGrbit.ReadOnly))
|
|
{
|
|
Api.JetSetCurrentIndex(_session, table, _primaryKeyindexName);
|
|
|
|
if (!Api.TryMoveFirst(_session, table))
|
|
return results;
|
|
|
|
do
|
|
{
|
|
var row = new Dictionary<string, object>();
|
|
foreach (var col in _schema.Columns)
|
|
{
|
|
if (!_columnIds.TryGetValue(col.Name, out var colid))
|
|
continue;
|
|
|
|
var value = RetrieveColumnByType(_session, table, colid, col.Type);
|
|
row[col.Name] = value;
|
|
}
|
|
results.Add(row);
|
|
}
|
|
while (Api.TryMoveNext(_session, table));
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
public bool DeleteById(object keyValue)
|
|
{
|
|
var keyType = _primaryKey.Type;
|
|
|
|
using (var table = new Table(_session, _dbid, _schema.TableName, OpenTableGrbit.Updatable))
|
|
{
|
|
Api.JetSetCurrentIndex(_session, table, _primaryKeyindexName);
|
|
MakeKeyByType(keyValue, keyType, _session, table);
|
|
if (!Api.TrySeek(_session, table, SeekGrbit.SeekEQ))
|
|
return false;
|
|
|
|
Api.JetDelete(_session, table);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
private object RetrieveColumnByType(Session session, Table table, JET_COLUMNID columnId, JET_coltyp type)
|
|
{
|
|
switch (type)
|
|
{
|
|
case JET_coltyp.Text:
|
|
return Api.RetrieveColumnAsString(session, table, columnId, Encoding.Unicode);
|
|
case JET_coltyp.Long:
|
|
return Api.RetrieveColumnAsInt32(session, table, columnId);
|
|
case JET_coltyp.IEEEDouble:
|
|
return Api.RetrieveColumnAsDouble(session, table, columnId);
|
|
case JET_coltyp.DateTime:
|
|
return Api.RetrieveColumnAsDateTime(session, table, columnId);
|
|
case JET_coltyp.Binary:
|
|
case JET_coltyp.LongBinary:
|
|
return Api.RetrieveColumn(session, table, columnId);
|
|
default:
|
|
_logger.Warn($"[ESENT] Unsupported RetrieveColumn type: {type}");
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private bool TryGetPrimaryKeyValue(Dictionary<string, object> values, out object keyValue)
|
|
{
|
|
keyValue = null;
|
|
|
|
if (!values.TryGetValue(_primaryKey.Name, out keyValue))
|
|
{
|
|
_logger.Warn($"[ESENT] Missing primary key '{_primaryKey.Name}'.");
|
|
return false;
|
|
}
|
|
|
|
if (keyValue == null)
|
|
{
|
|
_logger.Warn("[ESENT] Primary key value cannot be null.");
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private JET_coltyp GetColumnType(string columnName)
|
|
{
|
|
var column = _schema.Columns.FirstOrDefault(c => c.Name == columnName);
|
|
if (column == null)
|
|
throw new ArgumentException($"Column '{columnName}' not found in schema.");
|
|
|
|
return column.Type;
|
|
}
|
|
|
|
private void SetAllColumns(Dictionary<string, object> values, Table table)
|
|
{
|
|
foreach (var kv in values)
|
|
{
|
|
if (!_columnIds.TryGetValue(kv.Key, out var colid))
|
|
{
|
|
_logger.Warn($"[ESENT] Column '{kv.Key}' not found in cache.");
|
|
continue;
|
|
}
|
|
|
|
var colType = GetColumnType(kv.Key);
|
|
SetColumnByType(_session, table, colid, kv.Value, colType);
|
|
}
|
|
}
|
|
|
|
private void SetColumnByType(Session session, Table table, JET_COLUMNID columnId, object value, JET_coltyp type)
|
|
{
|
|
if (value == null)
|
|
return;
|
|
|
|
switch (type)
|
|
{
|
|
case JET_coltyp.Text:
|
|
Api.SetColumn(session, table, columnId, value.ToString(), Encoding.Unicode);
|
|
break;
|
|
case JET_coltyp.Long:
|
|
Api.SetColumn(session, table, columnId, Convert.ToInt32(value));
|
|
break;
|
|
case JET_coltyp.IEEEDouble:
|
|
Api.SetColumn(session, table, columnId, Convert.ToDouble(value));
|
|
break;
|
|
case JET_coltyp.DateTime:
|
|
Api.SetColumn(session, table, columnId, Convert.ToDateTime(value));
|
|
break;
|
|
case JET_coltyp.Binary:
|
|
case JET_coltyp.LongBinary:
|
|
Api.SetColumn(session, table, columnId, (byte[])value);
|
|
break;
|
|
default:
|
|
_logger.Warn($"[ESENT] Unsupported SetColumn type: {type}");
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void MakeKeyByType(object value, JET_coltyp type, Session session, Table table)
|
|
{
|
|
switch (type)
|
|
{
|
|
case JET_coltyp.Text:
|
|
Api.MakeKey(session, table, value.ToString(), Encoding.Unicode, MakeKeyGrbit.NewKey);
|
|
break;
|
|
case JET_coltyp.Long:
|
|
Api.MakeKey(session, table, Convert.ToInt32(value), MakeKeyGrbit.NewKey);
|
|
break;
|
|
case JET_coltyp.IEEEDouble:
|
|
Api.MakeKey(session, table, Convert.ToDouble(value), MakeKeyGrbit.NewKey);
|
|
break;
|
|
case JET_coltyp.DateTime:
|
|
Api.MakeKey(session, table, Convert.ToDateTime(value), MakeKeyGrbit.NewKey);
|
|
break;
|
|
case JET_coltyp.Binary:
|
|
case JET_coltyp.LongBinary:
|
|
Api.MakeKey(session, table, (byte[])value, MakeKeyGrbit.NewKey);
|
|
break;
|
|
default:
|
|
_logger.Warn($"[ESENT] Unsupported MakeKey type: {type}");
|
|
break;
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
_session?.Dispose();
|
|
}
|
|
}
|
|
}
|