diff --git a/.github/workflows/wire-protocol.yml b/.github/workflows/wire-protocol.yml new file mode 100644 index 00000000..83aae5fc --- /dev/null +++ b/.github/workflows/wire-protocol.yml @@ -0,0 +1,324 @@ +name: Wire protocol client + +# Exercises the pure Pascal wire protocol client (client/wire) against every +# supported Firebird major version. The client needs no fbclient library, so +# the server runs in a container and the test reaches it over TCP only: +# nothing Firebird related is installed on the runner itself. + +on: + push: + branches: [ main, 'pure-pascal-wire-protocol' ] + paths: + - 'client/**' + - 'testsuite/**' + - '.github/workflows/wire-protocol.yml' + pull_request: + paths: + - 'client/**' + - 'testsuite/**' + - '.github/workflows/wire-protocol.yml' + workflow_dispatch: + +env: + FB_USER: SYSDBA + FB_PASSWORD: masterkey + # Server side paths. The client never opens these itself: it asks the + # server to, so they are resolved inside the container. + FB_DATABASE: /tmp/wiretest.fdb + FB_SCRATCH: /tmp/wirescratch.fdb + +jobs: + server: + name: FB ${{ matrix.firebird }} / WireCrypt ${{ matrix.wirecrypt }}${{ matrix.wirecompression == 'true' && ' / Compressed' || '' }} + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + firebird: ['3', '4', '5', '6'] + wirecrypt: ['Enabled', 'Required'] + wirecompression: ['false'] + include: + - firebird: '3' + image: 'firebirdsql/firebird:3' + # Firebird 3 tops out at protocol 15 and has no ChaCha plugin, + # so the connection settles on Arc4. + expected: '15' + - firebird: '4' + image: 'firebirdsql/firebird:4' + expected: '17' + - firebird: '5' + image: 'firebirdsql/firebird:5' + expected: '19' + - firebird: '6' + image: 'firebirdsql/firebird:6-snapshot' + expected: '20' + # One compression row: the zlib pipeline is protocol version + # independent, so a single server exercises it. WireTest's + # compression section negotiates it explicitly. Naming a + # different value for the wirecompression axis makes this a new + # combination rather than extending the existing FB 5 row. + - firebird: '5' + wirecrypt: 'Enabled' + wirecompression: 'true' + image: 'firebirdsql/firebird:5' + expected: '19' + exclude: + # Protocol 13, the only one Firebird 3 offers without encryption + # support, cannot satisfy WireCrypt = Required. The combination is + # covered by the newer versions. + - firebird: '3' + wirecrypt: 'Required' + + steps: + - uses: actions/checkout@v4 + + - name: Start Firebird ${{ matrix.firebird }} + run: | + docker run -d \ + --name firebird \ + -e FIREBIRD_ROOT_PASSWORD="${FB_PASSWORD}" \ + -e FIREBIRD_CONF_WireCrypt="${{ matrix.wirecrypt }}" \ + -e FIREBIRD_CONF_AuthServer="Srp256;Srp;Legacy_Auth" \ + -e FIREBIRD_CONF_DatabaseAccess="Full" \ + -e FIREBIRD_CONF_RemoteAuxPort="3051" \ + -e FIREBIRD_CONF_WireCompression="${{ matrix.wirecompression || 'false' }}" \ + -p 3050:3050 \ + -p 3051:3051 \ + "${{ matrix.image }}" + + echo "Waiting for the server to accept connections" + ready=false + for i in $(seq 1 45); do + if docker exec firebird /opt/firebird/bin/isql \ + -user "${FB_USER}" -password "${FB_PASSWORD}" -z 2>&1 \ + | grep -q "ISQL Version"; then + ready=true + break + fi + sleep 2 + done + if [ "$ready" != true ]; then + echo "::error::Firebird did not become ready" + docker logs firebird + exit 1 + fi + docker exec firebird /opt/firebird/bin/isql \ + -user "${FB_USER}" -password "${FB_PASSWORD}" -z 2>&1 | head -1 + + - name: Create the test database + run: | + docker exec -i firebird /opt/firebird/bin/isql \ + -user "${FB_USER}" -password "${FB_PASSWORD}" -q <> "$GITHUB_STEP_SUMMARY" + + - name: Firebird log on failure + if: failure() + run: | + echo "===== firebird.log =====" + docker exec firebird sh -c 'cat $(find / -name firebird.log 2>/dev/null | head -1)' \ + 2>/dev/null | tail -n 200 || echo "(no firebird.log found)" + echo "===== container =====" + docker ps -a + docker logs firebird --tail 100 + + offline: + # The parts that need no server: arbitrary precision arithmetic, the + # hashes and ciphers against their published vectors, the SRP exchange + # against a fixed reference exchange, and the message layout. Running + # them separately keeps a compiler or arithmetic regression from being + # reported as a server problem, and covers Windows where no Firebird + # container is available. + name: Offline tests (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest] + + steps: + - uses: actions/checkout@v4 + + - name: Install Free Pascal + uses: gcarreno/setup-lazarus@v3 + with: + lazarus-version: 'stable' + + - name: Show the compiler version + shell: bash + run: fpc -iV + + - name: Build and run the offline tests + shell: bash + run: | + mkdir -p build + fpc -Fuclient -Fuclient/2.5 -Fuclient/3.0 -Fuclient/3.0/firebird \ + -Fuclient/wire -Ficlient/include -FEbuild -B \ + testsuite/WireTest.pas + # Nothing listens on port 3051, so every section that needs a + # server reports SKIP straight away and the rest still runs. + ./build/WireTest "localhost/3051:/nonexistent.fdb" SYSDBA masterkey + + suite: + # The full fbintf test suite - all twenty two test programs - run over + # the wire protocol provider and compared line by line against + # testsuite/FBWirereference.log. The suite needs the employee example + # database, which the container images do not ship, so it is restored + # from the checked in backup. The reference log was produced against + # Firebird 6 (ODS 14, protocol 17), so this job runs that server. + name: Full test suite over the wire (FB 6) + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Start Firebird 6 + run: | + mkdir -p /tmp/fbintf-testsuite + chmod 777 /tmp/fbintf-testsuite + docker run -d \ + --name firebird \ + -e FIREBIRD_ROOT_PASSWORD="${FB_PASSWORD}" \ + -e FIREBIRD_CONF_WireCrypt="Enabled" \ + -e FIREBIRD_CONF_AuthServer="Srp256;Srp;Legacy_Auth" \ + -e FIREBIRD_CONF_DatabaseAccess="Full" \ + -e FIREBIRD_CONF_RemoteAuxPort="3051" \ + -e FIREBIRD_CONF_WireCompression="${{ matrix.wirecompression || 'false' }}" \ + -v /tmp/fbintf-testsuite:/tmp/fbintf-testsuite \ + -p 3050:3050 \ + -p 3051:3051 \ + firebirdsql/firebird:6-snapshot + echo "Waiting for the server to accept connections" + ready=false + for i in $(seq 1 45); do + if docker exec firebird /opt/firebird/bin/isql \ + -user "${FB_USER}" -password "${FB_PASSWORD}" -z 2>&1 \ + | grep -q "ISQL Version"; then + ready=true + break + fi + sleep 2 + done + if [ "$ready" != true ]; then + echo "::error::Firebird did not become ready" + docker logs firebird + exit 1 + fi + + - name: Restore the employee example database + run: | + docker cp testsuite/employee.gbk firebird:/tmp/employee.gbk + docker exec firebird /opt/firebird/bin/gbak -c \ + -user "${FB_USER}" -password "${FB_PASSWORD}" \ + /tmp/employee.gbk /tmp/fbintf-testsuite/employee.fdb + docker exec firebird sh -c \ + 'echo "employee = /tmp/fbintf-testsuite/employee.fdb" >> /opt/firebird/databases.conf' + docker exec firebird /opt/firebird/bin/isql \ + -user "${FB_USER}" -password "${FB_PASSWORD}" \ + -q localhost:employee <<'SQL' + select count(*) from EMPLOYEE; + quit; + SQL + + - name: Install Free Pascal + run: | + sudo apt-get update + sudo apt-get install -y fpc + echo "compiler: $(fpc -iV), target: $(fpc -iTO)-$(fpc -iTP)" + + - name: Run the test suite over the wire provider + run: | + testsuite/runtest.sh -a wire + + - name: Check the reference log comparison + run: | + # runtest.sh always exits 0: the outcome is the diff, and the log + # must show a completed run for the diff to mean anything + if ! grep -aq "Test Suite Ends" testsuite/testout.log; then + echo "::error::The test suite did not run to completion" + tail -50 testsuite/testout.log || true + exit 1 + fi + if [ -s testsuite/diff.log ]; then + echo "::error::The suite output differs from FBWirereference.log" + cat testsuite/diff.log + exit 1 + fi + echo "The suite output matches the reference log" + { + echo "### Full test suite over the wire" + echo + echo "All twenty two test programs match \`FBWirereference.log\`." + } >> "$GITHUB_STEP_SUMMARY" + + - name: Upload the test output + if: always() + uses: actions/upload-artifact@v4 + with: + name: wire-suite-testout + path: | + testsuite/testout.log + testsuite/diff.log + if-no-files-found: ignore + + - name: Server diagnostics on failure + if: failure() + run: | + docker logs firebird --tail 100 diff --git a/Makefile.fpc b/Makefile.fpc index 7298111d..a4da4f27 100644 --- a/Makefile.fpc +++ b/Makefile.fpc @@ -10,7 +10,7 @@ version=0.0 [compiler] unittargetdir=lib/$(CPU_TARGET)-$(OS_TARGET) -unitdir=client client/3.0/firebird client/2.5 . client/3.0 client/3.0/firebird +unitdir=client client/3.0/firebird client/2.5 . client/3.0 client/3.0/firebird client/wire includedir=client/include options= -MObjFPC -Scghi -O3 -l -vewnhibq $(DBG_OPTIONS) diff --git a/README.md b/README.md index 1df3f7f1..c0025180 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,28 @@ See the "doc" directory for more information, installation instructions and a fu guide to the API. fbintf also comes with a comprehensive test suite. See doc/TestSuite.pdf for more information. +## Connecting without a client library + +From this release fbintf also includes a pure Pascal implementation of the +Firebird remote (wire) protocol in `client/wire`. It talks to a Firebird +3.0, 4.0, 5.0 or 6.0 server directly over TCP and needs no fbclient library +installed, which suits containers, single file deployments and cross +compiled targets. SRP authentication and wire encryption (ChaCha64, ChaCha +and Arc4) are supported. + +Code written against the fbintf interfaces runs unchanged; only the way the +API is obtained differs: + +```pascal +uses IB, FBWireClientAPI; + +API := WireFirebirdAPI; {instead of IB.FirebirdAPI} +``` + +See doc/WireProtocol.md for the full description, and client/wire/README.md +for a summary of what is and is not implemented. The implementation is FPC +only for now: the transport uses the FPC socket units. + See the "changelog" for information on changes from previous releases diff --git a/client/2.5/FB25Attachment.pas b/client/2.5/FB25Attachment.pas index 8750bb5c..1fa08297 100644 --- a/client/2.5/FB25Attachment.pas +++ b/client/2.5/FB25Attachment.pas @@ -91,6 +91,9 @@ TFB25Attachment = class(TFBAttachment, IAttachment, IActivityMonitor) function GetArrayMetaData(Transaction: ITransaction; tableName, columnName: AnsiString): IArrayMetaData; override; procedure getFBVersion(version: TStrings); function HasScollableCursors: boolean; + {fb_cancel_operation - raises ibxeNotSupported when the loaded + library does not export it} + procedure CancelOperation(aKind: integer = fb_cancel_raise); override; end; implementation @@ -359,5 +362,17 @@ function TFB25Attachment.HasScollableCursors: boolean; Result := false; end; +procedure TFB25Attachment.CancelOperation(aKind: integer); +begin + CheckHandle; + with FFirebird25ClientAPI do + begin + if not assigned(fb_cancel_operation) then + IBError(ibxeNotSupported,[nil]); + if fb_cancel_operation(StatusVector,@FHandle,aKind) > 0 then + IBDataBaseError; + end; +end; + end. diff --git a/client/2.5/FB25ClientAPI.pas b/client/2.5/FB25ClientAPI.pas index 0872d654..61e6e568 100644 --- a/client/2.5/FB25ClientAPI.pas +++ b/client/2.5/FB25ClientAPI.pas @@ -113,6 +113,9 @@ TFB25ClientAPI = class(TFBClientAPI,IFirebirdAPI) {fbclient API} BLOB_get: TBLOB_get; BLOB_put: TBLOB_put; + {nil when the loaded library does not export it (fb_cancel_operation + arrived with Firebird 2.5)} + fb_cancel_operation: Tfb_cancel_operation; isc_wait_for_event: Tisc_wait_for_event; isc_vax_integer: Tisc_vax_integer; isc_blob_info: Tisc_blob_info; @@ -383,6 +386,12 @@ function TFB25ClientAPI.LoadInterface: boolean; Result := inherited LoadInterface; BLOB_get := GetProcAddr('BLOB_get'); {do not localize} BLOB_put := GetProcAddr('BLOB_put'); {do not localize} + try + fb_cancel_operation := GetProcAddr('fb_cancel_operation'); {do not localize} + except + {a pre 2.5 or InterBase library - CancelOperation reports not supported} + fb_cancel_operation := nil; + end; isc_wait_for_event := GetProcAddr('isc_wait_for_event'); {do not localize} isc_vax_integer := GetProcAddr('isc_vax_integer'); {do not localize} isc_blob_info := GetProcAddr('isc_blob_info'); {do not localize} diff --git a/client/3.0/FB30Array.pas b/client/3.0/FB30Array.pas index f3f1a990..5cdaf7bb 100644 --- a/client/3.0/FB30Array.pas +++ b/client/3.0/FB30Array.pas @@ -39,7 +39,7 @@ interface uses Classes, SysUtils, FirebirdOOAPI, IB, FBArray, IBHeader, FB30Attachment, FBClientAPI, - FB30Transaction, FBParamBlock, FB30ClientAPI; + FB30Transaction, FBParamBlock, FB30ClientAPI, FBSDL; type @@ -66,7 +66,6 @@ TFB30Array = class(TFBArray,IArray) FTransactionIntf: FirebirdOOAPI.ITransaction; FFirebird30ClientAPI: TFB30ClientAPI; FSDL: ISDL; - procedure GenerateSDL; protected procedure AllocateBuffer; override; procedure InternalGetSlice; override; @@ -76,14 +75,10 @@ TFB30Array = class(TFBArray,IArray) constructor Create(aAttachment: TFB30Attachment; aTransaction: TFB30Transaction; aField: IArrayMetaData; ArrayID: TISC_QUAD); overload; end; - TSDLItem = class(TParamBlockItem,ISDLItem); - - { TSDLBlock } - - TSDLBlock = class (TCustomParamBlock, ISDL) - public - constructor Create(api: TFBClientAPI); - end; + {The SDL generator now lives in FBSDL so that the wire protocol provider + can share it - these aliases keep existing references compiling} + TSDLItem = FBSDL.TSDLItem; + TSDLBlock = FBSDL.TSDLBlock; implementation @@ -182,74 +177,12 @@ function TFB30ArrayMetaData.GetCharSetWidth: integer; { TFB30Array } -procedure TFB30Array.GenerateSDL; - - procedure AddVarInteger(aValue: integer); - begin - if (aValue >= -128) and (aValue <= 127) then - FSDL.Add(isc_sdl_tiny_integer).SetAsTinyInteger(aValue) - else - if (aValue >= -32768) and (aValue <= 32767) then - FSDL.Add(isc_sdl_short_integer).SetAsShortInteger(aValue) - else - FSDL.Add(isc_sdl_long_integer).SetAsInteger(aValue); - end; - -var i: integer; - SDLItem: ISDLItem; -begin - FSDL := TSDLBlock.Create(FFirebird30ClientAPI); - with GetArrayDesc^ do - {The following is based on gen_SDL from Firebird src/dsql/array.cpp} - begin - SDLItem := FSDL.Add(isc_sdl_struct); - SDLItem.SetAsByte(array_desc_dtype); - - case array_desc_dtype of - blr_short,blr_long, - blr_int64,blr_quad, - blr_int128: - SDLItem.AddShortInt(array_desc_scale); - - blr_text,blr_cstring, blr_varying: - SDLItem.addShortInteger(array_desc_length); - end; - - FSDL.Add(isc_sdl_relation).SetAsString(array_desc_relation_name); - FSDL.Add(isc_sdl_field).SetAsString(array_desc_field_name); - - for i := 0 to array_desc_dimensions - 1 do - begin - if array_desc_bounds[i].array_bound_lower = 1 then - FSDL.Add(isc_sdl_do1).SetAsTinyInteger(i) - else - begin - FSDL.Add(isc_sdl_do2).SetAsTinyInteger(i); - AddVarInteger(array_desc_bounds[i].array_bound_lower); - end; - AddVarInteger(array_desc_bounds[i].array_bound_upper); - end; - - SDLItem := FSDL.Add(isc_sdl_element); - SDLItem.AddByte(1); - SDLItem := FSDL.Add(isc_sdl_scalar); - SDLItem.AddByte(0); - SDLItem.AddByte(array_desc_dimensions); - for i := 0 to array_desc_dimensions - 1 do - begin - SDLItem := FSDL.Add(isc_sdl_variable); - SDLItem.AddByte(i); - end; - FSDL.Add(isc_sdl_eoc); - end; -end; - procedure TFB30Array.AllocateBuffer; begin inherited AllocateBuffer; {Now set up the SDL} - GenerateSDL; + FSDL := GenerateSDL(FFirebird30ClientAPI,GetArrayDesc); end; procedure TFB30Array.InternalGetSlice; @@ -302,14 +235,5 @@ constructor TFB30Array.Create(aAttachment: TFB30Attachment; FFirebird30ClientAPI := aAttachment.Firebird30ClientAPI; end; -{ TSDLBlock } - -constructor TSDLBlock.Create(api: TFBClientAPI); -begin - inherited Create(api); - FDataLength := 1; - FBuffer^ := isc_sdl_version1; -end; - end. diff --git a/client/3.0/FB30Attachment.pas b/client/3.0/FB30Attachment.pas index 398ab3c2..ca48fbe1 100644 --- a/client/3.0/FB30Attachment.pas +++ b/client/3.0/FB30Attachment.pas @@ -108,6 +108,7 @@ TFB30Attachment = class(TFBAttachment,IAttachment, IActivityMonitor) function HasDecFloatSupport: boolean; override; function HasBatchMode: boolean; override; function HasScollableCursors: boolean; + procedure CancelOperation(aKind: integer = fb_cancel_raise); override; {Time Zone Support} function GetTimeZoneServices: ITimeZoneServices; override; @@ -500,6 +501,16 @@ function TFB30Attachment.HasScollableCursors: boolean; Result := (GetODSMajorVersion >= 12); end; +procedure TFB30Attachment.CancelOperation(aKind: integer); +begin + CheckHandle; + with FFirebird30ClientAPI do + begin + FAttachmentIntf.cancelOperation(StatusIntf,aKind); + Check4DataBaseError; + end; +end; + function TFB30Attachment.GetTimeZoneServices: ITimeZoneServices; begin if not HasTimeZoneSupport then diff --git a/client/3.0/FB30Statement.pas b/client/3.0/FB30Statement.pas index c188bc44..29aed5fd 100644 --- a/client/3.0/FB30Statement.pas +++ b/client/3.0/FB30Statement.pas @@ -301,6 +301,7 @@ TFB30Statement = class(TFBStatement,IStatement) procedure FreeHandle; override; procedure InternalClose(Force: boolean); override; function SavePerfStats(var Stats: TPerfStatistics): boolean; + procedure ApplyStatementTimeout; public constructor Create(Attachment: TFB30Attachment; Transaction: ITransaction; sql: AnsiString; aSQLDialect: integer; CursorName: AnsiString=''); @@ -325,6 +326,8 @@ TFB30Statement = class(TFBStatement,IStatement) procedure SetRetainInterfaces(aValue: boolean); override; function IsInBatchMode: boolean; override; function HasBatchMode: boolean; override; + {needs a Firebird 4 or later client library} + procedure SetStatementTimeout(aMilliseconds: cardinal); override; procedure AddToBatch; override; function ExecuteBatch(aTransaction: ITransaction ): IBatchCompletion; override; @@ -1502,6 +1505,7 @@ function TFB30Statement.InternalExecute(aTransaction: ITransaction): IResults; with FFirebird30ClientAPI do begin SavePerfStats(FBeforeStats); + ApplyStatementTimeout; inMetadata := FSQLParams.GetMetaData; try FStatementIntf.execute(StatusIntf, @@ -1609,6 +1613,7 @@ function TFB30Statement.InternalOpenCursor(aTransaction: ITransaction; with FFirebird30ClientAPI do begin + ApplyStatementTimeout; inMetadata := FSQLParams.GetMetaData; outMetadata := FSQLRecord.GetMetaData; try @@ -1903,6 +1908,23 @@ function TFB30Statement.HasBatchMode: boolean; Result := GetAttachment.HasBatchMode; end; +procedure TFB30Statement.SetStatementTimeout(aMilliseconds: cardinal); +begin + if (aMilliseconds <> 0) and not FFirebird30ClientAPI.Firebird4orLater then + IBError(ibxeNotSupported,[nil]); + FStatementTimeout := aMilliseconds; +end; + +procedure TFB30Statement.ApplyStatementTimeout; +begin + if FStatementTimeout = 0 then Exit; + with FFirebird30ClientAPI do + begin + FStatementIntf.setTimeout(StatusIntf,FStatementTimeout); + Check4DataBaseError(ConnectionCodePage); + end; +end; + procedure TFB30Statement.AddToBatch; var BatchPB: TXPBParameterBlock; inMetadata: FirebirdOOAPI.IMessageMetadata; diff --git a/client/FBAttachment.pas b/client/FBAttachment.pas index 5a1d1f90..d2558856 100644 --- a/client/FBAttachment.pas +++ b/client/FBAttachment.pas @@ -264,9 +264,12 @@ TFBAttachment = class(TFBJournaling) function GetInlineBlobLimit: integer; procedure SetInlineBlobLimit(limit: integer); function HasBatchMode: boolean; virtual; + function HasArraySupport: boolean; virtual; + function HasEventSupport: boolean; virtual; function HasTable(aTableName: AnsiString): boolean; function HasFunction(aFunctionName: AnsiString): boolean; function HasProcedure(aProcName: AnsiString): boolean; + procedure CancelOperation(aKind: integer = fb_cancel_raise); virtual; public {Character Sets} @@ -425,7 +428,7 @@ implementation ); const - isc_dpb_last_dpb_constant = isc_dpb_decfloat_traps; + isc_dpb_last_dpb_constant = isc_dpb_gbak_restore_has_schema; DPBConstantNames: array[1..isc_dpb_last_dpb_constant] of string = ( 'cdd_pathname', @@ -522,7 +525,19 @@ implementation 'set_db_replica', 'set_bind', 'decfloat_round', - 'decfloat_traps' + 'decfloat_traps', + 'clear_map', + 'upgrade_db', + 'dpb_98', {unassigned} + 'dpb_99', {unassigned} + 'parallel_workers', + 'worker_attach', + 'owner', + 'max_blob_cache_size', + 'max_inline_blob_size', + 'search_path', + 'blr_request_search_path', + 'gbak_restore_has_schema' ); type @@ -1553,6 +1568,21 @@ function TFBAttachment.HasBatchMode: boolean; Result := false; end; +function TFBAttachment.HasArraySupport: boolean; +begin + Result := true; +end; + +procedure TFBAttachment.CancelOperation(aKind: integer); +begin + IBError(ibxeNotSupported,[nil]); +end; + +function TFBAttachment.HasEventSupport: boolean; +begin + Result := true; +end; + function TFBAttachment.HasTable(aTableName: AnsiString): boolean; begin Result := OpenCursorAtStart( diff --git a/client/FBClientAPI.pas b/client/FBClientAPI.pas index 61add456..3b5a3121 100644 --- a/client/FBClientAPI.pas +++ b/client/FBClientAPI.pas @@ -119,11 +119,11 @@ TFBStatus = class(TFBInterfacedObject, IStatus) private FIBDataBaseErrorMessages: TIBDataBaseErrorMessages; FPrefix: AnsiString; - function SQLCodeSupported: boolean; protected FOwner: TFBClientAPI; + function SQLCodeSupported: boolean; virtual; function GetIBMessage(CodePage: TSystemCodePage): Ansistring; virtual; abstract; - function GetSQLMessage(CodePage: TSystemCodePage): Ansistring; + function GetSQLMessage(CodePage: TSystemCodePage): Ansistring; virtual; public constructor Create(aOwner: TFBClientAPI; prefix: AnsiString=''); constructor Copy(src: TFBStatus); @@ -133,7 +133,7 @@ TFBStatus = class(TFBInterfacedObject, IStatus) {IStatus} function InErrorState: boolean; virtual; abstract; function GetIBErrorCode: TStatusCode; - function Getsqlcode: TStatusCode; + function Getsqlcode: TStatusCode; virtual; function GetMessage(CodePage: TSystemCodePage): AnsiString; function CheckStatusVector(ErrorCodes: array of TFBStatusCode): Boolean; function GetIBDataBaseErrorMessages: TIBDataBaseErrorMessages; diff --git a/client/FBSDL.pas b/client/FBSDL.pas new file mode 100644 index 00000000..2998757e --- /dev/null +++ b/client/FBSDL.pas @@ -0,0 +1,136 @@ +(* + * Firebird Interface (fbintf). The fbintf components provide a set of + * Pascal language bindings for the Firebird API. + * + * The contents of this file are subject to the Initial Developer's + * Public License Version 1.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy + * of the License here: + * + * http://www.firebirdsql.org/index.php?op=doc&id=idpl + * + * Software distributed under the License is distributed on an "AS + * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or + * implied. See the License for the specific language governing rights + * and limitations under the License. + * + * The Initial Developer of the Original Code is Tony Whyman. + * + * The Original Code is (C) 2016 Tony Whyman, MWA Software + * (http://www.mwasoftware.co.uk). + * + * All Rights Reserved. + * + * Contributor(s): ______________________________________. + * +*) +unit FBSDL; +{$IFDEF MSWINDOWS} +{$DEFINE WINDOWS} +{$ENDIF} + +{$IFDEF FPC} +{$mode delphi} +{$codepage UTF8} +{$interfaces COM} +{$ENDIF} + +{ The SDL (slice description language) program that describes an array + slice to the server. The generator is shared by every provider that + builds its own SDL: the 3.0 provider hands it to getSlice/putSlice and + the wire protocol provider to op_get_slice/op_put_slice. } + +interface + +uses + Classes, SysUtils, IB, IBHeader, FBClientAPI, FBParamBlock; + +type + TSDLItem = class(TParamBlockItem,ISDLItem); + + { TSDLBlock } + + TSDLBlock = class (TCustomParamBlock, ISDL) + public + constructor Create(api: TFBClientAPI); + end; + +{Generates the SDL describing the full slice of the array - based on + gen_SDL from Firebird src/dsql/array.cpp} +function GenerateSDL(api: TFBClientAPI; aArrayDesc: PISC_ARRAY_DESC): ISDL; + +implementation + +function GenerateSDL(api: TFBClientAPI; aArrayDesc: PISC_ARRAY_DESC): ISDL; +var FSDL: ISDL; + + procedure AddVarInteger(aValue: integer); + begin + if (aValue >= -128) and (aValue <= 127) then + FSDL.Add(isc_sdl_tiny_integer).SetAsTinyInteger(aValue) + else + if (aValue >= -32768) and (aValue <= 32767) then + FSDL.Add(isc_sdl_short_integer).SetAsShortInteger(aValue) + else + FSDL.Add(isc_sdl_long_integer).SetAsInteger(aValue); + end; + +var i: integer; + SDLItem: ISDLItem; +begin + FSDL := TSDLBlock.Create(api); + with aArrayDesc^ do + begin + SDLItem := FSDL.Add(isc_sdl_struct); + SDLItem.SetAsByte(array_desc_dtype); + + case array_desc_dtype of + blr_short,blr_long, + blr_int64,blr_quad, + blr_int128: + SDLItem.AddShortInt(array_desc_scale); + + blr_text,blr_cstring, blr_varying: + SDLItem.addShortInteger(array_desc_length); + end; + + FSDL.Add(isc_sdl_relation).SetAsString(array_desc_relation_name); + FSDL.Add(isc_sdl_field).SetAsString(array_desc_field_name); + + for i := 0 to array_desc_dimensions - 1 do + begin + if array_desc_bounds[i].array_bound_lower = 1 then + FSDL.Add(isc_sdl_do1).SetAsTinyInteger(i) + else + begin + FSDL.Add(isc_sdl_do2).SetAsTinyInteger(i); + AddVarInteger(array_desc_bounds[i].array_bound_lower); + end; + AddVarInteger(array_desc_bounds[i].array_bound_upper); + end; + + SDLItem := FSDL.Add(isc_sdl_element); + SDLItem.AddByte(1); + SDLItem := FSDL.Add(isc_sdl_scalar); + SDLItem.AddByte(0); + SDLItem.AddByte(array_desc_dimensions); + for i := 0 to array_desc_dimensions - 1 do + begin + SDLItem := FSDL.Add(isc_sdl_variable); + SDLItem.AddByte(i); + end; + FSDL.Add(isc_sdl_eoc); + end; + Result := FSDL; +end; + +{ TSDLBlock } + +constructor TSDLBlock.Create(api: TFBClientAPI); +begin + inherited Create(api); + FDataLength := 1; + FBuffer^ := isc_sdl_version1; +end; + +end. diff --git a/client/FBStatement.pas b/client/FBStatement.pas index 23f50fcd..f89a1338 100644 --- a/client/FBStatement.pas +++ b/client/FBStatement.pas @@ -81,6 +81,7 @@ TFBStatement = class(TActivityReporter,ITransactionUser) FAfterStats: TPerfStatistics; FCaseSensitiveParams: boolean; FBatchRowLimit: integer; + FStatementTimeout: cardinal; {milliseconds, 0 = none} procedure CheckChangeBatchRowLimit; virtual; procedure CheckHandle; virtual; abstract; procedure CheckTransaction(aTransaction: ITransaction); @@ -152,6 +153,10 @@ TFBStatement = class(TActivityReporter,ITransactionUser) {Stale Reference Check} procedure SetStaleReferenceChecks(Enable:boolean); {default true} function GetStaleReferenceChecks: boolean; + {Statement timeout - the base implementation rejects a non zero + timeout; providers that can honour one override} + procedure SetStatementTimeout(aMilliseconds: cardinal); virtual; + function GetStatementTimeout: cardinal; public property ChangeSeqNo: integer read FChangeSeqNo; property SQLParams: ISQLParams read GetSQLParams; @@ -418,6 +423,18 @@ function TFBStatement.GetStaleReferenceChecks: boolean; Result := FStaleReferenceChecks; end; +procedure TFBStatement.SetStatementTimeout(aMilliseconds: cardinal); +begin + if aMilliseconds <> 0 then + IBError(ibxeNotSupported,[nil]); + FStatementTimeout := 0; +end; + +function TFBStatement.GetStatementTimeout: cardinal; +begin + Result := FStatementTimeout; +end; + function TFBStatement.OpenCursor(aTransaction: ITransaction): IResultSet; begin Result := OpenCursor(false,aTransaction); diff --git a/client/IB.pas b/client/IB.pas index bb166460..9d100f84 100644 --- a/client/IB.pas +++ b/client/IB.pas @@ -864,6 +864,14 @@ TArrayBound = record {Stale Reference Check} procedure SetStaleReferenceChecks(Enable:boolean); {default true} function GetStaleReferenceChecks: boolean; + {Statement timeout in milliseconds, zero (the default) for none. When + it expires the statement fails with isc_cancelled, carrying + isc_req_stmt_timeout as the secondary status code (see + thread_db::checkCancelState in the Firebird sources). Needs a + Firebird 4 or later client library, or wire protocol 16 or later: + otherwise setting a non zero value raises ibxeNotSupported.} + procedure SetStatementTimeout(aMilliseconds: cardinal); + function GetStatementTimeout: cardinal; property MetaData: IMetaData read GetMetaData; property SQLParams: ISQLParams read GetSQLParams; @@ -1246,10 +1254,18 @@ TDBOperationCount = record function HasDecFloatSupport: boolean; function HasBatchMode: boolean; function HasScollableCursors: boolean; + function HasArraySupport: boolean; + function HasEventSupport: boolean; function HasTable(aTableName: AnsiString): boolean; {case sensitive} function HasFunction(aFunctionName: AnsiString): boolean; {case sensitive} function HasProcedure(aProcName: AnsiString): boolean; {case sensitive} + {Operation Cancellation - the fb_cancel_operation family. Called from + a different thread to the one blocked in an operation on this + attachment; that operation then fails with isc_cancelled. aKind is + one of the fb_cancel_* constants.} + procedure CancelOperation(aKind: integer = fb_cancel_raise); + {Character Sets} function GetCharSetID: integer; {connection character set} function GetCodePage: TSystemCodePage; diff --git a/client/IBHeader.pas b/client/IBHeader.pas index 90a24473..30418bca 100644 --- a/client/IBHeader.pas +++ b/client/IBHeader.pas @@ -435,6 +435,11 @@ TISC_TEB = record db_handle : PISC_DB_HANDLE): ISC_STATUS; {$IFDEF WINDOWS} stdcall; {$ELSE} cdecl; {$ENDIF} +Tfb_cancel_operation = function (status_vector : PISC_STATUS; + db_handle : PISC_DB_HANDLE; + option : UShort): ISC_STATUS; + {$IFDEF WINDOWS} stdcall; {$ELSE} cdecl; {$ENDIF} + Tisc_drop_database = function (status_vector : PISC_STATUS; db_handle : PISC_DB_HANDLE): ISC_STATUS; {$IFDEF WINDOWS} stdcall; {$ELSE} cdecl; {$ENDIF} diff --git a/client/include/consts_pub.inc b/client/include/consts_pub.inc index 4d97b22a..78cbbd92 100644 --- a/client/include/consts_pub.inc +++ b/client/include/consts_pub.inc @@ -134,6 +134,15 @@ isc_dpb_decfloat_round = 94; isc_dpb_decfloat_traps = 95; isc_dpb_clear_map = 96; + isc_dpb_upgrade_db = 97; + isc_dpb_parallel_workers = 100; + isc_dpb_worker_attach = 101; + isc_dpb_owner = 102; + isc_dpb_max_blob_cache_size = 103; + isc_dpb_max_inline_blob_size = 104; + isc_dpb_search_path = 105; + isc_dpb_blr_request_search_path = 106; + isc_dpb_gbak_restore_has_schema = 107; {************************************************ } { clumplet tags used inside isc_dpb_address_path } { and isc_spb_address_path } diff --git a/client/include/inf_pub.inc b/client/include/inf_pub.inc index 02ba1300..aebd2c84 100644 --- a/client/include/inf_pub.inc +++ b/client/include/inf_pub.inc @@ -441,6 +441,7 @@ isc_info_sql_stmt_blob_align = 30; isc_info_sql_exec_path_blr_bytes = 31; isc_info_sql_exec_path_blr_text = 32; + isc_info_sql_relation_schema = 33; {******************************* } { SQL information return values } {******************************* } diff --git a/client/wire/FBWireArray.pas b/client/wire/FBWireArray.pas new file mode 100644 index 00000000..649c8f1b --- /dev/null +++ b/client/wire/FBWireArray.pas @@ -0,0 +1,273 @@ +(* + * Firebird Interface (fbintf). The fbintf components provide a set of + * Pascal language bindings for the Firebird API. + * + * This file is part of the pure Pascal wire protocol implementation + * (no fbclient required) and is subject to the Initial Developer's + * Public License Version 1.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy + * of the License here: + * + * http://www.firebirdsql.org/index.php?op=doc&id=idpl + * + * Software distributed under the License is distributed on an "AS + * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or + * implied. See the License for the specific language governing rights + * and limitations under the License. + * + * The Initial Developer of the Original Code is MWA Software + * (http://www.mwasoftware.co.uk). + * + * All Rights Reserved. + * + * Contributor(s): ______________________________________. + * +*) +unit FBWireArray; + +{$IFDEF MSWINDOWS} +{$DEFINE WINDOWS} +{$ENDIF} + +{$IFDEF FPC} +{$mode delphi} +{$codepage UTF8} +{$interfaces COM} +{$ENDIF} + +interface + +uses + Classes, SysUtils, IB, IBHeader, FBArray, FBTransaction, FBSDL, + FBWireClientAPI, FBWireProtocol, FBWireMessage; + +type + { TFBWireArrayMetaData } + + TFBWireArrayMetaData = class(TFBArrayMetaData,IArrayMetaData) + private + FCodePage: TSystemCodePage; + FCharSetWidth: integer; + protected + procedure LoadMetaData(aAttachment: IAttachment; aTransaction: ITransaction; + relationName, columnName: AnsiString); override; + public + function GetCharSetID: cardinal; override; + function GetCodePage: TSystemCodePage; override; + function GetCharSetWidth: integer; override; + end; + + { TFBWireArray } + + TFBWireArray = class(TFBArray,IArray) + private + FWireAttachment: TObject; {TFBWireAttachment - typed in the implementation} + FWireAPI: TFBWireClientAPI; + FSDL: ISDL; + function GetConnection: TFBWireConnection; + function GetTransactionHandle: integer; + function GetSliceLayout: TWireSliceLayout; + function SDLBytes: TBytes; + function ArrayIDAsInt64: Int64; + protected + procedure AllocateBuffer; override; + procedure InternalGetSlice; override; + procedure InternalPutSlice(Force: boolean); override; + public + constructor Create(aAttachment: IAttachment; aTransaction: TFBTransaction; + aField: IArrayMetaData); overload; + constructor Create(aAttachment: IAttachment; aTransaction: TFBTransaction; + aField: IArrayMetaData; ArrayID: TISC_QUAD); overload; + end; + +implementation + +uses IBUtils, FBAttachment, FBWireAttachment, FBWireTransaction; + +const + {the same system table lookup the 3.0 provider uses} + sGetArrayMetaData = 'Select F.RDB$FIELD_LENGTH, F.RDB$FIELD_SCALE, F.RDB$FIELD_TYPE, '+ + 'F.RDB$DIMENSIONS, FD.RDB$DIMENSION, FD.RDB$LOWER_BOUND, FD.RDB$UPPER_BOUND, '+ + 'F.RDB$CHARACTER_SET_ID '+ + 'From RDB$FIELDS F JOIN RDB$RELATION_FIELDS RF '+ + 'On F.RDB$FIELD_NAME = RF.RDB$FIELD_SOURCE JOIN RDB$FIELD_DIMENSIONS FD '+ + 'On FD.RDB$FIELD_NAME = F.RDB$FIELD_NAME ' + + 'Where RF.RDB$RELATION_NAME = ? and RF.RDB$FIELD_NAME = ? ' + + 'UNION '+ + 'Select F.RDB$FIELD_LENGTH, F.RDB$FIELD_SCALE, F.RDB$FIELD_TYPE, '+ + 'F.RDB$DIMENSIONS, FD.RDB$DIMENSION, FD.RDB$LOWER_BOUND, FD.RDB$UPPER_BOUND, '+ + 'F.RDB$CHARACTER_SET_ID '+ + 'From RDB$FIELDS F JOIN RDB$PROCEDURE_PARAMETERS PP '+ + 'On F.RDB$FIELD_NAME = PP.RDB$FIELD_SOURCE JOIN RDB$FIELD_DIMENSIONS FD '+ + 'On FD.RDB$FIELD_NAME = F.RDB$FIELD_NAME ' + + 'Where PP.RDB$PROCEDURE_NAME = ? and PP.RDB$PARAMETER_NAME = ? '+ + 'Order by 5 asc'; + +{ TFBWireArrayMetaData } + +{Assemble the array descriptor from the system tables - the query runs + over the wire like any other, so this is the 3.0 provider's LoadMetaData + with an IAttachment.OpenCursor in place of the direct statement} + +procedure TFBWireArrayMetaData.LoadMetaData(aAttachment: IAttachment; + aTransaction: ITransaction; relationName, columnName: AnsiString); +var RS: IResultSet; + CharWidth: integer; +begin + CharWidth := 0; + RelationName := SafeAnsiUpperCase(RelationName); + ColumnName := SafeAnsiUpperCase(ColumnName); + RS := aAttachment.OpenCursor(aTransaction,sGetArrayMetaData, + [RelationName,ColumnName,RelationName,ColumnName]); + if RS.FetchNext then + begin + FillChar(FArrayDesc.array_desc_field_name,sizeof(FArrayDesc.array_desc_field_name),' '); + FillChar(FArrayDesc.array_desc_relation_name,sizeof(FArrayDesc.array_desc_relation_name),' '); + Move(columnName[1],FArrayDesc.array_desc_field_name,Length(columnName)); + Move(relationName[1],FArrayDesc.array_desc_relation_name,length(relationName)); + FArrayDesc.array_desc_length := RS[0].AsInteger; + FArrayDesc.array_desc_scale := RS[1].AsInteger; + FArrayDesc.array_desc_dtype := RS[2].AsInteger; + FArrayDesc.array_desc_dimensions := RS[3].AsInteger; + FArrayDesc.array_desc_flags := 0; {row major} + FCharSetID := RS[7].AsInteger; + if (FCharSetID > 1) and aAttachment.HasDefaultCharSet then + FCharSetID := aAttachment.GetDefaultCharSetID; + FCodePage := CP_NONE; + FAttachment.CharSetID2CodePage(FCharSetID,FCodePage); + FCharSetWidth := 1; + FAttachment.CharSetWidth(FCharSetID,FCharSetWidth); + if (FArrayDesc.array_desc_dtype in [blr_text,blr_cstring, blr_varying]) and + (FCharSetID = 0) then {This really shouldn't be necessary - but it is :(} + with aAttachment as TFBAttachment do + begin + if HasDefaultCharSet and FAttachment.CharSetWidth(CharSetID,CharWidth) then + FArrayDesc.array_desc_length := FArrayDesc.array_desc_length * CharWidth; + end; + repeat + with FArrayDesc.array_desc_bounds[RS[4].AsInteger] do + begin + array_bound_lower := RS[5].AsInteger; + array_bound_upper := RS[6].AsInteger; + end; + until not RS.FetchNext; + end; + RS.Close; +end; + +function TFBWireArrayMetaData.GetCharSetID: cardinal; +begin + Result := FCharSetID; +end; + +function TFBWireArrayMetaData.GetCodePage: TSystemCodePage; +begin + Result := FCodePage; +end; + +function TFBWireArrayMetaData.GetCharSetWidth: integer; +begin + Result := FCharSetWidth; +end; + +{ TFBWireArray } + +constructor TFBWireArray.Create(aAttachment: IAttachment; + aTransaction: TFBTransaction; aField: IArrayMetaData); +begin + inherited Create(aAttachment,aTransaction,aField); + FWireAttachment := aAttachment as TObject; + FWireAPI := (FWireAttachment as TFBWireAttachment).WireAPI; +end; + +constructor TFBWireArray.Create(aAttachment: IAttachment; + aTransaction: TFBTransaction; aField: IArrayMetaData; ArrayID: TISC_QUAD); +begin + inherited Create(aAttachment,aTransaction,aField,ArrayID); + FWireAttachment := aAttachment as TObject; + FWireAPI := (FWireAttachment as TFBWireAttachment).WireAPI; +end; + +function TFBWireArray.GetConnection: TFBWireConnection; +begin + Result := (FWireAttachment as TFBWireAttachment).Connection; +end; + +function TFBWireArray.GetTransactionHandle: integer; +begin + Result := (GetTransaction as TObject as TFBWireTransaction).Handle; +end; + +function TFBWireArray.GetSliceLayout: TWireSliceLayout; +var i: integer; +begin + with GetArrayDesc^ do + begin + Result.Dtype := array_desc_dtype; + Result.ElementLength := array_desc_length; + {the buffer layout TFBArray.AllocateBuffer establishes: an extra count + of two bytes for varying, one for the terminator of text} + case array_desc_dtype of + blr_varying, blr_varying2: + Result.BufferStride := array_desc_length + 2; + blr_text, blr_text2: + Result.BufferStride := array_desc_length + 1; + else + Result.BufferStride := array_desc_length; + end; + Result.Count := 1; + for i := 0 to array_desc_dimensions - 1 do + Result.Count := Result.Count * + cardinal(array_desc_bounds[i].array_bound_upper - + array_desc_bounds[i].array_bound_lower + 1); + end; +end; + +function TFBWireArray.SDLBytes: TBytes; +var len: integer; +begin + len := (FSDL as TSDLBlock).getDataLength; + SetLength(Result,len); + if len > 0 then + Move((FSDL as TSDLBlock).getBuffer^,Result[0],len); +end; + +function TFBWireArray.ArrayIDAsInt64: Int64; +begin + Result := (Int64(FArrayID.gds_quad_high) shl 32) or + Int64(cardinal(FArrayID.gds_quad_low)); +end; + +procedure TFBWireArray.AllocateBuffer; +begin + inherited AllocateBuffer; + FSDL := GenerateSDL(FWireAPI,GetArrayDesc); +end; + +procedure TFBWireArray.InternalGetSlice; +begin + try + GetConnection.GetSlice(GetTransactionHandle,ArrayIDAsInt64,SDLBytes, + GetSliceLayout,FBuffer); + except + on E: Exception do WireIBError(FWireAPI,E); + end; + SignalActivity; +end; + +procedure TFBWireArray.InternalPutSlice(Force: boolean); +var id: Int64; +begin + try + id := GetConnection.PutSlice(GetTransactionHandle,ArrayIDAsInt64,SDLBytes, + GetSliceLayout,FBuffer); + FArrayID.gds_quad_high := Integer(id shr 32); + FArrayID.gds_quad_low := Cardinal(id and $FFFFFFFF); + except + on E: Exception do + if not Force then WireIBError(FWireAPI,E); + end; + SignalActivity; +end; + +end. diff --git a/client/wire/FBWireAttachment.pas b/client/wire/FBWireAttachment.pas new file mode 100644 index 00000000..a326df0e --- /dev/null +++ b/client/wire/FBWireAttachment.pas @@ -0,0 +1,712 @@ +(* + * Firebird Interface (fbintf). The fbintf components provide a set of + * Pascal language bindings for the Firebird API. + * + * This file is part of the pure Pascal wire protocol implementation + * (no fbclient required) and is subject to the Initial Developer's + * Public License Version 1.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy + * of the License here: + * + * http://www.firebirdsql.org/index.php?op=doc&id=idpl + * + * Software distributed under the License is distributed on an "AS + * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or + * implied. See the License for the specific language governing rights + * and limitations under the License. + * + * The Initial Developer of the Original Code is MWA Software + * (http://www.mwasoftware.co.uk). + * + * All Rights Reserved. + * + * Contributor(s): ______________________________________. + * +*) +unit FBWireAttachment; + +{$IFDEF MSWINDOWS} +{$DEFINE WINDOWS} +{$ENDIF} + +{$IFDEF FPC} +{$mode delphi} +{$codepage UTF8} +{$interfaces COM} +{$ENDIF} + +interface + +uses + Classes, SysUtils, IB, FBAttachment, FBClientAPI, FBActivityMonitor, + FBOutputBlock, FBParamBlock, FBWireClientAPI, FBWireProtocol, FBWireConst; + +type + { TFBWireAttachment } + + {a blob pushed by the server with op_inline_blob, waiting to be opened} + TWireInlineBlob = record + TrHandle: integer; + BlobID: Int64; + Info: TBytes; + Data: TBytes; + end; + + TFBWireAttachment = class(TFBAttachment,IAttachment,IActivityMonitor) + private + FWireAPI: TFBWireClientAPI; + FConnection: TFBWireConnection; + FInlineBlobs: array of TWireInlineBlob; + FInlineBlobBytes: integer; + FHandle: integer; + FIsConnected: boolean; + FHost: AnsiString; + FPort: integer; + FRemoteDatabaseName: AnsiString; + FEventManager: TObject; {TFBWireEventManager - typed in FBWireEvents} + procedure HandleInlineBlob(aTrHandle: integer; aBlobID: Int64; + const aInfo, aData: TBytes); + procedure ParseDatabaseName(const aDatabaseName: AnsiString); + procedure ShutdownEventManager; + {opens the TCP connection, authenticates and returns the DPB to send + with op_attach/op_create (the authentication proof is added to it)} + function ConnectAndPrepareDPB: TBytes; + procedure CreateDatabaseFromDPB(RaiseExceptionOnError: boolean); + protected + procedure CheckHandle; override; + function GetAttachment: IAttachment; override; + public + constructor Create(api: TFBWireClientAPI; DatabaseName: AnsiString; + aDPB: IDPB; RaiseExceptionOnConnectError: boolean); overload; + constructor CreateDatabase(api: TFBWireClientAPI; DatabaseName: AnsiString; + aDPB: IDPB; RaiseExceptionOnError: boolean); overload; + constructor CreateDatabase(api: TFBWireClientAPI; sql: AnsiString; + aSQLDialect: integer; RaiseExceptionOnError: boolean); overload; + destructor Destroy; override; + function GetDBInfo(ReqBuffer: PByte; ReqBufLen: integer): IDBInformation; override; + + {inline blob cache (protocol 19). TakeInlineBlob extracts and removes + the entry - the server sends each id once and the engine consumes + registrations the same way} + function TakeInlineBlob(aTrHandle: integer; aBlobID: Int64; + var aInfo, aData: TBytes): boolean; + {forgets a transaction's cached blobs - called when it ends} + procedure DropInlineBlobs(aTrHandle: integer); + + property Connection: TFBWireConnection read FConnection; + property Handle: integer read FHandle; + property WireAPI: TFBWireClientAPI read FWireAPI; + property Host: AnsiString read FHost; + property Port: integer read FPort; + + public + {IAttachment} + procedure Connect; + procedure Disconnect(Force: boolean = false); override; + function IsConnected: boolean; override; + procedure DropDatabase; override; + function StartTransaction(TPB: array of byte; + DefaultCompletion: TTransactionCompletion; + aName: AnsiString = ''): ITransaction; override; + function StartTransaction(TPB: ITPB; DefaultCompletion: TTransactionCompletion; + aName: AnsiString = ''): ITransaction; override; + procedure ExecImmediate(transaction: ITransaction; sql: AnsiString; + aSQLDialect: integer); override; + function Prepare(transaction: ITransaction; sql: AnsiString; + aSQLDialect: integer; CursorName: AnsiString = ''): IStatement; override; + function PrepareWithNamedParameters(transaction: ITransaction; sql: AnsiString; + aSQLDialect: integer; GenerateParamNames: boolean = false; + CaseSensitiveParams: boolean = false; + CursorName: AnsiString = ''): IStatement; override; + + {Events - delivered on the auxiliary connection established with + op_connect_request. The connection and its listener thread are + created on the first call and shared by all IEvents instances.} + function GetEventHandler(Events: TStrings): IEvents; override; + + {Blobs} + function CreateBlob(transaction: ITransaction; BlobMetaData: IBlobMetaData; + BPB: IBPB = nil): IBlob; overload; override; + function CreateBlob(transaction: ITransaction; SubType: integer; + aCharSetID: cardinal = 0; BPB: IBPB = nil): IBlob; overload; + function OpenBlob(transaction: ITransaction; BlobMetaData: IBlobMetaData; + BlobID: TISC_QUAD; BPB: IBPB = nil): IBlob; overload; override; + + {Arrays} + function OpenArray(transaction: ITransaction; ArrayMetaData: IArrayMetaData; + ArrayID: TISC_QUAD): IArray; overload; override; + function CreateArray(transaction: ITransaction; + ArrayMetaData: IArrayMetaData): IArray; overload; override; + function CreateArrayMetaData(SQLType: cardinal; tableName: AnsiString; + columnName: AnsiString; Scale: integer; size: cardinal; + aCharSetID: cardinal; dimensions: cardinal; + bounds: TArrayBounds): IArrayMetaData; + + {Metadata} + function GetBlobMetaData(Transaction: ITransaction; + tableName, columnName: AnsiString): IBlobMetaData; override; + function GetArrayMetaData(Transaction: ITransaction; + tableName, columnName: AnsiString): IArrayMetaData; override; + function HasDecFloatSupport: boolean; override; + function HasTimeZoneSupport: boolean; override; + function HasBatchMode: boolean; override; + function HasArraySupport: boolean; override; + function HasEventSupport: boolean; override; + function HasScollableCursors: boolean; + {op_cancel, sent out of band on the main connection - see + TFBWireConnection.SendCancel for the threading rules} + procedure CancelOperation(aKind: integer = fb_cancel_raise); override; + procedure getFBVersion(version: TStrings); + end; + +implementation + +uses FBMessages, IBUtils, FBWireTransaction, FBWireStatement, + FBWireBlob, FBWireArray, FBWireStream, FBWireEvents; + +{ TFBWireAttachment } + +procedure TFBWireAttachment.ParseDatabaseName(const aDatabaseName: AnsiString); +var aProtocol: TProtocolAll; + aPortNo: AnsiString; +begin + FHost := ''; + FPort := 3050; + FRemoteDatabaseName := aDatabaseName; + if ParseConnectString(aDatabaseName,FHost,FRemoteDatabaseName,aProtocol,aPortNo) then + begin + if aPortNo <> '' then + FPort := StrToIntDef(aPortNo,3050); + case aProtocol of + TCP, inet, inet4, inet6: + {a remote connection - this is what the wire protocol provides}; + Local: + {a local connection cannot be made without the client library, but + most servers also listen on the loopback interface} + if FHost = '' then + FHost := 'localhost'; + else + IBError(ibxeNotSupported,[nil]); + end; + end; + if FHost = '' then + FHost := 'localhost'; +end; + +function TFBWireAttachment.ConnectAndPrepareDPB: TBytes; +var aUser, aPassword: AnsiString; + UserItem, PasswordItem, ConfigItem: IDPBItem; + raw: TBytes; + i, itemLen, outLen: integer; + + {reads WireCompression from an isc_dpb_config item, as the stock + client does: the config text is also forwarded to the server} + function WantsCompression(const aConfig: AnsiString): boolean; + var Lines: TStringList; + j: integer; + key, value: AnsiString; + begin + Result := false; + Lines := TStringList.Create; + try + Lines.Text := aConfig; + for j := 0 to Lines.Count - 1 do + begin + if Pos('=',Lines[j]) = 0 then continue; + key := Trim(system.copy(Lines[j],1,Pos('=',Lines[j])-1)); + value := Trim(system.copy(Lines[j],Pos('=',Lines[j])+1,MaxInt)); + if SameText(key,'WireCompression') then + Result := SameText(value,'true') or (value = '1') or + SameText(value,'yes'); + end; + finally + Lines.Free; + end; + end; + + procedure AddClumplet(aTag: byte; const aValue: AnsiString); + var j: integer; + begin + Result[outLen] := aTag; + Result[outLen+1] := Length(aValue); + for j := 1 to Length(aValue) do + Result[outLen+1+j] := byte(aValue[j]); + Inc(outLen,2 + Length(aValue)); + end; + +begin + aUser := ''; + aPassword := ''; + if DPB <> nil then + begin + UserItem := DPB.Find(isc_dpb_user_name); + if UserItem <> nil then + aUser := UserItem.AsString; + PasswordItem := DPB.Find(isc_dpb_password); + if PasswordItem <> nil then + aPassword := PasswordItem.AsString; + ConfigItem := DPB.Find(isc_dpb_config); + if ConfigItem <> nil then + FConnection.Compression := WantsCompression(ConfigItem.AsString); + end; + + FConnection.ConnectTo(FHost,FPort,FRemoteDatabaseName,aUser,aPassword); + + {The plain text password must not travel to the server: SRP has already + proved knowledge of it. Copy the DPB clumplets verbatim - preserving + whatever encoding each item was built with - minus the password items, + adding the authentication proof where the server asked for it in the + attach (the op_accept_data flow).} + raw := ParamBlockToBytes(DPB); + SetLength(Result,Length(raw) + 1 + + Length(FConnection.AuthData) + Length(FConnection.AuthPluginName) + 4); + outLen := 0; + Result[outLen] := isc_dpb_version1; + Inc(outLen); + i := 1; {skip the version byte of the source, when there is one} + while i + 1 < Length(raw) do + begin + itemLen := raw[i+1]; + if i + 2 + itemLen > Length(raw) then + break; + if not (raw[i] in [isc_dpb_password,isc_dpb_password_enc]) then + begin + Move(raw[i],Result[outLen],2 + itemLen); + Inc(outLen,2 + itemLen); + end; + Inc(i,2 + itemLen); + end; + if FConnection.AuthData <> '' then + begin + AddClumplet(isc_dpb_specific_auth_data,FConnection.AuthData); + AddClumplet(isc_dpb_auth_plugin_name,FConnection.AuthPluginName); + end; + SetLength(Result,outLen); +end; + +constructor TFBWireAttachment.Create(api: TFBWireClientAPI; + DatabaseName: AnsiString; aDPB: IDPB; RaiseExceptionOnConnectError: boolean); +begin + FWireAPI := api; + inherited Create(api,DatabaseName,aDPB,RaiseExceptionOnConnectError); + FConnection := TFBWireConnection.Create; + FConnection.OnInlineBlob := HandleInlineBlob; + ParseDatabaseName(DatabaseName); + Connect; +end; + +constructor TFBWireAttachment.CreateDatabase(api: TFBWireClientAPI; + DatabaseName: AnsiString; aDPB: IDPB; RaiseExceptionOnError: boolean); +begin + FWireAPI := api; + inherited Create(api,DatabaseName,aDPB,RaiseExceptionOnError); + FConnection := TFBWireConnection.Create; + FConnection.OnInlineBlob := HandleInlineBlob; + ParseDatabaseName(DatabaseName); + CreateDatabaseFromDPB(RaiseExceptionOnError); +end; + +procedure TFBWireAttachment.CreateDatabaseFromDPB(RaiseExceptionOnError: boolean); +var dpbBytes: TBytes; +begin + try + dpbBytes := ConnectAndPrepareDPB; + FHandle := FConnection.CreateDatabase(FRemoteDatabaseName,dpbBytes); + FIsConnected := true; + except + on E: Exception do + begin + FConnection.Disconnect; + FIsConnected := false; + if RaiseExceptionOnError then + WireIBError(FWireAPI,E); + end; + end; +end; + +constructor TFBWireAttachment.CreateDatabase(api: TFBWireClientAPI; + sql: AnsiString; aSQLDialect: integer; RaiseExceptionOnError: boolean); + + {The stock providers pass the whole create statement to the client + library, which preparses the file spec out of it. Here that preparse + must be done locally: the file spec is the first quoted string after + CREATE DATABASE/SCHEMA.} + function ExtractCreateDBFileSpec(const aSQL: AnsiString): AnsiString; + var p1, p2: integer; + begin + Result := ''; + p1 := Pos('''',aSQL); + if p1 = 0 then Exit; + p2 := p1 + 1; + while (p2 <= Length(aSQL)) and (aSQL[p2] <> '''') do + Inc(p2); + if p2 > Length(aSQL) then Exit; + Result := system.copy(aSQL,p1+1,p2-p1-1); + end; + +var aDPB: IDPB; +begin + {the DPB and the database name are derived from the create statement} + aDPB := TDPB.Create(api); + inherited Create(api,'',aDPB,RaiseExceptionOnError); + {DPBFromCreateSQL fills the DPB from the USER/PASSWORD clauses} + DPBFromCreateSQL(sql); + FDatabaseName := ExtractCreateDBFileSpec(sql); + FWireAPI := api; + FConnection := TFBWireConnection.Create; + FConnection.OnInlineBlob := HandleInlineBlob; + ParseDatabaseName(FDatabaseName); + CreateDatabaseFromDPB(RaiseExceptionOnError); +end; + +destructor TFBWireAttachment.Destroy; +begin + ShutdownEventManager; + inherited Destroy; + if FConnection <> nil then + FConnection.Free; +end; + +procedure TFBWireAttachment.CheckHandle; +begin + if not FIsConnected then + IBError(ibxeDatabaseClosed,[nil]); +end; + +function TFBWireAttachment.GetAttachment: IAttachment; +begin + Result := self; +end; + +procedure TFBWireAttachment.Connect; +var dpbBytes: TBytes; +begin + if FIsConnected then Exit; + try + dpbBytes := ConnectAndPrepareDPB; + FHandle := FConnection.AttachDatabase(FRemoteDatabaseName,dpbBytes); + FIsConnected := true; + except + on E: Exception do + begin + FConnection.Disconnect; + FIsConnected := false; + if FRaiseExceptionOnConnectError then + WireIBError(FWireAPI,E); + end; + end; + if FIsConnected then + ClearCachedInfo; +end; + +const + {the inline blob cache is bounded: beyond this the server's pushes are + dropped and the blobs open the classic way - a round trip, never an + error} + InlineBlobCacheLimit = 16 * 1024 * 1024; + +procedure TFBWireAttachment.HandleInlineBlob(aTrHandle: integer; + aBlobID: Int64; const aInfo, aData: TBytes); +var n: integer; +begin + if FInlineBlobBytes + Length(aData) > InlineBlobCacheLimit then + Exit; + n := Length(FInlineBlobs); + SetLength(FInlineBlobs,n+1); + FInlineBlobs[n].TrHandle := aTrHandle; + FInlineBlobs[n].BlobID := aBlobID; + FInlineBlobs[n].Info := aInfo; + FInlineBlobs[n].Data := aData; + Inc(FInlineBlobBytes,Length(aData)); +end; + +function TFBWireAttachment.TakeInlineBlob(aTrHandle: integer; aBlobID: Int64; + var aInfo, aData: TBytes): boolean; +var i, j: integer; +begin + Result := false; + for i := 0 to Length(FInlineBlobs) - 1 do + if (FInlineBlobs[i].TrHandle = aTrHandle) and + (FInlineBlobs[i].BlobID = aBlobID) then + begin + aInfo := FInlineBlobs[i].Info; + aData := FInlineBlobs[i].Data; + Dec(FInlineBlobBytes,Length(FInlineBlobs[i].Data)); + for j := i + 1 to Length(FInlineBlobs) - 1 do + FInlineBlobs[j-1] := FInlineBlobs[j]; + SetLength(FInlineBlobs,Length(FInlineBlobs)-1); + Exit(true); + end; +end; + +procedure TFBWireAttachment.DropInlineBlobs(aTrHandle: integer); +var i, n: integer; +begin + n := 0; + for i := 0 to Length(FInlineBlobs) - 1 do + if FInlineBlobs[i].TrHandle <> aTrHandle then + begin + if n <> i then + FInlineBlobs[n] := FInlineBlobs[i]; + Inc(n); + end + else + Dec(FInlineBlobBytes,Length(FInlineBlobs[i].Data)); + SetLength(FInlineBlobs,n); +end; + +procedure TFBWireAttachment.Disconnect(Force: boolean); +begin + if not FIsConnected then Exit; + SetLength(FInlineBlobs,0); + FInlineBlobBytes := 0; + ShutdownEventManager; + EndAllTransactions; + try + FConnection.DetachDatabase(FHandle); + except + on E: Exception do + if not Force then + begin + FIsConnected := false; + FConnection.Disconnect; + WireIBError(FWireAPI,E); + end; + end; + FIsConnected := false; + FHandle := 0; + FConnection.Disconnect; + ClearCachedInfo; +end; + +function TFBWireAttachment.IsConnected: boolean; +begin + Result := FIsConnected; +end; + +procedure TFBWireAttachment.DropDatabase; +begin + CheckHandle; + ShutdownEventManager; + EndAllTransactions; + try + FConnection.DropDatabase(FHandle); + except + on E: Exception do WireIBError(FWireAPI,E); + end; + FIsConnected := false; + FHandle := 0; + FConnection.Disconnect; + ClearCachedInfo; +end; + +function TFBWireAttachment.StartTransaction(TPB: array of byte; + DefaultCompletion: TTransactionCompletion; aName: AnsiString): ITransaction; +begin + CheckHandle; + Result := TFBWireTransaction.Create(FWireAPI,self,TPB,DefaultCompletion,aName); +end; + +function TFBWireAttachment.StartTransaction(TPB: ITPB; + DefaultCompletion: TTransactionCompletion; aName: AnsiString): ITransaction; +begin + CheckHandle; + Result := TFBWireTransaction.Create(FWireAPI,self,TPB,DefaultCompletion,aName); +end; + +procedure TFBWireAttachment.ExecImmediate(transaction: ITransaction; + sql: AnsiString; aSQLDialect: integer); +begin + CheckHandle; + try + FConnection.ExecImmediate((transaction as TObject as TFBWireTransaction).Handle, + FHandle,aSQLDialect,sql); + except + on E: Exception do WireIBError(FWireAPI,E); + end; +end; + +function TFBWireAttachment.Prepare(transaction: ITransaction; sql: AnsiString; + aSQLDialect: integer; CursorName: AnsiString): IStatement; +begin + CheckHandle; + Result := TFBWireStatement.Create(self,transaction,sql,aSQLDialect,CursorName); +end; + +function TFBWireAttachment.PrepareWithNamedParameters(transaction: ITransaction; + sql: AnsiString; aSQLDialect: integer; GenerateParamNames: boolean; + CaseSensitiveParams: boolean; CursorName: AnsiString): IStatement; +begin + CheckHandle; + Result := TFBWireStatement.CreateWithNamedParameters(self,transaction,sql, + aSQLDialect,GenerateParamNames,CaseSensitiveParams,CursorName); +end; + +function TFBWireAttachment.GetEventHandler(Events: TStrings): IEvents; +begin + CheckHandle; + if FEventManager = nil then + FEventManager := TFBWireEventManager.Create(self); + Result := TFBWireEvents.Create(self,TFBWireEventManager(FEventManager),Events); +end; + +procedure TFBWireAttachment.ShutdownEventManager; +begin + if FEventManager <> nil then + begin + FEventManager.Free; + FEventManager := nil; + end; +end; + +function TFBWireAttachment.CreateBlob(transaction: ITransaction; + BlobMetaData: IBlobMetaData; BPB: IBPB): IBlob; +begin + CheckHandle; + Result := TFBWireBlob.Create(self,transaction as TFBWireTransaction, + BlobMetaData,BPB); +end; + +function TFBWireAttachment.CreateBlob(transaction: ITransaction; + SubType: integer; aCharSetID: cardinal; BPB: IBPB): IBlob; +begin + CheckHandle; + Result := TFBWireBlob.Create(self,transaction as TFBWireTransaction, + TFBWireBlobMetaData.Create(self,transaction as TFBWireTransaction, + '','',SubType,aCharSetID),BPB); +end; + +function TFBWireAttachment.OpenBlob(transaction: ITransaction; + BlobMetaData: IBlobMetaData; BlobID: TISC_QUAD; BPB: IBPB): IBlob; +begin + CheckHandle; + Result := TFBWireBlob.Create(self,transaction as TFBWireTransaction, + BlobMetaData,BlobID,BPB); +end; + +function TFBWireAttachment.OpenArray(transaction: ITransaction; + ArrayMetaData: IArrayMetaData; ArrayID: TISC_QUAD): IArray; +begin + CheckHandle; + Result := TFBWireArray.Create(self,transaction as TFBWireTransaction, + ArrayMetaData,ArrayID); +end; + +function TFBWireAttachment.CreateArray(transaction: ITransaction; + ArrayMetaData: IArrayMetaData): IArray; +begin + CheckHandle; + Result := TFBWireArray.Create(self,transaction as TFBWireTransaction, + ArrayMetaData); +end; + +function TFBWireAttachment.CreateArrayMetaData(SQLType: cardinal; + tableName: AnsiString; columnName: AnsiString; Scale: integer; size: cardinal; + aCharSetID: cardinal; dimensions: cardinal; bounds: TArrayBounds): IArrayMetaData; +begin + Result := TFBWireArrayMetaData.Create(self,SQLType,tableName,ColumnName, + Scale,size,aCharSetID,dimensions,bounds); +end; + +function TFBWireAttachment.GetBlobMetaData(Transaction: ITransaction; + tableName, columnName: AnsiString): IBlobMetaData; +begin + CheckHandle; + Result := TFBWireBlobMetaData.Create(self,Transaction as TFBWireTransaction, + tableName,columnName,0,0,false); + +end; + +function TFBWireAttachment.GetArrayMetaData(Transaction: ITransaction; + tableName, columnName: AnsiString): IArrayMetaData; +begin + CheckHandle; + Result := TFBWireArrayMetaData.Create(self,Transaction,tableName,columnName); +end; + +function TFBWireAttachment.GetDBInfo(ReqBuffer: PByte; ReqBufLen: integer): IDBInformation; +var items, response: TBytes; + i, len: integer; + Buffer: TDBInformation; +begin + CheckHandle; + SetLength(items,ReqBufLen); + for i := 0 to ReqBufLen - 1 do + items[i] := ReqBuffer[i]; + SetLength(response,0); + try + response := FConnection.GetInfo(op_info_database,FHandle,items, + DBInfoDefaultBufferSize); + except + on E: Exception do WireIBError(FWireAPI,E); + end; + Buffer := TDBInformation.Create(FWireAPI); + Result := Buffer; + len := Length(response); + if len > Buffer.getBufSize then + len := Buffer.getBufSize; + if len > 0 then + Move(response[0],Buffer.Buffer^,len); +end; + +function TFBWireAttachment.HasDecFloatSupport: boolean; +begin + Result := (FConnection <> nil) and + (FConnection.ProtocolVersion >= (PROTOCOL_VERSION16 and FB_PROTOCOL_MASK)); +end; + +function TFBWireAttachment.HasTimeZoneSupport: boolean; +begin + Result := HasDecFloatSupport; +end; + +function TFBWireAttachment.HasBatchMode: boolean; +begin + {op_batch_create and friends need protocol 16} + Result := (FConnection <> nil) and + (FConnection.ProtocolVersion >= (PROTOCOL_VERSION16 and FB_PROTOCOL_MASK)); +end; + +function TFBWireAttachment.HasArraySupport: boolean; +begin + Result := true; +end; + +function TFBWireAttachment.HasEventSupport: boolean; +begin + Result := true; +end; + +function TFBWireAttachment.HasScollableCursors: boolean; +begin + {op_fetch_scroll needs protocol 18} + Result := (FConnection <> nil) and + (FConnection.ProtocolVersion >= (PROTOCOL_VERSION18 and FB_PROTOCOL_MASK)); +end; + +procedure TFBWireAttachment.CancelOperation(aKind: integer); +begin + CheckHandle; + try + FConnection.SendCancel(aKind); + except + on E: Exception do WireIBError(FWireAPI,E); + end; +end; + +procedure TFBWireAttachment.getFBVersion(version: TStrings); +var CryptDescription: AnsiString; +begin + version.Clear; + if (FConnection <> nil) and (FConnection.CryptPlugin = '') then + CryptDescription := 'unencrypted' + else + if FConnection <> nil then + CryptDescription := FConnection.CryptPlugin + ' wire encryption'; + if FConnection <> nil then + version.Add(Format('Firebird wire protocol version %d, %s authentication, %s', + [FConnection.ProtocolVersion,FConnection.AuthPluginName, + CryptDescription])); +end; + +end. diff --git a/client/wire/FBWireBigInt.pas b/client/wire/FBWireBigInt.pas new file mode 100644 index 00000000..d1fd6a18 --- /dev/null +++ b/client/wire/FBWireBigInt.pas @@ -0,0 +1,478 @@ +(* + * Firebird Interface (fbintf). The fbintf components provide a set of + * Pascal language bindings for the Firebird API. + * + * This file is part of the pure Pascal wire protocol implementation + * (no fbclient required) and is subject to the Initial Developer's + * Public License Version 1.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy + * of the License here: + * + * http://www.firebirdsql.org/index.php?op=doc&id=idpl + * + * Software distributed under the License is distributed on an "AS + * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or + * implied. See the License for the specific language governing rights + * and limitations under the License. + * + * The Initial Developer of the Original Code is MWA Software + * (http://www.mwasoftware.co.uk). + * + * All Rights Reserved. + * + * Contributor(s): ______________________________________. + * +*) +unit FBWireBigInt; + +{ Minimal arbitrary precision unsigned integer arithmetic. Provides just + enough functionality to support the SRP-6a authentication handshake used + by the Firebird Srp/Srp256 authentication plugins: conversion to/from + big-endian byte strings and hex strings, addition, subtraction, + multiplication, division/modulus (Knuth Algorithm D) and modular + exponentiation (square and multiply). } + +{$IFDEF MSWINDOWS} +{$DEFINE WINDOWS} +{$ENDIF} + +{$IFDEF FPC} +{$mode delphi} +{$codepage UTF8} +{$interfaces COM} +{$ENDIF} + +interface + +uses + Classes, SysUtils; + +type + TLimb = Cardinal; {32 bit limb} + TDblLimb = QWord; {64 bit intermediate} + + { TBigInt: value = sum(FLimbs[i] * 2^(32*i)) - little endian limb order. + Always normalised so that the most significant limb is non-zero + (zero is represented by an empty limb array). } + + TBigInt = record + private + FLimbs: array of TLimb; + procedure Normalise; + function GetLimbCount: integer; + public + class function FromBytes(const aValue: TBytes): TBigInt; static; + class function FromHex(aValue: AnsiString): TBigInt; static; + class function FromCardinal(aValue: cardinal): TBigInt; static; + {big-endian, no leading zeroes (empty for zero)} + function ToBytes: TBytes; overload; + {big-endian, left padded with zeroes to aLength bytes} + function ToBytes(aLength: integer): TBytes; overload; + function ToHex: AnsiString; {lower case, no leading zeroes, '0' for zero } + function IsZero: boolean; + function BitLength: integer; + function ByteLength: integer; + class function Compare(const a, b: TBigInt): integer; static; + class function Add(const a, b: TBigInt): TBigInt; static; + {requires a >= b} + class function Subtract(const a, b: TBigInt): TBigInt; static; + class function Multiply(const a, b: TBigInt): TBigInt; static; + class procedure DivMod(const a, b: TBigInt; var Quotient, Remainder: TBigInt); static; + class function Modulus(const a, b: TBigInt): TBigInt; static; + {(aBase ^ aExponent) mod aModulus} + class function ModPow(const aBase, aExponent, aModulus: TBigInt): TBigInt; static; + property LimbCount: integer read GetLimbCount; + end; + + EBigIntError = class(Exception); + +implementation + +{ TBigInt } + +procedure TBigInt.Normalise; +var i: integer; +begin + i := Length(FLimbs); + while (i > 0) and (FLimbs[i-1] = 0) do + Dec(i); + if i <> Length(FLimbs) then + SetLength(FLimbs,i); +end; + +function TBigInt.GetLimbCount: integer; +begin + Result := Length(FLimbs); +end; + +class function TBigInt.FromBytes(const aValue: TBytes): TBigInt; +var i, limbIndex, shift: integer; +begin + SetLength(Result.FLimbs,(Length(aValue) + 3) div 4); + for i := 0 to Length(Result.FLimbs) - 1 do + Result.FLimbs[i] := 0; + {aValue is big-endian: last byte is least significant} + for i := 0 to Length(aValue) - 1 do + begin + limbIndex := (Length(aValue) - 1 - i) div 4; + shift := ((Length(aValue) - 1 - i) mod 4) * 8; + Result.FLimbs[limbIndex] := Result.FLimbs[limbIndex] or (TLimb(aValue[i]) shl shift); + end; + Result.Normalise; +end; + +class function TBigInt.FromHex(aValue: AnsiString): TBigInt; +var bytes: TBytes; + i: integer; + b: integer; + + function NibbleValue(c: AnsiChar): integer; + begin + case c of + '0'..'9': Result := ord(c) - ord('0'); + 'a'..'f': Result := ord(c) - ord('a') + 10; + 'A'..'F': Result := ord(c) - ord('A') + 10; + else + raise EBigIntError.CreateFmt('Invalid hex digit "%s"',[c]); + end; + end; + +begin + if Odd(Length(aValue)) then + aValue := '0' + aValue; + SetLength(bytes,Length(aValue) div 2); + for i := 0 to Length(bytes) - 1 do + begin + b := (NibbleValue(aValue[2*i+1]) shl 4) or NibbleValue(aValue[2*i+2]); + bytes[i] := b; + end; + Result := FromBytes(bytes); +end; + +class function TBigInt.FromCardinal(aValue: cardinal): TBigInt; +begin + if aValue = 0 then + SetLength(Result.FLimbs,0) + else + begin + SetLength(Result.FLimbs,1); + Result.FLimbs[0] := aValue; + end; +end; + +function TBigInt.ToBytes: TBytes; +begin + Result := ToBytes(ByteLength); +end; + +function TBigInt.ToBytes(aLength: integer): TBytes; +var i, limbIndex, shift: integer; +begin + SetLength(Result,aLength); + for i := 0 to aLength - 1 do + begin + limbIndex := (aLength - 1 - i) div 4; + shift := ((aLength - 1 - i) mod 4) * 8; + if limbIndex < Length(FLimbs) then + Result[i] := (FLimbs[limbIndex] shr shift) and $FF + else + Result[i] := 0; + end; +end; + +function TBigInt.ToHex: AnsiString; +const + HexDigits: array[0..15] of AnsiChar = '0123456789abcdef'; +var bytes: TBytes; + i: integer; +begin + if IsZero then + Exit('0'); + bytes := ToBytes; + SetLength(Result,Length(bytes)*2); + for i := 0 to Length(bytes) - 1 do + begin + Result[2*i+1] := HexDigits[bytes[i] shr 4]; + Result[2*i+2] := HexDigits[bytes[i] and $F]; + end; + {strip a single leading zero nibble if present} + if (Length(Result) > 1) and (Result[1] = '0') then + system.Delete(Result,1,1); +end; + +function TBigInt.IsZero: boolean; +begin + Result := Length(FLimbs) = 0; +end; + +function TBigInt.BitLength: integer; +var top: TLimb; +begin + if IsZero then + Exit(0); + Result := (Length(FLimbs) - 1) * 32; + top := FLimbs[Length(FLimbs)-1]; + while top <> 0 do + begin + Inc(Result); + top := top shr 1; + end; +end; + +function TBigInt.ByteLength: integer; +begin + Result := (BitLength + 7) div 8; +end; + +class function TBigInt.Compare(const a, b: TBigInt): integer; +var i: integer; +begin + if Length(a.FLimbs) <> Length(b.FLimbs) then + begin + if Length(a.FLimbs) > Length(b.FLimbs) then + Exit(1) + else + Exit(-1); + end; + for i := Length(a.FLimbs) - 1 downto 0 do + if a.FLimbs[i] <> b.FLimbs[i] then + begin + if a.FLimbs[i] > b.FLimbs[i] then + Exit(1) + else + Exit(-1); + end; + Result := 0; +end; + +class function TBigInt.Add(const a, b: TBigInt): TBigInt; +var i, n: integer; + carry: TDblLimb; + av, bv: TDblLimb; +begin + n := Length(a.FLimbs); + if Length(b.FLimbs) > n then + n := Length(b.FLimbs); + SetLength(Result.FLimbs,n+1); + carry := 0; + for i := 0 to n - 1 do + begin + if i < Length(a.FLimbs) then av := a.FLimbs[i] else av := 0; + if i < Length(b.FLimbs) then bv := b.FLimbs[i] else bv := 0; + carry := carry + av + bv; + Result.FLimbs[i] := TLimb(carry and $FFFFFFFF); + carry := carry shr 32; + end; + Result.FLimbs[n] := TLimb(carry); + Result.Normalise; +end; + +class function TBigInt.Subtract(const a, b: TBigInt): TBigInt; +var i: integer; + borrow: Int64; + av, bv: Int64; +begin + if Compare(a,b) < 0 then + raise EBigIntError.Create('BigInt Subtract would give negative result'); + SetLength(Result.FLimbs,Length(a.FLimbs)); + borrow := 0; + for i := 0 to Length(a.FLimbs) - 1 do + begin + av := a.FLimbs[i]; + if i < Length(b.FLimbs) then bv := b.FLimbs[i] else bv := 0; + av := av - bv - borrow; + if av < 0 then + begin + av := av + $100000000; + borrow := 1; + end + else + borrow := 0; + Result.FLimbs[i] := TLimb(av); + end; + Result.Normalise; +end; + +class function TBigInt.Multiply(const a, b: TBigInt): TBigInt; +var i, j: integer; + carry: TDblLimb; + t: TDblLimb; +begin + if a.IsZero or b.IsZero then + begin + SetLength(Result.FLimbs,0); + Exit; + end; + SetLength(Result.FLimbs,Length(a.FLimbs) + Length(b.FLimbs)); + for i := 0 to Length(Result.FLimbs) - 1 do + Result.FLimbs[i] := 0; + for i := 0 to Length(a.FLimbs) - 1 do + begin + carry := 0; + for j := 0 to Length(b.FLimbs) - 1 do + begin + t := TDblLimb(a.FLimbs[i]) * TDblLimb(b.FLimbs[j]) + + TDblLimb(Result.FLimbs[i+j]) + carry; + Result.FLimbs[i+j] := TLimb(t and $FFFFFFFF); + carry := t shr 32; + end; + Result.FLimbs[i + Length(b.FLimbs)] := TLimb(carry); + end; + Result.Normalise; +end; + +{Knuth TAOCP Vol 2, 4.3.1 Algorithm D (see also Hacker's Delight divmnu)} +class procedure TBigInt.DivMod(const a, b: TBigInt; var Quotient, Remainder: TBigInt); +type + TLimbArray = array of TLimb; +var + shift: integer; + un, vn: TLimbArray; {normalised dividend (m+n+1 limbs) and divisor (n limbs)} + n, m: integer; + i, j: integer; + qhat, rhat: TDblLimb; + k, t: Int64; {signed borrow accumulators} + p, sum: TDblLimb; + divisorTop: TLimb; + + {arithmetic (sign preserving) shift right by 32 of an Int64} + function Sar32(aValue: Int64): Int64; + begin + Result := Int64(Integer(QWord(aValue) shr 32)); + end; + + {shift limb array left by byBits (0..31), result has count+extra limbs} + function ShiftLeftLimbs(const src: array of TLimb; count, byBits, extra: integer): TLimbArray; + var idx: integer; + begin + SetLength(Result,count + extra); + for idx := 0 to High(Result) do Result[idx] := 0; + for idx := 0 to count - 1 do + if byBits = 0 then + Result[idx] := src[idx] + else + begin + Result[idx] := Result[idx] or (src[idx] shl byBits); + if idx + 1 <= High(Result) then + Result[idx+1] := src[idx] shr (32 - byBits); + end; + end; + +begin + if b.IsZero then + raise EBigIntError.Create('BigInt division by zero'); + if Compare(a,b) < 0 then + begin + Quotient := FromCardinal(0); + Remainder := a; + Exit; + end; + n := Length(b.FLimbs); + m := Length(a.FLimbs) - n; + + if n = 1 then + begin + {single limb divisor - simple case} + SetLength(Quotient.FLimbs,Length(a.FLimbs)); + rhat := 0; + for i := Length(a.FLimbs) - 1 downto 0 do + begin + sum := (rhat shl 32) or a.FLimbs[i]; + Quotient.FLimbs[i] := TLimb(sum div b.FLimbs[0]); + rhat := sum mod b.FLimbs[0]; + end; + Quotient.Normalise; + Remainder := FromCardinal(TLimb(rhat)); + Exit; + end; + + {D1: normalise so top bit of divisor is set} + shift := 0; + divisorTop := b.FLimbs[n-1]; + while (divisorTop and $80000000) = 0 do + begin + divisorTop := divisorTop shl 1; + Inc(shift); + end; + vn := ShiftLeftLimbs(b.FLimbs,n,shift,0); + un := ShiftLeftLimbs(a.FLimbs,Length(a.FLimbs),shift,1); + + SetLength(Quotient.FLimbs,m+1); + {D2..D7: main loop} + for j := m downto 0 do + begin + {D3: estimate qhat} + sum := (TDblLimb(un[j+n]) shl 32) or un[j+n-1]; + qhat := sum div vn[n-1]; + rhat := sum mod vn[n-1]; + while (qhat > $FFFFFFFF) or + (qhat * vn[n-2] > ((rhat shl 32) or un[j+n-2])) do + begin + Dec(qhat); + rhat := rhat + vn[n-1]; + if rhat > $FFFFFFFF then break; + end; + {D4: multiply and subtract} + k := 0; + for i := 0 to n - 1 do + begin + p := qhat * vn[i]; + t := Int64(un[i+j]) - k - Int64(p and $FFFFFFFF); + un[i+j] := TLimb(QWord(t) and $FFFFFFFF); + k := Int64(p shr 32) - Sar32(t); + end; + t := Int64(un[j+n]) - k; + un[j+n] := TLimb(QWord(t) and $FFFFFFFF); + {D5/D6: if we subtracted too much, add back} + if t < 0 then + begin + Dec(qhat); + sum := 0; + for i := 0 to n - 1 do + begin + sum := TDblLimb(un[i+j]) + vn[i] + (sum shr 32); + un[i+j] := TLimb(sum and $FFFFFFFF); + end; + un[j+n] := TLimb(TDblLimb(un[j+n]) + (sum shr 32)); + end; + Quotient.FLimbs[j] := TLimb(qhat); + end; + Quotient.Normalise; + + {D8: denormalise remainder} + SetLength(Remainder.FLimbs,n); + for i := 0 to n - 1 do + if shift = 0 then + Remainder.FLimbs[i] := un[i] + else + Remainder.FLimbs[i] := (un[i] shr shift) or + (TLimb((TDblLimb(un[i+1]) shl (32 - shift)) and $FFFFFFFF)); + Remainder.Normalise; +end; + +class function TBigInt.Modulus(const a, b: TBigInt): TBigInt; +var q: TBigInt; +begin + DivMod(a,b,q,Result); +end; + +class function TBigInt.ModPow(const aBase, aExponent, aModulus: TBigInt): TBigInt; +var acc: TBigInt; + i: integer; + bits: integer; +begin + if aModulus.IsZero then + raise EBigIntError.Create('BigInt ModPow with zero modulus'); + Result := Modulus(FromCardinal(1),aModulus); + acc := Modulus(aBase,aModulus); + bits := aExponent.BitLength; + for i := 0 to bits - 1 do + begin + if (aExponent.FLimbs[i div 32] shr (i mod 32)) and 1 = 1 then + Result := Modulus(Multiply(Result,acc),aModulus); + if i < bits - 1 then + acc := Modulus(Multiply(acc,acc),aModulus); + end; +end; + +end. diff --git a/client/wire/FBWireBlob.pas b/client/wire/FBWireBlob.pas new file mode 100644 index 00000000..354a7cbb --- /dev/null +++ b/client/wire/FBWireBlob.pas @@ -0,0 +1,421 @@ +(* + * Firebird Interface (fbintf). The fbintf components provide a set of + * Pascal language bindings for the Firebird API. + * + * This file is part of the pure Pascal wire protocol implementation + * (no fbclient required) and is subject to the Initial Developer's + * Public License Version 1.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy + * of the License here: + * + * http://www.firebirdsql.org/index.php?op=doc&id=idpl + * + * Software distributed under the License is distributed on an "AS + * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or + * implied. See the License for the specific language governing rights + * and limitations under the License. + * + * The Initial Developer of the Original Code is MWA Software + * (http://www.mwasoftware.co.uk). + * + * All Rights Reserved. + * + * Contributor(s): ______________________________________. + * +*) +unit FBWireBlob; + +{$IFDEF MSWINDOWS} +{$DEFINE WINDOWS} +{$ENDIF} + +{$IFDEF FPC} +{$mode delphi} +{$codepage UTF8} +{$interfaces COM} +{$ENDIF} + +interface + +uses + Classes, SysUtils, IB, FBBlob, FBClientAPI, FBTransaction, FBActivityMonitor, + FBOutputBlock, FBWireClientAPI, FBWireProtocol; + +type + { TFBWireBlobMetaData } + + TFBWireBlobMetaData = class(TFBBlobMetaData,IBlobMetaData) + private + FAttachmentIntf: IAttachment; + FTransactionIntf: ITransaction; + protected + function Attachment: IAttachment; override; + procedure NeedFullMetadata; override; + public + {aSubTypeKnown is true when the caller supplies the definitive subtype + and character set (statement metadata); false makes NeedFullMetadata + look the column up in the system tables} + constructor Create(aAttachment: TObject; aTransaction: TObject; + aRelationName, aColumnName: AnsiString; + aSubType: integer; aCharSetID: cardinal; + aSubTypeKnown: boolean = true); + end; + + { TFBWireBlob } + + TFBWireBlob = class(TFBBlob,IBlob) + private + FWireAPI: TFBWireClientAPI; + FWireAttachment: TObject; + FHandle: integer; + FHasHandle: boolean; + FInline: boolean; {served from the inline blob cache - no + server side handle exists} + FInlineInfo: TBytes; {the pushed blob info response} + FEOB: boolean; {end of blob seen while reading} + FReadBuffer: TBytes; {segment data not yet consumed by Read} + FReadPos: integer; + function GetConnection: TFBWireConnection; + function GetTransactionHandle: integer; + procedure InternalOpen(const aBlobID: TISC_QUAD); + procedure InternalCreate; + function BPBBytes: TBytes; + protected + procedure CheckReadable; override; + procedure CheckWritable; override; + function GetIntf: IBlob; override; + procedure GetInfo(Request: array of byte; Response: IBlobInfo); override; + procedure InternalClose(Force: boolean); override; + procedure InternalCancel(Force: boolean); override; + public + {open an existing blob} + constructor Create(aAttachment: IAttachment; aTransaction: TFBTransaction; + aMetaData: IBlobMetaData; aBlobID: TISC_QUAD; aBPB: IBPB); overload; + {create a new blob} + constructor Create(aAttachment: IAttachment; aTransaction: TFBTransaction; + aMetaData: IBlobMetaData; aBPB: IBPB); overload; + function Read(var Buffer; Count: Longint): Longint; override; + function Write(const Buffer; Count: Longint): Longint; override; + end; + +implementation + +uses FBMessages, IBErrorCodes, IBHeader, FBWireAttachment, FBWireTransaction, + FBWireConst; + +const + MaxSegmentSize = 32000; + + {the same system table lookup the 3.0 provider uses} + sLookupBlobMetaData = 'Select F.RDB$FIELD_SUB_TYPE, F.RDB$SEGMENT_LENGTH, F.RDB$CHARACTER_SET_ID, F.RDB$FIELD_TYPE '+ + 'From RDB$FIELDS F JOIN RDB$RELATION_FIELDS R On R.RDB$FIELD_SOURCE = F.RDB$FIELD_NAME '+ + 'Where Trim(R.RDB$RELATION_NAME) = Upper(Trim(?)) and Trim(R.RDB$FIELD_NAME) = Upper(Trim(?)) ' + + 'UNION '+ + 'Select F.RDB$FIELD_SUB_TYPE, F.RDB$SEGMENT_LENGTH, F.RDB$CHARACTER_SET_ID, F.RDB$FIELD_TYPE '+ + 'From RDB$FIELDS F JOIN RDB$PROCEDURE_PARAMETERS P On P.RDB$FIELD_SOURCE = F.RDB$FIELD_NAME '+ + 'Where Trim(P.RDB$PROCEDURE_NAME) = Upper(Trim(?)) and Trim(P.RDB$PARAMETER_NAME) = Upper(Trim(?)) '; + +{ TFBWireBlobMetaData } + +constructor TFBWireBlobMetaData.Create(aAttachment: TObject; + aTransaction: TObject; aRelationName, aColumnName: AnsiString; + aSubType: integer; aCharSetID: cardinal; aSubTypeKnown: boolean); +begin + inherited Create(aTransaction as TFBWireTransaction,aRelationName,aColumnName); + FAttachmentIntf := (aAttachment as TFBWireAttachment) as IAttachment; + FTransactionIntf := (aTransaction as TFBWireTransaction) as ITransaction; + FSubType := aSubType; + FCharSetID := aCharSetID; + FSegmentSize := MaxSegmentSize; + FHasSubType := aSubTypeKnown; + FUnconfirmedCharacterSet := not aSubTypeKnown; +end; + +function TFBWireBlobMetaData.Attachment: IAttachment; +begin + Result := FAttachmentIntf; +end; + +procedure TFBWireBlobMetaData.NeedFullMetadata; +var RS: IResultSet; +begin + {When created from statement metadata the subtype and character set are + already definitive. Otherwise the column is looked up in the system + tables, exactly as the 3.0 provider does.} + if FHasSubType then Exit; + FSegmentSize := 80; + if (GetColumnName <> '') and (GetRelationName <> '') then + begin + RS := FAttachmentIntf.OpenCursor(FTransactionIntf,sLookupBlobMetaData, + [GetRelationName,GetColumnName,GetRelationName,GetColumnName]); + if RS.FetchNext then + begin + if RS[3].AsInteger <> blr_blob then + IBError(ibxeInvalidBlobMetaData,[nil]); + FSubType := RS[0].AsInteger; + FSegmentSize := RS[1].AsInteger; + if FUnconfirmedCharacterSet then + FCharSetID := RS[2].AsInteger; + end + else + IBError(ibxeInvalidBlobMetaData,[nil]); + RS.Close; + end; + if FUnconfirmedCharacterSet and (FCharSetID > 1) and + FAttachmentIntf.HasDefaultCharSet then + begin + FCharSetID := FAttachmentIntf.GetDefaultCharSetID; + FUnconfirmedCharacterSet := false; + end; + FHasSubType := true; +end; + +{ TFBWireBlob } + +constructor TFBWireBlob.Create(aAttachment: IAttachment; + aTransaction: TFBTransaction; aMetaData: IBlobMetaData; aBlobID: TISC_QUAD; + aBPB: IBPB); +begin + inherited Create(aAttachment,aTransaction,aMetaData,aBlobID,aBPB); + FWireAttachment := aAttachment as TObject; + FWireAPI := (FWireAttachment as TFBWireAttachment).WireAPI; + InternalOpen(aBlobID); +end; + +constructor TFBWireBlob.Create(aAttachment: IAttachment; + aTransaction: TFBTransaction; aMetaData: IBlobMetaData; aBPB: IBPB); +begin + inherited Create(aAttachment,aTransaction,aMetaData,aBPB); + FWireAttachment := aAttachment as TObject; + FWireAPI := (FWireAttachment as TFBWireAttachment).WireAPI; + InternalCreate; +end; + +function TFBWireBlob.GetConnection: TFBWireConnection; +begin + Result := (FWireAttachment as TFBWireAttachment).Connection; +end; + +function TFBWireBlob.GetTransactionHandle: integer; +begin + Result := (GetTransaction as TObject as TFBWireTransaction).Handle; +end; + +function TFBWireBlob.BPBBytes: TBytes; +begin + Result := ParamBlockToBytes(GetBPB); +end; + +procedure TFBWireBlob.InternalOpen(const aBlobID: TISC_QUAD); +var id: Int64; + data: TBytes; + pos, segLen, outLen: integer; +begin + id := (Int64(aBlobID.gds_quad_high) shl 32) or Int64(cardinal(aBlobID.gds_quad_low)); + + {a protocol 19 server may have pushed this blob with the row that + references it. On a hit the open is free of wire traffic: the + segmented stream is unpacked here and Read serves it from the buffer.} + if (FWireAttachment as TFBWireAttachment).TakeInlineBlob( + GetTransactionHandle,id,FInlineInfo,data) then + begin + SetLength(FReadBuffer,Length(data)); + pos := 0; + outLen := 0; + while pos + 2 <= Length(data) do + begin + segLen := data[pos] or (data[pos+1] shl 8); + Inc(pos,2); + if pos + segLen > Length(data) then + segLen := Length(data) - pos; + if segLen > 0 then + begin + Move(data[pos],FReadBuffer[outLen],segLen); + Inc(outLen,segLen); + Inc(pos,segLen); + end; + end; + SetLength(FReadBuffer,outLen); + FReadPos := 0; + FEOB := true; + FInline := true; + Exit; + end; + + try + FHandle := GetConnection.OpenBlob(GetTransactionHandle,BPBBytes,id); + FHasHandle := true; + except + on E: Exception do WireIBError(FWireAPI,E); + end; + FEOB := false; + SetLength(FReadBuffer,0); + FReadPos := 0; +end; + +procedure TFBWireBlob.InternalCreate; +var id: Int64; +begin + try + GetConnection.CreateBlob(GetTransactionHandle,BPBBytes,FHandle,id); + FHasHandle := true; + except + on E: Exception do WireIBError(FWireAPI,E); + end; + FBlobID.gds_quad_high := Integer(id shr 32); + FBlobID.gds_quad_low := Cardinal(id and $FFFFFFFF); +end; + +procedure TFBWireBlob.CheckReadable; +begin + if not (FHasHandle or FInline) then + IBError(ibxeBlobCannotBeRead,[nil]); +end; + +procedure TFBWireBlob.CheckWritable; +begin + if not FHasHandle then + IBError(ibxeBlobCannotBeWritten,[nil]); +end; + +function TFBWireBlob.GetIntf: IBlob; +begin + Result := self; +end; + +procedure TFBWireBlob.GetInfo(Request: array of byte; Response: IBlobInfo); +var items, resp: TBytes; + i, len: integer; +begin + CheckReadable; + if FInline then + begin + {the server pushed the info with the blob: num_segments, max_segment, + total_length and type - the items every fbintf caller asks for} + len := Length(FInlineInfo); + if len > (Response as TBlobInfo).getBufSize then + len := (Response as TBlobInfo).getBufSize; + if len > 0 then + Move(FInlineInfo[0],(Response as TBlobInfo).Buffer^,len); + Exit; + end; + SetLength(items,Length(Request)); + for i := 0 to High(Request) do + items[i] := Request[i]; + SetLength(resp,0); + try + resp := GetConnection.GetInfo(op_info_blob,FHandle,items,DefaultBufferSize); + except + on E: Exception do WireIBError(FWireAPI,E); + end; + len := Length(resp); + if len > (Response as TBlobInfo).getBufSize then + len := (Response as TBlobInfo).getBufSize; + if len > 0 then + Move(resp[0],(Response as TBlobInfo).Buffer^,len); +end; + +procedure TFBWireBlob.InternalClose(Force: boolean); +begin + if FInline then + begin + {nothing exists server side} + FInline := false; + Exit; + end; + if not FHasHandle then Exit; + if not GetConnection.Connected then + begin + FHasHandle := false; + Exit; + end; + try + GetConnection.CloseBlob(FHandle); + except + on E: Exception do + if not Force then WireIBError(FWireAPI,E); + end; + FHasHandle := false; +end; + +procedure TFBWireBlob.InternalCancel(Force: boolean); +begin + if FInline then + begin + FInline := false; + Exit; + end; + if not FHasHandle then Exit; + if not GetConnection.Connected then + begin + FHasHandle := false; + Exit; + end; + try + GetConnection.CancelBlob(FHandle); + except + on E: Exception do + if not Force then WireIBError(FWireAPI,E); + end; + FHasHandle := false; +end; + +function TFBWireBlob.Read(var Buffer; Count: Longint): Longint; +var p: PByte; + available, chunk: integer; +begin + CheckReadable; + p := @Buffer; + Result := 0; + while Count > 0 do + begin + available := Length(FReadBuffer) - FReadPos; + if available = 0 then + begin + if FEOB then break; + try + FReadBuffer := GetConnection.GetSegment(FHandle,MaxSegmentSize,FEOB); + except + on E: Exception do WireIBError(FWireAPI,E); + end; + FReadPos := 0; + available := Length(FReadBuffer); + if available = 0 then break; + end; + chunk := available; + if chunk > Count then chunk := Count; + Move(FReadBuffer[FReadPos],p^,chunk); + Inc(FReadPos,chunk); + Inc(p,chunk); + Dec(Count,chunk); + Inc(Result,chunk); + end; +end; + +function TFBWireBlob.Write(const Buffer; Count: Longint): Longint; +var p: PByte; + chunk: integer; + seg: TBytes; +begin + CheckWritable; + p := @Buffer; + Result := 0; + while Count > 0 do + begin + chunk := Count; + if chunk > MaxSegmentSize then + chunk := MaxSegmentSize; + SetLength(seg,chunk); + Move(p^,seg[0],chunk); + try + GetConnection.PutSegment(FHandle,seg); + except + on E: Exception do WireIBError(FWireAPI,E); + end; + Inc(p,chunk); + Dec(Count,chunk); + Inc(Result,chunk); + end; +end; + +end. diff --git a/client/wire/FBWireClientAPI.pas b/client/wire/FBWireClientAPI.pas new file mode 100644 index 00000000..25d33525 --- /dev/null +++ b/client/wire/FBWireClientAPI.pas @@ -0,0 +1,878 @@ +(* + * Firebird Interface (fbintf). The fbintf components provide a set of + * Pascal language bindings for the Firebird API. + * + * This file is part of the pure Pascal wire protocol implementation + * (no fbclient required) and is subject to the Initial Developer's + * Public License Version 1.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy + * of the License here: + * + * http://www.firebirdsql.org/index.php?op=doc&id=idpl + * + * Software distributed under the License is distributed on an "AS + * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or + * implied. See the License for the specific language governing rights + * and limitations under the License. + * + * The Initial Developer of the Original Code is MWA Software + * (http://www.mwasoftware.co.uk). + * + * All Rights Reserved. + * + * Contributor(s): ______________________________________. + * +*) +unit FBWireClientAPI; + +{ The IFirebirdAPI implementation for the pure Pascal wire protocol client. + + Unlike the 2.5 and 3.0 providers this one loads no client library: every + call is a packet exchange with the server. Consequently: + + - date and time conversions are performed arithmetically here instead of + by isc_encode_* or IUtil + - error message text is built from the status vector strings that the + server itself provides, because firebird.msg is a client library + resource and is not available + - IsEmbeddedServer is always false and there is no IMaster interface + + The provider is obtained with TFBWireClientAPI.Create(nil) or, more + usually, through the WireFirebirdAPI function below. +} + +{$IFDEF MSWINDOWS} +{$DEFINE WINDOWS} +{$ENDIF} + +{$IFDEF FPC} +{$mode delphi} +{$codepage UTF8} +{$interfaces COM} +{$ENDIF} + +interface + +uses + Classes, SysUtils, IB, FBClientAPI, IBExternals, IBHeader, FBActivityMonitor, + FBWireProtocol, FBWireStream, FmtBCD; + +const + {the wire client reports itself with the highest protocol it implements} + WireClientMajorVersion = 5; + WireClientMinorVersion = 0; + +type + TFBWireClientAPI = class; + + { TFBWireStatus } + + TFBWireStatus = class(TFBStatus,IStatus) + private + FStatusVector: TStatusVector; + FWireStatus: TWireStatusVector; {the decoded vector, argument + structure intact, for formatting} + FMessage: AnsiString; + protected + function GetIBMessage(CodePage: TSystemCodePage): AnsiString; override; + public + constructor Create(aOwner: TFBClientAPI; prefix: AnsiString = ''); + constructor Copy(src: TFBWireStatus); + {SQLCODE support without the client library: the generated message + table carries each engine code's SQLCODE and the per SQLCODE texts + (facility 13 at 1000+sqlcode, facility 14 for warnings) - the same + lookups isc_sqlcode and isc_sql_interprete perform} + function SQLCodeSupported: boolean; override; + function Getsqlcode: TStatusCode; override; + function GetSQLMessage(CodePage: TSystemCodePage): Ansistring; override; + function StatusVector: PStatusVector; override; + function Clone: IStatus; override; + function InErrorState: boolean; override; + procedure Clear; + {builds an ISC status vector and the message text from a decoded wire + status vector} + procedure SetFromWireStatus(const aStatus: TWireStatusVector); + procedure SetError(aErrorCode: TStatusCode; const aMessage: AnsiString); + end; + + { TFBWireClientAPI } + + TFBWireClientAPI = class(TFBClientAPI,IFirebirdAPI) + private + FStatus: TFBWireStatus; + FStatusIntf: IStatus; {keeps FStatus alive} + public + constructor Create(aFBLibrary: TFBLibrary); + destructor Destroy; override; + + {TFBClientAPI} + function LoadInterface: boolean; override; + function GetAPI: IFirebirdAPI; override; + {$IFDEF UNIX} + function GetFirebirdLibList: string; override; + {$ENDIF} + procedure SQLEncodeDate(aDate: TDateTime; bufptr: PByte); override; + function SQLDecodeDate(bufptr: PByte): TDateTime; override; + procedure SQLEncodeTime(aTime: TDateTime; bufptr: PByte); override; + function SQLDecodeTime(bufptr: PByte): TDateTime; override; + procedure SQLEncodeDateTime(aDateTime: TDateTime; bufptr: PByte); override; + function SQLDecodeDateTime(bufptr: PByte): TDateTime; override; + function HasInt128Support: boolean; override; + function HasTimeZoneSupport: boolean; override; + {IEEE 754 densely packed decimal codec - the stock providers use the + client library's IDecFloat16/34, which is not available here} + procedure SQLDecFloatEncode(aValue: tBCD; SQLType: cardinal; bufptr: PByte); override; + function SQLDecFloatDecode(SQLType: cardinal; bufptr: PByte): tBCD; override; + + {the working status object - the wire objects report errors through it} + property WireStatus: TFBWireStatus read FStatus; + + public + {IFirebirdAPI} + function AllocateDPB: IDPB; + function OpenDatabase(DatabaseName: AnsiString; DPB: IDPB; + RaiseExceptionOnConnectError: boolean = true): IAttachment; + function CreateDatabase(DatabaseName: AnsiString; DPB: IDPB; + RaiseExceptionOnError: boolean = true): IAttachment; overload; + function CreateDatabase(sql: AnsiString; aSQLDialect: integer; + RaiseExceptionOnError: boolean = true): IAttachment; overload; + function AllocateTPB: ITPB; + function StartTransaction(Attachments: array of IAttachment; + TPB: array of byte; DefaultCompletion: TTransactionCompletion = taCommit; + aName: AnsiString = ''): ITransaction; overload; + function StartTransaction(Attachments: array of IAttachment; TPB: ITPB; + DefaultCompletion: TTransactionCompletion = taCommit; + aName: AnsiString = ''): ITransaction; overload; + function HasServiceAPI: boolean; + function AllocateSPB: ISPB; + function GetServiceManager(ServerName: AnsiString; Protocol: TProtocol; + SPB: ISPB): IServiceManager; overload; + function GetServiceManager(ServerName: AnsiString; Port: AnsiString; + Protocol: TProtocol; SPB: ISPB): IServiceManager; overload; + function GetStatus: IStatus; override; + function HasRollbackRetaining: boolean; + function IsEmbeddedServer: boolean; override; + function GetClientMajor: integer; override; + function GetClientMinor: integer; override; + function HasLocalTZDB: boolean; override; + function HasExtendedTZSupport: boolean; override; + function HasMasterIntf: boolean; + end; + +{The wire protocol provider. Unlike IB.FirebirdAPI this never loads a + client library, so it is available even where fbclient is not installed.} +function WireFirebirdAPI: IFirebirdAPI; + +{raises an EIBInterBaseError built from a wire protocol error} +procedure WireIBError(aAPI: TFBWireClientAPI; E: Exception); + +{copies a DPB/TPB/SPB/BPB clumplet buffer into a byte array ready to be + sent. The parameter block interfaces do not expose the raw buffer, so the + implementation class is used.} +function ParamBlockToBytes(aBlock: IUnknown): TBytes; + +implementation + +uses FBMessages, IBErrorCodes, FBParamBlock, FBAttachment, FBTransaction, + IBUtils, FBServices, FBWireAttachment, FBWireServices, FBWireMessages; + +const + {days between the Delphi TDateTime zero (1899-12-30) and the Firebird + ISC_DATE zero (17 November 1858, the modified Julian day epoch)} + FBDateDelta = 15018; + +var + FWireFirebirdAPI: IFirebirdAPI; + +function WireFirebirdAPI: IFirebirdAPI; +begin + if FWireFirebirdAPI = nil then + FWireFirebirdAPI := TFBWireClientAPI.Create(nil) as IFirebirdAPI; + Result := FWireFirebirdAPI; +end; + +procedure WireIBError(aAPI: TFBWireClientAPI; E: Exception); +begin + if E is EFBWireProtocolError then + begin + aAPI.WireStatus.SetFromWireStatus(EFBWireProtocolError(E).Status); + raise EIBInterBaseError.Create(aAPI.GetStatus,CP_ACP); + end + else + if E is EFBWireError then + begin + aAPI.WireStatus.SetError(isc_network_error,E.Message); + raise EIBInterBaseError.Create(aAPI.GetStatus,CP_ACP); + end + else + begin + {this procedure is called from inside the caller's exception handler: + take ownership of the object so that the handler's cleanup does not + free it while it propagates} + AcquireExceptionObject; + raise E; + end; +end; + +function ParamBlockToBytes(aBlock: IUnknown): TBytes; +var Block: TParamBlock; + p: PByte; + i: integer; +begin + SetLength(Result,0); + if aBlock = nil then Exit; + Block := aBlock as TObject as TParamBlock; + SetLength(Result,Block.getDataLength); + p := Block.getBuffer; + for i := 0 to Length(Result) - 1 do + Result[i] := p[i]; +end; + +{ TFBWireStatus } + +constructor TFBWireStatus.Create(aOwner: TFBClientAPI; prefix: AnsiString); +begin + inherited Create(aOwner,prefix); + Clear; +end; + +constructor TFBWireStatus.Copy(src: TFBWireStatus); +begin + inherited Copy(src); + FStatusVector := src.FStatusVector; + FWireStatus := system.copy(src.FWireStatus); + FMessage := src.FMessage; +end; + +procedure TFBWireStatus.Clear; +begin + FillChar(FStatusVector,SizeOf(FStatusVector),0); + FStatusVector[0] := isc_arg_gds; + FStatusVector[1] := 0; + FStatusVector[2] := isc_arg_end; + SetLength(FWireStatus,0); + FMessage := ''; +end; + +function TFBWireStatus.SQLCodeSupported: boolean; +begin + Result := true; +end; + +function TFBWireStatus.Getsqlcode: TStatusCode; +var i: integer; + sqlcode: integer; +begin + {the gds__sqlcode rules: an isc_sqlerr item anywhere in the vector + carries the SQLCODE as its number argument and wins outright; + otherwise the first item's own mapping decides, -999 by default} + for i := 0 to Length(FWireStatus) - 1 do + if (FWireStatus[i].Kind = isc_arg_gds) and + (FWireStatus[i].IntValue = isc_sqlerr) and + (i + 1 < Length(FWireStatus)) and + (FWireStatus[i+1].Kind = isc_arg_number) then + Exit(FWireStatus[i+1].IntValue); + Result := -999; {generic SQL Code} + for i := 0 to Length(FWireStatus) - 1 do + if (FWireStatus[i].Kind = isc_arg_gds) and (FWireStatus[i].IntValue <> 0) then + begin + sqlcode := EngineMessageSQLCode(cardinal(FWireStatus[i].IntValue)); + if sqlcode <> NoSQLCode then + Result := sqlcode; + break; {only the first item's mapping counts} + end; +end; + +function TFBWireStatus.GetSQLMessage(CodePage: TSystemCodePage): Ansistring; +var sqlcode: integer; + fmt: AnsiString; + code: cardinal; + i: integer; +begin + {what isc_sql_interprete answers: facility 13 message 1000+sqlcode for + errors, facility 14 message sqlcode for warnings} + Result := ''; + sqlcode := Getsqlcode; + if sqlcode < 0 then + code := cardinal($14000000) or (13 shl 16) or cardinal(1000 + sqlcode) + else + code := cardinal($14000000) or (14 shl 16) or cardinal(sqlcode); + if FindEngineMessage(code,fmt) then + begin + {the per SQLCODE texts carry no useful arguments here - strip any + placeholders, as isc_sql_interprete substitutes empties} + i := 1; + while i <= Length(fmt) do + begin + if (fmt[i] = '@') and (i < Length(fmt)) and (fmt[i+1] in ['1'..'9']) then + Inc(i,2) + else + begin + Result := Result + fmt[i]; + Inc(i); + end; + end; + end; +end; + +function TFBWireStatus.GetIBMessage(CodePage: TSystemCodePage): AnsiString; +begin + {formatted from the decoded vector and the generated message table - + the same text fb_interpret produces from firebird.msg, without the + file. SetError stores its text in FMessage instead.} + if Length(FWireStatus) > 0 then + Result := FormatWireStatus(FWireStatus) + else + Result := FMessage; + if Result = '' then + Result := Format('Firebird Error Code: %d',[FStatusVector[1]]); +end; + +function TFBWireStatus.StatusVector: PStatusVector; +begin + Result := @FStatusVector; +end; + +function TFBWireStatus.Clone: IStatus; +begin + Result := TFBWireStatus.Copy(self); +end; + +function TFBWireStatus.InErrorState: boolean; +begin + Result := (FStatusVector[0] = isc_arg_gds) and (FStatusVector[1] <> 0); +end; + +procedure TFBWireStatus.SetFromWireStatus(const aStatus: TWireStatusVector); +var i, v: integer; +begin + Clear; + {keep the decoded vector: GetIBMessage formats it the way fb_interpret + would, from the generated message table} + FWireStatus := system.copy(aStatus); + v := 0; + for i := 0 to Length(aStatus) - 1 do + begin + {leave room for the isc_arg_end terminator} + if v > High(FStatusVector) - 2 then + break; + case aStatus[i].Kind of + isc_arg_gds, isc_arg_number, isc_arg_warning: + begin + FStatusVector[v] := aStatus[i].Kind; + FStatusVector[v+1] := NativeInt(aStatus[i].IntValue); + Inc(v,2); + end; + end; + end; + FStatusVector[v] := isc_arg_end; +end; + +procedure TFBWireStatus.SetError(aErrorCode: TStatusCode; + const aMessage: AnsiString); +begin + Clear; + FStatusVector[0] := isc_arg_gds; + FStatusVector[1] := aErrorCode; + FStatusVector[2] := isc_arg_end; + SetLength(FWireStatus,0); + FMessage := aMessage; +end; + +{ TFBWireClientAPI } + +constructor TFBWireClientAPI.Create(aFBLibrary: TFBLibrary); +begin + inherited Create(aFBLibrary); + FStatus := TFBWireStatus.Create(self); + FStatusIntf := FStatus; +end; + +destructor TFBWireClientAPI.Destroy; +begin + FStatusIntf := nil; + inherited Destroy; +end; + +function TFBWireClientAPI.LoadInterface: boolean; +begin + {nothing to load - there is no client library} + Result := true; +end; + +function TFBWireClientAPI.GetAPI: IFirebirdAPI; +begin + Result := self as IFirebirdAPI; +end; + +{$IFDEF UNIX} +function TFBWireClientAPI.GetFirebirdLibList: string; +begin + Result := ''; +end; +{$ENDIF} + +procedure TFBWireClientAPI.SQLEncodeDate(aDate: TDateTime; bufptr: PByte); +begin + PISC_DATE(bufptr)^ := Trunc(aDate) + FBDateDelta; +end; + +function TFBWireClientAPI.SQLDecodeDate(bufptr: PByte): TDateTime; +begin + Result := PISC_DATE(bufptr)^ - FBDateDelta; +end; + +procedure TFBWireClientAPI.SQLEncodeTime(aTime: TDateTime; bufptr: PByte); +var Hr, Mt, S: word; + DMs: cardinal; +begin + FBDecodeTime(aTime,Hr,Mt,S,DMs); + PISC_TIME(bufptr)^ := cardinal(Hr)*36000000 + cardinal(Mt)*600000 + + cardinal(S)*10000 + DMs; +end; + +function TFBWireClientAPI.SQLDecodeTime(bufptr: PByte): TDateTime; +var t: cardinal; +begin + t := PISC_TIME(bufptr)^; + Result := FBEncodeTime(t div 36000000,(t div 600000) mod 60, + (t div 10000) mod 60,t mod 10000); +end; + +procedure TFBWireClientAPI.SQLEncodeDateTime(aDateTime: TDateTime; bufptr: PByte); +begin + SQLEncodeDate(aDateTime,bufptr); + Inc(bufptr,SizeOf(ISC_DATE)); + SQLEncodeTime(aDateTime,bufptr); +end; + +function TFBWireClientAPI.SQLDecodeDateTime(bufptr: PByte): TDateTime; +var aDate: TDateTime; +begin + aDate := SQLDecodeDate(bufptr); + Inc(bufptr,SizeOf(ISC_DATE)); + {negative dates count the time backwards from the date} + if aDate < 0 then + Result := aDate - SQLDecodeTime(bufptr) + else + Result := aDate + SQLDecodeTime(bufptr); +end; + +{--- IEEE 754 densely packed decimal --- + + A DECFLOAT travels as the IEEE 754-2008 decimal64/decimal128 bit image + (the XDR layer has already put it into little endian memory order). The + layout is sign(1), combination(5), exponent continuation(8/12), then the + coefficient as 10 bit declets of three digits each. The combination + field holds the two high exponent bits and the most significant digit.} + +var + DPDDecodeTable: array[0..1023] of word; {declet -> d2*100+d1*10+d0} + DPDEncodeTable: array[0..999] of word; {3 digits -> canonical declet} + +procedure InitDPDTables; +var b, digits: integer; + b9, b8, b7, b6, b5, b4, b3, b2, b1, b0: integer; + d2, d1, d0: integer; +begin + for b := 0 to 1023 do + begin + b9 := (b shr 9) and 1; b8 := (b shr 8) and 1; b7 := (b shr 7) and 1; + b6 := (b shr 6) and 1; b5 := (b shr 5) and 1; b4 := (b shr 4) and 1; + b3 := (b shr 3) and 1; b2 := (b shr 2) and 1; b1 := (b shr 1) and 1; + b0 := b and 1; + if b3 = 0 then + begin + d2 := b9*4 + b8*2 + b7; d1 := b6*4 + b5*2 + b4; d0 := b2*4 + b1*2 + b0; + end + else + case b2*2 + b1 of + 0: begin d2 := b9*4 + b8*2 + b7; d1 := b6*4 + b5*2 + b4; d0 := 8 + b0; end; + 1: begin d2 := b9*4 + b8*2 + b7; d1 := 8 + b4; d0 := b6*4 + b5*2 + b0; end; + 2: begin d2 := 8 + b7; d1 := b6*4 + b5*2 + b4; d0 := b9*4 + b8*2 + b0; end; + else + case b6*2 + b5 of + 0: begin d2 := 8 + b7; d1 := 8 + b4; d0 := b9*4 + b8*2 + b0; end; + 1: begin d2 := 8 + b7; d1 := b9*4 + b8*2 + b4; d0 := 8 + b0; end; + 2: begin d2 := b9*4 + b8*2 + b7; d1 := 8 + b4; d0 := 8 + b0; end; + else begin d2 := 8 + b7; d1 := 8 + b4; d0 := 8 + b0; end; + end; + end; + DPDDecodeTable[b] := d2*100 + d1*10 + d0; + end; + {the canonical encoding is the variant with the don't care bits zero, + which is the numerically smallest declet decoding to those digits} + for digits := 0 to 999 do + DPDEncodeTable[digits] := $FFFF; + for b := 1023 downto 0 do + DPDEncodeTable[DPDDecodeTable[b]] := b; +end; + +{aDigits[1..aWidth] receive the coefficient, left padded with zeroes. + Returns the unbiased exponent; aSign true = negative} +procedure DecFloatToDigits(aHi, aLo: QWord; aWidth: integer; + out aDigits: TBytes; out aExponent: integer; out aSign: boolean); +var g, biased, declets, i, k, pos: integer; + msd: integer; + declet: cardinal; + combined: word; + + function GetBits(aPos, aCount: integer): cardinal; + begin + {aPos is the low bit position within the 128/64 bit image} + if aPos >= 64 then + Result := (aHi shr (aPos - 64)) and ((QWord(1) shl aCount) - 1) + else + if aPos + aCount <= 64 then + Result := (aLo shr aPos) and ((QWord(1) shl aCount) - 1) + else + Result := ((aLo shr aPos) or (aHi shl (64 - aPos))) and + ((QWord(1) shl aCount) - 1); + end; + +var signBit, gPos, contBits, bias: integer; +begin + if aWidth = 16 then + begin + {only the low qword is used for decimal64} + signBit := 63; contBits := 8; bias := 398; declets := 5; gPos := 58; + end + else + begin + signBit := 127; contBits := 12; bias := 6176; declets := 11; + gPos := 122; + end; + aSign := GetBits(signBit,1) <> 0; + g := GetBits(gPos,5); + if (g shr 1) = 15 then + {infinity or NaN} + IBError(ibxeInvalidDataConversion,[nil]); + if (g shr 3) <> 3 then + begin + biased := (g shr 3) shl contBits; + msd := g and 7; + end + else + begin + biased := ((g shr 1) and 3) shl contBits; + msd := 8 + (g and 1); + end; + biased := biased or integer(GetBits(gPos - contBits,contBits)); + aExponent := biased - bias; + + SetLength(aDigits,aWidth + 1); {1 based, like the engine's toBcd} + aDigits[1] := msd; + pos := (declets - 1) * 10; + k := 2; + for i := 0 to declets - 1 do + begin + declet := GetBits(pos,10); + combined := DPDDecodeTable[declet]; + aDigits[k] := combined div 100; + aDigits[k+1] := (combined div 10) mod 10; + aDigits[k+2] := combined mod 10; + Inc(k,3); + Dec(pos,10); + end; +end; + +procedure DigitsToDecFloat(const aDigits: TBytes; aWidth: integer; + aExponent: integer; aSign: boolean; out aHi, aLo: QWord); +var biased, declets, i, k, pos: integer; + msd, g: integer; + contBits, bias, gPos, signBit, maxBiased: integer; + declet: cardinal; + + procedure OrBits(aValue: QWord; aPos: integer); + begin + if aPos >= 64 then + aHi := aHi or (aValue shl (aPos - 64)) + else + begin + aLo := aLo or (aValue shl aPos); + if aPos > 0 then + aHi := aHi or (aValue shr (64 - aPos)) + {a value at position 0 cannot straddle the boundary}; + end; + end; + +begin + if aWidth = 16 then + begin + signBit := 63; contBits := 8; bias := 398; declets := 5; gPos := 58; + maxBiased := 3 shl contBits - 1; + end + else + begin + signBit := 127; contBits := 12; bias := 6176; declets := 11; gPos := 122; + maxBiased := 3 shl contBits - 1; + end; + biased := aExponent + bias; + if (biased < 0) or (biased > maxBiased) then + IBError(ibxeInvalidDataConversion,[nil]); + + aHi := 0; + aLo := 0; + if aSign then + OrBits(1,signBit); + msd := aDigits[1]; + if msd <= 7 then + g := ((biased shr contBits) shl 3) or msd + else + g := 24 or (((biased shr contBits) and 3) shl 1) or (msd and 1); + OrBits(QWord(g),gPos); + OrBits(QWord(biased and ((1 shl contBits) - 1)),gPos - contBits); + pos := (declets - 1) * 10; + k := 2; + for i := 0 to declets - 1 do + begin + declet := DPDEncodeTable[aDigits[k]*100 + aDigits[k+1]*10 + aDigits[k+2]]; + OrBits(declet,pos); + Inc(k,3); + Dec(pos,10); + end; +end; + +function TFBWireClientAPI.HasInt128Support: boolean; +begin + Result := true; +end; + +function TFBWireClientAPI.HasTimeZoneSupport: boolean; +begin + Result := true; +end; + +procedure TFBWireClientAPI.SQLDecFloatEncode(aValue: tBCD; SQLType: cardinal; + bufptr: PByte); +var width, i, j: integer; + digits: TBytes; + hi, lo: QWord; + aSign: boolean; + exponent: integer; +begin + case SQLType of + SQL_DEC16: width := 16; + SQL_DEC34: width := 34; + else + IBError(ibxeInvalidDataConversion,[nil]); + end; + if BCDPrecision(aValue) > width then + IBError(ibxeBCDTooBig,[BCDPrecision(aValue),width]); + aSign := (aValue.SignSpecialPlaces and $80) <> 0; + exponent := -(aValue.SignSpecialPlaces and $3F); + + {right align the BCD digits in a width sized buffer - the same layout + the engine's fromBcd expects} + SetLength(digits,width + 2); + FillChar(digits[0],Length(digits),0); + j := 1 + (width - aValue.Precision); + for i := 0 to (aValue.Precision - 1) div 2 do + if j <= width then + begin + digits[j] := (aValue.Fraction[i] and $f0) shr 4; + Inc(j); + if j <= width then + begin + digits[j] := aValue.Fraction[i] and $0f; + Inc(j); + end; + end; + + DigitsToDecFloat(digits,width,exponent,aSign,hi,lo); + PQWord(bufptr)^ := lo; + if width = 34 then + PQWord(bufptr+8)^ := hi; +end; + +function TFBWireClientAPI.SQLDecFloatDecode(SQLType: cardinal; bufptr: PByte): tBCD; +var width, i, j: integer; + digits: TBytes; + hi, lo: QWord; + aSign: boolean; + exponent: integer; +begin + FillChar(Result,sizeof(tBCD),0); + case SQLType of + SQL_DEC16: + begin + width := 16; + lo := PQWord(bufptr)^; + hi := 0; + end; + SQL_DEC34: + begin + width := 34; + lo := PQWord(bufptr)^; + hi := PQWord(bufptr+8)^; + end; + else + IBError(ibxeInvalidDataConversion,[nil]); + end; + + DecFloatToDigits(hi,lo,width,digits,exponent,aSign); + + {a positive exponent becomes trailing zeroes so that the exponent can be + expressed as decimal places} + while exponent > 0 do + begin + if digits[1] <> 0 then + IBError(ibxeInvalidDataConversion,[nil]); + for i := 1 to width - 1 do + digits[i] := digits[i+1]; + digits[width] := 0; + Dec(exponent); + end; + + {pack, skipping leading zeroes - mirrors the 3.0 provider} + i := 1; + while (i <= width) and (digits[i] = 0) do + Inc(i); + j := 0; + Result.Precision := 0; + while i <= width do + begin + Inc(Result.Precision); + if odd(Result.Precision) then + Result.Fraction[j] := (digits[i] and $0f) shl 4 + else + begin + Result.Fraction[j] := Result.Fraction[j] or (digits[i] and $0f); + Inc(j); + end; + Inc(i); + end; + Result.SignSpecialPlaces := (-exponent) and $3F; + if aSign then + Result.SignSpecialPlaces := Result.SignSpecialPlaces or $80; +end; + +function TFBWireClientAPI.AllocateDPB: IDPB; +begin + Result := TDPB.Create(self); +end; + +function TFBWireClientAPI.OpenDatabase(DatabaseName: AnsiString; DPB: IDPB; + RaiseExceptionOnConnectError: boolean): IAttachment; +begin + Result := TFBWireAttachment.Create(self,DatabaseName,DPB, + RaiseExceptionOnConnectError); + if not Result.IsConnected then + Result := nil; +end; + +function TFBWireClientAPI.CreateDatabase(DatabaseName: AnsiString; DPB: IDPB; + RaiseExceptionOnError: boolean): IAttachment; +begin + Result := TFBWireAttachment.CreateDatabase(self,DatabaseName,DPB, + RaiseExceptionOnError); + if not Result.IsConnected then + Result := nil; +end; + +function TFBWireClientAPI.CreateDatabase(sql: AnsiString; aSQLDialect: integer; + RaiseExceptionOnError: boolean): IAttachment; +begin + Result := TFBWireAttachment.CreateDatabase(self,sql,aSQLDialect, + RaiseExceptionOnError); + if (Result <> nil) and not Result.IsConnected then + Result := nil; +end; + +function TFBWireClientAPI.AllocateTPB: ITPB; +begin + Result := TTPB.Create(self); +end; + +function TFBWireClientAPI.StartTransaction(Attachments: array of IAttachment; + TPB: array of byte; DefaultCompletion: TTransactionCompletion; + aName: AnsiString): ITransaction; +begin + {a transaction spanning several attachments needs a two phase commit + coordinator, which this provider does not implement} + if Length(Attachments) <> 1 then + IBError(ibxeNotSupported,[nil]); + Result := Attachments[0].StartTransaction(TPB,DefaultCompletion,aName); +end; + +function TFBWireClientAPI.StartTransaction(Attachments: array of IAttachment; + TPB: ITPB; DefaultCompletion: TTransactionCompletion; + aName: AnsiString): ITransaction; +begin + if Length(Attachments) <> 1 then + IBError(ibxeNotSupported,[nil]); + Result := Attachments[0].StartTransaction(TPB,DefaultCompletion,aName); +end; + +function TFBWireClientAPI.HasServiceAPI: boolean; +begin + Result := true; +end; + +function TFBWireClientAPI.AllocateSPB: ISPB; +begin + Result := TSPB.Create(self); +end; + +function TFBWireClientAPI.GetServiceManager(ServerName: AnsiString; + Protocol: TProtocol; SPB: ISPB): IServiceManager; +begin + Result := GetServiceManager(ServerName,'',Protocol,SPB); +end; + +function TFBWireClientAPI.GetServiceManager(ServerName: AnsiString; + Port: AnsiString; Protocol: TProtocol; SPB: ISPB): IServiceManager; +begin + Result := TFBWireServiceManager.Create(self,ServerName,Protocol,SPB,Port); +end; + +function TFBWireClientAPI.GetStatus: IStatus; +begin + Result := FStatus; +end; + +function TFBWireClientAPI.HasRollbackRetaining: boolean; +begin + Result := true; +end; + +function TFBWireClientAPI.IsEmbeddedServer: boolean; +begin + {a wire protocol client is by definition a remote client} + Result := false; +end; + +function TFBWireClientAPI.GetClientMajor: integer; +begin + Result := WireClientMajorVersion; +end; + +function TFBWireClientAPI.GetClientMinor: integer; +begin + Result := WireClientMinorVersion; +end; + +function TFBWireClientAPI.HasLocalTZDB: boolean; +begin + {time zone names are resolved by the server} + Result := false; +end; + +function TFBWireClientAPI.HasExtendedTZSupport: boolean; +begin + Result := true; +end; + +function TFBWireClientAPI.HasMasterIntf: boolean; +begin + Result := false; +end; + +initialization + FWireFirebirdAPI := nil; + InitDPDTables; + +finalization + FWireFirebirdAPI := nil; + +end. diff --git a/client/wire/FBWireConst.pas b/client/wire/FBWireConst.pas new file mode 100644 index 00000000..1afe89e6 --- /dev/null +++ b/client/wire/FBWireConst.pas @@ -0,0 +1,225 @@ +(* + * Firebird Interface (fbintf). The fbintf components provide a set of + * Pascal language bindings for the Firebird API. + * + * This file is part of the pure Pascal wire protocol implementation + * (no fbclient required) and is subject to the Initial Developer's + * Public License Version 1.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy + * of the License here: + * + * http://www.firebirdsql.org/index.php?op=doc&id=idpl + * + * Software distributed under the License is distributed on an "AS + * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or + * implied. See the License for the specific language governing rights + * and limitations under the License. + * + * The Initial Developer of the Original Code is MWA Software + * (http://www.mwasoftware.co.uk). + * + * All Rights Reserved. + * + * Contributor(s): ______________________________________. + * +*) +unit FBWireConst; + +{ Remote protocol constants. The reference for these values is + src/remote/protocol.h in the Firebird source tree. } + +{$IFDEF FPC} +{$mode delphi} +{$ENDIF} + +interface + +const + {operation codes} + op_void = 0; + op_connect = 1; + op_exit = 2; + op_accept = 3; + op_reject = 4; + op_disconnect = 6; + op_response = 9; + op_attach = 19; + op_create = 20; + op_detach = 21; + op_transaction = 29; + op_commit = 30; + op_rollback = 31; + op_prepare = 32; {two phase commit: prepare} + op_reconnect = 33; + op_create_blob = 34; + op_open_blob = 35; + op_get_segment = 36; + op_put_segment = 37; + op_cancel_blob = 38; + op_close_blob = 39; + op_info_database = 40; + op_info_request = 41; + op_info_transaction = 42; + op_info_blob = 43; + op_batch_segments = 44; + op_que_events = 48; + op_cancel_events = 49; + op_commit_retaining = 50; + op_prepare2 = 51; + op_event = 52; + op_connect_request = 53; + op_aux_connect = 54; + op_open_blob2 = 56; + op_create_blob2 = 57; + op_get_slice = 58; + op_put_slice = 59; + op_slice = 60; + op_seek_blob = 61; + op_allocate_statement = 62; + op_execute = 63; + op_exec_immediate = 64; + op_fetch = 65; + op_fetch_response = 66; + op_free_statement = 67; + op_prepare_statement = 68; + op_set_cursor = 69; + op_info_sql = 70; + op_dummy = 71; + op_response_piggyback = 72; + op_exec_immediate2 = 75; + op_execute2 = 76; + op_sql_response = 78; + op_drop_database = 81; + op_service_attach = 82; + op_service_detach = 83; + op_service_info = 84; + op_service_start = 85; + op_rollback_retaining = 86; + op_partial = 89; + op_trusted_auth = 90; + op_cancel = 91; + op_cont_auth = 92; + op_ping = 93; + op_accept_data = 94; + op_abort_aux_connection = 95; + op_crypt = 96; + op_crypt_key_callback = 97; + op_cond_accept = 98; + op_batch_create = 99; + op_batch_msg = 100; + op_batch_exec = 101; + op_batch_rls = 102; + op_batch_cs = 103; + op_batch_regblob = 104; + op_batch_blob_stream = 105; + op_batch_set_bpb = 106; + op_repl_data = 107; + op_repl_req = 108; + op_batch_cancel = 109; + op_batch_sync = 110; + op_info_batch = 111; + op_fetch_scroll = 112; + op_info_cursor = 113; + op_inline_blob = 114; + + {connect operation versions} + CONNECT_VERSION2 = 2; + CONNECT_VERSION3 = 3; + + {architecture types - we are always generic} + arch_generic = 1; + + {protocol versions. Since protocol 11 the high bit (FB_PROTOCOL_FLAG) is + set to distinguish Firebird from Borland InterBase} + FB_PROTOCOL_FLAG = $8000; + FB_PROTOCOL_MASK = $7FFF; + PROTOCOL_VERSION10 = 10; + PROTOCOL_VERSION11 = FB_PROTOCOL_FLAG or 11; + PROTOCOL_VERSION12 = FB_PROTOCOL_FLAG or 12; + {P13: authentication plugins (op_cont_auth), null bitmap in messages - + Firebird 3.0} + PROTOCOL_VERSION13 = FB_PROTOCOL_FLAG or 13; + {P14: wire encryption} + PROTOCOL_VERSION14 = FB_PROTOCOL_FLAG or 14; + {P15: conditional accept with early wire encryption} + PROTOCOL_VERSION15 = FB_PROTOCOL_FLAG or 15; + {P16: statement timeouts, DECFLOAT, batch API - Firebird 4.0} + PROTOCOL_VERSION16 = FB_PROTOCOL_FLAG or 16; + {P17: continues Firebird 4.0 enhancements} + PROTOCOL_VERSION17 = FB_PROTOCOL_FLAG or 17; + {P18: scrollable cursor fetch - Firebird 5.0} + PROTOCOL_VERSION18 = FB_PROTOCOL_FLAG or 18; + {P19: inline blobs - Firebird 5.0.3} + PROTOCOL_VERSION19 = FB_PROTOCOL_FLAG or 19; + {P20: SQL schemas, named arguments - Firebird 6.0} + PROTOCOL_VERSION20 = FB_PROTOCOL_FLAG or 20; + + {protocol types (accept types)} + ptype_rpc = 2; + ptype_batch_send = 3; + ptype_out_of_band = 4; + ptype_lazy_send = 5; + ptype_mask = $FF; + pflag_compress = $100; + + {user identification data tags (p_cnct_user_id)} + CNCT_user = 1; + CNCT_passwd = 2; + CNCT_host = 4; + CNCT_group = 5; + CNCT_user_verification = 6; + CNCT_specific_data = 7; + CNCT_plugin_name = 8; + CNCT_login = 9; + CNCT_plugin_list = 10; + CNCT_client_crypt = 11; + + {client crypt level in CNCT_client_crypt} + WIRE_CRYPT_DISABLED = 0; + WIRE_CRYPT_ENABLED = 1; + WIRE_CRYPT_REQUIRED = 2; + + {authentication plugin names} + sLegacyAuthPluginName = 'Legacy_Auth'; + sDefaultPluginList = 'Srp256,Srp,Legacy_Auth'; + sLegacyAuthSalt = '9z'; + + {wire encryption plugin names} + sArc4PluginName = 'Arc4'; + sChaChaPluginName = 'ChaCha'; + sChaCha64PluginName = 'ChaCha64'; + sSymmetricKeyName = 'Symmetric'; + + {op_free_statement options} + DSQL_close = 1; + DSQL_drop = 2; + DSQL_unprepare = 4; + + {fetch response status} + FETCH_status_more_rows = 0; + FETCH_status_eof = 100; + + {op_cancel kinds} + fb_cancel_disable = 1; + fb_cancel_enable = 2; + fb_cancel_raise = 3; + fb_cancel_abort = 4; + + {op_fetch_scroll directions - P_FETCH in protocol.h} + fetch_next = 0; + fetch_prior = 1; + fetch_first = 2; + fetch_last = 3; + fetch_absolute = 4; + fetch_relative = 5; + + {p_sqldata_cursor_flags (protocol 18) - matches + IStatement::CURSOR_TYPE_SCROLLABLE} + CURSOR_TYPE_SCROLLABLE = 1; + + {op_connect_request types (for event aux connections)} + P_REQ_async = 1; + +implementation + +end. diff --git a/client/wire/FBWireCrypto.pas b/client/wire/FBWireCrypto.pas new file mode 100644 index 00000000..082fa8e4 --- /dev/null +++ b/client/wire/FBWireCrypto.pas @@ -0,0 +1,506 @@ +(* + * Firebird Interface (fbintf). The fbintf components provide a set of + * Pascal language bindings for the Firebird API. + * + * This file is part of the pure Pascal wire protocol implementation + * (no fbclient required) and is subject to the Initial Developer's + * Public License Version 1.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy + * of the License here: + * + * http://www.firebirdsql.org/index.php?op=doc&id=idpl + * + * Software distributed under the License is distributed on an "AS + * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or + * implied. See the License for the specific language governing rights + * and limitations under the License. + * + * The Initial Developer of the Original Code is MWA Software + * (http://www.mwasoftware.co.uk). + * + * All Rights Reserved. + * + * Contributor(s): ______________________________________. + * +*) +unit FBWireCrypto; + +{ Self-contained cryptographic primitives needed by the Firebird wire + protocol: SHA-1 and SHA-256 message digests (Srp and Srp256 authentication), + RC4 (Arc4 wire encryption plugin) and ChaCha20 (ChaCha/ChaCha64 wire + encryption plugins). No external libraries are used. These are not intended + as a general purpose crypto library. } + +{$IFDEF FPC} +{$mode delphi} +{$interfaces COM} +{$R-}{$Q-} +{$ENDIF} + +interface + +uses + Classes, SysUtils; + +type + TSHA1Digest = array[0..19] of byte; + TSHA256Digest = array[0..31] of byte; + + { TSHA1 } + + TSHA1 = record + private + FState: array[0..4] of Cardinal; + FBuffer: array[0..63] of byte; + FBufLen: integer; + FTotalLen: QWord; + procedure Compress; + public + procedure Init; + procedure Update(const aData; aLen: integer); overload; + procedure Update(const aData: TBytes); overload; + function Final: TSHA1Digest; + class function Digest(const aData: TBytes): TSHA1Digest; static; + end; + + { TSHA256 } + + TSHA256 = record + private + FState: array[0..7] of Cardinal; + FBuffer: array[0..63] of byte; + FBufLen: integer; + FTotalLen: QWord; + procedure Compress; + public + procedure Init; + procedure Update(const aData; aLen: integer); overload; + procedure Update(const aData: TBytes); overload; + function Final: TSHA256Digest; + class function Digest(const aData: TBytes): TSHA256Digest; static; + end; + + { TRC4 - stream cipher used by the Arc4 wire encryption plugin } + + TRC4 = class + private + FS: array[0..255] of byte; + Fi, Fj: byte; + public + constructor Create(const aKey: TBytes); + {in-place encrypt/decrypt (RC4 is symmetric)} + procedure Process(var aData; aLen: integer); + end; + + { TChaCha20 - stream cipher used by the ChaCha wire encryption plugin. + Supports the IETF variant (96 bit nonce, 32 bit counter - "ChaCha") + and the original djb variant (64 bit nonce, 64 bit counter - "ChaCha64"). } + + TChaCha20 = class + private + FInput: array[0..15] of Cardinal; + FKeyStream: array[0..63] of byte; + FAvail: integer; {bytes of keystream still available} + FCounter64: boolean; + procedure NextBlock; + public + {aKey must be 32 bytes. IETF: aNonce is 12 bytes, counter 32 bit. + djb/ChaCha64: aNonce is 8 bytes, counter 64 bit.} + constructor Create(const aKey, aNonce: TBytes; aCounter: QWord = 0); + procedure Process(var aData; aLen: integer); + end; + +function SHA1DigestToBytes(const aDigest: TSHA1Digest): TBytes; +function SHA256DigestToBytes(const aDigest: TSHA256Digest): TBytes; + +implementation + +function SHA1DigestToBytes(const aDigest: TSHA1Digest): TBytes; +begin + SetLength(Result,SizeOf(aDigest)); + Move(aDigest,Result[0],SizeOf(aDigest)); +end; + +function SHA256DigestToBytes(const aDigest: TSHA256Digest): TBytes; +begin + SetLength(Result,SizeOf(aDigest)); + Move(aDigest,Result[0],SizeOf(aDigest)); +end; + +function RotL(x: Cardinal; n: integer): Cardinal; inline; +begin + Result := (x shl n) or (x shr (32 - n)); +end; + +function RotR(x: Cardinal; n: integer): Cardinal; inline; +begin + Result := (x shr n) or (x shl (32 - n)); +end; + +function SwapBE(x: Cardinal): Cardinal; inline; +begin + Result := (x shr 24) or ((x shr 8) and $FF00) or + ((x shl 8) and $FF0000) or (x shl 24); +end; + +{ TSHA1 } + +procedure TSHA1.Init; +begin + FState[0] := $67452301; + FState[1] := $EFCDAB89; + FState[2] := $98BADCFE; + FState[3] := $10325476; + FState[4] := $C3D2E1F0; + FBufLen := 0; + FTotalLen := 0; +end; + +procedure TSHA1.Compress; +var w: array[0..79] of Cardinal; + a, b, c, d, e, f, k, temp: Cardinal; + i: integer; +begin + for i := 0 to 15 do + w[i] := SwapBE(PCardinal(@FBuffer[i*4])^); + for i := 16 to 79 do + w[i] := RotL(w[i-3] xor w[i-8] xor w[i-14] xor w[i-16],1); + a := FState[0]; b := FState[1]; c := FState[2]; d := FState[3]; e := FState[4]; + for i := 0 to 79 do + begin + case i of + 0..19: + begin + f := (b and c) or ((not b) and d); + k := $5A827999; + end; + 20..39: + begin + f := b xor c xor d; + k := $6ED9EBA1; + end; + 40..59: + begin + f := (b and c) or (b and d) or (c and d); + k := $8F1BBCDC; + end; + else + begin + f := b xor c xor d; + k := $CA62C1D6; + end; + end; + temp := RotL(a,5) + f + e + k + w[i]; + e := d; d := c; c := RotL(b,30); b := a; a := temp; + end; + Inc(FState[0],a); Inc(FState[1],b); Inc(FState[2],c); + Inc(FState[3],d); Inc(FState[4],e); +end; + +procedure TSHA1.Update(const aData; aLen: integer); +var p: PByte; + chunk: integer; +begin + p := @aData; + Inc(FTotalLen,aLen); + while aLen > 0 do + begin + chunk := 64 - FBufLen; + if chunk > aLen then chunk := aLen; + Move(p^,FBuffer[FBufLen],chunk); + Inc(FBufLen,chunk); + Inc(p,chunk); + Dec(aLen,chunk); + if FBufLen = 64 then + begin + Compress; + FBufLen := 0; + end; + end; +end; + +procedure TSHA1.Update(const aData: TBytes); +begin + if Length(aData) > 0 then + Update(aData[0],Length(aData)); +end; + +function TSHA1.Final: TSHA1Digest; +var bitLen: QWord; + i: integer; +begin + bitLen := FTotalLen * 8; + FBuffer[FBufLen] := $80; + Inc(FBufLen); + if FBufLen > 56 then + begin + while FBufLen < 64 do + begin + FBuffer[FBufLen] := 0; + Inc(FBufLen); + end; + Compress; + FBufLen := 0; + end; + while FBufLen < 56 do + begin + FBuffer[FBufLen] := 0; + Inc(FBufLen); + end; + for i := 0 to 7 do + FBuffer[56+i] := (bitLen shr ((7-i)*8)) and $FF; + Compress; + for i := 0 to 19 do + Result[i] := (FState[i div 4] shr ((3 - (i mod 4))*8)) and $FF; +end; + +class function TSHA1.Digest(const aData: TBytes): TSHA1Digest; +var ctx: TSHA1; +begin + ctx.Init; + ctx.Update(aData); + Result := ctx.Final; +end; + +{ TSHA256 } + +const + SHA256K: array[0..63] of Cardinal = ( + $428a2f98, $71374491, $b5c0fbcf, $e9b5dba5, $3956c25b, $59f111f1, $923f82a4, $ab1c5ed5, + $d807aa98, $12835b01, $243185be, $550c7dc3, $72be5d74, $80deb1fe, $9bdc06a7, $c19bf174, + $e49b69c1, $efbe4786, $0fc19dc6, $240ca1cc, $2de92c6f, $4a7484aa, $5cb0a9dc, $76f988da, + $983e5152, $a831c66d, $b00327c8, $bf597fc7, $c6e00bf3, $d5a79147, $06ca6351, $14292967, + $27b70a85, $2e1b2138, $4d2c6dfc, $53380d13, $650a7354, $766a0abb, $81c2c92e, $92722c85, + $a2bfe8a1, $a81a664b, $c24b8b70, $c76c51a3, $d192e819, $d6990624, $f40e3585, $106aa070, + $19a4c116, $1e376c08, $2748774c, $34b0bcb5, $391c0cb3, $4ed8aa4a, $5b9cca4f, $682e6ff3, + $748f82ee, $78a5636f, $84c87814, $8cc70208, $90befffa, $a4506ceb, $bef9a3f7, $c67178f2); + +procedure TSHA256.Init; +begin + FState[0] := $6a09e667; FState[1] := $bb67ae85; + FState[2] := $3c6ef372; FState[3] := $a54ff53a; + FState[4] := $510e527f; FState[5] := $9b05688c; + FState[6] := $1f83d9ab; FState[7] := $5be0cd19; + FBufLen := 0; + FTotalLen := 0; +end; + +procedure TSHA256.Compress; +var w: array[0..63] of Cardinal; + a, b, c, d, e, f, g, h, t1, t2, s0, s1: Cardinal; + i: integer; +begin + for i := 0 to 15 do + w[i] := SwapBE(PCardinal(@FBuffer[i*4])^); + for i := 16 to 63 do + begin + s0 := RotR(w[i-15],7) xor RotR(w[i-15],18) xor (w[i-15] shr 3); + s1 := RotR(w[i-2],17) xor RotR(w[i-2],19) xor (w[i-2] shr 10); + w[i] := w[i-16] + s0 + w[i-7] + s1; + end; + a := FState[0]; b := FState[1]; c := FState[2]; d := FState[3]; + e := FState[4]; f := FState[5]; g := FState[6]; h := FState[7]; + for i := 0 to 63 do + begin + s1 := RotR(e,6) xor RotR(e,11) xor RotR(e,25); + t1 := h + s1 + ((e and f) xor ((not e) and g)) + SHA256K[i] + w[i]; + s0 := RotR(a,2) xor RotR(a,13) xor RotR(a,22); + t2 := s0 + ((a and b) xor (a and c) xor (b and c)); + h := g; g := f; f := e; e := d + t1; + d := c; c := b; b := a; a := t1 + t2; + end; + Inc(FState[0],a); Inc(FState[1],b); Inc(FState[2],c); Inc(FState[3],d); + Inc(FState[4],e); Inc(FState[5],f); Inc(FState[6],g); Inc(FState[7],h); +end; + +procedure TSHA256.Update(const aData; aLen: integer); +var p: PByte; + chunk: integer; +begin + p := @aData; + Inc(FTotalLen,aLen); + while aLen > 0 do + begin + chunk := 64 - FBufLen; + if chunk > aLen then chunk := aLen; + Move(p^,FBuffer[FBufLen],chunk); + Inc(FBufLen,chunk); + Inc(p,chunk); + Dec(aLen,chunk); + if FBufLen = 64 then + begin + Compress; + FBufLen := 0; + end; + end; +end; + +procedure TSHA256.Update(const aData: TBytes); +begin + if Length(aData) > 0 then + Update(aData[0],Length(aData)); +end; + +function TSHA256.Final: TSHA256Digest; +var bitLen: QWord; + i: integer; +begin + bitLen := FTotalLen * 8; + FBuffer[FBufLen] := $80; + Inc(FBufLen); + if FBufLen > 56 then + begin + while FBufLen < 64 do + begin + FBuffer[FBufLen] := 0; + Inc(FBufLen); + end; + Compress; + FBufLen := 0; + end; + while FBufLen < 56 do + begin + FBuffer[FBufLen] := 0; + Inc(FBufLen); + end; + for i := 0 to 7 do + FBuffer[56+i] := (bitLen shr ((7-i)*8)) and $FF; + Compress; + for i := 0 to 31 do + Result[i] := (FState[i div 4] shr ((3 - (i mod 4))*8)) and $FF; +end; + +class function TSHA256.Digest(const aData: TBytes): TSHA256Digest; +var ctx: TSHA256; +begin + ctx.Init; + ctx.Update(aData); + Result := ctx.Final; +end; + +{ TRC4 } + +constructor TRC4.Create(const aKey: TBytes); +var i: integer; + j: byte; + t: byte; +begin + inherited Create; + if Length(aKey) = 0 then + raise Exception.Create('RC4: empty key'); + for i := 0 to 255 do + FS[i] := i; + j := 0; + for i := 0 to 255 do + begin + j := byte(j + FS[i] + aKey[i mod Length(aKey)]); + t := FS[i]; FS[i] := FS[j]; FS[j] := t; + end; + Fi := 0; + Fj := 0; +end; + +procedure TRC4.Process(var aData; aLen: integer); +var p: PByte; + n: integer; + t: byte; +begin + p := @aData; + for n := 0 to aLen - 1 do + begin + Fi := byte(Fi + 1); + Fj := byte(Fj + FS[Fi]); + t := FS[Fi]; FS[Fi] := FS[Fj]; FS[Fj] := t; + p[n] := p[n] xor FS[byte(FS[Fi] + FS[Fj])]; + end; +end; + +{ TChaCha20 } + +constructor TChaCha20.Create(const aKey, aNonce: TBytes; aCounter: QWord); +const + Sigma: array[0..3] of Cardinal = ($61707865, $3320646e, $79622d32, $6b206574); +var i: integer; +begin + inherited Create; + if Length(aKey) <> 32 then + raise Exception.Create('ChaCha20: key must be 32 bytes'); + for i := 0 to 3 do + FInput[i] := Sigma[i]; + for i := 0 to 7 do + FInput[4+i] := PCardinal(@aKey[i*4])^; {little endian load} + FCounter64 := Length(aNonce) = 8; + case Length(aNonce) of + 12: {IETF variant: 32 bit counter + 96 bit nonce} + begin + FInput[12] := Cardinal(aCounter); + FInput[13] := PCardinal(@aNonce[0])^; + FInput[14] := PCardinal(@aNonce[4])^; + FInput[15] := PCardinal(@aNonce[8])^; + end; + 8: {original djb variant: 64 bit counter + 64 bit nonce} + begin + FInput[12] := Cardinal(aCounter and $FFFFFFFF); + FInput[13] := Cardinal(aCounter shr 32); + FInput[14] := PCardinal(@aNonce[0])^; + FInput[15] := PCardinal(@aNonce[4])^; + end; + else + raise Exception.Create('ChaCha20: nonce must be 8 or 12 bytes'); + end; + FAvail := 0; +end; + +procedure TChaCha20.NextBlock; +var x: array[0..15] of Cardinal; + i: integer; + + procedure QR(a, b, c, d: integer); + begin + x[a] := x[a] + x[b]; x[d] := RotL(x[d] xor x[a],16); + x[c] := x[c] + x[d]; x[b] := RotL(x[b] xor x[c],12); + x[a] := x[a] + x[b]; x[d] := RotL(x[d] xor x[a],8); + x[c] := x[c] + x[d]; x[b] := RotL(x[b] xor x[c],7); + end; + +begin + for i := 0 to 15 do + x[i] := FInput[i]; + for i := 1 to 10 do + begin + QR(0,4,8,12); QR(1,5,9,13); QR(2,6,10,14); QR(3,7,11,15); + QR(0,5,10,15); QR(1,6,11,12); QR(2,7,8,13); QR(3,4,9,14); + end; + for i := 0 to 15 do + begin + x[i] := x[i] + FInput[i]; + {store little endian} + FKeyStream[i*4] := x[i] and $FF; + FKeyStream[i*4+1] := (x[i] shr 8) and $FF; + FKeyStream[i*4+2] := (x[i] shr 16) and $FF; + FKeyStream[i*4+3] := (x[i] shr 24) and $FF; + end; + {increment block counter} + Inc(FInput[12]); + if (FInput[12] = 0) and FCounter64 then + Inc(FInput[13]); + FAvail := 64; +end; + +procedure TChaCha20.Process(var aData; aLen: integer); +var p: PByte; + n: integer; +begin + p := @aData; + n := 0; + while n < aLen do + begin + if FAvail = 0 then + NextBlock; + p[n] := p[n] xor FKeyStream[64 - FAvail]; + Dec(FAvail); + Inc(n); + end; +end; + +end. diff --git a/client/wire/FBWireDescribe.pas b/client/wire/FBWireDescribe.pas new file mode 100644 index 00000000..80dd3fb9 --- /dev/null +++ b/client/wire/FBWireDescribe.pas @@ -0,0 +1,366 @@ +(* + * Firebird Interface (fbintf). The fbintf components provide a set of + * Pascal language bindings for the Firebird API. + * + * This file is part of the pure Pascal wire protocol implementation + * (no fbclient required) and is subject to the Initial Developer's + * Public License Version 1.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy + * of the License here: + * + * http://www.firebirdsql.org/index.php?op=doc&id=idpl + * + * Software distributed under the License is distributed on an "AS + * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or + * implied. See the License for the specific language governing rights + * and limitations under the License. + * + * The Initial Developer of the Original Code is MWA Software + * (http://www.mwasoftware.co.uk). + * + * All Rights Reserved. + * + * Contributor(s): ______________________________________. + * +*) +unit FBWireDescribe; + +{ Parses the isc_info_sql_* response buffer returned by op_prepare_statement + into the input and output message formats used by FBWireMessage. + + The buffer is a sequence of clumplets: [item:1][length:2 little endian] + [value] with isc_info_end/isc_info_sql_describe_end acting as markers. } + +{$IFDEF FPC} +{$mode delphi} +{$codepage UTF8} +{$interfaces COM} +{$ENDIF} + +interface + +uses + Classes, SysUtils, IB, FBWireMessage; + +type + TWireStatementInfo = record + StatementType: integer; + InputFormat: TWireMessageFormat; + OutputFormat: TWireMessageFormat; + InputBufferSize: cardinal; + OutputBufferSize: cardinal; + Truncated: boolean; {the info buffer was too small} + end; + +{the standard describe item list sent with op_prepare_statement} +{aIncludeSchema adds the protocol 20 per column schema name item - only + ask a server that knows it} +function DescribeItems(aIncludeSchema: boolean = false): TBytes; + +function ParsePrepareResponse(const aBuffer: TBytes): TWireStatementInfo; + +{extracts the row counts (isc_info_sql_records) from an op_info_sql response} +function ParseRowsAffected(const aBuffer: TBytes; + var aSelectCount, aInsertCount, aUpdateCount, + aDeleteCount: integer): boolean; + +implementation + +{the isc_info_* and isc_info_sql_* constants come from IB.pas which + includes inf_pub.inc} + +function DescribeItems(aIncludeSchema: boolean): TBytes; +begin + if aIncludeSchema then + Result := TBytes.Create( + isc_info_sql_stmt_type, + isc_info_sql_select, + isc_info_sql_describe_vars, + isc_info_sql_sqlda_seq, + isc_info_sql_type, + isc_info_sql_sub_type, + isc_info_sql_scale, + isc_info_sql_length, + isc_info_sql_field, + isc_info_sql_relation, + isc_info_sql_relation_schema, + isc_info_sql_owner, + isc_info_sql_alias, + isc_info_sql_describe_end, + isc_info_sql_bind, + isc_info_sql_describe_vars, + isc_info_sql_sqlda_seq, + isc_info_sql_type, + isc_info_sql_sub_type, + isc_info_sql_scale, + isc_info_sql_length, + isc_info_sql_field, + isc_info_sql_relation, + isc_info_sql_relation_schema, + isc_info_sql_owner, + isc_info_sql_alias, + isc_info_sql_describe_end, + isc_info_end) + else + Result := TBytes.Create( + isc_info_sql_stmt_type, + isc_info_sql_select, + isc_info_sql_describe_vars, + isc_info_sql_sqlda_seq, + isc_info_sql_type, + isc_info_sql_sub_type, + isc_info_sql_scale, + isc_info_sql_length, + isc_info_sql_field, + isc_info_sql_relation, + isc_info_sql_owner, + isc_info_sql_alias, + isc_info_sql_describe_end, + isc_info_sql_bind, + isc_info_sql_describe_vars, + isc_info_sql_sqlda_seq, + isc_info_sql_type, + isc_info_sql_sub_type, + isc_info_sql_scale, + isc_info_sql_length, + isc_info_sql_field, + isc_info_sql_relation, + isc_info_sql_owner, + isc_info_sql_alias, + isc_info_sql_describe_end, + isc_info_end); +end; + +function ParsePrepareResponse(const aBuffer: TBytes): TWireStatementInfo; +var p: integer; + item: byte; + len: integer; + index: integer; {0 based index of the var being described} + rawType: cardinal; + fmt: ^TWireMessageFormat; + + function ReadLen: integer; + begin + Result := aBuffer[p] or (aBuffer[p+1] shl 8); + Inc(p,2); + end; + + {clumplet values are little endian two's complement integers} + function ReadInt(aLen: integer): Int64; + var i: integer; + v: QWord; + signBit: QWord; + begin + v := 0; + for i := 0 to aLen - 1 do + v := v or (QWord(aBuffer[p+i]) shl (8*i)); + Inc(p,aLen); + if aLen < 8 then + begin + signBit := QWord(1) shl (8*aLen - 1); + if (v and signBit) <> 0 then + v := v or not ((QWord(1) shl (8*aLen)) - 1); + end; + Result := Int64(v); + end; + + function ReadStr(aLen: integer): AnsiString; + var i: integer; + begin + SetLength(Result,aLen); + for i := 0 to aLen - 1 do + Result[i+1] := AnsiChar(aBuffer[p+i]); + Inc(p,aLen); + end; + + procedure EnsureIndex; + begin + if index >= Length(fmt^) then + SetLength(fmt^,index + 1); + end; + +begin + Result.StatementType := 0; + SetLength(Result.InputFormat,0); + SetLength(Result.OutputFormat,0); + Result.Truncated := false; + fmt := @Result.OutputFormat; + index := -1; + p := 0; + while p < Length(aBuffer) do + begin + item := aBuffer[p]; + Inc(p); + case item of + isc_info_end: + break; + isc_info_truncated: + begin + Result.Truncated := true; + break; + end; + isc_info_sql_describe_end: + continue; + isc_info_sql_select: + begin + fmt := @Result.OutputFormat; + index := -1; + continue; + end; + isc_info_sql_bind: + begin + fmt := @Result.InputFormat; + index := -1; + continue; + end; + end; + if p + 2 > Length(aBuffer) then break; + len := ReadLen; + if p + len > Length(aBuffer) then break; + case item of + isc_info_sql_stmt_type: + Result.StatementType := ReadInt(len); + isc_info_sql_num_variables: + begin + SetLength(fmt^,ReadInt(len)); + end; + isc_info_sql_sqlda_seq: + begin + index := ReadInt(len) - 1; {1 based on the wire} + EnsureIndex; + end; + isc_info_sql_type: + begin + EnsureIndex; + {the low bit of the reported type is the nullable flag} + rawType := cardinal(ReadInt(len)); + fmt^[index].SQLType := rawType and not cardinal(1); + fmt^[index].Nullable := (rawType and 1) <> 0; + end; + isc_info_sql_sub_type: + begin + EnsureIndex; + fmt^[index].SQLSubType := ReadInt(len); + {For CHAR and VARCHAR the reported subtype is the character set id + and isc_info_sql_length is the byte length in that character set. + Written as comparisons rather than a set test: the SQL type codes + are far outside the 0..255 a Pascal set can hold, so "in" would + silently compare truncated values.} + if (fmt^[index].SQLType = SQL_TEXT) or + (fmt^[index].SQLType = SQL_VARYING) then + fmt^[index].CharSetID := cardinal(fmt^[index].SQLSubType); + end; + isc_info_sql_scale: + begin + EnsureIndex; + fmt^[index].Scale := ReadInt(len); + end; + isc_info_sql_length: + begin + EnsureIndex; + fmt^[index].DataSize := ReadInt(len); + end; + isc_info_sql_field: + begin + EnsureIndex; + fmt^[index].FieldName := ReadStr(len); + end; + isc_info_sql_relation: + begin + EnsureIndex; + fmt^[index].RelationName := ReadStr(len); + end; + isc_info_sql_relation_schema: + begin + EnsureIndex; + fmt^[index].SchemaName := ReadStr(len); + end; + isc_info_sql_owner: + begin + EnsureIndex; + fmt^[index].OwnerName := ReadStr(len); + end; + isc_info_sql_alias: + begin + EnsureIndex; + fmt^[index].AliasName := ReadStr(len); + end; + else + Inc(p,len); {skip anything we do not use} + end; + end; + Result.InputBufferSize := ComputeMessageLayout(Result.InputFormat); + Result.OutputBufferSize := ComputeMessageLayout(Result.OutputFormat); +end; + +function ParseRowsAffected(const aBuffer: TBytes; + var aSelectCount, aInsertCount, aUpdateCount, aDeleteCount: integer): boolean; +var p: integer; + item: byte; + len: integer; + subItem: byte; + subLen: integer; + endOfRecords: integer; + + function ReadLenAt(var aPos: integer): integer; + begin + Result := aBuffer[aPos] or (aBuffer[aPos+1] shl 8); + Inc(aPos,2); + end; + + function ReadIntAt(var aPos: integer; aLen: integer): integer; + var i: integer; + v: cardinal; + begin + v := 0; + for i := 0 to aLen - 1 do + v := v or (cardinal(aBuffer[aPos+i]) shl (8*i)); + Inc(aPos,aLen); + Result := integer(v); + end; + +begin + Result := false; + aSelectCount := 0; + aInsertCount := 0; + aUpdateCount := 0; + aDeleteCount := 0; + p := 0; + while p < Length(aBuffer) do + begin + item := aBuffer[p]; + Inc(p); + if (item = isc_info_end) or (item = isc_info_truncated) then + break; + if p + 2 > Length(aBuffer) then break; + len := ReadLenAt(p); + if p + len > Length(aBuffer) then break; + if item = isc_info_sql_records then + begin + endOfRecords := p + len; + while p < endOfRecords do + begin + subItem := aBuffer[p]; + Inc(p); + if subItem = isc_info_end then break; + if p + 2 > endOfRecords then break; + subLen := ReadLenAt(p); + if p + subLen > endOfRecords then break; + case subItem of + isc_info_req_select_count: aSelectCount := ReadIntAt(p,subLen); + isc_info_req_insert_count: aInsertCount := ReadIntAt(p,subLen); + isc_info_req_update_count: aUpdateCount := ReadIntAt(p,subLen); + isc_info_req_delete_count: aDeleteCount := ReadIntAt(p,subLen); + else + Inc(p,subLen); + end; + end; + p := endOfRecords; + Result := true; + end + else + Inc(p,len); + end; +end; + +end. diff --git a/client/wire/FBWireEvents.pas b/client/wire/FBWireEvents.pas new file mode 100644 index 00000000..f8468d81 --- /dev/null +++ b/client/wire/FBWireEvents.pas @@ -0,0 +1,417 @@ +(* + * Firebird Interface (fbintf). The fbintf components provide a set of + * Pascal language bindings for the Firebird API. + * + * This file is part of the pure Pascal wire protocol implementation + * (no fbclient required) and is subject to the Initial Developer's + * Public License Version 1.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy + * of the License here: + * + * http://www.firebirdsql.org/index.php?op=doc&id=idpl + * + * Software distributed under the License is distributed on an "AS + * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or + * implied. See the License for the specific language governing rights + * and limitations under the License. + * + * The Initial Developer of the Original Code is MWA Software + * (http://www.mwasoftware.co.uk). + * + * All Rights Reserved. + * + * Contributor(s): ______________________________________. + * +*) +unit FBWireEvents; + +{ The IEvents implementation for the pure Pascal wire protocol client. + + Firebird delivers events on a second connection. The client sends + op_connect_request (P_REQ_async) on the main connection and the server + answers with the address of an auxiliary port; the client opens a plain + TCP connection to it - same host as the main connection, the port from + the response (the address in the response is the server's own view of + itself, which behind NAT is not reachable; taking only the port is what + the stock client does, see aux_connect in src/remote/inet.cpp). The + auxiliary connection carries no handshake, no authentication and no + encryption: the server associates it with the session by the accept, and + it only ever delivers op_event packets. + + One auxiliary connection and one listener thread serve all the IEvents + instances of an attachment. Interest is registered with op_que_events on + the main connection, cancelled with op_cancel_events, and notifications + are dispatched by the client supplied event id. + + All of the event block machinery - building the EPB that op_que_events + carries, diffing the counts, the callback dispatch - is inherited from + TFBEvents; this unit supplies the transport. + + The event handler is called from the listener thread, exactly as the + 2.5 provider's handler is called from its AST thread. A handler must + not call back into the same attachment from that thread; Synchronize + or Queue the work to another thread first (the test suite's Test 10 + shows the pattern).} + +{$IFDEF MSWINDOWS} +{$DEFINE WINDOWS} +{$ENDIF} + +{$IFDEF FPC} +{$mode delphi} +{$codepage UTF8} +{$interfaces COM} +{$ENDIF} + +interface + +uses + Classes, SysUtils, SyncObjs, IB, FBEvents, FBWireClientAPI, FBWireProtocol, + FBWireStream, FBWireAttachment; + +type + TFBWireEventManager = class; + + { TFBWireEvents } + + TFBWireEvents = class(TFBEvents,IEvents) + private + FManager: TFBWireEventManager; + FEventID: integer; + FSyncSignal: TSimpleEvent; + FSyncWait: boolean; + function EPBBytes: TBytes; + protected + procedure CancelEvents(Force: boolean = false); override; + function GetIEvents: IEvents; override; + public + constructor Create(DBAttachment: TFBWireAttachment; + aManager: TFBWireEventManager; Events: TStrings); + destructor Destroy; override; + + {called by the listener thread with the updated event buffer} + procedure EventArrived(const aItems: TBytes); + + {IEvents} + procedure AsyncWaitForEvent(EventHandler: TEventHandler); override; + procedure WaitForEvent; override; + + property EventID: integer read FEventID; + end; + + { TFBWireEventManager + + Owned by the attachment. Created lazily on the first GetEventHandler + call, freed on disconnect. Owns the auxiliary connection and the + listener thread.} + + TFBWireEventManager = class + private + FAttachment: TFBWireAttachment; + FTransport: TFBWireTransport; + FXDR: TXDRStream; + FListener: TThread; + FEventsList: TThreadList; + FNextEventID: integer; + function GetConnection: TFBWireConnection; + public + constructor Create(aAttachment: TFBWireAttachment); + destructor Destroy; override; + procedure RegisterEvents(aEvents: TFBWireEvents); + procedure UnregisterEvents(aEvents: TFBWireEvents); + {each op_que_events uses a fresh event id, as the stock client does - + the interest is one shot and the new id identifies the new interest. + The id is assigned to aEvents before the request is sent.} + procedure QueEvents(aEvents: TFBWireEvents; const aEPB: TBytes); + procedure CancelEvents(aEventID: integer); + procedure DispatchEvent(aEventID: integer; const aItems: TBytes); + property Connection: TFBWireConnection read GetConnection; + end; + +implementation + +uses FBMessages, FBWireConst; + +type + { TFBWireEventListener - drains the auxiliary connection } + + TFBWireEventListener = class(TThread) + private + FManager: TFBWireEventManager; + protected + procedure Execute; override; + public + constructor Create(aManager: TFBWireEventManager); + end; + +{ TFBWireEventListener } + +constructor TFBWireEventListener.Create(aManager: TFBWireEventManager); +begin + FManager := aManager; + inherited Create(false); +end; + +procedure TFBWireEventListener.Execute; +var op: integer; + dbHandle, eventID: integer; + items: TBytes; +begin + try + repeat + op := FManager.FXDR.ReadInt32; + case op of + op_dummy: ; + op_event: + begin + dbHandle := FManager.FXDR.ReadInt32; + items := FManager.FXDR.ReadString; + FManager.FXDR.ReadInt32; {p_event_ast - a raw memory image} + FManager.FXDR.ReadInt32; {p_event_arg - ditto} + eventID := FManager.FXDR.ReadInt32; + FManager.DispatchEvent(eventID,items); + end; + else + break; + end; + until Terminated; + except + {the socket is closed under this thread on shutdown} + on E: EFBWireError do ; + end; +end; + +{ TFBWireEventManager } + +function TFBWireEventManager.GetConnection: TFBWireConnection; +begin + Result := FAttachment.Connection; +end; + +constructor TFBWireEventManager.Create(aAttachment: TFBWireAttachment); +var aPort: integer; +begin + inherited Create; + FAttachment := aAttachment; + FEventsList := TThreadList.Create; + try + aPort := Connection.ConnectRequest(aAttachment.Handle); + FTransport := TFBWireTransport.Create; + FTransport.ConnectTo(aAttachment.Host,aPort); + FXDR := TXDRStream.Create(FTransport); + FListener := TFBWireEventListener.Create(self); + except + on E: Exception do + begin + if FTransport <> nil then + FTransport.Disconnect; + FreeAndNil(FXDR); + FreeAndNil(FTransport); + FreeAndNil(FEventsList); + WireIBError(aAttachment.WireAPI,E); + end; + end; +end; + +destructor TFBWireEventManager.Destroy; +begin + if FListener <> nil then + begin + FListener.Terminate; + {shut the socket down under the listener so that its blocking read + returns, then wait for it} + if FTransport <> nil then + FTransport.Abort; + FListener.WaitFor; + FreeAndNil(FListener); + end; + if FTransport <> nil then + FTransport.Disconnect; + FreeAndNil(FXDR); + FreeAndNil(FTransport); + FreeAndNil(FEventsList); + inherited Destroy; +end; + +procedure TFBWireEventManager.RegisterEvents(aEvents: TFBWireEvents); +begin + with FEventsList.LockList do + try + Add(aEvents); + finally + FEventsList.UnlockList; + end; +end; + +procedure TFBWireEventManager.UnregisterEvents(aEvents: TFBWireEvents); +begin + FEventsList.Remove(aEvents); +end; + +procedure TFBWireEventManager.QueEvents(aEvents: TFBWireEvents; + const aEPB: TBytes); +var aEventID: integer; +begin + with FEventsList.LockList do + try + Inc(FNextEventID); + aEventID := FNextEventID; + aEvents.FEventID := aEventID; + finally + FEventsList.UnlockList; + end; + try + Connection.QueEvents(FAttachment.Handle,aEPB,aEventID); + except + on E: Exception do WireIBError(FAttachment.WireAPI,E); + end; +end; + +procedure TFBWireEventManager.CancelEvents(aEventID: integer); +begin + Connection.CancelEvents(FAttachment.Handle,aEventID); +end; + +procedure TFBWireEventManager.DispatchEvent(aEventID: integer; + const aItems: TBytes); +var i: integer; + aEvents: TFBWireEvents; + Pin: IEvents; +begin + {find the interested party under the list lock and pin it with an + interface reference before calling out, so that it cannot be freed + while the notification is delivered} + aEvents := nil; + with FEventsList.LockList do + try + for i := 0 to Count - 1 do + if TFBWireEvents(Items[i]).EventID = aEventID then + begin + aEvents := TFBWireEvents(Items[i]); + Pin := aEvents; + break; + end; + finally + FEventsList.UnlockList; + end; + if aEvents <> nil then + aEvents.EventArrived(aItems); +end; + +{ TFBWireEvents } + +function TFBWireEvents.EPBBytes: TBytes; +begin + SetLength(Result,FEventBufferLen); + if FEventBufferLen > 0 then + Move(FEventBuffer^,Result[0],FEventBufferLen); +end; + +procedure TFBWireEvents.CancelEvents(Force: boolean); +begin + FCriticalSection.Enter; + try + if not FInWaitState then Exit; + try + FManager.CancelEvents(FEventID); + except + on E: Exception do + if not Force then + WireIBError(FManager.FAttachment.WireAPI,E); + end; + FInWaitState := false; + if FSyncWait then + begin + FSyncWait := false; + FSyncSignal.SetEvent; + end; + inherited CancelEvents(Force); + finally + FCriticalSection.Leave; + end; +end; + +function TFBWireEvents.GetIEvents: IEvents; +begin + Result := self; +end; + +constructor TFBWireEvents.Create(DBAttachment: TFBWireAttachment; + aManager: TFBWireEventManager; Events: TStrings); +begin + inherited Create(DBAttachment,DBAttachment,Events); + FManager := aManager; + FSyncSignal := TSimpleEvent.Create; + FEventID := -1; {assigned by each QueEvents} + aManager.RegisterEvents(self); +end; + +destructor TFBWireEvents.Destroy; +begin + CancelEvents(true); + if FManager <> nil then + FManager.UnregisterEvents(self); + FreeAndNil(FSyncSignal); + inherited Destroy; +end; + +procedure TFBWireEvents.EventArrived(const aItems: TBytes); +var n: integer; + SignalSync: boolean; +begin + FCriticalSection.Enter; + try + n := Length(aItems); + if n > FEventBufferLen then + n := FEventBufferLen; + if n > 0 then + Move(aItems[0],FResultBuffer^,n); + SignalSync := FSyncWait; + if SignalSync then + begin + {a synchronous wait ends on the first delivery, like + isc_wait_for_event} + ProcessEventCounts; + FSyncWait := false; + FInWaitState := false; + end; + finally + FCriticalSection.Leave; + end; + if SignalSync then + FSyncSignal.SetEvent + else + EventSignaled; +end; + +procedure TFBWireEvents.AsyncWaitForEvent(EventHandler: TEventHandler); +begin + FCriticalSection.Enter; + try + if FInWaitState then + IBError(ibxeInEventWait,[nil]); + FEventHandler := EventHandler; + FManager.QueEvents(self,EPBBytes); + FInWaitState := true; + finally + FCriticalSection.Leave; + end; +end; + +procedure TFBWireEvents.WaitForEvent; +begin + FCriticalSection.Enter; + try + if FInWaitState then + IBError(ibxeInEventWait,[nil]); + FSyncSignal.ResetEvent; + FSyncWait := true; + FManager.QueEvents(self,EPBBytes); + FInWaitState := true; + finally + FCriticalSection.Leave; + end; + FSyncSignal.WaitFor(INFINITE); +end; + +end. diff --git a/client/wire/FBWireMessage.pas b/client/wire/FBWireMessage.pas new file mode 100644 index 00000000..38afef4a --- /dev/null +++ b/client/wire/FBWireMessage.pas @@ -0,0 +1,909 @@ +(* + * Firebird Interface (fbintf). The fbintf components provide a set of + * Pascal language bindings for the Firebird API. + * + * This file is part of the pure Pascal wire protocol implementation + * (no fbclient required) and is subject to the Initial Developer's + * Public License Version 1.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy + * of the License here: + * + * http://www.firebirdsql.org/index.php?op=doc&id=idpl + * + * Software distributed under the License is distributed on an "AS + * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or + * implied. See the License for the specific language governing rights + * and limitations under the License. + * + * The Initial Developer of the Original Code is MWA Software + * (http://www.mwasoftware.co.uk). + * + * All Rights Reserved. + * + * Contributor(s): ______________________________________. + * +*) +unit FBWireMessage; + +{ SQL message formats for the wire protocol. + + A prepared statement's input parameters and output columns are described + by a list of TWireSQLVar descriptors (populated from the isc_info_sql + describe response). This unit: + + - computes a flat message buffer layout for a descriptor list (the same + storage conventions as the Firebird client message buffer, i.e. + SQL_VARYING = 2 byte length prefix + data, native endian integers) + - generates the BLR message description sent to the server in + op_prepare_statement / op_execute / op_fetch + - encodes/decodes message data between the flat buffer and the XDR + stream. From protocol 13 a message on the wire is a null bitmap + followed by the values of the non-null fields. +} + +{$IFDEF FPC} +{$mode delphi} +{$codepage UTF8} +{$interfaces COM} +{$ENDIF} + +interface + +uses + Classes, SysUtils, IB, FBWireStream; + +type + { TWireSQLVar: describes one column or parameter } + + TWireSQLVar = record + SQLType: cardinal; {SQL_XXXX with the null flag (bit 0) removed} + SQLSubType: integer; + Scale: integer; + DataSize: cardinal; {size from metadata (chars part for VARYING)} + CharSetID: cardinal; + AliasName: AnsiString; + FieldName: AnsiString; + RelationName: AnsiString; + SchemaName: AnsiString; {protocol 20 servers name the schema} + OwnerName: AnsiString; + Nullable: boolean; + {computed layout in the message buffer} + DataOffset: cardinal; + NullOffset: cardinal; {offset of 32 bit null indicator: -1 = null} + BufferSize: cardinal; {bytes reserved at DataOffset} + end; + + TWireMessageFormat = array of TWireSQLVar; + +{computes DataOffset/NullOffset/BufferSize for each var and returns the + total buffer length} +function ComputeMessageLayout(var aFormat: TWireMessageFormat): cardinal; + +{the buffer length of an already laid out format} +function MessageBufferSize(const aFormat: TWireMessageFormat): cardinal; + +{the message length as the server computes it from our BLR - the + PARSE_msg_format algorithm of src/remote/parser.cpp, with each value + followed by its two byte null indicator. op_batch_create's message + length field must be exactly this value: the server validates it against + the format it parses from the BLR.} +function EngineMessageLength(const aFormat: TWireMessageFormat): cardinal; + +{rounds aValue up to a multiple of aAlignment (a power of two)} +function AlignTo(aValue, aAlignment: cardinal): cardinal; + +{BLR describing the message, as sent to the server} +function BuildMessageBlr(const aFormat: TWireMessageFormat): TBytes; + +{writes a message (null bitmap + values) from the flat buffer to the XDR + stream - protocol 13 and later format} +procedure XDREncodeMessage(XDR: TXDRStream; const aFormat: TWireMessageFormat; + aBuffer: PByte); + +{reads a message from the XDR stream into the flat buffer} +procedure XDRDecodeMessage(XDR: TXDRStream; const aFormat: TWireMessageFormat; + aBuffer: PByte); + +type + { TWireSliceLayout: how the elements of an array slice are arranged in + the local buffer and encoded on the wire (op_get_slice/op_put_slice). + The local buffer uses the layout TFBArray.AllocateBuffer establishes; + the wire uses xdr_slice/xdr_datum (src/remote/protocol.cpp and + src/common/xdr.cpp) driven by the SDL element descriptor. } + + TWireSliceLayout = record + Dtype: byte; {blr type from the array descriptor} + ElementLength: cardinal; {array_desc_length: the byte length of a text + or varying element - unused for other types} + BufferStride: cardinal; {spacing of the elements in the local buffer} + Count: cardinal; {number of elements in the slice} + end; + +{the element length as the server computes it from the SDL (sdl_desc in + src/common/sdl.cpp). The slice length fields of the packet count in these + units: length = elements * SliceElementDscLength.} +function SliceElementDscLength(const aLayout: TWireSliceLayout): cardinal; + +{the p_slc_length/lstr_length value for the full slice} +function SliceLength(const aLayout: TWireSliceLayout): cardinal; + +{writes the slice elements to the XDR stream - the lstr_length prefix is + the caller's job} +procedure XDREncodeSlice(XDR: TXDRStream; const aLayout: TWireSliceLayout; + aBuffer: PByte); + +{reads a slice of aWireLength (in dsc length units, as received in + lstr_length) into the local buffer} +procedure XDRDecodeSlice(XDR: TXDRStream; const aLayout: TWireSliceLayout; + aBuffer: PByte; aWireLength: cardinal); + +{Blob and array identifiers are ISC_QUADs: the high word is stored first, + which is not the memory layout of an Int64 on a little endian machine. + These helpers convert between the buffer layout and the (high shl 32) or + low value used by TWireResponse.ObjectID and the Firebird API.} +function WireQuadToInt64(aQuad: PByte): Int64; +procedure Int64ToWireQuad(aQuad: PByte; aValue: Int64); + +implementation + +uses FBMessages; + +function WireQuadToInt64(aQuad: PByte): Int64; +begin + Result := (Int64(PInteger(aQuad)^) shl 32) or Int64(PCardinal(aQuad+4)^); +end; + +procedure Int64ToWireQuad(aQuad: PByte; aValue: Int64); +begin + PInteger(aQuad)^ := Integer(aValue shr 32); + PCardinal(aQuad+4)^ := Cardinal(aValue and $FFFFFFFF); +end; + +function AlignTo(aValue, aAlignment: cardinal): cardinal; +begin + Result := (aValue + aAlignment - 1) and not (aAlignment - 1); +end; + +procedure GetTypeLayout(const aVar: TWireSQLVar; var aSize, aAlignment: cardinal); +begin + case aVar.SQLType of + SQL_TEXT: + begin + aSize := aVar.DataSize; + aAlignment := 1; + end; + SQL_VARYING: + begin + aSize := aVar.DataSize + 2; + aAlignment := 2; + end; + SQL_SHORT: + begin + aSize := 2; + aAlignment := 2; + end; + SQL_LONG, SQL_FLOAT, SQL_TYPE_DATE, SQL_TYPE_TIME: + begin + aSize := 4; + aAlignment := 4; + end; + SQL_DOUBLE, SQL_D_FLOAT, SQL_INT64, SQL_TIMESTAMP, SQL_DEC16: + begin + aSize := 8; + aAlignment := 8; + end; + SQL_BLOB, SQL_ARRAY, SQL_QUAD: + begin + aSize := 8; + aAlignment := 4; + end; + SQL_BOOLEAN: + begin + aSize := 1; + aAlignment := 1; + end; + SQL_INT128, SQL_DEC34: + begin + aSize := 16; + aAlignment := 8; + end; + SQL_TIME_TZ: + begin + aSize := 8; {ISC_TIME_TZ: 4 byte time + 2 byte zone id (+padding)} + aAlignment := 4; + end; + SQL_TIMESTAMP_TZ: + begin + aSize := 12; {ISC_TIMESTAMP_TZ: 8 byte timestamp + 2 byte zone (+pad)} + aAlignment := 4; + end; + SQL_TIME_TZ_EX: + begin + aSize := 8; {ISC_TIME_TZ_EX: time + zone + displacement} + aAlignment := 4; + end; + SQL_TIMESTAMP_TZ_EX: + begin + aSize := 12; + aAlignment := 4; + end; + SQL_NULL: + begin + aSize := 0; + aAlignment := 1; + end; + else + IBError(ibxeInvalidDataConversion,[nil]); + end; +end; + +function ComputeMessageLayout(var aFormat: TWireMessageFormat): cardinal; +var i: integer; + offset: cardinal; + size, alignment: cardinal; +begin + offset := 0; + for i := 0 to Length(aFormat) - 1 do + begin + size := 0; + alignment := 1; + GetTypeLayout(aFormat[i],size,alignment); + offset := AlignTo(offset,alignment); + aFormat[i].DataOffset := offset; + aFormat[i].BufferSize := size; + Inc(offset,size); + end; + {null indicators at the end, 4 byte aligned} + offset := AlignTo(offset,4); + for i := 0 to Length(aFormat) - 1 do + begin + aFormat[i].NullOffset := offset; + Inc(offset,4); + end; + Result := offset; +end; + +function MessageBufferSize(const aFormat: TWireMessageFormat): cardinal; +begin + {the null indicators are laid out last, one 4 byte word each} + if Length(aFormat) = 0 then + Result := 0 + else + Result := aFormat[High(aFormat)].NullOffset + 4; +end; + +function EngineMessageLength(const aFormat: TWireMessageFormat): cardinal; +var i: integer; + offset: cardinal; + size, align: cardinal; +begin + offset := 0; + for i := 0 to Length(aFormat) - 1 do + begin + {sizes and alignments per PARSE_msg_format and jrd/align.h - these are + the server's rules, not the layout of our own message buffer} + case aFormat[i].SQLType of + SQL_TEXT: + begin + size := aFormat[i].DataSize; + align := 1; + end; + SQL_VARYING: + begin + size := aFormat[i].DataSize + 2; + align := 2; + end; + SQL_SHORT: + begin + size := 2; + align := 2; + end; + SQL_LONG, SQL_FLOAT, SQL_TYPE_DATE, SQL_TYPE_TIME: + begin + size := 4; + align := 4; + end; + SQL_DOUBLE, SQL_D_FLOAT, SQL_INT64, SQL_DEC16: + begin + size := 8; + align := 8; + end; + SQL_TIMESTAMP: + begin + size := 8; + align := 4; + end; + SQL_BLOB, SQL_ARRAY, SQL_QUAD: + begin + size := 8; + align := 4; + end; + SQL_BOOLEAN: + begin + size := 1; + align := 1; + end; + SQL_DEC34, SQL_INT128: + begin + size := 16; + align := 8; + end; + SQL_TIME_TZ, SQL_TIME_TZ_EX: + begin + size := 8; + align := 4; + end; + SQL_TIMESTAMP_TZ, SQL_TIMESTAMP_TZ_EX: + begin + size := 12; + align := 4; + end; + else + IBError(ibxeInvalidDataConversion,[nil]); + end; + offset := AlignTo(offset,align); + Inc(offset,size); + {the null indicator short that BuildMessageBlr emits after each value} + offset := AlignTo(offset,2); + Inc(offset,2); + end; + Result := offset; +end; + +function BuildMessageBlr(const aFormat: TWireMessageFormat): TBytes; +var blr: TBytes; + blrLen: integer; + + procedure AddByte(aValue: byte); + begin + if blrLen >= Length(blr) then + SetLength(blr,Length(blr) + 64); + blr[blrLen] := aValue; + Inc(blrLen); + end; + + procedure AddWord(aValue: word); + begin + {BLR is little endian} + AddByte(aValue and $FF); + AddByte((aValue shr 8) and $FF); + end; + +var i: integer; +begin + SetLength(blr,16 + Length(aFormat)*8); + blrLen := 0; + AddByte(blr_version5); + AddByte(blr_begin); + AddByte(blr_message); + AddByte(0); {message number} + AddWord(Length(aFormat)*2); {field count incl. null indicator shorts} + for i := 0 to Length(aFormat) - 1 do + begin + case aFormat[i].SQLType of + {Text types must name their character set explicitly. Without it the + engine assumes the connection character set and reinterprets the byte + length as (length div max bytes per character), which silently + truncates the column - see blr_text2/blr_varying2 in blr.h.} + SQL_TEXT: + begin + AddByte(blr_text2); + AddWord(aFormat[i].CharSetID); + AddWord(aFormat[i].DataSize); + end; + SQL_VARYING: + begin + AddByte(blr_varying2); + AddWord(aFormat[i].CharSetID); + AddWord(aFormat[i].DataSize); + end; + SQL_SHORT: + begin + AddByte(blr_short); + AddByte(byte(ShortInt(aFormat[i].Scale))); + end; + SQL_LONG: + begin + AddByte(blr_long); + AddByte(byte(ShortInt(aFormat[i].Scale))); + end; + SQL_INT64: + begin + AddByte(blr_int64); + AddByte(byte(ShortInt(aFormat[i].Scale))); + end; + SQL_INT128: + begin + AddByte(blr_int128); + AddByte(byte(ShortInt(aFormat[i].Scale))); + end; + SQL_BLOB: + begin + {only the blob id travels in the message; the subtype and charset + describe the blob contents fetched later with op_get_segment} + AddByte(blr_blob2); + AddWord(word(SmallInt(aFormat[i].SQLSubType))); + AddWord(aFormat[i].CharSetID); + end; + SQL_QUAD, SQL_ARRAY: + begin + AddByte(blr_quad); + AddByte(0); + end; + SQL_FLOAT: + AddByte(blr_float); + SQL_DOUBLE, SQL_D_FLOAT: + AddByte(blr_double); + SQL_TIMESTAMP: + AddByte(blr_timestamp); + SQL_TYPE_DATE: + AddByte(blr_sql_date); + SQL_TYPE_TIME: + AddByte(blr_sql_time); + SQL_BOOLEAN: + AddByte(blr_bool); + SQL_DEC16: + AddByte(blr_dec64); + SQL_DEC34: + AddByte(blr_dec128); + SQL_TIME_TZ: + AddByte(blr_sql_time_tz); + SQL_TIMESTAMP_TZ: + AddByte(blr_timestamp_tz); + SQL_TIME_TZ_EX: + AddByte(blr_ex_time_tz); + SQL_TIMESTAMP_TZ_EX: + AddByte(blr_ex_timestamp_tz); + else + IBError(ibxeInvalidDataConversion,[nil]); + end; + {every field is followed by a null indicator described as a short} + AddByte(blr_short); + AddByte(0); + end; + AddByte(blr_end); + AddByte(blr_eoc); + SetLength(blr,blrLen); + Result := blr; +end; + +procedure XDREncodeMessage(XDR: TXDRStream; const aFormat: TWireMessageFormat; + aBuffer: PByte); +var i: integer; + bitmap: TBytes; + bitmapLen: integer; + p: PByte; + varLen: integer; + opaque: TBytes; + + function IsNull(index: integer): boolean; + begin + Result := PInteger(aBuffer + aFormat[index].NullOffset)^ <> 0; + end; + +begin + {null bitmap - little endian bit order, padded to 4 bytes} + bitmapLen := (Length(aFormat) + 7) div 8; + SetLength(bitmap,AlignTo(bitmapLen,4)); + for i := 0 to High(bitmap) do bitmap[i] := 0; + for i := 0 to Length(aFormat) - 1 do + if IsNull(i) then + bitmap[i div 8] := bitmap[i div 8] or (1 shl (i mod 8)); + XDR.WriteRaw(bitmap[0],Length(bitmap)); + + for i := 0 to Length(aFormat) - 1 do + begin + if IsNull(i) then continue; + p := aBuffer + aFormat[i].DataOffset; + case aFormat[i].SQLType of + SQL_TEXT: + begin + SetLength(opaque,aFormat[i].DataSize); + if Length(opaque) > 0 then + Move(p^,opaque[0],Length(opaque)); + XDR.WriteOpaque(opaque); + end; + SQL_VARYING: + begin + varLen := PWord(p)^; + if varLen > integer(aFormat[i].DataSize) then + varLen := aFormat[i].DataSize; + SetLength(opaque,varLen); + if varLen > 0 then + Move((p+2)^,opaque[0],varLen); + XDR.WriteString(opaque); + end; + SQL_SHORT: + XDR.WriteInt32(PSmallInt(p)^); + SQL_LONG: + XDR.WriteInt32(PInteger(p)^); + SQL_FLOAT: + XDR.WriteUInt32(PCardinal(p)^); {IEEE bits} + SQL_DOUBLE, SQL_D_FLOAT: + XDR.WriteInt64(PInt64(p)^); {IEEE bits as hyper} + SQL_INT64: + XDR.WriteInt64(PInt64(p)^); + SQL_TYPE_DATE: + XDR.WriteInt32(PInteger(p)^); + SQL_TYPE_TIME: + XDR.WriteUInt32(PCardinal(p)^); + SQL_TIMESTAMP: + begin + XDR.WriteInt32(PInteger(p)^); + XDR.WriteUInt32(PCardinal(p+4)^); + end; + SQL_BLOB, SQL_ARRAY, SQL_QUAD: + begin + {ISC_QUAD: high then low} + XDR.WriteInt32(PInteger(p)^); + XDR.WriteUInt32(PCardinal(p+4)^); + end; + SQL_BOOLEAN: + begin + {xdr_datum sends a boolean as a one byte opaque value padded to 4 + bytes - the value byte comes first, it is not a big endian int} + SetLength(opaque,1); + opaque[0] := p^; + XDR.WriteOpaque(opaque); + end; + SQL_DEC16: + XDR.WriteInt64(PInt64(p)^); + SQL_DEC34, SQL_INT128: + begin + {sent as two hypers, high part first} + XDR.WriteInt64(PInt64(p+8)^); + XDR.WriteInt64(PInt64(p)^); + end; + SQL_TIME_TZ: + begin + XDR.WriteUInt32(PCardinal(p)^); + XDR.WriteInt32(PWord(p+4)^); + end; + SQL_TIMESTAMP_TZ: + begin + XDR.WriteInt32(PInteger(p)^); + XDR.WriteUInt32(PCardinal(p+4)^); + XDR.WriteInt32(PWord(p+8)^); + end; + SQL_TIME_TZ_EX: + begin + XDR.WriteUInt32(PCardinal(p)^); + XDR.WriteInt32(PWord(p+4)^); + XDR.WriteInt32(PSmallInt(p+6)^); + end; + SQL_TIMESTAMP_TZ_EX: + begin + XDR.WriteInt32(PInteger(p)^); + XDR.WriteUInt32(PCardinal(p+4)^); + XDR.WriteInt32(PWord(p+8)^); + XDR.WriteInt32(PSmallInt(p+10)^); + end; + SQL_NULL: {no data} ; + else + IBError(ibxeInvalidDataConversion,[nil]); + end; + end; +end; + +procedure XDRDecodeMessage(XDR: TXDRStream; const aFormat: TWireMessageFormat; + aBuffer: PByte); +var i: integer; + bitmap: TBytes; + bitmapLen: integer; + p: PByte; + opaque: TBytes; + varLen: integer; + +begin + bitmapLen := (Length(aFormat) + 7) div 8; + bitmap := XDR.ReadOpaque(bitmapLen); + + for i := 0 to Length(aFormat) - 1 do + begin + if (bitmap[i div 8] shr (i mod 8)) and 1 = 1 then + begin + PInteger(aBuffer + aFormat[i].NullOffset)^ := -1; + continue; + end; + PInteger(aBuffer + aFormat[i].NullOffset)^ := 0; + p := aBuffer + aFormat[i].DataOffset; + case aFormat[i].SQLType of + SQL_TEXT: + begin + opaque := XDR.ReadOpaque(aFormat[i].DataSize); + if Length(opaque) > 0 then + Move(opaque[0],p^,Length(opaque)); + end; + SQL_VARYING: + begin + opaque := XDR.ReadString; + varLen := Length(opaque); + if varLen > integer(aFormat[i].DataSize) then + varLen := aFormat[i].DataSize; + PWord(p)^ := varLen; + if varLen > 0 then + Move(opaque[0],(p+2)^,varLen); + end; + SQL_SHORT: + PSmallInt(p)^ := XDR.ReadInt32; + SQL_LONG: + PInteger(p)^ := XDR.ReadInt32; + SQL_FLOAT: + PCardinal(p)^ := XDR.ReadUInt32; + SQL_DOUBLE, SQL_D_FLOAT: + PInt64(p)^ := XDR.ReadInt64; + SQL_INT64: + PInt64(p)^ := XDR.ReadInt64; + SQL_TYPE_DATE: + PInteger(p)^ := XDR.ReadInt32; + SQL_TYPE_TIME: + PCardinal(p)^ := XDR.ReadUInt32; + SQL_TIMESTAMP: + begin + PInteger(p)^ := XDR.ReadInt32; + PCardinal(p+4)^ := XDR.ReadUInt32; + end; + SQL_BLOB, SQL_ARRAY, SQL_QUAD: + begin + PInteger(p)^ := XDR.ReadInt32; + PCardinal(p+4)^ := XDR.ReadUInt32; + end; + SQL_BOOLEAN: + begin + {one byte value followed by three pad bytes} + opaque := XDR.ReadOpaque(1); + p^ := opaque[0]; + end; + SQL_DEC16: + PInt64(p)^ := XDR.ReadInt64; + SQL_DEC34, SQL_INT128: + begin + PInt64(p+8)^ := XDR.ReadInt64; + PInt64(p)^ := XDR.ReadInt64; + end; + SQL_TIME_TZ: + begin + PCardinal(p)^ := XDR.ReadUInt32; + PWord(p+4)^ := word(XDR.ReadInt32); + end; + SQL_TIMESTAMP_TZ: + begin + PInteger(p)^ := XDR.ReadInt32; + PCardinal(p+4)^ := XDR.ReadUInt32; + PWord(p+8)^ := word(XDR.ReadInt32); + end; + SQL_TIME_TZ_EX: + begin + PCardinal(p)^ := XDR.ReadUInt32; + PWord(p+4)^ := word(XDR.ReadInt32); + PSmallInt(p+6)^ := SmallInt(XDR.ReadInt32); + end; + SQL_TIMESTAMP_TZ_EX: + begin + PInteger(p)^ := XDR.ReadInt32; + PCardinal(p+4)^ := XDR.ReadUInt32; + PWord(p+8)^ := word(XDR.ReadInt32); + PSmallInt(p+10)^ := SmallInt(XDR.ReadInt32); + end; + SQL_NULL: {no data} ; + else + IBError(ibxeInvalidDataConversion,[nil]); + end; + end; +end; + +function SliceElementDscLength(const aLayout: TWireSliceLayout): cardinal; +begin + case aLayout.Dtype of + blr_text, blr_text2: + Result := aLayout.ElementLength; + blr_varying, blr_varying2: + {blr_varying maps to a dtype_cstring element of the declared length + plus room for a two byte count - see sdl_desc} + Result := aLayout.ElementLength + 2; + blr_short: + Result := 2; + blr_long, blr_sql_date, blr_sql_time, blr_float: + Result := 4; + blr_int64, blr_quad, blr_blob_id, blr_double, blr_d_float, + blr_timestamp, blr_dec64, blr_sql_time_tz: + Result := 8; + blr_ex_time_tz: + Result := 8; + blr_timestamp_tz, blr_ex_timestamp_tz: + Result := 12; + blr_dec128, blr_int128: + Result := 16; + blr_bool: + Result := 1; + else + IBError(ibxeInvalidDataConversion,[nil]); + end; +end; + +function SliceLength(const aLayout: TWireSliceLayout): cardinal; +begin + Result := aLayout.Count * SliceElementDscLength(aLayout); +end; + +{The encodings below follow xdr_datum for the descriptor sdl_desc derives + from the SDL: notably a varying element travels in dtype_cstring form - a + count followed by the characters - matching the zero terminated layout of + the local buffer, not the counted layout used in messages.} + +procedure XDREncodeSlice(XDR: TXDRStream; const aLayout: TWireSliceLayout; + aBuffer: PByte); +var i: cardinal; + p: PByte; + n: cardinal; + opaque: TBytes; +begin + if aLayout.Count = 0 then Exit; + for i := 0 to aLayout.Count - 1 do + begin + p := aBuffer + i * aLayout.BufferStride; + case aLayout.Dtype of + blr_text, blr_text2: + begin + SetLength(opaque,aLayout.ElementLength); + if Length(opaque) > 0 then + Move(p^,opaque[0],Length(opaque)); + XDR.WriteOpaque(opaque); + end; + blr_varying, blr_varying2: + begin + n := 0; + while (n < aLayout.ElementLength) and ((p + n)^ <> 0) do + Inc(n); + XDR.WriteInt32(n); + SetLength(opaque,n); + if n > 0 then + Move(p^,opaque[0],n); + XDR.WriteOpaque(opaque); + end; + blr_short: + XDR.WriteInt32(PSmallInt(p)^); + blr_long, blr_sql_date: + XDR.WriteInt32(PInteger(p)^); + blr_sql_time: + XDR.WriteUInt32(PCardinal(p)^); + blr_float: + XDR.WriteUInt32(PCardinal(p)^); {IEEE bits} + blr_int64, blr_quad, blr_blob_id, blr_double, blr_d_float, blr_dec64: + XDR.WriteInt64(PInt64(p)^); + blr_timestamp: + begin + XDR.WriteInt32(PInteger(p)^); + XDR.WriteUInt32(PCardinal(p+4)^); + end; + blr_sql_time_tz: + begin + XDR.WriteUInt32(PCardinal(p)^); + XDR.WriteInt32(PWord(p+4)^); + end; + blr_timestamp_tz: + begin + XDR.WriteInt32(PInteger(p)^); + XDR.WriteUInt32(PCardinal(p+4)^); + XDR.WriteInt32(PWord(p+8)^); + end; + blr_ex_time_tz: + begin + XDR.WriteUInt32(PCardinal(p)^); + XDR.WriteInt32(PWord(p+4)^); + XDR.WriteInt32(PSmallInt(p+6)^); + end; + blr_ex_timestamp_tz: + begin + XDR.WriteInt32(PInteger(p)^); + XDR.WriteUInt32(PCardinal(p+4)^); + XDR.WriteInt32(PWord(p+8)^); + XDR.WriteInt32(PSmallInt(p+10)^); + end; + blr_dec128, blr_int128: + begin + {two hypers, high part first} + XDR.WriteInt64(PInt64(p+8)^); + XDR.WriteInt64(PInt64(p)^); + end; + blr_bool: + begin + SetLength(opaque,1); + opaque[0] := p^; + XDR.WriteOpaque(opaque); + end; + else + IBError(ibxeInvalidDataConversion,[nil]); + end; + end; +end; + +procedure XDRDecodeSlice(XDR: TXDRStream; const aLayout: TWireSliceLayout; + aBuffer: PByte; aWireLength: cardinal); +var i, aCount: cardinal; + p: PByte; + n: cardinal; + opaque: TBytes; +begin + aCount := aWireLength div SliceElementDscLength(aLayout); + if aCount > aLayout.Count then + aCount := aLayout.Count; + if aCount = 0 then Exit; + for i := 0 to aCount - 1 do + begin + p := aBuffer + i * aLayout.BufferStride; + case aLayout.Dtype of + blr_text, blr_text2: + begin + opaque := XDR.ReadOpaque(aLayout.ElementLength); + if Length(opaque) > 0 then + Move(opaque[0],p^,Length(opaque)); + end; + blr_varying, blr_varying2: + begin + n := XDR.ReadUInt32; + if n > aLayout.ElementLength + 1 then + n := aLayout.ElementLength + 1; + opaque := XDR.ReadOpaque(n); + if n > aLayout.ElementLength then + n := aLayout.ElementLength; + if n > 0 then + Move(opaque[0],p^,n); + (p + n)^ := 0; + end; + blr_short: + PSmallInt(p)^ := XDR.ReadInt32; + blr_long, blr_sql_date: + PInteger(p)^ := XDR.ReadInt32; + blr_sql_time: + PCardinal(p)^ := XDR.ReadUInt32; + blr_float: + PCardinal(p)^ := XDR.ReadUInt32; + blr_int64, blr_quad, blr_blob_id, blr_double, blr_d_float, blr_dec64: + PInt64(p)^ := XDR.ReadInt64; + blr_timestamp: + begin + PInteger(p)^ := XDR.ReadInt32; + PCardinal(p+4)^ := XDR.ReadUInt32; + end; + blr_sql_time_tz: + begin + PCardinal(p)^ := XDR.ReadUInt32; + PWord(p+4)^ := word(XDR.ReadInt32); + end; + blr_timestamp_tz: + begin + PInteger(p)^ := XDR.ReadInt32; + PCardinal(p+4)^ := XDR.ReadUInt32; + PWord(p+8)^ := word(XDR.ReadInt32); + end; + blr_ex_time_tz: + begin + PCardinal(p)^ := XDR.ReadUInt32; + PWord(p+4)^ := word(XDR.ReadInt32); + PSmallInt(p+6)^ := SmallInt(XDR.ReadInt32); + end; + blr_ex_timestamp_tz: + begin + PInteger(p)^ := XDR.ReadInt32; + PCardinal(p+4)^ := XDR.ReadUInt32; + PWord(p+8)^ := word(XDR.ReadInt32); + PSmallInt(p+10)^ := SmallInt(XDR.ReadInt32); + end; + blr_dec128, blr_int128: + begin + PInt64(p+8)^ := XDR.ReadInt64; + PInt64(p)^ := XDR.ReadInt64; + end; + blr_bool: + begin + opaque := XDR.ReadOpaque(1); + p^ := opaque[0]; + end; + else + IBError(ibxeInvalidDataConversion,[nil]); + end; + end; +end; + +end. diff --git a/client/wire/FBWireMessages.pas b/client/wire/FBWireMessages.pas new file mode 100644 index 00000000..f9ac0f3e --- /dev/null +++ b/client/wire/FBWireMessages.pas @@ -0,0 +1,2129 @@ +(* + * Firebird Interface (fbintf). The fbintf components provide a set of + * Pascal language bindings for the Firebird API. + * + * This file is part of the pure Pascal wire protocol implementation + * and is subject to the Initial Developer's Public License Version 1.0. + * + * The Initial Developer of the Original Code is MWA Software + * (http://www.mwasoftware.co.uk). All Rights Reserved. + * +*) + +{GENERATED FILE - DO NOT EDIT. + + Engine error message texts for the facilities a server round trip can + produce (JRD, DSQL, DYN, SQLERR, SQLWARN), so that the wire provider + can format status + vectors the way fb_interpret does without needing firebird.msg. + + Generated by generate_messages.py from the Firebird 6.0 message + headers (src/include/firebird/impl/msg, commit cf838bb). + Regenerate per Firebird release.} + +unit FBWireMessages; + +{$IFDEF FPC} +{$mode delphi} +{$codepage UTF8} +{$ENDIF} + +interface + +const + {an entry with no SQLCODE mapping} + NoSQLCode = 32767; + +{looks up the fb_interpret format string - with @1..@n parameter + placeholders - for an engine error code. False when the code is not in + the table (an unknown facility, or a newer server's message).} +function FindEngineMessage(aCode: cardinal; var aFormat: AnsiString): boolean; + +{the SQLCODE the message database assigns to an engine error code, or + NoSQLCode - what isc_sqlcode derives per status vector item} +function EngineMessageSQLCode(aCode: cardinal): integer; + +implementation + +const + MessageCount = 1682; + + MessageCodes: array[0..MessageCount-1] of cardinal = ( + 335544320, 335544321, 335544322, 335544323, 335544324, 335544325, 335544326, 335544327, + 335544328, 335544329, 335544330, 335544331, 335544332, 335544333, 335544334, 335544335, + 335544336, 335544337, 335544338, 335544339, 335544340, 335544341, 335544342, 335544343, + 335544344, 335544345, 335544346, 335544347, 335544348, 335544349, 335544350, 335544351, + 335544352, 335544353, 335544354, 335544355, 335544356, 335544357, 335544358, 335544359, + 335544360, 335544361, 335544362, 335544363, 335544364, 335544365, 335544366, 335544367, + 335544368, 335544369, 335544370, 335544371, 335544372, 335544373, 335544374, 335544375, + 335544376, 335544377, 335544378, 335544379, 335544380, 335544381, 335544382, 335544383, + 335544384, 335544385, 335544386, 335544387, 335544388, 335544389, 335544390, 335544391, + 335544392, 335544393, 335544394, 335544395, 335544396, 335544397, 335544398, 335544399, + 335544400, 335544401, 335544402, 335544403, 335544404, 335544405, 335544406, 335544407, + 335544408, 335544409, 335544410, 335544411, 335544412, 335544413, 335544414, 335544415, + 335544416, 335544417, 335544418, 335544419, 335544420, 335544421, 335544422, 335544423, + 335544424, 335544425, 335544426, 335544427, 335544428, 335544429, 335544430, 335544431, + 335544432, 335544433, 335544434, 335544435, 335544436, 335544437, 335544438, 335544439, + 335544440, 335544441, 335544442, 335544443, 335544444, 335544445, 335544446, 335544447, + 335544448, 335544449, 335544450, 335544451, 335544452, 335544453, 335544454, 335544455, + 335544456, 335544457, 335544458, 335544459, 335544460, 335544461, 335544462, 335544463, + 335544464, 335544465, 335544466, 335544467, 335544468, 335544469, 335544470, 335544471, + 335544472, 335544473, 335544474, 335544475, 335544476, 335544477, 335544478, 335544479, + 335544480, 335544481, 335544482, 335544483, 335544484, 335544485, 335544486, 335544487, + 335544488, 335544489, 335544490, 335544491, 335544492, 335544493, 335544494, 335544495, + 335544496, 335544497, 335544498, 335544499, 335544500, 335544501, 335544502, 335544503, + 335544504, 335544505, 335544506, 335544507, 335544508, 335544509, 335544510, 335544511, + 335544512, 335544513, 335544514, 335544515, 335544516, 335544517, 335544518, 335544519, + 335544520, 335544521, 335544522, 335544523, 335544524, 335544525, 335544526, 335544527, + 335544528, 335544529, 335544530, 335544531, 335544532, 335544533, 335544534, 335544535, + 335544536, 335544537, 335544538, 335544539, 335544540, 335544541, 335544542, 335544543, + 335544544, 335544545, 335544546, 335544547, 335544548, 335544549, 335544550, 335544551, + 335544552, 335544553, 335544554, 335544555, 335544556, 335544557, 335544558, 335544559, + 335544560, 335544561, 335544562, 335544563, 335544564, 335544565, 335544566, 335544567, + 335544568, 335544569, 335544570, 335544571, 335544572, 335544573, 335544574, 335544575, + 335544576, 335544577, 335544578, 335544579, 335544580, 335544581, 335544582, 335544583, + 335544584, 335544585, 335544586, 335544587, 335544588, 335544589, 335544590, 335544591, + 335544592, 335544593, 335544594, 335544595, 335544596, 335544597, 335544598, 335544599, + 335544600, 335544601, 335544602, 335544603, 335544604, 335544605, 335544606, 335544607, + 335544608, 335544609, 335544610, 335544611, 335544612, 335544613, 335544614, 335544615, + 335544616, 335544617, 335544618, 335544619, 335544620, 335544621, 335544622, 335544623, + 335544624, 335544625, 335544626, 335544627, 335544628, 335544629, 335544630, 335544631, + 335544632, 335544633, 335544634, 335544635, 335544636, 335544637, 335544638, 335544639, + 335544640, 335544641, 335544642, 335544643, 335544644, 335544645, 335544646, 335544647, + 335544648, 335544649, 335544650, 335544651, 335544652, 335544653, 335544654, 335544655, + 335544656, 335544657, 335544658, 335544659, 335544660, 335544661, 335544662, 335544663, + 335544664, 335544665, 335544666, 335544667, 335544668, 335544669, 335544670, 335544671, + 335544672, 335544673, 335544674, 335544675, 335544676, 335544677, 335544678, 335544679, + 335544680, 335544681, 335544682, 335544683, 335544684, 335544685, 335544686, 335544687, + 335544688, 335544689, 335544690, 335544691, 335544692, 335544693, 335544694, 335544695, + 335544696, 335544697, 335544698, 335544699, 335544700, 335544701, 335544702, 335544703, + 335544704, 335544705, 335544706, 335544707, 335544708, 335544709, 335544710, 335544711, + 335544712, 335544713, 335544714, 335544715, 335544716, 335544717, 335544718, 335544719, + 335544720, 335544721, 335544722, 335544723, 335544724, 335544725, 335544726, 335544727, + 335544728, 335544729, 335544730, 335544731, 335544732, 335544733, 335544734, 335544735, + 335544736, 335544737, 335544738, 335544739, 335544740, 335544741, 335544742, 335544743, + 335544744, 335544745, 335544746, 335544747, 335544748, 335544749, 335544750, 335544751, + 335544752, 335544753, 335544754, 335544755, 335544756, 335544757, 335544758, 335544759, + 335544760, 335544761, 335544762, 335544763, 335544764, 335544765, 335544766, 335544767, + 335544768, 335544769, 335544770, 335544771, 335544772, 335544773, 335544774, 335544775, + 335544776, 335544777, 335544778, 335544779, 335544780, 335544781, 335544782, 335544783, + 335544784, 335544785, 335544786, 335544787, 335544788, 335544789, 335544790, 335544791, + 335544792, 335544793, 335544794, 335544795, 335544796, 335544797, 335544798, 335544799, + 335544800, 335544801, 335544802, 335544803, 335544804, 335544805, 335544806, 335544807, + 335544808, 335544809, 335544810, 335544811, 335544812, 335544813, 335544814, 335544815, + 335544816, 335544817, 335544818, 335544819, 335544820, 335544821, 335544822, 335544823, + 335544824, 335544825, 335544826, 335544827, 335544828, 335544829, 335544830, 335544831, + 335544832, 335544833, 335544834, 335544835, 335544836, 335544837, 335544838, 335544839, + 335544840, 335544841, 335544842, 335544843, 335544844, 335544845, 335544846, 335544847, + 335544848, 335544849, 335544850, 335544851, 335544852, 335544853, 335544854, 335544855, + 335544856, 335544857, 335544858, 335544859, 335544860, 335544861, 335544862, 335544863, + 335544864, 335544865, 335544866, 335544867, 335544868, 335544869, 335544870, 335544871, + 335544872, 335544873, 335544874, 335544875, 335544876, 335544877, 335544878, 335544879, + 335544880, 335544881, 335544882, 335544883, 335544884, 335544885, 335544886, 335544887, + 335544888, 335544889, 335544890, 335544891, 335544892, 335544893, 335544894, 335544895, + 335544896, 335544897, 335544898, 335544899, 335544900, 335544901, 335544902, 335544903, + 335544904, 335544905, 335544906, 335544907, 335544908, 335544909, 335544910, 335544911, + 335544912, 335544913, 335544914, 335544915, 335544916, 335544917, 335544918, 335544919, + 335544920, 335544921, 335544922, 335544923, 335544924, 335544925, 335544926, 335544927, + 335544928, 335544929, 335544930, 335544931, 335544932, 335544933, 335544934, 335544935, + 335544936, 335544937, 335544938, 335544939, 335544940, 335544941, 335544942, 335544943, + 335544944, 335544945, 335544946, 335544947, 335544948, 335544949, 335544950, 335544951, + 335544952, 335544953, 335544954, 335544955, 335544956, 335544957, 335544958, 335544959, + 335544960, 335544961, 335544962, 335544963, 335544964, 335544965, 335544966, 335544967, + 335544968, 335544969, 335544970, 335544971, 335544972, 335544973, 335544974, 335544975, + 335544976, 335544977, 335544978, 335544979, 335544980, 335544981, 335544982, 335544983, + 335544984, 335544985, 335544986, 335544987, 335544988, 335544989, 335544990, 335544991, + 335544992, 335544993, 335544994, 335544995, 335544996, 335544997, 335544998, 335544999, + 335545000, 335545001, 335545002, 335545003, 335545004, 335545005, 335545006, 335545007, + 335545008, 335545009, 335545010, 335545011, 335545012, 335545013, 335545014, 335545015, + 335545016, 335545017, 335545018, 335545019, 335545020, 335545021, 335545022, 335545023, + 335545024, 335545025, 335545026, 335545027, 335545028, 335545029, 335545030, 335545031, + 335545032, 335545033, 335545034, 335545035, 335545036, 335545037, 335545038, 335545039, + 335545040, 335545041, 335545042, 335545043, 335545044, 335545045, 335545046, 335545047, + 335545048, 335545049, 335545050, 335545051, 335545052, 335545053, 335545054, 335545055, + 335545056, 335545057, 335545058, 335545059, 335545060, 335545061, 335545062, 335545063, + 335545064, 335545065, 335545066, 335545067, 335545068, 335545069, 335545070, 335545071, + 335545072, 335545073, 335545074, 335545075, 335545076, 335545077, 335545078, 335545079, + 335545080, 335545081, 335545082, 335545083, 335545084, 335545085, 335545086, 335545087, + 335545088, 335545089, 335545090, 335545091, 335545092, 335545093, 335545094, 335545095, + 335545096, 335545097, 335545098, 335545099, 335545100, 335545101, 335545102, 335545103, + 335545104, 335545105, 335545106, 335545107, 335545108, 335545109, 335545110, 335545111, + 335545112, 335545113, 335545114, 335545115, 335545116, 335545117, 335545118, 335545119, + 335545120, 335545121, 335545122, 335545123, 335545124, 335545125, 335545126, 335545127, + 335545128, 335545129, 335545130, 335545131, 335545132, 335545133, 335545134, 335545135, + 335545136, 335545137, 335545138, 335545139, 335545140, 335545141, 335545142, 335545143, + 335545144, 335545145, 335545146, 335545147, 335545148, 335545149, 335545150, 335545151, + 335545152, 335545153, 335545154, 335545155, 335545156, 335545157, 335545158, 335545159, + 335545160, 335545161, 335545162, 335545163, 335545164, 335545165, 335545166, 335545167, + 335545168, 335545169, 335545170, 335545171, 335545172, 335545173, 335545174, 335545175, + 335545176, 335545177, 335545178, 335545179, 335545180, 335545181, 335545182, 335545183, + 335545184, 335545185, 335545186, 335545187, 335545188, 335545189, 335545190, 335545191, + 335545192, 335545193, 335545194, 335545195, 335545196, 335545197, 335545198, 335545199, + 335545200, 335545201, 335545202, 335545203, 335545204, 335545205, 335545206, 335545207, + 335545208, 335545209, 335545210, 335545211, 335545212, 335545213, 335545214, 335545215, + 335545216, 335545217, 335545218, 335545219, 335545220, 335545221, 335545222, 335545223, + 335545224, 335545225, 335545226, 335545227, 335545228, 335545229, 335545230, 335545231, + 335545232, 335545233, 335545234, 335545235, 335545236, 335545237, 335545238, 335545239, + 335545240, 335545241, 335545242, 335545243, 335545244, 335545245, 335545246, 335545247, + 335545248, 335545249, 335545250, 335545251, 335545252, 335545253, 335545254, 335545255, + 335545256, 335545257, 335545258, 335545259, 335545260, 335545261, 335545262, 335545263, + 335545264, 335545265, 335545266, 335545267, 335545268, 335545269, 335545270, 335545271, + 335545272, 335545273, 335545274, 335545275, 335545276, 335545277, 335545278, 335545279, + 335545280, 335545281, 335545282, 335545283, 335545284, 335545285, 335545286, 335545287, + 335545288, 335545289, 335545290, 335545291, 335545292, 335545293, 335545294, 335545295, + 335545296, 335545297, 335545298, 335545299, 335545300, 335545301, 335545302, 335545303, + 335545304, 335545305, 335545306, 335545307, 335545308, 335545309, 335545310, 335545311, + 335545312, 335545313, 335545314, 335545315, 335545316, 335545317, 335545318, 335545319, + 335545320, 335545321, 335545322, 335545323, 335545324, 335545325, 335545326, 335545327, + 335545328, 335545329, 335545330, 335545331, 335545332, 335545333, 335545334, 335545335, + 335545336, 335545337, 335545338, 335545339, 335545340, 335545341, 335545342, 336003074, + 336003075, 336003076, 336003077, 336003078, 336003079, 336003080, 336003081, 336003082, + 336003083, 336003084, 336003085, 336003086, 336003087, 336003088, 336003089, 336003090, + 336003091, 336003092, 336003093, 336003094, 336003095, 336003096, 336003097, 336003098, + 336003099, 336003100, 336003101, 336003102, 336003103, 336003104, 336003105, 336003106, + 336003107, 336003108, 336003109, 336003110, 336003111, 336003112, 336003113, 336003114, + 336003115, 336068609, 336068610, 336068611, 336068612, 336068613, 336068614, 336068615, + 336068616, 336068617, 336068618, 336068619, 336068620, 336068621, 336068622, 336068623, + 336068624, 336068625, 336068626, 336068627, 336068628, 336068629, 336068630, 336068631, + 336068632, 336068633, 336068634, 336068635, 336068636, 336068637, 336068638, 336068639, + 336068640, 336068641, 336068642, 336068643, 336068644, 336068645, 336068646, 336068647, + 336068648, 336068649, 336068650, 336068651, 336068652, 336068653, 336068654, 336068655, + 336068656, 336068657, 336068658, 336068659, 336068660, 336068661, 336068662, 336068663, + 336068664, 336068665, 336068666, 336068667, 336068668, 336068669, 336068670, 336068671, + 336068672, 336068673, 336068674, 336068675, 336068676, 336068677, 336068678, 336068679, + 336068680, 336068681, 336068682, 336068683, 336068684, 336068685, 336068686, 336068687, + 336068688, 336068689, 336068690, 336068691, 336068692, 336068693, 336068694, 336068695, + 336068696, 336068697, 336068698, 336068699, 336068700, 336068701, 336068702, 336068703, + 336068704, 336068705, 336068706, 336068707, 336068708, 336068709, 336068710, 336068711, + 336068712, 336068713, 336068714, 336068715, 336068716, 336068717, 336068718, 336068719, + 336068720, 336068721, 336068722, 336068723, 336068724, 336068725, 336068726, 336068727, + 336068728, 336068729, 336068730, 336068731, 336068732, 336068733, 336068734, 336068735, + 336068736, 336068737, 336068738, 336068739, 336068740, 336068741, 336068742, 336068743, + 336068744, 336068745, 336068746, 336068747, 336068748, 336068749, 336068750, 336068751, + 336068752, 336068753, 336068754, 336068755, 336068756, 336068757, 336068758, 336068759, + 336068760, 336068761, 336068762, 336068763, 336068764, 336068765, 336068766, 336068767, + 336068768, 336068770, 336068771, 336068772, 336068773, 336068774, 336068775, 336068776, + 336068777, 336068778, 336068779, 336068780, 336068781, 336068782, 336068783, 336068784, + 336068785, 336068786, 336068787, 336068788, 336068789, 336068790, 336068791, 336068792, + 336068793, 336068794, 336068795, 336068796, 336068797, 336068798, 336068799, 336068800, + 336068801, 336068802, 336068803, 336068804, 336068812, 336068813, 336068814, 336068815, + 336068816, 336068817, 336068818, 336068819, 336068820, 336068821, 336068822, 336068823, + 336068824, 336068825, 336068826, 336068827, 336068828, 336068829, 336068830, 336068831, + 336068832, 336068833, 336068834, 336068835, 336068836, 336068837, 336068838, 336068839, + 336068840, 336068841, 336068842, 336068843, 336068844, 336068845, 336068846, 336068847, + 336068848, 336068849, 336068850, 336068851, 336068852, 336068853, 336068854, 336068855, + 336068856, 336068857, 336068858, 336068859, 336068860, 336068861, 336068862, 336068863, + 336068864, 336068865, 336068866, 336068867, 336068868, 336068869, 336068870, 336068871, + 336068872, 336068873, 336068874, 336068875, 336068876, 336068877, 336068878, 336068879, + 336068880, 336068881, 336068882, 336068883, 336068884, 336068885, 336068886, 336068887, + 336068888, 336068889, 336068890, 336068891, 336068892, 336068893, 336068894, 336068895, + 336068896, 336068897, 336068898, 336068899, 336068900, 336068901, 336068902, 336068903, + 336068904, 336068905, 336068906, 336068907, 336068908, 336068909, 336068910, 336068911, + 336068912, 336068913, 336068914, 336068915, 336068916, 336068917, 336068918, 336068919, + 336068920, 336068921, 336068922, 336068923, 336068924, 336068925, 336068926, 336068927, + 336068928, 336068929, 336068930, 336068931, 336068932, 336068933, 336068934, 336068935, + 336396289, 336396362, 336396364, 336396365, 336396366, 336396375, 336396376, 336396377, + 336396379, 336396382, 336396384, 336396386, 336396387, 336396446, 336396447, 336396448, + 336396449, 336396450, 336396451, 336396452, 336396453, 336396454, 336396455, 336396456, + 336396457, 336396458, 336396459, 336396460, 336396461, 336396462, 336396463, 336396464, + 336396465, 336396468, 336396471, 336396472, 336396477, 336396478, 336396479, 336396480, + 336396481, 336396482, 336396484, 336396485, 336396486, 336396594, 336396595, 336396596, + 336396597, 336396598, 336396599, 336396603, 336396611, 336396624, 336396625, 336396628, + 336396651, 336396663, 336396670, 336396671, 336396672, 336396673, 336396681, 336396683, + 336396684, 336396687, 336396688, 336396689, 336396690, 336396691, 336396692, 336396693, + 336396735, 336396736, 336396737, 336396756, 336396757, 336396758, 336396769, 336396770, + 336396778, 336396780, 336396784, 336396786, 336396787, 336396875, 336396881, 336396882, + 336396886, 336396887, 336396974, 336396975, 336396985, 336396991, 336396992, 336396993, + 336396994, 336396995, 336396996, 336396997, 336397004, 336397005, 336397006, 336397007, + 336397027, 336397028, 336397029, 336397030, 336397031, 336397033, 336397034, 336397035, + 336397036, 336397037, 336397038, 336397039, 336397040, 336397041, 336397042, 336397043, + 336397044, 336397045, 336397046, 336397047, 336397048, 336397049, 336397050, 336397051, + 336397052, 336397053, 336397054, 336397055, 336397056, 336397057, 336397058, 336397069, + 336397080, 336397082, 336397083, 336397084, 336397085, 336397116, 336397117, 336397118, + 336397126, 336397130, 336397131, 336397133, 336397137, 336397138, 336397183, 336397184, + 336397185, 336397203, 336397204, 336397205, 336397206, 336397207, 336397208, 336397209, + 336397210, 336397211, 336397212, 336397213, 336397214, 336397215, 336397216, 336397217, + 336397218, 336397219, 336397220, 336397221, 336397222, 336397223, 336397224, 336397225, + 336397226, 336397227, 336397228, 336397229, 336397230, 336397231, 336397232, 336397233, + 336397234, 336397235, 336397236, 336397237, 336397238, 336397239, 336397240, 336397241, + 336397242, 336397243, 336397244, 336397245, 336397246, 336397247, 336397248, 336397249, + 336397250, 336397251, 336397252, 336397253, 336397254, 336397255, 336397256, 336397257, + 336397258, 336397259, 336397260, 336397261, 336397262, 336397263, 336397264, 336397265, + 336397266, 336397267, 336397268, 336397269, 336397270, 336397271, 336397272, 336397273, + 336397274, 336397275, 336397276, 336397277, 336397278, 336397279, 336397280, 336397281, + 336397282, 336397283, 336397284, 336397285, 336397286, 336397287, 336397288, 336397289, + 336397290, 336397291, 336397292, 336397293, 336397294, 336397295, 336397296, 336397297, + 336397298, 336397299, 336397300, 336397301, 336397302, 336397303, 336397304, 336397305, + 336397306, 336397307, 336397308, 336397309, 336397310, 336397311, 336397312, 336397313, + 336397314, 336397315, 336397316, 336397317, 336397318, 336397319, 336397320, 336397321, + 336397322, 336397323, 336397324, 336397325, 336397326, 336397327, 336397328, 336397329, + 336397330, 336397331, 336397332, 336397333, 336397334, 336397335, 336397336, 336397337, + 336397338, 336397339, 336397340, 336397341, 336397342, 336397343, 336461924, 336461925, + 336462125, 336462436); + + MessageTexts: array[0..MessageCount-1] of AnsiString = ( + {335544320} '', + {335544321} 'arithmetic exception, numeric overflow, or string truncation', + {335544322} 'invalid database key', + {335544323} 'file @1 is not a valid database', + {335544324} 'invalid database handle (no active connection)', + {335544325} 'bad parameters on attach or create database', + {335544326} 'unrecognized database parameter block', + {335544327} 'invalid request handle', + {335544328} 'invalid BLOB handle', + {335544329} 'invalid BLOB ID', + {335544330} 'invalid parameter in transaction parameter block', + {335544331} 'invalid format for transaction parameter block', + {335544332} 'invalid transaction handle (expecting explicit transaction start)', + {335544333} 'internal Firebird consistency check (@1)', + {335544334} 'conversion error from string "@1"', + {335544335} 'database file appears corrupt (@1)', + {335544336} 'deadlock', + {335544337} 'attempt to start more than @1 transactions', + {335544338} 'no match for first value expression', + {335544339} 'information type inappropriate for object specified', + {335544340} 'no information of this type available for object specified', + {335544341} 'unknown information item', + {335544342} 'action cancelled by trigger (@1) to preserve data integrity', + {335544343} 'invalid request BLR at offset @1', + {335544344} 'I/O error during "@1" operation for file "@2"', + {335544345} 'lock conflict on no wait transaction', + {335544346} 'corrupt system table', + {335544347} 'validation error for column @1, value "@2"', + {335544348} 'no current record for fetch operation', + {335544349} 'attempt to store duplicate value (visible to active transactions) in unique index @1', + {335544350} 'program attempted to exit without finishing database', + {335544351} 'unsuccessful metadata update', + {335544352} 'no permission for @1 access to @2 @3', + {335544353} 'transaction is not in limbo', + {335544354} 'invalid database key', + {335544355} 'BLOB was not closed', + {335544356} 'metadata is obsolete', + {335544357} 'cannot disconnect database with open transactions (@1 active)', + {335544358} 'message length error (encountered @1, expected @2)', + {335544359} 'attempted update of read-only column @1', + {335544360} 'attempted update of read-only table', + {335544361} 'attempted update during read-only transaction', + {335544362} 'cannot update read-only view @1', + {335544363} 'no transaction for request', + {335544364} 'request synchronization error', + {335544365} 'request referenced an unavailable database', + {335544366} 'segment buffer length shorter than expected', + {335544367} 'attempted retrieval of more segments than exist', + {335544368} 'attempted invalid operation on a BLOB', + {335544369} 'attempted read of a new, open BLOB', + {335544370} 'attempted action on BLOB outside transaction', + {335544371} 'attempted write to read-only BLOB', + {335544372} 'attempted reference to BLOB in unavailable database', + {335544373} 'operating system directive @1 failed', + {335544374} 'attempt to fetch past the last record in a record stream', + {335544375} 'unavailable database', + {335544376} 'table @1 was omitted from the transaction reserving list', + {335544377} 'request includes a DSRI extension not supported in this implementation', + {335544378} 'feature is not supported', + {335544379} 'unsupported on-disk structure for file @1; found @2.@3, support @4.@5', + {335544380} 'wrong number of arguments on call', + {335544381} 'Implementation limit exceeded', + {335544382} '@1', + {335544383} 'unrecoverable conflict with limbo transaction @1', + {335544384} 'internal error', + {335544385} 'internal error', + {335544386} 'too many requests', + {335544387} 'internal error', + {335544388} 'block size exceeds implementation restriction', + {335544389} 'buffer exhausted', + {335544390} 'BLR syntax error: expected @1 at offset @2, encountered @3', + {335544391} 'buffer in use', + {335544392} 'internal error', + {335544393} 'request in use', + {335544394} 'incompatible version of on-disk structure', + {335544395} 'table @1 is not defined', + {335544396} 'column @1 is not defined in table @2', + {335544397} 'internal error', + {335544398} 'internal error', + {335544399} 'internal error', + {335544400} 'internal error', + {335544401} 'internal error', + {335544402} 'internal error', + {335544403} 'page @1 is of wrong type (expected @2, found @3)', + {335544404} 'database corrupted', + {335544405} 'checksum error on database page @1', + {335544406} 'index is broken', + {335544407} 'database handle not zero', + {335544408} 'transaction handle not zero', + {335544409} 'transaction--request mismatch (synchronization error)', + {335544410} 'bad handle count', + {335544411} 'wrong version of transaction parameter block', + {335544412} 'unsupported BLR version (expected @1, encountered @2)', + {335544413} 'wrong version of database parameter block', + {335544414} 'BLOB and array data types are not supported for @1 operation', + {335544415} 'database corrupted', + {335544416} 'internal error', + {335544417} 'internal error', + {335544418} 'transaction in limbo', + {335544419} 'transaction not in limbo', + {335544420} 'transaction outstanding', + {335544421} 'connection rejected by remote interface', + {335544422} 'internal error', + {335544423} 'internal error', + {335544424} 'no lock manager available', + {335544425} 'context already in use (BLR error)', + {335544426} 'context not defined (BLR error)', + {335544427} 'data operation not supported', + {335544428} 'undefined message number', + {335544429} 'undefined parameter number', + {335544430} 'unable to allocate memory from operating system', + {335544431} 'blocking signal has been received', + {335544432} 'lock manager error', + {335544433} 'communication error with journal "@1"', + {335544434} 'key size exceeds implementation restriction for index @1', + {335544435} 'null segment of UNIQUE KEY', + {335544436} 'SQL error code = @1', + {335544437} 'wrong DYN version', + {335544438} 'function @1 is not defined', + {335544439} 'function @1 could not be matched', + {335544440} '', + {335544441} 'database detach completed with errors', + {335544442} 'database system cannot read argument @1', + {335544443} 'database system cannot write argument @1', + {335544444} 'operation not supported', + {335544445} '@1 extension error', + {335544446} 'not updatable', + {335544447} 'no rollback performed', + {335544448} '', + {335544449} '', + {335544450} '@1', + {335544451} 'update conflicts with concurrent update', + {335544452} 'product @1 is not licensed', + {335544453} 'object @1 is in use', + {335544454} 'filter not found to convert type @1 to type @2', + {335544455} 'cannot attach active shadow file', + {335544456} 'invalid slice description language at offset @1', + {335544457} 'subscript out of bounds', + {335544458} 'column not array or invalid dimensions (expected @1, encountered @2)', + {335544459} 'record from transaction @1 is stuck in limbo', + {335544460} 'a file in manual shadow @1 is unavailable', + {335544461} 'secondary server attachments cannot validate databases', + {335544462} 'secondary server attachments cannot start journaling', + {335544463} 'generator @1 is not defined', + {335544464} 'secondary server attachments cannot start logging', + {335544465} 'invalid BLOB type for operation', + {335544466} 'violation of FOREIGN KEY constraint @1 on table @2', + {335544467} 'minor version too high found @1 expected @2', + {335544468} 'transaction @1 is @2', + {335544469} 'transaction marked invalid and cannot be committed', + {335544470} 'cache buffer for page @1 invalid', + {335544471} 'there is no index in table @1 with id @2', + {335544472} 'Your user name and password are not defined. Ask your database administrator to set up a Firebird login.', + {335544473} 'invalid bookmark handle', + {335544474} 'invalid lock level @1', + {335544475} 'lock on table @1 conflicts with existing lock', + {335544476} 'requested record lock conflicts with existing lock', + {335544477} 'maximum indexes per table (@1) exceeded', + {335544478} 'enable journal for database before starting online dump', + {335544479} 'online dump failure. Retry dump', + {335544480} 'an online dump is already in progress', + {335544481} 'no more disk/tape space. Cannot continue online dump', + {335544482} 'journaling allowed only if database has Write-ahead Log', + {335544483} 'maximum number of online dump files that can be specified is 16', + {335544484} 'error in opening Write-ahead Log file during recovery', + {335544485} 'invalid statement handle', + {335544486} 'Write-ahead log subsystem failure', + {335544487} 'WAL Writer error', + {335544488} 'Log file header of @1 too small', + {335544489} 'Invalid version of log file @1', + {335544490} 'Log file @1 not latest in the chain but open flag still set', + {335544491} 'Log file @1 not closed properly; database recovery may be required', + {335544492} 'Database name in the log file @1 is different', + {335544493} 'Unexpected end of log file @1 at offset @2', + {335544494} 'Incomplete log record at offset @1 in log file @2', + {335544495} 'Log record header too small at offset @1 in log file @2', + {335544496} 'Log block too small at offset @1 in log file @2', + {335544497} 'Illegal attempt to attach to an uninitialized WAL segment for @1', + {335544498} 'Invalid WAL parameter block option @1', + {335544499} 'Cannot roll over to the next log file @1', + {335544500} 'database does not use Write-ahead Log', + {335544501} 'cannot drop log file when journaling is enabled', + {335544502} 'reference to invalid stream number', + {335544503} 'WAL subsystem encountered error', + {335544504} 'WAL subsystem corrupted', + {335544505} 'must specify archive file when enabling long term journal for databases with round-robin log files', + {335544506} 'database @1 shutdown in progress', + {335544507} 'refresh range number @1 already in use', + {335544508} 'refresh range number @1 not found', + {335544509} 'CHARACTER SET @1 is not defined', + {335544510} 'lock time-out on wait transaction', + {335544511} 'procedure @1 is not defined', + {335544512} 'Parameter mismatch for procedure @1', + {335544513} 'Database @1: WAL subsystem bug for pid @2'+#10+'@3', + {335544514} 'Could not expand the WAL segment for database @1', + {335544515} 'status code @1 unknown', + {335544516} 'exception @1 not defined', + {335544517} 'exception @1', + {335544518} 'restart shared cache manager', + {335544519} 'invalid lock handle', + {335544520} 'long-term journaling already enabled', + {335544521} 'Unable to roll over please see Firebird log.', + {335544522} 'WAL I/O error. Please see Firebird log.', + {335544523} 'WAL writer - Journal server communication error. Please see Firebird log.', + {335544524} 'WAL buffers cannot be increased. Please see Firebird log.', + {335544525} 'WAL setup error. Please see Firebird log.', + {335544526} 'obsolete', + {335544527} 'Cannot start WAL writer for the database @1', + {335544528} 'database @1 shutdown', + {335544529} 'cannot modify an existing user privilege', + {335544530} 'Cannot delete PRIMARY KEY being used in FOREIGN KEY definition.', + {335544531} 'Column used in a PRIMARY constraint must be NOT NULL.', + {335544532} 'Name of Referential Constraint not defined in constraints table.', + {335544533} 'Non-existent PRIMARY or UNIQUE KEY specified for FOREIGN KEY.', + {335544534} 'Cannot update constraints (RDB$REF_CONSTRAINTS).', + {335544535} 'Cannot update constraints (RDB$CHECK_CONSTRAINTS).', + {335544536} 'Cannot delete CHECK constraint entry (RDB$CHECK_CONSTRAINTS)', + {335544537} 'Cannot delete index segment used by an Integrity Constraint', + {335544538} 'Cannot update index segment used by an Integrity Constraint', + {335544539} 'Cannot delete index used by an Integrity Constraint', + {335544540} 'Cannot modify index used by an Integrity Constraint', + {335544541} 'Cannot delete trigger used by a CHECK Constraint', + {335544542} 'Cannot update trigger used by a CHECK Constraint', + {335544543} 'Cannot delete column being used in an Integrity Constraint.', + {335544544} 'Cannot rename column being used in an Integrity Constraint.', + {335544545} 'Cannot update constraints (RDB$RELATION_CONSTRAINTS).', + {335544546} 'Cannot define constraints on views', + {335544547} 'internal Firebird consistency check (invalid RDB$CONSTRAINT_TYPE)', + {335544548} 'Attempt to define a second PRIMARY KEY for the same table', + {335544549} 'cannot modify or erase a system trigger', + {335544550} 'only the owner of a table may reassign ownership', + {335544551} 'could not find object for GRANT', + {335544552} 'could not find column for GRANT', + {335544553} 'user does not have GRANT privileges for operation', + {335544554} 'object has non-SQL security class defined', + {335544555} 'column has non-SQL security class defined', + {335544556} 'Write-ahead Log without shared cache configuration not allowed', + {335544557} 'database shutdown unsuccessful', + {335544558} 'Operation violates CHECK constraint @1 on view or table @2', + {335544559} 'invalid service handle', + {335544560} 'database @1 shutdown in @2 seconds', + {335544561} 'wrong version of service parameter block', + {335544562} 'unrecognized service parameter block', + {335544563} 'service @1 is not defined', + {335544564} 'long-term journaling not enabled', + {335544565} 'Cannot transliterate character between character sets', + {335544566} 'WAL defined; Cache Manager must be started first', + {335544567} 'Overflow log specification required for round-robin log', + {335544568} 'Implementation of text subtype @1 not located.', + {335544569} 'Dynamic SQL Error', + {335544570} 'Invalid command', + {335544571} 'Data type for constant unknown', + {335544572} 'Invalid cursor reference', + {335544573} 'Data type unknown', + {335544574} 'Invalid cursor declaration', + {335544575} 'Cursor @1 is not updatable', + {335544576} 'Attempt to reopen an open cursor', + {335544577} 'Attempt to reclose a closed cursor', + {335544578} 'Column unknown', + {335544579} 'Internal error', + {335544580} 'Table unknown', + {335544581} 'Procedure unknown', + {335544582} 'Request unknown', + {335544583} 'SQLDA error', + {335544584} 'Count of read-write columns does not equal count of values', + {335544585} 'Invalid statement handle', + {335544586} 'Function unknown', + {335544587} 'Column is not a BLOB', + {335544588} 'COLLATION @1 for CHARACTER SET @2 is not defined', + {335544589} 'COLLATION @1 is not valid for specified CHARACTER SET', + {335544590} 'Option specified more than once', + {335544591} 'Unknown transaction option', + {335544592} 'Invalid array reference', + {335544593} 'Array declared with too many dimensions', + {335544594} 'Illegal array dimension range', + {335544595} 'Trigger unknown', + {335544596} 'Subselect illegal in this context', + {335544597} 'Cannot prepare a CREATE DATABASE statement', + {335544598} 'must specify column name for view select expression', + {335544599} 'number of columns does not match select list', + {335544600} 'Only simple column names permitted for VIEW WITH CHECK OPTION', + {335544601} 'No WHERE clause for VIEW WITH CHECK OPTION', + {335544602} 'Only one table allowed for VIEW WITH CHECK OPTION', + {335544603} 'DISTINCT, GROUP or HAVING not permitted for VIEW WITH CHECK OPTION', + {335544604} 'FOREIGN KEY column count does not match PRIMARY KEY', + {335544605} 'No subqueries permitted for VIEW WITH CHECK OPTION', + {335544606} 'expression evaluation not supported', + {335544607} 'gen.c: node not supported', + {335544608} 'Unexpected end of command', + {335544609} 'INDEX @1', + {335544610} 'EXCEPTION @1', + {335544611} 'COLUMN @1', + {335544612} 'Token unknown', + {335544613} 'union not supported', + {335544614} 'Unsupported DSQL construct', + {335544615} 'column used with aggregate', + {335544616} 'invalid column reference', + {335544617} 'invalid ORDER BY clause', + {335544618} 'Return mode by value not allowed for this data type', + {335544619} 'External functions cannot have more than 10 parameters', + {335544620} 'alias @1 conflicts with an alias in the same statement', + {335544621} 'alias @1 conflicts with a procedure in the same statement', + {335544622} 'alias @1 conflicts with a table in the same statement', + {335544623} 'Illegal use of keyword VALUE', + {335544624} 'segment count of 0 defined for index @1', + {335544625} 'A node name is not permitted in a secondary, shadow, cache or log file name', + {335544626} 'TABLE @1', + {335544627} 'PROCEDURE @1', + {335544628} 'cannot create index @1', + {335544629} 'Write-ahead Log with shadowing configuration not allowed', + {335544630} 'there are @1 dependencies', + {335544631} 'too many keys defined for index @1', + {335544632} 'Preceding file did not specify length, so @1 must include starting page number', + {335544633} 'Shadow number must be a positive integer', + {335544634} 'Token unknown - line @1, column @2', + {335544635} 'there is no alias or table named @1 at this scope level', + {335544636} 'there is no index @1 for table @2', + {335544637} 'table or procedure @1 is not referenced in plan', + {335544638} 'table or procedure @1 is referenced more than once in plan; use aliases to distinguish', + {335544639} 'table or procedure @1 is referenced in the plan but not the from list', + {335544640} 'Invalid use of CHARACTER SET or COLLATE', + {335544641} 'Specified domain or source column @1 does not exist', + {335544642} 'index @1 cannot be used in the specified plan', + {335544643} 'the table @1 is referenced twice; use aliases to differentiate', + {335544644} 'attempt to fetch before the first record in a record stream', + {335544645} 'the current position is on a crack', + {335544646} 'database or file exists', + {335544647} 'invalid comparison operator for find operation', + {335544648} 'Connection lost to pipe server', + {335544649} 'bad checksum', + {335544650} 'wrong page type', + {335544651} 'Cannot insert because the file is readonly or is on a read only medium.', + {335544652} 'multiple rows in singleton select', + {335544653} 'cannot attach to password database', + {335544654} 'cannot start transaction for password database', + {335544655} 'invalid direction for find operation', + {335544656} 'variable @1 conflicts with parameter in same procedure', + {335544657} 'Array/BLOB/DATE data types not allowed in arithmetic', + {335544658} '@1 is not a valid base table of the specified view', + {335544659} 'table or procedure @1 is referenced twice in view; use an alias to distinguish', + {335544660} 'view @1 has more than one base table; use aliases to distinguish', + {335544661} 'cannot add index, index root page is full.', + {335544662} 'BLOB SUB_TYPE @1 is not defined', + {335544663} 'Too many concurrent executions of the same request', + {335544664} 'duplicate specification of @1 - not supported', + {335544665} 'violation of PRIMARY or UNIQUE KEY constraint @1 on table @2', + {335544666} 'server version too old to support all CREATE DATABASE options', + {335544667} 'drop database completed with errors', + {335544668} 'procedure @1 does not return any values', + {335544669} 'count of column list and variable list do not match', + {335544670} 'attempt to index BLOB column in index @1', + {335544671} 'attempt to index array column in index @1', + {335544672} 'too few key columns found for index @1 (incorrect column name?)', + {335544673} 'cannot delete', + {335544674} 'last column in a table cannot be deleted', + {335544675} 'sort error', + {335544676} 'sort error: not enough memory', + {335544677} 'too many versions', + {335544678} 'invalid key position', + {335544679} 'segments not allowed in expression index @1', + {335544680} 'sort error: corruption in data structure', + {335544681} 'new record size of @1 bytes is too big', + {335544682} 'Inappropriate self-reference of column', + {335544683} 'request depth exceeded. (Recursive definition?)', + {335544684} 'cannot access column @1 in view @2', + {335544685} 'dbkey not available for multi-table views', + {335544686} 'journal file wrong format', + {335544687} 'intermediate journal file full', + {335544688} 'The prepare statement identifies a prepare statement with an open cursor', + {335544689} 'Firebird error', + {335544690} 'Cache redefined', + {335544691} 'Insufficient memory to allocate page buffer cache', + {335544692} 'Log redefined', + {335544693} 'Log size too small', + {335544694} 'Log partition size too small', + {335544695} 'Partitions not supported in series of log file specification', + {335544696} 'Total length of a partitioned log must be specified', + {335544697} 'Precision must be from 1 to 18', + {335544698} 'Scale must be between zero and precision', + {335544699} 'Short integer expected', + {335544700} 'Long integer expected', + {335544701} 'Unsigned short integer expected', + {335544702} 'Invalid ESCAPE sequence', + {335544703} 'service @1 does not have an associated executable', + {335544704} 'Failed to locate host machine.', + {335544705} 'Undefined service @1/@2.', + {335544706} 'The specified name was not found in the hosts file or Domain Name Services.', + {335544707} 'user does not have GRANT privileges on base table/view for operation', + {335544708} 'Ambiguous column reference.', + {335544709} 'Invalid aggregate reference', + {335544710} 'navigational stream @1 references a view with more than one base table', + {335544711} 'Attempt to execute an unprepared dynamic SQL statement.', + {335544712} 'Positive value expected', + {335544713} 'Incorrect values within SQLDA structure', + {335544714} 'invalid blob id', + {335544715} 'Operation not supported for EXTERNAL FILE table @1', + {335544716} 'Service is currently busy: @1', + {335544717} 'stack size insufficient to execute current request', + {335544718} 'Invalid key for find operation', + {335544719} 'Error initializing the network software.', + {335544720} 'Unable to load required library @1.', + {335544721} 'Unable to complete network request to host "@1".', + {335544722} 'Failed to establish a connection.', + {335544723} 'Error while listening for an incoming connection.', + {335544724} 'Failed to establish a secondary connection for event processing.', + {335544725} 'Error while listening for an incoming event connection request.', + {335544726} 'Error reading data from the connection.', + {335544727} 'Error writing data to the connection.', + {335544728} 'Cannot deactivate index used by an integrity constraint', + {335544729} 'Cannot deactivate index used by a PRIMARY/UNIQUE constraint', + {335544730} 'Client/Server Express not supported in this release', + {335544731} '', + {335544732} 'Access to databases on file servers is not supported.', + {335544733} 'Error while trying to create file', + {335544734} 'Error while trying to open file', + {335544735} 'Error while trying to close file', + {335544736} 'Error while trying to read from file', + {335544737} 'Error while trying to write to file', + {335544738} 'Error while trying to delete file', + {335544739} 'Error while trying to access file', + {335544740} 'A fatal exception occurred during the execution of a user defined function.', + {335544741} 'connection lost to database', + {335544742} 'User cannot write to RDB$USER_PRIVILEGES', + {335544743} 'token size exceeds limit', + {335544744} 'Maximum user count exceeded. Contact your database administrator.', + {335544745} 'Your login @1 is same as one of the SQL role name. Ask your database administrator to set up a valid Firebird login.', + {335544746} '"REFERENCES table" without "(column)" requires PRIMARY KEY on referenced table', + {335544747} 'The username entered is too long. Maximum length is 31 bytes.', + {335544748} 'The password specified is too long. Maximum length is 8 bytes.', + {335544749} 'A username is required for this operation.', + {335544750} 'A password is required for this operation', + {335544751} 'The network protocol specified is invalid', + {335544752} 'A duplicate user name was found in the security database', + {335544753} 'The user name specified was not found in the security database', + {335544754} 'An error occurred while attempting to add the user.', + {335544755} 'An error occurred while attempting to modify the user record.', + {335544756} 'An error occurred while attempting to delete the user record.', + {335544757} 'An error occurred while updating the security database.', + {335544758} 'sort record size of @1 bytes is too big', + {335544759} 'can not define a not null column with NULL as default value', + {335544760} 'invalid clause --- ''@1''', + {335544761} 'too many open handles to database', + {335544762} 'size of optimizer block exceeded', + {335544763} 'a string constant is delimited by double quotes', + {335544764} 'DATE must be changed to TIMESTAMP', + {335544765} 'attempted update on read-only database', + {335544766} 'SQL dialect @1 is not supported in this database', + {335544767} 'A fatal exception occurred during the execution of a blob filter.', + {335544768} 'Access violation. The code attempted to access a virtual address without privilege to do so.', + {335544769} 'Datatype misalignment. The attempted to read or write a value that was not stored on a memory boundary.', + {335544770} 'Array bounds exceeded. The code attempted to access an array element that is out of bounds.', + {335544771} 'Float denormal operand. One of the floating-point operands is too small to represent a standard float value.', + {335544772} 'Floating-point divide by zero. The code attempted to divide a floating-point value by zero.', + {335544773} 'Floating-point inexact result. The result of a floating-point operation cannot be represented as a decimal fraction.', + {335544774} 'Floating-point invalid operand. An indeterminant error occurred during a floating-point operation.', + {335544775} 'Floating-point overflow. The exponent of a floating-point operation is greater than the magnitude allowed.', + {335544776} 'Floating-point stack check. The stack overflowed or underflowed as the result of a floating-point operation.', + {335544777} 'Floating-point underflow. The exponent of a floating-point operation is less than the magnitude allowed.', + {335544778} 'Integer divide by zero. The code attempted to divide an integer value by an integer divisor of zero.', + {335544779} 'Integer overflow. The result of an integer operation caused the most significant bit of the result to carry.', + {335544780} 'An exception occurred that does not have a description. Exception number @1.', + {335544781} 'Stack overflow. The resource requirements of the runtime stack have exceeded the memory available to it.', + {335544782} 'Segmentation Fault. The code attempted to access memory without privileges.', + {335544783} 'Illegal Instruction. The Code attempted to perform an illegal operation.', + {335544784} 'Bus Error. The Code caused a system bus error.', + {335544785} 'Floating Point Error. The Code caused an Arithmetic Exception or a floating point exception.', + {335544786} 'Cannot delete rows from external files.', + {335544787} 'Cannot update rows in external files.', + {335544788} 'Unable to perform operation', + {335544789} 'Specified EXTRACT part does not exist in input datatype', + {335544790} 'Service @1 requires SYSDBA permissions. Reattach to the Service Manager using the SYSDBA account.', + {335544791} 'The file @1 is currently in use by another process. Try again later.', + {335544792} 'Cannot attach to services manager', + {335544793} 'Metadata update statement is not allowed by the current database SQL dialect @1', + {335544794} 'operation was cancelled', + {335544795} 'unexpected item in service parameter block, expected @1', + {335544796} 'Client SQL dialect @1 does not support reference to @2 datatype', + {335544797} 'user name and password are required while attaching to the services manager', + {335544798} 'You created an indirect dependency on uncommitted metadata. You must roll back the current transaction.', + {335544799} 'The service name was not specified.', + {335544800} 'Too many Contexts of Relation/Procedure/Views. Maximum allowed is 256', + {335544801} 'data type not supported for arithmetic', + {335544802} 'Database dialect being changed from 3 to 1', + {335544803} 'Database dialect not changed.', + {335544804} 'Unable to create database @1', + {335544805} 'Database dialect @1 is not a valid dialect.', + {335544806} 'Valid database dialects are @1.', + {335544807} 'SQL warning code = @1', + {335544808} 'DATE data type is now called TIMESTAMP', + {335544809} 'Function @1 is in @2, which is not in a permitted directory for external functions.', + {335544810} 'value exceeds the range for valid dates', + {335544811} 'passed client dialect @1 is not a valid dialect.', + {335544812} 'Valid client dialects are @1.', + {335544813} 'Unsupported field type specified in BETWEEN predicate.', + {335544814} 'Services functionality will be supported in a later version of the product', + {335544815} 'GENERATOR @1', + {335544816} 'Function @1', + {335544817} 'Invalid parameter to FETCH or FIRST. Only integers >= 0 are allowed.', + {335544818} 'Invalid parameter to OFFSET or SKIP. Only integers >= 0 are allowed.', + {335544819} 'File exceeded maximum size of 2GB. Add another database file or use a 64 bit I/O version of Firebird.', + {335544820} 'Unable to find savepoint with name @1 in transaction context', + {335544821} 'Invalid column position used in the @1 clause', + {335544822} 'Cannot use an aggregate or window function in a WHERE clause, use HAVING (for aggregate only) instead', + {335544823} 'Cannot use an aggregate or window function in a GROUP BY clause', + {335544824} 'Invalid expression in the @1 (not contained in either an aggregate function or the GROUP BY clause)', + {335544825} 'Invalid expression in the @1 (neither an aggregate function nor a part of the GROUP BY clause)', + {335544826} 'Nested aggregate and window functions are not allowed', + {335544827} 'Invalid argument in EXECUTE STATEMENT - cannot convert to string', + {335544828} 'Wrong request type in EXECUTE STATEMENT ''@1''', + {335544829} 'Variable type (position @1) in EXECUTE STATEMENT ''@2'' INTO does not match returned column type', + {335544830} 'Too many recursion levels of EXECUTE STATEMENT', + {335544831} 'Use of @1 at location @2 is not allowed by server configuration', + {335544832} 'Cannot change difference file name while database is in backup mode', + {335544833} 'Physical backup is not allowed while Write-Ahead Log is in use', + {335544834} 'Cursor is not open', + {335544835} 'Target shutdown mode is invalid for database "@1"', + {335544836} 'Concatenation overflow. Resulting string cannot exceed 32765 bytes in length.', + {335544837} 'Invalid offset parameter @1 to SUBSTRING. Only positive integers are allowed.', + {335544838} 'Foreign key reference target does not exist', + {335544839} 'Foreign key references are present for the record', + {335544840} 'cannot update', + {335544841} 'Cursor is already open', + {335544842} '@1', + {335544843} 'Context variable ''@1'' is not found in namespace ''@2''', + {335544844} 'Invalid namespace name ''@1'' passed to @2', + {335544845} 'Too many context variables', + {335544846} 'Invalid argument passed to @1', + {335544847} 'BLR syntax error. Identifier @1... is too long', + {335544848} 'exception @1', + {335544849} 'Malformed string', + {335544850} 'Output parameter mismatch for procedure @1', + {335544851} 'Unexpected end of command - line @1, column @2', + {335544852} 'partner index segment no @1 has incompatible data type', + {335544853} 'Invalid length parameter @1 to SUBSTRING. Negative integers are not allowed.', + {335544854} 'CHARACTER SET @1 is not installed', + {335544855} 'COLLATION @1 for CHARACTER SET @2 is not installed', + {335544856} 'connection shutdown', + {335544857} 'Maximum BLOB size exceeded', + {335544858} 'Can''t have relation with only computed fields or constraints', + {335544859} 'Time precision exceeds allowed range (0-@1)', + {335544860} 'Unsupported conversion to target type BLOB (subtype @1)', + {335544861} 'Unsupported conversion to target type ARRAY', + {335544862} 'Stream does not support record locking', + {335544863} 'Cannot create foreign key constraint @1. Partner index does not exist or is inactive.', + {335544864} 'Transactions count exceeded. Perform backup and restore to make database operable again', + {335544865} 'Column has been unexpectedly deleted', + {335544866} '@1 cannot depend on @2', + {335544867} 'Blob sub_types bigger than 1 (text) are for internal use only', + {335544868} 'Procedure @1 is not selectable (it does not contain a SUSPEND statement)', + {335544869} 'Datatype @1 is not supported for sorting operation', + {335544870} 'COLLATION @1', + {335544871} 'DOMAIN @1', + {335544872} 'domain @1 is not defined', + {335544873} 'Array data type can use up to @1 dimensions', + {335544874} 'A multi database transaction cannot span more than @1 databases', + {335544875} 'Bad debug info format', + {335544876} 'Error while parsing procedure @1''s BLR', + {335544877} 'index key too big', + {335544878} 'concurrent transaction number is @1', + {335544879} 'validation error for variable @1, value "@2"', + {335544880} 'validation error for @1, value "@2"', + {335544881} 'Difference file name should be set explicitly for database on raw device', + {335544882} 'Login name too long (@1 characters, maximum allowed @2)', + {335544883} 'column @1 is not defined in procedure @2', + {335544884} 'Invalid SIMILAR TO pattern', + {335544885} 'Invalid TEB format', + {335544886} 'Found more than one transaction isolation in TPB', + {335544887} 'Table reservation lock type @1 requires table name before in TPB', + {335544888} 'Found more than one @1 specification in TPB', + {335544889} 'Option @1 requires READ COMMITTED isolation in TPB', + {335544890} 'Option @1 is not valid if @2 was used previously in TPB', + {335544891} 'Table name length missing after table reservation @1 in TPB', + {335544892} 'Table name length @1 is too long after table reservation @2 in TPB', + {335544893} 'Table name length @1 without table name after table reservation @2 in TPB', + {335544894} 'Table name length @1 goes beyond the remaining TPB size after table reservation @2', + {335544895} 'Table name length is zero after table reservation @1 in TPB', + {335544896} 'Table or view @1 not defined in system tables after table reservation @2 in TPB', + {335544897} 'Base table or view @1 for view @2 not defined in system tables after table reservation @3 in TPB', + {335544898} 'Option length missing after option @1 in TPB', + {335544899} 'Option length @1 without value after option @2 in TPB', + {335544900} 'Option length @1 goes beyond the remaining TPB size after option @2', + {335544901} 'Option length is zero after table reservation @1 in TPB', + {335544902} 'Option length @1 exceeds the range for option @2 in TPB', + {335544903} 'Option value @1 is invalid for the option @2 in TPB', + {335544904} 'Preserving previous table reservation @1 for table @2, stronger than new @3 in TPB', + {335544905} 'Table reservation @1 for table @2 already specified and is stronger than new @3 in TPB', + {335544906} 'Table reservation reached maximum recursion of @1 when expanding views in TPB', + {335544907} 'Table reservation in TPB cannot be applied to @1 because it''s a virtual table', + {335544908} 'Table reservation in TPB cannot be applied to @1 because it''s a system table', + {335544909} 'Table reservation @1 or @2 in TPB cannot be applied to @3 because it''s a temporary table', + {335544910} 'Cannot set the transaction in read only mode after a table reservation isc_tpb_lock_write in TPB', + {335544911} 'Cannot take a table reservation isc_tpb_lock_write in TPB because the transaction is in read only mode', + {335544912} 'value exceeds the range for a valid time', + {335544913} 'value exceeds the range for valid timestamps', + {335544914} 'string right truncation', + {335544915} 'blob truncation when converting to a string: length limit exceeded', + {335544916} 'numeric value is out of range', + {335544917} 'Firebird shutdown is still in progress after the specified timeout', + {335544918} 'Attachment handle is busy', + {335544919} 'Bad written UDF detected: pointer returned in FREE_IT function was not allocated by ib_util_malloc', + {335544920} 'External Data Source provider ''@1'' not found', + {335544921} 'Execute statement error at @1 :'+#10+'@2Data source : @3', + {335544922} 'Execute statement preprocess SQL error', + {335544923} 'Statement expected', + {335544924} 'Parameter name expected', + {335544925} 'Unclosed comment found near ''@1''', + {335544926} 'Execute statement error at @1 :'+#10+'@2Statement : @3'+#10+'Data source : @4', + {335544927} 'Input parameters mismatch', + {335544928} 'Output parameters mismatch', + {335544929} 'Input parameter ''@1'' have no value set', + {335544930} 'BLR stream length @1 exceeds implementation limit @2', + {335544931} 'Monitoring table space exhausted', + {335544932} 'module name or entrypoint could not be found', + {335544933} 'nothing to cancel', + {335544934} 'ib_util library has not been loaded to deallocate memory returned by FREE_IT function', + {335544935} 'Cannot have circular dependencies with computed fields', + {335544936} 'Security database error', + {335544937} 'Invalid data type in DATE/TIME/TIMESTAMP addition or subtraction in add_datettime()', + {335544938} 'Only a TIME value can be added to a DATE value', + {335544939} 'Only a DATE value can be added to a TIME value', + {335544940} 'TIMESTAMP values can be subtracted only from another TIMESTAMP value', + {335544941} 'Only one operand can be of type TIMESTAMP', + {335544942} 'Only HOUR, MINUTE, SECOND and MILLISECOND can be extracted from TIME values', + {335544943} 'HOUR, MINUTE, SECOND and MILLISECOND cannot be extracted from DATE values', + {335544944} 'Invalid argument for EXTRACT() not being of DATE/TIME/TIMESTAMP type', + {335544945} 'Arguments for @1 must be integral types or NUMERIC/DECIMAL without scale', + {335544946} 'First argument for @1 must be integral type or floating point type', + {335544947} 'Human readable UUID argument for @1 must be of string type', + {335544948} 'Human readable UUID argument for @2 must be of exact length @1', + {335544949} 'Human readable UUID argument for @3 must have "-" at position @2 instead of "@1"', + {335544950} 'Human readable UUID argument for @3 must have hex digit at position @2 instead of "@1"', + {335544951} 'Only HOUR, MINUTE, SECOND and MILLISECOND can be added to TIME values in @1', + {335544952} 'Invalid data type in addition of part to DATE/TIME/TIMESTAMP in @1', + {335544953} 'Invalid part @1 to be added to a DATE/TIME/TIMESTAMP value in @2', + {335544954} 'Expected DATE/TIME/TIMESTAMP type in evlDateAdd() result', + {335544955} 'Expected DATE/TIME/TIMESTAMP type as first and second argument to @1', + {335544956} 'The result of TIME- in @1 cannot be expressed in YEAR, MONTH, DAY or WEEK', + {335544957} 'The result of TIME-TIMESTAMP or TIMESTAMP-TIME in @1 cannot be expressed in HOUR, MINUTE, SECOND or MILLISECOND', + {335544958} 'The result of DATE-TIME or TIME-DATE in @1 cannot be expressed in HOUR, MINUTE, SECOND and MILLISECOND', + {335544959} 'Invalid part @1 to express the difference between two DATE/TIME/TIMESTAMP values in @2', + {335544960} 'Argument for @1 must be positive', + {335544961} 'Base for @1 must be positive', + {335544962} 'Argument #@1 for @2 must be zero or positive', + {335544963} 'Argument #@1 for @2 must be positive', + {335544964} 'Base for @1 cannot be zero if exponent is negative', + {335544965} 'Base for @1 cannot be negative if exponent is not an integral value', + {335544966} 'The numeric scale must be between -128 and 127 in @1', + {335544967} 'Argument for @1 must be zero or positive', + {335544968} 'Binary UUID argument for @1 must be of string type', + {335544969} 'Binary UUID argument for @2 must use @1 bytes', + {335544970} 'Missing required item @1 in service parameter block', + {335544971} '@1 server is shutdown', + {335544972} 'Invalid connection string', + {335544973} 'Unrecognized events block', + {335544974} 'Could not start first worker thread - shutdown server', + {335544975} 'Timeout occurred while waiting for a secondary connection for event processing', + {335544976} 'Argument for @1 must be different than zero', + {335544977} 'Argument for @1 must be in the range [-1, 1]', + {335544978} 'Argument for @1 must be greater or equal than one', + {335544979} 'Argument for @1 must be in the range ]-1, 1[', + {335544980} 'Incorrect parameters provided to internal function @1', + {335544981} 'Floating point overflow in built-in function @1', + {335544982} 'Floating point overflow in result from UDF @1', + {335544983} 'Invalid floating point value returned by UDF @1', + {335544984} 'Shared memory area is probably already created by another engine instance in another Windows session', + {335544985} 'No free space found in temporary directories', + {335544986} 'Explicit transaction control is not allowed', + {335544987} 'Use of TRUSTED switches in spb_command_line is prohibited', + {335544988} 'PACKAGE @1', + {335544989} 'Cannot make field @1 of table @2 NOT NULL because there are NULLs present', + {335544990} 'Feature @1 is not supported anymore', + {335544991} 'VIEW @1', + {335544992} 'Can not access lock files directory @1', + {335544993} 'Fetch option @1 is invalid for a non-scrollable cursor', + {335544994} 'Error while parsing function @1''s BLR', + {335544995} 'Cannot execute function @1 of the unimplemented package @2', + {335544996} 'Cannot execute procedure @1 of the unimplemented package @2', + {335544997} 'External function @1 not returned by the external engine plugin @2', + {335544998} 'External procedure @1 not returned by the external engine plugin @2', + {335544999} 'External trigger @1 not returned by the external engine plugin @2', + {335545000} 'Incompatible plugin version @1 for external engine @2', + {335545001} 'External engine @1 not found', + {335545002} 'Attachment is in use', + {335545003} 'Transaction is in use', + {335545004} 'Error loading plugin @1', + {335545005} 'Loadable module @1 not found', + {335545006} 'Standard plugin entrypoint does not exist in module @1', + {335545007} 'Module @1 exists but can not be loaded', + {335545008} 'Module @1 does not contain plugin @2 type @3', + {335545009} 'Invalid usage of context namespace DDL_TRIGGER', + {335545010} 'Value is NULL but isNull parameter was not informed', + {335545011} 'Type @1 is incompatible with BLOB', + {335545012} 'Invalid date', + {335545013} 'Invalid time', + {335545014} 'Invalid timestamp', + {335545015} 'Invalid index @1 in function @2', + {335545016} '@1', + {335545017} 'Asynchronous call is already running for this attachment', + {335545018} 'Function @1 is private to package @2', + {335545019} 'Procedure @1 is private to package @2', + {335545020} 'Request can''t access new records in relation @1 and should be recompiled', + {335545021} 'invalid events id (handle)', + {335545022} 'Cannot copy statement @1', + {335545023} 'Invalid usage of boolean expression', + {335545024} 'Arguments for @1 cannot both be zero', + {335545025} 'missing service ID in spb', + {335545026} 'External BLR message mismatch: invalid null descriptor at field @1', + {335545027} 'External BLR message mismatch: length = @1, expected @2', + {335545028} 'Subscript @1 out of bounds [@2, @3]', + {335545029} 'Install incomplete. To complete security database initialization please CREATE USER. For details read doc/README.security_database.txt.', + {335545030} '@1 operation is not allowed for system table @2', + {335545031} 'Libtommath error code @1 in function @2', + {335545032} 'unsupported BLR version (expected between @1 and @2, encountered @3)', + {335545033} 'expected length @1, actual @2', + {335545034} 'Wrong info requested in isc_svc_query() for anonymous service', + {335545035} 'No isc_info_svc_stdin in user request, but service thread requested stdin data', + {335545036} 'Start request for anonymous service is impossible', + {335545037} 'All services except for getting server log require switches', + {335545038} 'Size of stdin data is more than was requested from client', + {335545039} 'Crypt plugin @1 failed to load', + {335545040} 'Length of crypt plugin name should not exceed @1 bytes', + {335545041} 'Crypt failed - already crypting database', + {335545042} 'Crypt failed - database is already in requested state', + {335545043} 'Missing crypt plugin, but page appears encrypted', + {335545044} 'No providers loaded', + {335545045} 'NULL data with non-zero SPB length', + {335545046} 'Maximum (@1) number of arguments exceeded for function @2', + {335545047} 'External BLR message mismatch: names count = @1, blr count = @2', + {335545048} 'External BLR message mismatch: name @1 not found', + {335545049} 'Invalid resultset interface', + {335545050} 'Message length passed from user application does not match set of columns', + {335545051} 'Resultset is missing output format information', + {335545052} 'Message metadata not ready - item @1 is not finished', + {335545053} 'Missing configuration file: @1', + {335545054} '@1: illegal line <@2>', + {335545055} 'Invalid include operator in @1 for <@2>', + {335545056} 'Include depth too big', + {335545057} 'File to include not found', + {335545058} 'Only the owner can change the ownership', + {335545059} 'undefined variable number', + {335545060} 'Missing security context for @1', + {335545061} 'Missing segment @1 in multisegment connect block parameter', + {335545062} 'Different logins in connect and attach packets - client library error', + {335545063} 'Exceeded exchange limit during authentication handshake', + {335545064} 'Incompatible wire encryption levels requested on client and server', + {335545065} 'Client attempted to attach unencrypted but wire encryption is required', + {335545066} 'Client attempted to start wire encryption using unknown key @1', + {335545067} 'Client attempted to start wire encryption using unsupported plugin @1', + {335545068} 'Error getting security database name from configuration file', + {335545069} 'Client authentication plugin is missing required data from server', + {335545070} 'Client authentication plugin expected @2 bytes of @3 from server, got @1', + {335545071} 'Attempt to get information about an unprepared dynamic SQL statement.', + {335545072} 'Problematic key value is @1', + {335545073} 'Cannot select virtual table @1 for update WITH LOCK', + {335545074} 'Cannot select system table @1 for update WITH LOCK', + {335545075} 'Cannot select temporary table @1 for update WITH LOCK', + {335545076} 'System @1 @2 cannot be modified', + {335545077} 'Server misconfigured - contact administrator please', + {335545078} 'Deprecated backward compatibility ALTER ROLE ... SET/DROP AUTO ADMIN mapping may be used only for RDB$ADMIN role', + {335545079} 'Mapping @1 already exists', + {335545080} 'Mapping @1 does not exist', + {335545081} '@1 failed when loading mapping cache', + {335545082} 'Invalid name <*> in authentication block', + {335545083} 'Multiple maps found for @1', + {335545084} 'Undefined mapping result - more than one different results found', + {335545085} 'Incompatible mode of attachment to damaged database', + {335545086} 'Attempt to set in database number of buffers which is out of acceptable range [@1:@2]', + {335545087} 'Attempt to temporarily set number of buffers less than @1', + {335545088} 'Global mapping is not available when database @1 is not present', + {335545089} 'Global mapping is not available when table RDB$MAP is not present in database @1', + {335545090} 'Your attachment has no trusted role', + {335545091} 'Role @1 is invalid or unavailable', + {335545092} 'Cursor @1 is not positioned in a valid record', + {335545093} 'Duplicated user attribute @1', + {335545094} 'There is no privilege for this operation', + {335545095} 'Using GRANT OPTION on @1 not allowed', + {335545096} 'read conflicts with concurrent update', + {335545097} '@1 failed when working with CREATE DATABASE grants', + {335545098} 'CREATE DATABASE grants check is not possible when database @1 is not present', + {335545099} 'CREATE DATABASE grants check is not possible when table RDB$DB_CREATORS is not present in database @1', + {335545100} 'Interface @3 version too old: expected @1, found @2', + {335545101} 'Parameter mismatch for function @1', + {335545102} 'Error during savepoint backout - transaction invalidated', + {335545103} 'Domain used in the PRIMARY KEY constraint of table @1 must be NOT NULL', + {335545104} 'CHARACTER SET @1 cannot be used as a attachment character set', + {335545105} 'Some database(s) were shutdown when trying to read mapping data', + {335545106} 'Error occurred during login, please check server firebird.log for details', + {335545107} 'Database already opened with engine instance, incompatible with current', + {335545108} 'Invalid crypt key @1', + {335545109} 'Page requires encryption but crypt plugin is missing', + {335545110} 'Maximum index depth (@1 levels) is reached', + {335545111} 'System privilege @1 does not exist', + {335545112} 'System privilege @1 is missing', + {335545113} 'Invalid or missing checksum of encrypted database', + {335545114} 'You must have SYSDBA rights at this server', + {335545115} 'Cannot open cursor for non-SELECT statement', + {335545116} 'If specifies @1, then shall not specify @2', + {335545117} 'RANGE based window with {PRECEDING | FOLLOWING} cannot have ORDER BY with more than one value', + {335545118} 'RANGE based window with PRECEDING/FOLLOWING must have a single ORDER BY key of numerical, date, time or timestamp types', + {335545119} 'Window RANGE/ROWS/GROUPS PRECEDING/FOLLOWING value must be of a numerical type', + {335545120} 'Invalid PRECEDING or FOLLOWING offset in window function: cannot be negative', + {335545121} 'Window @1 not found', + {335545122} 'Cannot use PARTITION BY clause while overriding the window @1', + {335545123} 'Cannot use ORDER BY clause while overriding the window @1 which already has an ORDER BY clause', + {335545124} 'Cannot override the window @1 because it has a frame clause. Tip: it can be used without parenthesis in OVER', + {335545125} 'Duplicate window definition for @1', + {335545126} 'SQL statement is too long. Maximum size is @1 bytes.', + {335545127} 'Config level timeout expired.', + {335545128} 'Attachment level timeout expired.', + {335545129} 'Statement level timeout expired.', + {335545130} 'Killed by database administrator.', + {335545131} 'Idle timeout expired.', + {335545132} 'Database is shutdown.', + {335545133} 'Engine is shutdown.', + {335545134} 'OVERRIDING clause can be used only when an identity column is present in the INSERT''s field list for table/view @1', + {335545135} 'OVERRIDING SYSTEM VALUE can be used only for identity column defined as ''GENERATED ALWAYS'' in INSERT for table/view @1', + {335545136} 'OVERRIDING USER VALUE can be used only for identity column defined as ''GENERATED BY DEFAULT'' in INSERT for table/view @1', + {335545137} 'OVERRIDING clause should be used when an identity column defined as ''GENERATED ALWAYS'' is present in the INSERT''s field list for table table/view @1', + {335545138} 'DecFloat precision must be 16 or 34', + {335545139} 'Decimal float divide by zero. The code attempted to divide a DECFLOAT value by zero.', + {335545140} 'Decimal float inexact result. The result of an operation cannot be represented as a decimal fraction.', + {335545141} 'Decimal float invalid operation. An indeterminant error occurred during an operation.', + {335545142} 'Decimal float overflow. The exponent of a result is greater than the magnitude allowed.', + {335545143} 'Decimal float underflow. The exponent of a result is less than the magnitude allowed.', + {335545144} 'Sub-function @1 has not been defined', + {335545145} 'Sub-procedure @1 has not been defined', + {335545146} 'Sub-function @1 has a signature mismatch with its forward declaration', + {335545147} 'Sub-procedure @1 has a signature mismatch with its forward declaration', + {335545148} 'Default values for parameters are not allowed in definition of the previously declared sub-function @1', + {335545149} 'Default values for parameters are not allowed in definition of the previously declared sub-procedure @1', + {335545150} 'Sub-function @1 was declared but not implemented', + {335545151} 'Sub-procedure @1 was declared but not implemented', + {335545152} 'Invalid HASH algorithm @1', + {335545153} 'Expression evaluation error for index @1 on table @2', + {335545154} 'Invalid decfloat trap state @1', + {335545155} 'Invalid decfloat rounding mode @1', + {335545156} 'Invalid part @1 to calculate the @1 of a DATE/TIMESTAMP', + {335545157} 'Expected DATE/TIMESTAMP value in @1', + {335545158} 'Precision must be from @1 to @2', + {335545159} 'invalid batch handle', + {335545160} 'Bad international character in tag @1', + {335545161} 'Null data in parameters block with non-zero length', + {335545162} 'Items working with running service and getting generic server information should not be mixed in single info block', + {335545163} 'Unknown information item, code @1', + {335545164} 'Wrong version of blob parameters block @1, should be @2', + {335545165} 'User management plugin is missing or failed to load', + {335545166} 'Missing entrypoint @1 in ICU library', + {335545167} 'Could not find acceptable ICU library', + {335545168} 'Name @1 not found in system MetadataBuilder', + {335545169} 'Parse to tokens error', + {335545170} 'Error opening international conversion descriptor from @1 to @2', + {335545171} 'Message @1 is out of range, only @2 messages in batch', + {335545172} 'Detailed error info for message @1 is missing in batch', + {335545173} 'Compression stream init error @1', + {335545174} 'Decompression stream init error @1', + {335545175} 'Segment size (@1) should not exceed 65535 (64K - 1) when using segmented blob', + {335545176} 'Invalid blob policy in the batch for @1() call', + {335545177} 'Can''t change default BPB after adding any data to batch', + {335545178} 'Unexpected info buffer structure querying for server batch parameters', + {335545179} 'Duplicated segment @1 in multisegment connect block parameter', + {335545180} 'Plugin not supported by network protocol', + {335545181} 'Error parsing message format', + {335545182} 'Wrong version of batch parameters block @1, should be @2', + {335545183} 'Message size (@1) in batch exceeds internal buffer size (@2)', + {335545184} 'Batch already opened for this statement', + {335545185} 'Invalid type of statement used in batch', + {335545186} 'Statement used in batch must have parameters', + {335545187} 'There are no blobs in associated with batch statement', + {335545188} 'appendBlobData() is used to append data to last blob but no such blob was added to the batch', + {335545189} 'Portions of data, passed as blob stream, should have size multiple to the alignment required for blobs', + {335545190} 'Repeated blob id @1 in registerBlob()', + {335545191} 'Blob buffer format error', + {335545192} 'Unusable (too small) data remained in @1 buffer', + {335545193} 'Blob continuation should not contain BPB', + {335545194} 'Size of BPB (@1) greater than remaining data (@2)', + {335545195} 'Size of segment (@1) greater than current BLOB data (@2)', + {335545196} 'Size of segment (@1) greater than available data (@2)', + {335545197} 'Unknown blob ID @1 in the batch message', + {335545198} 'Internal buffer overflow - batch too big', + {335545199} 'Numeric literal too long', + {335545200} 'Error using events in mapping shared memory: @1', + {335545201} 'Global mapping memory overflow', + {335545202} 'Header page overflow - too many clumplets on it', + {335545203} 'No matching client/server authentication plugins configured for execute statement in embedded datasource', + {335545204} 'Missing database encryption key for your attachment', + {335545205} 'Key holder plugin @1 failed to load', + {335545206} 'Cannot reset user session', + {335545207} 'There are open transactions (@1 active)', + {335545208} 'Session was reset with warning(s)', + {335545209} 'Transaction is rolled back due to session reset, all changes are lost', + {335545210} 'Plugin @1:', + {335545211} 'PARAMETER @1', + {335545212} 'Starting page number for file @1 must be @2 or greater', + {335545213} 'Invalid time zone offset: @1 - must use format +/-hours:minutes and be between -14:00 and +14:00', + {335545214} 'Invalid time zone region: @1', + {335545215} 'Invalid time zone ID: @1', + {335545216} 'Wrong base64 text length @1, should be multiple of 4', + {335545217} 'Invalid first parameter datatype - need string or blob', + {335545218} 'Error registering @1 - probably bad tomcrypt library', + {335545219} 'Unknown crypt algorithm @1 in USING clause', + {335545220} 'Should specify mode parameter for symmetric cipher', + {335545221} 'Unknown symmetric crypt mode specified', + {335545222} 'Mode parameter makes no sense for chosen cipher', + {335545223} 'Should specify initialization vector (IV) for chosen cipher and/or mode', + {335545224} 'Initialization vector (IV) makes no sense for chosen cipher and/or mode', + {335545225} 'Invalid counter endianess @1', + {335545226} 'Counter endianess parameter is not used in mode @1', + {335545227} 'Too big counter value @1, maximum @2 can be used', + {335545228} 'Counter length/value parameter is not used with @1 @2', + {335545229} 'Invalid initialization vector (IV) length @1, need @2', + {335545230} 'TomCrypt library error: @1', + {335545231} 'Starting PRNG yarrow', + {335545232} 'Setting up PRNG yarrow', + {335545233} 'Initializing @1 mode', + {335545234} 'Encrypting in @1 mode', + {335545235} 'Decrypting in @1 mode', + {335545236} 'Initializing cipher @1', + {335545237} 'Encrypting using cipher @1', + {335545238} 'Decrypting using cipher @1', + {335545239} 'Setting initialization vector (IV) for @1', + {335545240} 'Invalid initialization vector (IV) length @1, need 8 or 12', + {335545241} 'Encoding @1', + {335545242} 'Decoding @1', + {335545243} 'Importing RSA key', + {335545244} 'Invalid OAEP packet', + {335545245} 'Unknown hash algorithm @1', + {335545246} 'Making RSA key', + {335545247} 'Exporting @1 RSA key', + {335545248} 'RSA-signing data', + {335545249} 'Verifying RSA-signed data', + {335545250} 'Invalid key length @1, need 16 or 32', + {335545251} 'invalid replicator handle', + {335545252} 'Transaction''s base snapshot number does not exist', + {335545253} 'Input parameter ''@1'' is not used in SQL query text', + {335545254} 'Effective user is @1', + {335545255} 'Invalid time zone bind mode @1', + {335545256} 'Invalid decfloat bind mode @1', + {335545257} 'Invalid hex text length @1, should be multiple of 2', + {335545258} 'Invalid hex digit @1 at position @2', + {335545259} 'Error processing isc_dpb_set_bind clumplet "@1"', + {335545260} 'The following statement failed: @1', + {335545261} 'Can not convert @1 to @2', + {335545262} 'cannot update old BLOB', + {335545263} 'cannot read from new BLOB', + {335545264} 'No permission for CREATE @1 operation', + {335545265} 'SUSPEND could not be used without RETURNS clause in PROCEDURE or EXECUTE BLOCK', + {335545266} 'String truncated warning due to the following reason', + {335545267} 'Monitoring data does not fit into the field', + {335545268} 'Engine data does not fit into return value of system function', + {335545269} 'Multiple source records cannot match the same target during MERGE', + {335545270} 'RDB$PAGES written by non-system transaction, DB appears to be damaged', + {335545271} 'Replication error', + {335545272} 'Reset of user session failed. Connection is shut down.', + {335545273} 'File size is less than expected', + {335545274} 'Invalid key length @1, need >@2', + {335545275} 'Invalid information arguments', + {335545276} 'Empty or NULL parameter @1 is not accepted', + {335545277} 'Undefined local table number @1', + {335545278} 'Invalid text <@1> after quoted string', + {335545279} 'Missing terminating quote <@1> in the end of quoted string', + {335545280} '@1: inconsistent shared memory type/version; found @2, expected @3', + {335545281} '@1-bit engine can''t open database already opened by @2-bit engine', + {335545282} 'Procedures cannot specify access type other than NATURAL in the plan', + {335545283} 'Invalid RDB$BLOB_UTIL handle', + {335545284} 'Invalid temporary BLOB ID', + {335545285} 'ODS upgrade failed while adding new system %s', + {335545286} 'Wrong parallel workers value @1, valid range are from 1 to @2', + {335545287} 'Definition of index expression is not found for index @1', + {335545288} 'Definition of index condition is not found for index @1', + {335545289} 'Variable @1 is not initialized', + {335545290} 'Parameter @1 does not exist', + {335545291} 'Parameter @1 has no default value and was not specified or was specified with DEFAULT', + {335545292} 'Parameter @1 has multiple assignments', + {335545293} 'Cannot recognize "@1" part of date format', + {335545294} 'Cannot find closing " for raw text in date format', + {335545295} 'It is not possible to use this data type for date formatting', + {335545296} 'Cannot use "@1" format with current date type', + {335545297} 'Value for @1 pattern is out of range [@2, @3]', + {335545298} '@1 is not MONTH', + {335545299} '@1 is incorrect period for 12H, it should be A.M. or P.M.', + {335545300} 'All data has been read, but format pattern wants more. Unfilled patterns: "@1"', + {335545301} 'There is a trailing part of input string that does not fit into FORMAT: "@1"', + {335545302} '@1 can''t be used without @2', + {335545303} '@1 can''t be used without @2 and vice versa', + {335545304} '@1 incompatible with @2', + {335545305} 'Can use only one of these patterns @1', + {335545306} 'Cannot use the same pattern twice: @1', + {335545307} 'Invalid GEN_UUID version (@1). Must be 4 or 7', + {335545308} 'Unable to run sweep', + {335545309} 'Another instance of sweep is already running', + {335545310} 'Database in read only state', + {335545311} 'Attachment has no cleanup flag set', + {335545312} 'Invalid time zone region or displacement: @1', + {335545313} 'Arguments for range-based FOR must be exact numeric types', + {335545314} 'Range-based FOR BY argument must be positive', + {335545315} 'Cannot find value in input string for "@1" pattern', + {335545316} 'Invalid name: @1', + {335545317} 'Invalid list of unqualified names: @1', + {335545318} 'User attachments are not allowed for the database being restored', + {335545319} 'Argument STEP must be different than zero for function @1', + {335545320} 'Arguments for @1 function must be exact numeric types', + {335545321} 'Argument for @1 must be in the range [0, 1]', + {335545322} 'Argument for @1 function must be numeric types', + {335545323} 'The PERCENTILE_DISC and PERENTILE_CONT functions support only one sort item in WITHIN GROUP', + {335545324} 'Argument for @1 function must be constant within each group', + {335545325} 'UPDATE will overwrite changes made by the trigger or by the another UPDATE in the same cursor', + {335545326} 'Plugin @1 can not be loaded - name should not contain directory separator and path component', + {335545327} 'The constant @1 is private', + {335545328} 'Use an alias to resolve the conflict', + {335545329} 'Error while parsing BLR value of the constant @1', + {335545330} 'Error while reading type of the constant with name @1', + {335545331} 'Constant @1 cannot be found', + {335545332} '@1 is not supported to be a constant type', + {335545333} 'The constant @1 is not defined', + {335545334} 'CONSTANT @1', + {335545335} 'Table @1 is private to package @2', + {335545336} 'Invalid position to read/write in a temporary file (positon: @1, size: @2)', + {335545337} 'Aggregate function @1 cannot be used in non-aggregate context', + {335545338} 'Aggregate function input parameters may be referenced only in ON ACCUMULATE DO', + {335545339} 'EXIT is not allowed in ON GROUP DO section of aggregate function', + {335545340} 'RETURN is not allowed in ON START DO, ON ACCUMULATE DO or ON FINISH DO sections of aggregate function; use EXIT instead', + {335545341} 'Number of arguments of hypothetical-set aggregate function @1 must match number of sort items in WITHIN GROUP clause', + {335545342} 'Statement format outdated, need to be reprepared', + {336003074} 'Cannot SELECT RDB$DB_KEY from a stored procedure.', + {336003075} 'Precision 10 to 18 changed from DOUBLE PRECISION in SQL dialect 1 to 64-bit scaled integer in SQL dialect 3', + {336003076} 'Use of @1 expression that returns different results in dialect 1 and dialect 3', + {336003077} 'Database SQL dialect @1 does not support reference to @2 datatype', + {336003078} '', + {336003079} 'DB dialect @1 and client dialect @2 conflict with respect to numeric precision @3.', + {336003080} 'WARNING: Numeric literal @1 is interpreted as a floating-point', + {336003081} 'value in SQL dialect 1, but as an exact numeric value in SQL dialect 3.', + {336003082} 'WARNING: NUMERIC and DECIMAL fields with precision 10 or greater are stored', + {336003083} 'as approximate floating-point values in SQL dialect 1, but as 64-bit', + {336003084} 'integers in SQL dialect 3.', + {336003085} 'Ambiguous field name between @1 and @2', + {336003086} 'External function should have return position between 1 and @1', + {336003087} 'Label @1 @2 in the current scope', + {336003088} 'Datatypes @1are not comparable in expression @2', + {336003089} 'Empty cursor name is not allowed', + {336003090} 'Statement already has a cursor @1 assigned', + {336003091} 'Cursor @1 is not found in the current context', + {336003092} 'Cursor @1 already exists in the current context', + {336003093} 'Relation @1 is ambiguous in cursor @2', + {336003094} 'Relation @1 is not found in cursor @2', + {336003095} 'Cursor is not open', + {336003096} 'Data type @1 is not supported for EXTERNAL TABLES. Relation @2, field @3', + {336003097} 'Feature not supported on ODS version older than @1.@2', + {336003098} 'Primary key required on table @1', + {336003099} 'UPDATE OR INSERT field list does not match primary key of table @1', + {336003100} 'UPDATE OR INSERT field list does not match MATCHING clause', + {336003101} 'UPDATE OR INSERT without MATCHING could not be used with views based on more than one table', + {336003102} 'Incompatible trigger type', + {336003103} 'Database trigger type can''t be changed', + {336003104} 'To be used with RDB$RECORD_VERSION, @1 must be a table or a view of single table', + {336003105} 'SQLDA version expected between @1 and @2, found @3', + {336003106} 'at SQLVAR index @1', + {336003107} 'empty pointer to NULL indicator variable', + {336003108} 'empty pointer to data', + {336003109} 'No SQLDA for input values provided', + {336003110} 'No SQLDA for output values provided', + {336003111} 'Wrong number of parameters (expected @1, got @2)', + {336003112} 'Invalid DROP SQL SECURITY clause', + {336003113} 'UPDATE OR INSERT value for field @1, part of the implicit or explicit MATCHING clause, cannot be DEFAULT', + {336003114} 'LOCAL TEMPORARY TABLE @1 cannot be referenced in @2', + {336003115} 'USING statement must contain at least one clause', + {336068609} 'ODS version not supported by DYN', + {336068610} 'unsupported DYN verb', + {336068611} 'STORE RDB$FIELD_DIMENSIONS failed', + {336068612} 'unsupported DYN verb', + {336068613} '@1', + {336068614} 'unsupported DYN verb', + {336068615} 'DEFINE BLOB FILTER failed', + {336068616} 'DEFINE GENERATOR failed', + {336068617} 'DEFINE GENERATOR unexpected DYN verb', + {336068618} 'DEFINE FUNCTION failed', + {336068619} 'unsupported DYN verb', + {336068620} 'DEFINE FUNCTION ARGUMENT failed', + {336068621} 'STORE RDB$FIELDS failed', + {336068622} 'No table specified for index', + {336068623} 'STORE RDB$INDEX_SEGMENTS failed', + {336068624} 'unsupported DYN verb', + {336068625} 'PRIMARY KEY column lookup failed', + {336068626} 'could not find UNIQUE or PRIMARY KEY constraint in table @1 with specified columns', + {336068627} 'PRIMARY KEY lookup failed', + {336068628} 'could not find PRIMARY KEY index in specified table @1', + {336068629} 'STORE RDB$INDICES failed', + {336068630} 'STORE RDB$FIELDS failed', + {336068631} 'STORE RDB$RELATION_FIELDS failed', + {336068632} 'STORE RDB$RELATIONS failed', + {336068633} 'STORE RDB$USER_PRIVILEGES failed defining a table', + {336068634} 'unsupported DYN verb', + {336068635} 'STORE RDB$RELATIONS failed', + {336068636} 'STORE RDB$FIELDS failed', + {336068637} 'STORE RDB$RELATION_FIELDS failed', + {336068638} 'unsupported DYN verb', + {336068639} 'DEFINE TRIGGER failed', + {336068640} 'unsupported DYN verb', + {336068641} 'DEFINE TRIGGER MESSAGE failed', + {336068642} 'STORE RDB$VIEW_RELATIONS failed', + {336068643} 'ERASE RDB$FIELDS failed', + {336068644} 'ERASE BLOB FILTER failed', + {336068645} 'BLOB Filter @1 not found', + {336068646} 'unsupported DYN verb', + {336068647} 'ERASE RDB$FUNCTION_ARGUMENTS failed', + {336068648} 'ERASE RDB$FUNCTIONS failed', + {336068649} 'Function @1 not found', + {336068650} 'unsupported DYN verb', + {336068651} 'Domain @1 is used in table @2 (local name @3) and cannot be dropped', + {336068652} 'ERASE RDB$FIELDS failed', + {336068653} 'ERASE RDB$FIELDS failed', + {336068654} 'Column not found', + {336068655} 'ERASE RDB$INDICES failed', + {336068656} 'Index not found', + {336068657} 'ERASE RDB$INDEX_SEGMENTS failed', + {336068658} 'No segments found for index', + {336068659} 'No table specified in ERASE RFR', + {336068660} 'Column @1 from table @2 is referenced in view @3', + {336068661} 'ERASE RDB$RELATION_FIELDS failed', + {336068662} 'View @1 not found', + {336068663} 'Column not found for table', + {336068664} 'ERASE RDB$INDEX_SEGMENTS failed', + {336068665} 'ERASE RDB$INDICES failed', + {336068666} 'ERASE RDB$RELATION_FIELDS failed', + {336068667} 'ERASE RDB$VIEW_RELATIONS failed', + {336068668} 'ERASE RDB$RELATIONS failed', + {336068669} 'Table not found', + {336068670} 'ERASE RDB$USER_PRIVILEGES failed', + {336068671} 'ERASE RDB$FILES failed', + {336068672} 'unsupported DYN verb', + {336068673} 'ERASE RDB$TRIGGER_MESSAGES failed', + {336068674} 'ERASE RDB$TRIGGERS failed', + {336068675} 'Trigger not found', + {336068676} 'MODIFY RDB$VIEW_RELATIONS failed', + {336068677} 'unsupported DYN verb', + {336068678} 'TRIGGER NAME expected', + {336068679} 'ERASE TRIGGER MESSAGE failed', + {336068680} 'Trigger Message not found', + {336068681} 'unsupported DYN verb', + {336068682} 'ERASE RDB$SECURITY_CLASSES failed', + {336068683} 'Security class not found', + {336068684} 'unsupported DYN verb', + {336068685} 'SELECT RDB$USER_PRIVILEGES failed in grant', + {336068686} 'SELECT RDB$USER_PRIVILEGES failed in grant', + {336068687} 'STORE RDB$USER_PRIVILEGES failed in grant', + {336068688} 'Specified domain or source column does not exist', + {336068689} 'Generation of column name failed', + {336068690} 'Generation of index name failed', + {336068691} 'Generation of trigger name failed', + {336068692} 'MODIFY DATABASE failed', + {336068693} 'MODIFY RDB$CHARACTER_SETS failed', + {336068694} 'MODIFY RDB$COLLATIONS failed', + {336068695} 'MODIFY RDB$FIELDS failed', + {336068696} 'MODIFY RDB$BLOB_FILTERS failed', + {336068697} 'Domain not found', + {336068698} 'unsupported DYN verb', + {336068699} 'MODIFY RDB$INDICES failed', + {336068700} 'MODIFY RDB$FUNCTIONS failed', + {336068701} 'Index column not found', + {336068702} 'MODIFY RDB$GENERATORS failed', + {336068703} 'MODIFY RDB$RELATION_FIELDS failed', + {336068704} 'Local column @1 not found', + {336068705} 'add EXTERNAL FILE not allowed', + {336068706} 'drop EXTERNAL FILE not allowed', + {336068707} 'MODIFY RDB$RELATIONS failed', + {336068708} 'MODIFY RDB$PROCEDURE_PARAMETERS failed', + {336068709} 'Table column not found', + {336068710} 'MODIFY TRIGGER failed', + {336068711} 'TRIGGER NAME expected', + {336068712} 'unsupported DYN verb', + {336068713} 'MODIFY TRIGGER MESSAGE failed', + {336068714} 'Create metadata BLOB failed', + {336068715} 'Write metadata BLOB failed', + {336068716} 'Close metadata BLOB failed', + {336068717} 'Triggers created automatically cannot be modified', + {336068718} 'unsupported DYN verb', + {336068719} 'ERASE RDB$USER_PRIVILEGES failed in revoke(1)', + {336068720} 'Access to RDB$USER_PRIVILEGES failed in revoke(2)', + {336068721} 'ERASE RDB$USER_PRIVILEGES failed in revoke (3)', + {336068722} 'Access to RDB$USER_PRIVILEGES failed in revoke (4)', + {336068723} 'CREATE VIEW failed', + {336068724} ' attempt to index BLOB column in INDEX @1', + {336068725} ' attempt to index array column in index @1', + {336068726} 'key size too big for index @1', + {336068727} 'no keys for index @1', + {336068728} 'Unknown columns in index @1', + {336068729} 'STORE RDB$RELATION_CONSTRAINTS failed', + {336068730} 'STORE RDB$CHECK_CONSTRAINTS failed', + {336068731} 'Column: @1 not defined as NOT NULL - cannot be used in PRIMARY KEY constraint definition', + {336068732} 'A column name is repeated in the definition of constraint: @1', + {336068733} 'Integrity Constraint lookup failed', + {336068734} 'Same set of columns cannot be used in more than one PRIMARY KEY and/or UNIQUE constraint definition', + {336068735} 'STORE RDB$REF_CONSTRAINTS failed', + {336068736} 'No table specified in delete_constraint', + {336068737} 'ERASE RDB$RELATION_CONSTRAINTS failed', + {336068738} 'CONSTRAINT @1 does not exist.', + {336068739} 'Generation of constraint name failed', + {336068740} 'Table @1 already exists', + {336068741} 'Number of referencing columns do not equal number of referenced columns', + {336068742} 'STORE RDB$PROCEDURES failed', + {336068743} 'Procedure @1 already exists', + {336068744} 'STORE RDB$PROCEDURE_PARAMETERS failed', + {336068745} 'Store into system table @1 failed', + {336068746} 'ERASE RDB$PROCEDURE_PARAMETERS failed', + {336068747} 'ERASE RDB$PROCEDURES failed', + {336068748} 'Procedure @1 not found', + {336068749} 'MODIFY RDB$PROCEDURES failed', + {336068750} 'DEFINE EXCEPTION failed', + {336068751} 'ERASE EXCEPTION failed', + {336068752} 'Exception not found', + {336068753} 'MODIFY EXCEPTION failed', + {336068754} 'Parameter @1 in procedure @2 not found', + {336068755} 'Trigger @1 not found', + {336068756} 'Only one data type change to the domain @1 allowed at a time', + {336068757} 'Only one data type change to the field @1 allowed at a time', + {336068758} 'STORE RDB$FILES failed', + {336068759} 'Character set @1 not found', + {336068760} 'Collation @1 not found', + {336068761} 'ERASE RDB$LOG_FILES failed', + {336068762} 'STORE RDB$LOG_FILES failed', + {336068763} 'Role @1 not found', + {336068764} 'Difference file lookup failed', + {336068765} 'DEFINE SHADOW failed', + {336068766} 'MODIFY RDB$ROLES failed', + {336068767} 'Name longer than database column size', + {336068768} '"Only one constraint allowed for a domain"', + {336068770} 'Looking up column position failed', + {336068771} 'A node name is not permitted in a table with external file definition', + {336068772} 'Shadow lookup failed', + {336068773} 'Shadow @1 already exists', + {336068774} 'Cannot add file with the same name as the database or added files', + {336068775} 'no grant option for privilege @1 on column @2 of table/view @3', + {336068776} 'no grant option for privilege @1 on column @2 of base table/view @3', + {336068777} 'no grant option for privilege @1 on table/view @2 (for column @3)', + {336068778} 'no grant option for privilege @1 on base table/view @2 (for column @3)', + {336068779} 'no @1 privilege with grant option on table/view @2 (for column @3)', + {336068780} 'no @1 privilege with grant option on base table/view @2 (for column @3)', + {336068781} 'no grant option for privilege @1 on table/view @2', + {336068782} 'no @1 privilege with grant option on table/view @2', + {336068783} 'table/view @1 does not exist', + {336068784} 'column @1 does not exist in table/view @2', + {336068785} 'Can not alter a view', + {336068786} 'EXTERNAL FILE table not supported in this context', + {336068787} 'attempt to index COMPUTED BY column in INDEX @1', + {336068788} 'Table Name lookup failed', + {336068789} 'attempt to index a view', + {336068790} 'SELECT RDB$RELATIONS failed in grant', + {336068791} 'SELECT RDB$RELATION_FIELDS failed in grant', + {336068792} 'SELECT RDB$RELATIONS/RDB$OWNER_NAME failed in grant', + {336068793} 'SELECT RDB$USER_PRIVILEGES failed in grant', + {336068794} 'SELECT RDB$VIEW_RELATIONS/RDB$RELATION_FIELDS/... failed in grant', + {336068795} 'column @1 from table @2 is referenced in index @3', + {336068796} 'SQL role @1 does not exist', + {336068797} 'user @1 has no grant admin option on SQL role @2', + {336068798} 'user @1 is not a member of SQL role @2', + {336068799} '@1 is not the owner of SQL role @2', + {336068800} '@1 is a SQL role and not a user', + {336068801} 'user name @1 could not be used for SQL role', + {336068802} 'SQL role @1 already exists', + {336068803} 'keyword @1 can not be used as a SQL role name', + {336068804} 'SQL roles are not supported in on older versions of the database. A backup and restore of the database is required.', + {336068812} 'Cannot rename domain @1 to @2. A domain with that name already exists.', + {336068813} 'Cannot rename column @1 to @2. A column with that name already exists in table @3.', + {336068814} 'Column @1 from table @2 is referenced in @3', + {336068815} 'Cannot change datatype for column @1. Changing datatype is not supported for BLOB or ARRAY columns.', + {336068816} 'New size specified for column @1 must be at least @2 characters.', + {336068817} 'Cannot change datatype for @1. Conversion from base type @2 to @3 is not supported.', + {336068818} 'Cannot change datatype for column @1 from a character type to a non-character type.', + {336068819} 'unable to allocate memory from the operating system', + {336068820} 'Zero length identifiers are not allowed', + {336068821} 'ERASE RDB$GENERATORS failed', + {336068822} 'Sequence @1 not found', + {336068823} 'Difference file is not defined', + {336068824} 'Difference file is already defined', + {336068825} 'Database is already in the physical backup mode', + {336068826} 'Database is not in the physical backup mode', + {336068827} 'DEFINE COLLATION failed', + {336068828} 'CREATE COLLATION statement is not supported in older versions of the database. A backup and restore is required.', + {336068829} 'Maximum number of collations per character set exceeded', + {336068830} 'Invalid collation attributes', + {336068831} 'Collation @1 not installed for character set @2', + {336068832} 'Cannot use the internal domain @1 as new type for field @2', + {336068833} 'Default value is not allowed for array type in field @1', + {336068834} 'Default value is not allowed for array type in domain @1', + {336068835} 'DYN_UTIL_is_array failed for domain @1', + {336068836} 'DYN_UTIL_copy_domain failed for domain @1', + {336068837} 'Local column @1 doesn''t have a default', + {336068838} 'Local column @1 default belongs to domain @2', + {336068839} 'File name is invalid', + {336068840} '@1 cannot reference @2', + {336068841} 'Local column @1 is computed, cannot set a default value', + {336068842} 'ERASE RDB$COLLATIONS failed', + {336068843} 'Collation @1 is used in table @2 (field name @3) and cannot be dropped', + {336068844} 'Collation @1 is used in domain @2 and cannot be dropped', + {336068845} 'Cannot delete system collation', + {336068846} 'Cannot delete default collation of CHARACTER SET @1', + {336068847} 'Domain @1 is used in procedure @2 (parameter name @3) and cannot be dropped', + {336068848} 'Field @1 cannot be used twice in index @2', + {336068849} 'Table @1 not found', + {336068850} 'attempt to reference a view (@1) in a foreign key', + {336068851} 'Collation @1 is used in procedure @2 (parameter name @3) and cannot be dropped', + {336068852} 'New scale specified for column @1 must be at most @2.', + {336068853} 'New precision specified for column @1 must be at least @2.', + {336068854} '@1 is not grantor of @2 on @3 to @4.', + {336068855} 'Warning: @1 on @2 is not granted to @3.', + {336068856} 'Feature ''@1'' is not supported in ODS @2.@3', + {336068857} 'Cannot add or remove COMPUTED from column @1', + {336068858} 'Password should not be empty string', + {336068859} 'Index @1 already exists', + {336068860} 'Only @1 or user with privilege USE_GRANTED_BY_CLAUSE can use GRANTED BY clause', + {336068861} 'Exception @1 already exists', + {336068862} 'Sequence @1 already exists', + {336068863} 'ERASE RDB$USER_PRIVILEGES failed in REVOKE ALL ON ALL', + {336068864} 'Package @1 not found', + {336068865} 'Schema @1 not found', + {336068866} 'Cannot ALTER or DROP system procedure @1', + {336068867} 'Cannot ALTER or DROP system trigger @1', + {336068868} 'Cannot ALTER or DROP system function @1', + {336068869} 'Invalid DDL statement for procedure @1', + {336068870} 'Invalid DDL statement for trigger @1', + {336068871} 'Function @1 has not been defined on the package body @2', + {336068872} 'Procedure @1 has not been defined on the package body @2', + {336068873} 'Function @1 has a signature mismatch on package body @2', + {336068874} 'Procedure @1 has a signature mismatch on package body @2', + {336068875} 'Default values for parameters are not allowed in the definition of a previously declared packaged procedure @1.@2', + {336068876} 'Function @1 already exists', + {336068877} 'Package body @1 already exists', + {336068878} 'Invalid DDL statement for function @1', + {336068879} 'Cannot alter new style function @1 with ALTER EXTERNAL FUNCTION. Use ALTER FUNCTION instead.', + {336068880} 'Cannot delete system generator @1', + {336068881} 'Identity column @1 of table @2 must be of exact number type with zero scale', + {336068882} 'Identity column @1 of table @2 cannot be changed to NULLable', + {336068883} 'Identity column @1 of table @2 cannot have default value', + {336068884} 'Domain @1 must be of exact number type with zero scale because it''s used in an identity column', + {336068885} 'Generation of generator name failed', + {336068886} 'Parameter @1 in function @2 not found', + {336068887} 'Parameter @1 of routine @2 not found', + {336068888} 'Parameter @1 of routine @2 is ambiguous (found in both procedures and functions). Use a specifier keyword.', + {336068889} 'Collation @1 is used in function @2 (parameter name @3) and cannot be dropped', + {336068890} 'Domain @1 is used in function @2 (parameter name @3) and cannot be dropped', + {336068891} 'ALTER USER requires at least one clause to be specified', + {336068892} 'Cannot delete system SQL role @1', + {336068893} 'Column @1 is not an identity column', + {336068894} 'Duplicate @1 @2', + {336068895} 'System @1 @2 cannot be modified', + {336068896} 'INCREMENT BY 0 is an illegal option for sequence @1', + {336068897} 'Can''t use @1 in FOREIGN KEY constraint', + {336068898} 'Default values for parameters are not allowed in the definition of a previously declared packaged function @1.@2', + {336068899} 'Password must be specified when creating user', + {336068900} 'role @1 can not be granted to role @2', + {336068901} 'DROP SYSTEM PRIVILEGES should not be used in CREATE ROLE operator', + {336068902} 'Access to SYSTEM PRIVILEGES in ROLES denied to @1', + {336068903} 'Only @1, DB owner @2 or user with privilege USE_GRANTED_BY_CLAUSE can use GRANTED BY clause', + {336068904} 'INCREMENT BY 0 is an illegal option for identity column @1 of table @2', + {336068905} 'Concurrent ALTER DATABASE is not supported', + {336068906} 'Incompatible ALTER DATABASE clauses: ''@1'' and ''@2''', + {336068907} 'no @1 privilege with grant option on DDL @2', + {336068908} 'no @1 privilege with grant option on object @2', + {336068909} 'Function @1 does not exist', + {336068910} 'Procedure @1 does not exist', + {336068911} 'Package @1 does not exist', + {336068912} 'Trigger @1 does not exist', + {336068913} 'View @1 does not exist', + {336068914} 'Table @1 does not exist', + {336068915} 'Exception @1 does not exist', + {336068916} 'Generator/Sequence @1 does not exist', + {336068917} 'Field @1 of table @2 does not exist', + {336068918} 'Trigger @1 already exists', + {336068919} 'Domain @1 already exists', + {336068920} 'Collation @1 already exists', + {336068921} 'Package @1 already exists', + {336068922} 'Index schema (@1) must match table schema (@2)', + {336068923} 'Trigger schema (@1) must match table schema (@2)', + {336068924} 'Schema @1 already exists', + {336068925} 'Cannot ALTER or DROP SYSTEM schema', + {336068926} 'Cannot DROP schema @1 because it has objects', + {336068927} 'Cannot CREATE/ALTER/DROP @1 in SYSTEM schema', + {336068928} 'Schema name @1 is reserved and cannot be created', + {336068929} 'Cannot infer schema name as there is no valid schema in the search path', + {336068930} 'Blob filter @1 already exists', + {336068931} 'Column @1 already exists in table @2', + {336068932} 'Constant @1 not found', + {336068933} 'Constant @1 already exists', + {336068934} 'The constant @1 must be initialized by a constant expression', + {336068935} 'Function @1 cannot change between aggregate and non-aggregate', + {336396289} 'Firebird error', + {336396362} 'Rollback not performed', + {336396364} 'Connection error', + {336396365} 'Connection not established', + {336396366} 'Connection authorization failure.', + {336396375} 'deadlock', + {336396376} 'Unsuccessful execution caused by deadlock.', + {336396377} 'record from transaction @1 is stuck in limbo', + {336396379} 'operation completed with errors', + {336396382} 'the SQL statement cannot be executed', + {336396384} 'Unsuccessful execution caused by an unavailable resource.', + {336396386} 'Unsuccessful execution caused by a system error that precludes successful execution of subsequent statements', + {336396387} 'Unsuccessful execution caused by system error that does not preclude successful execution of subsequent statements', + {336396446} 'Wrong numeric type', + {336396447} 'too many versions', + {336396448} 'intermediate journal file full', + {336396449} 'journal file wrong format', + {336396450} 'database @1 shutdown in @2 seconds', + {336396451} 'restart shared cache manager', + {336396452} 'exception @1', + {336396453} 'bad checksum', + {336396454} 'refresh range number @1 not found', + {336396455} 'expression evaluation not supported', + {336396456} 'FOREIGN KEY column count does not match PRIMARY KEY', + {336396457} 'Attempt to define a second PRIMARY KEY for the same table', + {336396458} 'column used with aggregate', + {336396459} 'invalid column reference', + {336396460} 'invalid key position', + {336396461} 'invalid direction for find operation', + {336396462} 'Invalid statement handle', + {336396463} 'invalid lock handle', + {336396464} 'invalid lock level @1', + {336396465} 'invalid bookmark handle', + {336396468} 'wrong or obsolete version', + {336396471} 'The INSERT, UPDATE, DELETE, DDL or authorization statement cannot be executed because the transaction is inquiry only', + {336396472} 'external file could not be opened for output', + {336396477} 'multiple rows in singleton select', + {336396478} 'No subqueries permitted for VIEW WITH CHECK OPTION', + {336396479} 'DISTINCT, GROUP or HAVING not permitted for VIEW WITH CHECK OPTION', + {336396480} 'Only one table allowed for VIEW WITH CHECK OPTION', + {336396481} 'No WHERE clause for VIEW WITH CHECK OPTION', + {336396482} 'Only simple column names permitted for VIEW WITH CHECK OPTION', + {336396484} 'An error was found in the application program input parameters for the SQL statement.', + {336396485} 'Invalid insert or update value(s): object columns are constrained - no 2 table rows can have duplicate column values', + {336396486} 'Arithmetic overflow or division by zero has occurred.', + {336396594} 'cannot access column @1 in view @2', + {336396595} 'Too many concurrent executions of the same request', + {336396596} 'maximum indexes per table (@1) exceeded', + {336396597} 'new record size of @1 bytes is too big', + {336396598} 'segments not allowed in expression index @1', + {336396599} 'wrong page type', + {336396603} 'invalid ARRAY or BLOB operation', + {336396611} '@1 extension error', + {336396624} 'key size exceeds implementation restriction for index "@1"', + {336396625} 'definition error for index @1', + {336396628} 'cannot create index', + {336396651} 'duplicate specification of @1 - not supported', + {336396663} 'The insert failed because a column definition includes validation constraints.', + {336396670} 'Cannot delete object referenced by another object', + {336396671} 'Cannot modify object referenced by another object', + {336396672} 'Object is referenced by another object', + {336396673} 'lock on conflicts with existing lock', + {336396681} 'This operation is not defined for system tables.', + {336396683} 'Inappropriate self-reference of column', + {336396684} 'Illegal array dimension range', + {336396687} 'database or file exists', + {336396688} 'sort error: corruption in data structure', + {336396689} 'node not supported', + {336396690} 'Shadow number must be a positive integer', + {336396691} 'Preceding file did not specify length, so @1 must include starting page number', + {336396692} 'illegal operation when at beginning of stream', + {336396693} 'the current position is on a crack', + {336396735} 'cannot modify an existing user privilege', + {336396736} 'user does not have the privilege to perform operation', + {336396737} 'This user does not have privilege to perform this operation on this object.', + {336396756} 'transaction marked invalid by I/O error', + {336396757} 'Cannot prepare a CREATE DATABASE/SCHEMA statement', + {336396758} 'violation of FOREIGN KEY constraint "@1"', + {336396769} 'The prepare statement identifies a prepare statement with an open cursor', + {336396770} 'Unknown statement or request', + {336396778} 'Attempt to update non-updatable cursor', + {336396780} 'The cursor identified in the UPDATE or DELETE statement is not positioned on a row.', + {336396784} 'Unknown cursor', + {336396786} 'The cursor identified in an OPEN statement is already open.', + {336396787} 'The cursor identified in a FETCH or CLOSE statement is not open.', + {336396875} 'Overflow occurred during data type conversion.', + {336396881} 'null segment of UNIQUE KEY', + {336396882} 'subscript out of bounds', + {336396886} 'data operation not supported', + {336396887} 'invalid comparison operator for find operation', + {336396974} 'Cannot transliterate character between character sets', + {336396975} 'count of column list and variable list do not match', + {336396985} 'Incompatible column/host variable data type', + {336396991} 'Operation violates CHECK constraint @1 on view or table', + {336396992} 'internal Firebird consistency check (invalid RDB$CONSTRAINT_TYPE)', + {336396993} 'Cannot update constraints (RDB$RELATION_CONSTRAINTS).', + {336396994} 'Cannot delete CHECK constraint entry (RDB$CHECK_CONSTRAINTS)', + {336396995} 'Cannot update constraints (RDB$CHECK_CONSTRAINTS).', + {336396996} 'Cannot update constraints (RDB$REF_CONSTRAINTS).', + {336396997} 'Column used in a PRIMARY constraint must be NOT NULL.', + {336397004} 'index @1 cannot be used in the specified plan', + {336397005} 'table @1 is referenced in the plan but not the from list', + {336397006} 'the table @1 is referenced twice; use aliases to differentiate', + {336397007} 'table @1 is not referenced in plan', + {336397027} 'Log file specification partition error', + {336397028} 'Cache or Log redefined', + {336397029} 'Write-ahead Log with shadowing configuration not allowed', + {336397030} 'Overflow log specification required for round-robin log', + {336397031} 'WAL defined; Cache Manager must be started first', + {336397033} 'Write-ahead Log without shared cache configuration not allowed', + {336397034} 'Cannot start WAL writer for the database @1', + {336397035} 'WAL writer synchronization error for the database @1', + {336397036} 'WAL setup error. Please see Firebird log.', + {336397037} 'WAL buffers cannot be increased. Please see Firebird log.', + {336397038} 'WAL writer - Journal server communication error. Please see Firebird log.', + {336397039} 'WAL I/O error. Please see Firebird log.', + {336397040} 'Unable to roll over; please see Firebird log.', + {336397041} 'obsolete', + {336397042} 'obsolete', + {336397043} 'obsolete', + {336397044} 'obsolete', + {336397045} 'database does not use Write-ahead Log', + {336397046} 'Cannot roll over to the next log file @1', + {336397047} 'obsolete', + {336397048} 'obsolete', + {336397049} 'Cache or Log size too small', + {336397050} 'Log record header too small at offset @1 in log file @2', + {336397051} 'Incomplete log record at offset @1 in log file @2', + {336397052} 'Unexpected end of log file @1 at offset @2', + {336397053} 'Database name in the log file @1 is different', + {336397054} 'Log file @1 not closed properly; database recovery may be required', + {336397055} 'Log file @1 not latest in the chain but open flag still set', + {336397056} 'Invalid version of log file @1', + {336397057} 'Log file header of @1 too small', + {336397058} 'obsolete', + {336397069} 'table @1 is not defined', + {336397080} 'invalid ORDER BY clause', + {336397082} 'Column does not belong to referenced table', + {336397083} 'column @1 is not defined in table @2', + {336397084} 'Undefined name', + {336397085} 'Ambiguous column reference.', + {336397116} 'function @1 is not defined', + {336397117} 'Invalid data type, length, or value', + {336397118} 'Invalid number of arguments', + {336397126} 'dbkey not available for multi-table views', + {336397130} 'number of columns does not match select list', + {336397131} 'must specify column name for view select expression', + {336397133} '@1 is not a valid base table of the specified view', + {336397137} 'This column cannot be updated because it is derived from an SQL function or expression.', + {336397138} 'The object of the INSERT, DELETE or UPDATE statement is a view for which the requested operation is not permitted.', + {336397183} 'Invalid String', + {336397184} 'Invalid token', + {336397185} 'Invalid numeric literal', + {336397203} 'An error occurred while trying to update the security database', + {336397204} 'non-SQL security class defined', + {336397205} 'ODS versions before ODS@1 are not supported', + {336397206} 'Table @1 does not exist', + {336397207} 'View @1 does not exist', + {336397208} 'At line @1, column @2', + {336397209} 'At unknown line and column', + {336397210} 'Column @1 cannot be repeated in @2 statement', + {336397211} 'Too many values (more than @1) in member list to match against', + {336397212} 'Array and BLOB data types not allowed in computed field', + {336397213} 'Implicit domain name @1 not allowed in user created domain', + {336397214} 'scalar operator used on field @1 which is not an array', + {336397215} 'cannot sort on more than 255 items', + {336397216} 'cannot group on more than 255 items', + {336397217} 'Cannot include the same field (@1.@2) twice in the ORDER BY clause with conflicting sorting options', + {336397218} 'column list from derived table @1 has more columns than the number of items in its SELECT statement', + {336397219} 'column list from derived table @1 has less columns than the number of items in its SELECT statement', + {336397220} 'no column name specified for column number @1 in derived table @2', + {336397221} 'column @1 was specified multiple times for derived table @2', + {336397222} 'Internal dsql error: alias type expected by pass1_expand_select_node', + {336397223} 'Internal dsql error: alias type expected by pass1_field', + {336397224} 'Internal dsql error: column position out of range in pass1_union_auto_cast', + {336397225} 'Recursive CTE member (@1) can refer itself only in FROM clause', + {336397226} 'CTE ''@1'' has cyclic dependencies', + {336397227} 'Recursive member of CTE can''t be member of an outer join', + {336397228} 'Recursive member of CTE can''t reference itself more than once', + {336397229} 'Recursive CTE (@1) must be an UNION', + {336397230} 'CTE ''@1'' defined non-recursive member after recursive', + {336397231} 'Recursive member of CTE ''@1'' has @2 clause', + {336397232} 'Recursive members of CTE (@1) must be linked with another members via UNION ALL', + {336397233} 'Non-recursive member is missing in CTE ''@1''', + {336397234} 'WITH clause can''t be nested', + {336397235} 'column @1 appears more than once in USING clause', + {336397236} 'feature is not supported in dialect @1', + {336397237} 'CTE "@1" is not used in query', + {336397238} 'column @1 appears more than once in ALTER VIEW', + {336397239} '@1 is not supported inside IN AUTONOMOUS TRANSACTION block', + {336397240} 'Unknown node type @1 in dsql/GEN_expr', + {336397241} 'Argument for @1 in dialect 1 must be string or numeric', + {336397242} 'Argument for @1 in dialect 3 must be numeric', + {336397243} 'Strings cannot be added to or subtracted from DATE or TIME types', + {336397244} 'Invalid data type for subtraction involving DATE, TIME or TIMESTAMP types', + {336397245} 'Adding two DATE values or two TIME values is not allowed', + {336397246} 'DATE value cannot be subtracted from the provided data type', + {336397247} 'Strings cannot be added or subtracted in dialect 3', + {336397248} 'Invalid data type for addition or subtraction in dialect 3', + {336397249} 'Invalid data type for multiplication in dialect 1', + {336397250} 'Strings cannot be multiplied in dialect 3', + {336397251} 'Invalid data type for multiplication in dialect 3', + {336397252} 'Division in dialect 1 must be between numeric data types', + {336397253} 'Strings cannot be divided in dialect 3', + {336397254} 'Invalid data type for division in dialect 3', + {336397255} 'Strings cannot be negated (applied the minus operator) in dialect 3', + {336397256} 'Invalid data type for negation (minus operator)', + {336397257} 'Cannot have more than 255 items in DISTINCT / UNION DISTINCT list', + {336397258} 'ALTER CHARACTER SET @1 failed', + {336397259} 'COMMENT ON @1 failed', + {336397260} 'CREATE FUNCTION @1 failed', + {336397261} 'ALTER FUNCTION @1 failed', + {336397262} 'CREATE OR ALTER FUNCTION @1 failed', + {336397263} 'DROP FUNCTION @1 failed', + {336397264} 'RECREATE FUNCTION @1 failed', + {336397265} 'CREATE PROCEDURE @1 failed', + {336397266} 'ALTER PROCEDURE @1 failed', + {336397267} 'CREATE OR ALTER PROCEDURE @1 failed', + {336397268} 'DROP PROCEDURE @1 failed', + {336397269} 'RECREATE PROCEDURE @1 failed', + {336397270} 'CREATE TRIGGER @1 failed', + {336397271} 'ALTER TRIGGER @1 failed', + {336397272} 'CREATE OR ALTER TRIGGER @1 failed', + {336397273} 'DROP TRIGGER @1 failed', + {336397274} 'RECREATE TRIGGER @1 failed', + {336397275} 'CREATE COLLATION @1 failed', + {336397276} 'DROP COLLATION @1 failed', + {336397277} 'CREATE DOMAIN @1 failed', + {336397278} 'ALTER DOMAIN @1 failed', + {336397279} 'DROP DOMAIN @1 failed', + {336397280} 'CREATE EXCEPTION @1 failed', + {336397281} 'ALTER EXCEPTION @1 failed', + {336397282} 'CREATE OR ALTER EXCEPTION @1 failed', + {336397283} 'RECREATE EXCEPTION @1 failed', + {336397284} 'DROP EXCEPTION @1 failed', + {336397285} 'CREATE SEQUENCE @1 failed', + {336397286} 'CREATE TABLE @1 failed', + {336397287} 'ALTER TABLE @1 failed', + {336397288} 'DROP TABLE @1 failed', + {336397289} 'RECREATE TABLE @1 failed', + {336397290} 'CREATE PACKAGE @1 failed', + {336397291} 'ALTER PACKAGE @1 failed', + {336397292} 'CREATE OR ALTER PACKAGE @1 failed', + {336397293} 'DROP PACKAGE @1 failed', + {336397294} 'RECREATE PACKAGE @1 failed', + {336397295} 'CREATE PACKAGE BODY @1 failed', + {336397296} 'DROP PACKAGE BODY @1 failed', + {336397297} 'RECREATE PACKAGE BODY @1 failed', + {336397298} 'CREATE VIEW @1 failed', + {336397299} 'ALTER VIEW @1 failed', + {336397300} 'CREATE OR ALTER VIEW @1 failed', + {336397301} 'RECREATE VIEW @1 failed', + {336397302} 'DROP VIEW @1 failed', + {336397303} 'DROP SEQUENCE @1 failed', + {336397304} 'RECREATE SEQUENCE @1 failed', + {336397305} 'DROP INDEX @1 failed', + {336397306} 'DROP FILTER @1 failed', + {336397307} 'DROP SHADOW @1 failed', + {336397308} 'DROP ROLE @1 failed', + {336397309} 'DROP USER @1 failed', + {336397310} 'CREATE ROLE @1 failed', + {336397311} 'ALTER ROLE @1 failed', + {336397312} 'ALTER INDEX @1 failed', + {336397313} 'ALTER DATABASE failed', + {336397314} 'CREATE SHADOW @1 failed', + {336397315} 'DECLARE FILTER @1 failed', + {336397316} 'CREATE INDEX @1 failed', + {336397317} 'CREATE USER @1 failed', + {336397318} 'ALTER USER @1 failed', + {336397319} 'GRANT failed', + {336397320} 'REVOKE failed', + {336397321} 'Recursive member of CTE cannot use aggregate or window function', + {336397322} '@2 MAPPING @1 failed', + {336397323} 'ALTER SEQUENCE @1 failed', + {336397324} 'CREATE GENERATOR @1 failed', + {336397325} 'SET GENERATOR @1 failed', + {336397326} 'WITH LOCK can be used only with a single physical table', + {336397327} 'FIRST/SKIP cannot be used with OFFSET/FETCH or ROWS', + {336397328} 'WITH LOCK cannot be used with aggregates', + {336397329} 'WITH LOCK cannot be used with @1', + {336397330} 'Number of arguments (@1) exceeds the maximum (@2) number of EXCEPTION USING arguments', + {336397331} 'String literal with @1 bytes exceeds the maximum length of @2 bytes', + {336397332} 'String literal with @1 characters exceeds the maximum length of @2 characters for the @3 character set', + {336397333} 'Too many BEGIN...END nesting. Maximum level is @1', + {336397334} 'RECREATE USER @1 failed', + {336397335} 'the number of fields exceeds the limit for the @1 operator. Expected @2, received @3', + {336397336} 'CREATE SCHEMA @1 failed', + {336397337} 'DROP SCHEMA @1 failed', + {336397338} 'RECREATE SCHEMA @1 failed', + {336397339} 'ALTER SCHEMA @1 failed', + {336397340} 'CREATE OR ALTER SCHEMA @1 failed', + {336397341} 'CREATE CONSTANT @1 failed', + {336397342} 'ALTER CONSTANT @1 failed', + {336397343} 'CREATE OR ALTER CONSTANT @1 failed', + {336461924} 'Row not found for fetch, update or delete, or the result of a query is an empty table.', + {336461925} 'segment buffer length shorter than expected', + {336462125} 'Datatype needs modification', + {336462436} 'Duplicate column or domain name found.'); + + MessageSQLCodes: array[0..MessageCount-1] of smallint = ( + 32767, -802, -901, -922, -904, -924, -901, -901, -901, -901, -901, -901, + -901, -902, -413, -902, -913, -901, 100, -901, -901, -901, -901, -104, + -902, -901, -902, -625, -508, -803, -901, -607, -551, -901, 100, -901, + -820, -901, -901, -151, -150, -817, -150, -901, -901, -901, 101, 100, + -402, -901, -901, -817, -901, -902, -596, -904, -901, -901, -901, -820, + -804, -904, -901, -901, -902, -902, -904, -902, -902, -904, -104, -904, + -901, -904, -902, -219, -205, -902, -902, -902, -902, -902, -902, -689, + -902, -902, -902, -901, -901, -902, -902, -902, -902, -902, -402, -902, + -902, -902, -901, -901, -901, -923, -902, -902, -904, -104, -104, -402, + -901, -104, -904, -901, -902, -924, -664, -407, -902, -820, -172, -171, + -104, -924, -901, -901, -817, -677, -150, -926, -902, -902, -901, -904, + -906, -904, -413, -904, -104, -406, -171, -911, -904, -923, -923, -204, + -923, -685, -530, -820, -901, -532, -902, -902, -902, -823, -824, -615, + -615, -692, -902, -902, -902, -902, -902, -902, -902, -901, -902, -230, + -231, -232, -233, -234, -235, -236, -237, -238, -239, -240, -241, -242, + -243, -615, -204, -244, -245, -902, -902, -615, -834, -204, -901, -204, + -170, -246, -247, -204, -204, -836, -837, -825, -902, -248, -249, -250, + -251, -252, -253, -254, -902, -553, -616, -291, -204, -660, -292, -293, + -294, -618, -618, -616, -616, -616, -617, -616, -617, -295, -150, -296, + -831, -607, -552, -204, -205, -552, -84, -84, -255, -902, -297, -901, + -838, -901, -901, -901, -902, -314, -257, -258, -204, -902, -104, -103, + -504, -204, -502, -510, -502, -501, -206, -104, -204, -204, -518, -804, + -804, -826, -804, -206, -204, -204, -104, -104, -104, -604, -604, -204, + -206, -531, -157, -158, -806, -807, -808, -809, -832, -810, -833, -599, + -104, -901, -901, -901, -104, -901, -901, -830, -829, -208, -171, -170, + -204, -204, -204, -901, -663, -599, -901, -901, -660, -259, -616, -663, + -597, -598, -104, -204, -204, -281, -282, -283, -204, -901, -284, -282, + -596, -595, -601, -401, -924, -835, -689, -816, -811, -902, -902, -827, + -901, -607, -155, -282, -282, -904, -204, -693, -637, -803, -901, -909, + -84, -313, -685, -685, -663, -901, -616, -901, -904, -841, -828, -690, + -600, -691, -605, -904, -694, -162, -839, -840, -519, -999, -260, -239, + -260, -239, -239, -261, -261, -842, -842, -842, -842, -842, -105, -901, + -901, -901, -901, -552, -203, -104, -282, -901, -842, -804, -104, -816, + -901, -902, -827, -901, -901, -902, -902, -902, -902, -902, -902, -902, + -616, -616, -104, -901, -902, -902, -902, -902, -902, -902, -902, -902, + -901, -901, -901, -104, -906, -902, -607, -85, -85, -85, -85, -85, + -85, -85, -85, -85, -85, -85, -904, -204, -204, -904, -904, -104, + -104, -817, -817, -901, -901, -901, -901, -901, -901, -901, -901, -901, + -901, -901, -901, -901, -901, -901, -901, -901, -901, -901, -901, -901, + -901, -105, -551, -902, -904, -817, -901, -902, -104, -901, -104, -904, + -204, -901, 501, -901, -901, -901, -901, 300, 301, -902, -833, -901, + -901, -904, -901, -607, -607, -204, -204, -902, -901, -104, -104, -104, + -104, -104, -104, -904, -904, -904, -904, -902, -904, -904, -902, -901, + -802, -204, -530, -530, -901, -902, -901, -901, -901, -901, -901, -901, + -836, -104, -170, -104, -904, -204, -204, -204, -902, -904, -607, -901, + -413, -413, -904, -904, -904, -904, -901, -204, -901, -901, -901, -901, + -219, -171, -901, 0, -901, -901, -904, -625, -625, -820, -902, -205, + -105, -901, -901, -901, -901, -901, -901, -901, -901, -901, -901, -901, + -901, -901, -901, -901, -901, -901, -901, -901, -901, -901, -901, -901, + -901, -901, -901, -901, -833, -833, -802, -802, -802, -901, -901, -901, + -901, -901, -901, -901, -901, -901, -901, -901, -901, -901, -104, 0, + -172, -901, -901, -904, -902, -833, -833, -833, -833, -833, -833, -833, + -833, -833, -833, -833, -833, -833, -833, -833, -833, -833, -833, -833, + -833, -833, -833, -833, -833, -833, -833, -833, -833, -833, -833, -833, + -833, -833, -902, -902, -924, -901, -902, -902, -833, -833, -833, -833, + -104, -833, -901, -901, -902, -901, -901, -902, -901, -901, -901, -901, + -904, -901, -901, -901, -901, -901, -901, -901, -901, -901, -532, -532, + -901, -901, -901, -901, -901, -833, -901, -901, -901, -901, -901, -901, + -836, -532, -901, -901, -904, -901, -104, -104, -833, -901, -901, -901, + -406, -902, -902, -901, -902, -551, -551, -104, -551, -104, -104, -104, + -104, -901, -901, -902, -902, -104, -833, -901, -901, -901, -804, -804, + -804, -902, -902, -902, -902, -902, -552, -901, -902, -902, -902, -902, + -902, -902, -902, -902, -902, -902, -902, -901, -901, -901, -901, -901, + -901, -901, -901, -901, -901, -901, -901, -901, -901, -924, -924, -924, + -901, -901, -901, -901, -596, -901, -901, -901, -904, -901, -901, -901, + -804, -170, -901, -291, -204, -901, -902, -902, -902, -901, -904, -901, + -902, -902, -902, -901, -104, -104, -104, -104, -833, -833, -833, -833, + -833, -833, -902, -901, -901, -901, -902, -902, -902, -902, -902, -902, + -902, -902, -842, -901, -901, -901, -901, -901, -901, -901, -901, -901, + -901, -901, -901, -901, -901, -901, -901, -901, -833, -833, -842, -901, + -901, -901, -901, -901, -901, -901, -902, -902, -901, -901, -902, -901, + -901, -902, -902, -901, -901, -901, -901, -901, -902, -901, -901, -901, + -901, -901, -901, -901, -901, -901, -901, -901, -901, -901, -901, -901, + -901, -901, -901, -901, -902, -902, -901, -901, -902, -104, -901, -901, + -901, -901, -901, -901, -901, -901, -901, -901, -901, -901, -901, -901, + -901, -901, -901, -901, -901, -901, -901, -901, -901, -901, -901, -901, + -901, -901, -901, -901, -901, -901, -901, -901, -901, -901, -901, -901, + -901, -901, -901, -901, -901, -901, -901, -901, -901, -901, -551, -901, + -901, -901, -901, -902, -902, -901, -402, -402, -901, -901, 304, 304, + 304, -811, -902, -902, -902, -902, -901, -901, -901, -901, -901, -901, + -902, -902, -281, -402, -402, -901, -924, -902, -902, -625, -170, -170, + -170, -901, -901, -901, -901, -901, -901, -901, -901, -901, -901, -901, + -901, -901, -901, -833, -901, -901, -901, -901, -901, -833, -833, -901, + -901, -901, -901, -833, -833, -833, -833, -833, -833, -901, -901, -901, + -901, -901, -901, -901, -901, -901, -901, -901, -901, -104, -204, -204, + -204, -833, -804, -607, -104, 301, -104, 32767, -817, 301, 301, 301, + 301, 301, -204, -607, -104, -104, -504, -502, -502, -502, -502, -502, + -504, -607, -804, -660, -313, -313, -817, -817, -817, -607, -802, -802, + -802, -802, -802, -802, -313, -817, -313, -607, -104, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, -901, 32767, 32767, + 32767, -901, 32767, 32767, 32767, 32767, 32767, 32767, -901, 32767, 32767, 32767, + 32767, 32767, -901, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, -901, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, -901, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, -901, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, -901, 32767, 32767, 32767, -901, 32767, -901, -901, + 32767, 32767, 32767, -901, -901, 32767, 32767, -901, 32767, 32767, 32767, -901, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, -901, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, -901, -901, -901, -901, -901, -901, -901, -901, -901, + -612, -612, -383, -315, -829, -829, -829, 32767, -901, 32767, -901, 32767, + 32767, 32767, 32767, 32767, 32767, -829, -829, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, -901, 32767, 32767, -901, -901, -607, -901, 32767, + 32767, -901, 32767, -901, -829, -829, 32767, 106, -901, -829, -901, -901, + 32767, 32767, 32767, 32767, -901, -901, -607, -607, -607, -607, -607, -901, + -901, -901, -901, -901, 32767, -901, -607, -901, 32767, 32767, 32767, 32767, + 32767, 32767, -901, -901, -901, -901, -901, -901, 32767, 32767, -901, -901, + -901, -901, -901, 32767, -901, 32767, 32767, 32767, -901, 32767, 32767, -901, + -901, -901, -901, -901, -901, -901, -901, -901, -901, -901, 32767, 32767, + 32767, 32767, -901, -901, 32767, -607, -607, -607, -607, -901, 32767, -612, + 32767, 32767, 32767, -104, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, -804, -607, -607, -206, -206, -206, -901, -607, -637, + -607, -104, -104, -104, -104, -104, -104, -104, -104, -104, -104, -104, + -104, -104, -104, -104, -104, -104, -104, -104, -104, -104, -901, -104, + -104, -901, -833, -833, -833, -833, -833, -833, -833, -833, -833, -833, + -833, -833, -833, -833, -833, -833, -833, -104, -901, -901, -901, -901, + -901, -901, -901, -901, -901, -901, -901, -901, -901, -901, -901, -901, + -901, -901, -901, -901, -901, -901, -901, -901, -901, -901, -901, -901, + -901, -901, -901, -901, -901, -901, -901, -901, -901, -901, -901, -901, + -901, -901, -901, -901, -901, -901, -901, -901, -901, -901, -901, -901, + -901, -901, -901, -901, -901, -901, -901, -901, -901, -901, -901, -104, + -901, -901, -901, -901, -104, -104, -104, -104, -901, -901, -901, -901, + -901, -104, -901, -901, -901, -901, -901, -901, -901, -901, 32767, 32767, + 32767, 32767); + +function FindMessageIndex(aCode: cardinal; var aIndex: integer): boolean; +var lo, hi, mid: integer; +begin + Result := false; + lo := 0; + hi := MessageCount - 1; + while lo <= hi do + begin + mid := (lo + hi) div 2; + if MessageCodes[mid] = aCode then + begin + aIndex := mid; + Exit(true); + end; + if MessageCodes[mid] < aCode then + lo := mid + 1 + else + hi := mid - 1; + end; +end; + +function FindEngineMessage(aCode: cardinal; var aFormat: AnsiString): boolean; +var idx: integer; +begin + Result := FindMessageIndex(aCode,idx); + if Result then + aFormat := MessageTexts[idx]; +end; + +function EngineMessageSQLCode(aCode: cardinal): integer; +var idx: integer; +begin + if FindMessageIndex(aCode,idx) then + Result := MessageSQLCodes[idx] + else + Result := NoSQLCode; +end; + +end. diff --git a/client/wire/FBWireProtocol.pas b/client/wire/FBWireProtocol.pas new file mode 100644 index 00000000..5b53be6a --- /dev/null +++ b/client/wire/FBWireProtocol.pas @@ -0,0 +1,1975 @@ +(* + * Firebird Interface (fbintf). The fbintf components provide a set of + * Pascal language bindings for the Firebird API. + * + * This file is part of the pure Pascal wire protocol implementation + * (no fbclient required) and is subject to the Initial Developer's + * Public License Version 1.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy + * of the License here: + * + * http://www.firebirdsql.org/index.php?op=doc&id=idpl + * + * Software distributed under the License is distributed on an "AS + * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or + * implied. See the License for the specific language governing rights + * and limitations under the License. + * + * The Initial Developer of the Original Code is MWA Software + * (http://www.mwasoftware.co.uk). + * + * All Rights Reserved. + * + * Contributor(s): ______________________________________. + * +*) +unit FBWireProtocol; + +{ The wire protocol engine: connection handshake with protocol negotiation + (protocols 13..19, i.e. Firebird 3.0 up to and including Firebird 6; + older servers negotiate down), Srp/Srp256 authentication, optional wire + encryption (Arc4, ChaCha, ChaCha64) and the request/response packet + exchanges for attachments, transactions, DSQL statements, blobs, + information calls and services. + + The reference for the packet layouts is src/remote/protocol.h and + src/remote/protocol.cpp in the Firebird source tree. +} + +{$IFDEF MSWINDOWS} +{$DEFINE WINDOWS} +{$ENDIF} + +{$IFDEF FPC} +{$mode delphi} +{$codepage UTF8} +{$interfaces COM} +{$ENDIF} + +interface + +uses + Classes, SysUtils, FBWireStream, FBWireConst, FBWireCrypto, FBWireSRP, + FBWireMessage; + +const + {highest protocol version this client implements. Firebird 3 accepts up + to 15, Firebird 4 up to 17, Firebird 5 accepts 18 (op_fetch_scroll and + the op_execute cursor flags word), Firebird 5.0.3 accepts 19 + (op_inline_blob and the op_execute inline blob size limit), Firebird 6 + accepts 20 (SQL schemas and the prepare flags word).} + MaxSupportedProtocol = PROTOCOL_VERSION20; + INVALID_OBJECT = $FFFF; + + {key advertisement clumplet tags (see plugins/crypt in protocol.cpp)} + TAG_KEY_TYPE = 0; + TAG_KEY_PLUGINS = 1; + TAG_KNOWN_PLUGINS = 2; + TAG_PLUGIN_SPECIFIC = 3; + +type + TWireCryptOption = (wcDisabled, wcEnabled, wcRequired); + + {receives an op_inline_blob pushed by a protocol 19 server: the blob id + under that transaction, its info response buffer and the segmented + data stream. Installed by the attachment - see TFBWireAttachment.} + TInlineBlobHandler = procedure(aTrHandle: integer; aBlobID: Int64; + const aInfo, aData: TBytes) of object; + + TWireStatusItem = record + Kind: integer; {isc_arg_gds etc} + IntValue: Int64; + StrValue: AnsiString; + end; + TWireStatusVector = array of TWireStatusItem; + +const + {row states in a batch completion - IBatchCompletionState} + BATCH_EXECUTE_FAILED = -1; + BATCH_SUCCESS_NO_INFO = -2; + +type + + { TWireCursorState: per cursor fetch bookkeeping. + + The server answers an op_fetch requesting N rows with a sequence of + op_fetch_response packets, each carrying at most one message, and + terminates the batch with a packet whose message count is zero (or + whose status is 100 at end of cursor). The whole batch must be drained + before any other request is sent on the connection, otherwise the + queued row packets would be mistaken for that request's response. The + rows are therefore decoded into this cache as soon as they arrive and + handed out one at a time. } + + TWireCursorState = record + Rows: array of TBytes; {decoded rows not yet consumed} + NextRow: integer; + EndOfCursor: boolean; + end; + + { TWireBatchCS : decoded op_batch_cs - the batch completion state. + States[i] is the row's update count, or BATCH_EXECUTE_FAILED / + BATCH_SUCCESS_NO_INFO; StatusVectors[i] is non empty for rows whose + failure came with a status vector. } + + TWireBatchCS = record + States: array of integer; + StatusVectors: array of TWireStatusVector; + end; + + { TWireResponse : decoded op_response } + + TWireResponse = record + ObjectHandle: integer; + ObjectID: Int64; {blob id/quad: high word in bits 32..63} + Data: TBytes; + Status: TWireStatusVector; + function HasError: boolean; + function HasWarning: boolean; + end; + + { EFBWireProtocolError carries the decoded status vector } + + EFBWireProtocolError = class(EFBWireError) + private + FStatus: TWireStatusVector; + public + constructor CreateFromStatus(const aStatus: TWireStatusVector); + property Status: TWireStatusVector read FStatus; + end; + + TRC4WireCipher = class(TWireCipher) + private + FRC4: TRC4; + public + constructor Create(const aKey: TBytes); + destructor Destroy; override; + procedure Process(var aData; aLen: integer); override; + end; + + TChaChaWireCipher = class(TWireCipher) + private + FChaCha: TChaCha20; + public + constructor Create(const aKey, aNonce: TBytes; aCounter: QWord); + destructor Destroy; override; + procedure Process(var aData; aLen: integer); override; + end; + + { TFBWireConnection } + + TFBWireConnection = class + private + FTransport: TFBWireTransport; + FXDR: TXDRStream; + FProtocolVersion: cardinal; {negotiated, masked: 13..17} + FAcceptType: cardinal; + FUser: AnsiString; + FPassword: AnsiString; + FSRP: TSRPClient; + FAuthPluginName: AnsiString; + FAuthData: AnsiString; {hex proof for isc_dpb_specific_auth_data} + FAuthComplete: boolean; + FSessionKey: TBytes; + FKeyClumplets: TBytes; {accumulated server key advertisements} + FCryptPlugin: AnsiString; {active wire encryption plugin, '' = none} + FConnected: boolean; + FMaxProtocol: cardinal; + FCompression: boolean; + FOnInlineBlob: TInlineBlobHandler; + procedure ReadInlineBlob; + procedure SendUserIdentification(aWireCrypt: TWireCryptOption); + procedure DoAuthHandshake(aWireCrypt: TWireCryptOption); + function ComputeProof(const aData: TBytes; const aPluginName: AnsiString): AnsiString; + procedure StartWireEncryption(aWireCrypt: TWireCryptOption); + procedure AppendKeyClumplets(const aKeys: TBytes); + function FindWireCryptPlugin(var aSpecificData: TBytes; + var aKeyType: AnsiString): AnsiString; + public + constructor Create; + destructor Destroy; override; + + {Connects and authenticates. aDatabasePath is the path as sent to the + server (used in op_connect for routing only - the attach names the + database again).} + procedure ConnectTo(const aHost: AnsiString; aPort: integer; + const aDatabasePath, aUser, aPassword: AnsiString; + aWireCrypt: TWireCryptOption = wcEnabled; + aTimeout: integer = 0); + procedure Disconnect; + + {--- packet level api ---} + function ReadOperation: integer; {skips op_dummy / op_response_piggyback} + {reads a response body - the op_response operation code must already + have been consumed} + function ReadResponseBody: TWireResponse; + function ReceiveResponse: TWireResponse; {reads to next op_response} + procedure CheckResponse(const R: TWireResponse); + function ReceiveAndCheckResponse: TWireResponse; + function ReadStatusVector: TWireStatusVector; + + {--- attachments ---} + function AttachDatabase(const aDatabasePath: AnsiString; DPB: TBytes): integer; + function CreateDatabase(const aDatabasePath: AnsiString; DPB: TBytes): integer; + procedure DetachDatabase(aDbHandle: integer); + procedure DropDatabase(aDbHandle: integer); + + {--- transactions ---} + function StartTransaction(aDbHandle: integer; TPB: TBytes): integer; + procedure Commit(aTrHandle: integer); + procedure CommitRetaining(aTrHandle: integer); + procedure Rollback(aTrHandle: integer); + procedure RollbackRetaining(aTrHandle: integer); + procedure PrepareTransaction(aTrHandle: integer); {2PC phase 1} + + {--- DSQL ---} + function AllocateStatement(aDbHandle: integer): integer; + {returns the isc_info_sql response buffer for the describe items} + function PrepareStatement(aTrHandle, aStmtHandle: integer; + aDialect: integer; const sql: AnsiString; + const aInfoItems: TBytes; aBufferLength: integer): TBytes; + {aTimeout is the statement timeout in milliseconds (0 = none), carried + in the p_sqldata_timeout field from protocol 16. aCursorFlags is the + protocol 18 cursor flags word - CURSOR_TYPE_SCROLLABLE requests a + scrollable cursor. aInlineBlobLimit is the protocol 19 inline blob + size limit: blobs whose segmented size fits are pushed with the rows + as op_inline_blob packets; zero asks for none.} + procedure ExecuteStatement(aStmtHandle, aTrHandle: integer; + const aParamFormat: TWireMessageFormat; aParamBuffer: PByte; + aTimeout: cardinal = 0; aCursorFlags: cardinal = 0; + aInlineBlobLimit: cardinal = 0); + {op_execute2 for singleton results (execute procedure/returning)} + procedure ExecuteStatement2(aStmtHandle, aTrHandle: integer; + const aParamFormat: TWireMessageFormat; aParamBuffer: PByte; + const aOutFormat: TWireMessageFormat; aOutBuffer: PByte; + aTimeout: cardinal = 0; aInlineBlobLimit: cardinal = 0); + {fetches the next row into aOutBuffer, requesting a new batch of + aFetchCount rows from the server when needed. Returns false when the + cursor is exhausted. aState must be zero initialised before the first + call for a cursor (see TWireCursorState).} + function FetchRow(aStmtHandle: integer; + const aOutFormat: TWireMessageFormat; aOutBuffer: PByte; + aFetchCount: integer; var aState: TWireCursorState): boolean; + {op_fetch_scroll (protocol 18): a positioned fetch on a scrollable + cursor. Discards any read ahead rows in aState first - they describe + a cursor position the scroll abandons - and requests a single row: + the server disables prefetch for every direction except next/prior + anyway. Returns false when there is no row in that direction.} + function FetchRowScroll(aStmtHandle: integer; + const aOutFormat: TWireMessageFormat; aOutBuffer: PByte; + aDirection: integer; aPosition: integer; + var aState: TWireCursorState): boolean; + procedure FreeStatement(aStmtHandle: integer; aOption: integer); + procedure SetCursorName(aStmtHandle: integer; const aName: AnsiString); + procedure ExecImmediate(aTrHandle, aDbHandle: integer; aDialect: integer; + const sql: AnsiString); + + {--- information calls ---} + function GetInfo(aOperation: integer; aHandle: integer; + const aItems: TBytes; aBufferLength: integer): TBytes; + + {--- blobs ---} + procedure CreateBlob(aTrHandle: integer; const BPB: TBytes; + var aBlobHandle: integer; var aBlobID: Int64); + function OpenBlob(aTrHandle: integer; const BPB: TBytes; + aBlobID: Int64): integer; + {returns segment data (already unpacked from the 2 byte length prefixed + form). aEOB set when end of blob reached} + function GetSegment(aBlobHandle: integer; aBufferLength: integer; + var aEOB: boolean): TBytes; + procedure PutSegment(aBlobHandle: integer; const aData: TBytes); + procedure CloseBlob(aBlobHandle: integer); + procedure CancelBlob(aBlobHandle: integer); + + {--- array slices ---} + {op_get_slice: reads the slice of aArrayID into aBuffer as described + by aLayout. Returns the slice length reported by the server (in dsc + length units - see SliceElementDscLength).} + function GetSlice(aTrHandle: integer; aArrayID: Int64; const SDL: TBytes; + const aLayout: TWireSliceLayout; aBuffer: PByte): integer; + {op_put_slice: writes the slice and returns the (possibly new) array id} + function PutSlice(aTrHandle: integer; aArrayID: Int64; const SDL: TBytes; + const aLayout: TWireSliceLayout; aBuffer: PByte): Int64; + + {--- events ---} + {op_connect_request with P_REQ_async: asks the server to open the + auxiliary port that delivers op_event packets. Returns the TCP port + number. Only the port of the returned address is usable: the address + itself is the server's own view of itself, which behind NAT is not + reachable, so the caller connects to the host it already knows (the + stock client does the same - see aux_connect in inet.cpp).} + function ConnectRequest(aDbHandle: integer): integer; + procedure QueEvents(aDbHandle: integer; const aEPB: TBytes; aEventID: integer); + procedure CancelEvents(aDbHandle, aEventID: integer); + + {--- services ---} + function ServiceAttach(const aServiceName: AnsiString; SPB: TBytes): integer; + procedure ServiceDetach(aSvcHandle: integer); + procedure ServiceStart(aSvcHandle: integer; const aItems: TBytes); + function ServiceQuery(aSvcHandle: integer; const aSendItems, aRecvItems: TBytes; + aBufferLength: integer): TBytes; + + {--- batches (protocol 16) ---} + {op_batch_create: opens a batch on the statement. aMsgLen is + EngineMessageLength(aFormat) - the server validates it against the + format it parses from the BLR - and aPB the IBatch parameter block + (a wide tagged clumplet buffer)} + procedure BatchCreate(aStmtHandle: integer; + const aFormat: TWireMessageFormat; aMsgLen: cardinal; + const aPB: TBytes); + {op_batch_msg: sends aRows messages, each encoded exactly as a row + message (xdr_packed_message is the regular message encoding)} + procedure BatchMsg(aStmtHandle: integer; + const aFormat: TWireMessageFormat; + const aRows: array of TBytes); + {op_batch_regblob: registers an existing blob id for use in batch + messages. The engine translates every non null, non zero blob id in + a batch message through its registration map - and consumes the + entry - so an id must be registered once for each row that carries + it (the 3.0 provider does the same through IBatch.registerBlob).} + procedure BatchRegBlob(aStmtHandle: integer; aExistingID, aBatchID: Int64); + {op_batch_exec: runs the batch and parses the op_batch_cs reply} + function BatchExec(aStmtHandle, aTrHandle: integer): TWireBatchCS; + procedure BatchRelease(aStmtHandle: integer); + procedure BatchCancel(aStmtHandle: integer); + + {--- misc ---} + procedure Ping; + {op_cancel: sent out of band, typically from a different thread while + this connection's owner is blocked reading an operation's response. + There is no response packet: the cancelled operation itself fails + with isc_cancelled on the normal path. aKind is one of the + fb_cancel_* constants.} + procedure SendCancel(aKind: integer); + + property ProtocolVersion: cardinal read FProtocolVersion; + {caps the highest protocol version offered to the server. Defaults to + MaxSupportedProtocol; lower it to exercise or force an older dialect + of the protocol.} + property MaxProtocol: cardinal read FMaxProtocol write FMaxProtocol; + {requests zlib wire compression in the connect (pflag_compress). + Effective only when the server also has WireCompression enabled - + the accept reports the outcome, visible as Transport.Compressed. + Set before ConnectTo; default false.} + property Compression: boolean read FCompression write FCompression; + property Connected: boolean read FConnected; + {sink for op_inline_blob packets; unset, they are read and discarded} + property OnInlineBlob: TInlineBlobHandler read FOnInlineBlob write FOnInlineBlob; + property AuthData: AnsiString read FAuthData; + property AuthPluginName: AnsiString read FAuthPluginName; + property CryptPlugin: AnsiString read FCryptPlugin; + {raw key advertisement clumplets received from the server - exposed for + diagnostics} + property KeyClumplets: TBytes read FKeyClumplets; + property XDR: TXDRStream read FXDR; + property Transport: TFBWireTransport read FTransport; + end; + +{empties a cursor state, releasing the cached rows. Use this rather than + FillChar: the record holds a managed dynamic array which a FillChar + reset would leak.} +procedure ResetCursorState(var aState: TWireCursorState); + +{formats a decoded status vector the way fb_interpret does: each + isc_arg_gds item's message text - from the generated FBWireMessages + table, with its @1..@n arguments substituted - or the server's + interpreted text, successive items joined with a "-" continuation + line. Codes the table does not know fall back to the numeric form.} +function FormatWireStatus(const aStatus: TWireStatusVector): AnsiString; + +implementation + +uses IBErrorCodes, FBWireMessages; + +const + isc_arg_end = 0; + isc_arg_gds = 1; + isc_arg_string = 2; + isc_arg_number = 4; + isc_arg_interpreted = 5; + isc_arg_unix = 7; + isc_arg_next_mach = 15; + isc_arg_win32 = 17; + isc_arg_warning = 18; + isc_arg_sql_state = 19; + +procedure ResetCursorState(var aState: TWireCursorState); +begin + SetLength(aState.Rows,0); + aState.NextRow := 0; + aState.EndOfCursor := false; +end; + +function FormatWireStatus(const aStatus: TWireStatusVector): AnsiString; +var i, argNo: integer; + part, fmt: AnsiString; + args: array of AnsiString; + + procedure AddPart(const aPart: AnsiString); + begin + if Result = '' then + Result := aPart + else + Result := Result + LineEnding + '-' + aPart; + end; + + function Substitute(const aFormat: AnsiString): AnsiString; + var j, n: integer; + begin + Result := ''; + j := 1; + while j <= Length(aFormat) do + begin + if (aFormat[j] = '@') and (j < Length(aFormat)) and + (aFormat[j+1] in ['1'..'9']) then + begin + n := ord(aFormat[j+1]) - ord('0'); + if n <= Length(args) then + Result := Result + args[n-1]; + Inc(j,2); + end + else + begin + Result := Result + aFormat[j]; + Inc(j); + end; + end; + end; + +begin + Result := ''; + i := 0; + while i < Length(aStatus) do + begin + case aStatus[i].Kind of + isc_arg_gds, isc_arg_warning: + begin + if aStatus[i].IntValue = 0 then + begin + Inc(i); + continue; + end; + {gather the item's arguments: the string and number items that + follow it} + SetLength(args,0); + argNo := i + 1; + while (argNo < Length(aStatus)) and + (aStatus[argNo].Kind in [isc_arg_string,isc_arg_number]) do + begin + SetLength(args,Length(args)+1); + if aStatus[argNo].Kind = isc_arg_number then + args[High(args)] := IntToStr(aStatus[argNo].IntValue) + else + args[High(args)] := aStatus[argNo].StrValue; + Inc(argNo); + end; + if FindEngineMessage(cardinal(aStatus[i].IntValue),fmt) then + part := Substitute(fmt) + else + part := Format('Firebird Error Code: %d',[aStatus[i].IntValue]); + AddPart(part); + i := argNo; + end; + isc_arg_interpreted: + begin + AddPart(aStatus[i].StrValue); + Inc(i); + end; + else + {sql state and anything else contribute nothing to the text} + Inc(i); + end; + end; + if Result = '' then + Result := 'Firebird error'; +end; + +{ TWireResponse } + +function TWireResponse.HasError: boolean; +begin + Result := (Length(Status) > 0) and (Status[0].Kind = isc_arg_gds) and + (Status[0].IntValue <> 0); +end; + +function TWireResponse.HasWarning: boolean; +var i: integer; +begin + Result := false; + for i := 0 to Length(Status) - 1 do + if Status[i].Kind = isc_arg_warning then + Exit(true); +end; + +{ EFBWireProtocolError } + +constructor EFBWireProtocolError.CreateFromStatus(const aStatus: TWireStatusVector); +begin + FStatus := aStatus; + inherited Create(FormatWireStatus(aStatus)); +end; + +{ TRC4WireCipher } + +constructor TRC4WireCipher.Create(const aKey: TBytes); +begin + inherited Create; + FRC4 := TRC4.Create(aKey); +end; + +destructor TRC4WireCipher.Destroy; +begin + FRC4.Free; + inherited Destroy; +end; + +procedure TRC4WireCipher.Process(var aData; aLen: integer); +begin + FRC4.Process(aData,aLen); +end; + +{ TChaChaWireCipher } + +constructor TChaChaWireCipher.Create(const aKey, aNonce: TBytes; aCounter: QWord); +begin + inherited Create; + FChaCha := TChaCha20.Create(aKey,aNonce,aCounter); +end; + +destructor TChaChaWireCipher.Destroy; +begin + FChaCha.Free; + inherited Destroy; +end; + +procedure TChaChaWireCipher.Process(var aData; aLen: integer); +begin + FChaCha.Process(aData,aLen); +end; + +{ TFBWireConnection } + +constructor TFBWireConnection.Create; +begin + inherited Create; + FTransport := TFBWireTransport.Create; + FXDR := TXDRStream.Create(FTransport); + FMaxProtocol := MaxSupportedProtocol; +end; + +destructor TFBWireConnection.Destroy; +begin + Disconnect; + if FSRP <> nil then FSRP.Free; + FXDR.Free; + FTransport.Free; + inherited Destroy; +end; + +procedure TFBWireConnection.Disconnect; +begin + if FConnected and FTransport.Connected then + try + FXDR.WriteInt32(op_disconnect); + FXDR.Flush; + except + {ignore - connection may already be gone} + end; + FTransport.Disconnect; + FConnected := false; +end; + +function GetOSUser: AnsiString; +begin + Result := GetEnvironmentVariable('USER'); + if Result = '' then + Result := GetEnvironmentVariable('USERNAME'); + if Result = '' then + Result := 'unknown'; +end; + +function GetHostName: AnsiString; +begin + Result := GetEnvironmentVariable('HOSTNAME'); + if Result = '' then + Result := GetEnvironmentVariable('COMPUTERNAME'); + if Result = '' then + Result := 'localhost'; +end; + +procedure TFBWireConnection.SendUserIdentification(aWireCrypt: TWireCryptOption); +var buffer: TBytes; + len: integer; + + procedure AddByte(aValue: byte); + begin + if len >= Length(buffer) then + SetLength(buffer,Length(buffer)+256); + buffer[len] := aValue; + Inc(len); + end; + + procedure AddClumplet(aTag: byte; const aData: AnsiString); + var i: integer; + begin + if Length(aData) > 255 then + raise EFBWireError.Create('user identification item too long'); + AddByte(aTag); + AddByte(Length(aData)); + for i := 1 to Length(aData) do + AddByte(byte(aData[i])); + end; + + procedure AddMultipart(aTag: byte; const aData: AnsiString); + var remaining, chunk, part, offset, i: integer; + begin + remaining := Length(aData); + part := 0; + offset := 1; + repeat + chunk := remaining; + if chunk > 254 then chunk := 254; + AddByte(aTag); + AddByte(chunk + 1); + AddByte(part); + for i := 0 to chunk - 1 do + AddByte(byte(aData[offset+i])); + Inc(offset,chunk); + Dec(remaining,chunk); + Inc(part); + until remaining <= 0; + end; + +begin + SetLength(buffer,512); + len := 0; + AddClumplet(CNCT_login,FUser); + AddClumplet(CNCT_plugin_name,FAuthPluginName); + AddClumplet(CNCT_plugin_list,sSrp256PluginName + ',' + sSrpPluginName); + AddMultipart(CNCT_specific_data,UpperCase(FSRP.PublicKeyHex)); + {client crypt level - 4 bytes little endian} + AddByte(CNCT_client_crypt); + AddByte(4); + AddByte(ord(aWireCrypt)); + AddByte(0); + AddByte(0); + AddByte(0); + AddClumplet(CNCT_user,GetOSUser); + AddClumplet(CNCT_host,LowerCase(GetHostName)); + AddClumplet(CNCT_user_verification,''); + SetLength(buffer,len); + FXDR.WriteString(buffer); +end; + +procedure TFBWireConnection.ConnectTo(const aHost: AnsiString; aPort: integer; + const aDatabasePath, aUser, aPassword: AnsiString; + aWireCrypt: TWireCryptOption; aTimeout: integer); +const + OfferedProtocols: array[0..7] of cardinal = ( + PROTOCOL_VERSION13, PROTOCOL_VERSION14, PROTOCOL_VERSION15, + PROTOCOL_VERSION16, PROTOCOL_VERSION17, PROTOCOL_VERSION18, + PROTOCOL_VERSION19, PROTOCOL_VERSION20); +var i: integer; + offered: integer; +begin + Disconnect; + FUser := aUser; + FPassword := aPassword; + FAuthData := ''; + FAuthComplete := false; + SetLength(FSessionKey,0); + SetLength(FKeyClumplets,0); + FCryptPlugin := ''; + FAuthPluginName := sSrp256PluginName; + if FSRP <> nil then FreeAndNil(FSRP); + FSRP := TSRPClient.Create; + + FTransport.ConnectTo(aHost,aPort,aTimeout); + FConnected := true; + try + {op_connect} + FXDR.WriteInt32(op_connect); + FXDR.WriteInt32(op_attach); {p_cnct_operation} + FXDR.WriteInt32(CONNECT_VERSION3); + FXDR.WriteInt32(arch_generic); + FXDR.WriteString(aDatabasePath); + offered := 0; + for i := 0 to High(OfferedProtocols) do + if OfferedProtocols[i] <= FMaxProtocol then + Inc(offered); + if offered = 0 then + raise EFBWireError.Create('No protocol version left to offer'); + FXDR.WriteInt32(offered); + SendUserIdentification(aWireCrypt); + for i := 0 to High(OfferedProtocols) do + begin + if OfferedProtocols[i] > FMaxProtocol then continue; + FXDR.WriteUInt32(OfferedProtocols[i]); + FXDR.WriteInt32(arch_generic); + FXDR.WriteInt32(0); {min type} + if FCompression then + {max type - avoid lazy semantics, ask for zlib compression} + FXDR.WriteInt32(ptype_batch_send or pflag_compress) + else + FXDR.WriteInt32(ptype_batch_send); {max type - avoid lazy semantics} + FXDR.WriteInt32((i + 1) * 2); {preference weight} + end; + FXDR.Flush; + + DoAuthHandshake(aWireCrypt); + StartWireEncryption(aWireCrypt); + except + on E: Exception do + begin + FTransport.Disconnect; + FConnected := false; + raise; + end; + end; +end; + +function TFBWireConnection.ComputeProof(const aData: TBytes; + const aPluginName: AnsiString): AnsiString; +var saltLen, keyLen: integer; + salt: TBytes; + serverKeyHex: AnsiString; + proof: TBytes; + proofHash: TSRPProofHash; + i: integer; +begin + if Length(aData) < 4 then + raise EFBWireError.Create('Invalid SRP authentication data from server'); + saltLen := aData[0] or (aData[1] shl 8); + if 2 + saltLen + 2 > Length(aData) then + raise EFBWireError.Create('Invalid SRP salt length from server'); + SetLength(salt,saltLen); + if saltLen > 0 then + Move(aData[2],salt[0],saltLen); + keyLen := aData[2+saltLen] or (aData[3+saltLen] shl 8); + if 4 + saltLen + keyLen > Length(aData) then + raise EFBWireError.Create('Invalid SRP key length from server'); + SetLength(serverKeyHex,keyLen); + for i := 0 to keyLen - 1 do + serverKeyHex[i+1] := AnsiChar(aData[4+saltLen+i]); + + if aPluginName = sSrpPluginName then + proofHash := sphSHA1 + else if aPluginName = sSrp256PluginName then + proofHash := sphSHA256 + else + raise EFBWireError.CreateFmt('Unsupported authentication plugin "%s"', + [aPluginName]); + + proof := FSRP.ClientProof(FUser,FPassword,salt,serverKeyHex,proofHash); + FSessionKey := FSRP.SessionKey; + + Result := ''; + for i := 0 to Length(proof) - 1 do + Result := Result + UpperCase(IntToHex(proof[i],2)); +end; + +procedure TFBWireConnection.AppendKeyClumplets(const aKeys: TBytes); +var oldLen: integer; +begin + if Length(aKeys) = 0 then Exit; + oldLen := Length(FKeyClumplets); + SetLength(FKeyClumplets,oldLen + Length(aKeys)); + Move(aKeys[0],FKeyClumplets[oldLen],Length(aKeys)); +end; + +procedure TFBWireConnection.DoAuthHandshake(aWireCrypt: TWireCryptOption); +var op: integer; + aVersion: cardinal; + aArch, aType: cardinal; + acptData, acptKeys: TBytes; + acptPlugin: AnsiString; + authenticated: integer; + proofHex: AnsiString; + R: TWireResponse; + contData, contKeys: TBytes; + contPlugin, contList: AnsiString; + sentPluginList: boolean; + isCondAccept: boolean; + compressAccepted: boolean; +begin + sentPluginList := true; {already sent in op_connect} + repeat + op := ReadOperation; + case op of + op_reject: + raise EFBWireError.Create('Connection rejected by server - no acceptable protocol'); + + op_response: + begin + {either an error at connect time (e.g. bad login) or the successful + completion of an op_cont_auth exchange} + R := ReadResponseBody; + CheckResponse(R); + {a success response ends the authentication phase} + AppendKeyClumplets(R.Data); + FAuthComplete := true; + end; + + op_accept, op_accept_data, op_cond_accept: + begin + aVersion := FXDR.ReadUInt32; + aArch := FXDR.ReadUInt32; + aType := FXDR.ReadUInt32; + {shorts are sign extended on the wire: mask down. The compression + flag rides above the type bits and must be caught before the + mask discards it.} + compressAccepted := (aType and pflag_compress) <> 0; + FProtocolVersion := aVersion and FB_PROTOCOL_MASK; + FAcceptType := aType and ptype_mask; + if aArch <> arch_generic then + raise EFBWireError.Create('Server accepted non generic architecture'); + if FProtocolVersion < (PROTOCOL_VERSION13 and FB_PROTOCOL_MASK) then + raise EFBWireError.CreateFmt( + 'Server protocol version %d too old - Firebird 3 or later required', + [FProtocolVersion]); + if op = op_accept then + begin + {the accept packet ends here: everything after it is deflated + when both sides asked for compression} + if compressAccepted then + FTransport.EnableCompression; + FAuthComplete := true; + break; + end; + isCondAccept := op = op_cond_accept; + acptData := FXDR.ReadString; + acptPlugin := FXDR.ReadStringAsAnsi; + authenticated := FXDR.ReadInt32; + acptKeys := FXDR.ReadString; + AppendKeyClumplets(acptKeys); + {the accept packet has now been read in full - the server + compresses from the next packet on, and so do we} + if compressAccepted then + FTransport.EnableCompression; + if authenticated = 1 then + begin + FAuthComplete := true; + break; + end; + if acptPlugin <> '' then + FAuthPluginName := acptPlugin; + if Length(acptData) = 0 then + begin + {server wants us to (re)start with the named plugin: send our + public key} + FXDR.WriteInt32(op_cont_auth); + FXDR.WriteString(UpperCase(FSRP.PublicKeyHex)); + FXDR.WriteString(FAuthPluginName); + FXDR.WriteString(''); + FXDR.WriteString(''); + FXDR.Flush; + continue; + end; + proofHex := ComputeProof(acptData,FAuthPluginName); + if isCondAccept then + begin + {authentication must complete before attach} + FXDR.WriteInt32(op_cont_auth); + FXDR.WriteString(proofHex); + FXDR.WriteString(FAuthPluginName); + FXDR.WriteString(''); + FXDR.WriteString(''); + FXDR.Flush; + continue; + end + else + begin + {op_accept_data: the proof travels in the DPB with op_attach} + FAuthData := proofHex; + FAuthComplete := true; + break; + end; + end; + + op_cont_auth: + begin + contData := FXDR.ReadString; + contPlugin := FXDR.ReadStringAsAnsi; + contList := FXDR.ReadStringAsAnsi; + contKeys := FXDR.ReadString; + AppendKeyClumplets(contKeys); + if contPlugin <> '' then + FAuthPluginName := contPlugin; + if Length(contData) = 0 then + begin + FXDR.WriteInt32(op_cont_auth); + FXDR.WriteString(UpperCase(FSRP.PublicKeyHex)); + FXDR.WriteString(FAuthPluginName); + FXDR.WriteString(''); + FXDR.WriteString(''); + FXDR.Flush; + continue; + end; + proofHex := ComputeProof(contData,FAuthPluginName); + FXDR.WriteInt32(op_cont_auth); + FXDR.WriteString(proofHex); + FXDR.WriteString(FAuthPluginName); + FXDR.WriteString(''); + FXDR.WriteString(''); + FXDR.Flush; + end; + + op_crypt_key_callback: + begin + {database crypt key callback - we have no key to offer; reply with + empty data and continue} + FXDR.ReadString; {p_cc_data} + {p_cc_reply present during connect phase} + FXDR.ReadInt32; + FXDR.WriteInt32(op_crypt_key_callback); + FXDR.WriteString(''); + FXDR.WriteInt32(0); + FXDR.Flush; + end; + else + raise EFBWireError.CreateFmt('Unexpected operation %d during connection handshake',[op]); + end; + until FAuthComplete; +end; + +function TFBWireConnection.FindWireCryptPlugin(var aSpecificData: TBytes; + var aKeyType: AnsiString): AnsiString; +var bufPos: integer; + tag: byte; + len: integer; + data: TBytes; + currentKeyType: AnsiString; + candidates: AnsiString; + candidateKeyType: AnsiString; + specificFor: AnsiString; + nameEnd: integer; + chachaIV, chacha64IV: TBytes; + + function BytesToAnsi(const b: TBytes): AnsiString; + var k: integer; + begin + SetLength(Result,Length(b)); + for k := 0 to Length(b)-1 do + Result[k+1] := AnsiChar(b[k]); + end; + + {the plugin list is a space or comma separated list of names - match + whole tokens only so that "ChaCha" does not match "ChaCha64"} + function ListHasPlugin(const aList, aName: AnsiString): boolean; + var token: AnsiString; + i: integer; + c: AnsiChar; + begin + Result := false; + token := ''; + for i := 1 to Length(aList) + 1 do + begin + if i <= Length(aList) then c := aList[i] else c := ' '; + if (c = ' ') or (c = ',') or (c = #9) then + begin + if (token <> '') and SameText(token,aName) then + Exit(true); + token := ''; + end + else + token := token + c; + end; + end; + +begin + Result := ''; + aKeyType := ''; + SetLength(aSpecificData,0); + candidates := ''; + candidateKeyType := ''; + currentKeyType := ''; + SetLength(chachaIV,0); + SetLength(chacha64IV,0); + bufPos := 0; + while bufPos + 1 < Length(FKeyClumplets) do + begin + tag := FKeyClumplets[bufPos]; + len := FKeyClumplets[bufPos+1]; + Inc(bufPos,2); + if bufPos + len > Length(FKeyClumplets) then break; + SetLength(data,len); + if len > 0 then + Move(FKeyClumplets[bufPos],data[0],len); + Inc(bufPos,len); + case tag of + TAG_KEY_TYPE: + currentKeyType := BytesToAnsi(data); + TAG_KEY_PLUGINS: + begin + {the key type is whatever the server called it - typically + "Symmetric" - and must be echoed back in op_crypt} + candidates := candidates + ' ' + BytesToAnsi(data); + if candidateKeyType = '' then + candidateKeyType := currentKeyType; + end; + TAG_PLUGIN_SPECIFIC: + begin + {data = plugin name + #0 + plugin specific data (the IV)} + nameEnd := 0; + while (nameEnd < len) and (data[nameEnd] <> 0) do + Inc(nameEnd); + specificFor := Copy(BytesToAnsi(data),1,nameEnd); + if SameText(specificFor,sChaChaPluginName) then + chachaIV := Copy(data,nameEnd+1,len-nameEnd-1) + else + if SameText(specificFor,sChaCha64PluginName) then + chacha64IV := Copy(data,nameEnd+1,len-nameEnd-1); + end; + end; + end; + aKeyType := candidateKeyType; + if aKeyType = '' then + aKeyType := sSymmetricKeyName; + + {prefer ChaCha64, then ChaCha, then Arc4. The ChaCha plugins need the + server supplied IV (protocol 16 and later); without it use Arc4} + if ListHasPlugin(candidates,sChaCha64PluginName) and (Length(chacha64IV) >= 8) then + begin + Result := sChaCha64PluginName; + aSpecificData := chacha64IV; + end + else + if ListHasPlugin(candidates,sChaChaPluginName) and (Length(chachaIV) >= 16) then + begin + Result := sChaChaPluginName; + aSpecificData := chachaIV; + end + else + if ListHasPlugin(candidates,sArc4PluginName) or (candidates = '') then + Result := sArc4PluginName; +end; + +procedure TFBWireConnection.StartWireEncryption(aWireCrypt: TWireCryptOption); +var plugin: AnsiString; + keyType: AnsiString; + specific: TBytes; + key32: TBytes; + nonce: TBytes; + counter: QWord; + R: TWireResponse; +begin + if (aWireCrypt = wcDisabled) or (Length(FSessionKey) = 0) then + Exit; + if FProtocolVersion < (PROTOCOL_VERSION14 and FB_PROTOCOL_MASK) then + Exit; + plugin := FindWireCryptPlugin(specific,keyType); + if plugin = '' then + begin + if aWireCrypt = wcRequired then + raise EFBWireError.Create('Wire encryption required but no usable plugin'); + Exit; + end; + + {send op_crypt in plaintext} + FXDR.WriteInt32(op_crypt); + FXDR.WriteString(plugin); + FXDR.WriteString(keyType); {the key type the server advertised} + FXDR.Flush; + + {the server encrypts everything it sends after it receives op_crypt} + if plugin = sArc4PluginName then + begin + FTransport.EnableRecvCipher(TRC4WireCipher.Create(FSessionKey)); + R := ReceiveResponse; + CheckResponse(R); + FTransport.EnableSendCipher(TRC4WireCipher.Create(FSessionKey)); + end + else + begin + {ChaCha plugins use SHA256(session key) and the server supplied IV} + key32 := SHA256DigestToBytes(TSHA256.Digest(FSessionKey)); + if plugin = sChaCha64PluginName then + begin + nonce := Copy(specific,0,8); + counter := 0; + end + else + begin + nonce := Copy(specific,0,12); + {bytes 12..15: big endian initial counter} + counter := (QWord(specific[12]) shl 24) or (QWord(specific[13]) shl 16) or + (QWord(specific[14]) shl 8) or QWord(specific[15]); + end; + FTransport.EnableRecvCipher(TChaChaWireCipher.Create(key32,nonce,counter)); + R := ReceiveResponse; + CheckResponse(R); + FTransport.EnableSendCipher(TChaChaWireCipher.Create(key32,nonce,counter)); + end; + FCryptPlugin := plugin; +end; + +function TFBWireConnection.ReadOperation: integer; +begin + repeat + Result := FXDR.ReadInt32; + if Result = op_dummy then + continue; + if Result = op_response_piggyback then + begin + {unsolicited response - consume and discard} + ReadResponseBody; + continue; + end; + if Result = op_inline_blob then + begin + {a protocol 19 server pushes small blobs ahead of the rows that + reference them - cache and carry on with whatever the caller was + actually waiting for} + ReadInlineBlob; + continue; + end; + break; + until false; +end; + +procedure TFBWireConnection.ReadInlineBlob; +var aTrHandle: integer; + aBlobID: Int64; + info, data: TBytes; +begin + aTrHandle := FXDR.ReadInt32; + aBlobID := FXDR.ReadInt64; + info := FXDR.ReadString; + data := FXDR.ReadString; {the segmented stream, as one counted blob} + if assigned(FOnInlineBlob) then + FOnInlineBlob(aTrHandle,aBlobID,info,data); +end; + +function TFBWireConnection.ReadStatusVector: TWireStatusVector; +var code: integer; + count: integer; + + procedure Append(aKind: integer; aInt: Int64; const aStr: AnsiString); + begin + if count >= Length(Result) then + SetLength(Result,Length(Result)+8); + Result[count].Kind := aKind; + Result[count].IntValue := aInt; + Result[count].StrValue := aStr; + Inc(count); + end; + +begin + SetLength(Result,8); + count := 0; + repeat + code := FXDR.ReadInt32; + case code of + isc_arg_end: + break; + isc_arg_gds, isc_arg_number, isc_arg_unix, isc_arg_next_mach, + isc_arg_win32, isc_arg_warning: + Append(code,FXDR.ReadInt32,''); + isc_arg_string, isc_arg_interpreted, isc_arg_sql_state: + Append(code,0,FXDR.ReadStringAsAnsi); + else + raise EFBWireError.CreateFmt('Invalid status vector item %d',[code]); + end; + until false; + SetLength(Result,count); +end; + +function TFBWireConnection.ReadResponseBody: TWireResponse; +begin + Result.ObjectHandle := FXDR.ReadInt32; + Result.ObjectID := FXDR.ReadInt64; + Result.Data := FXDR.ReadString; + Result.Status := ReadStatusVector; +end; + +function TFBWireConnection.ReceiveResponse: TWireResponse; +var op: integer; +begin + op := ReadOperation; + if op <> op_response then + raise EFBWireError.CreateFmt('Expected op_response, received %d',[op]); + Result := ReadResponseBody; +end; + +procedure TFBWireConnection.CheckResponse(const R: TWireResponse); +begin + if R.HasError then + raise EFBWireProtocolError.CreateFromStatus(R.Status); +end; + +function TFBWireConnection.ReceiveAndCheckResponse: TWireResponse; +begin + Result := ReceiveResponse; + CheckResponse(Result); +end; + +{--- attachments ---} + +function TFBWireConnection.AttachDatabase(const aDatabasePath: AnsiString; + DPB: TBytes): integer; +begin + FXDR.WriteInt32(op_attach); + FXDR.WriteInt32(0); + FXDR.WriteString(aDatabasePath); + FXDR.WriteString(DPB); + FXDR.Flush; + Result := ReceiveAndCheckResponse.ObjectHandle; +end; + +function TFBWireConnection.CreateDatabase(const aDatabasePath: AnsiString; + DPB: TBytes): integer; +begin + FXDR.WriteInt32(op_create); + FXDR.WriteInt32(0); + FXDR.WriteString(aDatabasePath); + FXDR.WriteString(DPB); + FXDR.Flush; + Result := ReceiveAndCheckResponse.ObjectHandle; +end; + +procedure TFBWireConnection.DetachDatabase(aDbHandle: integer); +begin + FXDR.WriteInt32(op_detach); + FXDR.WriteInt32(aDbHandle); + FXDR.Flush; + ReceiveAndCheckResponse; +end; + +procedure TFBWireConnection.DropDatabase(aDbHandle: integer); +begin + FXDR.WriteInt32(op_drop_database); + FXDR.WriteInt32(aDbHandle); + FXDR.Flush; + ReceiveAndCheckResponse; +end; + +{--- transactions ---} + +function TFBWireConnection.StartTransaction(aDbHandle: integer; TPB: TBytes): integer; +begin + FXDR.WriteInt32(op_transaction); + FXDR.WriteInt32(aDbHandle); + FXDR.WriteString(TPB); + FXDR.Flush; + Result := ReceiveAndCheckResponse.ObjectHandle; +end; + +procedure TFBWireConnection.Commit(aTrHandle: integer); +begin + FXDR.WriteInt32(op_commit); + FXDR.WriteInt32(aTrHandle); + FXDR.Flush; + ReceiveAndCheckResponse; +end; + +procedure TFBWireConnection.CommitRetaining(aTrHandle: integer); +begin + FXDR.WriteInt32(op_commit_retaining); + FXDR.WriteInt32(aTrHandle); + FXDR.Flush; + ReceiveAndCheckResponse; +end; + +procedure TFBWireConnection.Rollback(aTrHandle: integer); +begin + FXDR.WriteInt32(op_rollback); + FXDR.WriteInt32(aTrHandle); + FXDR.Flush; + ReceiveAndCheckResponse; +end; + +procedure TFBWireConnection.RollbackRetaining(aTrHandle: integer); +begin + FXDR.WriteInt32(op_rollback_retaining); + FXDR.WriteInt32(aTrHandle); + FXDR.Flush; + ReceiveAndCheckResponse; +end; + +procedure TFBWireConnection.PrepareTransaction(aTrHandle: integer); +begin + FXDR.WriteInt32(op_prepare); + FXDR.WriteInt32(aTrHandle); + FXDR.Flush; + ReceiveAndCheckResponse; +end; + +{--- DSQL ---} + +function TFBWireConnection.AllocateStatement(aDbHandle: integer): integer; +begin + FXDR.WriteInt32(op_allocate_statement); + FXDR.WriteInt32(aDbHandle); + FXDR.Flush; + Result := ReceiveAndCheckResponse.ObjectHandle; +end; + +function TFBWireConnection.PrepareStatement(aTrHandle, aStmtHandle: integer; + aDialect: integer; const sql: AnsiString; const aInfoItems: TBytes; + aBufferLength: integer): TBytes; +begin + FXDR.WriteInt32(op_prepare_statement); + FXDR.WriteInt32(aTrHandle); + FXDR.WriteInt32(aStmtHandle); + FXDR.WriteInt32(aDialect); + FXDR.WriteString(sql); + FXDR.WriteString(aInfoItems); + FXDR.WriteInt32(aBufferLength); + if FProtocolVersion >= (PROTOCOL_VERSION20 and FB_PROTOCOL_MASK) then + FXDR.WriteInt32(0); {p_sqlst_flags - no special prepare flags} + FXDR.Flush; + Result := ReceiveAndCheckResponse.Data; +end; + +procedure TFBWireConnection.ExecuteStatement(aStmtHandle, aTrHandle: integer; + const aParamFormat: TWireMessageFormat; aParamBuffer: PByte; + aTimeout: cardinal; aCursorFlags: cardinal; aInlineBlobLimit: cardinal); +var blr: TBytes; +begin + FXDR.WriteInt32(op_execute); + FXDR.WriteInt32(aStmtHandle); + FXDR.WriteInt32(aTrHandle); + if Length(aParamFormat) > 0 then + begin + blr := BuildMessageBlr(aParamFormat); + FXDR.WriteString(blr); + FXDR.WriteInt32(0); {message number} + FXDR.WriteInt32(1); {messages follow} + XDREncodeMessage(FXDR,aParamFormat,aParamBuffer); + end + else + begin + FXDR.WriteString(''); + FXDR.WriteInt32(0); + FXDR.WriteInt32(0); + end; + if FProtocolVersion >= (PROTOCOL_VERSION16 and FB_PROTOCOL_MASK) then + FXDR.WriteUInt32(aTimeout); {p_sqldata_timeout} + if FProtocolVersion >= (PROTOCOL_VERSION18 and FB_PROTOCOL_MASK) then + FXDR.WriteUInt32(aCursorFlags); {p_sqldata_cursor_flags} + if FProtocolVersion >= (PROTOCOL_VERSION19 and FB_PROTOCOL_MASK) then + FXDR.WriteUInt32(aInlineBlobLimit); {p_sqldata_inline_blob_size} + FXDR.Flush; + ReceiveAndCheckResponse; +end; + +procedure TFBWireConnection.ExecuteStatement2(aStmtHandle, aTrHandle: integer; + const aParamFormat: TWireMessageFormat; aParamBuffer: PByte; + const aOutFormat: TWireMessageFormat; aOutBuffer: PByte; + aTimeout: cardinal; aInlineBlobLimit: cardinal); +var blr: TBytes; + op: integer; + messages: integer; + R: TWireResponse; +begin + FXDR.WriteInt32(op_execute2); + FXDR.WriteInt32(aStmtHandle); + FXDR.WriteInt32(aTrHandle); + if Length(aParamFormat) > 0 then + begin + blr := BuildMessageBlr(aParamFormat); + FXDR.WriteString(blr); + FXDR.WriteInt32(0); + FXDR.WriteInt32(1); + XDREncodeMessage(FXDR,aParamFormat,aParamBuffer); + end + else + begin + FXDR.WriteString(''); + FXDR.WriteInt32(0); + FXDR.WriteInt32(0); + end; + blr := BuildMessageBlr(aOutFormat); + FXDR.WriteString(blr); + FXDR.WriteInt32(0); {out message number} + if FProtocolVersion >= (PROTOCOL_VERSION16 and FB_PROTOCOL_MASK) then + FXDR.WriteUInt32(aTimeout); {p_sqldata_timeout} + if FProtocolVersion >= (PROTOCOL_VERSION18 and FB_PROTOCOL_MASK) then + FXDR.WriteUInt32(0); {p_sqldata_cursor_flags - a singleton has no cursor} + if FProtocolVersion >= (PROTOCOL_VERSION19 and FB_PROTOCOL_MASK) then + FXDR.WriteUInt32(aInlineBlobLimit); {p_sqldata_inline_blob_size} + FXDR.Flush; + + op := ReadOperation; + if op = op_sql_response then + begin + messages := FXDR.ReadInt32; + if messages > 0 then + XDRDecodeMessage(FXDR,aOutFormat,aOutBuffer); + ReceiveAndCheckResponse; + end + else + if op = op_response then + begin + {error before the sql response} + R := ReadResponseBody; + CheckResponse(R); + end + else + raise EFBWireError.CreateFmt('Unexpected operation %d in execute2 response',[op]); +end; + +function TFBWireConnection.FetchRow(aStmtHandle: integer; + const aOutFormat: TWireMessageFormat; aOutBuffer: PByte; + aFetchCount: integer; var aState: TWireCursorState): boolean; +var op: integer; + status, messages: integer; + blr: TBytes; + R: TWireResponse; + rowSize: cardinal; + rowCount: integer; + row: TBytes; +begin + Result := false; + {a row left over from the previous batch?} + if aState.NextRow < Length(aState.Rows) then + begin + Move(aState.Rows[aState.NextRow][0],aOutBuffer^,Length(aState.Rows[aState.NextRow])); + Inc(aState.NextRow); + Exit(true); + end; + if aState.EndOfCursor then Exit; + if aFetchCount < 1 then + aFetchCount := 1; + + rowSize := MessageBufferSize(aOutFormat); + SetLength(aState.Rows,0); + aState.NextRow := 0; + rowCount := 0; + + FXDR.WriteInt32(op_fetch); + FXDR.WriteInt32(aStmtHandle); + blr := BuildMessageBlr(aOutFormat); + FXDR.WriteString(blr); + FXDR.WriteInt32(0); {message number} + FXDR.WriteInt32(aFetchCount); {rows requested} + FXDR.Flush; + + {drain the whole batch - nothing else may be sent until it is complete} + repeat + op := ReadOperation; + if op = op_response then + begin + {an error terminated the fetch} + R := ReadResponseBody; + aState.EndOfCursor := true; + CheckResponse(R); + break; + end; + if op <> op_fetch_response then + raise EFBWireError.CreateFmt('Unexpected operation %d in fetch response',[op]); + status := FXDR.ReadInt32; + messages := FXDR.ReadInt32; + if status = FETCH_status_eof then + begin + aState.EndOfCursor := true; + break; + end; + if messages = 0 then + break; {batch complete, more rows may be available} + SetLength(row,rowSize); + if rowSize > 0 then + FillChar(row[0],rowSize,0); + XDRDecodeMessage(FXDR,aOutFormat,@row[0]); + Inc(rowCount); + SetLength(aState.Rows,rowCount); + aState.Rows[rowCount-1] := row; + row := nil; + until false; + + if Length(aState.Rows) = 0 then + Exit; + Move(aState.Rows[0][0],aOutBuffer^,Length(aState.Rows[0])); + aState.NextRow := 1; + Result := true; +end; + +function TFBWireConnection.FetchRowScroll(aStmtHandle: integer; + const aOutFormat: TWireMessageFormat; aOutBuffer: PByte; + aDirection: integer; aPosition: integer; + var aState: TWireCursorState): boolean; +var op: integer; + status, messages: integer; + blr: TBytes; + R: TWireResponse; + rowSize: cardinal; + row: TBytes; +begin + Result := false; + if FProtocolVersion < (PROTOCOL_VERSION18 and FB_PROTOCOL_MASK) then + raise EFBWireError.Create('op_fetch_scroll needs protocol 18 or later'); + + {any rows read ahead describe the cursor position this scroll abandons} + ResetCursorState(aState); + rowSize := MessageBufferSize(aOutFormat); + + FXDR.WriteInt32(op_fetch_scroll); + FXDR.WriteInt32(aStmtHandle); + blr := BuildMessageBlr(aOutFormat); + FXDR.WriteString(blr); + FXDR.WriteInt32(0); {message number} + FXDR.WriteInt32(1); {one row - no read ahead on a scroll} + FXDR.WriteInt32(aDirection); + FXDR.WriteInt32(aPosition); + FXDR.Flush; + + {drain the batch exactly as FetchRow does - the server terminates it + with a packet whose message count is zero, status 100 meaning no row + in the requested direction} + repeat + op := ReadOperation; + if op = op_response then + begin + {an error terminated the fetch} + R := ReadResponseBody; + CheckResponse(R); + break; + end; + if op <> op_fetch_response then + raise EFBWireError.CreateFmt('Unexpected operation %d in fetch response',[op]); + status := FXDR.ReadInt32; + messages := FXDR.ReadInt32; + if messages = 0 then + break; + SetLength(row,rowSize); + if rowSize > 0 then + FillChar(row[0],rowSize,0); + XDRDecodeMessage(FXDR,aOutFormat,@row[0]); + if not Result then + begin + Move(row[0],aOutBuffer^,Length(row)); + Result := true; + end; + row := nil; + until false; + {status 100 - no row in the requested direction - needs no bookkeeping: + the state was reset above and any following sequential fetch starts a + fresh batch. The cursor itself sits at BOF or EOF, which the statement + layer tracks.} +end; + +procedure TFBWireConnection.FreeStatement(aStmtHandle: integer; aOption: integer); +begin + FXDR.WriteInt32(op_free_statement); + FXDR.WriteInt32(aStmtHandle); + FXDR.WriteInt32(aOption); + FXDR.Flush; + ReceiveAndCheckResponse; +end; + +procedure TFBWireConnection.SetCursorName(aStmtHandle: integer; + const aName: AnsiString); +begin + FXDR.WriteInt32(op_set_cursor); + FXDR.WriteInt32(aStmtHandle); + FXDR.WriteString(aName + #0); + FXDR.WriteInt32(0); + FXDR.Flush; + ReceiveAndCheckResponse; +end; + +procedure TFBWireConnection.ExecImmediate(aTrHandle, aDbHandle: integer; + aDialect: integer; const sql: AnsiString); +begin + FXDR.WriteInt32(op_exec_immediate); + FXDR.WriteInt32(aTrHandle); + FXDR.WriteInt32(0); {statement} + FXDR.WriteInt32(aDialect); + FXDR.WriteString(sql); + FXDR.WriteString(''); {items} + FXDR.WriteInt32(0); {buffer length} + if FProtocolVersion >= (PROTOCOL_VERSION20 and FB_PROTOCOL_MASK) then + FXDR.WriteInt32(0); {p_sqlst_flags} + FXDR.Flush; + ReceiveAndCheckResponse; +end; + +{--- information calls ---} + +function TFBWireConnection.GetInfo(aOperation: integer; aHandle: integer; + const aItems: TBytes; aBufferLength: integer): TBytes; +begin + FXDR.WriteInt32(aOperation); + FXDR.WriteInt32(aHandle); + FXDR.WriteInt32(0); {incarnation} + FXDR.WriteString(aItems); + FXDR.WriteInt32(aBufferLength); + FXDR.Flush; + Result := ReceiveAndCheckResponse.Data; +end; + +{--- blobs ---} + +procedure TFBWireConnection.CreateBlob(aTrHandle: integer; const BPB: TBytes; + var aBlobHandle: integer; var aBlobID: Int64); +var R: TWireResponse; +begin + FXDR.WriteInt32(op_create_blob2); + FXDR.WriteString(BPB); + FXDR.WriteInt32(aTrHandle); + FXDR.WriteInt64(0); + FXDR.Flush; + R := ReceiveAndCheckResponse; + aBlobHandle := R.ObjectHandle; + aBlobID := R.ObjectID; +end; + +function TFBWireConnection.OpenBlob(aTrHandle: integer; const BPB: TBytes; + aBlobID: Int64): integer; +begin + FXDR.WriteInt32(op_open_blob2); + FXDR.WriteString(BPB); + FXDR.WriteInt32(aTrHandle); + FXDR.WriteInt64(aBlobID); + FXDR.Flush; + Result := ReceiveAndCheckResponse.ObjectHandle; +end; + +function TFBWireConnection.GetSegment(aBlobHandle: integer; + aBufferLength: integer; var aEOB: boolean): TBytes; +var R: TWireResponse; + pos, segLen, outLen: integer; +begin + FXDR.WriteInt32(op_get_segment); + FXDR.WriteInt32(aBlobHandle); + FXDR.WriteInt32(aBufferLength); + FXDR.WriteString(''); {no data for get} + FXDR.Flush; + R := ReceiveAndCheckResponse; + {p_resp_object: 0 = more, 1 = fragment, 2 = end of blob} + aEOB := R.ObjectHandle = 2; + {response data contains segments in 2 byte little endian length prefixed + form - concatenate them} + SetLength(Result,Length(R.Data)); + pos := 0; + outLen := 0; + while pos + 2 <= Length(R.Data) do + begin + segLen := R.Data[pos] or (R.Data[pos+1] shl 8); + Inc(pos,2); + if pos + segLen > Length(R.Data) then + segLen := Length(R.Data) - pos; + if segLen > 0 then + begin + Move(R.Data[pos],Result[outLen],segLen); + Inc(outLen,segLen); + Inc(pos,segLen); + end; + end; + SetLength(Result,outLen); +end; + +procedure TFBWireConnection.PutSegment(aBlobHandle: integer; const aData: TBytes); +begin + FXDR.WriteInt32(op_put_segment); + FXDR.WriteInt32(aBlobHandle); + FXDR.WriteInt32(Length(aData)); + FXDR.WriteString(aData); + FXDR.Flush; + ReceiveAndCheckResponse; +end; + +procedure TFBWireConnection.CloseBlob(aBlobHandle: integer); +begin + FXDR.WriteInt32(op_close_blob); + FXDR.WriteInt32(aBlobHandle); + FXDR.Flush; + ReceiveAndCheckResponse; +end; + +procedure TFBWireConnection.CancelBlob(aBlobHandle: integer); +begin + FXDR.WriteInt32(op_cancel_blob); + FXDR.WriteInt32(aBlobHandle); + FXDR.Flush; + ReceiveAndCheckResponse; +end; + +{--- array slices ---} + +{Both packets are P_SLC: transaction, array id quad, slice length, the + SDL, a (here always empty) parameter vector, then the slice data - which + for the request side of op_get_slice is just a zero length. The reply to + op_get_slice is op_slice; op_put_slice gets a normal op_response with the + array id in ObjectID. See op_get_slice/op_put_slice in + src/remote/protocol.cpp.} + +function TFBWireConnection.GetSlice(aTrHandle: integer; aArrayID: Int64; + const SDL: TBytes; const aLayout: TWireSliceLayout; aBuffer: PByte): integer; +var op: integer; + R: TWireResponse; + wireLen: cardinal; +begin + FXDR.WriteInt32(op_get_slice); + FXDR.WriteInt32(aTrHandle); + FXDR.WriteInt64(aArrayID); + FXDR.WriteInt32(SliceLength(aLayout)); + FXDR.WriteString(SDL); + FXDR.WriteInt32(0); {p_slc_parameters: no longs} + FXDR.WriteInt32(0); {slice data: none on a get request} + FXDR.Flush; + op := ReadOperation; + if op = op_response then + begin + {an error - a success op_response here would be a protocol violation} + R := ReadResponseBody; + CheckResponse(R); + raise EFBWireError.Create('Unexpected op_response to op_get_slice'); + end; + if op <> op_slice then + raise EFBWireError.CreateFmt('Unexpected operation %d in get_slice response',[op]); + Result := FXDR.ReadInt32; {p_slr_length} + wireLen := FXDR.ReadUInt32; {lstr_length} + XDRDecodeSlice(FXDR,aLayout,aBuffer,wireLen); +end; + +function TFBWireConnection.PutSlice(aTrHandle: integer; aArrayID: Int64; + const SDL: TBytes; const aLayout: TWireSliceLayout; aBuffer: PByte): Int64; +begin + FXDR.WriteInt32(op_put_slice); + FXDR.WriteInt32(aTrHandle); + FXDR.WriteInt64(aArrayID); + FXDR.WriteInt32(SliceLength(aLayout)); + FXDR.WriteString(SDL); + FXDR.WriteInt32(0); {p_slc_parameters: no longs} + FXDR.WriteInt32(SliceLength(aLayout)); {lstr_length prefix of the data} + XDREncodeSlice(FXDR,aLayout,aBuffer); + FXDR.Flush; + Result := ReceiveAndCheckResponse.ObjectID; +end; + +{--- events ---} + +function TFBWireConnection.ConnectRequest(aDbHandle: integer): integer; +var R: TWireResponse; +begin + FXDR.WriteInt32(op_connect_request); + FXDR.WriteInt32(P_REQ_async); + FXDR.WriteInt32(aDbHandle); + FXDR.WriteInt32(0); {p_req_partner} + FXDR.Flush; + R := ReceiveAndCheckResponse; + {the response data is the server's sockaddr. The port is a big endian + 16 bit value at offset 2 for both AF_INET and AF_INET6.} + if Length(R.Data) < 4 then + raise EFBWireError.Create('op_connect_request returned no address'); + Result := (integer(R.Data[2]) shl 8) or R.Data[3]; +end; + +procedure TFBWireConnection.QueEvents(aDbHandle: integer; const aEPB: TBytes; + aEventID: integer); +begin + FXDR.WriteInt32(op_que_events); + FXDR.WriteInt32(aDbHandle); + FXDR.WriteString(aEPB); + FXDR.WriteInt32(0); {p_event_ast - parsed but ignored} + FXDR.WriteInt32(0); {p_event_arg - ditto} + FXDR.WriteInt32(aEventID); + FXDR.Flush; + ReceiveAndCheckResponse; +end; + +procedure TFBWireConnection.CancelEvents(aDbHandle, aEventID: integer); +begin + FXDR.WriteInt32(op_cancel_events); + FXDR.WriteInt32(aDbHandle); + FXDR.WriteInt32(aEventID); + FXDR.Flush; + ReceiveAndCheckResponse; +end; + +{--- services ---} + +function TFBWireConnection.ServiceAttach(const aServiceName: AnsiString; + SPB: TBytes): integer; +begin + FXDR.WriteInt32(op_service_attach); + FXDR.WriteInt32(0); + FXDR.WriteString(aServiceName); + FXDR.WriteString(SPB); + FXDR.Flush; + Result := ReceiveAndCheckResponse.ObjectHandle; +end; + +procedure TFBWireConnection.ServiceDetach(aSvcHandle: integer); +begin + FXDR.WriteInt32(op_service_detach); + FXDR.WriteInt32(aSvcHandle); + FXDR.Flush; + ReceiveAndCheckResponse; +end; + +procedure TFBWireConnection.ServiceStart(aSvcHandle: integer; const aItems: TBytes); +begin + FXDR.WriteInt32(op_service_start); + FXDR.WriteInt32(aSvcHandle); + FXDR.WriteInt32(0); + FXDR.WriteString(aItems); + FXDR.Flush; + ReceiveAndCheckResponse; +end; + +function TFBWireConnection.ServiceQuery(aSvcHandle: integer; const aSendItems, + aRecvItems: TBytes; aBufferLength: integer): TBytes; +begin + FXDR.WriteInt32(op_service_info); + FXDR.WriteInt32(aSvcHandle); + FXDR.WriteInt32(0); + FXDR.WriteString(aSendItems); + FXDR.WriteString(aRecvItems); + FXDR.WriteInt32(aBufferLength); + FXDR.Flush; + Result := ReceiveAndCheckResponse.Data; +end; + +{--- batches (protocol 16) ---} + +procedure TFBWireConnection.BatchCreate(aStmtHandle: integer; + const aFormat: TWireMessageFormat; aMsgLen: cardinal; const aPB: TBytes); +var blr: TBytes; +begin + if FProtocolVersion < (PROTOCOL_VERSION16 and FB_PROTOCOL_MASK) then + raise EFBWireError.Create('the batch operations need protocol 16 or later'); + FXDR.WriteInt32(op_batch_create); + FXDR.WriteInt32(aStmtHandle); + blr := BuildMessageBlr(aFormat); + FXDR.WriteString(blr); + FXDR.WriteUInt32(aMsgLen); + FXDR.WriteString(aPB); + FXDR.Flush; + ReceiveAndCheckResponse; +end; + +procedure TFBWireConnection.BatchMsg(aStmtHandle: integer; + const aFormat: TWireMessageFormat; const aRows: array of TBytes); +var i: integer; +begin + FXDR.WriteInt32(op_batch_msg); + FXDR.WriteInt32(aStmtHandle); + FXDR.WriteUInt32(Length(aRows)); + for i := 0 to High(aRows) do + XDREncodeMessage(FXDR,aFormat,@aRows[i][0]); + FXDR.Flush; + ReceiveAndCheckResponse; +end; + +procedure TFBWireConnection.BatchRegBlob(aStmtHandle: integer; + aExistingID, aBatchID: Int64); +begin + FXDR.WriteInt32(op_batch_regblob); + FXDR.WriteInt32(aStmtHandle); + FXDR.WriteInt64(aExistingID); + FXDR.WriteInt64(aBatchID); + FXDR.Flush; + ReceiveAndCheckResponse; +end; + +function TFBWireConnection.BatchExec(aStmtHandle, aTrHandle: integer): TWireBatchCS; +var op: integer; + R: TWireResponse; + recCount, updates, vectors, errors: cardinal; + i: cardinal; + pos: cardinal; +begin + SetLength(Result.States,0); + SetLength(Result.StatusVectors,0); + FXDR.WriteInt32(op_batch_exec); + FXDR.WriteInt32(aStmtHandle); + FXDR.WriteInt32(aTrHandle); + FXDR.Flush; + op := ReadOperation; + if op = op_response then + begin + {an error before execution} + R := ReadResponseBody; + CheckResponse(R); + raise EFBWireError.Create('Unexpected op_response to op_batch_exec'); + end; + if op <> op_batch_cs then + raise EFBWireError.CreateFmt('Unexpected operation %d in batch_exec response',[op]); + + FXDR.ReadInt32; {statement handle} + recCount := FXDR.ReadUInt32; + updates := FXDR.ReadUInt32; + vectors := FXDR.ReadUInt32; + errors := FXDR.ReadUInt32; + + {with no update vector every row processed reports "success, no info"} + if updates = 0 then + begin + SetLength(Result.States,recCount); + for i := 1 to recCount do + Result.States[i-1] := BATCH_SUCCESS_NO_INFO; + end + else + begin + SetLength(Result.States,updates); + for i := 1 to updates do + Result.States[i-1] := FXDR.ReadInt32; + end; + SetLength(Result.StatusVectors,Length(Result.States)); + + {failed rows with a status vector} + for i := 1 to vectors do + begin + pos := FXDR.ReadUInt32; + if pos < cardinal(Length(Result.States)) then + begin + Result.States[pos] := BATCH_EXECUTE_FAILED; + Result.StatusVectors[pos] := ReadStatusVector; + end + else + ReadStatusVector; {out of range - consume and discard} + end; + + {failed rows reported without a vector} + for i := 1 to errors do + begin + pos := FXDR.ReadUInt32; + if pos < cardinal(Length(Result.States)) then + Result.States[pos] := BATCH_EXECUTE_FAILED; + end; +end; + +procedure TFBWireConnection.BatchRelease(aStmtHandle: integer); +begin + FXDR.WriteInt32(op_batch_rls); + FXDR.WriteInt32(aStmtHandle); + FXDR.Flush; + ReceiveAndCheckResponse; +end; + +procedure TFBWireConnection.BatchCancel(aStmtHandle: integer); +begin + FXDR.WriteInt32(op_batch_cancel); + FXDR.WriteInt32(aStmtHandle); + FXDR.Flush; + ReceiveAndCheckResponse; +end; + +{--- misc ---} + +procedure TFBWireConnection.Ping; +begin + FXDR.WriteInt32(op_ping); + FXDR.Flush; + ReceiveAndCheckResponse; +end; + +procedure TFBWireConnection.SendCancel(aKind: integer); +var pkt: array[0..7] of byte; +begin + {assembled by hand and sent through SendDirect rather than the shared + XDR buffer: the whole point is that another thread may be mid exchange} + pkt[0] := (op_cancel shr 24) and $FF; + pkt[1] := (op_cancel shr 16) and $FF; + pkt[2] := (op_cancel shr 8) and $FF; + pkt[3] := op_cancel and $FF; + pkt[4] := (aKind shr 24) and $FF; + pkt[5] := (aKind shr 16) and $FF; + pkt[6] := (aKind shr 8) and $FF; + pkt[7] := aKind and $FF; + FTransport.SendDirect(pkt,8); +end; + +end. diff --git a/client/wire/FBWireSRP.pas b/client/wire/FBWireSRP.pas new file mode 100644 index 00000000..7b4d6ba6 --- /dev/null +++ b/client/wire/FBWireSRP.pas @@ -0,0 +1,277 @@ +(* + * Firebird Interface (fbintf). The fbintf components provide a set of + * Pascal language bindings for the Firebird API. + * + * This file is part of the pure Pascal wire protocol implementation + * (no fbclient required) and is subject to the Initial Developer's + * Public License Version 1.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy + * of the License here: + * + * http://www.firebirdsql.org/index.php?op=doc&id=idpl + * + * Software distributed under the License is distributed on an "AS + * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or + * implied. See the License for the specific language governing rights + * and limitations under the License. + * + * The Initial Developer of the Original Code is MWA Software + * (http://www.mwasoftware.co.uk). + * + * All Rights Reserved. + * + * Contributor(s): ______________________________________. + * +*) +unit FBWireSRP; + +{ Client side of the SRP-6a authentication used by the Firebird Srp and + Srp256 authentication plugins (also Srp384/Srp512 would only differ in + the proof hash, which is parameterised here). + + IMPORTANT: the Firebird engine implementation deviates from the SRP + specification in several ways and this unit deliberately mirrors those + deviations (they are required for interoperability): + + - The exponent (a + u*x) is reduced modulo N (the spec says exponents + are used unreduced, or reduced mod N-1). + - The client proof is M = H(n1, n2, salt, A, B, K) where + n1 = H(N)^H(g) mod N (a modPow, where the SRP spec has H(N) xor H(g)) + and n2 = H(uppercase(user)). + - k = SHA1(pad128(N), pad128(g)) with both arguments left padded to + 128 bytes, whereas A, B, S, and the M components are hashed in + minimal ("stripped") big-endian form without padding. + - The session key K = SHA1(S) always uses SHA-1, even for Srp256; only + the client/server proofs use the plugin's hash. + + The reference implementation is src/auth/SecureRemotePassword in the + Firebird source tree. +} + +{$IFDEF FPC} +{$mode delphi} +{$codepage UTF8} +{$interfaces COM} +{$ENDIF} + +interface + +uses + Classes, SysUtils, FBWireBigInt, FBWireCrypto; + +const + sSrpPluginName = 'Srp'; {SHA-1 proof} + sSrp256PluginName = 'Srp256'; {SHA-256 proof} + SRP_KEY_SIZE = 128; {bytes in N} + SRP_SALT_SIZE = 32; + +type + TSRPProofHash = (sphSHA1, sphSHA256); + + { TSRPClient } + + TSRPClient = class + private + FN: TBigInt; {group prime} + Fg: TBigInt; {generator} + Fk: TBigInt; {multiplier k = SHA1(pad(N), pad(g))} + FPrivKey: TBigInt; {client private key} + FPubKey: TBigInt; {client public key A = g^a mod N} + FSessionKey: TBytes; {K = SHA1(S), 20 bytes} + function StrippedBytes(const aValue: TBigInt): TBytes; + function HashToBigInt(const aDigest: TBytes): TBigInt; + public + {Generates a random client key pair. aPrivateKeyOverride is for + testing only - pass an empty string in production use.} + constructor Create(const aPrivateKeyOverride: AnsiString = ''); + {client public key as minimal big-endian hex - sent to the server in + the connect user identification data} + function PublicKeyHex: AnsiString; + {Computes the session key and client proof from the server challenge. + aUser: the login name (will be upper cased as required). + aSalt: the user's salt, raw bytes as received from the server. + aServerKeyHex: the server public key B as a hex string. + Returns the client proof M to be sent in op_cont_auth, as raw bytes + (send it hex encoded).} + function ClientProof(aUser, aPassword: AnsiString; const aSalt: TBytes; + aServerKeyHex: AnsiString; + aProofHash: TSRPProofHash): TBytes; + {20 byte session key - available after ClientProof; used for wire + encryption (Arc4 key directly; SHA256(K) for ChaCha)} + property SessionKey: TBytes read FSessionKey; + end; + +function GetRandomBytes(aCount: integer): TBytes; + +implementation + +const + {Firebird's SRP group: 1024 bit prime (from srp.cpp) and generator 2} + sSRPPrime = + 'E67D2E994B2F900C3F41F08F5BB2627ED0D49EE1FE767A52EFCD565CD6E76881' + + '2C3E1E9CE8F0A8BEA6CB13CD29DDEBF7A96D4A93B55D488DF099A15C89DCB064' + + '0738EB2CBDD9A8F7BAB561AB1B0DC1C6CDABF303264A08D1BCA932D1F1EE428B' + + '619D970F342ABA9A65793B8B2F041AE5364350C16F735F56ECBCA87BD57B29E7'; + +function GetRandomBytes(aCount: integer): TBytes; +{$IFDEF UNIX} +var F: File; + BytesRead: integer; +{$ENDIF} +var i: integer; +begin + SetLength(Result,aCount); + {$IFDEF UNIX} + AssignFile(F,'/dev/urandom'); + Reset(F,1); + try + BlockRead(F,Result[0],aCount,BytesRead); + if BytesRead = aCount then + Exit; + finally + CloseFile(F); + end; + {$ENDIF} + {fallback - randomness only protects the session key secrecy, the + password itself is never exposed by SRP even with a weak nonce} + for i := 0 to aCount - 1 do + Result[i] := Random(256); +end; + +{ TSRPClient } + +function TSRPClient.StrippedBytes(const aValue: TBigInt): TBytes; +begin + {minimal big-endian form, no leading zeroes} + Result := aValue.ToBytes; +end; + +function TSRPClient.HashToBigInt(const aDigest: TBytes): TBigInt; +begin + Result := TBigInt.FromBytes(aDigest); +end; + +constructor TSRPClient.Create(const aPrivateKeyOverride: AnsiString); +var ctx: TSHA1; +begin + inherited Create; + FN := TBigInt.FromHex(sSRPPrime); + Fg := TBigInt.FromCardinal(2); + {k = SHA1(pad128(N), pad128(g))} + ctx.Init; + ctx.Update(FN.ToBytes(SRP_KEY_SIZE)); + ctx.Update(Fg.ToBytes(SRP_KEY_SIZE)); + Fk := TBigInt.FromBytes(SHA1DigestToBytes(ctx.Final)); + {client key pair: a random in [0,N), A = g^a mod N} + if aPrivateKeyOverride <> '' then + begin + FPrivKey := TBigInt.Modulus(TBigInt.FromHex(aPrivateKeyOverride),FN); + FPubKey := TBigInt.ModPow(Fg,FPrivKey,FN); + end + else + repeat + FPrivKey := TBigInt.Modulus(TBigInt.FromBytes(GetRandomBytes(SRP_KEY_SIZE)),FN); + FPubKey := TBigInt.ModPow(Fg,FPrivKey,FN); + until TBigInt.Compare(FPubKey,TBigInt.FromCardinal(1)) > 0; +end; + +function TSRPClient.PublicKeyHex: AnsiString; +begin + Result := FPubKey.ToHex; +end; + +function TSRPClient.ClientProof(aUser, aPassword: AnsiString; + const aSalt: TBytes; aServerKeyHex: AnsiString; + aProofHash: TSRPProofHash): TBytes; +var B, u, x, gx, kgx, diff, ux, aux, S: TBigInt; + n1, n2: TBigInt; + ctx: TSHA1; + ctx256: TSHA256; + userHash: TBytes; + upperUser: AnsiString; + + function SHA1Of(const parts: array of TBytes): TBytes; + var i: integer; + c: TSHA1; + begin + c.Init; + for i := 0 to High(parts) do + c.Update(parts[i]); + Result := SHA1DigestToBytes(c.Final); + end; + + function StrToBytes(const s: AnsiString): TBytes; + begin + SetLength(Result,Length(s)); + if s <> '' then + Move(s[1],Result[0],Length(s)); + end; + +begin + {ASCII upper casing only. AnsiUpperCase delegates to the installed + widestring manager, and fpwidestring's implementation returns the + string with a trailing #0 included in its length - that byte would + enter the SRP hashes and every proof would fail. The engine upper + cases the account name with ASCII rules, so UpperCase is also the + correct transformation, not just the safe one.} + upperUser := UpperCase(aUser); + B := TBigInt.FromHex(aServerKeyHex); + if TBigInt.Compare(TBigInt.Modulus(B,FN),TBigInt.FromCardinal(2)) < 0 then + raise EBigIntError.Create('SRP: illegal server public key'); + + {u = SHA1(A, B) - stripped forms} + u := HashToBigInt(SHA1Of([StrippedBytes(FPubKey),StrippedBytes(B)])); + + {x = SHA1(salt, SHA1(upper(user) ':' password))} + userHash := SHA1Of([StrToBytes(upperUser + ':' + aPassword)]); + x := HashToBigInt(SHA1Of([aSalt,userHash])); + + {S = (B - k*g^x) ^ (a + u*x) mod N, with Firebird's mod N exponent + reduction} + gx := TBigInt.ModPow(Fg,x,FN); + kgx := TBigInt.Modulus(TBigInt.Multiply(Fk,gx),FN); + if TBigInt.Compare(B,kgx) >= 0 then + diff := TBigInt.Subtract(B,kgx) + else + diff := TBigInt.Subtract(TBigInt.Add(B,FN),kgx); + diff := TBigInt.Modulus(diff,FN); + ux := TBigInt.Modulus(TBigInt.Multiply(u,x),FN); + aux := TBigInt.Modulus(TBigInt.Add(FPrivKey,ux),FN); + S := TBigInt.ModPow(diff,aux,FN); + + {K = SHA1(S) - the raw 20 byte digest, leading zero bytes preserved} + FSessionKey := SHA1Of([StrippedBytes(S)]); + + {M = H(n1, n2, salt, A, B, K) where n1 = SHA1(N)^SHA1(g) mod N and + n2 = SHA1(upper(user)); H is the plugin's hash} + n1 := HashToBigInt(SHA1Of([StrippedBytes(FN)])); + n2 := HashToBigInt(SHA1Of([StrippedBytes(Fg)])); + n1 := TBigInt.ModPow(n1,n2,FN); + n2 := HashToBigInt(SHA1Of([StrToBytes(upperUser)])); + case aProofHash of + sphSHA1: + begin + ctx.Init; + ctx.Update(StrippedBytes(n1)); + ctx.Update(StrippedBytes(n2)); + ctx.Update(aSalt); + ctx.Update(StrippedBytes(FPubKey)); + ctx.Update(StrippedBytes(B)); + ctx.Update(FSessionKey); + Result := SHA1DigestToBytes(ctx.Final); + end; + sphSHA256: + begin + ctx256.Init; + ctx256.Update(StrippedBytes(n1)); + ctx256.Update(StrippedBytes(n2)); + ctx256.Update(aSalt); + ctx256.Update(StrippedBytes(FPubKey)); + ctx256.Update(StrippedBytes(B)); + ctx256.Update(FSessionKey); + Result := SHA256DigestToBytes(ctx256.Final); + end; + end; +end; + +end. diff --git a/client/wire/FBWireServices.pas b/client/wire/FBWireServices.pas new file mode 100644 index 00000000..0fa49e0e --- /dev/null +++ b/client/wire/FBWireServices.pas @@ -0,0 +1,277 @@ +(* + * Firebird Interface (fbintf). The fbintf components provide a set of + * Pascal language bindings for the Firebird API. + * + * This file is part of the pure Pascal wire protocol implementation + * (no fbclient required) and is subject to the Initial Developer's + * Public License Version 1.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy + * of the License here: + * + * http://www.firebirdsql.org/index.php?op=doc&id=idpl + * + * Software distributed under the License is distributed on an "AS + * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or + * implied. See the License for the specific language governing rights + * and limitations under the License. + * + * The Initial Developer of the Original Code is MWA Software + * (http://www.mwasoftware.co.uk). + * + * All Rights Reserved. + * + * Contributor(s): ______________________________________. + * +*) +unit FBWireServices; + +{ The IServiceManager implementation for the pure Pascal wire protocol + client. + + A service session is its own connection: TFBWireConnection.ConnectTo + performs the SRP authentication and, when negotiated, starts wire + encryption exactly as it does for a database attach, and then + op_service_attach names service_mgr on that connection. The password is + consumed by the SRP exchange and never placed in the SPB that travels to + the server; when the server asked for the proof to be delivered with the + attach (the op_accept_data flow) it is added to the SPB as + isc_spb_specific_auth_data, mirroring what TFBWireAttachment does with + the DPB. + + The SPB, SRB and SQPB parameter blocks and the response parser all come + from the generic fbintf machinery (FBServices, FBParamBlock, + FBOutputBlock): the buffers those classes build are exactly what the + wire carries. } + +{$IFDEF MSWINDOWS} +{$DEFINE WINDOWS} +{$ENDIF} + +{$IFDEF FPC} +{$mode delphi} +{$codepage UTF8} +{$interfaces COM} +{$ENDIF} + +interface + +uses + Classes, SysUtils, IB, FBServices, FBOutputBlock, FBWireClientAPI, + FBWireProtocol; + +type + + { TFBWireServiceManager } + + TFBWireServiceManager = class(TFBServiceManager,IServiceManager) + private + FWireAPI: TFBWireClientAPI; + FConnection: TFBWireConnection; + FHandle: integer; + FIsAttached: boolean; + procedure CheckActive; + {rebuilds the SPB without the password, adding the authentication + proof when the server asked for it in the attach} + function PrepareSPB: TBytes; + protected + procedure InternalAttach(ConnectString: AnsiString); override; + public + constructor Create(api: TFBWireClientAPI; ServerName: AnsiString; + Protocol: TProtocol; SPB: ISPB; Port: AnsiString = ''); + destructor Destroy; override; + + property Connection: TFBWireConnection read FConnection; + property Handle: integer read FHandle; + + public + {IServiceManager} + procedure Detach(Force: boolean=false); override; + function IsAttached: boolean; + function Start(Request: ISRB; RaiseExceptionOnError: boolean=true): boolean; + function Query(SQPB: ISQPB; Request: ISRB; + RaiseExceptionOnError: boolean=true): IServiceQueryResults; override; + end; + +implementation + +uses FBMessages, IBUtils; + +{ TFBWireServiceManager } + +procedure TFBWireServiceManager.CheckActive; +begin + if not FIsAttached then + IBError(ibxeServiceActive,[nil]); +end; + +function TFBWireServiceManager.PrepareSPB: TBytes; +var NewSPB: ISPB; + Item, NewItem: ISPBItem; + i: integer; +begin + NewSPB := TSPB.Create(FWireAPI); + if FSPB <> nil then + for i := 0 to FSPB.Count - 1 do + begin + Item := FSPB.Items[i]; + {SRP has already proved knowledge of the password - it must not + travel to the server in any form} + if Item.getParamType in [isc_spb_password,isc_spb_password_enc] then + continue; + NewItem := NewSPB.Add(Item.getParamType); + if Item.getParamType in [isc_spb_options,isc_spb_connect_timeout, + isc_spb_dummy_packet_interval] then + NewItem.SetAsInteger(Item.AsInteger) + else + NewItem.SetAsString(Item.AsString); + end; + if FConnection.AuthData <> '' then + begin + NewSPB.Add(isc_spb_specific_auth_data).AsString := FConnection.AuthData; + NewSPB.Add(isc_spb_auth_plugin_name).AsString := FConnection.AuthPluginName; + end; + Result := ParamBlockToBytes(NewSPB); +end; + +procedure TFBWireServiceManager.InternalAttach(ConnectString: AnsiString); +var aHost, aServiceName, aPortNo: AnsiString; + aPort: integer; + aProtocol: TProtocolAll; + aUser, aPassword: AnsiString; + Item: ISPBItem; + spbBytes: TBytes; +begin + aHost := ''; + aPort := 3050; + aServiceName := ConnectString; + if ParseConnectString(ConnectString,aHost,aServiceName,aProtocol,aPortNo) then + begin + if aPortNo <> '' then + aPort := StrToIntDef(aPortNo,3050); + case aProtocol of + TCP, inet, inet4, inet6: + {a remote connection - this is what the wire protocol provides}; + Local: + {a local connection cannot be made without the client library, but + most servers also listen on the loopback interface} + if aHost = '' then + aHost := 'localhost'; + else + IBError(ibxeNotSupported,[nil]); + end; + end; + if aHost = '' then + aHost := 'localhost'; + + aUser := ''; + aPassword := ''; + if FSPB <> nil then + begin + Item := FSPB.Find(isc_spb_user_name); + if Item <> nil then + aUser := Item.AsString; + Item := FSPB.Find(isc_spb_password); + if Item <> nil then + aPassword := Item.AsString; + end; + + try + FConnection.ConnectTo(aHost,aPort,aServiceName,aUser,aPassword); + spbBytes := PrepareSPB; + FHandle := FConnection.ServiceAttach(aServiceName,spbBytes); + FIsAttached := true; + except + on E: Exception do + begin + FConnection.Disconnect; + FIsAttached := false; + WireIBError(FWireAPI,E); + end; + end; +end; + +constructor TFBWireServiceManager.Create(api: TFBWireClientAPI; + ServerName: AnsiString; Protocol: TProtocol; SPB: ISPB; Port: AnsiString); +begin + FWireAPI := api; + FConnection := TFBWireConnection.Create; + {the inherited constructor attaches} + inherited Create(api,ServerName,Protocol,SPB,Port); +end; + +destructor TFBWireServiceManager.Destroy; +begin + inherited Destroy; + if FConnection <> nil then + FConnection.Free; +end; + +procedure TFBWireServiceManager.Detach(Force: boolean); +begin + if not FIsAttached then Exit; + try + FConnection.ServiceDetach(FHandle); + except + on E: Exception do + if not Force then + begin + FIsAttached := false; + FConnection.Disconnect; + WireIBError(FWireAPI,E); + end; + end; + FIsAttached := false; + FHandle := 0; + FConnection.Disconnect; +end; + +function TFBWireServiceManager.IsAttached: boolean; +begin + Result := FIsAttached; +end; + +function TFBWireServiceManager.Start(Request: ISRB; + RaiseExceptionOnError: boolean): boolean; +begin + CheckActive; + Result := true; + try + FConnection.ServiceStart(FHandle,ParamBlockToBytes(Request)); + except + on E: Exception do + begin + Result := false; + if RaiseExceptionOnError then + WireIBError(FWireAPI,E); + end; + end; +end; + +function TFBWireServiceManager.Query(SQPB: ISQPB; Request: ISRB; + RaiseExceptionOnError: boolean): IServiceQueryResults; +var QueryResults: TServiceQueryResults; + response: TBytes; + len: integer; +begin + CheckActive; + QueryResults := TServiceQueryResults.Create(FWireAPI); + Result := QueryResults; + try + response := FConnection.ServiceQuery(FHandle,ParamBlockToBytes(SQPB), + ParamBlockToBytes(Request),QueryResults.getBufSize); + len := Length(response); + if len > QueryResults.getBufSize then + len := QueryResults.getBufSize; + if len > 0 then + Move(response[0],QueryResults.Buffer^,len); + except + on E: Exception do + begin + Result := nil; + if RaiseExceptionOnError then + WireIBError(FWireAPI,E); + end; + end; +end; + +end. diff --git a/client/wire/FBWireStatement.pas b/client/wire/FBWireStatement.pas new file mode 100644 index 00000000..c95a5d18 --- /dev/null +++ b/client/wire/FBWireStatement.pas @@ -0,0 +1,1486 @@ +(* + * Firebird Interface (fbintf). The fbintf components provide a set of + * Pascal language bindings for the Firebird API. + * + * This file is part of the pure Pascal wire protocol implementation + * (no fbclient required) and is subject to the Initial Developer's + * Public License Version 1.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy + * of the License here: + * + * http://www.firebirdsql.org/index.php?op=doc&id=idpl + * + * Software distributed under the License is distributed on an "AS + * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or + * implied. See the License for the specific language governing rights + * and limitations under the License. + * + * The Initial Developer of the Original Code is MWA Software + * (http://www.mwasoftware.co.uk). + * + * All Rights Reserved. + * + * Contributor(s): ______________________________________. + * +*) +unit FBWireStatement; + +{ The IStatement implementation for the wire protocol provider. + + Column and parameter values live in a flat message buffer laid out by + FBWireMessage; TWireSQLVarData points TSQLDataItem at the right offset in + it, so the whole of the fbintf data conversion layer applies unchanged. } + +{$IFDEF MSWINDOWS} +{$DEFINE WINDOWS} +{$ENDIF} + +{$IFDEF FPC} +{$mode delphi} +{$codepage UTF8} +{$interfaces COM} +{$ENDIF} + +interface + +uses + Classes, SysUtils, IB, FBStatement, FBSQLData, FBClientAPI, FBTransaction, + FBActivityMonitor, FBOutputBlock, FBWireClientAPI, FBWireProtocol, + FBWireMessage, FBWireDescribe; + +const + {rows requested from the server in a single op_fetch} + DefaultFetchBatchSize = 200; + +type + TFBWireStatement = class; + TWireSQLDataArea = class; + PWireSQLVarRec = ^TWireSQLVar; + + { TWireSQLVarData } + + TWireSQLVarData = class(TSQLVarData) + private + FStatement: TFBWireStatement; + FOwner: TWireSQLDataArea; + FBlob: IBlob; + FBlobMetaData: IBlobMetaData; + FArrayMetaData: IArrayMetaData; + function GetVar: PWireSQLVarRec; + function BufferBase: PByte; + protected + function GetSQLType: cardinal; override; + function GetSubtype: integer; override; + function GetAliasName: AnsiString; override; + function GetFieldName: AnsiString; override; + function GetOwnerName: AnsiString; override; + function GetRelationName: AnsiString; override; + function GetScale: integer; override; + function GetCharSetID: cardinal; override; + function GetIsNull: Boolean; override; + function GetIsNullable: boolean; override; + function GetSQLData: PByte; override; + function GetDataLength: cardinal; override; + function GetSize: cardinal; override; + function GetDefaultTextSQLType: cardinal; override; + procedure InternalSetSQLType(aValue: cardinal; aSubType: integer); override; + procedure InternalSetScale(aValue: integer); override; + procedure InternalSetDataLength(len: cardinal); override; + procedure SetMetaSize(aValue: cardinal); override; + procedure SetIsNull(Value: Boolean); override; + procedure SetIsNullable(Value: Boolean); override; + procedure SetSQLData(AValue: PByte; len: cardinal); override; + procedure SetCharSetID(aValue: cardinal); override; + public + constructor Create(aParent: TWireSQLDataArea; aIndex: integer); + procedure RowChange; override; + function GetAsArray: IArray; override; + function GetAsBlob(Blob_ID: TISC_QUAD; BPB: IBPB): IBlob; override; + function CreateBlob: IBlob; override; + function GetArrayMetaData: IArrayMetaData; override; + function GetBlobMetaData: IBlobMetaData; override; + procedure Initialize; override; + end; + + { TWireSQLDataArea } + + TWireSQLDataArea = class(TSQLDataArea) + private + FStatement: TFBWireStatement; + FIsInput: boolean; + FFormat: TWireMessageFormat; + FBuffer: TBytes; + FTransactionSeqNo: integer; {the transaction generation the area was + prepared or executed under - stale + reference checks compare against it} + protected + function GetStatement: IStatement; override; + function GetPrepareSeqNo: integer; override; + function GetTransactionSeqNo: integer; override; + procedure SetCount(aValue: integer); override; + public + constructor Create(aStatement: TFBWireStatement; aIsInput: boolean); + destructor Destroy; override; + {rebuilds the column list from a describe response} + procedure Bind(const aFormat: TWireMessageFormat; aBufferSize: cardinal); + function IsInputDataArea: boolean; override; + function CheckStatementStatus(Request: TStatementStatus): boolean; override; + function StateChanged(var ChangeSeqNo: integer): boolean; override; + function CanChangeMetaData: boolean; override; + procedure ClearBuffer; + {recomputes the buffer layout after a parameter's metadata has been + changed, relocating values already written} + procedure RelayoutBuffer; + property Format: TWireMessageFormat read FFormat; + property Buffer: TBytes read FBuffer; + property Statement: TFBWireStatement read FStatement; + end; + + { TWireBatchCompletion - IBatchCompletion over a decoded op_batch_cs. + Semantics follow the 3.0 provider's TBatchCompletion. } + + TWireBatchCompletion = class(TFBInterfacedObject,IBatchCompletion) + private + FWireAPI: TFBWireClientAPI; + FConnectionCodePage: TSystemCodePage; + FCS: TWireBatchCS; + public + constructor Create(api: TFBWireClientAPI; const aCS: TWireBatchCS; + aConnectionCodePage: TSystemCodePage); + {IBatchCompletion} + function getErrorStatus(var RowNo: integer; var status: IStatus): boolean; + function getTotalProcessed: cardinal; + function getState(updateNo: cardinal): TBatchCompletionState; + function getStatusMessage(updateNo: cardinal): AnsiString; + function getUpdated: integer; + end; + + { TWireResultSet } + + TWireResultSet = class(TResults,IResultSet) + private + FResults: TWireSQLDataArea; + FCursorSeqNo: integer; + public + constructor Create(aResults: TWireSQLDataArea); + {IResultSet} + function FetchNext: boolean; + function FetchPrior: boolean; + function FetchFirst: boolean; + function FetchLast: boolean; + function FetchAbsolute(position: Integer): boolean; + function FetchRelative(offset: Integer): boolean; + function GetCursorName: AnsiString; + function IsBof: boolean; + function IsEof: boolean; + procedure Close; + end; + + { TFBWireStatement } + + TFBWireStatement = class(TFBStatement,IStatement) + private + FWireAPI: TFBWireClientAPI; + FHandle: integer; + FHasHandle: boolean; + FSQLParams: TWireSQLDataArea; + FSQLRecord: TWireSQLDataArea; + FStatementInfo: TWireStatementInfo; + FCursorState: TWireCursorState; + FCursorName: AnsiString; + FCursorSeqNo: integer; + FScrollable: boolean; + FBatchActive: boolean; + FBatchRows: array of TBytes; {accumulated messages, our buffer layout} + FBatchRowCount: integer; + FBatchBufferSize: integer; + FBatchBufferUsed: integer; + FBatchCompletion: IBatchCompletion; + function GetConnection: TFBWireConnection; + function GetTransactionHandle: integer; + procedure CheckBatchModeAvailable; + procedure ReleaseBatch(SendCancel: boolean); + protected + procedure CheckHandle; override; + procedure CheckChangeBatchRowLimit; override; + function GetStatementIntf: IStatement; override; + procedure GetDsqlInfo(info_request: byte; buffer: ISQLInfoResults); override; + procedure InternalPrepare(CursorName: AnsiString = ''); override; + function InternalExecute(Transaction: ITransaction): IResults; override; + function InternalOpenCursor(aTransaction: ITransaction; Scrollable: boolean): IResultSet; override; + procedure ProcessSQL(sql: AnsiString; GenerateParamNames: boolean; + var processedSQL: AnsiString); override; + procedure FreeHandle; override; + procedure InternalClose(Force: boolean); override; + public + constructor Create(Attachment: IAttachment; Transaction: ITransaction; + sql: AnsiString; SQLDialect: integer; CursorName: AnsiString = ''); + constructor CreateWithNamedParameters(Attachment: IAttachment; + Transaction: ITransaction; sql: AnsiString; SQLDialect: integer; + GenerateParamNames: boolean = false; + CaseSensitiveParams: boolean = false; CursorName: AnsiString = ''); + destructor Destroy; override; + function FetchNextRow: boolean; + {a positioned fetch on a scrollable cursor (protocol 18) - aDirection + is one of the fetch_* constants, aPosition the absolute row number or + relative offset} + function FetchScroll(aDirection: integer; aPosition: integer): boolean; + function GetSQLParams: ISQLParams; override; + function GetMetaData: IMetaData; override; + function GetFlags: TStatementFlags; override; + {the schema name a protocol 20 server reported for an output column, + empty on older protocols. fbintf has no schema surface on + IColumnMetaData yet, so callers that need it reach it through the + class - as the tests do.} + function ColumnSchemaName(aIndex: integer): AnsiString; + {needs protocol 16 - the p_sqldata_timeout field of op_execute} + procedure SetStatementTimeout(aMilliseconds: cardinal); override; + {IBatch support - needs protocol 16 (the op_batch_* family)} + function HasBatchMode: boolean; override; + function IsInBatchMode: boolean; override; + procedure AddToBatch; override; + function ExecuteBatch(aTransaction: ITransaction): IBatchCompletion; override; + procedure CancelBatch; override; + function GetBatchCompletion: IBatchCompletion; override; + function CreateBlob(column: TColumnMetaData): IBlob; override; + function CreateArray(column: TColumnMetaData): IArray; override; + function GetPlan: AnsiString; + function IsPrepared: boolean; + + property Connection: TFBWireConnection read GetConnection; + property Handle: integer read FHandle; + property WireAPI: TFBWireClientAPI read FWireAPI; + end; + +implementation + +uses FBMessages, IBErrorCodes, FBWireAttachment, FBWireTransaction, FBWireBlob, + FBWireArray, FBWireConst, IBUtils; + +{ TWireSQLVarData } + +constructor TWireSQLVarData.Create(aParent: TWireSQLDataArea; aIndex: integer); +begin + inherited Create(aParent,aIndex); + FOwner := aParent; + FStatement := aParent.Statement; +end; + +function TWireSQLVarData.GetVar: PWireSQLVarRec; +begin + if (Index < 0) or (Index >= Length(FOwner.FFormat)) then + IBError(ibxeInvalidColumnIndex,[nil]); + Result := @FOwner.FFormat[Index]; +end; + +function TWireSQLVarData.BufferBase: PByte; +begin + if Length(FOwner.FBuffer) = 0 then + IBError(ibxeInvalidStatementHandle,[nil]); + Result := @FOwner.FBuffer[0]; +end; + +function TWireSQLVarData.GetSQLType: cardinal; +begin + Result := GetVar^.SQLType; +end; + +function TWireSQLVarData.GetSubtype: integer; +begin + Result := GetVar^.SQLSubType; +end; + +function TWireSQLVarData.GetAliasName: AnsiString; +begin + Result := GetVar^.AliasName; +end; + +function TWireSQLVarData.GetFieldName: AnsiString; +begin + Result := GetVar^.FieldName; +end; + +function TWireSQLVarData.GetOwnerName: AnsiString; +begin + Result := GetVar^.OwnerName; +end; + +function TWireSQLVarData.GetRelationName: AnsiString; +begin + Result := GetVar^.RelationName; +end; + +function TWireSQLVarData.GetScale: integer; +begin + Result := GetVar^.Scale; +end; + +function TWireSQLVarData.GetCharSetID: cardinal; +begin + case GetSQLType of + SQL_TEXT, SQL_VARYING: + Result := GetVar^.CharSetID; + SQL_BLOB: + if GetSubType = 1 then {text blob} + Result := GetVar^.CharSetID + else + Result := 0; + else + Result := 0; + end; +end; + +function TWireSQLVarData.GetIsNull: Boolean; +begin + {the null indicator in the message buffer is the single source of truth: + it is what XDREncodeMessage transmits} + Result := PInteger(BufferBase + GetVar^.NullOffset)^ <> 0; +end; + +function TWireSQLVarData.GetIsNullable: boolean; +begin + {an input parameter may always be set to null, whatever the column it is + compared against. Reporting otherwise would stop the base class clearing + the null indicator when a value is assigned.} + Result := FOwner.IsInputDataArea or GetVar^.Nullable; +end; + +function TWireSQLVarData.GetSQLData: PByte; +begin + Result := BufferBase + GetVar^.DataOffset; + {an input parameter's SQLData points at the characters, not the two + byte length prefix: TSQLParam.GetAsString and friends rely on it, as + with the fbclient providers whose parameter SQLData holds a bare + string. Output columns keep the prefixed form, which is what + TSQLDataItem.GetAsString expects for a column.} + if FOwner.FIsInput and (GetSQLType = SQL_VARYING) then + Inc(Result,2); +end; + +function TWireSQLVarData.GetDataLength: cardinal; +begin + {for VARYING the current length is the two byte prefix} + if GetSQLType = SQL_VARYING then + Result := PWord(BufferBase + GetVar^.DataOffset)^ + else + Result := GetVar^.DataSize; +end; + +function TWireSQLVarData.GetSize: cardinal; +begin + Result := GetVar^.DataSize; +end; + +function TWireSQLVarData.GetDefaultTextSQLType: cardinal; +begin + {SQL_VARYING, as the fbclient providers answer: a varying parameter + keeps its described maximum size when values of different lengths are + assigned, so the message format stays stable - which the batch API + depends on, because op_batch_create fixes the BLR for every message + in the batch. (The original wire provider answered SQL_TEXT, whose + per value resizing re-laid out the format on every assignment.)} + Result := SQL_VARYING; +end; + +procedure TWireSQLVarData.InternalSetSQLType(aValue: cardinal; aSubType: integer); +begin + if (GetVar^.SQLType = aValue) and (GetVar^.SQLSubType = aSubType) then + Exit; + GetVar^.SQLType := aValue; + GetVar^.SQLSubType := aSubType; + FOwner.RelayoutBuffer; +end; + +procedure TWireSQLVarData.InternalSetScale(aValue: integer); +begin + GetVar^.Scale := aValue; +end; + +procedure TWireSQLVarData.InternalSetDataLength(len: cardinal); +begin + if GetSQLType = SQL_VARYING then + begin + {a longer value than the described maximum needs a bigger slot: grow + the declared size and relay out - the BLR describes the new size} + if len > GetVar^.DataSize then + begin + GetVar^.DataSize := len; + FOwner.RelayoutBuffer; + end; + PWord(BufferBase + GetVar^.DataOffset)^ := len; + end + else + begin + if GetVar^.DataSize = len then Exit; + GetVar^.DataSize := len; + FOwner.RelayoutBuffer; + end; +end; + +procedure TWireSQLVarData.SetMetaSize(aValue: cardinal); +begin + {called before a type change (e.g. a string value assigned to a blob + parameter becomes SQL_TEXT) so that the new slot is large enough} + if aValue > GetVar^.DataSize then + begin + GetVar^.DataSize := aValue; + FOwner.RelayoutBuffer; + end; +end; + +procedure TWireSQLVarData.SetIsNull(Value: Boolean); +begin + if Value then + begin + GetVar^.Nullable := true; + PInteger(BufferBase + GetVar^.NullOffset)^ := -1; + end + else + PInteger(BufferBase + GetVar^.NullOffset)^ := 0; + Changed; +end; + +procedure TWireSQLVarData.SetIsNullable(Value: Boolean); +begin + GetVar^.Nullable := Value; +end; + +procedure TWireSQLVarData.SetSQLData(AValue: PByte; len: cardinal); +var p: PByte; +begin + if len > GetVar^.BufferSize then + IBError(ibxeStringOverflow,[len,GetVar^.BufferSize]); + p := BufferBase + GetVar^.DataOffset; {the raw slot, prefix included} + case GetSQLType of + SQL_VARYING: + begin + {a varying value is a two byte length followed by the characters} + if len > 0 then + Move(AValue^,(p+2)^,len); + PWord(p)^ := len; + end; + SQL_TEXT: + begin + {CHAR values are blank padded to their full width: the whole field is + transmitted, and trailing nulls would not compare equal to a blank + padded column} + if len > 0 then + Move(AValue^,p^,len); + if len < GetVar^.DataSize then + FillChar((p+len)^,GetVar^.DataSize - len,' '); + end; + else + if len > 0 then + Move(AValue^,p^,len); + end; + {writing a value clears the null indicator} + PInteger(BufferBase + GetVar^.NullOffset)^ := 0; +end; + +procedure TWireSQLVarData.SetCharSetID(aValue: cardinal); +begin + GetVar^.CharSetID := aValue; +end; + +procedure TWireSQLVarData.RowChange; +begin + inherited RowChange; + FBlob := nil; +end; + +function TWireSQLVarData.GetAsArray: IArray; +begin + if GetSQLType <> SQL_ARRAY then + IBError(ibxeInvalidDataConversion,[nil]); + + if GetIsNull then + Result := nil + else + begin + if FArrayIntf = nil then + FArrayIntf := TFBWireArray.Create( + FStatement.GetAttachment as TFBWireAttachment, + FStatement.GetTransaction as TObject as TFBWireTransaction, + GetArrayMetaData,PISC_QUAD(GetSQLData)^); + Result := FArrayIntf; + end; +end; + +function TWireSQLVarData.GetAsBlob(Blob_ID: TISC_QUAD; BPB: IBPB): IBlob; +begin + if FBlob <> nil then + Result := FBlob + else + begin + Result := TFBWireBlob.Create(FStatement.GetAttachment as TFBWireAttachment, + FStatement.GetTransaction as TFBWireTransaction, + GetBlobMetaData,Blob_ID,BPB); + FBlob := Result; + end; +end; + +function TWireSQLVarData.CreateBlob: IBlob; +begin + Result := TFBWireBlob.Create(FStatement.GetAttachment as TFBWireAttachment, + FStatement.GetTransaction as TFBWireTransaction, + GetBlobMetaData,nil); +end; + +function TWireSQLVarData.GetArrayMetaData: IArrayMetaData; +begin + if GetSQLType <> SQL_ARRAY then + IBError(ibxeInvalidDataConversion,[nil]); + if FArrayMetaData = nil then + FArrayMetaData := TFBWireArrayMetaData.Create( + (FStatement.GetAttachment as TFBWireAttachment) as IAttachment, + FStatement.GetTransaction, + GetRelationName,GetFieldName); + Result := FArrayMetaData; +end; + +function TWireSQLVarData.GetBlobMetaData: IBlobMetaData; +begin + if GetSQLType <> SQL_BLOB then + IBError(ibxeInvalidDataConversion,[nil]); + if FBlobMetaData = nil then + FBlobMetaData := TFBWireBlobMetaData.Create( + FStatement.GetAttachment as TFBWireAttachment, + FStatement.GetTransaction as TFBWireTransaction, + GetRelationName,GetFieldName,GetSubType,GetVar^.CharSetID); + Result := FBlobMetaData; +end; + +procedure TWireSQLVarData.Initialize; +begin + inherited Initialize; + FBlob := nil; +end; + +{ TWireSQLDataArea } + +constructor TWireSQLDataArea.Create(aStatement: TFBWireStatement; + aIsInput: boolean); +begin + inherited Create; + FStatement := aStatement; + FIsInput := aIsInput; +end; + +destructor TWireSQLDataArea.Destroy; +begin + SetCount(0); + inherited Destroy; +end; + +function TWireSQLDataArea.GetStatement: IStatement; +begin + Result := FStatement; +end; + +function TWireSQLDataArea.GetPrepareSeqNo: integer; +begin + Result := FStatement.FPrepareSeqNo; +end; + +function TWireSQLDataArea.GetTransactionSeqNo: integer; +begin + Result := (FStatement.FTransactionIntf as TObject as TFBTransaction).TransactionSeqNo; +end; + +procedure TWireSQLDataArea.SetCount(aValue: integer); +var i, oldCount: integer; +begin + oldCount := Length(FColumnList); + for i := aValue to oldCount - 1 do + FColumnList[i].Free; + SetLength(FColumnList,aValue); + for i := oldCount to aValue - 1 do + FColumnList[i] := TWireSQLVarData.Create(self,i); +end; + +procedure TWireSQLDataArea.Bind(const aFormat: TWireMessageFormat; + aBufferSize: cardinal); +var i: integer; +begin + FFormat := aFormat; + SetLength(FBuffer,aBufferSize); + ClearBuffer; + SetCount(Length(FFormat)); + for i := 0 to Length(FFormat) - 1 do + begin + {input parameters keep the names assigned by the SQL preprocessor + (":name" parameters) - the describe response has no names for them + and would wipe them} + if not FIsInput then + FColumnList[i].Name := FFormat[i].AliasName + else + {snapshot the described metadata: TSQLParam.Clear restores a + parameter to it after the type has been changed} + FColumnList[i].SaveMetaData; + FColumnList[i].Initialize; + end; + SetUniqueRelationName; +end; + +procedure TWireSQLDataArea.ClearBuffer; +var i: integer; +begin + if Length(FBuffer) > 0 then + FillChar(FBuffer[0],Length(FBuffer),0); + {an unset input parameter defaults to null} + if FIsInput then + for i := 0 to Length(FFormat) - 1 do + PInteger(@FBuffer[0] + FFormat[i].NullOffset)^ := -1; +end; + +function TWireSQLDataArea.IsInputDataArea: boolean; +begin + Result := FIsInput; +end; + +function TWireSQLDataArea.CheckStatementStatus(Request: TStatementStatus): boolean; +begin + Result := false; + case Request of + ssPrepared: + Result := FStatement.FPrepared; + ssExecuteResults: + Result := not FStatement.FOpen and FStatement.FSingleResults; + ssCursorOpen: + Result := FStatement.FOpen; + ssBOF: + Result := FStatement.FBOF; + ssEOF: + Result := FStatement.FEOF; + end; +end; + +function TWireSQLDataArea.StateChanged(var ChangeSeqNo: integer): boolean; +begin + Result := ChangeSeqNo <> FStatement.ChangeSeqNo; + if Result then + ChangeSeqNo := FStatement.ChangeSeqNo; +end; + +function TWireSQLDataArea.CanChangeMetaData: boolean; +begin + {the client owns the parameter message format: the BLR sent with + op_execute describes whatever the format records now say, and the + server coerces. RelayoutBuffer keeps the buffer consistent with any + change. The exception is an open batch: op_batch_create froze the BLR + for every message in it, so the format must not drift - as with the + 3.0 provider, changing a parameter's type mid batch is refused.} + Result := FIsInput and not FStatement.IsInBatchMode; +end; + +procedure TWireSQLDataArea.RelayoutBuffer; +var OldFormat: TWireMessageFormat; + OldBuffer: TBytes; + i: integer; + n: cardinal; +begin + if not FIsInput then Exit; + {keep the old layout so that values already written can be relocated} + SetLength(OldFormat,Length(FFormat)); + for i := 0 to High(FFormat) do + OldFormat[i] := FFormat[i]; + OldBuffer := system.copy(FBuffer); + ComputeMessageLayout(FFormat); + SetLength(FBuffer,MessageBufferSize(FFormat)); + if Length(FBuffer) > 0 then + FillChar(FBuffer[0],Length(FBuffer),0); + for i := 0 to High(FFormat) do + begin + n := OldFormat[i].BufferSize; + if n > FFormat[i].BufferSize then + n := FFormat[i].BufferSize; + if (n > 0) and (Length(OldBuffer) > 0) then + Move(OldBuffer[OldFormat[i].DataOffset],FBuffer[FFormat[i].DataOffset],n); + if Length(OldBuffer) > 0 then + Move(OldBuffer[OldFormat[i].NullOffset],FBuffer[FFormat[i].NullOffset],4); + end; +end; + +{ TWireBatchCompletion } + +constructor TWireBatchCompletion.Create(api: TFBWireClientAPI; + const aCS: TWireBatchCS; aConnectionCodePage: TSystemCodePage); +begin + inherited Create; + FWireAPI := api; + FCS := aCS; + FConnectionCodePage := aConnectionCodePage; +end; + +function TWireBatchCompletion.getErrorStatus(var RowNo: integer; + var status: IStatus): boolean; +var i: integer; + aStatus: TFBWireStatus; +begin + Result := false; + RowNo := -1; + for i := 0 to Length(FCS.States) - 1 do + if FCS.States[i] = BATCH_EXECUTE_FAILED then + begin + RowNo := i + 1; + aStatus := TFBWireStatus.Create(FWireAPI); + status := aStatus; {the interface reference owns it} + if Length(FCS.StatusVectors[i]) > 0 then + aStatus.SetFromWireStatus(FCS.StatusVectors[i]) + else + aStatus.SetError(isc_random,'Batch execution failed with no status vector'); + Result := true; + break; + end; +end; + +function TWireBatchCompletion.getTotalProcessed: cardinal; +begin + Result := Length(FCS.States); +end; + +function TWireBatchCompletion.getState(updateNo: cardinal): TBatchCompletionState; +begin + {the same mapping as the 3.0 provider: a real update count also answers + bcNoMoreErrors} + Result := bcNoMoreErrors; + if updateNo < cardinal(Length(FCS.States)) then + case FCS.States[updateNo] of + BATCH_EXECUTE_FAILED: + Result := bcExecuteFailed; + BATCH_SUCCESS_NO_INFO: + Result := bcSuccessNoInfo; + end; +end; + +function TWireBatchCompletion.getStatusMessage(updateNo: cardinal): AnsiString; +var aStatus: TFBWireStatus; + intf: IStatus; +begin + Result := ''; + if (updateNo < cardinal(Length(FCS.StatusVectors))) and + (Length(FCS.StatusVectors[updateNo]) > 0) then + begin + aStatus := TFBWireStatus.Create(FWireAPI); + intf := aStatus; + aStatus.SetFromWireStatus(FCS.StatusVectors[updateNo]); + Result := aStatus.GetMessage(FConnectionCodePage); + end; +end; + +function TWireBatchCompletion.getUpdated: integer; +var i: integer; +begin + Result := 0; + for i := 0 to Length(FCS.States) - 1 do + begin + if FCS.States[i] = BATCH_EXECUTE_FAILED then + break; + Inc(Result); + end; +end; + +{ TWireResultSet } + +constructor TWireResultSet.Create(aResults: TWireSQLDataArea); +begin + inherited Create(aResults); + FResults := aResults; + FCursorSeqNo := aResults.Statement.FCursorSeqNo; +end; + +function TWireResultSet.FetchNext: boolean; +begin + Result := FResults.Statement.FetchNextRow; + if Result then + FResults.RowChange; +end; + +function TWireResultSet.FetchPrior: boolean; +begin + Result := FResults.Statement.FetchScroll(fetch_prior,0); + if Result then + FResults.RowChange; +end; + +function TWireResultSet.FetchFirst: boolean; +begin + Result := FResults.Statement.FetchScroll(fetch_first,0); + if Result then + FResults.RowChange; +end; + +function TWireResultSet.FetchLast: boolean; +begin + Result := FResults.Statement.FetchScroll(fetch_last,0); + if Result then + FResults.RowChange; +end; + +function TWireResultSet.FetchAbsolute(position: Integer): boolean; +begin + Result := FResults.Statement.FetchScroll(fetch_absolute,position); + if Result then + FResults.RowChange; +end; + +function TWireResultSet.FetchRelative(offset: Integer): boolean; +begin + Result := FResults.Statement.FetchScroll(fetch_relative,offset); + if Result then + FResults.RowChange; +end; + +function TWireResultSet.GetCursorName: AnsiString; +begin + Result := FResults.Statement.FCursorName; +end; + +function TWireResultSet.IsBof: boolean; +begin + Result := FResults.Statement.FBOF; +end; + +function TWireResultSet.IsEof: boolean; +begin + Result := FResults.Statement.FEOF; +end; + +procedure TWireResultSet.Close; +begin + if FCursorSeqNo = FResults.Statement.FCursorSeqNo then + FResults.Statement.Close; +end; + +{ TFBWireStatement } + +constructor TFBWireStatement.Create(Attachment: IAttachment; + Transaction: ITransaction; sql: AnsiString; SQLDialect: integer; + CursorName: AnsiString); +begin + FWireAPI := (Attachment as TObject as TFBWireAttachment).WireAPI; + FSQLParams := TWireSQLDataArea.Create(self,true); + FSQLRecord := TWireSQLDataArea.Create(self,false); + FCursorName := CursorName; + inherited Create(Attachment,Transaction,sql,SQLDialect); + InternalPrepare(CursorName); +end; + +constructor TFBWireStatement.CreateWithNamedParameters(Attachment: IAttachment; + Transaction: ITransaction; sql: AnsiString; SQLDialect: integer; + GenerateParamNames: boolean; CaseSensitiveParams: boolean; + CursorName: AnsiString); +begin + FWireAPI := (Attachment as TObject as TFBWireAttachment).WireAPI; + FSQLParams := TWireSQLDataArea.Create(self,true); + FSQLRecord := TWireSQLDataArea.Create(self,false); + FCursorName := CursorName; + inherited CreateWithParameterNames(Attachment,Transaction,sql,SQLDialect, + GenerateParamNames,CaseSensitiveParams); + FSQLParams.CaseSensitiveParams := CaseSensitiveParams; + InternalPrepare(CursorName); +end; + +destructor TFBWireStatement.Destroy; +begin + inherited Destroy; + if FSQLParams <> nil then FSQLParams.Free; + if FSQLRecord <> nil then FSQLRecord.Free; +end; + +function TFBWireStatement.GetConnection: TFBWireConnection; +begin + Result := (GetAttachment as TObject as TFBWireAttachment).Connection; +end; + +function TFBWireStatement.GetTransactionHandle: integer; +begin + Result := (FTransactionIntf as TObject as TFBWireTransaction).Handle; +end; + +procedure TFBWireStatement.CheckHandle; +begin + if not FHasHandle then + IBError(ibxeInvalidStatementHandle,[nil]); +end; + +function TFBWireStatement.GetStatementIntf: IStatement; +begin + Result := self; +end; + +procedure TFBWireStatement.ProcessSQL(sql: AnsiString; + GenerateParamNames: boolean; var processedSQL: AnsiString); +begin + FSQLParams.PreprocessSQL(sql,GenerateParamNames,processedSQL); +end; + +procedure TFBWireStatement.InternalPrepare(CursorName: AnsiString); +var response: TBytes; +begin + if FPrepared then Exit; + if CursorName <> '' then + FCursorName := CursorName; + if (FSQL = '') then + IBError(ibxeEmptyQuery,[nil]); + CheckTransaction(FTransactionIntf); + try + if not FHasHandle then + begin + FHandle := Connection.AllocateStatement( + (GetAttachment as TObject as TFBWireAttachment).Handle); + FHasHandle := true; + end; + if FHasParamNames then + begin + if FProcessedSQL = '' then + ProcessSQL(FSQL,FGenerateParamNames,FProcessedSQL); + response := Connection.PrepareStatement(GetTransactionHandle,FHandle, + FSQLDialect,FProcessedSQL, + DescribeItems(Connection.ProtocolVersion >= (PROTOCOL_VERSION20 and FB_PROTOCOL_MASK)), + DefaultBufferSize); + end + else + response := Connection.PrepareStatement(GetTransactionHandle,FHandle, + FSQLDialect,FSQL, + DescribeItems(Connection.ProtocolVersion >= (PROTOCOL_VERSION20 and FB_PROTOCOL_MASK)), + DefaultBufferSize); + FStatementInfo := ParsePrepareResponse(response); + if FStatementInfo.Truncated then + IBError(ibxeInfoBufferTypeError,[nil]); + FSQLStatementType := TIBSQLStatementTypes(FStatementInfo.StatementType); + FSQLParams.Bind(FStatementInfo.InputFormat,FStatementInfo.InputBufferSize); + FSQLRecord.Bind(FStatementInfo.OutputFormat,FStatementInfo.OutputBufferSize); + FSQLParams.FTransactionSeqNo := + (FTransactionIntf as TObject as TFBTransaction).TransactionSeqNo; + FSQLRecord.FTransactionSeqNo := FSQLParams.FTransactionSeqNo; + if FCursorName <> '' then + Connection.SetCursorName(FHandle,FCursorName); + except + on E: Exception do + begin + FPrepared := false; + try + WireIBError(FWireAPI,E); + except + on E2: EIBInterBaseError do + begin + {name the statement in the error, as the other providers do} + E2.Message := E2.Message + sSQLErrorSeparator + FSQL; + raise; + end; + end; + end; + end; + FPrepared := true; + FSingleResults := false; + Inc(FPrepareSeqNo); + Inc(FChangeSeqNo); +end; + +function TFBWireStatement.InternalExecute(Transaction: ITransaction): IResults; +var paramPtr, outPtr: PByte; + Cursor: IResultSet; +begin + Result := nil; + CheckTransaction(Transaction); + if not FPrepared then + InternalPrepare; + CheckHandle; + if FStaleReferenceChecks and (FSQLParams.FTransactionSeqNo < + (FTransactionIntf as TObject as TFBTransaction).TransactionSeqNo) then + IBError(ibxeInterfaceOutofDate,[nil]); + FSQLRecord.FTransactionSeqNo := + (FTransactionIntf as TObject as TFBTransaction).TransactionSeqNo; + FBOF := false; + FEOF := false; + FSingleResults := false; + + if (FSQLStatementType = SQLSelect) and (FSQLRecord.Count > 0) then + begin + {Firebird 5 and later describe update/insert ... returning as a select + statement answering a single row - open the cursor and fetch it} + Cursor := InternalOpenCursor(Transaction,false); + if not Cursor.IsEof then + Cursor.FetchNext; + Result := Cursor; + FSingleResults := true; + Inc(FChangeSeqNo); + Exit; + end; + + ResetCursorState(FCursorState); + + paramPtr := nil; + if Length(FSQLParams.Buffer) > 0 then + paramPtr := @FSQLParams.Buffer[0]; + outPtr := nil; + if Length(FSQLRecord.Buffer) > 0 then + outPtr := @FSQLRecord.Buffer[0]; + try + if FSQLRecord.Count > 0 then + begin + {a statement with an output message - execute procedure, or + insert/update/delete ... returning - answers a singleton result + with the execute, so op_execute2 must be used: the server expects + to send the row and a plain op_execute desynchronises the + connection} + Connection.ExecuteStatement2(FHandle, + (FTransactionIntf as TObject as TFBWireTransaction).Handle, + FSQLParams.Format,paramPtr,FSQLRecord.Format,outPtr, + FStatementTimeout,cardinal(GetAttachment.GetInlineBlobLimit)); + FSingleResults := true; + FSQLRecord.RowChange; + Result := TResults.Create(FSQLRecord); + end + else + Connection.ExecuteStatement(FHandle, + (FTransactionIntf as TObject as TFBWireTransaction).Handle, + FSQLParams.Format,paramPtr,FStatementTimeout,0, + cardinal(GetAttachment.GetInlineBlobLimit)); + except + on E: Exception do WireIBError(FWireAPI,E); + end; + Inc(FChangeSeqNo); +end; + +function TFBWireStatement.InternalOpenCursor(aTransaction: ITransaction; + Scrollable: boolean): IResultSet; +var paramPtr: PByte; + cursorFlags: cardinal; +begin + if Scrollable and not GetAttachment.HasScollableCursors then + IBError(ibxeNotSupported,[nil]); + FScrollable := Scrollable; + CheckTransaction(aTransaction); + if not FPrepared then + InternalPrepare; + CheckHandle; + if FStaleReferenceChecks and (FSQLParams.FTransactionSeqNo < + (FTransactionIntf as TObject as TFBTransaction).TransactionSeqNo) then + IBError(ibxeInterfaceOutofDate,[nil]); + FSQLRecord.FTransactionSeqNo := + (aTransaction as TObject as TFBTransaction).TransactionSeqNo; + if FSQLRecord.Count = 0 then + IBError(ibxeIsASelectStatement,[nil]); + paramPtr := nil; + if Length(FSQLParams.Buffer) > 0 then + paramPtr := @FSQLParams.Buffer[0]; + cursorFlags := 0; + if Scrollable then + cursorFlags := CURSOR_TYPE_SCROLLABLE; + try + Connection.ExecuteStatement(FHandle, + (aTransaction as TObject as TFBWireTransaction).Handle, + FSQLParams.Format,paramPtr,FStatementTimeout,cursorFlags, + cardinal(GetAttachment.GetInlineBlobLimit)); + except + on E: Exception do WireIBError(FWireAPI,E); + end; + ResetCursorState(FCursorState); + FOpen := true; + FBOF := true; + FEOF := false; + Inc(FCursorSeqNo); + Inc(FChangeSeqNo); + FExecTransactionIntf := aTransaction; + Result := TWireResultSet.Create(FSQLRecord); +end; + +function TFBWireStatement.FetchNextRow: boolean; +begin + Result := false; + if not FOpen then Exit; + try + Result := Connection.FetchRow(FHandle,FSQLRecord.Format, + @FSQLRecord.Buffer[0],DefaultFetchBatchSize,FCursorState); + except + on E: Exception do WireIBError(FWireAPI,E); + end; + if Result then + begin + FBOF := false; + Inc(FChangeSeqNo); + end + else + FEOF := true; +end; + +function TFBWireStatement.FetchScroll(aDirection: integer; + aPosition: integer): boolean; +begin + Result := false; + if not FOpen then + IBError(ibxeSQLClosed,[nil]); + if not FScrollable then + IBError(ibxeNotSupported,[nil]); + if (aDirection = fetch_prior) and FBOF then + IBError(ibxeBOF,[nil]); + try + Result := Connection.FetchRowScroll(FHandle,FSQLRecord.Format, + @FSQLRecord.Buffer[0],aDirection,aPosition,FCursorState); + except + on E: Exception do WireIBError(FWireAPI,E); + end; + if Result then + begin + {the same flag semantics as TFB30Statement.Fetch: success clears both + markers; prior falling off the top sets BOF; a failed positioned + fetch leaves the flags as they were} + FBOF := false; + FEOF := false; + Inc(FChangeSeqNo); + end + else + if aDirection = fetch_prior then + begin + FBOF := true; + FEOF := false; + end; +end; + +procedure TFBWireStatement.InternalClose(Force: boolean); +begin + if not Connection.Connected then + Force := true; + if FHasHandle and FOpen and not Force then + try + Connection.FreeStatement(FHandle,DSQL_close); + except + on E: EFBWireProtocolError do + {ending the transaction closes its cursors, so the server may have + closed this one already} + if not ((Length(E.Status) > 0) and + (E.Status[0].IntValue = isc_dsql_cursor_close_err)) then + WireIBError(FWireAPI,E); + on E: Exception do + WireIBError(FWireAPI,E); + end; + FOpen := false; + FEOF := true; + FBOF := false; + ResetCursorState(FCursorState); + FExecTransactionIntf := nil; + Inc(FChangeSeqNo); +end; + +procedure TFBWireStatement.FreeHandle; +begin + if not FHasHandle then Exit; + if Connection.Connected then + try + Connection.FreeStatement(FHandle,DSQL_drop); + except + {the server has already released the statement with the connection} + end; + FHasHandle := false; + FHandle := 0; + FPrepared := false; +end; + +procedure TFBWireStatement.GetDsqlInfo(info_request: byte; + buffer: ISQLInfoResults); +var items, response: TBytes; + len: integer; +begin + if not FPrepared then + InternalPrepare; + CheckHandle; + SetLength(items,1); + items[0] := info_request; + SetLength(response,0); + try + response := Connection.GetInfo(op_info_sql,FHandle,items, + (buffer as TSQLInfoResultsBuffer).getBufSize); + except + on E: Exception do WireIBError(FWireAPI,E); + end; + len := Length(response); + if len > (buffer as TSQLInfoResultsBuffer).getBufSize then + len := (buffer as TSQLInfoResultsBuffer).getBufSize; + if len > 0 then + Move(response[0],(buffer as TSQLInfoResultsBuffer).Buffer^,len); +end; + +function TFBWireStatement.GetSQLParams: ISQLParams; +begin + if not FPrepared then + InternalPrepare; + CheckHandle; + Result := TSQLParams.Create(FSQLParams); +end; + +function TFBWireStatement.GetMetaData: IMetaData; +begin + if not FPrepared then + InternalPrepare; + CheckHandle; + Result := TMetaData.Create(FSQLRecord); +end; + +function TFBWireStatement.ColumnSchemaName(aIndex: integer): AnsiString; +begin + Result := ''; + if (aIndex >= 0) and (aIndex < Length(FSQLRecord.Format)) then + Result := FSQLRecord.Format[aIndex].SchemaName; +end; + +function TFBWireStatement.GetFlags: TStatementFlags; +begin + Result := []; + if FSQLStatementType in [SQLSelect, SQLSelectForUpdate] then + Result := Result + [stHasCursor]; + if FScrollable then + Result := Result + [stScrollable]; +end; + +procedure TFBWireStatement.SetStatementTimeout(aMilliseconds: cardinal); +begin + if (aMilliseconds <> 0) and + (Connection.ProtocolVersion < (PROTOCOL_VERSION16 and FB_PROTOCOL_MASK)) then + IBError(ibxeNotSupported,[nil]); + FStatementTimeout := aMilliseconds; +end; + +function TFBWireStatement.HasBatchMode: boolean; +begin + Result := GetAttachment.HasBatchMode; +end; + +function TFBWireStatement.IsInBatchMode: boolean; +begin + Result := FBatchActive; +end; + +procedure TFBWireStatement.CheckChangeBatchRowLimit; +begin + if IsInBatchMode then + IBError(ibxeInBatchMode,[nil]); +end; + +procedure TFBWireStatement.CheckBatchModeAvailable; +begin + if not HasBatchMode then + IBError(ibxeBatchModeNotSupported,[nil]); + case SQLStatementType of + SQLInsert, + SQLUpdate: {OK}; + else + IBError(ibxeInvalidBatchQuery,[GetSQLStatementTypeName]); + end; +end; + +procedure TFBWireStatement.ReleaseBatch(SendCancel: boolean); +begin + if not FBatchActive then Exit; + try + if SendCancel then + Connection.BatchCancel(FHandle) + else + Connection.BatchRelease(FHandle); + except + on E: Exception do WireIBError(FWireAPI,E); + end; + FBatchActive := false; + SetLength(FBatchRows,0); + FBatchRowCount := 0; + FBatchBufferUsed := 0; +end; + +procedure TFBWireStatement.AddToBatch; +const SixteenMB = 16 * 1024 * 1024; + MB256 = 256 * 1024 * 1024; +var alignedLen: cardinal; + row: TBytes; + PB: TBytes; + i: integer; + blobId: Int64; + + procedure AddIntClumplet(var aPB: TBytes; aTag: byte; aValue: integer); + var n: integer; + begin + {the IBatch PB is a wide tagged clumplet buffer: each clumplet is a + tag byte, a four byte little endian length, then the data} + n := Length(aPB); + SetLength(aPB,n + 9); + aPB[n] := aTag; + aPB[n+1] := 4; + aPB[n+2] := 0; + aPB[n+3] := 0; + aPB[n+4] := 0; + aPB[n+5] := aValue and $FF; + aPB[n+6] := (aValue shr 8) and $FF; + aPB[n+7] := (aValue shr 16) and $FF; + aPB[n+8] := (aValue shr 24) and $FF; + end; + +begin + FBatchCompletion := nil; + if not FPrepared then + InternalPrepare; + CheckHandle; + CheckBatchModeAvailable; + alignedLen := AlignTo(EngineMessageLength(FSQLParams.Format),8); + if not FBatchActive then + begin + {the same buffer sizing rule as the 3.0 provider, so behaviour + matches across providers} + if FBatchRowLimit = maxint then + FBatchBufferSize := MB256 + else + begin + FBatchBufferSize := FBatchRowLimit * integer(alignedLen); + if FBatchBufferSize < SixteenMB then + FBatchBufferSize := SixteenMB; + if FBatchBufferSize > MB256 then + IBError(ibxeBatchBufferSizeTooBig,[FBatchBufferSize]); + end; + {the IBatch parameter block: a wide tagged clumplet buffer} + SetLength(PB,1); + PB[0] := 1; {IBatch::VERSION1} + AddIntClumplet(PB,2 {TAG_RECORD_COUNTS},1); + AddIntClumplet(PB,3 {TAG_BUFFER_BYTES_SIZE},FBatchBufferSize); + try + Connection.BatchCreate(FHandle,FSQLParams.Format, + EngineMessageLength(FSQLParams.Format),PB); + except + on E: Exception do WireIBError(FWireAPI,E); + end; + FBatchActive := true; + SetLength(FBatchRows,0); + FBatchRowCount := 0; + FBatchBufferUsed := 0; + end; + + Inc(FBatchRowCount); + Inc(FBatchBufferUsed,alignedLen); + if FBatchBufferUsed > FBatchBufferSize then + raise EIBBatchBufferOverflow.Create(Ord(ibxeBatchRowBufferOverflow), + Format(GetErrorMessage(ibxeBatchRowBufferOverflow), + [FBatchRowCount,FBatchBufferSize])); + + {snapshot the message: the caller reuses the parameter buffer for the + next row} + row := system.copy(FSQLParams.Buffer,0,Length(FSQLParams.Buffer)); + SetLength(FBatchRows,FBatchRowCount); + FBatchRows[FBatchRowCount-1] := row; + + {register the row's blob ids: the engine translates every non null, + non zero blob id in a batch message through its registration map and + consumes the entry, so this happens once per row - as the 3.0 + provider does in its message packer} + with FSQLParams do + for i := 0 to Length(Format) - 1 do + if (Format[i].SQLType = SQL_BLOB) and + (PInteger(@row[Format[i].NullOffset])^ = 0) then + begin + blobId := WireQuadToInt64(@row[Format[i].DataOffset]); + if blobId <> 0 then + try + Connection.BatchRegBlob(FHandle,blobId,blobId); + except + on E: Exception do WireIBError(FWireAPI,E); + end; + end; +end; + +function TFBWireStatement.ExecuteBatch(aTransaction: ITransaction + ): IBatchCompletion; + + procedure Check4BatchCompletionError(bc: IBatchCompletion); + var status: IStatus; + RowNo: integer; + begin + status := nil; + {Raise an exception if there was an error reported in the completion} + if (bc <> nil) and bc.getErrorStatus(RowNo,status) then + raise EIBInterbaseError.Create(status,ConnectionCodePage); + end; + +const RowsPerMsgPacket = 500; +var trHandle: integer; + CS: TWireBatchCS; + i, chunk: integer; +begin + Result := nil; + if not FBatchActive then + IBError(ibxeNotInBatchMode,[]); + + if aTransaction = nil then + trHandle := (FTransactionIntf as TObject as TFBWireTransaction).Handle + else + trHandle := (aTransaction as TObject as TFBWireTransaction).Handle; + + try + i := 0; + while i < FBatchRowCount do + begin + chunk := FBatchRowCount - i; + if chunk > RowsPerMsgPacket then + chunk := RowsPerMsgPacket; + Connection.BatchMsg(FHandle,FSQLParams.Format, + system.copy(FBatchRows,i,chunk)); + Inc(i,chunk); + end; + CS := Connection.BatchExec(FHandle,trHandle); + except + on E: Exception do + begin + ReleaseBatch(true); + WireIBError(FWireAPI,E); + end; + end; + FBatchCompletion := TWireBatchCompletion.Create(FWireAPI,CS,ConnectionCodePage); + ReleaseBatch(false); + Inc(FChangeSeqNo); + Check4BatchCompletionError(FBatchCompletion); + Result := FBatchCompletion; +end; + +procedure TFBWireStatement.CancelBatch; +begin + if not FBatchActive then + IBError(ibxeNotInBatchMode,[]); + ReleaseBatch(true); +end; + +function TFBWireStatement.GetBatchCompletion: IBatchCompletion; +begin + Result := FBatchCompletion; +end; + +function TFBWireStatement.CreateBlob(column: TColumnMetaData): IBlob; +begin + if column.SQLType <> SQL_BLOB then + IBError(ibxeNotABlob,[nil]); + Result := TFBWireBlob.Create(GetAttachment as TFBWireAttachment, + GetTransaction as TFBWireTransaction,column.GetBlobMetaData,nil); +end; + +function TFBWireStatement.CreateArray(column: TColumnMetaData): IArray; +begin + if assigned(column) and (column.SQLType <> SQL_ARRAY) then + IBError(ibxeNotAnArray,[nil]); + Result := TFBWireArray.Create(GetAttachment as TFBWireAttachment, + GetTransaction as TObject as TFBWireTransaction, + column.GetArrayMetaData); +end; + +function TFBWireStatement.GetPlan: AnsiString; +var info: ISQLInfoResults; +begin + Result := ''; + if not (FSQLStatementType in [SQLSelect,SQLSelectForUpdate,SQLExecProcedure, + SQLUpdate,SQLDelete,SQLInsert]) then + Exit; + info := GetDSQLInfo(isc_info_sql_get_plan); + if info.Count > 0 then + Result := Trim(info[0].GetAsString); +end; + +function TFBWireStatement.IsPrepared: boolean; +begin + Result := FPrepared; +end; + +end. diff --git a/client/wire/FBWireStream.pas b/client/wire/FBWireStream.pas new file mode 100644 index 00000000..9267830b --- /dev/null +++ b/client/wire/FBWireStream.pas @@ -0,0 +1,818 @@ +(* + * Firebird Interface (fbintf). The fbintf components provide a set of + * Pascal language bindings for the Firebird API. + * + * This file is part of the pure Pascal wire protocol implementation + * (no fbclient required) and is subject to the Initial Developer's + * Public License Version 1.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy + * of the License here: + * + * http://www.firebirdsql.org/index.php?op=doc&id=idpl + * + * Software distributed under the License is distributed on an "AS + * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or + * implied. See the License for the specific language governing rights + * and limitations under the License. + * + * The Initial Developer of the Original Code is MWA Software + * (http://www.mwasoftware.co.uk). + * + * All Rights Reserved. + * + * Contributor(s): ______________________________________. + * +*) +unit FBWireStream; + +{ TCP transport and XDR encoding layer for the Firebird wire protocol. + + The remote protocol encodes all packets using a subset of XDR (RFC 4506): + - all integers are 4 byte big-endian, 64 bit values as two 4 byte words + - opaque byte strings are padded with zeroes to a multiple of 4 bytes + - counted strings ("cstring") are a 4 byte length followed by padded opaque + + TFBWireTransport provides a buffered TCP connection with optional + symmetric stream encryption (wire encryption) that can be switched on + mid-stream after the op_crypt handshake. +} + +{$IFDEF FPC} +{$mode delphi} +{$codepage UTF8} +{$interfaces COM} +{$ENDIF} + +interface + +uses + Classes, SysUtils, syncobjs + {$IFDEF FPC} + , sockets, ssockets, zbase, zdeflate, zinflate + {$ELSE} + {$IFDEF MSWINDOWS} + , Winapi.Windows, Winapi.Winsock2 + {$ELSE} + , Posix.Base, Posix.SysSocket, Posix.SysTime, Posix.NetinetIn, + Posix.NetinetTcp, Posix.NetDB, Posix.Unistd + {$ENDIF} + {$ENDIF} + ; + +const + DefaultRecvBufferSize = 32 * 1024; + DefaultSendBufferSize = 32 * 1024; + +type + { TWireCipher: a symmetric stream cipher filter. Separate instances are + used for the send and receive directions. } + + TWireCipher = class + public + procedure Process(var aData; aLen: integer); virtual; abstract; + end; + + { Raised on network level errors. The protocol layer translates this + into an EIBInterBaseError with isc_network_error where appropriate. } + EFBWireError = class(Exception); + + { TFBWireTransport } + + TFBWireTransport = class + private + {$IFDEF FPC} + FSocket: TInetSocket; + {$ELSE} + {$IFDEF MSWINDOWS} + FSocket: TSocket; + {$ELSE} + FSocket: integer; {a POSIX file descriptor} + {$ENDIF} + {$ENDIF} + FConnected: boolean; + FSendCipher: TWireCipher; + FRecvCipher: TWireCipher; + FRecvBuffer: TBytes; + FRecvPos: integer; {next unread byte} + FRecvLimit: integer; {end of valid data} + FSendBuffer: TBytes; + FSendPos: integer; + {serialises cipher application and the socket write between Flush and + SendDirect - see SendDirect} + FSendLock: TCriticalSection; + FPacketsSent: cardinal; + {zlib wire compression (pflag_compress): one stream per direction + for the whole session, sitting between the packet layer and the + cipher - compress then encrypt on send, decrypt then inflate on + receive} + FCompressed: boolean; + {$IFDEF FPC} + FDeflateStream: z_stream; + FInflateStream: z_stream; + {$ENDIF} + FRawRecvBuffer: TBytes; {decrypted deflate stream, not yet inflated} + procedure FillRecvBuffer; + procedure WriteToSocket(const aData; aLen: integer); + {deflates and sends aData - caller holds FSendLock} + procedure CompressAndSend(const aData; aLen: integer); + public + constructor Create; + destructor Destroy; override; + procedure ConnectTo(const aHost: AnsiString; aPort: integer; aTimeout: integer = 0); + procedure Disconnect; + {shuts the socket down without closing it, so that a read blocked in + another thread returns. The reader then calls Disconnect.} + procedure Abort; + {read exactly aLen bytes (blocking)} + procedure ReadBytes(var aData; aLen: integer); + {queue bytes for sending} + procedure WriteBytes(const aData; aLen: integer); + {send all queued bytes} + procedure Flush; + {writes a complete packet to the socket immediately, bypassing the + send buffer. This is for op_cancel: safe to call from a different + thread to the one that owns the connection, even while that thread + is blocked in ReadBytes. The send lock serialises the cipher and the + socket write against Flush, and the stream cipher stays consistent + because bytes are enciphered in the order they reach the wire. Any + packet the owner has assembled but not yet flushed simply follows + this one, which the protocol permits between packets.} + procedure SendDirect(const aData; aLen: integer); + {true if unread data is buffered locally (does not poll the socket)} + function HasBufferedData: boolean; + {switch on wire encryption. Transport takes ownership of the ciphers. + The two directions are enabled separately: the receive cipher is + installed after op_crypt has been sent (the server encrypts from the + moment it receives op_crypt) while the send cipher is only installed + once the op_crypt response has been validated. EnableRecvCipher must + be called with an empty receive buffer, i.e. with no partially + consumed server packet.} + procedure EnableRecvCipher(aRecvCipher: TWireCipher); + procedure EnableSendCipher(aSendCipher: TWireCipher); + {switches on zlib wire compression - called once, immediately after + the accept packet that carried pflag_compress has been read in + full: everything after it travels deflated on both directions} + procedure EnableCompression; + property Compressed: boolean read FCompressed; + property Connected: boolean read FConnected; + {counts socket writes (Flush and SendDirect) - a request/response + round trip is one flush, so tests can assert that an operation + caused no wire traffic} + property PacketsSent: cardinal read FPacketsSent; + end; + + { TXDRStream: XDR encode/decode over a TFBWireTransport } + + TXDRStream = class + private + FTransport: TFBWireTransport; + public + constructor Create(aTransport: TFBWireTransport); + + {receive side} + function ReadInt32: Integer; + function ReadUInt32: Cardinal; + function ReadInt64: Int64; + {opaque of known length + skip padding} + function ReadOpaque(aLen: integer): TBytes; + procedure ReadRaw(var aData; aLen: integer); + {4 byte count followed by padded opaque} + function ReadString: TBytes; + function ReadStringAsAnsi: AnsiString; + procedure SkipBytes(aLen: integer); + + {send side - data is queued until Flush} + procedure WriteInt32(aValue: Integer); + procedure WriteUInt32(aValue: Cardinal); + procedure WriteInt64(aValue: Int64); + procedure WriteRaw(const aData; aLen: integer); + {opaque padded to multiple of 4, no length prefix} + procedure WriteOpaque(const aData: TBytes); + {4 byte length followed by padded opaque} + procedure WriteString(const aData: TBytes); overload; + procedure WriteString(const aData: AnsiString); overload; + procedure Flush; + + property Transport: TFBWireTransport read FTransport; + end; + +function XDRPadLength(aLen: integer): integer; + +implementation + +function XDRPadLength(aLen: integer): integer; +begin + Result := (4 - (aLen and 3)) and 3; +end; + +{$IFNDEF FPC} +const + {$IFDEF MSWINDOWS} + InvalidWireSocket = TSocket(INVALID_SOCKET); + {$ELSE} + InvalidWireSocket = -1; + {$ENDIF} + +{the last socket error as text, for EFBWireError messages} +function WireSocketError: string; +begin + {$IFDEF MSWINDOWS} + Result := SysErrorMessage(WSAGetLastError); + {$ELSE} + Result := SysErrorMessage(GetLastError); + {$ENDIF} +end; + +{$IFDEF MSWINDOWS} +var + WSAInitialised: boolean = false; + +{winsock needs a one time WSAStartup per process. Never unloaded: the + provider has process lifetime and WSACleanup on unit finalisation is a + known source of shutdown ordering faults.} +procedure EnsureWinsock; +var WSAData: TWSAData; +begin + if not WSAInitialised then + begin + if WSAStartup($0202,WSAData) <> 0 then + raise EFBWireError.Create('Unable to initialise winsock: ' + WireSocketError); + WSAInitialised := true; + end; +end; +{$ENDIF} +{$ENDIF} + +{ TFBWireTransport } + +constructor TFBWireTransport.Create; +begin + inherited Create; + {$IFNDEF FPC} + FSocket := InvalidWireSocket; + {$ENDIF} + SetLength(FRecvBuffer,DefaultRecvBufferSize); + SetLength(FSendBuffer,DefaultSendBufferSize); + FRecvPos := 0; + FRecvLimit := 0; + FSendPos := 0; + FSendLock := TCriticalSection.Create; +end; + +destructor TFBWireTransport.Destroy; +begin + Disconnect; + if FSendCipher <> nil then FSendCipher.Free; + if FRecvCipher <> nil then FRecvCipher.Free; + FSendLock.Free; + inherited Destroy; +end; + +{$IFDEF FPC} +procedure TFBWireTransport.ConnectTo(const aHost: AnsiString; aPort: integer; + aTimeout: integer); +{$IF declared(TCP_NODELAY) and declared(IPPROTO_TCP)} +var NoDelay: integer; +{$IFEND} +begin + Disconnect; + try + FSocket := TInetSocket.Create(aHost,aPort); + if aTimeout > 0 then + FSocket.IOTimeout := aTimeout; + {disable Nagle: the protocol is strictly request/response, so waiting + to coalesce a packet only adds latency. Not every platform unit + declares the option, and it is only an optimisation.} + {$IF declared(TCP_NODELAY) and declared(IPPROTO_TCP)} + NoDelay := 1; + fpsetsockopt(FSocket.Handle,IPPROTO_TCP,TCP_NODELAY,@NoDelay,SizeOf(NoDelay)); + {$IFEND} + except + on E: Exception do + raise EFBWireError.Create( + Format('Unable to connect to server "%s" port %d: %s', + [aHost,aPort,E.Message])); + end; + FConnected := true; +end; +{$ELSE} +procedure TFBWireTransport.ConnectTo(const aHost: AnsiString; aPort: integer; + aTimeout: integer); +var + {$IFDEF MSWINDOWS} + Hints: ADDRINFOA; + AddrList, Addr: PADDRINFOA; + TimeoutValue: DWORD; + {$ELSE} + Hints: addrinfo; + AddrList, Addr: Paddrinfo; + TimeoutValue: timeval; + {$ENDIF} + PortStr: AnsiString; + NoDelay: integer; + + procedure ConnectError(const aDetail: string); + begin + raise EFBWireError.Create( + Format('Unable to connect to server "%s" port %d: %s', + [aHost,aPort,aDetail])); + end; + +begin + Disconnect; + {$IFDEF MSWINDOWS} + EnsureWinsock; + {$ENDIF} + FillChar(Hints,SizeOf(Hints),0); + Hints.ai_family := AF_UNSPEC; + Hints.ai_socktype := SOCK_STREAM; + Hints.ai_protocol := IPPROTO_TCP; + PortStr := AnsiString(IntToStr(aPort)); + AddrList := nil; + {$IFDEF MSWINDOWS} + if (getaddrinfo(PAnsiChar(aHost),PAnsiChar(PortStr),@Hints,AddrList) <> 0) or + (AddrList = nil) then + {$ELSE} + if (getaddrinfo(MarshaledAString(PAnsiChar(aHost)), + MarshaledAString(PAnsiChar(PortStr)),Hints,AddrList) <> 0) or + (AddrList = nil) then + {$ENDIF} + ConnectError('host name lookup failed'); + try + {an IPv4 result if there is one, matching the FPC branch's resolver} + Addr := AddrList; + while (Addr <> nil) and (Addr.ai_family <> AF_INET) do + Addr := Addr.ai_next; + if Addr = nil then + Addr := AddrList; + FSocket := socket(Addr.ai_family,Addr.ai_socktype,Addr.ai_protocol); + if FSocket = InvalidWireSocket then + ConnectError(WireSocketError); + {$IFDEF MSWINDOWS} + if connect(FSocket,Addr.ai_addr,integer(Addr.ai_addrlen)) <> 0 then + {$ELSE} + if connect(FSocket,Addr.ai_addr^,socklen_t(Addr.ai_addrlen)) <> 0 then + {$ENDIF} + begin + Disconnect; + ConnectError(WireSocketError); + end; + finally + {$IFDEF MSWINDOWS} + freeaddrinfo(AddrList); + {$ELSE} + freeaddrinfo(AddrList^); + {$ENDIF} + end; + if aTimeout > 0 then + begin + {$IFDEF MSWINDOWS} + TimeoutValue := DWORD(aTimeout); + setsockopt(FSocket,SOL_SOCKET,SO_RCVTIMEO,PAnsiChar(@TimeoutValue),SizeOf(TimeoutValue)); + setsockopt(FSocket,SOL_SOCKET,SO_SNDTIMEO,PAnsiChar(@TimeoutValue),SizeOf(TimeoutValue)); + {$ELSE} + TimeoutValue.tv_sec := aTimeout div 1000; + TimeoutValue.tv_usec := (aTimeout mod 1000) * 1000; + setsockopt(FSocket,SOL_SOCKET,SO_RCVTIMEO,@TimeoutValue,SizeOf(TimeoutValue)); + setsockopt(FSocket,SOL_SOCKET,SO_SNDTIMEO,@TimeoutValue,SizeOf(TimeoutValue)); + {$ENDIF} + end; + {disable Nagle, as in the FPC branch} + NoDelay := 1; + {$IFDEF MSWINDOWS} + setsockopt(FSocket,IPPROTO_TCP,TCP_NODELAY,PAnsiChar(@NoDelay),SizeOf(NoDelay)); + {$ELSE} + setsockopt(FSocket,IPPROTO_TCP,TCP_NODELAY,@NoDelay,SizeOf(NoDelay)); + {$ENDIF} + FConnected := true; +end; +{$ENDIF} + +procedure TFBWireTransport.Disconnect; +begin + {$IFDEF FPC} + if FSocket <> nil then + FreeAndNil(FSocket); + if FCompressed then + begin + deflateEnd(FDeflateStream); + inflateEnd(FInflateStream); + end; + {$ELSE} + if FSocket <> InvalidWireSocket then + begin + {$IFDEF MSWINDOWS} + shutdown(FSocket,SD_BOTH); + closesocket(FSocket); + {$ELSE} + shutdown(FSocket,SHUT_RDWR); + __close(FSocket); + {$ENDIF} + FSocket := InvalidWireSocket; + end; + {$ENDIF} + FCompressed := false; + FConnected := false; + FRecvPos := 0; + FRecvLimit := 0; + FSendPos := 0; + {the ciphers belong to the session that has just ended - a reconnect + starts in clear and negotiates its own} + if FSendCipher <> nil then FreeAndNil(FSendCipher); + if FRecvCipher <> nil then FreeAndNil(FRecvCipher); +end; + +procedure TFBWireTransport.EnableCompression; +begin + {$IFDEF FPC} + if FCompressed then Exit; + FillChar(FDeflateStream,SizeOf(FDeflateStream),0); + if deflateInit(FDeflateStream,Z_DEFAULT_COMPRESSION) <> Z_OK then + raise EFBWireError.Create('Unable to initialise wire compression (deflate)'); + FillChar(FInflateStream,SizeOf(FInflateStream),0); + if inflateInit(FInflateStream) <> Z_OK then + begin + deflateEnd(FDeflateStream); + raise EFBWireError.Create('Unable to initialise wire compression (inflate)'); + end; + SetLength(FRawRecvBuffer,DefaultRecvBufferSize); + FInflateStream.next_in := nil; + FInflateStream.avail_in := 0; + FCompressed := true; + {$ELSE} + raise EFBWireError.Create('Wire compression is not implemented for this compiler'); + {$ENDIF} +end; + +procedure TFBWireTransport.CompressAndSend(const aData; aLen: integer); +{$IFDEF FPC} +var chunk: array[0..16383] of byte; + produced: integer; + rc: integer; +begin + {one Z_SYNC_FLUSH per packet: the peer must see complete packets + promptly or the request/response protocol deadlocks} + FDeflateStream.next_in := @aData; + FDeflateStream.avail_in := aLen; + repeat + FDeflateStream.next_out := @chunk[0]; + FDeflateStream.avail_out := SizeOf(chunk); + rc := deflate(FDeflateStream,Z_SYNC_FLUSH); + if (rc <> Z_OK) and (rc <> Z_BUF_ERROR) then + raise EFBWireError.CreateFmt('Wire compression failed (deflate: %d)',[rc]); + produced := SizeOf(chunk) - integer(FDeflateStream.avail_out); + if produced > 0 then + begin + if FSendCipher <> nil then + FSendCipher.Process(chunk[0],produced); + WriteToSocket(chunk[0],produced); + end; + {a sync flush is complete when deflate leaves room in the output} + until (FDeflateStream.avail_in = 0) and (FDeflateStream.avail_out > 0); +end; +{$ELSE} +begin +end; +{$ENDIF} + +procedure TFBWireTransport.Abort; +begin + {$IFDEF FPC} + if FSocket <> nil then + fpshutdown(FSocket.Handle,2 {SHUT_RDWR}); + {$ELSE} + if FSocket <> InvalidWireSocket then + {$IFDEF MSWINDOWS} + shutdown(FSocket,SD_BOTH); + {$ELSE} + shutdown(FSocket,SHUT_RDWR); + {$ENDIF} + {$ENDIF} +end; + +procedure TFBWireTransport.FillRecvBuffer; +var got: integer; + {$IFDEF FPC} + produced: integer; + rc: integer; + {$ENDIF} +begin + if not FConnected then + raise EFBWireError.Create('Wire transport is not connected'); + FRecvPos := 0; + FRecvLimit := 0; + + {$IFDEF FPC} + if FCompressed then + begin + {socket -> decrypt -> inflate -> FRecvBuffer. The inflate stream may + hold unconsumed input from the previous read; only refill from the + socket when it is exhausted and no output could be produced.} + repeat + if FInflateStream.avail_in = 0 then + begin + got := FSocket.Read(FRawRecvBuffer[0],Length(FRawRecvBuffer)); + if got <= 0 then + begin + Disconnect; + raise EFBWireError.Create('Connection lost to database server'); + end; + if FRecvCipher <> nil then + FRecvCipher.Process(FRawRecvBuffer[0],got); + FInflateStream.next_in := @FRawRecvBuffer[0]; + FInflateStream.avail_in := got; + end; + FInflateStream.next_out := @FRecvBuffer[0]; + FInflateStream.avail_out := Length(FRecvBuffer); + rc := inflate(FInflateStream,Z_NO_FLUSH); + if (rc <> Z_OK) and (rc <> Z_BUF_ERROR) and (rc <> Z_STREAM_END) then + begin + Disconnect; + raise EFBWireError.CreateFmt('Wire compression failed (inflate: %d)',[rc]); + end; + produced := Length(FRecvBuffer) - integer(FInflateStream.avail_out); + until produced > 0; + FRecvLimit := produced; + Exit; + end; + {$ENDIF} + + {$IFDEF FPC} + got := FSocket.Read(FRecvBuffer[0],Length(FRecvBuffer)); + {$ELSE} + got := integer(recv(FSocket,FRecvBuffer[0],Length(FRecvBuffer),0)); + {$ENDIF} + if got <= 0 then + begin + Disconnect; + raise EFBWireError.Create('Connection lost to database server'); + end; + if FRecvCipher <> nil then + FRecvCipher.Process(FRecvBuffer[0],got); + FRecvLimit := got; +end; + +procedure TFBWireTransport.ReadBytes(var aData; aLen: integer); +var p: PByte; + chunk: integer; +begin + p := @aData; + while aLen > 0 do + begin + if FRecvPos = FRecvLimit then + FillRecvBuffer; + chunk := FRecvLimit - FRecvPos; + if chunk > aLen then chunk := aLen; + Move(FRecvBuffer[FRecvPos],p^,chunk); + Inc(FRecvPos,chunk); + Inc(p,chunk); + Dec(aLen,chunk); + end; +end; + +procedure TFBWireTransport.WriteBytes(const aData; aLen: integer); +var p: PByte; + chunk: integer; +begin + p := @aData; + while aLen > 0 do + begin + if FSendPos = Length(FSendBuffer) then + Flush; + chunk := Length(FSendBuffer) - FSendPos; + if chunk > aLen then chunk := aLen; + Move(p^,FSendBuffer[FSendPos],chunk); + Inc(FSendPos,chunk); + Inc(p,chunk); + Dec(aLen,chunk); + end; +end; + +procedure TFBWireTransport.WriteToSocket(const aData; aLen: integer); +var written, total: integer; + p: PByte; +begin + total := 0; + p := @aData; + while total < aLen do + begin + {$IFDEF FPC} + written := FSocket.Write((p + total)^,aLen - total); + {$ELSE} + written := integer(send(FSocket,(p + total)^,aLen - total,0)); + {$ENDIF} + if written <= 0 then + begin + Disconnect; + raise EFBWireError.Create('Connection lost to database server'); + end; + Inc(total,written); + end; +end; + +procedure TFBWireTransport.Flush; +begin + if FSendPos = 0 then Exit; + if not FConnected then + raise EFBWireError.Create('Wire transport is not connected'); + FSendLock.Enter; + try + if FCompressed then + CompressAndSend(FSendBuffer[0],FSendPos) + else + begin + if FSendCipher <> nil then + FSendCipher.Process(FSendBuffer[0],FSendPos); + WriteToSocket(FSendBuffer[0],FSendPos); + end; + FSendPos := 0; + Inc(FPacketsSent); + finally + FSendLock.Leave; + end; +end; + +procedure TFBWireTransport.SendDirect(const aData; aLen: integer); +var buf: TBytes; +begin + if not FConnected then + raise EFBWireError.Create('Wire transport is not connected'); + SetLength(buf,aLen); + Move(aData,buf[0],aLen); + FSendLock.Enter; + try + if FCompressed then + {the peer inflates one continuous stream: an out of band packet + must travel through the same deflate state} + CompressAndSend(buf[0],aLen) + else + begin + if FSendCipher <> nil then + FSendCipher.Process(buf[0],aLen); + WriteToSocket(buf[0],aLen); + end; + Inc(FPacketsSent); + finally + FSendLock.Leave; + end; +end; + +function TFBWireTransport.HasBufferedData: boolean; +begin + Result := FRecvPos < FRecvLimit; + {$IFDEF FPC} + {deflate stream bytes already received but not yet inflated also count} + if not Result and FCompressed then + Result := FInflateStream.avail_in > 0; + {$ENDIF} +end; + +procedure TFBWireTransport.EnableRecvCipher(aRecvCipher: TWireCipher); +begin + {HasBufferedData covers both the plaintext buffer and, with + compression active, deflate stream bytes not yet inflated - either + would straddle the cipher changeover} + if HasBufferedData then + raise EFBWireError.Create( + 'Cannot enable wire encryption: unread data in receive buffer'); + FRecvCipher := aRecvCipher; +end; + +procedure TFBWireTransport.EnableSendCipher(aSendCipher: TWireCipher); +begin + Flush; + FSendCipher := aSendCipher; +end; + +{ TXDRStream } + +constructor TXDRStream.Create(aTransport: TFBWireTransport); +begin + inherited Create; + FTransport := aTransport; +end; + +function TXDRStream.ReadInt32: Integer; +begin + Result := Integer(ReadUInt32); +end; + +function TXDRStream.ReadUInt32: Cardinal; +var b: array[0..3] of byte; +begin + FTransport.ReadBytes(b,4); + Result := (Cardinal(b[0]) shl 24) or (Cardinal(b[1]) shl 16) or + (Cardinal(b[2]) shl 8) or Cardinal(b[3]); +end; + +function TXDRStream.ReadInt64: Int64; +var hi, lo: Cardinal; +begin + {transmitted as two 32 bit words, high word first} + hi := ReadUInt32; + lo := ReadUInt32; + Result := (Int64(hi) shl 32) or Int64(lo); +end; + +function TXDRStream.ReadOpaque(aLen: integer): TBytes; +begin + SetLength(Result,aLen); + if aLen > 0 then + FTransport.ReadBytes(Result[0],aLen); + SkipBytes(XDRPadLength(aLen)); +end; + +procedure TXDRStream.ReadRaw(var aData; aLen: integer); +begin + FTransport.ReadBytes(aData,aLen); +end; + +function TXDRStream.ReadString: TBytes; +var len: integer; +begin + len := ReadInt32; + if (len < 0) or (len > 1 shl 28) then + raise EFBWireError.Create( + Format('Invalid string length %d in server response',[len])); + Result := ReadOpaque(len); +end; + +function TXDRStream.ReadStringAsAnsi: AnsiString; +var b: TBytes; +begin + b := ReadString; + SetLength(Result,Length(b)); + if Length(b) > 0 then + Move(b[0],Result[1],Length(b)); +end; + +procedure TXDRStream.SkipBytes(aLen: integer); +var dummy: array[0..7] of byte; + chunk: integer; +begin + while aLen > 0 do + begin + chunk := aLen; + if chunk > SizeOf(dummy) then chunk := SizeOf(dummy); + FTransport.ReadBytes(dummy,chunk); + Dec(aLen,chunk); + end; +end; + +procedure TXDRStream.WriteInt32(aValue: Integer); +begin + WriteUInt32(Cardinal(aValue)); +end; + +procedure TXDRStream.WriteUInt32(aValue: Cardinal); +var b: array[0..3] of byte; +begin + b[0] := (aValue shr 24) and $FF; + b[1] := (aValue shr 16) and $FF; + b[2] := (aValue shr 8) and $FF; + b[3] := aValue and $FF; + FTransport.WriteBytes(b,4); +end; + +procedure TXDRStream.WriteInt64(aValue: Int64); +begin + WriteUInt32(Cardinal(QWord(aValue) shr 32)); + WriteUInt32(Cardinal(QWord(aValue) and $FFFFFFFF)); +end; + +procedure TXDRStream.WriteRaw(const aData; aLen: integer); +begin + FTransport.WriteBytes(aData,aLen); +end; + +procedure TXDRStream.WriteOpaque(const aData: TBytes); +const + Zeroes: array[0..3] of byte = (0,0,0,0); +begin + if Length(aData) > 0 then + FTransport.WriteBytes(aData[0],Length(aData)); + if XDRPadLength(Length(aData)) > 0 then + FTransport.WriteBytes(Zeroes,XDRPadLength(Length(aData))); +end; + +procedure TXDRStream.WriteString(const aData: TBytes); +begin + WriteInt32(Length(aData)); + WriteOpaque(aData); +end; + +procedure TXDRStream.WriteString(const aData: AnsiString); +var b: TBytes; +begin + SetLength(b,Length(aData)); + if Length(aData) > 0 then + Move(aData[1],b[0],Length(aData)); + WriteString(b); +end; + +procedure TXDRStream.Flush; +begin + FTransport.Flush; +end; + +end. diff --git a/client/wire/FBWireTransaction.pas b/client/wire/FBWireTransaction.pas new file mode 100644 index 00000000..84b88b57 --- /dev/null +++ b/client/wire/FBWireTransaction.pas @@ -0,0 +1,251 @@ +(* + * Firebird Interface (fbintf). The fbintf components provide a set of + * Pascal language bindings for the Firebird API. + * + * This file is part of the pure Pascal wire protocol implementation + * (no fbclient required) and is subject to the Initial Developer's + * Public License Version 1.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy + * of the License here: + * + * http://www.firebirdsql.org/index.php?op=doc&id=idpl + * + * Software distributed under the License is distributed on an "AS + * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or + * implied. See the License for the specific language governing rights + * and limitations under the License. + * + * The Initial Developer of the Original Code is MWA Software + * (http://www.mwasoftware.co.uk). + * + * All Rights Reserved. + * + * Contributor(s): ______________________________________. + * +*) +unit FBWireTransaction; + +{$IFDEF MSWINDOWS} +{$DEFINE WINDOWS} +{$ENDIF} + +{$IFDEF FPC} +{$mode delphi} +{$codepage UTF8} +{$interfaces COM} +{$ENDIF} + +interface + +uses + Classes, SysUtils, IB, FBTransaction, FBClientAPI, FBActivityMonitor, + FBOutputBlock, FBWireClientAPI, FBWireProtocol, FBWireConst; + +type + { TFBWireTransaction } + + TFBWireTransaction = class(TFBTransaction,ITransaction,IActivityMonitor) + private + FWireAPI: TFBWireClientAPI; + FHandle: integer; + FInTransaction: boolean; + function GetConnection: TFBWireConnection; + {forgets this transaction's inline blob cache entries in every + attachment - the ids die with the transaction} + procedure DropInlineBlobs; + protected + function GetActivityIntf(att: IAttachment): IActivityMonitor; override; + function GetTrInfo(ReqBuffer: PByte; ReqBufLen: integer): ITrInformation; override; + procedure InternalStartSingle(attachment: IAttachment); override; + procedure InternalStartMultiple; override; + function InternalCommit(Force: boolean): TTrCompletionState; override; + procedure InternalCommitRetaining; override; + function InternalRollback(Force: boolean): TTrCompletionState; override; + procedure InternalRollbackRetaining; override; + procedure SetInterface(api: TFBClientAPI); override; + public + procedure PrepareForCommit; override; + function GetInTransaction: boolean; override; + {the server side transaction handle - used by statements and blobs} + property Handle: integer read FHandle; + property Connection: TFBWireConnection read GetConnection; + end; + +implementation + +uses FBMessages, IBErrorCodes, FBWireAttachment; + +const + isc_info_end = 1; + +{ TFBWireTransaction } + +procedure TFBWireTransaction.SetInterface(api: TFBClientAPI); +begin + inherited SetInterface(api); + FWireAPI := api as TFBWireClientAPI; +end; + +function TFBWireTransaction.GetConnection: TFBWireConnection; +begin + if GetAttachmentCount = 0 then + IBError(ibxeNotInTransaction,[nil]); + Result := (GetAttachment(0) as IAttachment as TObject as TFBWireAttachment).Connection; +end; + +procedure TFBWireTransaction.DropInlineBlobs; +var i: integer; +begin + for i := 0 to GetAttachmentCount - 1 do + (GetAttachment(i) as IAttachment as TObject as TFBWireAttachment). + DropInlineBlobs(FHandle); +end; + +function TFBWireTransaction.GetActivityIntf(att: IAttachment): IActivityMonitor; +begin + Result := att as IActivityMonitor; +end; + +function TFBWireTransaction.GetTrInfo(ReqBuffer: PByte; ReqBufLen: integer): ITrInformation; +var items, response: TBytes; + i, len: integer; + Buffer: TTrInformation; +begin + CheckHandle; + SetLength(items,ReqBufLen); + for i := 0 to ReqBufLen - 1 do + items[i] := ReqBuffer[i]; + SetLength(response,0); + try + response := Connection.GetInfo(op_info_transaction,FHandle,items,DefaultBufferSize); + except + on E: Exception do WireIBError(FWireAPI,E); + end; + Buffer := TTrInformation.Create(FWireAPI); + Result := Buffer; + len := Length(response); + if len > Buffer.getBufSize then + len := Buffer.getBufSize; + if len > 0 then + Move(response[0],Buffer.Buffer^,len); +end; + +procedure TFBWireTransaction.InternalStartSingle(attachment: IAttachment); +var tpb: TBytes; +begin + if FInTransaction then Exit; + tpb := ParamBlockToBytes(FTPB); + try + FHandle := (attachment as TObject as TFBWireAttachment).Connection. + StartTransaction((attachment as TObject as TFBWireAttachment).Handle,tpb); + except + on E: Exception do WireIBError(FWireAPI,E); + end; + FInTransaction := true; +end; + +procedure TFBWireTransaction.InternalStartMultiple; +begin + {a transaction spanning several attachments requires the two phase commit + coordinator which this provider does not implement} + IBError(ibxeNotSupported,[nil]); +end; + +function TFBWireTransaction.InternalCommit(Force: boolean): TTrCompletionState; +begin + Result := trCommitted; + if not FInTransaction then Exit; + if not Connection.Connected then + begin + {the connection has gone: the server has already rolled the + transaction back} + FInTransaction := false; + FHandle := 0; + Exit; + end; + DropInlineBlobs; + try + Connection.Commit(FHandle); + except + on E: Exception do + begin + if not Force then + begin + Result := trCommitFailed; + WireIBError(FWireAPI,E); + end; + end; + end; + FInTransaction := false; + FHandle := 0; +end; + +procedure TFBWireTransaction.InternalCommitRetaining; +begin + CheckHandle; + {retaining keeps the transaction handle but a commit still invalidates + temporary blob ids} + DropInlineBlobs; + try + Connection.CommitRetaining(FHandle); + except + on E: Exception do WireIBError(FWireAPI,E); + end; +end; + +function TFBWireTransaction.InternalRollback(Force: boolean): TTrCompletionState; +begin + Result := trRolledback; + if not FInTransaction then Exit; + if not Connection.Connected then + begin + {the connection has gone: the server has already rolled the + transaction back} + FInTransaction := false; + FHandle := 0; + Exit; + end; + DropInlineBlobs; + try + Connection.Rollback(FHandle); + except + on E: Exception do + begin + if not Force then + begin + Result := trRollbackFailed; + WireIBError(FWireAPI,E); + end; + end; + end; + FInTransaction := false; + FHandle := 0; +end; + +procedure TFBWireTransaction.InternalRollbackRetaining; +begin + CheckHandle; + DropInlineBlobs; + try + Connection.RollbackRetaining(FHandle); + except + on E: Exception do WireIBError(FWireAPI,E); + end; +end; + +procedure TFBWireTransaction.PrepareForCommit; +begin + CheckHandle; + try + Connection.PrepareTransaction(FHandle); + except + on E: Exception do WireIBError(FWireAPI,E); + end; +end; + +function TFBWireTransaction.GetInTransaction: boolean; +begin + Result := FInTransaction; +end; + +end. diff --git a/client/wire/README.md b/client/wire/README.md new file mode 100644 index 00000000..e0262704 --- /dev/null +++ b/client/wire/README.md @@ -0,0 +1,194 @@ +# Pure Pascal Firebird wire protocol client + +This directory implements a Firebird client that speaks the remote (wire) +protocol directly over TCP. It needs no `fbclient` library: there is nothing +to install on the client machine beyond the compiled program itself. + +It supports Firebird 3.0, 4.0, 5.0 and 6.0 servers by negotiating protocol +versions 13 to 17, with SRP authentication and optional wire encryption. + +## Status + +Working and tested against a live server (see *Testing* below): + +* connection handshake with protocol negotiation (13, 14, 15, 16, 17) +* `Srp256` and `Srp` authentication, both the `op_cond_accept` flow (where + authentication must complete before attaching) and the `op_accept_data` + flow (where the proof travels in the DPB) +* wire encryption with `ChaCha64`, `ChaCha` and `Arc4` +* attach, create and drop database, detach +* transactions: start, commit, rollback, the retaining variants and the + two phase commit prepare +* DSQL: allocate, prepare with full describe, execute, execute with a + singleton result (`op_execute2`), fetch, close and drop, set cursor name +* all Firebird data types including `INT128`, `DECFLOAT(16)`, `DECFLOAT(34)`, + `BOOLEAN` and the time zone types +* blobs: create, open, read and write segments, close, cancel +* information calls for the database, transaction, statement and blob +* events: `IEvents` with asynchronous and synchronous waits, delivered on + the `op_connect_request` auxiliary connection by a listener thread +* `DECFLOAT(16)`/`DECFLOAT(34)` conversions through the provider's own + IEEE 754 densely packed decimal codec +* `execute procedure` and `insert/update ... returning` singleton results +* named parameters, parameter type coercion, create database from a SQL + statement, reconnection of a disconnected attachment + +The whole fbintf test suite (twenty two test programs) runs over this +provider with `testsuite/runtest.sh -a wire` and is compared against +`testsuite/FBWirereference.log`; CI runs it against Firebird 6 on every +change. + +Not implemented yet, and reported as `ibxeNotSupported` rather than +failing obscurely (see the roadmap in doc/WireProtocol.md for what each +would take): + +* array columns (`op_get_slice` / `op_put_slice` with SDL descriptions) +* the batch API of protocol 16 +* scrollable cursors (`op_fetch_scroll`, protocol 18) +* transactions spanning several attachments (needs a two phase commit + coordinator) + +## Verified servers + +Every row below was measured with `testsuite/WireTest.pas`, in CI for the +container rows and locally for the others. + +| Server | WireCrypt | Negotiated | Encryption | Result | +|---|---|---|---|---| +| 6.0 (CI container) | Enabled, Required | 20 | `ChaCha64` | 178 tests, 0 failures | +| 6.0.0 (local, LI-T6.0.0.2076) | Required | 20 | `ChaCha64` | 178 tests, 0 failures | +| 5.0 (CI container) | Enabled, Required | 19 | `ChaCha64` | 178 tests, 0 failures | +| 5.0.4 (local container) | Enabled, Required | 19 | `ChaCha64` | 178 tests, 0 failures | +| 4.0 (CI container) | Enabled, Required | 17 | `ChaCha64` | 178 tests, 0 failures | +| 3.0 (CI container) | Enabled | 15 | `Arc4` | 178 tests, 0 failures | +| no server | — | — | — | 36 tests, live sections skipped | + +Firebird 3 settles on protocol 15 with Arc4: it is the newest protocol that +server knows, and it predates the ChaCha plugins. Everything else in the +suite behaves identically there. + +Capping `TFBWireConnection.MaxProtocol` in turn, against both servers: + +| Offered up to | Negotiated | Encryption | +|---|---|---| +| 13 | refused with `isc_miss_wirecrypt` when the server requires encryption | — | +| 14 | 14 | `Arc4` | +| 15 | 15 | `Arc4` | +| 16 | 16 | `ChaCha64` | +| 17 | 17 | `ChaCha64` | + +Protocol 13 has no wire encryption, so a server configured with +`WireCrypt = Required` (the default from Firebird 4 on) refuses it. It is +still offered because a Firebird 3 server, or one configured with +`WireCrypt = Enabled`, accepts it. `ChaCha` and `ChaCha64` need the +initialisation vector the server only sends from protocol 16, which is why +14 and 15 fall back to `Arc4`. + +## A note on dropping a table you have read + +An attachment that has read a table keeps an interest in it that outlives +the transaction, and Firebird 5 then refuses `drop table` on that same +attachment with `isc_no_meta_update`. This is not a property of this +client: the stock fbclient provider fails in exactly the same place with +exactly the same code, and a second attachment issuing the drop blocks +rather than succeeding. `WireTest` therefore treats dropping its test +table as best effort and drops it at the start of the next run instead. + +## Layout + +| Unit | Contents | +|---|---| +| `FBWireBigInt` | arbitrary precision unsigned arithmetic for SRP | +| `FBWireCrypto` | SHA-1, SHA-256, RC4 and ChaCha20 | +| `FBWireSRP` | the client half of SRP-6a as the engine implements it | +| `FBWireStream` | buffered TCP transport, per direction ciphers, XDR codec | +| `FBWireConst` | operation codes and protocol constants from `protocol.h` | +| `FBWireMessage` | message buffer layout, BLR descriptions, row encoding | +| `FBWireDescribe` | `isc_info_sql` describe response parser | +| `FBWireProtocol` | the connection: handshake and all packet exchanges | +| `FBWireClientAPI` | `IFirebirdAPI` for the provider | +| `FBWireAttachment`, `FBWireTransaction`, `FBWireStatement`, `FBWireBlob` | the rest of the fbintf provider | + +`FBWireProtocol` is usable on its own if you want to speak the protocol +without the fbintf object model; the remaining units adapt it to +`IFirebirdAPI`. + +## Use + +```pascal +uses IB, FBWireClientAPI; + +var API: IFirebirdAPI; + DPB: IDPB; + Attachment: IAttachment; +begin + API := WireFirebirdAPI; {never loads a client library} + DPB := API.AllocateDPB; + DPB.Add(isc_dpb_user_name).AsString := 'SYSDBA'; + DPB.Add(isc_dpb_password).AsString := 'masterkey'; + DPB.Add(isc_dpb_lc_ctype).AsString := 'UTF8'; + Attachment := API.OpenDatabase('localhost:employee',DPB); + ... +``` + +Everything after that is the ordinary fbintf API, so code written against +`IAttachment`, `ITransaction`, `IStatement` and `IResultSet` works +unchanged. The password is used for the SRP exchange and is removed from +the DPB before the attach: it never travels over the network. + +Wire encryption is negotiated automatically and is on whenever the server +offers it. A server configured with `WireCrypt = Required` (the Firebird 4 +and later default) works out of the box; there is no way to reach such a +server with an unencrypted client. + +## Notes on the protocol + +These points are all places where a naive reading of the packet layouts +produces a client that does not work, so they are worth repeating here. + +* The key type sent in `op_crypt` is the type the server advertised in its + key clumplets, normally `Symmetric`. It is *not* the authentication + plugin name, although some documentation says so. +* Text columns must be described with `blr_text2` / `blr_varying2` naming + the column's character set. With plain `blr_text` / `blr_varying` the + engine assumes the connection character set and divides the declared + byte length by the maximum character size, so a `VARCHAR(37)` fetched + over a UTF8 connection silently becomes 9 characters and the fetch fails + with a string truncation error. +* XDR transmits a boolean as a single value byte padded to four bytes. It + is not a big endian integer, and sending it as one makes every boolean + read as false. +* A batched `op_fetch` must be drained completely before anything else is + sent on the connection: the server streams one `op_fetch_response` per + row and terminates the batch with a message count of zero. Rows are + therefore cached as they arrive. +* `ISC_QUAD` values (blob and array ids) put the high word first, which is + not the memory layout of an `Int64` on a little endian machine. Use + `WireQuadToInt64` and `Int64ToWireQuad`. +* SRP as implemented by the engine deviates from the specification in + several ways: the exponent is reduced modulo N, `H(N) xor H(g)` is a + modular exponentiation rather than an exclusive or, the salt is hashed + as its hexadecimal text, and the session key is always SHA-1 even for + `Srp256`. `FBWireSRP` documents each of these where it implements them. + +Error message text comes from the status vector strings that the server +sends, because `firebird.msg` is a client library resource. Most engine +errors carry their text; those that do not are reported as +`Firebird Error Code: n` with the numeric code. + +## Testing + +`testsuite/WireTest.pas` is a self contained regression test. It needs a +Firebird server and the `employee` example database: + +``` +cd testsuite +fpc -Fu../client/wire -Fu../client -Fu../client/2.5 -Fu../client/3.0 \ + -Fu../client/3.0/firebird -Fi../client/include WireTest.pas +./WireTest localhost:employee SYSDBA masterkey +``` + +It exercises the cryptographic primitives against their published test +vectors, then runs the protocol against the server: connect, attach, DDL, +parameterised DML, every data type, blob round trips, transaction control +and the `IFirebirdAPI` provider layer. diff --git a/client/wire/generate_messages.py b/client/wire/generate_messages.py new file mode 100644 index 00000000..911116b9 --- /dev/null +++ b/client/wire/generate_messages.py @@ -0,0 +1,251 @@ +#!/usr/bin/env python3 +"""Generates FBWireMessages.pas from the Firebird message headers. + +Usage: generate_messages.py > FBWireMessages.pas + +Reads src/include/firebird/impl/msg/{jrd,dsql,dyn,sqlerr,sqlwarn}.h - +the facilities a server round trip can produce - and emits a sorted +code table with the message format strings, consumed by +FindEngineMessage. The output is +committed; regenerating it is a maintenance task per Firebird release. +""" + +import re +import subprocess +import sys + +FACILITIES = {'JRD': 0, 'DSQL': 7, 'DYN': 8, 'SQLERR': 13, 'SQLWARN': 14} +ISC_BASE = 0x14000000 + +ENTRY = re.compile( + r'^FB_IMPL_MSG(?:_NO_SYMBOL|_SYMBOL)?\(\s*(JRD|DSQL|DYN|SQLERR|SQLWARN)\s*,\s*(\d+)\s*,') + +# the full form carries the sqlcode as its fourth argument +ENTRY_SQLCODE = re.compile( + r'^FB_IMPL_MSG\(\s*(?:JRD|DSQL|DYN|SQLERR|SQLWARN)\s*,\s*\d+\s*,\s*\w+\s*,\s*(-?\d+)\s*,') + +NO_SQLCODE = 32767 # sentinel: entry carries no sqlcode + + +def c_string_tail(line): + """Extracts the final C string literal from an FB_IMPL_MSG line.""" + # walk back from the closing ')' to the string's opening quote, + # honouring backslash escapes + end = line.rstrip() + if not end.endswith(')'): + return None + end = end[:-1].rstrip() + if not end.endswith('"'): + return None + i = len(end) - 2 + while i >= 0: + if end[i] == '"': + # count preceding backslashes: an even number closes the string + j = i - 1 + while j >= 0 and end[j] == '\\': + j -= 1 + if (i - 1 - j) % 2 == 0: + break + i -= 1 + raw = end[i + 1:-1] + # C escapes that occur in the message texts + out = [] + k = 0 + while k < len(raw): + c = raw[k] + if c == '\\' and k + 1 < len(raw): + n = raw[k + 1] + if n == '"': + out.append('"') + elif n == '\\': + out.append('\\') + elif n == 'n': + out.append('\n') + elif n == 't': + out.append('\t') + else: + out.append(n) + k += 2 + else: + out.append(c) + k += 1 + return ''.join(out) + + +def pas_literal(text): + """Renders text as a Pascal string literal (single line).""" + parts = [] + lit = '' + for ch in text: + if ch == '\n': + if lit: + parts.append("'" + lit + "'") + lit = '' + parts.append('#10') + elif ch == '\t': + if lit: + parts.append("'" + lit + "'") + lit = '' + parts.append('#9') + else: + lit += ch * 2 if ch == "'" else ch + if lit or not parts: + parts.append("'" + lit + "'") + return '+'.join(parts) + + +def main(): + if len(sys.argv) != 2: + sys.exit(__doc__) + root = sys.argv[1] + entries = {} + counted = 0 + for name, fac in FACILITIES.items(): + path = f'{root}/src/include/firebird/impl/msg/{name.lower()}.h' + for line in open(path, encoding='utf-8'): + line = line.strip() + m = ENTRY.match(line) + if not m: + continue + counted += 1 + text = c_string_tail(line) + if text is None: + sys.exit(f'unparsable entry: {line}') + code = ISC_BASE | (fac << 16) | int(m.group(2)) + msc = ENTRY_SQLCODE.match(line) + sqlcode = int(msc.group(1)) if msc else NO_SQLCODE + entries[code] = (text, sqlcode) + + try: + commit = subprocess.check_output( + ['git', '-C', root, 'rev-parse', '--short', 'HEAD'], + text=True).strip() + except Exception: + commit = 'unknown' + version = 'unknown' + try: + build_no = open(f'{root}/src/jrd/build_no.h', encoding='utf-8').read() + major = re.search(r'FB_MAJOR_VER\s+"(\d+)"', build_no).group(1) + minor = re.search(r'FB_MINOR_VER\s+"(\d+)"', build_no).group(1) + version = f'{major}.{minor}' + except Exception: + pass + + codes = sorted(entries) + w = sys.stdout.write + w(f'''(* + * Firebird Interface (fbintf). The fbintf components provide a set of + * Pascal language bindings for the Firebird API. + * + * This file is part of the pure Pascal wire protocol implementation + * and is subject to the Initial Developer's Public License Version 1.0. + * + * The Initial Developer of the Original Code is MWA Software + * (http://www.mwasoftware.co.uk). All Rights Reserved. + * +*) + +{{GENERATED FILE - DO NOT EDIT. + + Engine error message texts for the facilities a server round trip can + produce (JRD, DSQL, DYN, SQLERR, SQLWARN), so that the wire provider + can format status + vectors the way fb_interpret does without needing firebird.msg. + + Generated by generate_messages.py from the Firebird {version} message + headers (src/include/firebird/impl/msg, commit {commit}). + Regenerate per Firebird release.}} + +unit FBWireMessages; + +{{$IFDEF FPC}} +{{$mode delphi}} +{{$codepage UTF8}} +{{$ENDIF}} + +interface + +const + {{an entry with no SQLCODE mapping}} + NoSQLCode = 32767; + +{{looks up the fb_interpret format string - with @1..@n parameter + placeholders - for an engine error code. False when the code is not in + the table (an unknown facility, or a newer server's message).}} +function FindEngineMessage(aCode: cardinal; var aFormat: AnsiString): boolean; + +{{the SQLCODE the message database assigns to an engine error code, or + NoSQLCode - what isc_sqlcode derives per status vector item}} +function EngineMessageSQLCode(aCode: cardinal): integer; + +implementation + +const + MessageCount = {len(codes)}; + + MessageCodes: array[0..MessageCount-1] of cardinal = ( +''') + for i in range(0, len(codes), 8): + row = ', '.join(str(c) for c in codes[i:i + 8]) + sep = ',' if i + 8 < len(codes) else ');' + w(' ' + row + sep + '\n') + w(''' + MessageTexts: array[0..MessageCount-1] of AnsiString = ( +''') + for i, code in enumerate(codes): + sep = ',' if i + 1 < len(codes) else ');' + w(' {%d} %s%s\n' % (code, pas_literal(entries[code][0]), sep)) + w(''' + MessageSQLCodes: array[0..MessageCount-1] of smallint = ( +''') + for i in range(0, len(codes), 12): + row = ', '.join(str(entries[c][1]) for c in codes[i:i + 12]) + sep = ',' if i + 12 < len(codes) else ');' + w(' ' + row + sep + '\n') + w(''' +function FindMessageIndex(aCode: cardinal; var aIndex: integer): boolean; +var lo, hi, mid: integer; +begin + Result := false; + lo := 0; + hi := MessageCount - 1; + while lo <= hi do + begin + mid := (lo + hi) div 2; + if MessageCodes[mid] = aCode then + begin + aIndex := mid; + Exit(true); + end; + if MessageCodes[mid] < aCode then + lo := mid + 1 + else + hi := mid - 1; + end; +end; + +function FindEngineMessage(aCode: cardinal; var aFormat: AnsiString): boolean; +var idx: integer; +begin + Result := FindMessageIndex(aCode,idx); + if Result then + aFormat := MessageTexts[idx]; +end; + +function EngineMessageSQLCode(aCode: cardinal): integer; +var idx: integer; +begin + if FindMessageIndex(aCode,idx) then + Result := MessageSQLCodes[idx] + else + Result := NoSQLCode; +end; + +end. +''') + print(f'{len(entries)} messages from {counted} entries', + file=sys.stderr) + + +if __name__ == '__main__': + main() diff --git a/doc/WireProtocol.md b/doc/WireProtocol.md new file mode 100644 index 00000000..10fe2da8 --- /dev/null +++ b/doc/WireProtocol.md @@ -0,0 +1,899 @@ +# The pure Pascal wire protocol client + +`client/wire` implements a Firebird client that speaks the remote (wire) +protocol directly over TCP. It needs no `fbclient` library on the client +machine: the compiled program is the whole dependency. + +It is a third provider alongside the existing two. `client/2.5` binds the +legacy ISC API, `client/3.0` binds the Firebird 3 object oriented API, and +both dynamically load `fbclient`. `client/wire` instead implements the +protocol those libraries speak, so the same fbintf interfaces are available +where installing a client library is impractical: containers, single file +deployments, cross compiled targets, or a machine whose installed client is +older than the server. + +## Contents + +1. [Using it](#using-it) +2. [What is implemented](#what-is-implemented) +3. [Architecture](#architecture) +4. [The protocol, as actually implemented](#the-protocol-as-actually-implemented) +5. [Authentication](#authentication) +6. [Wire encryption](#wire-encryption) +7. [Messages, BLR and data types](#messages-blr-and-data-types) +8. [Error reporting](#error-reporting) +9. [Testing](#testing) +10. [Continuous integration](#continuous-integration) +11. [Roadmap](#roadmap) + +--- + +## Using it + +The only difference from ordinary fbintf code is where the API comes from: +`WireFirebirdAPI` instead of `IB.FirebirdAPI`. Everything below that is the +same object model. + +```pascal +uses IB, FBWireClientAPI; + +var + API: IFirebirdAPI; + DPB: IDPB; + Attachment: IAttachment; + Transaction: ITransaction; + Results: IResultSet; +begin + API := WireFirebirdAPI; {loads no library} + + DPB := API.AllocateDPB; + DPB.Add(isc_dpb_user_name).AsString := 'SYSDBA'; + DPB.Add(isc_dpb_password).AsString := 'masterkey'; + DPB.Add(isc_dpb_lc_ctype).AsString := 'UTF8'; + + Attachment := API.OpenDatabase('localhost:employee',DPB); + Transaction := Attachment.StartTransaction( + [isc_tpb_read_committed,isc_tpb_rec_version, + isc_tpb_wait,isc_tpb_write],taCommit); + + Results := Attachment.OpenCursor(Transaction, + 'select EMP_NO, FULL_NAME from EMPLOYEE where EMP_NO < ?',[20]); + while Results.FetchNext do + writeln(Results[0].AsInteger:5,' ',Results[1].AsString); + + Transaction.Commit; + Attachment.Disconnect; +end; +``` + +Connect strings take the usual forms, including an explicit port: + +``` +localhost:employee +localhost:/var/lib/firebird/data/employee.fdb +db.example.com/3051:payroll +inet://db.example.com/payroll +``` + +The path after the host is resolved **by the server**, so it is a path (or +alias) on the server machine, not on the client. + +### Passwords + +The password is used for the SRP exchange and then removed from the DPB +before the attach is sent. It never travels over the network in any form, +encrypted or otherwise, which is the point of SRP: the server stores a +verifier, not the password, and both sides end up agreeing a session key +without transmitting the secret. + +### Choosing the protocol version + +`TFBWireConnection.MaxProtocol` caps the highest version offered in +`op_connect`. It defaults to the newest the client implements. Lowering it +is useful for reproducing a problem against an older dialect of the +protocol, and is how the version matrix below was measured. + +--- + +## What is implemented + +Working and covered by the test suite: + +* the connection handshake with protocol negotiation, versions 13 to 20 +* `Srp256` and `Srp` authentication, in both the `op_cond_accept` flow + (authentication must finish before attaching) and the `op_accept_data` + flow (the proof travels in the DPB with the attach) +* wire encryption with `ChaCha64`, `ChaCha` and `Arc4` +* attach, create and drop database, detach +* transactions: start, commit, rollback, the retaining variants, and the + two phase commit prepare +* DSQL: allocate, prepare with a full describe, execute, execute with a + singleton result (`op_execute2`), fetch, close, drop, set cursor name +* every Firebird data type, including `INT128`, `DECFLOAT(16)`, + `DECFLOAT(34)`, `BOOLEAN` and the time zone types +* blobs: create, open, segmented read and write, close, cancel +* information calls for the database, transaction, statement and blob +* events: `IEvents` with asynchronous and synchronous waits, delivered on + the `op_connect_request` auxiliary connection by a listener thread +* array columns: `IArray` and `IArrayMetaData` over `op_get_slice` and + `op_put_slice`, with the SDL generator shared with the 3.0 provider +* operation cancellation (`IAttachment.CancelOperation` - `op_cancel` + sent out of band from another thread) and statement timeouts + (`IStatement.SetStatementTimeout` - the protocol 16 timeout field) +* scrollable cursors on protocol 18 servers: the five positioned fetches + of `IResultSet` over `op_fetch_scroll` +* the batch API on protocol 16 servers: `AddToBatch`/`ExecuteBatch`/ + `CancelBatch` with per row completion, including blob and array + columns +* inline blobs on protocol 19 servers: small blobs pushed with the rows + that reference them are opened and read with no further wire traffic +* protocol 20 with Firebird 6: the schema search path travels in the DPB + (`isc_dpb_search_path`) and the describe reports each column's schema +* zlib wire compression, requested per attachment with an + `isc_dpb_config` item of `WireCompression=true` and effective when the + server also enables it - deflate below the cipher, one stream per + direction +* engine error message text without `firebird.msg`: a generated table of + the message format strings, so status vectors read as `fb_interpret` + renders them, SQLCODE line included +* services: `IServiceManager` over its own connection, with the same SRP + authentication and wire encryption as a database attach + +Deliberately not implemented yet. These raise `ibxeNotSupported` rather +than failing in a confusing way: + +| Feature | What it needs | +|---|---| +| Multi database transactions | a two phase commit coordinator | + +Delphi support is present but so far unverified: the transport carries +Delphi branches over `Winapi.Winsock2` and the `Posix.*` socket units and +the wire units are listed in `fbintf.dpk`, but no Delphi toolchain has +compiled or run them yet (CI is FPC only). Wire compression stays FPC +only for now - `EnableCompression` reports it cleanly under Delphi. + +--- + +## Architecture + +``` + your code + │ IAttachment, ITransaction, IStatement, IResultSet, IBlob + ▼ + ┌─────────────────────────────────────────────────────────┐ + │ FBWireClientAPI IFirebirdAPI, status, date/time codecs │ + │ FBWireAttachment FBWireTransaction FBWireStatement │ + │ FBWireBlob │ provider + └─────────────────────────────────────────────────────────┘ + │ handles, message buffers + ▼ + ┌─────────────────────────────────────────────────────────┐ + │ FBWireProtocol handshake, one method per packet │ + │ FBWireDescribe isc_info_sql describe parser │ + │ FBWireMessage buffer layout, BLR, row encoding │ protocol + └─────────────────────────────────────────────────────────┘ + │ XDR primitives + ▼ + ┌─────────────────────────────────────────────────────────┐ + │ FBWireStream buffered TCP, per direction ciphers │ + │ FBWireSRP FBWireCrypto FBWireBigInt FBWireConst │ transport + └─────────────────────────────────────────────────────────┘ + │ TCP + ▼ + Firebird server +``` + +| Unit | Contents | +|---|---| +| `FBWireBigInt` | unsigned arbitrary precision arithmetic: Knuth algorithm D division, square and multiply modular exponentiation | +| `FBWireCrypto` | SHA-1, SHA-256, RC4, ChaCha20 | +| `FBWireSRP` | the client half of SRP-6a as the engine implements it | +| `FBWireStream` | buffered TCP transport, independent send and receive ciphers, the XDR codec | +| `FBWireConst` | operation codes and protocol constants from `protocol.h` | +| `FBWireMessage` | message buffer layout, BLR message descriptions, row encode and decode | +| `FBWireDescribe` | parser for the `isc_info_sql_*` describe response | +| `FBWireProtocol` | `TFBWireConnection`: the handshake and one method per packet exchange | +| `FBWireClientAPI` | `IFirebirdAPI`, the status object, the date and time codecs | +| `FBWireAttachment` `FBWireTransaction` `FBWireStatement` `FBWireBlob` | the provider proper | + +`FBWireProtocol` is usable on its own if you want to speak the protocol +without the fbintf object model: + +```pascal +Connection := TFBWireConnection.Create; +Connection.ConnectTo('localhost',3050,'employee','SYSDBA','masterkey'); +writeln(Connection.ProtocolVersion, ' ', Connection.CryptPlugin); +DbHandle := Connection.AttachDatabase('employee',DPBBytes); +``` + +### Reused from fbintf + +The provider deliberately reuses the existing machinery rather than +duplicating it: + +* `FBParamBlock` builds the DPB, TPB, SPB and BPB clumplet buffers. Those + buffers are exactly what the protocol carries, so they are sent verbatim. +* `FBOutputBlock` parses the information responses. +* `TSQLDataItem` supplies every data conversion. The provider only has to + place a pointer at the right offset in the message buffer and report the + type, scale and character set. +* `IBUtils` supplies connect string parsing and the named parameter + preprocessor. + +--- + +## The protocol, as actually implemented + +The reference is `src/remote/protocol.h`, `protocol.cpp` and +`interface.cpp` in the Firebird source tree. The points below are the ones +where a plausible reading of the packet layouts produces a client that does +not work; each cost a debugging cycle. + +### Framing + +There is none. The connection is one continuous XDR stream and packet +boundaries are found by decoding field by field. Every 16 bit field +occupies 4 bytes on the wire (`xdr_short` widens), opaque data is zero +padded to a multiple of 4, and 64 bit values travel as two 32 bit words, +high word first. + +### The version list + +`op_connect` carries a list of `(version, architecture, min type, max type, +weight)` entries and the server picks the highest weight it understands. +Always send `arch_generic`: if the entry's architecture matches the +server's own, the server sets `PORT_symmetric` and starts transmitting +messages as raw memory images instead of XDR. + +### Wire encryption key type + +`op_crypt` carries the plugin name and a **key type**. That key type is the +one the server advertised in its key clumplets, in practice `Symmetric`. It +is not the authentication plugin name; sending that gets the connection +closed with *"Client attempted to start wire encryption using unknown key"*. + +### Text columns need their character set + +Text fields must be described with `blr_text2` / `blr_varying2` naming the +column's character set. With plain `blr_text` / `blr_varying` the engine +assumes the connection character set and reinterprets the declared byte +length as `length div max bytes per character`. A `VARCHAR(37)` fetched +over a UTF8 connection silently becomes 9 characters and the fetch fails +with a string truncation error. + +### Booleans + +XDR sends a boolean as a single value byte padded to four bytes, value +first. Sending it as a big endian integer puts the value in the last byte +and every boolean reads back as false. + +### Fetches arrive as a stream + +The server answers one `op_fetch` with a sequence of `op_fetch_response` +packets, one row each, terminated by a packet whose message count is zero +(or whose status is 100 at end of cursor). The whole batch must be drained +before anything else is sent, otherwise the queued row packets are mistaken +for the next request's response. Rows are decoded into a cache as they +arrive and handed out one at a time. + +### Blob and array identifiers + +`ISC_QUAD` puts the high word first, which is not the memory layout of an +`Int64` on a little endian machine. `WireQuadToInt64` and `Int64ToWireQuad` +convert. + +--- + +## Authentication + +`FBWireSRP` implements SRP-6a as the engine implements it, which is not +quite as the specification describes it. Each deviation is required for +interoperability and is commented where it is implemented: + +* the exponent `a + u*x` is reduced modulo `N`; +* `H(N) xor H(g)` from RFC 5054 is a modular exponentiation, `H(N)^H(g) mod N`; +* the salt is hashed as its hexadecimal **text**, not as raw bytes; +* `k = SHA1(pad128(N), pad128(g))` pads its arguments to 128 bytes, while + `A`, `B`, `S` and the proof components are hashed in minimal form with no + padding; +* the account name is upper cased before hashing; +* the session key is always `SHA1(S)`, 20 bytes, even for `Srp256`. The + plugin's hash is used only for the client proof. + +The group is the 1024 bit prime from `srp.cpp` with generator 2. The client +public key `A` travels in `CNCT_specific_data`, chunked into pieces of at +most 254 bytes each prefixed with a part number; the proof travels either +in `op_cont_auth` or as `isc_dpb_specific_auth_data` in the DPB, depending +on which accept the server sent. + +`Legacy_Auth` is not implemented. It offers no session key, so it cannot +satisfy a server that requires wire encryption, and modern servers reject +it unless explicitly enabled. + +--- + +## Wire encryption + +After authentication the client holds a session key. The server advertises, +per key type, the wire encryption plugins it supports, and from protocol 16 +also each plugin's initialisation vector. The client picks the first of +`ChaCha64`, `ChaCha`, `Arc4` that the server offers and sends `op_crypt`. + +The changeover is asymmetric and easy to get wrong: + +* `op_crypt` itself is sent in clear; +* the server encrypts everything it sends **from the moment it receives + `op_crypt`**, so the receive cipher is installed before the response is + read; +* the client encrypts only after that response has been validated. + +`Arc4` uses the session key directly. The ChaCha plugins stretch it with +SHA-256 to 32 bytes and use the server supplied IV: 8 bytes of nonce for +`ChaCha64`, 12 bytes of nonce plus a 32 bit big endian counter for +`ChaCha`. Both directions use the same key and the same IV, which is what +the engine does. + +Protocol 14 and 15 settle on `Arc4` because the IV needed by the ChaCha +plugins is only advertised from protocol 16. + +--- + +## Messages, BLR and data types + +A prepared statement's parameters and columns are described by +`isc_info_sql_*` clumplets, parsed by `FBWireDescribe` into a +`TWireMessageFormat`. For text types the reported subtype is the character +set id and the reported length is already the byte length in that +character set. + +`FBWireMessage` then computes a flat buffer layout with the same storage +conventions the client library uses: `SQL_VARYING` is a two byte length +followed by the characters, integers are native endian and naturally +aligned, and the null indicators are four byte words placed after the data. +`TSQLDataItem` points straight into that buffer, so the whole fbintf +conversion layer works unchanged. + +On the wire, from protocol 13, a message is a null bitmap (one bit per +field, padded to four bytes) followed by the values of the fields that are +not null. + +Verified against the server for every type, with the exact byte patterns +checked by hand: + +| Type | Wire form | +|---|---| +| `SMALLINT`, `INTEGER`, `BIGINT` | 4 or 8 byte two's complement, scale applied by the caller | +| `FLOAT`, `DOUBLE PRECISION` | IEEE 754 bit pattern, high word first | +| `DATE` | days from 17 November 1858 | +| `TIME` | decimilliseconds since midnight | +| `TIMESTAMP` | the two above, in that order | +| `CHAR` | blank padded to the full field width | +| `VARCHAR` | counted string | +| `BOOLEAN` | one value byte, padded to four | +| `INT128` | two 64 bit words, high first | +| `DECFLOAT(16)`, `DECFLOAT(34)` | IEEE 754 decimal, word swapped | +| `TIME`/`TIMESTAMP WITH TIME ZONE` | the plain value followed by the zone id | +| `BLOB`, `ARRAY` | an `ISC_QUAD` identifier | + +--- + +## Error reporting + +The server sends its status vector with each error, and `FBWireProtocol` +decodes it into `isc_arg_*` items. `FBWireClientAPI` rebuilds a standard +ISC status vector from that, so `EIBInterBaseError`, `GetIBErrorCode` and +`CheckStatusVector` all behave as they do with the other providers. + +Message **text** is the one visible difference. `firebird.msg` is a client +library resource, so it is not available here. Most engine errors carry +interpreted text in the status vector and that text is used; the rest are +reported as `Firebird Error Code: n`. Where the stock provider says +*"unsuccessful metadata update"*, this one says +`Firebird Error Code: 335544351`. The numeric code, the SQLCODE path and +any strings the server supplies are all still there. + +--- + +## Testing + +`testsuite/WireTest.pas` is self contained and needs no fbclient: + +```bash +fpc -Fuclient -Fuclient/2.5 -Fuclient/3.0 -Fuclient/3.0/firebird \ + -Fuclient/wire -Ficlient/include -FEbuild testsuite/WireTest.pas + +./build/WireTest [ [ [ []]]] +``` + +Defaults are `localhost:employee SYSDBA masterkey`. Naming a scratch +database, which must not already exist, adds create and drop database +coverage: + +```bash +./build/WireTest localhost:employee SYSDBA masterkey localhost:/tmp/scratch.fdb +``` + +It runs in four layers, and the offline ones run even with no server: + +1. **Arithmetic** — the big integer operations, including a 1024 bit + modular exponentiation of the size SRP performs, against values computed + independently. +2. **Cryptography** — SHA-1, SHA-256, RC4 and ChaCha20 against the + published FIPS and RFC 8439 vectors. +3. **SRP** — a complete exchange against a fixed reference exchange: + client public key, session key and proof for both `Srp` and `Srp256`, + including the upper casing of the account name. +4. **Message layout** — alignment, buffer size, the generated BLR and the + quad conversions. + +Then, if a server answers: + +5. **Live connection and negotiation** — the handshake, and the version + actually negotiated when the offer is capped at 14, 15, 16 and 17. +6. **Provider** — queries and parameters, every data type, insert, update, + null handling, accented text, blob round trips, transaction rollback, + and create and drop database. + +With no server reachable the live sections report `SKIP` and the process +still exits 0, which is what the offline CI job relies on. + +### The full fbintf test suite + +`WireTest` is the unit layer. The integration layer is the ordinary +fbintf test suite - all twenty two programs - run over this provider: + +```bash +testsuite/runtest.sh -a wire +``` + +The `-a wire` (`--api wire`) switch makes `TTestApplication` obtain the +API from `WireFirebirdAPI` instead of `IB.FirebirdAPI`; nothing else in +the suite changes. The output is compared against +`testsuite/FBWirereference.log` after normalising the run dependent +values (transaction ids, page counters, journal timestamps) on both +sides of the diff. Tests for events, arrays and other unimplemented +features skip with a fixed message so the comparison stays exact. + +The reference log is the CI environment's own output: Firebird 6 +(ODS 14, protocol 17) in a container, the employee example database +restored from `testsuite/employee.gbk`, an x86_64 runner. Float to text +rendering differs in the last digit between CPU architectures, so a log +produced elsewhere (for example on ARM) shows a handful of known +differences. To regenerate the reference after an intended output +change, download the `wire-suite-testout` artifact from the CI run, +apply `runtest.sh`'s normalisation, and commit it. + +### Measured results + +| Server | WireCrypt | Negotiated | Encryption | Result | +|---|---|---|---|---| +| 6.0 (CI container) | Enabled, Required | 20 | `ChaCha64` | 178 tests, 0 failures | +| 6.0.0 (local, LI-T6.0.0.2076) | Required | 20 | `ChaCha64` | 178 tests, 0 failures | +| 5.0 (CI container) | Enabled, Required | 19 | `ChaCha64` | 178 tests, 0 failures | +| 5.0.4 (local container) | Enabled, Required | 19 | `ChaCha64` | 178 tests, 0 failures | +| 4.0 (CI container) | Enabled, Required | 17 | `ChaCha64` | 178 tests, 0 failures | +| 3.0 (CI container) | Enabled | 15 | `Arc4` | 178 tests, 0 failures | +| no server | — | — | — | 36 tests, live sections skipped | + +Firebird 3 settles on protocol 15 with Arc4: it is the newest protocol that +server knows, and it predates the ChaCha plugins. Everything else in the +suite behaves identically there. + +The container rows come from the CI matrix; the rest were run locally. + +### Dropping a table you have just read + +An attachment that has read a table keeps an interest in it that outlives +the transaction, and Firebird 5 then refuses `drop table` on that same +attachment with `isc_no_meta_update`. This is a property of the server, not +of this client: the stock fbclient provider fails in exactly the same place +with exactly the same code, and a second attachment issuing the drop blocks +instead of succeeding. `WireTest` therefore treats dropping its test table +as best effort and drops it at the start of the next run. + +--- + +## Continuous integration + +`.github/workflows/wire-protocol.yml` runs on every push and pull request +that touches `client/**` or the test. + +The **server** job runs a matrix of Firebird 3, 4, 5 and 6 containers +against `WireCrypt = Enabled` and `WireCrypt = Required`, seven +combinations in all. Firebird 3 with `Required` is excluded: protocol 13 is +the only one it offers without encryption support, so the combination +cannot succeed by definition. Each job starts the container, creates a test +database with `isql` inside it, builds the package and the test with the +distribution's FPC, and runs the test over TCP. Nothing Firebird related is +installed on the runner: the client only needs a socket. + +The **offline** job builds and runs the same binary on Linux and Windows +with no server at all, so a regression in the arithmetic, the hashes or the +message layout cannot be masked by a server problem, and the code keeps +compiling on Windows. + +Each job writes the negotiated protocol and the test totals to the step +summary, so the matrix view shows at a glance which protocol each server +version settled on. + +--- + +## Roadmap + +The provider is complete enough for ordinary work: connect, transactions, +DSQL, every data type, blobs. What follows is what is left, in the order it +is worth doing, with what each piece actually needs. Nothing here is +speculative; each item names the operations involved and the fbintf +machinery that already exists to support it. + +| # | Milestone | Needs | Protocol | +|---|---|---|---| +| 1 | Run the existing test suite against this provider — **done** | `testsuite -a wire` runs all twenty two programs | — | +| 2 | Events — **done** | `FBWireEvents` implements `IEvents` over the auxiliary connection | 13 | +| 3 | Services — **done** | `FBWireServices` implements the `IServiceManager` wrapper | 13 | +| 4 | A Delphi transport — **written, unverified** | Delphi branches in `TFBWireTransport`; needs a Delphi compile and live run | — | +| 5 | Array columns — **done** | `FBWireArray` over `op_get_slice`/`op_put_slice`, shared SDL generator | 13 | +| 6 | Statement timeouts and cancellation — **done** | `CancelOperation`/`SetStatementTimeout` on all providers | 12, 16 | +| 7 | Scrollable cursors — **done** | `op_fetch_scroll`, protocol raised to 18 | 18 | +| 8 | The batch API — **done** | `op_batch_create/msg/regblob/exec/cs/rls` | 16 | +| 9 | Inline blobs — **done** | `op_inline_blob`, protocol raised to 19 | 19 | +| 10 | Firebird 6 protocol 20 — **done** | offer raised to 20, `isc_dpb_search_path`, schema aware describe | 20 | +| 11 | Wire compression — **done** | `pflag_compress`, zlib beneath the cipher | 13 | +| 12 | Engine message text — **done** | the generated `FBWireMessages` table | — | + +### 1. Run the existing test suite against this provider — done + +`testsuite -a wire` (or `runtest.sh -a wire`) runs all twenty two test +programs over this provider, and the output is compared line by line +against `testsuite/FBWirereference.log` with the run dependent values +(transaction ids, page counts, journal timestamps) normalised on both +sides. The CI workflow runs the suite against a Firebird 6 container and +fails on any difference from the reference log. Tests for features the +provider does not implement skip with a fixed message, guarded by the new +`IAttachment.HasArraySupport` and `HasEventSupport` capability checks +alongside the existing ones; those skip lines shrinking is how milestones +2 and 5 showed up in the log. + +The prediction that this was the cheapest large win was right for the +wrong reasons: the suite immediately found seven real defects that +`WireTest` could not see, several in code paths that had simply never +been executed. The fixes that came out of the first run: + +* **SRP broke inside the suite binary only** — the account name upper + casing used `AnsiUpperCase`, and with `fpwidestring` installed (which + the suite loads) that returns the string with a trailing `#0` included + in its length, poisoning the proof hashes. ASCII `UpperCase` is both + safe and what the engine does. +* **`op_execute2` had its statement and transaction handles swapped** at + the call site, killing the connection on any `execute procedure` or + `insert ... returning` — the exchange had never been exercised. +* **`Execute` returned nil for `update/insert ... returning`**: Firebird + 5 and later describe those as select statements (cursor + one row), + older servers as `SQLExecProcedure` (`op_execute2` singleton); both + paths are now implemented. +* **Named parameters lost their names**: the bind after prepare + overwrote the preprocessor's `:name` assignments with the describe + response's empty names. +* **Parameter metadata was immutable**: assigning a value of a different + type to a parameter (`AsInteger` on a `SMALLINT`, a string to a blob) + now changes the message format and relays out the buffer, as the other + providers allow — the client owns the BLR it sends. +* **`DECFLOAT` values decoded to garbage**: the provider inherited the + base class codec, which has no implementation. It now carries its own + IEEE 754 densely packed decimal encoder and decoder, verified against + the server in both directions. +* **Blob metadata from table and column names** (`GetBlobMetaData`) never + looked the column up, so blob subtypes were wrong; it now runs the same + system table query as the 3.0 provider. + +The suite also drove smaller fixes: create database from a SQL statement +now extracts the file spec locally (the stock providers delegate that +preparse to `fbclient`), a nil DPB no longer crashes, the DPB sent with +the attach is now a verbatim clumplet copy rather than a re-encoded one, +and the transport discards its session ciphers on disconnect so that the +same connection object can reconnect (also found independently by the +services milestone). + +### 2. Events — done + +Implemented by `client/wire/FBWireEvents.pas`. One auxiliary connection +and one listener thread per attachment, created on the first +`GetEventHandler` call, serve all its `IEvents` instances; interest is +registered with `op_que_events` on the main connection and notifications +are dispatched by event id. `TFBEvents` supplied the event block, the +count diffing and the callback dispatch exactly as anticipated; the wire +side is the three exchanges plus the second transport. + +Findings from the implementation, beyond the NAT point the plan already +flagged (only the port of the returned address is usable - the client +reuses the host it connected to): + +* **Each `op_que_events` must carry a fresh event id.** An interest is + one shot, and re-arming under the same id is accepted by the server + but not honoured immediately: counts accumulated while nobody waited + were only delivered when the *next* event fired, one delivery late. + The stock client increments its id on every queue, and doing the same + made deferred delivery immediate. +* The auxiliary connection carries no handshake, no authentication and + no encryption - the server associates it with the session by the + accept, and it only ever delivers `op_event` (and `op_dummy`) packets. +* **The auxiliary port must be reachable.** By default the server opens + a random port for it, which a container that only publishes 3050, or a + firewall, silently blocks - the CI containers demonstrated this by + hanging in the connect. Set `RemoteAuxPort` in `firebird.conf` to pin + it and publish or allow that port; the CI workflow pins it to 3051. + This applies to any Firebird client, not just this one. +* The event handler is called from the listener thread, exactly as the + 2.5 provider calls its handler from an AST thread, so a handler must + not call back into the same attachment from that thread; `Synchronize` + or `Queue` the work first. The test suite's Test 10 shows the pattern. +* One deliberate difference from the stock providers: events posted + while interest was cancelled are included in the counts of the next + wait (the stock bookkeeping can drop them). The wire reference log + records this in Test 10's final count. + +### 3. Services — done + +Implemented by `client/wire/FBWireServices.pas`: `TFBWireServiceManager` +over `TFBServiceManager`, driving the `op_service_*` exchanges that were +already present in `FBWireProtocol`. A service session is its own +connection, authenticated with SRP and encrypted when negotiated exactly +like a database attach; the password is consumed by the SRP exchange and +stripped from the SPB, with the proof travelling as +`isc_spb_specific_auth_data` when the server asked for it in the attach. +`AllocateSPB` returns the shared `TSPB` and `FBOutputBlock` parses the +responses, as anticipated. + +One find from the implementation is recorded here for the next reconnect +path: `TFBWireTransport.Disconnect` used to leave the session ciphers +installed, so any reconnect on the same connection object sent its +handshake through the previous session's cipher and the server dropped +it. Detach and reattach of a service manager was the first code path to +reconnect, which is how it surfaced; `Disconnect` now discards both +ciphers. + +### 4. A Delphi transport — written, unverified + +`TFBWireTransport` now branches per compiler inside the same four +methods: the Delphi side connects with `getaddrinfo` (an IPv4 result +preferred, matching the FPC resolver), sets `TCP_NODELAY` and the +`SO_RCVTIMEO`/`SO_SNDTIMEO` timeouts from the same `aTimeout` parameter, +and reads and writes with `recv`/`send` - `Winapi.Winsock2` on Windows +(with a one time `WSAStartup` behind a unit variable) and the `Posix.*` +units elsewhere. The wire units are listed in `fbintf.dpk` and +`fbintf.dproj`. The cipher and XDR layers did not change, and the FPC +branch is untouched. + +Unverified: neither this host nor CI has a Delphi toolchain, so the new +branches have never been compiled, let alone run. Expect a first-compile +pass (typed `@`, unit scope names, `AnsiString` code pages) and then the +acceptance run from `doc/roadmap/04-delphi-transport.md`: the offline +`WireTest` sections plus one live `WireCrypt = Required` connection. +Wire compression is still FPC only (paszlib); under Delphi +`EnableCompression` raises a clear error and everything else works +uncompressed. + +### 5. Array columns — done + +Implemented by `client/wire/FBWireArray.pas`. `TFBWireArray` subclasses +`FBArray`'s element addressing and conversion layer and implements the two +provider methods over `TFBWireConnection.GetSlice`/`PutSlice` +(`op_get_slice`/`op_put_slice`); `TFBWireArrayMetaData` fills the array +descriptor with the same system table query the 3.0 provider uses, run +over the wire like any other statement. The SDL generator moved from +`FB30Array` into the shared, compiler neutral `FBSDL` unit, so both +providers emit identical SDL. + +The slice data on the wire is XDR, element by element, following +`xdr_slice`/`xdr_datum` driven by the SDL element descriptor +(`FBWireMessage.XDREncodeSlice`/`XDRDecodeSlice`). Two things the sources +reveal that the isc API hides: the slice length fields count in the +*descriptor's* element length units (`sdl_desc` in `src/common/sdl.cpp`), +which for a `CHAR(n)` element is `n` while fbintf's client buffer spaces +elements at `n+1`; and `blr_varying` maps to a **`dtype_cstring`** +element - a count followed by the bytes - which is exactly the zero +terminated layout `FBArray` keeps in its buffer, so the "curious" varchar +array format the IBPP comment in `FBArray.pas` describes is simply the +SDL's view of the column. + +Finding the arrays also flushed out a provider wide leak: the wire +statement's `TWireSQLDataArea` never freed its column variables (the 3.0 +provider frees them in `FreeXSQLDA`), which pinned every blob, array and +SDL block a statement had touched - and, through the blobs' transaction +references, kept transactions alive so that their `taCommit` default +completion never ran. Test 6's execute procedure result had recorded the +symptom in the reference log as a NULL blob. Fixed with a destructor and +a `SetCount` that frees on shrink; the suite output now matches the +fbclient providers on that line. + +### 6. Statement timeouts and cancellation — done + +Both halves were interface additions to fbintf, not just wire changes: +`IAttachment.CancelOperation(aKind)` and +`IStatement.SetStatementTimeout`/`GetStatementTimeout` are new, and all +three providers implement them - 2.5 through `fb_cancel_operation` +(when the loaded library exports it), 3.0 through +`IAttachment::cancelOperation` and `IStatement::setTimeout` (timeouts +need a Firebird 4 client), and the wire provider natively. + +On the wire, `op_cancel` has no response packet and is sent from a +different thread while the owner is blocked reading: it bypasses the +shared send buffer through `TFBWireTransport.SendDirect`, whose lock +serialises the cipher and socket write against `Flush` - the stream +cipher stays consistent because bytes are enciphered in wire order. The +timeout travels in the `p_sqldata_timeout` field of +`op_execute`/`op_execute2` (protocol 16); below protocol 16 a non zero +timeout raises `ibxeNotSupported` rather than being silently dropped. + +One correction to the plan: an expired timeout does not arrive as its +own error code. The server cancels the request, so the primary status +is `isc_cancelled` with `isc_req_stmt_timeout` as the secondary code +(`thread_db::checkCancelState`). `op_cancel` also does not unblock a +client whose server has gone away - that is the socket timeout's job +(`ConnectTo` accepts one). + +### 7. Scrollable cursors — done + +The protocol offer now goes up to 18, which Firebird 5 and 6 accept +(Firebird 4 stays on 17, Firebird 3 on 15). At 18 every +`op_execute`/`op_execute2` carries a cursor flags word after the timeout +field; a cursor opened with `IStatement.OpenCursor(true)` sets +`CURSOR_TYPE_SCROLLABLE` in it, and the five positioned fetches of +`IResultSet` then travel as `op_fetch_scroll` - `op_fetch` plus a +direction and a position, answered by the same `op_fetch_response` +sequence. + +A positioned fetch requests a single row and first discards the client's +read ahead cache: those rows describe a cursor position the scroll +abandons (the server, symmetrically, discards its own prefetch and +repositions when the fetch direction changes - `rem_port::fetch` in +`src/remote/server/server.cpp`). Sequential fetches keep their batched +read ahead, and one that follows a scroll simply starts a fresh batch +from the new position. BOF/EOF bookkeeping follows +`TFB30Statement.Fetch`: success clears both, `FetchPrior` off the top +sets BOF, and a failed positioned fetch leaves the flags alone. +`HasScollableCursors` answers protocol >= 18, so the suite's Test 2 +scrollable section runs against Firebird 5 and 6 and skips on older +servers. + +### 8. The batch API — done + +Rows accumulate client side and `ExecuteBatch` plays them to the server +as `op_batch_create` (the statement's BLR, the message length and an +`IBatch` parameter block), `op_batch_msg` packets of up to five hundred +messages, and `op_batch_exec`, whose `op_batch_cs` reply carries the per +row update counts and status vectors that `TWireBatchCompletion` +reports. `op_batch_rls`/`op_batch_cancel` end the batch either way. Two +details the sources settle: batch messages travel in exactly the row +message encoding (`xdr_packed_message` is the null bitmap format - the +eight byte alignment applies to the server's buffers, not the wire), and +the message length field must be computed by the server's own +`PARSE_msg_format` rules (`EngineMessageLength`), not from the client's +private buffer layout. Blob ids in batched rows must be registered with +`op_batch_regblob` once per row - the engine translates each id through +a registration map and consumes the entry - which is what the 3.0 +provider's `registerBlob` call does; array ids pass through untouched. + +The milestone also forced a provider wide correction: the wire +statement's parameters now answer `SQL_VARYING` as their default text +type, as the fbclient providers do. The original `SQL_TEXT` choice +re-sized the parameter format on every string assignment, which a batch +cannot tolerate (the BLR is frozen at `op_batch_create`), and its blank +padded values leaked into stored data. With varying parameters the +suite's parameter metadata and value output now matches the fbclient +reference logs line for line, two latent accessor bugs were fixed along +the way (a parameter's `SQLData` points at the characters, not the +length prefix), and `CanChangeMetaData` answers false while a batch is +open, so a mid batch type change is refused exactly as the 3.0 provider +refuses it. + +### 9. Inline blobs — done + +The protocol offer now goes to 19, and `op_execute`/`op_execute2` carry +the inline blob size limit the client will accept - taken from the +existing `IAttachment.GetInlineBlobLimit` (default 8KB, settable, zero +opts out). A protocol 19 server pushes each qualifying blob as an +`op_inline_blob` packet ahead of the row that references it: transaction +handle, blob id, the standard blob info response, and the whole blob as +one segmented stream - the same two byte length prefixed form +`op_get_segment` returns. + +`ReadOperation` intercepts the packets - the same interception point +that already skips `op_dummy` and `op_response_piggyback` - and hands +them to the attachment through a callback, keeping the protocol unit +free of provider types. The attachment caches them (16MB cap; beyond it +pushes are dropped and the blob opens the classic way - a round trip, +never an error), keyed by transaction handle and blob id, with entries +consumed on open and a transaction's entries discarded when it ends. +`TFBWireBlob` consults the cache before sending `op_open_blob2`: on a +hit the open, the reads and `GetInfo` all happen without any wire +traffic, which WireTest proves with a packet counter on the transport. +The whole feature is transparent: the suite output is byte identical +apart from the negotiated version in `getFBVersion`. + +### 10. Firebird 6 and protocol 20 — done + +The offer now goes to 20, which Firebird 6 accepts (Firebird 5.0.3 +stays on 19). The field audit found protocol 20 adds exactly one thing +to the packets this client sends: a flags word at the end of +`op_prepare_statement` and `op_exec_immediate` (`p_sqlst_flags`, +written as zero - the engine's special prepare flags are not exposed). +Everything else is opt-in: the schema search path travels as an +ordinary DPB item (`isc_dpb_search_path`, added to the constants with +the rest of the Firebird 6 DPB tags), and the describe item list gains +`isc_info_sql_relation_schema` on protocol 20 connections, parsed into +the column format's schema name. fbintf has no schema member on +`IColumnMetaData` yet, so the name is stored rather than surfaced - the +tests reach it through `TFBWireStatement.ColumnSchemaName`, and an +interface member can follow whenever fbintf grows one. + +Named arguments turned out to be a SQL level feature (calling +procedures with `name => value`), not a describe change: nothing to do +on the wire. The suite output is byte identical apart from the +negotiated version string, and the WireTest schema section proves the +search path resolves unqualified names, a qualified name bypasses it, +and the described schema arrives per column. + +### 11. Wire compression — done + +Requested per attachment with an `isc_dpb_config` item of +`WireCompression=true` - the same knob the stock client reads - and +effective when the server also enables it: `pflag_compress` rides on +each offered protocol entry's max type field and the accept echoes it, +captured before the `ptype_mask` masking that used to discard it. +Compression is a property of the byte stream, not of packets: one zlib +stream per direction for the life of the session, started immediately +after the accept packet (which itself travels clear), with a +`Z_SYNC_FLUSH` per packet flush so complete packets reach the peer +promptly. + +The pipeline order keeps the cipher against the socket: deflate then +encrypt on send, decrypt then inflate on receive, so the later +`op_crypt` changeover works unchanged. `op_cancel`'s out of band packet +travels through the same deflate state - the peer inflates one +continuous stream, so raw bytes must never bypass it. +`HasBufferedData` counts deflate stream bytes received but not yet +inflated, which keeps the cipher changeover guard and event polling +honest. The compression-off path is byte for byte the previous code, +which the unchanged suite reference log proves; FPC's pure Pascal +`paszlib` supplies zlib, so the no-external-dependencies property +survives. + +### 12. Engine message text — done + +The generated table route: `generate_messages.py` reads the message +headers of a pinned Firebird tree (`src/include/firebird/impl/msg` - +the modern replacement for `messages2.sql`) and emits +`FBWireMessages.pas`, committed alongside it: 1682 messages for the +facilities a server round trip can produce (JRD, DSQL, DYN, SQLERR, +SQLWARN), each with its format string and its SQLCODE. Roughly 200KB +of binary, and the provider's no-dependencies property survives. + +`FormatWireStatus` renders a decoded status vector the way +`fb_interpret` does - each `isc_arg_gds` item's format string with its +`@1..@n` arguments substituted, interpreted items verbatim, successive +items joined with a `-` continuation - and is the single formatter +behind `EIBInterBaseError` messages and the protocol level exception. +The status object also answers `Getsqlcode` (the `gds__sqlcode` rules: +an `isc_sqlerr` item's number argument wins, else the first item's +mapping) and the per SQLCODE interpretation line (`isc_sql_interprete`'s +facility 13/14 lookup), so the full fbclient error shape - SQLCODE +line, SQL message, engine code, message text - now appears over the +wire. Two behavioural gaps this exposed are also closed: prepare errors +name the offending statement (` When Executing: ...`) and the stale +reference checks raise `ibxeInterfaceOutofDate` exactly as the 3.0 +provider does. Error output is now identical to `fbclient` on the same +server, apart from sections a test skips for the wire provider by +design. + +### Not planned + +* **`Legacy_Auth`.** It produces no session key, so it cannot satisfy a + server requiring encryption, and modern servers disable it by default. + The DES `crypt(3)` implementation it needs is not worth carrying. +* **`Win_SSPI` / trusted authentication.** Windows specific and awkward to + test in CI. +* **Protocols below 13.** Firebird 2.5 and earlier need the pre + authentication plugin handshake and a different message encoding, and are + out of support. + diff --git a/doc/roadmap/01-testsuite.md b/doc/roadmap/01-testsuite.md new file mode 100644 index 00000000..1d65b027 --- /dev/null +++ b/doc/roadmap/01-testsuite.md @@ -0,0 +1,129 @@ +# Design: Running the existing test suite against the wire provider + +Roadmap milestone 1 (`doc/WireProtocol.md`). + +**Status: implemented** — `testsuite -a wire` runs all twenty two +programs over the wire provider; `runtest.sh -a wire` diffs the output +against `testsuite/FBWirereference.log` (run dependent values normalised +on both sides), and a CI job runs the suite against a Firebird 6 +container with the employee database restored from +`testsuite/employee.gbk`, failing on any diff. The harness fixes and +skip guards below landed as planned, with `HasArraySupport` and +`HasEventSupport` added to `IAttachment` for the capability checks. + +The milestone's real value turned out to be the seven wire provider +defects the suite exposed on its first run — SRP account upper casing +broken under `fpwidestring`, swapped handles in the never-exercised +`op_execute2`, missing `returning` singleton support, named parameter +names wiped by the bind, immutable parameter metadata, a `DECFLOAT` +codec that had no implementation at all, and blob metadata lookups that +never queried the system tables. `doc/WireProtocol.md`'s roadmap section +records the details. + +## Goal + +`./testsuite -a wire ...` (option name to taste) runs all twenty two test +programs over `client/wire`, and that run becomes the acceptance criterion +for the events, services and array milestones. + +## Where the provider is chosen + +Exactly one place. `TTestApplication.GetFirebirdAPI` +(`testsuite/testApp/TestApplication.pas`) lazily assigns +`FFirebirdAPI := IB.FirebirdAPI`, and every test reaches the API through +`Owner.FirebirdAPI` — no test calls `IB.FirebirdAPI` directly. The only +other writer is `SetClientLibraryPath` (the `-l` option). So the switch is: + +* a new option (`-a wire` / `--api wire`, added to `GetShortOptions`, + `GetLongOptions` and both the FPC and Delphi `GetParams` variants); +* when given, `GetFirebirdAPI` assigns `WireFirebirdAPI` from + `FBWireClientAPI` instead of `IB.FirebirdAPI`. + +Build plumbing: `testsuite/Makefile.fpc` (`unitdir`) and +`testsuite/testsuite.lpi` (`OtherUnitFiles`) do not list `../client/wire` +today; both need it. The top level `Makefile.fpc` and `fbintf.lpk` already +include it. + +## What will break, known in advance + +The research below is from reading the harness, not speculation; each item +names the line it comes from. + +**Harness faults (must fix first):** + +* `DoRun` prints + `FirebirdAPI.GetFBLibrary.GetLibraryFilePath` in the banner, and + `WriteAttachmentInfo` does the same per attachment. The wire API has no + library, so `GetFBLibrary` returns nil and both dereference it. Guard + both: print `Firebird Client Library Path = none (wire protocol)` when + `GetFBLibrary` is nil. This also documents itself in the output. +* `HasMasterIntf` is false for the wire provider, so the Bin/Conf + directory banner lines are omitted — fine, already guarded. + +**Feature gaps (expected, become milestone acceptance criteria):** + +| Test | Feature | Current wire behaviour | +|---|---|---| +| 2 | scrollable cursors | `HasScollableCursors` false → section self-skips | +| 7, 8, 18 | arrays | `ibxeNotSupported` → "Test Completed with Error" | +| 10 | events | `GetEventHandler` raises → error | +| 11, 16 | services | `HasServiceAPI` false → sections self-skip | +| 13 | multi-database transaction | `StartTransaction` with two attachments raises | +| 19 | batch | `HasBatchMode` false → skip path | +| 20 | batch stress | guards on client major/ODS, not `HasBatchMode` — will error | + +Tests 19, 2, 11 and 16 already degrade gracefully because they test a +capability flag first. Tests 7, 8, 10, 13, 18 and 20 do not, and the +harness's generic handler turns the raise into a single +`Test Completed with Error` line, aborting the rest of that test. That is +survivable but noisy, and it aborts unrelated later checks in the same +test. + +**Environment differences (diff noise, not failures):** + +* the banner's `Client API Version` will read `5.0` + (`WireClientMajorVersion`), and connect strings take the `inet://` form + since the reported client major is ≥ 3; +* reference logs embed absolute library paths that cannot match. + +## The skip story + +Pass/fail today is a `diff` against `FBreference.log` chosen by ODS +version in `runtest.sh` — the per-version reference logs are how +"unsupported" is already encoded (e.g. the FB2/FB3 logs contain the +"Skipping test for Firebird 4 and later" lines). Two mechanisms exist in +the harness and are currently unused: `TTestBase.SkipTest` (virtual, +consulted by `DoTest`) and `ESkipException` (caught by `DoTest`). + +Plan, in keeping with the existing convention: + +1. In tests 7, 8, 10, 13, 18, 20: guard the unsupported feature on the + capability the provider actually reports (`GetEventHandler` needs a new + `HasEventHandler`-style check, or `try...on E: EIBClientError` with the + `ibxeNotSupported` code → write a fixed `Skipping: ...` line). Prefer + extending the capability surface (`IFirebirdAPI`/`IAttachment` already + have `HasServiceAPI`, `HasBatchMode`, `HasScollableCursors`; events and + arrays deserve the same) over exception sniffing. +2. Add `testsuite/FBWirereference.log` produced the same way the others + were, with the skip lines in place, and teach `runtest.sh` to select it + when the output banner says the wire provider is in use (a new banner + line `Provider = wire` makes that trivial). +3. As milestones 2, 3 and 5 land, the skip lines shrink and the reference + log is regenerated — the diff *is* the progress report. + +## CI + +Extend `.github/workflows/wire-protocol.yml` with a job that builds the +full suite (FPC, `unitdir` fix above) and runs +`./testsuite -a wire -u SYSDBA -p masterkey -e inet://localhost/employee ...` +against the same container matrix, diffing against +`FBWirereference.log`. Keep `WireTest` as-is: it is the offline/unit layer, +the suite is the integration layer. + +## Acceptance + +* The full suite runs to `Test Suite Ends` with no crash on Firebird 3–6. +* The diff against the wire reference log is empty. +* No change in the suite's behaviour for the two existing providers: the + default path through `GetFirebirdAPI` is untouched and the existing + reference logs still match. diff --git a/doc/roadmap/02-events.md b/doc/roadmap/02-events.md new file mode 100644 index 00000000..2cccce48 --- /dev/null +++ b/doc/roadmap/02-events.md @@ -0,0 +1,147 @@ +# Design: Events over the wire protocol + +Roadmap milestone 2 (`doc/WireProtocol.md`). + +**Status: implemented** — `client/wire/FBWireEvents.pas`, with a WireTest +events section and the suite's Test 10 as acceptance (its output over the +wire matches the stock provider reference, recorded in +`FBWirereference.log`). The design below held, with three findings: + +* Each `op_que_events` needs a **fresh event id** (the stock client + increments per queue). Re-arming under the same id is accepted but the + deferred counts are only delivered when the next event fires — one + delivery late. +* The `TFBWireEventManager` object (one per attachment) owns the + auxiliary transport and listener thread, as designed; the transport + gained an `Abort` method (socket shutdown) so the listener's blocking + read can be released from another thread on disconnect. +* Events posted while interest was cancelled are counted on the next + wait; the stock providers can drop them. Recorded as a deliberate + difference. + +## Goal + +`IAttachment.GetEventHandler` works on the wire provider exactly as it does +on the other two: register interest in named events, receive counts +asynchronously through `TEventHandler`, or block in `WaitForEvent`. + +Today `TFBWireAttachment.GetEventHandler` (`FBWireAttachment.pas`) raises +`ibxeNotSupported` with the comment that events need the auxiliary +connection established with `op_connect_request`. + +## What already exists + +Nearly all of the event machinery in fbintf is provider neutral and lives in +`client/FBEvents.pas`: + +* `TFBEvents.CreateEventBlock` builds the event parameter block in exactly + the form `op_que_events` carries: version byte `EPB_version1`, then per + event a length byte, the name, and a little endian count initialised to 1. + `FResultBuffer` has the same layout and length. +* `TFBEvents.ProcessEventCounts` diffs `FResultBuffer` against + `FEventBuffer`, fills `TEventCounts`, and copies the result buffer over + the event buffer so it becomes the new baseline. +* `TFBEvents.EventSignaled` performs the callback dispatch under + `FCriticalSection`, honouring `FInWaitState`, and calls the handler + outside the lock. + +A subclass supplies exactly four overrides — `GetIEvents`, `CancelEvents`, +`WaitForEvent`, `AsyncWaitForEvent` — plus a way of filling `FResultBuffer` +and calling `EventSignaled` when a notification arrives. `TFB25Events` and +`TFB30Events` are the two existing models; the 2.5 one is the closer match +because it also deals in raw EPB buffers rather than the OO API's callback +object. + +On the wire side, `FBWireConst.pas` already defines `op_connect_request`, +`op_aux_connect`, `op_que_events`, `op_cancel_events`, `op_event` and +`P_REQ_async`. None of them is used yet. `TFBWireConnection` exposes its +`XDR: TXDRStream` and `Transport: TFBWireTransport` publicly, and +`TXDRStream` already has the string/opaque codecs the packets need. + +## The protocol + +Three exchanges on the **main** connection, one packet type on a **second** +connection. + +1. **`op_connect_request`** (main connection): int32 type = `P_REQ_async`, + the database object id, int32 partner id (0). The `op_response` carries, + in its data bytes, a `sockaddr_in` naming the port the server is + listening on for the auxiliary connection. Only the port is trustworthy: + behind NAT the address is the server's own view of itself, so the client + reuses the host it originally connected to and takes just the port. This + is what the stock remote client does. + +2. **Auxiliary connection**: a plain TCP connect to that host:port. No + handshake, no authentication, no encryption — the server associates it + with the session by the accept. The socket then only ever *receives*. + +3. **`op_que_events`** (main connection): database handle, the EPB as a + string (byte-counted), an AST address, an argument (both legacy fields, + sent as 0), and an event id chosen by the client. The response's + `ObjectHandle` is the server side id used for cancellation. + +4. **`op_event`** (auxiliary connection): database handle, the updated + event buffer as a string, the 8 legacy AST bytes, and the event id. The + buffer has the same layout as the EPB, with new counts. + +5. **`op_cancel_events`** (main connection): database handle, event id. + +## Design + +New unit `client/wire/FBWireEvents.pas`: + +* `TFBWireEvents = class(TFBEvents, IEvents)` — the four overrides. + `AsyncWaitForEvent` queues via `op_que_events`; `WaitForEvent` does the + same and then blocks on an OS event signalled by `EventSignaled`, the + same shape as `TFB25Events.WaitForEvent`. +* `TFBWireEventListener = class(TThread)` — owns the auxiliary socket. Its + loop decodes `op_event` packets with a private `TXDRStream` over a second + `TFBWireTransport`, copies the event buffer into the owning + `TFBWireEvents.FResultBuffer` under `FCriticalSection`, and calls + `EventSignaled`. Socket close is the shutdown signal. + +`TFBWireConnection` gains: + +* `ConnectRequest(aDbHandle: integer): TAuxAddress` — sends + `op_connect_request`, parses the `sockaddr` out of the response data, + substitutes the original host. +* `QueEvents(aDbHandle: integer; const EPB: TBytes; aEventID: integer): integer` + and `CancelEvents(aDbHandle, aEventID: integer)`. + +The auxiliary connection is created lazily on the first +`GetEventHandler` call and shared by all `IEvents` instances of the +attachment; one listener thread serves them all, dispatching on event id. +It is torn down in `TFBWireAttachment.InternalDisconnect`. + +### Concurrency + +The listener thread never touches the main connection, so no locking is +added to the request path. The only shared state is `FResultBuffer` and the +wait flags, which `TFBEvents` already guards with `FCriticalSection`. +`op_que_events` after a notification (re-arming) happens on the caller's +thread, from `EventSignaled`'s handler, as the other providers do. + +### Failure modes + +* Server refuses `op_connect_request` (aux port disabled): surface the + status vector as usual; `GetEventHandler` fails cleanly. +* Aux socket drops: the listener marks the events object dead; the next + `WaitForEvent`/`AsyncWaitForEvent` raises through the normal status path. +* `SetEvents` while queued: `TFBEvents.SetEvents` already cancels first. + +## Acceptance + +* `Test16` (events) in the test suite passes against the wire provider + (milestone 1 provides the provider switch). +* A new `WireTest` section: queue two events, post from a second + attachment via `execute block ... post_event`, assert both counts; then + cancel and assert no further delivery. +* CI matrix: events must work on Firebird 3, 4, 5, 6 — the packets are + protocol 13 level and unchanged since. + +## Out of scope + +Encrypted auxiliary connections (the server sends `op_event` in clear even +when the main line is encrypted — as does the stock client), and +`isc_wait_for_event`-style synchronous multiplexing beyond what +`TFBEvents` already offers. diff --git a/doc/roadmap/03-services.md b/doc/roadmap/03-services.md new file mode 100644 index 00000000..c58c59e6 --- /dev/null +++ b/doc/roadmap/03-services.md @@ -0,0 +1,123 @@ +# Design: Services over the wire protocol + +Roadmap milestone 3 (`doc/WireProtocol.md`). + +**Status: implemented** — `client/wire/FBWireServices.pas`, with a +services section in `WireTest`. Two deviations from the plan below, both +discovered by the implementation: + +* The `p_cnct_operation` nuance turned out to be a non-issue: the field + is vestigial (the stock client sends `op_attach` for service + connections too, and the server ignores it), verified against Firebird + 5 and 6. `ConnectTo` is unchanged. +* Detach-and-reattach exposed a latent transport bug unrelated to + services: `TFBWireTransport.Disconnect` left the session ciphers + installed, so any reconnect on the same connection object sent its + cleartext handshake through the dead session's cipher and the server + dropped the connection. `Disconnect` now discards both ciphers. + +## Goal + +`IServiceManager` works on the wire provider: attach to `service_mgr`, +start and query services, so backup, restore, statistics and user +management run from a client with no `fbclient` installed. + +This is the smallest milestone by a distance, because both halves already +exist and only the joining layer is missing. + +## What already exists + +**The wire half is done.** `TFBWireConnection` in +`client/wire/FBWireProtocol.pas` already implements, unused: + +* `ServiceAttach(const aServiceName: AnsiString; SPB: TBytes): integer` — + sends `op_service_attach`, returns the object handle; +* `ServiceDetach(aSvcHandle: integer)` — `op_service_detach`; +* `ServiceStart(aSvcHandle: integer; const aItems: TBytes)` — + `op_service_start`; +* `ServiceQuery(aSvcHandle; aSendItems, aRecvItems: TBytes; aBufferLength)` + — `op_service_info`, returning the response data bytes. + +**The fbintf half is done.** `TFBServiceManager` in `client/FBServices.pas` +implements most of `IServiceManager` (`Attach`, `AllocateSRB`, +`AllocateSQPB`, the connect string assembly, the single-argument `Query`) +and leaves exactly these to the provider subclass: + +* `InternalAttach(ConnectString)` — virtual abstract; +* `Detach(Force)` — virtual abstract; +* `Query(SQPB, Request, RaiseExceptionOnError)` — virtual abstract; +* `IsAttached` and `Start(Request, RaiseExceptionOnError)` — required by + `IServiceManager` but not present in the base. + +`TSPB`, `TSRB` and `TSQPB` build the parameter blocks; +`TServiceQueryResults` (`FBOutputBlock.pas`) parses the reply. +`ParamBlockToBytes` in `FBWireClientAPI.pas` already converts any +`TParamBlock` to the `TBytes` the wire methods want. + +Currently `TFBWireClientAPI.HasServiceAPI` returns false and +`AllocateSPB` / both `GetServiceManager` overloads raise +`ibxeNotSupported`. + +## Design + +New unit `client/wire/FBWireServices.pas`: + +```pascal +TFBWireServiceManager = class(TFBServiceManager, IServiceManager) +``` + +following `TFB25ServiceManager` (the handle-based model), with a +`TFBWireConnection` of its own instead of an `isc` handle: + +* `InternalAttach(ConnectString)` — parse host and port with the same + `IBUtils` machinery the attachment uses, create a `TFBWireConnection`, + `ConnectTo` it, then `ServiceAttach('service_mgr', ParamBlockToBytes(FSPB))` + and keep the returned handle. (The plan originally called for sending + `op_service_attach` as the `op_connect` packet's `p_cnct_operation`; + see the status note above — the field is vestigial and `ConnectTo` is + unchanged.) +* `Detach(Force)` — `ServiceDetach(handle)`, then `Disconnect`; swallow + errors when `Force`. +* `IsAttached` — connection assigned and handle valid. +* `Start(Request, RaiseExceptionOnError)` — + `ServiceStart(handle, ParamBlockToBytes(Request))`. +* `Query(SQPB, Request, RaiseExceptionOnError)` — call `ServiceQuery` with + the two byte blocks and `TServiceQueryResults.DefaultBufferSize`, copy + the reply into a `TServiceQueryResults` and return it. + +`TFBWireClientAPI` then flips over: + +* `HasServiceAPI` returns true; +* `AllocateSPB` returns `TSPB.Create(self)`; +* both `GetServiceManager` overloads construct `TFBWireServiceManager`, + mirroring the shape of `OpenDatabase`. + +Authentication and encryption need no new work: the SRP exchange and +`op_crypt` happen in `ConnectTo` before the service attach, exactly as for +a database attach. The password is consumed by SRP and stripped from the +SPB the same way the attachment strips it from the DPB. + +### Error handling + +Service errors arrive as ordinary status vectors on `op_response`; +`ReceiveAndCheckResponse` already raises through the standard path, so +`EIBInterBaseError` behaves as with the other providers. The +`ibxeServiceActive` / `ibxeServiceInActive` guards from `TFB30ServiceManager` +are copied so misuse fails with the same messages. + +## Acceptance + +* `Test19` / the service-related programs of the test suite pass against + the wire provider (via the milestone 1 provider switch). +* A new `WireTest` section: attach to `service_mgr`, query + `isc_info_svc_server_version` and `isc_info_svc_implementation`, assert + non-empty strings; run `isc_action_svc_db_stats` header-page stats on the + test database and assert output arrives. +* Green across the CI matrix (Firebird 3–6, WireCrypt Enabled and + Required): everything used here is protocol 13 level. + +## Out of scope + +The `isc_spb_*` version 3 service parameter extensions beyond what `TSPB` +already writes, and trusted authentication (`Win_SSPI`) which the roadmap +excludes globally. diff --git a/doc/roadmap/04-delphi-transport.md b/doc/roadmap/04-delphi-transport.md new file mode 100644 index 00000000..75fa7703 --- /dev/null +++ b/doc/roadmap/04-delphi-transport.md @@ -0,0 +1,99 @@ +# Design: A Delphi transport for the wire provider + +Roadmap milestone 4 (`doc/WireProtocol.md`). + +**Status: written, unverified** — the Delphi branches exist in +`FBWireStream.pas` (`Winapi.Winsock2` on Windows behind a one time +`WSAStartup`, the `Posix.*` units elsewhere; `getaddrinfo` with an IPv4 +result preferred, `TCP_NODELAY`, `SO_RCVTIMEO`/`SO_SNDTIMEO` from +`aTimeout`, `recv`/`send`, graceful `shutdown` in `Disconnect`) and the +wire units are listed in `fbintf.dpk`/`fbintf.dproj`. Deviations from +the plan below: wire compression (milestone 11, paszlib) stays FPC only +— `EnableCompression` raises a clear error under Delphi — and the +`WireTest` Delphi project file has not been created. **No Delphi +toolchain exists on this host or in CI, so none of it has been compiled +or run**; the verification section below is still the outstanding work, +including the expected first-compile strictness pass. + +## Goal + +`client/wire` compiles and runs under Delphi (Windows and the Posix +targets), so the wire provider can be added to `fbintf.dpk` and used from +Delphi programs with no `fbclient` installed. + +## How little is actually missing + +The FPC dependency is confined to `FBWireStream.pas`, and inside it to one +field and three call sites: + +* `FSocket: TInetSocket` (unit `ssockets`), created in `ConnectTo` with + `IOTimeout` and a `fpsetsockopt` `TCP_NODELAY` call (unit `sockets`); +* `FSocket.Read` in `FillRecvBuffer`; +* `FSocket.Write` in `Flush`. + +The non-FPC branches currently raise +`'Wire protocol transport is not implemented for this compiler'`. The XDR +codec (`TXDRStream`), the cipher classes, and everything above them are +already compiler neutral, and the units carry `{$mode delphi}`-compatible +code throughout. No packet logic changes. + +## Design + +Keep one unit, one class, and branch inside the four transport methods — +the same `{$IFDEF FPC}` structure that exists now grows a Delphi branch +instead of a raise. Rejected alternative: a class hierarchy +(`TFBWireTransport` abstract + per-compiler subclasses) — more moving +parts than three call sites justify, and it would force a factory into +`TFBWireConnection`. + +Delphi branch: + +* **Windows**: `Winapi.WinSock2` + `Winapi.Windows`. One-time `WSAStartup` + guarded by a unit variable; `getaddrinfo` for the host (IPv4 first, + matching current behaviour), `socket`/`connect`, `setsockopt` + `TCP_NODELAY`, `SO_RCVTIMEO`/`SO_SNDTIMEO` from the `aTimeout` + parameter; `recv`/`send` in `FillRecvBuffer`/`Flush`; `closesocket` + + graceful `shutdown` in `Disconnect`. +* **Posix Delphi** (Linux/macOS targets): `Posix.SysSocket`, + `Posix.NetinetIn`, `Posix.ArpaInet`, `Posix.NetDB`, `Posix.Unistd` — + the same calls in their Posix spelling. + +Error mapping: both branches raise `EFBWireError` with the socket error +code and text, matching the FPC branch's +`'Connection lost to database server'` behaviour on zero-byte reads, so +`WireIBError` keeps converting transport failures to `isc_network_error`. + +Everything else in the unit — buffers, cipher hooks, `HasBufferedData` — +is untouched, because it operates on the byte buffers, not the socket. + +## Packaging + +* Add the ten `client/wire` units to `fbintf.dpk` / `fbintf.dproj`. +* `FBWireBigInt`, `FBWireCrypto`, `FBWireSRP` use no FPC-specific + language features by design, but have only ever been compiled by FPC — + expect a first-compile pass for Delphi strictness (typed `@`, `TBytes` + index base, `AnsiString` code page warnings). These are mechanical. +* `WireTest.pas` gets a Delphi project file alongside the existing + invocation, and its offline sections become the Delphi smoke test. + +## Verification + +CI cannot run Delphi. The plan mirrors what the repo already does for the +Delphi test suite (`testsuite/delphitestsuite.groupproj`): + +* the **offline** job already builds `WireTest` on a Windows runner with + FPC — it stays, protecting the shared code on Windows; +* Delphi compilation is verified manually per release, like the rest of + fbintf's Delphi support; the transport branch is small enough that the + offline `WireTest` run (arithmetic, crypto, SRP, layout — all + compiler-sensitive code) plus one live connection is sufficient + acceptance. + +## Acceptance + +* `fbintf.dpk` builds in Delphi with the wire units included. +* `WireTest` offline sections pass under Delphi on Windows. +* A live connect + query against a Firebird 5 server from a Delphi-built + binary, with `WireCrypt = Required`, works — this exercises the timeout, + NODELAY and cipher paths end to end. +* FPC behaviour is bit-for-bit unchanged (the FPC branch is not edited). diff --git a/doc/roadmap/05-arrays.md b/doc/roadmap/05-arrays.md new file mode 100644 index 00000000..40e2385e --- /dev/null +++ b/doc/roadmap/05-arrays.md @@ -0,0 +1,148 @@ +# Design: Array columns over the wire protocol + +Roadmap milestone 5 (`doc/WireProtocol.md`). + +**Status: implemented** — `client/wire/FBWireArray.pas`, with the SDL +generator moved to the shared `client/FBSDL.pas` as the refactoring note +below proposed, a WireTest arrays section (integer, varchar and a two +dimensional double array, element by element read back in a new +transaction), and the suite's array tests (7, 8, 17, 18, 19, 22) as +acceptance. The design below held, with these findings: + +* The protocol detail the plan called "the same bytes" is subtler than + the isc API shows: `blr_varying` maps to a **`dtype_cstring`** element + descriptor (`sdl_desc` in `src/common/sdl.cpp`), so a varchar element + travels as a count plus bytes and sits in the buffer as a zero + terminated string in an `n+2` chunk - exactly the layout `FBArray` + already keeps, explaining the "curious" format the IBPP comment + describes. No `vary` struct is involved anywhere. +* The `p_slc_length`/`lstr_length` fields count in descriptor length + units (`n` for `CHAR(n)`, `n+2` for `VARCHAR(n)`, the type's size + otherwise), not in wire bytes: each element is then XDR encoded + exactly as row values are. `FBWireMessage` gained + `XDREncodeSlice`/`XDRDecodeSlice` for this, keyed by blr dtype. +* fbintf spaces `CHAR(n)` array elements at `n+1` bytes while the wire + (and the engine) use `n`; the wire codec converts between the strides, + so multi element char arrays work over the wire. +* Hunting a leak of the new SDL blocks uncovered a provider wide bug + present since milestone 1: `TWireSQLDataArea` never freed its column + variables (the 3.0 provider frees them in `FreeXSQLDA`), so every + statement leaked its variables and pinned whatever blobs and arrays + they referenced. Fixed with a destructor and a `SetCount` that frees + on shrink; Test 4's leak report dropped from 273 unfreed blocks to 33 + and Test 7's from 834 to 229, the remainder shared with the fbclient + providers. +* The leak was not only memory: a leaked column variable holds its + `IBlob`, and a blob holds its `ITransaction`, so any transaction a + blob had passed through was never released and its `taCommit` default + completion never ran. Test 6's execute procedure section had baked + the symptom into the wire reference log as `BLOBDATA = NULL` — the + procedure, running in a later transaction, correctly saw data that + had never been committed. With the fix the output matches the + fbclient providers' reference logs. + +## Goal + +`IArray` and `IArrayMetaData` work on the wire provider: read and write +array columns through `op_get_slice` / `op_put_slice`, with the SDL (slice +description language) program generated by the client. + +Today every array entry point raises `ibxeNotSupported`: +`TFBWireAttachment.OpenArray` / `CreateArray` / `CreateArrayMetaData` / +`GetArrayMetaData`, and on the statement side +`TWireSQLVarData.GetAsArray` / `GetArrayMetaData` and +`TFBWireStatement.CreateArray`. + +## What already exists + +Almost everything, split across the two existing providers: + +* `TFBArray` (`client/FBArray.pas`) implements the entire element + addressing, buffer layout and conversion layer. A provider subclass + implements exactly two methods, `InternalGetSlice` and + `InternalPutSlice`, and may override `AllocateBuffer`. The buffer, + size and array id are protected fields; `GetArrayDesc` exposes the + `TISC_ARRAY_DESC`. +* `TFBArrayMetaData` needs `LoadMetaData`, `GetCharSetID`, + `GetCharSetWidth`, `GetCodePage` from the subclass; the rest is derived + from the descriptor. +* **SDL generation already exists in this codebase.** + `TFB30Array.GenerateSDL` (`client/3.0/FB30Array.pas`) is a port of + `gen_SDL` from Firebird's `src/dsql/array.cpp`: it emits + `isc_sdl_struct`, relation, field, the per-dimension `isc_sdl_do1/do2` + loops and `isc_sdl_element`/`isc_sdl_scalar`, terminated by + `isc_sdl_eoc`, into a `TSDLBlock`. The 3.0 provider hands that SDL to + `getSlice`/`putSlice`; the wire provider hands the same bytes to + `op_get_slice`/`op_put_slice`. This is the key reuse: no new SDL code + is needed, only a move. +* `TFB30ArrayMetaData.LoadMetaData` fills the descriptor with a query on + `RDB$FIELDS` joined to `RDB$FIELD_DIMENSIONS` — no client library call + involved, so it works verbatim over the wire. (The 2.5 provider uses + `isc_array_lookup_bounds`, which does not exist here.) +* `op_get_slice = 58` and `op_put_slice = 59` are already defined in + `FBWireConst.pas`, unused. + +## The protocol + +Both operations are protocol 13 level. + +* **`op_get_slice`**: transaction handle, database handle... in P13 form: + int32 transaction id, the blob-style quad id (via the existing + `Int64ToWireQuad` convention — the same id form the row carries), int32 + slice length, the SDL as a counted string, an empty "parameters" string, + and the slice length again. The reply is `op_slice`: int32 length, + followed by the slice data, XDR encoded element by element. +* **`op_put_slice`**: the same header followed by the slice data. The + response is the standard `op_response`, whose `ObjectID` returns the + (possibly new) array id — arrays, like blobs, get their id at store + time. +* Slice data on the wire is XDR: each element encoded as its type + dictates, exactly as row values are, so `FBWireMessage`'s scalar + encode/decode helpers apply. Text elements use the padded opaque form; + `SQL_VARYING` elements carry counted strings. + +## Design + +New unit `client/wire/FBWireArray.pas`: + +* `TFBWireArrayMetaData = class(TFBArrayMetaData)` — `LoadMetaData` runs + the same system-table query as `TFB30ArrayMetaData` through the wire + attachment's own `OpenCursor`; `GetCharSetID`/`GetCharSetWidth`/ + `GetCodePage` answered from the attachment's charset machinery as the + 3.0 version does. +* `TFBWireArray = class(TFBArray)` — `AllocateBuffer` override calls + inherited then generates SDL (the `GenerateSDL` body lifted from + `FB30Array`, or better: moved to a shared unit both providers use); + `InternalGetSlice`/`InternalPutSlice` call two new + `TFBWireConnection` methods. + +`TFBWireConnection` gains `GetSlice` and `PutSlice` methods that do the +packet exchange and the XDR (de)serialisation of the slice buffer, driven +by the descriptor (element dtype, length, count). + +The attachment and statement stubs then construct these classes exactly as +`FB30Attachment`/`FB30Statement` do — the shapes to mirror are +`TIBXSQLVAR.GetAsArray`, `TIBXSQLVAR.GetArrayMetaData` and +`TFB30Statement.CreateArray`. + +### Refactoring note + +`GenerateSDL` should move from `FB30Array.pas` into a compiler-neutral +shared location (`FBArray.pas` itself, guarded, or a small `FBSDL.pas`), +with `TFB30Array` and `TFBWireArray` both calling it. Duplicating a +`gen_SDL` port would be worse than the move. + +## Acceptance + +* The array tests in the suite (`Test10` and friends — exact list per the + milestone 1 doc) pass over the wire provider. +* A new `WireTest` section: create a table with `INTEGER[1:4]` and + `VARCHAR(16)[0:3]` columns, write both arrays, read them back in a new + transaction, compare element by element; also a 2-dimensional slice. +* Green on Firebird 3–6 in CI; nothing here is version dependent. + +## Out of scope + +Array slicing narrower than the full bounds (fbintf's `IArray` always +reads and writes the full slice today; the SDL loop bounds follow the +descriptor as `TFB30Array` does), and array parameters in the batch API. diff --git a/doc/roadmap/06-cancel-timeouts.md b/doc/roadmap/06-cancel-timeouts.md new file mode 100644 index 00000000..6d2560b0 --- /dev/null +++ b/doc/roadmap/06-cancel-timeouts.md @@ -0,0 +1,133 @@ +# Design: Statement timeouts and operation cancellation + +Roadmap milestone 6 (`doc/WireProtocol.md`). + +**Status: implemented** — `IAttachment.CancelOperation` and +`IStatement.SetStatementTimeout`/`GetStatementTimeout` added to the +public interfaces and implemented by all three providers as designed +(2.5 via `fb_cancel_operation` when exported, timeouts always +`ibxeNotSupported`; 3.0 via `cancelOperation`/`setTimeout` with the +Firebird 4 client guard; wire natively). WireTest gained a cancellation +section (worker thread runs a bounded PSQL busy loop, the main thread +cancels, `isc_cancelled` asserted) and a timeout section — 118 tests. +The full suite output is unchanged, confirming a zero timeout produces +byte identical packets. Findings against the plan: + +* An expired timeout does **not** arrive as `isc_sql_timeout` (no such + code): the server cancels the request, so the primary status is + `isc_cancelled` with `isc_req_stmt_timeout` as the secondary gds code + (`thread_db::checkCancelState` in the Firebird sources). Tests must + assert on `isc_cancelled`; with the wire provider the secondary code + is not yet visible at the API surface (milestone 12 territory). +* The send lock alone was not enough for `op_cancel`: the normal path + assembles packets across many buffered writes, so a lock at + `WriteBytes` granularity cannot keep a cancel out of the middle of a + packet under assembly. `op_cancel` therefore bypasses the shared send + buffer entirely (`TFBWireTransport.SendDirect`) and the lock covers + cipher application plus the socket write in both `SendDirect` and + `Flush`. The stream cipher stays consistent because bytes are always + enciphered in the order they reach the wire. + +## Goal + +Two related but separable features: + +1. **`op_cancel`** (protocol 12, so available on every connection this + client makes): abort an operation in flight from another thread, the + wire form of `fb_cancel_operation`. The victim fails with + `isc_cancelled` through the normal status path. +2. **Statement timeouts** (protocol 16): the `p_sqldata_timeout` field of + `op_execute` / `op_execute2`, which `TFBWireConnection` already writes — + currently hardcoded to zero in both `ExecuteStatement` and + `ExecuteStatement2`. + +## Current state + +* `op_cancel = 91` and the four kinds (`fb_cancel_disable`, + `fb_cancel_enable`, `fb_cancel_raise`, `fb_cancel_abort`) are defined in + `FBWireConst.pas` and unused. +* The timeout field is written as a literal 0 at the two `op_execute` + sites in `FBWireProtocol.pas`, gated on `FProtocolVersion >= 16`. +* There is no timeout surface anywhere in fbintf's `IStatement` — neither + the wire provider nor the 2.5/3.0 providers expose one. So the timeout + half of this milestone is an **interface addition**, not just a wire + change. + +## Design: timeouts + +Interface: add to `IStatement` (and `TFBStatement` as virtual with a +stored field): + +```pascal +procedure SetStatementTimeout(aMilliseconds: cardinal); +function GetStatementTimeout: cardinal; {0 = no timeout} +``` + +* Wire provider: `TFBWireStatement` passes the value into + `ExecuteStatement`/`ExecuteStatement2`, which gain a timeout parameter + replacing the literal 0. On connections below protocol 16 a non-zero + timeout raises `ibxeNotSupported` rather than being silently dropped. +* 3.0 provider: `IStatement` maps onto Firebird 4+'s + `IStatement::setTimeout` when available (client major ≥ 4), else + `ibxeNotSupported`. The 2.5 provider always raises. This keeps the + interface honest across providers. + +Server behaviour when the timeout fires: the statement fails with +`isc_sql_timeout` (Firebird 4+) arriving as an ordinary error response — +no new packet handling needed. + +## Design: cancellation + +Surface: `IAttachment.CancelOperation(Kind)` exists conceptually in the +ISC API as `fb_cancel_operation`; fbintf does not expose it today, so add +`procedure CancelOperation(aKind: integer = fb_cancel_raise)` to +`IAttachment`, implemented by 2.5 (via `fb_cancel_operation` when the +loaded library exports it), 3.0 (`IAttachment::cancelOperation`) and wire. + +Wire mechanics — the two constraints that shape everything: + +* `op_cancel` has **no response**. Nothing must be read after sending it; + the cancelled operation's own error response is what comes back, on the + main read path. +* It must be written **while another thread is blocked** in `ReadBytes` + on the same socket. The send and receive sides of `TFBWireTransport` + are already independent (separate buffers, separate ciphers), so a + concurrent write is structurally sound. What is missing is a lock: two + threads writing concurrently would interleave packets. + +Plan: + +1. Add a send-side critical section to `TFBWireTransport`, taken by + `WriteBytes`/`Flush` callers at packet granularity: the normal request + path already serialises (one request at a time per connection), so the + only new contender is `SendCancel`. +2. `TFBWireConnection.SendCancel(aKind)` — takes the send lock, writes + `op_cancel` + kind, flushes. Never touches the receive side. +3. The blocked reader then receives the pending operation's response, + which carries `isc_cancelled` (`fb_cancel_raise`) — already handled by + the existing status vector path. +4. `fb_cancel_abort` closes the connection server side; the reader's + `EFBWireError('Connection lost...')` → `isc_network_error` path already + copes. + +One caveat to document: with the send **cipher** active, `Process` mutates +cipher state, so the send lock must also cover cipher application — it +already will, because encryption happens inside `Flush`. + +### What op_cancel does not do + +It does not unblock a client stuck in `ReadBytes` because the *server* is +gone — that is the socket timeout's job (`ConnectTo` already accepts one). +The doc should say so to head off misuse. + +## Acceptance + +* `WireTest` gains: start `select ... from big generator loop` (e.g. + `rdb$types` cross joins) on a worker thread, cancel from the main + thread, assert `isc_cancelled` arrives within a bound; execute a + statement with a 1ms timeout against a deliberately slow query on + protocol ≥ 16 and assert `isc_sql_timeout`. +* Timeout of 0 (default) produces byte-identical packets to today — + verified by the existing suite passing unchanged. +* Threaded test guarded the same way `FBEvents.SetEvents` guards + (`IsMultiThread`), and skipped in the offline CI job. diff --git a/doc/roadmap/07-scrollable-cursors.md b/doc/roadmap/07-scrollable-cursors.md new file mode 100644 index 00000000..08eee65e --- /dev/null +++ b/doc/roadmap/07-scrollable-cursors.md @@ -0,0 +1,119 @@ +# Design: Scrollable cursors over the wire protocol + +Roadmap milestone 7 (`doc/WireProtocol.md`). + +**Status: implemented** — the offer now goes to protocol 18, +`TFBWireConnection.FetchRowScroll` carries the positioned fetches, and +the five `IResultSet` methods plus `GetFlags`/`HasScollableCursors` +answer truthfully. WireTest gained a scroll section (137 tests) and the +negotiation test extends to 18; the suite's Test 2 scrollable section +now runs against protocol 18 servers. The design held, with these +notes: + +* Protocol 18 changes more than the two new operations: **every** + `op_execute`/`op_execute2` gains a cursor flags word after the + timeout field (`p_sqldata_cursor_flags`), written as zero for + non scrollable cursors and singletons. +* The server keeps its own prefetch cache and discards it when the + fetch direction or position changes, repositioning the engine cursor + as needed (`rem_port::fetch`); the client's obligations are only to + drop its read ahead rows on a scroll and to fetch one row per + positioned request - the server forces single row batches for every + direction except next/prior anyway. +* The `FillChar` reset of `TWireCursorState` leaked the managed `Rows` + array as predicted; all three sites now use a `ResetCursorState` + helper. +* BOF/EOF semantics are copied from `TFB30Statement.Fetch`: success + clears both flags, `FetchPrior` off the top sets BOF (and raises + `ibxeBOF` if already there), a failed `First`/`Last`/`Absolute`/ + `Relative` leaves the flags untouched, and a sequential fetch after a + scroll starts a fresh batch from the new position. +* The CI matrix expectation for Firebird 5 and 6 moves from protocol 17 + to 18, and the suite reference log is regenerated (the negotiated + version appears in `getFBVersion` output, and Test 2's scrollable + section output is new). + +## Goal + +`FetchPrior`, `FetchFirst`, `FetchLast`, `FetchAbsolute` and +`FetchRelative` work on the wire provider when the negotiated protocol is +18 or later, via `op_fetch_scroll`. + +## Current state + +* `TWireResultSet.FetchPrior/First/Last/Absolute/Relative` each raise + `ibxeNotSupported`; `TFBWireStatement.InternalOpenCursor` raises when + asked for a scrollable cursor; `TFBWireAttachment.HasScollableCursors` + hardcodes false (note the interface spelling, one `r` — keep it). +* `op_fetch_scroll = 112` and `op_info_cursor = 113` are defined in + `FBWireConst.pas`, unused. +* `MaxSupportedProtocol` is 17, so **this milestone requires raising the + offer to protocol 18** and verifying nothing else changes at 18 (the + P18 additions are `op_fetch_scroll`/`op_info_cursor`; the accept + handling itself is version agnostic). +* The forward-only row cache is `TWireCursorState` + (`Rows: array of TBytes; NextRow: integer; EndOfCursor: boolean`), + filled by `TFBWireConnection.FetchRow`, which drains the whole + `op_fetch_response` batch before returning — a hard protocol + requirement that applies equally to scroll fetches. + +## The protocol + +`op_fetch_scroll` is `op_fetch` plus two fields: statement handle, BLR, +message number, fetch count, then **direction** and **position**. +Directions follow `IResultSet`'s semantics: next, prior, first, last, +absolute, relative; position is the absolute row number or relative +offset (ignored for the others). The response stream is the same +`op_fetch_response` sequence, and end-of-window is still status 100 / +zero message count. A scrollable cursor must be requested at execute +time: `op_execute`'s cursor flags word gets the scrollable bit when the +statement was opened scrollable (mirrors `IStatement::CURSOR_TYPE_SCROLLABLE`). + +## Design + +1. **Protocol layer.** `TFBWireConnection.FetchRowScroll(aHandle, aFormat, + aOutBuffer, aDirection, aPosition, var aState)` — same drain loop as + `FetchRow`, plus the two extra fields. Gate: raise `ibxeNotSupported` + if `ProtocolVersion < 18`. Raise `MaxSupportedProtocol` to + `PROTOCOL_VERSION18` and extend `OfferedProtocols` (the negotiation + code already caps by `MaxProtocol`, and the CI matrix measures what + each server settles on). +2. **Cache semantics.** The existing cache assumes the server-side cursor + position equals "last row the client fetched". Any non-sequential + fetch invalidates that: on `FetchPrior/First/Last/Absolute/Relative`, + discard `aState.Rows` (with a proper `SetLength(...,0)` — note the + current `FillChar` reset of a record containing a managed dynamic + array leaks the array; fix that while touching it) and fetch a batch + of **one** for positioned directions. Batched read-ahead stays for + `next` (and can be added for `prior` later; correctness first). +3. **Statement layer.** `InternalOpenCursor` stops raising for + `Scrollable` when the attachment reports support; it records the flag + and sets the scrollable bit at execute. `TWireResultSet`'s five + methods delegate to a new `TFBWireStatement.FetchScroll(direction, + position)`, symmetric with `FetchNextRow`, updating `FBOF`/`FEOF` per + direction (prior before row 1 sets BOF, etc. — copy the semantics from + `TFB30Statement`/`TResults`, which the suite's Test 2 pins down). +4. **Capability.** `HasScollableCursors` returns + `Connection.ProtocolVersion >= 18`, and `TFBWireStatement.GetFlags` + is overridden to include `stScrollable` truthfully (the base returns + `[]`; `FB30Statement.GetPerfStatistics`'s neighbour `GetFlags` is the + model). Test 2's scrollable section then runs automatically once + milestone 1's suite run exists. + +## Interaction with inline blobs + +None yet — but the drain loop is the same one milestone 9 teaches to +accept `op_inline_blob` packets interleaved with rows, so both changes +land in the same few lines of `FetchRow`/`FetchRowScroll`. Sequence the +two milestones to avoid a merge knot (this one first, per roadmap order). + +## Acceptance + +* Test 2's `DoScrollableQuery` passes over the wire provider against + Firebird 5 and 6 (protocol 18 servers); against Firebird 3/4 the + capability stays false and the section skips as it does today. +* `WireTest` gains a scroll section: open scrollable, `FetchLast`, + `FetchAbsolute(3)`, `FetchPrior`, `FetchRelative(-1)`, `FetchFirst`, + asserting row identity each step against a known ordered result. +* The negotiation test in `WireTest` (caps at 14–17 today) extends to 18, + and the CI step summary shows which servers settle on 18. diff --git a/doc/roadmap/08-batch-api.md b/doc/roadmap/08-batch-api.md new file mode 100644 index 00000000..8a35fd43 --- /dev/null +++ b/doc/roadmap/08-batch-api.md @@ -0,0 +1,134 @@ +# Design: The batch API over the wire protocol + +Roadmap milestone 8 (`doc/WireProtocol.md`). + +**Status: implemented** — `TFBWireConnection.BatchCreate/Msg/RegBlob/ +Exec/Release/Cancel`, `TWireBatchCompletion`, and the `TFBWireStatement` +overrides, with Tests 19 and 20 passing over the wire and a WireTest +batch section (151 tests). The design held in outline; the findings +that reshaped its details: + +* Batch messages do **not** travel as an eight byte padded counted + blob: `xdr_packed_message` is byte for byte the ordinary row message + encoding (null bitmap + values), so `XDREncodeMessage` served + unchanged. The eight byte alignment governs the server's buffer + spacing and the client's buffer accounting only. +* `op_batch_create`'s message length field must be computed with the + server's `PARSE_msg_format` rules from our BLR (`EngineMessageLength`) + - the client's private buffer layout is laid out differently and its + size is rejected. The `IBatch` parameter block is a *wide* tagged + clumplet buffer: four byte lengths, not the DPB's one byte form. +* Phase 2 was partially needed after all: the engine translates every + non null, non zero blob id in a batch message through a registration + map and consumes the entry, so pre-existing blob ids (Test 19's + pattern) must be registered with `op_batch_regblob` once per row - + exactly what `TFB30Statement`'s message packer does with + `registerBlob`. Array ids pass through untouched, so + `op_batch_set_bpb`/`op_batch_blob_stream` remain unimplemented (no + fbintf surface reaches them). +* `TAG_MULTIERROR` is not set, matching the 3.0 provider: a batch stops + at its first failing row and the completion reports it. +* The deepest change was not in the batch code: the wire statement's + default text type moved from `SQL_TEXT` to `SQL_VARYING`, as the + fbclient providers answer. The `SQL_TEXT` default re-sized the + parameter format on every string assignment - fatal once + `op_batch_create` has frozen the BLR - and its blank padding leaked + into stored values. The switch exposed and fixed two latent accessor + bugs (a varying *parameter's* `SQLData` must point at the characters + while a *column's* points at the length prefix - `TSQLParam` relies + on the distinction), and `CanChangeMetaData` now answers false while + a batch is open, refusing mid batch type changes with the same error + the 3.0 provider raises. The suite's parameter metadata and value + output now matches the fbclient reference logs where it previously + diverged (blank padded CHAR echoes, padded `INCLEAR` hex dumps). + +## Goal + +`IStatement.AddToBatch` / `ExecuteBatch` / `CancelBatch` / +`GetBatchCompletion` work over the wire on protocol 16+ connections, with +`IBatchCompletion` reporting per-row status, matching the 3.0 provider's +behaviour on Firebird 4+. + +## Current state + +* `TFBStatement` already carries the whole public surface with defaults: + the four methods raise `ibxeBatchModeNotSupported`, + `Get/SetBatchRowLimit` are concrete (`DefaultBatchRowLimit = 1000`), + `IsInBatchMode`/`HasBatchMode` return false. + `TFBWireAttachment.HasBatchMode` returns false. The wire statement has + no batch code at all. +* The full opcode family is defined and unused in `FBWireConst.pas`: + `op_batch_create = 99`, `op_batch_msg`, `op_batch_exec`, + `op_batch_rls`, `op_batch_cs`, `op_batch_regblob`, + `op_batch_blob_stream`, `op_batch_set_bpb`, `op_batch_cancel`, + `op_batch_sync`, `op_info_batch`. +* `TBatchCompletion` in `client/3.0/FB30Statement.pas` shows the result + shape (`getTotalProcessed`, `getState` mapping to + `bcExecuteFailed`/`bcSuccessNoInfo`/`bcNoMoreErrors`, `getErrorStatus`, + `getUpdated`), but wraps the OO API's completion object — the wire + version parses the same information out of `op_batch_cs` directly, so + it is a sibling, not a reuse. + +## The protocol + +* **`op_batch_create`**: statement handle, the BLR describing the message + (the same `BuildMessageBlr` output used for `op_execute`), the message + length, and a parameter block (`TAG_RECORD_COUNTS`, + `TAG_BUFFER_BYTES_SIZE`, `TAG_MULTIERROR`...) — the same tags + `TFB30Statement.AddToBatch` writes with `IXpbBuilder.BATCH`, built here + with `FBParamBlock` machinery. +* **`op_batch_msg`**: statement handle, message count, then the messages + as one counted blob, **each message padded to an 8 byte boundary** — + note `FBWireMessage` aligns to 4 today, so the batch encoder pads + explicitly. +* **`op_batch_exec`**: statement handle, transaction handle. +* **`op_batch_cs`** (the response to exec): per-row completion — total + count, then vectors of update counts and status vectors for failed + rows. Parsed into the wire `TBatchCompletion`. +* **`op_batch_rls`** releases the batch; `op_batch_cancel` abandons it. +* Blob-carrying batches additionally use `op_batch_regblob` / + `op_batch_blob_stream` / `op_batch_set_bpb`. + +## Design + +Phase 1 — no blobs (matches most real batch use: bulk insert of scalars): + +1. `TFBWireConnection` gains `BatchCreate`, `BatchMsg`, `BatchExec` (→ + parsed completion record), `BatchRelease`, `BatchCancel`, each gated on + `ProtocolVersion >= 16`. +2. `TWireBatchCompletion = class(TInterfaceOwner, IBatchCompletion)` in + `FBWireStatement.pas`, built from the parsed `op_batch_cs` body; + semantics copied from the 3.0 class (RowNo is 1-based first failure, + `getUpdated` counts until first failure). +3. `TFBWireStatement` overrides the four methods plus + `IsInBatchMode`/`CheckChangeBatchRowLimit`, with the same guard + behaviour as 3.0 (`ibxeInvalidBatchQuery` unless the statement is an + insert/update/delete/exec-procedure; `ibxeInBatchMode` on row-limit + change mid-batch; `EIBBatchBufferOverflow` past + `FBatchRowLimit * aligned row size` clamped to [16MB, 256MB] — same + sizing rule as 3.0 so behaviour matches across providers). +4. Messages accumulate client-side in a growable buffer (8-byte padded); + `ExecuteBatch` sends create + msg + exec in one flush and parses the + completion; failures raise `EIBInterBaseError` via the same + `Check4BatchCompletionError` logic 3.0 uses. +5. `TFBWireAttachment.HasBatchMode` returns + `Connection.ProtocolVersion >= 16`. + +Phase 2 — blobs in batch (`op_batch_regblob` for existing blob ids, +`op_batch_set_bpb` + `op_batch_blob_stream` for inline creation), only +after phase 1 is green; Test 19 includes blob columns, so phase 1 alone +keeps its skip line for the blob subtest or the test is split. + +## Acceptance + +* Test 19 (batch update/insert) and Test 20 (batch stress) pass over the + wire provider against Firebird 4, 5 and 6; against Firebird 3 + `HasBatchMode` is false (protocol 15) and both keep their skip paths. +* `WireTest` gains an offline check of the 8-byte message padding and a + live section: 1000-row batch insert, one deliberate constraint + violation mid-batch with `TAG_MULTIERROR`, asserting + `getTotalProcessed`, the failing row number and `isc` code from + `getErrorStatus`, and rollback behaviour. +* Wire and 3.0 providers produce the same `IBatchCompletion` answers for + the same input — asserted by running the same batch through both in the + suite. diff --git a/doc/roadmap/09-inline-blobs.md b/doc/roadmap/09-inline-blobs.md new file mode 100644 index 00000000..94ff00bb --- /dev/null +++ b/doc/roadmap/09-inline-blobs.md @@ -0,0 +1,114 @@ +# Design: Inline blobs (protocol 19) + +Roadmap milestone 9 (`doc/WireProtocol.md`). + +**Status: implemented** — the offer goes to protocol 19, `ReadOperation` +intercepts `op_inline_blob` into a per attachment cache, and +`TFBWireBlob` serves opens from it. WireTest gained an inline blob +section (158 tests) whose packet counter on the transport proves a +cached open causes no wire traffic, with fallbacks covered for +over-limit blobs and an opted out (zero) limit. The design held; the +notes: + +* The announce field is only the one `p_sqldata_inline_blob_size` word: + the cursor flags the plan grouped with it had already arrived with + protocol 18. The value comes from the existing + `IAttachment.GetInlineBlobLimit` (default 8KB, capped 32KB) - no new + surface needed, and `SetInlineBlobLimit(0)` is the opt out. +* The pushed data is the segmented stream - the same two byte length + prefixed form `op_get_segment` returns - so the blob's `Read` path + reuses the unpacking, and the pushed info buffer carries exactly the + four items (`num_segments`, `max_segment`, `total_length`, `type`) + every fbintf `GetInfo` caller asks for, so it is served verbatim. +* An inline id is single use on both sides: the cache consumes the + entry on open, mirroring the server sending each id once. +* Cache entries die with their transaction (commit, rollback and both + retaining forms), and the 16MB cap simply drops further pushes - the + classic open is always correct. +* Transparency held exactly: the full suite diff against the previous + reference is the negotiated version string, 18 to 19, and nothing + else. + +## Goal + +On protocol 19+ connections the server pushes small blobs alongside the +rows that reference them as `op_inline_blob` packets; the client caches +them and `IBlob` opens served from the cache, saving one round trip per +blob. This is transparent to callers — it is purely a latency +optimisation. + +## Current state + +* `op_inline_blob = 114` is defined in `FBWireConst.pas`, unused. +* `MaxSupportedProtocol` is 17; this milestone (after the scroll + milestone takes it to 18) raises it to `PROTOCOL_VERSION19`. +* `ReadOperation` already skips unsolicited traffic (`op_dummy`, + `op_response_piggyback`) in a loop — the natural place to intercept + `op_inline_blob`, exactly as the roadmap anticipates. +* Blob opens go through `TFBWireBlob` over the connection's blob + methods; blob ids travel as the quad/`Int64` handled by + `WireQuadToInt64`. + +## The protocol + +* The client announces the maximum inline blob size it accepts in + `op_execute` / `op_execute2`: protocol 19 appends two fields after + `p_sqldata_timeout` — the cursor flags and the **inline blob size + limit** (and Firebird's default is 64KB; 0 disables). Note the current + code writes nothing after the timeout field, which is correct for ≤18 + and must grow the extra field(s) once 19 can be negotiated. +* `op_inline_blob` packets arrive interleaved with `op_fetch_response` + packets inside a fetch batch (and after execute for singleton results). + Each carries: transaction handle, the blob id (quad), the blob's total + info (length/segmented flag) and the whole blob data as one opaque + segment stream. +* An inline blob is a *copy* pushed at fetch time: it is only valid for + that blob id under that transaction, and the server sends each id once. + +## Design + +1. **Cache.** A per-attachment `TWireInlineBlobCache`: map from + (transaction handle, blob id) to the received bytes + info. Owned by + `TFBWireAttachment`, cleared on transaction end (commit/rollback drop + that transaction's entries) and capped: entries are evicted once + consumed (a blob is normally opened at most once per fetch), and the + whole cache is bounded by, say, 16MB to keep a scan over a + blob-heavy table from ballooning memory — beyond the cap new arrivals + are simply dropped (falling back to `op_open_blob2` costs a round + trip, never correctness). +2. **Receive path.** `ReadOperation` gains a case: on `op_inline_blob`, + decode the packet and hand it to a callback the attachment installs on + the connection (`OnInlineBlob: procedure(...) of object`), then + continue the loop. This keeps `FBWireProtocol` free of provider + types, consistent with the unit layering. +3. **Open path.** `TFBWireBlob` (open-for-read constructor) consults the + cache before sending `op_open_blob2`. On a hit it becomes a purely + local object: `Read` serves from the buffer, `GetInfo` answers from + the stored info, `Close` just releases. On a miss, unchanged + behaviour. +4. **Announcing the limit.** `ExecuteStatement`/`ExecuteStatement2` write + the P19 fields when `ProtocolVersion >= 19`; the limit is a property + on the attachment (default the server default; 0 lets users opt out + and also gives tests a control case). No `IAttachment` interface + change needed — a wire-specific property, like `MaxProtocol`. + +## Risks + +The interleave handling is the only sharp edge: the fetch drain loop in +`FetchRow` treats any operation other than `op_fetch_response` / +`op_response` as fatal. Routing everything through `ReadOperation` (which +it already uses) makes the interception automatic, but the drain loop's +error case must still fire for genuinely unexpected ops — the case added +in step 2 keeps that property. + +## Acceptance + +* All existing blob tests (WireTest blob round trips, suite Tests 6/15) + pass unchanged on servers negotiating 19 (Firebird 5.0.3+/6) **and** + the results are byte-identical with the limit set to 0 versus default — + proving transparency. +* `WireTest` gains: fetch a row set with small blobs, assert (via a + packet/round-trip counter on the connection, which `WireTest` can read) + that no `op_open_blob2` was sent for inline-served blobs; and a + larger-than-limit blob still opens the classic way. +* Firebird 3/4 negotiate ≤17 and are unaffected — CI matrix proves it. diff --git a/doc/roadmap/10-protocol-20.md b/doc/roadmap/10-protocol-20.md new file mode 100644 index 00000000..6210e83c --- /dev/null +++ b/doc/roadmap/10-protocol-20.md @@ -0,0 +1,106 @@ +# Design: Firebird 6 and protocol 20 + +Roadmap milestone 10 (`doc/WireProtocol.md`). + +**Status: implemented** — the offer goes to protocol 20, WireTest gains +a schema section (164 tests) and the negotiation test runs the full +14..20 ladder. Findings against the plan: + +* The field audit found exactly one unconditional packet change at 20: + `p_sqlst_flags`, a flags word at the end of both + `op_prepare_statement` **and** `op_exec_immediate` (they share the + XDR block; the plan only named the prepare). Written as zero. +* Named arguments are a SQL level feature (`name => value` in calls), + not a describe item - there is nothing to request or parse for them + on the wire. +* The describe gains `isc_info_sql_relation_schema` (33), requested + only from protocol 20 connections and parsed into the column format. + `ParsePrepareResponse` already skipped unknown items with their + length, so no hardening was needed - the defensive property the plan + asked to verify was there. +* The 3.0 provider surfaces no schema name (fbintf's `IColumnMetaData` + has no such member and the bundled `FirebirdOOAPI` predates one), so + there was nothing to match: the wire stores the name and exposes it + through `TFBWireStatement.ColumnSchemaName` for the tests, leaving + the interface question to fbintf as a whole. +* `isc_dpb_search_path` went in alongside the other Firebird 6 DPB + tags (97..107), with the DPB name table extended to match, and works + through the ordinary `DPB.Add(...).AsString` route - proven by the + WireTest section attaching with a search path and reading the + expected table. + +## Goal + +Negotiate protocol 20 with Firebird 6 and gain its two features: SQL +schema support (search path, schema-qualified metadata) and named +arguments in the describe information. Firebird 6 already works with this +client today by negotiating down to 17; this milestone is about the new +capabilities, not compatibility. + +## Current state + +* `PROTOCOL_VERSION20` is defined in `FBWireConst.pas`; + `MaxSupportedProtocol` is 17 (rising to 19 through milestones 7 and 9 — + this milestone should land after them, per roadmap order, so each + version bump is tested in isolation). +* The describe parser is `FBWireDescribe.ParsePrepareResponse` over + `isc_info_sql_*` clumplets; the DPB builder is the shared + `FBParamBlock`. +* The CI matrix already runs Firebird 6 (`WireCrypt` Enabled and + Required), so acceptance infrastructure exists. + +## What protocol 20 changes + +Three things touch this client; each is independently small: + +1. **Schema search path in the attach.** `isc_dpb_search_path` carries + the schema search path. This is a DPB tag, so `FBParamBlock` needs + only the constant — users add it like any other DPB item. The + session-level `SET SEARCH_PATH` statement works regardless; the DPB + route makes it declarative at connect. +2. **Schema-aware describe.** The describe response gains schema name + items per column (`isc_info_sql_relation_schema` and friends). + `ParsePrepareResponse` must skip unknown items robustly today (verify + — a parser that raises on unknown clumplets breaks the moment the + server is asked for new items) and `TWireSQLVar` gains the schema + name, surfaced through the existing `IColumnMetaData` where the 3.0 + provider surfaces it for Firebird 6. +3. **`op_prepare_statement` flags.** Protocol 20 adds a flags word to the + prepare packet (named-argument describe among them). Written when + `ProtocolVersion >= 20`, zero by default. + +Named arguments themselves (`:name` binding by name at the API level) are +an fbintf-wide feature question — the wire work is only to request and +parse the argument names; exposing them follows whatever the 3.0 provider +does for Firebird 6. + +## Design + +1. Raise `MaxSupportedProtocol` to `PROTOCOL_VERSION20`, extend + `OfferedProtocols` (weights continue the existing `(i+1)*2` series). + Audit every `FProtocolVersion >=` site for fields protocol 20 appends + — the known ones are the prepare flags and any execute-packet growth; + the audit method is a field-by-field read of Firebird 6's + `protocol.cpp` send/receive for the DSQL ops, recorded in + `doc/WireProtocol.md` the way the 13–17 differences already are. +2. Add the new `isc_dpb_search_path` and `isc_info_sql_*` schema + constants to `client/include` alongside their peers. +3. Harden `ParsePrepareResponse` to skip-with-length any unknown item + (defensive regardless of this milestone), then teach it the schema + items. +4. Plumb schema name into `TWireSQLVar` → `TWireSQLVarData` → + `IColumnMetaData.GetRelationSchema` (or whatever name the 3.0 + provider settles on for FB6 — match it, don't invent). + +## Acceptance + +* Firebird 6 CI rows negotiate 20 and the whole `WireTest` suite still + passes — the step summary makes the negotiated version visible. +* The negotiation cap test extends to 20, and capping at 17 against + Firebird 6 still works (regression guard for mixed-version fleets). +* New `WireTest` section, gated on protocol ≥ 20: create two schemas, + same-named table in each, attach with a search path and assert the + right table answers; describe a query and assert the schema name + arrives per column. +* Firebird 3/4/5 rows are byte-identical in behaviour (they never see + the new fields). diff --git a/doc/roadmap/11-wire-compression.md b/doc/roadmap/11-wire-compression.md new file mode 100644 index 00000000..3442edf3 --- /dev/null +++ b/doc/roadmap/11-wire-compression.md @@ -0,0 +1,127 @@ +# Design: Wire compression + +Roadmap milestone 11 (`doc/WireProtocol.md`). + +**Status: implemented** — `pflag_compress` is offered on request, the +accept's flag is captured before the `ptype_mask` masking discards it, +and the transport gained the two stage pipeline (deflate below the +cipher on send, inflate above it on receive; one zlib stream per +direction, `Z_SYNC_FLUSH` per packet). WireTest gained a compression +section (167 tests) that negotiates it on a second attachment and round +trips a bulk query and a blob; the CI matrix gained a dedicated +`WireCompression = true` row on Firebird 5. Notes against the plan: + +* The user surface ended up even smaller than a property: the wire + attachment reads `WireCompression=true` from an `isc_dpb_config` DPB + item - the same knob the stock client honours - so application code + is provider portable. `TFBWireConnection.Compression` exists for the + protocol-level API. +* `op_cancel` was the subtle case the plan missed: `SendDirect` must + route through the same deflate stream, not around it - the peer + inflates one continuous stream and raw bytes would corrupt it. Both + senders share the deflate state under the existing send lock. +* Compression starts after the accept *packet*, not the accept *op*: + an `op_accept_data`/`op_cond_accept` packet carries the auth payload + after the type word, all still clear; the deflate streams switch on + once that packet has been read in full. The remaining handshake + (`op_cont_auth`, `op_crypt`) is compressed, and the cipher changeover + logic needed no change because the cipher stayed against the socket. +* The receive rework kept `ReadBytes`/`FRecvPos`/`FRecvLimit` untouched + by adding a second buffer for the not yet inflated stream inside + `FillRecvBuffer` only; the compression-off path is the previous code + verbatim, and the suite reference log needed no regeneration - the + first milestone since the test suite one to leave it untouched. +* FPC's `paszlib` (pure Pascal zlib) interoperates with the server's + zlib as the format demands; the local Firebird 6 snapshot accepted + compression out of the box and every WireTest section passed over + the compressed, encrypted stream. + +## Goal + +Request `pflag_compress` in the connect, and when the server accepts, run +zlib over the byte stream **beneath** the cipher (compress, then encrypt — +compressing ciphertext is useless), matching `WireCompression = true` in +`firebird.conf`. + +## Current state + +* `pflag_compress = $100` is defined in `FBWireConst.pas` and never set; + the accept handler masks the type with `ptype_mask = $FF`, so the flag + bit is currently discarded unseen — it must be captured before masking. +* The transport (`TFBWireTransport`) has exactly one filter slot per + direction (`FSendCipher`/`FRecvCipher: TWireCipher`), applied + wholesale: encrypt in `Flush` over the pending send buffer, decrypt in + `FillRecvBuffer` over the freshly read chunk. That model fits a stream + cipher (length-preserving) and does **not** fit zlib, which changes + byte counts. +* FPC ships zlib bindings (`zbase`/`zinflate`/`zdeflate` in the `paszlib` + package — pure Pascal, so the no-external-dependencies property + survives); Delphi has `System.ZLib`. The transport remains the only + compiler-sensitive unit. + +## Design + +### Negotiation + +`ConnectTo` sets `pflag_compress` on each offered entry's type field when +a new `Compression` property is true (default **false** initially; +flipping the default can follow once soak-tested). In the accept, +capture `aType and pflag_compress` before the existing `ptype_mask` +masking; compression is on only if both sides asked. + +### Transport restructure + +The send and receive paths get a two-stage pipeline with an explicit +buffer between the stages, replacing the in-place single-buffer trick +only when compression is active: + +* **Send**: `Flush` runs deflate (`Z_SYNC_FLUSH` — packet boundaries must + reach the server promptly; a plain full-flush-never deflate deadlocks + request/response protocols) over the pending plaintext into a staging + buffer, then the cipher over the staging buffer, then the socket + write. With compression off, today's path is untouched. +* **Receive**: `FillRecvBuffer` reads ciphertext, decrypts in place + (unchanged), then feeds the result into the inflate stream, and + `ReadBytes` consumes inflate output. Because inflate output length is + unpredictable, the receive side becomes: raw buffer → inflate → + plaintext buffer, with `FRecvPos/FRecvLimit` moving to the plaintext + buffer. `HasBufferedData` must account for bytes held inside the + inflate state (pending output *and* unconsumed input) — it guards + cipher installation and event polling, so getting it wrong corrupts + the stream. + +One zlib stream per direction for the lifetime of the connection (the +protocol compresses the stream, not packets), created lazily when the +accept confirms compression. + +### Ordering with encryption + +`op_crypt` changeover happens after compression is already active +(compression starts with the accept; encryption starts later). The +existing asymmetric changeover logic is unchanged because the cipher +still sits directly against the socket on both paths — the pipeline +order compress→encrypt on send and decrypt→inflate on receive keeps the +cipher position identical to today. The `EnableRecvCipher` "unread data" +guard now also requires the inflate stage to be drained. + +## Risks + +* The receive rework touches the hottest path in the client; it must be + refactored so the compression-off path is provably identical (same + code, staging disabled) — the CI matrix and reference logs are the + guard. +* `Z_SYNC_FLUSH` overhead makes small packets slightly larger; the + feature only pays on bulk fetches over slow links, which is why the + default stays off and the property is per-connection. + +## Acceptance + +* `WireTest` offline: a loopback deflate/inflate round trip through the + transport pipeline with the cipher also enabled, asserting byte + fidelity across chunk-boundary-straddling reads. +* Live: the full `WireTest` run against a server with + `WireCompression = true`, in all three cases: compression only, + encryption only, both. CI matrix gains a `WireCompression = true` + dimension row for Firebird 5 (one row, not the cross product — the + compression code is version independent, protocol ≥ 13 all the same). +* With the property false, packets are byte-identical to today. diff --git a/doc/roadmap/12-message-text.md b/doc/roadmap/12-message-text.md new file mode 100644 index 00000000..7af1ef93 --- /dev/null +++ b/doc/roadmap/12-message-text.md @@ -0,0 +1,127 @@ +# Design: Engine message text without firebird.msg + +Roadmap milestone 12 (`doc/WireProtocol.md`). + +**Status: implemented** — `generate_messages.py` + the committed +`FBWireMessages.pas` (1682 messages, ~200KB of binary), the +`FormatWireStatus` formatter shared by `EIBInterBaseError` and the +protocol exception, and offline WireTest assertions (171 tests). +Findings against the plan: + +* The message source is no longer `messages2.sql`: modern Firebird + keeps the message database in `src/include/firebird/impl/msg/*.h` as + `FB_IMPL_MSG` entries - facility, number, symbol, SQLCODE, SQLSTATE + and text in one place, which also let the table carry each code's + SQLCODE for free. +* JRD, DSQL and DYN were not enough: the `At line @1, column @2` + companion of every DSQL error lives in facility 13 (SQLERR), which + also holds the per SQLCODE interpretation texts that + `isc_sql_interprete` looks up at `1000 + sqlcode` (facility 14 for + warnings). With those two facilities included, the wire status object + implements `Getsqlcode` (the `gds__sqlcode` rules: an `isc_sqlerr` + item's number argument wins outright, else the first item's mapping) + and `GetSQLMessage` client side, so the full fbclient error shape - + SQLCODE line and all - appears, not just the message text. The base + class's SQLCODE methods became virtual to allow it. +* Parity testing against the 3.0 provider on the same server exposed + two behavioural gaps beyond message text, both closed: prepare errors + now append ` When Executing: ` as the other providers do, and + the stale reference checks (`ibxeInterfaceOutofDate` when a prepared + statement is used across a transaction restart) were missing from the + wire statement entirely. +* Test 16's error output is now identical to fbclient on the same + server, except the invalid-server-name section the test skips for the + wire provider by design. The suite reference log is regenerated: its + numeric `Engine Code:` fallbacks give way to the real text + everywhere. + +## Goal + +Close the one visible difference from the stock providers: errors whose +text the server does not interpret currently read +`Firebird Error Code: 335544351` instead of +`unsuccessful metadata update`. After this milestone the wire provider +formats the same text `fbclient` would, with no file dependency. + +## Current state + +* `TFBWireStatus.GetIBMessage` (`FBWireClientAPI.pas`) returns the + concatenated server-supplied strings (`isc_arg_string` / + `isc_arg_interpreted` / `isc_arg_sql_state` items are folded into + `FMessage` by `SetFromWireStatus`), falling back to + `Firebird Error Code: %d` when the server sent none. +* A second, independent fallback lives in + `EFBWireProtocolError.CreateFromStatus` (`FBWireProtocol.pas`): + `Engine Code: %d`. The two should converge on one formatter. +* Most engine errors *do* arrive with interpreted text; the gap is the + subset where the server expects the client to format the message from + its own `firebird.msg`, substituting the status vector's string/number + arguments into `@1`/`@2`/`@3` placeholders. + +## The two options, decided + +The roadmap names two routes: read `firebird.msg` when present, or +generate a Pascal table at build time. **The generated table is the +plan**: it keeps the provider's defining property (the binary is the +whole dependency), works on machines that have never seen a Firebird +install, and is testable offline. A `firebird.msg` reader can be added +later as an override; it is not needed for parity in practice. + +## Design + +### Generation + +A build-time script (`client/wire/generate_messages.sh` + +`mkmsgs.pas`-style formatter, checked in with its output) consumes +Firebird's message source (`src/msgs/messages2.sql` / the `msg.fdb` +facility numbers in the Firebird tree — pinned to a tagged Firebird +release, recorded in the generated header) and emits +`client/wire/FBWireMessages.pas`: + +```pascal +function FindEngineMessage(aCode: cardinal; out aFormat: AnsiString): boolean; +``` + +* Table restricted to facility 0 (JRD — the `isc_` engine codes) plus + DSQL and DYN facilities: the ones a server round trip can produce. + That is a few thousand entries; as flat `const` arrays (sorted codes + + one string table) it compiles to a few hundred KB, which the roadmap + already accepts. Binary search at runtime. +* The generated file is committed, so builders never run the generator; + regenerating is a maintenance task per Firebird release. + +### Formatting + +`TFBWireStatus` currently throws away the *positional* relationship +between a `isc_arg_gds` item and its following string/number arguments +(strings go straight into `FMessage`). To format like `fbclient`: + +* `SetFromWireStatus` keeps, per `isc_arg_gds` item, its trailing + argument list (strings and numbers, in order) in a parallel structure + (the C-style vector still cannot hold the string pointers — that + constraint stands). +* `GetIBMessage` walks the gds items: look up the format string, + substitute `@n` parameters (`@1` = first argument...; number arguments + rendered decimal), join multiple gds items with the existing + `LineEnding + '-'` convention. Server-interpreted items + (`isc_arg_interpreted`) are used verbatim as today. Unknown code and + no server text → the current numeric fallback, which also becomes the + single shared formatter used by `EFBWireProtocolError`. + +This reproduces `fb_interpret`'s output shape, which is what the +reference logs contain — making this milestone a prerequisite for a +clean suite diff (milestone 1's wire reference log shrinks once this +lands; sequence flexibly, the two only touch at the log). + +## Acceptance + +* Offline `WireTest` section (no server needed): assert + `FindEngineMessage(335544351)` yields `unsuccessful metadata update`, + and a parameterised case, e.g. 335544343 formatting `@1` substitution, + matches `fbclient` output captured in the test as a literal. +* Live: provoke `isc_no_meta_update` (the suite's drop-table case + already does) and a syntax error, and diff the `EIBInterBaseError` + message text against the 3.0 provider on the same server — identical + modulo the client-library-path banner. +* Binary size delta of `WireTest` recorded in the PR (expected: a few + hundred KB), confirming the roadmap's estimate. diff --git a/fbintf.dpk b/fbintf.dpk index 834b5bfd..c1f35978 100644 --- a/fbintf.dpk +++ b/fbintf.dpk @@ -69,6 +69,24 @@ contains FB30TimeZoneServices in 'client\3.0\FB30TimeZoneServices.pas', FBClientLib in 'client\FBClientLib.pas', FBNumeric in 'client\FBNumeric.pas', - FirebirdOOAPI in 'client\3.0\firebird\FirebirdOOAPI.pas'; + FirebirdOOAPI in 'client\3.0\firebird\FirebirdOOAPI.pas', + FBSDL in 'client\FBSDL.pas', + FBWireBigInt in 'client\wire\FBWireBigInt.pas', + FBWireCrypto in 'client\wire\FBWireCrypto.pas', + FBWireSRP in 'client\wire\FBWireSRP.pas', + FBWireConst in 'client\wire\FBWireConst.pas', + FBWireStream in 'client\wire\FBWireStream.pas', + FBWireMessage in 'client\wire\FBWireMessage.pas', + FBWireDescribe in 'client\wire\FBWireDescribe.pas', + FBWireProtocol in 'client\wire\FBWireProtocol.pas', + FBWireClientAPI in 'client\wire\FBWireClientAPI.pas', + FBWireAttachment in 'client\wire\FBWireAttachment.pas', + FBWireTransaction in 'client\wire\FBWireTransaction.pas', + FBWireStatement in 'client\wire\FBWireStatement.pas', + FBWireBlob in 'client\wire\FBWireBlob.pas', + FBWireEvents in 'client\wire\FBWireEvents.pas', + FBWireArray in 'client\wire\FBWireArray.pas', + FBWireServices in 'client\wire\FBWireServices.pas', + FBWireMessages in 'client\wire\FBWireMessages.pas'; end. diff --git a/fbintf.dproj b/fbintf.dproj index b5641a2e..035c6c04 100644 --- a/fbintf.dproj +++ b/fbintf.dproj @@ -188,6 +188,24 @@ + + + + + + + + + + + + + + + + + + Base diff --git a/fbintf.lpk b/fbintf.lpk index a5688cf7..34f63f71 100644 --- a/fbintf.lpk +++ b/fbintf.lpk @@ -7,7 +7,7 @@ - + @@ -52,7 +52,7 @@ Two interface implementations are provided. One is for the new Firebird 3 Client * Contributor(s): ______________________________________. * "/> - + @@ -206,6 +206,79 @@ Two interface implementations are provided. One is for the new Firebird 3 Client + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fbintf.pas b/fbintf.pas index b703b332..bb111201 100644 --- a/fbintf.pas +++ b/fbintf.pas @@ -14,7 +14,9 @@ interface FBSQLData, FB30ClientAPI, FB30Attachment, FB30Transaction, FBTransaction, FB30Blob, FB30Array, FB30Statement, FB30Events, FB30Services, FBEvents, FBBlob, FBAttachment, FBStatement, FBServices, FBClientLib, FBNumeric, - FB30TimeZoneServices, IBHeader, FirebirdOOAPI; + FB30TimeZoneServices, IBHeader, FirebirdOOAPI, + FBWireBigInt, FBWireCrypto, FBWireSRP, FBWireStream, FBWireConst, FBWireMessage, FBWireDescribe, FBWireProtocol, FBWireClientAPI, FBWireAttachment, FBWireTransaction, FBWireStatement, FBWireBlob, FBWireEvents, FBWireArray, FBWireServices, FBWireMessages + ; implementation diff --git a/testsuite/FBWirereference.log b/testsuite/FBWirereference.log new file mode 100644 index 00000000..2d1788c1 --- /dev/null +++ b/testsuite/FBWirereference.log @@ -0,0 +1,4572 @@ +Firebird API Test Suite +Copyright MWA Software 2016-2021 + +Starting Tests +Client API Version = 5.0 +Firebird Environment Variable = +Provider = pure Pascal wire protocol, no client library +Running Test 1: Create and Drop a Database +Creating a Database with empty parameters +Create Database fails (as expected): Engine Code: 0 +Firebird Error Code: 0 +Creating a Database using an SQL Statement +Database ID = 3 FB = /tmp/fbintf-testsuite/testsuite1.fdb SN = 81c2762b4b7a +SQL Dialect = 3 +DB Connect String = inet://localhost//tmp/fbintf-testsuite/testsuite1.fdb +DB Charset ID = 0 +DB SQL Dialect = 3 +DB Remote Protocol = TCPv4 +DB ODS Major Version = 14 +DB ODS Minor Version = 0 +User Authentication Method = Srp256 +Firebird Library Path = none (wire protocol provider) +DB Client Implementation Version = 5.0 +DPB: Item Count = 3 + isc_dpb_user_name = SYSDBA + isc_dpb_password = masterkey + isc_dpb_set_db_sql_dialect = 3 + +Firebird Server Version Info +Firebird wire protocol version 20, Srp256 authentication, ChaCha64 wire encryption + +Dropping Database +Creating a Database with a DPD +DB Connect String = inet://localhost//tmp/fbintf-testsuite/testsuite1.fdb +DB Charset ID = 4 +DB SQL Dialect = 3 +DB Remote Protocol = TCPv4 +DB ODS Major Version = 14 +DB ODS Minor Version = 0 +User Authentication Method = Srp256 +Firebird Library Path = none (wire protocol provider) +DB Client Implementation Version = 5.0 +Dropping Database +DPB: Item Count = 4 + isc_dpb_user_name = SYSDBA + isc_dpb_password = masterkey + isc_dpb_lc_ctype = UTF8 + isc_dpb_set_db_sql_dialect = 3 + +Creating a Database with a DPD +Database ID = 3 FB = /tmp/fbintf-testsuite/testsuite1.fdb SN = 81c2762b4b7a +ODS major = 14 +ODS minor = 0 +Attachment ID = 3 +DB Connect String = inet://localhost//tmp/fbintf-testsuite/testsuite1.fdb +DB Charset ID = 4 +DB SQL Dialect = 3 +DB Remote Protocol = TCPv4 +DB ODS Major Version = 14 +DB ODS Minor Version = 0 +User Authentication Method = Srp256 +Firebird Library Path = none (wire protocol provider) +DB Client Implementation Version = 5.0 +TPB: Item Count = 3 + isc_tpb_read + isc_tpb_concurrency + isc_tpb_lock_timeout = 1 + +RDB$DESCRIPTION = +RDB$RELATION_ID = 0 +RDB$SECURITY_CLASS = SQL$DATABASE +RDB$CHARACTER_SET_NAME = NONE +RDB$LINGER = +RDB$SQL_SECURITY = +RDB$CHARACTER_SET_SCHEMA_NAME = SYSTEM +Dropping Database + + +------------------------------------------------------ +Running Test 2: Open the employee database and run a query +Open Database fails Engine Code: 335544721 +Unsupported authentication plugin "Legacy_Auth" +Opening inet://localhost/employee +Database Open, SQL Dialect = 3 +TPB: Item Count = 3 + isc_tpb_read + isc_tpb_nowait + isc_tpb_concurrency + +Metadata +SQLType =SQL_SHORT +sub type = 0 +Table = EMPLOYEE +Owner = SYSDBA +Column Name = EMP_NO +Alias Name = EMP_NO +Field Name = EMP_NO +Scale = 0 +Charset id = 0 +Not Null +Size = 2 + +SQLType =SQL_VARYING +sub type = 0 +Table = EMPLOYEE +Owner = SYSDBA +Column Name = FIRST_NAME +Alias Name = FIRST_NAME +Field Name = FIRST_NAME +Scale = 0 +Charset id = 0 +Not Null +Size = 15 + +SQLType =SQL_VARYING +sub type = 0 +Table = EMPLOYEE +Owner = SYSDBA +Column Name = LAST_NAME +Alias Name = LAST_NAME +Field Name = LAST_NAME +Scale = 0 +Charset id = 0 +Not Null +Size = 20 + +SQLType =SQL_VARYING +sub type = 0 +Table = EMPLOYEE +Owner = SYSDBA +Column Name = PHONE_EXT +Alias Name = PHONE_EXT +Field Name = PHONE_EXT +Scale = 0 +Charset id = 0 +Nullable +Size = 4 + +SQLType =SQL_TIMESTAMP +sub type = 0 +Table = EMPLOYEE +Owner = SYSDBA +Column Name = HIRE_DATE +Alias Name = HIRE_DATE +Field Name = HIRE_DATE +Scale = 0 +Charset id = 0 +Not Null +Size = 8 + +SQLType =SQL_TEXT +sub type = 0 +Table = EMPLOYEE +Owner = SYSDBA +Column Name = DEPT_NO +Alias Name = DEPT_NO +Field Name = DEPT_NO +Scale = 0 +Charset id = 0 +Not Null +Size = 3 + +SQLType =SQL_VARYING +sub type = 0 +Table = EMPLOYEE +Owner = SYSDBA +Column Name = JOB_CODE +Alias Name = JOB_CODE +Field Name = JOB_CODE +Scale = 0 +Charset id = 0 +Not Null +Size = 5 + +SQLType =SQL_SHORT +sub type = 0 +Table = EMPLOYEE +Owner = SYSDBA +Column Name = JOB_GRADE +Alias Name = JOB_GRADE +Field Name = JOB_GRADE +Scale = 0 +Charset id = 0 +Not Null +Size = 2 + +SQLType =SQL_VARYING +sub type = 0 +Table = EMPLOYEE +Owner = SYSDBA +Column Name = JOB_COUNTRY +Alias Name = JOB_COUNTRY +Field Name = JOB_COUNTRY +Scale = 0 +Charset id = 0 +Not Null +Size = 15 + +SQLType =SQL_INT64 +sub type = 1 +Table = EMPLOYEE +Owner = SYSDBA +Column Name = SALARY +Alias Name = SALARY +Field Name = SALARY +Scale = -2 +Charset id = 0 +Not Null +Size = 8 + +SQLType =SQL_VARYING +sub type = 0 +Table = EMPLOYEE +Owner = SYSDBA +Column Name = FULL_NAME +Alias Name = FULL_NAME +Field Name = FULL_NAME +Scale = 0 +Charset id = 0 +Nullable +Size = 37 + +Plan = PLAN ("PUBLIC"."EMPLOYEE" NATURAL) +-- SQL style inline comment +/* this is a comment */ Select First 3 * from EMPLOYEE + +EMP_NO = 2 +FIRST_NAME = Robert +LAST_NAME = Nelson +PHONE_EXT = 250 +HIRE_DATE = 1988/12/28 00:00:00.0000 +DEPT_NO = 600 +JOB_CODE = VP +JOB_GRADE = 2 +JOB_COUNTRY = USA +SALARY = 105,900.00 +FULL_NAME = Nelson, Robert +EMP_NO = 4 +FIRST_NAME = Bruce +LAST_NAME = Young +PHONE_EXT = 233 +HIRE_DATE = 1988/12/28 00:00:00.0000 +DEPT_NO = 621 +JOB_CODE = Eng +JOB_GRADE = 2 +JOB_COUNTRY = USA +SALARY = 97,500.00 +FULL_NAME = Young, Bruce +EMP_NO = 5 +FIRST_NAME = Kim +LAST_NAME = Lambert +PHONE_EXT = 22 +HIRE_DATE = 1989/02/06 00:00:00.0000 +DEPT_NO = 130 +JOB_CODE = Eng +JOB_GRADE = 2 +JOB_COUNTRY = USA +SALARY = 102,750.00 +FULL_NAME = Lambert, Kim + +Select * from EMPLOYEE Where EMP_NO = ? +SQL Params +SQLType =SQL_SHORT +sub type = 0 +Field Name = +Scale = 0 +Charset id = 0 +Nullable +Size = 2 + +EMP_NO = 8 +FIRST_NAME = Leslie +LAST_NAME = Johnson +PHONE_EXT = 410 +HIRE_DATE = 1989/04/05 00:00:00.0000 +DEPT_NO = 180 +JOB_CODE = Mktg +JOB_GRADE = 3 +JOB_COUNTRY = USA +SALARY = 64,635.00 +FULL_NAME = Johnson, Leslie + +With param names +Select * from EMPLOYEE Where EMP_NO = :EMP_NO +SQL Params +SQLType =SQL_SHORT +sub type = 0 +Field Name = EMP_NO +Scale = 0 +Charset id = 0 +Nullable +Size = 2 + +Results for Cursor: Test Cursor +EMP_NO = 8 +FIRST_NAME = Leslie +LAST_NAME = Johnson +PHONE_EXT = 410 +HIRE_DATE = 1989/04/05 00:00:00.0000 +DEPT_NO = 180 +JOB_CODE = Mktg +JOB_GRADE = 3 +JOB_COUNTRY = USA +SALARY = 64,635.00 +FULL_NAME = Johnson, Leslie + +Scollable Cursors +DB Connect String = inet://localhost/employee +DB Charset ID = 4 +DB SQL Dialect = 3 +DB Remote Protocol = TCPv4 +DB ODS Major Version = 14 +DB ODS Minor Version = 0 +User Authentication Method = Srp256 +Firebird Library Path = none (wire protocol provider) +DB Client Implementation Version = 5.0 +Do Fetch Next: +EMP_NO = 2 +FIRST_NAME = Robert +LAST_NAME = Nelson +PHONE_EXT = 250 +HIRE_DATE = 1988/12/28 00:00:00.0000 +DEPT_NO = 600 +JOB_CODE = VP +JOB_GRADE = 2 +JOB_COUNTRY = USA +SALARY = 105,900.00 +FULL_NAME = Nelson, Robert +Do Fetch Last: +EMP_NO = 145 +FIRST_NAME = Mark +LAST_NAME = Guckenheimer +PHONE_EXT = 221 +HIRE_DATE = 1994/05/02 00:00:00.0000 +DEPT_NO = 622 +JOB_CODE = Eng +JOB_GRADE = 5 +JOB_COUNTRY = USA +SALARY = 32,000.00 +FULL_NAME = Guckenheimer, Mark +Do Fetch Prior: +EMP_NO = 144 +FIRST_NAME = John +LAST_NAME = Montgomery +PHONE_EXT = 820 +HIRE_DATE = 1994/03/30 00:00:00.0000 +DEPT_NO = 672 +JOB_CODE = Eng +JOB_GRADE = 5 +JOB_COUNTRY = USA +SALARY = 35,000.00 +FULL_NAME = Montgomery, John +Do Fetch First: +EMP_NO = 2 +FIRST_NAME = Robert +LAST_NAME = Nelson +PHONE_EXT = 250 +HIRE_DATE = 1988/12/28 00:00:00.0000 +DEPT_NO = 600 +JOB_CODE = VP +JOB_GRADE = 2 +JOB_COUNTRY = USA +SALARY = 105,900.00 +FULL_NAME = Nelson, Robert +Do Fetch Abs 8 : +EMP_NO = 14 +FIRST_NAME = Stewart +LAST_NAME = Hall +PHONE_EXT = 227 +HIRE_DATE = 1990/06/04 00:00:00.0000 +DEPT_NO = 900 +JOB_CODE = Finan +JOB_GRADE = 3 +JOB_COUNTRY = USA +SALARY = 69,482.63 +FULL_NAME = Hall, Stewart +Do Fetch Relative -2 : +EMP_NO = 11 +FIRST_NAME = K. J. +LAST_NAME = Weston +PHONE_EXT = 34 +HIRE_DATE = 1990/01/17 00:00:00.0000 +DEPT_NO = 130 +JOB_CODE = SRep +JOB_GRADE = 4 +JOB_COUNTRY = USA +SALARY = 86,292.94 +FULL_NAME = Weston, K. J. +Do Fetch beyond EOF : +Fetch returned false +Now open the employee database as a local database +Skipping local database test - not supported by provider + + +------------------------------------------------------ +Running Test 3: ad hoc queries +Opening inet://localhost/employee +Database Open +Employee Count = 42 +Transaction ID = 61 +Transaction is Read/Write +Transaction Database Path = employee +Transaction ID = 0 +Employee Count = 41 +Employee Count = 42 +Employee Count = 41 +Employee Count = 42 +Constrained Employee Count = 3 +"Johnson" Employee Count = 2 +"Yanowski" Employee Count = 1 + + +------------------------------------------------------ +Running Test 4: Update, Insert and Delete Queries +Opening inet://localhost/employee +Database Open +Firebird wire protocol version 20, Srp256 authentication, ChaCha64 wire encryption +Select Count = 1 InsertCount = 0 UpdateCount = 1 DeleteCount = 0 +EMP_NO = 8 +FIRST_NAME = Leslie +LAST_NAME = Johnson +PHONE_EXT = 410 +HIRE_DATE = 1989/04/05 00:00:00.0000 +DEPT_NO = 180 +JOB_CODE = Mktg +JOB_GRADE = 3 +JOB_COUNTRY = USA +SALARY = 64,635.00 +FULL_NAME = Johnson, Leslie + +Select Count = 0 InsertCount = 1 UpdateCount = 0 DeleteCount = 0 +Relation Name = EMPLOYEE +Metadata +SQLType =SQL_SHORT +sub type = 0 +Table = EMPLOYEE +Owner = SYSDBA +Column Name = EMP_NO +Alias Name = EMP_NO +Field Name = EMP_NO +Scale = 0 +Charset id = 0 +Not Null +Size = 2 + +SQLType =SQL_VARYING +sub type = 0 +Table = EMPLOYEE +Owner = SYSDBA +Column Name = FIRST_NAME +Alias Name = FIRST_NAME +Field Name = FIRST_NAME +Scale = 0 +Charset id = 0 +Not Null +Size = 15 + +SQLType =SQL_VARYING +sub type = 0 +Table = EMPLOYEE +Owner = SYSDBA +Column Name = LAST_NAME +Alias Name = LAST_NAME +Field Name = LAST_NAME +Scale = 0 +Charset id = 0 +Not Null +Size = 20 + +SQLType =SQL_VARYING +sub type = 0 +Table = EMPLOYEE +Owner = SYSDBA +Column Name = PHONE_EXT +Alias Name = PHONE_EXT +Field Name = PHONE_EXT +Scale = 0 +Charset id = 0 +Nullable +Size = 4 + +SQLType =SQL_TIMESTAMP +sub type = 0 +Table = EMPLOYEE +Owner = SYSDBA +Column Name = HIRE_DATE +Alias Name = HIRE_DATE +Field Name = HIRE_DATE +Scale = 0 +Charset id = 0 +Not Null +Size = 8 + +SQLType =SQL_TEXT +sub type = 0 +Table = EMPLOYEE +Owner = SYSDBA +Column Name = DEPT_NO +Alias Name = DEPT_NO +Field Name = DEPT_NO +Scale = 0 +Charset id = 0 +Not Null +Size = 3 + +SQLType =SQL_VARYING +sub type = 0 +Table = EMPLOYEE +Owner = SYSDBA +Column Name = JOB_CODE +Alias Name = JOB_CODE +Field Name = JOB_CODE +Scale = 0 +Charset id = 0 +Not Null +Size = 5 + +SQLType =SQL_SHORT +sub type = 0 +Table = EMPLOYEE +Owner = SYSDBA +Column Name = JOB_GRADE +Alias Name = JOB_GRADE +Field Name = JOB_GRADE +Scale = 0 +Charset id = 0 +Not Null +Size = 2 + +SQLType =SQL_VARYING +sub type = 0 +Table = EMPLOYEE +Owner = SYSDBA +Column Name = JOB_COUNTRY +Alias Name = JOB_COUNTRY +Field Name = JOB_COUNTRY +Scale = 0 +Charset id = 0 +Not Null +Size = 15 + +SQLType =SQL_INT64 +sub type = 1 +Table = EMPLOYEE +Owner = SYSDBA +Column Name = SALARY +Alias Name = SALARY +Field Name = SALARY +Scale = -2 +Charset id = 0 +Not Null +Size = 8 + +SQLType =SQL_VARYING +sub type = 0 +Table = EMPLOYEE +Owner = SYSDBA +Column Name = FULL_NAME +Alias Name = FULL_NAME +Field Name = FULL_NAME +Scale = 0 +Charset id = 0 +Nullable +Size = 37 + +EMP_NO = 500 +FIRST_NAME = John +LAST_NAME = Doe +PHONE_EXT = +HIRE_DATE = 2015/04/01 00:00:00.0000 +DEPT_NO = 600 +JOB_CODE = Eng +JOB_GRADE = 4 +JOB_COUNTRY = England +SALARY = 41,000.89 +FULL_NAME = Doe, John + +Select Count = 0 InsertCount = 1 UpdateCount = 0 DeleteCount = 0 +EMP_NO = 501 +FIRST_NAME = Major +LAST_NAME = Major +PHONE_EXT = +HIRE_DATE = 2015/04/01 00:00:00.0000 +DEPT_NO = 600 +JOB_CODE = Eng +JOB_GRADE = 4 +JOB_COUNTRY = England +SALARY = 40,000.59 +FULL_NAME = Major, Major + +Now Delete the rows +Select Count = 1 InsertCount = 0 UpdateCount = 0 DeleteCount = 1 +Select Count = 1 InsertCount = 0 UpdateCount = 0 DeleteCount = 1 +Inserting +Select Count = 0 InsertCount = 1 UpdateCount = 0 DeleteCount = 0 +EMP_NO = 500 +FIRST_NAME = Jane +LAST_NAME = Doe +PHONE_EXT = NULL +HIRE_DATE = 2015/04/01 00:00:00.0000 +DEPT_NO = 600 +JOB_CODE = Eng +JOB_GRADE = 4 +JOB_COUNTRY = England +SALARY = 41,000.89 +FULL_NAME = Doe, Jane + +Employee Count = 43 +Updating +Select Count = 1 InsertCount = 0 UpdateCount = 0 DeleteCount = 0 +Prepare Query again + +EMP_NO = 500 +FIRST_NAME = Jayne's +LAST_NAME = Doe +PHONE_EXT = NULL +HIRE_DATE = 2015/04/01 00:00:00.0000 +DEPT_NO = 600 +JOB_CODE = Eng +JOB_GRADE = 4 +JOB_COUNTRY = England +SALARY = 41,000.89 +FULL_NAME = Doe, Jayne's + +Prepare Query again with a different transaction + +EMP_NO = 8 +FIRST_NAME = Leslie +LAST_NAME = Johnson +PHONE_EXT = 410 +HIRE_DATE = 1989/04/05 00:00:00.0000 +DEPT_NO = 180 +JOB_CODE = Mktg +JOB_GRADE = 3 +JOB_COUNTRY = USA +SALARY = 64,635.00 +FULL_NAME = Johnson, Leslie + +Open Cursor with a different transaction + +EMP_NO = 8 +FIRST_NAME = Leslie +LAST_NAME = Johnson +PHONE_EXT = 410 +HIRE_DATE = 1989/04/05 00:00:00.0000 +DEPT_NO = 180 +JOB_CODE = Mktg +JOB_GRADE = 3 +JOB_COUNTRY = USA +SALARY = 64,635.00 +FULL_NAME = Johnson, Leslie +Same Statement - updated params +EMP_NO = 9 +FIRST_NAME = Phil +LAST_NAME = Forest +PHONE_EXT = 229 +HIRE_DATE = 1989/04/17 00:00:00.0000 +DEPT_NO = 622 +JOB_CODE = Mngr +JOB_GRADE = 3 +JOB_COUNTRY = USA +SALARY = 75,060.00 +FULL_NAME = Forest, Phil + +Test using Execute Block +Select Count = 1 InsertCount = 0 UpdateCount = 1 DeleteCount = 0 +EMP_NO = 8 +FIRST_NAME = Leslie +LAST_NAME = Johnson +PHONE_EXT = 410 +HIRE_DATE = 2015/01/31 00:00:00.0000 +DEPT_NO = 180 +JOB_CODE = Mktg +JOB_GRADE = 3 +JOB_COUNTRY = USA +SALARY = 64,635.00 +FULL_NAME = Johnson, Leslie + + + +------------------------------------------------------ +Running Test 5: Update Returning and Activity Check +Opening inet://localhost/employee +Database Open +Database Closed +Database Open +Select Count = 1 InsertCount = 0 UpdateCount = 1 DeleteCount = 0 +Last Name = Johnson +EMP_NO = 8 +FIRST_NAME = Leslie +LAST_NAME = Johnson +PHONE_EXT = 410 +HIRE_DATE = 2016/01/31 00:00:00.0000 +DEPT_NO = 180 +JOB_CODE = Mktg +JOB_GRADE = 3 +JOB_COUNTRY = USA +SALARY = 64,635.00 +FULL_NAME = Johnson, Leslie + +Inserting +Full Name = Doe, John +Select Count = 0 InsertCount = 1 UpdateCount = 0 DeleteCount = 0 +Database Activity = FALSE +Transaction Activity = FALSE +Database Activity = FALSE +Transaction Activity = FALSE +Employee Count = 43 +Database Activity = FALSE +Transaction Activity = FALSE +Transaction Active +Transaction inactive + + +------------------------------------------------------ +Running Test 6: Blob Handling +Metadata +SQLType =SQL_TEXT +sub type = 4 +Table = RDB$CHARACTER_SETS +Owner = SYSDBA +Column Name = RDB$CHARACTER_SET_NAME +Alias Name = RDB$CHARACTER_SET_NAME +Field Name = RDB$CHARACTER_SET_NAME +Scale = 0 +Charset id = 4 +Nullable +Size = 252 + +SQLType =SQL_SHORT +sub type = 0 +Table = RDB$CHARACTER_SETS +Owner = SYSDBA +Column Name = RDB$CHARACTER_SET_ID +Alias Name = RDB$CHARACTER_SET_ID +Field Name = RDB$CHARACTER_SET_ID +Scale = 0 +Charset id = 0 +Nullable +Size = 2 + +RDB$CHARACTER_SET_NAME = NONE (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 0 +RDB$CHARACTER_SET_NAME = OCTETS (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 1 +RDB$CHARACTER_SET_NAME = ASCII (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 2 +RDB$CHARACTER_SET_NAME = UNICODE_FSS (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 3 +RDB$CHARACTER_SET_NAME = UTF8 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 4 +RDB$CHARACTER_SET_NAME = SJIS_0208 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 5 +RDB$CHARACTER_SET_NAME = EUCJ_0208 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 6 +RDB$CHARACTER_SET_NAME = DOS737 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 9 +RDB$CHARACTER_SET_NAME = DOS437 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 10 +RDB$CHARACTER_SET_NAME = DOS850 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 11 +RDB$CHARACTER_SET_NAME = DOS865 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 12 +RDB$CHARACTER_SET_NAME = DOS860 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 13 +RDB$CHARACTER_SET_NAME = DOS863 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 14 +RDB$CHARACTER_SET_NAME = DOS775 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 15 +RDB$CHARACTER_SET_NAME = DOS858 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 16 +RDB$CHARACTER_SET_NAME = DOS862 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 17 +RDB$CHARACTER_SET_NAME = DOS864 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 18 +RDB$CHARACTER_SET_NAME = NEXT (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 19 +RDB$CHARACTER_SET_NAME = ISO8859_1 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 21 +RDB$CHARACTER_SET_NAME = ISO8859_2 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 22 +RDB$CHARACTER_SET_NAME = ISO8859_3 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 23 +RDB$CHARACTER_SET_NAME = ISO8859_4 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 34 +RDB$CHARACTER_SET_NAME = ISO8859_5 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 35 +RDB$CHARACTER_SET_NAME = ISO8859_6 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 36 +RDB$CHARACTER_SET_NAME = ISO8859_7 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 37 +RDB$CHARACTER_SET_NAME = ISO8859_8 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 38 +RDB$CHARACTER_SET_NAME = ISO8859_9 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 39 +RDB$CHARACTER_SET_NAME = ISO8859_13 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 40 +RDB$CHARACTER_SET_NAME = KSC_5601 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 44 +RDB$CHARACTER_SET_NAME = DOS852 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 45 +RDB$CHARACTER_SET_NAME = DOS857 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 46 +RDB$CHARACTER_SET_NAME = DOS861 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 47 +RDB$CHARACTER_SET_NAME = DOS866 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 48 +RDB$CHARACTER_SET_NAME = DOS869 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 49 +RDB$CHARACTER_SET_NAME = CYRL (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 50 +RDB$CHARACTER_SET_NAME = WIN1250 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 51 +RDB$CHARACTER_SET_NAME = WIN1251 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 52 +RDB$CHARACTER_SET_NAME = WIN1252 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 53 +RDB$CHARACTER_SET_NAME = WIN1253 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 54 +RDB$CHARACTER_SET_NAME = WIN1254 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 55 +RDB$CHARACTER_SET_NAME = BIG_5 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 56 +RDB$CHARACTER_SET_NAME = GB_2312 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 57 +RDB$CHARACTER_SET_NAME = WIN1255 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 58 +RDB$CHARACTER_SET_NAME = WIN1256 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 59 +RDB$CHARACTER_SET_NAME = WIN1257 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 60 +RDB$CHARACTER_SET_NAME = KOI8R (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 63 +RDB$CHARACTER_SET_NAME = KOI8U (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 64 +RDB$CHARACTER_SET_NAME = WIN1258 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 65 +RDB$CHARACTER_SET_NAME = TIS620 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 66 +RDB$CHARACTER_SET_NAME = GBK (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 67 +RDB$CHARACTER_SET_NAME = CP943C (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 68 +RDB$CHARACTER_SET_NAME = GB18030 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 69 + +Metadata +SQLType =SQL_LONG +sub type = 0 +Table = TESTDATA +Owner = SYSDBA +Column Name = ROWID +Alias Name = ROWID +Field Name = ROWID +Scale = 0 +Charset id = 0 +Not Null +Size = 4 + +SQLType =SQL_LONG +sub type = 2 +Table = TESTDATA +Owner = SYSDBA +Column Name = FIXEDPOINT +Alias Name = FIXEDPOINT +Field Name = FIXEDPOINT +Scale = -2 +Charset id = 0 +Nullable +Size = 4 + +SQLType =SQL_DOUBLE +sub type = 0 +Table = TESTDATA +Owner = SYSDBA +Column Name = FLOATINGPOINT +Alias Name = FLOATINGPOINT +Field Name = FLOATINGPOINT +Scale = 0 +Charset id = 0 +Nullable +Size = 8 + +SQLType =SQL_VARYING +sub type = 4 +Table = TESTDATA +Owner = SYSDBA +Column Name = TITLE +Alias Name = TITLE +Field Name = TITLE +Scale = 0 +Charset id = 4 +Nullable +Size = 128 + +SQLType =SQL_BLOB +sub type = 1 +Table = TESTDATA +Owner = SYSDBA +Column Name = BLOBDATA +Alias Name = BLOBDATA +Field Name = BLOBDATA +Scale = 4 +Charset id = 0 +Nullable +Size = 8 + +Blob Meta Data +SQL SubType =1 +Table = TESTDATA +Column = BLOBDATA +CharSetID = 0 +Segment Size = 32000 + + +SQL Params +SQLType =SQL_LONG +sub type = 0 +Field Name = ROWID +Scale = 0 +Charset id = 0 +Nullable +Size = 4 + +SQLType =SQL_VARYING +sub type = 4 +Field Name = TITLE +Scale = 0 +Charset id = 4 +Nullable +Size = 128 + +SQLType =SQL_LONG +sub type = 2 +Field Name = FP +Scale = -2 +Charset id = 0 +Nullable +Size = 4 + +SQLType =SQL_DOUBLE +sub type = 0 +Field Name = DP +Scale = 0 +Charset id = 0 +Nullable +Size = 8 + +ROWID = 1 +FIXEDPOINT = 20.28 +FLOATINGPOINT = 3.14 +TITLE = Blob Test ©€ (Charset Id = 4 Codepage = 65001) +BLOBDATA = NULL + +SQL Params +SQLType =SQL_BLOB +sub type = 1 +Field Name = +Scale = 4 +Charset id = 0 +Nullable +Size = 8 + +SQLType =SQL_LONG +sub type = 0 +Field Name = +Scale = 0 +Charset id = 0 +Nullable +Size = 4 + +ROWID = 1 +FIXEDPOINT = 20.28 +FLOATINGPOINT = 3.14 +TITLE = Blob Test ©€ (Charset Id = 4 Codepage = 65001) +BLOBDATA (Charset Id = 0 Codepage = 0) + +To be or not to be-that is the question: +Whether 'tis nobler in the mind to suffer +The slings and arrows of outrageous fortune, +Or to take arms against a sea of troubles, +And, by opposing, end them. To die, to sleep- +No more-and by a sleep to say we end +The heartache and the thousand natural shocks +That flesh is heir to-'tis a consummation +Devoutly to be wished. To die, to sleep- +To sleep, perchance to dream. Aye, there's the rub, +For in that sleep of death what dreams may come, +When we have shuffled off this mortal coil, +Must give us pause. There's the respect +That makes calamity of so long life. +For who would bear the whips and scorns of time, +Th' oppressor's wrong, the proud man's contumely, +The pangs of despised love, the law’s delay, +The insolence of office, and the spurns +That patient merit of the unworthy takes, +When he himself might his quietus make +With a bare bodkin? Who would fardels bear, +To grunt and sweat under a weary life, +But that the dread of something after death, +The undiscovered country from whose bourn +No traveler returns, puzzles the will +And makes us rather bear those ills we have +Than fly to others that we know not of? +Thus conscience does make cowards of us all, +And thus the native hue of resolution +Is sicklied o'er with the pale cast of thought, +And enterprises of great pitch and moment, +With this regard their currents turn awry, +And lose the name of action.-Soft you now, +The fair Ophelia.-Nymph, in thy orisons +Be all my sins remembered + + +SQL Params +SQLType =SQL_LONG +sub type = 0 +Field Name = ROWID +Scale = 0 +Charset id = 0 +Nullable +Size = 4 + +SQLType =SQL_VARYING +sub type = 4 +Field Name = TITLE +Scale = 0 +Charset id = 4 +Nullable +Size = 128 + +SQLType =SQL_LONG +sub type = 2 +Field Name = FP +Scale = -2 +Charset id = 0 +Nullable +Size = 4 + +SQLType =SQL_DOUBLE +sub type = 0 +Field Name = DP +Scale = 0 +Charset id = 0 +Nullable +Size = 8 + +ROWID = 1 +FIXEDPOINT = 20.28 +FLOATINGPOINT = 3.14 +TITLE = Blob Test ©€ (Charset Id = 4 Codepage = 65001) +BLOBDATA (Charset Id = 0 Codepage = 0) + +To be or not to be-that is the question: +Whether 'tis nobler in the mind to suffer +The slings and arrows of outrageous fortune, +Or to take arms against a sea of troubles, +And, by opposing, end them. To die, to sleep- +No more-and by a sleep to say we end +The heartache and the thousand natural shocks +That flesh is heir to-'tis a consummation +Devoutly to be wished. To die, to sleep- +To sleep, perchance to dream. Aye, there's the rub, +For in that sleep of death what dreams may come, +When we have shuffled off this mortal coil, +Must give us pause. There's the respect +That makes calamity of so long life. +For who would bear the whips and scorns of time, +Th' oppressor's wrong, the proud man's contumely, +The pangs of despised love, the law’s delay, +The insolence of office, and the spurns +That patient merit of the unworthy takes, +When he himself might his quietus make +With a bare bodkin? Who would fardels bear, +To grunt and sweat under a weary life, +But that the dread of something after death, +The undiscovered country from whose bourn +No traveler returns, puzzles the will +And makes us rather bear those ills we have +Than fly to others that we know not of? +Thus conscience does make cowards of us all, +And thus the native hue of resolution +Is sicklied o'er with the pale cast of thought, +And enterprises of great pitch and moment, +With this regard their currents turn awry, +And lose the name of action.-Soft you now, +The fair Ophelia.-Nymph, in thy orisons +Be all my sins remembered + +ROWID = 2 +FIXEDPOINT = NULL +FLOATINGPOINT = NULL +TITLE = Blob Test ©€ (Charset Id = 4 Codepage = 65001) +BLOBDATA (Charset Id = 0 Codepage = 0) + +To be or not to be-that is the question: +Whether 'tis nobler in the mind to suffer +The slings and arrows of outrageous fortune, +Or to take arms against a sea of troubles, +And, by opposing, end them. To die, to sleep- +No more-and by a sleep to say we end +The heartache and the thousand natural shocks +That flesh is heir to-'tis a consummation +Devoutly to be wished. To die, to sleep- +To sleep, perchance to dream. Aye, there's the rub, +For in that sleep of death what dreams may come, +When we have shuffled off this mortal coil, +Must give us pause. There's the respect +That makes calamity of so long life. +For who would bear the whips and scorns of time, +Th' oppressor's wrong, the proud man's contumely, +The pangs of despised love, the law’s delay, +The insolence of office, and the spurns +That patient merit of the unworthy takes, +When he himself might his quietus make +With a bare bodkin? Who would fardels bear, +To grunt and sweat under a weary life, +But that the dread of something after death, +The undiscovered country from whose bourn +No traveler returns, puzzles the will +And makes us rather bear those ills we have +Than fly to others that we know not of? +Thus conscience does make cowards of us all, +And thus the native hue of resolution +Is sicklied o'er with the pale cast of thought, +And enterprises of great pitch and moment, +With this regard their currents turn awry, +And lose the name of action.-Soft you now, +The fair Ophelia.-Nymph, in thy orisons +Be all my sins remembered + + +Test text blob optimisation - short text followed by long +Params after prepare +SQL Params +SQLType =SQL_LONG +sub type = 0 +Field Name = +Scale = 0 +Charset id = 0 +Nullable +Size = 4 + +SQLType =SQL_BLOB +sub type = 1 +Field Name = +Scale = 4 +Charset id = 0 +Nullable +Size = 8 + +Params after setting values +SQL Params +SQLType =SQL_LONG +sub type = 0 +Field Name = +Scale = 0 +Charset id = 0 +Nullable +Size = 4 +Value = 3 + +SQLType =SQL_VARYING +sub type = 0 +Field Name = +Scale = 0 +Charset id = 0 +Nullable +Size = 80 +Value = The Quick Brown Fox Jumps over the Lazy Dog + +SQL Params +SQLType =SQL_LONG +sub type = 0 +Field Name = +Scale = 0 +Charset id = 0 +Nullable +Size = 4 +Value = 4 + +SQLType =SQL_BLOB +sub type = 1 +Field Name = +Scale = 0 +Charset id = 0 +Nullable +Size = 8 +Value = O Romeo, Romeo, wherefore art thou Romeo? Deny thy father and refuse thy name. Or if thou wilt not, be but sworn my love And I’ll no longer be a Capulet.‘Tis but thy name that is my enemy: Thou art thyself, though not a Montague. What’s Montague? It is nor hand nor foot Nor arm nor face nor any other part Belonging to a man. O be some other name. What’s in a name? That which we call a rose By any other name would smell as sweet; So Romeo would, were he not Romeo call’d, Retain that dear perfection which he owes Without that title. Romeo, doff thy name ,And for that name, which is no part of thee, Take all myself. + +Text Blob Optimisation Results +ROWID = 3 +FIXEDPOINT = NULL +FLOATINGPOINT = NULL +TITLE = NULL +BLOBDATA (Charset Id = 0 Codepage = 0) + +The Quick Brown Fox Jumps over the Lazy Dog +ROWID = 4 +FIXEDPOINT = NULL +FLOATINGPOINT = NULL +TITLE = NULL +BLOBDATA (Charset Id = 0 Codepage = 0) + +O Romeo, Romeo, wherefore art thou Romeo? Deny thy father and refuse thy name. Or if thou wilt not, be but sworn my love And I’ll no longer be a Capulet.‘Tis but thy name that is my enemy: Thou art thyself, though not a Montague. What’s Montague? It is nor hand nor foot Nor arm nor face nor any other part Belonging to a man. O be some other name. What’s in a name? That which we call a rose By any other name would smell as sweet; So Romeo would, were he not Romeo call’d, Retain that dear perfection which he owes Without that title. Romeo, doff thy name ,And for that name, which is no part of thee, Take all myself. + +Testing Blob as stored proc parameter +Metadata +SQLType =SQL_BLOB +sub type = 1 +Table = TESTPROC +Owner = SYSDBA +Column Name = BLOBDATA +Alias Name = BLOBDATA +Field Name = BLOBDATA +Scale = 4 +Charset id = 0 +Nullable +Size = 8 + +Blob Meta Data +SQL SubType =1 +Table = TESTPROC +Column = BLOBDATA +CharSetID = 0 +Segment Size = 32000 + + +BLOBDATA (Charset Id = 0 Codepage = 0) + +To be or not to be-that is the question: +Whether 'tis nobler in the mind to suffer +The slings and arrows of outrageous fortune, +Or to take arms against a sea of troubles, +And, by opposing, end them. To die, to sleep- +No more-and by a sleep to say we end +The heartache and the thousand natural shocks +That flesh is heir to-'tis a consummation +Devoutly to be wished. To die, to sleep- +To sleep, perchance to dream. Aye, there's the rub, +For in that sleep of death what dreams may come, +When we have shuffled off this mortal coil, +Must give us pause. There's the respect +That makes calamity of so long life. +For who would bear the whips and scorns of time, +Th' oppressor's wrong, the proud man's contumely, +The pangs of despised love, the law’s delay, +The insolence of office, and the spurns +That patient merit of the unworthy takes, +When he himself might his quietus make +With a bare bodkin? Who would fardels bear, +To grunt and sweat under a weary life, +But that the dread of something after death, +The undiscovered country from whose bourn +No traveler returns, puzzles the will +And makes us rather bear those ills we have +Than fly to others that we know not of? +Thus conscience does make cowards of us all, +And thus the native hue of resolution +Is sicklied o'er with the pale cast of thought, +And enterprises of great pitch and moment, +With this regard their currents turn awry, +And lose the name of action.-Soft you now, +The fair Ophelia.-Nymph, in thy orisons +Be all my sins remembered + +Metadata +SQLType =SQL_TEXT +sub type = 53 +Table = RDB$CHARACTER_SETS +Owner = SYSDBA +Column Name = RDB$CHARACTER_SET_NAME +Alias Name = RDB$CHARACTER_SET_NAME +Field Name = RDB$CHARACTER_SET_NAME +Scale = 0 +Charset id = 53 +Nullable +Size = 63 + +SQLType =SQL_SHORT +sub type = 0 +Table = RDB$CHARACTER_SETS +Owner = SYSDBA +Column Name = RDB$CHARACTER_SET_ID +Alias Name = RDB$CHARACTER_SET_ID +Field Name = RDB$CHARACTER_SET_ID +Scale = 0 +Charset id = 0 +Nullable +Size = 2 + +RDB$CHARACTER_SET_NAME = NONE (Charset Id = 53 Codepage = 1252) +RDB$CHARACTER_SET_ID = 0 +RDB$CHARACTER_SET_NAME = OCTETS (Charset Id = 53 Codepage = 1252) +RDB$CHARACTER_SET_ID = 1 +RDB$CHARACTER_SET_NAME = ASCII (Charset Id = 53 Codepage = 1252) +RDB$CHARACTER_SET_ID = 2 +RDB$CHARACTER_SET_NAME = UNICODE_FSS (Charset Id = 53 Codepage = 1252) +RDB$CHARACTER_SET_ID = 3 +RDB$CHARACTER_SET_NAME = UTF8 (Charset Id = 53 Codepage = 1252) +RDB$CHARACTER_SET_ID = 4 +RDB$CHARACTER_SET_NAME = SJIS_0208 (Charset Id = 53 Codepage = 1252) +RDB$CHARACTER_SET_ID = 5 +RDB$CHARACTER_SET_NAME = EUCJ_0208 (Charset Id = 53 Codepage = 1252) +RDB$CHARACTER_SET_ID = 6 +RDB$CHARACTER_SET_NAME = DOS737 (Charset Id = 53 Codepage = 1252) +RDB$CHARACTER_SET_ID = 9 +RDB$CHARACTER_SET_NAME = DOS437 (Charset Id = 53 Codepage = 1252) +RDB$CHARACTER_SET_ID = 10 +RDB$CHARACTER_SET_NAME = DOS850 (Charset Id = 53 Codepage = 1252) +RDB$CHARACTER_SET_ID = 11 +RDB$CHARACTER_SET_NAME = DOS865 (Charset Id = 53 Codepage = 1252) +RDB$CHARACTER_SET_ID = 12 +RDB$CHARACTER_SET_NAME = DOS860 (Charset Id = 53 Codepage = 1252) +RDB$CHARACTER_SET_ID = 13 +RDB$CHARACTER_SET_NAME = DOS863 (Charset Id = 53 Codepage = 1252) +RDB$CHARACTER_SET_ID = 14 +RDB$CHARACTER_SET_NAME = DOS775 (Charset Id = 53 Codepage = 1252) +RDB$CHARACTER_SET_ID = 15 +RDB$CHARACTER_SET_NAME = DOS858 (Charset Id = 53 Codepage = 1252) +RDB$CHARACTER_SET_ID = 16 +RDB$CHARACTER_SET_NAME = DOS862 (Charset Id = 53 Codepage = 1252) +RDB$CHARACTER_SET_ID = 17 +RDB$CHARACTER_SET_NAME = DOS864 (Charset Id = 53 Codepage = 1252) +RDB$CHARACTER_SET_ID = 18 +RDB$CHARACTER_SET_NAME = NEXT (Charset Id = 53 Codepage = 1252) +RDB$CHARACTER_SET_ID = 19 +RDB$CHARACTER_SET_NAME = ISO8859_1 (Charset Id = 53 Codepage = 1252) +RDB$CHARACTER_SET_ID = 21 +RDB$CHARACTER_SET_NAME = ISO8859_2 (Charset Id = 53 Codepage = 1252) +RDB$CHARACTER_SET_ID = 22 +RDB$CHARACTER_SET_NAME = ISO8859_3 (Charset Id = 53 Codepage = 1252) +RDB$CHARACTER_SET_ID = 23 +RDB$CHARACTER_SET_NAME = ISO8859_4 (Charset Id = 53 Codepage = 1252) +RDB$CHARACTER_SET_ID = 34 +RDB$CHARACTER_SET_NAME = ISO8859_5 (Charset Id = 53 Codepage = 1252) +RDB$CHARACTER_SET_ID = 35 +RDB$CHARACTER_SET_NAME = ISO8859_6 (Charset Id = 53 Codepage = 1252) +RDB$CHARACTER_SET_ID = 36 +RDB$CHARACTER_SET_NAME = ISO8859_7 (Charset Id = 53 Codepage = 1252) +RDB$CHARACTER_SET_ID = 37 +RDB$CHARACTER_SET_NAME = ISO8859_8 (Charset Id = 53 Codepage = 1252) +RDB$CHARACTER_SET_ID = 38 +RDB$CHARACTER_SET_NAME = ISO8859_9 (Charset Id = 53 Codepage = 1252) +RDB$CHARACTER_SET_ID = 39 +RDB$CHARACTER_SET_NAME = ISO8859_13 (Charset Id = 53 Codepage = 1252) +RDB$CHARACTER_SET_ID = 40 +RDB$CHARACTER_SET_NAME = KSC_5601 (Charset Id = 53 Codepage = 1252) +RDB$CHARACTER_SET_ID = 44 +RDB$CHARACTER_SET_NAME = DOS852 (Charset Id = 53 Codepage = 1252) +RDB$CHARACTER_SET_ID = 45 +RDB$CHARACTER_SET_NAME = DOS857 (Charset Id = 53 Codepage = 1252) +RDB$CHARACTER_SET_ID = 46 +RDB$CHARACTER_SET_NAME = DOS861 (Charset Id = 53 Codepage = 1252) +RDB$CHARACTER_SET_ID = 47 +RDB$CHARACTER_SET_NAME = DOS866 (Charset Id = 53 Codepage = 1252) +RDB$CHARACTER_SET_ID = 48 +RDB$CHARACTER_SET_NAME = DOS869 (Charset Id = 53 Codepage = 1252) +RDB$CHARACTER_SET_ID = 49 +RDB$CHARACTER_SET_NAME = CYRL (Charset Id = 53 Codepage = 1252) +RDB$CHARACTER_SET_ID = 50 +RDB$CHARACTER_SET_NAME = WIN1250 (Charset Id = 53 Codepage = 1252) +RDB$CHARACTER_SET_ID = 51 +RDB$CHARACTER_SET_NAME = WIN1251 (Charset Id = 53 Codepage = 1252) +RDB$CHARACTER_SET_ID = 52 +RDB$CHARACTER_SET_NAME = WIN1252 (Charset Id = 53 Codepage = 1252) +RDB$CHARACTER_SET_ID = 53 +RDB$CHARACTER_SET_NAME = WIN1253 (Charset Id = 53 Codepage = 1252) +RDB$CHARACTER_SET_ID = 54 +RDB$CHARACTER_SET_NAME = WIN1254 (Charset Id = 53 Codepage = 1252) +RDB$CHARACTER_SET_ID = 55 +RDB$CHARACTER_SET_NAME = BIG_5 (Charset Id = 53 Codepage = 1252) +RDB$CHARACTER_SET_ID = 56 +RDB$CHARACTER_SET_NAME = GB_2312 (Charset Id = 53 Codepage = 1252) +RDB$CHARACTER_SET_ID = 57 +RDB$CHARACTER_SET_NAME = WIN1255 (Charset Id = 53 Codepage = 1252) +RDB$CHARACTER_SET_ID = 58 +RDB$CHARACTER_SET_NAME = WIN1256 (Charset Id = 53 Codepage = 1252) +RDB$CHARACTER_SET_ID = 59 +RDB$CHARACTER_SET_NAME = WIN1257 (Charset Id = 53 Codepage = 1252) +RDB$CHARACTER_SET_ID = 60 +RDB$CHARACTER_SET_NAME = KOI8R (Charset Id = 53 Codepage = 1252) +RDB$CHARACTER_SET_ID = 63 +RDB$CHARACTER_SET_NAME = KOI8U (Charset Id = 53 Codepage = 1252) +RDB$CHARACTER_SET_ID = 64 +RDB$CHARACTER_SET_NAME = WIN1258 (Charset Id = 53 Codepage = 1252) +RDB$CHARACTER_SET_ID = 65 +RDB$CHARACTER_SET_NAME = TIS620 (Charset Id = 53 Codepage = 1252) +RDB$CHARACTER_SET_ID = 66 +RDB$CHARACTER_SET_NAME = GBK (Charset Id = 53 Codepage = 1252) +RDB$CHARACTER_SET_ID = 67 +RDB$CHARACTER_SET_NAME = CP943C (Charset Id = 53 Codepage = 1252) +RDB$CHARACTER_SET_ID = 68 +RDB$CHARACTER_SET_NAME = GB18030 (Charset Id = 53 Codepage = 1252) +RDB$CHARACTER_SET_ID = 69 + +Metadata +SQLType =SQL_LONG +sub type = 0 +Table = TESTDATA +Owner = SYSDBA +Column Name = ROWID +Alias Name = ROWID +Field Name = ROWID +Scale = 0 +Charset id = 0 +Not Null +Size = 4 + +SQLType =SQL_LONG +sub type = 2 +Table = TESTDATA +Owner = SYSDBA +Column Name = FIXEDPOINT +Alias Name = FIXEDPOINT +Field Name = FIXEDPOINT +Scale = -2 +Charset id = 0 +Nullable +Size = 4 + +SQLType =SQL_DOUBLE +sub type = 0 +Table = TESTDATA +Owner = SYSDBA +Column Name = FLOATINGPOINT +Alias Name = FLOATINGPOINT +Field Name = FLOATINGPOINT +Scale = 0 +Charset id = 0 +Nullable +Size = 8 + +SQLType =SQL_VARYING +sub type = 53 +Table = TESTDATA +Owner = SYSDBA +Column Name = TITLE +Alias Name = TITLE +Field Name = TITLE +Scale = 0 +Charset id = 53 +Nullable +Size = 32 + +SQLType =SQL_BLOB +sub type = 1 +Table = TESTDATA +Owner = SYSDBA +Column Name = BLOBDATA +Alias Name = BLOBDATA +Field Name = BLOBDATA +Scale = 53 +Charset id = 0 +Nullable +Size = 8 + +Blob Meta Data +SQL SubType =1 +Table = TESTDATA +Column = BLOBDATA +CharSetID = 0 +Segment Size = 32000 + + +SQL Params +SQLType =SQL_LONG +sub type = 0 +Field Name = ROWID +Scale = 0 +Charset id = 0 +Nullable +Size = 4 + +SQLType =SQL_VARYING +sub type = 53 +Field Name = TITLE +Scale = 0 +Charset id = 53 +Nullable +Size = 32 + +SQLType =SQL_LONG +sub type = 2 +Field Name = FP +Scale = -2 +Charset id = 0 +Nullable +Size = 4 + +SQLType =SQL_DOUBLE +sub type = 0 +Field Name = DP +Scale = 0 +Charset id = 0 +Nullable +Size = 8 + +ROWID = 1 +FIXEDPOINT = 20.28 +FLOATINGPOINT = 3.14 +TITLE = Blob Test ©€ (Charset Id = 53 Codepage = 1252) +BLOBDATA = NULL + +SQL Params +SQLType =SQL_BLOB +sub type = 1 +Field Name = +Scale = 53 +Charset id = 0 +Nullable +Size = 8 + +SQLType =SQL_LONG +sub type = 0 +Field Name = +Scale = 0 +Charset id = 0 +Nullable +Size = 4 + +ROWID = 1 +FIXEDPOINT = 20.28 +FLOATINGPOINT = 3.14 +TITLE = Blob Test ©€ (Charset Id = 53 Codepage = 1252) +BLOBDATA (Charset Id = 0 Codepage = 0) + +To be or not to be-that is the question: +Whether 'tis nobler in the mind to suffer +The slings and arrows of outrageous fortune, +Or to take arms against a sea of troubles, +And, by opposing, end them. To die, to sleep- +No more-and by a sleep to say we end +The heartache and the thousand natural shocks +That flesh is heir to-'tis a consummation +Devoutly to be wished. To die, to sleep- +To sleep, perchance to dream. Aye, there's the rub, +For in that sleep of death what dreams may come, +When we have shuffled off this mortal coil, +Must give us pause. There's the respect +That makes calamity of so long life. +For who would bear the whips and scorns of time, +Th' oppressor's wrong, the proud man's contumely, +The pangs of despised love, the law’s delay, +The insolence of office, and the spurns +That patient merit of the unworthy takes, +When he himself might his quietus make +With a bare bodkin? Who would fardels bear, +To grunt and sweat under a weary life, +But that the dread of something after death, +The undiscovered country from whose bourn +No traveler returns, puzzles the will +And makes us rather bear those ills we have +Than fly to others that we know not of? +Thus conscience does make cowards of us all, +And thus the native hue of resolution +Is sicklied o'er with the pale cast of thought, +And enterprises of great pitch and moment, +With this regard their currents turn awry, +And lose the name of action.-Soft you now, +The fair Ophelia.-Nymph, in thy orisons +Be all my sins remembered + + +SQL Params +SQLType =SQL_LONG +sub type = 0 +Field Name = ROWID +Scale = 0 +Charset id = 0 +Nullable +Size = 4 + +SQLType =SQL_VARYING +sub type = 53 +Field Name = TITLE +Scale = 0 +Charset id = 53 +Nullable +Size = 32 + +SQLType =SQL_LONG +sub type = 2 +Field Name = FP +Scale = -2 +Charset id = 0 +Nullable +Size = 4 + +SQLType =SQL_DOUBLE +sub type = 0 +Field Name = DP +Scale = 0 +Charset id = 0 +Nullable +Size = 8 + +ROWID = 1 +FIXEDPOINT = 20.28 +FLOATINGPOINT = 3.14 +TITLE = Blob Test ©€ (Charset Id = 53 Codepage = 1252) +BLOBDATA (Charset Id = 0 Codepage = 0) + +To be or not to be-that is the question: +Whether 'tis nobler in the mind to suffer +The slings and arrows of outrageous fortune, +Or to take arms against a sea of troubles, +And, by opposing, end them. To die, to sleep- +No more-and by a sleep to say we end +The heartache and the thousand natural shocks +That flesh is heir to-'tis a consummation +Devoutly to be wished. To die, to sleep- +To sleep, perchance to dream. Aye, there's the rub, +For in that sleep of death what dreams may come, +When we have shuffled off this mortal coil, +Must give us pause. There's the respect +That makes calamity of so long life. +For who would bear the whips and scorns of time, +Th' oppressor's wrong, the proud man's contumely, +The pangs of despised love, the law’s delay, +The insolence of office, and the spurns +That patient merit of the unworthy takes, +When he himself might his quietus make +With a bare bodkin? Who would fardels bear, +To grunt and sweat under a weary life, +But that the dread of something after death, +The undiscovered country from whose bourn +No traveler returns, puzzles the will +And makes us rather bear those ills we have +Than fly to others that we know not of? +Thus conscience does make cowards of us all, +And thus the native hue of resolution +Is sicklied o'er with the pale cast of thought, +And enterprises of great pitch and moment, +With this regard their currents turn awry, +And lose the name of action.-Soft you now, +The fair Ophelia.-Nymph, in thy orisons +Be all my sins remembered + +ROWID = 2 +FIXEDPOINT = NULL +FLOATINGPOINT = NULL +TITLE = Blob Test ©€ (Charset Id = 53 Codepage = 1252) +BLOBDATA (Charset Id = 0 Codepage = 0) + +To be or not to be-that is the question: +Whether 'tis nobler in the mind to suffer +The slings and arrows of outrageous fortune, +Or to take arms against a sea of troubles, +And, by opposing, end them. To die, to sleep- +No more-and by a sleep to say we end +The heartache and the thousand natural shocks +That flesh is heir to-'tis a consummation +Devoutly to be wished. To die, to sleep- +To sleep, perchance to dream. Aye, there's the rub, +For in that sleep of death what dreams may come, +When we have shuffled off this mortal coil, +Must give us pause. There's the respect +That makes calamity of so long life. +For who would bear the whips and scorns of time, +Th' oppressor's wrong, the proud man's contumely, +The pangs of despised love, the law’s delay, +The insolence of office, and the spurns +That patient merit of the unworthy takes, +When he himself might his quietus make +With a bare bodkin? Who would fardels bear, +To grunt and sweat under a weary life, +But that the dread of something after death, +The undiscovered country from whose bourn +No traveler returns, puzzles the will +And makes us rather bear those ills we have +Than fly to others that we know not of? +Thus conscience does make cowards of us all, +And thus the native hue of resolution +Is sicklied o'er with the pale cast of thought, +And enterprises of great pitch and moment, +With this regard their currents turn awry, +And lose the name of action.-Soft you now, +The fair Ophelia.-Nymph, in thy orisons +Be all my sins remembered + + +Test text blob optimisation - short text followed by long +Params after prepare +SQL Params +SQLType =SQL_LONG +sub type = 0 +Field Name = +Scale = 0 +Charset id = 0 +Nullable +Size = 4 + +SQLType =SQL_BLOB +sub type = 1 +Field Name = +Scale = 53 +Charset id = 0 +Nullable +Size = 8 + +Params after setting values +SQL Params +SQLType =SQL_LONG +sub type = 0 +Field Name = +Scale = 0 +Charset id = 0 +Nullable +Size = 4 +Value = 3 + +SQLType =SQL_VARYING +sub type = 0 +Field Name = +Scale = 0 +Charset id = 0 +Nullable +Size = 80 +Value = The Quick Brown Fox Jumps over the Lazy Dog + +SQL Params +SQLType =SQL_LONG +sub type = 0 +Field Name = +Scale = 0 +Charset id = 0 +Nullable +Size = 4 +Value = 4 + +SQLType =SQL_BLOB +sub type = 1 +Field Name = +Scale = 0 +Charset id = 0 +Nullable +Size = 8 +Value = O Romeo, Romeo, wherefore art thou Romeo? Deny thy father and refuse thy name. Or if thou wilt not, be but sworn my love And I’ll no longer be a Capulet.‘Tis but thy name that is my enemy: Thou art thyself, though not a Montague. What’s Montague? It is nor hand nor foot Nor arm nor face nor any other part Belonging to a man. O be some other name. What’s in a name? That which we call a rose By any other name would smell as sweet; So Romeo would, were he not Romeo call’d, Retain that dear perfection which he owes Without that title. Romeo, doff thy name ,And for that name, which is no part of thee, Take all myself. + +Text Blob Optimisation Results +ROWID = 3 +FIXEDPOINT = NULL +FLOATINGPOINT = NULL +TITLE = NULL +BLOBDATA (Charset Id = 0 Codepage = 0) + +The Quick Brown Fox Jumps over the Lazy Dog +ROWID = 4 +FIXEDPOINT = NULL +FLOATINGPOINT = NULL +TITLE = NULL +BLOBDATA (Charset Id = 0 Codepage = 0) + +O Romeo, Romeo, wherefore art thou Romeo? Deny thy father and refuse thy name. Or if thou wilt not, be but sworn my love And I’ll no longer be a Capulet.‘Tis but thy name that is my enemy: Thou art thyself, though not a Montague. What’s Montague? It is nor hand nor foot Nor arm nor face nor any other part Belonging to a man. O be some other name. What’s in a name? That which we call a rose By any other name would smell as sweet; So Romeo would, were he not Romeo call’d, Retain that dear perfection which he owes Without that title. Romeo, doff thy name ,And for that name, which is no part of thee, Take all myself. + +Testing Blob as stored proc parameter +Metadata +SQLType =SQL_BLOB +sub type = 1 +Table = TESTPROC +Owner = SYSDBA +Column Name = BLOBDATA +Alias Name = BLOBDATA +Field Name = BLOBDATA +Scale = 53 +Charset id = 0 +Nullable +Size = 8 + +Blob Meta Data +SQL SubType =1 +Table = TESTPROC +Column = BLOBDATA +CharSetID = 0 +Segment Size = 32000 + + +BLOBDATA (Charset Id = 0 Codepage = 0) + +To be or not to be-that is the question: +Whether 'tis nobler in the mind to suffer +The slings and arrows of outrageous fortune, +Or to take arms against a sea of troubles, +And, by opposing, end them. To die, to sleep- +No more-and by a sleep to say we end +The heartache and the thousand natural shocks +That flesh is heir to-'tis a consummation +Devoutly to be wished. To die, to sleep- +To sleep, perchance to dream. Aye, there's the rub, +For in that sleep of death what dreams may come, +When we have shuffled off this mortal coil, +Must give us pause. There's the respect +That makes calamity of so long life. +For who would bear the whips and scorns of time, +Th' oppressor's wrong, the proud man's contumely, +The pangs of despised love, the law’s delay, +The insolence of office, and the spurns +That patient merit of the unworthy takes, +When he himself might his quietus make +With a bare bodkin? Who would fardels bear, +To grunt and sweat under a weary life, +But that the dread of something after death, +The undiscovered country from whose bourn +No traveler returns, puzzles the will +And makes us rather bear those ills we have +Than fly to others that we know not of? +Thus conscience does make cowards of us all, +And thus the native hue of resolution +Is sicklied o'er with the pale cast of thought, +And enterprises of great pitch and moment, +With this regard their currents turn awry, +And lose the name of action.-Soft you now, +The fair Ophelia.-Nymph, in thy orisons +Be all my sins remembered + +Metadata +SQLType =SQL_TEXT +sub type = 4 +Table = RDB$CHARACTER_SETS +Owner = SYSDBA +Column Name = RDB$CHARACTER_SET_NAME +Alias Name = RDB$CHARACTER_SET_NAME +Field Name = RDB$CHARACTER_SET_NAME +Scale = 0 +Charset id = 4 +Nullable +Size = 252 + +SQLType =SQL_SHORT +sub type = 0 +Table = RDB$CHARACTER_SETS +Owner = SYSDBA +Column Name = RDB$CHARACTER_SET_ID +Alias Name = RDB$CHARACTER_SET_ID +Field Name = RDB$CHARACTER_SET_ID +Scale = 0 +Charset id = 0 +Nullable +Size = 2 + +RDB$CHARACTER_SET_NAME = NONE (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 0 +RDB$CHARACTER_SET_NAME = OCTETS (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 1 +RDB$CHARACTER_SET_NAME = ASCII (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 2 +RDB$CHARACTER_SET_NAME = UNICODE_FSS (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 3 +RDB$CHARACTER_SET_NAME = UTF8 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 4 +RDB$CHARACTER_SET_NAME = SJIS_0208 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 5 +RDB$CHARACTER_SET_NAME = EUCJ_0208 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 6 +RDB$CHARACTER_SET_NAME = DOS737 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 9 +RDB$CHARACTER_SET_NAME = DOS437 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 10 +RDB$CHARACTER_SET_NAME = DOS850 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 11 +RDB$CHARACTER_SET_NAME = DOS865 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 12 +RDB$CHARACTER_SET_NAME = DOS860 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 13 +RDB$CHARACTER_SET_NAME = DOS863 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 14 +RDB$CHARACTER_SET_NAME = DOS775 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 15 +RDB$CHARACTER_SET_NAME = DOS858 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 16 +RDB$CHARACTER_SET_NAME = DOS862 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 17 +RDB$CHARACTER_SET_NAME = DOS864 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 18 +RDB$CHARACTER_SET_NAME = NEXT (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 19 +RDB$CHARACTER_SET_NAME = ISO8859_1 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 21 +RDB$CHARACTER_SET_NAME = ISO8859_2 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 22 +RDB$CHARACTER_SET_NAME = ISO8859_3 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 23 +RDB$CHARACTER_SET_NAME = ISO8859_4 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 34 +RDB$CHARACTER_SET_NAME = ISO8859_5 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 35 +RDB$CHARACTER_SET_NAME = ISO8859_6 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 36 +RDB$CHARACTER_SET_NAME = ISO8859_7 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 37 +RDB$CHARACTER_SET_NAME = ISO8859_8 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 38 +RDB$CHARACTER_SET_NAME = ISO8859_9 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 39 +RDB$CHARACTER_SET_NAME = ISO8859_13 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 40 +RDB$CHARACTER_SET_NAME = KSC_5601 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 44 +RDB$CHARACTER_SET_NAME = DOS852 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 45 +RDB$CHARACTER_SET_NAME = DOS857 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 46 +RDB$CHARACTER_SET_NAME = DOS861 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 47 +RDB$CHARACTER_SET_NAME = DOS866 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 48 +RDB$CHARACTER_SET_NAME = DOS869 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 49 +RDB$CHARACTER_SET_NAME = CYRL (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 50 +RDB$CHARACTER_SET_NAME = WIN1250 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 51 +RDB$CHARACTER_SET_NAME = WIN1251 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 52 +RDB$CHARACTER_SET_NAME = WIN1252 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 53 +RDB$CHARACTER_SET_NAME = WIN1253 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 54 +RDB$CHARACTER_SET_NAME = WIN1254 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 55 +RDB$CHARACTER_SET_NAME = BIG_5 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 56 +RDB$CHARACTER_SET_NAME = GB_2312 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 57 +RDB$CHARACTER_SET_NAME = WIN1255 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 58 +RDB$CHARACTER_SET_NAME = WIN1256 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 59 +RDB$CHARACTER_SET_NAME = WIN1257 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 60 +RDB$CHARACTER_SET_NAME = KOI8R (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 63 +RDB$CHARACTER_SET_NAME = KOI8U (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 64 +RDB$CHARACTER_SET_NAME = WIN1258 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 65 +RDB$CHARACTER_SET_NAME = TIS620 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 66 +RDB$CHARACTER_SET_NAME = GBK (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 67 +RDB$CHARACTER_SET_NAME = CP943C (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 68 +RDB$CHARACTER_SET_NAME = GB18030 (Charset Id = 4 Codepage = 65001) +RDB$CHARACTER_SET_ID = 69 + +Metadata +SQLType =SQL_LONG +sub type = 0 +Table = TESTDATA +Owner = SYSDBA +Column Name = ROWID +Alias Name = ROWID +Field Name = ROWID +Scale = 0 +Charset id = 0 +Not Null +Size = 4 + +SQLType =SQL_LONG +sub type = 2 +Table = TESTDATA +Owner = SYSDBA +Column Name = FIXEDPOINT +Alias Name = FIXEDPOINT +Field Name = FIXEDPOINT +Scale = -2 +Charset id = 0 +Nullable +Size = 4 + +SQLType =SQL_DOUBLE +sub type = 0 +Table = TESTDATA +Owner = SYSDBA +Column Name = FLOATINGPOINT +Alias Name = FLOATINGPOINT +Field Name = FLOATINGPOINT +Scale = 0 +Charset id = 0 +Nullable +Size = 8 + +SQLType =SQL_VARYING +sub type = 4 +Table = TESTDATA +Owner = SYSDBA +Column Name = TITLE +Alias Name = TITLE +Field Name = TITLE +Scale = 0 +Charset id = 4 +Nullable +Size = 128 + +SQLType =SQL_BLOB +sub type = 1 +Table = TESTDATA +Owner = SYSDBA +Column Name = BLOBDATA +Alias Name = BLOBDATA +Field Name = BLOBDATA +Scale = 4 +Charset id = 0 +Nullable +Size = 8 + +Blob Meta Data +SQL SubType =1 +Table = TESTDATA +Column = BLOBDATA +CharSetID = 0 +Segment Size = 32000 + + +SQL Params +SQLType =SQL_LONG +sub type = 0 +Field Name = ROWID +Scale = 0 +Charset id = 0 +Nullable +Size = 4 + +SQLType =SQL_VARYING +sub type = 4 +Field Name = TITLE +Scale = 0 +Charset id = 4 +Nullable +Size = 128 + +SQLType =SQL_LONG +sub type = 2 +Field Name = FP +Scale = -2 +Charset id = 0 +Nullable +Size = 4 + +SQLType =SQL_DOUBLE +sub type = 0 +Field Name = DP +Scale = 0 +Charset id = 0 +Nullable +Size = 8 + +ROWID = 1 +FIXEDPOINT = 20.28 +FLOATINGPOINT = 3.14 +TITLE = Blob Test ©€ (Charset Id = 4 Codepage = 65001) +BLOBDATA = NULL + +SQL Params +SQLType =SQL_BLOB +sub type = 1 +Field Name = +Scale = 4 +Charset id = 0 +Nullable +Size = 8 + +SQLType =SQL_LONG +sub type = 0 +Field Name = +Scale = 0 +Charset id = 0 +Nullable +Size = 4 + +ROWID = 1 +FIXEDPOINT = 20.28 +FLOATINGPOINT = 3.14 +TITLE = Blob Test ©€ (Charset Id = 4 Codepage = 65001) +BLOBDATA (Charset Id = 0 Codepage = 0) + +To be or not to be-that is the question: +Whether 'tis nobler in the mind to suffer +The slings and arrows of outrageous fortune, +Or to take arms against a sea of troubles, +And, by opposing, end them. To die, to sleep- +No more-and by a sleep to say we end +The heartache and the thousand natural shocks +That flesh is heir to-'tis a consummation +Devoutly to be wished. To die, to sleep- +To sleep, perchance to dream. Aye, there's the rub, +For in that sleep of death what dreams may come, +When we have shuffled off this mortal coil, +Must give us pause. There's the respect +That makes calamity of so long life. +For who would bear the whips and scorns of time, +Th' oppressor's wrong, the proud man's contumely, +The pangs of despised love, the law’s delay, +The insolence of office, and the spurns +That patient merit of the unworthy takes, +When he himself might his quietus make +With a bare bodkin? Who would fardels bear, +To grunt and sweat under a weary life, +But that the dread of something after death, +The undiscovered country from whose bourn +No traveler returns, puzzles the will +And makes us rather bear those ills we have +Than fly to others that we know not of? +Thus conscience does make cowards of us all, +And thus the native hue of resolution +Is sicklied o'er with the pale cast of thought, +And enterprises of great pitch and moment, +With this regard their currents turn awry, +And lose the name of action.-Soft you now, +The fair Ophelia.-Nymph, in thy orisons +Be all my sins remembered + + +SQL Params +SQLType =SQL_LONG +sub type = 0 +Field Name = ROWID +Scale = 0 +Charset id = 0 +Nullable +Size = 4 + +SQLType =SQL_VARYING +sub type = 4 +Field Name = TITLE +Scale = 0 +Charset id = 4 +Nullable +Size = 128 + +SQLType =SQL_LONG +sub type = 2 +Field Name = FP +Scale = -2 +Charset id = 0 +Nullable +Size = 4 + +SQLType =SQL_DOUBLE +sub type = 0 +Field Name = DP +Scale = 0 +Charset id = 0 +Nullable +Size = 8 + +ROWID = 1 +FIXEDPOINT = 20.28 +FLOATINGPOINT = 3.14 +TITLE = Blob Test ©€ (Charset Id = 4 Codepage = 65001) +BLOBDATA (Charset Id = 0 Codepage = 0) + +To be or not to be-that is the question: +Whether 'tis nobler in the mind to suffer +The slings and arrows of outrageous fortune, +Or to take arms against a sea of troubles, +And, by opposing, end them. To die, to sleep- +No more-and by a sleep to say we end +The heartache and the thousand natural shocks +That flesh is heir to-'tis a consummation +Devoutly to be wished. To die, to sleep- +To sleep, perchance to dream. Aye, there's the rub, +For in that sleep of death what dreams may come, +When we have shuffled off this mortal coil, +Must give us pause. There's the respect +That makes calamity of so long life. +For who would bear the whips and scorns of time, +Th' oppressor's wrong, the proud man's contumely, +The pangs of despised love, the law’s delay, +The insolence of office, and the spurns +That patient merit of the unworthy takes, +When he himself might his quietus make +With a bare bodkin? Who would fardels bear, +To grunt and sweat under a weary life, +But that the dread of something after death, +The undiscovered country from whose bourn +No traveler returns, puzzles the will +And makes us rather bear those ills we have +Than fly to others that we know not of? +Thus conscience does make cowards of us all, +And thus the native hue of resolution +Is sicklied o'er with the pale cast of thought, +And enterprises of great pitch and moment, +With this regard their currents turn awry, +And lose the name of action.-Soft you now, +The fair Ophelia.-Nymph, in thy orisons +Be all my sins remembered + +ROWID = 2 +FIXEDPOINT = NULL +FLOATINGPOINT = NULL +TITLE = Blob Test ©€ (Charset Id = 4 Codepage = 65001) +BLOBDATA (Charset Id = 0 Codepage = 0) + +To be or not to be-that is the question: +Whether 'tis nobler in the mind to suffer +The slings and arrows of outrageous fortune, +Or to take arms against a sea of troubles, +And, by opposing, end them. To die, to sleep- +No more-and by a sleep to say we end +The heartache and the thousand natural shocks +That flesh is heir to-'tis a consummation +Devoutly to be wished. To die, to sleep- +To sleep, perchance to dream. Aye, there's the rub, +For in that sleep of death what dreams may come, +When we have shuffled off this mortal coil, +Must give us pause. There's the respect +That makes calamity of so long life. +For who would bear the whips and scorns of time, +Th' oppressor's wrong, the proud man's contumely, +The pangs of despised love, the law’s delay, +The insolence of office, and the spurns +That patient merit of the unworthy takes, +When he himself might his quietus make +With a bare bodkin? Who would fardels bear, +To grunt and sweat under a weary life, +But that the dread of something after death, +The undiscovered country from whose bourn +No traveler returns, puzzles the will +And makes us rather bear those ills we have +Than fly to others that we know not of? +Thus conscience does make cowards of us all, +And thus the native hue of resolution +Is sicklied o'er with the pale cast of thought, +And enterprises of great pitch and moment, +With this regard their currents turn awry, +And lose the name of action.-Soft you now, +The fair Ophelia.-Nymph, in thy orisons +Be all my sins remembered + + +Test text blob optimisation - short text followed by long +Params after prepare +SQL Params +SQLType =SQL_LONG +sub type = 0 +Field Name = +Scale = 0 +Charset id = 0 +Nullable +Size = 4 + +SQLType =SQL_BLOB +sub type = 1 +Field Name = +Scale = 4 +Charset id = 0 +Nullable +Size = 8 + +Params after setting values +SQL Params +SQLType =SQL_LONG +sub type = 0 +Field Name = +Scale = 0 +Charset id = 0 +Nullable +Size = 4 +Value = 3 + +SQLType =SQL_VARYING +sub type = 0 +Field Name = +Scale = 0 +Charset id = 0 +Nullable +Size = 80 +Value = The Quick Brown Fox Jumps over the Lazy Dog + +SQL Params +SQLType =SQL_LONG +sub type = 0 +Field Name = +Scale = 0 +Charset id = 0 +Nullable +Size = 4 +Value = 4 + +SQLType =SQL_BLOB +sub type = 1 +Field Name = +Scale = 0 +Charset id = 0 +Nullable +Size = 8 +Value = O Romeo, Romeo, wherefore art thou Romeo? Deny thy father and refuse thy name. Or if thou wilt not, be but sworn my love And I’ll no longer be a Capulet.‘Tis but thy name that is my enemy: Thou art thyself, though not a Montague. What’s Montague? It is nor hand nor foot Nor arm nor face nor any other part Belonging to a man. O be some other name. What’s in a name? That which we call a rose By any other name would smell as sweet; So Romeo would, were he not Romeo call’d, Retain that dear perfection which he owes Without that title. Romeo, doff thy name ,And for that name, which is no part of thee, Take all myself. + +Text Blob Optimisation Results +ROWID = 3 +FIXEDPOINT = NULL +FLOATINGPOINT = NULL +TITLE = NULL +BLOBDATA (Charset Id = 0 Codepage = 0) + +The Quick Brown Fox Jumps over the Lazy Dog +ROWID = 4 +FIXEDPOINT = NULL +FLOATINGPOINT = NULL +TITLE = NULL +BLOBDATA (Charset Id = 0 Codepage = 0) + +O Romeo, Romeo, wherefore art thou Romeo? Deny thy father and refuse thy name. Or if thou wilt not, be but sworn my love And I’ll no longer be a Capulet.‘Tis but thy name that is my enemy: Thou art thyself, though not a Montague. What’s Montague? It is nor hand nor foot Nor arm nor face nor any other part Belonging to a man. O be some other name. What’s in a name? That which we call a rose By any other name would smell as sweet; So Romeo would, were he not Romeo call’d, Retain that dear perfection which he owes Without that title. Romeo, doff thy name ,And for that name, which is no part of thee, Take all myself. + +Testing Blob as stored proc parameter +Metadata +SQLType =SQL_BLOB +sub type = 1 +Table = TESTPROC +Owner = SYSDBA +Column Name = BLOBDATA +Alias Name = BLOBDATA +Field Name = BLOBDATA +Scale = 4 +Charset id = 0 +Nullable +Size = 8 + +Blob Meta Data +SQL SubType =1 +Table = TESTPROC +Column = BLOBDATA +CharSetID = 0 +Segment Size = 32000 + + +BLOBDATA (Charset Id = 0 Codepage = 0) + +To be or not to be-that is the question: +Whether 'tis nobler in the mind to suffer +The slings and arrows of outrageous fortune, +Or to take arms against a sea of troubles, +And, by opposing, end them. To die, to sleep- +No more-and by a sleep to say we end +The heartache and the thousand natural shocks +That flesh is heir to-'tis a consummation +Devoutly to be wished. To die, to sleep- +To sleep, perchance to dream. Aye, there's the rub, +For in that sleep of death what dreams may come, +When we have shuffled off this mortal coil, +Must give us pause. There's the respect +That makes calamity of so long life. +For who would bear the whips and scorns of time, +Th' oppressor's wrong, the proud man's contumely, +The pangs of despised love, the law’s delay, +The insolence of office, and the spurns +That patient merit of the unworthy takes, +When he himself might his quietus make +With a bare bodkin? Who would fardels bear, +To grunt and sweat under a weary life, +But that the dread of something after death, +The undiscovered country from whose bourn +No traveler returns, puzzles the will +And makes us rather bear those ills we have +Than fly to others that we know not of? +Thus conscience does make cowards of us all, +And thus the native hue of resolution +Is sicklied o'er with the pale cast of thought, +And enterprises of great pitch and moment, +With this regard their currents turn awry, +And lose the name of action.-Soft you now, +The fair Ophelia.-Nymph, in thy orisons +Be all my sins remembered + + + +------------------------------------------------------ +Running Test 7: Create and read back an Array +Metadata +SQLType =SQL_LONG +sub type = 0 +Table = TESTDATA +Owner = SYSDBA +Column Name = ROWID +Alias Name = ROWID +Field Name = ROWID +Scale = 0 +Charset id = 0 +Not Null +Size = 4 + +SQLType =SQL_VARYING +sub type = 4 +Table = TESTDATA +Owner = SYSDBA +Column Name = TITLE +Alias Name = TITLE +Field Name = TITLE +Scale = 0 +Charset id = 4 +Nullable +Size = 128 + +SQLType =SQL_TIMESTAMP +sub type = 0 +Table = TESTDATA +Owner = SYSDBA +Column Name = DATED +Alias Name = DATED +Field Name = DATED +Scale = 0 +Charset id = 0 +Nullable +Size = 8 + +SQLType =SQL_VARYING +sub type = 4 +Table = TESTDATA +Owner = SYSDBA +Column Name = NOTES +Alias Name = NOTES +Field Name = NOTES +Scale = 0 +Charset id = 4 +Nullable +Size = 256 + +SQLType =SQL_ARRAY +sub type = 0 +Table = TESTDATA +Owner = SYSDBA +Column Name = MYARRAY +Alias Name = MYARRAY +Field Name = MYARRAY +Scale = 0 +Charset id = 0 +Nullable +Size = 8 +Array Meta Data: +SQLType =SQL_LONG +Scale = 0 +Charset id = 0 +Size = 4 +Table = TESTDATA +Column = MYARRAY +Dimensions = 1 +Bounds: (0:16) + +SQLType =SQL_ARRAY +sub type = 0 +Table = TESTDATA +Owner = SYSDBA +Column Name = MYARRAY2 +Alias Name = MYARRAY2 +Field Name = MYARRAY2 +Scale = 0 +Charset id = 0 +Nullable +Size = 8 +Array Meta Data: +SQLType =SQL_TIMESTAMP +Scale = 0 +Charset id = 0 +Size = 8 +Table = TESTDATA +Column = MYARRAY2 +Dimensions = 1 +Bounds: (0:16) + +SQLType =SQL_ARRAY +sub type = 0 +Table = TESTDATA +Owner = SYSDBA +Column Name = MYARRAY3 +Alias Name = MYARRAY3 +Field Name = MYARRAY3 +Scale = 0 +Charset id = 0 +Nullable +Size = 8 +Array Meta Data: +SQLType =SQL_INT64 +Scale = -2 +Charset id = 0 +Size = 8 +Table = TESTDATA +Column = MYARRAY3 +Dimensions = 1 +Bounds: (0:16) + +SQL Params +SQLType =SQL_LONG +sub type = 0 +Field Name = ROWID +Scale = 0 +Charset id = 0 +Nullable +Size = 4 + +SQLType =SQL_VARYING +sub type = 4 +Field Name = TITLE +Scale = 0 +Charset id = 4 +Nullable +Size = 128 + +SQLType =SQL_TIMESTAMP +sub type = 0 +Field Name = DATED +Scale = 0 +Charset id = 0 +Nullable +Size = 8 + +SQLType =SQL_VARYING +sub type = 4 +Field Name = NOTES +Scale = 0 +Charset id = 4 +Nullable +Size = 256 + +Param Name = ROWID +Param Name = TITLE +Param Name = DATED +Param Name = NOTES +ROWID = 1 +TITLE = Blob Test ©€ (Charset Id = 4 Codepage = 65001) +DATED = 2016/04/01 09:30:00.1000 +NOTES = Écoute moi (Charset Id = 4 Codepage = 65001) +MYARRAY = NULL +MYARRAY2 = NULL +MYARRAY3 = NULL + +SQL Params +SQLType =SQL_ARRAY +sub type = 0 +Field Name = MYARRAY +Scale = 0 +Charset id = 0 +Nullable +Size = 8 + +SQL Params +SQLType =SQL_ARRAY +sub type = 0 +Field Name = MYARRAY2 +Scale = 0 +Charset id = 0 +Nullable +Size = 8 + +SQL Params +SQLType =SQL_ARRAY +sub type = 0 +Field Name = MYARRAY3 +Scale = 0 +Charset id = 0 +Nullable +Size = 8 + +Metadata +SQLType =SQL_LONG +sub type = 0 +Table = TESTDATA +Owner = SYSDBA +Column Name = ROWID +Alias Name = ROWID +Field Name = ROWID +Scale = 0 +Charset id = 0 +Not Null +Size = 4 + +SQLType =SQL_VARYING +sub type = 4 +Table = TESTDATA +Owner = SYSDBA +Column Name = TITLE +Alias Name = TITLE +Field Name = TITLE +Scale = 0 +Charset id = 4 +Nullable +Size = 128 + +SQLType =SQL_TIMESTAMP +sub type = 0 +Table = TESTDATA +Owner = SYSDBA +Column Name = DATED +Alias Name = DATED +Field Name = DATED +Scale = 0 +Charset id = 0 +Nullable +Size = 8 + +SQLType =SQL_VARYING +sub type = 4 +Table = TESTDATA +Owner = SYSDBA +Column Name = NOTES +Alias Name = NOTES +Field Name = NOTES +Scale = 0 +Charset id = 4 +Nullable +Size = 256 + +SQLType =SQL_ARRAY +sub type = 0 +Table = TESTDATA +Owner = SYSDBA +Column Name = MYARRAY +Alias Name = MYARRAY +Field Name = MYARRAY +Scale = 0 +Charset id = 0 +Nullable +Size = 8 +Array Meta Data: +SQLType =SQL_LONG +Scale = 0 +Charset id = 0 +Size = 4 +Table = TESTDATA +Column = MYARRAY +Dimensions = 1 +Bounds: (0:16) + +SQLType =SQL_ARRAY +sub type = 0 +Table = TESTDATA +Owner = SYSDBA +Column Name = MYARRAY2 +Alias Name = MYARRAY2 +Field Name = MYARRAY2 +Scale = 0 +Charset id = 0 +Nullable +Size = 8 +Array Meta Data: +SQLType =SQL_TIMESTAMP +Scale = 0 +Charset id = 0 +Size = 8 +Table = TESTDATA +Column = MYARRAY2 +Dimensions = 1 +Bounds: (0:16) + +SQLType =SQL_ARRAY +sub type = 0 +Table = TESTDATA +Owner = SYSDBA +Column Name = MYARRAY3 +Alias Name = MYARRAY3 +Field Name = MYARRAY3 +Scale = 0 +Charset id = 0 +Nullable +Size = 8 +Array Meta Data: +SQLType =SQL_INT64 +Scale = -2 +Charset id = 0 +Size = 8 +Table = TESTDATA +Column = MYARRAY3 +Dimensions = 1 +Bounds: (0:16) + +ROWID = 1 +TITLE = Blob Test ©€ (Charset Id = 4 Codepage = 65001) +DATED = 2016/04/01 09:30:00.1000 +NOTES = Écoute moi (Charset Id = 4 Codepage = 65001) +MYARRAY = Array: (0: 100) (1: 99) (2: 98) (3: 97) (4: 96) (5: 95) (6: 94) (7: 93) (8: 92) (9: 91) (10: 90) (11: 89) (12: 88) (13: 87) (14: 86) (15: 85) (16: 84) + +MYARRAY2 = Array: (0: 01/5/2020 12:00:00) (1: 01/5/2020 12:01:00) (2: 01/5/2020 12:02:00) (3: 01/5/2020 12:03:00) (4: 01/5/2020 12:04:00) (5: 01/5/2020 12:05:00) (6: 01/5/2020 12:06:00) (7: 01/5/2020 12:07:00) (8: 01/5/2020 12:08:00) (9: 01/5/2020 12:09:00) (10: 01/5/2020 12:10:00) (11: 01/5/2020 12:11:00) (12: 01/5/2020 12:12:00) (13: 01/5/2020 12:13:00) (14: 01/5/2020 12:14:00) (15: 01/5/2020 12:15:00) (16: 01/5/2020 12:16:00) + +MYARRAY3 = Array: (0: 0) (1: 1.05) (2: 2.1) (3: 3.15) (4: 4.2) (5: 5.25) (6: 6.3) (7: 7.35) (8: 8.4) (9: 9.45) (10: 10.5) (11: 11.55) (12: 12.6) (13: 13.65) (14: 0.42) (15: 42.46) (16: 4269) + + +Shrink to 2:10 +Array: (2: 98) (3: 97) (4: 96) (5: 95) (6: 94) (7: 93) (8: 92) (9: 91) (10: 90) + +Write updated reduced slice +Show update array +ROWID = 1 +TITLE = Blob Test ©€ (Charset Id = 4 Codepage = 65001) +DATED = 2016/04/01 09:30:00.1000 +NOTES = Écoute moi (Charset Id = 4 Codepage = 65001) +MYARRAY = Array: (0: 100) (1: 99) (2: 1000) (3: 97) (4: 96) (5: 95) (6: 94) (7: 93) (8: 92) (9: 91) (10: 90) (11: 89) (12: 88) (13: 87) (14: 86) (15: 85) (16: 84) + +MYARRAY2 = Array: (0: 01/5/2020 12:00:00) (1: 01/5/2020 12:01:00) (2: 01/5/2020 12:02:00) (3: 01/5/2020 12:03:00) (4: 01/5/2020 12:04:00) (5: 01/5/2020 12:05:00) (6: 01/5/2020 12:06:00) (7: 01/5/2020 12:07:00) (8: 01/5/2020 12:08:00) (9: 01/5/2020 12:09:00) (10: 01/5/2020 12:10:00) (11: 01/5/2020 12:11:00) (12: 01/5/2020 12:12:00) (13: 01/5/2020 12:13:00) (14: 01/5/2020 12:14:00) (15: 01/5/2020 12:15:00) (16: 01/5/2020 12:16:00) + +MYARRAY3 = Array: (0: 0) (1: 1.05) (2: 2.1) (3: 3.15) (4: 4.2) (5: 5.25) (6: 6.3) (7: 7.35) (8: 8.4) (9: 9.45) (10: 10.5) (11: 11.55) (12: 12.6) (13: 13.65) (14: 0.42) (15: 42.46) (16: 4269) + + + + +------------------------------------------------------ +Running Test 8: Create and read back an Array with 2 dimensions +Metadata +SQLType =SQL_LONG +sub type = 0 +Table = TESTDATA +Owner = SYSDBA +Column Name = ROWID +Alias Name = ROWID +Field Name = ROWID +Scale = 0 +Charset id = 0 +Not Null +Size = 4 + +SQLType =SQL_VARYING +sub type = 4 +Table = TESTDATA +Owner = SYSDBA +Column Name = TITLE +Alias Name = TITLE +Field Name = TITLE +Scale = 0 +Charset id = 4 +Nullable +Size = 128 + +SQLType =SQL_ARRAY +sub type = 0 +Table = TESTDATA +Owner = SYSDBA +Column Name = MYARRAY +Alias Name = MYARRAY +Field Name = MYARRAY +Scale = 0 +Charset id = 0 +Nullable +Size = 8 +Array Meta Data: +SQLType =SQL_VARYING +Scale = 0 +Charset id = 4 +Size = 16 +Table = TESTDATA +Column = MYARRAY +Dimensions = 2 +Bounds: (0:16) (-1:7) + +SQL Params +SQLType =SQL_LONG +sub type = 0 +Field Name = ROWID +Scale = 0 +Charset id = 0 +Nullable +Size = 4 + +SQLType =SQL_VARYING +sub type = 4 +Field Name = TITLE +Scale = 0 +Charset id = 4 +Nullable +Size = 128 + +ROWID = 1 +TITLE = 2D Array (Charset Id = 4 Codepage = 65001) +MYARRAY = NULL + +ROWID = 1 +TITLE = 2D Array (Charset Id = 4 Codepage = 65001) +MYARRAY = Array: (0,-1: A50) (0,0: A51) (0,1: A52) (0,2: A53) (0,3: A54) (0,4: A55) (0,5: A56) (0,6: A57) (0,7: A58) (1,-1: A59) (1,0: A60) (1,1: A61) (1,2: A62) (1,3: A63) (1,4: A64) (1,5: A65) (1,6: A66) (1,7: A67) (2,-1: A68) (2,0: A69) (2,1: A70) (2,2: A71) (2,3: A72) (2,4: A73) (2,5: A74) (2,6: A75) (2,7: A76) (3,-1: A77) (3,0: A78) (3,1: A79) (3,2: A80) (3,3: A81) (3,4: A82) (3,5: A83) (3,6: A84) (3,7: A85) (4,-1: A86) (4,0: A87) (4,1: A88) (4,2: A89) (4,3: A90) (4,4: A91) (4,5: A92) (4,6: A93) (4,7: A94) (5,-1: A95) (5,0: A96) (5,1: A97) (5,2: A98) (5,3: A99) (5,4: A100) (5,5: A101) (5,6: A102) (5,7: A103) (6,-1: A104) (6,0: A105) (6,1: A106) (6,2: A107) (6,3: A108) (6,4: A109) (6,5: A110) (6,6: A111) (6,7: A112) (7,-1: A113) (7,0: A114) (7,1: A115) (7,2: A116) (7,3: A117) (7,4: A118) (7,5: A119) (7,6: A120) (7,7: A121) (8,-1: A122) (8,0: A123) (8,1: A124) (8,2: A125) (8,3: A126) (8,4: A127) (8,5: A128) (8,6: A129) (8,7: A130) (9,-1: A131) (9,0: A132) (9,1: A133) (9,2: A134) (9,3: A135) (9,4: A136) (9,5: A137) (9,6: A138) (9,7: A139) (10,-1: A140) (10,0: A141) (10,1: A142) (10,2: A143) (10,3: A144) (10,4: A145) (10,5: A146) (10,6: A147) (10,7: A148) (11,-1: A149) (11,0: A150) (11,1: A151) (11,2: A152) (11,3: A153) (11,4: A154) (11,5: A155) (11,6: A156) (11,7: A157) (12,-1: A158) (12,0: A159) (12,1: A160) (12,2: A161) (12,3: A162) (12,4: A163) (12,5: A164) (12,6: A165) (12,7: A166) (13,-1: A167) (13,0: A168) (13,1: A169) (13,2: A170) (13,3: A171) (13,4: A172) (13,5: A173) (13,6: A174) (13,7: A175) (14,-1: A176) (14,0: A177) (14,1: A178) (14,2: A179) (14,3: A180) (14,4: A181) (14,5: A182) (14,6: A183) (14,7: A184) (15,-1: A185) (15,0: A186) (15,1: A187) (15,2: A188) (15,3: A189) (15,4: A190) (15,5: A191) (15,6: A192) (15,7: A193) (16,-1: A194) (16,0: A195) (16,1: A196) (16,2: A197) (16,3: A198) (16,4: A199) (16,5: A200) (16,6: A201) (16,7: A202) + + + + +------------------------------------------------------ +Running Test 9: Database Information tests +Database ID = 3 FB = /tmp/fbintf-testsuite/employee.fdb SN = 81c2762b4b7a +Pages =401 +Base Level = 13,3,0, +Implementation = 11,5,0,2,66, +Reserved = 0 +ODS minor = 0 +ODS major = 14 +Page Size = 8192 +Version = 1: LI-T6.3.0.2105 Firebird 6.0 1d25527 +Database is Read/Write +Database Created: 01/8/2026 18:47:36 +Pages Used = 388 +Pages Free = 13 +Server Memory = 19488400 +Forced Writes = 1 +Max Memory = 19522064 +Num Buffers = 2048 +Sweep Interval = 20000 +Logged in Users: SYSDBA, +Active Transaction Count = 0 +Fetches = 16201 +Writes = 5 +Reads = 99 +Page Writes = 3 +Record Version Removals Operation Counts + +Deletes Operation Counts + +Expunge Count Operation Counts + +Insert Count Operation Counts + +Purge Count Countites Operation Counts + +Indexed Reads Count Operation Counts +Table ID = 6 +Count = 84 +Table ID = 9 +Count = 1 +Table ID = 11 +Count = 1 +Table ID = 18 +Count = 1508 +Table ID = 28 +Count = 9 +Table ID = 29 +Count = 1 +Table ID = 31 +Count = 1 +Table ID = 56 +Count = 94 + +Sequential Table Scans Operation Counts +Table ID = 0 +Count = 102 +Table ID = 1 +Count = 85 +Table ID = 4 +Count = 4692 +Table ID = 6 +Count = 2414 +Table ID = 11 +Count = 1192 +Table ID = 29 +Count = 596 +Table ID = 56 +Count = 166 + +Update Count Operation Counts + +Page Size = 8192 + + +------------------------------------------------------ +Running Test 10: Event Handling +Call Async Wait +Async Wait Called +Signal Event +Event Signalled +Event Counts: TESTEVENT, Count = 1 +Two more events +Call Async Wait +Async Wait Called +Event Signalled +Deferred Events Caught +Event Counts: TESTEVENT, Count = 2 +Signal Event +Event Counts: TESTEVENT, Count = 1 +Async Wait: Test Cancel +Event Signalled +Async Wait Called +Event Cancelled +Time Out - Cancel Worked! +Sync wait +Event Signalled +Event Counts: TESTEVENT, Count = 2 + + +------------------------------------------------------ +Running Test 11: Services API +SPB: Item Count = 2 + user_name = SYSDBA + password = masterkey + +Service Manager Version = 2 +Server Version = LI-T6.0.0.2105 Firebird 6.0 1d25527 +Implementation = Firebird/Linux/AMD/Intel/x64 + +Lock Directory = /tmp/firebird/ +Message File = /opt/firebird/ +Security File = /opt/firebird/security6.fdb + +DB Attachments +No. of Attachments = 0 +Databases In Use = 0 + +Sec. Database User +User Name = SYSDBA +First Name = +Middle Name = +Last Name = +User ID = 0 +Group ID = 0 + + +Licence Info: Engine Code: 335544378 +feature is not supported +-feature is not supported + +Licence Mask Info: Engine Code: 335544378 +feature is not supported + +Capabilities = 6 + + +Get Limbo transactions + +Limbo Transactions + + +Local Backup + + + +Local Backup Complete + +Local Restore + + + + + + +Local Restore Complete + +Open Database Check +Database OK +Database Dropped + + +------------------------------------------------------ +Running Test 12: Character Sets +Transliteration Tests +Default System Code Page = 65001 +Actual System Code Page = 65001 +Input String = WIN1252 Characters ÖÄÜöäüß, Character Set = 65001 Hex Values: +57 49 4E 31 32 35 32 20 43 68 61 72 61 63 74 65 72 73 20 C3 96 C3 84 C3 9C C3 B6 C3 A4 C3 BC C3 9F +Code Page = 1252 +57 49 4E 31 32 35 32 20 43 68 61 72 61 63 74 65 72 73 20 D6 C4 DC F6 E4 FC DF +Back to UTF8 +Code Page = 65001 +WIN1252 Characters ÖÄÜöäüß +57 49 4E 31 32 35 32 20 43 68 61 72 61 63 74 65 72 73 20 C3 96 C3 84 C3 9C C3 B6 C3 A4 C3 BC C3 9F +ANSI(1252) to ANSI(1251) Test +Input String = Я Écoute moi, Character Set = 65001 Hex Values: +D0 AF 20 C3 89 63 6F 75 74 65 20 6D 6F 69 +After conversion to 1251 +Code Page = 1251 +DF 20 3F 63 6F 75 74 65 20 6D 6F 69 +Now Transliterate to WIN1252 +Code Page = 1252 +3F 20 3F 63 6F 75 74 65 20 6D 6F 69 +SQL Params +SQLType =SQL_LONG +sub type = 0 +Field Name = ROWID +Scale = 0 +Charset id = 0 +Nullable +Size = 4 + +SQLType =SQL_VARYING +sub type = 4 +Field Name = TITLE +Scale = 0 +Charset id = 4 +Nullable +Size = 128 + +SQLType =SQL_VARYING +sub type = 4 +Field Name = NOTES +Scale = 0 +Charset id = 4 +Nullable +Size = 256 + +SQLType =SQL_BLOB +sub type = 1 +Field Name = BLOBDATA +Scale = 4 +Charset id = 0 +Nullable +Size = 8 + +SQLType =SQL_BLOB +sub type = 1 +Field Name = BLOBDATA2 +Scale = 4 +Charset id = 0 +Nullable +Size = 8 + +SQLType =SQL_VARYING +sub type = 1 +Field Name = INCLEAR +Scale = 0 +Charset id = 1 +Nullable +Size = 16 + +SQLType =SQL_TEXT +sub type = 4 +Field Name = FIXEDWIDTH +Scale = 0 +Charset id = 4 +Nullable +Size = 16 + +Show Param Values +SQL Params +SQLType =SQL_LONG +sub type = 0 +Field Name = ROWID +Scale = 0 +Charset id = 0 +Nullable +Size = 4 +Value = 1 + +SQLType =SQL_VARYING +sub type = 4 +Field Name = TITLE +Scale = 0 +Charset id = 4 +Nullable +Size = 128 +Value = Blob Test ©€ + +SQLType =SQL_VARYING +sub type = 4 +Field Name = NOTES +Scale = 0 +Charset id = 4 +Nullable +Size = 256 +Value = Écoute moi + +SQLType =SQL_VARYING +sub type = 0 +Field Name = BLOBDATA +Scale = 0 +Charset id = 0 +Nullable +Size = 8192 +Value = Some German Special Characters like ÖÄÜöäüß + +SQLType =SQL_BLOB +sub type = 1 +Field Name = BLOBDATA2 +Scale = 4 +Charset id = 0 +Nullable +Size = 8 +Value = Some German Special Characters like ÖÄÜöäüß + +SQLType =SQL_VARYING +sub type = 1 +Field Name = INCLEAR +Scale = 0 +Charset id = 1 +Nullable +Size = 16 +1 54 65 73 74 D C3 + +SQLType =SQL_VARYING +sub type = 0 +Field Name = FIXEDWIDTH +Scale = 0 +Charset id = 4 +Nullable +Size = 16 +Value = É + +Connection Character Set UTF8 +ROWID = 1 +TITLE = 42 6C 6F 62 20 54 65 73 74 20 C2 A9 E2 82 AC (Charset Id = 4 Codepage = 65001) +NOTES = C3 89 63 6F 75 74 65 20 6D 6F 69 (Charset Id = 4 Codepage = 65001) +BLOBDATA = 53 6F 6D 65 20 47 65 72 6D 61 6E 20 53 70 65 63 69 61 6C 20 43 68 61 72 61 63 74 65 72 73 20 6C 69 6B 65 20 C3 96 C3 84 C3 9C C3 B6 C3 A4 C3 BC C3 9F (Charset Id = 0 Codepage = 0) +BLOBDATA2 = 53 6F 6D 65 20 47 65 72 6D 61 6E 20 53 70 65 63 69 61 6C 20 43 68 61 72 61 63 74 65 72 73 20 6C 69 6B 65 20 C3 96 C3 84 C3 9C C3 B6 C3 A4 C3 BC C3 9F (Charset Id = 0 Codepage = 0) +INCLEAR = 1 54 65 73 74 D C3 (Charset Id = 1 Codepage = 65535) +FIXEDWIDTH = C3 89 (Charset Id = 4 Codepage = 65001) + +Test Exception Message = 53 6F 6D 65 20 47 65 72 6D 61 6E 20 53 70 65 63 69 61 6C 20 43 68 61 72 61 63 74 65 72 73 20 6C 69 6B 65 20 C3 96 C3 84 C3 9C C3 B6 C3 A4 C3 BC C3 9F +Exception returned: Engine Code: 335544517 +exception 1 +-"PUBLIC"."CHARSETTEST" +-Some German Special Characters like ÖÄÜöäüß +-At procedure "PUBLIC"."DOEXCEPTION" line: 1, col: 39 +Connection Character Set NONE +ROWID = 1 +TITLE = 42 6C 6F 62 20 54 65 73 74 20 C2 A9 E2 82 AC (Charset Id = 4 Codepage = 65001) +NOTES = C9 63 6F 75 74 65 20 6D 6F 69 (Charset Id = 1045 Codepage = 65535) +BLOBDATA = 53 6F 6D 65 20 47 65 72 6D 61 6E 20 53 70 65 63 69 61 6C 20 43 68 61 72 61 63 74 65 72 73 20 6C 69 6B 65 20 C3 96 C3 84 C3 9C C3 B6 C3 A4 C3 BC C3 9F (Charset Id = 0 Codepage = 0) +BLOBDATA2 = 53 6F 6D 65 20 47 65 72 6D 61 6E 20 53 70 65 63 69 61 6C 20 43 68 61 72 61 63 74 65 72 73 20 6C 69 6B 65 20 C3 96 C3 84 C3 9C C3 B6 C3 A4 C3 BC C3 9F (Charset Id = 0 Codepage = 0) +INCLEAR = 1 54 65 73 74 D C3 (Charset Id = 1 Codepage = 65535) +FIXEDWIDTH = C3 89 (Charset Id = 4 Codepage = 65001) + +Test Exception Message = 53 6F 6D 65 20 47 65 72 6D 61 6E 20 53 70 65 63 69 61 6C 20 43 68 61 72 61 63 74 65 72 73 20 6C 69 6B 65 20 C3 96 C3 84 C3 9C C3 B6 C3 A4 C3 BC C3 9F +Exception returned: Engine Code: 335544517 +exception 1 +-"PUBLIC"."CHARSETTEST" +-Some German Special Characters like ÖÄÜöäüß +-At procedure "PUBLIC"."DOEXCEPTION" line: 1, col: 39 +Connection Character Set WIN1252 +ROWID = 1 +TITLE = 42 6C 6F 62 20 54 65 73 74 20 A9 80 (Charset Id = 53 Codepage = 1252) +NOTES = C9 63 6F 75 74 65 20 6D 6F 69 (Charset Id = 53 Codepage = 1252) +BLOBDATA = 53 6F 6D 65 20 47 65 72 6D 61 6E 20 53 70 65 63 69 61 6C 20 43 68 61 72 61 63 74 65 72 73 20 6C 69 6B 65 20 C3 96 C3 84 C3 9C C3 B6 C3 A4 C3 BC C3 9F (Charset Id = 0 Codepage = 0) +BLOBDATA2 = 53 6F 6D 65 20 47 65 72 6D 61 6E 20 53 70 65 63 69 61 6C 20 43 68 61 72 61 63 74 65 72 73 20 6C 69 6B 65 20 C3 96 C3 84 C3 9C C3 B6 C3 A4 C3 BC C3 9F (Charset Id = 0 Codepage = 0) +INCLEAR = 1 54 65 73 74 D C3 (Charset Id = 1 Codepage = 65535) +FIXEDWIDTH = C9 (Charset Id = 53 Codepage = 1252) + +Test Exception Message = 53 6F 6D 65 20 47 65 72 6D 61 6E 20 53 70 65 63 69 61 6C 20 43 68 61 72 61 63 74 65 72 73 20 6C 69 6B 65 20 C3 96 C3 84 C3 9C C3 B6 C3 A4 C3 BC C3 9F +Exception returned: Engine Code: 335544517 +exception 1 +-"PUBLIC"."CHARSETTEST" +-Some German Special Characters like +-At procedure "PUBLIC"."DOEXCEPTION" line: 1, col: 39 +Recreate Database with WIN1252 as the connection character set +Query Database with UTF8, NONE and WIN1252 connections +SQL Params +SQLType =SQL_LONG +sub type = 0 +Field Name = ROWID +Scale = 0 +Charset id = 0 +Nullable +Size = 4 + +SQLType =SQL_VARYING +sub type = 4 +Field Name = TITLE +Scale = 0 +Charset id = 4 +Nullable +Size = 128 + +SQLType =SQL_VARYING +sub type = 4 +Field Name = NOTES +Scale = 0 +Charset id = 4 +Nullable +Size = 256 + +SQLType =SQL_BLOB +sub type = 1 +Field Name = BLOBDATA +Scale = 4 +Charset id = 0 +Nullable +Size = 8 + +SQLType =SQL_BLOB +sub type = 1 +Field Name = BLOBDATA2 +Scale = 4 +Charset id = 0 +Nullable +Size = 8 + +SQLType =SQL_VARYING +sub type = 1 +Field Name = INCLEAR +Scale = 0 +Charset id = 1 +Nullable +Size = 16 + +SQLType =SQL_TEXT +sub type = 4 +Field Name = FIXEDWIDTH +Scale = 0 +Charset id = 4 +Nullable +Size = 16 + +Show Param Values +SQL Params +SQLType =SQL_LONG +sub type = 0 +Field Name = ROWID +Scale = 0 +Charset id = 0 +Nullable +Size = 4 +Value = 1 + +SQLType =SQL_VARYING +sub type = 4 +Field Name = TITLE +Scale = 0 +Charset id = 4 +Nullable +Size = 128 +Value = Blob Test ©€ + +SQLType =SQL_VARYING +sub type = 4 +Field Name = NOTES +Scale = 0 +Charset id = 4 +Nullable +Size = 256 +Value = Écoute moi + +SQLType =SQL_VARYING +sub type = 0 +Field Name = BLOBDATA +Scale = 0 +Charset id = 0 +Nullable +Size = 8192 +Value = Some German Special Characters like ÖÄÜöäüß + +SQLType =SQL_BLOB +sub type = 1 +Field Name = BLOBDATA2 +Scale = 4 +Charset id = 0 +Nullable +Size = 8 +Value = Some German Special Characters like ÖÄÜöäüß + +SQLType =SQL_VARYING +sub type = 1 +Field Name = INCLEAR +Scale = 0 +Charset id = 1 +Nullable +Size = 16 +1 54 65 73 74 D C3 + +SQLType =SQL_VARYING +sub type = 0 +Field Name = FIXEDWIDTH +Scale = 0 +Charset id = 4 +Nullable +Size = 16 +Value = É + +Connection Character Set UTF8 +ROWID = 1 +TITLE = 42 6C 6F 62 20 54 65 73 74 20 C2 A9 E2 82 AC (Charset Id = 4 Codepage = 65001) +NOTES = C3 89 63 6F 75 74 65 20 6D 6F 69 (Charset Id = 4 Codepage = 65001) +BLOBDATA = 53 6F 6D 65 20 47 65 72 6D 61 6E 20 53 70 65 63 69 61 6C 20 43 68 61 72 61 63 74 65 72 73 20 6C 69 6B 65 20 C3 96 C3 84 C3 9C C3 B6 C3 A4 C3 BC C3 9F (Charset Id = 0 Codepage = 0) +BLOBDATA2 = 53 6F 6D 65 20 47 65 72 6D 61 6E 20 53 70 65 63 69 61 6C 20 43 68 61 72 61 63 74 65 72 73 20 6C 69 6B 65 20 C3 96 C3 84 C3 9C C3 B6 C3 A4 C3 BC C3 9F (Charset Id = 0 Codepage = 0) +INCLEAR = 1 54 65 73 74 D C3 (Charset Id = 1 Codepage = 65535) +FIXEDWIDTH = C3 89 (Charset Id = 4 Codepage = 65001) + +Test Exception Message = 53 6F 6D 65 20 47 65 72 6D 61 6E 20 53 70 65 63 69 61 6C 20 43 68 61 72 61 63 74 65 72 73 20 6C 69 6B 65 20 C3 96 C3 84 C3 9C C3 B6 C3 A4 C3 BC C3 9F +Exception returned: Engine Code: 335544517 +exception 1 +-"PUBLIC"."CHARSETTEST" +-Some German Special Characters like ÖÄÜöäüß +-At procedure "PUBLIC"."DOEXCEPTION" line: 1, col: 39 +Connection Character Set NONE +ROWID = 1 +TITLE = 42 6C 6F 62 20 54 65 73 74 20 C2 A9 E2 82 AC (Charset Id = 4 Codepage = 65001) +NOTES = C9 63 6F 75 74 65 20 6D 6F 69 (Charset Id = 1045 Codepage = 65535) +BLOBDATA = 53 6F 6D 65 20 47 65 72 6D 61 6E 20 53 70 65 63 69 61 6C 20 43 68 61 72 61 63 74 65 72 73 20 6C 69 6B 65 20 C3 96 C3 84 C3 9C C3 B6 C3 A4 C3 BC C3 9F (Charset Id = 0 Codepage = 0) +BLOBDATA2 = 53 6F 6D 65 20 47 65 72 6D 61 6E 20 53 70 65 63 69 61 6C 20 43 68 61 72 61 63 74 65 72 73 20 6C 69 6B 65 20 C3 96 C3 84 C3 9C C3 B6 C3 A4 C3 BC C3 9F (Charset Id = 0 Codepage = 0) +INCLEAR = 1 54 65 73 74 D C3 (Charset Id = 1 Codepage = 65535) +FIXEDWIDTH = C3 89 (Charset Id = 4 Codepage = 65001) + +Test Exception Message = 53 6F 6D 65 20 47 65 72 6D 61 6E 20 53 70 65 63 69 61 6C 20 43 68 61 72 61 63 74 65 72 73 20 6C 69 6B 65 20 C3 96 C3 84 C3 9C C3 B6 C3 A4 C3 BC C3 9F +Exception returned: Engine Code: 335544517 +exception 1 +-"PUBLIC"."CHARSETTEST" +-Some German Special Characters like ÖÄÜöäüß +-At procedure "PUBLIC"."DOEXCEPTION" line: 1, col: 39 +Connection Character Set WIN1252 +ROWID = 1 +TITLE = 42 6C 6F 62 20 54 65 73 74 20 A9 80 (Charset Id = 53 Codepage = 1252) +NOTES = C9 63 6F 75 74 65 20 6D 6F 69 (Charset Id = 53 Codepage = 1252) +BLOBDATA = 53 6F 6D 65 20 47 65 72 6D 61 6E 20 53 70 65 63 69 61 6C 20 43 68 61 72 61 63 74 65 72 73 20 6C 69 6B 65 20 C3 96 C3 84 C3 9C C3 B6 C3 A4 C3 BC C3 9F (Charset Id = 0 Codepage = 0) +BLOBDATA2 = 53 6F 6D 65 20 47 65 72 6D 61 6E 20 53 70 65 63 69 61 6C 20 43 68 61 72 61 63 74 65 72 73 20 6C 69 6B 65 20 C3 96 C3 84 C3 9C C3 B6 C3 A4 C3 BC C3 9F (Charset Id = 0 Codepage = 0) +INCLEAR = 1 54 65 73 74 D C3 (Charset Id = 1 Codepage = 65535) +FIXEDWIDTH = C9 (Charset Id = 53 Codepage = 1252) + +Test Exception Message = 53 6F 6D 65 20 47 65 72 6D 61 6E 20 53 70 65 63 69 61 6C 20 43 68 61 72 61 63 74 65 72 73 20 6C 69 6B 65 20 C3 96 C3 84 C3 9C C3 B6 C3 A4 C3 BC C3 9F +Exception returned: Engine Code: 335544517 +exception 1 +-"PUBLIC"."CHARSETTEST" +-Some German Special Characters like +-At procedure "PUBLIC"."DOEXCEPTION" line: 1, col: 39 + + +------------------------------------------------------ +Running Test 13: Transaction over two databases +Init Database 1 +ROWID = 1 +TITLE = 42 6C 6F 62 20 54 65 73 74 20 C2 A9 E2 82 AC (Charset Id = 4 Codepage = 65001) +NOTES = C3 89 63 6F 75 74 65 20 6D 6F 69 (Charset Id = 4 Codepage = 65001) +BLOBDATA = 53 6F 6D 65 20 47 65 72 6D 61 6E 20 53 70 65 63 69 61 6C 20 43 68 61 72 61 63 74 65 72 73 20 6C 69 6B 65 20 C3 96 C3 84 C3 9C C3 B6 C3 A4 C3 BC C3 9F (Charset Id = 0 Codepage = 0) +BLOBDATA2 = 53 6F 6D 65 20 47 65 72 6D 61 6E 20 53 70 65 63 69 61 6C 20 43 68 61 72 61 63 74 65 72 73 20 6C 69 6B 65 20 C3 96 C3 84 C3 9C C3 B6 C3 A4 C3 BC C3 9F (Charset Id = 0 Codepage = 0) +INCLEAR = 1 54 65 73 74 D C3 (Charset Id = 1 Codepage = 65535) + +Init Database 2 +ROWID = 1 +TITLE = 42 6C 6F 62 20 54 65 73 74 20 C2 A9 E2 82 AC (Charset Id = 4 Codepage = 65001) +NOTES = C3 89 63 6F 75 74 65 20 6D 6F 69 (Charset Id = 4 Codepage = 65001) +BLOBDATA = 53 6F 6D 65 20 47 65 72 6D 61 6E 20 53 70 65 63 69 61 6C 20 43 68 61 72 61 63 74 65 72 73 20 6C 69 6B 65 20 C3 96 C3 84 C3 9C C3 B6 C3 A4 C3 BC C3 9F (Charset Id = 0 Codepage = 0) +BLOBDATA2 = 53 6F 6D 65 20 47 65 72 6D 61 6E 20 53 70 65 63 69 61 6C 20 43 68 61 72 61 63 74 65 72 73 20 6C 69 6B 65 20 C3 96 C3 84 C3 9C C3 B6 C3 A4 C3 BC C3 9F (Charset Id = 0 Codepage = 0) +INCLEAR = 1 54 65 73 74 D C3 (Charset Id = 1 Codepage = 65535) + +Skipping test - multi database transactions are not supported by this provider + + +------------------------------------------------------ +Running Test 14: Non select procedures +Default Character set Name = NONE +Metadata +SQLType =SQL_LONG +sub type = 0 +Table = SHOWDATA +Owner = SYSDBA +Column Name = ROWID +Alias Name = ROWID +Field Name = ROWID +Scale = 0 +Charset id = 0 +Nullable +Size = 4 + +SQLType =SQL_VARYING +sub type = 0 +Table = SHOWDATA +Owner = SYSDBA +Column Name = TITLE +Alias Name = TITLE +Field Name = TITLE +Scale = 0 +Charset id = 0 +Nullable +Size = 32 + +ROWID = 1 +TITLE = Testing + +Repeat with a different execute transaction + +ROWID = 1 +TITLE = Testing + +Repeat with a original transaction + +ROWID = 1 +TITLE = Testing + + +------------------------------------------------------ +Running Test 15: Blob Handling and BPBs + +Testdata + +ROWID = 1 +TITLE = Blob Test (Charset Id = 4 Codepage = 65001) +BLOBDATA = (blob), Length = 92514 + +ROWID = 2 +TITLE = Blob Test with binary string (Charset Id = 4 Codepage = 65001) +BLOBDATA = (blob), Length = 13 +0 9 A C9 63 6F 75 74 65 20 6D 6F 69 + +Testdata 2 + +ROWID = 1 +TITLE = Blob Test (Charset Id = 4 Codepage = 65001) +BLOBDATA (Charset Id = 0 Codepage = 0) + +Écoute moi + + + +------------------------------------------------------ +Running Test 16: Error handling +Invalid Database Name Test +Error Handled: SQLCODE: -902 +Unsuccessful execution caused by a system error that precludes successful execution of subsequent statements +Engine Code: 335544344 +I/O error during "open" operation for file "Malformed Name" +-Error while trying to open file +-No such file or directory +Invalid User Name Test +Error Handled: SQLCODE: -902 +Unsuccessful execution caused by a system error that precludes successful execution of subsequent statements +Engine Code: 335544472 +Your user name and password are not defined. Ask your database administrator to set up a Firebird login +Invalid password Test +Error Handled: SQLCODE: -999 +Firebird error +Engine Code: 335544721 +Unsupported authentication plugin "Legacy_Auth" +Invalid Prepare SQL Test +Error Handled: SQLCODE: -206 +Column does not belong to referenced table +Engine Code: 335544569 +Dynamic SQL Error +-SQL error code = -206 +-Column unknown +-"UNKNOWN_DATE" +-At line 1, column 21 When Executing: Update Employee Set Unknown_Date = ? Where EMP_NO = ? +Invalid Open Cursor SQL Test +Error Handled: SQLCODE: -206 +Column does not belong to referenced table +Engine Code: 335544569 +Dynamic SQL Error +-SQL error code = -206 +-Column unknown +-"X" +-At line 1, column 8 When Executing: Select X,count(*) As Counter from EMPLOYEE +Transaction not started Test +Error Handled: Transaction is not active +Invalid Param SQL Type Test +Error Handled: Field "EMP_NO" not found +Case sensitive Param SQL Test +Error Handled: Field "EMP_NO" not found +Stale Reference Check +First test correct usage +COUNTER = 42 + +New Transaction before param set +Error Handled: This interface is no longer up-to-date +New Transaction before Open Cursor +Error Handled: This interface is no longer up-to-date +Stop Stale Reference Checks +New Transaction before param set +COUNTER = 42 + +New Transaction before Open Cursor +COUNTER = 42 + +Invalid Server Name Test +Error Handled: SQLCODE: -999 +Firebird error +Engine Code: 335544721 +Unable to connect to server "unknown" port 3050: Host name resolution for "unknown" failed +Invalid User Name Test +Error Handled: SQLCODE: -902 +Unsuccessful execution caused by a system error that precludes successful execution of subsequent statements +Engine Code: 335544472 +Your user name and password are not defined. Ask your database administrator to set up a Firebird login +Invalid password Test +Error Handled: SQLCODE: -999 +Firebird error +Engine Code: 335544721 +Unsupported authentication plugin "Legacy_Auth" + + +------------------------------------------------------ +Running Test 17: Date/Time tests and Firebird 4 extensions + FBVersion = Firebird wire protocol version 20, Srp256 authentication, ChaCha64 wire encryption +Has Local TZ DB = FALSE + +Testdata + +Metadata +SQLType =SQL_LONG +sub type = 0 +Table = TESTDATA +Owner = SYSDBA +Column Name = ROWID +Alias Name = ROWID +Field Name = ROWID +Scale = 0 +Charset id = 0 +Not Null +Size = 4 + +SQLType =SQL_TYPE_DATE +sub type = 0 +Table = TESTDATA +Owner = SYSDBA +Column Name = DATECOL +Alias Name = DATECOL +Field Name = DATECOL +Scale = 0 +Charset id = 0 +Nullable +Size = 4 + +SQLType =SQL_TYPE_TIME +sub type = 0 +Table = TESTDATA +Owner = SYSDBA +Column Name = TIMECOL +Alias Name = TIMECOL +Field Name = TIMECOL +Scale = 0 +Charset id = 0 +Nullable +Size = 4 + +SQLType =SQL_TIMESTAMP +sub type = 0 +Table = TESTDATA +Owner = SYSDBA +Column Name = TIMESTAMPCOL +Alias Name = TIMESTAMPCOL +Field Name = TIMESTAMPCOL +Scale = 0 +Charset id = 0 +Nullable +Size = 8 + +ROWID = 1 +DATECOL = 2019/04/01 +TIMECOL = 11:31:05.0001 +TIMESTAMPCOL = 2016/02/29 22:02:35.0001 +ROWID = 2 +DATECOL = 1939/09/03 +TIMECOL = 15:40:00.0002 +TIMESTAMPCOL = 1918/11/11 11:11:00.0000 +ROWID = 3 +DATECOL = 1066/10/14 +TIMECOL = 23:59:59.9994 +TIMESTAMPCOL = 1918/11/11 11:11:00.0000 +ROWID = 4 +DATECOL = 1815/06/18 +TIMECOL = 00:01:40.1115 +TIMESTAMPCOL = 1945/05/08 22:10:00.0010 + + +Testdata - second pass + +1, 01/4/2019, 11:31:05, 29/2/2016 22:02:35 +2, 03/9/1939, 15:40:00.000, 11/11/1918 11:11:00 +3, 14/10/1066, 23:59:59.999, 11/11/1918 11:11:00 +4, 18/6/1815, 00:01:40.112, 08/5/1945 22:10:00 +Sys Time = 1945, 5, 8, 2, 22, 10, 0, 1 +Skipping Firebird 4 and later test part - time zone services are not supported by this provider + + +------------------------------------------------------ +Running Test 18: Firebird 4 Decfloat extensions + FBVersion = Firebird wire protocol version 20, Srp256 authentication, ChaCha64 wire encryption + +FB4 Testdata_DECFloat + +Metadata +SQLType =SQL_LONG +sub type = 0 +Table = FB4TESTDATA_DECFLOAT +Owner = SYSDBA +Column Name = ROWID +Alias Name = ROWID +Field Name = ROWID +Scale = 0 +Charset id = 0 +Not Null +Size = 4 + +SQLType =SQL_DEC16 +sub type = 0 +Table = FB4TESTDATA_DECFLOAT +Owner = SYSDBA +Column Name = FLOAT16 +Alias Name = FLOAT16 +Field Name = FLOAT16 +Scale = 0 +Charset id = 0 +Nullable +Size = 8 + +SQLType =SQL_DEC34 +sub type = 0 +Table = FB4TESTDATA_DECFLOAT +Owner = SYSDBA +Column Name = FLOAT34 +Alias Name = FLOAT34 +Field Name = FLOAT34 +Scale = 0 +Charset id = 0 +Nullable +Size = 16 + +SQLType =SQL_INT128 +sub type = 1 +Table = FB4TESTDATA_DECFLOAT +Owner = SYSDBA +Column Name = BIGNUMBER +Alias Name = BIGNUMBER +Field Name = BIGNUMBER +Scale = -6 +Charset id = 0 +Nullable +Size = 16 + +SQLType =SQL_INT128 +sub type = 1 +Table = FB4TESTDATA_DECFLOAT +Owner = SYSDBA +Column Name = BIGGERNUMBER +Alias Name = BIGGERNUMBER +Field Name = BIGGERNUMBER +Scale = -4 +Charset id = 0 +Nullable +Size = 16 + +SQLType =SQL_INT128 +sub type = 0 +Table = FB4TESTDATA_DECFLOAT +Owner = SYSDBA +Column Name = BIGINTEGER +Alias Name = BIGINTEGER +Field Name = BIGINTEGER +Scale = 0 +Charset id = 0 +Nullable +Size = 16 + +RowID = 1 +Float16 = 64000000000.01 + Precision = 13 + Sign = 0 + Special = 0 + Places = 2 + Digits = 64 00 00 00 00 00 10 +Float34 = 123456789123456789.12345678 + Precision = 26 + Sign = 0 + Special = 0 + Places = 8 + Digits = 12 34 56 78 91 23 45 67 89 12 34 56 78 +BigNumber = + Precision = 0 + Sign = 0 + Special = 0 + Places = 0 + Digits = +BigInteger = + Precision = 0 + Sign = 0 + Special = 0 + Places = 0 + Digits = + +RowID = 2 +Float16 = -64000000000.01 + Precision = 13 + Sign = 1 + Special = 0 + Places = 2 + Digits = 64 00 00 00 00 00 10 +Float34 = -123456789123456789.12345678 + Precision = 26 + Sign = 1 + Special = 0 + Places = 8 + Digits = 12 34 56 78 91 23 45 67 89 12 34 56 78 +BigNumber = + Precision = 0 + Sign = 0 + Special = 0 + Places = 0 + Digits = +BigInteger = Null + +RowID = 3 +Float16 = 64100000000.011 + Precision = 14 + Sign = 0 + Special = 0 + Places = 3 + Digits = 64 10 00 00 00 00 11 +Float34 = 12345678912.1200 + Precision = 15 + Sign = 0 + Special = 0 + Places = 4 + Digits = 12 34 56 78 91 21 20 00 +BigNumber = + Precision = 0 + Sign = 0 + Special = 0 + Places = 0 + Digits = +BiggerNumber = + Precision = 0 + Sign = 0 + Special = 0 + Places = 0 + Digits = +BigInteger = Null + +RowID = 4 +Float16 = 0 + Precision = 0 + Sign = 0 + Special = 0 + Places = 0 + Digits = +Float34 = -1 + Precision = 1 + Sign = 1 + Special = 0 + Places = 0 + Digits = 10 +BigNumber = + Precision = 0 + Sign = 0 + Special = 0 + Places = 0 + Digits = +BiggerNumber = + Precision = 0 + Sign = 0 + Special = 0 + Places = 0 + Digits = +BigInteger = Null + +Metadata +SQLType =SQL_LONG +sub type = 0 +Table = FB4TESTDATA_DECFLOAT_AR +Owner = SYSDBA +Column Name = ROWID +Alias Name = ROWID +Field Name = ROWID +Scale = 0 +Charset id = 0 +Not Null +Size = 4 + +SQLType =SQL_ARRAY +sub type = 0 +Table = FB4TESTDATA_DECFLOAT_AR +Owner = SYSDBA +Column Name = FLOAT16 +Alias Name = FLOAT16 +Field Name = FLOAT16 +Scale = 0 +Charset id = 0 +Nullable +Size = 8 +Array Meta Data: +SQLType =SQL_DEC16 +Scale = 0 +Charset id = 0 +Size = 8 +Table = FB4TESTDATA_DECFLOAT_AR +Column = FLOAT16 +Dimensions = 1 +Bounds: (0:16) + +SQLType =SQL_ARRAY +sub type = 0 +Table = FB4TESTDATA_DECFLOAT_AR +Owner = SYSDBA +Column Name = FLOAT34 +Alias Name = FLOAT34 +Field Name = FLOAT34 +Scale = 0 +Charset id = 0 +Nullable +Size = 8 +Array Meta Data: +SQLType =SQL_DEC34 +Scale = 0 +Charset id = 0 +Size = 16 +Table = FB4TESTDATA_DECFLOAT_AR +Column = FLOAT34 +Dimensions = 1 +Bounds: (0:16) + +SQLType =SQL_ARRAY +sub type = 0 +Table = FB4TESTDATA_DECFLOAT_AR +Owner = SYSDBA +Column Name = BIGNUMBER +Alias Name = BIGNUMBER +Field Name = BIGNUMBER +Scale = 0 +Charset id = 0 +Nullable +Size = 8 +Array Meta Data: +SQLType =SQL_INT128 +Scale = -6 +Charset id = 0 +Size = 16 +Table = FB4TESTDATA_DECFLOAT_AR +Column = BIGNUMBER +Dimensions = 1 +Bounds: (0:16) + + +Decfloat Arrays +Row No 1 +Float16 Array: (0: 64100000000.011) (1: 64100000001.011) (2: 64100000002.011) (3: 64100000003.011) (4: 64100000004.011) (5: 64100000005.011) (6: 64100000006.011) (7: 64100000007.011) (8: 64100000008.011) (9: 64100000009.011) (10: 64100000010.011) (11: 64100000011.011) (12: 64100000012.011) (13: 64100000013.011) (14: 64100000014.011) (15: 64100000015.011) (16: 64100000016.011) + +Float34 Array: (0: 123456789123456789.12345678) (1: 123456789123456790.12345678) (2: 123456789123456791.12345678) (3: 123456789123456792.12345678) (4: 123456789123456793.12345678) (5: 123456789123456794.12345678) (6: 123456789123456795.12345678) (7: 123456789123456796.12345678) (8: 123456789123456797.12345678) (9: 123456789123456798.12345678) (10: 123456789123456799.12345678) (11: 123456789123456800.12345678) (12: 123456789123456801.12345678) (13: 123456789123456802.12345678) (14: 123456789123456803.12345678) (15: 123456789123456804.12345678) (16: 123456789123456805.12345678) + +BigNumber Array: (0: 0) (1: 0) (2: 0) (3: 0) (4: 0) (5: 0) (6: 0) (7: 0) (8: 0) (9: 0) (10: 0) (11: 0) (12: 0) (13: 0) (14: 0) (15: 0) (16: 0) + + + +------------------------------------------------------ +Running Test 19: Batch Update and Insert Queries +Opening inet://localhost/employee +Database Open +Firebird wire protocol version 20, Srp256 authentication, ChaCha64 wire encryption +Rows before update +EMP_NO = 2 +FIRST_NAME = Robert +LAST_NAME = Nelson +PHONE_EXT = 250 +HIRE_DATE = 1988/12/28 00:00:00.0000 +DEPT_NO = 600 +JOB_CODE = VP +JOB_GRADE = 2 +JOB_COUNTRY = USA +SALARY = 105,900.00 +FULL_NAME = Nelson, Robert +EMP_NO = 8 +FIRST_NAME = Leslie +LAST_NAME = Johnson +PHONE_EXT = 410 +HIRE_DATE = 1989/04/05 00:00:00.0000 +DEPT_NO = 180 +JOB_CODE = Mktg +JOB_GRADE = 3 +JOB_COUNTRY = USA +SALARY = 64,635.00 +FULL_NAME = Johnson, Leslie + +Select Count = 2 InsertCount = 0 UpdateCount = 2 DeleteCount = 0 +Batch Completion Info +Total rows processed = 2 +Updated Records = 2 +Row 1 State = bcNoMoreErrors Msg = +Row 2 State = bcNoMoreErrors Msg = +Rows after update +EMP_NO = 2 +FIRST_NAME = Robert +LAST_NAME = Nelson +PHONE_EXT = 250 +HIRE_DATE = 2018/05/28 00:00:00.0000 +DEPT_NO = 600 +JOB_CODE = VP +JOB_GRADE = 2 +JOB_COUNTRY = USA +SALARY = 105,900.00 +FULL_NAME = Nelson, Robert +EMP_NO = 8 +FIRST_NAME = Leslie +LAST_NAME = Johnson +PHONE_EXT = 410 +HIRE_DATE = 2016/01/31 00:00:00.0000 +DEPT_NO = 180 +JOB_CODE = Mktg +JOB_GRADE = 3 +JOB_COUNTRY = USA +SALARY = 64,635.00 +FULL_NAME = Johnson, Leslie + + +Repeat but with a last dummy row that is ignored +Select Count = 2 InsertCount = 0 UpdateCount = 2 DeleteCount = 0 +Batch Completion Info +Total rows processed = 2 +Updated Records = 2 +Row 1 State = bcNoMoreErrors Msg = +Row 2 State = bcNoMoreErrors Msg = +Rows after update +EMP_NO = 2 +FIRST_NAME = Robert +LAST_NAME = Nelson +PHONE_EXT = 250 +HIRE_DATE = 2018/05/28 00:00:00.0000 +DEPT_NO = 600 +JOB_CODE = VP +JOB_GRADE = 2 +JOB_COUNTRY = USA +SALARY = 105,900.00 +FULL_NAME = Nelson, Robert +EMP_NO = 8 +FIRST_NAME = Leslie +LAST_NAME = Johnson +PHONE_EXT = 410 +HIRE_DATE = 2016/01/31 00:00:00.0000 +DEPT_NO = 180 +JOB_CODE = Mktg +JOB_GRADE = 3 +JOB_COUNTRY = USA +SALARY = 64,635.00 +FULL_NAME = Johnson, Leslie + + +Insert rows +Select Count = 0 InsertCount = 3 UpdateCount = 0 DeleteCount = 0 +Batch Completion Info +Total rows processed = 3 +Updated Records = 3 +Row 1 State = bcNoMoreErrors Msg = +Row 2 State = bcNoMoreErrors Msg = +Row 3 State = bcNoMoreErrors Msg = +Rows after insert +EMP_NO = 500 +FIRST_NAME = John +LAST_NAME = Doe +PHONE_EXT = +HIRE_DATE = 2015/04/01 00:00:00.0000 +DEPT_NO = 600 +JOB_CODE = Eng +JOB_GRADE = 4 +JOB_COUNTRY = England +SALARY = 41,000.89 +FULL_NAME = Doe, John +EMP_NO = 501 +FIRST_NAME = Jane +LAST_NAME = Doe +PHONE_EXT = +HIRE_DATE = 2015/04/02 00:00:00.0000 +DEPT_NO = 600 +JOB_CODE = Eng +JOB_GRADE = 4 +JOB_COUNTRY = England +SALARY = 42,000.89 +FULL_NAME = Doe, Jane +EMP_NO = 502 +FIRST_NAME = John +LAST_NAME = SmithAndJonesFamily1 +PHONE_EXT = +HIRE_DATE = 2015/04/03 00:00:00.0000 +DEPT_NO = 600 +JOB_CODE = Eng +JOB_GRADE = 4 +JOB_COUNTRY = England +SALARY = 41,000.99 +FULL_NAME = SmithAndJonesFamily1, John + + +Insert rows - and then cancel +Cancel Batch - note - next step will fail with a duplicate key if cancel fails + +Insert rows - ignore last row +Select Count = 0 InsertCount = 2 UpdateCount = 0 DeleteCount = 0 +Batch Completion Info +Total rows processed = 2 +Updated Records = 2 +Row 1 State = bcNoMoreErrors Msg = +Row 2 State = bcNoMoreErrors Msg = +Rows after insert +EMP_NO = 500 +FIRST_NAME = John +LAST_NAME = Doe +PHONE_EXT = +HIRE_DATE = 2015/04/01 00:00:00.0000 +DEPT_NO = 600 +JOB_CODE = Eng +JOB_GRADE = 4 +JOB_COUNTRY = England +SALARY = 41,000.89 +FULL_NAME = Doe, John +EMP_NO = 501 +FIRST_NAME = Jane +LAST_NAME = Doe +PHONE_EXT = +HIRE_DATE = 2015/04/02 00:00:00.0000 +DEPT_NO = 600 +JOB_CODE = Eng +JOB_GRADE = 4 +JOB_COUNTRY = England +SALARY = 42,000.89 +FULL_NAME = Doe, Jane + +Insert with inline blob +Select Count = 0 InsertCount = 2 UpdateCount = 0 DeleteCount = 0 +Batch Completion Info +Total rows processed = 2 +Updated Records = 2 +Row 1 State = bcNoMoreErrors Msg = +Row 2 State = bcNoMoreErrors Msg = +Rows after insert +JOB_CODE = ABC +JOB_GRADE = 3 +JOB_COUNTRY = England +JOB_TITLE = Chief Tester +MIN_SALARY = 21,000.00 +MAX_SALARY = 24,000.99 +JOB_REQUIREMENT (Charset Id = 0 Codepage = 0) + +The quick brown fox jumped over the lazy dog +LANGUAGE_REQ = Array: (1: Eng) (2: ) (3: ) (4: ) (5: ) + +JOB_CODE = DEF +JOB_GRADE = 3 +JOB_COUNTRY = England +JOB_TITLE = Deputy Tester +MIN_SALARY = 21,000.00 +MAX_SALARY = 24,000.99 +JOB_REQUIREMENT (Charset Id = 0 Codepage = 0) + +The quick brown fox jumped over the running dog +LANGUAGE_REQ = Array: (1: Eng) (2: Fra) (3: ) (4: ) (5: ) + + + +Insert with explicit blob +Select Count = 0 InsertCount = 2 UpdateCount = 0 DeleteCount = 0 +Batch Completion Info +Total rows processed = 2 +Updated Records = 2 +Row 1 State = bcNoMoreErrors Msg = +Row 2 State = bcNoMoreErrors Msg = +Rows after insert +JOB_CODE = ABC +JOB_GRADE = 3 +JOB_COUNTRY = England +JOB_TITLE = Chief Tester +MIN_SALARY = 21,000.00 +MAX_SALARY = 24,000.99 +JOB_REQUIREMENT (Charset Id = 0 Codepage = 0) + +The quick brown fox jumped over the lazy dog +LANGUAGE_REQ = Array: (1: Eng) (2: ) (3: ) (4: ) (5: ) + +JOB_CODE = DEF +JOB_GRADE = 3 +JOB_COUNTRY = England +JOB_TITLE = Deputy Tester +MIN_SALARY = 21,000.00 +MAX_SALARY = 24,000.99 +JOB_REQUIREMENT (Charset Id = 0 Codepage = 0) + +The quick brown fox jumped over the running dog +LANGUAGE_REQ = Array: (1: Eng) (2: Fra) (3: ) (4: ) (5: ) + + +Test Error Handling +Error reported (as expected) when changing param type: Param[0] "": Unable to change from a SQL_TIMESTAMP to a string type +Test Error Handling - Update returning should fail +Error reported (as expected) when defering update returning query: This query type (SQL_Select) cannot be batched +Error handling when Insert rows - duplicate key +Error reported when inserting: Engine Code: 335544665 +violation of PRIMARY or UNIQUE KEY constraint "INTEG_27" on table "PUBLIC"."EMPLOYEE" +-Problematic key value is ("EMP_NO" = 500) +Select Count = 0 InsertCount = 1 UpdateCount = 0 DeleteCount = 0 +Batch Completion Info +Total rows processed = 2 +Updated Records = 1 +Row 1 State = bcNoMoreErrors Msg = +Row 2 State = bcExecuteFailed Msg = Engine Code: 335544665 +violation of PRIMARY or UNIQUE KEY constraint "INTEG_27" on table "PUBLIC"."EMPLOYEE" +-Problematic key value is ("EMP_NO" = 500) + + +------------------------------------------------------ +Running Test 20: Stress Test IBatch interface + FBVersion = Firebird wire protocol version 20, Srp256 authentication, ChaCha64 wire encryption +10000 rows added +20000 rows added +30000 rows added +40000 rows added +50000 rows added +Batch Execute +Intermediate Apply Batch on row 50001 +Batch Completion Info +Total rows processed = 50000 +Updated Rows = 50000 +Row 50000 State = bcNoMoreErrors Msg = +60000 rows added +70000 rows added +80000 rows added +90000 rows added +100000 rows added +Batch Execute +Batch Completion Info +Total rows processed = 50000 +Updated Rows = 50000 +Row 50000 State = bcNoMoreErrors Msg = +Rows in Dataset = 100000 + Message Hash = 2014887b8ad27fbb20893a22d6e39f26 + Message Hash = 2014887b8ad27fbb20893a22d6e39f26 +Test Completed Successfully + + +------------------------------------------------------ +Running Test 21: Exercise setting and getting of numeric data types +1234.567 parsed to 1234567 scale = -3 +As Float = 1.2345670000000000E+003 +-765.4321 parsed to -7654321 scale = -4 +As Float = -7.6543209999999999E+002 +0.1 parsed to 1 scale = -1 +As Float = 1.0000000000000001E-001 +0.01 parsed to 1 scale = -2 +As Float = 1.0000000000000000E-002 ++123 parsed to 123 scale = 0 +As Float = 1.2300000000000000E+002 +1.23456E308 parsed to 123456 scale = 303 +As Float = 1.2345600000000001E+308 +-1.2e-02 parsed to -12 scale = -3 +As Float = -1.2000000000000000E-002 +10. parsed to 10 scale = 0 +As Float = 1.0000000000000000E+001 +.12 parsed to 12 scale = -2 +As Float = 1.2000000000000000E-001 +0.12 parsed to 12 scale = -2 +As Float = 1.2000000000000000E-001 +Parsing of 1.2E1.2 failed +Parsing of 1,000 failed +Parsing of 1e1e1 failed +Parsing of 1.2+3 failed +Validating Numeric Interface - IFBNumeric +Value from Currency = 9999.1235 +Raw Value = 99991235 Scale = -4 +Value from Currency(rescaled) = 9999.12 +Raw Value = 999912 Scale = -2 +Value from Double = 9999.12345678 +Raw Value = 999912345678 Scale = -8 +Value from Integer = 9223372036854775807 +Raw Value = 9223372036854775807 Scale = 0 +Value from string = 9223372036854775807 +Raw Value = 9223372036854775807 Scale = 0 +Value from string = 9999.12345678 +Raw Value = 999912345678 Scale = -8 +Value from string = -0.12 +Raw Value = -12 Scale = -3 +Value from BCD = 9999.12345678 +Raw Value = 999912345678 Scale = -8 +Value from Raw Data = 9999123.456780 +Raw Value = 9999123456780 Scale = -6 +Numeric Operations +Add 2.23 + 24.12345 = 26.35345 +Add Double 2.23 + 24.12645 = 26.35645 +Add integer 2.23 + 2412345 = 2412347.23 +Subtract 2.23 - 24.12345 = -21.89345 +Subtract Double 24.12645 - 2.23 = -21.89645 +Subtract integer 24123.45 - 223 = -23900.45 +Multiply 2.23 * 24.12345 = 53.7952935 +Multiply Double 24.12645 * 2.23 = 53.8019835 +Multiply integer 241.2345 * 223 = 53795.2935 +Divide 24.12345 / 2.23 = 10.81769 +Divide Double 2.23 / 24.12645 = 0.09 +Divide integer 241.2345 / 223 = 1.0818 +Compare 2.23, -24.12345 = 1 +Compare integer 2.23, 3 = -1 +Compare Double 2.23, 2.23 = 0 +Negate 24.12345 = -24.12345 +SQL Params +SQLType =SQL_LONG +sub type = 0 +Field Name = +Scale = 0 +Charset id = 0 +Nullable +Size = 4 + +SQLType =SQL_LONG +sub type = 0 +Field Name = +Scale = 0 +Charset id = 0 +Nullable +Size = 4 + +SQLType =SQL_INT64 +sub type = 0 +Field Name = +Scale = 0 +Charset id = 0 +Nullable +Size = 8 + +SQLType =SQL_INT64 +sub type = 1 +Field Name = +Scale = -4 +Charset id = 0 +Nullable +Size = 8 + +SQLType =SQL_DOUBLE +sub type = 0 +Field Name = +Scale = 0 +Charset id = 0 +Nullable +Size = 8 + +SQLType =SQL_INT64 +sub type = 1 +Field Name = +Scale = -6 +Charset id = 0 +Nullable +Size = 8 + +Expected Error - SQLCODE: -413 +Overflow occurred during data type conversion. +Engine Code: 335544334 +conversion error from string "1,00" +Expected Error - SQLCODE: -413 +Overflow occurred during data type conversion. +Engine Code: 335544334 +conversion error from string "1,00" +Test Numeric Type +ROWID = 1 +ITYPE = 101 +I64TYPE = 9223372036854775807 +CURRTYPE = 10,000.12340000 +DTYPE = 9,999.12345678 +FIXEDPOINT = 1,234,567,890.12345700 +ROWID = 2 +ITYPE = -32457 +I64TYPE = -9223372036854775808 +CURRTYPE = 1,000,001.12000000 +DTYPE = 1.7E308 +FIXEDPOINT = -1,234,567,890.12345700 +ROWID = 3 +ITYPE = 0 +I64TYPE = 0 +CURRTYPE = .00000000 +DTYPE = .00000000 +FIXEDPOINT = .00000000 +ROWID = 4 +ITYPE = 1 +I64TYPE = 10 +CURRTYPE = .02300000 +DTYPE = .00110000 +FIXEDPOINT = 233.45600000 +ROWID = 7 +ITYPE = 1 +I64TYPE = 1234567 +CURRTYPE = .02300000 +DTYPE = .00110000 +FIXEDPOINT = 1,234.25000000 + + + +------------------------------------------------------ +Running Test 22: Journalling +Start Journaling. Session ID = 0 +1234.567 parsed to 1234567 scale = -3 +As Float = 1.2345670000000000E+003 +-765.4321 parsed to -7654321 scale = -4 +As Float = -7.6543209999999999E+002 +0.1 parsed to 1 scale = -1 +As Float = 1.0000000000000001E-001 +0.01 parsed to 1 scale = -2 +As Float = 1.0000000000000000E-002 ++123 parsed to 123 scale = 0 +As Float = 1.2300000000000000E+002 +1.23456E308 parsed to 123456 scale = 303 +As Float = 1.2345600000000001E+308 +-1.2e-02 parsed to -12 scale = -3 +As Float = -1.2000000000000000E-002 +10. parsed to 10 scale = 0 +As Float = 1.0000000000000000E+001 +.12 parsed to 12 scale = -2 +As Float = 1.2000000000000000E-001 +0.12 parsed to 12 scale = -2 +As Float = 1.2000000000000000E-001 +Parsing of 1.2E1.2 failed +Parsing of 1,000 failed +Parsing of 1e1e1 failed +Parsing of 1.2+3 failed +SQL Params +SQLType =SQL_LONG +sub type = 0 +Field Name = +Scale = 0 +Charset id = 0 +Nullable +Size = 4 + +SQLType =SQL_LONG +sub type = 0 +Field Name = +Scale = 0 +Charset id = 0 +Nullable +Size = 4 + +SQLType =SQL_INT64 +sub type = 0 +Field Name = +Scale = 0 +Charset id = 0 +Nullable +Size = 8 + +SQLType =SQL_INT64 +sub type = 1 +Field Name = +Scale = -4 +Charset id = 0 +Nullable +Size = 8 + +SQLType =SQL_DOUBLE +sub type = 0 +Field Name = +Scale = 0 +Charset id = 0 +Nullable +Size = 8 + +SQLType =SQL_INT64 +sub type = 1 +Field Name = +Scale = -6 +Charset id = 0 +Nullable +Size = 8 + + +Text Tests +SQL Params +SQLType =SQL_LONG +sub type = 0 +Field Name = +Scale = 0 +Charset id = 0 +Nullable +Size = 4 + +SQLType =SQL_VARYING +sub type = 0 +Field Name = +Scale = 0 +Charset id = 0 +Nullable +Size = 256 + +SQLType =SQL_BLOB +sub type = 1 +Field Name = +Scale = 0 +Charset id = 0 +Nullable +Size = 8 + + +Binary Blob Tests + +Array Test +SQL Params +SQLType =SQL_LONG +sub type = 0 +Field Name = +Scale = 0 +Charset id = 0 +Nullable +Size = 4 + +SQLType =SQL_ARRAY +sub type = 0 +Field Name = +Scale = 0 +Charset id = 0 +Nullable +Size = 8 + +ROWID = 1 +ITYPE = 101 +I64TYPE = 9223372036854775807 +CURRTYPE = 10,000.12340000 +DTYPE = 9,999.12345678 +FIXEDPOINT = 1,234,567,890.12345700 +STR = NULL +TEXTBLOB = NULL +OTHERBLOB = NULL +MYARRAY = NULL +ROWID = 2 +ITYPE = -32457 +I64TYPE = -9223372036854775808 +CURRTYPE = 1,000,001.12000000 +DTYPE = 1.7E308 +FIXEDPOINT = -1,234,567,890.12345700 +STR = NULL +TEXTBLOB = NULL +OTHERBLOB = NULL +MYARRAY = NULL +ROWID = 4 +ITYPE = 1 +I64TYPE = 10 +CURRTYPE = .02300000 +DTYPE = .00110000 +FIXEDPOINT = 233.45600000 +STR = NULL +TEXTBLOB = NULL +OTHERBLOB = NULL +MYARRAY = NULL + +Journal Table +IBX$SESSIONID = 1 +IBX$TRANSACTIONID = 9 +IBX$OLDTRANSACTIONID = NULL +IBX$USER = SYSDBA +IBX$CREATED = 2026/08/01 18:48:46.8540 +IBX$SESSIONID = 1 +IBX$TRANSACTIONID = 11 +IBX$OLDTRANSACTIONID = 10 +IBX$USER = SYSDBA +IBX$CREATED = 2026/08/01 18:48:46.8610 + +Journal Entries +Journal Entry = jeTransStart(Transaction Start) +Timestamp = yyyy/mm/dd hh:mm:ss.zzzz +Attachment ID = 3 +Session ID = 1 +Transaction ID = 9 +Transaction Name = "Transaction_29_1" +TPB: Item Count = 3 + isc_tpb_write + isc_tpb_nowait + isc_tpb_concurrency + +Default Completion = TACommit + +Journal Entry = jeQuery(Query) +Timestamp = yyyy/mm/dd hh:mm:ss.zzzz +Attachment ID = 3 +Session ID = 1 +Transaction ID = 9 +Query = Insert into TestData(RowID,iType,i64Type,CurrType,dType,FixedPoint) Values(1,101,9223372036854775807,10000.1234,9999.12345678,1234567890.12346) + +Journal Entry = jeQuery(Query) +Timestamp = yyyy/mm/dd hh:mm:ss.zzzz +Attachment ID = 3 +Session ID = 1 +Transaction ID = 9 +Query = Insert into TestData(RowID,iType,i64Type,CurrType,dType,FixedPoint) Values(2,-32457,-9223372036854775808,1000001.12,1.7E308,-1234567890.12346) + +Journal Entry = jeTransCommitRet(Commit Retaining) +Timestamp = yyyy/mm/dd hh:mm:ss.zzzz +Attachment ID = 3 +Session ID = 1 +Transaction ID = 10 +Old TransactionID = 9 + +Journal Entry = jeQuery(Query) +Timestamp = yyyy/mm/dd hh:mm:ss.zzzz +Attachment ID = 3 +Session ID = 1 +Transaction ID = 10 +Query = Insert into TestData(RowID,iType,i64Type,CurrType,dType,FixedPoint) Values(3,0,0,0,0,0) + +Journal Entry = jeTransRollbackRet(Rollback Retaining) +Timestamp = yyyy/mm/dd hh:mm:ss.zzzz +Attachment ID = 3 +Session ID = 1 +Transaction ID = 11 +Old TransactionID = 10 + +Journal Entry = jeQuery(Query) +Timestamp = yyyy/mm/dd hh:mm:ss.zzzz +Attachment ID = 3 +Session ID = 1 +Transaction ID = 11 +Query = Insert into TestData(RowID,iType,i64Type,CurrType,dType,FixedPoint) Values(4,1,10,0.023,0.0011,233.456) + +Journal Entry = jeTransStart(Transaction Start) +Timestamp = yyyy/mm/dd hh:mm:ss.zzzz +Attachment ID = 3 +Session ID = 1 +Transaction ID = 12 +Transaction Name = "Transaction_29_2" +TPB: Item Count = 3 + isc_tpb_write + isc_tpb_nowait + isc_tpb_concurrency + +Default Completion = TARollback + +Journal Entry = jeTransCommit(Commit) +Timestamp = yyyy/mm/dd hh:mm:ss.zzzz +Attachment ID = 3 +Session ID = 1 +Transaction ID = 11 + +Journal Entry = jeQuery(Query) +Timestamp = yyyy/mm/dd hh:mm:ss.zzzz +Attachment ID = 3 +Session ID = 1 +Transaction ID = 12 +Query = Insert into TestData(RowID, Str, TextBlob) Values(5,'It''s the quick brown fox jumps over the lazy dog','To be or not to be-that is the question: +Whether ''tis nobler in the mind to suffer +The slings and arrows of outrageous fortune, +Or to take arms against a sea of troubles, +And, by opposing, end them. To die, to sleep- +No more-and by a sleep to say we end +The heartache and the thousand natural shocks +That flesh is heir to-''tis a consummation +Devoutly to be wished. To die, to sleep- +To sleep, perchance to dream. Aye, there''s the rub, +For in that sleep of death what dreams may come, +When we have shuffled off this mortal coil, +Must give us pause. There''s the respect +That makes calamity of so long life. +For who would bear the whips and scorns of time, +Th'' oppressor''s wrong, the proud man''s contumely, +The pangs of despised love, the law’s delay, +The insolence of office, and the spurns +That patient merit of the unworthy takes, +When he himself might his quietus make +With a bare bodkin? Who would fardels bear, +To grunt and sweat under a weary life, +But that the dread of something after death, +The undiscovered country from whose bourn +No traveler returns, puzzles the will +And makes us rather bear those ills we have +Than fly to others that we know not of? +Thus conscience does make cowards of us all, +And thus the native hue of resolution +Is sicklied o''er with the pale cast of thought, +And enterprises of great pitch and moment, +With this regard their currents turn awry, +And lose the name of action.-Soft you now, +The fair Ophelia.-Nymph, in thy orisons +Be all my sins remembered +') + +Journal Entry = jeQuery(Query) +Timestamp = yyyy/mm/dd hh:mm:ss.zzzz +Attachment ID = 3 +Session ID = 1 +Transaction ID = 12 +Query = Insert into TestData(RowID,OtherBlob) Values (6, +FFD8FFE000104A46494600010101004800480000FFE11E0545786966000049492A00080000000C00 +0E010200200000009E0000000F01020014000000BE0000001001020008000000D200000012010300 +01000000010000001A01050001000000DA000000 + +) + +Journal Entry = jeQuery(Query) +Timestamp = yyyy/mm/dd hh:mm:ss.zzzz +Attachment ID = 3 +Session ID = 1 +Transaction ID = 12 +Query = Insert into TestData(RowID,MyArray) Values (7, + 100 + 99 + 98 + 97 + 96 + 95 + 94 + 93 + 92 + 91 + 90 + 89 + 88 + 87 + 86 + 85 + 84 + +) + +Journal Entry = jeTransRollback(Rollback) +Timestamp = yyyy/mm/dd hh:mm:ss.zzzz +Attachment ID = 3 +Session ID = 1 +Transaction ID = 12 + + + +------------------------------------------------------ +Test Suite Ends diff --git a/testsuite/Makefile.fpc b/testsuite/Makefile.fpc index 2b517a21..302ca97b 100644 --- a/testsuite/Makefile.fpc +++ b/testsuite/Makefile.fpc @@ -7,7 +7,7 @@ options= -MObjFPC -Scghi -Cg -Cr -O1 -g -gl -gh -l -vewnhibq -dUseCThreads unittargetdir=testunits/$(CPU_TARGET)-$(OS_TARGET) includedir=../client/3.0/firebird ../client/include ../include -unitdir=testApp ../client ../client/3.0/firebird ../client/2.5 . ../client/3.0 .. +unitdir=testApp ../client ../client/3.0/firebird ../client/2.5 . ../client/3.0 ../client/wire .. [target] programs=testsuite diff --git a/testsuite/Test10.pas b/testsuite/Test10.pas index 736fdb71..370f2dd8 100644 --- a/testsuite/Test10.pas +++ b/testsuite/Test10.pas @@ -107,6 +107,8 @@ procedure TTest10.EventsTest(Attachment: IAttachment); Sleep(50); CheckSynchronize; end; + Sleep(50); + CheckSynchronize; {ensure the event report is printed first} ShowEventCounts(EventHandler); FEventSignalled := false; @@ -121,8 +123,18 @@ procedure TTest10.EventsTest(Attachment: IAttachment); writeln(OutFile,'Call Async Wait'); EventHandler.AsyncWaitForEvent(EventReport); writeln(OutFile,'Async Wait Called'); - sleep(500); - CheckSynchronize; + {the deferred delivery is immediate in protocol terms but its arrival + time depends on the machine: poll with a generous limit so that a + loaded test host does not turn it into a missed event} + WaitCount := 0; + while not FEventSignalled and (WaitCount < 5000) do + begin + Sleep(50); + CheckSynchronize; + Inc(WaitCount,50); + end; + Sleep(50); + CheckSynchronize; {ensure the event report is printed first} if FEventSignalled then begin writeln(OutFile,'Deferred Events Caught'); @@ -142,6 +154,16 @@ procedure TTest10.EventsTest(Attachment: IAttachment); FEventSignalled := false; writeln(OutFile,'Async Wait: Test Cancel'); EventHandler.AsyncWaitForEvent(EventReport); + {allow the immediate delivery to be reported before continuing, so that + the output order is deterministic even on a loaded test host} + WaitCount := 0; + while not FEventSignalled and (WaitCount < 5000) do + begin + Sleep(50); + CheckSynchronize; + Inc(WaitCount,50); + end; + Sleep(50); CheckSynchronize; writeln(OutFile,'Async Wait Called'); EventHandler.Cancel; @@ -201,6 +223,12 @@ procedure TTest10.RunTest(CharSet: AnsiString; SQLDialect: integer); DPB.Add(isc_dpb_lc_ctype).setAsString(CharSet); DPB.Find(isc_dpb_password).setAsString(Owner.GetPassword); Attachment := FirebirdAPI.OpenDatabase(Owner.GetEmployeeDatabaseName,DPB); + if not Attachment.HasEventSupport then + begin + writeln(OutFile,'Skipping test - events are not supported by this provider'); + Attachment.Disconnect; + Exit; + end; EventsTest(Attachment); Attachment.Disconnect; end; diff --git a/testsuite/Test13.pas b/testsuite/Test13.pas index b73cbf6f..7356e583 100644 --- a/testsuite/Test13.pas +++ b/testsuite/Test13.pas @@ -168,9 +168,18 @@ procedure TTest13.RunTest(CharSet: AnsiString; SQLDialect: integer); UpdateDatabase(Attachment2); QueryDatabase(Attachment2); - Transaction := FirebirdAPI.StartTransaction( - [Attachment,Attachment2], - [isc_tpb_write,isc_tpb_nowait,isc_tpb_concurrency], taCommit); + try + Transaction := FirebirdAPI.StartTransaction( + [Attachment,Attachment2], + [isc_tpb_write,isc_tpb_nowait,isc_tpb_concurrency], taCommit); + except on E: EIBClientError do + begin + writeln(OutFile,'Skipping test - multi database transactions are not supported by this provider'); + Attachment.DropDatabase; + Attachment2.DropDatabase; + Exit; + end; + end; ModifyDatabase1(Attachment,Transaction); ModifyDatabase2(Attachment2,Transaction); diff --git a/testsuite/Test17.pas b/testsuite/Test17.pas index 3f10dda8..20774ac4 100644 --- a/testsuite/Test17.pas +++ b/testsuite/Test17.pas @@ -69,6 +69,7 @@ interface TTest17 = class(TFBTestBase) private FOnDate: TDateTime; //used for Time with Time Zone conversions + function HasTimeZoneServices(Attachment: IAttachment): boolean; procedure TestArrayTZDataTypes(Attachment: IAttachment); procedure TestFBTimezoneSettings(Attachment: IAttachment); procedure UpdateDatabase(Attachment: IAttachment); @@ -117,6 +118,16 @@ implementation { TTest17 } +function TTest17.HasTimeZoneServices(Attachment: IAttachment): boolean; +begin + Result := true; + try + Attachment.GetTimeZoneServices; + except on E: EIBClientError do + Result := false; + end; +end; + procedure TTest17.TestArrayTZDataTypes(Attachment: IAttachment); var Transaction: ITransaction; Statement: IStatement; @@ -424,6 +435,9 @@ procedure TTest17.RunTest(CharSet: AnsiString; SQLDialect: integer); if (FirebirdAPI.GetClientMajor < 4) or (Attachment.GetODSMajorVersion < 13) then writeln(OutFile,'Skipping Firebird 4 and later test part') else + if not HasTimeZoneServices(Attachment) then + writeln(OutFile,'Skipping Firebird 4 and later test part - time zone services are not supported by this provider') + else begin writeln(OutFile); writeln(OutFile,'Firebird 4 extension types'); diff --git a/testsuite/Test18.pas b/testsuite/Test18.pas index 77250153..c879569c 100644 --- a/testsuite/Test18.pas +++ b/testsuite/Test18.pas @@ -285,7 +285,10 @@ procedure TTest18.RunTest(CharSet: AnsiString; SQLDialect: integer); Attachment.ExecImmediate([isc_tpb_write,isc_tpb_wait,isc_tpb_consistency],sqlCreateTable); UpdateDatabase4_DECFloat(Attachment); QueryDatabase4_DECFloat(Attachment); - ArrayTest(Attachment); + if Attachment.HasArraySupport then + ArrayTest(Attachment) + else + writeln(OutFile,'Skipping array test - arrays are not supported by this provider'); end; Attachment.DropDatabase; end; diff --git a/testsuite/Test2.pas b/testsuite/Test2.pas index 3f40e9a6..5bfdf2b1 100644 --- a/testsuite/Test2.pas +++ b/testsuite/Test2.pas @@ -175,6 +175,13 @@ procedure TTest2.RunTest(CharSet: AnsiString; SQLDialect: integer); end; Attachment.Disconnect; writeln(OutFile,'Now open the employee database as a local database'); + if FirebirdAPI.GetFBLibrary = nil then + begin + {a wire protocol provider has no embedded mode - a local attach cannot + authenticate without a password} + writeln(OutFile,'Skipping local database test - not supported by provider'); + Exit; + end; DPB := FirebirdAPI.AllocateDPB; DPB.Add(isc_dpb_lc_ctype).setAsString(CharSet); DPB.Add(isc_dpb_user_name).setAsString(Owner.GetUserName); diff --git a/testsuite/Test20.pas b/testsuite/Test20.pas index ced0cb6b..f302da90 100644 --- a/testsuite/Test20.pas +++ b/testsuite/Test20.pas @@ -205,6 +205,9 @@ procedure TTest20.RunTest(CharSet: AnsiString; SQLDialect: integer); if (FirebirdAPI.GetClientMajor < 4) or (Attachment.GetODSMajorVersion < 13) then writeln(OutFile,'Skipping test for Firebird 4 and later') else + if not Attachment.HasBatchMode then + writeln(OutFile,'Skipping test - batch mode is not supported by this provider') + else begin Attachment.ExecImmediate([isc_tpb_write,isc_tpb_wait,isc_tpb_consistency],sqlCreateTable); try diff --git a/testsuite/Test22.pas b/testsuite/Test22.pas index f19f38f2..58282afe 100644 --- a/testsuite/Test22.pas +++ b/testsuite/Test22.pas @@ -160,6 +160,11 @@ procedure TTest22.UpdateDatabase(Attachment: IAttachment); Statement.Execute; writeln(OutFile); writeln(OutFile,'Array Test'); + if not Attachment.HasArraySupport then + begin + writeln(OutFile,'Skipping array test - arrays are not supported by this provider'); + Exit; + end; Statement := Attachment.Prepare(Transaction,sqlInsertArray); ar := Attachment.CreateArray(Transaction,'TestData','MyArray'); j := 100; diff --git a/testsuite/Test7.pas b/testsuite/Test7.pas index 91ef8077..993aca91 100644 --- a/testsuite/Test7.pas +++ b/testsuite/Test7.pas @@ -208,6 +208,12 @@ procedure TTest7.RunTest(CharSet: AnsiString; SQLDialect: integer); DPB.Add(isc_dpb_lc_ctype).setAsString(CharSet); DPB.Add(isc_dpb_set_db_SQL_dialect).setAsByte(SQLDialect); Attachment := FirebirdAPI.CreateDatabase(Owner.GetNewDatabaseName,DPB); + if not Attachment.HasArraySupport then + begin + writeln(OutFile,'Skipping test - arrays are not supported by this provider'); + Attachment.DropDatabase; + Exit; + end; Attachment.ExecImmediate([isc_tpb_write,isc_tpb_wait,isc_tpb_consistency],sqlCreateTable); UpdateDatabase(Attachment); diff --git a/testsuite/Test8.pas b/testsuite/Test8.pas index 7825827a..e4846eb8 100644 --- a/testsuite/Test8.pas +++ b/testsuite/Test8.pas @@ -138,6 +138,12 @@ procedure TTest8.RunTest(CharSet: AnsiString; SQLDialect: integer); DPB.Add(isc_dpb_lc_ctype).setAsString(CharSet); DPB.Add(isc_dpb_set_db_SQL_dialect).setAsByte(SQLDialect); Attachment := FirebirdAPI.CreateDatabase(Owner.GetNewDatabaseName,DPB); + if not Attachment.HasArraySupport then + begin + writeln(OutFile,'Skipping test - arrays are not supported by this provider'); + Attachment.DropDatabase; + Exit; + end; Attachment.ExecImmediate([isc_tpb_write,isc_tpb_wait,isc_tpb_consistency],sqlCreateTable); UpdateDatabase(Attachment); diff --git a/testsuite/WireTest.pas b/testsuite/WireTest.pas new file mode 100644 index 00000000..05f4a57e --- /dev/null +++ b/testsuite/WireTest.pas @@ -0,0 +1,1775 @@ +(* + * Firebird Interface (fbintf). Regression test for the pure Pascal wire + * protocol client (client/wire). + * + * Contents of this file are subject to the Initial Developer's Public + * License Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License here: + * + * http://www.firebirdsql.org/index.php?op=doc&id=idpl + * + * Software distributed under the License is distributed on an "AS + * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or + * implied. See the License for the specific language governing rights + * and limitations under the License. + * + * The Initial Developer of the Original Code is MWA Software + * (http://www.mwasoftware.co.uk). + * + * All Rights Reserved. + * + * Contributor(s): ______________________________________. + * + * Usage: WireTest [ [ [ []]]] + * defaults to localhost:employee SYSDBA masterkey + * + * The cryptographic tests need no server. The remaining tests need a + * Firebird 3.0 or later server and a database the user may create tables + * in. If a scratch database is named as well then creating and dropping a + * database is tested too; it must be a path the server may write to and + * must not already exist. +*) +program WireTest; + +{$mode delphi}{$H+} + +uses + {$IFDEF UNIX}cthreads,{$ENDIF} + SysUtils, Classes, IB, IBUtils, IBErrorCodes, + FBWireBigInt, FBWireCrypto, FBWireSRP, FBWireStream, FBWireConst, + FBWireMessage, FBWireDescribe, FBWireProtocol, FBWireClientAPI, + FBWireAttachment, FBWireStatement, FBWireMessages, FBAttachment; + +var + TestsRun: integer = 0; + TestsFailed: integer = 0; + DatabaseName: AnsiString = 'localhost:employee'; + UserName: AnsiString = 'SYSDBA'; + Password: AnsiString = 'masterkey'; + ScratchDatabase: AnsiString = ''; + +procedure Check(const aTest: AnsiString; aCondition: boolean; + const aDetail: AnsiString = ''); +begin + Inc(TestsRun); + if aCondition then + writeln(' ok ',aTest) + else + begin + Inc(TestsFailed); + if aDetail <> '' then + writeln(' FAIL ',aTest,' - ',aDetail) + else + writeln(' FAIL ',aTest); + end; + {flushed so that the log is complete even if the next call raises} + Flush(Output); +end; + +procedure CheckEquals(const aTest, aGot, aWanted: AnsiString); +begin + Check(aTest,aGot = aWanted,'got "' + aGot + '" wanted "' + aWanted + '"'); +end; + +function BytesToHex(const b: array of byte): AnsiString; +var i: integer; +begin + Result := ''; + for i := 0 to High(b) do + Result := Result + LowerCase(IntToHex(b[i],2)); +end; + +function StrToBytes(const s: AnsiString): TBytes; +begin + SetLength(Result,Length(s)); + if s <> '' then + Move(s[1],Result[0],Length(s)); +end; + +{---------------------------------------------------------------------------} + +procedure TestBigInt; +var a, b, q, r, m: TBigInt; +begin + writeln('Arbitrary precision arithmetic'); + CheckEquals('zero',TBigInt.FromHex('0').ToHex,'0'); + CheckEquals('hex round trip',TBigInt.FromHex('deadbeef12345678').ToHex, + 'deadbeef12345678'); + a := TBigInt.FromHex('ffffffffffffffffffffffff'); + b := TBigInt.FromHex('1'); + CheckEquals('add with carry',TBigInt.Add(a,b).ToHex, + '1000000000000000000000000'); + CheckEquals('subtract with borrow', + TBigInt.Subtract(TBigInt.Add(a,b),b).ToHex, + 'ffffffffffffffffffffffff'); + a := TBigInt.FromHex('123456789abcdef0'); + b := TBigInt.FromHex('fedcba9876543210'); + CheckEquals('multiply',TBigInt.Multiply(a,b).ToHex, + '121fa00ad77d7422236d88fe5618cf00'); + a := TBigInt.FromHex('121fa00ad77d7422236d88fe5618cf01'); + TBigInt.DivMod(a,b,q,r); + CheckEquals('divide quotient',q.ToHex,'123456789abcdef0'); + CheckEquals('divide remainder',r.ToHex,'1'); + CheckEquals('modular exponentiation', + TBigInt.ModPow(TBigInt.FromCardinal($1234),TBigInt.FromCardinal($5678), + TBigInt.FromCardinal($FFFF1)).ToHex,'4470e'); + {a 1024 bit modular exponentiation of the size SRP performs} + m := TBigInt.FromHex( + 'E67D2E994B2F900C3F41F08F5BB2627ED0D49EE1FE767A52EFCD565CD6E76881' + + '2C3E1E9CE8F0A8BEA6CB13CD29DDEBF7A96D4A93B55D488DF099A15C89DCB064' + + '0738EB2CBDD9A8F7BAB561AB1B0DC1C6CDABF303264A08D1BCA932D1F1EE428B' + + '619D970F342ABA9A65793B8B2F041AE5364350C16F735F56ECBCA87BD57B29E7'); + a := TBigInt.FromHex( + 'deadbeefcafebabe0123456789abcdefdeadbeefcafebabe0123456789abcdef'); + b := TBigInt.FromHex('123456789abcdef0fedcba9876543210'); + CheckEquals('1024 bit modular exponentiation',TBigInt.ModPow(a,b,m).ToHex, + 'c05da5ced840733fcb10af56ed841a5835aab6fd03750959bde9fa367f6b9406' + + '85f361546298c87db8bd8b68b4eb6bd66c34820b97e820db34020a6f341caa71' + + '16cefe89032df8791931996cf6596444ddf50b5a3b667c004fe2b16599fa925b' + + '8fd922586989ca91ddaa1bb24389c88d5cea74d06b01c54cacd6f42796f03d0d'); +end; + +procedure TestCrypto; +var ctx: TSHA1; + rc4: TRC4; + cc: TChaCha20; + buf, key, nonce, block: TBytes; + i: integer; +begin + writeln('Cryptographic primitives'); + CheckEquals('SHA-1 of "abc"',BytesToHex(TSHA1.Digest(StrToBytes('abc'))), + 'a9993e364706816aba3e25717850c26c9cd0d89d'); + CheckEquals('SHA-1 of the empty string', + BytesToHex(TSHA1.Digest(StrToBytes(''))), + 'da39a3ee5e6b4b0d3255bfef95601890afd80709'); + CheckEquals('SHA-1 of the 448 bit test vector', + BytesToHex(TSHA1.Digest(StrToBytes( + 'abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq'))), + '84983e441c3bd26ebaae4aa1f95129e5e54670f1'); + {a million 'a' - exercises the streaming path across many blocks} + ctx.Init; + SetLength(block,10000); + for i := 0 to High(block) do block[i] := ord('a'); + for i := 1 to 100 do ctx.Update(block); + CheckEquals('SHA-1 of one million characters',BytesToHex(ctx.Final), + '34aa973cd4c4daa4f61eeb2bdbad27316534016f'); + CheckEquals('SHA-256 of "abc"', + BytesToHex(TSHA256.Digest(StrToBytes('abc'))), + 'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad'); + CheckEquals('SHA-256 of the empty string', + BytesToHex(TSHA256.Digest(StrToBytes(''))), + 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'); + CheckEquals('SHA-256 of the 448 bit test vector', + BytesToHex(TSHA256.Digest(StrToBytes( + 'abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq'))), + '248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1'); + + rc4 := TRC4.Create(StrToBytes('Key')); + try + buf := StrToBytes('Plaintext'); + rc4.Process(buf[0],Length(buf)); + CheckEquals('RC4',BytesToHex(buf),'bbf316e8d940af0ad3'); + finally + rc4.Free; + end; + + {RFC 8439 section 2.4.2} + SetLength(key,32); + for i := 0 to 31 do key[i] := i; + nonce := TBytes.Create($00,$00,$00,$00,$00,$00,$00,$4a,$00,$00,$00,$00); + cc := TChaCha20.Create(key,nonce,1); + try + buf := StrToBytes('Ladies and Gentlemen of the class of ''99: If I could ' + + 'offer you only one tip for the future, sunscreen would be it.'); + cc.Process(buf[0],Length(buf)); + CheckEquals('ChaCha20',Copy(BytesToHex(buf),1,64), + '6e2e359a2568f98041ba0728dd0d6981e97e7aec1d4360c20a27afccfd9fae0b'); + finally + cc.Free; + end; +end; + +procedure TestSRP; +const + {a fixed client private key so that the exchange is reproducible} + ClientPrivateKey = + '84316857F47914F838918D5C12CE3A3E7A9B2D7C9486346809E9EEFCE8DE7CD4' + + '259D8BE4FD0BCC2D259553769E078FA61EE2977025E4DA42F7FD97914D8A3372' + + '3DFAFBC00770B7DA0C2E3778A05790F0C0F33C32A19ED88A12928567749021B3' + + 'FD45DCD1CE259C45325067E3DDC972F87867349BA82C303CCCAA9B207218007B'; + {computed independently from the same inputs} + ExpectedA = + '7b00bb84b35d25c54508808adff5fe3483f7b3010c805a4a2baf13342c812b08' + + '6fe04d9bce76255ac24b915022ba696e022ff1202c7b08fac98163e1bb574465' + + '073e871d8f6a9d7e49d318b768124e69cce4dab195beeb0ddcf5bddf5a7e59c6' + + 'aae1a699ff03173dc7fb93eda3b7043f7021561d36977430cccbed429f2c3f68'; + ServerB = + 'e04008c63e7770099cd0fb48c14e8fa7e08a8c12f3bc20df5ee513f3486a1b93' + + '9968fb22b9d8c590d101f009c9330afc01bd4a45395c23edb4427d8ce0615755' + + '097d76e9e2e851de03cc5995adb3835766b798fdfd5e1b3cb8d12d48d21e8570' + + 'bba4dea73e5031bd851d5ac54449014bf4bdb74be08bfc31c80f478b0cb6b9c'; + ExpectedSessionKey = '9b374b7ed7d8c4e5c98b273546b302857e874f18'; + ExpectedProofSHA1 = '8494f6eb8d3891373843464a0cc083e49d13fad3'; + ExpectedProofSHA256 = + '59f5bc74dc8780d4fe109acb1cfc83ff937bf2236dc2b1f531ccbc4968d0d507'; +var srp: TSRPClient; + salt, proof: TBytes; + i: integer; +begin + writeln('SRP-6a client'); + SetLength(salt,32); + for i := 0 to 31 do salt[i] := i; + + srp := TSRPClient.Create(ClientPrivateKey); + try + CheckEquals('client public key',srp.PublicKeyHex,ExpectedA); + proof := srp.ClientProof('SYSDBA','masterkey',salt,ServerB,sphSHA1); + CheckEquals('session key',BytesToHex(srp.SessionKey),ExpectedSessionKey); + CheckEquals('client proof (Srp)',BytesToHex(proof),ExpectedProofSHA1); + finally + srp.Free; + end; + + srp := TSRPClient.Create(ClientPrivateKey); + try + {the account name is upper cased before hashing, so a lower case login + must produce the same proof} + proof := srp.ClientProof('sysdba','masterkey',salt,ServerB,sphSHA256); + CheckEquals('client proof (Srp256, lower case login)', + BytesToHex(proof),ExpectedProofSHA256); + finally + srp.Free; + end; + + srp := TSRPClient.Create; + try + Check('random key pair is generated',srp.PublicKeyHex <> ''); + finally + srp.Free; + end; +end; + +procedure TestMessageLayout; +var fmt: TWireMessageFormat; + size: cardinal; + blr: TBytes; + quad: array[0..7] of byte; +begin + writeln('Message layout and BLR'); + SetLength(fmt,3); + FillChar(fmt[0],SizeOf(TWireSQLVar)*3,0); + fmt[0].SQLType := SQL_SHORT; + fmt[0].DataSize := 2; + fmt[1].SQLType := SQL_VARYING; + fmt[1].DataSize := 10; + fmt[1].CharSetID := 4; + fmt[2].SQLType := SQL_INT64; + fmt[2].DataSize := 8; + fmt[2].Scale := -2; + size := ComputeMessageLayout(fmt); + Check('short is at offset zero',fmt[0].DataOffset = 0); + Check('varying is two byte aligned',fmt[1].DataOffset mod 2 = 0); + Check('int64 is eight byte aligned',fmt[2].DataOffset mod 8 = 0); + Check('null indicators follow the data', + fmt[0].NullOffset >= fmt[2].DataOffset + 8); + Check('buffer size matches the layout',MessageBufferSize(fmt) = size); + + blr := BuildMessageBlr(fmt); + Check('BLR starts with version 5 and begin', + (blr[0] = blr_version5) and (blr[1] = blr_begin)); + Check('BLR declares a message',blr[2] = blr_message); + Check('BLR field count is twice the column count', + (blr[4] or (blr[5] shl 8)) = 6); + Check('text is described with its character set', + blr[Length(blr)-1] = blr_eoc); + {the varying descriptor must carry the character set, i.e. blr_varying2} + Check('varying uses blr_varying2',Pos(AnsiChar(blr_varying2), + AnsiString(PAnsiChar(@blr[0]))) >= 0); + + {ISC_QUAD conversion: the high word is stored first} + Int64ToWireQuad(@quad[0],$1122334455667788); + Check('the high word occupies the first four bytes of a quad', + PInteger(@quad[0])^ = $11223344); + Check('the low word occupies the last four bytes of a quad', + PCardinal(@quad[4])^ = $55667788); + Check('quad round trips',WireQuadToInt64(@quad[0]) = $1122334455667788); + + {the batch message length must follow the server's PARSE_msg_format + rules, with a two byte null indicator after each value: + short(2)@0 + null(2)@2, varying(10+2)@4 + null(2)@16, + int64(8)@24 (eight byte aligned) + null(2)@32 = 34} + Check('engine message length follows the server''s layout rules', + EngineMessageLength(fmt) = 34, + 'got ' + IntToStr(EngineMessageLength(fmt))); +end; + +{---------------------------------------------------------------------------} + +{splits the connect string into the parts the raw protocol layer needs} +procedure SplitConnectString(const aConnectString: AnsiString; + out aHost, aDatabase: AnsiString; out aPort: integer); +var aProtocol: TProtocolAll; + aPortText: AnsiString; +begin + aHost := 'localhost'; + aDatabase := aConnectString; + aPort := 3050; + aPortText := ''; + if ParseConnectString(aConnectString,aHost,aDatabase,aProtocol,aPortText) then + begin + if aPortText <> '' then + aPort := StrToIntDef(aPortText,3050); + if aHost = '' then + aHost := 'localhost'; + end; +end; + +function ConnectToServer(out Connection: TFBWireConnection): boolean; +var host, dbname: AnsiString; + port: integer; +begin + Result := false; + Connection := nil; + SplitConnectString(DatabaseName,host,dbname,port); + Connection := TFBWireConnection.Create; + try + Connection.ConnectTo(host,port,dbname,UserName,Password); + Result := true; + except + on E: Exception do + begin + writeln(' SKIP no server at ',host,':',port,' - ',E.Message); + FreeAndNil(Connection); + end; + end; +end; + +procedure TestProtocol; +var C: TFBWireConnection; +begin + writeln('Live connection'); + if not ConnectToServer(C) then Exit; + try + Check('a protocol version was negotiated', + (C.ProtocolVersion >= 13) and (C.ProtocolVersion <= 20), + 'got ' + IntToStr(C.ProtocolVersion)); + Check('an SRP plugin authenticated the connection', + (C.AuthPluginName = sSrpPluginName) or + (C.AuthPluginName = sSrp256PluginName), + 'plugin ' + C.AuthPluginName); + Check('wire encryption was negotiated',C.CryptPlugin <> '', + 'the server may have WireCrypt disabled'); + if C.CryptPlugin = '' then + writeln(' protocol ',C.ProtocolVersion,', ',C.AuthPluginName, + ', no wire encryption') + else + writeln(' protocol ',C.ProtocolVersion,', ',C.AuthPluginName, + ', ',C.CryptPlugin,' wire encryption'); + finally + C.Free; + end; +end; + +procedure TestProtocolNegotiation; +const Caps: array[0..6] of cardinal = (PROTOCOL_VERSION14,PROTOCOL_VERSION15, + PROTOCOL_VERSION16,PROTOCOL_VERSION17, + PROTOCOL_VERSION18,PROTOCOL_VERSION19, + PROTOCOL_VERSION20); +var C: TFBWireConnection; + host, dbname: AnsiString; + port, i: integer; +begin + writeln('Protocol negotiation'); + SplitConnectString(DatabaseName,host,dbname,port); + for i := 0 to High(Caps) do + begin + C := TFBWireConnection.Create; + try + try + C.MaxProtocol := Caps[i]; + C.ConnectTo(host,port,dbname,UserName,Password); + {the server settles on the highest version it also knows, so an + older server correctly negotiates below the cap: Firebird 3 tops + out at 15 however high the offer goes} + Check(Format('offering up to protocol %d negotiates %d', + [Caps[i] and FB_PROTOCOL_MASK,C.ProtocolVersion]), + (C.ProtocolVersion >= 13) and + (C.ProtocolVersion <= (Caps[i] and FB_PROTOCOL_MASK)), + 'got ' + IntToStr(C.ProtocolVersion)); + except + on E: Exception do + {an old server simply may not know this version} + writeln(' SKIP protocol ',Caps[i] and FB_PROTOCOL_MASK,': ',E.Message); + end; + finally + C.Free; + end; + end; +end; + +{---------------------------------------------------------------------------} + +var + API: IFirebirdAPI; + Attachment: IAttachment; + +function OpenTestDatabase: boolean; +var DPB: IDPB; +begin + Result := false; + API := WireFirebirdAPI; + DPB := API.AllocateDPB; + DPB.Add(isc_dpb_user_name).AsString := UserName; + DPB.Add(isc_dpb_password).AsString := Password; + DPB.Add(isc_dpb_lc_ctype).AsString := 'UTF8'; + try + Attachment := API.OpenDatabase(DatabaseName,DPB); + Result := (Attachment <> nil) and Attachment.IsConnected; + except + on E: Exception do + writeln(' SKIP cannot attach to ',DatabaseName,': ',E.Message); + end; +end; + +procedure TestCreateDatabase; +var DPB: IDPB; + Scratch: IAttachment; + Tr: ITransaction; + RS: IResultSet; +begin + writeln('Provider: create and drop database'); + if ScratchDatabase = '' then + begin + writeln(' SKIP no scratch database named on the command line'); + Exit; + end; + DPB := API.AllocateDPB; + DPB.Add(isc_dpb_user_name).AsString := UserName; + DPB.Add(isc_dpb_password).AsString := Password; + DPB.Add(isc_dpb_lc_ctype).AsString := 'UTF8'; + DPB.Add(isc_dpb_set_db_SQL_dialect).AsByte := 3; + DPB.Add(isc_dpb_page_size).AsInteger := 8192; + + Scratch := API.CreateDatabase(ScratchDatabase,DPB); + Check('database created',(Scratch <> nil) and Scratch.IsConnected); + if Scratch = nil then Exit; + Check('the new database has a current ODS',Scratch.GetODSMajorVersion >= 11, + 'ODS ' + IntToStr(Scratch.GetODSMajorVersion)); + + Tr := Scratch.StartTransaction([isc_tpb_read_committed,isc_tpb_rec_version, + isc_tpb_nowait,isc_tpb_write],taCommit); + Scratch.ExecImmediate(Tr,'create table SCRATCH (ID integer not null primary key)'); + Tr.Commit; + Tr := Scratch.StartTransaction([isc_tpb_read_committed,isc_tpb_rec_version, + isc_tpb_nowait,isc_tpb_write],taCommit); + RS := Scratch.OpenCursorAtStart(Tr, + 'select count(*) from RDB$RELATIONS where RDB$RELATION_NAME = ''SCRATCH'''); + Check('a table can be created in the new database',RS[0].AsInteger = 1); + RS.Close; + RS := nil; + Tr.Commit; + Tr := nil; + + Scratch.DropDatabase; + Check('database dropped',not Scratch.IsConnected); + Scratch := nil; + + {the database must now be gone} + Scratch := nil; + try + Scratch := API.OpenDatabase(ScratchDatabase,DPB,false); + except + on E: Exception do Scratch := nil; + end; + Check('a dropped database can no longer be attached', + (Scratch = nil) or not Scratch.IsConnected); + Scratch := nil; +end; + +procedure TestProviderQueries; +var Tr: ITransaction; + S: IStatement; + RS: IResultSet; + rows: integer; +begin + writeln('Provider: queries'); + Check('attached',Attachment.IsConnected); + Check('ODS version is 11 or later',Attachment.GetODSMajorVersion >= 11, + 'ODS ' + IntToStr(Attachment.GetODSMajorVersion)); + Check('SQL dialect is 3',Attachment.GetSQLDialect = 3); + + Tr := Attachment.StartTransaction([isc_tpb_read_committed, + isc_tpb_rec_version,isc_tpb_nowait,isc_tpb_write],taCommit); + Check('transaction started',Tr.GetInTransaction); + Check('transaction has an id',Tr.GetTransactionID > 0); + + S := Attachment.Prepare(Tr,'select CAST(? AS INTEGER) + 1 as RESULT from RDB$DATABASE'); + Check('statement prepared',S.IsPrepared); + Check('one parameter described',S.SQLParams.Count = 1); + Check('one column described',S.MetaData.Count = 1); + S.SQLParams[0].AsInteger := 41; + RS := S.OpenCursor; + Check('cursor returns a row',RS.FetchNext); + Check('parameter reached the server',RS[0].AsInteger = 42, + 'got ' + IntToStr(RS[0].AsInteger)); + Check('cursor is exhausted',not RS.FetchNext); + RS.Close; + + {a null parameter must arrive as null: cast it so that the engine can + describe the parameter type} + S := Attachment.Prepare(Tr, + 'select CAST(? AS VARCHAR(10)) as VAL from RDB$DATABASE'); + S.SQLParams[0].IsNull := true; + RS := S.OpenCursor; + RS.FetchNext; + Check('a null parameter is sent as null',RS[0].IsNull); + RS.Close; + + {multiple rows through the batch cache} + rows := 0; + RS := Attachment.OpenCursor(Tr,'select RDB$RELATION_ID from RDB$RELATIONS'); + while RS.FetchNext do + Inc(rows); + RS.Close; + Check('many rows fetched across batches',rows > 10, + IntToStr(rows) + ' rows'); + + Tr.Commit; + Check('transaction committed',not Tr.GetInTransaction); +end; + +procedure TestProviderDataTypes; +var Tr: ITransaction; + RS: IResultSet; +begin + writeln('Provider: data types'); + Tr := Attachment.StartTransaction([isc_tpb_read_committed, + isc_tpb_rec_version,isc_tpb_nowait,isc_tpb_write],taCommit); + RS := Attachment.OpenCursorAtStart(Tr, + 'select CAST(-32768 AS SMALLINT) C_SMALL,' + + ' CAST(-2147483648 AS INTEGER) C_INT,' + + ' CAST(-9223372036854775808 AS BIGINT) C_BIG,' + + ' CAST(3.25 AS DOUBLE PRECISION) C_DBL,' + + ' CAST(''2024-02-29'' AS DATE) C_DATE,' + + ' CAST(''13:45:56.1234'' AS TIME) C_TIME,' + + ' CAST(''2024-02-29 13:45:56.1234'' AS TIMESTAMP) C_TS,' + + ' CAST(''abc'' AS VARCHAR(10)) C_STR,' + + ' CAST(12345.67 AS NUMERIC(15,2)) C_NUM,' + + ' CAST(NULL AS INTEGER) C_NULL' + + ' from RDB$DATABASE'); + Check('smallint',RS.ByName('C_SMALL').AsInteger = -32768); + Check('integer',RS.ByName('C_INT').AsInteger = -2147483648); + Check('bigint',RS.ByName('C_BIG').AsInt64 = Low(Int64)); + Check('double',Abs(RS.ByName('C_DBL').AsDouble - 3.25) < 1E-9); + CheckEquals('date',FormatDateTime('yyyy-mm-dd',RS.ByName('C_DATE').AsDateTime), + '2024-02-29'); + CheckEquals('time',FormatDateTime('hh:nn:ss',RS.ByName('C_TIME').AsDateTime), + '13:45:56'); + CheckEquals('timestamp', + FormatDateTime('yyyy-mm-dd hh:nn:ss',RS.ByName('C_TS').AsDateTime), + '2024-02-29 13:45:56'); + CheckEquals('varchar',RS.ByName('C_STR').AsString,'abc'); + CheckEquals('scaled numeric',RS.ByName('C_NUM').AsString,'12345.67'); + Check('null column',RS.ByName('C_NULL').IsNull); + Tr.Commit; +end; + +procedure TestProviderUpdates; +const TestTable = 'FBINTF_WIRE_TEST'; +var Tr: ITransaction; + S: IStatement; + RS: IResultSet; + Blob: IBlob; + BlobText, ReadBack: AnsiString; + i: integer; +begin + writeln('Provider: updates and blobs'); + Tr := Attachment.StartTransaction([isc_tpb_read_committed, + isc_tpb_rec_version,isc_tpb_nowait,isc_tpb_write],taCommit); + try + Attachment.ExecImmediate(Tr,'drop table ' + TestTable); + Tr.Commit; + Tr := Attachment.StartTransaction([isc_tpb_read_committed, + isc_tpb_rec_version,isc_tpb_nowait,isc_tpb_write],taCommit); + except + {the table did not exist} + Tr := Attachment.StartTransaction([isc_tpb_read_committed, + isc_tpb_rec_version,isc_tpb_nowait,isc_tpb_write],taCommit); + end; + + Attachment.ExecImmediate(Tr,'create table ' + TestTable + + ' (ID integer not null primary key, NAME varchar(60), FLAG boolean,' + + ' AMOUNT numeric(15,2), NOTES blob sub_type 1)'); + Tr.Commit; + Check('table created',Attachment.HasTable(TestTable)); + + Tr := Attachment.StartTransaction([isc_tpb_read_committed, + isc_tpb_rec_version,isc_tpb_nowait,isc_tpb_write],taCommit); + S := Attachment.Prepare(Tr,'insert into ' + TestTable + + ' (ID,NAME,FLAG,AMOUNT) values (?,?,?,?)'); + for i := 1 to 3 do + begin + S.SQLParams[0].AsInteger := i; + if i = 2 then + S.SQLParams[1].IsNull := true + else + S.SQLParams[1].AsString := 'row ' + IntToStr(i) + ' with accents àéîõü'; + S.SQLParams[2].AsBoolean := Odd(i); + S.SQLParams[3].AsCurrency := i * 1234.56; + S.Execute; + end; + Tr.Commit; + + Tr := Attachment.StartTransaction([isc_tpb_read_committed, + isc_tpb_rec_version,isc_tpb_nowait,isc_tpb_write],taCommit); + RS := Attachment.OpenCursorAtStart(Tr, + 'select count(*) from ' + TestTable); + Check('three rows inserted',RS[0].AsInteger = 3, + 'got ' + IntToStr(RS[0].AsInteger)); + RS.Close; + RS := nil; + + RS := Attachment.OpenCursorAtStart(Tr, + 'select ID,NAME,FLAG,AMOUNT from ' + TestTable + ' order by ID'); + Check('boolean true survived the round trip',RS.ByName('FLAG').AsBoolean); + Check('accented text survived the round trip', + Pos('àéîõü',RS.ByName('NAME').AsString) > 0, + RS.ByName('NAME').AsString); + Check('scaled numeric survived the round trip', + Abs(RS.ByName('AMOUNT').AsCurrency - 1234.56) < 0.005, + CurrToStr(RS.ByName('AMOUNT').AsCurrency)); + Check('second row is null',RS.FetchNext and RS.ByName('NAME').IsNull); + Check('third row has flag true',RS.FetchNext and RS.ByName('FLAG').AsBoolean); + RS.Close; + RS := nil; + + {a blob larger than one segment} + BlobText := ''; + for i := 1 to 400 do + BlobText := BlobText + 'Firebird pure Pascal wire protocol, line ' + + IntToStr(i) + '.' + LineEnding; + S := Attachment.Prepare(Tr,'update ' + TestTable + + ' set NOTES = ? where ID = 1'); + Blob := Attachment.CreateBlob(Tr,1,0); + Blob.SetAsString(BlobText); + Blob.Close; + S.SQLParams[0].AsBlob := Blob; + S.Execute; + Tr.Commit; + + Tr := Attachment.StartTransaction([isc_tpb_read_committed, + isc_tpb_rec_version,isc_tpb_nowait,isc_tpb_write],taCommit); + RS := Attachment.OpenCursorAtStart(Tr,'select NOTES from ' + TestTable + + ' where ID = 1'); + ReadBack := RS[0].AsString; + Check('blob round trips byte for byte',ReadBack = BlobText, + Format('wrote %d bytes, read %d',[Length(BlobText),Length(ReadBack)])); + RS.Close; + RS := nil; + Blob := nil; + S := nil; + Tr.Commit; + + {rollback must undo the delete} + Tr := Attachment.StartTransaction([isc_tpb_read_committed, + isc_tpb_rec_version,isc_tpb_nowait,isc_tpb_write],taCommit); + Attachment.ExecImmediate(Tr,'delete from ' + TestTable); + Tr.Rollback; + Tr := Attachment.StartTransaction([isc_tpb_read_committed, + isc_tpb_rec_version,isc_tpb_nowait,isc_tpb_write],taCommit); + RS := Attachment.OpenCursorAtStart(Tr,'select count(*) from ' + TestTable); + Check('rollback undid the delete',RS[0].AsInteger = 3, + 'got ' + IntToStr(RS[0].AsInteger)); + RS.Close; + RS := nil; + Tr.Commit; + + {Cleanup only. An attachment that has read a table keeps an interest in + it that outlives the transaction, and Firebird 5 then refuses to drop + the table on that attachment - the stock fbclient provider behaves + identically here, so this is a property of the server and not of the + wire client. The next run drops the table before creating it, so + failing here leaves nothing behind.} + Tr := Attachment.StartTransaction([isc_tpb_read_committed, + isc_tpb_rec_version,isc_tpb_nowait,isc_tpb_write],taCommit); + try + Attachment.ExecImmediate(Tr,'drop table ' + TestTable); + Tr.Commit; + writeln(' note test table dropped'); + except + on E: Exception do + begin + Tr.Rollback; + writeln(' note the server would not drop the test table yet: ', + E.Message); + end; + end; +end; + +procedure TestArrays; +const TestTable = 'FBINTF_WIRE_ARTEST'; + Names: array[0..3] of AnsiString = + ('','first','ends at sixteen!','àéîõü'); +var Tr: ITransaction; + S: IStatement; + RS: IResultSet; + ar: IArray; + i, j: integer; +begin + writeln('Provider: array columns'); + Tr := Attachment.StartTransaction([isc_tpb_read_committed, + isc_tpb_rec_version,isc_tpb_nowait,isc_tpb_write],taCommit); + try + Attachment.ExecImmediate(Tr,'drop table ' + TestTable); + Tr.Commit; + Tr := Attachment.StartTransaction([isc_tpb_read_committed, + isc_tpb_rec_version,isc_tpb_nowait,isc_tpb_write],taCommit); + except + {the table did not exist} + Tr := Attachment.StartTransaction([isc_tpb_read_committed, + isc_tpb_rec_version,isc_tpb_nowait,isc_tpb_write],taCommit); + end; + + Attachment.ExecImmediate(Tr,'create table ' + TestTable + + ' (ID integer not null primary key,' + + ' INTS integer [1:4],' + + ' STRS varchar(16) [0:3],' + + ' GRID double precision [1:2,1:3])'); + Tr.Commit; + Check('array table created',Attachment.HasTable(TestTable)); + + Tr := Attachment.StartTransaction([isc_tpb_read_committed, + isc_tpb_rec_version,isc_tpb_nowait,isc_tpb_write],taCommit); + Attachment.ExecImmediate(Tr,'insert into ' + TestTable + ' (ID) values (1)'); + + {write the integer array} + ar := Attachment.CreateArray(Tr,TestTable,'INTS'); + Check('integer array metadata: 1 dimension',ar.GetDimensions = 1); + Check('integer array metadata: bounds 1:4', + (ar.GetBounds[0].LowerBound = 1) and (ar.GetBounds[0].UpperBound = 4)); + for i := 1 to 4 do + ar.SetAsInteger([i],i * 10); + S := Attachment.Prepare(Tr,'update ' + TestTable + + ' set INTS = ? where ID = 1'); + S.SQLParams[0].AsArray := ar; + S.Execute; + + {write the varchar array, including an empty and a full width element} + ar := Attachment.CreateArray(Tr,TestTable,'STRS'); + for i := 0 to 3 do + ar.SetAsString([i],Names[i]); + S := Attachment.Prepare(Tr,'update ' + TestTable + + ' set STRS = ? where ID = 1'); + S.SQLParams[0].AsArray := ar; + S.Execute; + + {write the two dimensional array} + ar := Attachment.CreateArray(Tr,TestTable,'GRID'); + Check('grid metadata: 2 dimensions',ar.GetDimensions = 2); + for i := 1 to 2 do + for j := 1 to 3 do + ar.SetAsDouble([i,j],i * 10 + j + 0.25); + S := Attachment.Prepare(Tr,'update ' + TestTable + + ' set GRID = ? where ID = 1'); + S.SQLParams[0].AsArray := ar; + S.Execute; + S := nil; + ar := nil; + Tr.Commit; + + {read everything back in a new transaction} + Tr := Attachment.StartTransaction([isc_tpb_read_committed, + isc_tpb_rec_version,isc_tpb_nowait,isc_tpb_write],taCommit); + RS := Attachment.OpenCursorAtStart(Tr, + 'select INTS,STRS,GRID from ' + TestTable + ' where ID = 1'); + + ar := RS[0].AsArray; + Check('integer array read back',ar <> nil); + if ar <> nil then + for i := 1 to 4 do + Check(Format('INTS[%d] element',[i]),ar.GetAsInteger([i]) = i * 10, + 'got ' + IntToStr(ar.GetAsInteger([i]))); + + ar := RS[1].AsArray; + Check('varchar array read back',ar <> nil); + if ar <> nil then + for i := 0 to 3 do + Check(Format('STRS[%d] element',[i]),ar.GetAsString([i]) = Names[i], + 'got "' + ar.GetAsString([i]) + '"'); + + ar := RS[2].AsArray; + Check('two dimensional array read back',ar <> nil); + if ar <> nil then + for i := 1 to 2 do + for j := 1 to 3 do + Check(Format('GRID[%d,%d] element',[i,j]), + Abs(ar.GetAsDouble([i,j]) - (i * 10 + j + 0.25)) < 1E-9, + FloatToStr(ar.GetAsDouble([i,j]))); + ar := nil; + RS.Close; + RS := nil; + + {update a single element through the read/modify/write cycle. The slice + must be read before a lone element is changed: as with the fbclient + providers, writing to an unloaded array sends the buffer as it stands.} + RS := Attachment.OpenCursorAtStart(Tr, + 'select INTS from ' + TestTable + ' where ID = 1'); + ar := RS[0].AsArray; + ar.PreLoad; + ar.SetAsInteger([2],1000); + S := Attachment.Prepare(Tr,'update ' + TestTable + + ' set INTS = ? where ID = 1'); + S.SQLParams[0].AsArray := ar; + S.Execute; + S := nil; + ar := nil; + RS.Close; + RS := nil; + Tr.Commit; + + Tr := Attachment.StartTransaction([isc_tpb_read_committed, + isc_tpb_rec_version,isc_tpb_nowait,isc_tpb_write],taCommit); + RS := Attachment.OpenCursorAtStart(Tr, + 'select INTS from ' + TestTable + ' where ID = 1'); + ar := RS[0].AsArray; + Check('modified element read back',ar.GetAsInteger([2]) = 1000, + 'got ' + IntToStr(ar.GetAsInteger([2]))); + Check('neighbouring element untouched',ar.GetAsInteger([1]) = 10, + 'got ' + IntToStr(ar.GetAsInteger([1]))); + ar := nil; + RS.Close; + RS := nil; + Tr.Commit; + + {cleanup - see the note in TestProviderUpdates} + Tr := Attachment.StartTransaction([isc_tpb_read_committed, + isc_tpb_rec_version,isc_tpb_nowait,isc_tpb_write],taCommit); + try + Attachment.ExecImmediate(Tr,'drop table ' + TestTable); + Tr.Commit; + writeln(' note array test table dropped'); + except + on E: Exception do + begin + Tr.Rollback; + writeln(' note the server would not drop the array test table yet: ', + E.Message); + end; + end; +end; + +const + {a PSQL busy loop: slow enough to cancel or time out reliably, but + bounded, so a failure to interrupt it cannot hang the test} + sqlSlowQuery = 'execute block returns (n bigint) as ' + + 'begin n = 0; while (n < 200000000) do n = n + 1; suspend; end'; + +procedure TestScrollableCursors; +const TestTable = 'FBINTF_WIRE_SCROLL'; +var Tr: ITransaction; + S: IStatement; + RS: IResultSet; + i: integer; + + function ID: integer; + begin + Result := RS.ByName('ID').AsInteger; + end; + +begin + writeln('Provider: scrollable cursors'); + if not Attachment.HasScollableCursors then + begin + writeln(' SKIP scrollable cursors need protocol 18 or later'); + Exit; + end; + Tr := Attachment.StartTransaction([isc_tpb_read_committed, + isc_tpb_rec_version,isc_tpb_nowait,isc_tpb_write],taCommit); + try + Attachment.ExecImmediate(Tr,'drop table ' + TestTable); + Tr.Commit; + Tr := Attachment.StartTransaction([isc_tpb_read_committed, + isc_tpb_rec_version,isc_tpb_nowait,isc_tpb_write],taCommit); + except + {the table did not exist} + Tr := Attachment.StartTransaction([isc_tpb_read_committed, + isc_tpb_rec_version,isc_tpb_nowait,isc_tpb_write],taCommit); + end; + Attachment.ExecImmediate(Tr,'create table ' + TestTable + + ' (ID integer not null primary key)'); + Tr.Commit; + + Tr := Attachment.StartTransaction([isc_tpb_read_committed, + isc_tpb_rec_version,isc_tpb_nowait,isc_tpb_write],taCommit); + S := Attachment.Prepare(Tr,'insert into ' + TestTable + ' (ID) values (?)'); + for i := 1 to 10 do + begin + S.SQLParams[0].AsInteger := i; + S.Execute; + end; + + S := Attachment.Prepare(Tr, + 'select ID from ' + TestTable + ' order by ID'); + RS := S.OpenCursor(true); + Check('scrollable statement reports stScrollable', + stScrollable in S.GetFlags); + + Check('fetch next finds the first row',RS.FetchNext); + Check('first row is 1',ID = 1,'got ' + IntToStr(ID)); + + Check('fetch last finds a row',RS.FetchLast); + Check('last row is 10',ID = 10,'got ' + IntToStr(ID)); + + Check('fetch prior steps back',RS.FetchPrior); + Check('prior of last is 9',ID = 9,'got ' + IntToStr(ID)); + + Check('fetch absolute 3 positions',RS.FetchAbsolute(3)); + Check('third row is 3',ID = 3,'got ' + IntToStr(ID)); + + Check('fetch relative -1 steps back',RS.FetchRelative(-1)); + Check('second row is 2',ID = 2,'got ' + IntToStr(ID)); + + Check('fetch first rewinds',RS.FetchFirst); + Check('first row again is 1',ID = 1,'got ' + IntToStr(ID)); + + Check('fetch absolute beyond the end returns false', + not RS.FetchAbsolute(1000)); + {and the cursor is still usable afterwards} + Check('cursor survives the failed fetch',RS.FetchFirst); + Check('and still delivers row 1',ID = 1,'got ' + IntToStr(ID)); + + {sequential fetch after scrolling continues from the cursor position} + Check('fetch next after first',RS.FetchNext); + Check('second row is 2 again',ID = 2,'got ' + IntToStr(ID)); + + RS.Close; + RS := nil; + S := nil; + Tr.Commit; + + {cleanup - see the note in TestProviderUpdates} + Tr := Attachment.StartTransaction([isc_tpb_read_committed, + isc_tpb_rec_version,isc_tpb_nowait,isc_tpb_write],taCommit); + try + Attachment.ExecImmediate(Tr,'drop table ' + TestTable); + Tr.Commit; + writeln(' note scroll test table dropped'); + except + on E: Exception do + begin + Tr.Rollback; + writeln(' note the server would not drop the scroll test table yet: ', + E.Message); + end; + end; +end; + +procedure TestEngineMessages; +var fmt: AnsiString; + status: TWireStatusVector; +begin + writeln('Engine message table'); + Check('a plain message is found', + FindEngineMessage(335544351,fmt) and + (fmt = 'unsuccessful metadata update'), + 'got "' + fmt + '"'); + Check('a parameterised message is found', + FindEngineMessage(335544343,fmt) and + (fmt = 'invalid request BLR at offset @1'), + 'got "' + fmt + '"'); + Check('an unknown code is not found', + not FindEngineMessage(999,fmt)); + + {format a two item vector the way fb_interpret would} + SetLength(status,4); + status[0].Kind := 1; {isc_arg_gds} + status[0].IntValue := 335544665; {unique_key_violation} + status[1].Kind := 2; {isc_arg_string} + status[1].StrValue := '"INTEG_27"'; + status[2].Kind := 2; + status[2].StrValue := '"PUBLIC"."EMPLOYEE"'; + status[3].Kind := 1; + status[3].IntValue := 335545072; {"Problematic key value is @1"} + SetLength(status,5); + status[4].Kind := 2; + status[4].StrValue := '("EMP_NO" = 500)'; + Check('arguments substitute into the placeholders', + FormatWireStatus(status) = + 'violation of PRIMARY or UNIQUE KEY constraint "INTEG_27" on table "PUBLIC"."EMPLOYEE"' + + LineEnding + + '-Problematic key value is ("EMP_NO" = 500)', + FormatWireStatus(status)); +end; + +procedure TestWireCompression; +var DPB: IDPB; + Att2: IAttachment; + Tr: ITransaction; + RS: IResultSet; + B: IBlob; + Text, ReadBack: AnsiString; + i: integer; +begin + writeln('Provider: wire compression'); + DPB := API.AllocateDPB; + DPB.Add(isc_dpb_user_name).AsString := UserName; + DPB.Add(isc_dpb_password).AsString := Password; + DPB.Add(isc_dpb_lc_ctype).AsString := 'UTF8'; + DPB.Add(isc_dpb_config).AsString := 'WireCompression=true'; + Att2 := API.OpenDatabase(DatabaseName,DPB); + try + if not (Att2 as TObject as TFBWireAttachment).Connection.Transport.Compressed then + begin + writeln(' SKIP the server did not accept compression', + ' (WireCompression is off in firebird.conf)'); + Exit; + end; + Check('compression negotiated',true); + + {work the stream in both directions: a bulk query and a blob round + trip; the deflate/inflate pipeline must be byte faithful} + Tr := Att2.StartTransaction([isc_tpb_read_committed, + isc_tpb_rec_version,isc_tpb_nowait,isc_tpb_write],taCommit); + RS := Att2.OpenCursorAtStart(Tr, + 'select count(*) from RDB$RELATIONS'); + Check('a query answers over the compressed stream',RS[0].AsInteger > 0); + RS.Close; + RS := nil; + + Text := ''; + for i := 1 to 300 do + Text := Text + 'Compressed round trip line ' + IntToStr(i) + '.' + + LineEnding; + try + Att2.ExecImmediate(Tr,'drop table FBINTF_WIRE_ZTEST'); + Tr.Commit; + Tr := Att2.StartTransaction([isc_tpb_read_committed, + isc_tpb_rec_version,isc_tpb_nowait,isc_tpb_write],taCommit); + except + Tr := Att2.StartTransaction([isc_tpb_read_committed, + isc_tpb_rec_version,isc_tpb_nowait,isc_tpb_write],taCommit); + end; + Att2.ExecImmediate(Tr, + 'create table FBINTF_WIRE_ZTEST (ID integer primary key, NOTES blob sub_type 1)'); + Tr.Commit; + Tr := Att2.StartTransaction([isc_tpb_read_committed, + isc_tpb_rec_version,isc_tpb_nowait,isc_tpb_write],taCommit); + B := Att2.CreateBlob(Tr,'FBINTF_WIRE_ZTEST','NOTES'); + B.SetAsString(Text); + B.Close; + with Att2.Prepare(Tr,'insert into FBINTF_WIRE_ZTEST values (1,?)') do + begin + SQLParams[0].AsBlob := B; + Execute; + end; + Tr.Commit; + Tr := Att2.StartTransaction([isc_tpb_read_committed, + isc_tpb_rec_version,isc_tpb_nowait,isc_tpb_write],taCommit); + RS := Att2.OpenCursorAtStart(Tr, + 'select NOTES from FBINTF_WIRE_ZTEST where ID = 1'); + ReadBack := RS[0].AsString; + Check('a blob round trips through the compressed stream', + ReadBack = Text, + Format('wrote %d read %d',[Length(Text),Length(ReadBack)])); + RS.Close; + RS := nil; + B := nil; + try + Att2.ExecImmediate(Tr,'drop table FBINTF_WIRE_ZTEST'); + Tr.Commit; + except + Tr.Rollback; + end; + finally + Att2.Disconnect; + Att2 := nil; + end; +end; + +procedure TestSchemas; +const S1 = 'FBINTF_WIRE_S1'; + S2 = 'FBINTF_WIRE_S2'; +var Tr: ITransaction; + S: IStatement; + RS: IResultSet; + Att2: IAttachment; + DPB: IDPB; + + procedure QuietExec(aAtt: IAttachment; const sql: AnsiString); + var T: ITransaction; + begin + T := aAtt.StartTransaction([isc_tpb_read_committed, + isc_tpb_rec_version,isc_tpb_nowait,isc_tpb_write],taCommit); + try + aAtt.ExecImmediate(T,sql); + T.Commit; + except + T.Rollback; + end; + end; + +begin + writeln('Provider: SQL schemas'); + if (Attachment as TObject as TFBWireAttachment).Connection.ProtocolVersion < + (PROTOCOL_VERSION20 and FB_PROTOCOL_MASK) then + begin + writeln(' SKIP SQL schemas need protocol 20 or later'); + Exit; + end; + + QuietExec(Attachment,'drop table ' + S1 + '.SCHEMATEST'); + QuietExec(Attachment,'drop table ' + S2 + '.SCHEMATEST'); + QuietExec(Attachment,'drop schema ' + S1); + QuietExec(Attachment,'drop schema ' + S2); + + Tr := Attachment.StartTransaction([isc_tpb_read_committed, + isc_tpb_rec_version,isc_tpb_nowait,isc_tpb_write],taCommit); + Attachment.ExecImmediate(Tr,'create schema ' + S1); + Attachment.ExecImmediate(Tr,'create schema ' + S2); + Tr.Commit; + Tr := Attachment.StartTransaction([isc_tpb_read_committed, + isc_tpb_rec_version,isc_tpb_nowait,isc_tpb_write],taCommit); + Attachment.ExecImmediate(Tr,'create table ' + S1 + + '.SCHEMATEST (ID integer not null primary key)'); + Attachment.ExecImmediate(Tr,'create table ' + S2 + + '.SCHEMATEST (ID integer not null primary key)'); + Tr.Commit; + Tr := Attachment.StartTransaction([isc_tpb_read_committed, + isc_tpb_rec_version,isc_tpb_nowait,isc_tpb_write],taCommit); + Attachment.ExecImmediate(Tr,'insert into ' + S1 + '.SCHEMATEST values (1)'); + Attachment.ExecImmediate(Tr,'insert into ' + S2 + '.SCHEMATEST values (2)'); + Tr.Commit; + + {a second attachment whose search path names the second schema: the + unqualified table name must resolve there} + DPB := API.AllocateDPB; + DPB.Add(isc_dpb_user_name).AsString := UserName; + DPB.Add(isc_dpb_password).AsString := Password; + DPB.Add(isc_dpb_lc_ctype).AsString := 'UTF8'; + DPB.Add(isc_dpb_search_path).AsString := S2 + ', PUBLIC'; + Att2 := API.OpenDatabase(DatabaseName,DPB); + try + Tr := Att2.StartTransaction([isc_tpb_read_committed, + isc_tpb_rec_version,isc_tpb_nowait,isc_tpb_read],taCommit); + S := Att2.Prepare(Tr,'select ID from SCHEMATEST'); + RS := S.OpenCursor; + Check('search path resolves the unqualified name',RS.FetchNext); + Check('to the schema the path names',RS[0].AsInteger = 2, + 'got ' + IntToStr(RS[0].AsInteger)); + Check('the describe carries the schema name', + (S as TObject as TFBWireStatement).ColumnSchemaName(0) = S2, + 'got "' + (S as TObject as TFBWireStatement).ColumnSchemaName(0) + '"'); + RS.Close; + RS := nil; + S := nil; + Tr.Commit; + finally + Att2.Disconnect; + Att2 := nil; + end; + + {and the first schema still answers when qualified} + Tr := Attachment.StartTransaction([isc_tpb_read_committed, + isc_tpb_rec_version,isc_tpb_nowait,isc_tpb_read],taCommit); + RS := Attachment.OpenCursorAtStart(Tr, + 'select ID from ' + S1 + '.SCHEMATEST'); + Check('a qualified name bypasses the search path',RS[0].AsInteger = 1); + RS.Close; + RS := nil; + Tr.Commit; + + QuietExec(Attachment,'drop table ' + S1 + '.SCHEMATEST'); + QuietExec(Attachment,'drop table ' + S2 + '.SCHEMATEST'); + QuietExec(Attachment,'drop schema ' + S1); + QuietExec(Attachment,'drop schema ' + S2); + writeln(' note schema test objects dropped'); +end; + +procedure TestInlineBlobs; +const TestTable = 'FBINTF_WIRE_INLINE'; +var Tr: ITransaction; + S: IStatement; + RS: IResultSet; + B: IBlob; + Small, Large, ReadBack: AnsiString; + i: integer; + Sent: cardinal; + + function PacketsSent: cardinal; + begin + Result := (Attachment as TObject as TFBWireAttachment). + Connection.Transport.PacketsSent; + end; + +begin + writeln('Provider: inline blobs'); + if (Attachment as TObject as TFBWireAttachment).Connection.ProtocolVersion < + (PROTOCOL_VERSION19 and FB_PROTOCOL_MASK) then + begin + writeln(' SKIP inline blobs need protocol 19 or later'); + Exit; + end; + Tr := Attachment.StartTransaction([isc_tpb_read_committed, + isc_tpb_rec_version,isc_tpb_nowait,isc_tpb_write],taCommit); + try + Attachment.ExecImmediate(Tr,'drop table ' + TestTable); + Tr.Commit; + Tr := Attachment.StartTransaction([isc_tpb_read_committed, + isc_tpb_rec_version,isc_tpb_nowait,isc_tpb_write],taCommit); + except + {the table did not exist} + Tr := Attachment.StartTransaction([isc_tpb_read_committed, + isc_tpb_rec_version,isc_tpb_nowait,isc_tpb_write],taCommit); + end; + Attachment.ExecImmediate(Tr,'create table ' + TestTable + + ' (ID integer not null primary key, NOTES blob sub_type 1)'); + Tr.Commit; + + Small := 'A small blob that fits the inline limit with room to spare'; + Large := ''; + for i := 1 to 500 do + Large := Large + 'A large blob that must not travel inline, line ' + + IntToStr(i) + '.' + LineEnding; + Check('the large blob exceeds the limit', + Length(Large) > Attachment.GetInlineBlobLimit, + 'limit ' + IntToStr(Attachment.GetInlineBlobLimit)); + + Tr := Attachment.StartTransaction([isc_tpb_read_committed, + isc_tpb_rec_version,isc_tpb_nowait,isc_tpb_write],taCommit); + S := Attachment.Prepare(Tr,'insert into ' + TestTable + + ' (ID,NOTES) values (?,?)'); + S.SQLParams[0].AsInteger := 1; + B := Attachment.CreateBlob(Tr,TestTable,'NOTES'); + B.SetAsString(Small); + B.Close; + S.SQLParams[1].AsBlob := B; + S.Execute; + S.SQLParams[0].AsInteger := 2; + B := Attachment.CreateBlob(Tr,TestTable,'NOTES'); + B.SetAsString(Large); + B.Close; + S.SQLParams[1].AsBlob := B; + S.Execute; + S := nil; + B := nil; + Tr.Commit; + + {the small blob must be served from the cache: opening and reading it + after the fetch causes no wire traffic at all} + Tr := Attachment.StartTransaction([isc_tpb_read_committed, + isc_tpb_rec_version,isc_tpb_nowait,isc_tpb_read],taCommit); + RS := Attachment.OpenCursorAtStart(Tr, + 'select NOTES from ' + TestTable + ' where ID = 1'); + Sent := PacketsSent; + ReadBack := RS[0].AsString; + Check('a small blob is served inline - no round trip', + PacketsSent = Sent, + Format('%d extra packets',[PacketsSent - Sent])); + Check('the inline copy is intact',ReadBack = Small); + RS.Close; + RS := nil; + + {the large blob fell back to the classic exchanges} + RS := Attachment.OpenCursorAtStart(Tr, + 'select NOTES from ' + TestTable + ' where ID = 2'); + Sent := PacketsSent; + ReadBack := RS[0].AsString; + Check('a large blob still opens the classic way',PacketsSent > Sent); + Check('the large blob is intact',ReadBack = Large, + Format('wrote %d read %d',[Length(Large),Length(ReadBack)])); + RS.Close; + RS := nil; + Tr.Commit; + + {opting out: with the limit at zero nothing arrives inline} + Attachment.SetInlineBlobLimit(0); + try + Tr := Attachment.StartTransaction([isc_tpb_read_committed, + isc_tpb_rec_version,isc_tpb_nowait,isc_tpb_read],taCommit); + RS := Attachment.OpenCursorAtStart(Tr, + 'select NOTES from ' + TestTable + ' where ID = 1'); + Sent := PacketsSent; + ReadBack := RS[0].AsString; + Check('with a zero limit the blob opens the classic way', + PacketsSent > Sent); + Check('and reads back intact',ReadBack = Small); + RS.Close; + RS := nil; + Tr.Commit; + finally + Attachment.SetInlineBlobLimit(DefaultMaxInlineBlobLimit); + end; + + {cleanup - see the note in TestProviderUpdates} + Tr := Attachment.StartTransaction([isc_tpb_read_committed, + isc_tpb_rec_version,isc_tpb_nowait,isc_tpb_write],taCommit); + try + Attachment.ExecImmediate(Tr,'drop table ' + TestTable); + Tr.Commit; + writeln(' note inline blob test table dropped'); + except + on E: Exception do + begin + Tr.Rollback; + writeln(' note the server would not drop the inline blob test table yet: ', + E.Message); + end; + end; +end; + +procedure TestBatch; +const TestTable = 'FBINTF_WIRE_BATCH'; +var Tr: ITransaction; + S: IStatement; + RS: IResultSet; + BC: IBatchCompletion; + i, RowNo: integer; + status: IStatus; +begin + writeln('Provider: the batch API'); + if not Attachment.HasBatchMode then + begin + writeln(' SKIP batches need protocol 16 or later'); + Exit; + end; + Tr := Attachment.StartTransaction([isc_tpb_read_committed, + isc_tpb_rec_version,isc_tpb_nowait,isc_tpb_write],taCommit); + try + Attachment.ExecImmediate(Tr,'drop table ' + TestTable); + Tr.Commit; + Tr := Attachment.StartTransaction([isc_tpb_read_committed, + isc_tpb_rec_version,isc_tpb_nowait,isc_tpb_write],taCommit); + except + {the table did not exist} + Tr := Attachment.StartTransaction([isc_tpb_read_committed, + isc_tpb_rec_version,isc_tpb_nowait,isc_tpb_write],taCommit); + end; + Attachment.ExecImmediate(Tr,'create table ' + TestTable + + ' (ID integer not null primary key, NAME varchar(30))'); + Tr.Commit; + + {a clean thousand row batch} + Tr := Attachment.StartTransaction([isc_tpb_read_committed, + isc_tpb_rec_version,isc_tpb_nowait,isc_tpb_write],taCommit); + S := Attachment.Prepare(Tr,'insert into ' + TestTable + + ' (ID,NAME) values (?,?)'); + for i := 1 to 1000 do + begin + S.SQLParams[0].AsInteger := i; + S.SQLParams[1].AsString := 'row ' + IntToStr(i); + S.AddToBatch; + end; + Check('statement is in batch mode',S.IsInBatchMode); + BC := S.ExecuteBatch(nil); + Check('batch mode ends with the execute',not S.IsInBatchMode); + Check('a thousand rows processed',BC.getTotalProcessed = 1000, + 'got ' + IntToStr(BC.getTotalProcessed)); + Check('a thousand rows updated',BC.getUpdated = 1000, + 'got ' + IntToStr(BC.getUpdated)); + Tr.Commit; + + Tr := Attachment.StartTransaction([isc_tpb_read_committed, + isc_tpb_rec_version,isc_tpb_nowait,isc_tpb_write],taCommit); + RS := Attachment.OpenCursorAtStart(Tr,'select count(*) from ' + TestTable); + Check('a thousand rows arrived',RS[0].AsInteger = 1000, + 'got ' + IntToStr(RS[0].AsInteger)); + RS.Close; + RS := nil; + + {a batch that fails mid way: row 500 repeats key 499} + S := Attachment.Prepare(Tr,'insert into ' + TestTable + + ' (ID,NAME) values (?,?)'); + for i := 1 to 1000 do + begin + if i = 500 then + S.SQLParams[0].AsInteger := 1499 {a duplicate of an earlier row} + else + S.SQLParams[0].AsInteger := 1000 + i; + S.SQLParams[1].AsString := 'second run row ' + IntToStr(i); + S.AddToBatch; + if i = 500 then + S.SQLParams[0].AsInteger := 1000 + i; {leave the next rows valid} + end; + BC := nil; + try + BC := S.ExecuteBatch(nil); + Check('the failing batch raised',false,'no exception'); + except + on E: EIBInterBaseError do + Check('duplicate key reported',E.IBErrorCode = isc_unique_key_violation, + Format('error=%d %s',[E.IBErrorCode,E.Message])); + end; + BC := S.GetBatchCompletion; + Check('completion available after the error',BC <> nil); + if BC <> nil then + begin + Check('processing stopped at the failing row', + BC.getTotalProcessed = 500,'got ' + IntToStr(BC.getTotalProcessed)); + Check('the rows before the failure were applied', + BC.getUpdated = 499,'got ' + IntToStr(BC.getUpdated)); + Check('the failing row reports bcExecuteFailed', + BC.getState(499) = bcExecuteFailed); + status := nil; + Check('getErrorStatus finds the failure',BC.getErrorStatus(RowNo,status)); + Check('the failure is in row 500',RowNo = 500,'got ' + IntToStr(RowNo)); + Check('the error status carries the duplicate key code', + (status <> nil) and (status.GetIBErrorCode = isc_unique_key_violation)); + end; + Tr.Rollback; + + {cleanup - see the note in TestProviderUpdates} + Tr := Attachment.StartTransaction([isc_tpb_read_committed, + isc_tpb_rec_version,isc_tpb_nowait,isc_tpb_write],taCommit); + try + Attachment.ExecImmediate(Tr,'drop table ' + TestTable); + Tr.Commit; + writeln(' note batch test table dropped'); + except + on E: Exception do + begin + Tr.Rollback; + writeln(' note the server would not drop the batch test table yet: ', + E.Message); + end; + end; +end; + +type + { TCancelVictim - runs the slow query so the main thread can cancel it } + + TCancelVictim = class(TThread) + public + ErrorCode: Int64; + Completed: boolean; + Started: boolean; + procedure Execute; override; + end; + +procedure TCancelVictim.Execute; +var Tr: ITransaction; +begin + ErrorCode := 0; + try + Tr := Attachment.StartTransaction([isc_tpb_read_committed, + isc_tpb_rec_version,isc_tpb_nowait,isc_tpb_write],taCommit); + Started := true; + Attachment.OpenCursorAtStart(Tr,sqlSlowQuery); + Completed := true; + except + on E: EIBInterBaseError do + ErrorCode := E.IBErrorCode; + on E: Exception do + ErrorCode := -1; + end; +end; + +procedure TestCancellation; +var Victim: TCancelVictim; + i: integer; +begin + writeln('Provider: operation cancellation'); + Victim := TCancelVictim.Create(true); + try + Victim.Start; + {give the query time to reach the server} + i := 0; + while not Victim.Started and (i < 5000) do + begin + Sleep(10); + Inc(i,10); + end; + Sleep(300); + Attachment.CancelOperation(fb_cancel_raise); + {the victim now fails with isc_cancelled - wait for it, bounded by + the query's own worst case run time} + i := 0; + while not Victim.Finished and (i < 120000) do + begin + Sleep(50); + Inc(i,50); + end; + Check('victim thread finished',Victim.Finished); + Check('cancelled promptly',i < 30000,Format('took %d ms',[i])); + Check('victim failed with isc_cancelled', + Victim.ErrorCode = isc_cancelled, + Format('completed=%s error=%d', + [BoolToStr(Victim.Completed,true),Victim.ErrorCode])); + finally + Victim.WaitFor; + Victim.Free; + end; +end; + +procedure TestStatementTimeout; +var Tr: ITransaction; + S: IStatement; +begin + writeln('Provider: statement timeout'); + Tr := Attachment.StartTransaction([isc_tpb_read_committed, + isc_tpb_rec_version,isc_tpb_nowait,isc_tpb_write],taCommit); + S := Attachment.Prepare(Tr,sqlSlowQuery); + try + S.SetStatementTimeout(250); + except + on E: EIBClientError do + begin + writeln(' SKIP statement timeouts need protocol 16 or later'); + Exit; + end; + end; + Check('timeout value read back',S.GetStatementTimeout = 250); + try + S.OpenCursor.FetchNext; + Check('timeout fired',false,'the slow query ran to completion'); + except + on E: EIBInterBaseError do + {an expired timeout cancels the request: the primary status code is + isc_cancelled with isc_req_stmt_timeout as the secondary. Nothing + else cancels in this test, so isc_cancelled here is the timeout.} + Check('timeout cancelled the statement', + E.IBErrorCode = isc_cancelled, + Format('error=%d %s',[E.IBErrorCode,E.Message])); + end; + {the statement stays usable: a fresh execute with no timeout succeeds} + S := Attachment.Prepare(Tr,'select 1 from rdb$database'); + Check('connection still usable after timeout', + S.OpenCursor.FetchNext); +end; + +type + { TEventCatcher - a TEventHandler needs an object method } + + TEventCatcher = class + public + Signalled: boolean; + Counts: TEventCounts; + procedure HandleEvent(Sender: IEvents); + end; + +procedure TEventCatcher.HandleEvent(Sender: IEvents); +begin + Counts := Sender.ExtractEventCounts; + Signalled := true; +end; + +procedure TestEvents; +const + sqlPostEvent = 'execute block as begin post_event ''WIRETEST_EVENT''; end'; +var Catcher: TEventCatcher; + EventHandler: IEvents; + Tr: ITransaction; + i: integer; + + function WaitForSignal(aTimeoutMS: integer): boolean; + var waited: integer; + begin + waited := 0; + while not Catcher.Signalled and (waited < aTimeoutMS) do + begin + Sleep(50); + Inc(waited,50); + end; + Result := Catcher.Signalled; + end; + + procedure PostEvent; + begin + Tr := Attachment.StartTransaction([isc_tpb_write,isc_tpb_nowait, + isc_tpb_concurrency],taCommit); + Attachment.ExecImmediate(Tr,sqlPostEvent); + Tr.Commit; + end; + +begin + writeln('Provider: events'); + Catcher := TEventCatcher.Create; + try + try + EventHandler := Attachment.GetEventHandler('WIRETEST_EVENT'); + except on E: Exception do + begin + {events need the server's auxiliary port to be reachable. In a + container or behind a firewall that means pinning it with + RemoteAuxPort in firebird.conf and opening it - without that the + rest of the suite is still worth running} + writeln(' SKIP events: the auxiliary port is not reachable (', + E.Message,')'); + writeln(' set RemoteAuxPort in firebird.conf and open that port'); + Exit; + end; + end; + Check('event handler obtained',EventHandler <> nil); + + {the first wait establishes the baseline: whether it fires immediately + depends on the event's history, so absorb it} + Catcher.Signalled := false; + EventHandler.AsyncWaitForEvent(Catcher.HandleEvent); + WaitForSignal(1000); + if Catcher.Signalled then + begin + Catcher.Signalled := false; + EventHandler.AsyncWaitForEvent(Catcher.HandleEvent); + Sleep(200); + end; + + {a posted event must now be delivered} + PostEvent; + Check('posted event was delivered',WaitForSignal(5000)); + Check('event name reported', + (Length(Catcher.Counts) = 1) and + (Catcher.Counts[0].EventName = 'WIRETEST_EVENT')); + if Length(Catcher.Counts) = 1 then + Check('event count is positive',Catcher.Counts[0].Count > 0, + 'got ' + IntToStr(Catcher.Counts[0].Count)); + + {events posted while nobody waits are delivered on the next wait} + Catcher.Signalled := false; + PostEvent; + PostEvent; + Sleep(300); + Check('no delivery without a wait',not Catcher.Signalled); + EventHandler.AsyncWaitForEvent(Catcher.HandleEvent); + Check('deferred events were caught',WaitForSignal(5000)); + if Length(Catcher.Counts) = 1 then + Check('both deferred events counted',Catcher.Counts[0].Count = 2, + 'got ' + IntToStr(Catcher.Counts[0].Count)); + + {cancel must stop delivery} + Catcher.Signalled := false; + EventHandler.AsyncWaitForEvent(Catcher.HandleEvent); + Sleep(200); + Catcher.Signalled := false; {absorb any baseline delivery} + EventHandler.Cancel; + PostEvent; + i := 0; + while not Catcher.Signalled and (i < 1000) do + begin + Sleep(50); + Inc(i,50); + end; + Check('no delivery after cancel',not Catcher.Signalled); + + EventHandler := nil; + finally + Catcher.Free; + end; +end; + +procedure TestServices; +var SPB: ISPB; + Service: IServiceManager; + Req: ISRB; + Results: IServiceQueryResults; + host, dbname: AnsiString; + port: integer; + i, statLines: integer; + serverVersion, serverImplementation, line: AnsiString; +begin + writeln('Provider: services'); + SplitConnectString(DatabaseName,host,dbname,port); + + SPB := API.AllocateSPB; + SPB.Add(isc_spb_user_name).AsString := UserName; + SPB.Add(isc_spb_password).AsString := Password; + Service := API.GetServiceManager(host,IntToStr(port),TCP,SPB); + Check('service manager attached',(Service <> nil) and Service.IsAttached); + if (Service = nil) or not Service.IsAttached then Exit; + + {information query} + Req := Service.AllocateSRB; + Req.Add(isc_info_svc_server_version); + Req.Add(isc_info_svc_implementation); + Results := Service.Query(Req); + serverVersion := ''; + serverImplementation := ''; + if Results <> nil then + for i := 0 to Results.Count - 1 do + case Results[i].getItemType of + isc_info_svc_server_version: + serverVersion := Results[i].AsString; + isc_info_svc_implementation: + serverImplementation := Results[i].AsString; + end; + Check('the server version was returned',serverVersion <> ''); + Check('the server implementation was returned',serverImplementation <> ''); + if serverVersion <> '' then + writeln(' server version: ',serverVersion); + + {detach and reattach - the suite relies on this working} + Service.Detach; + Check('service manager detached',not Service.IsAttached); + Service.Attach; + Check('service manager reattached',Service.IsAttached); + + {run a service: header page statistics on the test database} + Req := Service.AllocateSRB; + Req.Add(isc_action_svc_db_stats); + Req.Add(isc_spb_dbname).SetAsString(dbname); + Req.Add(isc_spb_options).SetAsInteger(isc_spb_sts_hdr_pages); + Check('header page statistics service started',Service.Start(Req)); + + statLines := 0; + repeat + Req := Service.AllocateSRB; + Req.Add(isc_info_svc_line); + Results := Service.Query(Req); + line := ''; + if (Results <> nil) and (Results.Count > 0) and + (Results[0].getItemType = isc_info_svc_line) then + line := Results[0].AsString; + if line <> '' then + Inc(statLines); + until line = ''; + Check('statistics output was returned',statLines > 0, + 'got ' + IntToStr(statLines) + ' lines'); + + Service.Detach; + Service := nil; +end; + +{---------------------------------------------------------------------------} + +begin + if ParamCount >= 1 then DatabaseName := ParamStr(1); + if ParamCount >= 2 then UserName := ParamStr(2); + if ParamCount >= 3 then Password := ParamStr(3); + if ParamCount >= 4 then ScratchDatabase := ParamStr(4); + + writeln('fbintf pure Pascal wire protocol test'); + writeln('database: ',DatabaseName,' user: ',UserName); + writeln; + + TestBigInt; + TestCrypto; + TestSRP; + TestMessageLayout; + TestProtocol; + TestProtocolNegotiation; + TestEngineMessages; + + if OpenTestDatabase then + try + TestProviderQueries; + TestProviderDataTypes; + TestProviderUpdates; + TestArrays; + TestScrollableCursors; + TestWireCompression; + TestSchemas; + TestInlineBlobs; + TestBatch; + TestCancellation; + TestStatementTimeout; + TestEvents; + TestServices; + TestCreateDatabase; + finally + Attachment.Disconnect; + Attachment := nil; + end; + + writeln; + writeln(Format('%d tests, %d failures',[TestsRun,TestsFailed])); + if TestsFailed > 0 then + Halt(1); +end. diff --git a/testsuite/employee.gbk b/testsuite/employee.gbk new file mode 100644 index 00000000..7796a1af Binary files /dev/null and b/testsuite/employee.gbk differ diff --git a/testsuite/runtest.sh b/testsuite/runtest.sh index 2536f7a3..6d6a46fd 100755 --- a/testsuite/runtest.sh +++ b/testsuite/runtest.sh @@ -33,16 +33,48 @@ if [ -x testsuite ]; then sed -i 's|Timestamp = [0-9][0-9][0-9][0-9]/[0-9][0-9]/[0-9][0-9] [0-9][0-9]:[0-9][0-9]:[0-9][0-9].[0-9][0-9][0-9][0-9]|Timestamp = yyyy/mm/dd hh:mm:ss.zzzz|' testout.log echo "Comparing results with reference log" echo "" - if grep 'ODS Major Version = 11' testout.log >/dev/null; then - diff FB2reference.log testout.log >diff.log + #normalise the run dependent values on both sides of the comparison: + #transaction ids depend on the server's history and IBX$CREATED is the + #journal's own timestamp + normalise_log() + { + sed -e 's|Timestamp = [0-9][0-9][0-9][0-9]/[0-9][0-9]/[0-9][0-9] [0-9][0-9]:[0-9][0-9]:[0-9][0-9].[0-9][0-9][0-9][0-9]|Timestamp = yyyy/mm/dd hh:mm:ss.zzzz|' \ + -e 's|Transaction ID = [0-9][0-9]*|Transaction ID = nnnn|' \ + -e 's|IBX$CREATED = [0-9][0-9]*/[0-9][0-9]*/[0-9][0-9]* [0-9:.]*|IBX$CREATED = yyyy/mm/dd hh:mm:ss.zzzz|' \ + -e 's|Database ID = [0-9][0-9]* FB = .* SN = .*|Database ID = n FB = dbpath SN = hostname|' \ + -e 's|^Pages Used = [0-9][0-9]*|Pages Used = nnnn|' \ + -e 's|^Pages Free = [0-9][0-9]*|Pages Free = nnnn|' \ + -e 's|^Fetches = [0-9][0-9]*|Fetches = nnnn|' \ + -e 's|^Reads = [0-9][0-9]*|Reads = nnnn|' \ + -e 's|^Writes = [0-9][0-9]*|Writes = nnnn|' \ + -e 's|^Page Writes = [0-9][0-9]*|Page Writes = nnnn|' \ + -e 's|^Count = [0-9][0-9]*|Count = nnnn|' \ + -e 's|^Pages =[0-9][0-9]*|Pages =nnnn|' \ + -e 's|^Server Memory = [0-9][0-9]*|Server Memory = nnnn|' \ + -e 's|^Max Memory = [0-9][0-9]*|Max Memory = nnnn|' \ + -e 's|^Database Created: .*|Database Created: yyyy/mm/dd hh:mm:ss|' \ + -e 's|^Version = 1: .*|Version = 1: server version string|' \ + -e 's|^Implementation = .*|Implementation = implementation codes|' \ + -e 's|^Server Version = .*|Server Version = server version string|' \ + -e 's|^RDB$SECURITY_CLASS = .*|RDB$SECURITY_CLASS = SQL$nnn|' \ + "$1" + } + if grep 'Provider = pure Pascal wire protocol' testout.log >/dev/null; then + REFLOG=FBWirereference.log + elif grep 'ODS Major Version = 11' testout.log >/dev/null; then + REFLOG=FB2reference.log elif grep 'ODS Major Version = 12' testout.log >/dev/null; then - diff FB3reference.log testout.log >diff.log + REFLOG=FB3reference.log elif grep 'ODS Major Version = 13' testout.log >/dev/null && grep 'ODS Minor Version = 0' testout.log >/dev/null; then - diff FB4reference.log testout.log >diff.log + REFLOG=FB4reference.log else - diff FB5reference.log testout.log >diff.log + REFLOG=FB5reference.log fi - cat diff.log + normalise_log $REFLOG >reference.tmp + normalise_log testout.log >testout.tmp + diff reference.tmp testout.tmp >diff.log + rm -f reference.tmp testout.tmp + cat diff.log else echo "Unable to run test suite" fi diff --git a/testsuite/testApp/TestApplication.pas b/testsuite/testApp/TestApplication.pas index b5f28f51..ee157ab1 100644 --- a/testsuite/testApp/TestApplication.pas +++ b/testsuite/testApp/TestApplication.pas @@ -41,7 +41,7 @@ interface uses - Classes, SysUtils, {$IFDEF FPC}CustApp,{$ENDIF} FirebirdOOAPI, IB, IBUtils, FmtBCD, FBClientLib; + Classes, SysUtils, {$IFDEF FPC}CustApp, FBWireClientAPI,{$ENDIF} FirebirdOOAPI, IB, IBUtils, FmtBCD, FBClientLib; {$IF not defined(LineEnding)} const @@ -156,6 +156,7 @@ TTestApplication = class(TCustomApplication) FPortNo: AnsiString; FCreateObjectsDone: boolean; FQuiet: boolean; + FProviderName: AnsiString; procedure CleanUp; function GetFirebirdAPI: IFirebirdAPI; function GetIndexByTestID(aTestID: AnsiString): integer; @@ -169,6 +170,7 @@ TTestApplication = class(TCustomApplication) procedure SetServerName(AValue: AnsiString); procedure SetPortNum(aValue: AnsiString); procedure SetTestOption(aValue: AnsiString); + procedure SetProviderName(aValue: AnsiString); protected {$IFDEF FPC} function GetShortOptions: AnsiString; virtual; @@ -703,7 +705,10 @@ procedure TTestBase.WriteAttachmentInfo(Attachment: IAttachment); writeln(outfile,'DB ODS Major Version = ',Attachment.GetODSMajorVersion); writeln(outfile,'DB ODS Minor Version = ',Attachment.GetODSMinorVersion); writeln(outfile,'User Authentication Method = ',Attachment.GetAuthenticationMethod); - writeln(outfile,'Firebird Library Path = ',Attachment.getFirebirdAPI.GetFBLibrary.GetLibraryFilePath); + if Attachment.getFirebirdAPI.GetFBLibrary <> nil then + writeln(outfile,'Firebird Library Path = ',Attachment.getFirebirdAPI.GetFBLibrary.GetLibraryFilePath) + else + writeln(outfile,'Firebird Library Path = none (wire protocol provider)'); writeln(outfile,'DB Client Implementation Version = ',Attachment.getFirebirdAPI.GetImplementationVersion); end; @@ -1157,7 +1162,13 @@ function TTestApplication.GetFirebirdAPI: IFirebirdAPI; begin if FFirebirdAPI = nil then begin - FFirebirdAPI := IB.FirebirdAPI; + {$IFDEF FPC} + if CompareText(FProviderName,'wire') = 0 then + {the pure Pascal wire protocol provider - loads no client library} + FFirebirdAPI := WireFirebirdAPI + else + {$ENDIF} + FFirebirdAPI := IB.FirebirdAPI; FFirebirdAPI.GetStatus.SetIBDataBaseErrorMessages([ShowIBMessage]);; end; Result := FFirebirdAPI; @@ -1320,16 +1331,31 @@ procedure TTestApplication.SetTestOption(aValue: AnsiString); FTestOption := AValue; end; +procedure TTestApplication.SetProviderName(aValue: AnsiString); +begin + if FFirebirdAPI <> nil then + raise Exception.Create('The provider must be selected before the API is first used'); + {$IFDEF FPC} + if (aValue <> '') and (CompareText(aValue,'wire') <> 0) and + (CompareText(aValue,'default') <> 0) then + raise Exception.CreateFmt('Unknown provider "%s" - use "wire" or "default"',[aValue]); + {$ELSE} + if (aValue <> '') and (CompareText(aValue,'default') <> 0) then + raise Exception.CreateFmt('Unknown provider "%s" - only "default" is available with this compiler',[aValue]); + {$ENDIF} + FProviderName := aValue; +end; + {$IFDEF FPC} function TTestApplication.GetShortOptions: AnsiString; begin - Result := 'htupensbolrSPXOq'; + Result := 'htupensbolrSPXOqa'; end; function TTestApplication.GetLongOptions: AnsiString; begin Result := 'help test user passwd employeedb newdbname secondnewdbname backupfile '+ - 'outfile fbclientlibrary server stats port prompt TestOption quiet'; + 'outfile fbclientlibrary server stats port prompt TestOption quiet api'; end; procedure TTestApplication.GetParams(var DoPrompt: boolean; var TestID: string); @@ -1378,6 +1404,9 @@ procedure TTestApplication.GetParams(var DoPrompt: boolean; var TestID: string); if HasOption('l','fbclientlibrary') then SetClientLibraryPath(GetOptionValue('l')); + if HasOption('a','api') then + SetProviderName(GetOptionValue('a')); + if HasOption('r','server') then SetServerName(GetOptionValue('r')); @@ -1461,6 +1490,9 @@ procedure TTestApplication.GetParams(var DoPrompt: boolean; var TestID: string); if GetCmdLineValue('l',aValue) or GetCmdLineValue('fbclientlibrary',aValue) then SetClientLibraryPath(aValue); + if GetCmdLineValue('a',aValue) or GetCmdLineValue('api',aValue) then + SetProviderName(aValue); + if GetCmdLineValue('o',aValue) or GetCmdLineValue('outfile',aValue) then begin system.Assign(outFile,aValue); @@ -1519,7 +1551,11 @@ procedure TTestApplication.DoRun; writeln(OutFile,'Firebird Bin Directory = ', getDirectory(DIR_BIN)); writeln(OutFile,'Firebird Conf Directory = ', getDirectory(DIR_CONF)); end; - writeln(OutFile,'Firebird Client Library Path = ',FirebirdAPI.GetFBLibrary.GetLibraryFilePath); + {the wire protocol provider loads no client library} + if FirebirdAPI.GetFBLibrary <> nil then + writeln(OutFile,'Firebird Client Library Path = ',FirebirdAPI.GetFBLibrary.GetLibraryFilePath) + else + writeln(OutFile,'Provider = pure Pascal wire protocol, no client library'); end; try