From 779ed6ed0e3a5cec2143ec3da6a0e2caef9da849 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Tvrd=C3=ADk?= Date: Tue, 24 Feb 2026 09:24:38 +0100 Subject: [PATCH 1/3] Add SQLite multi-query parser Implement SqliteMultiQueryParser with support for all three SQLite identifier quoting styles (double quotes, backticks, brackets), single-quoted strings with doubled-quote escaping, non-nesting block comments, and BEGIN...END compound statements for CREATE TRIGGER while correctly treating BEGIN TRANSACTION as a simple statement. --- src/SqliteMultiQueryParser.php | 75 ++++++ tests/cases/SqliteMultiQueryParserTest.phpt | 247 ++++++++++++++++++++ tests/data/sqlite.sql | 237 +++++++++++++++++++ 3 files changed, 559 insertions(+) create mode 100644 src/SqliteMultiQueryParser.php create mode 100644 tests/cases/SqliteMultiQueryParserTest.phpt create mode 100644 tests/data/sqlite.sql diff --git a/src/SqliteMultiQueryParser.php b/src/SqliteMultiQueryParser.php new file mode 100644 index 0000000..a89ea28 --- /dev/null +++ b/src/SqliteMultiQueryParser.php @@ -0,0 +1,75 @@ +getQueryPattern()); + + foreach ($patternIterator as $match) { + if (isset($match['query']) && $match['query'] !== '') { + yield $match['query']; + } + } + } + + + private function getQueryPattern(): string + { + $simpleQuery = /** @lang PhpRegExp */ '~ + (?: + \s + | /\* (*PRUNE) (?: [^*]++ | \*(?!/) )*+ \*/ + | -- [^\n]*+ + )*+ + (? + (?: + [^;\'"`[/-]++ + | \' (*PRUNE) (?: \'\' | [^\'] )*+ \' + | " (*PRUNE) (?: "" | [^"] )*+ " + | ` (*PRUNE) (?: `` | [^`] )*+ ` + | \[ (*PRUNE) [^\]]*+ (?: \]\] [^\]]*+ )* \] + | /\* (*PRUNE) (?: [^*]++ | \*(?!/) )*+ \*/ + | -- [^\n]*+ + | (?!;) . + )++ + ) + ; + ~x'; + return /** @lang PhpRegExp */ '~ + (?: + \s + | /\* (*PRUNE) (?: [^*]++ | \*(?!/) )*+ \*/ + | -- [^\n]*+ + )*+ + + (?: + (?: + (? + (?: + [^bB;\'"`[/-]++ + | \' (*PRUNE) (?: \'\' | [^\'] )*+ \' + | " (*PRUNE) (?: "" | [^"] )*+ " + | ` (*PRUNE) (?: `` | [^`] )*+ ` + | \[ (*PRUNE) [^\]]*+ (?: \]\] [^\]]*+ )* \] + | /\* (*PRUNE) (?: [^*]++ | \*(?!/) )*+ \*/ + | (?i:BEGIN) (?!\s*(?:(?i:TRANSACTION|DEFERRED|IMMEDIATE|EXCLUSIVE)\b|;|\z)) (*PRUNE) (?: (?i:\s*END)\s*| ' . substr($simpleQuery, 1, -2) . ')* + | -- [^\n]*+ + | (?!;) . + )*+ + ) + (?: ; | \z ) + ) + | + (?: + \z + ) + ) + ~xsAS'; + } +} diff --git a/tests/cases/SqliteMultiQueryParserTest.phpt b/tests/cases/SqliteMultiQueryParserTest.phpt new file mode 100644 index 0000000..0591875 --- /dev/null +++ b/tests/cases/SqliteMultiQueryParserTest.phpt @@ -0,0 +1,247 @@ +createParser(); + $queries = iterator_to_array($parser->parseFile($this->getDataFilePath())); + Assert::count($this->getExpectedFileQueryCount(), $queries); + Assert::same("CREATE TRIGGER trigger_book_collections_update + AFTER UPDATE ON book_collections + FOR EACH ROW +BEGIN + UPDATE book_collections SET updated_at = datetime('now') WHERE id = NEW.id; +END", $queries[19]); + } + + + /** + * @return list}> + */ + protected function provideSuperfluousSemicolonsData(): array + { + return [ + [ + 'SELECT 1 AS semicolon_madness;;;', + ['SELECT 1 AS semicolon_madness'], + ], + [ + ';;', + [], + ], + [ + ';;;', + [], + ], + [ + ';SELECT 1;', + ['SELECT 1'], + ], + [ + 'SELECT 1;;SELECT 2;', + ['SELECT 1', 'SELECT 2'], + ], + [ + 'SELECT 1; ; SELECT 2;', + ['SELECT 1', 'SELECT 2'], + ], + ]; + } + + + /** + * @return list}> + */ + protected function provideEdgeCasesData(): array + { + return [ + // Empty / whitespace-only input + ['', []], + [" \n\t\n ", []], + + // Single-quoted strings protect semicolons + ["SELECT 'a;b';", ["SELECT 'a;b'"]], + ["SELECT ';;;';", ["SELECT ';;;'"]], + ["SELECT '';", ["SELECT ''"]], + + // Doubled single quotes + ["SELECT 'it''s';", ["SELECT 'it''s'"]], + + // Double-quoted identifiers protect semicolons + ['SELECT "col;name" FROM t;', ['SELECT "col;name" FROM t']], + + // Doubled double quotes inside identifiers + ['SELECT "col""name" FROM t;', ['SELECT "col""name" FROM t']], + + // Backtick identifiers protect semicolons + ['SELECT `col;name` FROM t;', ['SELECT `col;name` FROM t']], + + // Doubled backticks inside identifiers + ['SELECT `col``name` FROM t;', ['SELECT `col``name` FROM t']], + + // Bracket identifiers protect semicolons + ['SELECT [col;name] FROM t;', ['SELECT [col;name] FROM t']], + ['SELECT [a;b], [c;d] FROM t;', ['SELECT [a;b], [c;d] FROM t']], + + // Escaped brackets (doubled ]) inside bracket identifiers + ['SELECT [col]]name] FROM t;', ['SELECT [col]]name] FROM t']], + + // Semicolons inside comments are not delimiters + ["SELECT /* ; */ 1;", ["SELECT /* ; */ 1"]], + ["SELECT 1; -- has ; in comment\nSELECT 2;", ["SELECT 1", "SELECT 2"]], + + // Line comment inside a query + ["SELECT 1 -- comment with ;\nSELECT 2;", ["SELECT 1 -- comment with ;\nSELECT 2"]], + + // Queries without trailing semicolon + ["SELECT 1", ["SELECT 1"]], + ["SELECT 1; SELECT 2", ["SELECT 1", "SELECT 2"]], + + // Forward slash and dash not starting comments + ["SELECT 5/3;", ["SELECT 5/3"]], + ["SELECT 5-3;", ["SELECT 5-3"]], + + // Only comments + ["/* only a comment */", []], + ["-- only a comment", []], + + // Comment positioning + ["/* prefix */ SELECT 1;", ["SELECT 1"]], + ["-- prefix\nSELECT 1;", ["SELECT 1"]], + ["SELECT 1; /* between */ SELECT 2;", ["SELECT 1", "SELECT 2"]], + + // Block comment edge cases + ["SELECT /* contains * star */ 1;", ["SELECT /* contains * star */ 1"]], + + // Non-nesting block comments (SQLite does NOT support nesting) + ["SELECT /* outer /* inner */ 1;", ["SELECT /* outer /* inner */ 1"]], + + // CRLF line endings + ["SELECT 1;\r\nSELECT 2;\r\n", ["SELECT 1", "SELECT 2"]], + + // BEGIN...END block with internal semicolons (treated as single query) + [ + "BEGIN\n\tSELECT 1;\n\tSELECT 2;\nEND;", + ["BEGIN\n\tSELECT 1;\n\tSELECT 2;\nEND"], + ], + + // BEGIN...END with only END (no internal queries) + ["BEGIN END;", ["BEGIN END"]], + ["BEGIN\nEND;", ["BEGIN\nEND"]], + + // BEGIN keyword inside a string literal (should not trigger BEGIN...END) + ["SELECT 'BEGIN';", ["SELECT 'BEGIN'"]], + ['SELECT "BEGIN";', ['SELECT "BEGIN"']], + + // Multiple BEGIN...END blocks as separate queries + [ + "BEGIN\n\tSELECT 1;\nEND;\nBEGIN\n\tSELECT 2;\nEND;", + ["BEGIN\n\tSELECT 1;\nEND", "BEGIN\n\tSELECT 2;\nEND"], + ], + + // BEGIN...END with string containing semicolons + [ + "BEGIN\n\tSELECT 'a;b';\nEND;", + ["BEGIN\n\tSELECT 'a;b';\nEND"], + ], + + // BEGIN...END preceded by other content (CREATE TRIGGER) + [ + "CREATE TRIGGER t AFTER INSERT ON x FOR EACH ROW\nBEGIN\n\tSELECT 1;\nEND;", + ["CREATE TRIGGER t AFTER INSERT ON x FOR EACH ROW\nBEGIN\n\tSELECT 1;\nEND"], + ], + + // BEGIN TRANSACTION is a simple statement (not compound) + ["BEGIN TRANSACTION; SELECT 1;", ["BEGIN TRANSACTION", "SELECT 1"]], + ["BEGIN DEFERRED; SELECT 1;", ["BEGIN DEFERRED", "SELECT 1"]], + ["BEGIN IMMEDIATE; SELECT 1;", ["BEGIN IMMEDIATE", "SELECT 1"]], + ["BEGIN EXCLUSIVE; SELECT 1;", ["BEGIN EXCLUSIVE", "SELECT 1"]], + + // Bare BEGIN; is a simple statement (transaction) + ["BEGIN; SELECT 1;", ["BEGIN", "SELECT 1"]], + + // Mixed identifier styles in one query + ['SELECT [a], "b", `c` FROM t;', ['SELECT [a], "b", `c` FROM t']], + ]; + } + + + /** + * @return list, list}> + */ + protected function provideChunkBoundaryData(): array + { + return [ + // Single-quoted string spanning chunks + [ + ["SELECT 'a;b", "c';"], + ["SELECT 'a;bc'"], + ], + // Double-quoted identifier spanning chunks + [ + ['SELECT "a;b', 'c";'], + ['SELECT "a;bc"'], + ], + // Backtick identifier spanning chunks + [ + ['SELECT `a;b', 'c`;'], + ['SELECT `a;bc`'], + ], + // Bracket identifier spanning chunks + [ + ["SELECT [col;na", "me] FROM t;"], + ["SELECT [col;name] FROM t"], + ], + // Block comment spanning chunks + [ + ["SELECT /* a;b", "c */ 1;"], + ["SELECT /* a;bc */ 1"], + ], + // Block comment in leading whitespace spanning chunks + [ + ["/* x;y", "z */ SELECT 1;"], + ["SELECT 1"], + ], + // BEGIN...END with string spanning chunks + [ + ["BEGIN\n\tSELECT 'a;b", "c';\nEND;"], + ["BEGIN\n\tSELECT 'a;bc';\nEND"], + ], + ]; + } +} + + +(new SqliteMultiQueryParserTest())->run(); diff --git a/tests/data/sqlite.sql b/tests/data/sqlite.sql new file mode 100644 index 0000000..d2f2e2d --- /dev/null +++ b/tests/data/sqlite.sql @@ -0,0 +1,237 @@ +CREATE TABLE authors +( + id INTEGER NOT NULL, + name TEXT NOT NULL, + web TEXT NOT NULL, + born TEXT DEFAULT NULL, + favorite_author_id INTEGER, + PRIMARY KEY (id AUTOINCREMENT), + FOREIGN KEY (favorite_author_id) REFERENCES authors (id) +); + + +CREATE TABLE publishers +( + publisher_id INTEGER NOT NULL, + name TEXT NOT NULL, + PRIMARY KEY (publisher_id AUTOINCREMENT) +); + + +CREATE TABLE tags +( + id INTEGER NOT NULL, + name TEXT NOT NULL, + is_global TEXT NOT NULL, + PRIMARY KEY (id AUTOINCREMENT) +); + +CREATE TABLE eans +( + id INTEGER NOT NULL, + code TEXT NOT NULL, + type INTEGER NOT NULL, + PRIMARY KEY (id AUTOINCREMENT) +); + +CREATE TABLE books +( + id INTEGER NOT NULL, + author_id INTEGER NOT NULL, + translator_id INTEGER, + title TEXT NOT NULL, + next_part INTEGER, + publisher_id INTEGER NOT NULL, + published_at TEXT NOT NULL, + printed_at TEXT, + ean_id INTEGER, + price INTEGER, + price_currency TEXT, + orig_price_cents INTEGER, + orig_price_currency TEXT, + PRIMARY KEY (id AUTOINCREMENT), + FOREIGN KEY (author_id) REFERENCES authors (id), + FOREIGN KEY (translator_id) REFERENCES authors (id), + FOREIGN KEY (next_part) REFERENCES books (id), + FOREIGN KEY (publisher_id) REFERENCES publishers (publisher_id), + FOREIGN KEY (ean_id) REFERENCES eans (id) +); + +CREATE INDEX book_title ON books (title); + + +CREATE TABLE books_x_tags +( + book_id INTEGER NOT NULL, + tag_id INTEGER NOT NULL, + PRIMARY KEY (book_id, tag_id), + FOREIGN KEY (tag_id) REFERENCES tags (id), + FOREIGN KEY (book_id) REFERENCES books (id) ON DELETE CASCADE +); + + +CREATE TABLE tag_followers +( + tag_id INTEGER NOT NULL, + author_id INTEGER NOT NULL, + created_at TEXT NOT NULL, + PRIMARY KEY (tag_id, author_id), + FOREIGN KEY (tag_id) REFERENCES tags (id) ON DELETE CASCADE, + FOREIGN KEY (author_id) REFERENCES authors (id) ON DELETE CASCADE +); + + +CREATE TABLE contents +( + id INTEGER NOT NULL, + type TEXT NOT NULL, + thread_id INTEGER, + replied_at TEXT, + PRIMARY KEY (id), + FOREIGN KEY (thread_id) REFERENCES contents (id) +); + + +CREATE TABLE book_collections +( + id INTEGER NOT NULL, + name TEXT NOT NULL, + updated_at TEXT NULL, + PRIMARY KEY (id) +); + + +CREATE TABLE photo_albums +( + id INTEGER NOT NULL, + title TEXT NOT NULL, + preview_id INTEGER NULL, + PRIMARY KEY (id AUTOINCREMENT) +); + + +CREATE TABLE photos +( + id INTEGER NOT NULL, + title TEXT NOT NULL, + album_id INTEGER NOT NULL, + PRIMARY KEY (id AUTOINCREMENT), + FOREIGN KEY (album_id) REFERENCES photo_albums (id) ON DELETE CASCADE +); + + +CREATE TABLE users +( + id INTEGER NOT NULL, + PRIMARY KEY (id AUTOINCREMENT) +); + + +CREATE TABLE user_stats +( + user_id INTEGER NOT NULL, + date TEXT NOT NULL, + value INTEGER NOT NULL, + PRIMARY KEY (user_id, date), + FOREIGN KEY (user_id) REFERENCES users (id) +); + + +CREATE TABLE users_x_users +( + my_friends_id INTEGER NOT NULL, + friends_with_me_id INTEGER NOT NULL, + PRIMARY KEY (my_friends_id, friends_with_me_id), + FOREIGN KEY (my_friends_id) REFERENCES users (id) ON DELETE CASCADE, + FOREIGN KEY (friends_with_me_id) REFERENCES users (id) +); + + +CREATE TABLE logs +( + date TEXT NOT NULL, + count INTEGER NOT NULL, + PRIMARY KEY (date) +); + + +CREATE TABLE publishers_x_tags +( + publisher_id INTEGER NOT NULL, + tag_id INTEGER NOT NULL, + PRIMARY KEY (publisher_id, tag_id), + FOREIGN KEY (tag_id) REFERENCES tags (id), + FOREIGN KEY (publisher_id) REFERENCES publishers (publisher_id) ON DELETE CASCADE +); + +PRAGMA foreign_keys = ON; + +CREATE VIEW active_authors AS +SELECT a.id, a.name FROM authors a +WHERE EXISTS (SELECT 1 FROM books b WHERE b.author_id = a.id); + +CREATE TRIGGER trigger_book_collections_update + AFTER UPDATE ON book_collections + FOR EACH ROW +BEGIN + UPDATE book_collections SET updated_at = datetime('now') WHERE id = NEW.id; +END; + +CREATE TRIGGER trigger_book_collections_insert + AFTER INSERT ON book_collections + FOR EACH ROW +BEGIN + UPDATE book_collections SET updated_at = datetime('now') WHERE id = NEW.id; +END; + +DELETE FROM books_x_tags; +DELETE FROM publishers_x_tags; +DELETE FROM books; +DELETE FROM eans; +DELETE FROM tags; +DELETE FROM authors; +DELETE FROM publishers; +DELETE FROM tag_followers; +DELETE FROM contents; +DELETE FROM users_x_users; +DELETE FROM user_stats; +DELETE FROM users; +DELETE FROM logs; + +INSERT INTO authors (id, name, web, born) VALUES (1, 'Writer 1', 'http://example.com/1', NULL); +INSERT INTO authors (id, name, web, born) VALUES (2, 'Writer 2', 'http://example.com/2', NULL); + +INSERT INTO publishers (publisher_id, name) VALUES (1, 'Nextras publisher A'); +INSERT INTO publishers (publisher_id, name) VALUES (2, 'Nextras publisher B'); +INSERT INTO publishers (publisher_id, name) VALUES (3, 'Nextras publisher C'); + +INSERT INTO tags (id, name, is_global) VALUES (1, 'Tag 1', 'y'); +INSERT INTO tags (id, name, is_global) VALUES (2, 'Tag 2', 'y'); +INSERT INTO tags (id, name, is_global) VALUES (3, 'Tag 3', 'n'); + +INSERT INTO books (id, author_id, translator_id, title, next_part, publisher_id, published_at, price, price_currency) VALUES (1, 1, 1, 'Book 1', NULL, 1, '2021-12-14 21:10:04', 50, 'CZK'); +INSERT INTO books (id, author_id, translator_id, title, next_part, publisher_id, published_at, price, price_currency) VALUES (2, 1, NULL, 'Book 2', NULL, 2, '2021-12-14 21:10:02', 150, 'CZK'); +INSERT INTO books (id, author_id, translator_id, title, next_part, publisher_id, published_at, price, price_currency) VALUES (3, 2, 2, 'Book 3', NULL, 3, '2021-12-14 21:10:03', 20, 'CZK'); +INSERT INTO books (id, author_id, translator_id, title, next_part, publisher_id, published_at, price, price_currency) VALUES (4, 2, 2, 'Book 4', 3, 1, '2021-12-14 21:10:01', 220, 'CZK'); + +INSERT INTO `books_x_tags` (`book_id`, `tag_id`) VALUES (1, 1); +INSERT INTO books_x_tags (book_id, tag_id) VALUES (1, 2); +INSERT INTO books_x_tags (book_id, tag_id) VALUES (2, 2); +INSERT INTO books_x_tags (book_id, tag_id) VALUES (2, 3); +INSERT INTO books_x_tags (book_id, tag_id) VALUES (3, 3); + +INSERT INTO tag_followers (tag_id, author_id, created_at) VALUES (1, 1, '2014-01-01 00:10:00'); +INSERT INTO tag_followers (tag_id, author_id, created_at) VALUES (3, 1, '2014-01-02 00:10:00'); +INSERT INTO tag_followers (tag_id, author_id, created_at) VALUES (2, 2, '2014-01-03 00:10:00'); + +INSERT INTO contents (id, type, thread_id, replied_at) VALUES (1, 'thread', NULL, NULL); +INSERT INTO contents (id, type, thread_id, replied_at) VALUES (2, 'comment', 1, '2020-01-01 12:00:00'); +INSERT INTO contents (id, type, thread_id, replied_at) VALUES (3, 'comment', 1, '2020-01-02 12:00:00'); + +INSERT INTO authors (id, name, web, born) VALUES (99, 'it''s a; test', 'http://example.com/99', NULL); + +/* Block comment with ; semicolons inside */ +SELECT [bracket;identifier] FROM authors; +SELECT [escaped]]bracket] FROM authors; +SELECT "double;quoted" FROM authors; +SELECT `backtick;quoted` FROM authors; From 5a4022db3d4f59af2ae5a25a35c61e895b65afaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Tvrd=C3=ADk?= Date: Tue, 24 Feb 2026 09:26:09 +0100 Subject: [PATCH 2/3] Add SQLite to supported databases in readme --- readme.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 61fd863..e4e3c9e 100644 --- a/readme.md +++ b/readme.md @@ -12,6 +12,7 @@ A streaming PHP parser for splitting multi-query SQL files into individual state - **MySQL** -- backtick identifiers, `DELIMITER` command, `#` comments - **PostgreSQL** -- dollar-quoted strings (`$BODY$...$BODY$`), `E'...'` escape strings - **SQL Server** -- `[bracketed]` identifiers, `BEGIN...END` blocks +- **SQLite** -- all three identifier styles (`"double"`, `` `backtick` ``, `[bracket]`), `BEGIN...END` blocks for triggers All parsers handle standard SQL comments (`--`, `/* */`), quoted strings, and semicolon delimiters. @@ -57,7 +58,7 @@ foreach ($parser->parseFileStream($stream) as $query) { } ``` -Available parsers: `MySqlMultiQueryParser`, `PostgreSqlMultiQueryParser`, `SqlServerMultiQueryParser`. +Available parsers: `MySqlMultiQueryParser`, `PostgreSqlMultiQueryParser`, `SqlServerMultiQueryParser`, `SqliteMultiQueryParser`. ### License From b6990e19d8579c8a226c2b7eb27a606d3a68f0e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Tvrd=C3=ADk?= Date: Tue, 24 Feb 2026 09:34:46 +0100 Subject: [PATCH 3/3] Simplify SQLite parser regex using PCRE DEFINE Factor out inner construct bodies (sqI, dqI, btI, bkI, bcI) and reusable sub-patterns (skip, stmt, lc) into a DEFINE block, replacing the $simpleQuery string concatenation hack with a single self-contained pattern. (*PRUNE) remains inline at the call sites because PCRE confines backtracking verbs to subroutine scope. --- src/SqliteMultiQueryParser.php | 73 +++++++++++++++++++--------------- 1 file changed, 41 insertions(+), 32 deletions(-) diff --git a/src/SqliteMultiQueryParser.php b/src/SqliteMultiQueryParser.php index a89ea28..f8bdf82 100644 --- a/src/SqliteMultiQueryParser.php +++ b/src/SqliteMultiQueryParser.php @@ -21,45 +21,54 @@ public function parseStringStream(Iterator $stream): Iterator private function getQueryPattern(): string { - $simpleQuery = /** @lang PhpRegExp */ '~ - (?: - \s - | /\* (*PRUNE) (?: [^*]++ | \*(?!/) )*+ \*/ - | -- [^\n]*+ - )*+ - (? - (?: - [^;\'"`[/-]++ - | \' (*PRUNE) (?: \'\' | [^\'] )*+ \' - | " (*PRUNE) (?: "" | [^"] )*+ " - | ` (*PRUNE) (?: `` | [^`] )*+ ` - | \[ (*PRUNE) [^\]]*+ (?: \]\] [^\]]*+ )* \] - | /\* (*PRUNE) (?: [^*]++ | \*(?!/) )*+ \*/ - | -- [^\n]*+ - | (?!;) . - )++ - ) - ; - ~x'; + // (*PRUNE) must appear inline (not inside DEFINE subroutines) because PCRE confines + // backtracking verbs to the subroutine scope. The inner bodies are defined once in + // DEFINE and referenced after the inline (*PRUNE) to avoid pattern duplication. return /** @lang PhpRegExp */ '~ - (?: - \s - | /\* (*PRUNE) (?: [^*]++ | \*(?!/) )*+ \*/ - | -- [^\n]*+ - )*+ + (?(DEFINE) + (? (?: \'\' | [^\'] )*+ \' ) + (? (?: "" | [^"] )*+ " ) + (? (?: `` | [^`] )*+ ` ) + (? [^\]]*+ (?: \]\] [^\]]*+ )* \] ) + (? (?: [^*]++ | \*(?!/) )*+ \*/ ) + (? -- [^\n]*+ ) + (? + (?: + \s + | /\* (*PRUNE) (?&bcI) + | (?&lc) + )*+ + ) + (? + (?&skip) + (?: + [^;\'"`[/-]++ + | \' (*PRUNE) (?&sqI) + | " (*PRUNE) (?&dqI) + | ` (*PRUNE) (?&btI) + | \[ (*PRUNE) (?&bkI) + | /\* (*PRUNE) (?&bcI) + | (?&lc) + | (?!;) . + )++ + ; + ) + ) + + (?&skip) (?: (?: (? (?: [^bB;\'"`[/-]++ - | \' (*PRUNE) (?: \'\' | [^\'] )*+ \' - | " (*PRUNE) (?: "" | [^"] )*+ " - | ` (*PRUNE) (?: `` | [^`] )*+ ` - | \[ (*PRUNE) [^\]]*+ (?: \]\] [^\]]*+ )* \] - | /\* (*PRUNE) (?: [^*]++ | \*(?!/) )*+ \*/ - | (?i:BEGIN) (?!\s*(?:(?i:TRANSACTION|DEFERRED|IMMEDIATE|EXCLUSIVE)\b|;|\z)) (*PRUNE) (?: (?i:\s*END)\s*| ' . substr($simpleQuery, 1, -2) . ')* - | -- [^\n]*+ + | \' (*PRUNE) (?&sqI) + | " (*PRUNE) (?&dqI) + | ` (*PRUNE) (?&btI) + | \[ (*PRUNE) (?&bkI) + | /\* (*PRUNE) (?&bcI) + | (?i:BEGIN) (?!\s*(?:(?i:TRANSACTION|DEFERRED|IMMEDIATE|EXCLUSIVE)\b|;|\z)) (*PRUNE) (?: (?i:\s*END)\s* | (?&stmt) )* + | (?&lc) | (?!;) . )*+ )