Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
D
doctrine-dbal
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Tomáš Trávníček
doctrine-dbal
Commits
2dfe0afd
Unverified
Commit
2dfe0afd
authored
Jan 12, 2020
by
Sergei Morozov
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Extract conversion of positional to named placeholder to an utility class
parent
2da86a36
Changes
5
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
217 additions
and
220 deletions
+217
-220
psalm.xml
psalm.xml
+1
-1
ConvertPositionalToNamedPlaceholders.php
src/Driver/OCI8/ConvertPositionalToNamedPlaceholders.php
+162
-0
OCI8Statement.php
src/Driver/OCI8/OCI8Statement.php
+1
-156
ConvertPositionalToNamedPlaceholdersTest.php
.../Driver/OCI8/ConvertPositionalToNamedPlaceholdersTest.php
+53
-11
OCI8StatementTest.php
tests/Driver/OCI8/OCI8StatementTest.php
+0
-52
No files found.
psalm.xml
View file @
2dfe0afd
...
...
@@ -29,7 +29,7 @@
This one is just too convoluted for Psalm to figure out, by
its author's own admission
-->
<file
name=
"src/Driver/OCI8/
OCI8Statement
.php"
/>
<file
name=
"src/Driver/OCI8/
ConvertPositionalToNamedPlaceholders
.php"
/>
</errorLevel>
</ConflictingReferenceConstraint>
<FalsableReturnStatement>
...
...
src/Driver/OCI8/ConvertPositionalToNamedPlaceholders.php
0 → 100644
View file @
2dfe0afd
<?php
declare
(
strict_types
=
1
);
namespace
Doctrine\DBAL\Driver\OCI8
;
use
Doctrine\DBAL\Driver\OCI8\Exception\NonTerminatedStringLiteral
;
use
function
count
;
use
function
implode
;
use
function
preg_match
;
use
function
preg_quote
;
use
function
substr
;
use
const
PREG_OFFSET_CAPTURE
;
/**
* Converts positional (?) into named placeholders (:param<num>).
*
* Oracle does not support positional parameters, hence this method converts all
* positional parameters into artificially named parameters. Note that this conversion
* is not perfect. All question marks (?) in the original statement are treated as
* placeholders and converted to a named parameter.
*
* @internal This class is not covered by the backward compatibility promise
*/
final
class
ConvertPositionalToNamedPlaceholders
{
/**
* @param string $statement The SQL statement to convert.
*
* @return mixed[] [0] => the statement value (string), [1] => the paramMap value (array).
*
* @throws OCI8Exception
*/
public
function
__invoke
(
string
$statement
)
:
array
{
$fragmentOffset
=
$tokenOffset
=
0
;
$fragments
=
$paramMap
=
[];
$currentLiteralDelimiter
=
null
;
do
{
if
(
$currentLiteralDelimiter
===
null
)
{
$result
=
$this
->
findPlaceholderOrOpeningQuote
(
$statement
,
$tokenOffset
,
$fragmentOffset
,
$fragments
,
$currentLiteralDelimiter
,
$paramMap
);
}
else
{
$result
=
$this
->
findClosingQuote
(
$statement
,
$tokenOffset
,
$currentLiteralDelimiter
);
}
}
while
(
$result
);
if
(
$currentLiteralDelimiter
!==
null
)
{
throw
NonTerminatedStringLiteral
::
new
(
$tokenOffset
-
1
);
}
$fragments
[]
=
substr
(
$statement
,
$fragmentOffset
);
$statement
=
implode
(
''
,
$fragments
);
return
[
$statement
,
$paramMap
];
}
/**
* Finds next placeholder or opening quote.
*
* @param string $statement The SQL statement to parse
* @param int $tokenOffset The offset to start searching from
* @param int $fragmentOffset The offset to build the next fragment from
* @param string[] $fragments Fragments of the original statement not containing placeholders
* @param string|null $currentLiteralDelimiter The delimiter of the current string literal
* or NULL if not currently in a literal
* @param string[] $paramMap Mapping of the original parameter positions to their named replacements
*
* @return bool Whether the token was found
*/
private
function
findPlaceholderOrOpeningQuote
(
string
$statement
,
int
&
$tokenOffset
,
int
&
$fragmentOffset
,
array
&
$fragments
,
?
string
&
$currentLiteralDelimiter
,
array
&
$paramMap
)
:
bool
{
$token
=
$this
->
findToken
(
$statement
,
$tokenOffset
,
'/[?\'"]/'
);
if
(
$token
===
null
)
{
return
false
;
}
if
(
$token
===
'?'
)
{
$position
=
count
(
$paramMap
)
+
1
;
$param
=
':param'
.
$position
;
$fragments
[]
=
substr
(
$statement
,
$fragmentOffset
,
$tokenOffset
-
$fragmentOffset
);
$fragments
[]
=
$param
;
$paramMap
[
$position
]
=
$param
;
$tokenOffset
+=
1
;
$fragmentOffset
=
$tokenOffset
;
return
true
;
}
$currentLiteralDelimiter
=
$token
;
++
$tokenOffset
;
return
true
;
}
/**
* Finds closing quote
*
* @param string $statement The SQL statement to parse
* @param int $tokenOffset The offset to start searching from
* @param string $currentLiteralDelimiter The delimiter of the current string literal
*
* @return bool Whether the token was found
*/
private
function
findClosingQuote
(
string
$statement
,
int
&
$tokenOffset
,
string
&
$currentLiteralDelimiter
)
:
bool
{
$token
=
$this
->
findToken
(
$statement
,
$tokenOffset
,
'/'
.
preg_quote
(
$currentLiteralDelimiter
,
'/'
)
.
'/'
);
if
(
$token
===
null
)
{
return
false
;
}
$currentLiteralDelimiter
=
null
;
++
$tokenOffset
;
return
true
;
}
/**
* Finds the token described by regex starting from the given offset. Updates the offset with the position
* where the token was found.
*
* @param string $statement The SQL statement to parse
* @param int $offset The offset to start searching from
* @param string $regex The regex containing token pattern
*
* @return string|null Token or NULL if not found
*/
private
function
findToken
(
string
$statement
,
int
&
$offset
,
string
$regex
)
:
?
string
{
if
(
preg_match
(
$regex
,
$statement
,
$matches
,
PREG_OFFSET_CAPTURE
,
$offset
)
===
1
)
{
$offset
=
$matches
[
0
][
1
];
return
$matches
[
0
][
0
];
}
return
null
;
}
}
src/Driver/OCI8/OCI8Statement.php
View file @
2dfe0afd
...
...
@@ -2,15 +2,12 @@
namespace
Doctrine\DBAL\Driver\OCI8
;
use
Doctrine\DBAL\Driver\OCI8\Exception\NonTerminatedStringLiteral
;
use
Doctrine\DBAL\Driver\OCI8\Exception\UnknownParameterIndex
;
use
Doctrine\DBAL\Driver\Result
as
ResultInterface
;
use
Doctrine\DBAL\Driver\Statement
as
StatementInterface
;
use
Doctrine\DBAL\ParameterType
;
use
function
assert
;
use
function
count
;
use
function
implode
;
use
function
is_int
;
use
function
is_resource
;
use
function
oci_bind_by_name
;
...
...
@@ -18,9 +15,6 @@ use function oci_error;
use
function
oci_execute
;
use
function
oci_new_descriptor
;
use
function
oci_parse
;
use
function
preg_match
;
use
function
preg_quote
;
use
function
substr
;
use
const
OCI_B_BIN
;
use
const
OCI_B_BLOB
;
...
...
@@ -28,7 +22,6 @@ use const OCI_COMMIT_ON_SUCCESS;
use
const
OCI_D_LOB
;
use
const
OCI_NO_AUTO_COMMIT
;
use
const
OCI_TEMP_BLOB
;
use
const
PREG_OFFSET_CAPTURE
;
use
const
SQLT_CHR
;
/**
...
...
@@ -67,7 +60,7 @@ class OCI8Statement implements StatementInterface
*/
public
function
__construct
(
$dbh
,
$query
,
ExecutionMode
$executionMode
)
{
[
$query
,
$paramMap
]
=
self
::
convertPositionalToNamedPlaceholders
(
$query
);
[
$query
,
$paramMap
]
=
(
new
ConvertPositionalToNamedPlaceholders
())
(
$query
);
$stmt
=
oci_parse
(
$dbh
,
$query
);
assert
(
is_resource
(
$stmt
));
...
...
@@ -78,154 +71,6 @@ class OCI8Statement implements StatementInterface
$this
->
executionMode
=
$executionMode
;
}
/**
* Converts positional (?) into named placeholders (:param<num>).
*
* Oracle does not support positional parameters, hence this method converts all
* positional parameters into artificially named parameters. Note that this conversion
* is not perfect. All question marks (?) in the original statement are treated as
* placeholders and converted to a named parameter.
*
* The algorithm uses a state machine with two possible states: InLiteral and NotInLiteral.
* Question marks inside literal strings are therefore handled correctly by this method.
* This comes at a cost, the whole sql statement has to be looped over.
*
* @param string $statement The SQL statement to convert.
*
* @return mixed[] [0] => the statement value (string), [1] => the paramMap value (array).
*
* @throws OCI8Exception
*
* @todo extract into utility class in Doctrine\DBAL\Util namespace
* @todo review and test for lost spaces. we experienced missing spaces with oci8 in some sql statements.
*/
public
static
function
convertPositionalToNamedPlaceholders
(
$statement
)
{
$fragmentOffset
=
$tokenOffset
=
0
;
$fragments
=
$paramMap
=
[];
$currentLiteralDelimiter
=
null
;
do
{
if
(
!
$currentLiteralDelimiter
)
{
$result
=
self
::
findPlaceholderOrOpeningQuote
(
$statement
,
$tokenOffset
,
$fragmentOffset
,
$fragments
,
$currentLiteralDelimiter
,
$paramMap
);
}
else
{
$result
=
self
::
findClosingQuote
(
$statement
,
$tokenOffset
,
$currentLiteralDelimiter
);
}
}
while
(
$result
);
if
(
$currentLiteralDelimiter
)
{
throw
NonTerminatedStringLiteral
::
new
(
$tokenOffset
-
1
);
}
$fragments
[]
=
substr
(
$statement
,
$fragmentOffset
);
$statement
=
implode
(
''
,
$fragments
);
return
[
$statement
,
$paramMap
];
}
/**
* Finds next placeholder or opening quote.
*
* @param string $statement The SQL statement to parse
* @param string $tokenOffset The offset to start searching from
* @param int $fragmentOffset The offset to build the next fragment from
* @param string[] $fragments Fragments of the original statement not containing placeholders
* @param string|null $currentLiteralDelimiter The delimiter of the current string literal
* or NULL if not currently in a literal
* @param array<int, string> $paramMap Mapping of the original parameter positions to their named replacements
*
* @return bool Whether the token was found
*/
private
static
function
findPlaceholderOrOpeningQuote
(
$statement
,
&
$tokenOffset
,
&
$fragmentOffset
,
&
$fragments
,
&
$currentLiteralDelimiter
,
&
$paramMap
)
{
$token
=
self
::
findToken
(
$statement
,
$tokenOffset
,
'/[?\'"]/'
);
if
(
$token
===
null
)
{
return
false
;
}
if
(
$token
===
'?'
)
{
$position
=
count
(
$paramMap
)
+
1
;
$param
=
':param'
.
$position
;
$fragments
[]
=
substr
(
$statement
,
$fragmentOffset
,
$tokenOffset
-
$fragmentOffset
);
$fragments
[]
=
$param
;
$paramMap
[
$position
]
=
$param
;
$tokenOffset
+=
1
;
$fragmentOffset
=
$tokenOffset
;
return
true
;
}
$currentLiteralDelimiter
=
$token
;
++
$tokenOffset
;
return
true
;
}
/**
* Finds closing quote
*
* @param string $statement The SQL statement to parse
* @param string $tokenOffset The offset to start searching from
* @param string $currentLiteralDelimiter The delimiter of the current string literal
*
* @return bool Whether the token was found
*/
private
static
function
findClosingQuote
(
$statement
,
&
$tokenOffset
,
&
$currentLiteralDelimiter
)
{
$token
=
self
::
findToken
(
$statement
,
$tokenOffset
,
'/'
.
preg_quote
(
$currentLiteralDelimiter
,
'/'
)
.
'/'
);
if
(
$token
===
null
)
{
return
false
;
}
$currentLiteralDelimiter
=
false
;
++
$tokenOffset
;
return
true
;
}
/**
* Finds the token described by regex starting from the given offset. Updates the offset with the position
* where the token was found.
*
* @param string $statement The SQL statement to parse
* @param int $offset The offset to start searching from
* @param string $regex The regex containing token pattern
*
* @return string|null Token or NULL if not found
*/
private
static
function
findToken
(
$statement
,
&
$offset
,
$regex
)
{
if
(
preg_match
(
$regex
,
$statement
,
$matches
,
PREG_OFFSET_CAPTURE
,
$offset
)
===
1
)
{
$offset
=
$matches
[
0
][
1
];
return
$matches
[
0
][
0
];
}
return
null
;
}
/**
* {@inheritdoc}
*/
...
...
tests/
Util
Test.php
→
tests/
Driver/OCI8/ConvertPositionalToNamedPlaceholders
Test.php
View file @
2dfe0afd
<?php
namespace
Doctrine\DBAL\Tests
;
declare
(
strict_types
=
1
)
;
use
Doctrine\DBAL\Driver\OCI8\Statement
;
namespace
Doctrine\Tests\DBAL\Driver\OCI8
;
use
Doctrine\DBAL\Driver\OCI8\ConvertPositionalToNamedPlaceholders
;
use
Doctrine\DBAL\Driver\OCI8\OCI8Exception
;
use
PHPUnit\Framework\TestCase
;
class
Util
Test
extends
TestCase
class
ConvertPositionalToNamedPlaceholders
Test
extends
TestCase
{
/** @var ConvertPositionalToNamedPlaceholders */
private
$convertPositionalToNamedPlaceholders
;
protected
function
setUp
()
:
void
{
$this
->
convertPositionalToNamedPlaceholders
=
new
ConvertPositionalToNamedPlaceholders
();
}
/**
* @param mixed[] $expectedOutputParamsMap
*
* @dataProvider positionalToNamedPlaceholdersProvider
*/
public
function
testConvertPositionalToNamedParameters
(
string
$inputSQL
,
string
$expectedOutputSQL
,
array
$expectedOutputParamsMap
)
:
void
{
[
$statement
,
$params
]
=
(
$this
->
convertPositionalToNamedPlaceholders
)(
$inputSQL
);
self
::
assertEquals
(
$expectedOutputSQL
,
$statement
);
self
::
assertEquals
(
$expectedOutputParamsMap
,
$params
);
}
/**
* @return mixed[][]
*/
public
static
function
dataConvertPositionalToNamedParameters
()
:
iterable
public
static
function
positionalToNamedPlaceholdersProvider
()
:
iterable
{
return
[
[
...
...
@@ -67,15 +91,33 @@ class UtilTest extends TestCase
}
/**
* @param mixed[] $expectedOutputParamsMap
*
* @dataProvider dataConvertPositionalToNamedParameters
* @dataProvider nonTerminatedLiteralProvider
*/
public
function
testConvert
PositionalToNamedParameters
(
string
$inputSQL
,
string
$expectedOutputSQL
,
array
$expectedOutputParamsMa
p
)
:
void
public
function
testConvert
NonTerminatedLiteral
(
string
$sql
,
string
$expectedExceptionMessageRegEx
p
)
:
void
{
[
$statement
,
$params
]
=
Statement
::
convertPositionalToNamedPlaceholders
(
$inputSQL
);
$this
->
expectException
(
OCI8Exception
::
class
);
$this
->
expectExceptionMessageMatches
(
$expectedExceptionMessageRegExp
);
(
$this
->
convertPositionalToNamedPlaceholders
)(
$sql
);
}
self
::
assertEquals
(
$expectedOutputSQL
,
$statement
);
self
::
assertEquals
(
$expectedOutputParamsMap
,
$params
);
/**
* @return array<string, array<int, mixed>>
*/
public
static
function
nonTerminatedLiteralProvider
()
:
iterable
{
return
[
'no-matching-quote'
=>
[
"SELECT 'literal FROM DUAL"
,
'/offset 7./'
,
],
'no-matching-double-quote'
=>
[
'SELECT 1 "COL1 FROM DUAL'
,
'/offset 9./'
,
],
'incorrect-escaping-syntax'
=>
[
"SELECT 'quoted
\\
'string' FROM DUAL"
,
'/offset 23./'
,
],
];
}
}
tests/Driver/OCI8/OCI8StatementTest.php
deleted
100644 → 0
View file @
2da86a36
<?php
namespace
Doctrine\DBAL\Tests\Driver\OCI8
;
use
Doctrine\DBAL\Driver\OCI8\OCI8Exception
;
use
Doctrine\DBAL\Driver\OCI8\OCI8Statement
;
use
PHPUnit\Framework\TestCase
;
use
function
extension_loaded
;
class
OCI8StatementTest
extends
TestCase
{
protected
function
setUp
()
:
void
{
if
(
!
extension_loaded
(
'oci8'
))
{
$this
->
markTestSkipped
(
'oci8 is not installed.'
);
}
parent
::
setUp
();
}
/**
* @dataProvider nonTerminatedLiteralProvider
*/
public
function
testConvertNonTerminatedLiteral
(
string
$sql
,
string
$message
)
:
void
{
$this
->
expectException
(
OCI8Exception
::
class
);
$this
->
expectExceptionMessageMatches
(
$message
);
OCI8Statement
::
convertPositionalToNamedPlaceholders
(
$sql
);
}
/**
* @return array<string, array<int, mixed>>
*/
public
static
function
nonTerminatedLiteralProvider
()
:
iterable
{
return
[
'no-matching-quote'
=>
[
"SELECT 'literal FROM DUAL"
,
'/offset 7/'
,
],
'no-matching-double-quote'
=>
[
'SELECT 1 "COL1 FROM DUAL'
,
'/offset 9/'
,
],
'incorrect-escaping-syntax'
=>
[
"SELECT 'quoted
\\
'string' FROM DUAL"
,
'/offset 23/'
,
],
];
}
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment