using System;
using System.Collections;
using System.IO;
using LumiSoft.Net;
namespace LumiSoft.Data.lsDB
{
/// <summary>
/// LumiSoft database file.
/// </summary>
public class DbFile : IDisposable
{
private LDB_DataColumnCollection m_pColumns = null;
private FileStream m_pDbFile = null;
private string m_DbFileName = "";
private long m_DatapagesStartOffset = -1;
private LDB_Record m_pCurrentRecord = null;
private bool m_TableLocked = false;
private int m_DataPageDataAreaSize = 1000;
private long m_FileLength = 0;
private long m_FilePosition = 0;
/// <summary>
/// Default constructor.
/// </summary>
public DbFile()
{
m_pColumns = new LDB_DataColumnCollection(this);
}
#region method Dispose
/// <summary>
/// Clean up any resources being used.
/// </summary>
public void Dispose()
{
Close();
}
#endregion
#region method Open
/// <summary>
/// Opens specified data file.
/// </summary>
/// <param name="fileName">File name.</param>
public void Open(string fileName)
{
Open(fileName,0);
}
/// <summary>
/// Opens specified data file.
/// </summary>
/// <param name="fileName">File name.</param>
/// <param name="waitTime">If data base file is exclusively locked, then how many seconds to wait file to unlock before raising a error.</param>
public void Open(string fileName,int waitTime)
{
DateTime lockExpireTime = DateTime.Now.AddSeconds(waitTime);
while(true){
try{
m_pDbFile = File.Open(fileName,FileMode.Open,FileAccess.ReadWrite,FileShare.ReadWrite);
break;
}
catch(IOException x){
// Make this because to get rid of "The variable 'x' is declared but never used"
string dummy = x.Message;
System.Threading.Thread.Sleep(15);
// Lock wait time timed out
if(DateTime.Now > lockExpireTime){
throw new Exception("Database file is locked and lock wait time expired !");
}
}
}
/* Table structure:
50 bytes - version
2 bytes - CRLF
8 bytes - free datapages count
2 bytes - CRLF
4 bytes - datapage data area size
2 bytes - CRLF
100 x 500 bytes - 100 columns info store
2 bytes - CRLF
... data pages
*/
m_DbFileName = fileName;
StreamLineReader r = new StreamLineReader(m_pDbFile);
// TODO: check if LDB file
// Read version line (50 bytes + CRLF)
byte[] version = r.ReadLine();
// Skip free data pages count
byte[] freeDataPagesCount = new byte[10];
m_pDbFile.Read(freeDataPagesCount,0,freeDataPagesCount.Length);
// 4 bytes datapage data area size + CRLF
byte[] dataPageDataAreaSize = new byte[6];
m_pDbFile.Read(dataPageDataAreaSize,0,dataPageDataAreaSize.Length);
m_DataPageDataAreaSize = ldb_Utils.ByteToInt(dataPageDataAreaSize,0);
// Read 100 column lines (500 + CRLF bytes each)
for(int i=0;i<100;i++){
byte[] columnInfo = r.ReadLine();
if(columnInfo == null){
throw new Exception("Invalid columns data area length !");
}
if(columnInfo[0] != '\0'){
m_pColumns.Parse(columnInfo);
}
}
// Header terminator \0
m_pDbFile.Position++;
// No we have rows start offset
m_DatapagesStartOffset = m_pDbFile.Position;
// Store file length and position
m_FileLength = m_pDbFile.Length;
m_FilePosition = m_pDbFile.Position;
}
#endregion
#region method Close
/// <summary>
/// Closes database file.
/// </summary>
public void Close()
{
if(m_pDbFile != null){
m_pDbFile.Close();
m_pDbFile = null;
m_DbFileName = "";
m_FileLength = 0;
m_FilePosition = 0;
}
}
#endregion
#region method Create
/// <summary>
/// Creates new database file.
/// </summary>
/// <param name="fileName">File name.</param>
public void Create(string fileName)
{
Create(fileName,1000);
}
/// <summary>
/// Creates new database file.
/// </summary>
/// <param name="fileName">File name.</param>
/// <param name="dataPageDataAreaSize">Specifies how many data can data page store.</param>
public void Create(string fileName,int dataPageDataAreaSize)
{
m_pDbFile = new FileStream(fileName,FileMode.CreateNew,FileAccess.ReadWrite,FileShare.None);
/* Table structure:
50 bytes - version
2 bytes - CRLF
8 bytes - free datapages count
2 bytes - CRLF
4 bytes - datapage data area size
2 bytes - CRLF
100 x 500 bytes - 100 columns info store
2 bytes - CRLF
... data pages
*/
// Version 50 + CRLF bytes
byte[] versionData = new byte[52];
versionData[0] = (byte)'1';
versionData[1] = (byte)'.';
versionData[2] = (byte)'0';
versionData[50] = (byte)'\r';
versionData[51] = (byte)'\n';
m_pDbFile.Write(versionData,0,versionData.Length);
// 8 bytes free data pages count + CRLF
byte[] freeDataPagesCount = new byte[10];
freeDataPagesCount[8] = (byte)'\r';
freeDataPagesCount[9] = (byte)'\n';
m_pDbFile.Write(freeDataPagesCount,0,freeDataPagesCount.Length);
// 4 bytes datapage data area size + CRLF
byte[] dataPageDataAreaSizeB = new byte[6];
Array.Copy(ldb_Utils.IntToByte(dataPageDataAreaSize),0,dataPageDataAreaSizeB,0,4);
dataPageDataAreaSizeB[4] = (byte)'\r';
dataPageDataAreaSizeB[5] = (byte)'\n';
m_pDbFile.Write(dataPageDataAreaSizeB,0,dataPageDataAreaSizeB.Length);
// 100 x 100 + CRLF bytes header lines
for(int i=0;i<100;i++){
byte[] data = new byte[100];
m_pDbFile.Write(data,0,data.Length);
m_pDbFile.Write(new byte[]{(int)'\r',(int)'\n'},0,2);
}
// Headers terminator char(0)
m_pDbFile.WriteByte((int)'\0');
// Data pages start pointer
m_DatapagesStartOffset = m_pDbFile.Position - 1;
m_DbFileName = fileName;
m_DataPageDataAreaSize = dataPageDataAreaSize;
// Store file length and position
m_FileLength = m_pDbFile.Length;
m_FilePosition = m_pDbFile.Position;
}
#endregion
#region method LockTable
/// <summary>
/// Locks table.
/// </summary>
/// <param name="waitTime">If table is locked, then how many sconds to wait table to unlock, before teturning error.</param>
public void LockTable(int waitTime)
{
if(!this.IsDatabaseOpen){
throw new Exception("Database isn't open, please open database first !");
}
// Table is locked already, just skip locking
if(m_TableLocked){
return;
}
DateTime lockExpireTime = DateTime.Now.AddSeconds(waitTime);
while(true){
try{
// We just lock first byte
m_pDbFile.Lock(0,1);
m_TableLocked = true;
break;
}
// Catch the IOException generated if the
// specified part of the file is locked.
catch(IOException x){
// Make this because to get rid of "The variable 'x' is declared but never used"
string dummy = x.Message;
System.Threading.Thread.Sleep(15);
// Lock wait time timed out
if(DateTime.Now > lockExpireTime){
throw new Exception("Table is locked and lock wait time expired !");
}
}
}
}
#endregion
#region method UnlockTable
/// <summary>
/// Unlock table.
/// </summary>
public void UnlockTable()
{
if(!this.IsDatabaseOpen){
throw new Exception("Database isn't open, please open database first !");
}
if(m_TableLocked){
// We just unlock first byte
m_pDbFile.Unlock(0,1);
}
}
#endregion
#region method LockRecord
/*
/// <summary>
/// Locks current record.
/// </summary>
public void LockRecord()
{
if(!this.IsDatabaseOpen){
throw new Exception("Database isn't open, please open database first !");
}
}
*/
#endregion
#region method UnlockRecord
/*
/// <summary>
/// Unlocks current record.
/// </summary>
public void UnlockRecord()
{
if(!this.IsDatabaseOpen){
throw new Exception("Database isn't open, please open database first !");
}
}
*/
#endregion
#region method NextRecord
/// <summary>
/// Gets next record. Returns true if end of file reached and there are no more records.
/// </summary>
/// <returns>Returns true if end of file reached and there are no more records.</returns>
public bool NextRecord()
{
if(!this.IsDatabaseOpen){
throw new Exception("Database isn't open, please open database first !");
}
//--- Find next record ---------------------------------------------------//
long nextRowStartOffset = 0;
if(m_pCurrentRecord == null){
nextRowStartOffset = m_DatapagesStartOffset;
}
else{
nextRowStartOffset = m_pCurrentRecord.DataPage.Pointer + m_DataPageDataAreaSize + 33;
}
while(true){
if(m_FileLength > nextRowStartOffset){
DataPage dataPage = new DataPage(m_DataPageDataAreaSize,this,nextRowStartOffset);
// We want datapage with used space
if(dataPage.Used && dataPage.OwnerDataPagePointer < 1){
m_pCurrentRecord = new LDB_Record(this,dataPage);
break;
}
}
else{
return true;
}
nextRowStartOffset += m_DataPageDataAreaSize + 33;
}
//-------------------------------------------------------------------------//
return false;
}
#endregion
#region method AppendRecord
/// <summary>
/// Appends new record to table.
/// </summary>
public void AppendRecord(object[] values)
{
if(!this.IsDatabaseOpen){
throw new Exception("Database isn't open, please open database first !");
}
if(m_pColumns.Count != values.Length){
throw new Exception("Each column must have corresponding value !");
}
bool unlock = true;
// Table is already locked, don't lock it
if(this.TableLocked){
unlock = false;
}
else{
LockTable(15);
}
// Construct record data
byte[] record = LDB_Record.CreateRecord(this,values);
// Get free data pages
DataPage[] dataPages = GetDataPages(0,(int)Math.Ceiling(record.Length / (double)m_DataPageDataAreaSize));
DbFile.StoreDataToDataPages(m_DataPageDataAreaSize,record,dataPages);
if(unlock){
UnlockTable();
}
}
#endregion
#region method DeleteRecord
/// <summary>
/// Deletes current record.
/// </summary>
public void DeleteCurrentRecord()
{
if(!this.IsDatabaseOpen){
throw new Exception("Database isn't open, please open database first !");
}
if(m_pCurrentRecord == null){
throw new Exception("There is no current record !");
}
bool unlock = true;
// Table is already locked, don't lock it
if(this.TableLocked){
unlock = false;
}
else{
LockTable(15);
}
// Release all data pages hold by this row
DataPage[] dataPages = m_pCurrentRecord.DataPages;
for(int i=0;i<dataPages.Length;i++){
DataPage p = dataPages[i];
byte[] dataPage = DataPage.CreateDataPage(m_DataPageDataAreaSize,false,0,0,0,new byte[m_DataPageDataAreaSize]);
SetFilePosition(p.Pointer);
WriteToFile(dataPage,0,dataPage.Length);
}
// Increase free data pages count info in table header
byte[] freeDataPagesCount = new byte[8];
SetFilePosition(52);
ReadFromFile(freeDataPagesCount,0,freeDataPagesCount.Length);
freeDataPagesCount = ldb_Utils.LongToByte(ldb_Utils.ByteToLong(freeDataPagesCount,0) + dataPages.Length);
SetFilePosition(52);
WriteToFile(freeDataPagesCount,0,freeDataPagesCount.Length);
if(unlock){
UnlockTable();
}
// Activate next record **** Change it ???
NextRecord();
}
#endregion
#region static method StoreDataToDataPages
/// <summary>
/// Stores data to specified data pages.
/// </summary>
/// <param name="dataPageDataAreaSize">Data page data area size.</param>
/// <param name="data">Data to store.</param>
/// <param name="dataPages">Data pages where to store data.</param>
internal static void StoreDataToDataPages(int dataPageDataAreaSize,byte[] data,DataPage[] dataPages)
{
if((int)Math.Ceiling(data.Length / (double)dataPageDataAreaSize) > dataPages.Length){
throw new Exception("There isn't enough data pages to store data ! Data needs '" + (int)Math.Ceiling(data.Length / (double)dataPageDataAreaSize) + "' , but available '" + dataPages.Length + "'.");
}
//--- Store data to data page(s) -----------------------//
long position = 0;
for(int i=0;i<dataPages.Length;i++){
if((data.Length - position) > dataPageDataAreaSize){
byte[] d = new byte[dataPageDataAreaSize];
Array.Copy(data,position,d,0,d.Length);
dataPages[i].WriteData(d);
position += dataPageDataAreaSize;
}
else{
byte[] d = new byte[data.Length - position];
Array.Copy(data,position,d,0,d.Length);
dataPages[i].WriteData(d);
}
}
//------------------------------------------------------//
}
#endregion
#region method GetDataPages
/// <summary>
/// Gets specified number of free data pages. If free data pages won't exist, creates new ones.
/// Data pages are marked as used and OwnerDataPagePointer and NextDataPagePointer is set as needed.
/// </summary>
/// <param name="ownerDataPagePointer">Owner data page pointer that own first requested data page. If no owner then this value is 0.</param>
/// <param name="count">Number of data pages wanted.</param>
internal DataPage[] GetDataPages(long ownerDataPagePointer,int count)
{
if(!this.TableLocked){
throw new Exception("Table must be locked to acess GetDataPages() method !");
}
ArrayList freeDataPages = new ArrayList();
// Get free data pages count from table header
byte[] freeDataPagesCount = new byte[8];
SetFilePosition(52);
ReadFromFile(freeDataPagesCount,0,freeDataPagesCount.Length);
long nFreeDataPages = ldb_Utils.ByteToLong(freeDataPagesCount,0);
// We have plenty free data pages and enough for count requested, find requested count free data pages
if(nFreeDataPages > 1000 && nFreeDataPages > count){
long nextDataPagePointer = m_DatapagesStartOffset + 1;
while(freeDataPages.Count < count){
DataPage dataPage = new DataPage(m_DataPageDataAreaSize,this,nextDataPagePointer);
if(!dataPage.Used){
dataPage.Used = true;
freeDataPages.Add(dataPage);
}
nextDataPagePointer += m_DataPageDataAreaSize + 33;
}
// Decrease free data pages count in table header
SetFilePosition(52);
ReadFromFile(ldb_Utils.LongToByte(nFreeDataPages - count),0,8);
}
// Just create new data pages
else{
for(int i=0;i<count;i++){
byte[] dataPage = DataPage.CreateDataPage(m_DataPageDataAreaSize,true,0,0,0,new byte[m_DataPageDataAreaSize]);
GoToFileEnd();
long dataPageStartPointer = GetFilePosition();
WriteToFile(dataPage,0,dataPage.Length);
freeDataPages.Add(new DataPage(m_DataPageDataAreaSize,this,dataPageStartPointer));
}
}
// Relate data pages (chain)
for(int i=0;i<freeDataPages.Count;i++){
// First data page
if(i == 0){
// Owner data page poitner specified for first data page
if(ownerDataPagePointer > 0){
((DataPage)freeDataPages[i]).OwnerDataPagePointer = ownerDataPagePointer;
}
// There is continuing data page
if(freeDataPages.Count > 1){
((DataPage)freeDataPages[i]).NextDataPagePointer = ((DataPage)freeDataPages[i + 1]).Pointer;
}
}
// Last data page
else if(i == (freeDataPages.Count - 1)){
((DataPage)freeDataPages[i]).OwnerDataPagePointer = ((DataPage)freeDataPages[i - 1]).Pointer;
}
// Middle range data page
else{
((DataPage)freeDataPages[i]).OwnerDataPagePointer = ((DataPage)freeDataPages[i - 1]).Pointer;
((DataPage)freeDataPages[i]).NextDataPagePointer = ((DataPage)freeDataPages[i + 1]).Pointer;
}
}
DataPage[] retVal = new DataPage[freeDataPages.Count];
freeDataPages.CopyTo(retVal);
return retVal;
}
#endregion
#region method AddColumn
/// <summary>
/// Adds column to db file.
/// </summary>
/// <param name="column"></param>
internal void AddColumn(LDB_DataColumn column)
{
// Find free column data area
// Set position over version, free data pages count and data page data area size
m_pDbFile.Position = 68;
long freeColumnPosition = -1;
StreamLineReader r = new StreamLineReader(m_pDbFile);
// Loop all columns data areas, see it there any free left
for(int i=0;i<100;i++){
byte[] columnInfo = r.ReadLine();
if(columnInfo == null){
throw new Exception("Invalid columns data area length !");
}
// We found unused column data area
if(columnInfo[0] == '\0'){
freeColumnPosition = m_pDbFile.Position;
break;
}
}
m_FilePosition = m_pDbFile.Position;
if(freeColumnPosition != -1){
// TODO: If there is data ???
// Move to row start
SetFilePosition(GetFilePosition() - 102);
// Store column
byte[] columnData = column.ToColumnInfo();
WriteToFile(columnData,0,columnData.Length);
}
else{
throw new Exception("Couldn't find free column space ! ");
}
}
#endregion
#region method RemoveColumn
/// <summary>
/// Removes specified column from database file.
/// </summary>
/// <param name="column"></param>
internal void RemoveColumn(LDB_DataColumn column)
{
throw new Exception("TODO:");
}
#endregion
#region method ReadFromFile
/// <summary>
/// Reads data from file.
/// </summary>
/// <param name="data">Buffer where to store readed data..</param>
/// <param name="offset">Offset in array to where to start storing readed data.</param>
/// <param name="count">Number of bytes to read.</param>
/// <returns></returns>
internal int ReadFromFile(byte[] data,int offset,int count)
{
int readed = m_pDbFile.Read(data,offset,count);
m_FilePosition += readed;
return readed;
}
#endregion
#region method WriteToFile
/// <summary>
/// Writes data to file.
/// </summary>
/// <param name="data">Data to write.</param>
/// <param name="offset">Offset in array from where to start writing data.</param>
/// <param name="count">Number of bytes to write.</param>
/// <returns></returns>
internal void WriteToFile(byte[] data,int offset,int count)
{
m_pDbFile.Write(data,offset,count);
m_FilePosition += count;
}
#endregion
#region method GetFilePosition
/// <summary>
/// Gets current position in file.
/// </summary>
/// <returns></returns>
internal long GetFilePosition()
{
return m_FilePosition;
}
#endregion
#region method SetFilePosition
/// <summary>
/// Sets file position.
/// </summary>
/// <param name="position">Position in file.</param>
internal void SetFilePosition(long position)
{
if(m_FilePosition != position){
m_pDbFile.Position = position;
m_FilePosition = position;
}
}
#endregion
#region method GoToFileEnd
/// <summary>
/// Moves position to the end of file.
/// </summary>
private void GoToFileEnd()
{
m_pDbFile.Position = m_pDbFile.Length;
m_FileLength = m_pDbFile.Length;
m_FilePosition = m_FileLength;
}
#endregion
#region Properties Implementation
/// <summary>
/// Gets if there is active database file.
/// </summary>
public bool IsDatabaseOpen
{
get{ return m_pDbFile != null; }
}
/// <summary>
/// Gets open database file name. Throws exception if database isn't open.
/// </summary>
public string FileName
{
get{
if(!this.IsDatabaseOpen){
throw new Exception("Database isn't open, please open database first !");
}
return m_DbFileName;
}
}
/// <summary>
/// Gets table columns. Throws exception if database isn't open.
/// </summary>
public LDB_DataColumnCollection Columns
{
get{
if(!this.IsDatabaseOpen){
throw new Exception("Database isn't open, please open database first !");
}
return m_pColumns;
}
}
/// <summary>
/// Gets current record. Returns null if there isn't current record.
/// </summary>
public LDB_Record CurrentRecord
{
get{ return m_pCurrentRecord; }
}
/// <summary>
/// Gets table is locked.
/// </summary>
public bool TableLocked
{
get{ return m_TableLocked; }
}
/// <summary>
/// Gets how much data data page can store.
/// </summary>
public int DataPageDataAreaSize
{
get{ return m_DataPageDataAreaSize; }
}
#endregion
}
}