initial commit
This commit is contained in:
12
LibTSM/Util/DatabaseClasses/Constants.lua
Normal file
12
LibTSM/Util/DatabaseClasses/Constants.lua
Normal file
@@ -0,0 +1,12 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local Constants = TSM.Init("Util.DatabaseClasses.Constants")
|
||||
Constants.DB_INDEX_FIELD_SEP = "~"
|
||||
Constants.DB_INDEX_VALUE_SEP = "\001"
|
||||
Constants.OTHER_FIELD_QUERY_PARAM = newproxy()
|
||||
Constants.BOUND_QUERY_PARAM = newproxy()
|
||||
1247
LibTSM/Util/DatabaseClasses/DBTable.lua
Normal file
1247
LibTSM/Util/DatabaseClasses/DBTable.lua
Normal file
File diff suppressed because it is too large
Load Diff
1619
LibTSM/Util/DatabaseClasses/Query.lua
Normal file
1619
LibTSM/Util/DatabaseClasses/Query.lua
Normal file
File diff suppressed because it is too large
Load Diff
447
LibTSM/Util/DatabaseClasses/QueryClause.lua
Normal file
447
LibTSM/Util/DatabaseClasses/QueryClause.lua
Normal file
@@ -0,0 +1,447 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local QueryClause = TSM.Init("Util.DatabaseClasses.QueryClause")
|
||||
local Constants = TSM.Include("Util.DatabaseClasses.Constants")
|
||||
local Util = TSM.Include("Util.DatabaseClasses.Util")
|
||||
local ObjectPool = TSM.Include("Util.ObjectPool")
|
||||
local LibTSMClass = TSM.Include("LibTSMClass")
|
||||
local DatabaseQueryClause = LibTSMClass.DefineClass("DatabaseQueryClause")
|
||||
local private = {
|
||||
objectPool = nil,
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Loading
|
||||
-- ============================================================================
|
||||
|
||||
QueryClause:OnModuleLoad(function()
|
||||
private.objectPool = ObjectPool.New("DATABASE_QUERY_CLAUSES", DatabaseQueryClause, 1)
|
||||
end)
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function QueryClause.Get(query, parent)
|
||||
local clause = private.objectPool:Get()
|
||||
clause:_Acquire(query, parent)
|
||||
return clause
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Class Method Methods
|
||||
-- ============================================================================
|
||||
|
||||
function DatabaseQueryClause.__init(self)
|
||||
self._query = nil
|
||||
self._operation = nil
|
||||
self._parent = nil
|
||||
-- comparison
|
||||
self._field = nil
|
||||
self._value = nil
|
||||
self._boundValue = nil
|
||||
self._otherField = nil
|
||||
-- or / and
|
||||
self._subClauses = {}
|
||||
end
|
||||
|
||||
function DatabaseQueryClause._Acquire(self, query, parent)
|
||||
self._query = query
|
||||
self._parent = parent
|
||||
end
|
||||
|
||||
function DatabaseQueryClause._Release(self)
|
||||
self._query = nil
|
||||
self._operation = nil
|
||||
self._parent = nil
|
||||
self._field = nil
|
||||
self._value = nil
|
||||
self._boundValue = nil
|
||||
self._otherField = nil
|
||||
for _, clause in ipairs(self._subClauses) do
|
||||
clause:_Release()
|
||||
end
|
||||
wipe(self._subClauses)
|
||||
private.objectPool:Recycle(self)
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Public Class Method
|
||||
-- ============================================================================
|
||||
|
||||
function DatabaseQueryClause.Equal(self, field, value, otherField)
|
||||
return self:_SetComparisonOperation("EQUAL", field, value, otherField)
|
||||
end
|
||||
|
||||
function DatabaseQueryClause.NotEqual(self, field, value, otherField)
|
||||
return self:_SetComparisonOperation("NOT_EQUAL", field, value, otherField)
|
||||
end
|
||||
|
||||
function DatabaseQueryClause.LessThan(self, field, value, otherField)
|
||||
return self:_SetComparisonOperation("LESS", field, value, otherField)
|
||||
end
|
||||
|
||||
function DatabaseQueryClause.LessThanOrEqual(self, field, value, otherField)
|
||||
return self:_SetComparisonOperation("LESS_OR_EQUAL", field, value, otherField)
|
||||
end
|
||||
|
||||
function DatabaseQueryClause.GreaterThan(self, field, value, otherField)
|
||||
return self:_SetComparisonOperation("GREATER", field, value, otherField)
|
||||
end
|
||||
|
||||
function DatabaseQueryClause.GreaterThanOrEqual(self, field, value, otherField)
|
||||
return self:_SetComparisonOperation("GREATER_OR_EQUAL", field, value, otherField)
|
||||
end
|
||||
|
||||
function DatabaseQueryClause.Matches(self, field, value)
|
||||
return self:_SetComparisonOperation("MATCHES", field, value)
|
||||
end
|
||||
|
||||
function DatabaseQueryClause.Contains(self, field, value)
|
||||
return self:_SetComparisonOperation("CONTAINS", field, value)
|
||||
end
|
||||
|
||||
function DatabaseQueryClause.StartsWith(self, field, value)
|
||||
return self:_SetComparisonOperation("STARTS_WITH", field, value)
|
||||
end
|
||||
|
||||
function DatabaseQueryClause.IsNil(self, field)
|
||||
return self:_SetComparisonOperation("IS_NIL", field)
|
||||
end
|
||||
|
||||
function DatabaseQueryClause.IsNotNil(self, field)
|
||||
return self:_SetComparisonOperation("IS_NOT_NIL", field)
|
||||
end
|
||||
|
||||
function DatabaseQueryClause.Custom(self, func, arg)
|
||||
return self:_SetComparisonOperation("CUSTOM", func, arg)
|
||||
end
|
||||
|
||||
function DatabaseQueryClause.HashEqual(self, fields, value)
|
||||
return self:_SetComparisonOperation("HASH_EQUAL", fields, value)
|
||||
end
|
||||
|
||||
function DatabaseQueryClause.InTable(self, field, value)
|
||||
return self:_SetComparisonOperation("IN_TABLE", field, value)
|
||||
end
|
||||
|
||||
function DatabaseQueryClause.NotInTable(self, field, value)
|
||||
return self:_SetComparisonOperation("NOT_IN_TABLE", field, value)
|
||||
end
|
||||
|
||||
function DatabaseQueryClause.Or(self)
|
||||
return self:_SetSubClauseOperation("OR")
|
||||
end
|
||||
|
||||
function DatabaseQueryClause.And(self)
|
||||
return self:_SetSubClauseOperation("AND")
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Class Method
|
||||
-- ============================================================================
|
||||
|
||||
function DatabaseQueryClause._GetParent(self)
|
||||
return self._parent
|
||||
end
|
||||
|
||||
function DatabaseQueryClause._IsTrue(self, row)
|
||||
local value = self._value
|
||||
if value == Constants.BOUND_QUERY_PARAM then
|
||||
value = self._boundValue
|
||||
elseif value == Constants.OTHER_FIELD_QUERY_PARAM then
|
||||
value = row:GetField(self._otherField)
|
||||
end
|
||||
local operation = self._operation
|
||||
if operation == "EQUAL" then
|
||||
return row[self._field] == value
|
||||
elseif operation == "NOT_EQUAL" then
|
||||
return row[self._field] ~= value
|
||||
elseif operation == "LESS" then
|
||||
return row[self._field] < value
|
||||
elseif operation == "LESS_OR_EQUAL" then
|
||||
return row[self._field] <= value
|
||||
elseif operation == "GREATER" then
|
||||
return row[self._field] > value
|
||||
elseif operation == "GREATER_OR_EQUAL" then
|
||||
return row[self._field] >= value
|
||||
elseif operation == "MATCHES" then
|
||||
return strfind(strlower(row[self._field]), value) and true or false
|
||||
elseif operation == "CONTAINS" then
|
||||
return strfind(strlower(row[self._field]), value, 1, true) and true or false
|
||||
elseif operation == "STARTS_WITH" then
|
||||
return strsub(strlower(row[self._field]), 1, #value) == value
|
||||
elseif operation == "IS_NIL" then
|
||||
return row[self._field] == nil
|
||||
elseif operation == "IS_NOT_NIL" then
|
||||
return row[self._field] ~= nil
|
||||
elseif operation == "CUSTOM" then
|
||||
return self._field(row, value) and true or false
|
||||
elseif operation == "HASH_EQUAL" then
|
||||
return row:CalculateHash(self._field) == value
|
||||
elseif operation == "IN_TABLE" then
|
||||
return value[row[self._field]] ~= nil
|
||||
elseif operation == "NOT_IN_TABLE" then
|
||||
return value[row[self._field]] == nil
|
||||
elseif operation == "OR" then
|
||||
for i = 1, #self._subClauses do
|
||||
if self._subClauses[i]:_IsTrue(row) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
elseif operation == "AND" then
|
||||
for i = 1, #self._subClauses do
|
||||
if not self._subClauses[i]:_IsTrue(row) then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
else
|
||||
error("Invalid operation: " .. tostring(operation))
|
||||
end
|
||||
end
|
||||
|
||||
function DatabaseQueryClause._GetIndexValue(self, indexField)
|
||||
if self._operation == "EQUAL" then
|
||||
if self._field ~= indexField then
|
||||
return
|
||||
end
|
||||
if self._value == Constants.OTHER_FIELD_QUERY_PARAM then
|
||||
return
|
||||
elseif self._value == Constants.BOUND_QUERY_PARAM then
|
||||
local result = Util.ToIndexValue(self._boundValue)
|
||||
return result, result
|
||||
else
|
||||
local result = Util.ToIndexValue(self._value)
|
||||
return result, result
|
||||
end
|
||||
elseif self._operation == "LESS_OR_EQUAL" then
|
||||
if self._field ~= indexField then
|
||||
return
|
||||
end
|
||||
if self._value == Constants.OTHER_FIELD_QUERY_PARAM then
|
||||
return
|
||||
elseif self._value == Constants.BOUND_QUERY_PARAM then
|
||||
return nil, Util.ToIndexValue(self._boundValue)
|
||||
else
|
||||
return nil, Util.ToIndexValue(self._value)
|
||||
end
|
||||
elseif self._operation == "GREATER_OR_EQUAL" then
|
||||
if self._field ~= indexField then
|
||||
return
|
||||
end
|
||||
if self._value == Constants.OTHER_FIELD_QUERY_PARAM then
|
||||
return
|
||||
elseif self._value == Constants.BOUND_QUERY_PARAM then
|
||||
return Util.ToIndexValue(self._boundValue), nil
|
||||
else
|
||||
return Util.ToIndexValue(self._value), nil
|
||||
end
|
||||
elseif self._operation == "STARTS_WITH" then
|
||||
if self._field ~= indexField then
|
||||
return
|
||||
end
|
||||
local minValue = nil
|
||||
if self._value == Constants.OTHER_FIELD_QUERY_PARAM then
|
||||
return
|
||||
elseif self._value == Constants.BOUND_QUERY_PARAM then
|
||||
minValue = Util.ToIndexValue(self._boundValue)
|
||||
else
|
||||
minValue = Util.ToIndexValue(self._value)
|
||||
end
|
||||
-- calculate the max value
|
||||
assert(gsub(minValue, "\255", "") ~= "")
|
||||
local maxValue = nil
|
||||
for i = #minValue, 1, -1 do
|
||||
if strsub(minValue, i, i) ~= "\255" then
|
||||
maxValue = strsub(minValue, 1, i - 1)..strrep("\255", #minValue - i + 1)
|
||||
break
|
||||
end
|
||||
end
|
||||
return minValue, maxValue
|
||||
elseif self._operation == "OR" then
|
||||
local numSubClauses = #self._subClauses
|
||||
if numSubClauses == 0 then
|
||||
return
|
||||
end
|
||||
-- all of the subclauses need to support the same index
|
||||
local valueMin, valueMax = self._subClauses[1]:_GetIndexValue(indexField)
|
||||
for i = 2, numSubClauses do
|
||||
local subClauseValueMin, subClauseValueMax = self._subClauses[i]:_GetIndexValue(indexField)
|
||||
if subClauseValueMin ~= valueMin or subClauseValueMax ~= valueMax then
|
||||
return
|
||||
end
|
||||
end
|
||||
return valueMin, valueMax
|
||||
elseif self._operation == "AND" then
|
||||
-- get the most constrained range of index values from the subclauses
|
||||
local valueMin, valueMax = nil, nil
|
||||
for _, subClause in ipairs(self._subClauses) do
|
||||
local subClauseValueMin, subClauseValueMax = subClause:_GetIndexValue(indexField)
|
||||
if subClauseValueMin ~= nil and (valueMin == nil or subClauseValueMin > valueMin) then
|
||||
valueMin = subClauseValueMin
|
||||
end
|
||||
if subClauseValueMax ~= nil and (valueMax == nil or subClauseValueMax < valueMax) then
|
||||
valueMax = subClauseValueMax
|
||||
end
|
||||
end
|
||||
return valueMin, valueMax
|
||||
end
|
||||
end
|
||||
|
||||
function DatabaseQueryClause._GetTrigramIndexValue(self, indexField)
|
||||
if self._operation == "EQUAL" then
|
||||
if self._field ~= indexField then
|
||||
return
|
||||
end
|
||||
if self._value == Constants.OTHER_FIELD_QUERY_PARAM then
|
||||
return
|
||||
elseif self._value == Constants.BOUND_QUERY_PARAM then
|
||||
return self._boundValue
|
||||
else
|
||||
return self._value
|
||||
end
|
||||
elseif self._operation == "CONTAINS" then
|
||||
if self._field ~= indexField then
|
||||
return
|
||||
end
|
||||
if self._value == Constants.OTHER_FIELD_QUERY_PARAM then
|
||||
return
|
||||
elseif self._value == Constants.BOUND_QUERY_PARAM then
|
||||
return self._boundValue
|
||||
else
|
||||
return self._value
|
||||
end
|
||||
elseif self._operation == "OR" then
|
||||
-- all of the subclauses need to support the same trigram value
|
||||
local value = nil
|
||||
for i = 1, #self._subClauses do
|
||||
local subClause = self._subClauses[i]
|
||||
local subClauseValue = subClause:_GetTrigramIndexValue(indexField)
|
||||
if not subClauseValue then
|
||||
return
|
||||
end
|
||||
if i == 1 then
|
||||
value = subClauseValue
|
||||
elseif subClauseValue ~= value then
|
||||
return
|
||||
end
|
||||
end
|
||||
return value
|
||||
elseif self._operation == "AND" then
|
||||
-- at least one of the subclauses need to support the trigram
|
||||
for _, subClause in ipairs(self._subClauses) do
|
||||
local value = subClause:_GetTrigramIndexValue(indexField)
|
||||
if value then
|
||||
return value
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function DatabaseQueryClause._IsStrictIndex(self, indexField, indexValueMin, indexValueMax)
|
||||
if self._value == Constants.OTHER_FIELD_QUERY_PARAM then
|
||||
return false
|
||||
end
|
||||
if self._operation == "EQUAL" and self._field == indexField and indexValueMin == indexValueMax then
|
||||
if self._value == Constants.BOUND_QUERY_PARAM then
|
||||
return Util.ToIndexValue(self._boundValue) == indexValueMin
|
||||
else
|
||||
return Util.ToIndexValue(self._value) == indexValueMin
|
||||
end
|
||||
elseif self._operation == "GREATER_OR_EQUAL" and self._field == indexField then
|
||||
if self._value == Constants.BOUND_QUERY_PARAM then
|
||||
return Util.ToIndexValue(self._boundValue) == indexValueMin
|
||||
else
|
||||
return Util.ToIndexValue(self._value) == indexValueMin
|
||||
end
|
||||
elseif self._operation == "LESS_OR_EQUAL" and self._field == indexField then
|
||||
if self._value == Constants.BOUND_QUERY_PARAM then
|
||||
return Util.ToIndexValue(self._boundValue) == indexValueMax
|
||||
else
|
||||
return Util.ToIndexValue(self._value) == indexValueMax
|
||||
end
|
||||
elseif self._operation == "OR" and #self._subClauses == 1 then
|
||||
return self._subClauses[1]:_IsStrictIndex(indexField, indexValueMin, indexValueMax)
|
||||
elseif self._operation == "AND" then
|
||||
-- must be strict for all subclauses
|
||||
for _, subClause in ipairs(self._subClauses) do
|
||||
if not subClause:_IsStrictIndex(indexField, indexValueMin, indexValueMax) then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
function DatabaseQueryClause._UsesField(self, field)
|
||||
if field == self._field or self._operation == "CUSTOM" then
|
||||
return true
|
||||
end
|
||||
if self._operation == "OR" or self._operation == "AND" then
|
||||
for i = 1, #self._subClauses do
|
||||
if self._subClauses[i]:_UsesField(field) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function DatabaseQueryClause._InsertSubClause(self, subClause)
|
||||
assert(self._operation == "OR" or self._operation == "AND")
|
||||
tinsert(self._subClauses, subClause)
|
||||
self._query:_MarkResultStale()
|
||||
return self
|
||||
end
|
||||
|
||||
function DatabaseQueryClause._SetComparisonOperation(self, operation, field, value, otherField)
|
||||
assert(not self._operation)
|
||||
assert(value == Constants.OTHER_FIELD_QUERY_PARAM or not otherField)
|
||||
self._operation = operation
|
||||
self._field = field
|
||||
self._value = value
|
||||
self._otherField = otherField
|
||||
self._query:_MarkResultStale()
|
||||
return self
|
||||
end
|
||||
|
||||
function DatabaseQueryClause._SetSubClauseOperation(self, operation)
|
||||
assert(not self._operation)
|
||||
self._operation = operation
|
||||
assert(#self._subClauses == 0)
|
||||
self._query:_MarkResultStale()
|
||||
return self
|
||||
end
|
||||
|
||||
function DatabaseQueryClause._BindParams(self, ...)
|
||||
if self._value == Constants.BOUND_QUERY_PARAM then
|
||||
self._boundValue = ...
|
||||
self._query:_MarkResultStale()
|
||||
return 1
|
||||
end
|
||||
local valuesUsed = 0
|
||||
for _, clause in ipairs(self._subClauses) do
|
||||
valuesUsed = valuesUsed + clause:_BindParams(select(valuesUsed + 1, ...))
|
||||
end
|
||||
self._query:_MarkResultStale()
|
||||
return valuesUsed
|
||||
end
|
||||
297
LibTSM/Util/DatabaseClasses/QueryResultRow.lua
Normal file
297
LibTSM/Util/DatabaseClasses/QueryResultRow.lua
Normal file
@@ -0,0 +1,297 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local QueryResultRow = TSM.Init("Util.DatabaseClasses.QueryResultRow")
|
||||
local Math = TSM.Include("Util.Math")
|
||||
local TempTable = TSM.Include("Util.TempTable")
|
||||
local ObjectPool = TSM.Include("Util.ObjectPool")
|
||||
local private = {
|
||||
context = {},
|
||||
objectPool = nil,
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Metatable
|
||||
-- ============================================================================
|
||||
|
||||
local ROW_PROTOTYPE = {
|
||||
_Acquire = function(self, db, query, newRowUUID)
|
||||
local context = private.context[self]
|
||||
context.db = db
|
||||
context.query = query
|
||||
context.isNewRow = newRowUUID and true or false
|
||||
if newRowUUID then
|
||||
context.uuid = newRowUUID
|
||||
end
|
||||
end,
|
||||
|
||||
_Release = function(self)
|
||||
local context = private.context[self]
|
||||
context.db = nil
|
||||
context.query = nil
|
||||
context.isNewRow = nil
|
||||
context.uuid = nil
|
||||
assert(not context.pendingChanges)
|
||||
wipe(self)
|
||||
end,
|
||||
|
||||
Release = function(self)
|
||||
self:_Release()
|
||||
private.objectPool:Recycle(self)
|
||||
end,
|
||||
|
||||
_SetUUID = function(self, uuid)
|
||||
local context = private.context[self]
|
||||
context.uuid = uuid
|
||||
wipe(self)
|
||||
end,
|
||||
|
||||
GetUUID = function(self)
|
||||
local uuid = private.context[self].uuid
|
||||
assert(uuid)
|
||||
return uuid
|
||||
end,
|
||||
|
||||
GetQuery = function(self)
|
||||
local query = private.context[self].query
|
||||
assert(query)
|
||||
return query
|
||||
end,
|
||||
|
||||
GetField = function(self, field, ...)
|
||||
if ... then
|
||||
error("GetField() only supports 1 field")
|
||||
end
|
||||
return self[field]
|
||||
end,
|
||||
|
||||
GetFields = function(self, ...)
|
||||
local numFields = select("#", ...)
|
||||
local field1, field2, field3, field4, field5, field6, field7, field8, field9, field10 = ...
|
||||
if numFields == 0 then
|
||||
return
|
||||
elseif numFields == 1 then
|
||||
return self[field1]
|
||||
elseif numFields == 2 then
|
||||
return self[field1], self[field2]
|
||||
elseif numFields == 3 then
|
||||
return self[field1], self[field2], self[field3]
|
||||
elseif numFields == 4 then
|
||||
return self[field1], self[field2], self[field3], self[field4]
|
||||
elseif numFields == 5 then
|
||||
return self[field1], self[field2], self[field3], self[field4], self[field5]
|
||||
elseif numFields == 6 then
|
||||
return self[field1], self[field2], self[field3], self[field4], self[field5], self[field6]
|
||||
elseif numFields == 7 then
|
||||
return self[field1], self[field2], self[field3], self[field4], self[field5], self[field6], self[field7]
|
||||
elseif numFields == 8 then
|
||||
return self[field1], self[field2], self[field3], self[field4], self[field5], self[field6], self[field7], self[field8]
|
||||
elseif numFields == 9 then
|
||||
return self[field1], self[field2], self[field3], self[field4], self[field5], self[field6], self[field7], self[field8], self[field9]
|
||||
elseif numFields == 10 then
|
||||
return self[field1], self[field2], self[field3], self[field4], self[field5], self[field6], self[field7], self[field8], self[field9], self[field10]
|
||||
else
|
||||
error("GetFields() only supports up to 10 fields")
|
||||
end
|
||||
end,
|
||||
|
||||
CalculateHash = function(self, fields)
|
||||
local hash = nil
|
||||
for _, field in ipairs(fields) do
|
||||
hash = Math.CalculateHash(self[field], hash)
|
||||
end
|
||||
return hash
|
||||
end,
|
||||
|
||||
SetField = function(self, field, value)
|
||||
local context = private.context[self]
|
||||
local isSameValue = not context.isNewRow and value == self[field]
|
||||
if isSameValue and not context.pendingChanges then
|
||||
-- setting to the same value, so ignore this call
|
||||
return self
|
||||
end
|
||||
if context.db:_IsSmartMapField(field) then
|
||||
error(format("Cannot set smart map field (%s)", tostring(field)), 3)
|
||||
end
|
||||
local fieldType = context.db:_GetFieldType(field)
|
||||
if not fieldType then
|
||||
error(format("Field %s doesn't exist", tostring(field)), 3)
|
||||
elseif fieldType ~= type(value) then
|
||||
error(format("Field %s should be a %s, got %s", tostring(field), tostring(fieldType), type(value)), 2)
|
||||
end
|
||||
if isSameValue then
|
||||
-- setting the field to its original value, so clear any pending change
|
||||
context.pendingChanges[field] = nil
|
||||
if not next(context.pendingChanges) then
|
||||
TempTable.Release(context.pendingChanges)
|
||||
context.pendingChanges = nil
|
||||
end
|
||||
else
|
||||
context.pendingChanges = context.pendingChanges or TempTable.Acquire()
|
||||
context.pendingChanges[field] = value
|
||||
end
|
||||
return self
|
||||
end,
|
||||
|
||||
_CreateHelper = function(self)
|
||||
local context = private.context[self]
|
||||
assert(context.isNewRow and context.pendingChanges)
|
||||
|
||||
-- make sure all the fields are set
|
||||
for field in context.db:FieldIterator() do
|
||||
assert(context.pendingChanges[field] ~= nil)
|
||||
end
|
||||
|
||||
-- apply all the pending changes
|
||||
for field, value in pairs(context.pendingChanges) do
|
||||
-- cache this new value
|
||||
rawset(self, field, value)
|
||||
end
|
||||
|
||||
TempTable.Release(context.pendingChanges)
|
||||
context.pendingChanges = nil
|
||||
context.isNewRow = nil
|
||||
end,
|
||||
|
||||
Create = function(self)
|
||||
self:_CreateHelper()
|
||||
private.context[self].db:_InsertRow(self)
|
||||
end,
|
||||
|
||||
CreateAndClone = function(self)
|
||||
self:_CreateHelper()
|
||||
local clonedRow = self:Clone()
|
||||
private.context[self].db:_InsertRow(self)
|
||||
return clonedRow
|
||||
end,
|
||||
|
||||
Update = function(self)
|
||||
local context = private.context[self]
|
||||
assert(not context.isNewRow)
|
||||
if not context.pendingChanges then
|
||||
return
|
||||
end
|
||||
|
||||
-- apply all the pending changes
|
||||
local oldValues = TempTable.Acquire()
|
||||
for field, value in pairs(context.pendingChanges) do
|
||||
oldValues[field] = self[field]
|
||||
-- cache this new value
|
||||
rawset(self, field, value)
|
||||
end
|
||||
|
||||
TempTable.Release(context.pendingChanges)
|
||||
context.pendingChanges = nil
|
||||
context.db:_UpdateRow(self, oldValues)
|
||||
TempTable.Release(oldValues)
|
||||
return self
|
||||
end,
|
||||
|
||||
CreateOrUpdateAndRelease = function(self)
|
||||
local context = private.context[self]
|
||||
if context.isNewRow then
|
||||
self:Create()
|
||||
else
|
||||
self:Update()
|
||||
self:Release()
|
||||
end
|
||||
end,
|
||||
|
||||
Clone = function(self)
|
||||
local context = private.context[self]
|
||||
assert(not context.isNewRow and not context.pendingChanges)
|
||||
local newRow = QueryResultRow.Get()
|
||||
newRow:_Acquire(context.db)
|
||||
newRow:_SetUUID(context.uuid)
|
||||
return newRow
|
||||
end,
|
||||
}
|
||||
|
||||
local ROW_MT = {
|
||||
-- getter
|
||||
__index = function(self, key)
|
||||
if key == nil then
|
||||
error("Attempt to get nil key")
|
||||
end
|
||||
if ROW_PROTOTYPE[key] then
|
||||
return ROW_PROTOTYPE[key]
|
||||
end
|
||||
-- cache the value
|
||||
local context = private.context[self]
|
||||
if context.isNewRow then
|
||||
error("Getting value on a new row: "..tostring(key))
|
||||
end
|
||||
local result = nil
|
||||
if context.query then
|
||||
-- use the query to lookup the result
|
||||
result = context.query:_GetResultRowData(context.uuid, key)
|
||||
else
|
||||
-- we're not tied to a query so this should be a local DB field
|
||||
if not context.db:_GetFieldType(key) then
|
||||
error("Invalid field: "..tostring(key), 2)
|
||||
end
|
||||
result = context.db:GetRowFieldByUUID(context.uuid, key)
|
||||
end
|
||||
if result ~= nil then
|
||||
rawset(self, key, result)
|
||||
end
|
||||
return result
|
||||
end,
|
||||
-- setter
|
||||
__newindex = function(self, key, value)
|
||||
error("Table is read-only", 2)
|
||||
end,
|
||||
__eq = function(self, other)
|
||||
local uuid = private.context[self].uuid
|
||||
local uuidOther = private.context[other].uuid
|
||||
return uuid and uuidOther and uuid == uuidOther
|
||||
end,
|
||||
__tostring = function(self)
|
||||
local context = private.context[self]
|
||||
return "QueryResultRow:"..strmatch(tostring(context), "table:[^0-9a-fA-F]*([0-9a-fA-F]+)")..":"..self:GetUUID()
|
||||
end,
|
||||
__metatable = false,
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Loading
|
||||
-- ============================================================================
|
||||
|
||||
QueryResultRow:OnModuleLoad(function()
|
||||
private.objectPool = ObjectPool.New("DATABASE_QUERY_RESULT_ROWS", private.CreateNew, 2)
|
||||
end)
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function QueryResultRow.Get()
|
||||
return private.objectPool:Get()
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.CreateNew()
|
||||
local row = setmetatable({}, ROW_MT)
|
||||
private.context[row] = {
|
||||
db = nil,
|
||||
query = nil,
|
||||
isNewRow = nil,
|
||||
uuid = nil,
|
||||
}
|
||||
return row
|
||||
end
|
||||
198
LibTSM/Util/DatabaseClasses/Schema.lua
Normal file
198
LibTSM/Util/DatabaseClasses/Schema.lua
Normal file
@@ -0,0 +1,198 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local Schema = TSM.Init("Util.DatabaseClasses.Schema")
|
||||
local Constants = TSM.Include("Util.DatabaseClasses.Constants")
|
||||
local DBTable = TSM.Include("Util.DatabaseClasses.DBTable")
|
||||
local ObjectPool = TSM.Include("Util.ObjectPool")
|
||||
local LibTSMClass = TSM.Include("LibTSMClass")
|
||||
local DatabaseSchema = LibTSMClass.DefineClass("DatabaseSchema")
|
||||
local private = {
|
||||
objectPool = nil,
|
||||
}
|
||||
local FIELD_TYPE_IS_VALID = {
|
||||
string = true,
|
||||
number = true,
|
||||
boolean = true,
|
||||
}
|
||||
local MAX_MULTI_FIELD_INDEX_PARTS = 2
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Modules Functions
|
||||
-- ============================================================================
|
||||
|
||||
function Schema.Get(name)
|
||||
if not private.objectPool then
|
||||
private.objectPool = ObjectPool.New("DATABASE_SCHEMAS", DatabaseSchema, 2)
|
||||
end
|
||||
local schema = private.objectPool:Get()
|
||||
schema:_Acquire(name)
|
||||
return schema
|
||||
end
|
||||
|
||||
function Schema.IsClass(obj)
|
||||
return obj:__isa(DatabaseSchema)
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Class Method Methods
|
||||
-- ============================================================================
|
||||
|
||||
function DatabaseSchema.__init(self)
|
||||
self._name = nil
|
||||
self._fieldList = {}
|
||||
self._fieldTypeLookup = {}
|
||||
self._isIndex = {}
|
||||
self._isUnique = {}
|
||||
self._smartMapLookup = {}
|
||||
self._smartMapInputLookup = {}
|
||||
self._trigramIndexField = nil
|
||||
end
|
||||
|
||||
function DatabaseSchema._Acquire(self, name)
|
||||
assert(type(name) == "string")
|
||||
self._name = name
|
||||
end
|
||||
|
||||
function DatabaseSchema._Release(self)
|
||||
self._name = nil
|
||||
wipe(self._fieldList)
|
||||
wipe(self._fieldTypeLookup)
|
||||
wipe(self._isIndex)
|
||||
wipe(self._isUnique)
|
||||
wipe(self._smartMapLookup)
|
||||
wipe(self._smartMapInputLookup)
|
||||
self._trigramIndexField = nil
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Public Class Method
|
||||
-- ============================================================================
|
||||
|
||||
function DatabaseSchema.Release(self)
|
||||
self:_Release()
|
||||
private.objectPool:Recycle(self)
|
||||
end
|
||||
|
||||
function DatabaseSchema.AddStringField(self, fieldName)
|
||||
self:_AddField("string", fieldName)
|
||||
return self
|
||||
end
|
||||
|
||||
function DatabaseSchema.AddNumberField(self, fieldName)
|
||||
self:_AddField("number", fieldName)
|
||||
return self
|
||||
end
|
||||
|
||||
function DatabaseSchema.AddBooleanField(self, fieldName)
|
||||
self:_AddField("boolean", fieldName)
|
||||
return self
|
||||
end
|
||||
|
||||
function DatabaseSchema.AddUniqueStringField(self, fieldName)
|
||||
self:_AddField("string", fieldName, true)
|
||||
self._isUnique[fieldName] = true
|
||||
return self
|
||||
end
|
||||
|
||||
function DatabaseSchema.AddUniqueNumberField(self, fieldName)
|
||||
self:_AddField("number", fieldName, true)
|
||||
return self
|
||||
end
|
||||
|
||||
function DatabaseSchema.AddSmartMapField(self, fieldName, map, inputFieldName)
|
||||
assert(self._fieldTypeLookup[inputFieldName] == map:GetKeyType())
|
||||
self:_AddField(map:GetValueType(), fieldName)
|
||||
self._smartMapLookup[fieldName] = map
|
||||
self._smartMapInputLookup[fieldName] = inputFieldName
|
||||
return self
|
||||
end
|
||||
|
||||
function DatabaseSchema.AddIndex(self, ...)
|
||||
local numFields = select("#", ...)
|
||||
assert(numFields > 0)
|
||||
assert(numFields <= MAX_MULTI_FIELD_INDEX_PARTS, "Unsupported number of fields in index")
|
||||
for i = 1, numFields do
|
||||
local fieldName = select(i, ...)
|
||||
assert(self._fieldTypeLookup[fieldName])
|
||||
end
|
||||
self._isIndex[strjoin(Constants.DB_INDEX_FIELD_SEP, ...)] = true
|
||||
return self
|
||||
end
|
||||
|
||||
function DatabaseSchema.AddTrigramIndex(self, fieldName)
|
||||
assert(not self._trigramIndexField)
|
||||
self._trigramIndexField = fieldName
|
||||
return self
|
||||
end
|
||||
|
||||
function DatabaseSchema.Commit(self)
|
||||
local db = DBTable.Create(self)
|
||||
self:Release()
|
||||
return db
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Class Method
|
||||
-- ============================================================================
|
||||
|
||||
function DatabaseSchema._GetName(self)
|
||||
return self._name
|
||||
end
|
||||
|
||||
function DatabaseSchema._AddField(self, fieldType, fieldName, isUnique)
|
||||
assert(FIELD_TYPE_IS_VALID[fieldType])
|
||||
assert(type(fieldName) == "string" and strsub(fieldName, 1, 1) ~= "_" and not strmatch(fieldName, Constants.DB_INDEX_FIELD_SEP))
|
||||
assert(not self._fieldTypeLookup[fieldName])
|
||||
tinsert(self._fieldList, fieldName)
|
||||
self._fieldTypeLookup[fieldName] = fieldType
|
||||
if isUnique then
|
||||
self._isUnique[fieldName] = true
|
||||
end
|
||||
end
|
||||
|
||||
function DatabaseSchema._FieldIterator(self)
|
||||
return private.FieldIterator, self, 0
|
||||
end
|
||||
|
||||
function DatabaseSchema._MultiFieldIndexIterator(self)
|
||||
return private.MultiFieldIndexIterator, self, nil
|
||||
end
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.FieldIterator(self, index)
|
||||
index = index + 1
|
||||
if index > #self._fieldList then
|
||||
return
|
||||
end
|
||||
local fieldName = self._fieldList[index]
|
||||
return index, fieldName, self._fieldTypeLookup[fieldName], self._isIndex[fieldName], self._isUnique[fieldName], self._smartMapLookup[fieldName], self._smartMapInputLookup[fieldName]
|
||||
end
|
||||
|
||||
function private.MultiFieldIndexIterator(self, fieldName)
|
||||
while true do
|
||||
fieldName = next(self._isIndex, fieldName)
|
||||
if not fieldName then
|
||||
return
|
||||
end
|
||||
if strmatch(fieldName, Constants.DB_INDEX_FIELD_SEP) then
|
||||
return fieldName
|
||||
end
|
||||
end
|
||||
end
|
||||
31
LibTSM/Util/DatabaseClasses/Util.lua
Normal file
31
LibTSM/Util/DatabaseClasses/Util.lua
Normal file
@@ -0,0 +1,31 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local Util = TSM.Init("Util.DatabaseClasses.Util")
|
||||
local Math = TSM.Include("Util.Math")
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function Util.ToIndexValue(value)
|
||||
if value == nil then
|
||||
return nil
|
||||
end
|
||||
local valueType = type(value)
|
||||
if valueType == "string" then
|
||||
return strlower(value)
|
||||
elseif valueType == "boolean" then
|
||||
return value and 1 or 0
|
||||
elseif valueType == "number" and Math.IsNan(value) then
|
||||
return nil
|
||||
else
|
||||
return value
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user