-------------------------------------------------------------------------------- -- Title: Finder.lua -- Description: Like a square peg in a round hole -- Author: Raphaël Szwarc http://alt.textdrive.com/lua/ -- Creation Date: January 30, 2007 -- Legal: Copyright (C) 2007 Raphaël Szwarc -- Under the terms of the MIT License -- http://www.opensource.org/licenses/mit-license.html -------------------------------------------------------------------------------- -- import dependencies local Template = require( 'Template' ) local io = require( 'io' ) local string = require( 'string' ) local assert = assert local error = error local getmetatable = getmetatable local ipairs = ipairs local loadstring = loadstring local pairs = pairs local pcall = pcall local require = require local setmetatable = setmetatable local tonumber = tonumber local tostring = tostring local type = type -------------------------------------------------------------------------------- -- Finder -------------------------------------------------------------------------------- module( 'Finder' ) _VERSION = '1.0' local self = setmetatable( _M, {} ) local meta = getmetatable( self ) -------------------------------------------------------------------------------- -- DDL -------------------------------------------------------------------------------- local function Path( anExtension ) return ( '%s%s.%s' ):format( require( 'Bundle' )(), self._NAME, anExtension ) end local function Load( anExtension ) local aPath = Path( anExtension ) local aFile = assert( io.open( aPath, 'rb' ) ) local aContent = assert( aFile:read( '*a' ) ) local aContent = ( 'return { %s }' ):format( aContent ) local aChunk = assert( loadstring( aContent ) ) local aStatement = assert( aChunk() ) aFile:close() return aStatement end local DDL = Load( 'ddl' ) local DML = Load( 'dml' ) local PartitionCount = 10 -------------------------------------------------------------------------------- -- DB -------------------------------------------------------------------------------- local function Try( aFunction, aDB, ... ) local ok, aResult = pcall( aFunction, aDB, ... ) if ok then return aResult end pcall( function() aDB( DML[ 'Rollback' ] ) end ) error( aResult ) end local function HexDecode( aString ) local aCoder = function( aValue ) return string.char( tonumber( aValue, 16 ) ) end return aString:gsub( '(%x%x)', aCoder ) end local function PartitionName( aNumber ) return ( 'P%02d' ):format( aNumber ) end local function Partition( aDocument ) local crypto = require( 'crypto' ) local aValue = HexDecode( crypto.sha1( aDocument ) ) local aHash = 0 for anIndex = 1, 4 do aHash = aHash * 256 + aValue:byte( anIndex ) end aHash = ( aHash % PartitionCount ) + 1 return PartitionName( aHash ) end local function PartitionPath( aURL ) local URL = require( 'URL' ) local aURL = URL( aURL ) local aPath = aURL.path aPath.absolute = false return tostring( aPath ) end local function CreateText( aDB, aPartition ) local aCall = function() local aStatement = Template( DML[ 'CreateText' ] ) aStatement[ 'partition' ] = aPartition aDB( aStatement ) end pcall( aCall ) end local function NewDB( aURL ) local DB = require( 'DB' ) local aDB = DB( aURL ) local aPath = PartitionPath( aURL ) for anIndex = 1, PartitionCount do local aStatement = Template( DML[ 'Attach' ] ) local aPartition = PartitionName( anIndex ) aStatement[ 'path' ] = ( '%s%02d' ):format( aPath, anIndex ) aStatement[ 'name' ] = aPartition aDB( aStatement ) for anIndex, aStatement in ipairs( DDL ) do local aStatement = Template( aStatement ) aStatement[ 'partition' ] = aPartition aDB( aStatement ) end CreateText( aDB, aPartition ) end return aDB end -------------------------------------------------------------------------------- -- DML -------------------------------------------------------------------------------- local function InsertDocument( aDB, aDocument, aContent ) local aPartition = Partition( aDocument ) local aStatement = nil aDB( DML[ 'BeginTransaction' ] ) aStatement = Template( DML[ 'InsertDocument' ] ) aStatement[ 'partition' ] = aPartition aDB( aStatement, aDocument ) aStatement = Template( DML[ 'DeleteText' ] ) aStatement[ 'partition' ] = aPartition aDB( aStatement, aDocument ) aStatement = Template( DML[ 'InsertText' ] ) aStatement[ 'partition' ] = aPartition aDB( aStatement, aContent, aDocument ) aDB( DML[ 'EndTransaction' ] ) end local function DeleteDocument( aDB, aDocument ) local aPartition = Partition( aDocument ) local aStatement = nil aDB( DML[ 'BeginTransaction' ] ) aStatement = Template( DML[ 'DeleteDocument' ] ) aStatement[ 'partition' ] = aPartition aDB( aStatement, aDocument ) aStatement = Template( DML[ 'DeleteText' ] ) aStatement[ 'partition' ] = aPartition aDB( aStatement, aDocument ) aDB( DML[ 'EndTransaction' ] ) end local function SelectDocument( aDB, aDocument ) local aPartition = Partition( aDocument ) local aStatement = Template( DML[ 'SelectDocumentContent' ] ) aStatement[ 'partition' ] = aPartition local anIterator = aDB( aStatement, aDocument ) return function() local aRow = anIterator() if aRow then return aRow.name, aRow.content end end end local function FindDocument( aDB, aQuery, aLimit ) local aLimit = aLimit or 999 local anIterator = nil aDB( DML[ 'BeginTransaction' ] ) aDB( DML[ 'DeleteHit' ] ) for anIndex = 1, PartitionCount do local aPartition = PartitionName( anIndex ) local aStatement = Template( DML[ 'FindDocumentText' ] ) aStatement[ 'partition' ] = aPartition aDB( aStatement, aQuery, aLimit ) end aDB( DML[ 'EndTransaction' ] ) anIterator = aDB( DML[ 'SelectHit' ], aLimit ) return function() local aRow = anIterator() if aRow then return aRow.name, aRow.extract end end end -------------------------------------------------------------------------------- -- Metamethods -------------------------------------------------------------------------------- function meta:__call( aURL ) local aDB = NewDB( aURL ) local aFinder = { db = aDB } setmetatable( aFinder, self ) return aFinder end function meta:__concat( aValue ) return tostring( self ) .. tostring( aValue ) end function meta:__tostring() return ( '%s/%s' ):format( self._NAME, self._VERSION ) end function self:__call( aQuery, aLimit ) return Try( FindDocument, self.db, aQuery, aLimit ) end function self:__index( aKey ) return Try( SelectDocument, self.db, aKey ) end function self:__newindex( aKey, aValue ) if type( aKey ) == 'string' and type( aValue ) == 'string' then return Try( InsertDocument, self.db, aKey, aValue ) elseif type( aKey ) == 'string' and type( aValue ) == 'boolean' and aValue == false then return Try( DeleteDocument, self.db, aKey ) end error( ( 'Invalid parameters: %q = %q' ):format( tostring( aKey ), tostring( aValue ) ) ) end function self:__concat( aValue ) return tostring( self ) .. tostring( aValue ) end function self:__tostring() return tostring( self.db ) end