diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..3c44241 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +indent_style = space +indent_size = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/.github/workflows/CodingStyles.yml b/.github/workflows/CodingStyles.yml new file mode 100644 index 0000000..09840a9 --- /dev/null +++ b/.github/workflows/CodingStyles.yml @@ -0,0 +1,32 @@ +name: CodingStyles + +on: push + +jobs: + tests: + name: Coding Styles + runs-on: ubuntu-latest + + strategy: + fail-fast: true + matrix: + php: + - 8.0 + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: xdebug + ini-values: memory_limit=1G + tools: cs2pr + + - name: Install Composer dependencies + run: composer install --no-progress --no-suggest --prefer-dist --optimize-autoloader + + - name: Coding styles + run: php vendor/bin/php-cs-fixer fix --verbose --dry-run --format=checkstyle | cs2pr diff --git a/.github/workflows/Tests.yml b/.github/workflows/Tests.yml new file mode 100644 index 0000000..7baf59d --- /dev/null +++ b/.github/workflows/Tests.yml @@ -0,0 +1,62 @@ +name: Tests + +on: push + +jobs: + linux-tests: + name: Linux-Tests with PHP ${{ matrix.php-versions }} + runs-on: ubuntu-latest + + env: + DB_ADAPTER: pdo + DB_PDO_PROTOCOL: sqlite + DB_SQLITE_IN_MEMORY: true + + strategy: + fail-fast: true + matrix: + php: + - 8.0 + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: xdebug + ini-values: memory_limit=1G + + - name: Install Composer dependencies + run: composer install --no-progress --prefer-dist --optimize-autoloader + + - name: Tests + run: vendor/bin/phpunit -v + + windows-tests: + name: Windows-Tests with PHP ${{ matrix.php-versions }} + runs-on: windows-2019 + + strategy: + fail-fast: true + matrix: + php-versions: ['8.0'] + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + ini-values: memory_limit=1G, + extensions: pdo_sqlite + + - name: Install Composer dependencies + run: composer update --no-progress --prefer-dist --optimize-autoloader + + - name: Tests + run: vendor/bin/phpunit --exclude linux diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9f13141 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +composer.lock +html +.phpunit.result.cache +.php_cs.cache +vendor diff --git a/.php_cs b/.php_cs new file mode 100644 index 0000000..5ade4c4 --- /dev/null +++ b/.php_cs @@ -0,0 +1,17 @@ +setRules([ + '@Symfony' => true, + '@Symfony:risky' => true, + 'array_indentation' => true, + 'phpdoc_summary' => false, + ]) + ->setRiskyAllowed(true) + ->setFinder( + PhpCsFixer\Finder::create() + ->in(__DIR__.'/src') + ->in(__DIR__.'/tests') + ->name('*.php') + ->append([__FILE__]) + ); diff --git a/.scrutinizer.yml b/.scrutinizer.yml new file mode 100644 index 0000000..e010c8f --- /dev/null +++ b/.scrutinizer.yml @@ -0,0 +1,12 @@ +build: + nodes: + coverage: + tests: + override: + - command: XDEBUG_MODE=coverage php vendor/bin/phpunit --coverage-clover coverage/clover.xml + coverage: + file: coverage/clover.xml + format: clover + environment: + php: + version: 8.0 diff --git a/LICENSE b/LICENSE index de42d26..97d1eee 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,640 @@ -MIT License - -Copyright (c) 2021 QuickRdf - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +Also see section "Acknowledgement" in `README.md`. + +--- + +# GNU GENERAL PUBLIC LICENSE +Version 3, 29 June 2007 + +Copyright (C) 2007 [Free Software Foundation, Inc.](http://fsf.org/) + +Everyone is permitted to copy and distribute verbatim copies of this license +document, but changing it is not allowed. + +## Preamble + +The GNU General Public License is a free, copyleft license for software and +other kinds of works. + +The licenses for most software and other practical works are designed to take +away your freedom to share and change the works. By contrast, the GNU General +Public License is intended to guarantee your freedom to share and change all +versions of a program--to make sure it remains free software for all its users. +We, the Free Software Foundation, use the GNU General Public License for most +of our software; it applies also to any other work released this way by its +authors. You can apply it to your programs, too. + +When we speak of free software, we are referring to freedom, not price. Our +General Public Licenses are designed to make sure that you have the freedom to +distribute copies of free software (and charge for them if you wish), that you +receive source code or can get it if you want it, that you can change the +software or use pieces of it in new free programs, and that you know you can do +these things. + +To protect your rights, we need to prevent others from denying you these rights +or asking you to surrender the rights. Therefore, you have certain +responsibilities if you distribute copies of the software, or if you modify it: +responsibilities to respect the freedom of others. + +For example, if you distribute copies of such a program, whether gratis or for +a fee, you must pass on to the recipients the same freedoms that you received. +You must make sure that they, too, receive or can get the source code. And you +must show them these terms so they know their rights. + +Developers that use the GNU GPL protect your rights with two steps: + + 1. assert copyright on the software, and + 2. offer you this License giving you legal permission to copy, distribute + and/or modify it. + +For the developers' and authors' protection, the GPL clearly explains that +there is no warranty for this free software. For both users' and authors' sake, +the GPL requires that modified versions be marked as changed, so that their +problems will not be attributed erroneously to authors of previous versions. + +Some devices are designed to deny users access to install or run modified +versions of the software inside them, although the manufacturer can do so. This +is fundamentally incompatible with the aim of protecting users' freedom to +change the software. The systematic pattern of such abuse occurs in the area of +products for individuals to use, which is precisely where it is most +unacceptable. Therefore, we have designed this version of the GPL to prohibit +the practice for those products. If such problems arise substantially in other +domains, we stand ready to extend this provision to those domains in future +versions of the GPL, as needed to protect the freedom of users. + +Finally, every program is threatened constantly by software patents. States +should not allow patents to restrict development and use of software on +general-purpose computers, but in those that do, we wish to avoid the special +danger that patents applied to a free program could make it effectively +proprietary. To prevent this, the GPL assures that patents cannot be used to +render the program non-free. + +The precise terms and conditions for copying, distribution and modification +follow. + +## TERMS AND CONDITIONS + +### 0. Definitions. + +*This License* refers to version 3 of the GNU General Public License. + +*Copyright* also means copyright-like laws that apply to other kinds of works, +such as semiconductor masks. + +*The Program* refers to any copyrightable work licensed under this License. +Each licensee is addressed as *you*. *Licensees* and *recipients* may be +individuals or organizations. + +To *modify* a work means to copy from or adapt all or part of the work in a +fashion requiring copyright permission, other than the making of an exact copy. +The resulting work is called a *modified version* of the earlier work or a work +*based on* the earlier work. + +A *covered work* means either the unmodified Program or a work based on the +Program. + +To *propagate* a work means to do anything with it that, without permission, +would make you directly or secondarily liable for infringement under applicable +copyright law, except executing it on a computer or modifying a private copy. +Propagation includes copying, distribution (with or without modification), +making available to the public, and in some countries other activities as well. + +To *convey* a work means any kind of propagation that enables other parties to +make or receive copies. Mere interaction with a user through a computer +network, with no transfer of a copy, is not conveying. + +An interactive user interface displays *Appropriate Legal Notices* to the +extent that it includes a convenient and prominently visible feature that + + 1. displays an appropriate copyright notice, and + 2. tells the user that there is no warranty for the work (except to the + extent that warranties are provided), that licensees may convey the work + under this License, and how to view a copy of this License. + +If the interface presents a list of user commands or options, such as a menu, a +prominent item in the list meets this criterion. + +### 1. Source Code. + +The *source code* for a work means the preferred form of the work for making +modifications to it. *Object code* means any non-source form of a work. + +A *Standard Interface* means an interface that either is an official standard +defined by a recognized standards body, or, in the case of interfaces specified +for a particular programming language, one that is widely used among developers +working in that language. + +The *System Libraries* of an executable work include anything, other than the +work as a whole, that (a) is included in the normal form of packaging a Major +Component, but which is not part of that Major Component, and (b) serves only +to enable use of the work with that Major Component, or to implement a Standard +Interface for which an implementation is available to the public in source code +form. A *Major Component*, in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system (if any) on +which the executable work runs, or a compiler used to produce the work, or an +object code interpreter used to run it. + +The *Corresponding Source* for a work in object code form means all the source +code needed to generate, install, and (for an executable work) run the object +code and to modify the work, including scripts to control those activities. +However, it does not include the work's System Libraries, or general-purpose +tools or generally available free programs which are used unmodified in +performing those activities but which are not part of the work. For example, +Corresponding Source includes interface definition files associated with source +files for the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, such as +by intimate data communication or control flow between those subprograms and +other parts of the work. + +The Corresponding Source need not include anything that users can regenerate +automatically from other parts of the Corresponding Source. + +The Corresponding Source for a work in source code form is that same work. + +### 2. Basic Permissions. + +All rights granted under this License are granted for the term of copyright on +the Program, and are irrevocable provided the stated conditions are met. This +License explicitly affirms your unlimited permission to run the unmodified +Program. The output from running a covered work is covered by this License only +if the output, given its content, constitutes a covered work. This License +acknowledges your rights of fair use or other equivalent, as provided by +copyright law. + +You may make, run and propagate covered works that you do not convey, without +conditions so long as your license otherwise remains in force. You may convey +covered works to others for the sole purpose of having them make modifications +exclusively for you, or provide you with facilities for running those works, +provided that you comply with the terms of this License in conveying all +material for which you do not control copyright. Those thus making or running +the covered works for you must do so exclusively on your behalf, under your +direction and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under the +conditions stated below. Sublicensing is not allowed; section 10 makes it +unnecessary. + +### 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + +No covered work shall be deemed part of an effective technological measure +under any applicable law fulfilling obligations under article 11 of the WIPO +copyright treaty adopted on 20 December 1996, or similar laws prohibiting or +restricting circumvention of such measures. + +When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention is +effected by exercising rights under this License with respect to the covered +work, and you disclaim any intention to limit operation or modification of the +work as a means of enforcing, against the work's users, your or third parties' +legal rights to forbid circumvention of technological measures. + +### 4. Conveying Verbatim Copies. + +You may convey verbatim copies of the Program's source code as you receive it, +in any medium, provided that you conspicuously and appropriately publish on +each copy an appropriate copyright notice; keep intact all notices stating that +this License and any non-permissive terms added in accord with section 7 apply +to the code; keep intact all notices of the absence of any warranty; and give +all recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, and you may +offer support or warranty protection for a fee. + +### 5. Conveying Modified Source Versions. + +You may convey a work based on the Program, or the modifications to produce it +from the Program, in the form of source code under the terms of section 4, +provided that you also meet all of these conditions: + + - a) The work must carry prominent notices stating that you modified it, and + giving a relevant date. + - b) The work must carry prominent notices stating that it is released under + this License and any conditions added under section 7. This requirement + modifies the requirement in section 4 to *keep intact all notices*. + - c) You must license the entire work, as a whole, under this License to + anyone who comes into possession of a copy. This License will therefore + apply, along with any applicable section 7 additional terms, to the whole + of the work, and all its parts, regardless of how they are packaged. This + License gives no permission to license the work in any other way, but it + does not invalidate such permission if you have separately received it. + - d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your work need + not make them do so. + +A compilation of a covered work with other separate and independent works, +which are not by their nature extensions of the covered work, and which are not +combined with it such as to form a larger program, in or on a volume of a +storage or distribution medium, is called an *aggregate* if the compilation and +its resulting copyright are not used to limit the access or legal rights of the +compilation's users beyond what the individual works permit. Inclusion of a +covered work in an aggregate does not cause this License to apply to the other +parts of the aggregate. + +### 6. Conveying Non-Source Forms. + +You may convey a covered work in object code form under the terms of sections 4 +and 5, provided that you also convey the machine-readable Corresponding Source +under the terms of this License, in one of these ways: + + - a) Convey the object code in, or embodied in, a physical product (including + a physical distribution medium), accompanied by the Corresponding Source + fixed on a durable physical medium customarily used for software + interchange. + - b) Convey the object code in, or embodied in, a physical product (including + a physical distribution medium), accompanied by a written offer, valid for + at least three years and valid for as long as you offer spare parts or + customer support for that product model, to give anyone who possesses the + object code either + 1. a copy of the Corresponding Source for all the software in the product + that is covered by this License, on a durable physical medium + customarily used for software interchange, for a price no more than your + reasonable cost of physically performing this conveying of source, or + 2. access to copy the Corresponding Source from a network server at no + charge. + - c) Convey individual copies of the object code with a copy of the written + offer to provide the Corresponding Source. This alternative is allowed only + occasionally and noncommercially, and only if you received the object code + with such an offer, in accord with subsection 6b. + - d) Convey the object code by offering access from a designated place + (gratis or for a charge), and offer equivalent access to the Corresponding + Source in the same way through the same place at no further charge. You + need not require recipients to copy the Corresponding Source along with the + object code. If the place to copy the object code is a network server, the + Corresponding Source may be on a different server operated by you or a + third party) that supports equivalent copying facilities, provided you + maintain clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the Corresponding + Source, you remain obligated to ensure that it is available for as long as + needed to satisfy these requirements. + - e) Convey the object code using peer-to-peer transmission, provided you + inform other peers where the object code and Corresponding Source of the + work are being offered to the general public at no charge under subsection + 6d. + +A separable portion of the object code, whose source code is excluded from the +Corresponding Source as a System Library, need not be included in conveying the +object code work. + +A *User Product* is either + + 1. a *consumer product*, which means any tangible personal property which is + normally used for personal, family, or household purposes, or + 2. anything designed or sold for incorporation into a dwelling. + +In determining whether a product is a consumer product, doubtful cases shall be +resolved in favor of coverage. For a particular product received by a +particular user, *normally used* refers to a typical or common use of that +class of product, regardless of the status of the particular user or of the way +in which the particular user actually uses, or expects or is expected to use, +the product. A product is a consumer product regardless of whether the product +has substantial commercial, industrial or non-consumer uses, unless such uses +represent the only significant mode of use of the product. + +*Installation Information* for a User Product means any methods, procedures, +authorization keys, or other information required to install and execute +modified versions of a covered work in that User Product from a modified +version of its Corresponding Source. The information must suffice to ensure +that the continued functioning of the modified object code is in no case +prevented or interfered with solely because modification has been made. + +If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as part of a +transaction in which the right of possession and use of the User Product is +transferred to the recipient in perpetuity or for a fixed term (regardless of +how the transaction is characterized), the Corresponding Source conveyed under +this section must be accompanied by the Installation Information. But this +requirement does not apply if neither you nor any third party retains the +ability to install modified object code on the User Product (for example, the +work has been installed in ROM). + +The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates for a +work that has been modified or installed by the recipient, or for the User +Product in which it has been modified or installed. Access to a network may be +denied when the modification itself materially and adversely affects the +operation of the network or violates the rules and protocols for communication +across the network. + +Corresponding Source conveyed, and Installation Information provided, in accord +with this section must be in a format that is publicly documented (and with an +implementation available to the public in source code form), and must require +no special password or key for unpacking, reading or copying. + +### 7. Additional Terms. + +*Additional permissions* are terms that supplement the terms of this License by +making exceptions from one or more of its conditions. Additional permissions +that are applicable to the entire Program shall be treated as though they were +included in this License, to the extent that they are valid under applicable +law. If additional permissions apply only to part of the Program, that part may +be used separately under those permissions, but the entire Program remains +governed by this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option remove any +additional permissions from that copy, or from any part of it. (Additional +permissions may be written to require their own removal in certain cases when +you modify the work.) You may place additional permissions on material, added +by you to a covered work, for which you have or can give appropriate copyright +permission. + +Notwithstanding any other provision of this License, for material you add to a +covered work, you may (if authorized by the copyright holders of that material) +supplement the terms of this License with terms: + + - a) Disclaiming warranty or limiting liability differently from the terms of + sections 15 and 16 of this License; or + - b) Requiring preservation of specified reasonable legal notices or author + attributions in that material or in the Appropriate Legal Notices displayed + by works containing it; or + - c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in reasonable + ways as different from the original version; or + - d) Limiting the use for publicity purposes of names of licensors or authors + of the material; or + - e) Declining to grant rights under trademark law for use of some trade + names, trademarks, or service marks; or + - f) Requiring indemnification of licensors and authors of that material by + anyone who conveys the material (or modified versions of it) with + contractual assumptions of liability to the recipient, for any liability + that these contractual assumptions directly impose on those licensors and + authors. + +All other non-permissive additional terms are considered *further restrictions* +within the meaning of section 10. If the Program as you received it, or any +part of it, contains a notice stating that it is governed by this License along +with a term that is a further restriction, you may remove that term. If a +license document contains a further restriction but permits relicensing or +conveying under this License, you may add to a covered work material governed +by the terms of that license document, provided that the further restriction +does not survive such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you must place, +in the relevant source files, a statement of the additional terms that apply to +those files, or a notice indicating where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the form of a +separately written license, or stated as exceptions; the above requirements +apply either way. + +### 8. Termination. + +You may not propagate or modify a covered work except as expressly provided +under this License. Any attempt otherwise to propagate or modify it is void, +and will automatically terminate your rights under this License (including any +patent licenses granted under the third paragraph of section 11). + +However, if you cease all violation of this License, then your license from a +particular copyright holder is reinstated + + - a) provisionally, unless and until the copyright holder explicitly and + finally terminates your license, and + - b) permanently, if the copyright holder fails to notify you of the + violation by some reasonable means prior to 60 days after the cessation. + +Moreover, your license from a particular copyright holder is reinstated +permanently if the copyright holder notifies you of the violation by some +reasonable means, this is the first time you have received notice of violation +of this License (for any work) from that copyright holder, and you cure the +violation prior to 30 days after your receipt of the notice. + +Termination of your rights under this section does not terminate the licenses +of parties who have received copies or rights from you under this License. If +your rights have been terminated and not permanently reinstated, you do not +qualify to receive new licenses for the same material under section 10. + +### 9. Acceptance Not Required for Having Copies. + +You are not required to accept this License in order to receive or run a copy +of the Program. Ancillary propagation of a covered work occurring solely as a +consequence of using peer-to-peer transmission to receive a copy likewise does +not require acceptance. However, nothing other than this License grants you +permission to propagate or modify any covered work. These actions infringe +copyright if you do not accept this License. Therefore, by modifying or +propagating a covered work, you indicate your acceptance of this License to do +so. + +### 10. Automatic Licensing of Downstream Recipients. + +Each time you convey a covered work, the recipient automatically receives a +license from the original licensors, to run, modify and propagate that work, +subject to this License. You are not responsible for enforcing compliance by +third parties with this License. + +An *entity transaction* is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered work +results from an entity transaction, each party to that transaction who receives +a copy of the work also receives whatever licenses to the work the party's +predecessor in interest had or could give under the previous paragraph, plus a +right to possession of the Corresponding Source of the work from the +predecessor in interest, if the predecessor has it or can get it with +reasonable efforts. + +You may not impose any further restrictions on the exercise of the rights +granted or affirmed under this License. For example, you may not impose a +license fee, royalty, or other charge for exercise of rights granted under this +License, and you may not initiate litigation (including a cross-claim or +counterclaim in a lawsuit) alleging that any patent claim is infringed by +making, using, selling, offering for sale, or importing the Program or any +portion of it. + +### 11. Patents. + +A *contributor* is a copyright holder who authorizes use under this License of +the Program or a work on which the Program is based. The work thus licensed is +called the contributor's *contributor version*. + +A contributor's *essential patent claims* are all patent claims owned or +controlled by the contributor, whether already acquired or hereafter acquired, +that would be infringed by some manner, permitted by this License, of making, +using, or selling its contributor version, but do not include claims that would +be infringed only as a consequence of further modification of the contributor +version. For purposes of this definition, *control* includes the right to grant +patent sublicenses in a manner consistent with the requirements of this +License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free patent +license under the contributor's essential patent claims, to make, use, sell, +offer for sale, import and otherwise run, modify and propagate the contents of +its contributor version. + +In the following three paragraphs, a *patent license* is any express agreement +or commitment, however denominated, not to enforce a patent (such as an express +permission to practice a patent or covenant not to sue for patent +infringement). To *grant* such a patent license to a party means to make such +an agreement or commitment not to enforce a patent against the party. + +If you convey a covered work, knowingly relying on a patent license, and the +Corresponding Source of the work is not available for anyone to copy, free of +charge and under the terms of this License, through a publicly available +network server or other readily accessible means, then you must either + + 1. cause the Corresponding Source to be so available, or + 2. arrange to deprive yourself of the benefit of the patent license for this + particular work, or + 3. arrange, in a manner consistent with the requirements of this License, to + extend the patent license to downstream recipients. + +*Knowingly relying* means you have actual knowledge that, but for the patent +license, your conveying the covered work in a country, or your recipient's use +of the covered work in a country, would infringe one or more identifiable +patents in that country that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or arrangement, you +convey, or propagate by procuring conveyance of, a covered work, and grant a +patent license to some of the parties receiving the covered work authorizing +them to use, propagate, modify or convey a specific copy of the covered work, +then the patent license you grant is automatically extended to all recipients +of the covered work and works based on it. + +A patent license is *discriminatory* if it does not include within the scope of +its coverage, prohibits the exercise of, or is conditioned on the non-exercise +of one or more of the rights that are specifically granted under this License. +You may not convey a covered work if you are a party to an arrangement with a +third party that is in the business of distributing software, under which you +make payment to the third party based on the extent of your activity of +conveying the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory patent +license + + - a) in connection with copies of the covered work conveyed by you (or copies + made from those copies), or + - b) primarily for and in connection with specific products or compilations + that contain the covered work, unless you entered into that arrangement, or + that patent license was granted, prior to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting any implied +license or other defenses to infringement that may otherwise be available to +you under applicable patent law. + +### 12. No Surrender of Others' Freedom. + +If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not excuse +you from the conditions of this License. If you cannot convey a covered work so +as to satisfy simultaneously your obligations under this License and any other +pertinent obligations, then as a consequence you may not convey it at all. For +example, if you agree to terms that obligate you to collect a royalty for +further conveying from those to whom you convey the Program, the only way you +could satisfy both those terms and this License would be to refrain entirely +from conveying the Program. + +### 13. Use with the GNU Affero General Public License. + +Notwithstanding any other provision of this License, you have permission to +link or combine any covered work with a work licensed under version 3 of the +GNU Affero General Public License into a single combined work, and to convey +the resulting work. The terms of this License will continue to apply to the +part which is the covered work, but the special requirements of the GNU Affero +General Public License, section 13, concerning interaction through a network +will apply to the combination as such. + +### 14. Revised Versions of this License. + +The Free Software Foundation may publish revised and/or new versions of the GNU +General Public License from time to time. Such new versions will be similar in +spirit to the present version, but may differ in detail to address new problems +or concerns. + +Each version is given a distinguishing version number. If the Program specifies +that a certain numbered version of the GNU General Public License *or any later +version* applies to it, you have the option of following the terms and +conditions either of that numbered version or of any later version published by +the Free Software Foundation. If the Program does not specify a version number +of the GNU General Public License, you may choose any version ever published by +the Free Software Foundation. + +If the Program specifies that a proxy can decide which future versions of the +GNU General Public License can be used, that proxy's public statement of +acceptance of a version permanently authorizes you to choose that version for +the Program. + +Later license versions may give you additional or different permissions. +However, no additional obligations are imposed on any author or copyright +holder as a result of your choosing to follow a later version. + +### 15. Disclaimer of Warranty. + +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE +LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER +PARTIES PROVIDE THE PROGRAM *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER +EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE +QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE +DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR +CORRECTION. + +### 16. Limitation of Liability. + +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY +COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS +PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, +INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE +THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED +INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE +PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY +HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +### 17. Interpretation of Sections 15 and 16. + +If the disclaimer of warranty and limitation of liability provided above cannot +be given local legal effect according to their terms, reviewing courts shall +apply local law that most closely approximates an absolute waiver of all civil +liability in connection with the Program, unless a warranty or assumption of +liability accompanies a copy of the Program in return for a fee. + +## END OF TERMS AND CONDITIONS ### + +### How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest possible +use to the public, the best way to achieve this is to make it free software +which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest to attach +them to the start of each source file to most effectively state the exclusion +of warranty; and each file should have at least the *copyright* line and a +pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + +If the program does terminal interaction, make it output a short notice like +this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w` and `show c` should show the appropriate +parts of the General Public License. Of course, your program's commands might +be different; for a GUI interface, you would use an *about box*. + +You should also get your employer (if you work as a programmer) or school, if +any, to sign a *copyright disclaimer* for the program, if necessary. For more +information on this, and how to apply and follow the GNU GPL, see +[http://www.gnu.org/licenses/](http://www.gnu.org/licenses/). + +The GNU General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may consider +it more useful to permit linking proprietary applications with the library. If +this is what you want to do, use the GNU Lesser General Public License instead +of this License. But first, please read +[http://www.gnu.org/philosophy/why-not-lgpl.html](http://www.gnu.org/philosophy/why-not-lgpl.html). \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..aac5538 --- /dev/null +++ b/README.md @@ -0,0 +1,74 @@ +# sweetrdf - RDF In-Memory Quad Store (SQLite) + +![CI](https://github.com/sweetrdf/in-memory-store-sqlite/workflows/Tests/badge.svg) +[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/sweetrdf/in-memory-store-sqlite/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/sweetrdf/in-memory-store-sqlite/?branch=master) +[![Code Coverage](https://scrutinizer-ci.com/g/sweetrdf/in-memory-store-sqlite/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/sweetrdf/in-memory-store-sqlite/?branch=master) + +RDF in-memory quad store implementation using PDO and SQLite. + +## Installation + +Use Composer to install this library using: + +> composer install sweetrdf/in-memory-store-sqlite + +## Usage + +Use `InMemoryStoreSqlite::createInstance()` to get a ready-to-use store instance (see example below). +Sending SPARQL queries can be done via `query` method. +Your data is stored inside an in-memory SQLite database file. +**After the script ends all your data inside the store will be gone**. + +### Example + +```php + +use sweetrdf\InMemoryStoreSqlite\Log\LoggerPool; +use sweetrdf\InMemoryStoreSqlite\PDOSQLiteAdapter; +use sweetrdf\InMemoryStoreSqlite\KeyValueBag; +use sweetrdf\InMemoryStoreSqlite\Store\InMemoryStoreSqlite; + +// fast way +$store = InMemoryStoreSqlite::createInstance(); +// or a way with more data control +$store = new InMemoryStoreSqlite(new PDOSQLiteAdapter(), new LoggerPool(), new KeyValueBag()); + +// send a SPARQL query which creates two triples +$store->query('INSERT INTO { + "baz" . + "label1" . +}'); + +// send another SPARQL query asking for all triples +$res = $store->query('SELECT * WHERE {?s ?p ?o.}'); +echo \count($res['result']['rows']); // outputs: 2 +``` + +## SPARQL support + +Store supports a lot of SPARQL 1.0/1.1 features. +For more information please read [SPARQL-support.md](doc/SPARQL-support.md). + +## Performance + +Store uses an in-memory SQLite file configured with: + +* `PRAGMA synchronous = OFF`, +* `PRAGMA journal_mode = OFF`, +* `PRAGMA locking_mode = EXCLUSIVE` +* `PRAGMA page_size = 4096` + +Check [PDOSQLiteAdapter.php](src/PDOSQLiteAdapter.php#L45) for more information. + +When adding several hundred or more triples at once you may experience increased execution time. +Local tests showed that per **1000** triples to add store needs around 1 sec. +If better performance is required consider using a state-of-the-art quad store like Stardog or Virtuoso. + +## License + +This work is licensed under the terms of the GPL 3 or later. + +## Acknowledgement + +This work is based on the code of ARC2 from https://github.com/semsol/arc2 (by Benjamin Nowak and contributors). +ARC2 is dual licensed under the terms of GPL 2 (or later) as well as W3C Software License. diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..910a530 --- /dev/null +++ b/composer.json @@ -0,0 +1,38 @@ +{ + "name": "sweetrdf/in-memory-store-sqlite", + "type": "library", + "description": "RDF in-memory quad store implementation using PDO and SQLite.", + "keywords": ["rdf","sparql", "in-memory store"], + "homepage": "https://github.com/sweetrdf/in-memory-store-sqlite", + "license": ["GPL-3.0-or-later"], + "authors": [ + { + "name": "Konrad Abicht", + "homepage": "https://inspirito.de", + "email": "hi@inspirito.de", + "role": "Maintainer, Developer" + } + ], + "require": { + "php": ">=8.0", + "sweetrdf/rdf-interface": "^0.3.1" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.18.1", + "phpunit/phpunit": "^9.0" + }, + "autoload": { + "files": ["src/functions.php"], + "psr-4": { + "sweetrdf\\InMemoryStoreSqlite\\": ["src/"] + } + }, + "autoload-dev": { + "psr-4": { + "Tests\\": ["tests"] + } + }, + "scripts": { + "phpunit": "vendor/bin/phpunit" + } +} diff --git a/doc/SPARQL-support.md b/doc/SPARQL-support.md new file mode 100644 index 0000000..472e4bc --- /dev/null +++ b/doc/SPARQL-support.md @@ -0,0 +1,122 @@ +# SPARQL support + +## Introduction + +This store supports many [SPARQL Query Language](http://www.w3.org/TR/rdf-sparql-query/) features ([to a certain extent](http://www.w3.org/2001/sw/DataAccess/tests/implementations)) and also a number of pragmatic extensions such as aggregates (AVG / COUNT / MAX / MIN / SUM) and write mechanisms. +The changes to the SPARQL specification were kept at a minimum, so that the existing grammar parser and store functionality can be re-used. + +This page documents the core differences between SPARQL and what is called "SPARQL+" (originally from in [ARC2](https://github.com/semsol/ARC2)). + +## SELECT + +### Aggregates +```sql +SELECT COUNT(?contact) AS ?contacts WHERE { + <#me> foaf:knows ?contact . +} +ORDER BY DESC(?contacts) +``` +Note that the alias (... AS ...) has to be specified. + + +If you have more than a single result variable, you also have to provide GROUP BY information: +```sql +SELECT ?who COUNT(?contact) AS ?contacts WHERE { + ?who foaf:knows ?contact . +} +GROUP BY ?who +``` + +Store implementation currently has a bug in the `SUM` ([link](https://github.com/sweetrdf/in-memory-store-sqlite/issues/3)) and `AVG` ([link](https://github.com/sweetrdf/in-memory-store-sqlite/issues/4) function. + +#### Supported aggregate functions + +| | AVG | COUNT | MIN | MAX | SUM | +|---------|-------------------------------------------------------------------------------|-------|-----|-----|-----------------------------------------------------------------------------| +| Support | x (but [bugged](https://github.com/sweetrdf/in-memory-store-sqlite/issues/4)) | x | x | x | (but [bugged](https://github.com/sweetrdf/in-memory-store-sqlite/issues/4)) | + + +### Supported relational terms + +| | = | != | < | > | +|---------|---|----|---|---| +| Support | x | x | x | x | + +### Supported FILTER functions + +| | bound | datatype | isBlank | isIri | isLiteral | isUri | lang | langMatches | regex | str | +|---------|-------|----------|---------|-------|-----------|-------|------|-------------|-------|-----| +| Support | x | x | x | x | x | x | x | x | x | x | + +## INSERT INTO +```sql +INSERT INTO { + <#foo> "baz" . +} +``` +In this INSERT form the triples have to be fully specified, variables are not allowed. + +It is possible to dynamically generate the triples that should be inserted: +```sql +INSERT INTO { + ?s foaf:knows ?o . +} +WHERE { + ?s xfn:contact ?o . +} +``` + +## DELETE + +```sql +DELETE { + <#foo> "baz" . + <#foo2> ?any . +} +``` +Each specified triple will be deleted from the RDF store. It is possible to specify variables as wildcards, but they can't be used to build connected patterns. Each triple is handled as a stand-alone pattern. + + +FROM can be used to restrict the delete operations to selected graphs. It's also possible to not specify any triples. The whole graph will then be deleted. +```sql +DELETE FROM +``` + +DELETE can be combined with a WHERE query, like: + +```sql +DELETE FROM { + ?s rel:wouldLikeToKnow ?o . +} +WHERE { + ?s kiss:kissed ?o . +} +``` + +Instead of deleting triples only in one graph, you can in all graphs by using: + +```sql +DELETE { + ?s rel:wouldLikeToKnow ?o . +} +WHERE { + ?s kiss:kissed ?o . +} +``` + +## SPARQL Grammar Changes and Additions +```sql +Query ::= Prologue ( SelectQuery | DescribeQuery | AskQuery | InsertQuery | DeleteQuery ) + +SelectQuery ::= 'SELECT' ( 'DISTINCT' | 'REDUCED' )? ( Aggregate+ | Var+ | '*' ) DatasetClause* WhereClause SolutionModifier + +Aggregate ::= ( 'AVG' | 'COUNT' | 'MAX' | 'MIN' | 'SUM' ) '(' Var | '*' ')' 'AS' Var + +InsertQuery ::= 'INSERT' 'INTO' IRIref DatasetClause* WhereClause? SolutionModifier + +DeleteQuery ::= 'DELETE' ( 'FROM' IRIref )* DatasetClause* WhereClause? SolutionModifier + +SolutionModifier ::= GroupClause? OrderClause? LimitOffsetClauses? + +GroupClause ::= 'GROUP' 'BY' Var ( ',' Var )* +``` diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..01811ef --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,30 @@ + + + + + + + + + + ./tests + + + + + src + + + diff --git a/src/KeyValueBag.php b/src/KeyValueBag.php new file mode 100644 index 0000000..af0e937 --- /dev/null +++ b/src/KeyValueBag.php @@ -0,0 +1,36 @@ +bag[$key] = $value; + } + + public function get(string $key): array | null + { + return $this->bag[$key] ?? null; + } + + public function has(string $key): bool + { + return null !== $this->get($key); + } + + public function hasEntries(): bool + { + return 0 < \count($this->bag); + } + + public function reset(): void + { + $this->bag = []; + } +} diff --git a/src/Log/Logger.php b/src/Log/Logger.php new file mode 100644 index 0000000..3569e05 --- /dev/null +++ b/src/Log/Logger.php @@ -0,0 +1,56 @@ +> + */ + private array $entries = []; + + public function __construct() + { + $this->entries = [ + 'error' => [], + 'warning' => [], + ]; + } + + public function error(string $message, array $context = []): void + { + $this->log('error', $message, $context); + } + + public function warning(string $message, array $context = []): void + { + $this->log('warning', $message, $context); + } + + private function log($level, $message, array $context = []): void + { + if (!isset($this->entries[$level])) { + $this->entries[$level] = []; + } + + $this->entries[$level][] = ['message' => $message, 'context' => $context]; + } + + public function getEntries(?string $level = null): array + { + if (null !== $level && isset($this->entries[$level])) { + return $this->entries[$level]; + } elseif (null == $level) { + return $this->entries; + } + + throw new Exception('Level '.$level.' not set.'); + } + + public function hasEntries(?string $level = null): bool + { + return 0 < \count($this->getEntries($level)); + } +} diff --git a/src/Log/LoggerPool.php b/src/Log/LoggerPool.php new file mode 100644 index 0000000..0064fa1 --- /dev/null +++ b/src/Log/LoggerPool.php @@ -0,0 +1,51 @@ + + */ + private array $logger = []; + + public function createNewLogger(string $id): Logger + { + $this->logger[$id] = new Logger(); + + return $this->logger[$id]; + } + + public function getLogger(string $id): Logger + { + if (isset($this->logger[$id])) { + return $this->logger[$id]; + } + + throw new Exception('Invalid ID given.'); + } + + public function getEntriesFromAllLoggerInstances(?string $level = null): iterable + { + $result = []; + + foreach ($this->logger as $logger) { + $result = array_merge($result, $logger->getEntries($level)); + } + + return $result; + } + + public function hasEntriesInAnyLoggerInstance(?string $level = null): bool + { + foreach ($this->logger as $logger) { + if ($logger->hasEntries($level)) { + return true; + } + } + + return false; + } +} diff --git a/src/NamespaceHelper.php b/src/NamespaceHelper.php new file mode 100644 index 0000000..c0e5b3d --- /dev/null +++ b/src/NamespaceHelper.php @@ -0,0 +1,52 @@ + + * (c) Benjamin Nowack + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace sweetrdf\InMemoryStoreSqlite; + +/** + * This class provides helpers to handle RDF namespace related operations. + */ +final class NamespaceHelper +{ + const BASE_NAMESPACE = 'sweetrdf://in-memory-store-sqlite/'; + + const NAMESPACE_RDF = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'; + const NAMESPACE_XML = 'http://www.w3.org/XML/1998/namespace'; + const NAMESPACE_XSD = 'http://www.w3.org/2001/XMLSchema#'; + + private $namespaces = [ + 'owl:' => 'http://www.w3.org/2002/07/owl#', + 'rdf:' => 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', + 'rdfs:' => 'http://www.w3.org/2000/01/rdf-schema#', + 'xsd:' => 'http://www.w3.org/2001/XMLSchema#', + ]; + + /** + * @return array + */ + public function getNamespaces(): array + { + return $this->namespaces; + } + + public function getPrefix($namespace): ?string + { + foreach ($this->namespaces as $prefix => $ns) { + if ($namespace == $ns) { + return $prefix; + } + } + + return null; + } +} diff --git a/src/PDOSQLiteAdapter.php b/src/PDOSQLiteAdapter.php new file mode 100644 index 0000000..876ba4f --- /dev/null +++ b/src/PDOSQLiteAdapter.php @@ -0,0 +1,366 @@ + + * (c) Benjamin Nowack + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace sweetrdf\InMemoryStoreSqlite; + +use Exception; +use PDO; + +/** + * PDO SQLite adapter. + */ +class PDOSQLiteAdapter +{ + private ?\PDO $db; + + private int $lastRowCount = 0; + + /** + * Sent queries. + */ + private array $queries = []; + + public function __construct() + { + $this->checkRequirements(); + + // use in-memory + $dsn = 'sqlite::memory:'; + + $this->db = new PDO($dsn); + + $this->db->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); + + // errors lead to exceptions + $this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + + // default fetch mode is associative + $this->db->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); + + /* + * These PRAGMAs may speed up insert operations a bit. + * Because database runs exclusively in memory for a process + * journal mode etc. is not relevant. + */ + $this->db->query('PRAGMA synchronous = OFF;'); + $this->db->query('PRAGMA journal_mode = OFF;'); + $this->db->query('PRAGMA locking_mode = EXCLUSIVE;'); + $this->db->query('PRAGMA page_size = 4096;'); + + /* + * define CONCAT function (otherwise SQLite will throw an exception) + */ + $this->db->sqliteCreateFunction('CONCAT', function ($pattern, $string) { + $result = ''; + + foreach (\func_get_args() as $str) { + $result .= $str; + } + + return $result; + }); + + /* + * define REGEXP function (otherwise SQLite will throw an exception) + */ + $this->db->sqliteCreateFunction('REGEXP', function ($pattern, $string) { + if (0 < preg_match('/'.$pattern.'/i', $string)) { + return true; + } + + return false; + }, 2); + + $this->createTables(); + } + + public function checkRequirements() + { + if (false == \extension_loaded('pdo_sqlite')) { + throw new Exception('Extension pdo_sqlite is not loaded.'); + } + } + + public function deleteAllTables(): void + { + $this->exec( + 'SELECT "drop table " || name || ";" + FROM sqlite_master + WHERE type = "table";' + ); + } + + /** + * Creates all required tables. + */ + private function createTables(): void + { + // triple + $sql = 'CREATE TABLE IF NOT EXISTS triple ( + t INTEGER PRIMARY KEY AUTOINCREMENT, + s INTEGER UNSIGNED NOT NULL, + p INTEGER UNSIGNED NOT NULL, + o INTEGER UNSIGNED NOT NULL, + o_lang_dt INTEGER UNSIGNED NOT NULL, + o_comp TEXT NOT NULL, -- normalized value for ORDER BY operations + s_type INTEGER UNSIGNED NOT NULL DEFAULT 0, -- uri/bnode => 0/1 + o_type INTEGER UNSIGNED NOT NULL DEFAULT 0 -- uri/bnode/literal => 0/1/2 + )'; + + $this->exec($sql); + + // g2t + $sql = 'CREATE TABLE IF NOT EXISTS g2t ( + g INTEGER UNSIGNED NOT NULL, + t INTEGER UNSIGNED NOT NULL, + UNIQUE (g,t) + )'; + + $this->exec($sql); + + // id2val + $sql = 'CREATE TABLE IF NOT EXISTS id2val ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + val TEXT NOT NULL, + val_type INTEGER NOT NULL DEFAULT 0, -- uri/bnode/literal => 0/1/2 + UNIQUE (id,val_type) + )'; + + $this->exec($sql); + + // s2val + $sql = 'CREATE TABLE IF NOT EXISTS s2val ( + id INTEGER UNSIGNED NOT NULL, + val_hash TEXT NOT NULL, + val TEXT NOT NULL, + UNIQUE (id) + )'; + + $this->exec($sql); + + // o2val + $sql = 'CREATE TABLE IF NOT EXISTS o2val ( + id INTEGER NOT NULL, + val_hash TEXT NOT NULL, + val TEXT NOT NULL, + UNIQUE (id) + )'; + + $this->exec($sql); + } + + /** + * It gets all tables from the current database. + */ + public function getAllTables(): array + { + $tables = $this->fetchList('SELECT name FROM sqlite_master WHERE type="table";'); + $result = []; + foreach ($tables as $table) { + // ignore SQLite tables + if (false !== strpos($table['name'], 'sqlite_')) { + continue; + } + $result[] = $table['name']; + } + + return $result; + } + + public function getServerVersion() + { + return $this->fetchRow('select sqlite_version()')['sqlite_version()']; + } + + public function getAffectedRows(): int + { + return $this->lastRowCount; + } + + /** + * @return void + */ + public function disconnect() + { + // FYI: https://stackoverflow.com/questions/18277233/pdo-closing-connection + $this->db = null; + } + + public function escape($value) + { + $quoted = $this->db->quote($value); + + /* + * fixes the case, that we have double quoted strings like: + * ''x1'' + * + * remember, this value will be surrounded by quotes later on! + * so we don't send it back with quotes around. + */ + if ("'" == substr($quoted, 0, 1)) { + $quoted = substr($quoted, 1, \strlen($quoted) - 2); + } + + return $quoted; + } + + public function fetchList(string $sql, array $params = []): array + { + // save query + $this->queries[] = [ + 'query' => $sql, + 'by_function' => 'fetchList', + ]; + + $stmt = $this->db->prepare($sql); + $stmt->execute($params); + $rows = $stmt->fetchAll(); + $stmt->closeCursor(); + + return $rows; + } + + /** + * @return bool|array + */ + public function fetchRow(string $sql, array $params = []) + { + // save query + $this->queries[] = [ + 'query' => $sql, + 'by_function' => 'fetchRow', + ]; + + $row = false; + $stmt = $this->db->prepare($sql); + $stmt->execute($params); + $rows = $stmt->fetchAll(); + if (0 < \count($rows)) { + $row = array_values($rows)[0]; + } + $stmt->closeCursor(); + + return $row; + } + + public function getPDO() + { + return $this->db; + } + + public function getErrorCode() + { + return $this->db->errorCode(); + } + + public function getErrorMessage() + { + return $this->db->errorInfo()[2]; + } + + public function getLastInsertId() + { + return $this->db->lastInsertId(); + } + + public function getNumberOfRows($sql) + { + // save query + $this->queries[] = [ + 'query' => $sql, + 'by_function' => 'getNumberOfRows', + ]; + + $stmt = $this->db->prepare($sql); + $stmt->execute(); + $rowCount = \count($stmt->fetchAll()); + $stmt->closeCursor(); + + return $rowCount; + } + + public function simpleQuery(string $sql): bool + { + // save query + $this->queries[] = [ + 'query' => $sql, + 'by_function' => 'simpleQuery', + ]; + + $stmt = $this->db->prepare($sql); + $stmt->execute(); + $this->lastRowCount = $stmt->rowCount(); + $stmt->closeCursor(); + + return true; + } + + /** + * Encapsulates internal PDO::exec call. + * This allows us to extend it, e.g. with caching functionality. + * + * @param string $sql + * + * @return int number of affected rows + */ + public function exec($sql) + { + // save query + $this->queries[] = [ + 'query' => $sql, + 'by_function' => 'exec', + ]; + + return $this->db->exec($sql); + } + + /** + * @return int ID of new entry + * + * @throws Exception if invalid table name was given + */ + public function insert(string $table, array $data): int + { + $columns = array_keys($data); + + // we reject fishy table names + if (1 !== preg_match('/^[a-zA-Z0-9_]+$/i', $table)) { + throw new Exception('Invalid table name given.'); + } + + /* + * start building SQL + */ + $sql = 'INSERT INTO '.$table.' ('.implode(', ', $columns); + $sql .= ') VALUES ('; + + // add placeholders for each value; collect values + $values = []; + foreach ($data as $v) { + $values[] = '"'.$v.'"'; + } + $sql .= implode(', ', $values); + + $sql .= ')'; + + /* + * SQL looks like the following now: + * + * INSERT INTO foo (foo) ("bar") + */ + + $this->exec($sql); + + return $this->db->lastInsertId(); + } +} diff --git a/src/Parser/BaseParser.php b/src/Parser/BaseParser.php new file mode 100644 index 0000000..d5788a0 --- /dev/null +++ b/src/Parser/BaseParser.php @@ -0,0 +1,136 @@ + + * (c) Benjamin Nowack + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace sweetrdf\InMemoryStoreSqlite\Parser; + +use sweetrdf\InMemoryStoreSqlite\Log\Logger; +use sweetrdf\InMemoryStoreSqlite\NamespaceHelper; +use sweetrdf\InMemoryStoreSqlite\StringReader; + +abstract class BaseParser +{ + /** + * @var array + */ + protected $added_triples; + + protected string $base; + + protected string $bnode_id; + + protected array $blocks; + + protected Logger $logger; + + /** + * @var array + */ + protected array $prefixes; + + /** + * Query infos container. + */ + protected array $r = []; + + protected array $triples = []; + + protected int $t_count = 0; + + public function __construct(Logger $logger) + { + // TODO pass as constructor param + $this->reader = new StringReader(); + + $this->logger = $logger; + + // TODO make it a constructor param + $this->prefixes = (new NamespaceHelper())->getNamespaces(); + + // generates random prefix for blank nodes + $this->bnode_prefix = bin2hex(random_bytes(4)).'b'; + + $this->bnode_id = 0; + } + + public function getQueryInfos() + { + return $this->r; + } + + public function getTriples() + { + return $this->triples; + } + + public function getSimpleIndex($flatten_objects = 1, $vals = ''): array + { + return $this->_getSimpleIndex($this->getTriples(), $flatten_objects, $vals); + } + + /** + * @todo port from ARC2::getSimpleIndex; refactor and merge it with $this->getSimpleIndex + */ + private function _getSimpleIndex($triples, $flatten_objects = 1, $vals = ''): array + { + $r = []; + foreach ($triples as $t) { + $skip_t = 0; + foreach (['s', 'p', 'o'] as $term) { + $$term = $t[$term]; + /* template var */ + if (isset($t[$term.'_type']) && ('var' == $t[$term.'_type'])) { + $val = isset($vals[$$term]) ? $vals[$$term] : ''; + $skip_t = isset($vals[$$term]) ? $skip_t : 1; + $type = ''; + $type = !$type && isset($vals[$$term.' type']) ? $vals[$$term.' type'] : $type; + $type = !$type && preg_match('/^\_\:/', $val) ? 'bnode' : $type; + if ('o' == $term) { + $type = !$type && (preg_match('/\s/s', $val) || !preg_match('/\:/', $val)) ? 'literal' : $type; + $type = !$type && !preg_match('/[\/]/', $val) ? 'literal' : $type; + } + $type = !$type ? 'uri' : $type; + $t[$term.'_type'] = $type; + $$term = $val; + } + } + if ($skip_t) { + continue; + } + if (!isset($r[$s])) { + $r[$s] = []; + } + if (!isset($r[$s][$p])) { + $r[$s][$p] = []; + } + if ($flatten_objects) { + if (!\in_array($o, $r[$s][$p])) { + $r[$s][$p][] = $o; + } + } else { + $o = ['value' => $o]; + foreach (['lang', 'type', 'datatype'] as $suffix) { + if (isset($t['o_'.$suffix]) && $t['o_'.$suffix]) { + $o[$suffix] = $t['o_'.$suffix]; + } elseif (isset($t['o '.$suffix]) && $t['o '.$suffix]) { + $o[$suffix] = $t['o '.$suffix]; + } + } + if (!\in_array($o, $r[$s][$p])) { + $r[$s][$p][] = $o; + } + } + } + + return $r; + } +} diff --git a/src/Parser/SPARQLParser.php b/src/Parser/SPARQLParser.php new file mode 100644 index 0000000..650fe3f --- /dev/null +++ b/src/Parser/SPARQLParser.php @@ -0,0 +1,838 @@ + + * (c) Benjamin Nowack + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace sweetrdf\InMemoryStoreSqlite\Parser; + +use function sweetrdf\InMemoryStoreSqlite\calcBase; +use sweetrdf\InMemoryStoreSqlite\Log\Logger; +use sweetrdf\InMemoryStoreSqlite\NamespaceHelper; + +class SPARQLParser extends TurtleParser +{ + public function __construct(Logger $logger) + { + parent::__construct($logger); + + $this->bnode_prefix = 'arc'.substr(md5(uniqid(rand())), 0, 4).'b'; + $this->bnode_id = 0; + $this->bnode_pattern_index = ['patterns' => [], 'bnodes' => []]; + } + + public function parse(string $q, string $path = ''): void + { + $this->base = $path ? calcBase($path) : NamespaceHelper::BASE_NAMESPACE; + $this->r = [ + 'base' => '', + 'vars' => [], + 'prefixes' => [], + ]; + $this->unparsed_code = $q; + list($r, $v) = $this->xQuery($q); + if ($r) { + $this->r['query'] = $r; + $this->unparsed_code = trim($v); + } elseif (!$this->logger->hasEntries('error') && !$this->unparsed_code) { + $this->logger->error('Query not properly closed'); + } + $this->r['prefixes'] = $this->prefixes; + $this->r['base'] = $this->base; + /* remove trailing comments */ + while (preg_match('/^\s*(\#[^\xd\xa]*)(.*)$/si', $this->unparsed_code, $m)) { + $this->unparsed_code = $m[2]; + } + if ($this->unparsed_code && !$this->logger->hasEntries('error')) { + $rest = preg_replace('/[\x0a|\x0d]/i', ' ', substr($this->unparsed_code, 0, 30)); + $msg = trim($rest) ? 'Could not properly handle "'.$rest.'"' : 'Syntax error, probably an incomplete pattern'; + $this->logger->error($msg); + } + } + + /* 1 */ + + protected function xQuery($v) + { + list($r, $v) = $this->xPrologue($v); + foreach (['Select', 'Construct', 'Describe', 'Ask'] as $type) { + $m = 'x'.$type.'Query'; + if ((list($r, $v) = $this->$m($v)) && $r) { + return [$r, $v]; + } + } + + return [0, $v]; + } + + /* 2 */ + + protected function xPrologue($v) + { + $r = 0; + if ((list($sub_r, $v) = $this->xBaseDecl($v)) && $sub_r) { + $this->base = $sub_r; + $r = 1; + } + while ((list($sub_r, $v) = $this->xPrefixDecl($v)) && $sub_r) { + $this->prefixes[$sub_r['prefix']] = $sub_r['uri']; + $r = 1; + } + + return [$r, $v]; + } + + /* 5.. */ + + protected function xSelectQuery($v) + { + if ($sub_r = $this->x('SELECT\s+', $v)) { + $r = [ + 'type' => 'select', + 'result_vars' => [], + 'dataset' => [], + ]; + $all_vars = 0; + $sub_v = $sub_r[1]; + /* distinct, reduced */ + if ($sub_r = $this->x('(DISTINCT|REDUCED)\s+', $sub_v)) { + $r[strtolower($sub_r[1])] = 1; + $sub_v = $sub_r[2]; + } + /* result vars */ + if ($sub_r = $this->x('\*\s+', $sub_v)) { + $all_vars = 1; + $sub_v = $sub_r[1]; + } else { + while ((list($sub_r, $sub_v) = $this->xResultVar($sub_v)) && $sub_r) { + $r['result_vars'][] = $sub_r; + } + } + if (!$all_vars && !\count($r['result_vars'])) { + $this->logger->error('No result bindings specified.'); + } + /* dataset */ + while ((list($sub_r, $sub_v) = $this->xDatasetClause($sub_v)) && $sub_r) { + $r['dataset'][] = $sub_r; + } + /* where */ + if ((list($sub_r, $sub_v) = $this->xWhereClause($sub_v)) && $sub_r) { + $r['pattern'] = $sub_r; + } else { + return [0, $v]; + } + /* solution modifier */ + if ((list($sub_r, $sub_v) = $this->xSolutionModifier($sub_v)) && $sub_r) { + $r = array_merge($r, $sub_r); + } + /* all vars */ + if ($all_vars) { + foreach ($this->r['vars'] as $var) { + $r['result_vars'][] = ['var' => $var, 'aggregate' => 0, 'alias' => '']; + } + if (!$r['result_vars']) { + $r['result_vars'][] = '*'; + } + } + + return [$r, $sub_v]; + } + + return [0, $v]; + } + + protected function xResultVar($v) + { + return $this->xVar($v); + } + + /* 6.. */ + + protected function xConstructQuery($v) + { + if ($sub_r = $this->x('CONSTRUCT\s*', $v)) { + $r = [ + 'type' => 'construct', + 'dataset' => [], + ]; + $sub_v = $sub_r[1]; + /* construct template */ + if ((list($sub_r, $sub_v) = $this->xConstructTemplate($sub_v)) && \is_array($sub_r)) { + $r['construct_triples'] = $sub_r; + } else { + $this->logger->error('Construct Template not found'); + + return [0, $v]; + } + /* dataset */ + while ((list($sub_r, $sub_v) = $this->xDatasetClause($sub_v)) && $sub_r) { + $r['dataset'][] = $sub_r; + } + /* where */ + if ((list($sub_r, $sub_v) = $this->xWhereClause($sub_v)) && $sub_r) { + $r['pattern'] = $sub_r; + } else { + return [0, $v]; + } + /* solution modifier */ + if ((list($sub_r, $sub_v) = $this->xSolutionModifier($sub_v)) && $sub_r) { + $r = array_merge($r, $sub_r); + } + + return [$r, $sub_v]; + } + + return [0, $v]; + } + + /* 7.. */ + + protected function xDescribeQuery($v) + { + if ($sub_r = $this->x('DESCRIBE\s+', $v)) { + $r = [ + 'type' => 'describe', + 'result_vars' => [], + 'result_uris' => [], + 'dataset' => [], + ]; + $sub_v = $sub_r[1]; + $all_vars = 0; + /* result vars/uris */ + if ($sub_r = $this->x('\*\s+', $sub_v)) { + $all_vars = 1; + $sub_v = $sub_r[1]; + } else { + do { + $proceed = 0; + if ((list($sub_r, $sub_v) = $this->xResultVar($sub_v)) && $sub_r) { + $r['result_vars'][] = $sub_r; + $proceed = 1; + } + if ((list($sub_r, $sub_v) = $this->xIRIref($sub_v)) && $sub_r) { + $r['result_uris'][] = $sub_r; + $proceed = 1; + } + } while ($proceed); + } + if (!$all_vars && !\count($r['result_vars']) && !\count($r['result_uris'])) { + $this->logger->error('No result bindings specified.'); + } + /* dataset */ + while ((list($sub_r, $sub_v) = $this->xDatasetClause($sub_v)) && $sub_r) { + $r['dataset'][] = $sub_r; + } + /* where */ + if ((list($sub_r, $sub_v) = $this->xWhereClause($sub_v)) && $sub_r) { + $r['pattern'] = $sub_r; + } + /* solution modifier */ + if ((list($sub_r, $sub_v) = $this->xSolutionModifier($sub_v)) && $sub_r) { + $r = array_merge($r, $sub_r); + } + /* all vars */ + if ($all_vars) { + foreach ($this->r['vars'] as $var) { + $r['result_vars'][] = ['var' => $var, 'aggregate' => 0, 'alias' => '']; + } + } + + return [$r, $sub_v]; + } + + return [0, $v]; + } + + /* 8.. */ + + protected function xAskQuery($v) + { + if ($sub_r = $this->x('ASK\s+', $v)) { + $r = [ + 'type' => 'ask', + 'dataset' => [], + ]; + $sub_v = $sub_r[1]; + /* dataset */ + while ((list($sub_r, $sub_v) = $this->xDatasetClause($sub_v)) && $sub_r) { + $r['dataset'][] = $sub_r; + } + /* where */ + if ((list($sub_r, $sub_v) = $this->xWhereClause($sub_v)) && $sub_r) { + $r['pattern'] = $sub_r; + + return [$r, $sub_v]; + } else { + $this->logger->error('Missing or invalid WHERE clause.'); + } + } + + return [0, $v]; + } + + /* 9, 10, 11, 12 */ + + protected function xDatasetClause($v) + { + if ($r = $this->x('FROM(\s+NAMED)?\s+', $v)) { + $named = $r[1] ? 1 : 0; + if ((list($r, $sub_v) = $this->xIRIref($r[2])) && $r) { + return [['graph' => $r, 'named' => $named], $sub_v]; + } + } + + return [0, $v]; + } + + /* 13 */ + + protected function xWhereClause($v) + { + if ($r = $this->x('(WHERE)?', $v)) { + $v = $r[2]; + } + if ((list($r, $v) = $this->xGroupGraphPattern($v)) && $r) { + return [$r, $v]; + } + + return [0, $v]; + } + + /* 14, 15 */ + + protected function xSolutionModifier($v) + { + $r = []; + if ((list($sub_r, $sub_v) = $this->xOrderClause($v)) && $sub_r) { + $r['order_infos'] = $sub_r; + } + while ((list($sub_r, $sub_v) = $this->xLimitOrOffsetClause($sub_v)) && $sub_r) { + $r = array_merge($r, $sub_r); + } + + return ($v == $sub_v) ? [0, $v] : [$r, $sub_v]; + } + + /* 18, 19 */ + + protected function xLimitOrOffsetClause($v) + { + if ($sub_r = $this->x('(LIMIT|OFFSET)', $v)) { + $key = strtolower($sub_r[1]); + $sub_v = $sub_r[2]; + if ((list($sub_r, $sub_v) = $this->xINTEGER($sub_v)) && (false !== $sub_r)) { + return [[$key => $sub_r], $sub_v]; + } + if ((list($sub_r, $sub_v) = $this->xPlaceholder($sub_v)) && (false !== $sub_r)) { + return [[$key => $sub_r], $sub_v]; + } + } + + return [0, $v]; + } + + /* 16 */ + + protected function xOrderClause($v) + { + if ($sub_r = $this->x('ORDER BY\s+', $v)) { + $sub_v = $sub_r[1]; + $r = []; + while ((list($sub_r, $sub_v) = $this->xOrderCondition($sub_v)) && $sub_r) { + $r[] = $sub_r; + } + if (\count($r)) { + return [$r, $sub_v]; + } else { + $this->logger->error('No order conditions specified.'); + } + } + + return [0, $v]; + } + + /* 17, 27 */ + + protected function xOrderCondition($v) + { + if ($sub_r = $this->x('(ASC|DESC)', $v)) { + $dir = strtolower($sub_r[1]); + $sub_v = $sub_r[2]; + if ((list($sub_r, $sub_v) = $this->xBrackettedExpression($sub_v)) && $sub_r) { + $sub_r['direction'] = $dir; + + return [$sub_r, $sub_v]; + } + } elseif ((list($sub_r, $sub_v) = $this->xVar($v)) && $sub_r) { + $sub_r['direction'] = 'asc'; + + return [$sub_r, $sub_v]; + } elseif ((list($sub_r, $sub_v) = $this->xBrackettedExpression($v)) && $sub_r) { + return [$sub_r, $sub_v]; + } elseif ((list($sub_r, $sub_v) = $this->xBuiltInCall($v)) && $sub_r) { + $sub_r['direction'] = 'asc'; + + return [$sub_r, $sub_v]; + } elseif ((list($sub_r, $sub_v) = $this->xFunctionCall($v)) && $sub_r) { + $sub_r['direction'] = 'asc'; + + return [$sub_r, $sub_v]; + } + + return [0, $v]; + } + + /* 20 */ + + protected function xGroupGraphPattern($v) + { + $pattern_id = substr(md5(uniqid(rand())), 0, 4); + if ($sub_r = $this->x('\{', $v)) { + $r = ['type' => 'group', 'patterns' => []]; + $sub_v = $sub_r[1]; + if ((list($sub_r, $sub_v) = $this->xTriplesBlock($sub_v)) && $sub_r) { + $this->indexBnodes($sub_r, $pattern_id); + $r['patterns'][] = ['type' => 'triples', 'patterns' => $sub_r]; + } + do { + $proceed = 0; + if ((list($sub_r, $sub_v) = $this->xGraphPatternNotTriples($sub_v)) && $sub_r) { + $r['patterns'][] = $sub_r; + $pattern_id = substr(md5(uniqid(rand())), 0, 4); + $proceed = 1; + } elseif ((list($sub_r, $sub_v) = $this->xFilter($sub_v)) && $sub_r) { + $r['patterns'][] = ['type' => 'filter', 'constraint' => $sub_r]; + $proceed = 1; + } + if ($sub_r = $this->x('\.', $sub_v)) { + $sub_v = $sub_r[1]; + } + if ((list($sub_r, $sub_v) = $this->xTriplesBlock($sub_v)) && $sub_r) { + $this->indexBnodes($sub_r, $pattern_id); + $r['patterns'][] = ['type' => 'triples', 'patterns' => $sub_r]; + $proceed = 1; + } + if ((list($sub_r, $sub_v) = $this->xPlaceholder($sub_v)) && $sub_r) { + $r['patterns'][] = $sub_r; + $proceed = 1; + } + } while ($proceed); + if ($sub_r = $this->x('\}', $sub_v)) { + $sub_v = $sub_r[1]; + + return [$r, $sub_v]; + } + $rest = preg_replace('/[\x0a|\x0d]/i', ' ', substr($sub_v, 0, 30)); + $this->logger->error('Incomplete or invalid Group Graph pattern. Could not handle "'.$rest.'"'); + } + + return [0, $v]; + } + + protected function indexBnodes($triples, $pattern_id) + { + $index_id = \count($this->bnode_pattern_index['patterns']); + $index_id = $pattern_id; + $this->bnode_pattern_index['patterns'][] = $triples; + foreach ($triples as $t) { + foreach (['s', 'p', 'o'] as $term) { + if ('bnode' == $t[$term.'_type']) { + $val = $t[$term]; + if (isset($this->bnode_pattern_index['bnodes'][$val]) && ($this->bnode_pattern_index['bnodes'][$val] != $index_id)) { + $this->logger->error('Re-used bnode label "'.$val.'" across graph patterns'); + } else { + $this->bnode_pattern_index['bnodes'][$val] = $index_id; + } + } + } + } + } + + /* 22.., 25.. */ + + protected function xGraphPatternNotTriples($v) + { + if ((list($sub_r, $sub_v) = $this->xOptionalGraphPattern($v)) && $sub_r) { + return [$sub_r, $sub_v]; + } + if ((list($sub_r, $sub_v) = $this->xGraphGraphPattern($v)) && $sub_r) { + return [$sub_r, $sub_v]; + } + $r = ['type' => 'union', 'patterns' => []]; + $sub_v = $v; + do { + $proceed = 0; + if ((list($sub_r, $sub_v) = $this->xGroupGraphPattern($sub_v)) && $sub_r) { + $r['patterns'][] = $sub_r; + if ($sub_r = $this->x('UNION', $sub_v)) { + $sub_v = $sub_r[1]; + $proceed = 1; + } + } + } while ($proceed); + $pc = \count($r['patterns']); + if (1 == $pc) { + return [$r['patterns'][0], $sub_v]; + } elseif ($pc > 1) { + return [$r, $sub_v]; + } + + return [0, $v]; + } + + /* 23 */ + + protected function xOptionalGraphPattern($v) + { + if ($sub_r = $this->x('OPTIONAL', $v)) { + $sub_v = $sub_r[1]; + if ((list($sub_r, $sub_v) = $this->xGroupGraphPattern($sub_v)) && $sub_r) { + return [['type' => 'optional', 'patterns' => $sub_r['patterns']], $sub_v]; + } + $this->logger->error('Missing or invalid Group Graph Pattern after OPTIONAL'); + } + + return [0, $v]; + } + + /* 24.. */ + + protected function xGraphGraphPattern($v) + { + if ($sub_r = $this->x('GRAPH', $v)) { + $sub_v = $sub_r[1]; + $r = ['type' => 'graph', 'var' => '', 'uri' => '', 'patterns' => []]; + if ((list($sub_r, $sub_v) = $this->xVar($sub_v)) && $sub_r) { + $r['var'] = $sub_r; + } elseif ((list($sub_r, $sub_v) = $this->xIRIref($sub_v)) && $sub_r) { + $r['uri'] = $sub_r; + } + if ($r['var'] || $r['uri']) { + if ((list($sub_r, $sub_v) = $this->xGroupGraphPattern($sub_v)) && $sub_r) { + $r['patterns'][] = $sub_r; + + return [$r, $sub_v]; + } + $this->logger->error('Missing or invalid Graph Pattern'); + } + } + + return [0, $v]; + } + + /* 26.., 27.. */ + + protected function xFilter($v) + { + if ($r = $this->x('FILTER', $v)) { + $sub_v = $r[1]; + if ((list($r, $sub_v) = $this->xBrackettedExpression($sub_v)) && $r) { + return [$r, $sub_v]; + } + if ((list($r, $sub_v) = $this->xBuiltInCall($sub_v)) && $r) { + return [$r, $sub_v]; + } + if ((list($r, $sub_v) = $this->xFunctionCall($sub_v)) && $r) { + return [$r, $sub_v]; + } + $this->logger->error('Incomplete FILTER'); + } + + return [0, $v]; + } + + /* 28.. */ + + protected function xFunctionCall($v) + { + if ((list($r, $sub_v) = $this->xIRIref($v)) && $r) { + if ((list($sub_r, $sub_v) = $this->xArgList($sub_v)) && $sub_r) { + return [['type' => 'function_call', 'uri' => $r, 'args' => $sub_r], $sub_v]; + } + } + + return [0, $v]; + } + + /* 29 */ + + protected function xArgList($v) + { + $r = []; + $sub_v = $v; + $closed = 0; + if ($sub_r = $this->x('\(', $sub_v)) { + $sub_v = $sub_r[1]; + do { + $proceed = 0; + if ((list($sub_r, $sub_v) = $this->xExpression($sub_v)) && $sub_r) { + $r[] = $sub_r; + if ($sub_r = $this->x('\,', $sub_v)) { + $sub_v = $sub_r[1]; + $proceed = 1; + } + } + if ($sub_r = $this->x('\)', $sub_v)) { + $sub_v = $sub_r[1]; + $closed = 1; + $proceed = 0; + } + } while ($proceed); + } + + return $closed ? [$r, $sub_v] : [0, $v]; + } + + /* 30, 31 */ + + protected function xConstructTemplate($v) + { + if ($sub_r = $this->x('\{', $v)) { + $r = []; + if ((list($sub_r, $sub_v) = $this->xTriplesBlock($sub_r[1])) && \is_array($sub_r)) { + $r = $sub_r; + } + if ($sub_r = $this->x('\}', $sub_v)) { + return [$r, $sub_r[1]]; + } + } + + return [0, $v]; + } + + /* 46, 47 */ + + protected function xExpression($v) + { + if ((list($sub_r, $sub_v) = $this->xConditionalAndExpression($v)) && $sub_r) { + $r = ['type' => 'expression', 'sub_type' => 'or', 'patterns' => [$sub_r]]; + do { + $proceed = 0; + if ($sub_r = $this->x('\|\|', $sub_v)) { + $sub_v = $sub_r[1]; + if ((list($sub_r, $sub_v) = $this->xConditionalAndExpression($sub_v)) && $sub_r) { + $r['patterns'][] = $sub_r; + $proceed = 1; + } + } + } while ($proceed); + + return 1 == \count($r['patterns']) ? [$r['patterns'][0], $sub_v] : [$r, $sub_v]; + } + + return [0, $v]; + } + + /* 48.., 49.. */ + + protected function xConditionalAndExpression($v) + { + if ((list($sub_r, $sub_v) = $this->xRelationalExpression($v)) && $sub_r) { + $r = ['type' => 'expression', 'sub_type' => 'and', 'patterns' => [$sub_r]]; + do { + $proceed = 0; + if ($sub_r = $this->x('\&\&', $sub_v)) { + $sub_v = $sub_r[1]; + if ((list($sub_r, $sub_v) = $this->xRelationalExpression($sub_v)) && $sub_r) { + $r['patterns'][] = $sub_r; + $proceed = 1; + } + } + } while ($proceed); + + return 1 == \count($r['patterns']) ? [$r['patterns'][0], $sub_v] : [$r, $sub_v]; + } + + return [0, $v]; + } + + /* 50, 51 */ + + protected function xRelationalExpression($v) + { + if ((list($sub_r, $sub_v) = $this->xAdditiveExpression($v)) && $sub_r) { + $r = ['type' => 'expression', 'sub_type' => 'relational', 'patterns' => [$sub_r]]; + do { + $proceed = 0; + /* don't mistake '<' + uriref with '<'-operator ("longest token" rule) */ + if ((list($sub_r, $sub_v) = $this->xIRI_REF($sub_v)) && $sub_r) { + $this->logger->error('Expected operator, found IRIref: "'.$sub_r.'".'); + } + if ($sub_r = $this->x('(\!\=|\=\=|\=|\<\=|\>\=|\<|\>)', $sub_v)) { + $op = $sub_r[1]; + $sub_v = $sub_r[2]; + $r['operator'] = $op; + if ((list($sub_r, $sub_v) = $this->xAdditiveExpression($sub_v)) && $sub_r) { + //$sub_r['operator'] = $op; + $r['patterns'][] = $sub_r; + $proceed = 1; + } + } + } while ($proceed); + + return 1 == \count($r['patterns']) ? [$r['patterns'][0], $sub_v] : [$r, $sub_v]; + } + + return [0, $v]; + } + + /* 52 */ + + protected function xAdditiveExpression($v) + { + if ((list($sub_r, $sub_v) = $this->xMultiplicativeExpression($v)) && $sub_r) { + $r = ['type' => 'expression', 'sub_type' => 'additive', 'patterns' => [$sub_r]]; + do { + $proceed = 0; + if ($sub_r = $this->x('(\+|\-)', $sub_v)) { + $op = $sub_r[1]; + $sub_v = $sub_r[2]; + if ((list($sub_r, $sub_v) = $this->xMultiplicativeExpression($sub_v)) && $sub_r) { + $sub_r['operator'] = $op; + $r['patterns'][] = $sub_r; + $proceed = 1; + } elseif ((list($sub_r, $sub_v) = $this->xNumericLiteral($sub_v)) && $sub_r) { + $r['patterns'][] = ['type' => 'numeric', 'operator' => $op, 'value' => $sub_r]; + $proceed = 1; + } + } + } while ($proceed); + //return array($r, $sub_v); + return 1 == \count($r['patterns']) ? [$r['patterns'][0], $sub_v] : [$r, $sub_v]; + } + + return [0, $v]; + } + + /* 53 */ + + protected function xMultiplicativeExpression($v) + { + if ((list($sub_r, $sub_v) = $this->xUnaryExpression($v)) && $sub_r) { + $r = ['type' => 'expression', 'sub_type' => 'multiplicative', 'patterns' => [$sub_r]]; + do { + $proceed = 0; + if ($sub_r = $this->x('(\*|\/)', $sub_v)) { + $op = $sub_r[1]; + $sub_v = $sub_r[2]; + if ((list($sub_r, $sub_v) = $this->xUnaryExpression($sub_v)) && $sub_r) { + $sub_r['operator'] = $op; + $r['patterns'][] = $sub_r; + $proceed = 1; + } + } + } while ($proceed); + + return 1 == \count($r['patterns']) ? [$r['patterns'][0], $sub_v] : [$r, $sub_v]; + } + + return [0, $v]; + } + + /* 54 */ + + protected function xUnaryExpression($v) + { + $sub_v = $v; + $op = ''; + if ($sub_r = $this->x('(\!|\+|\-)', $sub_v)) { + $op = $sub_r[1]; + $sub_v = $sub_r[2]; + } + if ((list($sub_r, $sub_v) = $this->xPrimaryExpression($sub_v)) && $sub_r) { + if (!\is_array($sub_r)) { + $sub_r = ['type' => 'unary', 'expression' => $sub_r]; + } elseif ($sub_op = $sub_r['operator'] ?? '') { + $ops = ['!!' => '', '++' => '+', '--' => '+', '+-' => '-', '-+' => '-']; + $op = isset($ops[$op.$sub_op]) ? $ops[$op.$sub_op] : $op.$sub_op; + } + $sub_r['operator'] = $op; + + return [$sub_r, $sub_v]; + } + + return [0, $v]; + } + + /* 55 */ + + protected function xPrimaryExpression($v) + { + foreach (['BrackettedExpression', 'BuiltInCall', 'IRIrefOrFunction', 'RDFLiteral', 'NumericLiteral', 'BooleanLiteral', 'Var', 'Placeholder'] as $type) { + $m = 'x'.$type; + if ((list($sub_r, $sub_v) = $this->$m($v)) && $sub_r) { + return [$sub_r, $sub_v]; + } + } + + return [0, $v]; + } + + /* 56 */ + + protected function xBrackettedExpression($v) + { + if ($r = $this->x('\(', $v)) { + if ((list($r, $sub_v) = $this->xExpression($r[1])) && $r) { + if ($sub_r = $this->x('\)', $sub_v)) { + return [$r, $sub_r[1]]; + } + } + } + + return [0, $v]; + } + + /* 57.., 58.. */ + + protected function xBuiltInCall($v) + { + if ($sub_r = $this->x('(str|lang|langmatches|datatype|bound|sameterm|isiri|isuri|isblank|isliteral|regex)\s*\(', $v)) { + $r = ['type' => 'built_in_call', 'call' => strtolower($sub_r[1])]; + if ((list($sub_r, $sub_v) = $this->xArgList('('.$sub_r[2])) && \is_array($sub_r)) { + $r['args'] = $sub_r; + + return [$r, $sub_v]; + } + } + + return [0, $v]; + } + + /* 59.. */ + + protected function xIRIrefOrFunction($v) + { + if ((list($r, $v) = $this->xIRIref($v)) && $r) { + if ((list($sub_r, $sub_v) = $this->xArgList($v)) && \is_array($sub_r)) { + return [['type' => 'function', 'uri' => $r, 'args' => $sub_r], $sub_v]; + } + + return [['type' => 'uri', 'uri' => $r], $sub_v]; + } + } + + /* 70.. @@sync with TurtleParser */ + + protected function xIRI_REF($v) + { + if (($r = $this->x('\<(\$\{[^\>]*\})\>', $v)) && ($sub_r = $this->xPlaceholder($r[1]))) { + return [$r[1], $r[2]]; + } elseif ($r = $this->x('\<([^\<\>\s\"\|\^`]*)\>', $v)) { + return [$r[1] ? $r[1] : true, $r[2]]; + } + /* allow reserved chars in obvious IRIs */ + elseif ($r = $this->x('\<(https?\:[^\s][^\<\>]*)\>', $v)) { + return [$r[1] ? $r[1] : true, $r[2]]; + } + + return [0, $v]; + } +} diff --git a/src/Parser/SPARQLPlusParser.php b/src/Parser/SPARQLPlusParser.php new file mode 100644 index 0000000..5f2739f --- /dev/null +++ b/src/Parser/SPARQLPlusParser.php @@ -0,0 +1,214 @@ + + * (c) Benjamin Nowack + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace sweetrdf\InMemoryStoreSqlite\Parser; + +class SPARQLPlusParser extends SPARQLParser +{ + /* +1 */ + + protected function xQuery($v) + { + list($r, $v) = $this->xPrologue($v); + foreach (['Select', 'Construct', 'Describe', 'Ask', 'Insert', 'Delete', 'Load'] as $type) { + $m = 'x'.$type.'Query'; + if ((list($r, $v) = $this->$m($v)) && $r) { + return [$r, $v]; + } + } + + return [0, $v]; + } + + /* +3 */ + + protected function xResultVar($v) + { + $aggregate = ''; + /* aggregate */ + if ($sub_r = $this->x('\(?(AVG|COUNT|MAX|MIN|SUM)\s*\(\s*([^\)]+)\)\s+AS\s+([^\s\)]+)\)?', $v)) { + $aggregate = $sub_r[1]; + $result_var = $sub_r[3]; + $v = $sub_r[2].$sub_r[4]; + } + if ($sub_r && (list($sub_r, $sub_v) = $this->xVar($result_var)) && $sub_r) { + $result_var = $sub_r['value']; + } + /* * or var */ + if ((list($sub_r, $sub_v) = $this->x('\*', $v)) && $sub_r) { + return [['var' => '*', 'aggregate' => $aggregate, 'alias' => $aggregate ? $result_var : ''], $sub_v]; + } + if ((list($sub_r, $sub_v) = $this->xVar($v)) && $sub_r) { + return [['var' => $sub_r['value'], 'aggregate' => $aggregate, 'alias' => $aggregate ? $result_var : ''], $sub_v]; + } + + return [0, $v]; + } + + /* +4 */ + + protected function xLoadQuery($v) + { + if ($sub_r = $this->x('LOAD\s+', $v)) { + $sub_v = $sub_r[1]; + if ((list($sub_r, $sub_v) = $this->xIRIref($sub_v)) && $sub_r) { + $r = ['type' => 'load', 'url' => $sub_r, 'target_graph' => '']; + + return [$r, $sub_v]; + } + } + + return [0, $v]; + } + + /* +5 */ + + protected function xInsertQuery($v) + { + if ($sub_r = $this->x('INSERT\s+', $v)) { + $r = [ + 'type' => 'insert', + 'dataset' => [], + ]; + $sub_v = $sub_r[1]; + /* target */ + if ($sub_r = $this->x('INTO\s+', $sub_v)) { + $sub_v = $sub_r[1]; + if ((list($sub_r, $sub_v) = $this->xIRIref($sub_v)) && $sub_r) { + $r['target_graph'] = $sub_r; + /* CONSTRUCT keyword, optional */ + if ($sub_r = $this->x('CONSTRUCT\s+', $sub_v)) { + $sub_v = $sub_r[1]; + } + /* construct template */ + if ((list($sub_r, $sub_v) = $this->xConstructTemplate($sub_v)) && \is_array($sub_r)) { + $r['construct_triples'] = $sub_r; + } else { + $this->logger->error('Construct Template not found'); + + return [0, $v]; + } + /* dataset */ + while ((list($sub_r, $sub_v) = $this->xDatasetClause($sub_v)) && $sub_r) { + $r['dataset'][] = $sub_r; + } + /* where */ + if ((list($sub_r, $sub_v) = $this->xWhereClause($sub_v)) && $sub_r) { + $r['pattern'] = $sub_r; + } + /* solution modifier */ + if ((list($sub_r, $sub_v) = $this->xSolutionModifier($sub_v)) && $sub_r) { + $r = array_merge($r, $sub_r); + } + + return [$r, $sub_v]; + } + } + } + + return [0, $v]; + } + + /* +6 */ + + protected function xDeleteQuery($v): array + { + if ($sub_r = $this->x('DELETE\s+', $v)) { + $r = [ + 'type' => 'delete', + 'target_graphs' => [], + ]; + $sub_v = $sub_r[1]; + /* target */ + do { + $proceed = false; + if ($sub_r = $this->x('FROM\s+', $sub_v)) { + $sub_v = $sub_r[1]; + if ((list($sub_r, $sub_v) = $this->xIRIref($sub_v)) && $sub_r) { + $r['target_graphs'][] = $sub_r; + $proceed = 1; + } + } + } while ($proceed); + /* CONSTRUCT keyword, optional */ + if ($sub_r = $this->x('CONSTRUCT\s+', $sub_v)) { + $sub_v = $sub_r[1]; + } + /* construct template */ + if ((list($sub_r, $sub_v) = $this->xConstructTemplate($sub_v)) && \is_array($sub_r)) { + $r['construct_triples'] = $sub_r; + /* dataset */ + while ((list($sub_r, $sub_v) = $this->xDatasetClause($sub_v)) && $sub_r) { + $r['dataset'][] = $sub_r; + } + /* where */ + if ((list($sub_r, $sub_v) = $this->xWhereClause($sub_v)) && $sub_r) { + $r['pattern'] = $sub_r; + } + /* solution modifier */ + if ((list($sub_r, $sub_v) = $this->xSolutionModifier($sub_v)) && $sub_r) { + $r = array_merge($r, $sub_r); + } + } + + return [$r, $sub_v]; + } + + return [0, $v]; + } + + /* +7 */ + + protected function xSolutionModifier($v): array + { + $r = []; + if ((list($sub_r, $sub_v) = $this->xGroupClause($v)) && $sub_r) { + $r['group_infos'] = $sub_r; + } + if ((list($sub_r, $sub_v) = $this->xOrderClause($sub_v)) && $sub_r) { + $r['order_infos'] = $sub_r; + } + while ((list($sub_r, $sub_v) = $this->xLimitOrOffsetClause($sub_v)) && $sub_r) { + $r = array_merge($r, $sub_r); + } + + return ($v == $sub_v) ? [0, $v] : [$r, $sub_v]; + } + + /* +8 */ + + protected function xGroupClause($v): array + { + if ($sub_r = $this->x('GROUP BY\s+', $v)) { + $sub_v = $sub_r[1]; + $r = []; + do { + $proceed = 0; + if ((list($sub_r, $sub_v) = $this->xVar($sub_v)) && $sub_r) { + $r[] = $sub_r; + $proceed = 1; + if ($sub_r = $this->x('\,', $sub_v)) { + $sub_v = $sub_r[1]; + } + } + } while ($proceed); + if (\count($r)) { + return [$r, $sub_v]; + } else { + $this->logger->error('No columns specified in GROUP BY clause.'); + } + } + + return [0, $v]; + } +} diff --git a/src/Parser/TurtleParser.php b/src/Parser/TurtleParser.php new file mode 100644 index 0000000..fe90029 --- /dev/null +++ b/src/Parser/TurtleParser.php @@ -0,0 +1,976 @@ + + * (c) Benjamin Nowack + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace sweetrdf\InMemoryStoreSqlite\Parser; + +use Exception; +use function sweetrdf\InMemoryStoreSqlite\calcURI; +use sweetrdf\InMemoryStoreSqlite\Log\Logger; +use sweetrdf\InMemoryStoreSqlite\NamespaceHelper; +use sweetrdf\InMemoryStoreSqlite\StringReader; + +class TurtleParser extends BaseParser +{ + public function __construct(Logger $logger) + { + parent::__construct($logger); + + $this->state = 0; + $this->unparsed_code = ''; + $this->max_parsing_loops = 500; + } + + protected function x($re, $v, $options = 'si') + { + $v = preg_replace('/^[\xA0\xC2]+/', ' ', $v); + + /* comment removal */ + while (preg_match('/^\s*(\#[^\xd\xa]*)(.*)$/si', $v, $m)) { + $v = $m[2]; + } + + return preg_match("/^\s*".$re.'(.*)$/'.$options, $v, $m) ? $m : false; + } + + private function createBnodeID(): string + { + ++$this->bnode_id; + + return '_:'.$this->bnode_prefix.$this->bnode_id; + } + + protected function addT(array $t): void + { + $this->triples[$this->t_count] = $t; + ++$this->t_count; + } + + protected function countTriples() + { + return $this->t_count; + } + + protected function getUnparsedCode() + { + return $this->unparsed_code; + } + + public function parse(string $path, string $data = ''): void + { + $this->reader = new StringReader(); + $this->reader->init($path, $data); + $this->base = $this->reader->getBase(); + $this->r = ['vars' => []]; + /* parse */ + $buffer = ''; + $more_triples = []; + $sub_v = ''; + $sub_v2 = ''; + $loops = 0; + $prologue_done = 0; + while ($d = $this->reader->readStream(8192)) { + $buffer .= $d; + $sub_v = $buffer; + do { + $proceed = 0; + if (!$prologue_done) { + $proceed = 1; + if ((list($sub_r, $sub_v) = $this->xPrologue($sub_v)) && $sub_r) { + $loops = 0; + $sub_v .= $this->reader->readStream(128); + /* in case we missed the final DOT in the previous prologue loop */ + if ($sub_r = $this->x('\.', $sub_v)) { + $sub_v = $sub_r[1]; + } + /* more prologue to come, use outer loop */ + if ($this->x("\@?(base|prefix)", $sub_v)) { + $proceed = 0; + } + } else { + $prologue_done = 1; + } + } + if ( + $prologue_done + && (list($sub_r, $sub_v, $more_triples, $sub_v2) = $this->xTriplesBlock($sub_v)) + && \is_array($sub_r) + ) { + $proceed = 1; + $loops = 0; + foreach ($sub_r as $t) { + $this->addT($t); + } + } + } while ($proceed); + ++$loops; + $buffer = $sub_v; + if ($loops > $this->max_parsing_loops) { + $msg = 'too many loops: '.$loops.'. Could not parse "'.substr($buffer, 0, 200).'..."'; + throw new Exception($msg); + } + } + foreach ($more_triples as $t) { + $this->addT($t); + } + $sub_v = \count($more_triples) ? $sub_v2 : $sub_v; + $buffer = $sub_v; + $this->unparsed_code = $buffer; + + /* remove trailing comments */ + while (preg_match('/^\s*(\#[^\xd\xa]*)(.*)$/si', $this->unparsed_code, $m)) { + $this->unparsed_code = $m[2]; + } + + if ($this->unparsed_code && !$this->logger->hasEntries('error')) { + $rest = preg_replace('/[\x0a|\x0d]/i', ' ', substr($this->unparsed_code, 0, 30)); + if (trim($rest)) { + $this->logger->error('Could not parse "'.$rest.'"'); + } + } + } + + protected function xPrologue($v) + { + $r = 0; + if (!$this->t_count) { + if ((list($sub_r, $v) = $this->xBaseDecl($v)) && $sub_r) { + $this->base = $sub_r; + $r = 1; + } + while ((list($sub_r, $v) = $this->xPrefixDecl($v)) && $sub_r) { + $this->prefixes[$sub_r['prefix']] = $sub_r['uri']; + $r = 1; + } + } + + return [$r, $v]; + } + + /* 3 */ + + protected function xBaseDecl($v) + { + if ($r = $this->x("\@?base\s+", $v)) { + if ((list($r, $sub_v) = $this->xIRI_REF($r[1])) && $r) { + if ($sub_r = $this->x('\.', $sub_v)) { + $sub_v = $sub_r[1]; + } + + return [$r, $sub_v]; + } + } + + return [0, $v]; + } + + /* 4 */ + + protected function xPrefixDecl($v) + { + if ($r = $this->x("\@?prefix\s+", $v)) { + if ((list($r, $sub_v) = $this->xPNAME_NS($r[1])) && $r) { + $prefix = $r; + if ((list($r, $sub_v) = $this->xIRI_REF($sub_v)) && $r) { + $uri = calcURI($r, $this->base); + if ($sub_r = $this->x('\.', $sub_v)) { + $sub_v = $sub_r[1]; + } + + return [['prefix' => $prefix, 'uri_ref' => $r, 'uri' => $uri], $sub_v]; + } + } + } + + return [0, $v]; + } + + /* 21.., 32.. */ + + protected function xTriplesBlock($v) + { + $pre_r = []; + $r = []; + $state = 1; + $sub_v = $v; + $buffer = $sub_v; + do { + $proceed = 0; + if (1 == $state) {/* expecting subject */ + $t = ['type' => 'triple', 's' => '', 'p' => '', 'o' => '', 's_type' => '', 'p_type' => '', 'o_type' => '', 'o_datatype' => '', 'o_lang' => '']; + if ((list($sub_r, $sub_v) = $this->xVarOrTerm($sub_v)) && $sub_r) { + $t['s'] = $sub_r['value']; + $t['s_type'] = $sub_r['type']; + $state = 2; + $proceed = 1; + if ($sub_r = $this->x('(\}|\.)', $sub_v)) { + if ('placeholder' == $t['s_type']) { + $state = 4; + } else { + $this->logger->error('"'.$sub_r[1].'" after subject found.'); + } + } + } elseif ((list($sub_r, $sub_v) = $this->xCollection($sub_v)) && $sub_r) { + $t['s'] = $sub_r['id']; + $t['s_type'] = $sub_r['type']; + $pre_r = array_merge($pre_r, $sub_r['triples']); + $state = 2; + $proceed = 1; + if ($sub_r = $this->x('\.', $sub_v)) { + $this->logger->error('DOT after subject found.'); + } + } elseif ((list($sub_r, $sub_v) = $this->xBlankNodePropertyList($sub_v)) && $sub_r) { + $t['s'] = $sub_r['id']; + $t['s_type'] = $sub_r['type']; + $pre_r = array_merge($pre_r, $sub_r['triples']); + $state = 2; + $proceed = 1; + } elseif ($sub_r = $this->x('\.', $sub_v)) { + $this->logger->error('Subject expected, DOT found.'.$sub_v); + } + } + if (2 == $state) {/* expecting predicate */ + if ($sub_r = $this->x('a\s+', $sub_v)) { + $sub_v = $sub_r[1]; + $t['p'] = NamespaceHelper::NAMESPACE_RDF.'type'; + $t['p_type'] = 'uri'; + $state = 3; + $proceed = 1; + } elseif ((list($sub_r, $sub_v) = $this->xVarOrTerm($sub_v)) && $sub_r) { + if ('bnode' == $sub_r['type']) { + $this->logger->error('Blank node used as triple predicate'); + } + $t['p'] = $sub_r['value']; + $t['p_type'] = $sub_r['type']; + $state = 3; + $proceed = 1; + } elseif ($sub_r = $this->x('\.', $sub_v)) { + $state = 4; + } elseif ($sub_r = $this->x('\}', $sub_v)) { + $buffer = $sub_v; + $r = array_merge($r, $pre_r); + $pre_r = []; + $proceed = 0; + } + } + if (3 == $state) {/* expecting object */ + if ((list($sub_r, $sub_v) = $this->xVarOrTerm($sub_v)) && $sub_r) { + $t['o'] = $sub_r['value']; + $t['o_type'] = $sub_r['type']; + $t['o_lang'] = $sub_r['lang'] ?? ''; + $t['o_datatype'] = $sub_r['datatype'] ?? ''; + $pre_r[] = $t; + $state = 4; + $proceed = 1; + } elseif ((list($sub_r, $sub_v) = $this->xCollection($sub_v)) && $sub_r) { + $t['o'] = $sub_r['id']; + $t['o_type'] = $sub_r['type']; + $t['o_datatype'] = ''; + $pre_r = array_merge($pre_r, [$t], $sub_r['triples']); + $state = 4; + $proceed = 1; + } elseif ((list($sub_r, $sub_v) = $this->xBlankNodePropertyList($sub_v)) && $sub_r) { + $t['o'] = $sub_r['id']; + $t['o_type'] = $sub_r['type']; + $t['o_datatype'] = ''; + $pre_r = array_merge($pre_r, [$t], $sub_r['triples']); + $state = 4; + $proceed = 1; + } + } + if (4 == $state) {/* expecting . or ; or , or } */ + if ($sub_r = $this->x('\.', $sub_v)) { + $sub_v = $sub_r[1]; + $buffer = $sub_v; + $r = array_merge($r, $pre_r); + $pre_r = []; + $state = 1; + $proceed = 1; + } elseif ($sub_r = $this->x('\;', $sub_v)) { + $sub_v = $sub_r[1]; + $state = 2; + $proceed = 1; + } elseif ($sub_r = $this->x('\,', $sub_v)) { + $sub_v = $sub_r[1]; + $state = 3; + $proceed = 1; + if ($sub_r = $this->x('\}', $sub_v)) { + $this->logger->error('Object expected, } found.'); + } + } + if ($sub_r = $this->x('(\}|\{|OPTIONAL|FILTER|GRAPH)', $sub_v)) { + $buffer = $sub_v; + $r = array_merge($r, $pre_r); + $pre_r = []; + $proceed = 0; + } + } + } while ($proceed); + + return \count($r) ? [$r, $buffer, $pre_r, $sub_v] : [0, $buffer, $pre_r, $sub_v]; + } + + /* 39.. */ + + protected function xBlankNodePropertyList($v) + { + if ($sub_r = $this->x('\[', $v)) { + $sub_v = $sub_r[1]; + $s = $this->createBnodeID(); + $r = ['id' => $s, 'type' => 'bnode', 'triples' => []]; + $t = ['type' => 'triple', 's' => $s, 'p' => '', 'o' => '', 's_type' => 'bnode', 'p_type' => '', 'o_type' => '', 'o_datatype' => '', 'o_lang' => '']; + $state = 2; + $closed = 0; + do { + $proceed = 0; + if (2 == $state) {/* expecting predicate */ + if ($sub_r = $this->x('a\s+', $sub_v)) { + $sub_v = $sub_r[1]; + $t['p'] = NamespaceHelper::NAMESPACE_RDF.'type'; + $t['p_type'] = 'uri'; + $state = 3; + $proceed = 1; + } elseif ((list($sub_r, $sub_v) = $this->xVarOrTerm($sub_v)) && $sub_r) { + $t['p'] = $sub_r['value']; + $t['p_type'] = $sub_r['type']; + $state = 3; + $proceed = 1; + } + } + if (3 == $state) {/* expecting object */ + if ((list($sub_r, $sub_v) = $this->xVarOrTerm($sub_v)) && $sub_r) { + $t['o'] = $sub_r['value']; + $t['o_type'] = $sub_r['type']; + $t['o_lang'] = $sub_r['lang'] ?? ''; + $t['o_datatype'] = $sub_r['datatype'] ?? ''; + $r['triples'][] = $t; + $state = 4; + $proceed = 1; + } elseif ((list($sub_r, $sub_v) = $this->xCollection($sub_v)) && $sub_r) { + $t['o'] = $sub_r['id']; + $t['o_type'] = $sub_r['type']; + $t['o_datatype'] = ''; + $r['triples'] = array_merge($r['triples'], [$t], $sub_r['triples']); + $state = 4; + $proceed = 1; + } elseif ((list($sub_r, $sub_v) = $this->xBlankNodePropertyList($sub_v)) && $sub_r) { + $t['o'] = $sub_r['id']; + $t['o_type'] = $sub_r['type']; + $t['o_datatype'] = ''; + $r['triples'] = array_merge($r['triples'], [$t], $sub_r['triples']); + $state = 4; + $proceed = 1; + } + } + if (4 == $state) {/* expecting . or ; or , or ] */ + if ($sub_r = $this->x('\.', $sub_v)) { + $sub_v = $sub_r[1]; + $state = 1; + $proceed = 1; + } + if ($sub_r = $this->x('\;', $sub_v)) { + $sub_v = $sub_r[1]; + $state = 2; + $proceed = 1; + } + if ($sub_r = $this->x('\,', $sub_v)) { + $sub_v = $sub_r[1]; + $state = 3; + $proceed = 1; + } + if ($sub_r = $this->x('\]', $sub_v)) { + $sub_v = $sub_r[1]; + $proceed = 0; + $closed = 1; + } + } + } while ($proceed); + if ($closed) { + return [$r, $sub_v]; + } + + return [0, $v]; + } + + return [0, $v]; + } + + /* 40.. */ + + protected function xCollection($v) + { + if ($sub_r = $this->x('\(', $v)) { + $sub_v = $sub_r[1]; + $s = $this->createBnodeID(); + $r = ['id' => $s, 'type' => 'bnode', 'triples' => []]; + $closed = 0; + do { + $proceed = 0; + if ((list($sub_r, $sub_v) = $this->xVarOrTerm($sub_v)) && $sub_r) { + $r['triples'][] = [ + 'type' => 'triple', + 's' => $s, + 's_type' => 'bnode', + 'p' => NamespaceHelper::NAMESPACE_RDF.'first', + 'p_type' => 'uri', + 'o' => $sub_r['value'], + 'o_type' => $sub_r['type'], + 'o_lang' => $sub_r['lang'] ?? '', + 'o_datatype' => $sub_r['datatype'] ?? '', + ]; + $proceed = 1; + } elseif ((list($sub_r, $sub_v) = $this->xCollection($sub_v)) && $sub_r) { + $r['triples'][] = [ + 'type' => 'triple', + 's' => $s, + 's_type' => 'bnode', + 'p' => NamespaceHelper::NAMESPACE_RDF.'first', + 'p_type' => 'uri', + 'o' => $sub_r['id'], + 'o_type' => $sub_r['type'], + 'o_lang' => '', + 'o_datatype' => '', + ]; + $r['triples'] = array_merge($r['triples'], $sub_r['triples']); + $proceed = 1; + } elseif ((list($sub_r, $sub_v) = $this->xBlankNodePropertyList($sub_v)) && $sub_r) { + $r['triples'][] = [ + 'type' => 'triple', + 's' => $s, + 'p' => NamespaceHelper::NAMESPACE_RDF.'first', + 'o' => $sub_r['id'], + 's_type' => 'bnode', + 'p_type' => 'uri', + 'o_type' => $sub_r['type'], + 'o_lang' => '', + 'o_datatype' => '', + ]; + $r['triples'] = array_merge($r['triples'], $sub_r['triples']); + $proceed = 1; + } + if ($proceed) { + if ($sub_r = $this->x('\)', $sub_v)) { + $sub_v = $sub_r[1]; + $r['triples'][] = [ + 'type' => 'triple', + 's' => $s, + 's_type' => 'bnode', + 'p' => NamespaceHelper::NAMESPACE_RDF.'rest', + 'p_type' => 'uri', + 'o' => NamespaceHelper::NAMESPACE_RDF.'nil', + 'o_type' => 'uri', + 'o_lang' => '', + 'o_datatype' => '', + ]; + $closed = 1; + $proceed = 0; + } else { + $next_s = $this->createBnodeID(); + $r['triples'][] = [ + 'type' => 'triple', + 's' => $s, + 'p' => NamespaceHelper::NAMESPACE_RDF.'rest', + 'o' => $next_s, + 's_type' => 'bnode', + 'p_type' => 'uri', + 'o_type' => 'bnode', + 'o_lang' => '', + 'o_datatype' => '', + ]; + $s = $next_s; + } + } + } while ($proceed); + if ($closed) { + return [$r, $sub_v]; + } + } + + return [0, $v]; + } + + /* 42 */ + + protected function xVarOrTerm($v) + { + if ((list($sub_r, $sub_v) = $this->xVar($v)) && $sub_r) { + return [$sub_r, $sub_v]; + } elseif ((list($sub_r, $sub_v) = $this->xGraphTerm($v)) && $sub_r) { + return [$sub_r, $sub_v]; + } + + return [0, $v]; + } + + /* 44, 74.., 75.. */ + + protected function xVar($v) + { + if ($r = $this->x('(\?|\$)([^\s]+)', $v)) { + if ((list($sub_r, $sub_v) = $this->xVARNAME($r[2])) && $sub_r) { + if (!\in_array($sub_r, $this->r['vars'])) { + $this->r['vars'][] = $sub_r; + } + + return [['value' => $sub_r, 'type' => 'var'], $sub_v.$r[3]]; + } + } + + return [0, $v]; + } + + /* 45 */ + + protected function xGraphTerm($v) + { + foreach ([ + 'IRIref' => 'uri', + 'RDFLiteral' => 'literal', + 'NumericLiteral' => 'literal', + 'BooleanLiteral' => 'literal', + 'BlankNode' => 'bnode', + 'NIL' => 'uri', + 'Placeholder' => 'placeholder', + ] as $term => $type) { + $m = 'x'.$term; + if ((list($sub_r, $sub_v) = $this->$m($v)) && $sub_r) { + if (!\is_array($sub_r)) { + $sub_r = ['value' => $sub_r]; + } + $sub_r['type'] = $sub_r['type'] ?? $type; + + return [$sub_r, $sub_v]; + } + } + + return [0, $v]; + } + + /* 60 */ + + protected function xRDFLiteral($v) + { + if ((list($sub_r, $sub_v) = $this->xString($v)) && $sub_r) { + $sub_r['value'] = $this->unescapeNtripleUTF($sub_r['value']); + $r = $sub_r; + if ((list($sub_r, $sub_v) = $this->xLANGTAG($sub_v)) && $sub_r) { + $r['lang'] = $sub_r; + } elseif ( + !$this->x('\s', $sub_v) + && ($sub_r = $this->x('\^\^', $sub_v)) + && (list($sub_r, $sub_v) = $this->xIRIref($sub_r[1])) + && $sub_r[1] + ) { + $r['datatype'] = $sub_r; + } + + return [$r, $sub_v]; + } + + return [0, $v]; + } + + /* 61.., 62.., 63.., 64.. */ + + protected function xNumericLiteral($v) + { + $sub_r = $this->x('(\-|\+)?', $v); + $prefix = $sub_r[1]; + $sub_v = $sub_r[2]; + foreach (['DOUBLE' => 'double', 'DECIMAL' => 'decimal', 'INTEGER' => 'integer'] as $type => $xsd) { + $m = 'x'.$type; + if ((list($sub_r, $sub_v) = $this->$m($sub_v)) && (false !== $sub_r)) { + $r = [ + 'value' => $prefix.$sub_r, + 'type' => 'literal', + 'datatype' => NamespaceHelper::NAMESPACE_XSD.$xsd, + ]; + + return [$r, $sub_v]; + } + } + + return [0, $v]; + } + + /* 65.. */ + + protected function xBooleanLiteral($v) + { + if ($r = $this->x('(true|false)', $v)) { + return [$r[1], $r[2]]; + } + + return [0, $v]; + } + + /* 66.., 87.., 88.., 89.., 90.., 91.. */ + + protected function xString($v) + {/* largely simplified, may need some tweaks in following revisions */ + $sub_v = $v; + if (!preg_match('/^\s*([\']{3}|\'|[\"]{3}|\")(.*)$/s', $sub_v, $m)) { + return [0, $v]; + } + $delim = $m[1]; + $rest = $m[2]; + $sub_types = ["'''" => 'literal_long1', '"""' => 'literal_long2', "'" => 'literal1', '"' => 'literal2']; + $sub_type = $sub_types[$delim]; + $pos = 0; + $r = false; + do { + $proceed = 0; + $delim_pos = strpos($rest, $delim, $pos); + if (false === $delim_pos) { + break; + } + $new_rest = substr($rest, $delim_pos + \strlen($delim)); + $r = substr($rest, 0, $delim_pos); + if (!preg_match('/([\x5c]+)$/s', $r, $m) || !(\strlen($m[1]) % 2)) { + $rest = $new_rest; + } else { + $r = false; + $pos = $delim_pos + 1; + $proceed = 1; + } + } while ($proceed); + if (false !== $r) { + return [['value' => $r, 'type' => 'literal', 'sub_type' => $sub_type], $rest]; + } + + return [0, $v]; + } + + /* 67 */ + + protected function xIRIref($v) + { + if ((list($r, $v) = $this->xIRI_REF($v)) && $r) { + return [calcURI($r, $this->base), $v]; + } elseif ((list($r, $v) = $this->xPrefixedName($v)) && $r) { + return [$r, $v]; + } + + return [0, $v]; + } + + /* 68 */ + + protected function xPrefixedName($v) + { + if ((list($r, $v) = $this->xPNAME_LN($v)) && $r) { + return [$r, $v]; + } elseif ((list($r, $sub_v) = $this->xPNAME_NS($v)) && $r) { + return isset($this->prefixes[$r]) ? [$this->prefixes[$r], $sub_v] : [0, $v]; + } + + return [0, $v]; + } + + /* 69.., 73.., 93, 94.. */ + + protected function xBlankNode($v) + { + if (($r = $this->x('\_\:', $v)) && (list($r, $sub_v) = $this->xPN_LOCAL($r[1])) && $r) { + return [['type' => 'bnode', 'value' => '_:'.$r], $sub_v]; + } + if ($r = $this->x('\[[\x20\x9\xd\xa]*\]', $v)) { + return [['type' => 'bnode', 'value' => $this->createBnodeID()], $r[1]]; + } + + return [0, $v]; + } + + /* 70.. @@sync with SPARQLParser */ + + protected function xIRI_REF($v) + { + //if ($r = $this->x('\<([^\<\>\"\{\}\|\^\'[:space:]]*)\>', $v)) { + if (($r = $this->x('\<(\$\{[^\>]*\})\>', $v)) && ($sub_r = $this->xPlaceholder($r[1]))) { + return [$r[1], $r[2]]; + } elseif ($r = $this->x('\<\>', $v)) { + return [true, $r[1]]; + } elseif ($r = $this->x('\<([^\s][^\<\>]*)\>', $v)) { + return [$r[1] ? $r[1] : true, $r[2]]; + } + + return [0, $v]; + } + + /* 71 */ + + protected function xPNAME_NS($v) + { + list($r, $sub_v) = $this->xPN_PREFIX($v); + $prefix = $r ?: ''; + + return ($r = $this->x("\:", $sub_v)) ? [$prefix.':', $r[1]] : [0, $v]; + } + + /* 72 */ + + protected function xPNAME_LN($v) + { + if ((list($r, $sub_v) = $this->xPNAME_NS($v)) && $r) { + if (!$this->x('\s', $sub_v) && (list($sub_r, $sub_v) = $this->xPN_LOCAL($sub_v)) && $sub_r) { + if (!isset($this->prefixes[$r])) { + return [0, $v]; + } + + return [$this->prefixes[$r].$sub_r, $sub_v]; + } + } + + return [0, $v]; + } + + /* 76 */ + + protected function xLANGTAG($v) + { + if (!$this->x('\s', $v) && ($r = $this->x('\@([a-z]+(\-[a-z0-9]+)*)', $v))) { + return [$r[1], $r[3]]; + } + + return [0, $v]; + } + + /* 77.. */ + + protected function xINTEGER($v) + { + if ($r = $this->x('([0-9]+)', $v)) { + return [$r[1], $r[2]]; + } + + return [false, $v]; + } + + /* 78.. */ + + protected function xDECIMAL($v) + { + if ($r = $this->x('([0-9]+\.[0-9]*)', $v)) { + return [$r[1], $r[2]]; + } + if ($r = $this->x('(\.[0-9]+)', $v)) { + return [$r[1], $r[2]]; + } + + return [false, $v]; + } + + /* 79.., 86.. */ + + protected function xDOUBLE($v) + { + if ($r = $this->x('([0-9]+\.[0-9]*E[\+\-]?[0-9]+)', $v)) { + return [$r[1], $r[2]]; + } + if ($r = $this->x('(\.[0-9]+E[\+\-]?[0-9]+)', $v)) { + return [$r[1], $r[2]]; + } + if ($r = $this->x('([0-9]+E[\+\-]?[0-9]+)', $v)) { + return [$r[1], $r[2]]; + } + + return [false, $v]; + } + + /* 92 */ + + protected function xNIL($v) + { + if ($r = $this->x('\([\x20\x9\xd\xa]*\)', $v)) { + return [['type' => 'uri', 'value' => NamespaceHelper::NAMESPACE_RDF.'nil'], $r[1]]; + } + + return [0, $v]; + } + + /* 95.. */ + + protected function xPN_CHARS_BASE($v) + { + if ($r = $this->x("([a-z]+|\\\u[0-9a-f]{1,4})", $v)) { + return [$r[1], $r[2]]; + } + + return [0, $v]; + } + + /* 96 */ + + protected function xPN_CHARS_U($v) + { + if ((list($r, $sub_v) = $this->xPN_CHARS_BASE($v)) && $r) { + return [$r, $sub_v]; + } elseif ($r = $this->x('(_)', $v)) { + return [$r[1], $r[2]]; + } + + return [0, $v]; + } + + /* 97.. */ + + protected function xVARNAME($v) + { + $r = ''; + do { + $proceed = 0; + if ($sub_r = $this->x('([0-9]+)', $v)) { + $r .= $sub_r[1]; + $v = $sub_r[2]; + $proceed = 1; + } elseif ((list($sub_r, $sub_v) = $this->xPN_CHARS_U($v)) && $sub_r) { + $r .= $sub_r; + $v = $sub_v; + $proceed = 1; + } elseif ($r && ($sub_r = $this->x('([\xb7\x300-\x36f]+)', $v))) { + $r .= $sub_r[1]; + $v = $sub_r[2]; + $proceed = 1; + } + } while ($proceed); + + return [$r, $v]; + } + + /* 98.. */ + + protected function xPN_CHARS($v) + { + if ((list($r, $sub_v) = $this->xPN_CHARS_U($v)) && $r) { + return [$r, $sub_v]; + } elseif ($r = $this->x('([\-0-9\xb7\x300-\x36f])', $v)) { + return [$r[1], $r[2]]; + } + + return [false, $v]; + } + + /* 99 */ + + protected function xPN_PREFIX($v) + { + if ($sub_r = $this->x("([^\s\:\(\)\{\}\;\,]+)", $v, 's')) {/* accelerator */ + return [$sub_r[1], $sub_r[2]]; /* @@testing */ + } + if ((list($r, $sub_v) = $this->xPN_CHARS_BASE($v)) && $r) { + do { + $proceed = 0; + list($sub_r, $sub_v) = $this->xPN_CHARS($sub_v); + if (false !== $sub_r) { + $r .= $sub_r; + $proceed = 1; + } elseif ($sub_r = $this->x("\.", $sub_v)) { + $r .= '.'; + $sub_v = $sub_r[1]; + $proceed = 1; + } + } while ($proceed); + list($sub_r, $sub_v) = $this->xPN_CHARS($sub_v); + $r .= $sub_r ?: ''; + } + + return [$r, $sub_v]; + } + + /* 100 */ + + protected function xPN_LOCAL($v) + { + if (($sub_r = $this->x("([^\s\(\)\{\}\[\]\;\,\.]+)", $v, 's')) && !preg_match('/^\./', $sub_r[2])) {/* accelerator */ + return [$sub_r[1], $sub_r[2]]; /* @@testing */ + } + $r = ''; + $sub_v = $v; + do { + $proceed = 0; + if ($this->x('\s', $sub_v)) { + return [$r, $sub_v]; + } + if ($sub_r = $this->x('([0-9])', $sub_v)) { + $r .= $sub_r[1]; + $sub_v = $sub_r[2]; + $proceed = 1; + } elseif ((list($sub_r, $sub_v) = $this->xPN_CHARS_U($sub_v)) && $sub_r) { + $r .= $sub_r; + $proceed = 1; + } elseif ($r) { + if (($sub_r = $this->x('(\.)', $sub_v)) && !preg_match('/^[\s\}]/s', $sub_r[2])) { + $r .= $sub_r[1]; + $sub_v = $sub_r[2]; + } + if ((list($sub_r, $sub_v) = $this->xPN_CHARS($sub_v)) && $sub_r) { + $r .= $sub_r; + $proceed = 1; + } + } + } while ($proceed); + + return [$r, $sub_v]; + } + + protected function unescapeNtripleUTF($v) + { + if (false === strpos($v, '\\')) { + return $v; + } + $mappings = ['t' => "\t", 'n' => "\n", 'r' => "\r", '\"' => '"', '\'' => "'"]; + foreach ($mappings as $in => $out) { + $v = preg_replace('/\x5c(['.$in.'])/', $out, $v); + } + if (false === strpos(strtolower($v), '\u')) { + return $v; + } + while (preg_match('/\\\(U)([0-9A-F]{8})/', $v, $m) || preg_match('/\\\(u)([0-9A-F]{4})/', $v, $m)) { + $no = hexdec($m[2]); + if ($no < 128) { + $char = \chr($no); + } elseif ($no < 2048) { + $char = \chr(($no >> 6) + 192).\chr(($no & 63) + 128); + } elseif ($no < 65536) { + $char = \chr(($no >> 12) + 224).\chr((($no >> 6) & 63) + 128).\chr(($no & 63) + 128); + } elseif ($no < 2097152) { + $char = \chr(($no >> 18) + 240).\chr((($no >> 12) & 63) + 128).\chr((($no >> 6) & 63) + 128).\chr(($no & 63) + 128); + } else { + $char = ''; + } + $v = str_replace('\\'.$m[1].$m[2], $char, $v); + } + + return $v; + } + + protected function xPlaceholder($v) + { + //if ($r = $this->x('(\?|\$)\{([^\}]+)\}', $v)) { + if ($r = $this->x('(\?|\$)', $v)) { + if (preg_match('/(\{(?:[^{}]+|(?R))*\})/', $r[2], $m) && 0 === strpos(trim($r[2]), $m[1])) { + $ph = substr($m[1], 1, -1); + $rest = substr(trim($r[2]), \strlen($m[1])); + if (!isset($this->r['placeholders'])) { + $this->r['placeholders'] = []; + } + if (!\in_array($ph, $this->r['placeholders'])) { + $this->r['placeholders'][] = $ph; + } + + return [['value' => $ph, 'type' => 'placeholder'], $rest]; + } + } + + return [0, $v]; + } +} diff --git a/src/Rdf/BlankNode.php b/src/Rdf/BlankNode.php new file mode 100644 index 0000000..f8fac0e --- /dev/null +++ b/src/Rdf/BlankNode.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use rdfInterface\BlankNode as iBlankNode; +use rdfInterface\Term; +use rdfInterface\TYPE_BLANK_NODE; + +class BlankNode implements iBlankNode +{ + private string $id; + + public function __construct(?string $id = null) + { + if (empty($id)) { + // if no ID was given, generate random unique string + $id = bin2hex(random_bytes(16)); + } + + if (!str_starts_with($id, '_:')) { + $id = '_:'.$id; + } + + $this->id = $id; + } + + public function __toString(): string + { + return $this->id; + } + + public function equals(Term $term): bool + { + return $this === $term; + } + + public function getType(): string + { + return TYPE_BLANK_NODE; + } + + public function getValue(): string + { + return $this->id; + } +} diff --git a/src/Rdf/DataFactory.php b/src/Rdf/DataFactory.php new file mode 100644 index 0000000..57dd713 --- /dev/null +++ b/src/Rdf/DataFactory.php @@ -0,0 +1,73 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use rdfInterface\BlankNode as iBlankNode; +use rdfInterface\DataFactory as iDataFactory; +use rdfInterface\DefaultGraph as iDefaultGraph; +use rdfInterface\Literal as iLiteral; +use rdfInterface\NamedNode as iNamedNode; +use rdfInterface\Quad as iQuad; +use rdfInterface\QuadTemplate as iQuadTemplate; +use rdfInterface\Term as iTerm; +use rdfInterface\Variable as iVariable; +use Stringable; + +class DataFactory implements iDataFactory +{ + public static function blankNode(string | Stringable | null $iri = null): iBlankNode + { + return new BlankNode($iri); + } + + public static function namedNode(string | Stringable $iri): iNamedNode + { + return new NamedNode($iri); + } + + public static function defaultGraph(string | Stringable | null $iri = null): iDefaultGraph + { + return new DefaultGraph($iri); + } + + public static function literal( + int | float | string | bool | Stringable $value, + string | Stringable | null $lang = null, + string | Stringable | null $datatype = null + ): iLiteral { + return new Literal($value, $lang, $datatype); + } + + public static function quad( + iTerm $subject, + iNamedNode $predicate, + iTerm $object, + iNamedNode | iBlankNode | null $graphIri = null + ): iQuad { + return new Quad($subject, $predicate, $object, $graphIri); + } + + public static function quadTemplate( + iTerm | null $subject = null, + iNamedNode | null $predicate = null, + iTerm | null $object = null, + iNamedNode | iBlankNode | null $graphIri = null + ): iQuadTemplate { + throw new RdfException('quadTemplate is not implemented yet.'); + } + + public static function variable(string | Stringable $name): iVariable + { + throw new RdfException('variable is not implemented yet.'); + } +} diff --git a/src/Rdf/DefaultGraph.php b/src/Rdf/DefaultGraph.php new file mode 100644 index 0000000..6a321b7 --- /dev/null +++ b/src/Rdf/DefaultGraph.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use rdfInterface\DefaultGraph; +use rdfInterface\Term; +use rdfInterface\TYPE_DEFAULT_GRAPH; +use sweetrdf\InMemoryStoreSqlite\NamespaceHelper; + +class DefaultGraph implements iDefaultGraph +{ + private ?string $iri; + + public function __construct(?string $iri = null) + { + if (empty($iri)) { + $iri = NamespaceHelper::BASE_NAMESPACE; + } + + $this->iri = $iri; + } + + public function __toString(): string + { + return $this->getValue(); + } + + public function equals(Term $term): bool + { + return $this === $term; + } + + public function getType(): string + { + return TYPE_DEFAULT_GRAPH; + } + + public function getValue(): string + { + return $this->iri ?? TYPE_DEFAULT_GRAPH; + } +} diff --git a/src/Rdf/Literal.php b/src/Rdf/Literal.php new file mode 100644 index 0000000..141cc98 --- /dev/null +++ b/src/Rdf/Literal.php @@ -0,0 +1,91 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Exception; +use rdfInterface\Literal as iLiteral; +use rdfInterface\Term; +use rdfInterface\TYPE_LITERAL; +use Stringable; +use sweetrdf\InMemoryStoreSqlite\NamespaceHelper; + +class Literal implements iLiteral +{ + private int | float | string | bool | Stringable $value; + + private ?string $lang; + + private ?string $datatype; + + public function __construct( + int | float | string | bool | Stringable $value, + ?string $lang = null, + ?string $datatype = null + ) { + $this->value = $value; + $this->lang = $lang; + $this->datatype = $datatype; + } + + public function __toString(): string + { + $langtype = ''; + if (!empty($this->lang)) { + $langtype = '@'.$this->lang; + } elseif (!empty($this->datatype)) { + $langtype = "^^<$this->datatype>"; + } + + return '"'.$this->value.'"'.$langtype; + } + + public function getValue(): int | float | string | bool | Stringable + { + return $this->value; + } + + public function getLang(): ?string + { + return $this->lang; + } + + public function getDatatype(): string + { + return $this->datatype ?? NamespaceHelper::NAMESPACE_XSD; + } + + public function getType(): string + { + return TYPE_LITERAL; + } + + public function equals(Term $term): bool + { + return $this === $term; + } + + public function withValue(int | float | string | bool | Stringable $value): self + { + throw new Exception('withValue not implemented yet'); + } + + public function withLang(?string $lang): self + { + throw new Exception('withLang not implemented yet'); + } + + public function withDatatype(?string $datatype): self + { + throw new Exception('withDatatype not implemented yet'); + } +} diff --git a/src/Rdf/NamedNode.php b/src/Rdf/NamedNode.php new file mode 100644 index 0000000..64e3596 --- /dev/null +++ b/src/Rdf/NamedNode.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use rdfInterface\NamedNode as iNamedNode; +use rdfInterface\Term; +use rdfInterface\TYPE_NAMED_NODE; + +class NamedNode implements iNamedNode +{ + private string $iri; + + public function __construct(string $iri) + { + $this->iri = $iri; + } + + public function __toString(): string + { + return '<'.$this->iri.'>'; + } + + public function getValue(): string + { + return $this->iri; + } + + public function getType(): string + { + return TYPE_NAMED_NODE; + } + + public function equals(Term $term): bool + { + return $this === $term; + } +} diff --git a/src/Rdf/Quad.php b/src/Rdf/Quad.php new file mode 100644 index 0000000..3695ca2 --- /dev/null +++ b/src/Rdf/Quad.php @@ -0,0 +1,141 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use BadMethodCallException; +use Exception; +use rdfInterface\BlankNode as iBlankNode; +use rdfInterface\Literal as iLiteral; +use rdfInterface\NamedNode as iNamedNode; +use rdfInterface\Quad as iQuad; +use rdfInterface\Term as iTerm; + +class Quad implements iQuad +{ + private iTerm $subject; + + private iNamedNode $predicate; + + private iTerm $object; + + private iNamedNode | iBlankNode | null $graphIri; + + public function __construct( + iTerm $subject, + iNamedNode $predicate, + iTerm $object, + iNamedNode | iBlankNode | null $graphIri = null + ) { + if ($subject instanceof iLiteral) { + throw new BadMethodCallException('Subject must be of type NamedNode or BlankNode'); + } + $this->subject = $subject; + $this->predicate = $predicate; + $this->object = $object; + $this->graphIri = $graphIri ?? new DefaultGraph(); + } + + public function __toString(): string + { + return rtrim("$this->subject $this->predicate $this->object $this->graphIri"); + } + + public function getType(): string + { + return \rdfInterface\TYPE_QUAD; + } + + public function equals(iTerm $term): bool + { + return $this === $term; + } + + public function getValue(): string + { + throw new BadMethodCallException(); + } + + public function getSubject(): iTerm + { + return $this->subject; + } + + public function getPredicate(): iNamedNode + { + return $this->predicate; + } + + public function getObject(): iTerm + { + return $this->object; + } + + public function getGraphIri(): iNamedNode | iBlankNode + { + return $this->graphIri; + } + + public static function createFromArray(array $triple, string $graph): iQuad + { + /* + * subject + */ + if ('uri' == $triple['s_type']) { + $s = new NamedNode($triple['s']); + } elseif ('bnode' == $triple['s_type']) { + $s = new BlankNode($triple['s']); + } else { + throw new Exception('Invalid subject type given.'); + } + + // predicate + $p = new NamedNode($triple['p']); + + /* + * object + */ + if ('uri' == $triple['o_type']) { + $o = new NamedNode($triple['o']); + } elseif ('bnode' == $triple['o_type']) { + $o = new BlankNode($triple['o']); + } elseif ('literal' == $triple['o_type']) { + $o = new Literal($triple['o'], $triple['o_lang'], $triple['o_datatype']); + } else { + throw new Exception('Invalid object type given.'); + } + + $g = !empty($graph) ? new NamedNode($graph) : new DefaultGraph(); + + return new self($s, $p, $o, $g); + } + + public function withSubject(iTerm $subject): iQuad + { + throw new Exception('withSubject not implemented yet.'); + } + + public function withPredicate(iNamedNode $predicate): iQuad + { + throw new Exception('withPredicate not implemented yet.'); + } + + public function withObject(iTerm $object): iQuad + { + throw new Exception('withObject not implemented yet.'); + } + + public function withGraphIri(iNamedNode | iBlankNode $graphIri): iQuad + { + throw new Exception('withGraphIri not implemented yet.'); + } +} diff --git a/src/Serializer/TurtleSerializer.php b/src/Serializer/TurtleSerializer.php new file mode 100644 index 0000000..a7a0dc2 --- /dev/null +++ b/src/Serializer/TurtleSerializer.php @@ -0,0 +1,276 @@ + + * (c) Benjamin Nowack + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace sweetrdf\InMemoryStoreSqlite\Serializer; + +use sweetrdf\InMemoryStoreSqlite\NamespaceHelper; +use function sweetrdf\InMemoryStoreSqlite\splitURI; + +class TurtleSerializer +{ + private array $ns = []; + private array $nsp = []; + private int $ns_count = 0; + + public function __construct() + { + $this->qualifier = ['rdf:type', 'rdfs:domain', 'rdfs:range', 'rdfs:subClassOf']; + + $rdf = NamespaceHelper::NAMESPACE_RDF; + $this->nsp = [$rdf => 'rdf']; + $this->used_ns = [$rdf]; + $this->ns = ['rdf' => $rdf]; + } + + public function getSerializedTriples($triples, $raw = 0) + { + $index = $this->getSimpleIndex($triples, 0); + + return $this->getSerializedIndex($index, $raw); + } + + public function getSimpleIndex($triples, $flatten_objects = 1, $vals = '') + { + $r = []; + foreach ($triples as $t) { + $skip_t = 0; + foreach (['s', 'p', 'o'] as $term) { + $$term = $t[$term]; + /* template var */ + if (isset($t[$term.'_type']) && ('var' == $t[$term.'_type'])) { + $val = isset($vals[$$term]) ? $vals[$$term] : ''; + $skip_t = isset($vals[$$term]) ? $skip_t : 1; + $type = ''; + $type = !$type && isset($vals[$$term.' type']) ? $vals[$$term.' type'] : $type; + $type = !$type && preg_match('/^\_\:/', $val) ? 'bnode' : $type; + if ('o' == $term) { + $type = !$type && (preg_match('/\s/s', $val) || !preg_match('/\:/', $val)) ? 'literal' : $type; + $type = !$type && !preg_match('/[\/]/', $val) ? 'literal' : $type; + } + $type = !$type ? 'uri' : $type; + $t[$term.'_type'] = $type; + $$term = $val; + } + } + if ($skip_t) { + continue; + } + if (!isset($r[$s])) { + $r[$s] = []; + } + if (!isset($r[$s][$p])) { + $r[$s][$p] = []; + } + if ($flatten_objects) { + if (!\in_array($o, $r[$s][$p])) { + $r[$s][$p][] = $o; + } + } else { + $o = ['value' => $o]; + foreach (['lang', 'type', 'datatype'] as $suffix) { + if (isset($t['o_'.$suffix]) && $t['o_'.$suffix]) { + $o[$suffix] = $t['o_'.$suffix]; + } elseif (isset($t['o '.$suffix]) && $t['o '.$suffix]) { + $o[$suffix] = $t['o '.$suffix]; + } + } + if (!\in_array($o, $r[$s][$p])) { + $r[$s][$p][] = $o; + } + } + } + + return $r; + } + + /** + * @todo port to NamespaceHelper + */ + public function getPNameNamespace($v, $connector = ':') + { + $re = '/^([a-z0-9\_\-]+)\:([a-z0-9\_\-\.\%]+)$/i'; + if (':' != $connector) { + $connectors = ['\:', '\-', '\_', '\.']; + $chars = implode('', array_diff($connectors, [$connector])); + $re = '/^([a-z0-9'.$chars.']+)\\'.$connector.'([a-z0-9\_\-\.\%]+)$/i'; + } + if (!preg_match($re, $v, $m)) { + return 0; + } + if (!isset($this->ns[$m[1]])) { + return 0; + } + + return $this->ns[$m[1]]; + } + + /** + * @todo port to NamespaceHelper + */ + public function getPName($v, $connector = ':') + { + /* is already a pname */ + $ns = $this->getPNameNamespace($v, $connector); + if ($ns) { + if (!\in_array($ns, $this->used_ns)) { + $this->used_ns[] = $ns; + } + + return $v; + } + /* new pname */ + $parts = splitURI($v); + if ($parts) { + /* known prefix */ + foreach ($this->ns as $prefix => $ns) { + if ($parts[0] == $ns) { + if (!\in_array($ns, $this->used_ns)) { + $this->used_ns[] = $ns; + } + + return $prefix.$connector.$parts[1]; + } + } + /* new prefix */ + $prefix = $this->getPrefix($parts[0]); + + return $prefix.$connector.$parts[1]; + } + + return $v; + } + + /** + * @todo port to NamespaceHelper + */ + public function getPrefix($ns) + { + if (!isset($this->nsp[$ns])) { + $this->ns['ns'.$this->ns_count] = $ns; + $this->nsp[$ns] = 'ns'.$this->ns_count; + ++$this->ns_count; + } + if (!\in_array($ns, $this->used_ns)) { + $this->used_ns[] = $ns; + } + + return $this->nsp[$ns]; + } + + public function getTerm($v, $term = '', $qualifier = '') + { + if (!\is_array($v)) { + if (preg_match('/^\_\:/', $v)) { + return $v; + } + if (('p' === $term) && ($pn = $this->getPName($v))) { + return $pn; + } + if ( + ('o' === $term) + && \in_array($qualifier, $this->qualifier) + && ($pn = $this->getPName($v)) + ) { + return $pn; + } + if (preg_match('/^[a-z0-9]+\:[^\s]*$/isu', $v)) { + return '<'.$v.'>'; + } + + return $this->getTerm(['type' => 'literal', 'value' => $v], $term, $qualifier); + } + if (!isset($v['type']) || ('literal' != $v['type'])) { + return $this->getTerm($v['value'], $term, $qualifier); + } + /* literal */ + $quot = '"'; + if (false !== preg_match('/\"/', $v['value'])) { + $quot = "'"; + if (false !== preg_match('/\'/', $v['value']) || false !== preg_match('/[\x0d\x0a]/', $v['value'])) { + $quot = '"""'; + if ( + false !== preg_match('/\"\"\"/', $v['value']) + || false !== preg_match('/\"$/', $v['value']) + || false !== preg_match('/^\"/', $v['value']) + ) { + $quot = "'''"; + $v['value'] = preg_replace("/'$/", "' ", $v['value']); + $v['value'] = preg_replace("/^'/", " '", $v['value']); + $v['value'] = str_replace("'''", '\\\'\\\'\\\'', $v['value']); + } + } + } + if ((1 == \strlen($quot)) && false !== preg_match('/[\x0d\x0a]/', $v['value'])) { + $quot = $quot.$quot.$quot; + } + $suffix = isset($v['lang']) && $v['lang'] ? '@'.$v['lang'] : ''; + $suffix = isset($v['datatype']) && $v['datatype'] ? '^^'.$this->getTerm($v['datatype'], 'dt') : $suffix; + + return $quot.$v['value'].$quot.$suffix; + } + + public function getHead() + { + $r = ''; + $nl = "\n"; + foreach ($this->used_ns as $v) { + $r .= $r ? $nl : ''; + foreach ($this->ns as $prefix => $ns) { + if ($ns != $v) { + continue; + } + $r .= '@prefix '.$prefix.': <'.$v.'> .'; + break; + } + } + + return $r; + } + + public function getSerializedIndex($index, $raw = 0) + { + $r = ''; + $nl = "\n"; + foreach ($index as $s => $ps) { + $r .= $r ? ' .'.$nl.$nl : ''; + $s = $this->getTerm($s, 's'); + $r .= $s; + $first_p = 1; + foreach ($ps as $p => $os) { + if (!$os) { + continue; + } + $p = $this->getTerm($p, 'p'); + $r .= $first_p ? ' ' : ' ;'.$nl.str_pad('', \strlen($s) + 1); + $r .= $p; + $first_o = 1; + if (!\is_array($os)) {/* single literal o */ + $os = [['value' => $os, 'type' => 'literal']]; + } + foreach ($os as $o) { + $r .= $first_o ? ' ' : ' ,'.$nl.str_pad('', \strlen($s) + \strlen($p) + 2); + $o = $this->getTerm($o, 'o', $p); + $r .= $o; + $first_o = 0; + } + $first_p = 0; + } + } + $r .= $r ? ' .' : ''; + if ($raw) { + return $r; + } + + return $r ? $this->getHead().$nl.$nl.$r : ''; + } +} diff --git a/src/Store/InMemoryStoreSqlite.php b/src/Store/InMemoryStoreSqlite.php new file mode 100644 index 0000000..852e024 --- /dev/null +++ b/src/Store/InMemoryStoreSqlite.php @@ -0,0 +1,218 @@ + + * (c) Benjamin Nowack + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace sweetrdf\InMemoryStoreSqlite\Store; + +use Exception; +use rdfInterface\Term; +use sweetrdf\InMemoryStoreSqlite\KeyValueBag; +use sweetrdf\InMemoryStoreSqlite\Log\LoggerPool; +use sweetrdf\InMemoryStoreSqlite\Parser\SPARQLPlusParser; +use sweetrdf\InMemoryStoreSqlite\PDOSQLiteAdapter; +use sweetrdf\InMemoryStoreSqlite\Rdf\BlankNode; +use sweetrdf\InMemoryStoreSqlite\Rdf\Literal; +use sweetrdf\InMemoryStoreSqlite\Rdf\NamedNode; +use sweetrdf\InMemoryStoreSqlite\Serializer\TurtleSerializer; +use sweetrdf\InMemoryStoreSqlite\Store\QueryHandler\AskQueryHandler; +use sweetrdf\InMemoryStoreSqlite\Store\QueryHandler\ConstructQueryHandler; +use sweetrdf\InMemoryStoreSqlite\Store\QueryHandler\DeleteQueryHandler; +use sweetrdf\InMemoryStoreSqlite\Store\QueryHandler\DescribeQueryHandler; +use sweetrdf\InMemoryStoreSqlite\Store\QueryHandler\InsertQueryHandler; +use sweetrdf\InMemoryStoreSqlite\Store\QueryHandler\LoadQueryHandler; +use sweetrdf\InMemoryStoreSqlite\Store\QueryHandler\SelectQueryHandler; + +class InMemoryStoreSqlite +{ + private bool $bulkLoadModeIsActive = true; + + private int $bulkLoadModeNextTermId = 1; + + private PDOSQLiteAdapter $db; + + private KeyValueBag $rowCache; + + private LoggerPool $loggerPool; + + public function __construct(PDOSQLiteAdapter $db, LoggerPool $loggerPool, KeyValueBag $rowCache) + { + $this->db = $db; + $this->loggerPool = $loggerPool; + $this->rowCache = $rowCache; + } + + /** + * Shortcut for people who want a ready-to-use instance. + */ + public static function createInstance() + { + return new self(new PDOSQLiteAdapter(), new LoggerPool(), new KeyValueBag()); + } + + public function getLoggerPool(): LoggerPool + { + return $this->loggerPool; + } + + public function getDBObject(): ?PDOSQLiteAdapter + { + return $this->db; + } + + public function getDBVersion() + { + return $this->db->getServerVersion(); + } + + private function toTurtle($v): string + { + $ser = new TurtleSerializer(); + + return (isset($v[0]) && isset($v[0]['s'])) + ? $ser->getSerializedTriples($v) + : $ser->getSerializedIndex($v); + } + + /** + * @todo remove? + */ + public function insert($data, $g, $keep_bnode_ids = 0) + { + if (\is_array($data)) { + $data = $this->toTurtle($data); + } + + if (empty($data)) { + // TODO required to throw something here? + return; + } + + $infos = ['query' => ['url' => $g, 'target_graph' => $g]]; + $h = new LoadQueryHandler($this, $this->loggerPool->createNewLogger('Load')); + + return $h->runQuery($infos, $data, $keep_bnode_ids); + } + + public function delete($doc, $g) + { + if (!$doc) { + $infos = ['query' => ['target_graphs' => [$g]]]; + $h = new DeleteQueryHandler($this, $this->loggerPool->createNewLogger('Delete')); + $this->rowCache->reset(); + $r = $h->runQuery($infos); + + return $r; + } + } + + /** + * Executes a SPARQL query. + * + * @param string $q SPARQL query + * @param string $format One of: raw, instances + */ + public function query(string $q, string $format = 'raw'): array | bool | Term + { + $errors = []; + + if (preg_match('/^dump/i', $q)) { + $infos = ['query' => ['type' => 'dump']]; + } else { + $parserLogger = $this->loggerPool->createNewLogger('SPARQL'); + $p = new SPARQLPlusParser($parserLogger); + $p->parse($q); + $infos = $p->getQueryInfos(); + $errors = $parserLogger->getEntries('error'); + + if (0 < \count($errors)) { + throw new Exception('Query failed: '.json_encode($errors)); + } + } + + $queryType = $infos['query']['type']; + $validTypes = ['select', 'ask', 'describe', 'construct', 'load', 'insert', 'delete', 'dump']; + if (!\in_array($queryType, $validTypes)) { + throw new Exception('Unsupported query type "'.$queryType.'"'); + } + + $cls = match ($queryType) { + 'ask' => AskQueryHandler::class, + 'construct' => ConstructQueryHandler::class, + 'describe' => DescribeQueryHandler::class, + 'delete' => DeleteQueryHandler::class, + 'insert' => InsertQueryHandler::class, + 'load' => LoadQueryHandler::class, + 'select' => SelectQueryHandler::class, + }; + + if (empty($cls)) { + throw new Exception('Inalid query $type given.'); + } + + $queryHandlerLogger = $this->loggerPool->createNewLogger('QueryHandler'); + $queryHandler = new $cls($this, $queryHandlerLogger); + + if ('insert' == $queryType) { + $queryHandler->setRowCache($this->rowCache); + + if (true === $this->bulkLoadModeIsActive) { + $queryHandler->activateBulkLoadMode($this->bulkLoadModeNextTermId); + } + } elseif ('delete' == $queryType) { + // reset row cache, because it will not be notified of data changes + $this->rowCache->reset(); + $this->bulkLoadModeIsActive = false; + } + + $queryResult = $queryHandler->runQuery($infos); + + if ('insert' == $queryType && true === $this->bulkLoadModeIsActive) { + // save latest term ID in case further insert into queries follow + $this->bulkLoadModeNextTermId = $queryHandler->getBulkLoadModeNextTermId(); + } + + $result = null; + if ('raw' == $format) { + // use plain old ARC2 format which is an array of arrays + $result = ['query_type' => $queryType, 'result' => $queryResult]; + } elseif ('instances' == $format) { + // use rdfInstance instance(s) to represent result entries + if (\is_array($queryResult)) { + $variables = $queryResult['variables']; + + foreach ($queryResult['rows'] as $row) { + $resultEntry = []; + foreach ($variables as $variable) { + if ('uri' == $row[$variable.' type']) { + $resultEntry[$variable] = new NamedNode($row[$variable]); + } elseif ('bnode' == $row[$variable.' type']) { + $resultEntry[$variable] = new BlankNode($row[$variable]); + } elseif ('literal' == $row[$variable.' type']) { + $resultEntry[$variable] = new Literal( + $row[$variable], + $row[$variable.' lang'] ?? null, + $row[$variable.' datatype'] ?? null + ); + } else { + throw new Exception('Invalid type given: '.$row[$variable.' type']); + } + } + $result[] = $resultEntry; + } + } else { + $result = new Literal($queryResult); + } + } + + return $result; + } +} diff --git a/src/Store/QueryHandler/AskQueryHandler.php b/src/Store/QueryHandler/AskQueryHandler.php new file mode 100755 index 0000000..4305cc6 --- /dev/null +++ b/src/Store/QueryHandler/AskQueryHandler.php @@ -0,0 +1,43 @@ + + * (c) Benjamin Nowack + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace sweetrdf\InMemoryStoreSqlite\Store\QueryHandler; + +class AskQueryHandler extends SelectQueryHandler +{ + public function runQuery($infos) + { + $infos['query']['limit'] = 1; + $this->infos = $infos; + $this->buildResultVars(); + + return parent::runQuery($this->infos); + } + + public function buildResultVars() + { + $this->infos['query']['result_vars'][] = [ + 'var' => '1', + 'aggregate' => '', + 'alias' => 'success', + ]; + } + + public function getFinalQueryResult($q_sql, $tmp_tbl) + { + $row = $this->store->getDBObject()->fetchRow('SELECT success FROM '.$tmp_tbl); + $r = isset($row['success']) ? $row['success'] : 0; + + return $r ? true : false; + } +} diff --git a/src/Store/QueryHandler/ConstructQueryHandler.php b/src/Store/QueryHandler/ConstructQueryHandler.php new file mode 100755 index 0000000..c42366d --- /dev/null +++ b/src/Store/QueryHandler/ConstructQueryHandler.php @@ -0,0 +1,103 @@ + + * (c) Benjamin Nowack + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace sweetrdf\InMemoryStoreSqlite\Store\QueryHandler; + +class ConstructQueryHandler extends SelectQueryHandler +{ + public function runQuery($infos) + { + $this->infos = $infos; + $this->buildResultVars(); + $this->infos['query']['distinct'] = 1; + $sub_r = parent::runQuery($this->infos); + $rf = $infos['result_format'] ?? ''; + if (\in_array($rf, ['sql', 'structure', 'index'])) { + return $sub_r; + } + + return $this->getResultIndex($sub_r); + } + + public function buildResultVars() + { + $r = []; + foreach ($this->infos['query']['construct_triples'] as $t) { + foreach (['s', 'p', 'o'] as $term) { + if ('var' == $t[$term.'_type']) { + if (!\in_array($t[$term], $r)) { + $r[] = ['var' => $t[$term], 'aggregate' => '', 'alias' => '']; + } + } + } + } + $this->infos['query']['result_vars'] = $r; + } + + public function getResultIndex($qr) + { + $r = []; + $added = []; + $rows = $qr['rows'] ?? []; + $cts = $this->infos['query']['construct_triples']; + $bnc = 0; + foreach ($rows as $row) { + ++$bnc; + foreach ($cts as $ct) { + $skip_t = 0; + $t = []; + foreach (['s', 'p', 'o'] as $term) { + $val = $ct[$term]; + $type = $ct[$term.'_type']; + $val = ('bnode' == $type) ? $val.$bnc : $val; + if ('var' == $type) { + $skip_t = !isset($row[$val]) ? 1 : $skip_t; + $type = !$skip_t ? $row[$val.' type'] : ''; + $val = (!$skip_t) ? $row[$val] : ''; + } + $t[$term] = $val; + $t[$term.'_type'] = $type; + if (isset($row[$ct[$term].' lang'])) { + $t[$term.'_lang'] = $row[$ct[$term].' lang']; + } + if (isset($row[$ct[$term].' datatype'])) { + $t[$term.'_datatype'] = $row[$ct[$term].' datatype']; + } + } + if (!$skip_t) { + $s = $t['s']; + $p = $t['p']; + $o = $t['o']; + if (!isset($r[$s])) { + $r[$s] = []; + } + if (!isset($r[$s][$p])) { + $r[$s][$p] = []; + } + $o = ['value' => $o]; + foreach (['lang', 'type', 'datatype'] as $suffix) { + if (isset($t['o_'.$suffix]) && $t['o_'.$suffix]) { + $o[$suffix] = $t['o_'.$suffix]; + } + } + if (!isset($added[md5($s.' '.$p.' '.serialize($o))])) { + $r[$s][$p][] = $o; + $added[md5($s.' '.$p.' '.serialize($o))] = 1; + } + } + } + } + + return $r; + } +} diff --git a/src/Store/QueryHandler/DeleteQueryHandler.php b/src/Store/QueryHandler/DeleteQueryHandler.php new file mode 100644 index 0000000..ed722f4 --- /dev/null +++ b/src/Store/QueryHandler/DeleteQueryHandler.php @@ -0,0 +1,192 @@ + + * (c) Benjamin Nowack + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace sweetrdf\InMemoryStoreSqlite\Store\QueryHandler; + +use Exception; + +class DeleteQueryHandler extends QueryHandler +{ + private bool $refs_deleted; + + public function runQuery($infos) + { + $this->infos = $infos; + /* delete */ + $this->refs_deleted = false; + /* graph(s) only */ + $constructTriples = $this->infos['query']['construct_triples'] ?? []; + $pattern = $this->infos['query']['pattern'] ?? []; + if (!$constructTriples) { + $tc = $this->deleteTargetGraphs(); + } elseif (!$pattern) { + /* graph(s) + explicit triples */ + $tc = $this->deleteTriples(); + } else { + /* graph(s) + constructed triples */ + $tc = $this->deleteConstructedGraph(); + } + /* clean up */ + if ($tc && ($this->refs_deleted || (1 == rand(1, 100)))) { + $this->cleanTableReferences(); + } + + return [ + 't_count' => $tc, + 'delete_time' => 0, + 'index_update_time' => 0, + ]; + } + + private function deleteTargetGraphs() + { + $r = 0; + foreach ($this->infos['query']['target_graphs'] as $g) { + if ($g_id = $this->getTermID($g, 'g')) { + $r += $this->store->getDBObject()->exec('DELETE FROM g2t WHERE g = '.$g_id); + } + } + $this->refs_deleted = $r ? 1 : 0; + + return $r; + } + + private function deleteTriples() + { + $r = 0; + /* graph restriction */ + $tgs = $this->infos['query']['target_graphs']; + $gq = ''; + foreach ($tgs as $g) { + if ($g_id = $this->getTermID($g, 'g')) { + $gq .= $gq ? ', '.$g_id : $g_id; + } + } + $gq = $gq ? ' AND G.g IN ('.$gq.')' : ''; + /* triples */ + foreach ($this->infos['query']['construct_triples'] as $t) { + $q = ''; + $skip = 0; + foreach (['s', 'p', 'o'] as $term) { + if (isset($t[$term.'_type']) && preg_match('/(var)/', $t[$term.'_type'])) { + //$skip = 1; + } else { + $term_id = $this->getTermID($t[$term], $term); + $q .= ($q ? ' AND ' : '').'T.'.$term.'='.$term_id; + /* explicit lang/dt restricts the matching */ + if ('o' == $term) { + $o_lang = $t['o_lang'] ?? ''; + $o_lang_dt = $t['o_datatype'] ?? $o_lang; + if ($o_lang_dt) { + $q .= ($q ? ' AND ' : '').'T.o_lang_dt='.$this->getTermID($o_lang_dt, 'lang_dt'); + } + } + } + } + if ($skip) { + continue; + } + if ($gq) { + $sql = 'DELETE FROM g2t WHERE t IN ('; + $sql .= 'SELECT G.t FROM g2t G JOIN triple T ON T.t = G.t'.$gq.' WHERE '.$q; + $sql .= ')'; + } else {/* triples only */ + // it contains things like "T.s", but we can't use a table alias + // with SQLite when running DELETE queries. + $q = str_replace('T.', '', $q); + $sql = 'DELETE FROM triple WHERE '.$q; + } + $r += $this->store->getDBObject()->exec($sql); + if (!empty($this->store->getDBObject()->getErrorMessage())) { + // TODO deletable because never reachable? + throw new Exception($this->store->getDBObject()->getErrorMessage().' in '.$sql); + } + } + + return $r; + } + + private function deleteConstructedGraph() + { + $subLogger = $this->store->getLoggerPool()->createNewLogger('Construct'); + $h = new ConstructQueryHandler($this->store, $subLogger); + + $sub_r = $h->runQuery($this->infos); + $triples = $this->getTriplesFromIndex($sub_r); + $tgs = $this->infos['query']['target_graphs']; + + $this->infos = ['query' => ['construct_triples' => $triples, 'target_graphs' => $tgs]]; + + return $this->deleteTriples(); + } + + private function getTriplesFromIndex(array $index): array + { + $r = []; + foreach ($index as $s => $ps) { + foreach ($ps as $p => $os) { + foreach ($os as $o) { + $r[] = [ + 's' => $s, + 'p' => $p, + 'o' => $o['value'], + 's_type' => preg_match('/^\_\:/', $s) ? 'bnode' : 'uri', + 'o_type' => $o['type'], + 'o_datatype' => isset($o['datatype']) ? $o['datatype'] : '', + 'o_lang' => isset($o['lang']) ? $o['lang'] : '', + ]; + } + } + } + + return $r; + } + + private function cleanTableReferences() + { + /* check for unconnected triples */ + $sql = 'SELECT T.t + FROM triple T LEFT JOIN g2t G ON ( G.t = T.t ) + WHERE G.t IS NULL + LIMIT 1'; + + $numRows = $this->store->getDBObject()->getNumberOfRows($sql); + + if (0 < $numRows) { + /* delete unconnected triples */ + $sql = 'DELETE FROM triple WHERE t IN ('; + $sql .= ' SELECT T.t + FROM triple T + LEFT JOIN g2t G ON G.t = T.t + WHERE G.t IS NULL'; + $sql .= ')'; + $this->store->getDBObject()->simpleQuery($sql); + } + /* check for unconnected graph refs */ + if ((1 == rand(1, 10))) { + $sql = ' + SELECT G.g FROM g2t G LEFT JOIN triple T ON ( T.t = G.t ) + WHERE T.t IS NULL LIMIT 1 + '; + if (0 < $this->store->getDBObject()->getNumberOfRows($sql)) { + /* delete unconnected graph refs */ + $sql = 'DELETE G + FROM g2t G + LEFT JOIN triple T ON (T.t = G.t) + WHERE T.t IS NULL + '; + $this->store->getDBObject()->simpleQuery($sql); + } + } + } +} diff --git a/src/Store/QueryHandler/DescribeQueryHandler.php b/src/Store/QueryHandler/DescribeQueryHandler.php new file mode 100644 index 0000000..4fa2b89 --- /dev/null +++ b/src/Store/QueryHandler/DescribeQueryHandler.php @@ -0,0 +1,93 @@ + + * (c) Benjamin Nowack + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace sweetrdf\InMemoryStoreSqlite\Store\QueryHandler; + +class DescribeQueryHandler extends SelectQueryHandler +{ + public function runQuery($infos) + { + $ids = $infos['query']['result_uris']; + if ($vars = $infos['query']['result_vars']) { + $sub_r = parent::runQuery($infos); + $rf = $infos['result_format'] ?? ''; + if (\in_array($rf, ['sql', 'structure', 'index'])) { + return $sub_r; + } + $rows = $sub_r['rows'] ?? []; + foreach ($rows as $row) { + foreach ($vars as $info) { + $val = isset($row[$info['var']]) ? $row[$info['var']] : ''; + if ( + $val + && ('literal' != $row[$info['var'].' type']) && !\in_array($val, $ids) + ) { + $ids[] = $val; + } + } + } + } + $this->r = []; + $this->described_ids = []; + $this->ids = $ids; + $this->added_triples = []; + $is_sub_describe = 0; + while ($this->ids) { + $id = $this->ids[0]; + $this->described_ids[] = $id; + $q = 'CONSTRUCT { <'.$id.'> ?p ?o . } WHERE {<'.$id.'> ?p ?o .}'; + $sub_r = $this->store->query($q); + $sub_index = \is_array($sub_r['result']) ? $sub_r['result'] : []; + $this->mergeSubResults($sub_index, $is_sub_describe); + $is_sub_describe = 1; + } + + return $this->r; + } + + public function mergeSubResults($index, $is_sub_describe = 1) + { + foreach ($index as $s => $ps) { + if (!isset($this->r[$s])) { + $this->r[$s] = []; + } + foreach ($ps as $p => $os) { + if (!isset($this->r[$s][$p])) { + $this->r[$s][$p] = []; + } + foreach ($os as $o) { + $id = md5($s.' '.$p.' '.serialize($o)); + if (!isset($this->added_triples[$id])) { + if (1 || !$is_sub_describe) { + $this->r[$s][$p][] = $o; + if (\is_array($o) && ('bnode' == $o['type']) && !\in_array($o['value'], $this->ids)) { + $this->ids[] = $o['value']; + } + } elseif (!\is_array($o) || ('bnode' != $o['type'])) { + $this->r[$s][$p][] = $o; + } + $this->added_triples[$id] = 1; + } + } + } + } + /* adjust ids */ + $ids = $this->ids; + $this->ids = []; + foreach ($ids as $id) { + if (!\in_array($id, $this->described_ids)) { + $this->ids[] = $id; + } + } + } +} diff --git a/src/Store/QueryHandler/InsertQueryHandler.php b/src/Store/QueryHandler/InsertQueryHandler.php new file mode 100644 index 0000000..7fef7d4 --- /dev/null +++ b/src/Store/QueryHandler/InsertQueryHandler.php @@ -0,0 +1,358 @@ + + * (c) Benjamin Nowak + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace sweetrdf\InMemoryStoreSqlite\Store\QueryHandler; + +use sweetrdf\InMemoryStoreSqlite\KeyValueBag; + +class InsertQueryHandler extends QueryHandler +{ + /** + * If true, store assumes one or more insert into SPARQL queries and will + * skip certain DB operations to speed up insertion process. + */ + private bool $bulkLoadModeIsActive = false; + + /** + * Is used if $bulkLoadModeIsActive is true. Determines next term ID for + * entries in id2val, s2val and o2val. + */ + private int $bulkLoadModeNextTermId = 1; + + /** + * When set it is used to store term information to speed up insert into operations. + */ + private KeyValueBag $rowCache; + + /** + * Is being used for blank nodes to generate a hash which is not only dependent on + * blank node ID and graph, but also on a random value. + * Otherwise blank nodes inserted in different "insert-sessions" will have the same reference. + */ + private ?string $sessionId = null; + + public function activateBulkLoadMode(int $bulkLoadModeNextTermId): void + { + $this->bulkLoadModeIsActive = true; + $this->bulkLoadModeNextTermId = $bulkLoadModeNextTermId; + } + + public function getBulkLoadModeNextTermId(): int + { + return $this->bulkLoadModeNextTermId; + } + + public function setRowCache(KeyValueBag $rowCache): void + { + $this->rowCache = $rowCache; + } + + public function runQuery(array $infos) + { + $this->sessionId = bin2hex(random_bytes(4)); + $this->store->getDBObject()->getPDO()->beginTransaction(); + + foreach ($infos['query']['construct_triples'] as $triple) { + $this->addTripleToGraph($triple, $infos['query']['target_graph']); + } + + $this->store->getDBObject()->getPDO()->commit(); + + $this->sessionId = null; + } + + /** + * @todo cache once loaded triples/quads + */ + private function addTripleToGraph(array $triple, string $graph): void + { + /* + * information: + * + * + val_hash: hashed version of given value + * + val_type: type of the term; one of: bnode, uri, literal + */ + + $triple = $this->prepareTriple($triple, $graph); + + /* + * graph + */ + $graphId = $this->getIdOfExistingTerm($graph, 'id'); + if (null == $graphId) { + $graphId = $this->store->getDBObject()->insert('id2val', [ + 'id' => $this->getMaxTermId(), + 'val' => $graph, + 'val_type' => 0, // = uri + ]); + } + + /* + * s2val + */ + $subjectId = $this->getIdOfExistingTerm($triple['s'], 'subject'); + if (null == $subjectId) { + $subjectId = $this->getMaxTermId(); + $this->store->getDBObject()->insert('s2val', [ + 'id' => $subjectId, + 'val' => $triple['s'], + 'val_hash' => $this->getValueHash($triple['s']), + ]); + } + + /* + * predicate + */ + $predicateId = $this->getIdOfExistingTerm($triple['p'], 'id'); + if (null == $predicateId) { + $predicateId = $this->getMaxTermId(); + $this->store->getDBObject()->insert('id2val', [ + 'id' => $predicateId, + 'val' => $triple['p'], + 'val_type' => 0, // = uri + ]); + } + + /* + * o2val + */ + $objectId = $this->getIdOfExistingTerm($triple['o'], 'object'); + if (null == $objectId) { + $objectId = $this->getMaxTermId(); + $this->store->getDBObject()->insert('o2val', [ + 'id' => $objectId, + 'val' => $triple['o'], + 'val_hash' => $this->getValueHash($triple['o']), + ]); + } + + /* + * o_lang_dt + */ + // notice: only one of these two is set + $oLangDt = $triple['o_datatype'].$triple['o_lang']; + $oLangDtId = $this->getIdOfExistingTerm($oLangDt, 'id'); + if (null == $oLangDtId) { + $oLangDtId = $this->getMaxTermId(); + $this->store->getDBObject()->insert('id2val', [ + 'id' => $oLangDtId, + 'val' => $oLangDt, + 'val_type' => !empty($triple['o_datatype']) ? 0 : 2, + ]); + } + + /* + * triple + */ + $sql = 'SELECT * FROM triple WHERE s = ? AND p = ? AND o = ?'; + $check = $this->store->getDBObject()->fetchRow($sql, [$subjectId, $predicateId, $objectId]); + if (false === $check) { + $tripleId = $this->store->getDBObject()->insert('triple', [ + 's' => $subjectId, + 's_type' => $triple['s_type_int'], + 'p' => $predicateId, + 'o' => $objectId, + 'o_type' => $triple['o_type_int'], + 'o_lang_dt' => $oLangDtId, + 'o_comp' => $this->getOComp($triple['o']), + ]); + } else { + $tripleId = $check['t']; + } + + /* + * triple to graph + */ + $sql = 'SELECT * FROM g2t WHERE g = ? AND t = ?'; + $check = $this->store->getDBObject()->fetchRow($sql, [$graphId, $tripleId]); + if (false == $check) { + $this->store->getDBObject()->insert('g2t', [ + 'g' => $graphId, + 't' => $tripleId, + ]); + } + } + + private function prepareTriple(array $triple, string $graph): array + { + /* + * subject: set type int + */ + $triple['s_type_int'] = 0; // uri + if ('bnode' == $triple['s_type']) { + $triple['s_type_int'] = 1; + } elseif ('literal' == $triple['s_type']) { + $triple['s_type_int'] = 2; + } + + /* + * subject is a blank node + */ + if ('bnode' == $triple['s_type']) { + // transforms _:foo to _:b671320391_foo + $s = $triple['s']; + // TODO make bnode ID only unique for this session, not in general + $triple['s'] = '_:b'.$this->getValueHash($this->sessionId.$graph.$s).'_'; + $triple['s'] .= substr($s, 2); + } + + /* + * object: set type int + */ + $triple['o_type_int'] = 0; // uri + if ('bnode' == $triple['o_type']) { + $triple['o_type_int'] = 1; + } elseif ('literal' == $triple['o_type']) { + $triple['o_type_int'] = 2; + } + + /* + * object is a blank node + */ + if ('bnode' == $triple['o_type']) { + // transforms _:foo to _:b671320391_foo + $o = $triple['o']; + // TODO make bnode ID only unique for this session, not in general + $triple['o'] = '_:b'.$this->getValueHash($this->sessionId.$graph.$o).'_'; + $triple['o'] .= substr($o, 2); + } + + return $triple; + } + + /** + * Get normalized value for ORDER BY operations. + */ + private function getOComp($val): string + { + /* try date (e.g. 21 August 2007) */ + if ( + preg_match('/^[0-9]{1,2}\s+[a-z]+\s+[0-9]{4}/i', $val) + && ($uts = strtotime($val)) + && (-1 !== $uts) + ) { + return date("Y-m-d\TH:i:s", $uts); + } + + /* xsd date (e.g. 2009-05-28T18:03:38+09:00 2009-05-28T18:03:38GMT) */ + if (true === (bool) strtotime($val)) { + return date('Y-m-d\TH:i:s\Z', strtotime($val)); + } + + if (is_numeric($val)) { + $val = sprintf('%f', $val); + if (preg_match("/([\-\+])([0-9]*)\.([0-9]*)/", $val, $m)) { + return $m[1].sprintf('%018s', $m[2]).'.'.sprintf('%-015s', $m[3]); + } + if (preg_match("/([0-9]*)\.([0-9]*)/", $val, $m)) { + return '+'.sprintf('%018s', $m[1]).'.'.sprintf('%-015s', $m[2]); + } + + return $val; + } + + /* any other string: remove tags, linebreaks etc., but keep MB-chars */ + // [\PL\s]+ ( = non-Letters) kills digits + $re = '/[\PL\s]+/isu'; + $re = '/[\s\'\"\´\`]+/is'; + $val = trim(preg_replace($re, '-', strip_tags($val))); + if (\strlen($val) > 35) { + $fnc = \function_exists('mb_substr') ? 'mb_substr' : 'substr'; + $val = $fnc($val, 0, 17).'-'.$fnc($val, -17); + } + + return $val; + } + + /** + * Generates the next valid ID based on latest values in id2val, s2val and o2val. + * + * @return int returns 1 or higher + */ + private function getMaxTermId(): int + { + if (true === $this->bulkLoadModeIsActive) { + return $this->bulkLoadModeNextTermId++; + } else { + $sql = ''; + foreach (['id2val', 's2val', 'o2val'] as $table) { + $sql .= !empty($sql) ? ' UNION ' : ''; + $sql .= 'SELECT MAX(id) as id FROM '.$table; + } + $result = 0; + + $rows = $this->store->getDBObject()->fetchList($sql); + + if (\is_array($rows)) { + foreach ($rows as $row) { + $result = ($result < $row['id']) ? $row['id'] : $result; + } + } + + return $result + 1; + } + } + + /** + * @param string $type One of: bnode, uri, literal + * @param string $quadPart One of: id, subject, object + * + * @return int 1 (or higher), if available, or null + */ + private function getIdOfExistingTerm(string $value, string $quadPart): ?int + { + // id (predicate or graph) + if ('id' == $quadPart) { + $sql = 'SELECT id, val FROM id2val WHERE val = ?'; + + $hashKey = md5($sql.json_encode([$value])); + if (false === $this->rowCache->has($hashKey)) { + $row = $this->store->getDBObject()->fetchRow($sql, [$value]); + if (\is_array($row)) { + $this->rowCache->set($hashKey, $row); + } + } + + $entry = $this->rowCache->get($hashKey); + + // entry found, use its ID + if (\is_array($entry)) { + return $entry['id']; + } else { + return null; + } + } else { + // subject or object + $table = 'subject' == $quadPart ? 's2val' : 'o2val'; + $sql = 'SELECT id, val FROM '.$table.' WHERE val_hash = ?'; + $params = [$this->getValueHash($value)]; + + $hashKey = md5($sql.json_encode($params)); + if (false === $this->rowCache->has($hashKey)) { + $row = $this->store->getDBObject()->fetchRow($sql, $params); + if (\is_array($row)) { + $this->rowCache->set($hashKey, $row); + } + } + + $entry = $this->rowCache->get($hashKey); + + // entry found, use its ID + if (isset($entry['val']) && $entry['val'] == $value) { + return $entry['id']; + } else { + return null; + } + } + } +} diff --git a/src/Store/QueryHandler/LoadQueryHandler.php b/src/Store/QueryHandler/LoadQueryHandler.php new file mode 100644 index 0000000..a599715 --- /dev/null +++ b/src/Store/QueryHandler/LoadQueryHandler.php @@ -0,0 +1,367 @@ + + * (c) Benjamin Nowack + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace sweetrdf\InMemoryStoreSqlite\Store\QueryHandler; + +use function sweetrdf\InMemoryStoreSqlite\calcURI; +use sweetrdf\InMemoryStoreSqlite\Store\TurtleLoader; + +class LoadQueryHandler extends QueryHandler +{ + private string $target_graph; + + /** + * @todo required? + */ + private int $t_count; + + private int $write_buffer_size = 2500; + + public function runQuery($infos, $data = '', $keep_bnode_ids = 0) + { + $url = $infos['query']['url']; + $graph = $infos['query']['target_graph']; + $this->target_graph = $graph ? calcURI($graph) : calcURI($url); + $this->keep_bnode_ids = $keep_bnode_ids; + + // remove parameters + $parserLogger = $this->store->getLoggerPool()->createNewLogger('Turtle'); + $loader = new TurtleLoader($parserLogger); + $loader->setCaller($this); + + /* logging */ + $this->t_count = 0; + $this->t_start = 0; + /* load and parse */ + $this->max_term_id = $this->getMaxTermID(); + $this->max_triple_id = $this->getMaxTripleID(); + + $this->term_ids = []; + $this->triple_ids = []; + $this->sql_buffers = []; + $loader->parse($url, $data); + + /* done */ + $this->checkSQLBuffers(1); + + return [ + 't_count' => $this->t_count, + 'load_time' => 0, + ]; + } + + public function addT($s, $p, $o, $s_type, $o_type, $o_dt = '', $o_lang = '') + { + $type_ids = ['uri' => '0', 'bnode' => '1', 'literal' => '2']; + $g = $this->getStoredTermID($this->target_graph, '0', 'id'); + $s = (('bnode' == $s_type) && !$this->keep_bnode_ids) ? '_:b'.abs(crc32($g.$s)).'_'.(\strlen($s) > 12 ? substr(substr($s, 2), -10) : substr($s, 2)) : $s; + $o = (('bnode' == $o_type) && !$this->keep_bnode_ids) ? '_:b'.abs(crc32($g.$o)).'_'.(\strlen($o) > 12 ? substr(substr($o, 2), -10) : substr($o, 2)) : $o; + /* triple */ + $t = [ + 's' => $this->getStoredTermID($s, $type_ids[$s_type], 's'), + 'p' => $this->getStoredTermID($p, '0', 'id'), + 'o' => $this->getStoredTermID($o, $type_ids[$o_type], 'o'), + 'o_lang_dt' => $this->getStoredTermID($o_dt.$o_lang, $o_dt ? '0' : '2', 'id'), + 'o_comp' => $this->getOComp($o), + 's_type' => $type_ids[$s_type], + 'o_type' => $type_ids[$o_type], + ]; + $t['t'] = $this->getTripleID($t); + if (\is_array($t['t'])) {/* t exists already */ + $t['t'] = $t['t'][0]; + } else { + $this->bufferTripleSQL($t); + } + /* g2t */ + $g2t = ['g' => $g, 't' => $t['t']]; + $this->bufferGraphSQL($g2t); + ++$this->t_count; + /* check buffers */ + if (0 == ($this->t_count % $this->write_buffer_size)) { + $force_write = 1; + $reset_buffers = (0 == ($this->t_count % ($this->write_buffer_size * 2))); + $refresh_lock = (0 == ($this->t_count % 25000)); + $split_tables = (0 == ($this->t_count % ($this->write_buffer_size * 10))); + $this->checkSQLBuffers($force_write, $reset_buffers, $refresh_lock, $split_tables); + } + } + + public function getMaxTermID(): int + { + $sql = ''; + foreach (['id2val', 's2val', 'o2val'] as $tbl) { + $sql .= $sql ? ' UNION ' : ''; + $sql .= 'SELECT MAX(id) as id FROM '.$tbl; + } + $r = 0; + + $rows = $this->store->getDBObject()->fetchList($sql); + + if (\is_array($rows)) { + foreach ($rows as $row) { + $r = ($r < $row['id']) ? $row['id'] : $r; + } + } + + return $r + 1; + } + + /** + * @todo change DB schema and avoid using this function because it does not protect against race conditions + * + * @return int + */ + public function getMaxTripleID() + { + $sql = 'SELECT MAX(t) AS `id` FROM triple'; + + $row = $this->store->getDBObject()->fetchRow($sql); + if (isset($row['id'])) { + return $row['id'] + 1; + } + + return 1; + } + + public function getStoredTermID($val, $type_id, $tbl) + { + /* buffered */ + if (isset($this->term_ids[$val])) { + if (!isset($this->term_ids[$val][$tbl])) { + foreach (['id', 's', 'o'] as $other_tbl) { + if (isset($this->term_ids[$val][$other_tbl])) { + $this->term_ids[$val][$tbl] = $this->term_ids[$val][$other_tbl]; + $this->bufferIDSQL($tbl, $this->term_ids[$val][$tbl], $val, $type_id); + break; + } + } + } + + return $this->term_ids[$val][$tbl]; + } + /* db */ + $sub_tbls = ('id' == $tbl) + ? ['id2val', 's2val', 'o2val'] + : ('s' == $tbl + ? ['s2val', 'id2val', 'o2val'] + : ['o2val', 'id2val', 's2val'] + ); + + foreach ($sub_tbls as $sub_tbl) { + $id = 0; + /* via hash */ + if (preg_match('/^(s2val|o2val)$/', $sub_tbl)) { + $sql = 'SELECT id, val + FROM '.$sub_tbl.' + WHERE val_hash = "'.$this->getValueHash($val).'"'; + + $rows = $this->store->getDBObject()->fetchList($sql); + if (\is_array($rows)) { + foreach ($rows as $row) { + if ($row['val'] == $val) { + $id = $row['id']; + break; + } + } + } + } else { + $binaryValue = $this->store->getDBObject()->escape($val); + if (false !== empty($binaryValue)) { + $sql = 'SELECT id FROM '.$sub_tbl." WHERE val = '".$binaryValue."'"; + + $row = $this->store->getDBObject()->fetchRow($sql); + if (\is_array($row) && isset($row['id'])) { + $id = $row['id']; + } + } + } + if (0 < $id) { + $this->term_ids[$val] = [$tbl => $id]; + if ($sub_tbl != $tbl.'2val') { + $this->bufferIDSQL($tbl, $id, $val, $type_id); + } + break; + } + } + /* new */ + if (!isset($this->term_ids[$val])) { + $this->term_ids[$val] = [$tbl => $this->max_term_id]; + $this->bufferIDSQL($tbl, $this->max_term_id, $val, $type_id); + ++$this->max_term_id; + } + + return $this->term_ids[$val][$tbl]; + } + + public function getTripleID($t) + { + $val = serialize($t); + /* buffered */ + if (isset($this->triple_ids[$val])) { + /* hack for "don't insert this triple" */ + return [$this->triple_ids[$val]]; + } + /* db */ + $sql = 'SELECT t + FROM triple + WHERE s = '.$t['s'].' + AND p = '.$t['p'].' + AND o = '.$t['o'].' + AND o_lang_dt = '.$t['o_lang_dt'].' + AND s_type = '.$t['s_type'].' + AND o_type = '.$t['o_type'].' + LIMIT 1'; + $row = $this->store->getDBObject()->fetchRow($sql); + if (isset($row['t'])) { + /* hack for "don't insert this triple" */ + $this->triple_ids[$val] = $row['t']; + + return [$row['t']]; + } else { + /* new */ + $this->triple_ids[$val] = $this->max_triple_id; + ++$this->max_triple_id; + + return $this->triple_ids[$val]; + } + } + + public function getOComp($val) + { + /* try date (e.g. 21 August 2007) */ + if ( + preg_match('/^[0-9]{1,2}\s+[a-z]+\s+[0-9]{4}/i', $val) + && ($uts = strtotime($val)) + && (-1 !== $uts) + ) { + return date("Y-m-d\TH:i:s", $uts); + } + + /* xsd date (e.g. 2009-05-28T18:03:38+09:00 2009-05-28T18:03:38GMT) */ + if (true === (bool) strtotime($val)) { + return date('Y-m-d\TH:i:s\Z', strtotime($val)); + } + + if (is_numeric($val)) { + $val = sprintf('%f', $val); + if (preg_match("/([\-\+])([0-9]*)\.([0-9]*)/", $val, $m)) { + return $m[1].sprintf('%018s', $m[2]).'.'.sprintf('%-015s', $m[3]); + } + if (preg_match("/([0-9]*)\.([0-9]*)/", $val, $m)) { + return '+'.sprintf('%018s', $m[1]).'.'.sprintf('%-015s', $m[2]); + } + + return $val; + } + + /* any other string: remove tags, linebreaks etc., but keep MB-chars */ + // [\PL\s]+ ( = non-Letters) kills digits + $re = '/[\PL\s]+/isu'; + $re = '/[\s\'\"\´\`]+/is'; + $val = trim(preg_replace($re, '-', strip_tags($val))); + if (\strlen($val) > 35) { + $fnc = \function_exists('mb_substr') ? 'mb_substr' : 'substr'; + $val = $fnc($val, 0, 17).'-'.$fnc($val, -17); + } + + return $val; + } + + public function bufferTripleSQL($t) + { + $tbl = 'triple'; + $sql = ', '; + + $sqlHead = 'INSERT OR IGNORE INTO '; + + if (!isset($this->sql_buffers[$tbl])) { + $this->sql_buffers[$tbl] = $sqlHead; + $this->sql_buffers[$tbl] .= $tbl; + $this->sql_buffers[$tbl] .= ' (t, s, p, o, o_lang_dt, o_comp, s_type, o_type) VALUES'; + $sql = ' '; + } + + $oCompEscaped = $this->store->getDBObject()->escape($t['o_comp']); + + $this->sql_buffers[$tbl] .= $sql.'('.$t['t'].', '.$t['s'].', '.$t['p'].', '; + $this->sql_buffers[$tbl] .= $t['o'].', '.$t['o_lang_dt'].", '"; + $this->sql_buffers[$tbl] .= $oCompEscaped."', ".$t['s_type'].', '.$t['o_type'].')'; + } + + public function bufferGraphSQL($g2t) + { + $tbl = 'g2t'; + $sql = ', '; + + /* + * Use appropriate INSERT syntax, depending on the DBS. + */ + $sqlHead = 'INSERT OR IGNORE INTO '; + + if (!isset($this->sql_buffers[$tbl])) { + $this->sql_buffers[$tbl] = $sqlHead.$tbl.' (g, t) VALUES'; + $sql = ' '; + } + $this->sql_buffers[$tbl] .= $sql.'('.$g2t['g'].', '.$g2t['t'].')'; + } + + public function bufferIDSQL($tbl, $id, $val, $val_type) + { + $tbl = $tbl.'2val'; + if ('id2val' == $tbl) { + $cols = 'id, val, val_type'; + $vals = '('.$id.", '".$this->store->getDBObject()->escape($val)."', ".$val_type.')'; + } elseif (preg_match('/^(s2val|o2val)$/', $tbl)) { + $cols = 'id, val_hash, val'; + $vals = '('.$id.", '" + .$this->getValueHash($val) + ."', '" + .$this->store->getDBObject()->escape($val) + ."')"; + } else { + $cols = 'id, val'; + $vals = '('.$id.", '".$this->store->getDBObject()->escape($val)."')"; + } + if (!isset($this->sql_buffers[$tbl])) { + $this->sql_buffers[$tbl] = ''; + $sqlHead = 'INSERT OR IGNORE INTO '; + + $sql = $sqlHead.$tbl.'('.$cols.') VALUES '; + } else { + $sql = ', '; + } + $sql .= $vals; + $this->sql_buffers[$tbl] .= $sql; + } + + public function checkSQLBuffers($force_write = 0, $reset_id_buffers = 0) + { + foreach (['triple', 'g2t', 'id2val', 's2val', 'o2val'] as $tbl) { + $buffer_size = isset($this->sql_buffers[$tbl]) ? 1 : 0; + if ($buffer_size && $force_write) { + $this->store->getDBObject()->simpleQuery($this->sql_buffers[$tbl]); + /* table error */ + $error = $this->store->getDBObject()->getErrorMessage(); + unset($this->sql_buffers[$tbl]); + + /* reset term id buffers */ + if ($reset_id_buffers) { + $this->term_ids = []; + $this->triple_ids = []; + } + } + } + + return 1; + } +} diff --git a/src/Store/QueryHandler/QueryHandler.php b/src/Store/QueryHandler/QueryHandler.php new file mode 100755 index 0000000..d5272b0 --- /dev/null +++ b/src/Store/QueryHandler/QueryHandler.php @@ -0,0 +1,89 @@ + + * (c) Benjamin Nowack + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace sweetrdf\InMemoryStoreSqlite\Store\QueryHandler; + +use sweetrdf\InMemoryStoreSqlite\Log\Logger; +use sweetrdf\InMemoryStoreSqlite\NamespaceHelper; +use sweetrdf\InMemoryStoreSqlite\Store\InMemoryStoreSqlite; + +abstract class QueryHandler +{ + protected Logger $logger; + + protected InMemoryStoreSqlite $store; + + protected array $term_id_cache; + + protected string $xsd = NamespaceHelper::NAMESPACE_XSD; + + public function __construct(InMemoryStoreSqlite $store, Logger $logger) + { + $this->logger = $logger; + $this->store = $store; + } + + public function getTermID($val, $term = '') + { + /* mem cache */ + if (!isset($this->term_id_cache) || (\count(array_keys($this->term_id_cache)) > 100)) { + $this->term_id_cache = []; + } + if (!isset($this->term_id_cache[$term])) { + $this->term_id_cache[$term] = []; + } + + $tbl = preg_match('/^(s|o)$/', $term) ? $term.'2val' : 'id2val'; + /* cached? */ + if ((\strlen($val) < 100) && isset($this->term_id_cache[$term][$val])) { + return $this->term_id_cache[$term][$val]; + } + + $r = 0; + /* via hash */ + if (preg_match('/^(s2val|o2val)$/', $tbl)) { + $rows = $this->store->getDBObject()->fetchList( + 'SELECT id, val FROM '.$tbl.' WHERE val_hash = ? ORDER BY id', + [$this->getValueHash($val)] + ); + if (\is_array($rows) && 0 < \count($rows)) { + foreach ($rows as $row) { + if ($row['val'] == $val) { + $r = $row['id']; + break; + } + } + } + } + + /* exact match */ + else { + $sql = 'SELECT id FROM '.$tbl.' WHERE val = ? LIMIT 1'; + $row = $this->store->getDBObject()->fetchRow($sql, [$val]); + + if (null !== $row && isset($row['id'])) { + $r = $row['id']; + } + } + if ($r && (\strlen($val) < 100)) { + $this->term_id_cache[$term][$val] = $r; + } + + return $r; + } + + public function getValueHash(int | float | string $val): int | float + { + return abs(crc32($val)); + } +} diff --git a/src/Store/QueryHandler/SelectQueryHandler.php b/src/Store/QueryHandler/SelectQueryHandler.php new file mode 100644 index 0000000..e59ede7 --- /dev/null +++ b/src/Store/QueryHandler/SelectQueryHandler.php @@ -0,0 +1,1971 @@ + + * (c) Benjamin Nowack + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace sweetrdf\InMemoryStoreSqlite\Store\QueryHandler; + +use Exception; +use sweetrdf\InMemoryStoreSqlite\PDOSQLiteAdapter; + +class SelectQueryHandler extends QueryHandler +{ + public function runQuery($infos) + { + $this->infos = $infos; + $this->infos['null_vars'] = []; + $this->indexes = []; + $this->pattern_order_offset = 0; + $q_sql = $this->getSQL(); + + /* create intermediate results (ID-based) */ + $tmp_tbl = $this->createTempTable($q_sql); + + /* join values */ + $r = $this->getFinalQueryResult($q_sql, $tmp_tbl); + + /* remove intermediate results */ + $this->store->getDBObject()->simpleQuery('DROP TABLE IF EXISTS '.$tmp_tbl); + + return $r; + } + + public function getSQL() + { + $r = ''; + $nl = "\n"; + $this->buildInitialIndexes(); + foreach ($this->indexes as $i => $index) { + $this->index = array_merge($this->getEmptyIndex(), $index); + $this->analyzeIndex($this->getPattern('0')); + $sub_r = $this->getQuerySQL(); + $r .= $r ? $nl.'UNION'.$this->getDistinctSQL().$nl : ''; + + $setBracket = $this->is_union_query && !$this->store->getDBObject() instanceof PDOSQLiteAdapter; + $r .= $setBracket ? '('.$sub_r.')' : $sub_r; + + $this->indexes[$i] = $this->index; + } + $r .= $this->is_union_query ? $this->getLIMITSQL() : ''; + $orderInfos = $this->infos['query']['order_infos'] ?? 0; + if ($orderInfos) { + $r = preg_replace('/SELECT(\s+DISTINCT)?\s*/', 'SELECT\\1 NULL AS `TMPPOS`, ', $r); + } + $pd_count = $this->problematicDependencies(); + if ($pd_count) { + /* re-arranging the patterns sometimes reduces the LEFT JOIN dependencies */ + $set_sql = 0; + if (!$this->pattern_order_offset) { + $set_sql = 1; + } + if (!$set_sql && ($pd_count < $this->opt_sql_pd_count)) { + $set_sql = 1; + } + if (!$set_sql && ($pd_count == $this->opt_sql_pd_count) && (\strlen($r) < \strlen($this->opt_sql))) { + $set_sql = 1; + } + if ($set_sql) { + $this->opt_sql = $r; + $this->opt_sql_pd_count = $pd_count; + } + ++$this->pattern_order_offset; + if ($this->pattern_order_offset > 5) { + return $this->opt_sql; + } + + return $this->getSQL(); + } + + return $r; + } + + public function buildInitialIndexes() + { + $this->dependency_log = []; + $this->index = $this->getEmptyIndex(); + // if no pattern is in the query, the index "pattern" is undefined, which leads to an error. + // TODO throw an exception/raise an error and avoid "Undefined index: pattern" notification + $this->buildIndex($this->infos['query']['pattern'], 0); + $tmp = $this->index; + $this->analyzeIndex($this->getPattern('0')); + $this->initial_index = $this->index; + $this->index = $tmp; + $this->is_union_query = $this->index['union_branches'] ? 1 : 0; + $this->indexes = $this->is_union_query ? $this->getUnionIndexes($this->index) : [$this->index]; + } + + private function createTempTable($q_sql) + { + $tbl = 'Q'.md5($q_sql.time().uniqid(rand())); + if (\strlen($tbl) > 64) { + $tbl = 'Q'.md5($tbl); + } + + $tmp_sql = 'CREATE TABLE '.$tbl.' ( '.$this->getTempTableDefForSQLite($q_sql).')'; + $tmpSql2 = str_replace('CREATE TEMPORARY', 'CREATE', $tmp_sql); + + if ( + !$this->store->getDBObject()->simpleQuery($tmp_sql) + && !$this->store->getDBObject()->simpleQuery($tmpSql2) + && !empty($this->store->getDBObject()->getErrorMessage()) + ) { + return $this->logger->error( + $this->store->getDBObject()->getErrorMessage() + ); + } + if (false === $this->store->getDBObject()->exec('INSERT INTO '.$tbl.' '."\n".$q_sql)) { + $this->logger->error($this->store->getDBObject()->getErrorMessage()); + } + + return $tbl; + } + + private function getEmptyIndex() + { + return [ + 'from' => [], + 'join' => [], + 'left_join' => [], + 'vars' => [], 'graph_vars' => [], 'graph_uris' => [], + 'bnodes' => [], + 'triple_patterns' => [], + 'sub_joins' => [], + 'constraints' => [], + 'union_branches' => [], + 'patterns' => [], + 'havings' => [], + ]; + } + + public function getTempTableDefForSQLite($q_sql) + { + $col_part = preg_replace('/^SELECT\s*(DISTINCT)?(.*)FROM.*$/s', '\\2', $q_sql); + $parts = explode(',', $col_part); + $has_order_infos = $this->infos['query']['order_infos'] ?? 0; + $r = ''; + $added = []; + foreach ($parts as $part) { + if (preg_match('/\.?(.+)\s+AS\s+`(.+)`/U', trim($part), $m) && !isset($added[$m[2]])) { + $alias = $m[2]; + if ('TMPPOS' == $alias) { + continue; + } + $r .= $r ? ',' : ''; + $r .= "\n `".$alias.'` INTEGER UNSIGNED'; + $added[$alias] = 1; + } + } + if ($has_order_infos) { + $r = "\n".'`TMPPOS` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, '.$r; + } + + return $r ? $r."\n" : ''; + } + + public function getFinalQueryResult($q_sql, $tmp_tbl) + { + /* var names */ + $vars = []; + $aggregate_vars = []; + foreach ($this->infos['query']['result_vars'] as $entry) { + if ($entry['aggregate']) { + $vars[] = $entry['alias']; + $aggregate_vars[] = $entry['alias']; + } else { + $vars[] = $entry['var']; + } + } + /* result */ + $r = ['variables' => $vars]; + $v_sql = $this->getValueSQL($tmp_tbl, $q_sql); + + $entries = []; + try { + $entries = $this->store->getDBObject()->fetchList($v_sql); + } catch (\Exception $e) { + $this->logger->error($e->getMessage()); + } + + $rows = []; + $types = [0 => 'uri', 1 => 'bnode', 2 => 'literal']; + if (0 < \count($entries)) { + foreach ($entries as $pre_row) { + $row = []; + foreach ($vars as $var) { + if (isset($pre_row[$var])) { + $row[$var] = $pre_row[$var]; + $row[$var.' type'] = isset($pre_row[$var.' type']) + ? $types[$pre_row[$var.' type']] + : ( + \in_array($var, $aggregate_vars) + ? 'literal' + : 'uri' + ); + if ( + isset($pre_row[$var.' lang_dt']) + && ($lang_dt = $pre_row[$var.' lang_dt']) + ) { + if (preg_match('/^([a-z]+(\-[a-z0-9]+)*)$/i', $lang_dt)) { + $row[$var.' lang'] = $lang_dt; + } else { + $row[$var.' datatype'] = $lang_dt; + } + } + } + } + if ($row || !$vars) { + $rows[] = $row; + } + } + } + $r['rows'] = $rows; + + return $r; + } + + public function buildIndex($pattern, $id) + { + $pattern['id'] = $id; + $type = $pattern['type'] ?? ''; + $constraint = $pattern['constraint'] ?? 0; + if ('filter' == $type && $constraint) { + $sub_pattern = $pattern['constraint']; + $sub_pattern['parent_id'] = $id; + $sub_id = $id.'_0'; + $this->buildIndex($sub_pattern, $sub_id); + $pattern['constraint'] = $sub_id; + } else { + $sub_patterns = $pattern['patterns'] ?? []; + $keys = array_keys($sub_patterns); + $spc = \count($sub_patterns); + if ($spc > 4 && $this->pattern_order_offset) { + $keys = []; + for ($i = 0; $i < $spc; ++$i) { + $keys[$i] = $i + $this->pattern_order_offset; + while ($keys[$i] >= $spc) { + $keys[$i] -= $spc; + } + } + } + foreach ($keys as $i => $key) { + $sub_pattern = $sub_patterns[$key]; + $sub_pattern['parent_id'] = $id; + $sub_id = $id.'_'.$key; + $this->buildIndex($sub_pattern, $sub_id); + $pattern['patterns'][$i] = $sub_id; + if ('union' == $type) { + $this->index['union_branches'][] = $sub_id; + } + } + } + $this->index['patterns'][$id] = $pattern; + } + + public function analyzeIndex($pattern) + { + $type = $pattern['type'] ?? ''; + if (!$type) { + return false; + } + $type = $pattern['type']; + $id = $pattern['id']; + /* triple */ + if ('triple' == $type) { + foreach (['s', 'p', 'o'] as $term) { + if ('var' == $pattern[$term.'_type']) { + $val = $pattern[$term]; + $this->index['vars'][$val] = array_merge( + $this->index['vars'][$val] ?? [], + [['table' => $pattern['id'], 'col' => $term]] + ); + } + if ('bnode' == $pattern[$term.'_type']) { + $val = $pattern[$term]; + $this->index['bnodes'][$val] = array_merge( + $this->index['bnodes'][$val] ?? [], + [['table' => $pattern['id'], 'col' => $term]] + ); + } + } + $this->index['triple_patterns'][] = $pattern['id']; + /* joins */ + if ($this->isOptionalPattern($id)) { + $this->index['left_join'][] = $id; + } elseif (!$this->index['from']) { + $this->index['from'][] = $id; + } elseif (!$this->getJoinInfos($id)) { + $this->index['from'][] = $id; + } else { + $this->index['join'][] = $id; + } + /* graph infos, graph vars */ + $this->index['patterns'][$id]['graph_infos'] = $this->getGraphInfos($id); + foreach ($this->index['patterns'][$id]['graph_infos'] as $info) { + if ('graph' == $info['type']) { + if ($info['var']) { + $val = $info['var']['value']; + $this->index['graph_vars'][$val] = array_merge( + $this->index['graph_vars'][$val] ?? [], + [['table' => $id]] + ); + } elseif ($info['uri']) { + $val = $info['uri']; + $this->index['graph_uris'][$val] = array_merge( + $this->index['graph_uris'][$val] ?? [], + [['table' => $id]] + ); + } + } + } + } + $sub_ids = $pattern['patterns'] ?? []; + foreach ($sub_ids as $sub_id) { + $this->analyzeIndex($this->getPattern($sub_id)); + } + } + + public function getGraphInfos($id) + { + $r = []; + if ($id) { + $pattern = $this->index['patterns'][$id]; + $type = $pattern['type']; + /* graph */ + if ('graph' == $type) { + $r[] = ['type' => 'graph', 'var' => $pattern['var'], 'uri' => $pattern['uri']]; + } + $p_pattern = $this->index['patterns'][$pattern['parent_id']]; + if (isset($p_pattern['graph_infos'])) { + return array_merge($p_pattern['graph_infos'], $r); + } + + return array_merge($this->getGraphInfos($pattern['parent_id']), $r); + } else { + /* FROM / FROM NAMED */ + if (isset($this->infos['query']['dataset'])) { + foreach ($this->infos['query']['dataset'] as $set) { + $r[] = array_merge(['type' => 'dataset'], $set); + } + } + } + + return $r; + } + + public function getPattern($id): array + { + if (\is_array($id)) { + return $id; + } + + return $this->index['patterns'][$id] ?? []; + } + + public function getInitialPattern($id): array + { + return $this->initial_index['patterns'][$id] ?? []; + } + + public function getUnionIndexes($pre_index) + { + $r = []; + $branches = []; + $min_depth = 1000; + /* only process branches with minimum depth */ + foreach ($pre_index['union_branches'] as $id) { + $branches[$id] = \count(preg_split('/\_/', $id)); + $min_depth = min($min_depth, $branches[$id]); + } + foreach ($branches as $branch_id => $depth) { + if ($depth == $min_depth) { + $union_id = preg_replace('/\_[0-9]+$/', '', $branch_id); + $index = [ + 'keeping' => $branch_id, + 'union_branches' => [], + 'patterns' => $pre_index['patterns'], + ]; + $old_branches = $index['patterns'][$union_id]['patterns']; + $skip_id = ($old_branches[0] == $branch_id) ? $old_branches[1] : $old_branches[0]; + $index['patterns'][$union_id]['type'] = 'group'; + $index['patterns'][$union_id]['patterns'] = [$branch_id]; + foreach ($index['patterns'] as $pattern_id => $pattern) { + if (preg_match('/^'.$skip_id.'/', $pattern_id)) { + unset($index['patterns'][$pattern_id]); + } elseif ('union' == $pattern['type']) { + foreach ($pattern['patterns'] as $sub_union_branch_id) { + $index['union_branches'][] = $sub_union_branch_id; + } + } + } + if ($index['union_branches']) { + $r = array_merge($r, $this->getUnionIndexes($index)); + } else { + $r[] = $index; + } + } + } + + return $r; + } + + public function isOptionalPattern($id) + { + $pattern = $this->getPattern($id); + $type = $pattern['type'] ?? ''; + if ('optional' == $type) { + return 1; + } + + $parentId = $pattern['parent_id'] ?? '0'; + if ('0' == $parentId) { + return 0; + } + + return $this->isOptionalPattern($pattern['parent_id']); + } + + public function getOptionalPattern($id) + { + $pn = $this->getPattern($id); + do { + $pn = $this->getPattern($pn['parent_id']); + } while ($pn['parent_id'] && ('optional' != $pn['type'])); + + return $pn['id']; + } + + public function sameOptional($id, $id2) + { + return $this->getOptionalPattern($id) == $this->getOptionalPattern($id2); + } + + public function isUnionPattern($id) + { + $pattern = $this->getPattern($id); + + $type = $pattern['type'] ?? ''; + if ('union' == $type) { + return 1; + } + + $parentId = $pattern['parent_id'] ?? '0'; + if ('0' == $parentId) { + return 0; + } + + return $this->isUnionPattern($parentId); + } + + public function getValueTable($col) + { + return preg_match('/^(s|o)$/', $col) ? $col.'2val' : 'id2val'; + } + + public function getGraphTable() + { + return 'g2t'; + } + + public function getQuerySQL() + { + $nl = "\n"; + $where_sql = $this->getWHERESQL(); /* pre-fills $index['sub_joins'] $index['constraints'] */ + $order_sql = $this->getORDERSQL(); /* pre-fills $index['sub_joins'] $index['constraints'] */ + + return ''.( + $this->is_union_query + ? 'SELECT' + : 'SELECT'.$this->getDistinctSQL()).$nl. + $this->getResultVarsSQL().$nl. /* fills $index['sub_joins'] */ + $this->getFROMSQL(). + $this->getAllJoinsSQL(). + $this->getWHERESQL(). + $this->getGROUPSQL(). + $this->getORDERSQL(). + ($this->is_union_query + ? '' + : $this->getLIMITSQL() + ).$nl.''; + } + + public function getDistinctSQL() + { + $distinct = $this->infos['query']['distinct'] ?? 0; + $reduced = $this->infos['query']['reduced'] ?? 0; + + $check = $distinct || $reduced; + if ($this->is_union_query) { + return $check ? '' : ' ALL'; + } + + return $check ? ' DISTINCT' : ''; + } + + public function getResultVarsSQL() + { + $r = ''; + $vars = $this->infos['query']['result_vars']; + $nl = "\n"; + $added = []; + foreach ($vars as $var) { + $var_name = $var['var']; + $tbl_alias = ''; + if ($tbl_infos = $this->getVarTableInfos($var_name, 0)) { + $tbl = $tbl_infos['table']; + $col = $tbl_infos['col']; + $tbl_alias = $tbl_infos['table_alias']; + } elseif (1 == $var_name) {/* ASK query */ + $r .= '1 AS `success`'; + } else { + $msg = 'Result variable "'.$var_name.'" not used in query.'; + $this->logger->warning($msg); + } + + if ($tbl_alias) { + /* aggregate */ + if ($var['aggregate']) { + $conv_code = ''; + if ('count' != strtolower($var['aggregate'])) { + $tbl_alias = 'V_'.$tbl.'_'.$col.'.val'; + $conv_code = '0 + '; + } + if (!isset($added[$var['alias']])) { + $r .= $r ? ','.$nl.' ' : ' '; + + $distinct = $this->infos['query']['distinct'] ?? 0; + $distinct_code = ('count' == strtolower($var['aggregate'])) + && $distinct ? 'DISTINCT ' : ''; + + $r .= $var['aggregate'] + .'('.$conv_code.$distinct_code.$tbl_alias.') AS `'.$var['alias'].'`'; + $added[$var['alias']] = 1; + } + } else { + /* normal var */ + if (!isset($added[$var_name])) { + $r .= $r ? ','.$nl.' ' : ' '; + $r .= $tbl_alias.' AS `'.$var_name.'`'; + $is_s = ('s' == $col); + $is_o = ('o' == $col); + if ('NULL' == $tbl_alias) { + /* type / add in UNION queries? */ + if ($is_s || $is_o) { + $r .= ', '.$nl.' NULL AS `'.$var_name.' type`'; + } + /* lang_dt / always add it in UNION queries, the var may be used as s/p/o */ + if ($is_o || $this->is_union_query) { + $r .= ', '.$nl.' NULL AS `'.$var_name.' lang_dt`'; + } + } else { + /* type */ + if ($is_s || $is_o) { + $r .= ', '.$nl.' '.$tbl_alias.'_type AS `'.$var_name.' type`'; + } + /* lang_dt / always add it in UNION queries, the var may be used as s/p/o */ + if ($is_o) { + $r .= ', '.$nl.' '.$tbl_alias.'_lang_dt AS `'.$var_name.' lang_dt`'; + } elseif ($this->is_union_query) { + $r .= ', '.$nl.' NULL AS `'.$var_name.' lang_dt`'; + } + } + $added[$var_name] = 1; + } + } + if (!\in_array($tbl_alias, $this->index['sub_joins'])) { + $this->index['sub_joins'][] = $tbl_alias; + } + } + } + + return $r ? $r : '1 AS `success`'; + } + + public function getVarTableInfos($var, $ignore_initial_index = 1) + { + if ('*' == $var) { + return ['table' => '', 'col' => '', 'table_alias' => '*']; + } + if ($infos = $this->index['vars'][$var] ?? 0) { + $infos[0]['table_alias'] = 'T_'.$infos[0]['table'].'.'.$infos[0]['col']; + + return $infos[0]; + } + if ($infos = $this->index['graph_vars'][$var] ?? 0) { + $infos[0]['col'] = 'g'; + $infos[0]['table_alias'] = 'G_'.$infos[0]['table'].'.'.$infos[0]['col']; + + return $infos[0]; + } + if ($this->is_union_query && !$ignore_initial_index) { + if ( + ($infos = $this->initial_index['vars'][$var] ?? 0) + || ($infos = $this->initial_index['graph_vars'][$var] ?? 0) + ) { + if (!\in_array($var, $this->infos['null_vars'])) { + $this->infos['null_vars'][] = $var; + } + $infos[0]['table_alias'] = 'NULL'; + $infos[0]['col'] = !isset($infos[0]['col']) ? '' : $infos[0]['col']; + + return $infos[0]; + } + } + + return 0; + } + + public function getFROMSQL() + { + $from_ids = $this->index['from']; + $r = ''; + foreach ($from_ids as $from_id) { + $r .= $r ? ', ' : ''; + $r .= 'triple T_'.$from_id; + } + + return $r ? 'FROM '.$r : ''; + } + + public function getOrderedJoinIDs() + { + return array_merge($this->index['from'], $this->index['join'], $this->index['left_join']); + } + + public function getJoinInfos($id) + { + $r = []; + $tbl_ids = $this->getOrderedJoinIDs(); + $pattern = $this->getPattern($id); + foreach ($tbl_ids as $tbl_id) { + $tbl_pattern = $this->getPattern($tbl_id); + if ($tbl_id != $id) { + foreach (['s', 'p', 'o'] as $tbl_term) { + foreach (['var', 'bnode', 'uri'] as $term_type) { + if ($tbl_pattern[$tbl_term.'_type'] == $term_type) { + foreach (['s', 'p', 'o'] as $term) { + if (($pattern[$term.'_type'] == $term_type) && ($tbl_pattern[$tbl_term] == $pattern[$term])) { + $r[] = ['term' => $term, 'join_tbl' => $tbl_id, 'join_term' => $tbl_term]; + } + } + } + } + } + } + } + + return $r; + } + + public function getAllJoinsSQL() + { + $js = $this->getJoins(); + $ljs = $this->getLeftJoins(); + $entries = array_merge($js, $ljs); + $id2code = []; + foreach ($entries as $entry) { + if (preg_match('/([^\s]+) ON (.*)/s', $entry, $m)) { + $id2code[$m[1]] = $entry; + } + } + $deps = []; + foreach ($id2code as $id => $code) { + $deps[$id]['rank'] = 0; + foreach ($id2code as $other_id => $other_code) { + $deps[$id]['rank'] += ($id != $other_id) && preg_match('/'.$other_id.'/', $code) ? 1 : 0; + $deps[$id][$other_id] = ($id != $other_id) && preg_match('/'.$other_id.'/', $code) ? 1 : 0; + } + } + $r = ''; + do { + /* get next 0-rank */ + $next_id = 0; + foreach ($deps as $id => $infos) { + if (0 == $infos['rank']) { + $next_id = $id; + break; + } + } + if ($next_id) { + $r .= "\n".$id2code[$next_id]; + unset($deps[$next_id]); + foreach ($deps as $id => $infos) { + $deps[$id]['rank'] = 0; + unset($deps[$id][$next_id]); + foreach ($infos as $k => $v) { + if (!\in_array($k, ['rank', $next_id])) { + $deps[$id]['rank'] += $v; + $deps[$id][$k] = $v; + } + } + } + } + } while ($next_id); + + return $r; + } + + public function getJoins() + { + $r = []; + $nl = "\n"; + foreach ($this->index['join'] as $id) { + $sub_r = $this->getJoinConditionSQL($id); + $r[] = 'JOIN triple T_'.$id.' ON ('.$sub_r.$nl.')'; + } + foreach (array_merge($this->index['from'], $this->index['join']) as $id) { + if ($sub_r = $this->getRequiredSubJoinSQL($id)) { + $r[] = $sub_r; + } + } + + return $r; + } + + public function getLeftJoins() + { + $r = []; + $nl = "\n"; + foreach ($this->index['left_join'] as $id) { + $sub_r = $this->getJoinConditionSQL($id); + $r[] = 'LEFT JOIN triple T_'.$id.' ON ('.$sub_r.$nl.')'; + } + foreach ($this->index['left_join'] as $id) { + if ($sub_r = $this->getRequiredSubJoinSQL($id, 'LEFT')) { + $r[] = $sub_r; + } + } + + return $r; + } + + public function getJoinConditionSQL($id) + { + $r = ''; + $nl = "\n"; + $infos = $this->getJoinInfos($id); + $pattern = $this->getPattern($id); + + $tbl = 'T_'.$id; + /* core dependency */ + $d_tbls = $this->getDependentJoins($id); + foreach ($d_tbls as $d_tbl) { + if (preg_match('/^T_([0-9\_]+)\.[spo]+/', $d_tbl, $m) && ($m[1] != $id)) { + if ($this->isJoinedBefore($m[1], $id) && !\in_array($m[1], array_merge($this->index['from'], $this->index['join']))) { + $r .= $r ? $nl.' AND ' : $nl.' '; + $r .= '('.$d_tbl.' IS NOT NULL)'; + } + $this->logDependency($id, $d_tbl); + } + } + /* triple-based join info */ + foreach ($infos as $info) { + if ($this->isJoinedBefore($info['join_tbl'], $id) && $this->joinDependsOn($id, $info['join_tbl'])) { + $r .= $r ? $nl.' AND ' : $nl.' '; + $r .= '('.$tbl.'.'.$info['term'].' = T_'.$info['join_tbl'].'.'.$info['join_term'].')'; + } + } + /* filters etc */ + if ($sub_r = $this->getPatternSQL($pattern, 'join__T_'.$id)) { + $r .= $r ? $nl.' AND '.$sub_r : $nl.' '.'('.$sub_r.')'; + } + + return $r; + } + + /** + * A log of identified table join dependencies in getJoinConditionSQL. + */ + public function logDependency($id, $tbl) + { + if (!isset($this->dependency_log[$id])) { + $this->dependency_log[$id] = []; + } + if (!\in_array($tbl, $this->dependency_log[$id])) { + $this->dependency_log[$id][] = $tbl; + } + } + + /** + * checks whether entries in the dependecy log could perhaps be optimized + * (triggers re-ordering of patterns. + */ + public function problematicDependencies() + { + foreach ($this->dependency_log as $id => $tbls) { + if (\count($tbls) > 1) { + return \count($tbls); + } + } + + return 0; + } + + public function isJoinedBefore($tbl_1, $tbl_2) + { + $tbl_ids = $this->getOrderedJoinIDs(); + foreach ($tbl_ids as $id) { + if ($id == $tbl_1) { + return 1; + } + if ($id == $tbl_2) { + return 0; + } + } + } + + public function joinDependsOn($id, $id2) + { + if (\in_array($id2, array_merge($this->index['from'], $this->index['join']))) { + return 1; + } + $d_tbls = $this->getDependentJoins($id2); + //echo $id . ' :: ' . $id2 . '=>' . print_r($d_tbls, 1); + foreach ($d_tbls as $d_tbl) { + if (preg_match('/^T_'.$id.'\./', $d_tbl)) { + return 1; + } + } + + return 0; + } + + public function getDependentJoins($id) + { + $r = []; + /* sub joins */ + foreach ($this->index['sub_joins'] as $alias) { + if (preg_match('/^(T|V|G)_'.$id.'/', $alias)) { + $r[] = $alias; + } + } + /* siblings in shared optional */ + $o_id = $this->getOptionalPattern($id); + foreach ($this->index['sub_joins'] as $alias) { + if (preg_match('/^(T|V|G)_'.$o_id.'/', $alias) && !\in_array($alias, $r)) { + $r[] = $alias; + } + } + foreach ($this->index['left_join'] as $alias) { + if (preg_match('/^'.$o_id.'/', $alias) && !\in_array($alias, $r)) { + $r[] = 'T_'.$alias.'.s'; + } + } + + return $r; + } + + public function getRequiredSubJoinSQL($id, $prefix = '') + { + /* id is a triple pattern id. Optional FILTERS and GRAPHs are getting added to the join directly */ + $nl = "\n"; + $r = ''; + foreach ($this->index['sub_joins'] as $alias) { + if (preg_match('/^V_'.$id.'_([a-z\_]+)\.val$/', $alias, $m)) { + $col = $m[1]; + $sub_r = ''; + if ($this->isOptionalPattern($id)) { + $pattern = $this->getPattern($id); + do { + $pattern = $this->getPattern($pattern['parent_id']); + } while ($pattern['parent_id'] && ('optional' != $pattern['type'])); + $sub_r = $this->getPatternSQL($pattern, 'sub_join__V_'.$id); + } + $sub_r = $sub_r ? $nl.' AND ('.$sub_r.')' : ''; + /* lang dt only on literals */ + if ('o_lang_dt' == $col) { + $sub_sub_r = 'T_'.$id.'.o_type = 2'; + $sub_r .= $nl.' AND ('.$sub_sub_r.')'; + } + $cur_prefix = $prefix ? $prefix.' ' : ''; + if ('g' == $col) { + $r .= trim($cur_prefix.'JOIN '.$this->getValueTable($col).' V_'.$id.'_'.$col.' ON ('.$nl.' (G_'.$id.'.'.$col.' = V_'.$id.'_'.$col.'.id) '.$sub_r.$nl.')'); + } else { + $r .= trim($cur_prefix.'JOIN '.$this->getValueTable($col).' V_'.$id.'_'.$col.' ON ('.$nl.' (T_'.$id.'.'.$col.' = V_'.$id.'_'.$col.'.id) '.$sub_r.$nl.')'); + } + } elseif (preg_match('/^G_'.$id.'\.g$/', $alias, $m)) { + $pattern = $this->getPattern($id); + $sub_r = $this->getPatternSQL($pattern, 'graph_sub_join__G_'.$id); + $sub_r = $sub_r ? $nl.' AND '.$sub_r : ''; + /* dataset restrictions */ + $gi = $this->getGraphInfos($id); + $sub_sub_r = ''; + $added_gts = []; + foreach ($gi as $set) { + if (isset($set['graph']) && !\in_array($set['graph'], $added_gts)) { + $sub_sub_r .= '' !== $sub_sub_r ? ',' : ''; + $sub_sub_r .= $this->getTermID($set['graph'], 'g'); + $added_gts[] = $set['graph']; + } + } + $sub_r .= ('' !== $sub_sub_r) ? $nl.' AND (G_'.$id.'.g IN ('.$sub_sub_r.'))' : ''; + /* other graph join conditions */ + foreach ($this->index['graph_vars'] as $var => $occurs) { + $occur_tbls = []; + foreach ($occurs as $occur) { + $occur_tbls[] = $occur['table']; + if ($occur['table'] == $id) { + break; + } + } + foreach ($occur_tbls as $tbl) { + if (($tbl != $id) && \in_array($id, $occur_tbls) && $this->isJoinedBefore($tbl, $id)) { + $sub_r .= $nl.' AND (G_'.$id.'.g = G_'.$tbl.'.g)'; + } + } + } + $cur_prefix = $prefix ? $prefix.' ' : ''; + $r .= trim($cur_prefix.'JOIN '.$this->getGraphTable().' G_'.$id.' ON ('.$nl.' (T_'.$id.'.t = G_'.$id.'.t)'.$sub_r.$nl.')'); + } + } + + return $r; + } + + public function getWHERESQL() + { + $r = ''; + $nl = "\n"; + /* standard constraints */ + $sub_r = $this->getPatternSQL($this->getPattern('0'), 'where'); + /* additional constraints */ + foreach ($this->index['from'] as $id) { + if ($sub_sub_r = $this->getConstraintSQL($id)) { + $sub_r .= $sub_r ? $nl.' AND '.$sub_sub_r : $sub_sub_r; + } + } + $r .= $sub_r ?: ''; + /* left join dependencies */ + foreach ($this->index['left_join'] as $id) { + $d_joins = $this->getDependentJoins($id); + $added = []; + $d_aliases = []; + $id_alias = 'T_'.$id.'.s'; + foreach ($d_joins as $alias) { + if (preg_match('/^(T|V|G)_([0-9\_]+)(_[spo])?\.([a-z\_]+)/', $alias, $m)) { + $tbl_type = $m[1]; + $tbl_pattern_id = $m[2]; + $suffix = $m[3]; + /* get rid of dependency permutations and nested optionals */ + if (($tbl_pattern_id >= $id) && $this->sameOptional($tbl_pattern_id, $id)) { + if (!\in_array($tbl_type.'_'.$tbl_pattern_id.$suffix, $added)) { + $sub_r .= $sub_r ? ' AND ' : ''; + $sub_r .= $alias.' IS NULL'; + $d_aliases[] = $alias; + $added[] = $tbl_type.'_'.$tbl_pattern_id.$suffix; + $id_alias = ($tbl_pattern_id == $id) ? $alias : $id_alias; + } + } + } + } + /* TODO fix this! */ + if (\count($d_aliases) > 2) { + $sub_r1 = ' /* '.$id_alias.' dependencies */'; + $sub_r2 = '(('.$id_alias.' IS NULL) OR (CONCAT('.implode(', ', $d_aliases).') IS NOT NULL))'; + $r .= $r ? $nl.$sub_r1.$nl.' AND '.$sub_r2 : $sub_r1.$nl.$sub_r2; + } + } + + return $r ? $nl.'WHERE '.$r : ''; + } + + public function addConstraintSQLEntry($id, $sql) + { + if (!isset($this->index['constraints'][$id])) { + $this->index['constraints'][$id] = []; + } + if (!\in_array($sql, $this->index['constraints'][$id])) { + $this->index['constraints'][$id][] = $sql; + } + } + + public function getConstraintSQL($id) + { + $r = ''; + $nl = "\n"; + $constraints = $this->index['constraints'][$id] ?? []; + foreach ($constraints as $constraint) { + $r .= $r ? $nl.' AND '.$constraint : $constraint; + } + + return $r; + } + + public function getPatternSQL($pattern, $context) + { + $type = $pattern['type'] ?? ''; + if (!$type) { + return ''; + } + + $m = 'get'.ucfirst($type).'PatternSQL'; + + return method_exists($this, $m) + ? $this->$m($pattern, $context) + : $this->getDefaultPatternSQL($pattern, $context); + } + + public function getDefaultPatternSQL($pattern, $context) + { + $r = ''; + $nl = "\n"; + $sub_ids = $pattern['patterns'] ?? []; + foreach ($sub_ids as $sub_id) { + $sub_r = $this->getPatternSQL($this->getPattern($sub_id), $context); + $r .= ($r && $sub_r) ? $nl.' AND ('.$sub_r.')' : ($sub_r ?: ''); + } + + return $r ? $r : ''; + } + + public function getTriplePatternSQL($pattern, $context) + { + $r = ''; + $nl = "\n"; + $id = $pattern['id']; + /* s p o */ + $vars = []; + foreach (['s', 'p', 'o'] as $term) { + $sub_r = ''; + $type = $pattern[$term.'_type']; + if ('uri' == $type) { + $term_id = $this->getTermID($pattern[$term], $term); + $sub_r = '(T_'.$id.'.'.$term.' = '.$term_id.') /* ' + .preg_replace('/[\#\*\>]/', '::', $pattern[$term]).' */'; + } elseif ('literal' == $type) { + $term_id = $this->getTermID($pattern[$term], $term); + $sub_r = '(T_'.$id.'.'.$term.' = '.$term_id.') /* ' + .preg_replace('/[\#\n\*\>]/', ' ', $pattern[$term]).' */'; + if ( + ($lang_dt = $pattern[$term.'_lang'] ?? '') + || ($lang_dt = $pattern[$term.'_datatype'] ?? '') + ) { + $lang_dt_id = $this->getTermID($lang_dt); + $sub_r .= $nl + .' AND (T_'.$id.'.'.$term.'_lang_dt = '.$lang_dt_id.') /* ' + .preg_replace('/[\#\*\>]/', '::', $lang_dt).' */'; + } + } elseif ('var' == $type) { + $val = $pattern[$term]; + if (isset($vars[$val])) { + /* repeated var in pattern */ + $sub_r = '(T_'.$id.'.'.$term.'='.'T_'.$id.'.'.$vars[$val].')'; + } + $vars[$val] = $term; + if ($infos = $this->index['graph_vars'][$val] ?? 0) { + /* graph var in triple pattern */ + $sub_r .= $sub_r ? $nl.' AND ' : ''; + $tbl = $infos[0]['table']; + $sub_r .= 'G_'.$tbl.'.g = T_'.$id.'.'.$term; + } + } + if ($sub_r) { + if ( + preg_match('/^(join)/', $context) + || (preg_match('/^where/', $context) && \in_array($id, $this->index['from'])) + ) { + $r .= $r ? $nl.' AND '.$sub_r : $sub_r; + } + } + } + /* g */ + if ($infos = $pattern['graph_infos']) { + $tbl_alias = 'G_'.$id.'.g'; + if (!\in_array($tbl_alias, $this->index['sub_joins'])) { + $this->index['sub_joins'][] = $tbl_alias; + } + $sub_r = ['graph_var' => '', 'graph_uri' => '', 'from' => '', 'from_named' => '']; + foreach ($infos as $info) { + $type = $info['type']; + if ('graph' == $type) { + if ($info['uri']) { + $term_id = $this->getTermID($info['uri'], 'g'); + $sub_r['graph_uri'] .= $sub_r['graph_uri'] ? $nl.' AND ' : ''; + $sub_r['graph_uri'] .= '('.$tbl_alias.' = '.$term_id.') /* ' + .preg_replace('/[\#\*\>]/', '::', $info['uri']).' */'; + } + } + } + if ($sub_r['from'] && $sub_r['from_named']) { + $sub_r['from_named'] = ''; + } + if (!$sub_r['from'] && !$sub_r['from_named']) { + $sub_r['graph_var'] = ''; + } + if (preg_match('/^(graph_sub_join)/', $context)) { + foreach ($sub_r as $g_type => $g_sql) { + if ($g_sql) { + $r .= $r ? $nl.' AND '.$g_sql : $g_sql; + } + } + } + } + /* optional sibling filters? */ + if (preg_match('/^(join|sub_join)/', $context) && $this->isOptionalPattern($id)) { + $o_pattern = $pattern; + do { + $o_pattern = $this->getPattern($o_pattern['parent_id']); + } while ($o_pattern['parent_id'] && ('optional' != $o_pattern['type'])); + if ($sub_r = $this->getPatternSQL($o_pattern, 'optional_filter'.preg_replace('/^(.*)(__.*)$/', '\\2', $context))) { + $r .= $r ? $nl.' AND '.$sub_r : $sub_r; + } + /* created constraints */ + if ($sub_r = $this->getConstraintSQL($id)) { + $r .= $r ? $nl.' AND '.$sub_r : $sub_r; + } + } + /* result */ + if (preg_match('/^(where)/', $context) && $this->isOptionalPattern($id)) { + return ''; + } + + return $r; + } + + public function getFilterPatternSQL($pattern, $context) + { + $r = ''; + $id = $pattern['id']; + $constraint_id = $pattern['constraint'] ?? ''; + $constraint = $this->getPattern($constraint_id); + $constraint_type = $constraint['type']; + if ('built_in_call' == $constraint_type) { + $r = $this->getBuiltInCallSQL($constraint, $context); + } elseif ('expression' == $constraint_type) { + $r = $this->getExpressionSQL($constraint, $context, '', 'filter'); + } else { + $m = 'get'.ucfirst($constraint_type).'ExpressionSQL'; + if (method_exists($this, $m)) { + $r = $this->$m($constraint, $context, '', 'filter'); + } + } + if ($this->isOptionalPattern($id) && !preg_match('/^(join|optional_filter)/', $context)) { + return ''; + } + /* unconnected vars in FILTERs eval to false */ + $sub_r = $this->hasUnconnectedFilterVars($id); + if ($sub_r) { + if ('alias' == $sub_r) { + if (!\in_array($r, $this->index['havings'])) { + $this->index['havings'][] = $r; + } + + return ''; + } elseif (preg_match('/^T([^\s]+\.)g (.*)$/s', $r, $m)) {/* graph filter */ + return 'G'.$m[1].'t '.$m[2]; + } elseif (preg_match('/^\(*V[^\s]+_g\.val .*$/s', $r, $m)) { + /* graph value filter, @@improveMe */ + } else { + return 'FALSE'; + } + } + /* some really ugly tweaks */ + /* empty language filter: FILTER ( lang(?v) = '' ) */ + $r = preg_replace( + '/\(\/\* language call \*\/ ([^\s]+) = ""\)/s', '((\\1 = "") OR (\\1 LIKE "%:%"))', + $r + ); + + return $r; + } + + /** + * Checks if vars in the given (filter) pattern are used within the filter's scope. + */ + public function hasUnconnectedFilterVars($filter_pattern_id) + { + $scope_id = $this->getFilterScope($filter_pattern_id); + $vars = $this->getFilterVars($filter_pattern_id); + $r = 0; + foreach ($vars as $var_name) { + if ($this->isUsedTripleVar($var_name, $scope_id)) { + continue; + } + if ($this->isAliasVar($var_name)) { + $r = 'alias'; + break; + } + $r = 1; + break; + } + + return $r; + } + + /** + * Returns the given filter pattern's scope (the id of the parent group pattern). + */ + public function getFilterScope($filter_pattern_id) + { + $patterns = $this->initial_index['patterns']; + $r = ''; + foreach ($patterns as $id => $p) { + /* the id has to be sub-part of the given filter id */ + if (!preg_match('/^'.$id.'.+/', $filter_pattern_id)) { + continue; + } + /* we are looking for a group or union */ + if (!preg_match('/^(group|union)$/', $p['type'])) { + continue; + } + /* we are looking for the longest/deepest match */ + if (\strlen($id) > \strlen($r)) { + $r = $id; + } + } + + return $r; + } + + /** + * Builds a list of vars used in the given (filter) pattern. + */ + public function getFilterVars($filter_pattern_id) + { + $r = []; + $patterns = $this->initial_index['patterns']; + /* find vars in the given filter (i.e. the given id is part of their pattern id) */ + foreach ($patterns as $id => $p) { + if (!preg_match('/^'.$filter_pattern_id.'.+/', $id)) { + continue; + } + $var_name = ''; + if ('var' == $p['type']) { + $var_name = $p['value']; + } elseif (('built_in_call' == $p['type']) && ('bound' == $p['call'])) { + $var_name = $p['args'][0]['value']; + } + if ($var_name && !\in_array($var_name, $r)) { + $r[] = $var_name; + } + } + + return $r; + } + + /** + * Checks if $var_name appears as result projection alias. + */ + public function isAliasVar($var_name) + { + foreach ($this->infos['query']['result_vars'] as $r_var) { + if ($r_var['alias'] == $var_name) { + return 1; + } + } + + return 0; + } + + /** + * Checks if $var_name is used in a triple pattern in the given scope. + */ + public function isUsedTripleVar($var_name, $scope_id = '0') + { + $patterns = $this->initial_index['patterns']; + foreach ($patterns as $id => $p) { + if ('triple' != $p['type']) { + continue; + } + if (!preg_match('/^'.$scope_id.'.+/', $id)) { + continue; + } + foreach (['s', 'p', 'o'] as $term) { + if ('var' != $p[$term.'_type']) { + continue; + } + if ($p[$term] == $var_name) { + return 1; + } + } + } + } + + public function getExpressionSQL($pattern, $context, $val_type = '', $parent_type = '') + { + $r = ''; + $type = $pattern['type'] ?? ''; + $sub_type = $pattern['sub_type'] ?? $type; + if (preg_match('/^(and|or)$/', $sub_type)) { + foreach ($pattern['patterns'] as $sub_id) { + $sub_pattern = $this->getPattern($sub_id); + $sub_pattern_type = $sub_pattern['type']; + if ('built_in_call' == $sub_pattern_type) { + $sub_r = $this->getBuiltInCallSQL($sub_pattern, $context, '', $parent_type); + } else { + $sub_r = $this->getExpressionSQL($sub_pattern, $context, '', $parent_type); + } + if ($sub_r) { + $r .= $r ? ' '.strtoupper($sub_type).' ('.$sub_r.')' : '('.$sub_r.')'; + } + } + } elseif ('built_in_call' == $sub_type) { + $r = $this->getBuiltInCallSQL($pattern, $context, $val_type, $parent_type); + } elseif (preg_match('/literal/', $sub_type)) { + $r = $this->getLiteralExpressionSQL($pattern, $context, $val_type, $parent_type); + } elseif ($sub_type) { + $m = 'get'.ucfirst($sub_type).'ExpressionSQL'; + if (method_exists($this, $m)) { + $r = $this->$m($pattern, $context, '', $parent_type); + } + } + /* skip expressions that reference non-yet-joined tables */ + if (preg_match('/__(T|V|G)_(.+)$/', $context, $m)) { + $context_pattern_id = $m[2]; + $context_table_type = $m[1]; + if (preg_match_all('/((T|V|G)(\_[0-9])+)/', $r, $m)) { + $aliases = $m[1]; + $keep = 1; + foreach ($aliases as $alias) { + if (preg_match('/(T|V|G)_(.*)$/', $alias, $m)) { + $tbl_type = $m[1]; + $tbl = $m[2]; + if (!$this->isJoinedBefore($tbl, $context_pattern_id)) { + $keep = 0; + } elseif (($context_pattern_id == $tbl) && preg_match('/(TV)/', $context_table_type.$tbl_type)) { + $keep = 0; + } + } + } + $r = $keep ? $r : ''; + } + } + + return $r ? '('.$r.')' : $r; + } + + public function detectExpressionValueType($pattern_ids) + { + foreach ($pattern_ids as $id) { + $pattern = $this->getPattern($id); + $type = $pattern['type'] ?? ''; + if (('literal' == $type) && isset($pattern['datatype'])) { + $numericDatatypes = [$this->xsd.'integer', $this->xsd.'float', $this->xsd.'double']; + if (\in_array($pattern['datatype'], $numericDatatypes)) { + return 'numeric'; + } + } + } + + return ''; + } + + public function getRelationalExpressionSQL($pattern, $context, $val_type = '', $parent_type = '') + { + $r = ''; + $val_type = $this->detectExpressionValueType($pattern['patterns']); + $op = $pattern['operator']; + foreach ($pattern['patterns'] as $sub_id) { + $sub_pattern = $this->getPattern($sub_id); + $sub_pattern['parent_op'] = $op; + $sub_type = $sub_pattern['type']; + $m = ('built_in_call' == $sub_type) ? 'getBuiltInCallSQL' : 'get'.ucfirst($sub_type).'ExpressionSQL'; + $m = str_replace('ExpressionExpression', 'Expression', $m); + $sub_r = method_exists($this, $m) ? $this->$m($sub_pattern, $context, $val_type, 'relational') : ''; + $r .= $r ? ' '.$op.' '.$sub_r : $sub_r; + } + + /* + * SQLite related adaption for relational expressions like ?w < 100 + * + * We have to cast the variable behind ?w to a number otherwise we don't get + * meaningful results. + */ + if ($this->store->getDBObject() instanceof PDOSQLiteAdapter) { + // Regex to catch things like: ?w < 100, ?w > 20 + $regex = '/([T\_0-9]+\.o_comp)\s*[<>]{1}\s*[0-9]+/si'; + if (0 < preg_match_all($regex, $r, $matches)) { + foreach ($matches[1] as $variable) { + $r = str_replace($variable, 'CAST ('.$variable.' as float)', $r); + } + } + } + + return $r ? '('.$r.')' : $r; + } + + public function getAdditiveExpressionSQL($pattern, $context, $val_type = '') + { + $r = ''; + $val_type = $this->detectExpressionValueType($pattern['patterns']); + foreach ($pattern['patterns'] as $sub_id) { + $sub_pattern = $this->getPattern($sub_id); + $sub_type = $sub_pattern['type'] ?? ''; + $m = ('built_in_call' == $sub_type) + ? 'getBuiltInCallSQL' + : 'get'.ucfirst($sub_type).'ExpressionSQL'; + $m = str_replace('ExpressionExpression', 'Expression', $m); + $sub_r = method_exists($this, $m) + ? $this->$m($sub_pattern, $context, $val_type, 'additive') + : ''; + $r .= $r ? ' '.$sub_r : $sub_r; + } + + return $r; + } + + public function getMultiplicativeExpressionSQL($pattern, $context, $val_type = '', $parent_type = '') + { + $r = ''; + $val_type = $this->detectExpressionValueType($pattern['patterns']); + foreach ($pattern['patterns'] as $sub_id) { + $sub_pattern = $this->getPattern($sub_id); + $sub_type = $sub_pattern['type']; + $m = ('built_in_call' == $sub_type) + ? 'getBuiltInCallSQL' + : 'get'.ucfirst($sub_type).'ExpressionSQL'; + $m = str_replace('ExpressionExpression', 'Expression', $m); + $sub_r = method_exists($this, $m) + ? $this->$m($sub_pattern, $context, $val_type, 'multiplicative') + : ''; + $r .= $r ? ' '.$sub_r : $sub_r; + } + + return $r; + } + + public function getVarExpressionSQL($pattern, $context, $val_type = '', $parent_type = '') + { + $var = $pattern['value']; + $info = $this->getVarTableInfos($var); + + $tbl = false; + if (isset($info['table'])) { + $tbl = $info['table']; + } + + if (!$tbl) { + /* might be an aggregate var */ + $vars = $this->infos['query']['result_vars']; + foreach ($vars as $test_var) { + if ($test_var['alias'] == $pattern['value']) { + return '`'.$pattern['value'].'`'; + } + } + + return ''; + } + $col = $info['col']; + $parentOp = $pattern['parent_op'] ?? ''; + if ('order' == $context && 'o' == $col) { + $tbl_alias = 'T_'.$tbl.'.o_comp'; + } elseif ('sameterm' == $context) { + $tbl_alias = 'T_'.$tbl.'.'.$col; + } elseif ('relational' == $parent_type && 'o' == $col && preg_match('/[\<\>]/', $parentOp)) { + $tbl_alias = 'T_'.$tbl.'.o_comp'; + } else { + $tbl_alias = 'V_'.$tbl.'_'.$col.'.val'; + if (!\in_array($tbl_alias, $this->index['sub_joins'])) { + $this->index['sub_joins'][] = $tbl_alias; + } + } + + $op = $pattern['operator'] ?? ''; + if (preg_match('/^(filter|and)/', $parent_type)) { + if ('!' == $op) { + $r = '((('.$tbl_alias.' = 0) AND (CONCAT("1", '.$tbl_alias.') != 1))'; /* 0 and no string */ + $r .= ' OR ('.$tbl_alias.' IN ("", "false")))'; /* or "", or "false" */ + } else { + $r = '(('.$tbl_alias.' != 0)'; /* not null */ + $r .= ' OR ((CONCAT("1", '.$tbl_alias.') = 1) AND ('.$tbl_alias.' NOT IN ("", "false"))))'; /* string, and not "" or "false" */ + } + } else { + $r = trim($op.' '.$tbl_alias); + if ('numeric' == $val_type) { + if (preg_match('/__(T|V|G)_(.+)$/', $context, $m)) { + $context_pattern_id = $m[2]; + $context_table_type = $m[1]; + } else { + $context_pattern_id = $pattern['id']; + $context_table_type = 'T'; + } + if ($this->isJoinedBefore($tbl, $context_pattern_id)) { + $add = ($tbl != $context_pattern_id) ? 1 : 0; + $add = (!$add && ('V' == $context_table_type)) ? 1 : 0; + if ($add) { + $this->addConstraintSQLEntry($context_pattern_id, '('.$r.' = "0" OR '.$r.'*1.0 != 0)'); + } + } + } + } + + return $r; + } + + public function getUriExpressionSQL($pattern) + { + $val = $pattern['uri']; + $r = $pattern['operator']; + $r .= is_numeric($val) ? ' '.$val : ' "'.$this->store->getDBObject()->escape($val).'"'; + + return $r; + } + + public function getLiteralExpressionSQL($pattern, $context, $val_type = '', $parent_type = '') + { + $val = $pattern['value']; + $r = $pattern['operator']; + $datatype = $pattern['datatype'] ?? ''; + + if (is_numeric($val) && ($pattern['datatype'] ?? 0)) { + $r .= ' '.$val; + } elseif ( + preg_match('/^(true|false)$/i', $val) + && 'http://www.w3.org/2001/XMLSchema#boolean' == $datatype + ) { + $r .= ' '.strtoupper($val); + } elseif ('regex' == $parent_type) { + $sub_r = $this->store->getDBObject()->escape($val); + $r .= ' "'.preg_replace('/\x5c\x5c/', '\\', $sub_r).'"'; + } else { + $r .= ' "'.$this->store->getDBObject()->escape($val).'"'; + } + + $lang_dt = $pattern['lang'] ?? $pattern['datatype'] ?? ''; + if ($lang_dt) { + /* try table/alias via var in siblings */ + if ($var = $this->findSiblingVarExpression($pattern['id'])) { + if (isset($this->index['vars'][$var])) { + $infos = $this->index['vars'][$var]; + foreach ($infos as $info) { + if ('o' == $info['col']) { + $tbl = $info['table']; + $term_id = $this->getTermID($lang_dt); + if ('!=' != $pattern['operator']) { + if (preg_match('/__(T|V|G)_(.+)$/', $context, $m)) { + $context_pattern_id = $m[2]; + $context_table_type = $m[1]; + } elseif ('where' == $context) { + $context_pattern_id = $tbl; + } else { + $context_pattern_id = $pattern['id']; + } + // TODO better dependency check + if ($tbl == $context_pattern_id) { + if ($term_id || ('http://www.w3.org/2001/XMLSchema#integer' != $lang_dt)) { + /* skip, if simple int, but no id */ + $this->addConstraintSQLEntry($context_pattern_id, 'T_'.$tbl.'.o_lang_dt = '.$term_id.' /* '.preg_replace('/[\#\*\>]/', '::', $lang_dt).' */'); + } + } + } + break; + } + } + } + } + } + + return trim($r); + } + + public function findSiblingVarExpression($id) + { + $pattern = $this->getPattern($id); + do { + $pattern = $this->getPattern($pattern['parent_id']); + } while ($pattern['parent_id'] && ('expression' != $pattern['type'])); + + $sub_patterns = $pattern['patterns'] ?? []; + foreach ($sub_patterns as $sub_id) { + $sub_pattern = $this->getPattern($sub_id); + if ('var' == $sub_pattern['type']) { + return $sub_pattern['value']; + } + } + + return ''; + } + + public function getFunctionExpressionSQL($pattern, $context, $val_type = '', $parent_type = '') + { + $fnc_uri = $pattern['uri']; + $op = $pattern['operator'] ?? ''; + if ($op) { + $op .= ' '; + } + + /* simple type conversions */ + if (0 === strpos($fnc_uri, 'http://www.w3.org/2001/XMLSchema#')) { + return $op.$this->getExpressionSQL($pattern['args'][0], $context, $val_type, $parent_type); + } + + return ''; + } + + public function getBuiltInCallSQL($pattern, $context) + { + $call = $pattern['call']; + $m = 'get'.ucfirst($call).'CallSQL'; + if (method_exists($this, $m)) { + return $this->$m($pattern, $context); + } else { + throw new Exception('Unknown built-in call "'.$call.'"'); + } + + return ''; + } + + public function getBoundCallSQL($pattern, $context) + { + $r = ''; + $var = $pattern['args'][0]['value']; + $info = $this->getVarTableInfos($var); + if (!$tbl = $info['table']) { + return ''; + } + $col = $info['col']; + $tbl_alias = 'T_'.$tbl.'.'.$col; + if ('!' == $pattern['operator']) { + return $tbl_alias.' IS NULL'; + } + + return $tbl_alias.' IS NOT NULL'; + } + + public function getHasTypeCallSQL($pattern, $context, $type) + { + $r = ''; + $var = $pattern['args'][0]['value']; + $info = $this->getVarTableInfos($var); + if (!$tbl = $info['table']) { + return ''; + } + $col = $info['col']; + $tbl_alias = 'T_'.$tbl.'.'.$col.'_type'; + + $operator = $pattern['operator'] ?? ''; + + return $tbl_alias.' '.$operator.'= '.$type; + } + + public function getIsliteralCallSQL($pattern, $context) + { + return $this->getHasTypeCallSQL($pattern, $context, 2); + } + + public function getIsblankCallSQL($pattern, $context) + { + return $this->getHasTypeCallSQL($pattern, $context, 1); + } + + public function getIsiriCallSQL($pattern, $context) + { + return $this->getHasTypeCallSQL($pattern, $context, 0); + } + + public function getIsuriCallSQL($pattern, $context) + { + return $this->getHasTypeCallSQL($pattern, $context, 0); + } + + public function getStrCallSQL($pattern, $context) + { + $sub_pattern = $pattern['args'][0]; + $sub_type = $sub_pattern['type']; + $m = 'get'.ucfirst($sub_type).'ExpressionSQL'; + if (method_exists($this, $m)) { + return $this->$m($sub_pattern, $context); + } + } + + public function getFunctionCallSQL($pattern, $context) + { + $f_uri = $pattern['uri']; + if (preg_match('/(integer|double|float|string)$/', $f_uri)) {/* skip conversions */ + $sub_pattern = $pattern['args'][0]; + $sub_type = $sub_pattern['type']; + $m = 'get'.ucfirst($sub_type).'ExpressionSQL'; + if (method_exists($this, $m)) { + return $this->$m($sub_pattern, $context); + } + } + } + + public function getLangDatatypeCallSQL($pattern, $context) + { + $r = ''; + if (isset($pattern['patterns'])) { /* proceed with first argument only (assumed as base type for type promotion) */ + $sub_pattern = ['args' => [$pattern['patterns'][0]]]; + + return $this->getLangDatatypeCallSQL($sub_pattern, $context); + } + if (!isset($pattern['args'])) { + return 'FALSE'; + } + $sub_type = $pattern['args'][0]['type']; + if ('var' != $sub_type) { + return $this->getLangDatatypeCallSQL($pattern['args'][0], $context); + } + $var = $pattern['args'][0]['value']; + $info = $this->getVarTableInfos($var); + if (!$tbl = $info['table']) { + return ''; + } + $col = 'o_lang_dt'; + $tbl_alias = 'V_'.$tbl.'_'.$col.'.val'; + if (!\in_array($tbl_alias, $this->index['sub_joins'])) { + $this->index['sub_joins'][] = $tbl_alias; + } + $op = $pattern['operator'] ?? ''; + $r = trim($op.' '.$tbl_alias); + + return $r; + } + + public function getDatatypeCallSQL($pattern, $context) + { + return '/* datatype call */ '.$this->getLangDatatypeCallSQL($pattern, $context); + } + + public function getLangCallSQL($pattern, $context) + { + return '/* language call */ '.$this->getLangDatatypeCallSQL($pattern, $context); + } + + public function getLangmatchesCallSQL($pattern, $context) + { + if (2 == \count($pattern['args'])) { + $arg_1 = $pattern['args'][0]; + $arg_2 = $pattern['args'][1]; + $sub_r_1 = $this->getBuiltInCallSQL($arg_1, $context); /* adds value join */ + $sub_r_2 = $this->getExpressionSQL($arg_2, $context); + $op = $pattern['operator'] ?? ''; + if (preg_match('/^([\"\'])([^\'\"]+)/', $sub_r_2, $m)) { + if ('*' == $m[2]) { + $r = '!' == $op + ? 'NOT ('.$sub_r_1.' REGEXP "^[a-zA-Z\-]+$"'.')' + : $sub_r_1.' REGEXP "^[a-zA-Z\-]+$"'; + } else { + $r = ('!' == $op) ? $sub_r_1.' NOT LIKE '.$m[1].$m[2].'%'.$m[1] : $sub_r_1.' LIKE '.$m[1].$m[2].'%'.$m[1]; + } + } else { + $r = ('!' == $op) ? $sub_r_1.' NOT LIKE CONCAT('.$sub_r_2.', "%")' : $sub_r_1.' LIKE CONCAT('.$sub_r_2.', "%")'; + } + + return $r; + } + + return ''; + } + + /** + * @todo not in use, so remove? + */ + public function getSametermCallSQL($pattern, $context) + { + if (2 == \count($pattern['args'])) { + $arg_1 = $pattern['args'][0]; + $arg_2 = $pattern['args'][1]; + $sub_r_1 = $this->getExpressionSQL($arg_1, 'sameterm'); + $sub_r_2 = $this->getExpressionSQL($arg_2, 'sameterm'); + $op = $pattern['operator'] ?? ''; + $r = $sub_r_1.' '.$op.'= '.$sub_r_2; + + return $r; + } + + return ''; + } + + public function getRegexCallSQL($pattern, $context) + { + $ac = \count($pattern['args']); + if ($ac >= 2) { + foreach ($pattern['args'] as $i => $arg) { + $var = 'sub_r_'.($i + 1); + $$var = $this->getExpressionSQL($arg, $context, '', 'regex'); + } + $sub_r_3 = (isset($sub_r_3) && preg_match('/[\"\'](.+)[\"\']/', $sub_r_3, $m)) ? strtolower($m[1]) : ''; + $operator = $pattern['operator'] ?? ''; + $op = '!' == $operator ? ' NOT' : ''; + if (!$sub_r_1 || !$sub_r_2) { + return ''; + } + $is_simple_search = preg_match('/^[\(\"]+(\^)?([a-z0-9\_\-\s]+)(\$)?[\)\"]+$/is', $sub_r_2, $m); + $is_simple_search = preg_match('/^[\(\"]+(\^)?([^\\\*\[\]\}\{\(\)\"\'\?\+\.]+)(\$)?[\)\"]+$/is', $sub_r_2, $m); + $is_o_search = preg_match('/o\.val\)*$/', $sub_r_1); + /* fulltext search (may have "|") */ + if ($is_simple_search && $is_o_search && !$op && (\strlen($m[2]) > 8)) { + /* MATCH variations */ + if (($val_parts = preg_split('/\|/', $m[2]))) { + return 'MATCH('.trim($sub_r_1, '()').') AGAINST("'.implode(' ', $val_parts).'")'; + } else { + return 'MATCH('.trim($sub_r_1, '()').') AGAINST("'.$m[2].'")'; + } + } + if (preg_match('/\|/', $sub_r_2)) { + $is_simple_search = 0; + } + /* LIKE */ + if ($is_simple_search && ('i' == $sub_r_3)) { + $sub_r_2 = $m[1] ? $m[2] : '%'.$m[2]; + $sub_r_2 .= isset($m[3]) && $m[3] ? '' : '%'; + + return $sub_r_1.$op.' LIKE "'.$sub_r_2.'"'; + } + /* REGEXP */ + $opt = ''; + if (!$this->store->getDBObject() instanceof PDOSQLiteAdapter) { + $opt = ('i' == $sub_r_3) ? '' : 'BINARY '; + } + + return $sub_r_1.$op.' REGEXP '.$opt.$sub_r_2; + } + + return ''; + } + + public function getGROUPSQL() + { + $r = ''; + $nl = "\n"; + $infos = $this->infos['query']['group_infos'] ?? []; + foreach ($infos as $info) { + $var = $info['value']; + if ($tbl_infos = $this->getVarTableInfos($var, 0)) { + $tbl_alias = $tbl_infos['table_alias']; + $r .= $r ? ', ' : 'GROUP BY '; + $r .= $tbl_alias; + } + } + $hr = ''; + foreach ($this->index['havings'] as $having) { + $hr .= $hr ? ' AND' : ' HAVING'; + $hr .= '('.$having.')'; + } + $r .= $hr; + + return $r ? $nl.$r : $r; + } + + public function getORDERSQL() + { + $r = ''; + $nl = "\n"; + $infos = $this->infos['query']['order_infos'] ?? []; + foreach ($infos as $info) { + $type = $info['type']; + $ms = [ + 'expression' => 'getExpressionSQL', + 'built_in_call' => 'getBuiltInCallSQL', + 'function_call' => 'getFunctionCallSQL', + ]; + $m = isset($ms[$type]) ? $ms[$type] : 'get'.ucfirst($type).'ExpressionSQL'; + if (method_exists($this, $m)) { + $sub_r = '('.$this->$m($info, 'order').')'; + $direction = $info['direction'] ?? ''; + $sub_r .= 'desc' == $direction ? ' DESC' : ''; + $r .= $r ? ','.$nl.$sub_r : $sub_r; + } + } + + return $r ? $nl.'ORDER BY '.$r : ''; + } + + public function getLIMITSQL() + { + $r = ''; + $nl = "\n"; + $limit = $this->infos['query']['limit'] ?? -1; + $offset = $this->infos['query']['offset'] ?? -1; + if (-1 != $limit) { + $offset = (-1 == $offset) ? 0 : $this->store->getDBObject()->escape($offset); + $r = 'LIMIT '.$offset.','.$limit; + } elseif (-1 != $offset) { + // TODO is that if-else required with SQLite? + // mysql doesn't support stand-alone offsets + $r = 'LIMIT '.$this->store->getDBObject()->escape($offset).',999999999999'; + } + + return $r ? $nl.$r : ''; + } + + public function getValueSQL($q_tbl, $q_sql) + { + $r = ''; + /* result vars */ + $vars = $this->infos['query']['result_vars']; + $nl = "\n"; + $v_tbls = ['JOIN' => [], 'LEFT JOIN' => []]; + $vc = 1; + foreach ($vars as $var) { + $var_name = $var['var']; + $r .= $r ? ','.$nl.' ' : ' '; + $col = ''; + $tbl = ''; + if ('*' != $var_name) { + if (\in_array($var_name, $this->infos['null_vars'])) { + if (isset($this->initial_index['vars'][$var_name])) { + $col = $this->initial_index['vars'][$var_name][0]['col']; + $tbl = $this->initial_index['vars'][$var_name][0]['table']; + } + if (isset($this->initial_index['graph_vars'][$var_name])) { + $col = 'g'; + $tbl = $this->initial_index['graph_vars'][$var_name][0]['table']; + } + } elseif (isset($this->index['vars'][$var_name])) { + $col = $this->index['vars'][$var_name][0]['col']; + $tbl = $this->index['vars'][$var_name][0]['table']; + } + } + if ($var['aggregate']) { + $r .= 'TMP.`'.$var['alias'].'`'; + } else { + $join_type = \in_array($tbl, array_merge($this->index['from'], $this->index['join'])) ? 'JOIN' : 'LEFT JOIN'; /* val may be NULL */ + $v_tbls[$join_type][] = ['t_col' => $col, 'q_col' => $var_name, 'vc' => $vc]; + $r .= 'V'.$vc.'.val AS `'.$var_name.'`'; + if (\in_array($col, ['s', 'o'])) { + if (strpos($q_sql, '`'.$var_name.' type`')) { + $r .= ', '.$nl.' TMP.`'.$var_name.' type` AS `'.$var_name.' type`'; + //$r .= ', ' . $nl . ' CASE TMP.`' . $var_name . ' type` WHEN 2 THEN "literal" WHEN 1 THEN "bnode" ELSE "uri" END AS `' . $var_name . ' type`'; + } else { + $r .= ', '.$nl.' NULL AS `'.$var_name.' type`'; + } + } + ++$vc; + if ('o' == $col) { + $v_tbls[$join_type][] = ['t_col' => 'id', 'q_col' => $var_name.' lang_dt', 'vc' => $vc]; + if (strpos($q_sql, '`'.$var_name.' lang_dt`')) { + $r .= ', '.$nl.' V'.$vc.'.val AS `'.$var_name.' lang_dt`'; + ++$vc; + } else { + $r .= ', '.$nl.' NULL AS `'.$var_name.' lang_dt`'; + } + } + } + } + if (!$r) { + $r = '*'; + } + /* from */ + $r .= $nl.'FROM ('.$q_tbl.' TMP)'; + foreach (['JOIN', 'LEFT JOIN'] as $join_type) { + foreach ($v_tbls[$join_type] as $v_tbl) { + $tbl = $this->getValueTable($v_tbl['t_col']); + $var_name = preg_replace('/^([^\s]+)(.*)$/', '\\1', $v_tbl['q_col']); + $cur_join_type = \in_array($var_name, $this->infos['null_vars']) ? 'LEFT JOIN' : $join_type; + if (!strpos($q_sql, '`'.$v_tbl['q_col'].'`')) { + continue; + } + $r .= $nl + .' '.$cur_join_type + .' '.$tbl.' V'.$v_tbl['vc'] + .' ON ((V'.$v_tbl['vc'].'.id = TMP.`'.$v_tbl['q_col'].'`))'; + } + } + /* create pos columns, id needed */ + $orderInfos = $this->infos['query']['order_infos'] ?? []; + if ($orderInfos) { + $r .= $nl.' ORDER BY TMPPOS'; + } + + return 'SELECT'.$nl.$r; + } +} diff --git a/src/Store/TurtleLoader.php b/src/Store/TurtleLoader.php new file mode 100644 index 0000000..43b68e5 --- /dev/null +++ b/src/Store/TurtleLoader.php @@ -0,0 +1,42 @@ + + * (c) Benjamin Nowack + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace sweetrdf\InMemoryStoreSqlite\Store; + +use sweetrdf\InMemoryStoreSqlite\Parser\TurtleParser; +use sweetrdf\InMemoryStoreSqlite\Store\QueryHandler\LoadQueryHandler; + +class TurtleLoader extends TurtleParser +{ + private LoadQueryHandler $caller; + + public function setCaller(LoadQueryHandler $caller): void + { + $this->caller = $caller; + } + + public function addT(array $t): void + { + $this->caller->addT( + $t['s'], + $t['p'], + $t['o'], + $t['s_type'], + $t['o_type'], + $t['o_datatype'], + $t['o_lang'] + ); + + ++$this->t_count; + } +} diff --git a/src/StringReader.php b/src/StringReader.php new file mode 100755 index 0000000..81a002e --- /dev/null +++ b/src/StringReader.php @@ -0,0 +1,69 @@ + + * (c) Benjamin Nowack + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Provides a way to read a given string in chunks. + */ +final class StringReader +{ + private ?string $base; + + private array $stream = []; + + public function getBase(): ?string + { + return $this->base; + } + + public function init($path, $data) + { + $this->base = calcBase($path); + $this->uri = calcURI($path, $this->base); + + $this->stream = [ + 'type' => 'data', + 'pos' => 0, + 'headers' => [], + 'size' => \strlen($data), + 'data' => $data, + 'buffer' => '', + ]; + } + + public function readStream(int $d_size = 1024): string + { + $s = $this->stream; + $r = $this->stream['buffer']; + + $s['buffer'] = ''; + if ($s['size']) { + $d_size = min($d_size, $this->stream['size'] - $this->stream['pos']); + } + + if ('data' == $this->stream['type']) { + if ($d_size > 0) { + $d = substr($this->stream['data'], $s['pos'], $d_size); + } else { + $d = ''; + } + } + + $s['pos'] += \strlen($d); + + $this->stream = $s; + + return $r.$d; + } +} diff --git a/src/functions.php b/src/functions.php new file mode 100644 index 0000000..db3c710 --- /dev/null +++ b/src/functions.php @@ -0,0 +1,107 @@ + */ + return $base; + } + $path = preg_replace("/^\.\//", '', $path); + $root = preg_match('/(^[a-z0-9]+\:[\/]{1,3}[^\/]+)[\/|$]/i', $base, $m) ? $m[1] : $base; /* w/o trailing slash */ + $base .= ($base == $root) ? '/' : ''; + if (preg_match('/^\//', $path)) {/* leading slash */ + return $root.$path; + } + if (!$path) { + return $base; + } + if (preg_match('/^([\#\?])/', $path, $m)) { + return preg_replace('/\\'.$m[1].'.*$/', '', $base).$path; + } + if (preg_match('/^(\&)(.*)$/', $path, $m)) {/* not perfect yet */ + return preg_match('/\?/', $base) ? $base.$m[1].$m[2] : $base.'?'.$m[2]; + } + if (preg_match("/^[a-z0-9]+\:/i", $path)) {/* abs path */ + return $path; + } + /* rel path: remove stuff after last slash */ + $base = substr($base, 0, strrpos($base, '/') + 1); + + /* resolve ../ */ + while (preg_match('/^(\.\.\/)(.*)$/', $path, $m)) { + $path = $m[2]; + $base = ($base == $root.'/') ? $base : preg_replace('/^(.*\/)[^\/]+\/$/', '\\1', $base); + } + + return $base.$path; +} + +function calcBase(string $path): string +{ + $r = $path; + $r = preg_replace('/\#.*$/', '', $r); /* remove hash */ + $r = preg_replace('/^\/\//', 'http://', $r); /* net path (//), assume http */ + if (preg_match('/^[a-z0-9]+\:/', $r)) {/* scheme, abs path */ + while (preg_match('/^(.+\/)(\.\.\/.*)$/U', $r, $m)) { + $r = calcURI($m[1], $m[2]); + } + + return $r; + } + + return 'file://'.realpath($r); /* real path */ +} + +/** + * @return array + */ +function splitURI($v): array +{ + /* + * the following namespaces may lead to conflated URIs, + * we have to set the split position manually + */ + if (strpos($v, 'www.w3.org')) { + /* + * @todo port to NamespaceHelper + */ + $specials = [ + 'http://www.w3.org/XML/1998/namespace', + 'http://www.w3.org/2005/Atom', + 'http://www.w3.org/1999/xhtml', + ]; + foreach ($specials as $ns) { + if (str_contains($v, $ns)) { + $local_part = substr($v, \strlen($ns)); + if (!preg_match('/^[\/\#]/', $local_part)) { + return [$ns, $local_part]; + } + } + } + } + /* auto-splitting on / or # */ + //$re = '^(.*?)([A-Z_a-z][-A-Z_a-z0-9.]*)$'; + if (preg_match('/^(.*[\/\#])([^\/\#]+)$/', $v, $m)) { + return [$m[1], $m[2]]; + } + /* auto-splitting on last special char, e.g. urn:foo:bar */ + if (preg_match('/^(.*[\:\/])([^\:\/]+)$/', $v, $m)) { + return [$m[1], $m[2]]; + } + + return [$v, '']; +} diff --git a/tests/Integration/KeyValueBagTest.php b/tests/Integration/KeyValueBagTest.php new file mode 100644 index 0000000..04c1e63 --- /dev/null +++ b/tests/Integration/KeyValueBagTest.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tests\Integration; + +use sweetrdf\InMemoryStoreSqlite\KeyValueBag; +use Tests\TestCase; + +class KeyValueBagTest extends TestCase +{ + private function getSubjectUnderTest(): KeyValueBag + { + return new KeyValueBag(); + } + + public function testGetSetHasEntries() + { + $sut = $this->getSubjectUnderTest(); + + $this->assertFalse($sut->hasEntries()); + + $sut->set('foo', [1]); + + $this->assertEquals([1], $sut->get('foo')); + $this->assertTrue($sut->hasEntries()); + } + + public function testReset() + { + $sut = $this->getSubjectUnderTest(); + + $sut->set('foo', ['bar']); + + $this->assertTrue($sut->hasEntries()); + + $sut->reset(); + + $this->assertFalse($sut->hasEntries()); + } +} diff --git a/tests/Integration/Log/LoggerPoolTest.php b/tests/Integration/Log/LoggerPoolTest.php new file mode 100644 index 0000000..110afe3 --- /dev/null +++ b/tests/Integration/Log/LoggerPoolTest.php @@ -0,0 +1,97 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tests\Unit\Log; + +use Exception; +use sweetrdf\InMemoryStoreSqlite\Log\Logger; +use sweetrdf\InMemoryStoreSqlite\Log\LoggerPool; +use Tests\TestCase; + +class LoggerPoolTest extends TestCase +{ + private function getSubjectUnderTest(): LoggerPool + { + return new LoggerPool(); + } + + public function testCreateNewLogger() + { + $this->assertEquals(new Logger(), $this->getSubjectUnderTest()->createNewLogger('test')); + } + + public function testGetLoggerInvalid() + { + $this->expectException(Exception::class); + $this->expectExceptionMessage('Invalid ID given.'); + + $this->getSubjectUnderTest()->getLogger('test'); + } + + public function testGetLogger() + { + $sut = $this->getSubjectUnderTest(); + $logger = $sut->createNewLogger('test'); + + $this->assertEquals(new Logger(), $logger); + $this->assertEquals($logger, $sut->getLogger('test')); + } + + public function testGetEntriesFromAllLoggerInstancesInvalid() + { + $this->expectException(Exception::class); + $this->expectExceptionMessage('Level invalid not set'); + + $sut = $this->getSubjectUnderTest(); + $sut->createNewLogger('1'); + + $sut->getEntriesFromAllLoggerInstances('invalid'); + } + + public function testGetEntriesFromAllLoggerInstances() + { + $sut = $this->getSubjectUnderTest(); + + $logger1 = $sut->createNewLogger('1'); + $logger1->error('error1'); + + $logger2 = $sut->createNewLogger('2'); + $logger2->warning('warning1'); + + $this->assertEquals(2, \count($sut->getEntriesFromAllLoggerInstances())); + $this->assertEquals(1, \count($sut->getEntriesFromAllLoggerInstances('error'))); + $this->assertEquals(1, \count($sut->getEntriesFromAllLoggerInstances('warning'))); + } + + public function testHasEntriesInAnyLoggerInstanceInvalid() + { + $this->expectException(Exception::class); + $this->expectExceptionMessage('Level invalid not set'); + + $sut = $this->getSubjectUnderTest(); + $sut->createNewLogger('1'); + + $sut->hasEntriesInAnyLoggerInstance('invalid'); + } + + public function testHasEntriesInAnyLoggerInstance() + { + $sut = $this->getSubjectUnderTest(); + + $sut->createNewLogger('1')->error('error1'); + $sut->createNewLogger('2')->warning('warning1'); + + $this->assertTrue($sut->hasEntriesInAnyLoggerInstance()); + $this->assertTrue($sut->hasEntriesInAnyLoggerInstance('error')); + $this->assertTrue($sut->hasEntriesInAnyLoggerInstance('warning')); + } +} diff --git a/tests/Integration/PDOSQLiteAdapterTest.php b/tests/Integration/PDOSQLiteAdapterTest.php new file mode 100644 index 0000000..17158c9 --- /dev/null +++ b/tests/Integration/PDOSQLiteAdapterTest.php @@ -0,0 +1,196 @@ + + * (c) Benjamin Nowack + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tests\Integration; + +use Exception; +use sweetrdf\InMemoryStoreSqlite\PDOSQLiteAdapter; +use Tests\TestCase; + +class PDOSQLiteAdapterTest extends TestCase +{ + protected function setUp(): void + { + parent::setUp(); + + $this->subjectUnderTest = new PDOSQLiteAdapter(); + } + + /* + * Tests for connect + */ + + public function testConnectCreateNewConnection() + { + $this->subjectUnderTest->disconnect(); + + // do explicit reconnect + $this->subjectUnderTest = new PDOSQLiteAdapter(); + + $this->subjectUnderTest->exec('CREATE TABLE test (id INTEGER)'); + $this->assertEquals([], $this->subjectUnderTest->fetchList('SELECT * FROM test;')); + } + + public function testEscape() + { + $this->assertEquals('"hallo"', $this->subjectUnderTest->escape('"hallo"')); + } + + /* + * Tests for exec + */ + + public function testExec() + { + $this->subjectUnderTest->exec('CREATE TABLE users (id INTEGER, name TEXT NOT NULL)'); + $this->subjectUnderTest->exec('INSERT INTO users (id, name) VALUES (1, "foobar");'); + $this->subjectUnderTest->exec('INSERT INTO users (id, name) VALUES (2, "foobar2");'); + + $this->assertEquals(2, $this->subjectUnderTest->exec('DELETE FROM users;')); + } + + /* + * Tests for fetchRow + */ + + public function testFetchRow() + { + // valid query + $this->subjectUnderTest->exec('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL)'); + $this->assertFalse($this->subjectUnderTest->fetchRow('SELECT * FROM users')); + + // add data + $this->subjectUnderTest->exec('INSERT INTO users (id, name) VALUES (1, "foobar");'); + $this->assertEquals( + [ + 'id' => 1, + 'name' => 'foobar', + ], + $this->subjectUnderTest->fetchRow('SELECT * FROM users WHERE id = 1;') + ); + } + + /* + * Tests for fetchList + */ + + public function testFetchList() + { + // valid query + $sql = 'CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL)'; + $this->subjectUnderTest->exec($sql); + $this->assertEquals([], $this->subjectUnderTest->fetchList('SELECT * FROM users')); + + // add data + $this->subjectUnderTest->exec('INSERT INTO users (id, name) VALUES (1, "foobar");'); + $this->assertEquals( + [ + [ + 'id' => 1, + 'name' => 'foobar', + ], + ], + $this->subjectUnderTest->fetchList('SELECT * FROM users') + ); + } + + public function testGetPDO() + { + $this->assertTrue($this->subjectUnderTest->getPDO() instanceof \PDO); + } + + /* + * Tests for getNumberOfRows + */ + + public function testGetNumberOfRows() + { + // create test table + $this->subjectUnderTest->exec('CREATE TABLE pet (name TEXT)'); + $this->subjectUnderTest->exec('INSERT INTO pet VALUES ("cat")'); + $this->subjectUnderTest->exec('INSERT INTO pet VALUES ("dog")'); + + $this->assertEquals(2, $this->subjectUnderTest->getNumberOfRows('SELECT * FROM pet;')); + } + + public function testGetNumberOfRowsInvalidQuery() + { + $this->expectException('Exception'); + + $this->subjectUnderTest->getNumberOfRows('SHOW TABLES of x'); + } + + /* + * Tests for getServerVersion + */ + + public function testGetServerVersion() + { + // check server version + $this->assertEquals( + 1, + preg_match('/[0-9]{1,}\.[0-9]{1,}\.[0-9]{1,}/', + 'Found: '.$this->subjectUnderTest->getServerVersion()) + ); + } + + /* + * Tests for insert + */ + + public function testInsert() + { + // create test table + $this->subjectUnderTest->exec('CREATE TABLE pet (name TEXT)'); + + $this->subjectUnderTest->insert('pet', ['name' => 'test1']); + $this->subjectUnderTest->insert('pet', ['name' => 'test2']); + + $this->assertEquals(2, $this->subjectUnderTest->getNumberOfRows('SELECT * FROM pet;')); + } + + public function testInsertTableNameSpecialChars() + { + $this->expectException(Exception::class); + $this->expectExceptionMessage('Invalid table name given.'); + + // create test table + $this->subjectUnderTest->exec('CREATE TABLE pet (name TEXT)'); + + $this->subjectUnderTest->insert('pet"', ['name' => 'test1']); + } + + public function testQuery() + { + // valid query + $sql = 'CREATE TABLE MyGuests (id INTEGER PRIMARY KEY AUTOINCREMENT)'; + $this->subjectUnderTest->exec($sql); + + $foundTable = false; + foreach ($this->subjectUnderTest->getAllTables() as $table) { + if ('MyGuests' == $table) { + $foundTable = true; + break; + } + } + $this->assertTrue($foundTable, 'Expected table not found.'); + } + + public function testQueryInvalid() + { + $this->expectException('Exception'); + + // invalid query + $this->assertFalse($this->subjectUnderTest->simpleQuery('invalid query')); + } +} diff --git a/tests/Integration/Store/InMemoryStoreSqlite/Query/AskQueryOutputInstancesTest.php b/tests/Integration/Store/InMemoryStoreSqlite/Query/AskQueryOutputInstancesTest.php new file mode 100644 index 0000000..bba1d19 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/Query/AskQueryOutputInstancesTest.php @@ -0,0 +1,49 @@ + + * (c) Benjamin Nowack + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tests\Integration\Store\InMemoryStoreSqlite\Query; + +use sweetrdf\InMemoryStoreSqlite\Rdf\Literal; +use sweetrdf\InMemoryStoreSqlite\Store\InMemoryStoreSqlite; +use Tests\TestCase; + +/** + * Tests for query method - focus on ASK queries. + * + * Output format is: instances + */ +class AskQueryOutputInstancesTest extends TestCase +{ + protected function setUp(): void + { + parent::setUp(); + + $this->subjectUnderTest = InMemoryStoreSqlite::createInstance(); + } + + public function testAsk() + { + // test data + $this->subjectUnderTest->query('INSERT INTO { + "baz" . + }'); + + $sparql = 'ASK FROM { ?o.}'; + $result = $this->subjectUnderTest->query($sparql, 'instances'); + $this->assertEquals(new Literal(true), $result); + + $sparql = 'ASK FROM { ?o.}'; + $result = $this->subjectUnderTest->query($sparql, 'instances'); + $this->assertEquals(new Literal(false), $result); + } +} diff --git a/tests/Integration/Store/InMemoryStoreSqlite/Query/AskQueryTest.php b/tests/Integration/Store/InMemoryStoreSqlite/Query/AskQueryTest.php new file mode 100644 index 0000000..498f1be --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/Query/AskQueryTest.php @@ -0,0 +1,64 @@ + + * (c) Benjamin Nowack + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tests\Integration\Store\InMemoryStoreSqlite\Query; + +use sweetrdf\InMemoryStoreSqlite\Store\InMemoryStoreSqlite; +use Tests\TestCase; + +/** + * Tests for query method - focus on ASK queries. + */ +class AskQueryTest extends TestCase +{ + protected function setUp(): void + { + parent::setUp(); + + $this->subjectUnderTest = InMemoryStoreSqlite::createInstance(); + } + + public function testAskDefaultGraph() + { + // test data + $this->subjectUnderTest->query('INSERT INTO { + "baz" . + }'); + + $res = $this->subjectUnderTest->query('ASK { ?o.}'); + $this->assertEquals( + [ + 'query_type' => 'ask', + 'result' => true, + ], + $res + ); + } + + public function testAskGraphSpecified() + { + // test data + $this->subjectUnderTest->query('INSERT INTO { + "baz" . + }'); + + $res = $this->subjectUnderTest->query('ASK FROM { ?o.}'); + $this->assertEquals( + [ + 'query_type' => 'ask', + 'result' => true, + ], + $res + ); + } +} diff --git a/tests/Integration/Store/InMemoryStoreSqlite/Query/DeleteQueryTest.php b/tests/Integration/Store/InMemoryStoreSqlite/Query/DeleteQueryTest.php new file mode 100644 index 0000000..d4414fa --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/Query/DeleteQueryTest.php @@ -0,0 +1,147 @@ + + * (c) Benjamin Nowack + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tests\Integration\Store\InMemoryStoreSqlite\Query; + +use sweetrdf\InMemoryStoreSqlite\Store\InMemoryStoreSqlite; +use Tests\TestCase; + +/** + * Tests for query method - focus on DELETE queries. + */ +class DeleteQueryTest extends TestCase +{ + protected function setUp(): void + { + parent::setUp(); + + $this->subjectUnderTest = InMemoryStoreSqlite::createInstance(); + } + + protected function runSPOQuery($g = null) + { + return null == $g + ? $this->subjectUnderTest->query('SELECT * WHERE {?s ?p ?o.}') + : $this->subjectUnderTest->query('SELECT * FROM <'.$g.'> WHERE {?s ?p ?o.}'); + } + + public function testDelete() + { + // test data + $this->subjectUnderTest->query('INSERT INTO { + "baz" . + }'); + $this->subjectUnderTest->query('INSERT INTO { + "bar" . + }'); + + $this->assertEquals(2, \count($this->runSPOQuery()['result']['rows'])); + + $this->subjectUnderTest->query('DELETE { ?p ?o .}'); + + $this->assertEquals(0, \count($this->runSPOQuery()['result']['rows'])); + } + + public function testDelete2() + { + // test data + $this->subjectUnderTest->query('INSERT INTO { + "baz" . + }'); + $this->subjectUnderTest->query('INSERT INTO { + "bar" . + }'); + + $this->assertEquals(2, \count($this->runSPOQuery()['result']['rows'])); + + $this->subjectUnderTest->query('DELETE { ?o .}'); + + $this->assertEquals(1, \count($this->runSPOQuery()['result']['rows'])); + } + + public function testDeleteAGraph() + { + // test data + $this->subjectUnderTest->query('INSERT INTO { + "baz" . + }'); + + $this->assertEquals(1, \count($this->runSPOQuery()['result']['rows'])); + + $this->subjectUnderTest->query('DELETE FROM '); + + $this->assertEquals(0, \count($this->runSPOQuery()['result']['rows'])); + } + + public function testDeleteWhere1() + { + // test data + $this->subjectUnderTest->query('INSERT INTO { + 1, 2 . + 1, 2 . + rdf:type . + }'); + + $this->assertEquals(5, \count($this->runSPOQuery()['result']['rows'])); + + $this->subjectUnderTest->query('DELETE { + 1, 2 . + } WHERE { + 1, 2 . + }'); + + $this->assertEquals(3, \count($this->runSPOQuery()['result']['rows'])); + } + + public function testDeleteWhereWithBlankNode() + { + // test data + $this->subjectUnderTest->query('INSERT INTO { + _:a ; + . + }'); + + $this->assertEquals(2, \count($this->runSPOQuery()['result']['rows'])); + + $this->subjectUnderTest->query('DELETE { + _:a ?p ?o . + } WHERE { + _:a . + }'); + + // first we check the expected behavior and afterwards skip to notice the + // developer about it. + $this->assertEquals(2, \count($this->runSPOQuery()['result']['rows'])); + $this->markTestSkipped('DELETE queries with blank nodes are not working.'); + } + + public function testDeleteFromWhere() + { + // test data + $this->subjectUnderTest->query('INSERT INTO { + 1, 2 . + 1, 2 . + rdf:type . + }'); + + $this->assertEquals(5, \count($this->runSPOQuery('http://example.com/1')['result']['rows'])); + + $this->subjectUnderTest->query('DELETE FROM { + 1, 2 . + } WHERE { + 1, 2 . + }'); + + $this->assertEquals(3, \count($this->runSPOQuery('http://example.com/1')['result']['rows'])); + } +} diff --git a/tests/Integration/Store/InMemoryStoreSqlite/Query/DescribeQueryTest.php b/tests/Integration/Store/InMemoryStoreSqlite/Query/DescribeQueryTest.php new file mode 100644 index 0000000..0b84d06 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/Query/DescribeQueryTest.php @@ -0,0 +1,108 @@ + + * (c) Benjamin Nowack + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tests\Integration\Store\InMemoryStoreSqlite\Query; + +use sweetrdf\InMemoryStoreSqlite\Store\InMemoryStoreSqlite; +use Tests\TestCase; + +/** + * Tests for query method - focus on DESCRIBE queries. + */ +class DescribeQueryTest extends TestCase +{ + protected function setUp(): void + { + parent::setUp(); + + $this->subjectUnderTest = InMemoryStoreSqlite::createInstance(); + } + + public function testDescribeDefaultGraph() + { + // test data + $this->subjectUnderTest->query('INSERT INTO { + "baz" . + }'); + + $res = $this->subjectUnderTest->query('DESCRIBE '); + $this->assertEquals( + [ + 'query_type' => 'describe', + 'result' => [ + 'http://s' => [ + 'http://p1' => [ + [ + 'value' => 'baz', + 'type' => 'literal', + ], + ], + ], + ], + ], + $res + ); + } + + public function testDescribeWhereDefaultGraph() + { + // test data + $this->subjectUnderTest->query('INSERT INTO { + "baz" . + }'); + + $res = $this->subjectUnderTest->query('DESCRIBE ?s WHERE {?s ?p "baz".}'); + $this->assertEquals( + [ + 'query_type' => 'describe', + 'result' => [ + 'http://s' => [ + 'http://p1' => [ + [ + 'value' => 'baz', + 'type' => 'literal', + ], + ], + ], + ], + ], + $res + ); + } + + public function testDescribeWhereDefaultGraph2() + { + // test data + $this->subjectUnderTest->query('INSERT INTO { + "baz" . + }'); + + $res = $this->subjectUnderTest->query('DESCRIBE * WHERE {?s ?p "baz".}'); + $this->assertEquals( + [ + 'query_type' => 'describe', + 'result' => [ + 'http://s' => [ + 'http://p1' => [ + [ + 'value' => 'baz', + 'type' => 'literal', + ], + ], + ], + ], + ], + $res + ); + } +} diff --git a/tests/Integration/Store/InMemoryStoreSqlite/Query/ErrorHandlingInQueriesTest.php b/tests/Integration/Store/InMemoryStoreSqlite/Query/ErrorHandlingInQueriesTest.php new file mode 100644 index 0000000..58bc674 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/Query/ErrorHandlingInQueriesTest.php @@ -0,0 +1,60 @@ + + * (c) Benjamin Nowack + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tests\Integration\Store\InMemoryStoreSqlite\Query; + +use sweetrdf\InMemoryStoreSqlite\Store\InMemoryStoreSqlite; +use Tests\TestCase; + +/** + * Tests for query method - focus on how the system reacts, when errors occur. + */ +class ErrorHandlingInQueriesTest extends TestCase +{ + protected function setUp(): void + { + parent::setUp(); + + $this->subjectUnderTest = InMemoryStoreSqlite::createInstance(); + } + + /** + * What if a result variable is not used in query. + */ + public function testResultVariableNotUsedInQuery() + { + $res = $this->subjectUnderTest->query(' + SELECT ?not_used_in_query ?s WHERE { + ?s ?p ?o . + } + '); + + $this->assertEquals( + [ + 'query_type' => 'select', + 'result' => [ + 'variables' => [ + 'not_used_in_query', 's', + ], + 'rows' => [ + ], + ], + ], + $res + ); + + // TODO not bad if count is higher than 2 + $errors = \count($this->subjectUnderTest->getLoggerPool()->getEntriesFromAllLoggerInstances()); + $this->assertEquals(2, $errors); + } +} diff --git a/tests/Integration/Store/InMemoryStoreSqlite/Query/InsertIntoQueryTest.php b/tests/Integration/Store/InMemoryStoreSqlite/Query/InsertIntoQueryTest.php new file mode 100644 index 0000000..4a5ac12 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/Query/InsertIntoQueryTest.php @@ -0,0 +1,654 @@ + + * (c) Benjamin Nowack + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tests\Integration\Store\InMemoryStoreSqlite\Query; + +use Exception; +use sweetrdf\InMemoryStoreSqlite\NamespaceHelper; +use sweetrdf\InMemoryStoreSqlite\Store\InMemoryStoreSqlite; +use Tests\TestCase; + +/** + * Tests for query method - focus on INSERT INTO queries. + */ +class InsertIntoQueryTest extends TestCase +{ + protected function setUp(): void + { + parent::setUp(); + + $this->subjectUnderTest = InMemoryStoreSqlite::createInstance(); + } + + public function testInsertInto() + { + // test data + $this->subjectUnderTest->query('INSERT INTO { + "baz" . + }'); + + $res = $this->subjectUnderTest->query('SELECT * FROM {?s ?p ?o.}'); + $this->assertEquals(1, \count($res['result']['rows'])); + } + + public function testInsertIntoUriTriple() + { + // test data + $this->subjectUnderTest->query('INSERT INTO { .}'); + + $res = $this->subjectUnderTest->query('SELECT * FROM {?s ?p ?o.}'); + $this->assertEquals( + [ + [ + 's' => 'http://s', + 's type' => 'uri', + 'p' => 'http://p', + 'p type' => 'uri', + 'o' => 'http://o', + 'o type' => 'uri', + ], + ], + $res['result']['rows'] + ); + } + + public function testInsertIntoShortenedUri() + { + // test data + $this->subjectUnderTest->query('INSERT INTO { <#make> <#me> <#happy> .}'); + + $res = $this->subjectUnderTest->query('SELECT * FROM {?s ?p ?o.}'); + $this->assertEquals( + [ + [ + 's' => NamespaceHelper::BASE_NAMESPACE.'#make', + 's type' => 'uri', + 'p' => NamespaceHelper::BASE_NAMESPACE.'#me', + 'p type' => 'uri', + 'o' => NamespaceHelper::BASE_NAMESPACE.'#happy', + 'o type' => 'uri', + ], + ], + $res['result']['rows'] + ); + } + + public function testInsertIntoPrefixedUri() + { + // test data + $query = ' + PREFIX ex: + INSERT INTO { rdf:type ex:Person .} + '; + $this->subjectUnderTest->query($query); + + $res = $this->subjectUnderTest->query('SELECT * FROM {?s ?p ?o.}'); + $this->assertEquals( + [ + [ + 's' => 'http://s', + 's type' => 'uri', + 'p' => 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', + 'p type' => 'uri', + 'o' => 'http://ex/Person', + 'o type' => 'uri', + ], + ], + $res['result']['rows'] + ); + } + + public function testInsertIntoNumbers() + { + // test data + $this->subjectUnderTest->query('INSERT INTO { + 1 . + 2.0 . + "3" . + }'); + + $res = $this->subjectUnderTest->query('SELECT * FROM {?s ?p ?o.}'); + $this->assertEquals( + [ + [ + 's' => 'http://s', + 's type' => 'uri', + 'p' => 'http://foo', + 'p type' => 'uri', + 'o' => '1', + 'o type' => 'literal', + 'o datatype' => 'http://www.w3.org/2001/XMLSchema#integer', + ], + [ + 's' => 'http://s', + 's type' => 'uri', + 'p' => 'http://foo', + 'p type' => 'uri', + 'o' => '2.0', + 'o type' => 'literal', + 'o datatype' => 'http://www.w3.org/2001/XMLSchema#decimal', + ], + [ + 's' => 'http://s', + 's type' => 'uri', + 'p' => 'http://foo', + 'p type' => 'uri', + 'o' => '3', + 'o type' => 'literal', + ], + ], + $res['result']['rows'] + ); + } + + public function testInsertIntoObjectWithDatatype() + { + // test data + $this->subjectUnderTest->query('INSERT INTO { + "4"^^xsd:integer . + }'); + + $res = $this->subjectUnderTest->query('SELECT * FROM {?s ?p ?o.}'); + $this->assertEquals( + [ + [ + 's' => 'http://s', + 's type' => 'uri', + 'p' => 'http://foo', + 'p type' => 'uri', + 'o' => '4', + 'o type' => 'literal', + 'o datatype' => 'http://www.w3.org/2001/XMLSchema#integer', + ], + ], + $res['result']['rows'] + ); + } + + public function testInsertIntoObjectWithLanguage() + { + // test data + $this->subjectUnderTest->query('INSERT INTO { + "5"@en . + }'); + + $res = $this->subjectUnderTest->query('SELECT * FROM {?s ?p ?o.}'); + $this->assertEquals( + [ + [ + 's' => 'http://s', + 's type' => 'uri', + 'p' => 'http://foo', + 'p type' => 'uri', + 'o' => '5', + 'o type' => 'literal', + 'o lang' => 'en', + ], + ], + $res['result']['rows'] + ); + } + + public function testInsertIntoBlankNode1() + { + // test data + $this->subjectUnderTest->query('INSERT INTO { + _:foo "6" . + }'); + + $res = $this->subjectUnderTest->query('SELECT * FROM {?s ?p ?o.}'); + $this->assertEquals( + [ + [ + 's' => $res['result']['rows'][0]['s'], // blank node ID is dynamic + 's type' => 'bnode', + 'p' => 'http://foo', + 'p type' => 'uri', + 'o' => '6', + 'o type' => 'literal', + ], + ], + $res['result']['rows'] + ); + } + + public function testInsertIntoBlankNode2() + { + // test data + $this->subjectUnderTest->query('INSERT INTO { + [ + + ] . + }'); + + $res = $this->subjectUnderTest->query('SELECT * FROM {?s ?p ?o.}'); + + // because bnode ID is random, we check only its structure + $this->assertTrue(isset($res['result']['rows'][0])); + $this->assertEquals(1, preg_match('/_:[a-z0-9]+_[a-z0-9]+/', $res['result']['rows'][0]['o'])); + + $this->assertEquals( + [ + [ + 's' => 'http://s', + 's type' => 'uri', + 'p' => 'http://p1', + 'p type' => 'uri', + 'o' => $res['result']['rows'][0]['o'], + 'o type' => 'bnode', + ], + [ + 's' => $res['result']['rows'][0]['o'], + 's type' => 'bnode', + 'p' => 'http://foo', + 'p type' => 'uri', + 'o' => 'http://bar', + 'o type' => 'uri', + ], + ], + $res['result']['rows'] + ); + } + + public function testInsertIntoBlankNode3() + { + // test data + $this->subjectUnderTest->query(' + PREFIX ex: + INSERT INTO { + ex:3 ex:action [ + ex:query ; + ex:data + ] . + } + '); + + $res = $this->subjectUnderTest->query('SELECT * FROM {?s ?p ?o.}'); + + $this->assertEquals( + [ + [ + 's' => 'http://ex/3', + 's type' => 'uri', + 'p' => 'http://ex/action', + 'p type' => 'uri', + 'o' => $res['result']['rows'][0]['o'], + 'o type' => 'bnode', + ], + [ + 's' => $res['result']['rows'][0]['o'], + 's type' => 'bnode', + 'p' => 'http://ex/query', + 'p type' => 'uri', + 'o' => NamespaceHelper::BASE_NAMESPACE.'agg-avg-01.rq', + 'o type' => 'uri', + ], + [ + 's' => $res['result']['rows'][0]['o'], + 's type' => 'bnode', + 'p' => 'http://ex/data', + 'p type' => 'uri', + 'o' => NamespaceHelper::BASE_NAMESPACE.'agg-numeric.ttl', + 'o type' => 'uri', + ], + ], + $res['result']['rows'] + ); + } + + public function testInsertIntoDate() + { + // test data + $this->subjectUnderTest->query('INSERT INTO { + "2009-05-28T18:03:38+09:00" . + "2009-05-28T18:03:38+09:00GMT" . + "21 August 2007" . + }'); + + $res = $this->subjectUnderTest->query('SELECT * FROM {?s ?p ?o.}'); + + $this->assertEquals( + [ + 'query_type' => 'select', + 'result' => [ + 'variables' => ['s', 'p', 'o'], + 'rows' => [ + [ + 's' => 'http://s', + 's type' => 'uri', + 'p' => 'http://p1', + 'p type' => 'uri', + 'o' => '2009-05-28T18:03:38+09:00', + 'o type' => 'literal', + ], + [ + 's' => 'http://s', + 's type' => 'uri', + 'p' => 'http://p1', + 'p type' => 'uri', + 'o' => '2009-05-28T18:03:38+09:00GMT', + 'o type' => 'literal', + ], + [ + 's' => 'http://s', + 's type' => 'uri', + 'p' => 'http://p1', + 'p type' => 'uri', + 'o' => '21 August 2007', + 'o type' => 'literal', + ], + ], + ], + ], + $res + ); + } + + public function testInsertIntoList() + { + // test data + $this->subjectUnderTest->query('INSERT INTO { + 1, 2, 3 . + }'); + + $res = $this->subjectUnderTest->query('SELECT * FROM {?s ?p ?o.}'); + + $this->assertEquals( + [ + [ + 's' => 'http://s', + 's type' => 'uri', + 'p' => 'http://p1', + 'p type' => 'uri', + 'o' => '1', + 'o type' => 'literal', + 'o datatype' => 'http://www.w3.org/2001/XMLSchema#integer', + ], + [ + 's' => 'http://s', + 's type' => 'uri', + 'p' => 'http://p1', + 'p type' => 'uri', + 'o' => '2', + 'o type' => 'literal', + 'o datatype' => 'http://www.w3.org/2001/XMLSchema#integer', + ], + [ + 's' => 'http://s', + 's type' => 'uri', + 'p' => 'http://p1', + 'p type' => 'uri', + 'o' => '3', + 'o type' => 'literal', + 'o datatype' => 'http://www.w3.org/2001/XMLSchema#integer', + ], + ], + $res['result']['rows'] + ); + } + + /** + * Demonstrates that store can't save long values. + */ + public function testInsertIntoLongValue() + { + // create long URI (ca. 250 chars) + $longURI = 'http://'.hash('sha512', 'long') + .hash('sha512', 'URI'); + + $this->expectException(Exception::class); + + // test data + $this->subjectUnderTest->query('INSERT INTO { + <'.$longURI.'/s> <'.$longURI.'/p> <'.$longURI.'/o> ; + <'.$longURI.'/p2> <'.$longURI.'/o2> . + '); + } + + public function testInsertIntoListMoreComplex() + { + // test data + $this->subjectUnderTest->query('INSERT INTO { + _:b0 rdf:first 1 ; + rdf:rest _:b1 . + _:b1 rdf:first 2 ; + rdf:rest _:b2 . + _:b2 rdf:first 3 ; + rdf:rest rdf:nil . + }'); + + $res = $this->subjectUnderTest->query('SELECT * FROM {?s ?p ?o.}'); + + $this->assertEquals( + [ + [ + 's' => $res['result']['rows'][0]['s'], + 's type' => 'bnode', + 'p' => 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', + 'p type' => 'uri', + 'o' => '1', + 'o type' => 'literal', + 'o datatype' => 'http://www.w3.org/2001/XMLSchema#integer', + ], + [ + 's' => $res['result']['rows'][1]['s'], + 's type' => 'bnode', + 'p' => 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', + 'p type' => 'uri', + 'o' => $res['result']['rows'][1]['o'], + 'o type' => 'bnode', + ], + [ + 's' => $res['result']['rows'][2]['s'], + 's type' => 'bnode', + 'p' => 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', + 'p type' => 'uri', + 'o' => '2', + 'o type' => 'literal', + 'o datatype' => 'http://www.w3.org/2001/XMLSchema#integer', + ], + [ + 's' => $res['result']['rows'][3]['s'], + 's type' => 'bnode', + 'p' => 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', + 'p type' => 'uri', + 'o' => $res['result']['rows'][3]['o'], + 'o type' => 'bnode', + ], + [ + 's' => $res['result']['rows'][4]['s'], + 's type' => 'bnode', + 'p' => 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', + 'p type' => 'uri', + 'o' => '3', + 'o type' => 'literal', + 'o datatype' => 'http://www.w3.org/2001/XMLSchema#integer', + ], + [ + 's' => $res['result']['rows'][5]['s'], + 's type' => 'bnode', + 'p' => 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', + 'p type' => 'uri', + 'o' => 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil', + 'o type' => 'uri', + ], + ], + $res['result']['rows'] + ); + } + + public function testInsertIntoConstruct() + { + // test data + $this->subjectUnderTest->query('INSERT INTO CONSTRUCT { + "Leipzig" . + "Grimma" . + }'); + + $res = $this->subjectUnderTest->query('SELECT * FROM {?s ?p ?o.}'); + $this->assertEquals(2, \count($res['result']['rows'])); + } + + public function testInsertIntoWhere() + { + // test data + $this->subjectUnderTest->query('INSERT INTO CONSTRUCT { + "Leipzig" . + "Grimma" . + } WHERE { + ?s "Leipzig" . + }'); + + // we expect that 1 element gets added to the store, because of the WHERE clause. + // but store added none. + $res = $this->subjectUnderTest->query('SELECT * FROM {?s ?p ?o.}'); + $this->assertEquals(2, \count($res['result']['rows'])); + + $this->markTestSkipped( + 'Store does not check the WHERE clause when inserting data.' + .' Too many triples were added.' + .\PHP_EOL + .\PHP_EOL.'FYI: https://www.w3.org/Submission/SPARQL-Update/#sec_examples and ' + .\PHP_EOL.'https://github.com/semsol/arc2/wiki/SPARQL-#insert-example' + ); + } + + public function testInsertInto2GraphsSameTriples() + { + /* + * Test behavior if same triple get inserted into two different graphs: + * 1. add + * 2. check additions + * 3. delete graph2 content + * 4. check again + */ + + $triple = ' "Leipzig" .'; + $this->subjectUnderTest->query('INSERT INTO {'.$triple.'}'); + $this->subjectUnderTest->query('INSERT INTO {'.$triple.'}'); + + // check additions (graph1) + $res = $this->subjectUnderTest->query('SELECT * FROM {?s ?p ?o.}'); + $this->assertEquals(1, \count($res['result']['rows'])); + + // check additions (graph2) + $res = $this->subjectUnderTest->query('SELECT * FROM {?s ?p ?o.}'); + $this->assertEquals(1, \count($res['result']['rows'])); + + /* + * test isolation by removing the triple from graph2 + */ + $this->subjectUnderTest->query('DELETE FROM '); + + // check triples (graph1) + $res = $this->subjectUnderTest->query('SELECT * FROM {?s ?p ?o.}'); + $this->assertEquals(1, \count($res['result']['rows'])); + + // check triples (graph2) + $res = $this->subjectUnderTest->query('SELECT * FROM {?s ?p ?o.}'); + $this->assertEquals(0, \count($res['result']['rows'])); + } + + /** + * Tests old behavior of ARC2 store: its SQLite in-memory implementation was not able + * to recognize all triples added by separate query calls. + */ + public function testMultipleInsertsSameStore() + { + // add triples in separate query calls + $this->subjectUnderTest->query('INSERT INTO { . }'); + $this->subjectUnderTest->query('INSERT INTO { "c2"@de. }'); + $this->subjectUnderTest->query('INSERT INTO { "c3"^^xsd:string . }'); + + // check result + $res = $this->subjectUnderTest->query('SELECT * FROM WHERE {?s ?p ?o.}'); + + $this->assertEquals(3, \count($res['result']['rows'])); + + $this->assertEquals( + [ + [ + 's' => 'http://a', + 's type' => 'uri', + 'p' => 'http://b', + 'p type' => 'uri', + 'o' => 'http://c', + 'o type' => 'uri', + ], + [ + 's' => 'http://a2', + 's type' => 'uri', + 'p' => 'http://b2', + 'p type' => 'uri', + 'o' => 'c2', + 'o type' => 'literal', + 'o lang' => 'de', + ], + [ + 's' => 'http://a3', + 's type' => 'uri', + 'p' => 'http://b3', + 'p type' => 'uri', + 'o' => 'c3', + 'o type' => 'literal', + 'o datatype' => 'http://www.w3.org/2001/XMLSchema#string', + ], + ], + $res['result']['rows'] + ); + } + + public function testMultipleInsertQueriesInDifferentGraphs() + { + $this->subjectUnderTest->query('INSERT INTO { . }'); + $this->subjectUnderTest->query('INSERT INTO { . }'); + $this->subjectUnderTest->query('INSERT INTO { . }'); + + $res = $this->subjectUnderTest->query('SELECT * FROM WHERE {?s ?p ?o.}'); + $this->assertEquals(1, \count($res['result']['rows'])); + + $res = $this->subjectUnderTest->query('SELECT * FROM WHERE {?s ?p ?o.}'); + $this->assertEquals(2, \count($res['result']['rows'])); + + $res = $this->subjectUnderTest->query('SELECT * WHERE {?s ?p ?o.}'); + $this->assertEquals(3, \count($res['result']['rows'])); + } + + /** + * Adds bulk of triples to test behavior. + * May take at least one second to finish. + */ + public function testAdditionOfManyTriples() + { + $amount = 3000; + + $startTime = microtime(true); + + // add triples in separate query calls + for ($i = 0; $i < $amount; ++$i) { + $this->subjectUnderTest->query('INSERT INTO { + . + . + }'); + } + + // check result + $res = $this->subjectUnderTest->query('SELECT * FROM WHERE {?s ?p ?o.}'); + + $this->assertEquals($amount, \count($res['result']['rows'])); + + $timeUsed = microtime(true) - $startTime; + $info = 'Test took longer than expected: '.$timeUsed.' sec.'; + $this->assertTrue(2 > $timeUsed, $info); + } +} diff --git a/tests/Integration/Store/InMemoryStoreSqlite/Query/KnownNotWorkingSparqlQueriesTest.php b/tests/Integration/Store/InMemoryStoreSqlite/Query/KnownNotWorkingSparqlQueriesTest.php new file mode 100644 index 0000000..b08c68f --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/Query/KnownNotWorkingSparqlQueriesTest.php @@ -0,0 +1,182 @@ + + * (c) Benjamin Nowack + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tests\Integration\Store\InMemoryStoreSqlite\Query; + +use Exception; +use sweetrdf\InMemoryStoreSqlite\Store\InMemoryStoreSqlite; +use Tests\TestCase; + +/** + * Tests for query method - focus on queries which are known to fail. + */ +class KnownNotWorkingSparqlQueriesTest extends TestCase +{ + protected function setUp(): void + { + parent::setUp(); + + $this->subjectUnderTest = InMemoryStoreSqlite::createInstance(); + } + + /** + * Variable alias not working. + */ + public function testSelectAlias() + { + // test data + $this->subjectUnderTest->query('INSERT INTO { + "baz" . + }'); + + $this->expectException(Exception::class); + $this->subjectUnderTest->query(' + SELECT (?s AS ?s_alias) ?o FROM WHERE {?s ?o.} + '); + } + + /** + * FILTER: langMatches with *. + * + * Based on the specification (https://www.w3.org/TR/rdf-sparql-query/#func-langMatches) + * langMatches with * has to return all entries with no language set. + */ + public function testSelectFilterLangMatchesWithStar() + { + // test data + $this->subjectUnderTest->query('INSERT INTO { + "foo" . + "in de"@de . + "in en"@en . + }'); + + $res = $this->subjectUnderTest->query(' + SELECT ?s ?o WHERE { + ?s ?o . + FILTER langMatches (lang(?o), "*") + } + '); + $this->assertEquals( + [ + 'query_type' => 'select', + 'result' => [ + 'variables' => [ + 's', 'o', + ], + 'rows' => [], + ], + ], + $res + ); + } + + /** + * sameTerm. + */ + public function testSelectSameTerm() + { + $this->markTestSkipped( + 'Solving sameterm does not work properly. The result contains elements multiple times. ' + .\PHP_EOL.'Expected behavior is described here: https://www.w3.org/TR/rdf-sparql-query/#func-sameTerm' + ); + + /* + + demo code: + + // test data + $this->subjectUnderTest->query('INSERT INTO { + "100" . + "100" . + }'); + + $res = $this->subjectUnderTest->query('SELECT ?c1 ?c2 WHERE { + ?c1 ?weight ?w1. + + ?c2 ?weight ?w2. + + FILTER (sameTerm(?w1, ?w2)) + }'); + $this->assertEquals( + [ + 'query_type' => 'select', + 'result' => [ + 'variables' => [ + 'c1', 'c2', + ], + 'rows' => [ + [ + 'c1' => 'http://container1', + 'c1 type' => 'uri', + 'c2' => 'http://container1', + 'c2 type' => 'uri', + ], + [ + 'c1' => 'http://container2', + 'c1 type' => 'uri', + 'c2' => 'http://container1', + 'c2 type' => 'uri', + ], + [ + 'c1' => 'http://container1', + 'c1 type' => 'uri', + 'c2' => 'http://container2', + 'c2 type' => 'uri', + ], + [ + 'c1' => 'http://container2', + 'c1 type' => 'uri', + 'c2' => 'http://container2', + 'c2 type' => 'uri', + ], + ], + ], + ], + $res, + '', + 0, + 10, + true + ); + */ + } + + /** + * Sub Select. + */ + public function testSelectSubSelect() + { + // test data + $this->subjectUnderTest->query('INSERT INTO { + "1" . + "3" . + "2" . + + . + . + . + }'); + + $this->expectException(Exception::class); + $this->subjectUnderTest->query(' + SELECT * WHERE { + { + SELECT ?p WHERE { + ?p "1" . + } + } + ?p ?who . + } + '); + } +} diff --git a/tests/Integration/Store/InMemoryStoreSqlite/Query/README.md b/tests/Integration/Store/InMemoryStoreSqlite/Query/README.md new file mode 100644 index 0000000..9ef0cbf --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/Query/README.md @@ -0,0 +1,7 @@ +# Info + +These tests use query function to run queries of a certain type, like INSERT INTO. + +In `KnownNotWorkingSparqlQueriesTest` all queries can be found which are known for not working with this store. + +In `ErrorHandlingInQueriesTest` contains a few basic tests for error cases. diff --git a/tests/Integration/Store/InMemoryStoreSqlite/Query/SelectQueryOutputInstancesTest.php b/tests/Integration/Store/InMemoryStoreSqlite/Query/SelectQueryOutputInstancesTest.php new file mode 100644 index 0000000..3d6be6e --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/Query/SelectQueryOutputInstancesTest.php @@ -0,0 +1,69 @@ + + * (c) Benjamin Nowack + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tests\Integration\Store\InMemoryStoreSqlite\Query; + +use sweetrdf\InMemoryStoreSqlite\NamespaceHelper; +use sweetrdf\InMemoryStoreSqlite\Rdf\BlankNode; +use sweetrdf\InMemoryStoreSqlite\Rdf\Literal; +use sweetrdf\InMemoryStoreSqlite\Rdf\NamedNode; +use sweetrdf\InMemoryStoreSqlite\Store\InMemoryStoreSqlite; +use Tests\TestCase; + +/** + * Tests for query method - focus on SELECT queries. + * + * Output format is: instances + */ +class SelectQueryOutputInstancesTest extends TestCase +{ + protected function setUp(): void + { + parent::setUp(); + + $this->subjectUnderTest = InMemoryStoreSqlite::createInstance(); + } + + public function testSelect1() + { + // test data + $this->subjectUnderTest->query('INSERT INTO { + "baz" . + _:foo "baz"^^xsd:string . + _:foo2 "baz"@de . + }'); + + $result = $this->subjectUnderTest->query('SELECT * WHERE {?s ?p ?o.}', 'instances'); + + $this->assertEquals( + [ + [ + 's' => new NamedNode('http://s'), + 'p' => new NamedNode('http://p1'), + 'o' => new Literal('baz'), + ], + [ + 's' => new BlankNode($result[1]['s']->getValue()), // dynamic value + 'p' => new NamedNode('http://p1'), + 'o' => new Literal('baz', null, 'http://www.w3.org/2001/XMLSchema#string'), + ], + [ + 's' => new BlankNode($result[2]['s']->getValue()), // dynamic value + 'p' => new NamedNode(NamespaceHelper::BASE_NAMESPACE.'p2'), + 'o' => new Literal('baz', 'de'), + ], + ], + $result + ); + } +} diff --git a/tests/Integration/Store/InMemoryStoreSqlite/Query/SelectQueryTest.php b/tests/Integration/Store/InMemoryStoreSqlite/Query/SelectQueryTest.php new file mode 100644 index 0000000..ee77ada --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/Query/SelectQueryTest.php @@ -0,0 +1,1450 @@ + + * (c) Benjamin Nowack + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tests\Integration\Store\InMemoryStoreSqlite\Query; + +use Exception; +use sweetrdf\InMemoryStoreSqlite\Store\InMemoryStoreSqlite; +use Tests\TestCase; + +/** + * Tests for query method - focus on SELECT queries. + * + * Output format is: raw + */ +class SelectQueryTest extends TestCase +{ + protected function setUp(): void + { + parent::setUp(); + + $this->subjectUnderTest = InMemoryStoreSqlite::createInstance(); + } + + public function testSelectDefaultGraph() + { + // test data + $this->subjectUnderTest->query('INSERT INTO { + "baz" . + }'); + + $res = $this->subjectUnderTest->query('SELECT * WHERE { ?o.}'); + $this->assertEquals( + [ + 'query_type' => 'select', + 'result' => [ + 'variables' => [ + 'o', + ], + 'rows' => [ + [ + 'o' => 'baz', + 'o type' => 'literal', + ], + ], + ], + ], + $res + ); + } + + public function testSelectGraphSpecified() + { + // test data + $this->subjectUnderTest->query('INSERT INTO { + "baz" . + }'); + + $res = $this->subjectUnderTest->query('SELECT * FROM WHERE { ?o.}'); + $this->assertEquals( + [ + 'query_type' => 'select', + 'result' => [ + 'variables' => [ + 'o', + ], + 'rows' => [ + [ + 'o' => 'baz', + 'o type' => 'literal', + ], + ], + ], + ], + $res + ); + } + + // simulate a LEFT JOIN using OPTIONAL + public function testSelectLeftJoinUsingOptional() + { + // test data + $this->subjectUnderTest->query('INSERT INTO { + . + . + + . + . + + . + }'); + + $res = $this->subjectUnderTest->query(' + SELECT * WHERE { + ?s ?o . + OPTIONAL { + ?o ?o2 . + } + } + '); + $this->assertEquals( + [ + [ + 's' => 'http://a', + 's type' => 'uri', + 'o' => 'http://b', + 'o type' => 'uri', + ], + [ + 's' => 'http://a', + 's type' => 'uri', + 'o' => 'http://c', + 'o type' => 'uri', + ], + [ + 's' => 'http://b', + 's type' => 'uri', + 'o' => 'http://d', + 'o type' => 'uri', + ], + [ + 's' => 'http://b', + 's type' => 'uri', + 'o' => 'http://e', + 'o type' => 'uri', + ], + [ + 's' => 'http://c', + 's type' => 'uri', + 'o' => 'http://f', + 'o type' => 'uri', + ], + ], + $res['result']['rows'] + ); + } + + /* + * OPTIONAL, artifical query to extend coverage for store code. (SelectQueryHandler::sameOptional) + */ + public function testSelectOptional() + { + // test data + $this->subjectUnderTest->query('INSERT INTO { + . + }'); + + $res = $this->subjectUnderTest->query(' + SELECT * WHERE { + ?s ?o . + OPTIONAL { + ?o ?o2 . + } + OPTIONAL { + ?o ?o2 . + } + } + '); + $this->assertEquals( + [ + 'query_type' => 'select', + 'result' => [ + 'variables' => [ + 's', 'o', 'o2', + ], + 'rows' => [ + [ + 's' => 'http://s1', + 's type' => 'uri', + 'o' => 'http://s2', + 'o type' => 'uri', + ], + ], + ], + ], + $res + ); + } + + public function testSelectNoWhereClause() + { + // test data + $this->subjectUnderTest->query('INSERT INTO { + "baz" . + }'); + + $res = $this->subjectUnderTest->query('SELECT * FROM { ?o.}'); + $this->assertEquals( + [ + 'query_type' => 'select', + 'result' => [ + 'variables' => [ + 'o', + ], + 'rows' => [ + [ + 'o' => 'baz', + 'o type' => 'literal', + ], + ], + ], + ], + $res + ); + } + + /* + * FILTER + */ + + // bound: is variable set? + public function testSelectFilterBoundNotBounding() + { + // test data + $this->subjectUnderTest->query('INSERT INTO { + "foo" . + }'); + + $res = $this->subjectUnderTest->query(' + SELECT ?s ?o WHERE { + ?s ?o . + FILTER (bound(?o)) + } + '); + $this->assertEquals( + [ + 'query_type' => 'select', + 'result' => [ + 'variables' => [ + 's', 'o', + ], + 'rows' => [], + ], + ], + $res + ); + } + + // bound: is variable set? + public function testSelectFilterBoundVariableBounded() + { + // test data + $this->subjectUnderTest->query('INSERT INTO { + "foo" . + }'); + + $res = $this->subjectUnderTest->query(' + SELECT ?s ?o WHERE { + ?s ?o . + FILTER (bound(?o)) + } + '); + $this->assertEquals( + [ + 'query_type' => 'select', + 'result' => [ + 'variables' => [ + 's', 'o', + ], + 'rows' => [ + [ + 's' => 'http://s', + 's type' => 'uri', + 'o' => 'foo', + 'o type' => 'literal', + ], + ], + ], + ], + $res + ); + } + + // datatype + public function testSelectFilterDatatype() + { + // test data + $this->subjectUnderTest->query('INSERT INTO { + 3 . + }'); + + $res = $this->subjectUnderTest->query(' + SELECT ?s ?o WHERE { + ?s ?o . + FILTER (datatype(?o) = xsd:integer) + } + '); + $this->assertEquals( + [ + 'query_type' => 'select', + 'result' => [ + 'variables' => [ + 's', 'o', + ], + 'rows' => [ + [ + 's' => 'http://s', + 's type' => 'uri', + 'o' => '3', + 'o type' => 'literal', + 'o datatype' => 'http://www.w3.org/2001/XMLSchema#integer', + ], + ], + ], + ], + $res + ); + } + + // isBlank + public function testSelectFilterIsBlankFound() + { + // test data + $this->subjectUnderTest->query('INSERT INTO { + _:foo . + }'); + + $res = $this->subjectUnderTest->query(' + SELECT ?s ?o WHERE { + ?s ?o . + FILTER (isBlank(?o)) + } + '); + $this->assertEquals( + [ + 'query_type' => 'select', + 'result' => [ + 'variables' => [ + 's', 'o', + ], + 'rows' => [ + [ + 's' => 'http://s', + 's type' => 'uri', + 'o' => $res['result']['rows'][0]['o'], + 'o type' => 'bnode', + ], + ], + ], + ], + $res + ); + } + + // isBlank + public function testSelectFilterIsBlankNotFound() + { + // test data + $this->subjectUnderTest->query('INSERT INTO { + . + }'); + + $res = $this->subjectUnderTest->query(' + SELECT ?s ?o WHERE { + ?s ?o . + FILTER (isBlank(?o)) + } + '); + $this->assertEquals( + [ + 'query_type' => 'select', + 'result' => [ + 'variables' => [ + 's', 'o', + ], + 'rows' => [], + ], + ], + $res + ); + } + + // isIri + public function testSelectFilterIsIriFound() + { + // test data + $this->subjectUnderTest->query('INSERT INTO { + . + }'); + + $res = $this->subjectUnderTest->query(' + SELECT ?s ?o WHERE { + ?s ?o . + FILTER (isIri(?o)) + } + '); + $this->assertEquals( + [ + 'query_type' => 'select', + 'result' => [ + 'variables' => [ + 's', 'o', + ], + 'rows' => [ + [ + 's' => 'http://s', + 's type' => 'uri', + 'o' => 'urn:id', + 'o type' => 'uri', + ], + ], + ], + ], + $res + ); + } + + // isIri + public function testSelectFilterIsIriNotFound() + { + // test data + $this->subjectUnderTest->query('INSERT INTO { + "foo" . + }'); + + $res = $this->subjectUnderTest->query(' + SELECT ?s ?o WHERE { + ?s ?o . + FILTER (isIri(?o)) + } + '); + $this->assertEquals( + [ + 'query_type' => 'select', + 'result' => [ + 'variables' => [ + 's', 'o', + ], + 'rows' => [], + ], + ], + $res + ); + } + + // isLiteral + public function testSelectFilterIsLiteralFound() + { + // test data + $this->subjectUnderTest->query('INSERT INTO { + "foo" . + }'); + + $res = $this->subjectUnderTest->query(' + SELECT ?s ?o WHERE { + ?s ?o . + FILTER (isLiteral(?o)) + } + '); + $this->assertEquals( + [ + 'query_type' => 'select', + 'result' => [ + 'variables' => [ + 's', 'o', + ], + 'rows' => [ + [ + 's' => 'http://s', + 's type' => 'uri', + 'o' => 'foo', + 'o type' => 'literal', + ], + ], + ], + ], + $res + ); + } + + // isLiteral + public function testSelectFilterIsLiteralNotFound() + { + // test data + $this->subjectUnderTest->query('INSERT INTO { + . + }'); + + $res = $this->subjectUnderTest->query(' + SELECT ?s ?o WHERE { + ?s ?o . + FILTER (isLiteral(?o)) + } + '); + $this->assertEquals( + [ + 'query_type' => 'select', + 'result' => [ + 'variables' => [ + 's', 'o', + ], + 'rows' => [], + ], + ], + $res + ); + } + + // isUri + public function testSelectFilterIsUriFound() + { + // test data + $this->subjectUnderTest->query('INSERT INTO { + . + }'); + + $res = $this->subjectUnderTest->query(' + SELECT ?s ?o WHERE { + ?s ?o . + FILTER (isUri(?o)) + } + '); + $this->assertEquals( + [ + 'query_type' => 'select', + 'result' => [ + 'variables' => [ + 's', 'o', + ], + 'rows' => [ + [ + 's' => 'http://s', + 's type' => 'uri', + 'o' => 'urn:id', + 'o type' => 'uri', + ], + ], + ], + ], + $res + ); + } + + // isUri + public function testSelectFilterIsUriNotFound() + { + // test data + $this->subjectUnderTest->query('INSERT INTO { + "foo" . + }'); + + $res = $this->subjectUnderTest->query(' + SELECT ?s ?o WHERE { + ?s ?o . + FILTER (isUri(?o)) + } + '); + $this->assertEquals( + [ + 'query_type' => 'select', + 'result' => [ + 'variables' => [ + 's', 'o', + ], + 'rows' => [], + ], + ], + $res + ); + } + + // lang: test behavior when using a language + public function testSelectFilterLang() + { + // test data + $this->subjectUnderTest->query('INSERT INTO { + "foo" . + "in de"@de . + "in en"@en . + }'); + + $res = $this->subjectUnderTest->query(' + SELECT ?s ?o WHERE { + ?s ?o . + FILTER (lang(?o) = "en") + } + '); + $this->assertEquals( + [ + 'query_type' => 'select', + 'result' => [ + 'variables' => [ + 's', 'o', + ], + 'rows' => [ + [ + 's' => 'http://s', + 's type' => 'uri', + 'o' => 'in en', + 'o type' => 'literal', + 'o lang' => 'en', + ], + ], + ], + ], + $res + ); + } + + // langMatches + public function testSelectFilterLangMatches() + { + // test data + $this->subjectUnderTest->query('INSERT INTO { + "foo" . + "in de"@de . + "in en"@en . + }'); + + $res = $this->subjectUnderTest->query(' + SELECT ?s ?o WHERE { + ?s ?o . + FILTER langMatches (lang(?o), "en") + } + '); + $this->assertEquals( + [ + 'query_type' => 'select', + 'result' => [ + 'variables' => [ + 's', 'o', + ], + 'rows' => [ + [ + 's' => 'http://s', + 's type' => 'uri', + 'o' => 'in en', + 'o type' => 'literal', + 'o lang' => 'en', + ], + ], + ], + ], + $res + ); + } + + // regex + public function testSelectFilterRegex() + { + // test data + $this->subjectUnderTest->query('INSERT INTO { + "Alice". + "Bob" . + }'); + + $res = $this->subjectUnderTest->query(' + SELECT ?s ?o WHERE { + ?s ?o . + FILTER regex (?o, "^Ali") + } + '); + $this->assertEquals( + [ + 'query_type' => 'select', + 'result' => [ + 'variables' => [ + 's', 'o', + ], + 'rows' => [ + [ + 's' => 'http://s', + 's type' => 'uri', + 'o' => 'Alice', + 'o type' => 'literal', + ], + ], + ], + ], + $res + ); + } + + // regex + public function testSelectFilterRegexWithModifier() + { + // test data + $this->subjectUnderTest->query('INSERT INTO { + "Alice". + "Bob" . + }'); + + $res = $this->subjectUnderTest->query(' + SELECT ?s ?o WHERE { + ?s ?o . + FILTER regex (?o, "^ali", "i") + } + '); + $this->assertEquals( + [ + 'query_type' => 'select', + 'result' => [ + 'variables' => [ + 's', 'o', + ], + 'rows' => [ + [ + 's' => 'http://s', + 's type' => 'uri', + 'o' => 'Alice', + 'o type' => 'literal', + ], + ], + ], + ], + $res + ); + } + + // str + public function testSelectFilterStr() + { + // test data + $this->subjectUnderTest->query('INSERT INTO { + "foo" . + "in de"@de . + "in en"@en . + }'); + + $res = $this->subjectUnderTest->query(' + SELECT ?s ?o WHERE { + ?s ?o . + FILTER (str(?o) = "in en") + } + '); + $this->assertEquals( + [ + 'query_type' => 'select', + 'result' => [ + 'variables' => [ + 's', 'o', + ], + 'rows' => [ + [ + 's' => 'http://s', + 's type' => 'uri', + 'o' => 'in en', + 'o type' => 'literal', + 'o lang' => 'en', + ], + ], + ], + ], + $res + ); + } + + // str + public function testSelectFilterStrNotFound() + { + // test data + $this->subjectUnderTest->query('INSERT INTO { + "foo" . + "in de"@de . + "in en"@en . + }'); + + $res = $this->subjectUnderTest->query(' + SELECT ?s ?o WHERE { + ?s ?o . + FILTER (str(?o) = "in it") + } + '); + $this->assertEquals( + [ + 'query_type' => 'select', + 'result' => [ + 'variables' => [ + 's', 'o', + ], + 'rows' => [], + ], + ], + $res + ); + } + + // > + public function testSelectFilterRelationalGreaterThan() + { + // test data + $this->subjectUnderTest->query('INSERT INTO { + "150" . + "50" . + }'); + + $res = $this->subjectUnderTest->query('SELECT ?c WHERE { + ?c ?w . + + FILTER (?w > 100) + }'); + $this->assertEquals( + [ + 'query_type' => 'select', + 'result' => [ + 'variables' => [ + 'c', + ], + 'rows' => [ + [ + 'c' => 'http://container1', + 'c type' => 'uri', + ], + ], + ], + ], + $res + ); + } + + // < + public function testSelectFilterRelationalSmallerThan() + { + // test data + $this->subjectUnderTest->query('INSERT INTO { + "150" . + "50" . + }'); + + $res = $this->subjectUnderTest->query('SELECT ?c WHERE { + ?c ?w . + + FILTER (?w < 100) + }'); + $this->assertEquals( + [ + 'query_type' => 'select', + 'result' => [ + 'variables' => [ + 'c', + ], + 'rows' => [ + [ + 'c' => 'http://container2', + 'c type' => 'uri', + ], + ], + ], + ], + $res + ); + } + + // < + public function testSelectFilterRelationalSmallerThan2() + { + // test data + $this->subjectUnderTest->query('INSERT INTO { + "150" . + "50" . + }'); + + $res = $this->subjectUnderTest->query('SELECT ?c WHERE { + ?c ?w . + + FILTER (?w < 100 && ?w > 10) + }'); + $this->assertEquals( + [ + [ + 'c' => 'http://container2', + 'c type' => 'uri', + ], + ], + $res['result']['rows'] + ); + } + + // = + public function testSelectFilterRelationalEqual() + { + // test data + $this->subjectUnderTest->query('INSERT INTO { + "150" . + "50" . + }'); + + $res = $this->subjectUnderTest->query('SELECT ?c WHERE { + ?c ?w . + + FILTER (?w = 150) + }'); + $this->assertEquals( + [ + 'query_type' => 'select', + 'result' => [ + 'variables' => [ + 'c', + ], + 'rows' => [ + [ + 'c' => 'http://container1', + 'c type' => 'uri', + ], + ], + ], + ], + $res + ); + } + + // != + public function testSelectFilterRelationalNotEqual() + { + // test data + $this->subjectUnderTest->query('INSERT INTO { + "150" . + "50" . + }'); + + $res = $this->subjectUnderTest->query('SELECT ?c WHERE { + ?c ?w . + + FILTER (?w != 150) + }'); + $this->assertEquals( + [ + 'query_type' => 'select', + 'result' => [ + 'variables' => [ + 'c', + ], + 'rows' => [ + [ + 'c' => 'http://container2', + 'c type' => 'uri', + ], + ], + ], + ], + $res + ); + } + + /* + * SELECT COUNT + */ + + public function testSelectCount() + { + // test data + $this->subjectUnderTest->query('INSERT INTO { + "baz" . + "baz" . + "baz" . + }'); + + $res = $this->subjectUnderTest->query(' + SELECT COUNT(?s) AS ?count WHERE { + ?s "baz" . + } + ORDER BY DESC(?count) + '); + $this->assertEquals( + [ + 'query_type' => 'select', + 'result' => [ + 'variables' => [ + 'count', + ], + 'rows' => [ + [ + 'count' => '3', + 'count type' => 'literal', + ], + ], + ], + ], + $res + ); + } + + /* + * GROUP BY + */ + + public function testSelectGroupBy() + { + $query = 'SELECT ?who COUNT(?person) as ?persons WHERE { + ?who ?person . + } + GROUP BY ?who + '; + + // test data + $this->subjectUnderTest->query('INSERT INTO { + , . + . + }'); + + $res = $this->subjectUnderTest->query($query); + $this->assertEquals( + [ + 'query_type' => 'select', + 'result' => [ + 'variables' => [ + 'who', + 'persons', + ], + 'rows' => [ + [ + 'who' => 'http://person1', + 'who type' => 'uri', + 'persons' => '2', + 'persons type' => 'literal', + ], + [ + 'who' => 'http://person2', + 'who type' => 'uri', + 'persons' => '1', + 'persons type' => 'literal', + ], + ], + ], + ], + $res + ); + } + + /* + * OFFSET and LIMIT + */ + + public function testSelectOffset() + { + // test data + $this->subjectUnderTest->query('INSERT INTO { + "1" . + "3" . + "2" . + }'); + + $res = $this->subjectUnderTest->query(' + SELECT * WHERE { ?s ?p ?o . } + OFFSET 1 + '); + + $this->assertEquals( + [ + 'query_type' => 'select', + 'result' => [ + 'variables' => [ + 's', 'p', 'o', + ], + 'rows' => [ + [ + 's' => 'http://person3', + 's type' => 'uri', + 'p' => 'http://id', + 'p type' => 'uri', + 'o' => '3', + 'o type' => 'literal', + ], + [ + 's' => 'http://person2', + 's type' => 'uri', + 'p' => 'http://id', + 'p type' => 'uri', + 'o' => '2', + 'o type' => 'literal', + ], + ], + ], + ], + $res + ); + } + + public function testSelectOffsetLimit() + { + // test data + $this->subjectUnderTest->query('INSERT INTO { + "1" . + "3" . + "2" . + }'); + + $res = $this->subjectUnderTest->query(' + SELECT * WHERE { ?s ?p ?o . } + OFFSET 1 LIMIT 2 + '); + + $this->assertEquals( + [ + 'query_type' => 'select', + 'result' => [ + 'variables' => [ + 's', 'p', 'o', + ], + 'rows' => [ + [ + 's' => 'http://person3', + 's type' => 'uri', + 'p' => 'http://id', + 'p type' => 'uri', + 'o' => '3', + 'o type' => 'literal', + ], + [ + 's' => 'http://person2', + 's type' => 'uri', + 'p' => 'http://id', + 'p type' => 'uri', + 'o' => '2', + 'o type' => 'literal', + ], + ], + ], + ], + $res + ); + } + + public function testSelectLimit() + { + // test data + $this->subjectUnderTest->query('INSERT INTO { + "1" . + "3" . + "2" . + }'); + + $res = $this->subjectUnderTest->query(' + SELECT * WHERE { ?s ?p ?o . } + LIMIT 2 + '); + + $this->assertEquals( + [ + 'query_type' => 'select', + 'result' => [ + 'variables' => [ + 's', 'p', 'o', + ], + 'rows' => [ + [ + 's' => 'http://person1', + 's type' => 'uri', + 'p' => 'http://id', + 'p type' => 'uri', + 'o' => '1', + 'o type' => 'literal', + ], + [ + 's' => 'http://person3', + 's type' => 'uri', + 'p' => 'http://id', + 'p type' => 'uri', + 'o' => '3', + 'o type' => 'literal', + ], + ], + ], + ], + $res + ); + } + + /* + * ORDER BY + */ + + public function testSelectOrderByAsc() + { + // test data + $this->subjectUnderTest->query('INSERT INTO { + "1" . + "3" . + "2" . + }'); + + $res = $this->subjectUnderTest->query(' + SELECT * WHERE { + ?s ?id . + } + ORDER BY ASC(?id) + '); + $this->assertEquals( + [ + 'query_type' => 'select', + 'result' => [ + 'variables' => [ + 's', + 'id', + ], + 'rows' => [ + [ + 's' => 'http://person1', + 's type' => 'uri', + 'id' => '1', + 'id type' => 'literal', + ], + [ + 's' => 'http://person2', + 's type' => 'uri', + 'id' => '2', + 'id type' => 'literal', + ], + [ + 's' => 'http://person3', + 's type' => 'uri', + 'id' => '3', + 'id type' => 'literal', + ], + ], + ], + ], + $res + ); + } + + public function testSelectOrderByDesc() + { + // test data + $this->subjectUnderTest->query('INSERT INTO { + "1" . + "3" . + "2" . + }'); + + $res = $this->subjectUnderTest->query(' + SELECT * WHERE { + ?s ?id . + } + ORDER BY DESC(?id) + '); + $this->assertEquals( + [ + 'query_type' => 'select', + 'result' => [ + 'variables' => [ + 's', + 'id', + ], + 'rows' => [ + [ + 's' => 'http://person3', + 's type' => 'uri', + 'id' => '3', + 'id type' => 'literal', + ], + [ + 's' => 'http://person2', + 's type' => 'uri', + 'id' => '2', + 'id type' => 'literal', + ], + [ + 's' => 'http://person1', + 's type' => 'uri', + 'id' => '1', + 'id type' => 'literal', + ], + ], + ], + ], + $res + ); + } + + public function testSelectOrderByWithoutContent() + { + $this->expectException(Exception::class); + + $this->subjectUnderTest->query(' + SELECT * WHERE { + ?s ?id . + } + ORDER BY + '); + } + + /* + * PREFIX + */ + + public function testSelectPrefix() + { + // test data + $this->subjectUnderTest->query('INSERT INTO { + . + }'); + + $res = $this->subjectUnderTest->query(' + PREFIX ex: + SELECT * WHERE { + ?s ex:kennt ?o + } + '); + + $this->assertEquals( + [ + 'query_type' => 'select', + 'result' => [ + 'variables' => [ + 's', 'o', + ], + 'rows' => [ + [ + 's' => 'http://person1', + 's type' => 'uri', + 'o' => 'http://person2', + 'o type' => 'uri', + ], + ], + ], + ], + $res + ); + } + + /* + * UNION + */ + + public function testSelectUnion() + { + // test data + $this->subjectUnderTest->query('INSERT INTO { + "1" . + "3" . + "2" . + }'); + + $res = $this->subjectUnderTest->query(' + SELECT * WHERE { + { + ?p "1" . + } UNION { + ?p "3" . + } + } + '); + + $this->assertEquals( + [ + 'query_type' => 'select', + 'result' => [ + 'variables' => [ + 'p', + ], + 'rows' => [ + [ + 'p' => 'http://person1', + 'p type' => 'uri', + ], + [ + 'p' => 'http://person3', + 'p type' => 'uri', + ], + ], + ], + ], + $res + ); + } + + /* + * Tests using certain queries with SELECT FROM WHERE and not just SELECT WHERE + */ + + public function testSelectOrderByAscWithFromClause() + { + // test data + $this->subjectUnderTest->query('INSERT INTO { + "1" . + "3" . + "2" . + }'); + + $res = $this->subjectUnderTest->query(' + SELECT * FROM WHERE { + ?s ?id . + } + ORDER BY ASC(?id) + '); + $this->assertEquals( + [ + 'query_type' => 'select', + 'result' => [ + 'variables' => [ + 's', + 'id', + ], + 'rows' => [ + [ + 's' => 'http://person1', + 's type' => 'uri', + 'id' => '1', + 'id type' => 'literal', + ], + [ + 's' => 'http://person2', + 's type' => 'uri', + 'id' => '2', + 'id type' => 'literal', + ], + [ + 's' => 'http://person3', + 's type' => 'uri', + 'id' => '3', + 'id type' => 'literal', + ], + ], + ], + ], + $res + ); + } + + /** + * Select Distinct + * + * @see https://www.w3.org/TR/rdf-sparql-query/#modDistinct + */ + public function testSelectDistinct() + { + // test data + $this->subjectUnderTest->query('INSERT INTO { + "Test" . + "Test" . + "Test" . + "Test2" . + }'); + + $res = $this->subjectUnderTest->query(' + PREFIX ex: + SELECT DISTINCT ?name WHERE { + ?s ex:kennt ?name + } + '); + + $this->assertEquals( + [ + ['name' => 'Test', 'name type' => 'literal'], + ['name' => 'Test2', 'name type' => 'literal'], + ], + $res['result']['rows'] + ); + } +} diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/AggregatesTest.php b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/AggregatesTest.php new file mode 100644 index 0000000..d62b479 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/AggregatesTest.php @@ -0,0 +1,112 @@ + + * (c) Benjamin Nowack + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tests\Integration\Store\InMemoryStoreSqlite\SPARQL11; + +/** + * Runs W3C tests from https://www.w3.org/2009/sparql/docs/tests/. + * + * Version: 2012-10-23 20:52 (sparql11-test-suite-20121023.tar.gz) + * + * Tests are located in the w3c-tests folder. + * + * @group linux + */ +class AggregatesTest extends ComplianceTest +{ + protected function setUp(): void + { + parent::setUp(); + + $this->w3cTestsFolderPath = __DIR__.'/w3c-tests/aggregates'; + $this->testPref = 'http://www.w3.org/2009/sparql/docs/tests/data-sparql11/aggregates/manifest#'; + } + + /* + * tests + */ + + public function testAggAvg01() + { + $this->loadManifestFileIntoStore($this->w3cTestsFolderPath); + + $testname = 'agg-avg-01'; + + // get test data + $data = $this->getTestData($this->testPref.$testname); + + // load test data into graph + $this->store->insert($data, $this->dataGraphUri); + + // get query to test + $testQuery = $this->getTestQuery($this->testPref.$testname); + + // get actual result for given test query + $actualResult = $this->store->query($testQuery); + $actualResultAsXml = $this->getXmlVersionOfResult($actualResult); + + // SQLite related + $this->assertEquals(2.22, (string) $actualResultAsXml->results->result->binding->literal[0]); + } + + public function testAggEmptyGroup() + { + $this->assertTrue($this->runTestFor('agg-empty-group')); + } + + public function testAggMin01() + { + $this->markTestSkipped( + 'Skipped, because of known bug that our Turtle parser can not parse decimals. ' + .'For more information, see https://github.com/semsol/arc2/issues/136' + ); + } + + public function testAgg01() + { + $this->assertTrue($this->runTestFor('agg01')); + } + + public function testAgg02() + { + $this->assertTrue($this->runTestFor('agg02')); + } + + public function testAgg04() + { + $this->assertTrue($this->runTestFor('agg04')); + } + + public function testAgg05() + { + $this->assertTrue($this->runTestFor('agg05')); + } + + /* + * agg08 fails + */ + + public function testAgg09() + { + $this->assertTrue($this->runTestFor('agg09')); + } + + public function testAgg10() + { + $this->assertTrue($this->runTestFor('agg10')); + } + + /* + * agg11, agg12 fails + */ +} diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/ComplianceTest.php b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/ComplianceTest.php new file mode 100644 index 0000000..57a4e96 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/ComplianceTest.php @@ -0,0 +1,377 @@ + + * (c) Benjamin Nowack + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tests\Integration\Store\InMemoryStoreSqlite\SPARQL11; + +use sweetrdf\InMemoryStoreSqlite\Log\Logger; +use sweetrdf\InMemoryStoreSqlite\Parser\TurtleParser; +use sweetrdf\InMemoryStoreSqlite\Store\InMemoryStoreSqlite; +use Tests\TestCase; + +/** + * Runs W3C tests from https://www.w3.org/2009/sparql/docs/tests/. + * + * Version: 2012-10-23 20:52 (sparql11-test-suite-20121023.tar.gz) + * + * Tests are located in the w3c-tests folder. + */ +abstract class ComplianceTest extends TestCase +{ + /** + * @var InMemoryStoreSqlite + */ + protected $store; + + /** + * @var string + */ + protected $dataGraphUri; + + /** + * @var string + */ + protected $manifestGraphUri; + + /** + * @var string + */ + protected $testPref; + + /** + * @var string + */ + protected $w3cTestsFolderPath; + + protected function setUp(): void + { + parent::setUp(); + + // set graphs + $this->dataGraphUri = 'http://arc/data/'; + $this->manifestGraphUri = 'http://arc/manifest/'; + + /* + * Setup a store instance to load test information and data. + */ + $this->store = InMemoryStoreSqlite::createInstance(); + } + + /** + * Helper function to get expected query result. + * + * @param string $testUri + * + * @return \SimpleXMLElement instance of \SimpleXMLElement representing the result + */ + protected function getExpectedResult($testUri) + { + /* + example: + + :group1 mf:result + */ + $res = $this->store->query(' + PREFIX mf: . + SELECT * FROM <'.$this->manifestGraphUri.'> WHERE { + <'.$testUri.'> mf:result ?resultFile . + } + '); + + // if no result was given, expect test is of type NegativeSyntaxTest11, + // which has no data (group-data-X.ttl) and result (.srx) file. + if (0 < \count($res['result']['rows'])) { + return new \SimpleXMLElement(file_get_contents($res['result']['rows'][0]['resultFile'])); + } else { + return null; + } + } + + /** + * Helper function to load data for a given test. + * + * @param string $testUri + * + * @return array parsed file content + */ + protected function getTestData($testUri) + { + /* + example: + + :group1 mf:action [ + qt:data + ] + */ + $file = $this->store->query(' + PREFIX mf: . + PREFIX qt: . + SELECT * FROM <'.$this->manifestGraphUri.'> WHERE { + <'.$testUri.'> mf:action [ qt:data ?file ] . + } + '); + + // if no result was given, expect test is of type NegativeSyntaxTest11, + // which has no data (group-data-X.ttl) and result (.srx) file. + if (0 < \count($file['result']['rows'])) { + $parser = new TurtleParser(new Logger()); + $data = file_get_contents($file['result']['rows'][0]['file']); + $uri = $file['result']['rows'][0]['file']; + $parser->parse($uri, $data); + + return $parser->getSimpleIndex(); + } else { + return null; + } + } + + /** + * Helper function to get test query for a given test. + * + * @param string $testUri + * + * @return string query to test + */ + protected function getTestQuery($testUri) + { + /* + example: + + :group1 mf:action [ + qt:query + ] + */ + $query = $this->store->query(' + PREFIX mf: . + PREFIX qt: . + SELECT * FROM <'.$this->manifestGraphUri.'> WHERE { + <'.$testUri.'> mf:action [ qt:query ?queryFile ] . + } + '); + + // if test is of type NegativeSyntaxTest11, mf:action points not to a blank node, + // but directly to the query file. + if (0 == \count($query['result']['rows'])) { + $query = $this->store->query(' + PREFIX mf: . + SELECT * FROM <'.$this->manifestGraphUri.'> WHERE { + <'.$testUri.'> mf:action ?queryFile . + } + '); + } + + $query = file_get_contents($query['result']['rows'][0]['queryFile']); + + // add data graph information as FROM clause, because store can't handle default graph + // queries. for more information see https://github.com/semsol/arc2/issues/72. + if (false !== strpos($query, 'ASK') + || false !== strpos($query, 'CONSTRUCT') + || false !== strpos($query, 'SELECT')) { + $query = str_replace('WHERE', 'FROM <'.$this->dataGraphUri.'> WHERE', $query); + } + + return $query; + } + + /** + * Helper function to get test type. + * + * @param string $testUri + * + * @return string Type URI + */ + protected function getTestType($testUri) + { + $type = $this->store->query(' + PREFIX rdf: . + SELECT * FROM <'.$this->manifestGraphUri.'> WHERE { + <'.$testUri.'> rdf:type ?type . + } + '); + + return $type['result']['rows'][0]['type']; + } + + /** + * Transforms query result to a \SimpleXMLElement instance for later comparison. + * + * @return \SimpleXMLElement + */ + protected function getXmlVersionOfResult(array $result) + { + $w = new \XMLWriter(); + $w->openMemory(); + $w->startDocument('1.0'); + + // sparql (root element) + $w->startElement('sparql'); + $w->writeAttribute('xmlns', 'http://www.w3.org/2005/sparql-results#'); + + // sparql > head + $w->startElement('head'); + + foreach ($result['result']['variables'] as $var) { + $w->startElement('variable'); + $w->writeAttribute('name', $var); + $w->endElement(); + } + + // end sparql > head + $w->endElement(); + + // sparql > results + $w->startElement('results'); + + foreach ($result['result']['rows'] as $row) { + /* + example: + + + + http://example/s1 + + + */ + + // new result element + $w->startElement('result'); + + foreach ($result['result']['variables'] as $var) { + if (empty($row[$var])) { + continue; + } + + // sparql > results > result > binding + $w->startElement('binding'); + $w->writeAttribute('name', $var); + + // if a variable type is set + if (isset($row[$var.' type'])) { + // uri + if ('uri' == $row[$var.' type']) { + // example: http://example/s1 + $w->startElement('uri'); + $w->text($row[$var]); + $w->endElement(); + } elseif ('literal' == $row[$var.' type']) { + // example: 9 + $w->startElement('literal'); + + // its not part of the result set, but expected later on + if (true === ctype_digit($row[$var])) { + $w->writeAttribute('datatype', 'http://www.w3.org/2001/XMLSchema#integer'); + } + + $w->text($row[$var]); + $w->endElement(); + } + } + + // end sparql > results > result > binding + $w->endElement(); + } + + // end result + $w->endElement(); + } + + // add if no data were found + if (0 == \count($result['result']['rows'])) { + $w->startElement('result'); + $w->endElement(); + } + + // end sparql > results + $w->endElement(); + + // end sparql + $w->endElement(); + + return new \SimpleXMLElement($w->outputMemory(true)); + } + + /** + * Loads manifest.ttl into manifest graph. + * + * @param string $folderPath + */ + protected function loadManifestFileIntoStore($folderPath) + { + // parse manifest.ttl and load its content into $this->manifestGraphUri + $parser = new TurtleParser(new Logger()); + $data = file_get_contents($folderPath.'/manifest.ttl'); + $uri = $folderPath.'/manifest.ttl'; + $parser->parse($uri, $data); + $this->store->insert($parser->getSimpleIndex(), $this->manifestGraphUri); + } + + /** + * @param string $query + */ + protected function makeQueryA1Liner($query) + { + return preg_replace('/\s\s+/', ' ', $query); + } + + /** + * Helper function to run a certain test. + * + * @param string $testName E.g. group01 + */ + protected function runTestFor($testName) + { + $this->loadManifestFileIntoStore($this->w3cTestsFolderPath); + + // get test type (this determines, if we expect a normal test or one, that must fail) + $negTestUri = 'http://www.w3.org/2001/sw/DataAccess/tests/test-manifest#NegativeSyntaxTest11'; + $type = $this->getTestType($this->testPref.$testName); + + // test has to FAIL + if ($negTestUri == $type) { + // get query to test + $testQuery = $this->getTestQuery($this->testPref.$testName); + + $this->assertFalse(empty($testQuery), 'Can not test, because test query is empty.'); + + $result = $this->store->query($testQuery); + if (0 == $result) { + $this->assertEquals(0, $result); + } elseif (isset($result['result']['rows'])) { + $this->assertEquals(0, \count($result['result']['rows'])); + } else { + throw new \Exception('Invalid result by query method: '.json_encode($result)); + } + + // test has to be SUCCESSFUL + } else { + // get test data + $data = $this->getTestData($this->testPref.$testName); + + // load test data into graph + $this->store->insert($data, $this->dataGraphUri); + + // get query to test + $testQuery = $this->getTestQuery($this->testPref.$testName); + + // get expected result + $expectedResult = $this->getExpectedResult($this->testPref.$testName); + + // get actual result for given test query + $actualResult = $this->store->query($testQuery); + $actualResultAsXml = $this->getXmlVersionOfResult($actualResult); + + $this->assertEquals($expectedResult, $actualResultAsXml); + } + + return true; + } +} diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/DropTest.php b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/DropTest.php new file mode 100644 index 0000000..db6a4c0 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/DropTest.php @@ -0,0 +1,86 @@ + + * (c) Benjamin Nowack + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tests\Integration\Store\InMemoryStoreSqlite\SPARQL11; + +/** + * Runs tests which are based on W3C tests from https://www.w3.org/2009/sparql/docs/tests/. + * + * Version: 2012-10-23 20:52 (sparql11-test-suite-20121023.tar.gz) + * + * Tests are located in the w3c-tests folder. + */ +class DropTest extends ComplianceTest +{ + protected function setUp(): void + { + parent::setUp(); + + $this->w3cTestsFolderPath = __DIR__.'/w3c-tests/drop'; + $this->testPref = 'http://www.w3.org/2009/sparql/docs/tests/data-sparql11/drop/manifest#'; + } + + /** + * Helper function to get test query for a given test. + * + * @param string $testUri + * + * @return string query to test + */ + protected function getTestQuery($testUri) + { + /* + example: + + :group1 mf:action [ + qt:query + ] + */ + $query = $this->store->query(' + PREFIX mf: . + PREFIX ut: . + SELECT * FROM <'.$this->manifestGraphUri.'> WHERE { + <'.$testUri.'> mf:action [ ut:request ?queryFile ] . + } + '); + + return $query['result']['rows'][0]['queryFile']; + } + + /* + * tests + */ + + // this test is not part of the W3C test collection + // it tests DELETE FROM <...> command which is the store equivalent to DROP GRAPH <...> + public function testDeleteGraph() + { + $graphUri = 'http://example.org/g1'; + + $this->store->query('INSERT INTO <'.$graphUri.'> { + "G1" ; + "Graph 1" . + }'); + + // check if graph really contains data + $res = $this->store->query('SELECT * WHERE {?s ?p ?o.}'); + $this->assertTrue(0 < \count($res['result']['rows']), 'No test data in graph found.'); + + // run test query + $res = $this->store->query('DELETE FROM <'.$graphUri.'>'); + + // check if test data are still available + $res = $this->store->query('SELECT * FROM <'.$graphUri.'> WHERE {?s ?p ?o.}'); + $this->assertTrue(0 == \count($res['result']['rows'])); + } +} diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/README.md b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/README.md new file mode 100644 index 0000000..d20aa77 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/README.md @@ -0,0 +1,7 @@ +# W3C SPARQL 1.1 Test Suite + +These tests run W3C SPARQL 1.1 tests from https://www.w3.org/2009/sparql/docs/tests/. + +Version: **2012-10-23 20:52** (used `sparql11-test-suite-20121023.tar.gz`) + +Root test file is `ComplianceTest.php` which contains main test logic. diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-avg-01.rq b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-avg-01.rq new file mode 100644 index 0000000..4e0ecd4 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-avg-01.rq @@ -0,0 +1,5 @@ +PREFIX : +SELECT (AVG(?o) AS ?avg) +WHERE { + ?s :dec ?o +} diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-avg-01.srx b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-avg-01.srx new file mode 100644 index 0000000..8efbd37 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-avg-01.srx @@ -0,0 +1,13 @@ + + + + + + + + + 2.22 + + + + diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-avg-02.rq b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-avg-02.rq new file mode 100644 index 0000000..67fe6e6 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-avg-02.rq @@ -0,0 +1,7 @@ +PREFIX : +SELECT ?s (AVG(?o) AS ?avg) +WHERE { + ?s ?p ?o +} +GROUP BY ?s +HAVING (AVG(?o) <= 2.0) diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-avg-02.srx b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-avg-02.srx new file mode 100644 index 0000000..6a7172c --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-avg-02.srx @@ -0,0 +1,33 @@ + + + + + + + + + + http://www.example.org/mixed1 + + + 1.6 + + + + + http://www.example.org/mixed2 + + + 2.0E-1 + + + + + http://www.example.org/ints + + + 2.0 + + + + diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-empty-group.rq b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-empty-group.rq new file mode 100644 index 0000000..55bd424 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-empty-group.rq @@ -0,0 +1,5 @@ +PREFIX ex: +SELECT ?x (MAX(?value) AS ?max) +WHERE { + ?x ex:p ?value +} GROUP BY ?x diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-empty-group.srx b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-empty-group.srx new file mode 100644 index 0000000..c1c696b --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-empty-group.srx @@ -0,0 +1,16 @@ + + + + + + + + + + + diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-err-01.rq b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-err-01.rq new file mode 100644 index 0000000..e4c0714 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-err-01.rq @@ -0,0 +1,6 @@ +PREFIX : +SELECT ?g (AVG(?p) AS ?avg) ((MIN(?p) + MAX(?p)) / 2 AS ?c) +WHERE { + ?g :p ?p . +} +GROUP BY ?g diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-err-01.srx b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-err-01.srx new file mode 100644 index 0000000..8dc23e4 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-err-01.srx @@ -0,0 +1,23 @@ + + + + + + + + + +http://example.com/data/#x +2.5 +2.5 + + +http://example.com/data/#y + + +http://example.com/data/#z +2.5 +2.5 + + + \ No newline at end of file diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-err-01.ttl b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-err-01.ttl new file mode 100644 index 0000000..5104ea9 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-err-01.ttl @@ -0,0 +1,5 @@ +@prefix : . + +:x :p 1, 2, 3, 4 . +:y :p 1, _:b2, 3, 4 . +:z :p 1.0, 2.0, 3.0, 4 . diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-err-02.rq b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-err-02.rq new file mode 100644 index 0000000..b6466c7 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-err-02.rq @@ -0,0 +1,8 @@ +PREFIX xsd: +PREFIX : +SELECT ?g +(AVG(IF(isNumeric(?p), ?p, COALESCE(xsd:double(?p),0))) AS ?avg) +WHERE { + ?g :p ?p . +} +GROUP BY ?g diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-err-02.srx b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-err-02.srx new file mode 100644 index 0000000..8b1af9e --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-err-02.srx @@ -0,0 +1,33 @@ + + + + + + + + + + http://example.com/data/#x + + + 2.5E0 + + + + + http://example.com/data/#y + + + 2.0 + + + + + http://example.com/data/#z + + + 2.5E0 + + + + diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-err-02.ttl b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-err-02.ttl new file mode 100644 index 0000000..8351860 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-err-02.ttl @@ -0,0 +1,5 @@ +@prefix : . + +:x :p 1, "2", 3, 4 . +:y :p 1, _:b2, 3, 4 . +:z :p 2.5E0, "not a double" , 3.5, 4 . diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-groupconcat-1.rq b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-groupconcat-1.rq new file mode 100644 index 0000000..e6c8e24 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-groupconcat-1.rq @@ -0,0 +1,7 @@ +PREFIX : +ASK { + {SELECT (GROUP_CONCAT(?o) AS ?g) WHERE { + [] :p1 ?o + }} + FILTER(?g = "1 22" || ?g = "22 1") +} diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-groupconcat-1.srx b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-groupconcat-1.srx new file mode 100644 index 0000000..3b6bc6d --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-groupconcat-1.srx @@ -0,0 +1,5 @@ + + + + true + diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-groupconcat-1.ttl b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-groupconcat-1.ttl new file mode 100644 index 0000000..1438f3a --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-groupconcat-1.ttl @@ -0,0 +1,4 @@ +@prefix : . + +:s :p1 "1", "22" . +:s :p2 "aaa", "bb", "c" . diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-groupconcat-2.rq b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-groupconcat-2.rq new file mode 100644 index 0000000..da72015 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-groupconcat-2.rq @@ -0,0 +1,10 @@ +PREFIX : +SELECT (COUNT(*) AS ?c) { + {SELECT ?p (GROUP_CONCAT(?o) AS ?g) WHERE { + [] ?p ?o + } GROUP BY ?p} + FILTER( + (?p = :p1 && (?g = "1 22" || ?g = "22 1")) + || (?p = :p2 && (?g = "aaa bb c" || ?g = "aaa c bb" || ?g = "bb aaa c" || ?g = "bb c aaa" || ?g = "c aaa bb" || ?g = "c bb aaa")) + ) +} diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-groupconcat-2.srx b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-groupconcat-2.srx new file mode 100644 index 0000000..5f2ef92 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-groupconcat-2.srx @@ -0,0 +1,13 @@ + + + + + + + + + 2 + + + + diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-groupconcat-3.rq b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-groupconcat-3.rq new file mode 100644 index 0000000..1a49533 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-groupconcat-3.rq @@ -0,0 +1,7 @@ +PREFIX : +ASK { + {SELECT (GROUP_CONCAT(?o;SEPARATOR=":") AS ?g) WHERE { + [] :p1 ?o + }} + FILTER(?g = "1:22" || ?g = "22:1") +} diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-groupconcat-3.srx b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-groupconcat-3.srx new file mode 100644 index 0000000..3b6bc6d --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-groupconcat-3.srx @@ -0,0 +1,5 @@ + + + + true + diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-max-01.rq b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-max-01.rq new file mode 100644 index 0000000..d1634d8 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-max-01.rq @@ -0,0 +1,5 @@ +PREFIX : +SELECT (MAX(?o) AS ?max) +WHERE { + ?s ?p ?o +} diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-max-01.srx b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-max-01.srx new file mode 100644 index 0000000..1be7e47 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-max-01.srx @@ -0,0 +1,13 @@ + + + + + + + + + 3.0E4 + + + + diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-max-02.rq b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-max-02.rq new file mode 100644 index 0000000..fdf516d --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-max-02.rq @@ -0,0 +1,6 @@ +PREFIX : +SELECT ?s (MAX(?o) AS ?max) +WHERE { + ?s ?p ?o +} +GROUP BY ?s diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-max-02.srx b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-max-02.srx new file mode 100644 index 0000000..795dc13 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-max-02.srx @@ -0,0 +1,49 @@ + + + + + + + + + + http://www.example.org/ints + + + 3 + + + + + http://www.example.org/decimals + + + 3.5 + + + + + http://www.example.org/doubles + + + 3.0E4 + + + + + http://www.example.org/mixed1 + + + 2.2 + + + + + http://www.example.org/mixed2 + + + 2.2 + + + + diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-min-01.rq b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-min-01.rq new file mode 100644 index 0000000..f9e5033 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-min-01.rq @@ -0,0 +1,5 @@ +PREFIX : +SELECT (MIN(?o) AS ?min) +WHERE { + ?s :dec ?o +} diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-min-01.srx b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-min-01.srx new file mode 100644 index 0000000..102efe4 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-min-01.srx @@ -0,0 +1,13 @@ + + + + + + + + + 1.0 + + + + diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-min-02.rq b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-min-02.rq new file mode 100644 index 0000000..3ae3ea4 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-min-02.rq @@ -0,0 +1,6 @@ +PREFIX : +SELECT ?s (MIN(?o) AS ?min) +WHERE { + ?s ?p ?o +} +GROUP BY ?s diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-min-02.srx b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-min-02.srx new file mode 100644 index 0000000..fc97862 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-min-02.srx @@ -0,0 +1,49 @@ + + + + + + + + + + http://www.example.org/ints + + + 1 + + + + + http://www.example.org/decimals + + + 1.0 + + + + + http://www.example.org/doubles + + + 1.0E2 + + + + + http://www.example.org/mixed1 + + + 1 + + + + + http://www.example.org/mixed2 + + + 2.0E-1 + + + + diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-numeric.ttl b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-numeric.ttl new file mode 100644 index 0000000..61099a4 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-numeric.ttl @@ -0,0 +1,8 @@ +@prefix : . +@prefix xsd: . + +:ints :int 1, 2, 3 . +:decimals :dec 1.0, 2.2, 3.5 . +:doubles :double 1.0E2, 2.0E3, 3.0E4 . +:mixed1 :int 1 ; :dec 2.2 . +:mixed2 :double 2E-1 ; :dec 2.2 . diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-numeric2.ttl b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-numeric2.ttl new file mode 100644 index 0000000..bda35c3 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-numeric2.ttl @@ -0,0 +1,8 @@ +@prefix : . +@prefix xsd: . + +:ints :int 1, 2, 3 . +:decimals :dec 1.0, 2.2, 3.5 . +:doubles :double 1.0E2, 2.0E3, 3.0E4 . +:mixed1 :int 1 ; :dec 2.2 . +:mixed2 :double 2E-1 ; :dec 0.2 . diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-sample-01.rq b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-sample-01.rq new file mode 100644 index 0000000..7e7162c --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-sample-01.rq @@ -0,0 +1,10 @@ +PREFIX : +ASK { + { + SELECT (SAMPLE(?o) AS ?sample) + WHERE { + ?s :dec ?o + } + } + FILTER(?sample = 1.0 || ?sample = 2.2 || ?sample = 3.5) +} diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-sample-01.srx b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-sample-01.srx new file mode 100644 index 0000000..3b6bc6d --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-sample-01.srx @@ -0,0 +1,5 @@ + + + + true + diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-sum-01.rq b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-sum-01.rq new file mode 100644 index 0000000..57e45ca --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-sum-01.rq @@ -0,0 +1,5 @@ +PREFIX : +SELECT (SUM(?o) AS ?sum) +WHERE { + ?s :dec ?o +} diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-sum-01.srx b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-sum-01.srx new file mode 100644 index 0000000..6cbe0d6 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-sum-01.srx @@ -0,0 +1,13 @@ + + + + + + + + + 11.1 + + + + diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-sum-02.rq b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-sum-02.rq new file mode 100644 index 0000000..b9cced9 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-sum-02.rq @@ -0,0 +1,6 @@ +PREFIX : +SELECT ?s (SUM(?o) AS ?sum) +WHERE { + ?s ?p ?o +} +GROUP BY ?s diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-sum-02.srx b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-sum-02.srx new file mode 100644 index 0000000..dd85281 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg-sum-02.srx @@ -0,0 +1,49 @@ + + + + + + + + + + http://www.example.org/ints + + + 6 + + + + + http://www.example.org/decimals + + + 6.7 + + + + + http://www.example.org/doubles + + + 3.21E4 + + + + + http://www.example.org/mixed1 + + + 3.2 + + + + + http://www.example.org/mixed2 + + + 4.0E-1 + + + + diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg01.rq b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg01.rq new file mode 100644 index 0000000..0697642 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg01.rq @@ -0,0 +1,4 @@ +PREFIX : + +SELECT (COUNT(?O) AS ?C) +WHERE { ?S ?P ?O } diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg01.srx b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg01.srx new file mode 100644 index 0000000..9e13305 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg01.srx @@ -0,0 +1,13 @@ + + + + + + + + + 5 + + + + diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg01.ttl b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg01.ttl new file mode 100644 index 0000000..5d8f4c5 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg01.ttl @@ -0,0 +1,4 @@ +@prefix : . + +:s :p1 :o1, :o2, :o3. +:s :p2 :o1, :o2. diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg02.rq b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg02.rq new file mode 100644 index 0000000..f5fa6b2 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg02.rq @@ -0,0 +1,5 @@ +PREFIX : + +SELECT ?P (COUNT(?O) AS ?C) +WHERE { ?S ?P ?O } +GROUP BY ?P diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg02.srx b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg02.srx new file mode 100644 index 0000000..dff443c --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg02.srx @@ -0,0 +1,25 @@ + + + + + + + + + + http://www.example.org/p1 + + + 3 + + + + + http://www.example.org/p2 + + + 2 + + + + diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg03.rq b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg03.rq new file mode 100644 index 0000000..9c39780 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg03.rq @@ -0,0 +1,6 @@ +PREFIX : + +SELECT ?P (COUNT(?O) AS ?C) +WHERE { ?S ?P ?O } +GROUP BY ?P +HAVING (COUNT(?O) > 2 ) diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg03.srx b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg03.srx new file mode 100644 index 0000000..a257426 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg03.srx @@ -0,0 +1,17 @@ + + + + + + + + + + http://www.example.org/p1 + + + 3 + + + + diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg04.rq b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg04.rq new file mode 100644 index 0000000..6b873bd --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg04.rq @@ -0,0 +1,4 @@ +PREFIX : + +SELECT (COUNT(*) AS ?C) +WHERE { ?S ?P ?O } diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg04.srx b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg04.srx new file mode 100644 index 0000000..9e13305 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg04.srx @@ -0,0 +1,13 @@ + + + + + + + + + 5 + + + + diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg05.rq b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg05.rq new file mode 100644 index 0000000..839eada --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg05.rq @@ -0,0 +1,5 @@ +PREFIX : + +SELECT ?P (COUNT(*) AS ?C) +WHERE { ?S ?P ?O } +GROUP BY ?P diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg05.srx b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg05.srx new file mode 100644 index 0000000..dff443c --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg05.srx @@ -0,0 +1,25 @@ + + + + + + + + + + http://www.example.org/p1 + + + 3 + + + + + http://www.example.org/p2 + + + 2 + + + + diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg06.rq b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg06.rq new file mode 100644 index 0000000..051d8f7 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg06.rq @@ -0,0 +1,5 @@ +PREFIX : + +SELECT (COUNT(*) AS ?C) +WHERE { ?S ?P ?O } +HAVING (COUNT(*) > 0 ) diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg06.srx b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg06.srx new file mode 100644 index 0000000..9e13305 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg06.srx @@ -0,0 +1,13 @@ + + + + + + + + + 5 + + + + diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg07.rq b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg07.rq new file mode 100644 index 0000000..de31f26 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg07.rq @@ -0,0 +1,6 @@ +PREFIX : + +SELECT ?P (COUNT(*) AS ?C) +WHERE { ?S ?P ?O } +GROUP BY ?P +HAVING ( COUNT(*) > 2 ) diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg07.srx b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg07.srx new file mode 100644 index 0000000..a257426 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg07.srx @@ -0,0 +1,17 @@ + + + + + + + + + + http://www.example.org/p1 + + + 3 + + + + diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg08.rq b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg08.rq new file mode 100644 index 0000000..70a3bbb --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg08.rq @@ -0,0 +1,5 @@ +PREFIX : + +SELECT ((?O1 + ?O2) AS ?O12) (COUNT(?O1) AS ?C) +WHERE { ?S :p ?O1; :q ?O2 } GROUP BY (?O1 + ?O2) +ORDER BY ?O12 diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg08.ttl b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg08.ttl new file mode 100644 index 0000000..a450c22 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg08.ttl @@ -0,0 +1,4 @@ +@prefix : . + +:s :p 0,1,2 . +:s :q 0,1,2 . diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg08b.rq b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg08b.rq new file mode 100644 index 0000000..2e43148 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg08b.rq @@ -0,0 +1,5 @@ +PREFIX : + + SELECT ?O12 (COUNT(?O1) AS ?C) + WHERE { ?S :p ?O1; :q ?O2 } GROUP BY ((?O1 + ?O2) AS ?O12) + ORDER BY ?O12 diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg08b.srx b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg08b.srx new file mode 100644 index 0000000..e5bec04 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg08b.srx @@ -0,0 +1,49 @@ + + + + + + + + + + 0 + + + 1 + + + + + 1 + + + 2 + + + + + 2 + + + 3 + + + + + 3 + + + 2 + + + + + 4 + + + 1 + + + + diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg09.rq b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg09.rq new file mode 100644 index 0000000..922f560 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg09.rq @@ -0,0 +1,4 @@ +PREFIX : + +SELECT ?P (COUNT(?O) AS ?C) +WHERE { ?S ?P ?O } GROUP BY ?S diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg10.rq b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg10.rq new file mode 100644 index 0000000..899a18b --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg10.rq @@ -0,0 +1,4 @@ +PREFIX : + +SELECT ?P (COUNT(?O) AS ?C) +WHERE { ?S ?P ?O } diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg11.rq b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg11.rq new file mode 100644 index 0000000..fb22741 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg11.rq @@ -0,0 +1,4 @@ +PREFIX : + +SELECT ((?O1 + ?O2) AS ?O12) (COUNT(?O1) AS ?C) +WHERE { ?S :p ?O1; :q ?O2 } GROUP BY (?S) diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg12.rq b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg12.rq new file mode 100644 index 0000000..3a5ad97 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/agg12.rq @@ -0,0 +1,4 @@ +PREFIX : + +SELECT ?O1 (COUNT(?O2) AS ?C) +WHERE { ?S :p ?O1; :q ?O2 } GROUP BY (?O1 + ?O2) diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/empty.ttl b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/empty.ttl new file mode 100644 index 0000000..4cedc2d --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/empty.ttl @@ -0,0 +1 @@ +@prefix ex: . diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/manifest.ttl b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/manifest.ttl new file mode 100644 index 0000000..eedcfe0 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/aggregates/manifest.ttl @@ -0,0 +1,347 @@ +@prefix rdf: . +@prefix : . +@prefix rdfs: . +@prefix mf: . +@prefix qt: . +@prefix dawgt: . +@prefix sparql: . + +<> rdf:type mf:Manifest ; + rdfs:label "Aggregates" ; + mf:entries + ( + :agg01 + :agg02 + :agg03 + :agg04 + :agg05 + :agg06 + :agg07 + :agg08 + :agg08b + :agg09 + :agg10 + :agg11 + :agg12 + :agg-groupconcat-01 + :agg-groupconcat-02 + :agg-groupconcat-03 + :agg-sum-01 + :agg-sum-02 + :agg-avg-01 + :agg-avg-02 + :agg-min-01 + :agg-min-02 + :agg-max-01 + :agg-max-02 + :agg-sample-01 + :agg-err-01 + :agg-err-02 + :agg-empty-group +) . + + +:agg01 rdf:type mf:QueryEvaluationTest ; + mf:name "COUNT 1"; + mf:feature sparql:count ; + rdfs:comment "Simple count" ; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:action + [ qt:query ; + qt:data ] ; + mf:result + . + +:agg02 rdf:type mf:QueryEvaluationTest ; + mf:name "COUNT 2"; + mf:feature sparql:count ; + rdfs:comment "Count with grouping" ; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:action + [ qt:query ; + qt:data ] ; + mf:result + . + +:agg03 rdf:type mf:QueryEvaluationTest ; + mf:name "COUNT 3"; + mf:feature sparql:count ; + rdfs:comment "Count with grouping and HAVING clause" ; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:action + [ qt:query ; + qt:data ] ; + mf:result + . + + +:agg04 rdf:type mf:QueryEvaluationTest ; + mf:name "COUNT 4"; + mf:feature sparql:count ; + rdfs:comment "Count(*)" ; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:action + [ qt:query ; + qt:data ] ; + mf:result + . + +:agg05 rdf:type mf:QueryEvaluationTest ; + mf:name "COUNT 5"; + mf:feature sparql:count ; + rdfs:comment "Count(*) with grouping" ; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:action + [ qt:query ; + qt:data ] ; + mf:result + . + +:agg06 rdf:type mf:QueryEvaluationTest ; + mf:name "COUNT 6"; + mf:feature sparql:count ; + rdfs:comment "Count(*) with HAVING Count(*)" ; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:action + [ qt:query ; + qt:data ] ; + mf:result + . + +:agg07 rdf:type mf:QueryEvaluationTest; + mf:name "COUNT 7"; + mf:feature sparql:count ; + rdfs:comment "Count(*) with grouping and HAVING Count(*)" ; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:action + [ qt:query ; + qt:data ] ; + mf:result + . + +:agg08 rdf:type mf:NegativeSyntaxTest11; + mf:name "COUNT 8" ; + mf:feature sparql:count ; + rdfs:comment "grouping by expression, done wrong"; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:action . + +:agg08b rdf:type mf:QueryEvaluationTest; + mf:name "COUNT 8b" ; + mf:feature sparql:count ; + rdfs:comment "grouping by expression, done correctly"; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:action + [ qt:query ; + qt:data ] ; + mf:result . + +:agg09 rdf:type mf:NegativeSyntaxTest11; + mf:name "COUNT 9" ; + mf:feature sparql:count ; + rdfs:comment "Projection of an ungrouped variable (not appearing in the GROUP BY expression)"; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:action . + +:agg10 rdf:type mf:NegativeSyntaxTest11; + mf:name "COUNT 10" ; + mf:feature sparql:count ; + rdfs:comment "Projection of an ungrouped variable (no GROUP BY expression at all)"; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:action . + +:agg11 rdf:type mf:NegativeSyntaxTest11; + mf:name "COUNT 11" ; + mf:feature sparql:count ; + rdfs:comment "Use of an ungrouped variable in a project expression"; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:action . + +:agg12 rdf:type mf:NegativeSyntaxTest11; + mf:name "COUNT 12" ; + mf:feature sparql:count ; + rdfs:comment "Use of an ungrouped variable in a project expression, where the variable appears in a GROUP BY expression"; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:action . + +:agg-groupconcat-01 rdf:type mf:QueryEvaluationTest ; + mf:name "GROUP_CONCAT 1" ; + mf:feature sparql:group_concat ; + dawgt:approval dawgt:Approved; + dawgt:approvedBy ; + mf:action + [ qt:query ; + qt:data ] ; + mf:result + . + +:agg-groupconcat-02 rdf:type mf:QueryEvaluationTest ; + mf:name "GROUP_CONCAT 2" ; + mf:feature sparql:group_concat ; + dawgt:approval dawgt:Approved; + dawgt:approvedBy ; + mf:action + [ qt:query ; + qt:data ] ; + mf:result + . + +:agg-groupconcat-03 rdf:type mf:QueryEvaluationTest ; + mf:name "GROUP_CONCAT with SEPARATOR" ; + mf:feature sparql:group_concat ; + dawgt:approval dawgt:Approved; + dawgt:approvedBy ; + mf:action + [ qt:query ; + qt:data ] ; + mf:result + . + +:agg-avg-01 rdf:type mf:QueryEvaluationTest ; + mf:name "AVG" ; + mf:feature sparql:avg ; + dawgt:approval dawgt:Approved; + dawgt:approvedBy ; + mf:action + [ qt:query ; + qt:data ] ; + mf:result + . + +:agg-avg-02 rdf:type mf:QueryEvaluationTest ; + mf:name "AVG with GROUP BY" ; + mf:feature sparql:avg ; + dawgt:approval dawgt:Approved; + dawgt:approvedBy ; + mf:action + [ qt:query ; + qt:data ] ; + mf:result + . + +:agg-min-01 rdf:type mf:QueryEvaluationTest ; + mf:name "MIN" ; + mf:feature sparql:min ; + dawgt:approval dawgt:Approved; + dawgt:approvedBy ; + mf:action + [ qt:query ; + qt:data ] ; + mf:result + . + +:agg-min-02 rdf:type mf:QueryEvaluationTest ; + mf:name "MIN with GROUP BY" ; + mf:feature sparql:min ; + dawgt:approval dawgt:Approved; + dawgt:approvedBy ; + mf:action + [ qt:query ; + qt:data ] ; + mf:result + . + +:agg-max-01 rdf:type mf:QueryEvaluationTest ; + mf:name "MAX" ; + mf:feature sparql:max ; + dawgt:approval dawgt:Approved; + dawgt:approvedBy ; + mf:action + [ qt:query ; + qt:data ] ; + mf:result + . + +:agg-max-02 rdf:type mf:QueryEvaluationTest ; + mf:name "MAX with GROUP BY" ; + mf:feature sparql:max ; + dawgt:approval dawgt:Approved; + dawgt:approvedBy ; + mf:action + [ qt:query ; + qt:data ] ; + mf:result + . + +:agg-sum-01 rdf:type mf:QueryEvaluationTest ; + mf:name "SUM" ; + mf:feature sparql:sum ; + dawgt:approval dawgt:Approved; + dawgt:approvedBy ; + mf:action + [ qt:query ; + qt:data ] ; + mf:result + . + +:agg-sum-02 rdf:type mf:QueryEvaluationTest ; + mf:name "SUM with GROUP BY" ; + mf:feature sparql:sum ; + dawgt:approval dawgt:Approved; + dawgt:approvedBy ; + mf:action + [ qt:query ; + qt:data ] ; + mf:result + . + +:agg-sample-01 rdf:type mf:QueryEvaluationTest ; + mf:name "SAMPLE" ; + mf:feature sparql:sample ; + dawgt:approval dawgt:Approved; + dawgt:approvedBy ; + mf:action + [ qt:query ; + qt:data ] ; + mf:result + . + +:agg-err-01 rdf:type mf:QueryEvaluationTest ; + mf:name "Error in AVG" ; + mf:feature sparql:aggregate ; + rdfs:comment "Error in AVG return no binding"; + dawgt:approval dawgt:Approved; + dawgt:approvedBy ; + mf:action + [ qt:query ; + qt:data ] ; + mf:result + . + +:agg-err-02 rdf:type mf:QueryEvaluationTest ; + mf:name "Protect from error in AVG" ; + mf:feature sparql:aggregate ; + rdfs:comment "Protect from error in AVG using IF and COALESCE"; + dawgt:approval dawgt:Approved; + dawgt:approvedBy ; + mf:action + [ qt:query ; + qt:data ] ; + mf:result + . + +:agg-empty-group rdf:type mf:QueryEvaluationTest ; + mf:name "agg empty group" ; + mf:name "Aggregate over empty group resulting in a row with unbound variables" ; + mf:feature sparql:aggregate ; + rdfs:seeAlso ; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:action + [ qt:query ; + qt:data ] ; + mf:result + . diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/construct/constructwhere01.rq b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/construct/constructwhere01.rq new file mode 100644 index 0000000..34e007d --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/construct/constructwhere01.rq @@ -0,0 +1,3 @@ +PREFIX : + +CONSTRUCT WHERE { ?s ?p ?o} \ No newline at end of file diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/construct/constructwhere01result.ttl b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/construct/constructwhere01result.ttl new file mode 100644 index 0000000..bd2bd7e --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/construct/constructwhere01result.ttl @@ -0,0 +1,8 @@ +@prefix : . + +:s2 :p :o1 ; + :p :o2 . + +:s1 :p :o1 . + +:s3 :p :o3 . diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/construct/constructwhere02.rq b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/construct/constructwhere02.rq new file mode 100644 index 0000000..e97615c --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/construct/constructwhere02.rq @@ -0,0 +1,3 @@ +PREFIX : + +CONSTRUCT WHERE { :s1 :p ?o . ?s2 :p ?o } \ No newline at end of file diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/construct/constructwhere02result.ttl b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/construct/constructwhere02result.ttl new file mode 100644 index 0000000..d508000 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/construct/constructwhere02result.ttl @@ -0,0 +1,5 @@ +@prefix : . + +:s2 :p :o1 . + +:s1 :p :o1 . diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/construct/constructwhere03.rq b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/construct/constructwhere03.rq new file mode 100644 index 0000000..ae3919c --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/construct/constructwhere03.rq @@ -0,0 +1,3 @@ +PREFIX : + +CONSTRUCT WHERE { :s2 :p ?o1, ?o2 } \ No newline at end of file diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/construct/constructwhere03result.ttl b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/construct/constructwhere03result.ttl new file mode 100644 index 0000000..ffbaff8 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/construct/constructwhere03result.ttl @@ -0,0 +1,4 @@ +@prefix : . + +:s2 :p :o1 ; + :p :o2 . diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/construct/constructwhere04.rq b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/construct/constructwhere04.rq new file mode 100644 index 0000000..2429a5e --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/construct/constructwhere04.rq @@ -0,0 +1,5 @@ +PREFIX : + +CONSTRUCT +FROM +WHERE { ?s ?p ?o } \ No newline at end of file diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/construct/constructwhere04result.ttl b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/construct/constructwhere04result.ttl new file mode 100644 index 0000000..bd2bd7e --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/construct/constructwhere04result.ttl @@ -0,0 +1,8 @@ +@prefix : . + +:s2 :p :o1 ; + :p :o2 . + +:s1 :p :o1 . + +:s3 :p :o3 . diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/construct/constructwhere05.rq b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/construct/constructwhere05.rq new file mode 100644 index 0000000..f56edf8 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/construct/constructwhere05.rq @@ -0,0 +1,4 @@ +PREFIX : + +CONSTRUCT +WHERE { ?s ?p ?o FILTER ( ?o = :o1) } \ No newline at end of file diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/construct/constructwhere06.rq b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/construct/constructwhere06.rq new file mode 100644 index 0000000..3628b1e --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/construct/constructwhere06.rq @@ -0,0 +1,2 @@ +CONSTRUCT +WHERE { GRAPH { ?s ?p ?o } } \ No newline at end of file diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/construct/data.ttl b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/construct/data.ttl new file mode 100644 index 0000000..633812c --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/construct/data.ttl @@ -0,0 +1,6 @@ +@prefix : . + +:s1 :p :o1 . +:s2 :p :o1 . +:s2 :p :o2 . +:s3 :p :o3 . diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/construct/manifest.ttl b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/construct/manifest.ttl new file mode 100644 index 0000000..6bf5d1c --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/construct/manifest.ttl @@ -0,0 +1,74 @@ +@prefix rdf: . +@prefix : . +@prefix rdfs: . +@prefix mf: . +@prefix qt: . +@prefix dawgt: . + +<> rdf:type mf:Manifest ; + rdfs:label "CONSTRUCT" ; + mf:entries + ( + :constructwhere01 + :constructwhere02 + :constructwhere03 + :constructwhere04 + :constructwhere05 + :constructwhere06 + ) . + +:constructwhere01 rdf:type mf:QueryEvaluationTest ; + mf:name "constructwhere01 - CONSTRUCT WHERE" ; + rdfs:comment "CONSTRUCT WHERE { ?S ?P ?O }"; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:action + [ qt:query ; + qt:data ] ; + mf:result + . + +:constructwhere02 rdf:type mf:QueryEvaluationTest ; + mf:name "constructwhere02 - CONSTRUCT WHERE" ; + rdfs:comment "CONSTRUCT WHERE with join"; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:action + [ qt:query ; + qt:data ] ; + mf:result + . + +:constructwhere03 rdf:type mf:QueryEvaluationTest ; + mf:name "constructwhere03 - CONSTRUCT WHERE" ; + rdfs:comment "CONSTRUCT WHERE with join, using shortcut notation"; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:action + [ qt:query ; + qt:data ] ; + mf:result + . +:constructwhere04 rdf:type mf:QueryEvaluationTest ; + mf:name "constructwhere04 - CONSTRUCT WHERE" ; + rdfs:comment "CONSTRUCT WHERE with DatasetClause"; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:action + [ qt:query ] ; + mf:result + . + +:constructwhere05 rdf:type mf:NegativeSyntaxTest11 ; + mf:name "constructwhere05 - CONSTRUCT WHERE" ; + rdfs:comment "CONSTRUCT WHERE with FILTER"; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:action . + +:constructwhere06 rdf:type mf:NegativeSyntaxTest11 ; + mf:name "constructwhere06 - CONSTRUCT WHERE" ; + mf:description "CONSTRUCT WHERE with GRAPH"; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:action . diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-01.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-01.ru new file mode 100644 index 0000000..1c8b98c --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-01.ru @@ -0,0 +1,12 @@ +PREFIX : +PREFIX foaf: + +DELETE +{ + ?s ?p ?o . +} +WHERE +{ + :a foaf:knows ?s . + ?s ?p ?o +} diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-02.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-02.ru new file mode 100644 index 0000000..35439e5 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-02.ru @@ -0,0 +1,12 @@ +PREFIX : +PREFIX foaf: + +DELETE +{ + GRAPH { ?s ?p ?o } +} +WHERE +{ + GRAPH { :a foaf:knows ?s . + ?s ?p ?o } +} diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-03.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-03.ru new file mode 100644 index 0000000..0af1f8d --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-03.ru @@ -0,0 +1,12 @@ +PREFIX : +PREFIX foaf: + +DELETE +{ + ?s ?p ?o . +} +WHERE +{ + ?s foaf:knows :c . + ?s ?p ?o +} diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-04.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-04.ru new file mode 100644 index 0000000..6ce29f9 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-04.ru @@ -0,0 +1,12 @@ +PREFIX : +PREFIX foaf: + +DELETE +{ + GRAPH { ?s ?p ?o } +} +WHERE +{ + GRAPH { ?s foaf:knows :c . + ?s ?p ?o } +} diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-05.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-05.ru new file mode 100644 index 0000000..681cd57 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-05.ru @@ -0,0 +1,12 @@ +PREFIX : +PREFIX foaf: + +DELETE +{ + ?s ?p ?o . +} +WHERE +{ + :a foaf:knows ?s . + ?s ?p ?o +} diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-06.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-06.ru new file mode 100644 index 0000000..70825c1 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-06.ru @@ -0,0 +1,12 @@ +PREFIX : +PREFIX foaf: + +DELETE +{ + GRAPH { ?s ?p ?o } +} +WHERE +{ + GRAPH { ?s foaf:name "Chris" . + ?s ?p ?o } +} diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-07.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-07.ru new file mode 100644 index 0000000..6e189c4 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-07.ru @@ -0,0 +1,11 @@ +PREFIX : +PREFIX foaf: + +DELETE +{ + ?s ?p ?o . +} +WHERE +{ + :a foaf:knows ?s . +} diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-post-01f.ttl b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-post-01f.ttl new file mode 100644 index 0000000..3a12160 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-post-01f.ttl @@ -0,0 +1,9 @@ +@prefix : . +@prefix foaf: . + +:a foaf:name "Alan" . +:a foaf:mbox "alan@example.org" . +:b foaf:name "Bob" . +:b foaf:mbox "bob@example.org" . +:a foaf:knows :b . + diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-post-01s.ttl b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-post-01s.ttl new file mode 100644 index 0000000..f2161b6 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-post-01s.ttl @@ -0,0 +1,6 @@ +@prefix : . +@prefix foaf: . + +:a foaf:name "Alan" . +:a foaf:mbox "alan@example.org" . +:a foaf:knows :b . diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-post-01s2.ttl b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-post-01s2.ttl new file mode 100644 index 0000000..3c6e735 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-post-01s2.ttl @@ -0,0 +1,5 @@ +@prefix : . +@prefix foaf: . + +:b foaf:name "Bob" . +:b foaf:mbox "bob@example.org" . diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-post-02f.ttl b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-post-02f.ttl new file mode 100644 index 0000000..b623497 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-post-02f.ttl @@ -0,0 +1,9 @@ +@prefix : . +@prefix foaf: . + +:a foaf:knows :b . +:b foaf:name "Bob" . +:b foaf:mbox "bob@example.org" . +:c foaf:name "Chris" . +:c foaf:mbox "chris@example.org" . +:b foaf:knows :c . diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-post-02s.ttl b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-post-02s.ttl new file mode 100644 index 0000000..d3666f3 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-post-02s.ttl @@ -0,0 +1,7 @@ +@prefix : . +@prefix foaf: . + +:a foaf:knows :b . +:b foaf:name "Bob" . +:b foaf:mbox "bob@example.org" . +:b foaf:knows :c . diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-post-03f.ttl b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-post-03f.ttl new file mode 100644 index 0000000..3d79633 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-post-03f.ttl @@ -0,0 +1,8 @@ +@prefix : . +@prefix foaf: . + +:c foaf:name "Chris" . +:c foaf:mbox "chris@example.org" . +:d foaf:name "Dan" . +:d foaf:mbox "dan@example.org" . +:c foaf:knows :d . diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-pre-01.ttl b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-pre-01.ttl new file mode 100644 index 0000000..3a12160 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-pre-01.ttl @@ -0,0 +1,9 @@ +@prefix : . +@prefix foaf: . + +:a foaf:name "Alan" . +:a foaf:mbox "alan@example.org" . +:b foaf:name "Bob" . +:b foaf:mbox "bob@example.org" . +:a foaf:knows :b . + diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-pre-02.ttl b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-pre-02.ttl new file mode 100644 index 0000000..b623497 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-pre-02.ttl @@ -0,0 +1,9 @@ +@prefix : . +@prefix foaf: . + +:a foaf:knows :b . +:b foaf:name "Bob" . +:b foaf:mbox "bob@example.org" . +:c foaf:name "Chris" . +:c foaf:mbox "chris@example.org" . +:b foaf:knows :c . diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-pre-03.ttl b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-pre-03.ttl new file mode 100644 index 0000000..3d79633 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-pre-03.ttl @@ -0,0 +1,8 @@ +@prefix : . +@prefix foaf: . + +:c foaf:name "Chris" . +:c foaf:mbox "chris@example.org" . +:d foaf:name "Dan" . +:d foaf:mbox "dan@example.org" . +:c foaf:knows :d . diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-using-01.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-using-01.ru new file mode 100644 index 0000000..f9706cc --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-using-01.ru @@ -0,0 +1,13 @@ +PREFIX : +PREFIX foaf: + +DELETE +{ + ?s ?p ?o . +} +USING +WHERE +{ + :a foaf:knows ?s . + ?s ?p ?o +} diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-using-02.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-using-02.ru new file mode 100644 index 0000000..3be3eb1 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-using-02.ru @@ -0,0 +1,13 @@ +PREFIX : +PREFIX foaf: + +DELETE +{ + ?s ?p ?o . +} +USING +WHERE +{ + GRAPH { :a foaf:knows ?s . + ?s ?p ?o } +} diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-using-03.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-using-03.ru new file mode 100644 index 0000000..d88426a --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-using-03.ru @@ -0,0 +1,13 @@ +PREFIX : +PREFIX foaf: + +DELETE +{ + ?s ?p ?o . +} +USING +WHERE +{ + ?s foaf:knows :d . + ?s ?p ?o +} diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-using-04.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-using-04.ru new file mode 100644 index 0000000..b31ff3a --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-using-04.ru @@ -0,0 +1,13 @@ +PREFIX : +PREFIX foaf: + +DELETE +{ + ?s ?p ?o . +} +USING +WHERE +{ + GRAPH { ?s foaf:knows :d . + ?s ?p ?o } +} diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-using-05.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-using-05.ru new file mode 100644 index 0000000..0e1fc3e --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-using-05.ru @@ -0,0 +1,13 @@ +PREFIX : +PREFIX foaf: + +DELETE +{ + GRAPH { ?s ?p ?o } +} +USING +WHERE +{ + ?s foaf:knows :b . + ?s ?p ?o +} diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-using-06.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-using-06.ru new file mode 100644 index 0000000..5b00230 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-using-06.ru @@ -0,0 +1,13 @@ +PREFIX : +PREFIX foaf: + +DELETE +{ + GRAPH { ?s ?p ?o } +} +USING +WHERE +{ + GRAPH { ?s foaf:name "Chris" . + ?s ?p ?o } +} diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-with-01.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-with-01.ru new file mode 100644 index 0000000..ddf32e3 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-with-01.ru @@ -0,0 +1,13 @@ +PREFIX : +PREFIX foaf: + +WITH +DELETE +{ + ?s ?p ?o . +} +WHERE +{ + :a foaf:knows ?s . + ?s ?p ?o +} diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-with-02.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-with-02.ru new file mode 100644 index 0000000..a119fa1 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-with-02.ru @@ -0,0 +1,13 @@ +PREFIX : +PREFIX foaf: + +WITH +DELETE +{ + GRAPH { ?s ?p ?o } +} +WHERE +{ + GRAPH { :a foaf:knows ?s . + ?s ?p ?o } +} diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-with-03.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-with-03.ru new file mode 100644 index 0000000..86c8685 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-with-03.ru @@ -0,0 +1,13 @@ +PREFIX : +PREFIX foaf: + +WITH +DELETE +{ + ?s ?p ?o . +} +WHERE +{ + ?s foaf:knows :c . + ?s ?p ?o +} diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-with-04.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-with-04.ru new file mode 100644 index 0000000..49c8072 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-with-04.ru @@ -0,0 +1,13 @@ +PREFIX : +PREFIX foaf: + +WITH +DELETE +{ + GRAPH { ?s ?p ?o } +} +WHERE +{ + GRAPH { ?s foaf:knows :c . + ?s ?p ?o } +} diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-with-05.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-with-05.ru new file mode 100644 index 0000000..ee14ce9 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-with-05.ru @@ -0,0 +1,13 @@ +PREFIX : +PREFIX foaf: + +WITH +DELETE +{ + ?s ?p ?o . +} +WHERE +{ + ?s foaf:knows :b . + ?s ?p ?o +} diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-with-06.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-with-06.ru new file mode 100644 index 0000000..e1f8829 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/delete-with-06.ru @@ -0,0 +1,13 @@ +PREFIX : +PREFIX foaf: + +WITH +DELETE +{ + GRAPH { ?s ?p ?o } +} +WHERE +{ + GRAPH { ?s foaf:name "Chris" . + ?s ?p ?o } +} diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/manifest.ttl b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/manifest.ttl new file mode 100755 index 0000000..3954b17 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/delete/manifest.ttl @@ -0,0 +1,343 @@ +@prefix rdf: . +@prefix : . +@prefix rdfs: . +@prefix dawgt: . +@prefix mf: . +@prefix qt: . +@prefix ut: . + +<> rdf:type mf:Manifest ; + rdfs:comment "Tests for SPARQL UPDATE" ; + mf:entries + ( + :dawg-delete-01 + :dawg-delete-02 + :dawg-delete-03 + :dawg-delete-04 + :dawg-delete-05 + :dawg-delete-06 + :dawg-delete-07 + :dawg-delete-with-01 + :dawg-delete-with-02 + :dawg-delete-with-03 + :dawg-delete-with-04 + :dawg-delete-with-05 + :dawg-delete-with-06 + :dawg-delete-using-01 + :dawg-delete-using-02a + :dawg-delete-using-03 + :dawg-delete-using-04 + :dawg-delete-using-05 + :dawg-delete-using-06a + ). + +:dawg-delete-01 a mf:UpdateEvaluationTest ; + mf:name "Simple DELETE 1" ; + rdfs:comment "This is a simple delete of an existing triple from the default graph" ; + dawgt:approval dawgt:Approved; + dawgt:approvedBy ; + mf:action [ ut:request ; + ut:data + ] ; + mf:result [ ut:data + ] . + +:dawg-delete-02 a mf:UpdateEvaluationTest ; + mf:name "Simple DELETE 2" ; + rdfs:comment "This is a simple delete of an existing triple from a named graph" ; + dawgt:approval dawgt:Approved; + dawgt:approvedBy ; + mf:action [ ut:request ; + ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g1" ] + ] ; + mf:result [ ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g1" ] + ] . + +:dawg-delete-03 a mf:UpdateEvaluationTest ; + mf:name "Simple DELETE 3" ; + rdfs:comment "This is a simple delete of a non-existing triple from the default graph" ; + dawgt:approval dawgt:Approved; + dawgt:approvedBy ; + mf:action [ ut:request ; + ut:data + ] ; + mf:result [ ut:data + ] . + +:dawg-delete-04 a mf:UpdateEvaluationTest ; + mf:name "Simple DELETE 4" ; + rdfs:comment "This is a simple delete of a non-existing triple from a named graph" ; + dawgt:approval dawgt:Approved; + dawgt:approvedBy ; + mf:action [ ut:request ; + ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g1" ] + ] ; + mf:result [ ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g1" ] + ] . + +:dawg-delete-05 a mf:UpdateEvaluationTest ; + mf:name "Graph-specific DELETE 1" ; + rdfs:comment "Test 1 for DELETE only modifying the desired graph" ; + dawgt:approval dawgt:Approved; + dawgt:approvedBy ; + mf:action [ ut:request ; + ut:data ; + ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g2" ] ; + ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g3" ] + ] ; + mf:result [ ut:data ; + ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g2" ] ; + ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g3" ] + ] . + +:dawg-delete-06 a mf:UpdateEvaluationTest ; + mf:name "Graph-specific DELETE 2" ; + rdfs:comment "Test 2 for DELETE only modifying the desired graph" ; + dawgt:approval dawgt:Approved; + dawgt:approvedBy ; + mf:action [ ut:request ; + ut:data ; + ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g2" ] ; + ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g3" ] + ] ; + mf:result [ ut:data ; + ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g2" ] ; + ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g3" ] + ] . + +:dawg-delete-07 a mf:UpdateEvaluationTest ; + mf:name "Simple DELETE 7" ; + rdfs:comment "This is a simple delete to test that unbound variables in the DELETE clause do not act as wildcards" ; + dawgt:approval dawgt:Approved; + dawgt:approvedBy ; + mf:action [ ut:request ; + ut:data + ] ; + mf:result [ ut:data + ] . + +:dawg-delete-with-01 a mf:UpdateEvaluationTest ; + mf:name "Simple DELETE 1 (WITH)" ; + rdfs:comment "This is a simple delete using a WITH clause to identify the active graph" ; + dawgt:approval dawgt:Approved; + dawgt:approvedBy ; + mf:action [ ut:request ; + ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g1" ] + ] ; + mf:result [ ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g1" ] + ] . + +:dawg-delete-with-02 a mf:UpdateEvaluationTest ; + mf:name "Simple DELETE 2 (WITH)" ; + rdfs:comment "This is a simple test to make sure the GRAPH clause overrides the WITH clause" ; + dawgt:approval dawgt:Approved; + dawgt:approvedBy ; + mf:action [ ut:request ; + ut:graphData [ut:graph ; + rdfs:label "http://example.org/g1" ] ; + ut:graphData [ut:graph ; + rdfs:label "http://example.org/g2" ] + ] ; + mf:result [ ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g1" ] ; + ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g2" ] + ] . + +:dawg-delete-with-03 a mf:UpdateEvaluationTest ; + mf:name "Simple DELETE 3 (WITH)" ; + rdfs:comment "This is a simple delete of a non-existing triple using a WITH clause to identify the active graph" ; + dawgt:approval dawgt:Approved; + dawgt:approvedBy ; + mf:action [ ut:request ; + ut:graphData [ut:graph ; + rdfs:label "http://example.org/g1" ] + ] ; + mf:result [ ut:result ut:Success ; + ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g1" ] + ] . + +:dawg-delete-with-04 a mf:UpdateEvaluationTest ; + mf:name "Simple DELETE 4 (WITH)" ; + rdfs:comment "This is a simple delete of a non-existing triple making sure that the GRAPH clause overrides the WITH clause" ; + dawgt:approval dawgt:Approved; + dawgt:approvedBy ; + mf:action [ ut:request ; + ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g1" ] ; + ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g2" ] + ] ; + mf:result [ ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g1" ] ; + ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g2" ] + ] . + +:dawg-delete-with-05 a mf:UpdateEvaluationTest ; + mf:name "Graph-specific DELETE 1 (WITH)" ; + rdfs:comment "Test 1 for DELETE only modifying the desired graph using a WITH clause to specify the active graph" ; + dawgt:approval dawgt:Approved; + dawgt:approvedBy ; + mf:action [ ut:request ; + ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g1" ] ; + ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g2" ] ; + ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g3" ] + ] ; + mf:result [ ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g1" ] ; + ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g2" ] ; + ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g3" ] + ] . + +:dawg-delete-with-06 a mf:UpdateEvaluationTest ; + mf:name "Graph-specific DELETE 2 (WITH)" ; + rdfs:comment "Test 2 for DELETE only modifying the desired graph making sure the GRAPH clause overrides the WITH clause" ; + dawgt:approval dawgt:Approved; + dawgt:approvedBy ; + mf:action [ ut:request ; + ut:data ; + ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g2" ] ; + ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g3" ] + ] ; + mf:result [ ut:data ; + ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g2" ] ; + ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g3" ] + ] . + +:dawg-delete-using-01 a mf:UpdateEvaluationTest ; + mf:name "Simple DELETE 1 (USING)" ; + rdfs:comment "This is a simple delete using a USING clause to identify the active graph" ; + dawgt:approval dawgt:Approved; + dawgt:approvedBy ; + mf:action [ ut:request ; + ut:data ; + ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g2" ] + ] ; + mf:result [ ut:data ; + ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g2" ] + ] . + +:dawg-delete-using-02a a mf:UpdateEvaluationTest ; + mf:name "Simple DELETE 2 (USING)" ; + rdfs:comment "This is a simple test to make sure the GRAPH clause does not override the USING clause" ; + dawgt:approval dawgt:Approved; + dawgt:approvedBy ; + mf:action [ ut:request ; + ut:data ; + ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g2" ] ; + ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g3" ] + ] ; + mf:result [ ut:data ; + ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g2" ] ; + ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g3" ] + ] . + + +:dawg-delete-using-03 a mf:UpdateEvaluationTest ; + mf:name "Simple DELETE 3 (USING)" ; + rdfs:comment "This is a simple delete of a non-existing triple using a USING clause to identify the active graph" ; + dawgt:approval dawgt:Approved; + dawgt:approvedBy ; + mf:action [ ut:request ; + ut:data ; + ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g2" ] + ] ; + mf:result [ ut:data ; + ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g2" ] + ] . + +:dawg-delete-using-04 a mf:UpdateEvaluationTest ; + mf:name "Simple DELETE 4 (USING)" ; + rdfs:comment "This is a simple delete of a non-existing triple making sure that the GRAPH clause overrides the USING clause" ; + dawgt:approval dawgt:Approved; + dawgt:approvedBy ; + mf:action [ ut:request ; + ut:data ; + ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g2" ] ; + ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g3" ] + ] ; + mf:result [ ut:data ; + ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g2" ] ; + ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g3" ] + ] . + +:dawg-delete-using-05 a mf:UpdateEvaluationTest ; + mf:name "Graph-specific DELETE 1 (USING)" ; + rdfs:comment "Test 1 for DELETE only modifying the desired graph using a USING clause to specify the active graph" ; + dawgt:approval dawgt:Approved; + dawgt:approvedBy ; + mf:action [ ut:request ; + ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g1" ] ; + ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g2" ] ; + ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g3" ] + ] ; + mf:result [ ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g1" ] ; + ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g2" ] ; + ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g3" ] + ] . + +:dawg-delete-using-06a a mf:UpdateEvaluationTest ; + mf:name "Graph-specific DELETE 2 (USING)" ; + rdfs:comment "Test 2 for DELETE only modifying the desired graph making sure the GRAPH clause does not override the USING clause" ; + dawgt:approval dawgt:Approved; + dawgt:approvedBy ; + mf:action [ ut:request ; + ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g1" ] ; + ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g2" ] ; + ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g3" ] + ] ; + mf:result [ ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g1" ] ; + ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g2" ] ; + ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g3" ] + ] . + + diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/drop/drop-all-01.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/drop/drop-all-01.ru new file mode 100644 index 0000000..1d9f433 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/drop/drop-all-01.ru @@ -0,0 +1,4 @@ +PREFIX : + +DROP ALL + diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/drop/drop-default-01.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/drop/drop-default-01.ru new file mode 100644 index 0000000..65d5fd0 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/drop/drop-default-01.ru @@ -0,0 +1,4 @@ +PREFIX : + +DROP DEFAULT + diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/drop/drop-default.ttl b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/drop/drop-default.ttl new file mode 100644 index 0000000..0f1f944 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/drop/drop-default.ttl @@ -0,0 +1,3 @@ +@prefix : . + +<> :name "Default Graph" . diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/drop/drop-g1.ttl b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/drop/drop-g1.ttl new file mode 100644 index 0000000..cf26e8f --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/drop/drop-g1.ttl @@ -0,0 +1,5 @@ +@prefix : . + +:g1 :name "G1" ; + :description "Graph 1" ; + . diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/drop/drop-g2.ttl b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/drop/drop-g2.ttl new file mode 100644 index 0000000..afb0160 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/drop/drop-g2.ttl @@ -0,0 +1,4 @@ +@prefix : . + +:g2 :name "G2" ; + . diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/drop/drop-graph-01.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/drop/drop-graph-01.ru new file mode 100644 index 0000000..1a00832 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/drop/drop-graph-01.ru @@ -0,0 +1,3 @@ +PREFIX : + +DROP GRAPH :g1 diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/drop/drop-named-01.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/drop/drop-named-01.ru new file mode 100644 index 0000000..a38420c --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/drop/drop-named-01.ru @@ -0,0 +1,3 @@ +PREFIX : + +DROP NAMED diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/drop/manifest.ttl b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/drop/manifest.ttl new file mode 100755 index 0000000..509c546 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/drop/manifest.ttl @@ -0,0 +1,93 @@ +@prefix rdf: . +@prefix : . +@prefix rdfs: . +@prefix dawgt: . +@prefix mf: . +@prefix qt: . +@prefix ut: . + +<> rdf:type mf:Manifest ; + rdfs:comment "Tests for SPARQL UPDATE" ; + mf:entries + ( + :dawg-drop-default-01 + :dawg-drop-graph-01 + :dawg-drop-named-01 + :dawg-drop-all-01 + ). + +:dawg-drop-default-01 a mf:UpdateEvaluationTest ; + mf:name "DROP DEFAULT" ; + rdfs:comment "This is a DROP of the default graph" ; + dawgt:approval dawgt:Approved; + dawgt:approvedBy ; + mf:action [ + ut:request ; + ut:data ; + ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g1" ] ; + ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g2" ] ; + ] ; + mf:result [ + ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g1" ] ; + ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g2" ] ; + ] ; + . + +:dawg-drop-graph-01 a mf:UpdateEvaluationTest ; + mf:name "DROP GRAPH" ; + rdfs:comment "This is a DROP of an existing named graph" ; + dawgt:approval dawgt:Approved; + dawgt:approvedBy ; + mf:action [ + ut:request ; + ut:data ; + ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g1" ] ; + ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g2" ] ; + ] ; + mf:result [ + ut:data ; + ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g2" ] ; + ] ; + . + +:dawg-drop-named-01 a mf:UpdateEvaluationTest ; + mf:name "DROP NAMED" ; + rdfs:comment "This is a DROP of all the named graphs" ; + dawgt:approval dawgt:Approved; + dawgt:approvedBy ; + mf:action [ + ut:request ; + ut:data ; + ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g1" ] ; + ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g2" ] ; + ] ; + mf:result [ + ut:data ; + ] ; + . + +:dawg-drop-all-01 a mf:UpdateEvaluationTest ; + mf:name "DROP ALL" ; + rdfs:comment "This is a DROP of all graphs (default and named)" ; + dawgt:approval dawgt:Approved; + dawgt:approvedBy ; + mf:action [ + ut:request ; + ut:data ; + ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g1" ] ; + ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g2" ] ; + ] ; + mf:result [] ; + . + diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/exists/exists01.rq b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/exists/exists01.rq new file mode 100644 index 0000000..47c16a3 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/exists/exists01.rq @@ -0,0 +1,6 @@ +prefix ex: + +select * where { +?s ?p ?o +filter exists {?s ?p ex:o} +} diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/exists/exists01.srx b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/exists/exists01.srx new file mode 100644 index 0000000..41b3e0c --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/exists/exists01.srx @@ -0,0 +1,25 @@ + + + + + + + + + +http://www.example.org/s +http://www.example.org/o +http://www.example.org/p + + +http://www.example.org/s +http://www.example.org/o1 +http://www.example.org/p + + +http://www.example.org/s +http://www.example.org/o2 +http://www.example.org/p + + + \ No newline at end of file diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/exists/exists01.ttl b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/exists/exists01.ttl new file mode 100644 index 0000000..39dc3bb --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/exists/exists01.ttl @@ -0,0 +1,4 @@ +@prefix : . + +:s :p :o, :o1, :o2. +:t :p :o1, :o2. diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/exists/exists02.rq b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/exists/exists02.rq new file mode 100644 index 0000000..1a81e3d --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/exists/exists02.rq @@ -0,0 +1,6 @@ +prefix ex: + +select * where { +?s ?p ex:o2 +filter exists {ex:s ex:p ex:o} +} diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/exists/exists02.srx b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/exists/exists02.srx new file mode 100644 index 0000000..5ff350a --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/exists/exists02.srx @@ -0,0 +1,17 @@ + + + + + + + + +http://www.example.org/s +http://www.example.org/p + + +http://www.example.org/t +http://www.example.org/p + + + diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/exists/exists02.ttl b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/exists/exists02.ttl new file mode 100644 index 0000000..3f4a81a --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/exists/exists02.ttl @@ -0,0 +1,4 @@ +@prefix : . + +:a :p :o1. +:b :p :o1, :o2. diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/exists/exists03.rq b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/exists/exists03.rq new file mode 100644 index 0000000..5c17acb --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/exists/exists03.rq @@ -0,0 +1,9 @@ +prefix ex: + +select * where { +graph { + ?s ?p ex:o1 + filter exists { ?s ?p ex:o2 } +} + +} diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/exists/exists03.srx b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/exists/exists03.srx new file mode 100644 index 0000000..94d8b39 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/exists/exists03.srx @@ -0,0 +1,13 @@ + + + + + + + + +http://www.example.org/b +http://www.example.org/p + + + \ No newline at end of file diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/exists/exists04.rq b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/exists/exists04.rq new file mode 100644 index 0000000..8577d39 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/exists/exists04.rq @@ -0,0 +1,6 @@ +prefix ex: + +select * where { + ?s ?p ex:o + filter exists { ?s ?p ex:o1 filter exists { ?s ?p ex:o2 } } +} diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/exists/exists04.srx b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/exists/exists04.srx new file mode 100644 index 0000000..f6544d4 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/exists/exists04.srx @@ -0,0 +1,13 @@ + + + + + + + + +http://www.example.org/s +http://www.example.org/p + + + \ No newline at end of file diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/exists/exists05.rq b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/exists/exists05.rq new file mode 100644 index 0000000..0d698ff --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/exists/exists05.rq @@ -0,0 +1,6 @@ +prefix ex: + +select * where { + ?s ?p ex:o + filter exists { ?s ?p ex:o1 filter not exists { ?s ?p ex:o2 } } +} diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/exists/exists05.srx b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/exists/exists05.srx new file mode 100644 index 0000000..0674113 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/exists/exists05.srx @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/exists/manifest.ttl b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/exists/manifest.ttl new file mode 100644 index 0000000..f4addfc --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/exists/manifest.ttl @@ -0,0 +1,82 @@ +@prefix rdf: . +@prefix : . +@prefix rdfs: . +@prefix mf: . +@prefix qt: . +@prefix dawgt: . +@prefix sparql: . + +<> rdf:type mf:Manifest ; + rdfs:label "Positive Exists" ; + mf:entries + ( + :exists01 + :exists02 + :exists03 + :exists04 + :exists05 + ). + + +:exists01 rdf:type mf:QueryEvaluationTest ; + mf:name "Exists with one constant"; + mf:feature sparql:exists ; + dawgt:approval dawgt:Approved; + dawgt:approvedBy ; + mf:action + [ qt:query ; + qt:data ] ; + mf:result + . + + +:exists02 rdf:type mf:QueryEvaluationTest ; + mf:name "Exists with ground triple"; + mf:feature sparql:exists ; + dawgt:approval dawgt:Approved; + dawgt:approvedBy ; + mf:action + [ qt:query ; + qt:data ] ; + mf:result + . + +:exists03 rdf:type mf:QueryEvaluationTest ; + mf:name "Exists within graph pattern"; + mf:feature sparql:exists ; + rdfs:comment "Checks that exists is interpreted within named graph" ; + dawgt:approval dawgt:Approved; + dawgt:approvedBy ; + mf:action + [ qt:query ; + qt:data ; + qt:graphData + ] ; + mf:result + . + + + :exists04 rdf:type mf:QueryEvaluationTest ; + mf:name "Nested positive exists"; + mf:feature sparql:exists ; + dawgt:approval dawgt:Approved; + dawgt:approvedBy ; + mf:action + [ qt:query ; + qt:data ; + ] ; + mf:result + . + +:exists05 rdf:type mf:QueryEvaluationTest ; + mf:name "Nested negative exists in positive exists"; + mf:feature sparql:exists ; + dawgt:approval dawgt:Approved; + dawgt:approvedBy ; + mf:action + [ qt:query ; + qt:data ; + ] ; + mf:result + . + diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/move/manifest.ttl b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/move/manifest.ttl new file mode 100644 index 0000000..815a4ba --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/move/manifest.ttl @@ -0,0 +1,105 @@ +@prefix rdf: . +@prefix : . +@prefix rdfs: . +@prefix mf: . +@prefix qt: . +@prefix ut: . +@prefix dawgt: . + +<> rdf:type mf:Manifest ; + rdfs:label "Move" ; + mf:entries + ( + :move01 + :move02 + :move03 + :move04 + :move06 + :move07 + ) . + +:move01 rdf:type mf:UpdateEvaluationTest ; + mf:name "MOVE 1" ; + rdfs:comment "Move the default graph to an existing graph" ; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:action [ ut:request ; + ut:data ; + ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g1" ] + ] ; + mf:result [ ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g1" ] + ] . + +:move02 rdf:type mf:UpdateEvaluationTest ; + mf:name "MOVE 2" ; + rdfs:comment "Move the default graph to a non-existing graph" ; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:action [ ut:request ; + ut:data ; + ] ; + mf:result [ ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g1" ] + ] . + +:move03 rdf:type mf:UpdateEvaluationTest ; + mf:name "MOVE 3" ; + rdfs:comment "Move a named graph to an existing graph" ; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:action [ ut:request ; + ut:data ; + ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g1" ] ; + ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g2" ] + ] ; + mf:result [ ut:data ; + ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g2" ] + ] . + +:move04 rdf:type mf:UpdateEvaluationTest ; + mf:name "MOVE 4" ; + rdfs:comment "Move a named graph to a non-existing graph" ; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:action [ ut:request ; + ut:data ; + ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g1" ] + ] ; + mf:result [ ut:data ; + ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g2" ] + ] . + +:move06 rdf:type mf:UpdateEvaluationTest ; + mf:name "MOVE 6" ; + rdfs:comment "Move an existing graph to the default graph" ; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:action [ ut:request ; + ut:data ; + ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g1" ] + ] ; + mf:result [ ut:data ; + ] . + +:move07 rdf:type mf:UpdateEvaluationTest ; + mf:name "MOVE 7" ; + rdfs:comment "Move a graph to itself" ; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:action [ ut:request ; + ut:data ; + ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g1" ] + ] ; + mf:result [ ut:data ; + ut:graphData [ ut:graph ; + rdfs:label "http://example.org/g1" ] + ] . diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/move/move-01.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/move/move-01.ru new file mode 100644 index 0000000..9b531cb --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/move/move-01.ru @@ -0,0 +1,2 @@ +PREFIX : +MOVE DEFAULT TO :g1 \ No newline at end of file diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/move/move-01.ttl b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/move/move-01.ttl new file mode 100644 index 0000000..66f3762 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/move/move-01.ttl @@ -0,0 +1,6 @@ +@prefix foaf: . +@prefix : . + +:jerry a foaf:Person . +:jerry foaf:givenName "Jerry" . +:jerry foaf:mbox . \ No newline at end of file diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/move/move-02.ttl b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/move/move-02.ttl new file mode 100644 index 0000000..b6254f8 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/move/move-02.ttl @@ -0,0 +1,6 @@ +@prefix foaf: . +@prefix : . + +:mickey a foaf:Person . +:mickey foaf:givenName "Mickey" . +:mickey foaf:mbox . \ No newline at end of file diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/move/move-03.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/move/move-03.ru new file mode 100644 index 0000000..e23ecf9 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/move/move-03.ru @@ -0,0 +1,2 @@ +PREFIX : +MOVE :g1 TO :g2 \ No newline at end of file diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/move/move-06.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/move/move-06.ru new file mode 100644 index 0000000..7f79e81 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/move/move-06.ru @@ -0,0 +1,2 @@ +PREFIX : +MOVE :g1 TO DEFAULT \ No newline at end of file diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/move/move-07.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/move/move-07.ru new file mode 100644 index 0000000..993e07f --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/move/move-07.ru @@ -0,0 +1,2 @@ +PREFIX : +MOVE :g1 TO :g1 \ No newline at end of file diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/move/move-default.ttl b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/move/move-default.ttl new file mode 100644 index 0000000..ade3e4c --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/move/move-default.ttl @@ -0,0 +1,6 @@ +@prefix foaf: . +@prefix : . + +:tom a foaf:Person . +:tom foaf:givenName "Tom" . +:tom foaf:mbox . \ No newline at end of file diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/manifest.ttl b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/manifest.ttl new file mode 100644 index 0000000..ec6aa5c --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/manifest.ttl @@ -0,0 +1,392 @@ +@prefix : . +@prefix rdf: . +@prefix rdfs: . +@prefix mf: . +@prefix mfx: . +@prefix qt: . +@prefix dawgt: . + +<> rdf:type mf:Manifest ; + rdfs:comment "Syntax tests Syntax SPARQL Update" ; + mf:entries + ( + +:test_1 +:test_2 +:test_3 +:test_4 +:test_5 +:test_6 +:test_7 +:test_8 +:test_9 +:test_10 +:test_11 +:test_12 +:test_13 +:test_14 +:test_15 +:test_16 +:test_17 +:test_18 +:test_19 +:test_20 +:test_21 +:test_22 +:test_23 +:test_24 +:test_25 +:test_26 +:test_27 +:test_28 +:test_29 +:test_30 +:test_31 +:test_32 +:test_33 +:test_34 +:test_35 +:test_36 +:test_37 +:test_38 +:test_39 +:test_40 +:test_41 +:test_42 +:test_43 +:test_44 +:test_45 +:test_46 +:test_47 +:test_48 +:test_49 +:test_50 +:test_51 +:test_52 +:test_53 +:test_54 +) . + +:test_1 rdf:type mf:PositiveUpdateSyntaxTest11 ; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:name "syntax-update-01.ru" ; + mf:action ;. + +:test_2 rdf:type mf:PositiveUpdateSyntaxTest11 ; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:name "syntax-update-02.ru" ; + mf:action ;. + +:test_3 rdf:type mf:PositiveUpdateSyntaxTest11 ; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:name "syntax-update-03.ru" ; + mf:action ;. + +:test_4 rdf:type mf:PositiveUpdateSyntaxTest11 ; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:name "syntax-update-04.ru" ; + mf:action ;. + +:test_5 rdf:type mf:PositiveUpdateSyntaxTest11 ; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:name "syntax-update-05.ru" ; + mf:action ;. + +:test_6 rdf:type mf:PositiveUpdateSyntaxTest11 ; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:name "syntax-update-06.ru" ; + mf:action ;. + +:test_7 rdf:type mf:PositiveUpdateSyntaxTest11 ; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:name "syntax-update-07.ru" ; + mf:action ;. + +:test_8 rdf:type mf:PositiveUpdateSyntaxTest11 ; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:name "syntax-update-08.ru" ; + mf:action ;. + +:test_9 rdf:type mf:PositiveUpdateSyntaxTest11 ; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:name "syntax-update-09.ru" ; + mf:action ;. + +:test_10 rdf:type mf:PositiveUpdateSyntaxTest11 ; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:name "syntax-update-10.ru" ; + mf:action ;. + +:test_11 rdf:type mf:PositiveUpdateSyntaxTest11 ; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:name "syntax-update-11.ru" ; + mf:action ;. + +:test_12 rdf:type mf:PositiveUpdateSyntaxTest11 ; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:name "syntax-update-12.ru" ; + mf:action ;. + +:test_13 rdf:type mf:PositiveUpdateSyntaxTest11 ; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:name "syntax-update-13.ru" ; + mf:action ;. + +:test_14 rdf:type mf:PositiveUpdateSyntaxTest11 ; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:name "syntax-update-14.ru" ; + mf:action ;. + +:test_15 rdf:type mf:PositiveUpdateSyntaxTest11 ; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:name "syntax-update-15.ru" ; + mf:action ;. + +:test_16 rdf:type mf:PositiveUpdateSyntaxTest11 ; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:name "syntax-update-16.ru" ; + mf:action ;. + +:test_17 rdf:type mf:PositiveUpdateSyntaxTest11 ; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:name "syntax-update-17.ru" ; + mf:action ;. + +:test_18 rdf:type mf:PositiveUpdateSyntaxTest11 ; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:name "syntax-update-18.ru" ; + mf:action ;. + +:test_19 rdf:type mf:PositiveUpdateSyntaxTest11 ; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:name "syntax-update-19.ru" ; + mf:action ;. + +:test_20 rdf:type mf:PositiveUpdateSyntaxTest11 ; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:name "syntax-update-20.ru" ; + mf:action ;. + +:test_21 rdf:type mf:PositiveUpdateSyntaxTest11 ; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:name "syntax-update-21.ru" ; + mf:action ;. + +:test_22 rdf:type mf:PositiveUpdateSyntaxTest11 ; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:name "syntax-update-22.ru" ; + mf:action ;. + +:test_23 rdf:type mf:PositiveUpdateSyntaxTest11 ; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:name "syntax-update-23.ru" ; + mf:action ;. + +:test_24 rdf:type mf:PositiveUpdateSyntaxTest11 ; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:name "syntax-update-24.ru" ; + mf:action ;. + +:test_25 rdf:type mf:PositiveUpdateSyntaxTest11 ; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:name "syntax-update-25.ru" ; + mf:action ;. + +:test_26 rdf:type mf:PositiveUpdateSyntaxTest11 ; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:name "syntax-update-26.ru" ; + mf:action ;. + +:test_27 rdf:type mf:PositiveUpdateSyntaxTest11 ; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:name "syntax-update-27.ru" ; + mf:action ;. + +:test_28 rdf:type mf:PositiveUpdateSyntaxTest11 ; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:name "syntax-update-28.ru" ; + mf:action ;. + +:test_29 rdf:type mf:PositiveUpdateSyntaxTest11 ; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:name "syntax-update-29.ru" ; + mf:action ;. + +:test_30 rdf:type mf:PositiveUpdateSyntaxTest11 ; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:name "syntax-update-30.ru" ; + mf:action ;. + +:test_31 rdf:type mf:PositiveUpdateSyntaxTest11 ; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:name "syntax-update-31.ru" ; + mf:action ;. + +:test_32 rdf:type mf:PositiveUpdateSyntaxTest11 ; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:name "syntax-update-32.ru" ; + mf:action ;. + +:test_33 rdf:type mf:PositiveUpdateSyntaxTest11 ; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:name "syntax-update-33.ru" ; + mf:action ;. + +:test_34 rdf:type mf:PositiveUpdateSyntaxTest11 ; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:name "syntax-update-34.ru" ; + mf:action ;. + +:test_35 rdf:type mf:PositiveUpdateSyntaxTest11 ; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:name "syntax-update-35.ru" ; + mf:action ;. + +:test_36 rdf:type mf:PositiveUpdateSyntaxTest11 ; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:name "syntax-update-36.ru" ; + mf:action ;. + +:test_37 rdf:type mf:PositiveUpdateSyntaxTest11 ; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:name "syntax-update-37.ru" ; + mf:action ;. + +:test_38 rdf:type mf:PositiveUpdateSyntaxTest11 ; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:name "syntax-update-38.ru" ; + mf:action ;. + +:test_39 rdf:type mf:PositiveUpdateSyntaxTest11 ; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:name "syntax-update-39.ru" ; + mf:action ;. + +:test_40 rdf:type mf:PositiveUpdateSyntaxTest11 ; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:name "syntax-update-40.ru" ; + mf:action ;. + +:test_41 rdf:type mf:NegativeUpdateSyntaxTest11 ; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:name "syntax-update-bad-01.ru" ; + mf:action ;. + +:test_42 rdf:type mf:NegativeUpdateSyntaxTest11 ; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:name "syntax-update-bad-02.ru" ; + mf:action ;. + +:test_43 rdf:type mf:NegativeUpdateSyntaxTest11 ; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:name "syntax-update-bad-03.ru" ; + mf:action ;. + +:test_44 rdf:type mf:NegativeUpdateSyntaxTest11 ; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:name "syntax-update-bad-04.ru" ; + mf:action ;. + +:test_45 rdf:type mf:NegativeUpdateSyntaxTest11 ; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:name "syntax-update-bad-05.ru" ; + mf:action ;. + +:test_46 rdf:type mf:NegativeUpdateSyntaxTest11 ; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:name "syntax-update-bad-06.ru" ; + mf:action ;. + +:test_47 rdf:type mf:NegativeUpdateSyntaxTest11 ; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:name "syntax-update-bad-07.ru" ; + mf:action ;. + +:test_48 rdf:type mf:NegativeUpdateSyntaxTest11 ; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:name "syntax-update-bad-08.ru" ; + mf:action ;. + +:test_49 rdf:type mf:NegativeUpdateSyntaxTest11 ; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:name "syntax-update-bad-09.ru" ; + mf:action ;. + +:test_50 rdf:type mf:NegativeUpdateSyntaxTest11 ; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:name "syntax-update-bad-10.ru" ; + mf:action ;. + +:test_51 rdf:type mf:NegativeUpdateSyntaxTest11 ; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:name "syntax-update-bad-11.ru" ; + mf:action ;. + +:test_52 rdf:type mf:NegativeUpdateSyntaxTest11 ; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:name "syntax-update-bad-12.ru" ; + mf:action ;. + +:test_53 rdf:type mf:PositiveUpdateSyntaxTest11 ; + dawgt:approval dawgt:Approved ; + dawgt:approvedBy ; + mf:name "syntax-update-53.ru" ; + mf:action ;. + +:test_54 rdf:type mf:NegativeUpdateSyntaxTest11 ; + dawgt:approval dawgt:NotClassified ; + # Reuse of bNode label across operations. + mf:name "syntax-update-54.ru" ; + mf:action ;. diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-01.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-01.ru new file mode 100644 index 0000000..a1706e2 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-01.ru @@ -0,0 +1,3 @@ +BASE +PREFIX : +LOAD diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-02.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-02.ru new file mode 100644 index 0000000..8050bd6 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-02.ru @@ -0,0 +1,7 @@ +# Comment +BASE +# Comment +PREFIX : +# Comment +LOAD +# Comment diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-03.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-03.ru new file mode 100644 index 0000000..514a649 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-03.ru @@ -0,0 +1 @@ +LOAD ; diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-04.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-04.ru new file mode 100644 index 0000000..0777642 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-04.ru @@ -0,0 +1 @@ +LOAD INTO GRAPH diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-05.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-05.ru new file mode 100644 index 0000000..b3f4b87 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-05.ru @@ -0,0 +1 @@ +DROP NAMED diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-06.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-06.ru new file mode 100644 index 0000000..ef60445 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-06.ru @@ -0,0 +1 @@ +DROP DEFAULT diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-07.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-07.ru new file mode 100644 index 0000000..61a404f --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-07.ru @@ -0,0 +1 @@ +DROP ALL diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-08.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-08.ru new file mode 100644 index 0000000..ddd3a13 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-08.ru @@ -0,0 +1 @@ +DROP GRAPH diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-09.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-09.ru new file mode 100644 index 0000000..aff3357 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-09.ru @@ -0,0 +1 @@ +DROP SILENT NAMED diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-10.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-10.ru new file mode 100644 index 0000000..a2595a7 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-10.ru @@ -0,0 +1 @@ +DROP SILENT DEFAULT diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-11.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-11.ru new file mode 100644 index 0000000..e7a4b10 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-11.ru @@ -0,0 +1 @@ +DROP SILENT ALL diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-12.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-12.ru new file mode 100644 index 0000000..2d02e08 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-12.ru @@ -0,0 +1 @@ +DROP SILENT GRAPH diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-13.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-13.ru new file mode 100644 index 0000000..66d7177 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-13.ru @@ -0,0 +1 @@ +CREATE GRAPH diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-14.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-14.ru new file mode 100644 index 0000000..ba3709b --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-14.ru @@ -0,0 +1 @@ +CREATE SILENT GRAPH diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-15.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-15.ru new file mode 100644 index 0000000..cea7a88 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-15.ru @@ -0,0 +1 @@ +CLEAR NAMED diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-16.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-16.ru new file mode 100644 index 0000000..dd8f2b6 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-16.ru @@ -0,0 +1 @@ +CLEAR DEFAULT diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-17.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-17.ru new file mode 100644 index 0000000..1074b88 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-17.ru @@ -0,0 +1 @@ +CLEAR ALL diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-18.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-18.ru new file mode 100644 index 0000000..7391e48 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-18.ru @@ -0,0 +1 @@ +CLEAR GRAPH diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-19.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-19.ru new file mode 100644 index 0000000..9b7e0fb --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-19.ru @@ -0,0 +1 @@ +CLEAR SILENT NAMED diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-20.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-20.ru new file mode 100644 index 0000000..9370f0b --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-20.ru @@ -0,0 +1 @@ +CLEAR SILENT DEFAULT diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-21.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-21.ru new file mode 100644 index 0000000..7bcc945 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-21.ru @@ -0,0 +1 @@ +CLEAR SILENT ALL diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-22.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-22.ru new file mode 100644 index 0000000..761d6da --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-22.ru @@ -0,0 +1 @@ +CLEAR SILENT GRAPH diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-23.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-23.ru new file mode 100644 index 0000000..ebc8b60 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-23.ru @@ -0,0 +1 @@ +INSERT DATA {

'o1', 'o2', 'o3' } diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-24.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-24.ru new file mode 100644 index 0000000..e30391f --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-24.ru @@ -0,0 +1 @@ +INSERT DATA { GRAPH {

'o1', 'o2', 'o3' } } diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-25.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-25.ru new file mode 100644 index 0000000..2d04d10 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-25.ru @@ -0,0 +1,6 @@ +INSERT DATA { + + GRAPH { 'o1'; } + GRAPH { 'o1'; } + +} diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-26.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-26.ru new file mode 100644 index 0000000..c7b4b39 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-26.ru @@ -0,0 +1,3 @@ +INSERT +# Comment +DATA { GRAPH {

'o1', 'o2', 'o3' } } diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-27.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-27.ru new file mode 100644 index 0000000..a202410 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-27.ru @@ -0,0 +1,2 @@ +INSERT +DATA { } diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-28.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-28.ru new file mode 100644 index 0000000..06d836e --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-28.ru @@ -0,0 +1,2 @@ +INSERT +DATA { GRAPH {} } diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-29.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-29.ru new file mode 100644 index 0000000..e24468e --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-29.ru @@ -0,0 +1 @@ +DELETE DATA {

'o1', 'o2', 'o3' } diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-30.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-30.ru new file mode 100644 index 0000000..c9159e5 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-30.ru @@ -0,0 +1 @@ +DELETE DATA { GRAPH {

'o1', 'o2', 'o3' } } diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-31.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-31.ru new file mode 100644 index 0000000..73da084 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-31.ru @@ -0,0 +1,6 @@ +DELETE DATA { + + GRAPH { 'o1'; } + GRAPH { 'o1'; } + +} diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-32.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-32.ru new file mode 100644 index 0000000..1624bc3 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-32.ru @@ -0,0 +1,16 @@ +BASE +PREFIX : + +WITH :g +DELETE { + ?p ?o . +} +INSERT { + ?s ?p <#o> . +} +USING +USING +USING NAMED :gn1 +USING NAMED :gn2 +WHERE + { ?s ?p ?o } diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-33.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-33.ru new file mode 100644 index 0000000..56244f9 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-33.ru @@ -0,0 +1,7 @@ +PREFIX : +WITH :g +DELETE { + ?p ?o . +} +WHERE + { ?s ?p ?o } diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-34.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-34.ru new file mode 100644 index 0000000..15480a9 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-34.ru @@ -0,0 +1,7 @@ +PREFIX : +WITH :g +INSERT { + ?p ?o . +} +WHERE + { ?s ?p ?o } diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-35.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-35.ru new file mode 100644 index 0000000..0fdbd76 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-35.ru @@ -0,0 +1 @@ +DELETE WHERE { ?s ?p ?o } diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-36.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-36.ru new file mode 100644 index 0000000..34a3834 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-36.ru @@ -0,0 +1,6 @@ +# Comment +DELETE +# Comment +WHERE +# Comment +{ GRAPH {

123 ; 4567.0 . } } diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-37.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-37.ru new file mode 100644 index 0000000..cbaa918 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-37.ru @@ -0,0 +1,2 @@ +CREATE GRAPH ; +LOAD INTO GRAPH ; diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-38.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-38.ru new file mode 100644 index 0000000..b7db254 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-38.ru @@ -0,0 +1 @@ +# Empty diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-39.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-39.ru new file mode 100644 index 0000000..d0be85f --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-39.ru @@ -0,0 +1,2 @@ +BASE +# Otherwise empty diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-40.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-40.ru new file mode 100644 index 0000000..a494d59 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-40.ru @@ -0,0 +1,2 @@ +PREFIX : +# Otherwise empty diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-53.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-53.ru new file mode 100644 index 0000000..15b57c0 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-53.ru @@ -0,0 +1,6 @@ +PREFIX : + +INSERT DATA { + GRAPH { _:b1 :p :o } + GRAPH { _:b1 :p :o } + } diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-54.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-54.ru new file mode 100644 index 0000000..39b4c54 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-54.ru @@ -0,0 +1,5 @@ +PREFIX : + +INSERT DATA { _:b1 :p :o } +; +INSERT DATA { _:b1 :p :o } diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-bad-01.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-bad-01.ru new file mode 100644 index 0000000..1268c0b --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-bad-01.ru @@ -0,0 +1,2 @@ +# No URL +LOAD ; diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-bad-02.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-bad-02.ru new file mode 100644 index 0000000..305a246 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-bad-02.ru @@ -0,0 +1,2 @@ +# Typo in keyword. +CREATE DEAFULT diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-bad-03.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-bad-03.ru new file mode 100644 index 0000000..3609448 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-bad-03.ru @@ -0,0 +1,2 @@ +# Variable in data. +DELETE DATA { ?s

} diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-bad-04.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-bad-04.ru new file mode 100644 index 0000000..9aa3973 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-bad-04.ru @@ -0,0 +1,2 @@ +# Variable in data. +INSERT DATA { GRAPH ?g {

} } diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-bad-05.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-bad-05.ru new file mode 100644 index 0000000..590f830 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-bad-05.ru @@ -0,0 +1,7 @@ +# Nested GRAPH +DELETE DATA { + GRAPH { +

. + GRAPH { 'o1' } + } +} diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-bad-06.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-bad-06.ru new file mode 100644 index 0000000..1339cc1 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-bad-06.ru @@ -0,0 +1,2 @@ +# Missing template +INSERT WHERE { ?s ?p ?o } diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-bad-07.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-bad-07.ru new file mode 100644 index 0000000..640fc53 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-bad-07.ru @@ -0,0 +1,3 @@ +# No separator +CREATE GRAPH +LOAD INTO GRAPH diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-bad-08.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-bad-08.ru new file mode 100644 index 0000000..3fb2b1e --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-bad-08.ru @@ -0,0 +1,4 @@ +# Too many separators +CREATE GRAPH +;; +LOAD INTO GRAPH diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-bad-09.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-bad-09.ru new file mode 100644 index 0000000..9a2c4b8 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-bad-09.ru @@ -0,0 +1,4 @@ +CREATE GRAPH +; +LOAD INTO GRAPH +;; diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-bad-10.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-bad-10.ru new file mode 100644 index 0000000..5a638fe --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-bad-10.ru @@ -0,0 +1,2 @@ +# BNode in DELETE WHERE +DELETE WHERE { _:a

} diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-bad-11.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-bad-11.ru new file mode 100644 index 0000000..1d2f23a --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-bad-11.ru @@ -0,0 +1,2 @@ +# BNode in DELETE template +DELETE {

[] } WHERE { ?x

} diff --git a/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-bad-12.ru b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-bad-12.ru new file mode 100644 index 0000000..43e3a22 --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqlite/SPARQL11/w3c-tests/syntax-update-1/syntax-update-bad-12.ru @@ -0,0 +1,2 @@ +# BNode in DELETE DATA +DELETE DATA { _:a

} diff --git a/tests/Integration/Store/InMemoryStoreSqliteTest.php b/tests/Integration/Store/InMemoryStoreSqliteTest.php new file mode 100644 index 0000000..3bdd6ff --- /dev/null +++ b/tests/Integration/Store/InMemoryStoreSqliteTest.php @@ -0,0 +1,140 @@ + + * (c) Benjamin Nowack + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tests\Integration\Store; + +use sweetrdf\InMemoryStoreSqlite\KeyValueBag; +use sweetrdf\InMemoryStoreSqlite\Log\LoggerPool; +use sweetrdf\InMemoryStoreSqlite\PDOSQLiteAdapter; +use sweetrdf\InMemoryStoreSqlite\Store\InMemoryStoreSqlite; +use Tests\TestCase; + +class InMemoryStoreSqliteTest extends TestCase +{ + protected function setUp(): void + { + parent::setUp(); + + $this->subjectUnderTest = InMemoryStoreSqlite::createInstance(); + } + + /* + * Tests for createInstance + */ + + public function testCreateInstance() + { + $this->assertEquals( + InMemoryStoreSqlite::createInstance(), + new InMemoryStoreSqlite(new PDOSQLiteAdapter(), new LoggerPool(), new KeyValueBag()) + ); + } + + /* + * Tests for delete + */ + + public function testDelete() + { + // test data + $this->subjectUnderTest->query('INSERT INTO { + "baz" . + "label1" . + }'); + + $res = $this->subjectUnderTest->query('SELECT * WHERE {?s ?p ?o.}'); + $this->assertEquals(2, \count($res['result']['rows'])); + + // remove graph + $this->subjectUnderTest->delete(false, 'http://example.com/'); + + $res = $this->subjectUnderTest->query('SELECT * WHERE {?s ?p ?o.}'); + $this->assertEquals(0, \count($res['result']['rows'])); + } + + /* + * Tests for getDBVersion + */ + + /** + * just check pattern + */ + public function testGetDBVersion() + { + $pattern = '/[0-9]{1,}\.[0-9]{1,}\.[0-9]{1,}/'; + $result = preg_match($pattern, $this->subjectUnderTest->getDBVersion(), $match); + $this->assertEquals(1, $result); + } + + /** + * https://github.com/SaftIng/Saft/tree/master/src/Saft/Addition/ARC2 + * + * @group linux + */ + public function testInsertSaftRegressionTest1() + { + $res = $this->subjectUnderTest->query('SELECT * FROM WHERE { ?s ?p ?o. } '); + $this->assertEquals(0, \count($res['result']['rows'])); + + $this->subjectUnderTest->insert( + file_get_contents($this->rootPath.'/data/nt/saft-arc2-addition-regression1.nt'), + 'http://example.com/' + ); + + $res1 = $this->subjectUnderTest->query('SELECT * FROM WHERE { ?s ?p ?o. } '); + $this->assertEquals(442, \count($res1['result']['rows'])); + + $res2 = $this->subjectUnderTest->query('SELECT * WHERE { ?s ?p ?o. } '); + $this->assertEquals(442, \count($res2['result']['rows'])); + } + + /** + * This test checks gathering of freshly created resources. + */ + public function testInsertSaftRegressionTest2() + { + $res = $this->subjectUnderTest->query('INSERT INTO { . }'); + + $res1 = $this->subjectUnderTest->query('SELECT * FROM WHERE {?s ?p ?o.}'); + $this->assertEquals(1, \count($res1['result']['rows'])); + + $res2 = $this->subjectUnderTest->query('SELECT * WHERE {?s ?p ?o.}'); + $this->assertEquals(1, \count($res2['result']['rows'])); + + $res2 = $this->subjectUnderTest->query('SELECT ?s ?p ?o WHERE {?s ?p ?o.}'); + $this->assertEquals(1, \count($res2['result']['rows'])); + } + + /** + * This test checks side effects of update operations on different graphs. + * + * We add 1 triple to 1 and another to another graph. + * Afterwards first graph is removed. + * In the end second graph still should contain its triples. + */ + public function testInsertSaftRegressionTest3() + { + $this->subjectUnderTest->query( + 'INSERT INTO { . }' + ); + $this->subjectUnderTest->query( + 'INSERT INTO { . }' + ); + $this->subjectUnderTest->query( + 'DELETE FROM ' + ); + + $res = $this->subjectUnderTest->query('SELECT * FROM WHERE {?s ?p ?o.}'); + $this->assertEquals(1, \count($res['result']['rows'])); + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php new file mode 100644 index 0000000..d1bc8d2 --- /dev/null +++ b/tests/TestCase.php @@ -0,0 +1,23 @@ + + * (c) Benjamin Nowack + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tests; + +use PHPUnit\Framework\TestCase as PHPUnitTestCase; + +class TestCase extends PHPUnitTestCase +{ + protected mixed $subjectUnderTest; + + protected string $rootPath = __DIR__; +} diff --git a/tests/Unit/Log/LoggerTest.php b/tests/Unit/Log/LoggerTest.php new file mode 100644 index 0000000..5210cd4 --- /dev/null +++ b/tests/Unit/Log/LoggerTest.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tests\Unit\Log; + +use Exception; +use sweetrdf\InMemoryStoreSqlite\Log\Logger; +use Tests\TestCase; + +class LoggerTest extends TestCase +{ + private function getSubjectUnderTest(): Logger + { + return new Logger(); + } + + public function testGetEntriesWithInvalidLevel() + { + $this->expectException(Exception::class); + $this->expectExceptionMessage('Level invalid not set'); + + $this->getSubjectUnderTest()->getEntries('invalid'); + } + + public function testGetEntries() + { + $sut = $this->getSubjectUnderTest(); + + $sut->error('error1'); + $sut->warning('warning1'); + + $this->assertEquals(2, \count($sut->getEntries())); + $this->assertEquals(1, \count($sut->getEntries('error'))); + $this->assertEquals(1, \count($sut->getEntries('warning'))); + } + + public function testHasEntriesWithInvalidLevel() + { + $this->expectException(Exception::class); + $this->expectExceptionMessage('Level invalid not set'); + + $this->getSubjectUnderTest()->hasEntries('invalid'); + } + + public function testHasEntries() + { + $sut = $this->getSubjectUnderTest(); + + $sut->error('error1'); + $sut->warning('warning1'); + + $this->assertTrue($sut->hasEntries('error')); + $this->assertTrue($sut->hasEntries('warning')); + $this->assertTrue($sut->hasEntries()); + } +} diff --git a/tests/Unit/Store/QueryHandler/LoadQueryHandlerTest.php b/tests/Unit/Store/QueryHandler/LoadQueryHandlerTest.php new file mode 100644 index 0000000..a76f458 --- /dev/null +++ b/tests/Unit/Store/QueryHandler/LoadQueryHandlerTest.php @@ -0,0 +1,53 @@ + + * (c) Benjamin Nowack + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tests\Unit\Store\QueryHandler; + +use sweetrdf\InMemoryStoreSqlite\KeyValueBag; +use sweetrdf\InMemoryStoreSqlite\Log\LoggerPool; +use sweetrdf\InMemoryStoreSqlite\PDOSQLiteAdapter; +use sweetrdf\InMemoryStoreSqlite\Store\InMemoryStoreSqlite; +use sweetrdf\InMemoryStoreSqlite\Store\QueryHandler\LoadQueryHandler; +use Tests\TestCase; + +class LoadQueryHandlerTest extends TestCase +{ + protected function setUp(): void + { + parent::setUp(); + + $loggerPool = new LoggerPool(); + + $store = new InMemoryStoreSqlite(new PDOSQLiteAdapter(), $loggerPool, new KeyValueBag()); + + $this->subjectUnderTest = new LoadQueryHandler($store, $loggerPool->createNewLogger('test')); + } + + /* + * Tests for getOComp + */ + + /** + * Tests to behavior, if a datetime string was given. + */ + public function testGetOComp() + { + // case with +hourse + $string = '2009-05-28T18:03:38+09:00'; + $this->assertEquals('2009-05-28T09:03:38Z', $this->subjectUnderTest->getOComp($string)); + + // GMT case + $string = '2009-05-28T18:03:38GMT'; + $this->assertEquals('2009-05-28T18:03:38Z', $this->subjectUnderTest->getOComp($string)); + } +} diff --git a/tests/data/nt/saft-arc2-addition-regression1.nt b/tests/data/nt/saft-arc2-addition-regression1.nt new file mode 100644 index 0000000..3d6ca2e --- /dev/null +++ b/tests/data/nt/saft-arc2-addition-regression1.nt @@ -0,0 +1,442 @@ + "Cumberland"@en . + "Sydney"@en . + "151.2111111111111"^^ . + "4399722"^^ . + "33.859972222222225 151.2111111111111"@en . + "33.859972222222225"^^ . + . + "1911"^^ . + "0.4"^^ . + "1.0"^^ . + "3.53533377060864E9"^^ . + "3.534E9"^^ . + "1.4E7"^^ . + . + "Adams County"@en . + . + "0.9652553963561148"^^ . + "2000"^^ . + . + "1.294994055168E7"^^ . + . + "3476"^^ . + "3.548E9"^^ . + "3.54828371116032E9"^^ . + . + "Charles Douglas Cecil"@en . + "26"^^ . + . + . + "Chuck Cecil"@en . + "Free safety"@en . + "1.8288"^^ . + . + "1964-11-08"^^ . + "83462.4"^^ . + "Active (coach)"@en . + . + . + . + "0.0"^^ . + "221.0"^^ . + . + . + . + . + "Daugava"@en . + "1020000.0"^^ . + "8.79E10"^^ . + . + . + "678.0"^^ . + . + . + "-82.4"^^ . + . + . + . + "244620.288"^^ . + "331524.864"^^ . + . + . + "44.8"^^ . + "Lake Huron"@en . + "228.6"^^ . + "1.934721118420992E11"^^ . + . + "175.8696"^^ . + . + . + . + . + "59.436"^^ . + . + "6155740.8"^^ . + "44.8 -82.4"@en . + . + "3.5387863697990693E12"^^ . + "5.959562641883136E10"^^ . + . + "-118.15444444444445"^^ . + "36.72833333333333"^^ . + "36.72833333333333 -118.15444444444445"@en . + "2008"^^ . + "1976-07-30"^^ . + "78468"^^ . + "3293903.278336"^^ . + "Manzanar War Relocation Center"@en . + . + . + "76000484"@en . + "1942"^^ . + "Writer"@en . + "42.45333333333333"^^ . + "-74.60722222222222"^^ . + "42.45333333333333 -74.60722222222222"@en . + "2002-09-11"^^ . + "1.8542"^^ . + . + "1973"^^ . + "102"@en . + "1956"^^ . + "9"@en . + "87998.4"^^ . + . + "1955"^^ . + "1933-05-07"^^ . + "19"^^ . + . + . + "17024"^^ . + . + . + "Men/Women of Troy"@en . + "33389"^^ . + . + "14300"^^ . + "Let whoever earns the palm bear it"@en . + . + "4597"^^ . + . + . + . + "Trojans"@en . + . + . + "University of Southern California"@en . + "2.5E9"^^ . + "Traveler"@en . + "Palmam qui meruit ferat"@en . + "USC Cardinal and USC Gold"@en . + "16384"^^ . + . + . + . + "45.37351388888889"^^ . + "1866"^^ . + . + . + "-121.69591944444444"^^ . + "45.37351388888889 -121.69591944444444"@en . + . + "2348.7888"^^ . + . + . + "1857"^^ . + "3428.6952"^^ . + . + "USGS Mount Hood South 45121-C6"@en . + . + "Mount Hood"@en . + "Wah Yan College, Hong Kong"@en . + . + "22.27408"^^ . + . + "22.27408 114.17615"@en . + . + . + "20000.0"^^ . + "1919"^^ . + . + . + . + . + "WYHK"@en . + . + "281 Queen's Road East"@en . + . + "Open"@en . + . + . + "Red, green, blue, white"@en . + . + . + "In Hoc Signo Vinces"@en . + . + . + "114.17615"^^ . + . + "26"@en . + . + "(\"In this sign you shall conquer\")"@en . + . + . + "944"^^ . + "President"@en . + . + . + "Headmaster"@en . + . + "Honour All Men, Love the Brotherhood, Fear God, Honour the King."@en . + "1552"^^ . + "Christ's Hospital"@en . + . + . + "18"^^ . + . + . + . + "Blue & Yellow"@en . + "831"^^ . + "11"^^ . + . + . + . + "Today's Best Country Hits"@en . + . + . + "4.0"^^ . + "WBIE-FM (1968-1981)"@en . + "R&R"@en . + "33.80722222222222 -84.33944444444444"@en . + "33.80722222222222"^^ . + . + "Radio License Holding II, LLC, Debtor in possession"@en . + "WKHX (1981-1987)"@en . + . + "1.015E8"^^ . + . + "C0"@en . + "73161"^^ . + "WKHX-FM"@en . + "-84.33944444444444"^^ . + . + "Kicks 101.5"@en . + . + . + "1.8288"^^ . + "Left"@en . + "1990"^^ . + . + . + "1991"^^ . + . + "Centre"@en . + "6th overall"@en . + "1973-07-20"^^ . + . + . + . + . + . + . + "95256.0"^^ . + . + "Sunk by Surface Craft, 09 February 1944"@en . + . + "U-238"@en . + "1943-01-07"^^ . + "1943-02-20"^^ . + . + "1942-04-21"^^ . + "1971-11-08"^^ . + "4"^^ . + . + . + . + . + . + . + . + "Stairway to Heaven"@en . + . + . + "475.0"^^ . + "5800000"^^ . + "Frankfurt am Main"@en . + "069, 06109, 06101"@en . + "60001-60599, 65901-65936"@en . + "2008-03-31"^^ . + "651899"^^ . + "50.110277777777775 8.682222222222222"@en . + . + "0001"^^ . + "8.682222222222222"^^ . + . + "2.4831E8"^^ . + . + "50.110277777777775"^^ . + . + . + . + . + . + . + . + . + . + "2"^^ . + . + "3000.0"^^ . + . + . + . + "1965-09-30"^^ . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + "32"^^ . + . + . + . + . + . + "Thunderbirds"@en . + . + "3.6908352E9"^^ . + "57.0"^^ . + "6.833E9"^^ . + "2.748938461E12"^^ . + "1270.0"^^ . + "2.6610418080000005E9"^^ . + "2.748860873947125E12"^^ . + "49.0"^^ . + "1781-03-13"^^ . + "0.51"^^ . + "2.661031504799999E9"^^ . + "J2000"@en . + "Uranus"@en . + "63.086"^^ . + "76680.0"^^ . + "5.9"^^ . + "3.004374037087353E12"^^ . + "76.0"^^ . + "53.0"^^ . + "3.004419704E12"^^ . + "24516.0"^^ . + "0.3"^^ . + . + . + "0230"@en . + . + "Winterthur"@en . + . + . + "687.0"^^ . + . + "8400"@en . + . + . + . + . + "100000"^^ . + . + "6.793E7"^^ . + . + . + . + . + "8.75"^^ . + . + "439.0"^^ . + . + . + . + . + . + . + "2008"^^ . + "393.0"^^ . + . + "47.5 8.75"@en . + "47.5"^^ . + . + "327,452 active personnel"@en . + . + . + . + . + "Ultramarine Blue & Air Force Yellow"@en . + . + . + "United States Air Force"@en . + . + . + . + . + . + . + . + "1947"^^ . + . + . + . + "450 ICBMs"@en . + . + . + . + . + . + . + . + . + . + . + . + "50px"@en . + . + . + . + "5,573 aircraft, of which 2,132 are fighters"@en . + . + . + . + . + . + . + . + . + . + "\"To fly, fight and win ... in air, space and cyberspace.\""@en . + . + . + "32 satellites"@en . + "\"Above All\" (as of 19 Feb. 2008)"@en . + . + . + . + . + . + . + . + . diff --git a/tests/data/nt/test.nt b/tests/data/nt/test.nt new file mode 100644 index 0000000..3e52881 --- /dev/null +++ b/tests/data/nt/test.nt @@ -0,0 +1,78 @@ +# +# Copyright World Wide Web Consortium, (Massachusetts Institute of +# Technology, Institut National de Recherche en Informatique et en +# Automatique, Keio University). +# +# All Rights Reserved. +# +# Please see the full Copyright clause at +# +# +# Test file with a variety of legal N-Triples +# +# Dave Beckett - http://purl.org/net/dajobe/ +# +# $Id: test.nt,v 1.7 2003/10/06 15:52:19 dbeckett2 Exp $ +# +##################################################################### + +# comment lines + # comment line after whitespace +# empty blank line, then one with spaces and tabs + + + . +_:anon . + _:anon . +# spaces and tabs throughout: + . + +# line ending with CR NL (ASCII 13, ASCII 10) + . + +# 2 statement lines separated by single CR (ASCII 10) + . . + + +# All literal escapes + "simple literal" . + "backslash:\\" . + "dquote:\"" . + "newline:\n" . + "return\r" . + "tab:\t" . + +# Space is optional before final . + . + "x". + _:anon. + +# \u and \U escapes +# latin small letter e with acute symbol \u00E9 - 3 UTF-8 bytes #xC3 #A9 + "\u00E9" . +# Euro symbol \u20ac - 3 UTF-8 bytes #xE2 #x82 #xAC + "\u20AC" . +# resource18 test removed +# resource19 test removed +# resource20 test removed + +# XML Literals as Datatyped Literals + ""^^ . + " "^^ . + "x"^^ . + "\""^^ . + ""^^ . + "a "^^ . + "a c"^^ . + "a\n\nc"^^ . + "chat"^^ . +# resource28 test removed 2003-08-03 +# resource29 test removed 2003-08-03 + +# Plain literals with languages + "chat"@fr . + "chat"@en . + +# Typed Literals + "abc"^^ . +# resource33 test removed 2003-08-03