mirror of
https://gitlab.com/freepascal.org/fpc/source.git
synced 2025-08-18 15:29:11 +02:00
* Patch from Luiz Americo
- implements AutoInc fields - add IndexFieldName: now the user can choose what field use as primary key (used to apply updates). There's no more necessity of using _ROWID_ - SetRecNo and GetRecNo are working - Fixed issues when used in lazarus (i will send a package to lazarus list)
This commit is contained in:
parent
102de9b240
commit
abe11d29c2
@ -1,4 +1,4 @@
|
|||||||
unit SqliteDS;
|
unit sqliteds;
|
||||||
|
|
||||||
{
|
{
|
||||||
This is SqliteDS/TSqliteDataset, a TDataset descendant class for use with fpc compiler
|
This is SqliteDS/TSqliteDataset, a TDataset descendant class for use with fpc compiler
|
||||||
@ -23,7 +23,9 @@ unit SqliteDS;
|
|||||||
|
|
||||||
{$Mode ObjFpc}
|
{$Mode ObjFpc}
|
||||||
{$H+}
|
{$H+}
|
||||||
{ $Define DEBUG}
|
{ $Define USE_SQLITEDS_INTERNALS}
|
||||||
|
{ $Define DEBUG}
|
||||||
|
|
||||||
interface
|
interface
|
||||||
|
|
||||||
uses Classes, SysUtils, Db;
|
uses Classes, SysUtils, Db;
|
||||||
@ -44,6 +46,10 @@ type
|
|||||||
FFileName: String;
|
FFileName: String;
|
||||||
FSql: String;
|
FSql: String;
|
||||||
FTableName: String;
|
FTableName: String;
|
||||||
|
FIndexFieldName: String;
|
||||||
|
FIndexFieldNo: Integer;
|
||||||
|
FAutoIncFieldNo: Integer;
|
||||||
|
FNextAutoInc:Integer;
|
||||||
FCurrentItem: PDataRecord;
|
FCurrentItem: PDataRecord;
|
||||||
FBeginItem: PDataRecord;
|
FBeginItem: PDataRecord;
|
||||||
FEndItem: PDataRecord;
|
FEndItem: PDataRecord;
|
||||||
@ -52,6 +58,9 @@ type
|
|||||||
FRowBufferSize: Integer;
|
FRowBufferSize: Integer;
|
||||||
FRowCount: Integer;
|
FRowCount: Integer;
|
||||||
FRecordCount: Integer;
|
FRecordCount: Integer;
|
||||||
|
FExpectedAppends: Integer;
|
||||||
|
FExpectedDeletes: Integer;
|
||||||
|
FExpectedUpdates: Integer;
|
||||||
FSqliteReturnId: Integer;
|
FSqliteReturnId: Integer;
|
||||||
FDataAllocated: Boolean;
|
FDataAllocated: Boolean;
|
||||||
FSaveOnClose: Boolean;
|
FSaveOnClose: Boolean;
|
||||||
@ -88,6 +97,9 @@ type
|
|||||||
function IsCursorOpen: Boolean; override;
|
function IsCursorOpen: Boolean; override;
|
||||||
procedure SetBookmarkData(Buffer: PChar; Data: Pointer); override;
|
procedure SetBookmarkData(Buffer: PChar; Data: Pointer); override;
|
||||||
procedure SetBookmarkFlag(Buffer: PChar; Value: TBookmarkFlag); override;
|
procedure SetBookmarkFlag(Buffer: PChar; Value: TBookmarkFlag); override;
|
||||||
|
procedure SetExpectedAppends(AValue:Integer);
|
||||||
|
procedure SetExpectedUpdates(AValue:Integer);
|
||||||
|
procedure SetExpectedDeletes(AValue:Integer);
|
||||||
procedure SetFieldData(Field: TField; Buffer: Pointer); override;
|
procedure SetFieldData(Field: TField; Buffer: Pointer); override;
|
||||||
procedure SetRecNo(Value: Integer); override;
|
procedure SetRecNo(Value: Integer); override;
|
||||||
public
|
public
|
||||||
@ -100,17 +112,27 @@ type
|
|||||||
function ExecSQL:Integer;
|
function ExecSQL:Integer;
|
||||||
function ExecSQL(ASql:String):Integer;
|
function ExecSQL(ASql:String):Integer;
|
||||||
function SqliteReturnString: String;
|
function SqliteReturnString: String;
|
||||||
{$Ifdef DEBUG}
|
{$ifdef USE_SQLITEDS_INTERNALS}
|
||||||
property BeginItem: PDataRecord read FBeginItem;
|
property BeginItem: PDataRecord read FBeginItem;
|
||||||
property EndItem: PDataRecord read FEndItem;
|
property EndItem: PDataRecord read FEndItem;
|
||||||
{$Endif}
|
property UpdatedItems: TList read FUpdatedItems;
|
||||||
|
property AddedItems: TList read FAddedItems;
|
||||||
|
property DeletedItems: TList read FDeletedItems;
|
||||||
|
{$endif}
|
||||||
|
property ExpectedAppends: Integer read FExpectedAppends write SetExpectedAppends;
|
||||||
|
property ExpectedUpdates: Integer read FExpectedUpdates write SetExpectedUpdates;
|
||||||
|
property ExpectedDeletes: Integer read FExpectedDeletes write SetExpectedDeletes;
|
||||||
property SqliteReturnId: Integer read FSqliteReturnId;
|
property SqliteReturnId: Integer read FSqliteReturnId;
|
||||||
|
published
|
||||||
|
property FileName: String read FFileName write FFileName;
|
||||||
|
property IndexFieldName: String read FIndexFieldName write FIndexFieldName;
|
||||||
|
property SaveOnClose: Boolean read FSaveOnClose write FSaveOnClose;
|
||||||
property SQL: String read FSql write SetSql;
|
property SQL: String read FSql write SetSql;
|
||||||
property TableName: String read FTableName write FTableName;
|
property TableName: String read FTableName write FTableName;
|
||||||
property FileName: String read FFileName write FFileName;
|
//property Active;
|
||||||
property SaveOnClose: Boolean read FSaveOnClose write FSaveOnClose;
|
property FieldDefs;
|
||||||
published
|
|
||||||
property Active;
|
//Events
|
||||||
property BeforeOpen;
|
property BeforeOpen;
|
||||||
property AfterOpen;
|
property AfterOpen;
|
||||||
property BeforeClose;
|
property BeforeClose;
|
||||||
@ -137,6 +159,21 @@ implementation
|
|||||||
|
|
||||||
uses SQLite;
|
uses SQLite;
|
||||||
|
|
||||||
|
function GetAutoIncValue(NextValue: Pointer; Columns: Integer; ColumnValues: PPChar; ColumnNames: PPChar): integer; cdecl;
|
||||||
|
var
|
||||||
|
CodeError, TempInt: Integer;
|
||||||
|
begin
|
||||||
|
TempInt:=-1;
|
||||||
|
if ColumnValues[0] <> nil then
|
||||||
|
begin
|
||||||
|
Val(StrPas(ColumnValues[0]),TempInt,CodeError);
|
||||||
|
if CodeError <> 0 then
|
||||||
|
DatabaseError('SqliteDs - Error trying to get last autoinc value');
|
||||||
|
end;
|
||||||
|
Integer(NextValue^):=Succ(TempInt);
|
||||||
|
Result:=1;
|
||||||
|
end;
|
||||||
|
|
||||||
function GetFieldDefs(TheDataset: Pointer; Columns: Integer; ColumnValues: PPChar; ColumnNames: PPChar): integer; cdecl;
|
function GetFieldDefs(TheDataset: Pointer; Columns: Integer; ColumnValues: PPChar; ColumnNames: PPChar): integer; cdecl;
|
||||||
var
|
var
|
||||||
FieldSize:Word;
|
FieldSize:Word;
|
||||||
@ -181,7 +218,15 @@ begin
|
|||||||
begin
|
begin
|
||||||
AType:= ftTime;
|
AType:= ftTime;
|
||||||
FieldSize:=SizeOf(TDateTime);
|
FieldSize:=SizeOf(TDateTime);
|
||||||
end else
|
end else if (ColumnStr = 'AUTOINC') then
|
||||||
|
begin
|
||||||
|
if TSqliteDataset(TheDataset).Tablename = '' then
|
||||||
|
DatabaseError('Sqliteds - AutoInc fields requires Tablename to be set');
|
||||||
|
AType:= ftAutoInc;
|
||||||
|
FieldSize:=SizeOf(Integer);
|
||||||
|
if TSqliteDataset(TheDataset).FAutoIncFieldNo = -1 then
|
||||||
|
TSqliteDataset(TheDataset).FAutoIncFieldNo:= Counter;
|
||||||
|
end else
|
||||||
begin
|
begin
|
||||||
AType:= ftString;
|
AType:= ftString;
|
||||||
FieldSize:=0;
|
FieldSize:=0;
|
||||||
@ -215,6 +260,11 @@ var
|
|||||||
ColumnNames,ColumnValues:PPChar;
|
ColumnNames,ColumnValues:PPChar;
|
||||||
Counter:Integer;
|
Counter:Integer;
|
||||||
begin
|
begin
|
||||||
|
//Get AutoInc Field initial value
|
||||||
|
if FAutoIncFieldNo <> -1 then
|
||||||
|
sqlite_exec(FSqliteHandle,PChar('Select Max('+Fields[FAutoIncFieldNo].FieldName+') from ' + FTableName),
|
||||||
|
@GetAutoIncValue,@FNextAutoInc,nil);
|
||||||
|
|
||||||
FSqliteReturnId:=sqlite_compile(FSqliteHandle,Pchar(FSql),nil,@vm,nil);
|
FSqliteReturnId:=sqlite_compile(FSqliteHandle,Pchar(FSql),nil,@vm,nil);
|
||||||
if FSqliteReturnId <> SQLITE_OK then
|
if FSqliteReturnId <> SQLITE_OK then
|
||||||
case FSqliteReturnId of
|
case FSqliteReturnId of
|
||||||
@ -223,6 +273,7 @@ begin
|
|||||||
else
|
else
|
||||||
DatabaseError('Unknow Error',Self);
|
DatabaseError('Unknow Error',Self);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
FDataAllocated:=True;
|
FDataAllocated:=True;
|
||||||
New(FBeginItem);
|
New(FBeginItem);
|
||||||
FBeginItem^.Next:=nil;
|
FBeginItem^.Next:=nil;
|
||||||
@ -358,7 +409,7 @@ begin
|
|||||||
begin
|
begin
|
||||||
Move(FieldRow^,PChar(Buffer)^,StrLen(FieldRow)+1);
|
Move(FieldRow^,PChar(Buffer)^,StrLen(FieldRow)+1);
|
||||||
end;
|
end;
|
||||||
ftInteger,ftBoolean,ftWord:
|
ftInteger,ftBoolean,ftWord,ftAutoInc:
|
||||||
begin
|
begin
|
||||||
Val(StrPas(FieldRow),LongInt(Buffer^),ValError);
|
Val(StrPas(FieldRow),LongInt(Buffer^),ValError);
|
||||||
Result:= ValError = 0;
|
Result:= ValError = 0;
|
||||||
@ -416,22 +467,27 @@ var
|
|||||||
TempItem,TempActive:PDataRecord;
|
TempItem,TempActive:PDataRecord;
|
||||||
begin
|
begin
|
||||||
Result:= -1;
|
Result:= -1;
|
||||||
|
if FRecordCount = 0 then
|
||||||
|
Exit;
|
||||||
TempItem:=FBeginItem;
|
TempItem:=FBeginItem;
|
||||||
TempActive:=PPDataRecord(ActiveBuffer)^;
|
TempActive:=PPDataRecord(ActiveBuffer)^;
|
||||||
while TempActive <> TempItem do
|
if TempActive = FCacheItem then // Record not posted yet
|
||||||
begin
|
Result:=FRecordCount
|
||||||
if TempItem^.Next <> nil then
|
else
|
||||||
|
while TempActive <> TempItem do
|
||||||
begin
|
begin
|
||||||
inc(Result);
|
if TempItem^.Next <> nil then
|
||||||
TempItem:=TempItem^.Next;
|
begin
|
||||||
end
|
inc(Result);
|
||||||
else
|
TempItem:=TempItem^.Next;
|
||||||
begin
|
end
|
||||||
Result:=-1;
|
else
|
||||||
DatabaseError('GetRecNo - ActiveItem Not Found',Self);
|
begin
|
||||||
break;
|
Result:=-1;
|
||||||
|
DatabaseError('Sqliteds.GetRecNo - ActiveItem Not Found',Self);
|
||||||
|
break;
|
||||||
|
end;
|
||||||
end;
|
end;
|
||||||
end;
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TSqliteDataset.GetRecordSize: Word;
|
function TSqliteDataset.GetRecordSize: Word;
|
||||||
@ -456,6 +512,8 @@ begin
|
|||||||
NewItem^.Next:=FEndItem;
|
NewItem^.Next:=FEndItem;
|
||||||
FEndItem^.Previous:=NewItem;
|
FEndItem^.Previous:=NewItem;
|
||||||
Inc(FRecordCount);
|
Inc(FRecordCount);
|
||||||
|
if FAutoIncFieldNo <> - 1 then
|
||||||
|
Inc(FNextAutoInc);
|
||||||
FAddedItems.Add(NewItem);
|
FAddedItems.Add(NewItem);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
@ -499,6 +557,10 @@ begin
|
|||||||
else
|
else
|
||||||
FCurrentItem:= FCurrentItem^.Next;
|
FCurrentItem:= FCurrentItem^.Next;
|
||||||
end;
|
end;
|
||||||
|
// Dec FNextAutoInc
|
||||||
|
if FAutoIncFieldNo <> -1 then
|
||||||
|
if StrToInt(StrPas(TempItem^.Row[FAutoIncFieldNo])) = (FNextAutoInc - 1) then
|
||||||
|
Dec(FNextAutoInc);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TSqliteDataset.InternalFirst;
|
procedure TSqliteDataset.InternalFirst;
|
||||||
@ -532,12 +594,19 @@ end;
|
|||||||
procedure TSqliteDataset.InternalInitRecord(Buffer: PChar);
|
procedure TSqliteDataset.InternalInitRecord(Buffer: PChar);
|
||||||
var
|
var
|
||||||
Counter:Integer;
|
Counter:Integer;
|
||||||
|
TempStr:String;
|
||||||
begin
|
begin
|
||||||
for Counter:= 0 to FRowCount - 1 do
|
for Counter:= 0 to FRowCount - 1 do
|
||||||
begin
|
begin
|
||||||
StrDispose(FCacheItem^.Row[Counter]);
|
StrDispose(FCacheItem^.Row[Counter]);
|
||||||
FCacheItem^.Row[Counter]:=nil;
|
FCacheItem^.Row[Counter]:=nil;
|
||||||
end;
|
end;
|
||||||
|
if FAutoIncFieldNo <> - 1 then
|
||||||
|
begin
|
||||||
|
Str(FNextAutoInc,TempStr);
|
||||||
|
FCacheItem^.Row[FAutoIncFieldNo]:=StrAlloc(Length(TempStr)+1);
|
||||||
|
StrPCopy(FCacheItem^.Row[FAutoIncFieldNo],TempStr);
|
||||||
|
end;
|
||||||
PPDataRecord(Buffer)^:=FCacheItem;
|
PPDataRecord(Buffer)^:=FCacheItem;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
@ -548,15 +617,22 @@ end;
|
|||||||
|
|
||||||
procedure TSqliteDataset.InternalOpen;
|
procedure TSqliteDataset.InternalOpen;
|
||||||
begin
|
begin
|
||||||
|
FAutoIncFieldNo:=-1;
|
||||||
if not FileExists(FFileName) then
|
if not FileExists(FFileName) then
|
||||||
DatabaseError('File '+FFileName+' not found',Self);
|
DatabaseError('File '+FFileName+' not found',Self);
|
||||||
FSqliteHandle:=sqlite_open(PChar(FFileName),0,FDBError);
|
FSqliteHandle:=sqlite_open(PChar(FFileName),0,nil);
|
||||||
InternalInitFieldDefs;
|
InternalInitFieldDefs;
|
||||||
BuildLinkedList;
|
|
||||||
FCurrentItem:=FBeginItem;
|
|
||||||
if DefaultFields then
|
if DefaultFields then
|
||||||
CreateFields;
|
CreateFields;
|
||||||
BindFields(True);
|
BindFields(True);
|
||||||
|
// Get indexfieldno if available
|
||||||
|
if FIndexFieldName <> '' then
|
||||||
|
FIndexFieldNo:=FieldByName(FIndexFieldName).Index
|
||||||
|
else
|
||||||
|
FIndexFieldNo:=FAutoIncFieldNo;
|
||||||
|
|
||||||
|
BuildLinkedList;
|
||||||
|
FCurrentItem:=FBeginItem;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TSqliteDataset.InternalPost;
|
procedure TSqliteDataset.InternalPost;
|
||||||
@ -585,6 +661,24 @@ begin
|
|||||||
PPDataRecord(Buffer)^^.BookmarkFlag := Value;
|
PPDataRecord(Buffer)^^.BookmarkFlag := Value;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
procedure TSqliteDataset.SetExpectedAppends(AValue:Integer);
|
||||||
|
begin
|
||||||
|
if Assigned(FAddedItems) then
|
||||||
|
FAddedItems.Capacity:=AValue;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TSqliteDataset.SetExpectedUpdates(AValue:Integer);
|
||||||
|
begin
|
||||||
|
if Assigned(FUpdatedItems) then
|
||||||
|
FUpdatedItems.Capacity:=AValue;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TSqliteDataset.SetExpectedDeletes(AValue:Integer);
|
||||||
|
begin
|
||||||
|
if Assigned(FDeletedItems) then
|
||||||
|
FDeletedItems.Capacity:=AValue;
|
||||||
|
end;
|
||||||
|
|
||||||
procedure TSqliteDataset.SetFieldData(Field: TField; Buffer: Pointer);
|
procedure TSqliteDataset.SetFieldData(Field: TField; Buffer: Pointer);
|
||||||
var
|
var
|
||||||
TempStr:String;
|
TempStr:String;
|
||||||
@ -622,8 +716,16 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TSqliteDataset.SetRecNo(Value: Integer);
|
procedure TSqliteDataset.SetRecNo(Value: Integer);
|
||||||
|
var
|
||||||
|
Counter:Integer;
|
||||||
|
TempItem:PDataRecord;
|
||||||
begin
|
begin
|
||||||
//
|
if Value >= FRecordCount then
|
||||||
|
DatabaseError('SqliteDs - Record Number Out Of Range');
|
||||||
|
TempItem:=FBeginItem;
|
||||||
|
for Counter := 0 to Value do
|
||||||
|
TempItem:=TempItem^.Next;
|
||||||
|
PPDataRecord(ActiveBuffer)^:=TempItem;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
// Specific functions
|
// Specific functions
|
||||||
@ -652,12 +754,19 @@ end;
|
|||||||
function TSqliteDataset.ApplyUpdates:Boolean;
|
function TSqliteDataset.ApplyUpdates:Boolean;
|
||||||
var
|
var
|
||||||
CounterFields,CounterItems:Integer;
|
CounterFields,CounterItems:Integer;
|
||||||
SqlTemp:String;
|
SqlTemp,KeyName:String;
|
||||||
Quote:Char;
|
Quote:Char;
|
||||||
begin
|
begin
|
||||||
Result:=False;
|
Result:=False;
|
||||||
if (FTableName <> '') and (Fields[0].FieldName = '_ROWID_') then
|
if (FTableName <> '') and (FIndexFieldNo <> -1) then
|
||||||
begin
|
begin
|
||||||
|
KeyName:=Fields[FIndexFieldNo].FieldName;
|
||||||
|
{$ifdef DEBUG}
|
||||||
|
if FIndexFieldNo = FAutoIncFieldNo then
|
||||||
|
WriteLn('Using an AutoInc field as primary key');
|
||||||
|
WriteLn('IndexFieldName: ',KeyName);
|
||||||
|
WriteLn('IndexFieldNo: ',FIndexFieldNo);
|
||||||
|
{$endif}
|
||||||
SqlTemp:='BEGIN TRANSACTION; ';
|
SqlTemp:='BEGIN TRANSACTION; ';
|
||||||
// Update changed records
|
// Update changed records
|
||||||
For CounterItems:= 0 to FUpdatedItems.Count - 1 do
|
For CounterItems:= 0 to FUpdatedItems.Count - 1 do
|
||||||
@ -678,7 +787,7 @@ begin
|
|||||||
SqlTemp:=SqlTemp + Fields[CounterFields].FieldName +' = NULL , ';
|
SqlTemp:=SqlTemp + Fields[CounterFields].FieldName +' = NULL , ';
|
||||||
end;
|
end;
|
||||||
system.delete(SqlTemp,Length(SqlTemp)-2,2);
|
system.delete(SqlTemp,Length(SqlTemp)-2,2);
|
||||||
SqlTemp:=SqlTemp+'WHERE _ROWID_ = '+StrPas(PDataRecord(FUpdatedItems[CounterItems])^.Row[0])+';';
|
SqlTemp:=SqlTemp+'WHERE '+KeyName+' = '+StrPas(PDataRecord(FUpdatedItems[CounterItems])^.Row[FIndexFieldNo])+';';
|
||||||
end;
|
end;
|
||||||
// Add new records
|
// Add new records
|
||||||
For CounterItems:= 0 to FAddedItems.Count - 1 do
|
For CounterItems:= 0 to FAddedItems.Count - 1 do
|
||||||
@ -711,8 +820,8 @@ begin
|
|||||||
// Delete Items
|
// Delete Items
|
||||||
For CounterItems:= 0 to FDeletedItems.Count - 1 do
|
For CounterItems:= 0 to FDeletedItems.Count - 1 do
|
||||||
begin
|
begin
|
||||||
SqlTemp:=SqlTemp+'DELETE FROM '+FTableName+ ' WHERE _ROWID_ = '+
|
SqlTemp:=SqlTemp+'DELETE FROM '+FTableName+ ' WHERE '+KeyName+' = '+
|
||||||
StrPas(PDataRecord(FDeletedItems[CounterItems])^.Row[0])+';';
|
StrPas(PDataRecord(FDeletedItems[CounterItems])^.Row[FIndexFieldNo])+';';
|
||||||
end;
|
end;
|
||||||
SqlTemp:=SqlTemp+'END TRANSACTION; ';
|
SqlTemp:=SqlTemp+'END TRANSACTION; ';
|
||||||
{$ifdef DEBUG}
|
{$ifdef DEBUG}
|
||||||
@ -734,6 +843,12 @@ var
|
|||||||
SqlTemp:String;
|
SqlTemp:String;
|
||||||
Counter:Integer;
|
Counter:Integer;
|
||||||
begin
|
begin
|
||||||
|
{$ifdef DEBUG}
|
||||||
|
if FTableName = '' then
|
||||||
|
WriteLn('CreateTable : TableName Not Set');
|
||||||
|
if FieldDefs.Count = 0 then
|
||||||
|
WriteLn('CreateTable : FieldDefs Not Initialized');
|
||||||
|
{$endif}
|
||||||
if (FTableName <> '') and (FieldDefs.Count > 0) then
|
if (FTableName <> '') and (FieldDefs.Count > 0) then
|
||||||
begin
|
begin
|
||||||
FSqliteHandle:= sqlite_open(PChar(FFileName),0,FDBError);
|
FSqliteHandle:= sqlite_open(PChar(FFileName),0,FDBError);
|
||||||
@ -758,6 +873,8 @@ begin
|
|||||||
SqlTemp:=SqlTemp + ' DATE';
|
SqlTemp:=SqlTemp + ' DATE';
|
||||||
ftTime:
|
ftTime:
|
||||||
SqlTemp:=SqlTemp + ' TIME';
|
SqlTemp:=SqlTemp + ' TIME';
|
||||||
|
ftAutoInc:
|
||||||
|
SqlTemp:=SqlTemp + ' AUTOINC';
|
||||||
else
|
else
|
||||||
SqlTemp:=SqlTemp + ' VARCHAR';
|
SqlTemp:=SqlTemp + ' VARCHAR';
|
||||||
end;
|
end;
|
||||||
|
Loading…
Reference in New Issue
Block a user