ViewVC Help
View File | Revision Log | Show Annotations | Download File | View Changeset | Root Listing
root/public/ibx/branches/udr/client/FBAttachment.pas
(Generate patch)

Comparing:
ibx/trunk/fbintf/client/FBAttachment.pas (file contents), Revision 350 by tony, Wed Oct 20 14:58:56 2021 UTC vs.
ibx/branches/udr/client/FBAttachment.pas (file contents), Revision 371 by tony, Wed Jan 5 15:21:22 2022 UTC

# Line 16 | Line 16
16   *
17   *  The Initial Developer of the Original Code is Tony Whyman.
18   *
19 < *  The Original Code is (C) 2016 Tony Whyman, MWA Software
19 > *  The Original Code is (C) 2016-2021 Tony Whyman, MWA Software
20   *  (http://www.mwasoftware.co.uk).
21   *
22   *  All Rights Reserved.
# Line 39 | Line 39 | interface
39  
40   uses
41    Classes, SysUtils, {$IFDEF WINDOWS} windows, {$ENDIF} IB,  FBParamBlock,
42 <  FBActivityMonitor, FBClientAPI;
42 >  FBActivityMonitor, FBClientAPI, IBUtils;
43  
44   const
45    DefaultMaxInlineBlobLimit = 8192;
# Line 53 | Line 53 | type
53      AllowReverseLookup: boolean; {used to ensure that lookup of CP_UTF* does not return UNICODE_FSS}
54    end;
55  
56 +  { Database Journalling.
57 +
58 +    This class is intended to support a client side journal of all database
59 +    updates, inserts and deletes made by the client during a session. It also records
60 +    the transaction each update was made under.
61 +
62 +    The database schema is required to include a control table "IBX$JOURNALS" and
63 +    an SQL Sequence IBX$SESSIONS. These are created by the class when the
64 +    database is opened, if they are not already present. However, it is recommended
65 +    that they are created as an orginal part of the database schema in order to
66 +    unnecessarily avoid each user being given sufficient priviledge to create tables
67 +    and Sequences.
68 +
69 +    Syntax:
70 +
71 +    Transaction Start:
72 +    *S:<date/time>,<attachmentid>,<session id>,<transaction no.>,<string length>:<transaction Name>,<string length>:<TPB>,<default Completion>
73 +
74 +    Transaction Commit:
75 +    *C:<date/time>,<attachmentid>,<session id>,<transaction no.>
76 +
77 +    Transaction Commit retaining :
78 +    *c:<date/time>,<attachmentid>,<session id>,<transaction no.><old transaction no.>
79 +
80 +    Transaction Rollback:
81 +    *R:<date/time>,<attachmentid>,<session id>,<transaction no.>
82 +
83 +    Transaction Rollback retaining:
84 +    *r:<date/time>,<attachmentid>,<session id>,<transaction no.><old transaction no.>
85 +
86 +    Update/Insert/Delete
87 +    *Q:<date/time>,<attachmentid>,<session id>,<transaction no.>,<length of query text in bytes>:<query text>
88 +
89 +  }
90 +
91 +  { TFBJournaling }
92 +
93 +  TFBJournaling = class(TActivityHandler, IJournallingHook)
94 +  private
95 +    {Logfile}
96 +    const sQueryJournal          = '*Q:''%s'',%d,%d,%d,%d:%s' + LineEnding;
97 +    const sTransStartJnl         = '*S:''%s'',%d,%d,%d,%d:%s,%d:%s,%d' + LineEnding;
98 +    const sTransCommitJnl        = '*C:''%s'',%d,%d,%d' + LineEnding;
99 +    const sTransCommitRetJnl     = '*c:''%s'',%d,%d,%d,%d' + LineEnding;
100 +    const sTransRollBackJnl      = '*R:''%s'',%d,%d,%d' + LineEnding;
101 +    const sTransRollBackRetJnl   = '*r:''%s'',%d,%d,%d,%d' + LineEnding;
102 +  private
103 +    FOptions: TJournalOptions;
104 +    FJournalFilePath: string;
105 +    FJournalFileStream: TStream;
106 +    FSessionID: integer;
107 +    FDoNotJournal: boolean;
108 +    function GetDateTimeFmt: AnsiString;
109 +  protected
110 +    procedure EndSession(RetainJournal: boolean);
111 +    function GetAttachment: IAttachment; virtual; abstract;
112 +  public
113 +    {IAttachment}
114 +    procedure Disconnect(Force: boolean=false); virtual;
115 +  public
116 +    {IJournallingHook}
117 +    procedure TransactionStart(Tr: ITransaction);
118 +    function TransactionEnd( TransactionID: integer; Action: TTransactionAction): boolean;
119 +    procedure TransactionRetained(Tr: ITransaction; OldTransactionID: integer;
120 +      Action: TTransactionAction);
121 +    procedure ExecQuery(Stmt: IStatement);
122 +  public
123 +    {Client side Journaling}
124 +    function JournalingActive: boolean;
125 +    function GetJournalOptions: TJournalOptions;
126 +    function StartJournaling(aJournalLogFile: AnsiString): integer; overload;
127 +    function StartJournaling(aJournalLogFile: AnsiString; Options: TJournalOptions): integer; overload;
128 +    function StartJournaling(S: TStream; Options: TJournalOptions): integer; overload;
129 +    procedure StopJournaling(RetainJournal: boolean);
130 +  end;
131 +
132    { TFBAttachment }
133  
134 <  TFBAttachment = class(TActivityHandler)
134 >  TFBAttachment = class(TFBJournaling)
135    private
136      FDPB: IDPB;
137      FFirebirdAPI: IFirebirdAPI;
# Line 64 | Line 140 | type
140      FUserCharSetMap: array of TCharSetMap;
141      FSecDatabase: AnsiString;
142      FInlineBlobLimit: integer;
143 +    FAttachmentID: integer;
144    protected
145      FDatabaseName: AnsiString;
146      FRaiseExceptionOnConnectError: boolean;
# Line 90 | Line 167 | type
167      function getDPB: IDPB;
168      function AllocateBPB: IBPB;
169      function AllocateDIRB: IDIRB;
170 <    function StartTransaction(TPB: array of byte; DefaultCompletion: TTransactionCompletion): ITransaction; overload; virtual; abstract;
171 <    function StartTransaction(TPB: ITPB; DefaultCompletion: TTransactionCompletion): ITransaction; overload; virtual; abstract;
172 <    procedure Disconnect(Force: boolean=false); virtual; abstract;
170 >    function StartTransaction(TPB: array of byte;
171 >      DefaultCompletion: TTransactionCompletion;
172 >      aName: AnsiString=''): ITransaction; overload; virtual; abstract;
173 >    function StartTransaction(TPB: ITPB; DefaultCompletion: TTransactionCompletion;
174 >      aName: AnsiString=''): ITransaction; overload; virtual; abstract;
175      procedure ExecImmediate(transaction: ITransaction; sql: AnsiString; aSQLDialect: integer); overload; virtual; abstract;
176      procedure ExecImmediate(TPB: array of byte; sql: AnsiString; aSQLDialect: integer); overload;
177      procedure ExecImmediate(transaction: ITransaction; sql: AnsiString); overload;
# Line 140 | Line 219 | type
219      function GetEventHandler(Event: AnsiString): IEvents; overload;
220  
221      function GetSQLDialect: integer;
222 +    function GetAttachmentID: integer;
223      function CreateBlob(transaction: ITransaction; RelationName, ColumnName: AnsiString; BPB: IBPB=nil): IBlob; overload;
224      function CreateBlob(transaction: ITransaction; BlobMetaData: IBlobMetaData; BPB: IBPB=nil): IBlob; overload; virtual; abstract;
225      function OpenBlob(transaction: ITransaction; BlobMetaData: IBlobMetaData; BlobID: TISC_QUAD; BPB: IBPB=nil): IBlob; overload; virtual; abstract;
# Line 162 | Line 242 | type
242      function GetSecurityDatabase: AnsiString;
243      function GetODSMajorVersion: integer;
244      function GetODSMinorVersion: integer;
245 +    function GetCharSetID: integer;
246      function HasDecFloatSupport: boolean; virtual;
247      function GetInlineBlobLimit: integer;
248      procedure SetInlineBlobLimit(limit: integer);
249      function HasBatchMode: boolean; virtual;
250 +    function HasTable(aTableName: AnsiString): boolean;
251 +    function HasFunction(aFunctionName: AnsiString): boolean;
252 +    function HasProcedure(aProcName: AnsiString): boolean;
253  
254    public
255      {Character Sets}
# Line 214 | Line 298 | type
298  
299   implementation
300  
301 < uses FBMessages, IBUtils, FBTransaction {$IFDEF HASREQEX}, RegExpr{$ENDIF};
301 > uses FBMessages, IBErrorCodes, FBTransaction {$IFDEF HASREQEX}, RegExpr{$ENDIF};
302 >
303 > const
304 >  {Journaling}
305 >  sJournalTableName = 'IBX$JOURNALS';
306 >  sSequenceName = 'IBX$SESSIONS';
307 >
308 >  sqlCreateJournalTable =
309 >    'Create Table ' + sJournalTableName + '(' +
310 >    '  IBX$SessionID Integer not null, '+
311 >    '  IBX$TransactionID Integer not null, '+
312 >    '  IBX$OldTransactionID Integer, '+
313 >    '  IBX$USER VarChar(32) Default CURRENT_USER, '+
314 >    '  IBX$CREATED TIMESTAMP Default CURRENT_TIMESTAMP, '+
315 >    '  Primary Key(IBX$SessionID,IBX$TransactionID)' +
316 >    ')';
317 >
318 >  sqlCreateSequence = 'CREATE SEQUENCE ' + sSequenceName;
319 >
320 >  sqlGetNextSessionID = 'Select Gen_ID(' + sSequenceName + ',1) as SessionID From RDB$DATABASE';
321 >
322 >  sqlRecordJournalEntry = 'Insert into ' + sJournalTableName + '(IBX$SessionID,IBX$TransactionID,IBX$OldTransactionID) '+
323 >                        'Values(?,?,?)';
324 >
325 >  sqlCleanUpSession = 'Delete From ' + sJournalTableName + ' Where IBX$SessionID = ?';
326  
327   const
328    CharSetMap: array [0..69] of TCharsetMap = (
# Line 391 | Line 499 | const
499      'decfloat_traps'
500      );
501  
502 + type
503 +
504 +  { TQueryProcessor }
505 +
506 +  TQueryProcessor=class(TSQLTokeniser)
507 +  private
508 +    FInString: AnsiString;
509 +    FIndex: integer;
510 +    FStmt: IStatement;
511 +    function DoExecute: AnsiString;
512 +    function GetParamValue(ParamIndex: integer): AnsiString;
513 +  protected
514 +    function GetChar: AnsiChar; override;
515 +  public
516 +    class function Execute(Stmt: IStatement): AnsiString;
517 +  end;
518 +
519 +  { TQueryProcessor }
520 +
521 + function TQueryProcessor.DoExecute: AnsiString;
522 + var token: TSQLTokens;
523 +    ParamIndex: integer;
524 + begin
525 +  Result := '';
526 +  ParamIndex := 0;
527 +
528 +  while not EOF do
529 +  begin
530 +    token := GetNextToken;
531 +    case token of
532 +    sqltPlaceHolder:
533 +      begin
534 +        Result := Result + GetParamValue(ParamIndex);
535 +        Inc(ParamIndex);
536 +      end;
537 +    else
538 +      Result := Result + TokenText;
539 +    end;
540 +  end;
541 + end;
542 +
543 + function TQueryProcessor.GetParamValue(ParamIndex: integer): AnsiString;
544 + begin
545 +  with FStmt.SQLParams[ParamIndex] do
546 +  begin
547 +    if IsNull then
548 +      Result := 'NULL'
549 +    else
550 +    case GetSQLType of
551 +    SQL_BLOB:
552 +      if getSubType = 1 then {string}
553 +        Result := '''' + SQLSafeString(GetAsString) + ''''
554 +      else
555 +        Result := TSQLXMLReader.FormatBlob(GetAsString,getSubType);
556 +
557 +    SQL_ARRAY:
558 +        Result := TSQLXMLReader.FormatArray(getAsArray);
559 +
560 +    SQL_VARYING,
561 +    SQL_TEXT,
562 +    SQL_TIMESTAMP,
563 +    SQL_TYPE_DATE,
564 +    SQL_TYPE_TIME,
565 +    SQL_TIMESTAMP_TZ_EX,
566 +    SQL_TIME_TZ_EX,
567 +    SQL_TIMESTAMP_TZ,
568 +    SQL_TIME_TZ:
569 +      Result := '''' + SQLSafeString(GetAsString) + '''';
570 +    else
571 +      Result := GetAsString;
572 +    end;
573 +  end;
574 + end;
575 +
576 + function TQueryProcessor.GetChar: AnsiChar;
577 + begin
578 +  if FIndex <= Length(FInString) then
579 +  begin
580 +    Result := FInString[FIndex];
581 +    Inc(FIndex);
582 +  end
583 +  else
584 +    Result := #0;
585 + end;
586 +
587 + class function TQueryProcessor.Execute(Stmt: IStatement): AnsiString;
588 + begin
589 +  if not Stmt.IsPrepared then
590 +    IBError(ibxeSQLClosed,[]);
591 +  with self.Create do
592 +  try
593 +    FStmt := Stmt;
594 +    FInString := Stmt.GetProcessedSQLText;
595 +    FIndex := 1;
596 +    Result := Trim(DoExecute);
597 +  finally
598 +    Free;
599 +  end;
600 + end;
601 +
602 + { TFBJournaling }
603 +
604 + function TFBJournaling.GetDateTimeFmt: AnsiString;
605 + begin
606 +  {$IF declared(DefaultFormatSettings)}
607 +  with DefaultFormatSettings do
608 +  {$ELSE}
609 +  {$IF declared(FormatSettings)}
610 +  with FormatSettings do
611 +  {$IFEND}
612 +  {$IFEND}
613 +  Result := ShortDateFormat + ' ' + LongTimeFormat + '.zzzz'
614 + end;
615 +
616 + procedure TFBJournaling.EndSession(RetainJournal: boolean);
617 + begin
618 +  if JournalingActive and (FJournalFilePath <> '') then
619 +  begin
620 +    FreeAndNil(FJournalFileStream);
621 +    if not (joNoServerTable in FOptions) and not RetainJournal then
622 +    try
623 +        GetAttachment.ExecuteSQL([isc_tpb_write,isc_tpb_wait,isc_tpb_consistency],
624 +             sqlCleanUpSession,[FSessionID]);
625 +        sysutils.DeleteFile(FJournalFilePath);
626 +    except On E: EIBInterBaseError do
627 +      if E.IBErrorCode <> isc_lost_db_connection then
628 +        raise;
629 +      {ignore - do not delete journal if database gone away}
630 +    end;
631 +    FSessionID := -1;
632 +  end;
633 + end;
634 +
635 + procedure TFBJournaling.Disconnect(Force: boolean);
636 + begin
637 +  if JournalingActive then
638 +    EndSession(Force);
639 + end;
640 +
641 + procedure TFBJournaling.TransactionStart(Tr: ITransaction);
642 + var LogEntry: AnsiString;
643 +    TPBText: AnsiString;
644 + begin
645 +  FDoNotJournal := true;
646 +  if not (joNoServerTable in FOptions) then
647 +  try
648 +    GetAttachment.ExecuteSQL(Tr,sqlRecordJournalEntry,[FSessionID,Tr.GetTransactionID,NULL]);
649 +  finally
650 +    FDoNotJournal := false;
651 +  end;
652 +  TPBText := Tr.getTPB.AsText;
653 +  LogEntry := Format(sTransStartJnl,[FBFormatDateTime(GetDateTimeFmt,Now),
654 +                                     GetAttachment.GetAttachmentID,
655 +                                     FSessionID,
656 +                                     Tr.GetTransactionID,
657 +                                     Length(Tr.TransactionName),
658 +                                     Tr.TransactionName,
659 +                                     Length(TPBText),TPBText,
660 +                                     ord(tr.GetDefaultCompletion)]);
661 +  if assigned(FJournalFileStream) then
662 +    FJournalFileStream.Write(LogEntry[1],Length(LogEntry));
663 + end;
664 +
665 + function TFBJournaling.TransactionEnd(TransactionID: integer;
666 +  Action: TTransactionAction): boolean;
667 +
668 + var LogEntry: AnsiString;
669 + begin
670 +  Result := false;
671 +    case Action of
672 +    TARollback:
673 +      begin
674 +        LogEntry := Format(sTransRollbackJnl,[FBFormatDateTime(GetDateTimeFmt,Now),
675 +                                              GetAttachment.GetAttachmentID,
676 +                                              FSessionID,TransactionID]);
677 +        Result := true;
678 +      end;
679 +    TACommit:
680 +      begin
681 +        LogEntry := Format(sTransCommitJnl,[FBFormatDateTime(GetDateTimeFmt,Now),
682 +                                            GetAttachment.GetAttachmentID,
683 +                                            FSessionID,TransactionID]);
684 +        Result := true;
685 +      end;
686 +    end;
687 +    if assigned(FJournalFileStream) then
688 +      FJournalFileStream.Write(LogEntry[1],Length(LogEntry));
689 + end;
690 +
691 + procedure TFBJournaling.TransactionRetained(Tr: ITransaction;
692 +  OldTransactionID: integer; Action: TTransactionAction);
693 + var LogEntry: AnsiString;
694 + begin
695 +    case Action of
696 +      TACommitRetaining:
697 +          LogEntry := Format(sTransCommitRetJnl,[FBFormatDateTime(GetDateTimeFmt,Now),
698 +                                  GetAttachment.GetAttachmentID,
699 +                                  FSessionID,Tr.GetTransactionID,OldTransactionID]);
700 +      TARollbackRetaining:
701 +          LogEntry := Format(sTransRollbackRetJnl,[FBFormatDateTime(GetDateTimeFmt,Now),
702 +                                      GetAttachment.GetAttachmentID,
703 +                                      FSessionID,Tr.GetTransactionID,OldTransactionID]);
704 +    end;
705 +    if assigned(FJournalFileStream) then
706 +      FJournalFileStream.Write(LogEntry[1],Length(LogEntry));
707 +
708 +    FDoNotJournal := true;
709 +    if not (joNoServerTable in FOptions) then
710 +    try
711 +      GetAttachment.ExecuteSQL(Tr,sqlRecordJournalEntry,[FSessionID,Tr.GetTransactionID,OldTransactionID]);
712 +    finally
713 +      FDoNotJournal := false;
714 +   end;
715 + end;
716 +
717 + procedure TFBJournaling.ExecQuery(Stmt: IStatement);
718 + var SQL: AnsiString;
719 +    LogEntry: AnsiString;
720 + begin
721 +  SQL := TQueryProcessor.Execute(Stmt);
722 +  LogEntry := Format(sQueryJournal,[FBFormatDateTime(GetDateTimeFmt,Now),
723 +                                      GetAttachment.GetAttachmentID,
724 +                                      FSessionID,
725 +                                      Stmt.GetTransaction.GetTransactionID,
726 +                                      Length(SQL),SQL]);
727 +  if assigned(FJournalFileStream) then
728 +    FJournalFileStream.Write(LogEntry[1],Length(LogEntry));
729 + end;
730 +
731 + function TFBJournaling.JournalingActive: boolean;
732 + begin
733 +  Result := (FJournalFileStream <> nil) and not FDoNotJournal;
734 + end;
735 +
736 + function TFBJournaling.GetJournalOptions: TJournalOptions;
737 + begin
738 +  Result := FOptions;
739 + end;
740 +
741 + function TFBJournaling.StartJournaling(aJournalLogFile: AnsiString): integer;
742 + begin
743 +  Result := StartJournaling(aJournalLogFile,[joReadWriteTransactions,joModifyQueries]);
744 + end;
745 +
746 + function TFBJournaling.StartJournaling(aJournalLogFile: AnsiString;
747 +  Options: TJournalOptions): integer;
748 + begin
749 +  try
750 +    StartJournaling(TFileStream.Create(aJournalLogFile,fmCreate),Options);
751 +  finally
752 +    FJournalFilePath := aJournalLogFile;
753 +  end;
754 + end;
755 +
756 + function TFBJournaling.StartJournaling(S: TStream; Options: TJournalOptions
757 +  ): integer;
758 + begin
759 +  FOptions := Options;
760 +  if not (joNoServerTable in FOptions) then
761 +  with GetAttachment do
762 +  begin
763 +    if  not HasTable(sJournalTableName) then
764 +    begin
765 +      ExecImmediate([isc_tpb_write,isc_tpb_wait,isc_tpb_consistency],sqlCreateJournalTable);
766 +      ExecImmediate([isc_tpb_write,isc_tpb_wait,isc_tpb_consistency],sqlCreateSequence);
767 +    end;
768 +    FSessionID := OpenCursorAtStart(sqlGetNextSessionID)[0].AsInteger;
769 +  end;
770 +  FJournalFileStream := S;
771 +  Result := FSessionID;
772 + end;
773 +
774 + procedure TFBJournaling.StopJournaling(RetainJournal: boolean);
775 + begin
776 +  EndSession(RetainJournal);
777 + end;
778 +
779 +
780  
781  
782   { TFBAttachment }
# Line 404 | Line 790 | var DBInfo: IDBInformation;
790   begin
791    if not IsConnected then Exit;
792    DBInfo := GetDBInformation([isc_info_db_id,isc_info_ods_version,isc_info_ods_minor_version,
793 <                               isc_info_db_SQL_Dialect]);
793 >                               isc_info_db_SQL_Dialect, isc_info_attachment_id]);
794    for i := 0 to DBInfo.GetCount - 1 do
795      with DBInfo[i] do
796        case getItemType of
# Line 414 | Line 800 | begin
800          FODSMajorVersion := getAsInteger;
801        isc_info_db_SQL_Dialect:
802          FSQLDialect := getAsInteger;
803 +      isc_info_attachment_id:
804 +        FAttachmentID := getAsInteger;
805        end;
806  
807    FCharSetID := 0;
# Line 470 | Line 858 | begin
858    FFirebirdAPI := api.GetAPI; {Keep reference to interface}
859    FSQLDialect := 3;
860    FDatabaseName := DatabaseName;
473  FDPB := DPB;
861    SetLength(FUserCharSetMap,0);
475  FRaiseExceptionOnConnectError := RaiseExceptionOnConnectError;
862    FODSMajorVersion := 0;
863    FODSMinorVersion := 0;
864    FInlineBlobLimit := DefaultMaxInlineBlobLimit;
865 +  FDPB := DPB;
866 +  FRaiseExceptionOnConnectError := RaiseExceptionOnConnectError;
867   end;
868  
869   function TFBAttachment.GenerateCreateDatabaseSQL(DatabaseName: AnsiString;  aDPB: IDPB): AnsiString;
# Line 820 | Line 1208 | begin
1208    Result := FSQLDialect;
1209   end;
1210  
1211 + function TFBAttachment.GetAttachmentID: integer;
1212 + begin
1213 +  Result := FAttachmentID;
1214 + end;
1215 +
1216   function TFBAttachment.CreateBlob(transaction: ITransaction; RelationName,
1217    ColumnName: AnsiString; BPB: IBPB): IBlob;
1218   begin
# Line 919 | Line 1312 | begin
1312    Result := FODSMinorVersion;
1313   end;
1314  
1315 + function TFBAttachment.GetCharSetID: integer;
1316 + begin
1317 +  Result := FCharSetID;
1318 + end;
1319 +
1320   function TFBAttachment.HasDecFloatSupport: boolean;
1321   begin
1322    Result := false;
# Line 942 | Line 1340 | begin
1340    Result := false;
1341   end;
1342  
1343 + function TFBAttachment.HasTable(aTableName: AnsiString): boolean;
1344 + begin
1345 +  Result := OpenCursorAtStart(
1346 +       'Select count(*) From RDB$RELATIONS Where RDB$RELATION_NAME = ?',
1347 +          [aTableName])[0].AsInteger > 0;
1348 + end;
1349 +
1350 + function TFBAttachment.HasFunction(aFunctionName: AnsiString): boolean;
1351 + begin
1352 +  Result := OpenCursorAtStart(
1353 +       'Select count(*) From RDB$FUNCTIONS Where RDB$FUNCTION_NAME = ?',
1354 +          [aFunctionName])[0].AsInteger > 0;
1355 + end;
1356 +
1357 + function TFBAttachment.HasProcedure(aProcName: AnsiString): boolean;
1358 + begin
1359 +  Result := OpenCursorAtStart(
1360 +       'Select count(*) From RDB$PROCEDURES Where RDB$PROCEDURE_NAME = ?',
1361 +          [aProcName])[0].AsInteger > 0;
1362 + end;
1363 +
1364   function TFBAttachment.HasDefaultCharSet: boolean;
1365   begin
1366    Result := FHasDefaultCharSet

Diff Legend

Removed lines
+ Added lines
< Changed lines
> Changed lines